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 } ;
2
4
use ide_db:: {
3
5
assists:: { Assist , AssistId , AssistKind } ,
4
6
base_db:: FileRange ,
@@ -7,8 +9,13 @@ use ide_db::{
7
9
source_change:: { SourceChange , SourceChangeBuilder } ,
8
10
} ;
9
11
use 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 ,
12
19
} ;
13
20
use text_edit:: TextEdit ;
14
21
@@ -52,23 +59,19 @@ pub(crate) fn unresolved_field(
52
59
fn fixes ( ctx : & DiagnosticsContext < ' _ > , d : & hir:: UnresolvedField ) -> Option < Vec < Assist > > {
53
60
let mut fixes = if d. method_with_same_name_exists { method_fix ( ctx, & d. expr ) } else { None } ;
54
61
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) ;
56
63
}
57
64
fixes
58
65
}
59
66
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 > > {
61
68
// Get the FileRange of the invalid field access
62
69
let root = ctx. sema . db . parse_or_expand ( d. expr . file_id ) ;
63
70
let expr = d. expr . value . to_node ( & root) ;
64
71
65
72
let error_range = ctx. sema . original_range_opt ( expr. syntax ( ) ) ?;
66
73
// Convert the receiver to an ADT
67
74
let adt = d. receiver . as_adt ( ) ?;
68
- let Adt :: Struct ( adt) = adt else {
69
- return None ;
70
- } ;
71
-
72
75
let target_module = adt. module ( ctx. sema . db ) ;
73
76
74
77
let suggested_type =
@@ -83,54 +86,161 @@ fn add_field_fix(ctx: &DiagnosticsContext<'_>, d: &hir::UnresolvedField) -> Opti
83
86
if !is_editable_crate ( target_module. krate ( ) , ctx. sema . db ) {
84
87
return None ;
85
88
}
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 ) ?;
87
111
let adt_syntax = adt_source. syntax ( ) ;
112
+ let Some ( field_list) = adt_source. value . record_field_list ( ) else {
113
+ return None ;
114
+ } ;
88
115
let range = adt_syntax. original_file_range ( ctx. sema . db ) ;
116
+ let field_name = make:: name ( field_name) ;
89
117
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 ( ) )
96
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
+
97
223
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) ;
99
225
(
100
226
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,
103
230
)
104
231
}
232
+ // Empty Struct. Add a field right before the closing brace
105
233
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)
112
237
}
113
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) ;
114
242
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}" ) ) )
134
244
}
135
245
// FIXME: We should fill out the call here, move the cursor and trigger signature help
136
246
fn method_fix (
@@ -154,9 +264,11 @@ fn method_fix(
154
264
}
155
265
#[ cfg( test) ]
156
266
mod tests {
267
+
157
268
use crate :: {
158
269
tests:: {
159
270
check_diagnostics, check_diagnostics_with_config, check_diagnostics_with_disabled,
271
+ check_fix,
160
272
} ,
161
273
DiagnosticsConfig ,
162
274
} ;
@@ -245,4 +357,100 @@ fn foo() {
245
357
config. disabled . insert ( "syntax-error" . to_owned ( ) ) ;
246
358
check_diagnostics_with_config ( config, "fn foo() { (). }" ) ;
247
359
}
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
+ }
248
456
}
0 commit comments