Skip to content

Commit

Permalink
Rollup merge of #87775 - Kobzol:single-associated-item-hint, r=oli-obk
Browse files Browse the repository at this point in the history
Add hint for unresolved associated trait items if the trait has a single item

This PR introduces a special-cased hint for unresolved trait items paths. It is shown if:
- the path was not resolved to any existing trait item
- and no existing trait item's name was reasonably close with regard to edit distance
- and the trait only has a single item in the corresponding namespace

I didn't know where I should put tests, therefore so far I just managed to bless two existing tests. I would be glad for hints where should tests for a hint like this be created, how should they be named (with reference to the original issue?) and what tests should I create (is it enough to test it just for types? or create separate tests also for functions and constants?).

It could also be turned into a machine applicable suggestion I suppose.

This is my first `rustc` PR, so please go easy on me :)

Fixes: #87638
  • Loading branch information
JohnTitor committed Aug 6, 2021
2 parents 3b0e797 + d0d4947 commit 8ee962f
Show file tree
Hide file tree
Showing 7 changed files with 172 additions and 28 deletions.
62 changes: 43 additions & 19 deletions compiler/rustc_resolve/src/diagnostics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,14 +38,25 @@ crate type Suggestion = (Vec<(Span, String)>, String, Applicability);
/// similarly named label and whether or not it is reachable.
crate type LabelSuggestion = (Ident, bool);

crate enum SuggestionTarget {
/// The target has a similar name as the name used by the programmer (probably a typo)
SimilarlyNamed,
/// The target is the only valid item that can be used in the corresponding context
SingleItem,
}

crate struct TypoSuggestion {
pub candidate: Symbol,
pub res: Res,
pub target: SuggestionTarget,
}

impl TypoSuggestion {
crate fn from_res(candidate: Symbol, res: Res) -> TypoSuggestion {
TypoSuggestion { candidate, res }
crate fn typo_from_res(candidate: Symbol, res: Res) -> TypoSuggestion {
Self { candidate, res, target: SuggestionTarget::SimilarlyNamed }
}
crate fn single_item_from_res(candidate: Symbol, res: Res) -> TypoSuggestion {
Self { candidate, res, target: SuggestionTarget::SingleItem }
}
}

Expand Down Expand Up @@ -80,7 +91,7 @@ impl<'a> Resolver<'a> {
if let Some(binding) = resolution.borrow().binding {
let res = binding.res();
if filter_fn(res) {
names.push(TypoSuggestion::from_res(key.ident.name, res));
names.push(TypoSuggestion::typo_from_res(key.ident.name, res));
}
}
}
Expand Down Expand Up @@ -623,7 +634,7 @@ impl<'a> Resolver<'a> {
.get(&expn_id)
.into_iter()
.flatten()
.map(|ident| TypoSuggestion::from_res(ident.name, res)),
.map(|ident| TypoSuggestion::typo_from_res(ident.name, res)),
);
}
}
Expand All @@ -642,7 +653,7 @@ impl<'a> Resolver<'a> {
suggestions.extend(
ext.helper_attrs
.iter()
.map(|name| TypoSuggestion::from_res(*name, res)),
.map(|name| TypoSuggestion::typo_from_res(*name, res)),
);
}
}
Expand All @@ -652,8 +663,10 @@ impl<'a> Resolver<'a> {
if let MacroRulesScope::Binding(macro_rules_binding) = macro_rules_scope.get() {
let res = macro_rules_binding.binding.res();
if filter_fn(res) {
suggestions
.push(TypoSuggestion::from_res(macro_rules_binding.ident.name, res))
suggestions.push(TypoSuggestion::typo_from_res(
macro_rules_binding.ident.name,
res,
))
}
}
}
Expand All @@ -671,15 +684,15 @@ impl<'a> Resolver<'a> {
suggestions.extend(
this.registered_attrs
.iter()
.map(|ident| TypoSuggestion::from_res(ident.name, res)),
.map(|ident| TypoSuggestion::typo_from_res(ident.name, res)),
);
}
}
Scope::MacroUsePrelude => {
suggestions.extend(this.macro_use_prelude.iter().filter_map(
|(name, binding)| {
let res = binding.res();
filter_fn(res).then_some(TypoSuggestion::from_res(*name, res))
filter_fn(res).then_some(TypoSuggestion::typo_from_res(*name, res))
},
));
}
Expand All @@ -689,22 +702,22 @@ impl<'a> Resolver<'a> {
suggestions.extend(
BUILTIN_ATTRIBUTES
.iter()
.map(|(name, ..)| TypoSuggestion::from_res(*name, res)),
.map(|(name, ..)| TypoSuggestion::typo_from_res(*name, res)),
);
}
}
Scope::ExternPrelude => {
suggestions.extend(this.extern_prelude.iter().filter_map(|(ident, _)| {
let res = Res::Def(DefKind::Mod, DefId::local(CRATE_DEF_INDEX));
filter_fn(res).then_some(TypoSuggestion::from_res(ident.name, res))
filter_fn(res).then_some(TypoSuggestion::typo_from_res(ident.name, res))
}));
}
Scope::ToolPrelude => {
let res = Res::NonMacroAttr(NonMacroAttrKind::Tool);
suggestions.extend(
this.registered_tools
.iter()
.map(|ident| TypoSuggestion::from_res(ident.name, res)),
.map(|ident| TypoSuggestion::typo_from_res(ident.name, res)),
);
}
Scope::StdLibPrelude => {
Expand All @@ -721,7 +734,7 @@ impl<'a> Resolver<'a> {
Scope::BuiltinTypes => {
suggestions.extend(PrimTy::ALL.iter().filter_map(|prim_ty| {
let res = Res::PrimTy(*prim_ty);
filter_fn(res).then_some(TypoSuggestion::from_res(prim_ty.name(), res))
filter_fn(res).then_some(TypoSuggestion::typo_from_res(prim_ty.name(), res))
}))
}
}
Expand Down Expand Up @@ -993,20 +1006,31 @@ impl<'a> Resolver<'a> {
// | ^
return false;
}
let prefix = match suggestion.target {
SuggestionTarget::SimilarlyNamed => "similarly named ",
SuggestionTarget::SingleItem => "",
};

err.span_label(
self.session.source_map().guess_head_span(def_span),
&format!(
"similarly named {} `{}` defined here",
"{}{} `{}` defined here",
prefix,
suggestion.res.descr(),
suggestion.candidate.as_str(),
),
);
}
let msg = format!(
"{} {} with a similar name exists",
suggestion.res.article(),
suggestion.res.descr()
);
let msg = match suggestion.target {
SuggestionTarget::SimilarlyNamed => format!(
"{} {} with a similar name exists",
suggestion.res.article(),
suggestion.res.descr()
),
SuggestionTarget::SingleItem => {
format!("maybe you meant this {}", suggestion.res.descr())
}
};
err.span_suggestion(
span,
&msg,
Expand Down
52 changes: 45 additions & 7 deletions compiler/rustc_resolve/src/late/diagnostics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -541,6 +541,10 @@ impl<'a: 'ast, 'ast> LateResolutionVisitor<'a, '_, 'ast> {
}
_ => {}
}

// If the trait has a single item (which wasn't matched by Levenshtein), suggest it
let suggestion = self.get_single_associated_item(&path, span, &source, is_expected);
self.r.add_typo_suggestion(&mut err, suggestion, ident_span);
}
if fallback {
// Fallback label.
Expand Down Expand Up @@ -585,6 +589,40 @@ impl<'a: 'ast, 'ast> LateResolutionVisitor<'a, '_, 'ast> {
(err, candidates)
}

fn get_single_associated_item(
&mut self,
path: &[Segment],
span: Span,
source: &PathSource<'_>,
filter_fn: &impl Fn(Res) -> bool,
) -> Option<TypoSuggestion> {
if let crate::PathSource::TraitItem(_) = source {
let mod_path = &path[..path.len() - 1];
if let PathResult::Module(ModuleOrUniformRoot::Module(module)) =
self.resolve_path(mod_path, None, false, span, CrateLint::No)
{
let resolutions = self.r.resolutions(module).borrow();
let targets: Vec<_> =
resolutions
.iter()
.filter_map(|(key, resolution)| {
resolution.borrow().binding.map(|binding| binding.res()).and_then(
|res| if filter_fn(res) { Some((key, res)) } else { None },
)
})
.collect();
if targets.len() == 1 {
let target = targets[0];
return Some(TypoSuggestion::single_item_from_res(
target.0.ident.name,
target.1,
));
}
}
}
None
}

/// Given `where <T as Bar>::Baz: String`, suggest `where T: Bar<Baz = String>`.
fn restrict_assoc_type_in_where_clause(
&mut self,
Expand Down Expand Up @@ -1208,7 +1246,7 @@ impl<'a: 'ast, 'ast> LateResolutionVisitor<'a, '_, 'ast> {
// Locals and type parameters
for (ident, &res) in &rib.bindings {
if filter_fn(res) {
names.push(TypoSuggestion::from_res(ident.name, res));
names.push(TypoSuggestion::typo_from_res(ident.name, res));
}
}
// Items in scope
Expand All @@ -1231,7 +1269,9 @@ impl<'a: 'ast, 'ast> LateResolutionVisitor<'a, '_, 'ast> {
);

if filter_fn(crate_mod) {
Some(TypoSuggestion::from_res(ident.name, crate_mod))
Some(TypoSuggestion::typo_from_res(
ident.name, crate_mod,
))
} else {
None
}
Expand All @@ -1249,11 +1289,9 @@ impl<'a: 'ast, 'ast> LateResolutionVisitor<'a, '_, 'ast> {
}
// Add primitive types to the mix
if filter_fn(Res::PrimTy(PrimTy::Bool)) {
names.extend(
PrimTy::ALL.iter().map(|prim_ty| {
TypoSuggestion::from_res(prim_ty.name(), Res::PrimTy(*prim_ty))
}),
)
names.extend(PrimTy::ALL.iter().map(|prim_ty| {
TypoSuggestion::typo_from_res(prim_ty.name(), Res::PrimTy(*prim_ty))
}))
}
} else {
// Search in module.
Expand Down
22 changes: 22 additions & 0 deletions src/test/ui/associated-item/issue-87638.fixed
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// run-rustfix

trait Trait {
const FOO: usize;

type Target;
}

struct S;

impl Trait for S {
const FOO: usize = 0;
type Target = ();
}

fn main() {
let _: <S as Trait>::Target; //~ ERROR cannot find associated type `Output` in trait `Trait`
//~^ HELP maybe you meant this associated type

let _ = <S as Trait>::FOO; //~ ERROR cannot find method or associated constant `BAR` in trait `Trait`
//~^ HELP maybe you meant this associated constant
}
22 changes: 22 additions & 0 deletions src/test/ui/associated-item/issue-87638.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// run-rustfix

trait Trait {
const FOO: usize;

type Target;
}

struct S;

impl Trait for S {
const FOO: usize = 0;
type Target = ();
}

fn main() {
let _: <S as Trait>::Output; //~ ERROR cannot find associated type `Output` in trait `Trait`
//~^ HELP maybe you meant this associated type

let _ = <S as Trait>::BAR; //~ ERROR cannot find method or associated constant `BAR` in trait `Trait`
//~^ HELP maybe you meant this associated constant
}
27 changes: 27 additions & 0 deletions src/test/ui/associated-item/issue-87638.stderr
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
error[E0576]: cannot find associated type `Output` in trait `Trait`
--> $DIR/issue-87638.rs:17:26
|
LL | type Target;
| ------------ associated type `Target` defined here
...
LL | let _: <S as Trait>::Output;
| ^^^^^^
| |
| not found in `Trait`
| help: maybe you meant this associated type: `Target`

error[E0576]: cannot find method or associated constant `BAR` in trait `Trait`
--> $DIR/issue-87638.rs:20:27
|
LL | const FOO: usize;
| ----------------- associated constant `FOO` defined here
...
LL | let _ = <S as Trait>::BAR;
| ^^^
| |
| not found in `Trait`
| help: maybe you meant this associated constant: `FOO`

error: aborting due to 2 previous errors

For more information about this error, try `rustc --explain E0576`.
7 changes: 6 additions & 1 deletion src/test/ui/associated-types/issue-22037.stderr
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
error[E0576]: cannot find associated type `X` in trait `A`
--> $DIR/issue-22037.rs:3:33
|
LL | type Output;
| ------------ associated type `Output` defined here
LL | fn a(&self) -> <Self as A>::X;
| ^ not found in `A`
| ^
| |
| not found in `A`
| help: maybe you meant this associated type: `Output`

error: aborting due to previous error

Expand Down
8 changes: 7 additions & 1 deletion src/test/ui/issues/issue-19883.stderr
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
error[E0576]: cannot find associated type `Dst` in trait `From`
--> $DIR/issue-19883.rs:9:30
|
LL | type Output;
| ------------ associated type `Output` defined here
...
LL | <Dst as From<Self>>::Dst
| ^^^ not found in `From`
| ^^^
| |
| not found in `From`
| help: maybe you meant this associated type: `Output`

error: aborting due to previous error

Expand Down

0 comments on commit 8ee962f

Please sign in to comment.