mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-24 06:53:52 +08:00
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:
co-authored by
Claude Opus 5.5
parent
bd62d9c061
commit
17f1453a6c
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user