rust-analyzer/crates/ide-diagnostics/src/handlers/unresolved_method.rs

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

176 lines
4.2 KiB
Rust
Raw Normal View History

use hir::{db::ExpandDatabase, HirDisplay};
2023-03-03 19:41:17 +00:00
use ide_db::{
assists::{Assist, AssistId, AssistKind},
base_db::FileRange,
label::Label,
source_change::SourceChange,
};
use syntax::{ast, AstNode, TextRange};
use text_edit::TextEdit;
use crate::{adjusted_display_range_new, 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 {
let field_suffix = if d.field_with_same_name.is_some() {
", but a field with a similar name exists"
} else {
""
};
Diagnostic::new(
DiagnosticCode::RustcHardError("E0599"),
2023-03-03 19:41:17 +00:00
format!(
"no method `{}` on type `{}`{field_suffix}",
d.name.display(ctx.sema.db),
2023-03-03 19:41:17 +00:00
d.receiver.display(ctx.sema.db)
),
adjusted_display_range_new(ctx, d.expr, &|expr| {
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))
.experimental()
2023-03-03 19:41:17 +00:00
}
fn fixes(ctx: &DiagnosticsContext<'_>, d: &hir::UnresolvedMethodCall) -> Option<Vec<Assist>> {
if let Some(ty) = &d.field_with_same_name {
field_fix(ctx, d, ty)
} else {
// FIXME: add quickfix
None
}
}
fn field_fix(
ctx: &DiagnosticsContext<'_>,
d: &hir::UnresolvedMethodCall,
ty: &hir::Type,
) -> Option<Vec<Assist>> {
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,
};
Some(vec![Assist {
id: AssistId("expected-method-found-field-fix", AssistKind::QuickFix),
label: Label::new("Use parentheses to call the value of the field".to_string()),
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,
}])
}
#[cfg(test)]
mod tests {
use crate::tests::{check_diagnostics, check_fix};
#[test]
fn smoke_test() {
check_diagnostics(
r#"
fn main() {
().foo();
// ^^^ 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() {
Foo { bar: i32 }.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)();
}
"#,
);
}
}