mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-24 06:53:52 +08:00
fix(rust): a turbofish call reaches the function the turbofish wraps (#2720)
A Rust call written with a turbofish (`s.get::<u32>()`, `helper::<u8>()`, `Pool::<u8>::new()`) kept its type arguments in the callee's name (`s.get::<u32>`), which no resolution strategy reads, so every such call was left unresolved. Both engines now name the function the turbofish wraps and drop a path's type arguments (`Pool::<u8>::new` is `Pool::new`), so the call resolves as it would without the turbofish: on a typed receiver, on `self` or a field of `self`, and through an associated-function chain. The kernel and wasm extractors emit the same bytes; the parity fixture covers nested type arguments and a `->` inside them. A/B against main with fresh indexes, sites matched by position: clap 197 calls linked, dioxus 251 linked and 1 removed, axum 69, tokio 143, serde 48, ripgrep 3; gin and express are byte-identical. About 680 of the new links are right: typed method calls (clap's `matches.get_one::<String>()` to `ArgMatches::get_one`, dioxus' `self.query.new_query::<T>()` to `QueryEngine::new_query`) and generic free functions (tokio's `channel::<T>()`, dioxus' `consume_context::<T>()`, serde's `assert::<T>()`). About 22 are weak guesses at confidence 0.5 or below, where an instance chain's method becomes a bare name, as it is for the same call without a turbofish (dioxus' `.collect::<Vec<_>>()` to `ServerFunction::collect`; main already makes 23 of those for `.collect()`). The one removed link was wrong on main: `serde_json::from_str::<T>(...).context(...)` is no longer guessed as `DiffState::context`. The 54 sites reported as retargeted are positions a chain's inner and outer calls share, where an edge was added beside the existing one. Full suite on the tested tree: 8,037 passed, 0 failed. Follow-up 1 of the Rust receiver series (PR 2719's open list). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
7324faf69f
commit
bd62d9c061
@@ -245,6 +245,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
- In Rust, a variable that holds what a method call returns, like `let server = builder.port(80).build();`, `let settings = config.load()?;` or `let conn = client.connect().await;`, now has the type that method is declared to return, so calls on the variable link to that type's methods. A generic method's type comes from the call's turbofish, like `arg.get::<ValueHint>()`, and a call on a type that implements one trait twice, like `Encoder<Bytes>` and `Encoder<&[u8]>`, links to the implementation that takes the argument given. Before, such calls were linked by name and could reach another type's method of the same name. Re-index Rust projects after upgrading.
|
||||
- In Rust, a `for` loop's variable now has the type of an item of what the loop goes over, like `for item in &self.items` over a `Vec<Item>`, `for (key, value) in &map` over a `HashMap`, or `for sc in cmd.get_subcommands()` over the `impl Iterator` a method returns, and each name in a tuple pattern like `let (tx, rx) = mpsc::channel(8);` has the type of its part of the tuple, so calls on them link to that type's methods. Before, such calls were linked by name: tokio's `tx.send(…)` calls on channel senders went to an internal type's `send`. Re-index Rust projects after upgrading.
|
||||
- In Rust, a call on a field of `self`, like `self.store.save()` or `self.inner.lock()`, now links to the method of the type the struct declares for that field, also when the field's type is a parameter the `impl` bounds by a trait (`impl<S: Store> Worker<S>`), a trait object, or held in an `Arc` or a `Box`. A field whose type comes from the standard library or a dependency, like `jar: cookie::CookieJar` or `inner: std::sync::Mutex<…>`, no longer links to the project's own type of the same name, which often made a wrapper's method look like it called itself. Re-index Rust projects after upgrading.
|
||||
- In Rust, a call written with a turbofish, like `matches.get_one::<String>("name")`, `parse_config::<Settings>()` or `Pool::<u8>::new()`, now links to the function it calls. Before, the type arguments stayed in the call's name, so no such call was linked. Re-index Rust projects after upgrading.
|
||||
- In C++, every name a declaration lists now has the declaration's type, like `strings_found` in google/breakpad's `ByteBuffer symbols_found, strings_found;` or `data_` in a class's `Table *index_, *data_;`, so calls on it link to that class's methods. Before, only the first name had its type read, and a call on another name linked to nothing or to the class of an earlier variable with the same name.
|
||||
- In C++, a call on a parameter, a local lambda or functor, a lambda's parameter or a range-`for` variable no longer links to a function or method elsewhere that only shares its name, like protocolbuffers/protobuf's code generators calling `format(…)` on their `Formatter& format` parameter, which linked to the conformance suite's `TestResult::format`, or facebook/rocksdb's tests calling a local `auto key = [](int i) {…};`. C++ finds the local first, so that is what the call calls.
|
||||
- In TypeScript and JavaScript, an import of a package your project declares itself with `declare module '…'` in a `.d.ts` file, like a `typings/mammoth/index.d.ts` written for an untyped package or VS Code's own `vscode.d.ts`, now links to that declaration, so `codegraph callers`, impact and `codegraph affected` show the code that uses your typings instead of nothing or a project symbol that only shares a name. Named, default and namespace imports, `export =` and renaming export lists are read the way TypeScript reads them, a function such a file declares without a body (`export function convertToHtml(…): Promise<Result>;`) is now indexed so calls to it have something to link to, and only an import of its package reaches it. A `declare module` that augments a package's own types, a wildcard like `declare module '*.svg'`, and copies of typings kept in test fixtures are left alone. Re-index TypeScript projects after upgrading.
|
||||
|
||||
@@ -312,3 +312,24 @@ pub union Reg {
|
||||
}
|
||||
|
||||
impl Base for Reg {}
|
||||
|
||||
// Turbofish calls name the function they wrap: `s.get::<u32>()` is `s.get`,
|
||||
// and a path's type arguments drop (`TfPool::<u8>::new` is `TfPool::new`),
|
||||
// nested ones and a `->` inside them included.
|
||||
fn turbofish_calls(s: &TfStore, items: &[u8]) {
|
||||
let _a = s.get::<u32>();
|
||||
let _b = tf_free::<Vec<u8>>();
|
||||
let _c = TfPool::<u8>::new();
|
||||
let _d = TfPool::<Vec<u8>>::with_capacity::<u8>(4);
|
||||
let _e = TfPool::<u8>::new().len();
|
||||
let _f = TfPool::create::<u8>().len();
|
||||
let _g = tf_run::<fn() -> u8>(tf_free);
|
||||
let _h = items.iter().collect::<Vec<_>>();
|
||||
}
|
||||
|
||||
impl TfStore {
|
||||
fn turbofish_self(&self) {
|
||||
self.get::<u16>();
|
||||
self.cache.load::<u8>();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
/**
|
||||
* A Rust call written with a turbofish — `s.get::<u32>()`, `helper::<u8>()`,
|
||||
* `Pool::<u8>::new()` — calls the function the turbofish wraps. Both engines
|
||||
* kept the type arguments in the callee's name (`s.get::<u32>`), which no
|
||||
* resolution strategy reads, so every such call was left unresolved.
|
||||
*/
|
||||
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import { CodeGraph } from '../src';
|
||||
|
||||
let root = '';
|
||||
let cg: CodeGraph;
|
||||
|
||||
beforeAll(async () => {
|
||||
root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-rust-turbofish-'));
|
||||
fs.mkdirSync(path.join(root, 'src'));
|
||||
fs.writeFileSync(path.join(root, 'Cargo.toml'), '[package]\nname = "app"\nversion = "0.1.0"\n');
|
||||
fs.writeFileSync(path.join(root, 'src', 'main.rs'), [
|
||||
'pub struct Store {',
|
||||
' cache: Cache,',
|
||||
'}',
|
||||
'',
|
||||
'impl Store {',
|
||||
' pub fn get<T>(&self) -> Option<T> { None }',
|
||||
'',
|
||||
' pub fn own(&self) {',
|
||||
' let _ = self.get::<u16>();',
|
||||
' let _ = self.cache.load::<u8>();',
|
||||
' }',
|
||||
'}',
|
||||
'',
|
||||
'pub struct Cache;',
|
||||
'',
|
||||
'impl Cache {',
|
||||
' pub fn load<T>(&self) -> Option<T> { None }',
|
||||
'}',
|
||||
'',
|
||||
'/// A distractor: another `get` the typed receiver must win over.',
|
||||
'pub struct Other;',
|
||||
'',
|
||||
'impl Other {',
|
||||
' pub fn get<T>(&self) -> Option<T> { None }',
|
||||
'}',
|
||||
'',
|
||||
'pub struct Pool<T> {',
|
||||
' items: Vec<T>,',
|
||||
'}',
|
||||
'',
|
||||
'impl<T> Pool<T> {',
|
||||
' pub fn new() -> Pool<T> { Pool { items: Vec::new() } }',
|
||||
' pub fn create<U>() -> Pool<T> { Pool { items: Vec::new() } }',
|
||||
' pub fn len(&self) -> usize { self.items.len() }',
|
||||
'}',
|
||||
'',
|
||||
'pub fn helper<T>() -> u8 { 0 }',
|
||||
'',
|
||||
'pub fn run<F>(f: F) -> u8 { 0 }',
|
||||
'',
|
||||
'pub fn calls(s: &Store) {',
|
||||
' let _ = s.get::<u32>();',
|
||||
' let _ = helper::<Vec<u8>>();',
|
||||
' let _ = Pool::<u8>::new();',
|
||||
' let _ = Pool::<u8>::create::<u16>();',
|
||||
' let _ = run::<fn() -> u8>(helper::<u8>);',
|
||||
'}',
|
||||
'',
|
||||
'pub fn chains() -> usize {',
|
||||
' Pool::<u8>::new().len() + Pool::<u8>::create::<u16>().len()',
|
||||
'}',
|
||||
'',
|
||||
'fn main() {}',
|
||||
'',
|
||||
].join('\n'));
|
||||
cg = await CodeGraph.init(root, { index: true });
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
cg?.destroy();
|
||||
fs.rmSync(root, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
/** `qualifiedName` of every call target of the function or method `name`. */
|
||||
function callees(name: string): string[] {
|
||||
const fn = cg.getNodesByName(name).find((n) => n.kind === 'function' || n.kind === 'method');
|
||||
expect(fn).toBeDefined();
|
||||
return cg.getOutgoingEdges(fn!.id)
|
||||
.filter((e) => e.kind === 'calls')
|
||||
.map((e) => cg.getNode(e.target)!.qualifiedName)
|
||||
.sort();
|
||||
}
|
||||
|
||||
describe('Rust turbofish calls', () => {
|
||||
it('a method call on a typed local reaches its type\'s method', () => {
|
||||
expect(callees('calls')).toContain('Store::get');
|
||||
expect(callees('calls')).not.toContain('Other::get');
|
||||
});
|
||||
|
||||
it('a free function and a path call reach what they name, type arguments and all', () => {
|
||||
const c = callees('calls');
|
||||
expect(c).toContain('helper');
|
||||
expect(c).toContain('run');
|
||||
expect(c).toContain('Pool::new');
|
||||
expect(c).toContain('Pool::create');
|
||||
});
|
||||
|
||||
it('a call on self or on a field of self reaches the method', () => {
|
||||
expect(callees('own')).toEqual(['Cache::load', 'Store::get']);
|
||||
});
|
||||
|
||||
it('a chain through a turbofish path reaches the method of what it returns', () => {
|
||||
// Two `.len()` calls, one per chain.
|
||||
expect(callees('chains')).toEqual(['Pool::create', 'Pool::len', 'Pool::len', 'Pool::new']);
|
||||
});
|
||||
|
||||
it('keeps no type arguments in a call reference', () => {
|
||||
const fn = cg.getNodesByName('calls').find((n) => n.kind === 'function')!;
|
||||
const names = cg.getOutgoingEdges(fn.id)
|
||||
.filter((e) => e.kind === 'calls')
|
||||
.map((e) => String((e.metadata as Record<string, unknown> | undefined)?.refName ?? ''));
|
||||
expect(names.length).toBeGreaterThan(0);
|
||||
expect(names.filter((n) => n.includes('::<'))).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -882,6 +882,11 @@ impl<'t> Walker<'t> {
|
||||
let func = node
|
||||
.child_by_field_name("function")
|
||||
.or_else(|| node.named_child(0));
|
||||
// A turbofish — `s.get::<u32>()`, `helper::<u8>()`,
|
||||
// `Store::with_capacity::<u8>(1)` — calls the function it wraps: its
|
||||
// type arguments are no part of the callee's name. Mirrors
|
||||
// TreeSitterExtractor.extractCall.
|
||||
let func = func.map(unwrap_generic_function);
|
||||
let mut callee_name = String::new();
|
||||
|
||||
if let Some(func) = func {
|
||||
@@ -920,7 +925,7 @@ impl<'t> Walker<'t> {
|
||||
// function chain (`Foo::new().bar()`, inner
|
||||
// callee a scoped_identifier). Instance chains
|
||||
// keep the bare method name.
|
||||
let inner_fn = r.child_by_field_name("function");
|
||||
let inner_fn = r.child_by_field_name("function").map(unwrap_generic_function);
|
||||
let reencode =
|
||||
inner_fn.map(|f| f.kind() == "scoped_identifier").unwrap_or(false);
|
||||
if reencode {
|
||||
@@ -930,6 +935,7 @@ impl<'t> Walker<'t> {
|
||||
.chars()
|
||||
.filter(|c| !c.is_whitespace())
|
||||
.collect();
|
||||
let inner = strip_turbofish(&inner);
|
||||
callee_name = format!("{inner}().{method_name}");
|
||||
} else {
|
||||
callee_name = method_name.to_string();
|
||||
@@ -974,10 +980,11 @@ impl<'t> Walker<'t> {
|
||||
}
|
||||
}
|
||||
} else if matches!(func.kind(), "scoped_identifier" | "scoped_call_expression") {
|
||||
callee_name = self.text(func).to_string();
|
||||
// A path's turbofish is no part of the name either
|
||||
// (`Vec::<u8>::with_capacity` → `Vec::with_capacity`).
|
||||
callee_name = strip_turbofish(self.text(func));
|
||||
} else {
|
||||
// identifier; generic_function keeps the raw turbofish text
|
||||
// (`helper::<T>` — unresolvable downstream, preserved).
|
||||
// identifier (a turbofish was unwrapped above).
|
||||
callee_name = self.text(func).to_string();
|
||||
}
|
||||
}
|
||||
@@ -1601,6 +1608,52 @@ fn is_stoplisted(name: &str) -> bool {
|
||||
)
|
||||
}
|
||||
|
||||
/// The function a turbofish wraps: `s.get::<u32>` → `s.get`,
|
||||
/// `helper::<u8>` → `helper`; any other callee as is.
|
||||
fn unwrap_generic_function(func: Node<'_>) -> Node<'_> {
|
||||
if func.kind() == "generic_function" {
|
||||
func.child_by_field_name("function").unwrap_or(func)
|
||||
} else {
|
||||
func
|
||||
}
|
||||
}
|
||||
|
||||
/// A callee path without its turbofish type arguments: `Vec::<u8>::new` →
|
||||
/// `Vec::new`. The brackets nest (`::<Vec<u8>>`), and the `>` of a `->` is
|
||||
/// no closing one (`::<fn() -> u8>`). Mirrors stripRustTurbofish in
|
||||
/// TreeSitterExtractor: both emit the same bytes.
|
||||
fn strip_turbofish(text: &str) -> String {
|
||||
if !text.contains("::<") {
|
||||
return text.to_string();
|
||||
}
|
||||
let chars: Vec<char> = text.chars().collect();
|
||||
let mut out = String::with_capacity(text.len());
|
||||
let mut i = 0;
|
||||
while i < chars.len() {
|
||||
if chars[i] == ':' && i + 2 < chars.len() && chars[i + 1] == ':' && chars[i + 2] == '<' {
|
||||
let mut depth = 0i32;
|
||||
let mut j = i + 2;
|
||||
while j < chars.len() {
|
||||
let c = chars[j];
|
||||
if c == '<' {
|
||||
depth += 1;
|
||||
} else if c == '>' && chars[j - 1] != '-' {
|
||||
depth -= 1;
|
||||
if depth == 0 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
j += 1;
|
||||
}
|
||||
i = j + 1;
|
||||
continue;
|
||||
}
|
||||
out.push(chars[i]);
|
||||
i += 1;
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// LITERAL_RECEIVER_TYPES (shared table).
|
||||
fn is_literal_receiver(kind: &str) -> bool {
|
||||
matches!(
|
||||
|
||||
@@ -485,6 +485,33 @@ function peelSwiftCallWrappers(node: SyntaxNode, source: string): SyntaxNode {
|
||||
return cur;
|
||||
}
|
||||
|
||||
/**
|
||||
* A Rust callee path without its turbofish type arguments: `Vec::<u8>::new`
|
||||
* calls `Vec::new`, `Store::with_capacity::<u8>` calls `Store::with_capacity`.
|
||||
* The arguments' brackets nest (`::<Vec<u8>>`), and the `>` of a `->` is no
|
||||
* closing one (`::<fn() -> u8>`). Mirrored in the kernel's strip_turbofish
|
||||
* (rustlang.rs): both emit the same bytes.
|
||||
*/
|
||||
function stripRustTurbofish(text: string): string {
|
||||
if (!text.includes('::<')) return text;
|
||||
let out = '';
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
if (text.startsWith('::<', i)) {
|
||||
let depth = 0;
|
||||
let j = i + 2;
|
||||
for (; j < text.length; j++) {
|
||||
const c = text[j];
|
||||
if (c === '<') depth++;
|
||||
else if (c === '>' && text[j - 1] !== '-' && --depth === 0) break;
|
||||
}
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
out += text[i];
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* A Swift receiver's text from its innermost wrapped operand on: the wrappers
|
||||
* sit on the chain's left edge, so `!Settings.Defaults` reads
|
||||
@@ -5465,6 +5492,15 @@ export class TreeSitterExtractor {
|
||||
func = peelSwiftCallWrappers(func, this.source);
|
||||
}
|
||||
|
||||
// A Rust turbofish — `s.get::<u32>()`, `helper::<u8>()`,
|
||||
// `Store::with_capacity::<u8>(1)` — calls the function it wraps: its type
|
||||
// arguments are no part of the callee's name, which stayed in the ref
|
||||
// (`s.get::<u32>`) and resolved through no strategy. Mirrored in the
|
||||
// kernel's extract_call (rustlang.rs).
|
||||
if (func && this.language === 'rust' && func.type === 'generic_function') {
|
||||
func = getChildByField(func, 'function') ?? func;
|
||||
}
|
||||
|
||||
// C++ explicit operator call `a.operator+(b)` / `p->operator+(b)` (#1247):
|
||||
// tree-sitter-cpp can't parse an operator_name in field position, so the
|
||||
// callee is NOT a field_expression — the call_expression carries
|
||||
@@ -5640,10 +5676,16 @@ export class TreeSitterExtractor {
|
||||
// fluent codebases. Leave those to the bare-name path.
|
||||
reencode = /^[A-Z]/.test(innerCallee);
|
||||
} else {
|
||||
const innerFn = getChildByField(receiver, 'function');
|
||||
let innerFn = getChildByField(receiver, 'function');
|
||||
// Rust: the inner call's turbofish is no part of its name either
|
||||
// (`Foo::create::<T>().run()`, `Vec::<u8>::new().len()`).
|
||||
if (this.language === 'rust' && innerFn?.type === 'generic_function') {
|
||||
innerFn = getChildByField(innerFn, 'function') ?? innerFn;
|
||||
}
|
||||
innerCallee = innerFn
|
||||
? getNodeText(innerFn, this.source).replace(/->/g, '.').replace(/\s+/g, '')
|
||||
: '';
|
||||
if (this.language === 'rust') innerCallee = stripRustTurbofish(innerCallee);
|
||||
// Rust: only re-encode an associated-function chain
|
||||
// (`Foo::new().bar()`), whose inner callee is a path/`scoped_identifier`.
|
||||
// Go: only a bare package-level factory chain (`New().Method()`),
|
||||
@@ -5805,8 +5847,9 @@ export class TreeSitterExtractor {
|
||||
}
|
||||
}
|
||||
} else if (func.type === 'scoped_identifier' || func.type === 'scoped_call_expression') {
|
||||
// Scoped call: Module::function()
|
||||
calleeName = getNodeText(func, this.source);
|
||||
// Scoped call: Module::function(); a Rust path's turbofish is no
|
||||
// part of the name (`Vec::<u8>::with_capacity` → `Vec::with_capacity`).
|
||||
calleeName = this.language === 'rust' ? stripRustTurbofish(getNodeText(func, this.source)) : getNodeText(func, this.source);
|
||||
} else if (this.language === 'csharp' && func.type === 'member_access_expression') {
|
||||
// C# member call `recv.Method(...)`. When the receiver is itself a call
|
||||
// — a chained factory `Foo.Create(args).Bar()` — encode `inner().Bar`
|
||||
|
||||
Reference in New Issue
Block a user