Skip to content

Conversation

paldepind
Copy link
Contributor

@paldepind paldepind commented Jul 28, 2025

This PR implements method resolution for blanket implementations that satisfies the criteria: the type parameter that the implementation targets has a trait bound.

For instance, this impl block

impl<R: AsyncRead> AsyncReadExt for R {}

is handled as there is a trait bound AsyncRead on the type parameter R.

On the other hand, this impl block

impl<T, U> Into<U> for T where U: From<T> {

is not handled since there is not trait bound for T.

At a high level, the implementation works as follows:

  1. For a method call foo.bar check if there's a blanket implementation that has the method bar.
  2. For a trait bound on the blanket implementation's type parameter, check if foo satisfies the trait bound.
  3. Given the above, resolve foo.bar to the method from the blanket implementation.

If there are multiple trait bounds on the type parameter, then only one is checked to be satisfied. This could give false results, but from looking at some results in doesn't look too bad after filtering away some trivial traits (Clone, Send, etc.).

The DCA report seems great. A 0.819% point increase in resolved calls with little to no change to performance. Some inconsistencies are up, but a good chunk of that is probably explained by more type information in general.

@github-actions github-actions bot added the Rust Pull requests that update Rust code label Jul 28, 2025
@paldepind paldepind force-pushed the rust/type-inference-blanket-impl branch from 983257d to 2e98c24 Compare August 2, 2025 13:10
@paldepind paldepind force-pushed the rust/type-inference-blanket-impl branch from 2e98c24 to f130198 Compare August 3, 2025 08:37
@paldepind paldepind force-pushed the rust/type-inference-blanket-impl branch from 0314813 to 8c91ef0 Compare September 10, 2025 08:23
@paldepind paldepind marked this pull request as ready for review September 10, 2025 09:41
@paldepind paldepind requested a review from a team as a code owner September 10, 2025 09:41
@Copilot Copilot AI review requested due to automatic review settings September 10, 2025 09:41
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 implements method resolution for blanket implementations in Rust type inference, specifically targeting blanket implementations where the type parameter has trait bounds. The implementation enables CodeQL to better resolve method calls that depend on blanket trait implementations.

Key changes:

  • Added support for blanket implementation method resolution with trait bounds
  • Enhanced type inference to handle universally quantified type parameters in constraints
  • Updated test cases to reflect improved method resolution capabilities

Reviewed Changes

Copilot reviewed 15 out of 15 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
shared/typeinference/codeql/typeinference/internal/TypeInference.qll Added useUniversalConditions predicate to control constraint satisfaction for universally quantified type parameters
rust/ql/lib/codeql/rust/internal/TypeInference.qll Core implementation of blanket implementation support including detection, canonicalization, and method resolution
rust/ql/lib/codeql/rust/internal/PathResolution.qll Added resolveBound method to resolve type parameter bounds by index
rust/ql/test/library-tests/type-inference/blanket_impl.rs New test file demonstrating blanket implementation scenarios
Multiple test expected files Updated to reflect improved method resolution results from blanket implementation support

Comment on lines +2217 to +2209
not trait.getName().getText() =
[
"Sized", "Clone",
// The auto traits
"Send", "Sync", "Unpin", "UnwindSafe", "RefUnwindSafe"
]
Copy link
Preview

Copilot AI Sep 10, 2025

Choose a reason for hiding this comment

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

The hardcoded list of excluded trait names should be extracted to a constant or configuration to improve maintainability and make it easier to extend.

Copilot uses AI. Check for mistakes.

* We detect these duplicates based on some simple heuristics (same trait
* name, file name, etc.). For these duplicates we select the one with the
* greatest file name (which usually is also the one with the greatest library
* version in the path)
Copy link
Preview

Copilot AI Sep 10, 2025

Choose a reason for hiding this comment

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

The heuristic for selecting the 'canonical' implementation based on greatest file name seems fragile and may not always correspond to the greatest library version. Consider documenting the limitations of this approach or exploring more robust versioning detection.

Suggested change
* version in the path)
* version in the path). **Note:** This heuristic is fragile and may not always
* correspond to the greatest library version, as file names may not reliably
* encode version information and lexicographical order does not match semantic
* versioning. Users should be aware of this limitation and consider more robust
* version detection if precise canonicalization is required.

Copilot uses AI. Check for mistakes.

@paldepind paldepind added the no-change-note-required This PR does not need a change note label Sep 10, 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.

Nice work!

Comment on lines 115 to 116
// Here `TryFlagExt::try_read_flag_twice` is since there is a blanket
// implementaton of `TryFlag` for `Flag`.
Copy link
Contributor

Choose a reason for hiding this comment

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

grammar

Comment on lines 2153 to 2172
private TypeParamItemNode getBlanketImplementationTypeParam(Impl impl) {
result = impl.(ImplItemNode).resolveSelfTy() and
result = impl.getGenericParamList().getAGenericParam() and
// This impl block is not superseded by the expansion of an attribute macro.
not exists(impl.getAttributeMacroExpansion())
}

predicate isBlanketImplementation(Impl impl) { exists(getBlanketImplementationTypeParam(impl)) }
Copy link
Contributor

Choose a reason for hiding this comment

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

Perhaps make these two predicates member predicates on ImplItemNode instead?

)
or
isCanonicalImpl(impl) and result = impl
}
Copy link
Contributor

Choose a reason for hiding this comment

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

How about replacing from line 2169 with

  /**
   * Gets the canonical blanket implementation for `impl`.
   *
   * Libraries can often occur several times in the database for different
   * library versions. This causes the same blanket implementations to exist
   * multiple times, and these add no useful information.
   *
   * We detect these duplicates based on some simple heuristics (same trait
   * name, file name, etc.). For these duplicates we select the one with the
   * greatest file name (which usually is also the one with the greatest library
   * version in the path)
   */
  Impl getCanonicalImpl(Impl impl) {
    exists(string fileName, string traitName, int arity, string tpName |
      impl = getPotentialDuplicated(fileName, traitName, arity, tpName)
    |
      result =
        max(Impl impl0, Location l, string absolutePath, int startLine |
          impl0 = getPotentialDuplicated(fileName, traitName, arity, tpName) and
          l = impl0.getLocation() and
          absolutePath = l.getFile().getAbsolutePath() and
          startLine = l.getStartLine()
        |
          impl0 order by absolutePath, startLine
        )
    )
  }

Copy link
Contributor Author

Choose a reason for hiding this comment

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

As mentioned offline I realized that we don't actually need getCanonicalImpl at all. It was only used in isCanonicalBlanketImplementation which was just a re-implementation of isCanonicalImpl.

Comment on lines 2209 to 2210
* Holds if `impl` is a blanket implementation for a type parameter and the type
* parameter must implement `trait`.
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: how about Holds if `impl` is a blanket implementation for a type parameter with trait bound `trait`.?

Comment on lines +2245 to +2233
not exists(impl.getAssocItem(name)) and
f = impl.resolveTraitTy().getAssocItem(name)
Copy link
Contributor

Choose a reason for hiding this comment

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

How come this case is not filtered away by the constraint below?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If we have impl<T : Bar> Foo for T then:

  • If Foo has a super trait with a default implementation, then we want to consider that default implementation as a target. That's what this case allows for.
  • If Foo and Bar has a common super trait Baz then methods on Baz are available both through Bar and Foo, and finding them through the blanket implementation doesn't add anything. That what the below filter does.
    So the filter might remove some targets added by the case, but not all of them.

Copy link
Contributor

Choose a reason for hiding this comment

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

  • If Foo has a super trait with a default implementation, then we want to consider that default implementation as a target. That's what this case allows for.

But f = impl.resolveTraitTy().getAssocItem(name) means that f is defined inside Foo and not in a super trait of Foo?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You're right, my example doesn't add up.

I think I added the filter for cases like this:

trait ApplicationImpl: ApplicationImplExt {
  fn activate(&self) { /* body */ }
}

impl<T: ApplicationImpl> ApplicationImplExt for T {}

Here we can get the activate method through the trait bound ApplicationImpl and hence it's not interesting to get it through the blanket implementation. I don't understand what purpose this blanket implementation serves in the first case, but it's from the gio library used in tauri.

* `arity`.
*/
private predicate blanketImplementationMethod(
ImplItemNode impl, Trait trait, string name, int arity, Function f
Copy link
Contributor

Choose a reason for hiding this comment

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

Perhaps rename trait to traitBound, and in predicates that call this predicate? This makes it clear that it is not the same as trait in relevantTraitVisible.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good idea. I myself got confused by that at some point ;)

Comment on lines 2255 to 2291
predicate methodCallMatchesBlanketImpl(MethodCall mc, Type t, Impl impl, Trait trait, Function f) {
// Only check method calls where we have ruled out inherent method targets.
// Ideally we would also check if non-blanket method targets have been ruled
// out.
methodCallHasNoInherentTarget(mc) and
exists(string name, int arity |
isMethodCall(mc, t, name, arity) and
blanketImplementationMethod(impl, trait, name, arity, f)
)
}

private predicate relevantTraitVisible(Element mc, Trait trait) {
exists(ImplItemNode impl |
methodCallMatchesBlanketImpl(mc, _, impl, _, _) and
trait = impl.resolveTraitTy()
)
}

module SatisfiesConstraintInput implements SatisfiesConstraintInputSig<MethodCall> {
pragma[nomagic]
predicate relevantConstraint(MethodCall mc, Type constraint) {
exists(Trait trait, Trait trait2, ImplItemNode impl |
methodCallMatchesBlanketImpl(mc, _, impl, trait, _) and
TraitIsVisible<relevantTraitVisible/2>::traitIsVisible(mc, pragma[only_bind_into](trait2)) and
trait2 = pragma[only_bind_into](impl.resolveTraitTy()) and
trait = constraint.(TraitType).getTrait()
)
}

predicate useUniversalConditions() { none() }
}

predicate hasBlanketImpl(MethodCall mc, Type t, Impl impl, Trait trait, Function f) {
SatisfiesConstraint<MethodCall, SatisfiesConstraintInput>::satisfiesConstraintType(mc,
TTrait(trait), _, _) and
methodCallMatchesBlanketImpl(mc, t, impl, trait, f)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

How about

  pragma[nomagic]
  predicate methodCallMatchesBlanketImpl(
    MethodCall mc, Type t, ImplItemNode impl, Trait traitBound, Trait traitImpl, Function f
  ) {
    // Only check method calls where we have ruled out inherent method targets.
    // Ideally we would also check if non-blanket method targets have been ruled
    // out.
    methodCallHasNoInherentTarget(mc) and
    exists(string name, int arity |
      isMethodCall(mc, t, name, arity) and
      blanketImplementationMethod(impl, traitBound, name, arity, f)
    ) and
    traitImpl = impl.resolveTraitTy()
  }

  private predicate relevantTraitVisible(Element mc, Trait trait) {
    methodCallMatchesBlanketImpl(mc, _, _, _, trait, _)
  }

  module SatisfiesConstraintInput implements SatisfiesConstraintInputSig<MethodCall> {
    pragma[nomagic]
    predicate relevantConstraint(MethodCall mc, Type constraint) {
      exists(Trait traitBound, Trait traitImpl, ImplItemNode impl |
        methodCallMatchesBlanketImpl(mc, _, impl, traitBound, traitImpl, _) and
        TraitIsVisible<relevantTraitVisible/2>::traitIsVisible(mc, traitImpl) and
        traitBound = constraint.(TraitType).getTrait()
      )
    }

    predicate useUniversalConditions() { none() }
  }

  predicate hasBlanketImpl(MethodCall mc, Type t, Impl impl, Trait traitBound, Function f) {
    SatisfiesConstraint<MethodCall, SatisfiesConstraintInput>::satisfiesConstraintType(mc,
      TTrait(traitBound), _, _) and
    methodCallMatchesBlanketImpl(mc, t, impl, traitBound, _, f)
  }

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Nice, factors out the resolveTraitTy call so it appears just once.

@paldepind paldepind force-pushed the rust/type-inference-blanket-impl branch from 0799edc to 71a5549 Compare September 12, 2025 13:34
@paldepind paldepind force-pushed the rust/type-inference-blanket-impl branch from 71a5549 to b8eb19a Compare September 12, 2025 13:54
@paldepind paldepind force-pushed the rust/type-inference-blanket-impl branch from b8eb19a to e2e6fd0 Compare September 12, 2025 14:11
@paldepind
Copy link
Contributor Author

Thanks for the review :) I should've addressed the comments now.

}

/**
* Holds if `mc` has `rootType` as the root type of the reciever and the target
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: reciever -> receiver.

Comment on lines +2245 to +2233
not exists(impl.getAssocItem(name)) and
f = impl.resolveTraitTy().getAssocItem(name)
Copy link
Contributor

Choose a reason for hiding this comment

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

  • If Foo has a super trait with a default implementation, then we want to consider that default implementation as a target. That's what this case allows for.

But f = impl.resolveTraitTy().getAssocItem(name) means that f is defined inside Foo and not in a super trait of Foo?

@paldepind paldepind merged commit 78389c8 into github:main Sep 15, 2025
42 checks passed
@paldepind paldepind deleted the rust/type-inference-blanket-impl branch September 15, 2025 10:46
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