ide_diagnostics/handlers/
unresolved_field.rs

1use either::Either;
2use hir::{Adt, FileRange, HasSource, HirDisplay, InFile, Struct, Union, db::ExpandDatabase};
3use ide_db::text_edit::TextEdit;
4use ide_db::{
5    assists::{Assist, AssistId},
6    helpers::is_editable_crate,
7    label::Label,
8    source_change::{SourceChange, SourceChangeBuilder},
9};
10use syntax::{
11    AstNode, AstPtr, Direction, SyntaxKind, TextSize, algo,
12    ast::{self, FieldList, Name, Visibility, edit::IndentLevel, make},
13};
14use syntax::{
15    SyntaxNode,
16    ast::{Type, edit::AstNodeEdit},
17};
18
19use crate::{Diagnostic, DiagnosticCode, DiagnosticsContext, adjusted_display_range};
20
21// Diagnostic: unresolved-field
22//
23// This diagnostic is triggered if a field does not exist on a given type.
24pub(crate) fn unresolved_field(
25    ctx: &DiagnosticsContext<'_>,
26    d: &hir::UnresolvedField<'_>,
27) -> Diagnostic {
28    let method_suffix = if d.method_with_same_name_exists {
29        ", but a method with a similar name exists"
30    } else {
31        ""
32    };
33    Diagnostic::new(
34        DiagnosticCode::RustcHardError("E0559"),
35        format!(
36            "no field `{}` on type `{}`{method_suffix}",
37            d.name.display(ctx.sema.db, ctx.edition),
38            d.receiver.display(ctx.sema.db, ctx.display_target)
39        ),
40        adjusted_display_range(ctx, d.expr, &|expr| {
41            Some(
42                match expr.left()? {
43                    ast::Expr::MethodCallExpr(it) => it.name_ref(),
44                    ast::Expr::FieldExpr(it) => it.name_ref(),
45                    _ => None,
46                }?
47                .syntax()
48                .text_range(),
49            )
50        }),
51    )
52    .with_fixes(fixes(ctx, d))
53}
54
55fn fixes(ctx: &DiagnosticsContext<'_>, d: &hir::UnresolvedField<'_>) -> Option<Vec<Assist>> {
56    let mut fixes = Vec::new();
57    if d.method_with_same_name_exists {
58        fixes.extend(method_fix(ctx, &d.expr));
59    }
60    fixes.extend(field_fix(ctx, d));
61    if fixes.is_empty() { None } else { Some(fixes) }
62}
63
64// FIXME: Add Snippet Support
65fn field_fix(ctx: &DiagnosticsContext<'_>, d: &hir::UnresolvedField<'_>) -> Option<Assist> {
66    // Get the FileRange of the invalid field access
67    let root = ctx.sema.db.parse_or_expand(d.expr.file_id);
68    let expr = d.expr.value.to_node(&root).left()?;
69
70    let error_range = ctx.sema.original_range_opt(expr.syntax())?;
71    let field_name = d.name.as_str();
72    // Convert the receiver to an ADT
73    let adt = d.receiver.strip_references().as_adt()?;
74    let target_module = adt.module(ctx.sema.db);
75
76    let suggested_type = if let Some(new_field_type) =
77        ctx.sema.type_of_expr(&expr).map(|v| v.adjusted()).filter(|it| !it.is_unknown())
78    {
79        let display =
80            new_field_type.display_source_code(ctx.sema.db, target_module.into(), false).ok();
81        make::ty(display.as_deref().unwrap_or("()"))
82    } else {
83        make::ty("()")
84    };
85
86    if !is_editable_crate(target_module.krate(ctx.sema.db), ctx.sema.db)
87        || SyntaxKind::from_keyword(field_name, ctx.edition).is_some()
88    {
89        return None;
90    }
91
92    match adt {
93        Adt::Struct(adt_struct) => {
94            add_field_to_struct_fix(ctx, adt_struct, field_name, suggested_type, error_range)
95        }
96        Adt::Union(adt_union) => {
97            add_variant_to_union(ctx, adt_union, field_name, suggested_type, error_range)
98        }
99        _ => None,
100    }
101}
102
103fn 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<Assist> {
110    let adt_source = adt_union.source(ctx.sema.db)?;
111    let adt_syntax = adt_source.syntax();
112    let field_list = adt_source.value.record_field_list()?;
113    let range = adt_syntax.original_file_range_rooted(ctx.sema.db);
114    let field_name = make::name(field_name);
115
116    let (offset, record_field) =
117        record_field_layout(None, field_name, suggested_type, field_list, adt_syntax.value)?;
118
119    let mut src_change_builder = SourceChangeBuilder::new(range.file_id.file_id(ctx.sema.db));
120    src_change_builder.insert(offset, record_field);
121    Some(Assist {
122        id: AssistId::quick_fix("add-variant-to-union"),
123        label: Label::new("Add field to union".to_owned()),
124        group: None,
125        target: error_range.range,
126        source_change: Some(src_change_builder.finish()),
127        command: None,
128    })
129}
130
131fn add_field_to_struct_fix(
132    ctx: &DiagnosticsContext<'_>,
133    adt_struct: Struct,
134    field_name: &str,
135    suggested_type: Type,
136    error_range: FileRange,
137) -> Option<Assist> {
138    let struct_source = adt_struct.source(ctx.sema.db)?;
139    let struct_syntax = struct_source.syntax();
140    let struct_range = struct_syntax.original_file_range_rooted(ctx.sema.db);
141    let field_list = struct_source.value.field_list();
142    match field_list {
143        Some(FieldList::RecordFieldList(field_list)) => {
144            // Get range of final field in the struct
145            let visibility = if error_range.file_id == struct_range.file_id {
146                None
147            } else {
148                Some(make::visibility_pub_crate())
149            };
150
151            let field_name = match field_name.chars().next() {
152                Some(ch) if ch.is_numeric() => return None,
153                Some(_) => make::name(field_name),
154                None => return None,
155            };
156
157            let (offset, record_field) = record_field_layout(
158                visibility,
159                field_name,
160                suggested_type,
161                field_list,
162                struct_syntax.value,
163            )?;
164
165            let mut src_change_builder =
166                SourceChangeBuilder::new(struct_range.file_id.file_id(ctx.sema.db));
167
168            // FIXME: Allow for choosing a visibility modifier see https://github.com/rust-lang/rust-analyzer/issues/11563
169            src_change_builder.insert(offset, record_field);
170            Some(Assist {
171                id: AssistId::quick_fix("add-field-to-record-struct"),
172                label: Label::new("Add field to Record Struct".to_owned()),
173                group: None,
174                target: error_range.range,
175                source_change: Some(src_change_builder.finish()),
176                command: None,
177            })
178        }
179        None => {
180            // Add a field list to the Unit Struct
181            let mut src_change_builder =
182                SourceChangeBuilder::new(struct_range.file_id.file_id(ctx.sema.db));
183            let field_name = match field_name.chars().next() {
184                // FIXME : See match arm below regarding tuple structs.
185                Some(ch) if ch.is_numeric() => return None,
186                Some(_) => make::name(field_name),
187                None => return None,
188            };
189            let visibility = if error_range.file_id == struct_range.file_id {
190                None
191            } else {
192                Some(make::visibility_pub_crate())
193            };
194            // FIXME: Allow for choosing a visibility modifier see https://github.com/rust-lang/rust-analyzer/issues/11563
195            let indent = IndentLevel::from_node(struct_syntax.value);
196
197            let field =
198                make::record_field(visibility, field_name, suggested_type).indent(indent + 1);
199            // A Unit Struct with no `;` is invalid syntax. We should not suggest this fix.
200            let semi_colon =
201                algo::skip_trivia_token(struct_syntax.value.last_token()?, Direction::Prev)?;
202            if semi_colon.kind() != SyntaxKind::SEMICOLON {
203                return None;
204            }
205            src_change_builder.replace(
206                semi_colon.text_range(),
207                format!(" {{\n{}{field},\n{indent}}}", indent + 1),
208            );
209
210            Some(Assist {
211                id: AssistId::quick_fix("convert-unit-struct-to-record-struct"),
212                label: Label::new("Convert Unit Struct to Record Struct and add field".to_owned()),
213                group: None,
214                target: error_range.range,
215                source_change: Some(src_change_builder.finish()),
216                command: None,
217            })
218        }
219        Some(FieldList::TupleFieldList(_tuple)) => {
220            // FIXME: Add support for Tuple Structs. Tuple Structs are not sent to this diagnostic
221            None
222        }
223    }
224}
225
226/// Used to determine the layout of the record field in the struct.
227fn record_field_layout(
228    visibility: Option<Visibility>,
229    name: Name,
230    suggested_type: Type,
231    field_list: ast::RecordFieldList,
232    struct_syntax: &SyntaxNode,
233) -> Option<(TextSize, String)> {
234    let (offset, needs_comma, indent) = match field_list.fields().last() {
235        Some(record_field) => {
236            let syntax = algo::skip_trivia_token(field_list.r_curly_token()?, Direction::Prev)?;
237
238            let last_field_syntax = record_field.syntax();
239            let last_field_indent = IndentLevel::from_node(last_field_syntax);
240            (
241                last_field_syntax.text_range().end(),
242                syntax.kind() != SyntaxKind::COMMA,
243                last_field_indent,
244            )
245        }
246        // Empty Struct. Add a field right before the closing brace
247        None => {
248            let indent = IndentLevel::from_node(struct_syntax) + 1;
249            let offset = field_list.l_curly_token()?.text_range().end();
250            (offset, false, indent)
251        }
252    };
253    let trailing_new_line = if !field_list.syntax().text().contains_char('\n') {
254        format!("\n{}", field_list.indent_level())
255    } else {
256        String::new()
257    };
258    let comma = if needs_comma { ",\n" } else { "\n" };
259    let record_field = make::record_field(visibility, name, suggested_type);
260
261    Some((offset, format!("{comma}{indent}{record_field}{trailing_new_line}")))
262}
263
264// FIXME: We should fill out the call here, move the cursor and trigger signature help
265fn method_fix(
266    ctx: &DiagnosticsContext<'_>,
267    expr_ptr: &InFile<AstPtr<Either<ast::Expr, ast::Pat>>>,
268) -> Option<Assist> {
269    let root = ctx.sema.db.parse_or_expand(expr_ptr.file_id);
270    let expr = expr_ptr.value.to_node(&root);
271    let FileRange { range, file_id } = ctx.sema.original_range_opt(expr.syntax())?;
272    Some(Assist {
273        id: AssistId::quick_fix("expected-field-found-method-call-fix"),
274        label: Label::new("Use parentheses to call the method".to_owned()),
275        group: None,
276        target: range,
277        source_change: Some(SourceChange::from_text_edit(
278            file_id.file_id(ctx.sema.db),
279            TextEdit::insert(range.end(), "()".to_owned()),
280        )),
281        command: None,
282    })
283}
284#[cfg(test)]
285mod tests {
286
287    use crate::{
288        DiagnosticsConfig,
289        tests::{
290            check_diagnostics, check_diagnostics_with_config, check_diagnostics_with_disabled,
291            check_fix, check_no_fix,
292        },
293    };
294
295    #[test]
296    fn smoke_test() {
297        check_diagnostics(
298            r#"
299fn main() {
300    ().foo;
301    // ^^^ error: no field `foo` on type `()`
302}
303"#,
304        );
305    }
306
307    #[test]
308    fn method_clash() {
309        check_diagnostics(
310            r#"
311struct Foo;
312impl Foo {
313    fn bar(&self) {}
314}
315fn foo() {
316    Foo.bar;
317     // ^^^ 💡 error: no field `bar` on type `Foo`, but a method with a similar name exists
318}
319"#,
320        );
321    }
322
323    #[test]
324    fn method_trait_() {
325        check_diagnostics(
326            r#"
327struct Foo;
328trait Bar {
329    fn bar(&self) {}
330}
331impl Bar for Foo {}
332fn foo() {
333    Foo.bar;
334     // ^^^ 💡 error: no field `bar` on type `Foo`, but a method with a similar name exists
335}
336"#,
337        );
338    }
339
340    #[test]
341    fn method_trait_2() {
342        check_diagnostics(
343            r#"
344struct Foo;
345trait Bar {
346    fn bar(&self);
347}
348impl Bar for Foo {
349    fn bar(&self) {}
350}
351fn foo() {
352    Foo.bar;
353     // ^^^ 💡 error: no field `bar` on type `Foo`, but a method with a similar name exists
354}
355"#,
356        );
357    }
358
359    #[test]
360    fn no_diagnostic_on_unknown() {
361        check_diagnostics_with_disabled(
362            r#"
363fn foo() {
364    x.foo;
365    (&x).foo;
366    (&((x,),),).foo;
367}
368"#,
369            &["E0425"],
370        );
371    }
372
373    #[test]
374    fn no_diagnostic_for_missing_name() {
375        let mut config = DiagnosticsConfig::test_sample();
376        config.disabled.insert("syntax-error".to_owned());
377        check_diagnostics_with_config(config, "fn foo() { (). }");
378    }
379
380    #[test]
381    fn unresolved_field_fix_on_unit() {
382        check_fix(
383            r#"
384            mod indent {
385                struct Foo;
386
387                fn foo() {
388                    Foo.bar$0;
389                }
390            }
391            "#,
392            r#"
393            mod indent {
394                struct Foo {
395                    bar: (),
396                }
397
398                fn foo() {
399                    Foo.bar;
400                }
401            }
402            "#,
403        );
404    }
405    #[test]
406    fn unresolved_field_fix_on_empty() {
407        check_fix(
408            r#"
409            mod indent {
410                struct Foo{
411                }
412
413                fn foo() {
414                    let foo = Foo{};
415                    foo.bar$0;
416                }
417            }
418            "#,
419            r#"
420            mod indent {
421                struct Foo{
422                    bar: ()
423                }
424
425                fn foo() {
426                    let foo = Foo{};
427                    foo.bar;
428                }
429            }
430            "#,
431        );
432
433        check_fix(
434            r#"
435            mod indent {
436                struct Foo {}
437
438                fn foo() {
439                    let foo = Foo{};
440                    foo.bar$0;
441                }
442            }
443            "#,
444            r#"
445            mod indent {
446                struct Foo {
447                    bar: ()
448                }
449
450                fn foo() {
451                    let foo = Foo{};
452                    foo.bar;
453                }
454            }
455            "#,
456        );
457    }
458    #[test]
459    fn unresolved_field_fix_on_struct() {
460        check_fix(
461            r#"
462                struct Foo{
463                    a: i32
464                }
465
466                fn foo() {
467                    let foo = Foo{a: 0};
468                    foo.bar$0;
469                }
470            "#,
471            r#"
472                struct Foo{
473                    a: i32,
474                    bar: ()
475                }
476
477                fn foo() {
478                    let foo = Foo{a: 0};
479                    foo.bar;
480                }
481            "#,
482        );
483    }
484    #[test]
485    fn unresolved_field_fix_on_union() {
486        check_fix(
487            r#"
488                union Foo{
489                    a: i32
490                }
491
492                fn foo() {
493                    let foo = Foo{a: 0};
494                    foo.bar$0;
495                }
496            "#,
497            r#"
498                union Foo{
499                    a: i32,
500                    bar: ()
501                }
502
503                fn foo() {
504                    let foo = Foo{a: 0};
505                    foo.bar;
506                }
507            "#,
508        );
509    }
510
511    #[test]
512    fn no_fix_when_indexed() {
513        check_no_fix(
514            r#"
515            struct Kek {}
516impl Kek {
517    pub fn foo(self) {
518        self.$00
519    }
520}
521
522fn main() {}
523            "#,
524        )
525    }
526
527    #[test]
528    fn no_fix_when_without_field() {
529        check_no_fix(
530            r#"
531            struct Kek {}
532impl Kek {
533    pub fn foo(self) {
534        self.$0
535    }
536}
537
538fn main() {}
539            "#,
540        )
541    }
542
543    #[test]
544    fn regression_18683() {
545        check_diagnostics(
546            r#"
547struct S;
548impl S {
549    fn f(self) {
550        self.self
551          // ^^^^ error: no field `self` on type `S`
552    }
553}
554        "#,
555        );
556    }
557}