From 17f1453a6c14a117f8a3fa50562b03c3c8c489ac Mon Sep 17 00:00:00 2001 From: Colby Mchenry Date: Wed, 21 Oct 2026 01:15:31 +0000 Subject: [PATCH] fix(rust): a value's declared type is its binding's in scope at the call (#2722) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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`). 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 --- CHANGELOG.md | 1 + __tests__/rust-declared-type-scope.test.ts | 82 ++++++++++++++++++++++ src/resolution/name-matcher.ts | 24 +++---- 3 files changed, 93 insertions(+), 14 deletions(-) create mode 100644 __tests__/rust-declared-type-scope.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 99685dba..9258a5e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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`, `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 Worker`), 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::("name")`, `parse_config::()` or `Pool::::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;`) 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. diff --git a/__tests__/rust-declared-type-scope.test.ts b/__tests__/rust-declared-type-scope.test.ts new file mode 100644 index 00000000..f5c4a4a7 --- /dev/null +++ b/__tests__/rust-declared-type-scope.test.ts @@ -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>,', + '}', + '', + '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'); + }); +}); diff --git a/src/resolution/name-matcher.ts b/src/resolution/name-matcher.ts index acc50587..b1140f64 100644 --- a/src/resolution/name-matcher.ts +++ b/src/resolution/name-matcher.ts @@ -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 = …` — read off its nearest declaration in - * the calling function; null when that declaration writes none (`let f = - * pin!(f)`). + * R>`, `let timer: Option = …` — 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); } /**