2024-01-02 13:30:13 +00:00
|
|
|
use hir::{db::ExpandDatabase, AssocItem, HirDisplay, InFile};
|
2023-03-03 19:41:17 +00:00
|
|
|
use ide_db::{
|
|
|
|
assists::{Assist, AssistId, AssistKind},
|
|
|
|
base_db::FileRange,
|
|
|
|
label::Label,
|
|
|
|
source_change::SourceChange,
|
|
|
|
};
|
2023-12-12 12:24:33 +00:00
|
|
|
use syntax::{
|
|
|
|
ast::{self, make, HasArgList},
|
|
|
|
AstNode, SmolStr, TextRange,
|
|
|
|
};
|
2023-03-03 19:41:17 +00:00
|
|
|
use text_edit::TextEdit;
|
|
|
|
|
2024-01-31 08:16:30 +00:00
|
|
|
use crate::{adjusted_display_range, Diagnostic, DiagnosticCode, DiagnosticsContext};
|
2023-03-03 19:41:17 +00:00
|
|
|
|
|
|
|
// Diagnostic: unresolved-method
|
|
|
|
//
|
|
|
|
// This diagnostic is triggered if a method does not exist on a given type.
|
|
|
|
pub(crate) fn unresolved_method(
|
|
|
|
ctx: &DiagnosticsContext<'_>,
|
|
|
|
d: &hir::UnresolvedMethodCall,
|
|
|
|
) -> Diagnostic {
|
2023-12-12 12:24:33 +00:00
|
|
|
let suffix = if d.field_with_same_name.is_some() {
|
2023-03-03 19:41:17 +00:00
|
|
|
", but a field with a similar name exists"
|
2023-12-12 12:24:33 +00:00
|
|
|
} else if d.assoc_func_with_same_name.is_some() {
|
|
|
|
", but an associated function with a similar name exists"
|
2023-03-03 19:41:17 +00:00
|
|
|
} else {
|
|
|
|
""
|
|
|
|
};
|
2023-12-08 17:46:36 +00:00
|
|
|
Diagnostic::new(
|
2023-06-14 22:17:22 +00:00
|
|
|
DiagnosticCode::RustcHardError("E0599"),
|
2023-03-03 19:41:17 +00:00
|
|
|
format!(
|
2023-12-12 12:24:33 +00:00
|
|
|
"no method `{}` on type `{}`{suffix}",
|
2023-05-24 16:04:29 +00:00
|
|
|
d.name.display(ctx.sema.db),
|
2023-03-03 19:41:17 +00:00
|
|
|
d.receiver.display(ctx.sema.db)
|
|
|
|
),
|
2024-01-31 08:16:30 +00:00
|
|
|
adjusted_display_range(ctx, d.expr, &|expr| {
|
2023-12-08 17:46:36 +00:00
|
|
|
Some(
|
|
|
|
match expr {
|
|
|
|
ast::Expr::MethodCallExpr(it) => it.name_ref(),
|
|
|
|
ast::Expr::FieldExpr(it) => it.name_ref(),
|
|
|
|
_ => None,
|
|
|
|
}?
|
|
|
|
.syntax()
|
|
|
|
.text_range(),
|
|
|
|
)
|
|
|
|
}),
|
2023-03-03 19:41:17 +00:00
|
|
|
)
|
|
|
|
.with_fixes(fixes(ctx, d))
|
2023-03-06 21:24:38 +00:00
|
|
|
.experimental()
|
2023-03-03 19:41:17 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
fn fixes(ctx: &DiagnosticsContext<'_>, d: &hir::UnresolvedMethodCall) -> Option<Vec<Assist>> {
|
2023-12-12 12:24:33 +00:00
|
|
|
let field_fix = if let Some(ty) = &d.field_with_same_name {
|
2023-03-03 19:41:17 +00:00
|
|
|
field_fix(ctx, d, ty)
|
|
|
|
} else {
|
|
|
|
// FIXME: add quickfix
|
|
|
|
None
|
2023-12-12 12:24:33 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
let assoc_func_fix = assoc_func_fix(ctx, d);
|
|
|
|
|
|
|
|
let mut fixes = vec![];
|
|
|
|
if let Some(field_fix) = field_fix {
|
|
|
|
fixes.push(field_fix);
|
|
|
|
}
|
|
|
|
if let Some(assoc_func_fix) = assoc_func_fix {
|
|
|
|
fixes.push(assoc_func_fix);
|
|
|
|
}
|
|
|
|
|
|
|
|
if fixes.is_empty() {
|
|
|
|
None
|
|
|
|
} else {
|
|
|
|
Some(fixes)
|
2023-03-03 19:41:17 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn field_fix(
|
|
|
|
ctx: &DiagnosticsContext<'_>,
|
|
|
|
d: &hir::UnresolvedMethodCall,
|
|
|
|
ty: &hir::Type,
|
2023-12-12 12:24:33 +00:00
|
|
|
) -> Option<Assist> {
|
2023-03-03 19:41:17 +00:00
|
|
|
if !ty.impls_fnonce(ctx.sema.db) {
|
|
|
|
return None;
|
|
|
|
}
|
|
|
|
let expr_ptr = &d.expr;
|
2023-04-16 17:20:48 +00:00
|
|
|
let root = ctx.sema.db.parse_or_expand(expr_ptr.file_id);
|
2023-03-03 19:41:17 +00:00
|
|
|
let expr = expr_ptr.value.to_node(&root);
|
|
|
|
let (file_id, range) = match expr {
|
|
|
|
ast::Expr::MethodCallExpr(mcall) => {
|
|
|
|
let FileRange { range, file_id } =
|
|
|
|
ctx.sema.original_range_opt(mcall.receiver()?.syntax())?;
|
|
|
|
let FileRange { range: range2, file_id: file_id2 } =
|
|
|
|
ctx.sema.original_range_opt(mcall.name_ref()?.syntax())?;
|
|
|
|
if file_id != file_id2 {
|
|
|
|
return None;
|
|
|
|
}
|
|
|
|
(file_id, TextRange::new(range.start(), range2.end()))
|
|
|
|
}
|
|
|
|
_ => return None,
|
|
|
|
};
|
2023-12-12 12:24:33 +00:00
|
|
|
Some(Assist {
|
2023-03-03 19:41:17 +00:00
|
|
|
id: AssistId("expected-method-found-field-fix", AssistKind::QuickFix),
|
2024-02-09 15:35:55 +00:00
|
|
|
label: Label::new("Use parentheses to call the value of the field".to_owned()),
|
2023-03-03 19:41:17 +00:00
|
|
|
group: None,
|
|
|
|
target: range,
|
|
|
|
source_change: Some(SourceChange::from_iter([
|
|
|
|
(file_id, TextEdit::insert(range.start(), "(".to_owned())),
|
|
|
|
(file_id, TextEdit::insert(range.end(), ")".to_owned())),
|
|
|
|
])),
|
|
|
|
trigger_signature_help: false,
|
2023-12-12 12:24:33 +00:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
fn assoc_func_fix(ctx: &DiagnosticsContext<'_>, d: &hir::UnresolvedMethodCall) -> Option<Assist> {
|
|
|
|
if let Some(assoc_item_id) = d.assoc_func_with_same_name {
|
|
|
|
let db = ctx.sema.db;
|
|
|
|
|
|
|
|
let expr_ptr = &d.expr;
|
|
|
|
let root = db.parse_or_expand(expr_ptr.file_id);
|
|
|
|
let expr: ast::Expr = expr_ptr.value.to_node(&root);
|
|
|
|
|
|
|
|
let call = ast::MethodCallExpr::cast(expr.syntax().clone())?;
|
2024-01-02 13:30:13 +00:00
|
|
|
let range = InFile::new(expr_ptr.file_id, call.syntax().text_range())
|
|
|
|
.original_node_file_range_rooted(db)
|
|
|
|
.range;
|
2023-12-12 12:24:33 +00:00
|
|
|
|
|
|
|
let receiver = call.receiver()?;
|
|
|
|
let receiver_type = &ctx.sema.type_of_expr(&receiver)?.original;
|
|
|
|
|
|
|
|
let need_to_take_receiver_as_first_arg = match hir::AssocItem::from(assoc_item_id) {
|
|
|
|
AssocItem::Function(f) => {
|
|
|
|
let assoc_fn_params = f.assoc_fn_params(db);
|
|
|
|
if assoc_fn_params.is_empty() {
|
|
|
|
false
|
|
|
|
} else {
|
|
|
|
assoc_fn_params
|
|
|
|
.first()
|
|
|
|
.map(|first_arg| {
|
|
|
|
// For generic type, say `Box`, take `Box::into_raw(b: Self)` as example,
|
|
|
|
// type of `b` is `Self`, which is `Box<T, A>`, containing unspecified generics.
|
|
|
|
// However, type of `receiver` is specified, it could be `Box<i32, Global>` or something like that,
|
|
|
|
// so `first_arg.ty() == receiver_type` evaluate to `false` here.
|
|
|
|
// Here add `first_arg.ty().as_adt() == receiver_type.as_adt()` as guard,
|
|
|
|
// apply `.as_adt()` over `Box<T, A>` or `Box<i32, Global>` gets `Box`, so we get `true` here.
|
|
|
|
|
|
|
|
// FIXME: it fails when type of `b` is `Box` with other generic param different from `receiver`
|
|
|
|
first_arg.ty() == receiver_type
|
|
|
|
|| first_arg.ty().as_adt() == receiver_type.as_adt()
|
|
|
|
})
|
|
|
|
.unwrap_or(false)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
_ => false,
|
|
|
|
};
|
|
|
|
|
|
|
|
let mut receiver_type_adt_name = receiver_type.as_adt()?.name(db).to_smol_str().to_string();
|
|
|
|
|
|
|
|
let generic_parameters: Vec<SmolStr> = receiver_type.generic_parameters(db).collect();
|
|
|
|
// if receiver should be pass as first arg in the assoc func,
|
|
|
|
// we could omit generic parameters cause compiler can deduce it automatically
|
|
|
|
if !need_to_take_receiver_as_first_arg && !generic_parameters.is_empty() {
|
2024-01-06 23:17:48 +00:00
|
|
|
let generic_parameters = generic_parameters.join(", ");
|
2023-12-12 12:24:33 +00:00
|
|
|
receiver_type_adt_name =
|
|
|
|
format!("{}::<{}>", receiver_type_adt_name, generic_parameters);
|
|
|
|
}
|
|
|
|
|
|
|
|
let method_name = call.name_ref()?;
|
|
|
|
let assoc_func_call = format!("{}::{}()", receiver_type_adt_name, method_name);
|
|
|
|
|
|
|
|
let assoc_func_call = make::expr_path(make::path_from_text(&assoc_func_call));
|
|
|
|
|
|
|
|
let args: Vec<_> = if need_to_take_receiver_as_first_arg {
|
|
|
|
std::iter::once(receiver).chain(call.arg_list()?.args()).collect()
|
|
|
|
} else {
|
|
|
|
call.arg_list()?.args().collect()
|
|
|
|
};
|
|
|
|
let args = make::arg_list(args);
|
|
|
|
|
|
|
|
let assoc_func_call_expr_string = make::expr_call(assoc_func_call, args).to_string();
|
|
|
|
|
|
|
|
let file_id = ctx.sema.original_range_opt(call.receiver()?.syntax())?.file_id;
|
|
|
|
|
|
|
|
Some(Assist {
|
|
|
|
id: AssistId("method_call_to_assoc_func_call_fix", AssistKind::QuickFix),
|
|
|
|
label: Label::new(format!(
|
|
|
|
"Use associated func call instead: `{}`",
|
|
|
|
assoc_func_call_expr_string
|
|
|
|
)),
|
|
|
|
group: None,
|
|
|
|
target: range,
|
|
|
|
source_change: Some(SourceChange::from_text_edit(
|
|
|
|
file_id,
|
|
|
|
TextEdit::replace(range, assoc_func_call_expr_string),
|
|
|
|
)),
|
|
|
|
trigger_signature_help: false,
|
|
|
|
})
|
|
|
|
} else {
|
|
|
|
None
|
|
|
|
}
|
2023-03-03 19:41:17 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
mod tests {
|
|
|
|
use crate::tests::{check_diagnostics, check_fix};
|
|
|
|
|
2023-12-12 12:32:13 +00:00
|
|
|
#[test]
|
|
|
|
fn test_assoc_func_fix() {
|
|
|
|
check_fix(
|
|
|
|
r#"
|
|
|
|
struct A {}
|
|
|
|
|
|
|
|
impl A {
|
|
|
|
fn hello() {}
|
|
|
|
}
|
|
|
|
fn main() {
|
|
|
|
let a = A{};
|
|
|
|
a.hello$0();
|
|
|
|
}
|
|
|
|
"#,
|
|
|
|
r#"
|
|
|
|
struct A {}
|
|
|
|
|
|
|
|
impl A {
|
|
|
|
fn hello() {}
|
|
|
|
}
|
|
|
|
fn main() {
|
|
|
|
let a = A{};
|
|
|
|
A::hello();
|
|
|
|
}
|
|
|
|
"#,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_assoc_func_diagnostic() {
|
|
|
|
check_diagnostics(
|
|
|
|
r#"
|
|
|
|
struct A {}
|
|
|
|
impl A {
|
|
|
|
fn hello() {}
|
|
|
|
}
|
|
|
|
fn main() {
|
|
|
|
let a = A{};
|
|
|
|
a.hello();
|
2024-01-02 13:30:13 +00:00
|
|
|
// ^^^^^ 💡 error: no method `hello` on type `A`, but an associated function with a similar name exists
|
2023-12-12 12:32:13 +00:00
|
|
|
}
|
|
|
|
"#,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_assoc_func_fix_with_generic() {
|
|
|
|
check_fix(
|
|
|
|
r#"
|
|
|
|
struct A<T, U> {
|
|
|
|
a: T,
|
|
|
|
b: U
|
|
|
|
}
|
|
|
|
|
|
|
|
impl<T, U> A<T, U> {
|
|
|
|
fn foo() {}
|
|
|
|
}
|
|
|
|
fn main() {
|
|
|
|
let a = A {a: 0, b: ""};
|
|
|
|
a.foo()$0;
|
|
|
|
}
|
|
|
|
"#,
|
|
|
|
r#"
|
|
|
|
struct A<T, U> {
|
|
|
|
a: T,
|
|
|
|
b: U
|
|
|
|
}
|
|
|
|
|
|
|
|
impl<T, U> A<T, U> {
|
|
|
|
fn foo() {}
|
|
|
|
}
|
|
|
|
fn main() {
|
|
|
|
let a = A {a: 0, b: ""};
|
|
|
|
A::<i32, &str>::foo();
|
|
|
|
}
|
|
|
|
"#,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2023-03-03 19:41:17 +00:00
|
|
|
#[test]
|
|
|
|
fn smoke_test() {
|
|
|
|
check_diagnostics(
|
|
|
|
r#"
|
|
|
|
fn main() {
|
|
|
|
().foo();
|
2023-12-08 17:46:36 +00:00
|
|
|
// ^^^ error: no method `foo` on type `()`
|
|
|
|
}
|
|
|
|
"#,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn smoke_test_in_macro_def_site() {
|
|
|
|
check_diagnostics(
|
|
|
|
r#"
|
|
|
|
macro_rules! m {
|
|
|
|
($rcv:expr) => {
|
|
|
|
$rcv.foo()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
fn main() {
|
|
|
|
m!(());
|
|
|
|
// ^^^^^^ error: no method `foo` on type `()`
|
|
|
|
}
|
|
|
|
"#,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn smoke_test_in_macro_call_site() {
|
|
|
|
check_diagnostics(
|
|
|
|
r#"
|
|
|
|
macro_rules! m {
|
|
|
|
($ident:ident) => {
|
|
|
|
().$ident()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
fn main() {
|
|
|
|
m!(foo);
|
|
|
|
// ^^^ error: no method `foo` on type `()`
|
2023-03-03 19:41:17 +00:00
|
|
|
}
|
|
|
|
"#,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn field() {
|
|
|
|
check_diagnostics(
|
|
|
|
r#"
|
|
|
|
struct Foo { bar: i32 }
|
|
|
|
fn foo() {
|
2024-02-11 15:34:52 +00:00
|
|
|
Foo { bar: 0 }.bar();
|
|
|
|
// ^^^ error: no method `bar` on type `Foo`, but a field with a similar name exists
|
2023-03-03 19:41:17 +00:00
|
|
|
}
|
|
|
|
"#,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn callable_field() {
|
|
|
|
check_fix(
|
|
|
|
r#"
|
|
|
|
//- minicore: fn
|
|
|
|
struct Foo { bar: fn() }
|
|
|
|
fn foo() {
|
|
|
|
Foo { bar: foo }.b$0ar();
|
|
|
|
}
|
|
|
|
"#,
|
|
|
|
r#"
|
|
|
|
struct Foo { bar: fn() }
|
|
|
|
fn foo() {
|
|
|
|
(Foo { bar: foo }.bar)();
|
|
|
|
}
|
|
|
|
"#,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|