use either::Either; use hir::{Adt, FileRange, HasSource, HirDisplay, InFile, Struct, Union, db::ExpandDatabase}; use ide_db::text_edit::TextEdit; use ide_db::{ assists::{Assist, AssistId}, helpers::is_editable_crate, label::Label, source_change::{SourceChange, SourceChangeBuilder}, }; use syntax::{ AstNode, AstPtr, Direction, SyntaxKind, TextSize, algo, ast::{self, FieldList, Name, Visibility, edit::IndentLevel, make}, }; use syntax::{ SyntaxNode, ast::{Type, edit::AstNodeEdit}, }; use crate::{Diagnostic, DiagnosticCode, DiagnosticsContext, adjusted_display_range}; // Diagnostic: unresolved-field // // This diagnostic is triggered if a field does not exist on a given type. pub(crate) fn unresolved_field( ctx: &DiagnosticsContext<'_>, d: &hir::UnresolvedField<'_>, ) -> Diagnostic { let method_suffix = if d.method_with_same_name_exists { ", but a method with a similar name exists" } else { "" }; Diagnostic::new( DiagnosticCode::RustcHardError("E0559"), format!( "no field `{}` on type `{}`{method_suffix}", d.name.display(ctx.sema.db, ctx.edition), d.receiver.display(ctx.sema.db, ctx.display_target) ), adjusted_display_range(ctx, d.expr, &|expr| { Some( match expr.left()? { ast::Expr::MethodCallExpr(it) => it.name_ref(), ast::Expr::FieldExpr(it) => it.name_ref(), _ => None, }? .syntax() .text_range(), ) }), ) .with_fixes(fixes(ctx, d)) } fn fixes(ctx: &DiagnosticsContext<'_>, d: &hir::UnresolvedField<'_>) -> Option> { let mut fixes = Vec::new(); if d.method_with_same_name_exists { fixes.extend(method_fix(ctx, &d.expr)); } fixes.extend(field_fix(ctx, d)); if fixes.is_empty() { None } else { Some(fixes) } } // FIXME: Add Snippet Support fn field_fix(ctx: &DiagnosticsContext<'_>, d: &hir::UnresolvedField<'_>) -> Option { // Get the FileRange of the invalid field access let root = ctx.sema.db.parse_or_expand(d.expr.file_id); let expr = d.expr.value.to_node(&root).left()?; let error_range = ctx.sema.original_range_opt(expr.syntax())?; let field_name = d.name.as_str(); // Convert the receiver to an ADT let adt = d.receiver.strip_references().as_adt()?; let target_module = adt.module(ctx.sema.db); let suggested_type = if let Some(new_field_type) = ctx.sema.type_of_expr(&expr).map(|v| v.adjusted()).filter(|it| !it.is_unknown()) { let display = new_field_type.display_source_code(ctx.sema.db, target_module.into(), false).ok(); make::ty(display.as_deref().unwrap_or("()")) } else { make::ty("()") }; if !is_editable_crate(target_module.krate(ctx.sema.db), ctx.sema.db) || SyntaxKind::from_keyword(field_name, ctx.edition).is_some() { return None; } match adt { Adt::Struct(adt_struct) => { add_field_to_struct_fix(ctx, adt_struct, field_name, suggested_type, error_range) } Adt::Union(adt_union) => { add_variant_to_union(ctx, adt_union, field_name, suggested_type, error_range) } _ => None, } } fn add_variant_to_union( ctx: &DiagnosticsContext<'_>, adt_union: Union, field_name: &str, suggested_type: Type, error_range: FileRange, ) -> Option { let adt_source = adt_union.source(ctx.sema.db)?; let adt_syntax = adt_source.syntax(); let field_list = adt_source.value.record_field_list()?; let range = adt_syntax.original_file_range_rooted(ctx.sema.db); let field_name = make::name(field_name); let (offset, record_field) = record_field_layout(None, field_name, suggested_type, field_list, adt_syntax.value)?; let mut src_change_builder = SourceChangeBuilder::new(range.file_id.file_id(ctx.sema.db)); src_change_builder.insert(offset, record_field); Some(Assist { id: AssistId::quick_fix("add-variant-to-union"), label: Label::new("Add field to union".to_owned()), group: None, target: error_range.range, source_change: Some(src_change_builder.finish()), command: None, }) } fn add_field_to_struct_fix( ctx: &DiagnosticsContext<'_>, adt_struct: Struct, field_name: &str, suggested_type: Type, error_range: FileRange, ) -> Option { let struct_source = adt_struct.source(ctx.sema.db)?; let struct_syntax = struct_source.syntax(); let struct_range = struct_syntax.original_file_range_rooted(ctx.sema.db); let field_list = struct_source.value.field_list(); match field_list { Some(FieldList::RecordFieldList(field_list)) => { // Get range of final field in the struct let visibility = if error_range.file_id == struct_range.file_id { None } else { Some(make::visibility_pub_crate()) }; let field_name = match field_name.chars().next() { Some(ch) if ch.is_numeric() => return None, Some(_) => make::name(field_name), None => return None, }; let (offset, record_field) = record_field_layout( visibility, field_name, suggested_type, field_list, struct_syntax.value, )?; let mut src_change_builder = SourceChangeBuilder::new(struct_range.file_id.file_id(ctx.sema.db)); // FIXME: Allow for choosing a visibility modifier see https://github.com/rust-lang/rust-analyzer/issues/11563 src_change_builder.insert(offset, record_field); Some(Assist { id: AssistId::quick_fix("add-field-to-record-struct"), label: Label::new("Add field to Record Struct".to_owned()), group: None, target: error_range.range, source_change: Some(src_change_builder.finish()), command: None, }) } None => { // Add a field list to the Unit Struct let mut src_change_builder = SourceChangeBuilder::new(struct_range.file_id.file_id(ctx.sema.db)); let field_name = match field_name.chars().next() { // FIXME : See match arm below regarding tuple structs. Some(ch) if ch.is_numeric() => return None, Some(_) => make::name(field_name), None => return None, }; let visibility = if error_range.file_id == struct_range.file_id { None } else { Some(make::visibility_pub_crate()) }; // FIXME: Allow for choosing a visibility modifier see https://github.com/rust-lang/rust-analyzer/issues/11563 let indent = IndentLevel::from_node(struct_syntax.value); let field = make::record_field(visibility, field_name, suggested_type).indent(indent + 1); // A Unit Struct with no `;` is invalid syntax. We should not suggest this fix. let semi_colon = algo::skip_trivia_token(struct_syntax.value.last_token()?, Direction::Prev)?; if semi_colon.kind() != SyntaxKind::SEMICOLON { return None; } src_change_builder.replace( semi_colon.text_range(), format!(" {{\n{}{field},\n{indent}}}", indent + 1), ); Some(Assist { id: AssistId::quick_fix("convert-unit-struct-to-record-struct"), label: Label::new("Convert Unit Struct to Record Struct and add field".to_owned()), group: None, target: error_range.range, source_change: Some(src_change_builder.finish()), command: None, }) } Some(FieldList::TupleFieldList(_tuple)) => { // FIXME: Add support for Tuple Structs. Tuple Structs are not sent to this diagnostic None } } } /// Used to determine the layout of the record field in the struct. fn record_field_layout( visibility: Option, name: Name, suggested_type: Type, field_list: ast::RecordFieldList, struct_syntax: &SyntaxNode, ) -> Option<(TextSize, String)> { let (offset, needs_comma, indent) = match field_list.fields().last() { Some(record_field) => { let syntax = algo::skip_trivia_token(field_list.r_curly_token()?, Direction::Prev)?; let last_field_syntax = record_field.syntax(); let last_field_indent = IndentLevel::from_node(last_field_syntax); ( last_field_syntax.text_range().end(), syntax.kind() != SyntaxKind::COMMA, last_field_indent, ) } // Empty Struct. Add a field right before the closing brace None => { let indent = IndentLevel::from_node(struct_syntax) + 1; let offset = field_list.l_curly_token()?.text_range().end(); (offset, false, indent) } }; let trailing_new_line = if !field_list.syntax().text().contains_char('\n') { format!("\n{}", field_list.indent_level()) } else { String::new() }; let comma = if needs_comma { ",\n" } else { "\n" }; let record_field = make::record_field(visibility, name, suggested_type); Some((offset, format!("{comma}{indent}{record_field}{trailing_new_line}"))) } // FIXME: We should fill out the call here, move the cursor and trigger signature help fn method_fix( ctx: &DiagnosticsContext<'_>, expr_ptr: &InFile>>, ) -> Option { let root = ctx.sema.db.parse_or_expand(expr_ptr.file_id); let expr = expr_ptr.value.to_node(&root); let FileRange { range, file_id } = ctx.sema.original_range_opt(expr.syntax())?; Some(Assist { id: AssistId::quick_fix("expected-field-found-method-call-fix"), label: Label::new("Use parentheses to call the method".to_owned()), group: None, target: range, source_change: Some(SourceChange::from_text_edit( file_id.file_id(ctx.sema.db), TextEdit::insert(range.end(), "()".to_owned()), )), command: None, }) } #[cfg(test)] mod tests { use crate::{ DiagnosticsConfig, tests::{ check_diagnostics, check_diagnostics_with_config, check_diagnostics_with_disabled, check_fix, check_no_fix, }, }; #[test] fn smoke_test() { check_diagnostics( r#" fn main() { ().foo; // ^^^ error: no field `foo` on type `()` } "#, ); } #[test] fn method_clash() { check_diagnostics( r#" struct Foo; impl Foo { fn bar(&self) {} } fn foo() { Foo.bar; // ^^^ 💡 error: no field `bar` on type `Foo`, but a method with a similar name exists } "#, ); } #[test] fn method_trait_() { check_diagnostics( r#" struct Foo; trait Bar { fn bar(&self) {} } impl Bar for Foo {} fn foo() { Foo.bar; // ^^^ 💡 error: no field `bar` on type `Foo`, but a method with a similar name exists } "#, ); } #[test] fn method_trait_2() { check_diagnostics( r#" struct Foo; trait Bar { fn bar(&self); } impl Bar for Foo { fn bar(&self) {} } fn foo() { Foo.bar; // ^^^ 💡 error: no field `bar` on type `Foo`, but a method with a similar name exists } "#, ); } #[test] fn no_diagnostic_on_unknown() { check_diagnostics_with_disabled( r#" fn foo() { x.foo; (&x).foo; (&((x,),),).foo; } "#, &["E0425"], ); } #[test] fn no_diagnostic_for_missing_name() { let mut config = DiagnosticsConfig::test_sample(); config.disabled.insert("syntax-error".to_owned()); check_diagnostics_with_config(config, "fn foo() { (). }"); } #[test] fn unresolved_field_fix_on_unit() { check_fix( r#" mod indent { struct Foo; fn foo() { Foo.bar$0; } } "#, r#" mod indent { struct Foo { bar: (), } fn foo() { Foo.bar; } } "#, ); } #[test] fn unresolved_field_fix_on_empty() { check_fix( r#" mod indent { struct Foo{ } fn foo() { let foo = Foo{}; foo.bar$0; } } "#, r#" mod indent { struct Foo{ bar: () } fn foo() { let foo = Foo{}; foo.bar; } } "#, ); check_fix( r#" mod indent { struct Foo {} fn foo() { let foo = Foo{}; foo.bar$0; } } "#, r#" mod indent { struct Foo { bar: () } fn foo() { let foo = Foo{}; foo.bar; } } "#, ); } #[test] fn unresolved_field_fix_on_struct() { check_fix( r#" struct Foo{ a: i32 } fn foo() { let foo = Foo{a: 0}; foo.bar$0; } "#, r#" struct Foo{ a: i32, bar: () } fn foo() { let foo = Foo{a: 0}; foo.bar; } "#, ); } #[test] fn unresolved_field_fix_on_union() { check_fix( r#" union Foo{ a: i32 } fn foo() { let foo = Foo{a: 0}; foo.bar$0; } "#, r#" union Foo{ a: i32, bar: () } fn foo() { let foo = Foo{a: 0}; foo.bar; } "#, ); } #[test] fn no_fix_when_indexed() { check_no_fix( r#" struct Kek {} impl Kek { pub fn foo(self) { self.$00 } } fn main() {} "#, ) } #[test] fn no_fix_when_without_field() { check_no_fix( r#" struct Kek {} impl Kek { pub fn foo(self) { self.$0 } } fn main() {} "#, ) } #[test] fn regression_18683() { check_diagnostics( r#" struct S; impl S { fn f(self) { self.self // ^^^^ error: no field `self` on type `S` } } "#, ); } }