-
Notifications
You must be signed in to change notification settings - Fork 1.7k
New lint: ambiguous_method_names
#11528
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
Conversation
Thanks for the pull request, and welcome! The Rust team is excited to review your changes, and you should hear from @Jarcho (or someone else) soon. Please see the contribution instructions for more information. Namely, in order to ensure the minimum review times lag, PR authors and assigned reviewers should ensure that the review label (
|
There are merge commits (commits with multiple parents) in your changes. We have a no merge policy so these commits will need to be removed for this pull request to be merged. You can start a rebase with the following commands:
The following commits are merge commits: |
ambiguous_method_calls
ambiguous_method_calls
de383da
to
3ade53b
Compare
} | ||
|
||
fn trait_methods() -> &'static Mutex<FxHashMap<(String, Symbol), SpanData>> { | ||
static NAMES: OnceLock<Mutex<FxHashMap<(String, Symbol), SpanData>>> = OnceLock::new(); |
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.
Is there a reason for using static
s instead of storing the map directly in the AmbiguousMethodCalls
struct? The lint structs can have fields and take &mut self
in their check methods so its state can be accessed and mutated at any point, which means the Mutex
could go too.
A problem with the current approach is that it depends on the order in which it visits the items. For example, if a Base.ambiguous();
expression appears before an ambiguous method implementation, I think it wouldn't lint on it because clippy hasn't seen it yet.
I wonder if maintaining these name maps is necessary though. Rustc already has its own maps for trait/inherent impls per crate for its analysises and typechecking, and they can be accessed through the TyCtxt
.
For example, there's the all_local_trait_impls
query that returns all trait impls for a given type's DefId
. Maybe when visiting a method in an inherent impl, this could be used to look for traits that have methods with the same name.
I'm not sure if there's a better query to use for this... A problem that I can see here is that it probably wouldn't lint when calling ambiguous unqualified trait methods from foreign crates.
And for linting method call expressions, it could perhaps use all_local_trait_impls
to get all trait impls + inherent_impls
for the normal impls, then check if there's duplicate entries between the two.
But maybe having these two maps is faster over having to go through the impls and compare a bunch of Symbol
s that it justifies having them.
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.
Thanks for the suggestions!
I moved the maps to be stored directly in the AmbiguousMethodCalls
struct per your suggestions--I actually didn't know the lint structs could have fields, but it makes sense in retrospect.
For the visiting order problem, I decided to defer checking ambiguity to check_crate_post
, so linting happens only after visiting all implementations and exprs. I feel like the current method of maintaining a map of all method calls (and implementations, for that matter) could get pretty expensive for large projects/crates, though.
As for using rustc's own maps, I'm currently trying to figure something out with the all_traits
and all_local_trait_impls
queries, and iterating through methods in trait impls to find those that are ambiguous with inherent methods (i.e., same name and same parent type) with something like this in check_fn
:
let hir_id = cx.tcx.hir().local_def_id_to_hir_id(def_id);
let parent_id = cx.tcx.hir().get_parent_item(hir_id);
let parent_ty = cx.tcx.type_of(parent_id.to_def_id()).skip_binder();
let is_inherent_impl = !is_trait_impl_item(cx, hir_id);
if is_inherent_impl {
for tr in cx.tcx.all_traits() {
if implements_trait(cx, parent_ty, tr, &[]) {
if let Some(impl_blocks) = cx.tcx.all_local_trait_impls(()).get(&tr) {
for block in impl_blocks {
if let Some(hir::Node::Item(item)) =
cx.tcx.hir().find(cx.tcx.hir().local_def_id_to_hir_id(*block))
{
if let hir::ItemKind::Impl(impl_struct) = item.kind {
for item in impl_struct.items {
let item_hir_id =
cx.tcx.hir().local_def_id_to_hir_id(item.id.owner_id.def_id);
let item_parent_id = cx.tcx.hir().get_parent_item(item_hir_id);
let item_parent_ty = cx.tcx.type_of(item_parent_id.to_def_id()).skip_binder();
if item.ident.name == ident.name && parent_ty == item_parent_ty{
span_lint(cx, AMBIGUOUS_METHOD_CALLS, item.span, "trait impl");
span_lint(cx, AMBIGUOUS_METHOD_CALLS, span, "struct impl");
}
}
}
}
}
}
}
}
}
It's still very rough, and I haven't gotten to using this approach with linting method calls yet. I'll try experimenting some more with this approach over the next few days--I definitely agree that the maps shouldn't be maintained unless needed.
A problem that I can see here is that it probably wouldn't lint when calling ambiguous unqualified trait methods from foreign crates.
I think both the current approach and the rustc-based approach fall short in this regard, unfortunately.
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.
so linting happens only after visiting all implementations and exprs.
Yep that's a neat way to solve it, FWIW some other lints follow the same idea. For example unused_async
doesn't lint immediately once it sees an async function without .await
exprs, but instead stores the relevant stuff for later + delays linting until check_crate_post
so that we can be sure there aren't any path exprs to an async function in a context that requires a fn() -> impl Future
.
If going with manual maps, there's one subtle but important thing: the iteration order of a HashMap is not stable, so the order in which it then emits lints can "randomly" change. That makes it annoying for e.g. ui tests where it checks for the exact stderr literally and a different order could then make the tests fail.
I ran into the exact same problem before ^^ see the relevant comment by Jarcho here #11200 (comment)
The inherent_methods
field could just be a Vec
, right? Seems like it's only inserted into and iterated over.
As for using rustc's own maps, I'm currently trying to figure something out with the all_traits and all_local_trait_impls queries, and iterating through methods in trait impls to find those that are ambiguous with inherent methods (i.e., same name and same parent type) with something like this in check_fn
Oh huh I was under the impression that the key of all_local_trait_impls
's map was the DefId of the type and not the trait, which would have been way more convenient (wouldn't require going through all of the traits). Definitely changes the way I think about this now. All of the trait impl queries on the tyctxt seem to search by a trait id (unless I missed something), which might make this more complicated then..
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.
The inherent_methods field could just be a
Vec
, right? Seems like it's only inserted into and iterated over.
Yeah, I think I was initially worried about getting the correct spans for each inherent impl (hence the self.inherent_methods.get(k)
), but now that I think about it, this could also be accomplished using something like a Vec<(String, Symbol, Span)>
instead of a FxHashMap
as you suggested--I believe this also solves the random ordering problem.
I was under the impression that the key of
all_local_trait_impls
's map was the DefId of the type and not the trait
That tripped me up as well when I was working on the rustc-based approach. I think this fact would also make linting call sites a lot more difficult, though I haven’t gotten around to writing that yet—do you think it’s still worth pursuing, or should we just stick with manual maps? I’m personally more in favor of manual maps—I feel like querying all_local_trait_impls
, all_traits
, and inherent_impls
all at the same time when linting method calls could get complicated pretty quickly.
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.
Two inherent methods can share the same name assuming the impl blocks have different generic arguments. e.g.
struct S<T>(T);
impl S<i32> { fn f(self) {} }
impl S<u32> { fn f(self) {} }
If you aren't tracking the call sites, then I don't think you need to do anything to account for this, but I would have a test in case future changes need to do something special here.
if let hir::ExprKind::MethodCall(path, recv, _, call_span) = &expr.kind { | ||
let recv_ty = cx.typeck_results().expr_ty(recv).peel_refs(); | ||
|
||
self.insert_call_site(recv_ty, path.ident, *call_span); | ||
} |
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.
This tracks way more than needed. You can check if the target method is an inherent method of a locally defined type before tracking it.
You'll also need to check if the method is named directly as a path.
fn check_fn( | ||
&mut self, | ||
cx: &LateContext<'tcx>, | ||
kind: FnKind<'tcx>, | ||
_: &'tcx FnDecl<'_>, | ||
_: &'tcx Body<'_>, | ||
_: Span, | ||
def_id: LocalDefId, | ||
) { |
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.
Looks like this would be better split between check_impl_item
and check_item
. check_impl_item
would pick up inherent methods, check_item
would pick up trait impls.
Note for trait methods you'll always have to look at the trait definition to find default methods since they don't necessarily appear in the impl block.
span_lint( | ||
cx, | ||
AMBIGUOUS_METHOD_CALLS, | ||
*self.trait_methods.get(k).unwrap(), | ||
"ambiguous trait method name", | ||
); | ||
span_lint_and_help( | ||
cx, | ||
AMBIGUOUS_METHOD_CALLS, | ||
*span, | ||
"ambiguous struct method name", | ||
None, | ||
"consider renaming the struct impl's method", | ||
); | ||
|
||
if let Some(spans) = self.call_sites.get(k) { | ||
for span in spans { | ||
span_lint_and_help( | ||
cx, | ||
AMBIGUOUS_METHOD_CALLS, | ||
*span, | ||
"ambiguous method call", | ||
None, | ||
"consider renaming the struct impl's method or explicitly qualifying the call site", | ||
); | ||
} | ||
} |
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.
This should be a single lint call. I would lint on the inherent method since that's the most likely one to rename. The trait can be trait method(s) can be called out in a note. The call sites are probably not worth mentioning due to the amount of unhelpful noise that it can generate.
Thanks for all the feedback! Regarding structs with generic arguments and method call sites, I decided to remove call site linting as you suggested, so I don't think that'll be a concern for now. I've added a few test cases regardless. I've also split up the code in |
ambiguous_method_calls
ambiguous_method_names
Disregard the part about the ICE, actually--I was able to find a fix 🙂 . I also added some tests to check for the ICE. |
☔ The latest upstream changes (presumably #11556) made this pull request unmergeable. Please resolve the merge conflicts. |
FYI there is a similar lint |
Huh, I wasn't aware. If y'all think it'd be best to close this PR as a duplicate, I'm perfectly fine doing so. |
@lengyijun Thanks for pointing that out. That is exactly the same thing. @sjwang05 Sorry you got this far into the lint before this came up. That's a failure on our part for not triaging the issue properly. |
Closes #11499
This lint checks for methods in trait impls and struct impls with the same name. See lint description for more info and examples.
changelog:
new lint: [
ambiguous_method_calls
]