-
Notifications
You must be signed in to change notification settings - Fork 13k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Refactor dyn-compatibility error and suggestions #133372
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -433,34 +433,19 @@ pub fn report_dyn_incompatibility<'tcx>( | |
hir::Node::Item(item) => Some(item.ident.span), | ||
_ => None, | ||
}); | ||
|
||
let mut err = struct_span_code_err!( | ||
tcx.dcx(), | ||
span, | ||
E0038, | ||
"the {} `{}` cannot be made into an object", | ||
"the {} `{}` is not dyn compatible", | ||
tcx.def_descr(trait_def_id), | ||
trait_str | ||
); | ||
err.span_label(span, format!("`{trait_str}` cannot be made into an object")); | ||
|
||
if let Some(hir_id) = hir_id | ||
&& let hir::Node::Ty(ty) = tcx.hir_node(hir_id) | ||
&& let hir::TyKind::TraitObject([trait_ref, ..], ..) = ty.kind | ||
{ | ||
let mut hir_id = hir_id; | ||
while let hir::Node::Ty(ty) = tcx.parent_hir_node(hir_id) { | ||
hir_id = ty.hir_id; | ||
} | ||
if tcx.parent_hir_node(hir_id).fn_sig().is_some() { | ||
// Do not suggest `impl Trait` when dealing with things like super-traits. | ||
err.span_suggestion_verbose( | ||
ty.span.until(trait_ref.span), | ||
"consider using an opaque type instead", | ||
"impl ", | ||
Applicability::MaybeIncorrect, | ||
); | ||
} | ||
} | ||
err.span_label(span, format!("`{trait_str}` is not dyn compatible")); | ||
|
||
attempt_dyn_to_impl_suggestion(tcx, hir_id, &mut err); | ||
|
||
let mut reported_violations = FxIndexSet::default(); | ||
let mut multi_span = vec![]; | ||
let mut messages = vec![]; | ||
|
@@ -475,7 +460,7 @@ pub fn report_dyn_incompatibility<'tcx>( | |
if reported_violations.insert(violation.clone()) { | ||
let spans = violation.spans(); | ||
let msg = if trait_span.is_none() || spans.is_empty() { | ||
format!("the trait cannot be made into an object because {}", violation.error_msg()) | ||
format!("the trait is not dyn compatible because {}", violation.error_msg()) | ||
} else { | ||
format!("...because {}", violation.error_msg()) | ||
}; | ||
|
@@ -492,24 +477,20 @@ pub fn report_dyn_incompatibility<'tcx>( | |
let has_multi_span = !multi_span.is_empty(); | ||
let mut note_span = MultiSpan::from_spans(multi_span.clone()); | ||
if let (Some(trait_span), true) = (trait_span, has_multi_span) { | ||
note_span.push_span_label(trait_span, "this trait cannot be made into an object..."); | ||
note_span.push_span_label(trait_span, "this trait is not dyn compatible..."); | ||
} | ||
for (span, msg) in iter::zip(multi_span, messages) { | ||
note_span.push_span_label(span, msg); | ||
} | ||
// FIXME(dyn_compat_renaming): Update the URL. | ||
err.span_note( | ||
note_span, | ||
"for a trait to be \"dyn-compatible\" it needs to allow building a vtable to allow the call \ | ||
to be resolvable dynamically; for more information visit \ | ||
<https://doc.rust-lang.org/reference/items/traits.html#object-safety>", | ||
"for a trait to be dyn compatible it needs to allow building a vtable\n\ | ||
for more information, visit <https://doc.rust-lang.org/reference/items/traits.html#object-safety>", | ||
Comment on lines
+488
to
+489
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Preexisting: Except for the URL I find this diagnostic note questionable at best. Clearly this note is targeted at beginners — I guess ones who blindly copied or used There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. However, I see you removed the part "to allow the call to be resolvable dynamically" which might've given the word vtable the necessary context, maybe? Idk, probably not. (For one, if we were to keep it should at least say "to allow method calls to be resolvable dynamically" talking about method calls generally and not about a specific one which might not even exist in the erroneous snippet (e.g., in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Perhaps we could change it to something like
WDYT?
IMO mentioning "vtable" is useful to new Rust developers who are already familiar with C/C++. For me personally, "what kinds of things can be dynamically dispatched via a vtable" is exactly my mental model for what kinds of traits are object-safe, but I agree this advice isn't necessarily good for new users coming from higher-level languages. |
||
); | ||
|
||
// Only provide the help if its a local trait, otherwise it's not actionable. | ||
if trait_span.is_some() { | ||
let mut reported_violations: Vec<_> = reported_violations.into_iter().collect(); | ||
reported_violations.sort(); | ||
|
||
let mut potential_solutions: Vec<_> = | ||
reported_violations.into_iter().map(|violation| violation.solution()).collect(); | ||
potential_solutions.sort(); | ||
|
@@ -520,68 +501,116 @@ pub fn report_dyn_incompatibility<'tcx>( | |
} | ||
} | ||
|
||
attempt_dyn_to_enum_suggestion(tcx, trait_def_id, &*trait_str, &mut err); | ||
|
||
err | ||
} | ||
|
||
/// Attempt to suggest converting the `dyn Trait` argument to an enumeration | ||
/// over the types that implement `Trait`. | ||
fn attempt_dyn_to_enum_suggestion( | ||
cramertj marked this conversation as resolved.
Show resolved
Hide resolved
|
||
tcx: TyCtxt<'_>, | ||
trait_def_id: DefId, | ||
trait_str: &str, | ||
err: &mut Diag<'_>, | ||
) { | ||
let impls_of = tcx.trait_impls_of(trait_def_id); | ||
let impls = if impls_of.blanket_impls().is_empty() { | ||
impls_of | ||
.non_blanket_impls() | ||
.values() | ||
.flatten() | ||
.filter(|def_id| { | ||
!matches!(tcx.type_of(*def_id).instantiate_identity().kind(), ty::Dynamic(..)) | ||
}) | ||
.collect::<Vec<_>>() | ||
} else { | ||
vec![] | ||
}; | ||
let externally_visible = if !impls.is_empty() | ||
&& let Some(def_id) = trait_def_id.as_local() | ||
|
||
if !impls_of.blanket_impls().is_empty() { | ||
return; | ||
} | ||
|
||
let concrete_impls: Option<Vec<Ty<'_>>> = impls_of | ||
.non_blanket_impls() | ||
.values() | ||
.flatten() | ||
.map(|impl_id| { | ||
// Don't suggest conversion to enum if the impl types have type parameters. | ||
// It's unlikely the user wants to define a generic enum. | ||
let Some(impl_type) = tcx.type_of(*impl_id).no_bound_vars() else { return None }; | ||
cramertj marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
// Obviously unsized impl types won't be usable in an enum. | ||
// Note: this doesn't use `Ty::is_trivially_sized` because that function | ||
// defaults to assuming that things are *not* sized, whereas we want to | ||
// fall back to assuming that things may be sized. | ||
match impl_type.kind() { | ||
ty::Str | ty::Slice(_) | ty::Dynamic(_, _, ty::DynKind::Dyn) => { | ||
return None; | ||
} | ||
_ => {} | ||
} | ||
Some(impl_type) | ||
}) | ||
.collect(); | ||
let Some(concrete_impls) = concrete_impls else { return }; | ||
|
||
const MAX_IMPLS_TO_SUGGEST_CONVERTING_TO_ENUM: usize = 9; | ||
if concrete_impls.is_empty() || concrete_impls.len() > MAX_IMPLS_TO_SUGGEST_CONVERTING_TO_ENUM { | ||
return; | ||
} | ||
|
||
let externally_visible = if let Some(def_id) = trait_def_id.as_local() { | ||
// We may be executing this during typeck, which would result in cycle | ||
// if we used effective_visibilities query, which looks into opaque types | ||
// (and therefore calls typeck). | ||
&& tcx.resolutions(()).effective_visibilities.is_exported(def_id) | ||
{ | ||
true | ||
tcx.resolutions(()).effective_visibilities.is_exported(def_id) | ||
} else { | ||
false | ||
}; | ||
match &impls[..] { | ||
[] => {} | ||
_ if impls.len() > 9 => {} | ||
[only] if externally_visible => { | ||
err.help(with_no_trimmed_paths!(format!( | ||
"only type `{}` is seen to implement the trait in this crate, consider using it \ | ||
directly instead", | ||
tcx.type_of(*only).instantiate_identity(), | ||
))); | ||
} | ||
[only] => { | ||
err.help(with_no_trimmed_paths!(format!( | ||
"only type `{}` implements the trait, consider using it directly instead", | ||
tcx.type_of(*only).instantiate_identity(), | ||
))); | ||
} | ||
impls => { | ||
let types = impls | ||
.iter() | ||
.map(|t| { | ||
with_no_trimmed_paths!(format!(" {}", tcx.type_of(*t).instantiate_identity(),)) | ||
}) | ||
.collect::<Vec<_>>(); | ||
err.help(format!( | ||
"the following types implement the trait, consider defining an enum where each \ | ||
variant holds one of these types, implementing `{}` for this new enum and using \ | ||
it instead:\n{}", | ||
trait_str, | ||
types.join("\n"), | ||
)); | ||
} | ||
|
||
if let [only_impl] = &concrete_impls[..] { | ||
let within = if externally_visible { " within this crate" } else { "" }; | ||
err.help(with_no_trimmed_paths!(format!( | ||
"only type `{only_impl}` implements `{trait_str}`{within}; \ | ||
consider using it directly instead." | ||
))); | ||
} else { | ||
let types = concrete_impls | ||
.iter() | ||
.map(|t| with_no_trimmed_paths!(format!(" {}", t))) | ||
.collect::<Vec<String>>() | ||
.join("\n"); | ||
|
||
err.help(format!( | ||
"the following types implement `{trait_str}`:\n\ | ||
{types}\n\ | ||
consider defining an enum where each variant holds one of these types,\n\ | ||
implementing `{trait_str}` for this new enum and using it instead", | ||
)); | ||
} | ||
|
||
if externally_visible { | ||
err.note(format!( | ||
"`{trait_str}` can be implemented in other crates; if you want to support your users \ | ||
"`{trait_str}` may be implemented in other crates; if you want to support your users \ | ||
passing their own types here, you can't refer to a specific type", | ||
)); | ||
} | ||
} | ||
|
||
err | ||
/// Attempt to suggest that a `dyn Trait` argument or return type be converted | ||
/// to use `impl Trait`. | ||
fn attempt_dyn_to_impl_suggestion(tcx: TyCtxt<'_>, hir_id: Option<hir::HirId>, err: &mut Diag<'_>) { | ||
cramertj marked this conversation as resolved.
Show resolved
Hide resolved
|
||
let Some(hir_id) = hir_id else { return }; | ||
let hir::Node::Ty(ty) = tcx.hir_node(hir_id) else { return }; | ||
let hir::TyKind::TraitObject([trait_ref, ..], ..) = ty.kind else { return }; | ||
|
||
// Only suggest converting `dyn` to `impl` if we're in a function signature. | ||
// This ensures that we don't suggest converting e.g. | ||
// `type Alias = Box<dyn DynIncompatibleTrait>;` to | ||
// `type Alias = Box<impl DynIncompatibleTrait>;` | ||
let Some((_id, first_non_type_parent_node)) = | ||
tcx.hir().parent_iter(hir_id).find(|(_id, node)| !matches!(node, hir::Node::Ty(_))) | ||
else { | ||
return; | ||
}; | ||
if first_non_type_parent_node.fn_sig().is_none() { | ||
return; | ||
} | ||
|
||
err.span_suggestion_verbose( | ||
ty.span.until(trait_ref.span), | ||
"consider using an opaque type instead", | ||
"impl ", | ||
Applicability::MaybeIncorrect, | ||
); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
E0038 now no longer mentions the term trait object anywhere which I feel is a bit too extreme of a change? The original criticism which lead to the change in terminology was about the terms object (contrary to the spelled out trait object) and object safe(-ty). We haven't abolished trait object as far as I'm aware.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree that trait object could be useful terminology to introduce here and could give the user something to search for, but I think "dyn compatible" accomplishes a similar goal. I'm not sure where I'd add a mention of trait object here without introducing additional verbosity (this error message is already quite large and risks being noisy, IMO).
Note: the current error message actually doesn't refer to a trait object either:
It says "the trait ... cannot be made into an object", rather than "cannot be made into a trait object".
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps it would be more clear in any case to say:
Such verbiage would match the theory on which we adopted "dyn compatible". That is, traits that can be used with
dyn
are dyn compatible. So if a trait is not dyn compatible, then we'd say it cannot be used withdyn
.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I vaguely prefer including the phrase "dyn compatible" because it gives the user something to search for, but I don't feel strongly about it.