Skip to content

Commit

Permalink
Suggest field typo through derefs
Browse files Browse the repository at this point in the history
Take into account implicit dereferences when suggesting fields.

```
error[E0609]: no field `longname` on type `Arc<S>`
  --> $DIR/suggest-field-through-deref.rs:10:15
   |
LL |     let _ = x.longname;
   |               ^^^^^^^^ help: a field with a similar name exists: `long_name`
```

CC rust-lang#78374 (comment)
  • Loading branch information
estebank committed Oct 24, 2023
1 parent 642bfb2 commit 933341a
Show file tree
Hide file tree
Showing 29 changed files with 227 additions and 192 deletions.
141 changes: 69 additions & 72 deletions compiler/rustc_hir_typeck/src/expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2417,9 +2417,6 @@ impl<'a, 'tcx> FnCtxt<'a, 'tcx> {
ty::RawPtr(..) => {
self.suggest_first_deref_field(&mut err, expr, base, ident);
}
ty::Adt(def, _) if !def.is_enum() => {
self.suggest_fields_on_recordish(&mut err, expr, def, ident);
}
ty::Param(param_ty) => {
self.point_at_param_definition(&mut err, param_ty);
}
Expand Down Expand Up @@ -2579,34 +2576,6 @@ impl<'a, 'tcx> FnCtxt<'a, 'tcx> {
err.span_label(param_span, format!("type parameter '{param_name}' declared here"));
}

fn suggest_fields_on_recordish(
&self,
err: &mut Diagnostic,
expr: &hir::Expr<'_>,
def: ty::AdtDef<'tcx>,
field: Ident,
) {
let available_field_names = self.available_field_names(def.non_enum_variant(), expr, &[]);
if let Some(suggested_field_name) =
find_best_match_for_name(&available_field_names, field.name, None)
{
err.span_suggestion(
field.span,
"a field with a similar name exists",
suggested_field_name,
Applicability::MaybeIncorrect,
);
} else {
err.span_label(field.span, "unknown field");
if !available_field_names.is_empty() {
err.note(format!(
"available fields are: {}",
self.name_series_display(available_field_names),
));
}
}
}

fn maybe_suggest_array_indexing(
&self,
err: &mut Diagnostic,
Expand Down Expand Up @@ -2655,18 +2624,20 @@ impl<'a, 'tcx> FnCtxt<'a, 'tcx> {

let mut err = type_error_struct!(
self.tcx().sess,
field.span,
span,
expr_t,
E0609,
"no field `{field}` on type `{expr_t}`",
);

// try to add a suggestion in case the field is a nested field of a field of the Adt
let mod_id = self.tcx.parent_module(id).to_def_id();
if let Some((fields, args)) =
self.get_field_candidates_considering_privacy(span, expr_t, mod_id)
for (found_fields, args) in
self.get_field_candidates_considering_privacy(span, expr_t, mod_id, id)
{
let candidate_fields: Vec<_> = fields
let field_names = found_fields.iter().map(|field| field.name).collect::<Vec<_>>();
let candidate_fields: Vec<_> = found_fields
.into_iter()
.filter_map(|candidate_field| {
self.check_for_nested_field_satisfying(
span,
Expand All @@ -2675,6 +2646,7 @@ impl<'a, 'tcx> FnCtxt<'a, 'tcx> {
args,
vec![],
mod_id,
id,
)
})
.map(|mut field_path| {
Expand All @@ -2699,6 +2671,21 @@ impl<'a, 'tcx> FnCtxt<'a, 'tcx> {
candidate_fields.iter().map(|path| format!("{path}.")),
Applicability::MaybeIncorrect,
);
} else {
if let Some(field_name) = find_best_match_for_name(&field_names, field.name, None) {
err.span_suggestion(
field.span,
"a field with a similar name exists",
field_name,
Applicability::MaybeIncorrect,
);
} else if !field_names.is_empty() {
let is = if field_names.len() == 1 { " is" } else { "s are" };
err.note(format!(
"available field{is}: {}",
self.name_series_display(field_names),
));
}
}
}
err
Expand Down Expand Up @@ -2727,33 +2714,39 @@ impl<'a, 'tcx> FnCtxt<'a, 'tcx> {
span: Span,
base_ty: Ty<'tcx>,
mod_id: DefId,
) -> Option<(impl Iterator<Item = &'tcx ty::FieldDef> + 'tcx, GenericArgsRef<'tcx>)> {
hir_id: hir::HirId,
) -> Vec<(Vec<&'tcx ty::FieldDef>, GenericArgsRef<'tcx>)> {
debug!("get_field_candidates(span: {:?}, base_t: {:?}", span, base_ty);

for (base_t, _) in self.autoderef(span, base_ty) {
match base_t.kind() {
ty::Adt(base_def, args) if !base_def.is_enum() => {
let tcx = self.tcx;
let fields = &base_def.non_enum_variant().fields;
// Some struct, e.g. some that impl `Deref`, have all private fields
// because you're expected to deref them to access the _real_ fields.
// This, for example, will help us suggest accessing a field through a `Box<T>`.
if fields.iter().all(|field| !field.vis.is_accessible_from(mod_id, tcx)) {
continue;
self.autoderef(span, base_ty)
.filter_map(move |(base_t, _)| {
match base_t.kind() {
ty::Adt(base_def, args) if !base_def.is_enum() => {
let tcx = self.tcx;
let fields = &base_def.non_enum_variant().fields;
// Some struct, e.g. some that impl `Deref`, have all private fields
// because you're expected to deref them to access the _real_ fields.
// This, for example, will help us suggest accessing a field through a `Box<T>`.
if fields.iter().all(|field| !field.vis.is_accessible_from(mod_id, tcx)) {
return None;
}
return Some((
fields
.iter()
.filter(move |field| {
field.vis.is_accessible_from(mod_id, tcx)
&& self.is_field_suggestable(field, hir_id, span)
})
// For compile-time reasons put a limit on number of fields we search
.take(100)
.collect::<Vec<_>>(),
*args,
));
}
return Some((
fields
.iter()
.filter(move |field| field.vis.is_accessible_from(mod_id, tcx))
// For compile-time reasons put a limit on number of fields we search
.take(100),
args,
));
_ => None,
}
_ => {}
}
}
None
})
.collect()
}

/// This method is called after we have encountered a missing field error to recursively
Expand All @@ -2766,6 +2759,7 @@ impl<'a, 'tcx> FnCtxt<'a, 'tcx> {
subst: GenericArgsRef<'tcx>,
mut field_path: Vec<Ident>,
mod_id: DefId,
hir_id: HirId,
) -> Option<Vec<Ident>> {
debug!(
"check_for_nested_field_satisfying(span: {:?}, candidate_field: {:?}, field_path: {:?}",
Expand All @@ -2781,20 +2775,23 @@ impl<'a, 'tcx> FnCtxt<'a, 'tcx> {
let field_ty = candidate_field.ty(self.tcx, subst);
if matches(candidate_field, field_ty) {
return Some(field_path);
} else if let Some((nested_fields, subst)) =
self.get_field_candidates_considering_privacy(span, field_ty, mod_id)
{
// recursively search fields of `candidate_field` if it's a ty::Adt
for field in nested_fields {
if let Some(field_path) = self.check_for_nested_field_satisfying(
span,
matches,
field,
subst,
field_path.clone(),
mod_id,
) {
return Some(field_path);
} else {
for (nested_fields, subst) in
self.get_field_candidates_considering_privacy(span, field_ty, mod_id, hir_id)
{
// recursively search fields of `candidate_field` if it's a ty::Adt
for field in nested_fields {
if let Some(field_path) = self.check_for_nested_field_satisfying(
span,
matches,
field,
subst,
field_path.clone(),
mod_id,
hir_id,
) {
return Some(field_path);
}
}
}
}
Expand Down
125 changes: 64 additions & 61 deletions compiler/rustc_hir_typeck/src/method/suggest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1870,69 +1870,72 @@ impl<'a, 'tcx> FnCtxt<'a, 'tcx> {
item_name: Ident,
return_type: Option<Ty<'tcx>>,
) {
if let SelfSource::MethodCall(expr) = source
&& let mod_id = self.tcx.parent_module(expr.hir_id).to_def_id()
&& let Some((fields, args)) =
self.get_field_candidates_considering_privacy(span, actual, mod_id)
{
let call_expr = self.tcx.hir().expect_expr(self.tcx.hir().parent_id(expr.hir_id));
if let SelfSource::MethodCall(expr) = source {
let mod_id = self.tcx.parent_module(expr.hir_id).to_def_id();
for (fields, args) in
self.get_field_candidates_considering_privacy(span, actual, mod_id, expr.hir_id)
{
let call_expr = self.tcx.hir().expect_expr(self.tcx.hir().parent_id(expr.hir_id));

let lang_items = self.tcx.lang_items();
let never_mention_traits = [
lang_items.clone_trait(),
lang_items.deref_trait(),
lang_items.deref_mut_trait(),
self.tcx.get_diagnostic_item(sym::AsRef),
self.tcx.get_diagnostic_item(sym::AsMut),
self.tcx.get_diagnostic_item(sym::Borrow),
self.tcx.get_diagnostic_item(sym::BorrowMut),
];
let candidate_fields: Vec<_> = fields
.filter_map(|candidate_field| {
self.check_for_nested_field_satisfying(
span,
&|_, field_ty| {
self.lookup_probe_for_diagnostic(
item_name,
field_ty,
call_expr,
ProbeScope::TraitsInScope,
return_type,
)
.is_ok_and(|pick| {
!never_mention_traits
.iter()
.flatten()
.any(|def_id| self.tcx.parent(pick.item.def_id) == *def_id)
})
},
candidate_field,
args,
vec![],
mod_id,
)
})
.map(|field_path| {
field_path
.iter()
.map(|id| id.name.to_ident_string())
.collect::<Vec<String>>()
.join(".")
})
.collect();
let lang_items = self.tcx.lang_items();
let never_mention_traits = [
lang_items.clone_trait(),
lang_items.deref_trait(),
lang_items.deref_mut_trait(),
self.tcx.get_diagnostic_item(sym::AsRef),
self.tcx.get_diagnostic_item(sym::AsMut),
self.tcx.get_diagnostic_item(sym::Borrow),
self.tcx.get_diagnostic_item(sym::BorrowMut),
];
let candidate_fields: Vec<_> = fields
.iter()
.filter_map(|candidate_field| {
self.check_for_nested_field_satisfying(
span,
&|_, field_ty| {
self.lookup_probe_for_diagnostic(
item_name,
field_ty,
call_expr,
ProbeScope::TraitsInScope,
return_type,
)
.is_ok_and(|pick| {
!never_mention_traits
.iter()
.flatten()
.any(|def_id| self.tcx.parent(pick.item.def_id) == *def_id)
})
},
candidate_field,
args,
vec![],
mod_id,
expr.hir_id,
)
})
.map(|field_path| {
field_path
.iter()
.map(|id| id.name.to_ident_string())
.collect::<Vec<String>>()
.join(".")
})
.collect();

let len = candidate_fields.len();
if len > 0 {
err.span_suggestions(
item_name.span.shrink_to_lo(),
format!(
"{} of the expressions' fields {} a method of the same name",
if len > 1 { "some" } else { "one" },
if len > 1 { "have" } else { "has" },
),
candidate_fields.iter().map(|path| format!("{path}.")),
Applicability::MaybeIncorrect,
);
let len = candidate_fields.len();
if len > 0 {
err.span_suggestions(
item_name.span.shrink_to_lo(),
format!(
"{} of the expressions' fields {} a method of the same name",
if len > 1 { "some" } else { "one" },
if len > 1 { "have" } else { "has" },
),
candidate_fields.iter().map(|path| format!("{path}.")),
Applicability::MaybeIncorrect,
);
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ fn await_on_struct_missing() {
let x = S;
x.await;
//~^ ERROR no field `await` on type
//~| NOTE unknown field
//~| NOTE to `.await` a `Future`, switch to Rust 2018
//~| HELP set `edition = "2021"` in `Cargo.toml`
//~| NOTE for more on editions, read https://doc.rust-lang.org/edition-guide
Expand All @@ -32,7 +31,6 @@ fn await_on_struct_similar() {
fn await_on_63533(x: Pin<&mut dyn Future<Output = ()>>) {
x.await;
//~^ ERROR no field `await` on type
//~| NOTE unknown field
//~| NOTE to `.await` a `Future`, switch to Rust 2018
//~| HELP set `edition = "2021"` in `Cargo.toml`
//~| NOTE for more on editions, read https://doc.rust-lang.org/edition-guide
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@ error[E0609]: no field `await` on type `await_on_struct_missing::S`
--> $DIR/suggest-switching-edition-on-await-cargo.rs:11:7
|
LL | x.await;
| ^^^^^ unknown field
| ^^^^^
|
= note: to `.await` a `Future`, switch to Rust 2018 or later
= help: set `edition = "2021"` in `Cargo.toml`
= note: for more on editions, read https://doc.rust-lang.org/edition-guide

error[E0609]: no field `await` on type `await_on_struct_similar::S`
--> $DIR/suggest-switching-edition-on-await-cargo.rs:24:7
--> $DIR/suggest-switching-edition-on-await-cargo.rs:23:7
|
LL | x.await;
| ^^^^^ help: a field with a similar name exists: `awai`
Expand All @@ -19,17 +19,17 @@ LL | x.await;
= note: for more on editions, read https://doc.rust-lang.org/edition-guide

error[E0609]: no field `await` on type `Pin<&mut dyn Future<Output = ()>>`
--> $DIR/suggest-switching-edition-on-await-cargo.rs:33:7
--> $DIR/suggest-switching-edition-on-await-cargo.rs:32:7
|
LL | x.await;
| ^^^^^ unknown field
| ^^^^^
|
= note: to `.await` a `Future`, switch to Rust 2018 or later
= help: set `edition = "2021"` in `Cargo.toml`
= note: for more on editions, read https://doc.rust-lang.org/edition-guide

error[E0609]: no field `await` on type `impl Future<Output = ()>`
--> $DIR/suggest-switching-edition-on-await-cargo.rs:42:7
--> $DIR/suggest-switching-edition-on-await-cargo.rs:40:7
|
LL | x.await;
| ^^^^^
Expand Down
Loading

0 comments on commit 933341a

Please sign in to comment.