Skip to content

feat: Diagnose unresolved field, method call and call expression #14243

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Mar 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 66 additions & 6 deletions crates/hir-ty/src/infer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ use hir_def::{
AdtId, AssocItemId, DefWithBodyId, EnumVariantId, FieldId, FunctionId, HasModule,
ItemContainerId, Lookup, TraitId, TypeAliasId, VariantId,
};
use hir_expand::name::name;
use hir_expand::name::{name, Name};
use la_arena::ArenaMap;
use rustc_hash::FxHashMap;
use stdx::always;
Expand Down Expand Up @@ -164,12 +164,45 @@ pub(crate) type InferResult<T> = Result<InferOk<T>, TypeError>;

#[derive(Debug, PartialEq, Eq, Clone)]
pub enum InferenceDiagnostic {
NoSuchField { expr: ExprId },
PrivateField { expr: ExprId, field: FieldId },
PrivateAssocItem { id: ExprOrPatId, item: AssocItemId },
NoSuchField {
expr: ExprId,
},
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
BreakOutsideOfLoop { expr: ExprId, is_break: bool, bad_value_break: bool },
MismatchedArgCount { call_expr: ExprId, expected: usize, found: usize },
BreakOutsideOfLoop {
expr: ExprId,
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.
Expand Down Expand Up @@ -505,6 +538,33 @@ impl<'a> InferenceContext<'a> {
mismatch.expected = table.resolve_completely(mismatch.expected.clone());
mismatch.actual = table.resolve_completely(mismatch.actual.clone());
}
result.diagnostics.retain_mut(|diagnostic| {
if let InferenceDiagnostic::ExpectedFunction { found: ty, .. }
| InferenceDiagnostic::UnresolvedField { receiver: ty, .. }
| InferenceDiagnostic::UnresolvedMethodCall { receiver: ty, .. } = diagnostic
{
*ty = table.resolve_completely(ty.clone());
// FIXME: Remove this when we are on par with rustc in terms of inference
if ty.is_unknown() {
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
});
for (_, subst) in result.method_resolutions.values_mut() {
*subst = table.resolve_completely(subst.clone());
}
Expand Down
215 changes: 144 additions & 71 deletions crates/hir-ty/src/infer/expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -364,7 +364,13 @@ impl<'a> InferenceContext<'a> {
}
(params, ret_ty)
}
None => (Vec::new(), self.err_ty()), // FIXME diagnostic
None => {
self.result.diagnostics.push(InferenceDiagnostic::ExpectedFunction {
call_expr: tgt_expr,
found: callee_ty.clone(),
});
(Vec::new(), self.err_ty())
}
};
let indices_to_skip = self.check_legacy_const_generics(derefed_callee, args);
self.register_obligations_for_call(&callee_ty);
Expand Down Expand Up @@ -546,71 +552,7 @@ impl<'a> InferenceContext<'a> {
}
ty
}
Expr::Field { expr, name } => {
let receiver_ty = self.infer_expr_inner(*expr, &Expectation::none());

let mut autoderef = Autoderef::new(&mut self.table, receiver_ty);
let mut private_field = None;
let ty = autoderef.by_ref().find_map(|(derefed_ty, _)| {
let (field_id, parameters) = match derefed_ty.kind(Interner) {
TyKind::Tuple(_, substs) => {
return name.as_tuple_index().and_then(|idx| {
substs
.as_slice(Interner)
.get(idx)
.map(|a| a.assert_ty_ref(Interner))
.cloned()
});
}
TyKind::Adt(AdtId(hir_def::AdtId::StructId(s)), parameters) => {
let local_id = self.db.struct_data(*s).variant_data.field(name)?;
let field = FieldId { parent: (*s).into(), local_id };
(field, parameters.clone())
}
TyKind::Adt(AdtId(hir_def::AdtId::UnionId(u)), parameters) => {
let local_id = self.db.union_data(*u).variant_data.field(name)?;
let field = FieldId { parent: (*u).into(), local_id };
(field, parameters.clone())
}
_ => return None,
};
let is_visible = self.db.field_visibilities(field_id.parent)[field_id.local_id]
.is_visible_from(self.db.upcast(), self.resolver.module());
if !is_visible {
if private_field.is_none() {
private_field = Some(field_id);
}
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]
.clone()
.substitute(Interner, &parameters);
Some(ty)
});
let ty = match ty {
Some(ty) => {
let adjustments = auto_deref_adjust_steps(&autoderef);
self.write_expr_adj(*expr, adjustments);
let ty = self.insert_type_vars(ty);
let ty = self.normalize_associated_types_in(ty);
ty
}
_ => {
// Write down the first private field resolution if we found no field
// This aids IDE features for private fields like goto def
if let Some(field) = private_field {
self.result.field_resolutions.insert(tgt_expr, field);
self.result
.diagnostics
.push(InferenceDiagnostic::PrivateField { expr: tgt_expr, field });
}
self.err_ty()
}
};
ty
}
Expr::Field { expr, name } => self.infer_field_access(tgt_expr, *expr, name),
Expr::Await { expr } => {
let inner_ty = self.infer_expr_inner(*expr, &Expectation::none());
self.resolve_associated_type(inner_ty, self.resolve_future_future_output())
Expand Down Expand Up @@ -1270,6 +1212,118 @@ impl<'a> InferenceContext<'a> {
}
}

fn lookup_field(
&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 private_field = None;
let res = autoderef.by_ref().find_map(|(derefed_ty, _)| {
let (field_id, parameters) = match derefed_ty.kind(Interner) {
TyKind::Tuple(_, substs) => {
return name.as_tuple_index().and_then(|idx| {
substs
.as_slice(Interner)
.get(idx)
.map(|a| a.assert_ty_ref(Interner))
.cloned()
.map(|ty| (None, ty))
});
}
TyKind::Adt(AdtId(hir_def::AdtId::StructId(s)), parameters) => {
let local_id = self.db.struct_data(*s).variant_data.field(name)?;
let field = FieldId { parent: (*s).into(), local_id };
(field, parameters.clone())
}
TyKind::Adt(AdtId(hir_def::AdtId::UnionId(u)), parameters) => {
let local_id = self.db.union_data(*u).variant_data.field(name)?;
let field = FieldId { parent: (*u).into(), local_id };
(field, parameters.clone())
}
_ => return None,
};
let is_visible = self.db.field_visibilities(field_id.parent)[field_id.local_id]
.is_visible_from(self.db.upcast(), self.resolver.module());
if !is_visible {
if private_field.is_none() {
private_field = Some((field_id, parameters));
}
return None;
}
let ty = self.db.field_types(field_id.parent)[field_id.local_id]
.clone()
.substitute(Interner, &parameters);
Some((Some(field_id), ty))
});

Some(match res {
Some((field_id, ty)) => {
let adjustments = auto_deref_adjust_steps(&autoderef);
let ty = self.insert_type_vars(ty);
let ty = self.normalize_associated_types_in(ty);

(ty, field_id, adjustments, true)
}
None => {
let (field_id, subst) = private_field?;
let adjustments = auto_deref_adjust_steps(&autoderef);
let ty = self.db.field_types(field_id.parent)[field_id.local_id]
.clone()
.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
.diagnostics
.push(InferenceDiagnostic::PrivateField { expr: tgt_expr, field });
}
}
ty
}
None => {
// no field found,
let method_with_same_name_exists = {
let canonicalized_receiver = self.canonicalize(receiver_ty.clone());
let traits_in_scope = self.resolver.traits_in_scope(self.db.upcast());

method_resolution::lookup_method(
self.db,
&canonicalized_receiver.value,
self.trait_env.clone(),
&traits_in_scope,
VisibleFromModule::Filter(self.resolver.module()),
name,
)
.is_some()
};
self.result.diagnostics.push(InferenceDiagnostic::UnresolvedField {
expr: tgt_expr,
receiver: receiver_ty,
name: name.clone(),
method_with_same_name_exists,
});
self.err_ty()
}
}
}

fn infer_method_call(
&mut self,
tgt_expr: ExprId,
Expand Down Expand Up @@ -1307,11 +1361,30 @@ impl<'a> InferenceContext<'a> {
}
(ty, self.db.value_ty(func.into()), substs)
}
None => (
receiver_ty,
Binders::empty(Interner, self.err_ty()),
Substitution::empty(Interner),
),
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,
Binders::empty(Interner, self.err_ty()),
Substitution::empty(Interner),
)
}
};
let method_ty = method_ty.substitute(Interner, &substs);
self.register_obligations_for_call(&method_ty);
Expand Down
25 changes: 25 additions & 0 deletions crates/hir/src/diagnostics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ macro_rules! diagnostics {

diagnostics![
BreakOutsideOfLoop,
ExpectedFunction,
InactiveCode,
IncorrectCase,
InvalidDeriveTarget,
Expand All @@ -47,8 +48,10 @@ diagnostics![
TypeMismatch,
UnimplementedBuiltinMacro,
UnresolvedExternCrate,
UnresolvedField,
UnresolvedImport,
UnresolvedMacroCall,
UnresolvedMethodCall,
UnresolvedModule,
UnresolvedProcMacro,
];
Expand Down Expand Up @@ -130,6 +133,28 @@ pub struct PrivateAssocItem {
pub item: AssocItem,
}

#[derive(Debug)]
pub struct ExpectedFunction {
pub call: InFile<AstPtr<ast::Expr>>,
pub found: Type,
}

#[derive(Debug)]
pub struct UnresolvedField {
pub expr: InFile<AstPtr<ast::Expr>>,
pub receiver: Type,
pub name: Name,
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)]
pub struct PrivateField {
pub expr: InFile<AstPtr<ast::Expr>>,
Expand Down
Loading