fix(rust): a call through a trait-bounded generic, like Key::get(…) with Key: MapKey, reaches the bound trait's method (#2724)

A Rust call through a generic type parameter is a call of the method its
bounds bring in: protobuf's `Key::get(…)` in `fn map_get<Key: MapKey>`,
with `trait MapKey: … + FfiMapKey`, is `FfiMapKey::get`. No strategy
read the bounds for such a path. These calls linked only when a
qualified name happened to end with the written text, which the scope
boundary of the previous fix rightly stopped, and a parameter bounded
only by an outside trait (`T::default()` under `T: Default`) could reach
a project type's unrelated `default` by name.

matchRustGenericPath, right after the `Self::x` branch of
matchMethodCall, reads the bounds the function and the impl or trait
around the call put on the parameter (in `<…>` or a `where` clause) and
resolves the method among those traits and the traits they extend,
through the helpers typed receivers already use. Outside-only bounds,
markers, and two traits declaring the method give no edge. The ref is
recorded as typed, so a trait method's missing `pub` doesn't hide it
from another module.

Measured on main ac9f96f8: protobuf +47, tokio +43, axum +14, clap +10,
serde +7, nothing removed; ripgrep and eight controls in other languages
are byte-identical.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-10-21 02:25:32 +00:00
committed by GitHub
co-authored by Claude Opus 5.5
parent ac9f96f8a3
commit f6d8f19b29
3 changed files with 208 additions and 0 deletions
+1
View File
@@ -232,6 +232,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
- In Swift, a call written after `!` or another prefix operator, or chained after `try` or `await`, now links to what it calls. `if !isReady()` links to `isReady`, `!store.contains(id)` and `try! JSONDecoder().decode(…)` link to the method of the type they're called on rather than to whichever `contains` or `decode` came first, and `!Settings.Defaults.isOn()` lands on the type it names. Re-index Swift projects after upgrading. - In Swift, a call written after `!` or another prefix operator, or chained after `try` or `await`, now links to what it calls. `if !isReady()` links to `isReady`, `!store.contains(id)` and `try! JSONDecoder().decode(…)` link to the method of the type they're called on rather than to whichever `contains` or `decode` came first, and `!Settings.Defaults.isOn()` lands on the type it names. Re-index Swift projects after upgrading.
- In Swift, a call on a local created through `try`, `try!`, `try?` or `await`, or with a generic type's constructor, now links to that type's method: after `let storage = try! DiskStorage.Backend<String>(config: config)`, `storage.store(…)` links to `Backend`'s `store`. Before, such a local had no known type, so the call was left unlinked or matched by name to another type's method. Re-index Swift projects after upgrading. - In Swift, a call on a local created through `try`, `try!`, `try?` or `await`, or with a generic type's constructor, now links to that type's method: after `let storage = try! DiskStorage.Backend<String>(config: config)`, `storage.store(…)` links to `Backend`'s `store`. Before, such a local had no known type, so the call was left unlinked or matched by name to another type's method. Re-index Swift projects after upgrading.
- In Rust, a call written `Self::new(…)`, `Self::from_std(…)` or `Self::Text(…)` now links to that function or variant of the type whose `impl` it is written in, even when other types have one of the same name. Before, such a call was linked only when nothing else in the project shared the name, so most `Self::new(…)` constructors and many helper calls were missing from callers and impact. Re-index Rust projects after upgrading. - In Rust, a call written `Self::new(…)`, `Self::from_std(…)` or `Self::Text(…)` now links to that function or variant of the type whose `impl` it is written in, even when other types have one of the same name. Before, such a call was linked only when nothing else in the project shared the name, so most `Self::new(…)` constructors and many helper calls were missing from callers and impact. Re-index Rust projects after upgrading.
- In Rust, a call through a generic type parameter, like `Key::get(…)` in protocolbuffers/protobuf's `fn map_get<Key: MapKey>`, now links to the method the trait its bound names declares, or a trait that trait extends: `trait MapKey: … + FfiMapKey` makes it `FfiMapKey::get`, whether the bound is written in `<…>`, in a `where` clause or on the `impl`. A parameter bounded only by traits from outside your project, like `T::default()` under `T: Default`, no longer links to one of your types' unrelated `default` functions, and two bound traits that both declare the method link neither. Re-index Rust projects after upgrading.
- In Rust, a call like `String::from("…")`, `Vec::from([…])` or `NonNull::from(x)` no longer links to your project's own `impl From<YourType> for String` (or for `Vec`, `NonNull`, …) unless what it passes is a `YourType`. A method call on a standard-library value, like `files.iter()` or `buf.extend(…)`, links to your trait's implementation for that type only where the trait is imported. Before, every `String::from("…")` call in dioxus's CLI linked to one such conversion, and tokio's `vec.iter()` calls to an internal trait's `Vec` impl. Re-index Rust projects after upgrading. - In Rust, a call like `String::from("…")`, `Vec::from([…])` or `NonNull::from(x)` no longer links to your project's own `impl From<YourType> for String` (or for `Vec`, `NonNull`, …) unless what it passes is a `YourType`. A method call on a standard-library value, like `files.iter()` or `buf.extend(…)`, links to your trait's implementation for that type only where the trait is imported. Before, every `String::from("…")` call in dioxus's CLI linked to one such conversion, and tokio's `vec.iter()` calls to an internal trait's `Vec` impl. Re-index Rust projects after upgrading.
- In Rust, a method called through `self: Pin<&mut Self>`, `self: Box<Self>`, `self: Rc<Self>` or `self: Arc<Self>`, or on a value declared `Pin<&mut …>`, now links the way Rust looks it up: to the type's own method, like `self.poll_flush(cx)` inside tokio's `impl AsyncWrite for File`, rather than to a catch-all `impl AsyncWrite for Pin<P>`, while `Pin`'s own methods, like `self.get_mut()`, stay unlinked. A type's own method now also comes before a trait's of the same name, so tokio's `Arc::from_raw(…)` calls are the standard library's rather than its internal `Link` trait's, and a method called on a tuple, like serde's `(self.ip(), self.port()).serialize(…)`, no longer links to an unrelated type's. Before, most calls made through such a `self` linked to nothing. Re-index Rust projects after upgrading. - In Rust, a method called through `self: Pin<&mut Self>`, `self: Box<Self>`, `self: Rc<Self>` or `self: Arc<Self>`, or on a value declared `Pin<&mut …>`, now links the way Rust looks it up: to the type's own method, like `self.poll_flush(cx)` inside tokio's `impl AsyncWrite for File`, rather than to a catch-all `impl AsyncWrite for Pin<P>`, while `Pin`'s own methods, like `self.get_mut()`, stay unlinked. A type's own method now also comes before a trait's of the same name, so tokio's `Arc::from_raw(…)` calls are the standard library's rather than its internal `Link` trait's, and a method called on a tuple, like serde's `(self.ip(), self.port()).serialize(…)`, no longer links to an unrelated type's. Before, most calls made through such a `self` linked to nothing. Re-index Rust projects after upgrading.
- In Rust, a method called on what `.as_mut()` or `.as_ref()` hands back now links by the type its receiver is declared with: `self.delay.as_mut().reset(…)` on a `Pin<Box<Sleep>>` field reaches `Sleep::reset`, a field pin-project hands out reaches its own type's method, and a `Box` or `NonNull` lends what it holds. A method on an `Option` reached this way, like `self.child.as_mut().map(…)`, or on a generic type no longer links to an unrelated project method that shares its name. Before, these calls were matched by name alone, so tokio's `self.delay.as_mut().reset(…)` linked to the calling `Interval::reset`, and dioxus's `Option` calls to its own `map` and `filter` helpers. Re-index Rust projects after upgrading. - In Rust, a method called on what `.as_mut()` or `.as_ref()` hands back now links by the type its receiver is declared with: `self.delay.as_mut().reset(…)` on a `Pin<Box<Sleep>>` field reaches `Sleep::reset`, a field pin-project hands out reaches its own type's method, and a `Box` or `NonNull` lends what it holds. A method on an `Option` reached this way, like `self.child.as_mut().map(…)`, or on a generic type no longer links to an unrelated project method that shares its name. Before, these calls were matched by name alone, so tokio's `self.delay.as_mut().reset(…)` linked to the calling `Interval::reset`, and dioxus's `Option` calls to its own `map` and `filter` helpers. Re-index Rust projects after upgrading.
+173
View File
@@ -0,0 +1,173 @@
/**
* A Rust call through a generic type parameter — `Key::get(…)` in
* protocolbuffers/protobuf's `fn map_get<Key: MapKey>` — is to the method the
* traits its bounds name declare, or the traits they extend: `trait MapKey:
* … + FfiMapKey` makes it `FfiMapKey::get`.
*
* Before, such a call linked only when a declaration's qualified name happened
* to end with the written text (`FfiMapKey::get` ends with `Key::get`), which
* #2723 stopped; a bound from outside the project (`T: Default`) let a guess by
* the method's name take a project type's unrelated `default`.
*/
import { describe, it, expect, afterEach } from 'vitest';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { CodeGraph } from '../src';
import type { Node } from '../src/types';
const roots: string[] = [];
afterEach(() => {
for (const root of roots.splice(0)) fs.rmSync(root, { recursive: true, force: true });
});
async function indexed(files: Record<string, string>): Promise<CodeGraph> {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-rust-bound-paths-'));
roots.push(root);
for (const [rel, content] of Object.entries(files)) {
fs.mkdirSync(path.dirname(path.join(root, rel)), { recursive: true });
fs.writeFileSync(path.join(root, rel), content);
}
return CodeGraph.init(root, { index: true });
}
function callable(cg: CodeGraph, qualifiedName: string): Node {
const node = [...cg.getNodesByKind('method'), ...cg.getNodesByKind('function')].find((n) => n.qualifiedName === qualifiedName);
if (!node) throw new Error(`no function or method ${qualifiedName}`);
return node;
}
/** `calls` callees of a function or method, as `qualifiedName`. */
function calls(cg: CodeGraph, caller: string): string[] {
return cg
.getCallees(callable(cg, caller).id)
.filter((r) => r.edge.kind === 'calls')
.map((r) => r.node.qualifiedName)
.sort();
}
const lines = (...l: string[]): string => [...l, ''].join('\n');
const CARGO = lines('[package]', 'name = "pb"', 'version = "0.1.0"', 'edition = "2021"');
/** protobuf's map key traits: the FFI methods live on a supertrait of the bound. */
const KEYS = lines(
'pub struct RawMap;',
'',
'pub trait FfiMapKey: Sized {',
' unsafe fn get(map: RawMap, key: Self, value: *mut u8) -> bool;',
' unsafe fn insert(map: RawMap, key: Self, value: u8) -> bool;',
'}',
'',
'pub trait MapKey: Clone + FfiMapKey {}',
);
describe('a Rust call through a generic parameter reaches its bound trait\'s method', () => {
it('reproduction: `Key::get(…)` in `fn map_get<Key: MapKey>` is FfiMapKey::get, through the supertrait', async () => {
const cg = await indexed({
'Cargo.toml': CARGO,
'src/lib.rs': lines('pub mod keys;', 'pub mod map;'),
'src/keys.rs': KEYS,
'src/map.rs': lines(
'use crate::keys::{MapKey, RawMap};',
'',
'pub struct Map;',
'',
'impl Map {',
' pub fn map_get<Key: MapKey>(key: Key) -> bool {',
' unsafe { Key::get(RawMap, key, std::ptr::null_mut()) }',
' }',
'}',
),
});
try {
expect(calls(cg, 'Map::map_get')).toEqual(['FfiMapKey::get']);
} finally {
cg.close();
}
});
it('a bound in a `where` clause, and one on the `impl`, count the same', async () => {
const cg = await indexed({
'Cargo.toml': CARGO,
'src/lib.rs': lines('pub mod keys;', 'pub mod map;'),
'src/keys.rs': KEYS,
'src/map.rs': lines(
'use crate::keys::{MapKey, RawMap};',
'',
'pub struct Map;',
'',
'impl Map {',
' pub fn map_insert<Key>(key: Key) -> bool',
' where',
' Key: MapKey,',
' {',
' unsafe { Key::insert(RawMap, key, 1) }',
' }',
'}',
'',
'pub struct Typed<K>(K);',
'',
'impl<K: MapKey> Typed<K> {',
' pub fn lookup(&self) -> bool {',
' unsafe { K::get(RawMap, self.0.clone(), std::ptr::null_mut()) }',
' }',
'}',
),
});
try {
expect(calls(cg, 'Map::map_insert')).toEqual(['FfiMapKey::insert']);
expect(calls(cg, 'Typed::lookup')).toEqual(['FfiMapKey::get']);
} finally {
cg.close();
}
});
it('a bound from outside the project, `T: Default`, links no project `default`', async () => {
const cg = await indexed({
'Cargo.toml': CARGO,
'src/lib.rs': lines(
'pub struct Defaults;',
'',
'impl Defaults {',
' pub fn default() -> Defaults {',
' Defaults',
' }',
'}',
'',
'pub fn make<T: Default>() -> T {',
' T::default()',
'}',
),
});
try {
expect(calls(cg, 'make')).toEqual([]);
} finally {
cg.close();
}
});
it('two bound traits that both declare the method link neither', async () => {
const cg = await indexed({
'Cargo.toml': CARGO,
'src/lib.rs': lines(
'pub trait Engine {',
' fn run() -> u8;',
'}',
'',
'pub trait Task {',
' fn run() -> u8;',
'}',
'',
'pub fn start<T: Engine + Task>() -> u8 {',
' T::run()',
'}',
),
});
try {
expect(calls(cg, 'start')).toEqual([]);
} finally {
cg.close();
}
});
});
+34
View File
@@ -9243,6 +9243,35 @@ function matchRustSelfPath(name: string, ref: UnresolvedRef, context: Resolution
return { original: ref, targetNodeId: target.id, confidence: target.filePath === ref.filePath ? 0.9 : 0.8, resolvedBy: 'qualified-name' }; return { original: ref, targetNodeId: target.id, confidence: target.filePath === ref.filePath ? 0.9 : 0.8, resolvedBy: 'qualified-name' };
} }
/**
* A Rust call through a generic type parameter of the function or `impl`
* around it — `Key::get(…)` in protobuf's `fn map_get<Key: MapKey>`, with
* `trait MapKey: FfiMapKey` — is to the method one of the traits its bounds
* name declares, or one of the traits they extend: `FfiMapKey::get`. Bounds
* only from outside the project (`T: Default`) name no project method, nor do
* two traits that both declare it: null. Undefined when `name` is no generic
* parameter there.
*/
function matchRustGenericPath(name: string, method: string, ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | null | undefined {
const scopes = rustScopesOf(ref.filePath, context);
if (!scopes) return undefined;
const written: RustWrittenType = { text: name, file: ref.filePath, at: (scopes.lineStarts[ref.line - 1] ?? 0) + Math.max(0, ref.column) };
const bounds = rustGenericBounds(name, written, context);
if (bounds === undefined) return undefined;
const traits = bounds.filter((b) => !/^['?]/.test(b.trim()) && !RUST_MARKER_TRAITS.has(rustPathHead(b)?.name ?? ''));
if (traits.length === 0) return null;
const result = rustTraitsMethodCall(traits, written, method, ref, context);
if (result === 'unread') return null;
// (The bound names the trait, so its method is reached as a typed call is:
// its missing `pub` doesn't hide it from another module.)
if (result) {
let typed = RUST_TYPED_REFS.get(context);
if (!typed) RUST_TYPED_REFS.set(context, (typed = new WeakSet()));
typed.add(ref);
}
return result;
}
/** /**
* Whether `candidate` is declared directly in an inline `mod <name> { … }` of * Whether `candidate` is declared directly in an inline `mod <name> { … }` of
* its own file — the only way a path through `name` reaches it, since an * its own file — the only way a path through `name` reaches it, since an
@@ -18745,6 +18774,11 @@ export function matchMethodCall(
const own = matchRustSelfPath(methodName!, ref, context); const own = matchRustSelfPath(methodName!, ref, context);
if (own !== undefined) return own; if (own !== undefined) return own;
} }
// A Rust `T::x(…)` through a generic parameter: the method its bounds' traits declare (matchRustGenericPath).
if (ref.language === 'rust' && match === colonMatch && /^[A-Z]\w*$/.test(objectOrClass!) && objectOrClass !== 'Self') {
const bounded = matchRustGenericPath(objectOrClass!, methodName!, ref, context);
if (bounded !== undefined) return bounded;
}
// Strategy 1: Direct class name match (existing logic). When the receiver // Strategy 1: Direct class name match (existing logic). When the receiver
// names a class that exists in several files (`Logger.log()` / `Logger::log()` // names a class that exists in several files (`Logger.log()` / `Logger::log()`