diff --git a/CHANGELOG.md b/CHANGELOG.md index 901dd97b..4c0893bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -323,6 +323,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 method called through a chain of fields or on a struct literal, like `vis.map.get("a")`, `self.inner.config.port()` or `Xyz { x: 1.0 }.to_val()`, now links to the method of the type the last field is declared with, or of the literal's own type. A call through a type from the standard library or a dependency, like `J::from(i)` after `use serde_json::Value as J` or `Value::default()` after `use serde_json::Value`, no longer links to a project function of the same name. Before, such calls were linked by the method's name alone, so a `HashMap` field's `get` reached an unrelated project `get` and a struct literal's `to_val()` reached another type's. Re-index Rust projects after upgrading. Thanks @Egrn for the report and @danusha2345. (#2614) +- In Rust, a method call made through more than one field, or on what another method returns, like `self.services.store.put(…)` or `self.store().put(…)`, now keeps that whole receiver in the name the index records for the call, on one line or split over several by rustfmt, as a call through one field like `self.store.put(…)` already did. A call on what a method returns now links to the method of the type that method is declared to return, so builder chains like `builder.line_terminator(…).binary_detection(…)` reach the builder's own methods and a call on a standard-library value, like `buf.filled().len()`, no longer links to a project method of the same name. In a chain that calls the same method twice, like `self.table.get("a").get(0)`, each call links to its own receiver's method. Before, the index kept only the method's name, so tools reading a call couldn't tell which field it went through, `self.store().put(…)` linked to nothing, and the second `get` was guessed by its name. Re-index Rust projects after upgrading. Thanks @jocax for the report. (#2836) - 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. diff --git a/__tests__/extraction.test.ts b/__tests__/extraction.test.ts index 81634369..ad71de8c 100644 --- a/__tests__/extraction.test.ts +++ b/__tests__/extraction.test.ts @@ -1670,7 +1670,7 @@ impl From for Own { ).toBe(true); }); - it('keeps the owner shape for `self.()` and `self..()`, and collapses every other receiver (#1585, #1861)', () => { + it('keeps the owner shape for `self.()` and `self..()`, a longer receiver chain whole, and collapses a parenthesized receiver (#1585, #1861, #2836)', () => { const code = ` pub struct Outer { pub inner: Inner, pub deep: Deep } impl Outer { @@ -1689,22 +1689,23 @@ impl Outer { const calls = result.unresolvedReferences .filter((r) => r.referenceKind === 'calls') .map((r) => r.referenceName); - // Two shapes keep an owner the resolver can act on: the single-hop field - // receiver, whose type it reads off the owner struct (#1585), and the bare - // `self` receiver, whose type is the calling method's own owner (#1861). - // `self.make().run()` contributes `self.make` — the inner call — and its - // OUTER call collapses, because a method's return type is not read here. + // The single-hop field receiver keeps its owner, whose type the resolver + // reads off the owner struct (#1585), and so does the bare `self` + // receiver, whose type is the calling method's own owner (#1861). A + // longer chain of fields and method calls is kept whole, a call written + // `name()` (#2836): `self.make().run()` contributes `self.make`, the inner + // call, and `self.make().run`. expect(calls.filter((c) => c.startsWith('self.')).sort()).toEqual([ + 'self.deep.inner.run', 'self.inner.run', 'self.make', + 'self.make().run', 'self.run', ]); // A local receiver keeps its name as before… expect(calls).toContain('local.run'); - // …and the deeper chain, the call receiver and the parenthesized receiver - // still collapse to the method name. `self.run()` no longer does, so this - // is three rather than four. - expect(calls.filter((c) => c === 'run')).toHaveLength(3); + // …and the parenthesized receiver still collapses to the method name. + expect(calls.filter((c) => c === 'run')).toHaveLength(1); const outerRun = result.nodes.find((n) => n.qualifiedName === 'Outer::run'); expect(outerRun).toBeDefined(); const fieldRef = result.unresolvedReferences.find((r) => r.referenceName === 'self.inner.run'); diff --git a/__tests__/kernel-rustlang-parity.test.ts b/__tests__/kernel-rustlang-parity.test.ts index 523aab0f..a034ff0c 100644 --- a/__tests__/kernel-rustlang-parity.test.ts +++ b/__tests__/kernel-rustlang-parity.test.ts @@ -153,6 +153,42 @@ fn uses() { ]); }); + it('a call keeps its receiver chain of fields and method calls (#2836)', () => { + const links = Array.from({ length: 17 }, (_, i) => `l${i}()`).join('.'); + const source = ` +pub struct Svc { services: Services, store: Box, pair: (u8, Config) } +impl Svc { + fn chains(&self, vis: &Vis) { + self.services.store.put("a"); let _ = self.services.store.put("ü"); + self + .services // rustfmt + .store + .put("c"); + self.store().put("m"); self.store.put("d"); self.put("e"); vis.put(1); + vis.map.get("k").get(0); self.0.len(); self.pair.1.port(); + vis.get::(1).put::(2); vis.a.b().c.d(); + Foo::new().a().b(); (vis).a.b(); self.a?.b(); self.a.b().await.c(); vis[0].a.b(); + r#vis.a.b(); vis.${links}.m(); + } +} +`; + assertParity('chains.rs', source); + assertParity('chains.rs', source.replace(/\n/g, '\r\n')); + + // Not vacuous: the chains are kept, the other receivers keep their old names. + process.env.CODEGRAPH_KERNEL = '0'; + const names = new Set(extractFromSource('chains.rs', source, 'rust').unresolvedReferences + .filter((r) => r.referenceKind === 'calls').map((r) => r.referenceName)); + delete process.env.CODEGRAPH_KERNEL; + for (const name of [ + 'self.services.store.put', 'self.store().put', 'self.store.put', 'self.put', 'vis.put', 'vis.map.get', + 'vis.map.get().get', 'self.0.len', 'self.pair.1.port', 'vis.get', 'vis.get().put', 'vis.a.b().c.d', + 'Foo::new().a', 'b', 'c', 'm', + ]) { + expect(names, name).toContain(name); + } + }); + it('torture fixture: impl/trait quirks, use bindings, chains, fn-refs, value-refs, route macros', () => { const file = path.join(FIXTURE_DIR, 'torture.rs'); assertParity('fixtures/torture.rs', fs.readFileSync(file, 'utf8'), 20); diff --git a/__tests__/rust-receiver-chains.test.ts b/__tests__/rust-receiver-chains.test.ts new file mode 100644 index 00000000..0e213538 --- /dev/null +++ b/__tests__/rust-receiver-chains.test.ts @@ -0,0 +1,281 @@ +/** + * A Rust method call keeps the receiver chain it is written on (#2836): a + * chain of fields and method calls on `self` or on a name — + * `self.services.store.put(…)`, `self.store().put(…)`, `vis.map.get(…)` — is + * recorded with that chain, calls written `name()`, as the one-field + * `self.store.put(…)` always was. The extractor used to keep the bare method + * name, so the edge's `refName` said nothing of which value the call goes + * through, and the receiver of a call on a method call's value was never read. + * + * Resolution reads the receiver's type through the whole chain, where the + * call is written: `self.services` is a `Services`, its `store` a + * `Box`, so the call is `Store::put`; `self.store()` is what + * `Svc::store` returns. A later link calling the same name again is a call of + * its own (`self.table.get("a").get(0)` is `Table::get`, then `Row::get`). + */ +import { describe, it, expect, afterAll, beforeAll, beforeEach, afterEach } from 'vitest'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import { CodeGraph } from '../src'; +import { extractFromSource } from '../src/extraction'; +import { initGrammars, loadGrammarsForLanguages } from '../src/extraction/grammars'; + +const roots: string[] = []; +const graphs: Record<'issue' | 'chains', CodeGraph> = {} as never; + +function write(root: string, files: Record): void { + 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); + } +} + +async function index(name: keyof typeof graphs, files: Record): Promise { + const root = fs.mkdtempSync(path.join(os.tmpdir(), `cg-rust-chain-${name}-`)); + roots.push(root); + write(root, files); + graphs[name] = await CodeGraph.init(root, { index: true }); +} + +const crate = (name: string, deps = ''): string => + `[package]\nname = "${name}"\nversion = "0.1.0"\nedition = "2021"\n\n[dependencies]\n${deps}`; + +beforeAll(async () => { + // The issue's workspace, with the call on a method call's value it names. + await index('issue', { + 'Cargo.toml': '[workspace]\nmembers = ["api", "app"]\nresolver = "2"\n', + 'api/Cargo.toml': crate('api'), + 'api/src/lib.rs': 'pub trait Store {\n fn put(&self, key: &str);\n}\n', + 'app/Cargo.toml': crate('app', 'api = { path = "../api" }\n'), + 'app/src/lib.rs': [ + 'use api::Store;', + '', + 'pub struct Services { pub store: Box }', + 'pub struct Svc { pub services: Services, pub store: Box }', + '', + 'impl Svc {', + ' pub fn a(&self) { self.services.store.put("a"); }', + ' pub fn b(&self) { let _ = self.services.store.put("b"); }', + ' pub fn c(&self) {', + ' self', + ' .services', + ' .store', + ' .put("c");', + ' }', + ' pub fn d(&self) { self.store.put("d"); }', + ' pub fn e(&self) {', + ' self', + ' .store', + ' .put("e");', + ' }', + ' pub fn m(&self) { self.store().put("m"); }', + ' pub fn store(&self) -> &dyn Store { &*self.store }', + '}', + '', + ].join('\n'), + }); + + // Chains whose links call the same name, through `self`, a parameter and a + // tuple field, and a call on a method call's std value — beside a project + // namesake a guess by the name would take. + await index('chains', { + 'Cargo.toml': crate('chains'), + 'src/lib.rs': [ + 'pub struct Row { cells: Vec }', + '', + 'impl Row {', + ' pub fn get(&self, i: usize) -> u8 { self.cells[i] }', + '}', + '', + 'pub struct Table { rows: Vec, names: Vec }', + '', + 'impl Table {', + ' pub fn get(&self, _k: &str) -> &Row { &self.rows[0] }', + ' pub fn names(&self) -> &Vec { &self.names }', + '}', + '', + 'pub struct Db { pub table: Table }', + 'pub struct Wrap(pub Table);', + '', + '/// A project namesake of std\'s `len`.', + 'pub struct Counter;', + '', + 'impl Counter {', + ' pub fn len(&self) -> usize { 0 }', + '}', + '', + 'impl Db {', + ' pub fn cell(&self) -> u8 { self.table.get("a").get(0) }', + ' pub fn count(&self) -> usize { self.table.names().len() }', + '}', + '', + 'impl Wrap {', + ' pub fn first(&self) -> u8 { self.0.get("c").get(2) }', + '}', + '', + 'pub fn local(db: &Db) -> u8 {', + ' db.table', + ' .get("b")', + ' .get(1)', + '}', + '', + ].join('\n'), + }); +}, 120_000); + +afterAll(() => { + for (const cg of Object.values(graphs)) cg?.close(); + for (const root of roots) fs.rmSync(root, { recursive: true, force: true }); +}); + +/** The calls on `line` of `file`, as `qualifiedName resolvedBy refName`, sorted. */ +const callsAt = (name: keyof typeof graphs, file: string, line: number): string[] => { + const cg = graphs[name]; + const out: string[] = []; + for (const node of cg.getNodesInFile(file)) { + for (const edge of cg.getOutgoingEdges(node.id)) { + if (edge.kind !== 'calls' || edge.line !== line || edge.provenance === 'heuristic') continue; + out.push(`${cg.getNode(edge.target)!.qualifiedName} ${String(edge.metadata?.resolvedBy)} ${String(edge.metadata?.refName)}`); + } + } + return [...new Set(out)].sort(); +}; + +describe('the issue\'s workspace (#2836)', () => { + it('a call on two fields keeps its receiver: on one line, after `let _ =`, split by rustfmt', () => { + expect(callsAt('issue', 'app/src/lib.rs', 7)).toEqual(['Store::put instance-method self.services.store.put']); + expect(callsAt('issue', 'app/src/lib.rs', 8)).toEqual(['Store::put instance-method self.services.store.put']); + expect(callsAt('issue', 'app/src/lib.rs', 10)).toEqual(['Store::put instance-method self.services.store.put']); + }); + + it('a call on a method call\'s value keeps the path up to that call, and reaches what it returns', () => { + expect(callsAt('issue', 'app/src/lib.rs', 21)).toEqual([ + 'Store::put instance-method self.store().put', + 'Svc::store qualified-name self.store', + ]); + }); + + it('a call on one field keeps what it had', () => { + expect(callsAt('issue', 'app/src/lib.rs', 15)).toEqual(['Store::put instance-method self.store.put']); + expect(callsAt('issue', 'app/src/lib.rs', 17)).toEqual(['Store::put instance-method self.store.put']); + }); + + it('leaves no call unresolved', () => { + expect(graphs.issue.getUnresolvedReferencesInFile('app/src/lib.rs').filter((r) => r.referenceKind === 'calls')).toEqual([]); + }); +}); + +describe('a Rust call on a receiver chain', () => { + it('is told from a later link calling the same name: through `self`, a tuple field and a parameter', () => { + expect(callsAt('chains', 'src/lib.rs', 25)).toEqual([ + 'Row::get instance-method self.table.get().get', + 'Table::get instance-method self.table.get', + ]); + expect(callsAt('chains', 'src/lib.rs', 30)).toEqual([ + 'Row::get instance-method self.0.get().get', + 'Table::get instance-method self.0.get', + ]); + expect(callsAt('chains', 'src/lib.rs', 34)).toEqual([ + 'Row::get instance-method db.table.get().get', + 'Table::get instance-method db.table.get', + ]); + }); + + it('on a method call\'s std value is no project method', () => { + expect(callsAt('chains', 'src/lib.rs', 26)).toEqual(['Table::names instance-method self.table.names']); + }); +}); + +describe('Rust call references keep their receiver chain', () => { + const ENV_KEYS = ['CODEGRAPH_KERNEL'] as const; + let savedEnv: Record; + + beforeAll(async () => { + await initGrammars(); + await loadGrammarsForLanguages(['rust']); + }); + beforeEach(() => { + savedEnv = Object.fromEntries(ENV_KEYS.map((k) => [k, process.env[k]])); + process.env.CODEGRAPH_KERNEL = '0'; + }); + afterEach(() => { + for (const k of ENV_KEYS) { + if (savedEnv[k] === undefined) delete process.env[k]; + else process.env[k] = savedEnv[k]; + } + }); + + /** The names of the `calls` references extraction records for `body`, a method body's statements, sorted. */ + const callNames = (body: string): string[] => { + const source = `impl Svc {\n fn shapes(&self, x: &X) {\n${body}\n }\n}\n`; + return extractFromSource('shapes.rs', source, 'rust').unresolvedReferences + .filter((r) => r.referenceKind === 'calls') + .map((r) => r.referenceName) + .sort(); + }; + + it('of fields and method calls on `self` or a name, calls written `name()`', () => { + expect(callNames('self.services.store.put("a");')).toEqual(['self.services.store.put']); + expect(callNames('self.store().put("m");')).toEqual(['self.store', 'self.store().put']); + expect(callNames('self.0.len();')).toEqual(['self.0.len']); + expect(callNames('x.map.get("k");')).toEqual(['x.map.get']); + expect(callNames('x.get::(1).put(2);')).toEqual(['x.get', 'x.get().put']); + expect(callNames('self\n .services // the services\n .store\n .put("c");')).toEqual(['self.services.store.put']); + }); + + it('and keep the shapes they had', () => { + expect(callNames('self.store.put("d");')).toEqual(['self.store.put']); + expect(callNames('x.put(1);')).toEqual(['x.put']); + expect(callNames('self.put(1);')).toEqual(['self.put']); + expect(callNames('Foo::new().a().b();')).toEqual(['Foo::new', 'Foo::new().a', 'b']); + }); + + it('but not through parentheses, `?` or `.await`, nor past sixteen links', () => { + expect(callNames('(x).a.b();')).toEqual(['b']); + expect(callNames('self.a?.b();')).toEqual(['b']); + expect(callNames('self.a.b().await.c();')).toEqual(['c', 'self.a.b']); + const links = Array.from({ length: 17 }, (_, i) => `l${i}()`); + const names = callNames(`x.${links.join('.')}.m();`); + expect(names).toHaveLength(18); + expect(names).toContain('x.l0'); + expect(names).toContain(`x.${links.slice(0, 16).join('.')}.l16`); + expect(names).toContain('m'); + }); +}); + +describe('a Rust call on a receiver chain that names no method yet', () => { + let root: string; + let cg: CodeGraph | undefined; + beforeEach(() => { root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-rust-chain-sync-')); }); + afterEach(() => { cg?.close(); cg = undefined; fs.rmSync(root, { recursive: true, force: true }); }); + + it('waits under its method\'s name, and resolves once a sync adds the method', async () => { + write(root, { + 'Cargo.toml': crate('later'), + 'src/lib.rs': [ + 'pub mod disk;', + '', + 'pub struct Services { pub disk: disk::Disk }', + 'pub struct Svc { pub services: Services }', + '', + 'impl Svc {', + ' pub fn save(&self) { self.services.disk.flush(); }', + '}', + '', + ].join('\n'), + 'src/disk.rs': 'pub struct Disk;\n', + }); + cg = await CodeGraph.init(root, { index: true }); + const parked = cg.getUnresolvedReferencesInFile('src/lib.rs').filter((r) => r.referenceKind === 'calls'); + expect(parked.map((r) => r.referenceName)).toEqual(['self.services.disk.flush']); + + fs.writeFileSync(path.join(root, 'src/disk.rs'), 'pub struct Disk;\n\nimpl Disk {\n pub fn flush(&self) {}\n}\n'); + await cg.sync(); + const save = cg.getNodesInFile('src/lib.rs').find((n) => n.name === 'save')!; + const calls = cg.getOutgoingEdges(save.id).filter((e) => e.kind === 'calls'); + expect(calls.map((e) => `${cg!.getNode(e.target)!.qualifiedName} ${String(e.metadata?.refName)}`)).toEqual([ + 'Disk::flush self.services.disk.flush', + ]); + }); +}); diff --git a/codegraph-kernel/src/rustlang.rs b/codegraph-kernel/src/rustlang.rs index f963bf0b..50f7f54e 100644 --- a/codegraph-kernel/src/rustlang.rs +++ b/codegraph-kernel/src/rustlang.rs @@ -24,10 +24,13 @@ //! - Chained-call re-encode is scoped_identifier-gated (`Foo::new().bar()` → //! `Foo::new().bar`); a call through a field of the enclosing type keeps //! the owner-field shape (`self.inner.run()` → `self.inner.run`, #1585); -//! instance chains, parens, `.await`, deeper/non-self field chains, and -//! bare `self` receivers all collapse to the bare method name (`self` is -//! node kind `self`, not `identifier`, so it dodges SKIP_RECEIVERS by -//! falling through). Turbofish callees keep the raw `helper::` text. +//! a longer chain of fields and method calls on `self` or a name keeps +//! that chain, calls as `name()` (`self.services.store.put()` → +//! `self.services.store.put`, `self.store().put()` → `self.store().put`, +//! #2836); other instance chains, parens, `?`, `.await`, and longer +//! path-rooted chains collapse to the bare method name. A bare `self` +//! receiver keeps `self.` (#1861). Turbofish callees drop their type +//! arguments (`helper::` → `helper`). //! - `use` emits an import node named by the ROOT module (`crate`/`self`/…), //! one root `imports` ref, then one FULL-path `imports` ref per binding; //! `use x::*` (use_wildcard) emits nothing at all. @@ -927,10 +930,13 @@ impl<'t> Walker<'t> { } } "call_expression" => { - // Chained-call re-encode: ONLY an associated- + // Chained-call re-encode: an associated- // function chain (`Foo::new().bar()`, inner - // callee a scoped_identifier). Instance chains - // keep the bare method name. + // callee a scoped_identifier). A chain of + // fields and method calls on `self` or a name + // (`self.store().put()`, #2836) keeps that + // chain; any other instance chain the bare + // method name. 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); @@ -943,6 +949,8 @@ impl<'t> Walker<'t> { .collect(); let inner = strip_turbofish(&inner); callee_name = format!("{inner}().{method_name}"); + } else if let Some(chain) = receiver_chain(r, self.src) { + callee_name = format!("{chain}.{method_name}"); } else { callee_name = method_name.to_string(); } @@ -952,9 +960,11 @@ impl<'t> Walker<'t> { // field of the enclosing type (#1585): keep the // `self.` prefix so the resolver can type the // field from the owner struct's declaration - // (or leave it unresolved). Any other - // field_expression receiver — a deeper chain, - // a non-self base — keeps the bare name. + // (or leave it unresolved). A longer chain of + // fields and method calls on `self` or a name + // (`self.services.store.put()`, `vis.map.get()`, + // #2836) keeps that chain; any other + // field_expression receiver the bare name. let base = r.child_by_field_name("value"); let field = r.child_by_field_name("field"); match (base, field) { @@ -964,7 +974,12 @@ impl<'t> Walker<'t> { let field_name = self.text(f); callee_name = format!("self.{field_name}.{method_name}"); } - _ => callee_name = method_name.to_string(), + _ => { + callee_name = match receiver_chain(r, self.src) { + Some(chain) => format!("{chain}.{method_name}"), + None => method_name.to_string(), + } + } } } // `self.method()` — keep the `self.` prefix so the @@ -1628,6 +1643,64 @@ fn unwrap_generic_function(func: Node<'_>) -> Node<'_> { } } +/// The most links a receiver chain is kept with (receiver_chain). +const RECEIVER_CHAIN_MAX_LINKS: usize = 16; + +fn is_rust_ident(s: &str) -> bool { + let mut chars = s.chars(); + matches!(chars.next(), Some(c) if c.is_ascii_alphabetic() || c == '_') + && chars.all(|c| c.is_ascii_alphanumeric() || c == '_') +} + +/// The receiver chain a method call is written on, when it is fields and +/// method calls on `self` or on a name: `self.services.store`, +/// `self.store()`, `vis.map`, `self.0`, `x.get::(1)` — each field by its +/// name or index, each call as `name()`, its arguments and turbofish +/// dropped. None for any other receiver (a path's call, a literal, +/// parentheses, `?`, `.await`, an index, a macro), and past +/// RECEIVER_CHAIN_MAX_LINKS links. Mirrors rustReceiverChain in +/// TreeSitterExtractor (#2836): both emit the same bytes. +fn receiver_chain(node: Node<'_>, src: &str) -> Option { + let mut links: Vec = Vec::new(); + let mut cur = node; + while links.len() <= RECEIVER_CHAIN_MAX_LINKS { + if cur.kind() == "self" || cur.kind() == "identifier" { + let root = &src[cur.byte_range()]; + if links.is_empty() || !is_rust_ident(root) { + return None; + } + links.reverse(); + let mut out = root.to_string(); + for link in &links { + out.push('.'); + out.push_str(link); + } + return Some(out); + } + let call = cur.kind() == "call_expression"; + let field_of = if call { + unwrap_generic_function(cur.child_by_field_name("function")?) + } else { + cur + }; + if field_of.kind() != "field_expression" { + return None; + } + let name = field_of.child_by_field_name("field").map(|f| &src[f.byte_range()]).unwrap_or(""); + let ok = if call { + is_rust_ident(name) + } else { + is_rust_ident(name) || (!name.is_empty() && name.chars().all(|c| c.is_ascii_digit())) + }; + if !ok { + return None; + } + links.push(if call { format!("{name}()") } else { name.to_string() }); + cur = field_of.child_by_field_name("value")?; + } + None +} + /// A callee path without its turbofish type arguments: `Vec::::new` → /// `Vec::new`. The brackets nest (`::>`), and the `>` of a `->` is /// no closing one (`:: u8>`). Mirrors stripRustTurbofish in diff --git a/src/extraction/tree-sitter.ts b/src/extraction/tree-sitter.ts index d78099c7..9cc73f73 100644 --- a/src/extraction/tree-sitter.ts +++ b/src/extraction/tree-sitter.ts @@ -534,6 +534,45 @@ function stripRustTurbofish(text: string): string { return out; } +/** The most links a Rust receiver chain is kept with (rustReceiverChain). */ +const RUST_RECEIVER_CHAIN_MAX_LINKS = 16; + +/** + * The receiver chain a Rust method call is written on, when it is fields and + * method calls on `self` or on a name: `self.services.store`, `self.store()`, + * `vis.map`, `self.0`, `x.get::(1)` — each field by its name or index, + * each call as `name()`, its arguments and turbofish dropped. Null for any + * other receiver (a path's call, a literal, parentheses, `?`, `.await`, an + * index, a macro), and past RUST_RECEIVER_CHAIN_MAX_LINKS links, which keep + * the bare method name. Mirrored in the kernel's receiver_chain + * (rustlang.rs): both emit the same bytes. + */ +function rustReceiverChain(node: SyntaxNode, source: string): string | null { + const links: string[] = []; + let cur: SyntaxNode | null = node; + while (cur && links.length <= RUST_RECEIVER_CHAIN_MAX_LINKS) { + if (cur.type === 'self' || cur.type === 'identifier') { + const root = getNodeText(cur, source); + if (links.length === 0 || !/^[A-Za-z_]\w*$/.test(root)) return null; + return [root, ...links.reverse()].join('.'); + } + let fieldOf: SyntaxNode | null = cur; + let call = false; + if (cur.type === 'call_expression') { + fieldOf = getChildByField(cur, 'function'); + if (fieldOf?.type === 'generic_function') fieldOf = getChildByField(fieldOf, 'function'); + call = true; + } + if (fieldOf?.type !== 'field_expression') return null; + const field = getChildByField(fieldOf, 'field'); + const name = field ? getNodeText(field, source) : ''; + if (call ? !/^[A-Za-z_]\w*$/.test(name) : !/^(?:[A-Za-z_]\w*|\d+)$/.test(name)) return null; + links.push(call ? `${name}()` : name); + cur = getChildByField(fieldOf, 'value'); + } + return null; +} + /** * A Swift receiver's text from its innermost wrapped operand on: the wrappers * sit on the chain's left edge, so `!Settings.Defaults` reads @@ -5995,6 +6034,7 @@ export class TreeSitterExtractor { } const SKIP_RECEIVERS = new Set(['self', 'this', 'cls', 'super']); let constructed: string | null = null; + let rustChain: string | null = null; if (receiver && (receiver.type === 'identifier' || receiver.type === 'simple_identifier' || receiver.type === 'field_identifier')) { const receiverName = getNodeText(receiver, this.source); if (!SKIP_RECEIVERS.has(receiverName)) { @@ -6028,11 +6068,20 @@ export class TreeSitterExtractor { // or unknown. Previously this collapsed to the bare method name, // which exact-matched whichever same-named method was nearest — // often the calling method itself, a self-edge not in the source. - // Deeper chains (`self.a.b.m()`), `self.f().m()` and parenthesized - // receivers keep the bare name. Mirrored in the kernel's - // extract_call (rustlang.rs). + // Mirrored in the kernel's extract_call (rustlang.rs). const fieldName = getNodeText(getChildByField(receiver, 'field')!, this.source); calleeName = `self.${fieldName}.${methodName}`; + } else if (this.language === 'rust' && receiver && (rustChain = rustReceiverChain(receiver, this.source)) !== null) { + // Rust call on a longer chain of fields and method calls — + // `self.services.store.put()`, `self.store().put()`, + // `vis.map.get()`, split over lines by rustfmt or not (#2836). + // Keep the chain, calls written `name()`: the bare method name + // this used to emit said nothing of which value the call is + // made on. The resolver reads the receiver where the call is + // written. Parenthesized receivers, `?`, `.await` and a path's + // call (`Foo::new().a().b()`) keep the bare name. Mirrored in + // the kernel's extract_call (rustlang.rs). + calleeName = `${rustChain}.${methodName}`; } else if ( (this.language === 'cpp' || this.language === 'c' || diff --git a/src/resolution/index.ts b/src/resolution/index.ts index 1df37f6f..7e2474b6 100644 --- a/src/resolution/index.ts +++ b/src/resolution/index.ts @@ -21,7 +21,7 @@ import { isImportableKind, CPP_DEFINE_SIGNATURE, } from './types'; -import { isPythonSelfCall, matchJsStoreBindingCall, isUnresolvedJsMemberCall, matchObjectPathCall, thisScopeCaller, isVisibleAcrossFiles, matchReference, matchByFilePath, matchFunctionRef, matchDottedCallChain, matchScopedCallChain, matchMethodCall, sameLanguageFamily, crossesCodeBoundary, gateLanguageMatch, dumpNameMatcherProfile, clearNameMatcherMemos, isRustNameInScope, isRustForeignImplCallable, isRustPathCallReachable, CASE_INSENSITIVE_LANGUAGES, isDartMemberRead, matchDartMemberRead, isDartChainLink, matchDartChainLink, isDartAnnotation, matchDartAnnotation, isStdMethodName, isGoUnknownQualified, isGoProjectLocalCall, GO_STDLIB_PACKAGES, isGoBareName, goTypePositionTarget, goDotImportedSupertype, GO_TYPE_KINDS, matchGoAssertedCall, matchGoIndexedCall, matchGoChainedCall, preferGoBuildTwin, isGoAssertedLocal, isGoLocallyBound, isPythonFromImportModule, pythonAttrRefResolves, matchPythonTypedLocalCall, isImportShadowedByJsBinding, isManualMockPath, isTestSuitePath } from './name-matcher'; +import { isPythonSelfCall, matchJsStoreBindingCall, isUnresolvedJsMemberCall, matchObjectPathCall, thisScopeCaller, isVisibleAcrossFiles, matchReference, matchByFilePath, matchFunctionRef, matchDottedCallChain, matchScopedCallChain, matchMethodCall, sameLanguageFamily, crossesCodeBoundary, gateLanguageMatch, dumpNameMatcherProfile, clearNameMatcherMemos, isRustNameInScope, isRustForeignImplCallable, isRustPathCallReachable, rustChainedCallByName, CASE_INSENSITIVE_LANGUAGES, isDartMemberRead, matchDartMemberRead, isDartChainLink, matchDartChainLink, isDartAnnotation, matchDartAnnotation, isStdMethodName, isGoUnknownQualified, isGoProjectLocalCall, GO_STDLIB_PACKAGES, isGoBareName, goTypePositionTarget, goDotImportedSupertype, GO_TYPE_KINDS, matchGoAssertedCall, matchGoIndexedCall, matchGoChainedCall, preferGoBuildTwin, isGoAssertedLocal, isGoLocallyBound, isPythonFromImportModule, pythonAttrRefResolves, matchPythonTypedLocalCall, isImportShadowedByJsBinding, isManualMockPath, isTestSuitePath } from './name-matcher'; import { isVisibleCppMacro, clearCppMacroVisibility } from './cpp-macro-visibility'; import { isCppConstructorRef, matchCppConstructor } from './cpp-constructor'; import { isCppSupertypeRef, matchCppSupertype, clearCppSupertypeMemos } from './cpp-supertypes'; @@ -1421,6 +1421,17 @@ export class ReferenceResolver { } private resolveOneIn(ref: UnresolvedRef): ResolvedRef | null { + // A Rust call kept with its receiver chain — `self.services.store.put`, + // `self.store().put` (#2836) — resolves as the call by its method's name + // did before the extractor kept the chain: every strategy and gate reads + // that name the same way, and the receiver's type is read through the + // whole chain where the call is written (matchRustWrittenReceiverCall). + // The edge keeps the call as written. + const byName = rustChainedCallByName(ref); + if (byName) { + const resolved = this.resolveOneIn(byName); + return resolved && { ...resolved, original: ref }; + } // A C/C++ "call" whose name is a function-like macro visible in this // translation unit is a macro expansion, not a call — it must never bind // to a same-named function in another file (#1838). diff --git a/src/resolution/name-matcher.ts b/src/resolution/name-matcher.ts index 4e1f975d..e1263761 100644 --- a/src/resolution/name-matcher.ts +++ b/src/resolution/name-matcher.ts @@ -24489,9 +24489,14 @@ function matchRustAsMutChainCall(ref: UnresolvedRef, context: ResolutionContext) * the reporter's `vis.map.get("a")` on a `HashMap` field went to * the project's one `get`, `Ext::get`, an associated function at that, and * `Xyz { … }.to_val()` to `Stat::to_val`, the same trait's other impl. - * Undefined, for the name strategies to decide as before, where the receiver - * is anything else (a call's value, an index, a macro) or its type can't be - * read. + * A call the extractor keeps with its receiver's chain of fields and method + * calls — `self.services.store.put`, `self.store().put` (#2836) — comes here + * by its method's name with that chain (rustChainedCallByName), and its + * receiver is the chain where the call writes it: a method call's value is + * what the method's signature returns, and a later link calling the same + * name is a call of its own. Undefined, for the name strategies to decide as + * before, where the receiver is anything else (an index, a macro, a call on + * a path) or its type can't be read. */ function matchRustWrittenReceiverCall(ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | null | undefined { const scopes = rustScopesOf(ref.filePath, context); @@ -24499,7 +24504,10 @@ function matchRustWrittenReceiverCall(ref: UnresolvedRef, context: ResolutionCon if (!scopes || lineStart === undefined || ref.column < 0) return undefined; const start = lineStart + ref.column; if (start >= (scopes.lineStarts[ref.line] ?? scopes.code.length)) return undefined; - const receiver = rustWrittenCallReceiver(scopes, start, ref.referenceName); + const chain = RUST_CHAINED_CALLS.get(ref); + const receiver = chain + ? rustChainedReceiver(scopes, start, chain, ref.referenceName) + : rustWrittenCallReceiver(scopes, start, ref.referenceName); if (receiver === null) return undefined; const written = rustValueType(receiver, start, scopes, ref, context, 0); if (!written) return undefined; @@ -24699,6 +24707,65 @@ function rustWrittenCallReceiver(scopes: RustScopes, start: number, name: string return code.slice(start, dot).trim(); } +/** + * A Rust call the extractor keeps with the chain of fields and method calls + * its receiver is — `self.services.store.put`, `self.store().put`, + * `vis.map.get`, `self.0.len` (#2836): the chain's root, its links (a call + * written `name()`) and the method. The one-field `self..` + * has a reading of its own (matchMethodCall), and isn't one. + */ +const RUST_CHAINED_CALL = /^(self|[A-Za-z_]\w*)((?:\.(?:[A-Za-z_]\w*(?:\(\))?|\d+))+)\.([A-Za-z_]\w*)$/; + +/** The receiver chain, root first, of a Rust call resolution reads by its method's name (rustChainedCallByName). */ +const RUST_CHAINED_CALLS = new WeakMap(); + +/** + * What a Rust call kept with its receiver chain is resolved as: the call by + * its method's name, as the extractor recorded it before it kept the chain + * (#2836), so the frameworks, the strategies and the scope gates read it as + * they did; the chain goes with it, and the receiver's type is read through + * all of it (matchRustWrittenReceiverCall). Null for any other reference. + */ +export function rustChainedCallByName(ref: UnresolvedRef): UnresolvedRef | null { + if (ref.language !== 'rust' || ref.referenceKind !== 'calls') return null; + const m = RUST_CHAINED_CALL.exec(ref.referenceName); + if (!m) return null; + const links = m[2]!.slice(1).split('.'); + if (m[1] === 'self' && links.length === 1 && /^[A-Za-z_]\w*$/.test(links[0]!)) return null; + const byName: UnresolvedRef = { ...ref, referenceName: m[3]! }; + RUST_CHAINED_CALLS.set(byName, [m[1]!, ...links]); + return byName; +} + +/** + * The receiver of the Rust call `name(…)` whose expression starts at `start`, + * where its reference keeps the receiver's chain (`chain`, root first — `self`, + * `store()`): that chain as the file's blanked text writes it, a call's + * arguments and turbofish included, over lines too. Null where the text + * doesn't read as the chain and then the call. + */ +function rustChainedReceiver(scopes: RustScopes, start: number, chain: readonly string[], name: string): string | null { + const { code } = scopes; + const word = (at: number, id: string): boolean => code.startsWith(id, at) && !/\w/.test(code[at + id.length] ?? ''); + if (/\w/.test(code[start - 1] ?? '') || !word(start, chain[0]!)) return null; + let p = start + chain[0]!.length; + let end = -1; + // Each link — `.services`, `.store` and its `(…)` — and then the call itself. + for (const link of [...chain.slice(1), `${name}()`]) { + end = p; + const call = link.endsWith('()'); + const id = call ? link.slice(0, -2) : link; + const dot = rustSpaceEnd(code, p); + if (code[dot] !== '.' || code[dot + 1] === '.') return null; + const at = rustSpaceEnd(code, dot + 1); + if (!word(at, id)) return null; + const args = rustCallArgsEnd(scopes, at + id.length); + if (call !== args >= 0) return null; + p = call ? args : at + id.length; + } + return code.slice(start, end); +} + /** Std pointers whose `as_ref` / `as_mut` lend what they hold: `Box`, `NonNull` → `&mut T`. */ const RUST_LENDING_POINTERS: ReadonlySet = new Set(['Box', 'NonNull', 'Rc', 'Arc', 'Cow']);