fix(rust): a value's declared type is its binding's in scope at the call (#2722)

The type a Rust value is written with, read for `reader.as_mut().m()`
and calls through a `Pin`, came from a backward line scan for `reader:`
in the calling function. That took a `let reader: …` in a block the call
is outside of, or a struct literal's `reader: …` field, for the
declaration; it also stopped at a `let entry = entry.as_ref()…` on the
call's own line, whose new binding is not yet in scope there. It is now
the binding rustBindingAt finds in scope at the call, through the
block-scoped reader the typed-receiver path already uses.

A/B against main with fresh indexes: clap, dioxus, axum, serde, ripgrep
and gin are byte-identical; tokio has one call read more precisely, the
same target at a higher confidence (`entry.as_ref().handle()` on the
parameter `entry: NonNull<TimerShared>`). Two regression tests, both
failing on main. Full suite on the tested tree: 8,039 passed, 0 failed.

Follow-up 3 of the Rust receiver series (PR 2719's open list). Follow-up
2, an impl's type parameter in a return type read from the receiver's type
arguments, was measured and dropped: 84 value reads across six Rust repos
reach the case, most substituting a generic for a generic, an associated
type, or a head type already named, so about 10 to 20 calls would gain.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-10-21 01:15:31 +00:00
committed by GitHub
co-authored by Claude Opus 5.5
parent bd62d9c061
commit 17f1453a6c
3 changed files with 93 additions and 14 deletions
+1
View File
@@ -246,6 +246,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
- 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 Rust, a call through a value's declared type, like `reader.as_mut().poll_read(cx)` on `reader: Pin<&mut R>`, now reads the declaration in scope at the call. Before, a `let` of the same name inside an earlier block, or a struct literal's field of that name written above the call, could be taken for it. 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.
@@ -0,0 +1,82 @@
/**
* The type a Rust value is declared with — read for `reader.as_mut().m()` and
* calls through a `Pin` — is its binding's in scope at the call: a `let`
* in a block the call is outside of is not it, and neither is a struct
* literal's `reader: …` field. A backward line scan took the nearest line
* naming `reader:` above the call, whatever it was.
*/
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-declared-scope-'));
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'), [
'use std::pin::Pin;',
'',
'pub struct Reader;',
'',
'impl Reader {',
' pub fn poll_read(self: Pin<&mut Self>) {}',
'}',
'',
'pub struct Other;',
'',
'impl Other {',
' pub fn poll_read(self: Pin<&mut Self>) {}',
'}',
'',
'pub struct Holder {',
' reader: Pin<Box<Other>>,',
'}',
'',
'pub fn literal_field(mut reader: Pin<&mut Reader>) {',
' let _h = Holder {',
' reader: Box::pin(Other),',
' };',
' reader.as_mut().poll_read();',
'}',
'',
'pub fn inner_block(mut reader: Pin<&mut Reader>, mut other: Other) {',
' {',
' let reader: Pin<&mut Other> = Pin::new(&mut other);',
' let _ = reader;',
' }',
' reader.as_mut().poll_read();',
'}',
'',
'fn main() {}',
'',
].join('\n'));
cg = await CodeGraph.init(root, { index: true });
});
afterAll(() => {
cg?.destroy();
fs.rmSync(root, { recursive: true, force: true });
});
function callees(name: string): string[] {
const fn = cg.getNodesByName(name).find((n) => n.kind === 'function');
expect(fn).toBeDefined();
return cg.getOutgoingEdges(fn!.id).filter((e) => e.kind === 'calls').map((e) => cg.getNode(e.target)!.qualifiedName);
}
describe('Rust declared types are read from the binding in scope', () => {
it("a struct literal's field named like the value is no declaration of it", () => {
expect(callees('literal_field')).toContain('Reader::poll_read');
expect(callees('literal_field')).not.toContain('Other::poll_read');
});
it('a let in a block the call is outside of does not shadow it', () => {
expect(callees('inner_block')).toContain('Reader::poll_read');
expect(callees('inner_block')).not.toContain('Other::poll_read');
});
});
+10 -14
View File
@@ -21199,22 +21199,18 @@ function rustTypeTextAt(text: string): string {
/**
* The type a Rust local or parameter is written with — `reader: Pin<&mut
* R>`, `let timer: Option<Timer> = …` — read off its nearest declaration in
* the calling function; null when that declaration writes none (`let f =
* pin!(f)`).
* R>`, `let timer: Option<Timer> = …` — as its binding in scope at the call
* writes it (rustBindingAt: a block's own `let`, the latest shadow, never a
* struct literal's `reader: …` field); null when that binding writes none
* (`let f = pin!(f)`).
*/
function rustDeclaredTypeText(name: string, ref: UnresolvedRef, context: ResolutionContext): string | null {
const lines = context.getFileLines?.(ref.filePath) ?? context.readFile(ref.filePath)?.split(/\r?\n/) ?? [];
const start = Math.max(0, enclosingScopeStartLine(ref, context) - 1);
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const declared = new RegExp(`\\blet\\s+(?:mut\\s+)?${escaped}\\b(?:\\s*:(?!:)\\s*(.*))?|\\b${escaped}\\s*:(?!:)\\s*(.*)$`);
for (let i = Math.min(lines.length, ref.line) - 1; i >= start; i--) {
const m = declared.exec(lines[i] ?? '');
if (!m) continue;
const written = m[1] ?? m[2];
return written === undefined ? null : rustTypeTextAt(written);
}
return null;
const scopes = rustScopesOf(ref.filePath, context);
if (!scopes) return null;
const offset = (scopes.lineStarts[ref.line - 1] ?? 0) + Math.max(0, ref.column);
const found = rustBindingAt(scopes, name, offset);
if (!found || found.ambiguous || found.binding.type === undefined) return null;
return rustTypeTextAt(found.binding.type);
}
/**