1- use hir:: { db:: ExpandDatabase , HirDisplay , InFile } ;
1+ use std:: iter;
2+
3+ use hir:: { db:: ExpandDatabase , Adt , HasSource , HirDisplay , InFile , Struct , Union } ;
24use ide_db:: {
35 assists:: { Assist , AssistId , AssistKind } ,
46 base_db:: FileRange ,
7+ helpers:: is_editable_crate,
58 label:: Label ,
6- source_change:: SourceChange ,
9+ source_change:: { SourceChange , SourceChangeBuilder } ,
10+ } ;
11+ use syntax:: {
12+ algo,
13+ ast:: { self , edit:: IndentLevel , make, FieldList , Name , Visibility } ,
14+ AstNode , AstPtr , Direction , SyntaxKind , TextSize ,
15+ } ;
16+ use syntax:: {
17+ ast:: { edit:: AstNodeEdit , Type } ,
18+ SyntaxNode ,
719} ;
8- use syntax:: { ast, AstNode , AstPtr } ;
920use text_edit:: TextEdit ;
1021
1122use crate :: { adjusted_display_range, Diagnostic , DiagnosticCode , DiagnosticsContext } ;
@@ -46,15 +57,191 @@ pub(crate) fn unresolved_field(
4657}
4758
4859fn fixes ( ctx : & DiagnosticsContext < ' _ > , d : & hir:: UnresolvedField ) -> Option < Vec < Assist > > {
49- if d. method_with_same_name_exists {
50- method_fix ( ctx, & d. expr )
51- } else {
52- // FIXME: add quickfix
60+ let mut fixes = if d. method_with_same_name_exists { method_fix ( ctx, & d. expr ) } else { None } ;
61+ if let Some ( fix) = add_field_fix ( ctx, d) {
62+ fixes. get_or_insert_with ( Vec :: new) . extend ( fix) ;
63+ }
64+ fixes
65+ }
66+
67+ fn add_field_fix ( ctx : & DiagnosticsContext < ' _ > , d : & hir:: UnresolvedField ) -> Option < Vec < Assist > > {
68+ // Get the FileRange of the invalid field access
69+ let root = ctx. sema . db . parse_or_expand ( d. expr . file_id ) ;
70+ let expr = d. expr . value . to_node ( & root) ;
71+
72+ let error_range = ctx. sema . original_range_opt ( expr. syntax ( ) ) ?;
73+ // Convert the receiver to an ADT
74+ let adt = d. receiver . as_adt ( ) ?;
75+ let target_module = adt. module ( ctx. sema . db ) ;
76+
77+ let suggested_type =
78+ if let Some ( new_field_type) = ctx. sema . type_of_expr ( & expr) . map ( |v| v. adjusted ( ) ) {
79+ let display =
80+ new_field_type. display_source_code ( ctx. sema . db , target_module. into ( ) , true ) . ok ( ) ;
81+ make:: ty ( display. as_deref ( ) . unwrap_or ( "()" ) )
82+ } else {
83+ make:: ty ( "()" )
84+ } ;
5385
54- None
86+ if !is_editable_crate ( target_module. krate ( ) , ctx. sema . db ) {
87+ return None ;
88+ }
89+
90+ // FIXME: Add Snippet Support
91+ let field_name = d. name . as_str ( ) ?;
92+
93+ match adt {
94+ Adt :: Struct ( adt_struct) => {
95+ add_field_to_struct_fix ( ctx, adt_struct, field_name, suggested_type, error_range)
96+ }
97+ Adt :: Union ( adt_union) => {
98+ add_variant_to_union ( ctx, adt_union, field_name, suggested_type, error_range)
99+ }
100+ _ => None ,
55101 }
56102}
103+ fn add_variant_to_union (
104+ ctx : & DiagnosticsContext < ' _ > ,
105+ adt_union : Union ,
106+ field_name : & str ,
107+ suggested_type : Type ,
108+ error_range : FileRange ,
109+ ) -> Option < Vec < Assist > > {
110+ let adt_source = adt_union. source ( ctx. sema . db ) ?;
111+ let adt_syntax = adt_source. syntax ( ) ;
112+ let Some ( field_list) = adt_source. value . record_field_list ( ) else {
113+ return None ;
114+ } ;
115+ let range = adt_syntax. original_file_range ( ctx. sema . db ) ;
116+ let field_name = make:: name ( field_name) ;
57117
118+ let ( offset, record_field) =
119+ record_field_layout ( None , field_name, suggested_type, field_list, adt_syntax. value ) ?;
120+
121+ let mut src_change_builder = SourceChangeBuilder :: new ( range. file_id ) ;
122+ src_change_builder. insert ( offset, record_field) ;
123+ Some ( vec ! [ Assist {
124+ id: AssistId ( "add-variant-to-union" , AssistKind :: QuickFix ) ,
125+ label: Label :: new( "Add field to union" . to_owned( ) ) ,
126+ group: None ,
127+ target: error_range. range,
128+ source_change: Some ( src_change_builder. finish( ) ) ,
129+ trigger_signature_help: false ,
130+ } ] )
131+ }
132+ fn add_field_to_struct_fix (
133+ ctx : & DiagnosticsContext < ' _ > ,
134+ adt_struct : Struct ,
135+ field_name : & str ,
136+ suggested_type : Type ,
137+ error_range : FileRange ,
138+ ) -> Option < Vec < Assist > > {
139+ let struct_source = adt_struct. source ( ctx. sema . db ) ?;
140+ let struct_syntax = struct_source. syntax ( ) ;
141+ let struct_range = struct_syntax. original_file_range ( ctx. sema . db ) ;
142+ let field_list = struct_source. value . field_list ( ) ;
143+ match field_list {
144+ Some ( FieldList :: RecordFieldList ( field_list) ) => {
145+ // Get range of final field in the struct
146+ let visibility = if error_range. file_id == struct_range. file_id {
147+ None
148+ } else {
149+ Some ( make:: visibility_pub_crate ( ) )
150+ } ;
151+ let field_name = make:: name ( field_name) ;
152+
153+ let ( offset, record_field) = record_field_layout (
154+ visibility,
155+ field_name,
156+ suggested_type,
157+ field_list,
158+ struct_syntax. value ,
159+ ) ?;
160+
161+ let mut src_change_builder = SourceChangeBuilder :: new ( struct_range. file_id ) ;
162+
163+ // FIXME: Allow for choosing a visibility modifier see https://github.com/rust-lang/rust-analyzer/issues/11563
164+ src_change_builder. insert ( offset, record_field) ;
165+ Some ( vec ! [ Assist {
166+ id: AssistId ( "add-field-to-record-struct" , AssistKind :: QuickFix ) ,
167+ label: Label :: new( "Add field to Record Struct" . to_owned( ) ) ,
168+ group: None ,
169+ target: error_range. range,
170+ source_change: Some ( src_change_builder. finish( ) ) ,
171+ trigger_signature_help: false ,
172+ } ] )
173+ }
174+ None => {
175+ // Add a field list to the Unit Struct
176+ let mut src_change_builder = SourceChangeBuilder :: new ( struct_range. file_id ) ;
177+ let field_name = make:: name ( field_name) ;
178+ let visibility = if error_range. file_id == struct_range. file_id {
179+ None
180+ } else {
181+ Some ( make:: visibility_pub_crate ( ) )
182+ } ;
183+ // FIXME: Allow for choosing a visibility modifier see https://github.com/rust-lang/rust-analyzer/issues/11563
184+ let indent = IndentLevel :: from_node ( struct_syntax. value ) + 1 ;
185+
186+ let field = make:: record_field ( visibility, field_name, suggested_type) . indent ( indent) ;
187+ let record_field_list = make:: record_field_list ( iter:: once ( field) ) ;
188+ // A Unit Struct with no `;` is invalid syntax. We should not suggest this fix.
189+ let semi_colon =
190+ algo:: skip_trivia_token ( struct_syntax. value . last_token ( ) ?, Direction :: Prev ) ?;
191+ if semi_colon. kind ( ) != SyntaxKind :: SEMICOLON {
192+ return None ;
193+ }
194+ src_change_builder. replace ( semi_colon. text_range ( ) , record_field_list. to_string ( ) ) ;
195+
196+ Some ( vec ! [ Assist {
197+ id: AssistId ( "convert-unit-struct-to-record-struct" , AssistKind :: QuickFix ) ,
198+ label: Label :: new( "Convert Unit Struct to Record Struct and add field" . to_owned( ) ) ,
199+ group: None ,
200+ target: error_range. range,
201+ source_change: Some ( src_change_builder. finish( ) ) ,
202+ trigger_signature_help: false ,
203+ } ] )
204+ }
205+ Some ( FieldList :: TupleFieldList ( _tuple) ) => {
206+ // FIXME: Add support for Tuple Structs. Tuple Structs are not sent to this diagnostic
207+ None
208+ }
209+ }
210+ }
211+ /// Used to determine the layout of the record field in the struct.
212+ fn record_field_layout (
213+ visibility : Option < Visibility > ,
214+ name : Name ,
215+ suggested_type : Type ,
216+ field_list : ast:: RecordFieldList ,
217+ struct_syntax : & SyntaxNode ,
218+ ) -> Option < ( TextSize , String ) > {
219+ let ( offset, needs_comma, trailing_new_line, indent) = match field_list. fields ( ) . last ( ) {
220+ Some ( record_field) => {
221+ let syntax = algo:: skip_trivia_token ( field_list. r_curly_token ( ) ?, Direction :: Prev ) ?;
222+
223+ let last_field_syntax = record_field. syntax ( ) ;
224+ let last_field_indent = IndentLevel :: from_node ( last_field_syntax) ;
225+ (
226+ last_field_syntax. text_range ( ) . end ( ) ,
227+ syntax. kind ( ) != SyntaxKind :: COMMA ,
228+ false ,
229+ last_field_indent,
230+ )
231+ }
232+ // Empty Struct. Add a field right before the closing brace
233+ None => {
234+ let indent = IndentLevel :: from_node ( struct_syntax) + 1 ;
235+ let offset = field_list. r_curly_token ( ) ?. text_range ( ) . start ( ) ;
236+ ( offset, false , true , indent)
237+ }
238+ } ;
239+ let comma = if needs_comma { ",\n " } else { "" } ;
240+ let trailing_new_line = if trailing_new_line { "\n " } else { "" } ;
241+ let record_field = make:: record_field ( visibility, name, suggested_type) ;
242+
243+ Some ( ( offset, format ! ( "{comma}{indent}{record_field}{trailing_new_line}" ) ) )
244+ }
58245// FIXME: We should fill out the call here, move the cursor and trigger signature help
59246fn method_fix (
60247 ctx : & DiagnosticsContext < ' _ > ,
@@ -77,9 +264,11 @@ fn method_fix(
77264}
78265#[ cfg( test) ]
79266mod tests {
267+
80268 use crate :: {
81269 tests:: {
82270 check_diagnostics, check_diagnostics_with_config, check_diagnostics_with_disabled,
271+ check_fix,
83272 } ,
84273 DiagnosticsConfig ,
85274 } ;
@@ -168,4 +357,100 @@ fn foo() {
168357 config. disabled . insert ( "syntax-error" . to_owned ( ) ) ;
169358 check_diagnostics_with_config ( config, "fn foo() { (). }" ) ;
170359 }
360+
361+ #[ test]
362+ fn unresolved_field_fix_on_unit ( ) {
363+ check_fix (
364+ r#"
365+ struct Foo;
366+
367+ fn foo() {
368+ Foo.bar$0;
369+ }
370+ "# ,
371+ r#"
372+ struct Foo{ bar: () }
373+
374+ fn foo() {
375+ Foo.bar;
376+ }
377+ "# ,
378+ ) ;
379+ }
380+ #[ test]
381+ fn unresolved_field_fix_on_empty ( ) {
382+ check_fix (
383+ r#"
384+ struct Foo{
385+ }
386+
387+ fn foo() {
388+ let foo = Foo{};
389+ foo.bar$0;
390+ }
391+ "# ,
392+ r#"
393+ struct Foo{
394+ bar: ()
395+ }
396+
397+ fn foo() {
398+ let foo = Foo{};
399+ foo.bar;
400+ }
401+ "# ,
402+ ) ;
403+ }
404+ #[ test]
405+ fn unresolved_field_fix_on_struct ( ) {
406+ check_fix (
407+ r#"
408+ struct Foo{
409+ a: i32
410+ }
411+
412+ fn foo() {
413+ let foo = Foo{a: 0};
414+ foo.bar$0;
415+ }
416+ "# ,
417+ r#"
418+ struct Foo{
419+ a: i32,
420+ bar: ()
421+ }
422+
423+ fn foo() {
424+ let foo = Foo{a: 0};
425+ foo.bar;
426+ }
427+ "# ,
428+ ) ;
429+ }
430+ #[ test]
431+ fn unresolved_field_fix_on_union ( ) {
432+ check_fix (
433+ r#"
434+ union Foo{
435+ a: i32
436+ }
437+
438+ fn foo() {
439+ let foo = Foo{a: 0};
440+ foo.bar$0;
441+ }
442+ "# ,
443+ r#"
444+ union Foo{
445+ a: i32,
446+ bar: ()
447+ }
448+
449+ fn foo() {
450+ let foo = Foo{a: 0};
451+ foo.bar;
452+ }
453+ "# ,
454+ ) ;
455+ }
171456}
0 commit comments