diff --git a/compiler/rustc_resolve/src/late.rs b/compiler/rustc_resolve/src/late.rs index fe6346f6c6e92..504dacd958ec8 100644 --- a/compiler/rustc_resolve/src/late.rs +++ b/compiler/rustc_resolve/src/late.rs @@ -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 { + let required = generics + .params + .iter() + .filter_map(|param| match ¶m.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::>(); + + 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 { @@ -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); } } diff --git a/compiler/rustc_resolve/src/late/diagnostics.rs b/compiler/rustc_resolve/src/late/diagnostics.rs index 95f0d3e67ef2d..7bc8d280824fe 100644 --- a/compiler/rustc_resolve/src/late/diagnostics.rs +++ b/compiler/rustc_resolve/src/late/diagnostics.rs @@ -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 { + 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 + Trait` where suggesting `T::Assoc` would be ambiguous. + let mut matching_bounds: FxIndexMap< + Symbol, + FxIndexMap)>, + > = 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 + // `::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 ¶m.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 = 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 = 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; diff --git a/compiler/rustc_resolve/src/lib.rs b/compiler/rustc_resolve/src/lib.rs index 1399f9933ad4e..3a3ce879234ab 100644 --- a/compiler/rustc_resolve/src/lib.rs +++ b/compiler/rustc_resolve/src/lib.rs @@ -1338,6 +1338,8 @@ pub struct Resolver<'ra, 'tcx> { /// Amount of lifetime parameters for each item in the crate. item_generics_num_lifetimes: FxHashMap = default::fx_hash_map(), + /// Generic args to suggest for required params (e.g. `<'_>`, `<_, _>`), if any. + item_required_generic_args_suggestions: FxHashMap = default::fx_hash_map(), delegation_fn_sigs: LocalDefIdMap = Default::default(), delegation_infos: LocalDefIdMap = Default::default(), @@ -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::>(); + + if required.is_empty() { String::new() } else { format!("<{}>", required.join(", ")) } + } + } + pub fn tcx(&self) -> TyCtxt<'tcx> { self.tcx } diff --git a/tests/ui/associated-types/associated-types-eq-1.stderr b/tests/ui/associated-types/associated-types-eq-1.stderr index 869583df644fd..54e50f36fb6ad 100644 --- a/tests/ui/associated-types/associated-types-eq-1.stderr +++ b/tests/ui/associated-types/associated-types-eq-1.stderr @@ -1,16 +1,13 @@ error[E0425]: cannot find type `A` in this scope --> $DIR/associated-types-eq-1.rs:10:12 | -LL | fn foo2(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(x: I) { diff --git a/tests/ui/associated-types/suggest-assoc-type-from-bounds.rs b/tests/ui/associated-types/suggest-assoc-type-from-bounds.rs new file mode 100644 index 0000000000000..8b349f325cd73 --- /dev/null +++ b/tests/ui/associated-types/suggest-assoc-type-from-bounds.rs @@ -0,0 +1,64 @@ +pub trait Trait { + type Assoc; +} + +fn f + Trait>() { + 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 + Second>() { + let _: Assoc = todo!(); //~ ERROR cannot find type `Assoc` in this scope +} + +pub trait Gat { + type Assoc<'a>; +} + +fn i() { + let _: Assoc = todo!(); //~ ERROR cannot find type `Assoc` in this scope +} + +fn j() { + struct Local; + impl Local { + fn method() { + let _: Assoc = todo!(); //~ ERROR cannot find type `Assoc` in this scope + } + } + + let _ = std::marker::PhantomData::; +} + +pub struct S; +impl S { + fn method() { + fn inner() { + let _: Assoc = todo!(); //~ ERROR cannot find type `Assoc` in this scope + } + inner(); + } +} + +fn main() {} diff --git a/tests/ui/associated-types/suggest-assoc-type-from-bounds.stderr b/tests/ui/associated-types/suggest-assoc-type-from-bounds.stderr new file mode 100644 index 0000000000000..b5ce2d91ca4d2 --- /dev/null +++ b/tests/ui/associated-types/suggest-assoc-type-from-bounds.stderr @@ -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 _: >::Assoc = todo!(); + | +++++++++++++++++++ +LL | let _: >::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 _: >::A = todo!(); + | ++++++++++++++++++ +LL | let _: >::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 _: ::Assoc = todo!(); + | ++++++++++++++ +LL | let _: ::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`. diff --git a/tests/ui/typeck/sugg-swap-equality-in-macro-issue-139050.stderr b/tests/ui/typeck/sugg-swap-equality-in-macro-issue-139050.stderr index d74372c665a45..c217672b0050d 100644 --- a/tests/ui/typeck/sugg-swap-equality-in-macro-issue-139050.stderr +++ b/tests/ui/typeck/sugg-swap-equality-in-macro-issue-139050.stderr @@ -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