fix(rust): resolve self.field.method() on the field's declared type instead of a same-named method (#1585) (#1599)
Fixes #1585. **Stacked on #1596** (the base branch is `fix/1588-rust-impl-type-qualification`; this PR's own diff is the second commit). Merge #1596 first, then retarget/merge this one. ## What was wrong ```rust impl Outer { pub fn run(&mut self) { self.inner.run(); // inner: Inner } } ``` produced `Outer::run -> Outer::run` — recursion the source doesn't contain. The extractor collapsed every `self.<field>.<method>()` receiver to the bare method name (`run`), so the resolver only ever saw `run` and exact-matched the nearest same-named method — the calling method itself, or a method of an unrelated type. Nothing marked the edge as a guess, and no row stayed in `unresolved_refs`, so a consumer had no way to tell. The same happened when the field's type isn't a project type at all (`its: std::vec::IntoIter<_>` → `self.its.next()`, `matcher: Regex` → `self.matcher.is_match()`): the bare `next` / `is_match` attached to whatever local method shared the name. ripgrep had 279 self-edges on `main`; the issue lists three sites, all of this shape. (The issue's C++ control — "`Outer::run -> Inner::run` resolves correctly" — doesn't actually hold on `main`: `inner.h` is classified as C by the `.h` heuristic, so `Inner::run` never exists and the C++ repro self-edges too. That's #1592, fixed separately.) ## What this does Rust struct fields are not graph nodes, so the field's type can only come from the struct's declaration text. This follows the Go 2-hop precedent exactly (`matchGoFieldChainCall`, #1276), including its exclusivity rule: 1. **Extraction (TS walker + native kernel, identical, parity-tested):** a call whose receiver is `self.<field>` keeps the owner-field shape — `self.inner.run()` is emitted as `self.inner.run`. Deeper chains (`self.a.b.m()`), call receivers (`self.f().m()`), parenthesized receivers and bare `self` keep the bare name, exactly as before. 2. **Resolution (`matchRustSelfFieldCall`):** owner type = the calling method's qualified-name prefix (`Outer::run` → `Outer`); the field's declared type is read from the owner struct's **own declaration lines** (comment-stripped, line by line — same discipline as the Go helper); the method is resolved **and validated** on that type by `resolveMethodOnType` (confidence 0.85, `instance-method`). 3. **Exclusive:** when the field is declared with an external type, a generic parameter (`T`), a container that doesn't auto-deref (`Option`/`Vec`/`Mutex`/…), or can't be found, the ref **stays unresolved** — it never falls through to the bare-name strategies. That is the safe behaviour the issue asks for, and it is what #1276 already chose for Go. `rustFieldTypeName` looks through exactly the layers Rust's method-call auto-deref looks through: references (`&`, `&'a mut`) and the owning smart pointers `Box`/`Rc`/`Arc`. `Box<dyn Source>` yields the trait, whose method node the interface-impl synthesizer then fans out to every implementation. `Option<Inner>` is left alone — `self.inner.take()` is Option's method and must not become `Inner::take`. Why it stacks on #1596: the owner is taken from the method's qualified name, which for a generic/lifetime impl was the trait's name before that fix. ## Measured on ripgrep (110 `.rs` files, #1596 build vs this branch) | | #1596 | this PR | |---|---|---| | nodes | 4029 | 4029 | | `calls` self-edges | 279 | **146** (none of the `self.<field>` shape remain — 116 bare-receiver, 30 other dotted) | | `self.<field>.m()` calls resolved through a validated field type | — | **292** (`DecompressionMatcher::command -> GlobSet::matches`, `Parser::find_long -> FlagMap::find`, `Haystack::path -> DirEntry::path`, …) | | `self.<field>.m()` calls left unresolved | — | **417** — every sampled one is a std/container method: `self.commands.push`, `self.child.wait`, `self.pre.is_some`, `self.colors.clone`, `self.path_terminator.unwrap_or` | | `calls` edges total | 9150 | 8878 (the 272 removed are the former bare-name guesses for those 417) | The issue's three sites: `walk.rs:824` now resolves to `IgnoreBuilder::add_custom_ignore_filename` (was a self-edge); `walk.rs:1195` (`self.its.next`, `IntoIter`) and `globset/lib.rs:983` (`self.matcher.is_match`, `Regex`) are parked as unresolved instead of guessed. The issue's repro gives `Outer::run -> Inner::run` (`instance-method`, confidence 0.85) on both the kernel path and `CODEGRAPH_KERNEL=0`. ## Tests - `__tests__/extraction.test.ts`: only the single-hop `self.<field>.<method>()` call keeps the prefix; deeper / call / parenthesized / bare-`self` receivers and a local receiver are unchanged. - `__tests__/resolution.test.ts` (end-to-end, Cargo layout): the issue's repro → `Outer::run -> Inner::run`, no self-edge; an external field type (`std::vec::IntoIter`) with a local `next` decoy → no edge at all; `Box<Inner>` and `&'a mut Inner` resolve, `Option<Inner>` does not (even though `Inner` declares the method); a generic `T` field → no edge; genuine `self.run()` recursion keeps its self-edge; the #1588 repro's `UsesFile::go` / `UsesBuf::go` resolve to `FileSource::read` / `BufSource::read`, and a `Box<dyn Source>` field lands on `Source::read` with the synthesizer fanning out to both impls. - `__tests__/fixtures/kernel-parity/torture.rs` grows the receiver shapes; all 15 kernel parity suites pass against the rebuilt kernel (147 tests). - Full `npm test` on this branch: 189 files, 3187 passed, 9 skipped, 0 failed. Re-index after upgrading. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01LxZj6W6Y1SHXwvpT3uwJpK
This commit is contained in:
@@ -72,6 +72,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
- C/C++ function-pointer analysis now bounds its compiled-pattern caches, so very large repositories can no longer exhaust the JavaScript engine's regular-expression code space during indexing. (#1559)
|
||||
- JSX rendering analysis now runs only on JavaScript-family files, so JSX-looking strings in C/C++ (or any other language) no longer create impossible call edges — in pure-C projects and in mixed-language monorepos alike. (#1560)
|
||||
- Methods implemented in a generic or lifetime-parameterized `impl` block (`impl<T> Source for BufSource<T>`, `impl<'a> Iterator for Parents<'a>`) are now recorded under the implementing type instead of the trait. Previously such a method could not be found by its type — "who calls `BufSource::read`" had no answer — and it collided with the trait's own declaration, which could even invent a call-graph edge out of an impl body that contains no call at all. Impls on a reference (`impl Trait for &Foo`) and on a module-qualified type (`impl Trait for m::Foo`) are attributed to their type too. Re-index after upgrading. Thanks @Dshuishui. (#1588) (Rust)
|
||||
- A method call on a struct field — `self.inner.run()` with `inner: Inner` — now resolves to the method on the field's declared type. Previously the call was reduced to the bare method name and matched whichever same-named method was nearest, which was often the calling method itself, recording recursion that isn't in the source (a few hundred such self-edges in ripgrep alone), or a method of an unrelated type. References and `Box`/`Rc`/`Arc` fields are looked through, as Rust's own method calls are; a field whose type is external (a std or third-party type), a generic parameter, or a container like `Option`/`Vec` is left unresolved rather than guessed. Re-index after upgrading. Thanks @Dshuishui. (#1585) (Rust)
|
||||
|
||||
## [1.5.0] - 2026-07-21
|
||||
|
||||
|
||||
@@ -1216,6 +1216,41 @@ impl From<u32> for Own {
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('keeps the owner-field shape for `self.<field>.<method>()` and collapses every other receiver (#1585)', () => {
|
||||
const code = `
|
||||
pub struct Outer { pub inner: Inner, pub deep: Deep }
|
||||
impl Outer {
|
||||
pub fn run(&mut self) {
|
||||
self.inner.run();
|
||||
self.deep.inner.run();
|
||||
self.make().run();
|
||||
(self.inner).run();
|
||||
self.run();
|
||||
let local = Inner { n: 0 };
|
||||
local.run();
|
||||
}
|
||||
}
|
||||
`;
|
||||
const result = extractFromSource('outer.rs', code);
|
||||
const calls = result.unresolvedReferences
|
||||
.filter((r) => r.referenceKind === 'calls')
|
||||
.map((r) => r.referenceName);
|
||||
// Exactly one call keeps the `self.<field>` prefix — the single-hop field
|
||||
// receiver whose type the resolver can read off the owner struct.
|
||||
expect(calls.filter((c) => c.startsWith('self.'))).toEqual(['self.inner.run']);
|
||||
// A local receiver keeps its name as before…
|
||||
expect(calls).toContain('local.run');
|
||||
// …and the deeper chain, the call receiver, the parenthesized receiver and
|
||||
// the bare `self` receiver all still collapse to the method name.
|
||||
expect(calls.filter((c) => c === 'run')).toHaveLength(4);
|
||||
expect(calls).toContain('make');
|
||||
const outerRun = result.nodes.find((n) => n.qualifiedName === 'Outer::run');
|
||||
expect(outerRun).toBeDefined();
|
||||
const fieldRef = result.unresolvedReferences.find((r) => r.referenceName === 'self.inner.run');
|
||||
expect(fieldRef?.fromNodeId).toBe(outerRun!.id);
|
||||
expect(fieldRef?.line).toBe(5);
|
||||
});
|
||||
|
||||
it('gives no receiver to an impl whose target names no single type', () => {
|
||||
// A tuple / `dyn Trait` / primitive implementing type has no struct to
|
||||
// hang the methods off, so they are extracted as plain functions — the
|
||||
|
||||
@@ -72,6 +72,16 @@ impl Widget {
|
||||
self.n * mul()
|
||||
}
|
||||
|
||||
/// Receiver shapes (#1585): only `self.<field>.<method>()` keeps the
|
||||
/// owner-field prefix; deeper / parenthesized / call / bare-self collapse.
|
||||
fn via_field(&self) -> u32 {
|
||||
self.field.deep_call();
|
||||
self.field.z.clone();
|
||||
self.method_a().chain_b();
|
||||
(self.field).deep_call();
|
||||
self.area()
|
||||
}
|
||||
|
||||
fn clone_self(&self) -> Self {
|
||||
Self::assoc();
|
||||
Widget {
|
||||
|
||||
@@ -1172,6 +1172,115 @@ impl<T> Source for BufSource<T> {
|
||||
expect(synth(bufImpl!.id)).toHaveLength(0);
|
||||
});
|
||||
|
||||
// ── Rust `self.<field>.<method>()` receivers (#1585) ───────────────────
|
||||
// A Cargo layout (Cargo.toml + src/) so `use crate::…` paths resolve.
|
||||
function writeRustCrate(root: string, files: Record<string, string>): void {
|
||||
fs.writeFileSync(
|
||||
path.join(root, 'Cargo.toml'),
|
||||
'[package]\nname = "repro"\nversion = "0.1.0"\nedition = "2021"\n'
|
||||
);
|
||||
fs.mkdirSync(path.join(root, 'src'), { recursive: true });
|
||||
for (const [rel, content] of Object.entries(files)) {
|
||||
fs.writeFileSync(path.join(root, 'src', rel), content);
|
||||
}
|
||||
}
|
||||
const callsFrom = (qualifiedName: string) => {
|
||||
const from = cg.getNodesByKind('method').find((n) => n.qualifiedName === qualifiedName);
|
||||
expect(from, qualifiedName).toBeDefined();
|
||||
return cg
|
||||
.getOutgoingEdges(from!.id)
|
||||
.filter((e) => e.kind === 'calls')
|
||||
.map((e) => ({
|
||||
target: cg.getNode(e.target)?.qualifiedName,
|
||||
resolvedBy: (e.metadata as { resolvedBy?: string } | undefined)?.resolvedBy,
|
||||
provenance: e.provenance ?? undefined, // a resolved (non-synthesized) edge stores NULL
|
||||
}));
|
||||
};
|
||||
|
||||
it("resolves `self.field.method()` to the method on the field's declared type, never to the caller itself (#1585)", async () => {
|
||||
// The issue's repro: `Outer::run` forwards to `Inner::run` through the
|
||||
// typed field `inner`. The call used to collapse to the bare name `run`
|
||||
// and exact-match the nearest same-named method — the calling method —
|
||||
// recording recursion the source does not contain.
|
||||
writeRustCrate(tempDir, {
|
||||
'lib.rs': 'pub mod inner;\npub mod outer;\n',
|
||||
'inner.rs': 'pub struct Inner {\n pub n: usize,\n}\n\nimpl Inner {\n pub fn run(&mut self) {\n self.n += 1;\n }\n}\n',
|
||||
'outer.rs': 'use crate::inner::Inner;\n\npub struct Outer {\n pub inner: Inner,\n}\n\nimpl Outer {\n pub fn run(&mut self) {\n self.inner.run();\n }\n}\n',
|
||||
});
|
||||
cg = await CodeGraph.init(tempDir, { index: true });
|
||||
expect(callsFrom('Outer::run')).toEqual([
|
||||
{ target: 'Inner::run', resolvedBy: 'instance-method', provenance: undefined },
|
||||
]);
|
||||
});
|
||||
|
||||
it('leaves a `self.field.method()` call unresolved when the field type is external, instead of guessing a same-named local method', async () => {
|
||||
// `its` is a std type with no project node. Before, `self.its.next()`
|
||||
// became the bare `next`, which exact-matched a local `next` — the
|
||||
// calling method (self-edge) or the unrelated `Other::next` decoy.
|
||||
writeRustCrate(tempDir, {
|
||||
'lib.rs':
|
||||
'pub struct Scanner {\n its: std::vec::IntoIter<u8>,\n}\n\nimpl Scanner {\n pub fn next(&mut self) -> Option<u8> {\n self.its.next()\n }\n}\n\n' +
|
||||
'pub struct Other { pub n: u8 }\nimpl Other {\n pub fn next(&mut self) -> Option<u8> {\n None\n }\n}\n',
|
||||
});
|
||||
cg = await CodeGraph.init(tempDir, { index: true });
|
||||
expect(callsFrom('Scanner::next')).toEqual([]);
|
||||
});
|
||||
|
||||
it('looks through references and owning smart pointers, but not through containers (#1585)', async () => {
|
||||
// Method-call auto-deref reaches the pointee of `Box`/`&mut`, so those
|
||||
// fields resolve to `Inner::run`. `Option<Inner>` does not auto-deref —
|
||||
// `self.inner.take()` is Option's method, so it must NOT become
|
||||
// `Inner::take` even though Inner declares a `take` too.
|
||||
writeRustCrate(tempDir, {
|
||||
'lib.rs':
|
||||
'pub struct Inner { pub n: usize }\nimpl Inner {\n pub fn run(&mut self) { self.n += 1; }\n pub fn take(&mut self) {}\n}\n\n' +
|
||||
'pub struct Boxed { inner: Box<Inner> }\nimpl Boxed {\n pub fn go(&mut self) { self.inner.run(); }\n}\n\n' +
|
||||
"pub struct Borrowed<'a> { inner: &'a mut Inner }\nimpl<'a> Borrowed<'a> {\n pub fn go(&mut self) { self.inner.run(); }\n}\n\n" +
|
||||
'pub struct Optional { inner: Option<Inner> }\nimpl Optional {\n pub fn go(&mut self) { self.inner.take(); }\n}\n',
|
||||
});
|
||||
cg = await CodeGraph.init(tempDir, { index: true });
|
||||
expect(callsFrom('Boxed::go').map((c) => c.target)).toEqual(['Inner::run']);
|
||||
expect(callsFrom('Borrowed::go').map((c) => c.target)).toEqual(['Inner::run']);
|
||||
expect(callsFrom('Optional::go')).toEqual([]);
|
||||
});
|
||||
|
||||
it('leaves a call through a generic-typed field unresolved, and keeps genuine `self.method()` recursion (#1585)', async () => {
|
||||
writeRustCrate(tempDir, {
|
||||
'lib.rs':
|
||||
'pub struct Inner { pub n: usize }\nimpl Inner {\n pub fn run(&mut self) {}\n}\n\n' +
|
||||
'pub struct Holder<T> { item: T }\nimpl<T> Holder<T> {\n pub fn go(&mut self) { self.item.run(); }\n}\n\n' +
|
||||
'pub struct Countdown { pub n: usize }\nimpl Countdown {\n pub fn run(&mut self) {\n if self.n > 0 {\n self.n -= 1;\n self.run();\n }\n }\n}\n',
|
||||
});
|
||||
cg = await CodeGraph.init(tempDir, { index: true });
|
||||
// `T` names no project type: no edge, and in particular not `Inner::run`.
|
||||
expect(callsFrom('Holder::go')).toEqual([]);
|
||||
// A bare `self` receiver is untouched — real recursion stays a self-edge.
|
||||
expect(callsFrom('Countdown::run').map((c) => c.target)).toEqual(['Countdown::run']);
|
||||
});
|
||||
|
||||
it('resolves a trait-object field to the trait method and typed fields to the right implementation (#1585, #1588)', async () => {
|
||||
// The #1588 repro's second half: `UsesFile::go` / `UsesBuf::go` each
|
||||
// forward through a typed field, and a `Box<dyn Source>` field lands on
|
||||
// the trait's declaration — from which the interface-impl synthesizer
|
||||
// fans out to every implementation.
|
||||
writeRustCrate(tempDir, {
|
||||
'lib.rs':
|
||||
'pub trait Source {\n fn read(&mut self) -> usize;\n}\n\n' +
|
||||
'pub struct FileSource { pub n: usize }\nimpl Source for FileSource {\n fn read(&mut self) -> usize { self.n }\n}\n\n' +
|
||||
'pub struct BufSource<T> { pub inner: T }\nimpl<T> Source for BufSource<T> {\n fn read(&mut self) -> usize { 0 }\n}\n\n' +
|
||||
'pub struct UsesFile { pub src: FileSource }\nimpl UsesFile {\n pub fn go(&mut self) -> usize { self.src.read() }\n}\n\n' +
|
||||
'pub struct UsesBuf { pub src: BufSource<u8> }\nimpl UsesBuf {\n pub fn go(&mut self) -> usize { self.src.read() }\n}\n\n' +
|
||||
'pub struct UsesDyn { pub src: Box<dyn Source> }\nimpl UsesDyn {\n pub fn go(&mut self) -> usize { self.src.read() }\n}\n',
|
||||
});
|
||||
cg = await CodeGraph.init(tempDir, { index: true });
|
||||
expect(callsFrom('UsesFile::go').map((c) => c.target)).toEqual(['FileSource::read']);
|
||||
expect(callsFrom('UsesBuf::go').map((c) => c.target)).toEqual(['BufSource::read']);
|
||||
expect(callsFrom('UsesDyn::go').map((c) => c.target)).toEqual(['Source::read']);
|
||||
// …and dispatch continues from the trait declaration to both impls.
|
||||
const fanOut = callsFrom('Source::read').filter((c) => c.provenance === 'heuristic').map((c) => c.target).sort();
|
||||
expect(fanOut).toEqual(['BufSource::read', 'FileSource::read']);
|
||||
});
|
||||
|
||||
it('records instantiates for C++ stack/brace construction, targeting the class (#1035)', async () => {
|
||||
// `Calculator calc(0)` (direct-init) and `Widget w{1, 2}` (brace-init)
|
||||
// carry the constructor args directly on the declarator — there's no
|
||||
|
||||
@@ -23,10 +23,12 @@
|
||||
//! - Unit structs (`struct Unit;`, no body field) mint NO node; `mod_item`
|
||||
//! mints no module node and adds no QN prefix.
|
||||
//! - Chained-call re-encode is scoped_identifier-gated (`Foo::new().bar()` →
|
||||
//! `Foo::new().bar`); instance chains, parens, `.await`, 2-hop fields, and
|
||||
//! `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::<T>` text.
|
||||
//! `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::<T>` text.
|
||||
//! - `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.
|
||||
@@ -838,9 +840,29 @@ impl<'t> Walker<'t> {
|
||||
callee_name = method_name.to_string();
|
||||
}
|
||||
}
|
||||
"field_expression" => {
|
||||
// `self.<field>.<method>()` — a call through a
|
||||
// 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.
|
||||
let base = r.child_by_field_name("value");
|
||||
let field = r.child_by_field_name("field");
|
||||
match (base, field) {
|
||||
(Some(b), Some(f))
|
||||
if b.kind() == "self" && f.kind() == "field_identifier" =>
|
||||
{
|
||||
let field_name = self.text(f);
|
||||
callee_name = format!("self.{field_name}.{method_name}");
|
||||
}
|
||||
_ => callee_name = method_name.to_string(),
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
// field_expression 2-hop, parenthesized,
|
||||
// await_expression, `self` — bare method name.
|
||||
// parenthesized, await_expression, `self` —
|
||||
// bare method name.
|
||||
callee_name = method_name.to_string();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -226,9 +226,13 @@ Generic else-branch (4312+), `func = childForFieldName('function') ?? namedChild
|
||||
(4455) → `Foo::new().bar()` → ref `Foo::new().bar`; an instance chain
|
||||
`x.foo().bar()` (innerFn field_expression) → bare `bar`. When not
|
||||
re-encoding, calleeName = bare methodName.
|
||||
- receiver anything else (`field_expression` 2-hop `v.field.method()`,
|
||||
`parenthesized_expression`, `await_expression`, `self`) → bare
|
||||
methodName (probed all four).
|
||||
- receiver `field_expression` whose `value` is `self` and whose `field` is
|
||||
a `field_identifier` (`self.inner.run()`) → `self.inner.run` — the
|
||||
owner-field shape the resolver types from the struct declaration
|
||||
(#1585, both sides together).
|
||||
- receiver anything else (`field_expression` with a non-self base
|
||||
`v.field.method()` / deeper `self.a.b.m()`, `parenthesized_expression`,
|
||||
`await_expression`, `self`) → bare methodName (probed all four).
|
||||
2. `func.type === 'scoped_identifier'` (4499) → calleeName = FULL text
|
||||
(`Foo::new`, `m::helper2`, `std::mem::swap` — whatever the source spells,
|
||||
whitespace included).
|
||||
|
||||
@@ -4431,6 +4431,26 @@ export class TreeSitterExtractor {
|
||||
} else {
|
||||
calleeName = methodName;
|
||||
}
|
||||
} else if (
|
||||
this.language === 'rust' &&
|
||||
receiver &&
|
||||
receiver.type === 'field_expression' &&
|
||||
getChildByField(receiver, 'value')?.type === 'self' &&
|
||||
getChildByField(receiver, 'field')?.type === 'field_identifier'
|
||||
) {
|
||||
// Rust `self.<field>.<method>()` — a call through a field of the
|
||||
// enclosing type (#1585). Keep the `self.` prefix: the resolver
|
||||
// recognizes the shape, reads the field's declared type off the
|
||||
// owner struct's declaration, and resolves the method on THAT
|
||||
// type — or leaves the ref unresolved when the type is external
|
||||
// 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).
|
||||
const fieldName = getNodeText(getChildByField(receiver, 'field')!, this.source);
|
||||
calleeName = `self.${fieldName}.${methodName}`;
|
||||
} else if (
|
||||
(this.language === 'cpp' ||
|
||||
this.language === 'c' ||
|
||||
|
||||
@@ -1731,6 +1731,18 @@ export function matchMethodCall(
|
||||
return matchGoFieldChainCall(objectOrClass!, methodName!, ref, context);
|
||||
}
|
||||
|
||||
// Rust call through a field of the enclosing type — `self.inner.run()`,
|
||||
// emitted as `self.inner.run` (#1585). Same discipline as the Go branch
|
||||
// above, and EXCLUSIVE for the same reason: validated field-type inference
|
||||
// or nothing. Letting this shape reach the bare-name strategies below is
|
||||
// how `self.inner.run()` resolved to a same-named method on an unrelated
|
||||
// type — or to the calling method itself, a self-edge the source doesn't
|
||||
// contain — whenever the field's type was external or merely shared a
|
||||
// method name with something nearby.
|
||||
if (ref.language === 'rust' && dotMatch && objectOrClass!.startsWith('self.')) {
|
||||
return matchRustSelfFieldCall(objectOrClass!.slice('self.'.length), methodName!, ref, context);
|
||||
}
|
||||
|
||||
// Java/Kotlin: receiver may be a field whose name doesn't match the type by
|
||||
// Java naming convention (`userbo` → class `UserBO`, abbreviated). Look up
|
||||
// the field in the enclosing class to get its declared type, then resolve
|
||||
@@ -1992,6 +2004,110 @@ function matchGoFieldChainCall(
|
||||
return null;
|
||||
}
|
||||
|
||||
// Rust primitives and the prelude's own types: a field of one of these never
|
||||
// names a project type, so a `self.<field>.<method>()` on it stays unresolved.
|
||||
const RUST_NON_PROJECT_FIELD_TYPES = new Set([
|
||||
'bool', 'char', 'str', 'String',
|
||||
'i8', 'i16', 'i32', 'i64', 'i128', 'isize',
|
||||
'u8', 'u16', 'u32', 'u64', 'u128', 'usize',
|
||||
'f32', 'f64',
|
||||
'Self', 'self',
|
||||
]);
|
||||
|
||||
/**
|
||||
* Reduce a Rust field's declared type text to the simple name of the type a
|
||||
* method call on that field auto-derefs to, or null when there is none we can
|
||||
* name. Only the layers Rust's method-call auto-deref looks through are
|
||||
* unwrapped: references (`&`, `&'a mut`) and the owning smart pointers
|
||||
* (`Box`, `Rc`, `Arc`) — `self.inner.run()` with `inner: Box<Inner>` calls
|
||||
* `Inner::run`. Containers that do NOT auto-deref to their parameter
|
||||
* (`Option<Inner>`, `Vec<Inner>`, `Mutex<Inner>`, `RefCell<Inner>`) keep their
|
||||
* own name and, having no project node, resolve to nothing — `self.items.push()`
|
||||
* must never become `Inner::push`. A trait object (`Box<dyn Source>`) yields
|
||||
* the trait, whose method node the interface-impl synthesizer fans out. A
|
||||
* generic parameter (`T`), a primitive, a tuple / array / raw pointer / fn
|
||||
* type, or a non-identifier yields null.
|
||||
*/
|
||||
export function rustFieldTypeName(raw: string): string | null {
|
||||
let t = raw.trim();
|
||||
for (;;) {
|
||||
const before = t;
|
||||
t = t.replace(/^&\s*(?:'\w+\s+)?(?:mut\s+)?/, '');
|
||||
t = t.replace(/^(?:Box|Rc|Arc)\s*<\s*/, '');
|
||||
t = t.replace(/^(?:dyn|impl)\s+/, '');
|
||||
if (t === before) break;
|
||||
}
|
||||
// Drop generic args, the closing `>`s of unwrapped pointers, and trait-object
|
||||
// bounds (`dyn Source + Send`); keep the last path segment.
|
||||
t = t.replace(/[<>+].*$/, '').trim();
|
||||
const seg = t.split('::').filter(Boolean).pop();
|
||||
if (!seg || !/^[A-Za-z_]\w*$/.test(seg)) return null;
|
||||
if (RUST_NON_PROJECT_FIELD_TYPES.has(seg)) return null;
|
||||
if (/^[A-Z]$/.test(seg)) return null; // bare single-letter generic parameter
|
||||
return seg;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a Rust call through a field of the enclosing type —
|
||||
* `self.inner.run()`, emitted by the extractor as `self.inner.run` (#1585).
|
||||
* Mirrors the Go 2-hop precedent above (#1276): the owner type is the calling
|
||||
* method's qualified-name prefix (`Outer::run` → `Outer`), the field's declared
|
||||
* type comes from the owner struct's OWN declaration lines, and the method is
|
||||
* resolved AND VALIDATED on that type by resolveMethodOnType. The caller
|
||||
* treats this branch as exclusive for `self.<field>` receivers: a field whose
|
||||
* type is external (`std::vec::IntoIter`, `regex::Regex`), a generic
|
||||
* parameter, or not declared where we can see it yields null and the ref stays
|
||||
* unresolved. Rust struct fields are not graph nodes, so the declaration text
|
||||
* is the only place the type lives.
|
||||
*/
|
||||
function matchRustSelfFieldCall(
|
||||
field: string,
|
||||
methodName: string,
|
||||
ref: UnresolvedRef,
|
||||
context: ResolutionContext,
|
||||
): ResolvedRef | null {
|
||||
// The extractor only ever emits a single field hop; anything else is not ours.
|
||||
if (!field || field.includes('.')) return null;
|
||||
const caller = context.getNodeById?.(ref.fromNodeId);
|
||||
if (!caller) return null;
|
||||
const sep = caller.qualifiedName.lastIndexOf('::');
|
||||
if (sep <= 0) return null; // a free fn has no `self`
|
||||
const owner = caller.qualifiedName.slice(0, sep).split('::').pop();
|
||||
if (!owner) return null;
|
||||
|
||||
const owners = preferCallSiteFile(context.getNodesByName(owner), ref.filePath).filter(
|
||||
(n) =>
|
||||
(n.kind === 'struct' || n.kind === 'union' || n.kind === 'class') &&
|
||||
n.language === 'rust'
|
||||
);
|
||||
const fieldEsc = field.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
// `pub inner: Inner,` / `inner: Box<dyn Source>,` / `pub(crate) inner: T }` —
|
||||
// the type text runs to the field separator. A comma inside generic args
|
||||
// (`HashMap<K, V>`) truncates the capture, which rustFieldTypeName then
|
||||
// reduces to the container's own name — exactly the non-deref case it
|
||||
// refuses anyway.
|
||||
const fieldRe = new RegExp(`\\b${fieldEsc}\\s*:\\s*([^,{}]+)`);
|
||||
for (const s of owners) {
|
||||
const source = context.readFile(s.filePath);
|
||||
if (!source) continue;
|
||||
// Only the struct's own declaration lines, comment-stripped line by line —
|
||||
// same discipline as the Go helper: prose or a same-named identifier
|
||||
// elsewhere in the file can never donate a type.
|
||||
const declLines = source.split('\n').slice(Math.max(0, s.startLine - 1), s.endLine);
|
||||
for (const rawLine of declLines) {
|
||||
const line = rawLine.replace(/\/\/.*$/, '').replace(/\/\*.*?\*\//g, '');
|
||||
const m = line.match(fieldRe);
|
||||
if (!m || !m[1]) continue;
|
||||
const fieldType = rustFieldTypeName(m[1]);
|
||||
// The field is declared here; whether or not its type names a project
|
||||
// symbol, this owner is the answer — no other same-named struct applies.
|
||||
if (!fieldType) return null;
|
||||
return resolveMethodOnType(fieldType, methodName, ref, context, 0.85, 'instance-method');
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Split a camelCase or PascalCase string into words.
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user