-
Notifications
You must be signed in to change notification settings - Fork 1.8k
Rust: Take trait visibility into account when resolving paths and methods #20321
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
fd51708
to
135ebce
Compare
135ebce
to
322ef4d
Compare
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.
Pull Request Overview
This PR improves Rust trait scoping by ensuring that trait methods can only be invoked when the trait is visible or in scope. Previously, all traits were treated as globally visible, which led to incorrect path resolution and false positives.
Key changes include:
- Added trait visibility checking for method calls and qualified paths
- Implemented a
TraitIsVisible
module to determine trait scope - Added support for underscore imports (
use trait as _
) which make traits visible but unnameable
Reviewed Changes
Copilot reviewed 10 out of 11 changed files in this pull request and generated 4 comments.
Show a summary per file
File | Description |
---|---|
rust/ql/lib/codeql/rust/internal/TypeInference.qll | Updated method resolution to check trait visibility for method calls |
rust/ql/lib/codeql/rust/internal/PathResolution.qll | Added trait visibility infrastructure and path resolution improvements |
rust/ql/test/library-tests/type-inference/main.rs | Added test cases for trait visibility scenarios |
rust/ql/test/library-tests/path-resolution/main.rs | Added comprehensive trait visibility test module |
*.expected files | Updated expected test results reflecting improved path resolution |
@@ -216,7 +216,7 @@ abstract class ItemNode extends Locatable { | |||
// items made available through `use` are available to nodes that contain the `use` | |||
exists(UseItemNode use | | |||
use = this.getASuccessor(_, _) and | |||
result = use.(ItemNode).getASuccessor(name, kind) | |||
result = use.getASuccessor(name, kind) |
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 removed explicit cast use.(ItemNode)
was unnecessary since use
is already declared as UseItemNode
which extends ItemNode
. This change improves code readability by removing redundant casting.
Copilot uses AI. Check for mistakes.
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.
Correct.
kind.isExternalOrBoth() and | ||
result instanceof AssocItemNode | ||
) | ||
typeImplEdge(this, _, name, kind, result) |
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.
Refactoring the complex inline logic into the typeImplEdge
predicate improves code modularity and readability. This makes the logic reusable and easier to maintain.
Copilot uses AI. Check for mistakes.
// When the rename doesn't have a name it's an underscore import. This | ||
// makes the imported item visible but unnameable. We represent this | ||
// by using the name `_` which can never occur in a path. See also: | ||
// https://doc.rust-lang.org/reference/items/use-declarations.html#r-items.use.as-underscore | ||
not rename.hasName() and | ||
name = "_" |
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.
Excellent documentation explaining the underscore import feature. The comment clearly explains why _
is used as a special name and includes a reference to the official Rust documentation.
Copilot uses AI. Check for mistakes.
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 .
@@ -1891,7 +1891,7 @@ private predicate methodCandidate(Type type, string name, int arity, Impl impl) | |||
*/ | |||
pragma[nomagic] | |||
private predicate methodCandidateTrait(Type type, Trait trait, string name, int arity, Impl impl) { | |||
trait = resolvePath(impl.(ImplItemNode).getTraitPath()) and | |||
trait = impl.(ImplItemNode).resolveTraitTy() and |
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 change from resolvePath(impl.(ImplItemNode).getTraitPath())
to impl.(ImplItemNode).resolveTraitTy()
suggests using a more direct method for trait resolution, which likely improves performance and code clarity.
Copilot uses AI. Check for mistakes.
*/ | ||
module TraitIsVisible<relevantTraitVisibleSig/2 relevantTraitVisible> { | ||
/** Holds if the trait might be looked up in `encl`. */ | ||
private predicate traitLookup(ItemNode encl, Element element, Trait trait) { |
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 implementation of this predicate is inspired by unqualifiedPathLookup
. In my original attempt I used getADescendant*()
, but that seemed to be part of the performance issues in that PR.
// https://doc.rust-lang.org/reference/items/use-declarations.html#r-items.use.as-underscore | ||
not rename.hasName() and | ||
name = "_" | ||
) |
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.
Note, the old disjunct here did in fact not do anything. When there is a rename it's name is never _
. That is,
predicate isUnderscope(UseTree tree) { tree.getRename().getName().getText() = "_" }
is empty. Instead not rename.hasName()
holds when _
is used in the source code.
// order for the `impl` to be valid. | ||
exists(Trait trait | | ||
pragma[only_bind_into](trait) = impl.(ImplItemNode).resolveTraitTy() and | ||
TraitIsVisible<relevantTraitVisible/2>::traitIsVisible(mc, pragma[only_bind_into](trait)) |
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 pragma here is to prevent resolveTraitTy
from being joined with traitIsVisible
on the trait column.
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 great; good to have so many inconsistencies resolved.
In Rust a trait method can only be invoked if the trait is visible/in scope, but currently all traits are treated as globally visible.
With this PR, for a path
Type::method
we only considermethod
methods on traits that are visible. Similarly, for method callsfoo.bar(...)
we only considerbar
methods on traits that are visible.This PR is motived by a type explosion in Deno that is magnified by #20133. Accounting for visible traits in #20133 in the same way as in this PR should also reduce false positives for blanket implementations.
DCA shows:
multipleCallTargets
violations.databend
we loose targets for 1020 calls and path resolution inconsistencies is down 5191. One plausible interpretation is that there where 5191 calls where we found the correct target plus a wrong one and 1020 calls where we only found a wrong one.