Rust: Take trait visibility into account when resolving paths and methods#20321
Rust: Take trait visibility into account when resolving paths and methods#20321paldepind merged 2 commits intogithub:mainfrom
Conversation
fd51708 to
135ebce
Compare
135ebce to
322ef4d
Compare
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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.
| kind.isExternalOrBoth() and | ||
| result instanceof AssocItemNode | ||
| ) | ||
| typeImplEdge(this, _, name, kind, result) |
There was a problem hiding this comment.
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 = "_" |
There was a problem hiding this comment.
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.
| 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.
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.
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.
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.
The pragma here is to prevent resolveTraitTy from being joined with traitIsVisible on the trait column.
hvitved
left a comment
There was a problem hiding this comment.
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.