Diagnose unresolved method calls

This commit is contained in:
Lukas Wirth 2023-03-03 20:41:17 +01:00
parent 78b2dd813a
commit e7485a0416
8 changed files with 320 additions and 48 deletions

View file

@ -164,14 +164,45 @@ pub(crate) type InferResult<T> = Result<InferOk<T>, TypeError>;
#[derive(Debug, PartialEq, Eq, Clone)] #[derive(Debug, PartialEq, Eq, Clone)]
pub enum InferenceDiagnostic { pub enum InferenceDiagnostic {
NoSuchField { expr: ExprId }, NoSuchField {
PrivateField { expr: ExprId, field: FieldId }, expr: ExprId,
PrivateAssocItem { id: ExprOrPatId, item: AssocItemId }, },
UnresolvedField { expr: ExprId, receiver: Ty, name: Name, method_with_same_name_exists: bool }, PrivateField {
expr: ExprId,
field: FieldId,
},
PrivateAssocItem {
id: ExprOrPatId,
item: AssocItemId,
},
UnresolvedField {
expr: ExprId,
receiver: Ty,
name: Name,
method_with_same_name_exists: bool,
},
UnresolvedMethodCall {
expr: ExprId,
receiver: Ty,
name: Name,
/// Contains the type the field resolves to
field_with_same_name: Option<Ty>,
},
// FIXME: Make this proper // FIXME: Make this proper
BreakOutsideOfLoop { expr: ExprId, is_break: bool, bad_value_break: bool }, BreakOutsideOfLoop {
MismatchedArgCount { call_expr: ExprId, expected: usize, found: usize }, expr: ExprId,
ExpectedFunction { call_expr: ExprId, found: Ty }, is_break: bool,
bad_value_break: bool,
},
MismatchedArgCount {
call_expr: ExprId,
expected: usize,
found: usize,
},
ExpectedFunction {
call_expr: ExprId,
found: Ty,
},
} }
/// A mismatch between an expected and an inferred type. /// A mismatch between an expected and an inferred type.
@ -509,12 +540,28 @@ impl<'a> InferenceContext<'a> {
} }
result.diagnostics.retain_mut(|diagnostic| { result.diagnostics.retain_mut(|diagnostic| {
if let InferenceDiagnostic::ExpectedFunction { found: ty, .. } if let InferenceDiagnostic::ExpectedFunction { found: ty, .. }
| InferenceDiagnostic::UnresolvedField { receiver: ty, .. } = diagnostic | InferenceDiagnostic::UnresolvedField { receiver: ty, .. }
| InferenceDiagnostic::UnresolvedMethodCall { receiver: ty, .. } = diagnostic
{ {
*ty = table.resolve_completely(ty.clone()); *ty = table.resolve_completely(ty.clone());
// FIXME: Remove this when we are on par with rustc in terms of inference
if ty.is_unknown() { if ty.is_unknown() {
return false; return false;
} }
if let InferenceDiagnostic::UnresolvedMethodCall { field_with_same_name, .. } =
diagnostic
{
let clear = if let Some(ty) = field_with_same_name {
*ty = table.resolve_completely(ty.clone());
ty.is_unknown()
} else {
false
};
if clear {
*field_with_same_name = None;
}
}
} }
true true
}); });

View file

@ -1212,12 +1212,14 @@ impl<'a> InferenceContext<'a> {
} }
} }
fn infer_field_access(&mut self, tgt_expr: ExprId, expr: ExprId, name: &Name) -> Ty { fn lookup_field(
let receiver_ty = self.infer_expr_inner(expr, &Expectation::none()); &mut self,
receiver_ty: &Ty,
name: &Name,
) -> Option<(Ty, Option<FieldId>, Vec<Adjustment>, bool)> {
let mut autoderef = Autoderef::new(&mut self.table, receiver_ty.clone()); let mut autoderef = Autoderef::new(&mut self.table, receiver_ty.clone());
let mut private_field = None; let mut private_field = None;
let ty = autoderef.by_ref().find_map(|(derefed_ty, _)| { let res = autoderef.by_ref().find_map(|(derefed_ty, _)| {
let (field_id, parameters) = match derefed_ty.kind(Interner) { let (field_id, parameters) = match derefed_ty.kind(Interner) {
TyKind::Tuple(_, substs) => { TyKind::Tuple(_, substs) => {
return name.as_tuple_index().and_then(|idx| { return name.as_tuple_index().and_then(|idx| {
@ -1226,6 +1228,7 @@ impl<'a> InferenceContext<'a> {
.get(idx) .get(idx)
.map(|a| a.assert_ty_ref(Interner)) .map(|a| a.assert_ty_ref(Interner))
.cloned() .cloned()
.map(|ty| (None, ty))
}); });
} }
TyKind::Adt(AdtId(hir_def::AdtId::StructId(s)), parameters) => { TyKind::Adt(AdtId(hir_def::AdtId::StructId(s)), parameters) => {
@ -1244,58 +1247,81 @@ impl<'a> InferenceContext<'a> {
.is_visible_from(self.db.upcast(), self.resolver.module()); .is_visible_from(self.db.upcast(), self.resolver.module());
if !is_visible { if !is_visible {
if private_field.is_none() { if private_field.is_none() {
private_field = Some(field_id); private_field = Some((field_id, parameters));
} }
return None; return None;
} }
// can't have `write_field_resolution` here because `self.table` is borrowed :(
self.result.field_resolutions.insert(tgt_expr, field_id);
let ty = self.db.field_types(field_id.parent)[field_id.local_id] let ty = self.db.field_types(field_id.parent)[field_id.local_id]
.clone() .clone()
.substitute(Interner, &parameters); .substitute(Interner, &parameters);
Some(ty) Some((Some(field_id), ty))
}); });
let ty = match ty {
Some(ty) => { Some(match res {
Some((field_id, ty)) => {
let adjustments = auto_deref_adjust_steps(&autoderef); let adjustments = auto_deref_adjust_steps(&autoderef);
self.write_expr_adj(expr, adjustments);
let ty = self.insert_type_vars(ty); let ty = self.insert_type_vars(ty);
let ty = self.normalize_associated_types_in(ty); let ty = self.normalize_associated_types_in(ty);
ty
(ty, field_id, adjustments, true)
} }
_ => { None => {
// Write down the first private field resolution if we found no field let (field_id, subst) = private_field?;
// This aids IDE features for private fields like goto def let adjustments = auto_deref_adjust_steps(&autoderef);
if let Some(field) = private_field { let ty = self.db.field_types(field_id.parent)[field_id.local_id]
self.result.field_resolutions.insert(tgt_expr, field); .clone()
// FIXME: Merge this diagnostic into UnresolvedField .substitute(Interner, &subst);
let ty = self.insert_type_vars(ty);
let ty = self.normalize_associated_types_in(ty);
(ty, Some(field_id), adjustments, false)
}
})
}
fn infer_field_access(&mut self, tgt_expr: ExprId, receiver: ExprId, name: &Name) -> Ty {
let receiver_ty = self.infer_expr_inner(receiver, &Expectation::none());
match self.lookup_field(&receiver_ty, name) {
Some((ty, field_id, adjustments, is_public)) => {
self.write_expr_adj(receiver, adjustments);
if let Some(field_id) = field_id {
self.result.field_resolutions.insert(tgt_expr, field_id);
}
if !is_public {
if let Some(field) = field_id {
// FIXME: Merge this diagnostic into UnresolvedField?
self.result self.result
.diagnostics .diagnostics
.push(InferenceDiagnostic::PrivateField { expr: tgt_expr, field }); .push(InferenceDiagnostic::PrivateField { expr: tgt_expr, field });
} else { }
// no field found, try looking for a method of the same name }
ty
}
None => {
// no field found,
let method_with_same_name_exists = {
let canonicalized_receiver = self.canonicalize(receiver_ty.clone()); let canonicalized_receiver = self.canonicalize(receiver_ty.clone());
let traits_in_scope = self.resolver.traits_in_scope(self.db.upcast()); let traits_in_scope = self.resolver.traits_in_scope(self.db.upcast());
let resolved = method_resolution::lookup_method( method_resolution::lookup_method(
self.db, self.db,
&canonicalized_receiver.value, &canonicalized_receiver.value,
self.trait_env.clone(), self.trait_env.clone(),
&traits_in_scope, &traits_in_scope,
VisibleFromModule::Filter(self.resolver.module()), VisibleFromModule::Filter(self.resolver.module()),
name, name,
); )
.is_some()
};
self.result.diagnostics.push(InferenceDiagnostic::UnresolvedField { self.result.diagnostics.push(InferenceDiagnostic::UnresolvedField {
expr: tgt_expr, expr: tgt_expr,
receiver: receiver_ty, receiver: receiver_ty,
name: name.clone(), name: name.clone(),
method_with_same_name_exists: resolved.is_some(), method_with_same_name_exists,
}); });
}
self.err_ty() self.err_ty()
} }
}; }
ty
} }
fn infer_method_call( fn infer_method_call(
@ -1335,11 +1361,30 @@ impl<'a> InferenceContext<'a> {
} }
(ty, self.db.value_ty(func.into()), substs) (ty, self.db.value_ty(func.into()), substs)
} }
None => ( None => {
let field_with_same_name_exists = match self.lookup_field(&receiver_ty, method_name)
{
Some((ty, field_id, adjustments, _public)) => {
self.write_expr_adj(receiver, adjustments);
if let Some(field_id) = field_id {
self.result.field_resolutions.insert(tgt_expr, field_id);
}
Some(ty)
}
None => None,
};
self.result.diagnostics.push(InferenceDiagnostic::UnresolvedMethodCall {
expr: tgt_expr,
receiver: receiver_ty.clone(),
name: method_name.clone(),
field_with_same_name: field_with_same_name_exists,
});
(
receiver_ty, receiver_ty,
Binders::empty(Interner, self.err_ty()), Binders::empty(Interner, self.err_ty()),
Substitution::empty(Interner), Substitution::empty(Interner),
), )
}
}; };
let method_ty = method_ty.substitute(Interner, &substs); let method_ty = method_ty.substitute(Interner, &substs);
self.register_obligations_for_call(&method_ty); self.register_obligations_for_call(&method_ty);

View file

@ -51,6 +51,7 @@ diagnostics![
UnresolvedField, UnresolvedField,
UnresolvedImport, UnresolvedImport,
UnresolvedMacroCall, UnresolvedMacroCall,
UnresolvedMethodCall,
UnresolvedModule, UnresolvedModule,
UnresolvedProcMacro, UnresolvedProcMacro,
]; ];
@ -146,6 +147,14 @@ pub struct UnresolvedField {
pub method_with_same_name_exists: bool, pub method_with_same_name_exists: bool,
} }
#[derive(Debug)]
pub struct UnresolvedMethodCall {
pub expr: InFile<AstPtr<ast::Expr>>,
pub receiver: Type,
pub name: Name,
pub field_with_same_name: Option<Type>,
}
#[derive(Debug)] #[derive(Debug)]
pub struct PrivateField { pub struct PrivateField {
pub expr: InFile<AstPtr<ast::Expr>>, pub expr: InFile<AstPtr<ast::Expr>>,

View file

@ -89,7 +89,7 @@ pub use crate::{
MissingMatchArms, MissingUnsafe, NoSuchField, PrivateAssocItem, PrivateField, MissingMatchArms, MissingUnsafe, NoSuchField, PrivateAssocItem, PrivateField,
ReplaceFilterMapNextWithFindMap, TypeMismatch, UnimplementedBuiltinMacro, ReplaceFilterMapNextWithFindMap, TypeMismatch, UnimplementedBuiltinMacro,
UnresolvedExternCrate, UnresolvedField, UnresolvedImport, UnresolvedMacroCall, UnresolvedExternCrate, UnresolvedField, UnresolvedImport, UnresolvedMacroCall,
UnresolvedModule, UnresolvedProcMacro, UnresolvedMethodCall, UnresolvedModule, UnresolvedProcMacro,
}, },
has_source::HasSource, has_source::HasSource,
semantics::{PathResolution, Semantics, SemanticsScope, TypeInfo, VisibleTraits}, semantics::{PathResolution, Semantics, SemanticsScope, TypeInfo, VisibleTraits},
@ -1441,6 +1441,26 @@ impl DefWithBody {
.into(), .into(),
) )
} }
hir_ty::InferenceDiagnostic::UnresolvedMethodCall {
expr,
receiver,
name,
field_with_same_name,
} => {
let expr = expr_syntax(*expr);
acc.push(
UnresolvedMethodCall {
expr,
name: name.clone(),
receiver: Type::new(db, DefWithBodyId::from(self), receiver.clone()),
field_with_same_name: field_with_same_name
.clone()
.map(|ty| Type::new(db, DefWithBodyId::from(self), ty)),
}
.into(),
)
}
} }
} }
for (pat_or_expr, mismatch) in infer.type_mismatches() { for (pat_or_expr, mismatch) in infer.type_mismatches() {

View file

@ -83,6 +83,14 @@ impl From<NoHashHashMap<FileId, TextEdit>> for SourceChange {
} }
} }
impl FromIterator<(FileId, TextEdit)> for SourceChange {
fn from_iter<T: IntoIterator<Item = (FileId, TextEdit)>>(iter: T) -> Self {
let mut this = SourceChange::default();
this.extend(iter);
this
}
}
pub struct SourceChangeBuilder { pub struct SourceChangeBuilder {
pub edit: TextEditBuilder, pub edit: TextEditBuilder,
pub file_id: FileId, pub file_id: FileId,

View file

@ -55,7 +55,18 @@ fn fixes(
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::tests::{check_diagnostics, check_fix}; use crate::{
tests::{check_diagnostics_with_config, check_fix},
DiagnosticsConfig,
};
#[track_caller]
pub(crate) fn check_diagnostics(ra_fixture: &str) {
let mut config = DiagnosticsConfig::test_sample();
config.disabled.insert("inactive-code".to_string());
config.disabled.insert("unresolved-method".to_string());
check_diagnostics_with_config(config, ra_fixture)
}
#[test] #[test]
fn replace_filter_map_next_with_find_map2() { fn replace_filter_map_next_with_find_map2() {

View file

@ -0,0 +1,130 @@
use hir::{db::AstDatabase, HirDisplay};
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::{Diagnostic, DiagnosticsContext};
// 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(
"unresolved-method",
format!(
"no method `{}` on type `{}`{field_suffix}",
d.name,
d.receiver.display(ctx.sema.db)
),
ctx.sema.diagnostics_display_range(d.expr.clone().map(|it| it.into())).range,
)
.with_fixes(fixes(ctx, d))
}
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;
let root = ctx.sema.db.parse_or_expand(expr_ptr.file_id)?;
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 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
}
"#,
);
}
#[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)();
}
"#,
);
}
}

View file

@ -45,6 +45,7 @@ mod handlers {
pub(crate) mod unimplemented_builtin_macro; pub(crate) mod unimplemented_builtin_macro;
pub(crate) mod unresolved_extern_crate; pub(crate) mod unresolved_extern_crate;
pub(crate) mod unresolved_field; pub(crate) mod unresolved_field;
pub(crate) mod unresolved_method;
pub(crate) mod unresolved_import; pub(crate) mod unresolved_import;
pub(crate) mod unresolved_macro_call; pub(crate) mod unresolved_macro_call;
pub(crate) mod unresolved_module; pub(crate) mod unresolved_module;
@ -271,6 +272,7 @@ pub fn diagnostics(
AnyDiagnostic::UnresolvedProcMacro(d) => handlers::unresolved_proc_macro::unresolved_proc_macro(&ctx, &d, config.proc_macros_enabled, config.proc_attr_macros_enabled), AnyDiagnostic::UnresolvedProcMacro(d) => handlers::unresolved_proc_macro::unresolved_proc_macro(&ctx, &d, config.proc_macros_enabled, config.proc_attr_macros_enabled),
AnyDiagnostic::InvalidDeriveTarget(d) => handlers::invalid_derive_target::invalid_derive_target(&ctx, &d), AnyDiagnostic::InvalidDeriveTarget(d) => handlers::invalid_derive_target::invalid_derive_target(&ctx, &d),
AnyDiagnostic::UnresolvedField(d) => handlers::unresolved_field::unresolved_field(&ctx, &d), AnyDiagnostic::UnresolvedField(d) => handlers::unresolved_field::unresolved_field(&ctx, &d),
AnyDiagnostic::UnresolvedMethodCall(d) => handlers::unresolved_method::unresolved_method(&ctx, &d),
AnyDiagnostic::InactiveCode(d) => match handlers::inactive_code::inactive_code(&ctx, &d) { AnyDiagnostic::InactiveCode(d) => match handlers::inactive_code::inactive_code(&ctx, &d) {
Some(it) => it, Some(it) => it,