-
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
TraitIsVisiblemodule 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 |
| exists(UseItemNode use | | ||
| use = this.getASuccessor(_, _) and | ||
| result = use.(ItemNode).getASuccessor(name, kind) | ||
| result = use.getASuccessor(name, kind) |
Copilot
AI
Sep 2, 2025
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.
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) |
Copilot
AI
Sep 2, 2025
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.
| // 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 = "_" |
Copilot
AI
Sep 2, 2025
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.
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
.
| 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 |
Copilot
AI
Sep 2, 2025
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.
| */ | ||
| 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::methodwe only considermethodmethods on traits that are visible. Similarly, for method callsfoo.bar(...)we only considerbarmethods 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:
multipleCallTargetsviolations.databendwe 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.