Skip to content
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
33 changes: 33 additions & 0 deletions compiler/rustc_resolve/src/late.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5442,6 +5442,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 {
Expand Down Expand Up @@ -5500,6 +5526,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);
}
}
Expand Down
224 changes: 222 additions & 2 deletions compiler/rustc_resolve/src/late/diagnostics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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)
}
Comment on lines +82 to +97
Copy link
Contributor

@estebank estebank Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we don't already have something like this in pprint? I know there are a bunch of modifier macros that you can surround a format!("{ty}") with that will produce different output depending on the modifier.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've checked existing support a bit but couldn't find anything for what we do (in particular, ast::Path) here.


/// 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;
Expand Down Expand Up @@ -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],
Expand Down Expand Up @@ -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;
Expand Down
28 changes: 28 additions & 0 deletions compiler/rustc_resolve/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1338,6 +1338,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(),

Expand Down Expand Up @@ -1556,6 +1558,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
}
Expand Down
9 changes: 3 additions & 6 deletions tests/ui/associated-types/associated-types-eq-1.stderr
Original file line number Diff line number Diff line change
@@ -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) {
Expand Down
Loading
Loading