diff --git a/crates/hir-ty/src/infer.rs b/crates/hir-ty/src/infer.rs index e880438e3a78..b25c14b1511f 100644 --- a/crates/hir-ty/src/infer.rs +++ b/crates/hir-ty/src/infer.rs @@ -229,6 +229,7 @@ pub enum InferenceDiagnostic { expr: ExprId, receiver: Ty, name: Name, + arg_types: Vec, /// Contains the type the field resolves to field_with_same_name: Option, assoc_func_with_same_name: Option, diff --git a/crates/hir-ty/src/infer/expr.rs b/crates/hir-ty/src/infer/expr.rs index d43c99fc2827..ee7fe5445d79 100644 --- a/crates/hir-ty/src/infer/expr.rs +++ b/crates/hir-ty/src/infer/expr.rs @@ -1886,10 +1886,15 @@ impl InferenceContext<'_> { }, ); + let arg_types = args + .iter() + .map(|&arg| self.infer_expr_inner(arg, &Expectation::none(), ExprIsRead::Yes)) + .collect(); self.push_diagnostic(InferenceDiagnostic::UnresolvedMethodCall { expr: tgt_expr, receiver: receiver_ty.clone(), name: method_name.clone(), + arg_types, field_with_same_name: field_with_same_name_exists.clone(), assoc_func_with_same_name, }); diff --git a/crates/hir/src/diagnostics.rs b/crates/hir/src/diagnostics.rs index c1e814ec223e..ff979e3c6821 100644 --- a/crates/hir/src/diagnostics.rs +++ b/crates/hir/src/diagnostics.rs @@ -260,6 +260,7 @@ pub struct UnresolvedMethodCall<'db> { pub expr: InFile, pub receiver: Type<'db>, pub name: Name, + pub arg_types: Vec>, pub field_with_same_name: Option>, pub assoc_func_with_same_name: Option, } @@ -688,6 +689,7 @@ impl<'db> AnyDiagnostic<'db> { expr, receiver, name, + arg_types, field_with_same_name, assoc_func_with_same_name, } => { @@ -695,6 +697,7 @@ impl<'db> AnyDiagnostic<'db> { UnresolvedMethodCall { expr, name: name.clone(), + arg_types: arg_types.iter().map(|ty| Type::new(db, def, ty.clone())).collect(), receiver: Type::new(db, def, receiver.clone()), field_with_same_name: field_with_same_name .clone() diff --git a/crates/hir/src/has_source.rs b/crates/hir/src/has_source.rs index 4767d4792e71..6505d4428ec4 100644 --- a/crates/hir/src/has_source.rs +++ b/crates/hir/src/has_source.rs @@ -12,7 +12,7 @@ use syntax::ast; use tt::TextRange; use crate::{ - Adt, Callee, Const, Enum, ExternCrateDecl, Field, FieldSource, Function, Impl, + Adt, AssocItem, Callee, Const, Enum, ExternCrateDecl, Field, FieldSource, Function, Impl, InlineAsmOperand, Label, LifetimeParam, LocalSource, Macro, Module, Param, SelfParam, Static, Struct, Trait, TraitAlias, TypeAlias, TypeOrConstParam, Union, Variant, VariantDef, db::HirDatabase, @@ -100,6 +100,17 @@ impl HasSource for Field { Some(field_source) } } +impl HasSource for AssocItem { + type Ast = ast::AssocItem; + + fn source(self, db: &dyn HirDatabase) -> Option> { + match self { + AssocItem::Const(c) => Some(c.source(db)?.map(ast::AssocItem::Const)), + AssocItem::Function(f) => Some(f.source(db)?.map(ast::AssocItem::Fn)), + AssocItem::TypeAlias(t) => Some(t.source(db)?.map(ast::AssocItem::TypeAlias)), + } + } +} impl HasSource for Adt { type Ast = ast::Adt; fn source(self, db: &dyn HirDatabase) -> Option> { diff --git a/crates/ide-diagnostics/src/handlers/unresolved_method.rs b/crates/ide-diagnostics/src/handlers/unresolved_method.rs index dcca85d4db33..77cf01ad6b6a 100644 --- a/crates/ide-diagnostics/src/handlers/unresolved_method.rs +++ b/crates/ide-diagnostics/src/handlers/unresolved_method.rs @@ -1,17 +1,20 @@ +use hir::HasSource; use hir::{FileRange, HirDisplay, InFile, db::ExpandDatabase}; +use ide_db::assists::ExprFillDefaultMode; use ide_db::text_edit::TextEdit; use ide_db::{ assists::{Assist, AssistId}, label::Label, source_change::SourceChange, }; +use itertools::Itertools; use syntax::{ AstNode, SmolStr, TextRange, ToSmolStr, ast::{self, HasArgList, make}, format_smolstr, }; -use crate::{Diagnostic, DiagnosticCode, DiagnosticsContext, adjusted_display_range}; +use crate::{Diagnostic, DiagnosticCode, DiagnosticsContext, adjusted_display_range, fix}; // Diagnostic: unresolved-method // @@ -67,9 +70,73 @@ fn fixes(ctx: &DiagnosticsContext<'_>, d: &hir::UnresolvedMethodCall<'_>) -> Opt fixes.push(assoc_func_fix); } + if let Some(method_fix) = add_method_fix(ctx, d) { + fixes.push(method_fix); + } + if fixes.is_empty() { None } else { Some(fixes) } } +/// Fix to add the missing method. +fn add_method_fix( + ctx: &DiagnosticsContext<'_>, + d: &hir::UnresolvedMethodCall<'_>, +) -> Option { + let root = ctx.sema.db.parse_or_expand(d.expr.file_id); + let expr = d.expr.value.to_node(&root).left()?; + + let db = ctx.sema.db; + let ty = d.receiver.clone(); + + let mut all_impl_blocks = hir::Impl::all_for_type(db, ty.clone()) + .into_iter() + .chain(hir::Impl::all_for_type(db, ty.strip_reference())); + let impl_block = all_impl_blocks.find(|block| block.trait_(db).is_none())?; + + let items = impl_block.items(db); + let last_item = items.last()?; + let source = last_item.source(db)?; + let file_id = match source.file_id { + hir::HirFileId::FileId(file_id) => file_id, + hir::HirFileId::MacroFile(_) => return None, + }; + let module_id = last_item.module(db); + let end_of_last_item = source.node_file_range().file_range()?.range.end(); + + let method_body = match ctx.config.expr_fill_default { + ExprFillDefaultMode::Default | ExprFillDefaultMode::Todo => "todo!()", + ExprFillDefaultMode::Underscore => "_", + }; + + let method_name = d.name.as_str(); + let params: Vec = d + .arg_types + .iter() + .enumerate() + .map(|(i, ty)| { + let name = format!("arg{}", i + 1); + let ty = ty.display_source_code(db, module_id.into(), true).unwrap(); + format!("{name}: {ty}") + }) + .collect(); + let param_list = if params.is_empty() { + "&self".to_owned() + } else { + format!("&self, {}", params.into_iter().join(",")) + }; + + let text_to_insert = format!("\n fn {method_name}({param_list}) {{ {method_body} }}"); + Some(fix( + "add-missing-method", + "Add missing method", + SourceChange::from_text_edit( + file_id.file_id(db), + TextEdit::insert(end_of_last_item, text_to_insert), + ), + ctx.sema.original_range(expr.syntax()).range, + )) +} + fn field_fix( ctx: &DiagnosticsContext<'_>, d: &hir::UnresolvedMethodCall<'_>, @@ -286,6 +353,116 @@ fn main() { ); } + #[test] + fn test_add_method_fix_ref_self() { + check_fix( + r#" +struct Dolphin; + +impl Dolphin { + fn do_trick(&self) {} +} + +#[allow(unused)] +fn say_hi_to(dolphin: &Dolphin) { + "hello " + dolphin.name$0(); +}"#, + r#" +struct Dolphin; + +impl Dolphin { + fn do_trick(&self) {} + fn name(&self) { todo!() } +} + +#[allow(unused)] +fn say_hi_to(dolphin: &Dolphin) { + "hello " + dolphin.name(); +}"#, + ); + } + + #[test] + fn test_add_method_fix_self() { + check_fix( + r#" +struct Tiger; + +impl Tiger { + fn sleep(&self) {} +} + +fn main() { + let t = Tiger; + t.roar$0(); +}"#, + r#" +struct Tiger; + +impl Tiger { + fn sleep(&self) {} + fn roar(&self) { todo!() } +} + +fn main() { + let t = Tiger; + t.roar(); +}"#, + ); + } + + #[test] + fn test_add_method_from_same_impl_block() { + check_fix( + r#" +struct Tiger; + +impl Tiger { + fn sleep(&self) {} + fn do_stuff(self) { + self.sleep(); + self.roar$0(); + self.sleep(); + } +}"#, + r#" +struct Tiger; + +impl Tiger { + fn sleep(&self) {} + fn do_stuff(self) { + self.sleep(); + self.roar(); + self.sleep(); + } + fn roar(&self) { todo!() } +}"#, + ); + } + + #[test] + fn test_add_method_with_args() { + check_fix( + r#" +struct Speaker; + +impl Speaker { + fn greet(&self) { + self.say("hello")$0; + } +}"#, + r#" +struct Speaker; + +impl Speaker { + fn greet(&self) { + self.say("hello"); + } + fn say(&self, arg1: &'static str) { todo!() } +}"#, + ); + } + #[test] fn smoke_test_in_macro_def_site() { check_diagnostics(