1
- use hir:: { db:: ExpandDatabase , 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
+ helpers:: is_editable_crate,
5
8
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 ,
7
19
} ;
8
- use syntax:: { ast, AstNode , AstPtr } ;
9
20
use text_edit:: TextEdit ;
10
21
11
22
use crate :: { adjusted_display_range, Diagnostic , DiagnosticCode , DiagnosticsContext } ;
@@ -46,15 +57,191 @@ pub(crate) fn unresolved_field(
46
57
}
47
58
48
59
fn 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
+ } ;
53
85
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 ,
55
101
}
56
102
}
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) ;
57
117
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
+ }
58
245
// FIXME: We should fill out the call here, move the cursor and trigger signature help
59
246
fn method_fix (
60
247
ctx : & DiagnosticsContext < ' _ > ,
@@ -77,9 +264,11 @@ fn method_fix(
77
264
}
78
265
#[ cfg( test) ]
79
266
mod tests {
267
+
80
268
use crate :: {
81
269
tests:: {
82
270
check_diagnostics, check_diagnostics_with_config, check_diagnostics_with_disabled,
271
+ check_fix,
83
272
} ,
84
273
DiagnosticsConfig ,
85
274
} ;
@@ -168,4 +357,100 @@ fn foo() {
168
357
config. disabled . insert ( "syntax-error" . to_owned ( ) ) ;
169
358
check_diagnostics_with_config ( config, "fn foo() { (). }" ) ;
170
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
+ }
171
456
}
0 commit comments