Skip to content

Conversation

paldepind
Copy link
Contributor

@paldepind paldepind commented Aug 30, 2025

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 consider method methods on traits that are visible. Similarly, for method calls foo.bar(...) we only consider bar 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:

  • Insignificant change to analysis time. I struggled a bit with getting acceptable performance, but it seem ok now.
  • Path resolution inconsistencies are down by around 25%. I think this is explained by a reduction in multipleCallTargets violations.
  • Nodes with type at length limit is reduced by a small amount.
  • We loose call targets for 0.381% of all calls. I've inspected a bunch of lost targets, and they looked like false targets. Looking at the data, e.g., for 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.

@github-actions github-actions bot added the Rust Pull requests that update Rust code label Aug 30, 2025
@paldepind paldepind force-pushed the rust/trait-method-scope-2 branch from 135ebce to 322ef4d Compare September 2, 2025 07:01
@paldepind paldepind marked this pull request as ready for review September 2, 2025 07:02
@paldepind paldepind requested a review from a team as a code owner September 2, 2025 07:02
@Copilot Copilot AI review requested due to automatic review settings September 2, 2025 07:02
Copy link
Contributor

@Copilot Copilot AI left a 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)
Copy link
Preview

Copilot AI Sep 2, 2025

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.

Copy link
Contributor Author

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)
Copy link
Preview

Copilot AI Sep 2, 2025

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.

Comment on lines +1661 to +1666
// 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 = "_"
Copy link
Preview

Copilot AI Sep 2, 2025

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.

Copy link
Contributor Author

@paldepind paldepind Sep 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks :copilot:.

@@ -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
Copy link
Preview

Copilot AI Sep 2, 2025

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) {
Copy link
Contributor Author

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 = "_"
)
Copy link
Contributor Author

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))
Copy link
Contributor Author

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.

@paldepind paldepind added the no-change-note-required This PR does not need a change note label Sep 2, 2025
@paldepind paldepind changed the title Rust: Account for trait scope Rust: Take trait visibility into account when resolving paths and methods Sep 2, 2025
Copy link
Contributor

@hvitved hvitved left a 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.

@paldepind paldepind merged commit 0ed6428 into github:main Sep 2, 2025
21 of 22 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
no-change-note-required This PR does not need a change note Rust Pull requests that update Rust code
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants