1- use hir:: { db:: ExpandDatabase , Adt , HasSource , 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,8 +9,13 @@ use ide_db::{
79 source_change:: { SourceChange , SourceChangeBuilder } ,
810} ;
911use syntax:: {
10- ast:: { self , edit:: IndentLevel , make} ,
11- AstNode , AstPtr , SyntaxKind ,
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 ,
1219} ;
1320use text_edit:: TextEdit ;
1421
@@ -52,23 +59,19 @@ pub(crate) fn unresolved_field(
5259fn fixes ( ctx : & DiagnosticsContext < ' _ > , d : & hir:: UnresolvedField ) -> Option < Vec < Assist > > {
5360 let mut fixes = if d. method_with_same_name_exists { method_fix ( ctx, & d. expr ) } else { None } ;
5461 if let Some ( fix) = add_field_fix ( ctx, d) {
55- fixes. get_or_insert_with ( Vec :: new) . push ( fix) ;
62+ fixes. get_or_insert_with ( Vec :: new) . extend ( fix) ;
5663 }
5764 fixes
5865}
5966
60- fn add_field_fix ( ctx : & DiagnosticsContext < ' _ > , d : & hir:: UnresolvedField ) -> Option < Assist > {
67+ fn add_field_fix ( ctx : & DiagnosticsContext < ' _ > , d : & hir:: UnresolvedField ) -> Option < Vec < Assist > > {
6168 // Get the FileRange of the invalid field access
6269 let root = ctx. sema . db . parse_or_expand ( d. expr . file_id ) ;
6370 let expr = d. expr . value . to_node ( & root) ;
6471
6572 let error_range = ctx. sema . original_range_opt ( expr. syntax ( ) ) ?;
6673 // Convert the receiver to an ADT
6774 let adt = d. receiver . as_adt ( ) ?;
68- let Adt :: Struct ( adt) = adt else {
69- return None ;
70- } ;
71-
7275 let target_module = adt. module ( ctx. sema . db ) ;
7376
7477 let suggested_type =
@@ -83,54 +86,161 @@ fn add_field_fix(ctx: &DiagnosticsContext<'_>, d: &hir::UnresolvedField) -> Opti
8386 if !is_editable_crate ( target_module. krate ( ) , ctx. sema . db ) {
8487 return None ;
8588 }
86- let adt_source = adt. source ( ctx. sema . db ) ?;
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_varient_to_union ( ctx, adt_union, field_name, suggested_type, error_range)
99+ }
100+ _ => None ,
101+ }
102+ }
103+ fn add_varient_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 ) ?;
87111 let adt_syntax = adt_source. syntax ( ) ;
112+ let Some ( field_list) = adt_source. value . record_field_list ( ) else {
113+ return None ;
114+ } ;
88115 let range = adt_syntax. original_file_range ( ctx. sema . db ) ;
116+ let field_name = make:: name ( field_name) ;
89117
90- // Get range of final field in the struct
91- let ( offset, needs_comma, indent) = match adt. fields ( ctx. sema . db ) . last ( ) {
92- Some ( field) => {
93- let last_field = field. source ( ctx. sema . db ) ?. value ;
94- let hir:: FieldSource :: Named ( record_field) = last_field else {
95- return None ;
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-varient-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 ( ) )
96150 } ;
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+
97223 let last_field_syntax = record_field. syntax ( ) ;
98- let last_field_imdent = IndentLevel :: from_node ( last_field_syntax) ;
224+ let last_field_indent = IndentLevel :: from_node ( last_field_syntax) ;
99225 (
100226 last_field_syntax. text_range ( ) . end ( ) ,
101- !last_field_syntax. to_string ( ) . ends_with ( ',' ) ,
102- last_field_imdent,
227+ syntax. kind ( ) != SyntaxKind :: COMMA ,
228+ false ,
229+ last_field_indent,
103230 )
104231 }
232+ // Empty Struct. Add a field right before the closing brace
105233 None => {
106- // Empty Struct. Add a field right before the closing brace
107- let indent = IndentLevel :: from_node ( adt_syntax. value ) + 1 ;
108- let record_field_list =
109- adt_syntax. value . children ( ) . find ( |v| v. kind ( ) == SyntaxKind :: RECORD_FIELD_LIST ) ?;
110- let offset = record_field_list. first_token ( ) . map ( |f| f. text_range ( ) . end ( ) ) ?;
111- ( offset, false , indent)
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)
112237 }
113238 } ;
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) ;
114242
115- let field_name = make:: name ( d. name . as_str ( ) ?) ;
116-
117- // If the Type is in the same file. We don't need to add a visibility modifier. Otherwise make it pub(crate)
118- let visibility = if error_range. file_id == range. file_id { "" } else { "pub(crate)" } ;
119- let mut src_change_builder = SourceChangeBuilder :: new ( range. file_id ) ;
120- let comma = if needs_comma { "," } else { "" } ;
121- src_change_builder
122- . insert ( offset, format ! ( "{comma}\n {indent}{visibility}{field_name}: {suggested_type}\n " ) ) ;
123-
124- // FIXME: Add a Snippet for the new field type
125- let source_change = src_change_builder. finish ( ) ;
126- Some ( Assist {
127- id : AssistId ( "add-field-to-type" , AssistKind :: QuickFix ) ,
128- label : Label :: new ( "Add field to type" . to_owned ( ) ) ,
129- group : None ,
130- target : error_range. range ,
131- source_change : Some ( source_change) ,
132- trigger_signature_help : false ,
133- } )
243+ Some ( ( offset, format ! ( "{comma}{indent}{record_field}{trailing_new_line}" ) ) )
134244}
135245// FIXME: We should fill out the call here, move the cursor and trigger signature help
136246fn method_fix (
@@ -154,9 +264,11 @@ fn method_fix(
154264}
155265#[ cfg( test) ]
156266mod tests {
267+
157268 use crate :: {
158269 tests:: {
159270 check_diagnostics, check_diagnostics_with_config, check_diagnostics_with_disabled,
271+ check_fix,
160272 } ,
161273 DiagnosticsConfig ,
162274 } ;
@@ -245,4 +357,100 @@ fn foo() {
245357 config. disabled . insert ( "syntax-error" . to_owned ( ) ) ;
246358 check_diagnostics_with_config ( config, "fn foo() { (). }" ) ;
247359 }
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+ }
248456}
0 commit comments