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:
commit
b80512462f
7 changed files with 431 additions and 9 deletions
|
|
@ -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 ¶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::<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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 ¶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<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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
64
tests/ui/associated-types/suggest-assoc-type-from-bounds.rs
Normal file
64
tests/ui/associated-types/suggest-assoc-type-from-bounds.rs
Normal 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() {}
|
||||
|
|
@ -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`.
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue