Rollup merge of #152471 - JohnTitor:sugg-assoc-items-from-bounds, r=estebank

improve associated-type suggestions from bounds

Should address invalid suggestions I could come up with, but the suggestion is too hand-crafted and invalid patterns may exist.
r? @estebank
Fix https://github.com/rust-lang/rust/issues/73321
This commit is contained in:
Jacob Pratt 2026-02-13 22:26:33 -05:00 committed by GitHub
commit b80512462f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 431 additions and 9 deletions

View file

@ -5475,6 +5475,32 @@ fn create_delegation_attrs(attrs: &[Attribute]) -> DelegationAttrs {
DelegationAttrs { flags, to_inherit: to_inherit_attrs }
}
fn required_generic_args_suggestion(generics: &ast::Generics) -> Option<String> {
let required = generics
.params
.iter()
.filter_map(|param| match &param.kind {
ast::GenericParamKind::Lifetime => Some("'_"),
ast::GenericParamKind::Type { default } => {
if default.is_none() {
Some("_")
} else {
None
}
}
ast::GenericParamKind::Const { default, .. } => {
if default.is_none() {
Some("_")
} else {
None
}
}
})
.collect::<Vec<_>>();
if required.is_empty() { None } else { Some(format!("<{}>", required.join(", "))) }
}
impl<'ast> Visitor<'ast> for ItemInfoCollector<'_, '_, '_> {
fn visit_item(&mut self, item: &'ast Item) {
match &item.kind {
@ -5533,6 +5559,13 @@ impl<'ast> Visitor<'ast> for ItemInfoCollector<'_, '_, '_> {
if let AssocItemKind::Fn(box Fn { sig, .. }) = &item.kind {
self.collect_fn_info(sig.header, &sig.decl, item.id, &item.attrs);
}
if let AssocItemKind::Type(box ast::TyAlias { generics, .. }) = &item.kind {
let def_id = self.r.local_def_id(item.id);
if let Some(suggestion) = required_generic_args_suggestion(generics) {
self.r.item_required_generic_args_suggestions.insert(def_id, suggestion);
}
}
visit::walk_assoc_item(self, item, ctxt);
}
}

View file

@ -9,8 +9,8 @@ use rustc_ast::{
self as ast, AssocItemKind, DUMMY_NODE_ID, Expr, ExprKind, GenericParam, GenericParamKind,
Item, ItemKind, MethodCall, NodeId, Path, PathSegment, Ty, TyKind,
};
use rustc_ast_pretty::pprust::where_bound_predicate_to_string;
use rustc_data_structures::fx::{FxHashSet, FxIndexSet};
use rustc_ast_pretty::pprust::{path_to_string, where_bound_predicate_to_string};
use rustc_data_structures::fx::{FxHashSet, FxIndexMap, FxIndexSet};
use rustc_errors::codes::*;
use rustc_errors::{
Applicability, Diag, ErrorGuaranteed, MultiSpan, SuggestionStyle, pluralize,
@ -79,6 +79,23 @@ fn is_self_value(path: &[Segment], namespace: Namespace) -> bool {
namespace == ValueNS && path.len() == 1 && path[0].ident.name == kw::SelfLower
}
fn path_to_string_without_assoc_item_bindings(path: &Path) -> String {
let mut path = path.clone();
for segment in &mut path.segments {
let mut remove_args = false;
if let Some(args) = segment.args.as_deref_mut()
&& let ast::GenericArgs::AngleBracketed(angle_bracketed) = args
{
angle_bracketed.args.retain(|arg| matches!(arg, ast::AngleBracketedArg::Arg(_)));
remove_args = angle_bracketed.args.is_empty();
}
if remove_args {
segment.args = None;
}
}
path_to_string(&path)
}
/// Gets the stringified path for an enum from an `ImportSuggestion` for an enum variant.
fn import_candidate_to_enum_paths(suggestion: &ImportSuggestion) -> (String, String) {
let variant_path = &suggestion.path;
@ -169,6 +186,201 @@ impl TypoCandidate {
}
impl<'ast, 'ra, 'tcx> LateResolutionVisitor<'_, 'ast, 'ra, 'tcx> {
fn trait_assoc_type_def_id_by_name(
&mut self,
trait_def_id: DefId,
assoc_name: Symbol,
) -> Option<DefId> {
let module = self.r.get_module(trait_def_id)?;
self.r.resolutions(module).borrow().iter().find_map(|(key, resolution)| {
if key.ident.name != assoc_name {
return None;
}
let resolution = resolution.borrow();
let binding = resolution.best_decl()?;
match binding.res() {
Res::Def(DefKind::AssocTy, def_id) => Some(def_id),
_ => None,
}
})
}
/// This does best-effort work to generate suggestions for associated types.
fn suggest_assoc_type_from_bounds(
&mut self,
err: &mut Diag<'_>,
source: PathSource<'_, 'ast, 'ra>,
path: &[Segment],
ident_span: Span,
) -> bool {
// Filter out cases where we cannot emit meaningful suggestions.
if source.namespace() != TypeNS {
return false;
}
let [segment] = path else { return false };
if segment.has_generic_args {
return false;
}
if !ident_span.can_be_used_for_suggestions() {
return false;
}
let assoc_name = segment.ident.name;
if assoc_name == kw::Underscore {
return false;
}
// Map: type parameter name -> (trait def id -> (assoc type def id, trait paths as written)).
// We keep a set of paths per trait so we can detect cases like
// `T: Trait<i32> + Trait<u32>` where suggesting `T::Assoc` would be ambiguous.
let mut matching_bounds: FxIndexMap<
Symbol,
FxIndexMap<DefId, (DefId, FxIndexSet<String>)>,
> = FxIndexMap::default();
let mut record_bound = |this: &mut Self,
ty_param: Symbol,
poly_trait_ref: &ast::PolyTraitRef| {
// Avoid generating suggestions we can't print in a well-formed way.
if !poly_trait_ref.bound_generic_params.is_empty() {
return;
}
if poly_trait_ref.modifiers != ast::TraitBoundModifiers::NONE {
return;
}
let Some(trait_seg) = poly_trait_ref.trait_ref.path.segments.last() else {
return;
};
let Some(partial_res) = this.r.partial_res_map.get(&trait_seg.id) else {
return;
};
let Some(trait_def_id) = partial_res.full_res().and_then(|res| res.opt_def_id()) else {
return;
};
let Some(assoc_type_def_id) =
this.trait_assoc_type_def_id_by_name(trait_def_id, assoc_name)
else {
return;
};
// Preserve `::` and generic args so we don't generate broken suggestions like
// `<T as Foo>::Assoc` for bounds written as `T: ::Foo<'a>`, while stripping
// associated-item bindings that are rejected in qualified paths.
let trait_path =
path_to_string_without_assoc_item_bindings(&poly_trait_ref.trait_ref.path);
let trait_bounds = matching_bounds.entry(ty_param).or_default();
let trait_bounds = trait_bounds
.entry(trait_def_id)
.or_insert_with(|| (assoc_type_def_id, FxIndexSet::default()));
debug_assert_eq!(trait_bounds.0, assoc_type_def_id);
trait_bounds.1.insert(trait_path);
};
let mut record_from_generics = |this: &mut Self, generics: &ast::Generics| {
for param in &generics.params {
let ast::GenericParamKind::Type { .. } = param.kind else { continue };
for bound in &param.bounds {
let ast::GenericBound::Trait(poly_trait_ref) = bound else { continue };
record_bound(this, param.ident.name, poly_trait_ref);
}
}
for predicate in &generics.where_clause.predicates {
let ast::WherePredicateKind::BoundPredicate(where_bound) = &predicate.kind else {
continue;
};
let ast::TyKind::Path(None, bounded_path) = &where_bound.bounded_ty.kind else {
continue;
};
let [ast::PathSegment { ident, args: None, .. }] = &bounded_path.segments[..]
else {
continue;
};
// Only suggest for bounds that are explicitly on an in-scope type parameter.
let Some(partial_res) = this.r.partial_res_map.get(&where_bound.bounded_ty.id)
else {
continue;
};
if !matches!(partial_res.full_res(), Some(Res::Def(DefKind::TyParam, _))) {
continue;
}
for bound in &where_bound.bounds {
let ast::GenericBound::Trait(poly_trait_ref) = bound else { continue };
record_bound(this, ident.name, poly_trait_ref);
}
}
};
if let Some(item) = self.diag_metadata.current_item
&& let Some(generics) = item.kind.generics()
{
record_from_generics(self, generics);
}
if let Some(item) = self.diag_metadata.current_item
&& matches!(item.kind, ItemKind::Impl(..))
&& let Some(assoc) = self.diag_metadata.current_impl_item
{
let generics = match &assoc.kind {
AssocItemKind::Const(box ast::ConstItem { generics, .. })
| AssocItemKind::Fn(box ast::Fn { generics, .. })
| AssocItemKind::Type(box ast::TyAlias { generics, .. }) => Some(generics),
AssocItemKind::Delegation(..)
| AssocItemKind::MacCall(..)
| AssocItemKind::DelegationMac(..) => None,
};
if let Some(generics) = generics {
record_from_generics(self, generics);
}
}
let mut suggestions: FxIndexSet<String> = FxIndexSet::default();
for (ty_param, traits) in matching_bounds {
let ty_param = ty_param.to_ident_string();
let trait_paths_len: usize = traits.values().map(|(_, paths)| paths.len()).sum();
if traits.len() == 1 && trait_paths_len == 1 {
let assoc_type_def_id = traits.values().next().unwrap().0;
let assoc_segment = format!(
"{}{}",
assoc_name,
self.r.item_required_generic_args_suggestion(assoc_type_def_id)
);
suggestions.insert(format!("{ty_param}::{assoc_segment}"));
} else {
for (assoc_type_def_id, trait_paths) in traits.into_values() {
let assoc_segment = format!(
"{}{}",
assoc_name,
self.r.item_required_generic_args_suggestion(assoc_type_def_id)
);
for trait_path in trait_paths {
suggestions
.insert(format!("<{ty_param} as {trait_path}>::{assoc_segment}"));
}
}
}
}
if suggestions.is_empty() {
return false;
}
let mut suggestions: Vec<String> = suggestions.into_iter().collect();
suggestions.sort();
err.span_suggestions_with_style(
ident_span,
"you might have meant to use an associated type of the same name",
suggestions,
Applicability::MaybeIncorrect,
SuggestionStyle::ShowAlways,
);
true
}
fn make_base_error(
&mut self,
path: &[Segment],
@ -1038,6 +1250,14 @@ impl<'ast, 'ra, 'tcx> LateResolutionVisitor<'_, 'ast, 'ra, 'tcx> {
) -> bool {
let is_expected = &|res| source.is_expected(res);
let ident_span = path.last().map_or(span, |ident| ident.ident.span);
// Prefer suggestions based on associated types from in-scope bounds (e.g. `T::Item`)
// over purely edit-distance-based identifier suggestions.
// Otherwise suggestions could be verbose.
if self.suggest_assoc_type_from_bounds(err, source, path, ident_span) {
return false;
}
let typo_sugg =
self.lookup_typo_candidate(path, following_seg, source.namespace(), is_expected);
let mut fallback = false;

View file

@ -1337,6 +1337,8 @@ pub struct Resolver<'ra, 'tcx> {
/// Amount of lifetime parameters for each item in the crate.
item_generics_num_lifetimes: FxHashMap<LocalDefId, usize> = default::fx_hash_map(),
/// Generic args to suggest for required params (e.g. `<'_>`, `<_, _>`), if any.
item_required_generic_args_suggestions: FxHashMap<LocalDefId, String> = default::fx_hash_map(),
delegation_fn_sigs: LocalDefIdMap<DelegationFnSig> = Default::default(),
delegation_infos: LocalDefIdMap<DelegationInfo> = Default::default(),
@ -1555,6 +1557,32 @@ impl<'tcx> Resolver<'_, 'tcx> {
}
}
fn item_required_generic_args_suggestion(&self, def_id: DefId) -> String {
if let Some(def_id) = def_id.as_local() {
self.item_required_generic_args_suggestions.get(&def_id).cloned().unwrap_or_default()
} else {
let required = self
.tcx
.generics_of(def_id)
.own_params
.iter()
.filter_map(|param| match param.kind {
ty::GenericParamDefKind::Lifetime => Some("'_"),
ty::GenericParamDefKind::Type { has_default, .. }
| ty::GenericParamDefKind::Const { has_default } => {
if has_default {
None
} else {
Some("_")
}
}
})
.collect::<Vec<_>>();
if required.is_empty() { String::new() } else { format!("<{}>", required.join(", ")) }
}
}
pub fn tcx(&self) -> TyCtxt<'tcx> {
self.tcx
}

View file

@ -1,16 +1,13 @@
error[E0425]: cannot find type `A` in this scope
--> $DIR/associated-types-eq-1.rs:10:12
|
LL | fn foo2<I: Foo>(x: I) {
| - similarly named type parameter `I` defined here
LL | let _: A = x.boo();
| ^
|
help: a type parameter with a similar name exists
|
LL - let _: A = x.boo();
LL + let _: I = x.boo();
help: you might have meant to use an associated type of the same name
|
LL | let _: I::A = x.boo();
| +++
help: you might be missing a type parameter
|
LL | fn foo2<I: Foo, A>(x: I) {

View file

@ -0,0 +1,64 @@
pub trait Trait<T> {
type Assoc;
}
fn f<U: Trait<i32> + Trait<u32>>() {
let _: Assoc = todo!(); //~ ERROR cannot find type `Assoc` in this scope
}
pub trait Foo<'a> {
type A;
}
pub mod inner {
pub trait Foo<'a> {
type A;
}
}
fn g<'a, T: ::Foo<'a> + inner::Foo<'a>>() {
let _: A = todo!(); //~ ERROR cannot find type `A` in this scope
}
pub trait First {
type Assoc;
}
pub trait Second {
type Assoc;
}
fn h<T: First<Assoc = u32> + Second<Assoc = i32>>() {
let _: Assoc = todo!(); //~ ERROR cannot find type `Assoc` in this scope
}
pub trait Gat {
type Assoc<'a>;
}
fn i<T: Gat>() {
let _: Assoc = todo!(); //~ ERROR cannot find type `Assoc` in this scope
}
fn j<T: First>() {
struct Local;
impl Local {
fn method<U: First>() {
let _: Assoc = todo!(); //~ ERROR cannot find type `Assoc` in this scope
}
}
let _ = std::marker::PhantomData::<T>;
}
pub struct S;
impl S {
fn method<T: First>() {
fn inner() {
let _: Assoc = todo!(); //~ ERROR cannot find type `Assoc` in this scope
}
inner();
}
}
fn main() {}

View file

@ -0,0 +1,75 @@
error[E0425]: cannot find type `Assoc` in this scope
--> $DIR/suggest-assoc-type-from-bounds.rs:6:12
|
LL | let _: Assoc = todo!();
| ^^^^^
|
help: you might have meant to use an associated type of the same name
|
LL | let _: <U as Trait<i32>>::Assoc = todo!();
| +++++++++++++++++++
LL | let _: <U as Trait<u32>>::Assoc = todo!();
| +++++++++++++++++++
error[E0425]: cannot find type `A` in this scope
--> $DIR/suggest-assoc-type-from-bounds.rs:20:12
|
LL | let _: A = todo!();
| ^
|
help: you might have meant to use an associated type of the same name
|
LL | let _: <T as ::Foo<'a>>::A = todo!();
| ++++++++++++++++++
LL | let _: <T as inner::Foo<'a>>::A = todo!();
| +++++++++++++++++++++++
help: you might be missing a type parameter
|
LL | fn g<'a, T: ::Foo<'a> + inner::Foo<'a>, A>() {
| +++
error[E0425]: cannot find type `Assoc` in this scope
--> $DIR/suggest-assoc-type-from-bounds.rs:32:12
|
LL | let _: Assoc = todo!();
| ^^^^^
|
help: you might have meant to use an associated type of the same name
|
LL | let _: <T as First>::Assoc = todo!();
| ++++++++++++++
LL | let _: <T as Second>::Assoc = todo!();
| +++++++++++++++
error[E0425]: cannot find type `Assoc` in this scope
--> $DIR/suggest-assoc-type-from-bounds.rs:40:12
|
LL | let _: Assoc = todo!();
| ^^^^^
|
help: you might have meant to use an associated type of the same name
|
LL - let _: Assoc = todo!();
LL + let _: T::Assoc<'_> = todo!();
|
error[E0425]: cannot find type `Assoc` in this scope
--> $DIR/suggest-assoc-type-from-bounds.rs:47:20
|
LL | let _: Assoc = todo!();
| ^^^^^
|
help: you might have meant to use an associated type of the same name
|
LL | let _: U::Assoc = todo!();
| +++
error[E0425]: cannot find type `Assoc` in this scope
--> $DIR/suggest-assoc-type-from-bounds.rs:58:20
|
LL | let _: Assoc = todo!();
| ^^^^^ not found in this scope
error: aborting due to 6 previous errors
For more information about this error, try `rustc --explain E0425`.

View file

@ -2,7 +2,12 @@ error[E0425]: cannot find type `Item` in this scope
--> $DIR/sugg-swap-equality-in-macro-issue-139050.rs:29:5
|
LL | Item: Eq + Debug,
| ^^^^ not found in this scope
| ^^^^
|
help: you might have meant to use an associated type of the same name
|
LL | I::Item: Eq + Debug,
| +++
error[E0308]: mismatched types
--> $DIR/sugg-swap-equality-in-macro-issue-139050.rs:31:5