mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-29 06:32:28 +00:00
fix(ingestion): own generic Rust inherent-impl methods through the mod-qualified Impl node (#1992) (#2003)
* fix(ingestion): own generic Rust inherent-impl methods through the mod-qualified Impl node (#1992) A generic inherent-impl target (`impl<T> Inner<T>`) is a `generic_type` node, which the inherent-impl owner walk (findEnclosingClassInfo) did not match — so the walk returned null and the method got `File -> DEFINES` with NO HAS_METHOD edge (orphaned, and invisible to findDanglingEdges). The Impl node was already correctly mod-qualified (the @name capture drills into the inner type_identifier, tree-sitter-queries.ts), so this is an owner-walk-only fix: drill into the generic base and mirror the node gate so the owner id == the node id byte-for-byte. A scoped-generic target (`impl<T> a::Inner<T>`) materializes no Impl node and is left orphaned (deferred) rather than minting a phantom owner. The owner walk is shared by the sequential and worker paths. New fixture + tests assert positive HAS_METHOD ownership through distinct `a.Inner` / `b.Inner` nodes on both resolver legs and the worker path, plus a negative scoped-generic guard. rust-captures-golden regenerated additively for the new fixture. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(ingestion): qualify className for same-tail Rust generic impls + regen rust bench baseline (#1992) F3 follow-up to #1992: two same-tail generic inherent impls under sibling mods that ALSO share a method name (`mod a { impl Inner { fn m } }` + `mod b { impl Inner { fn m } }`) keyed the method node id `${className}.${name}` with the bare tail (`Inner.m`) and collapsed onto one Function node (graph addNode is first-write-wins), silently dropping the second. The owner Impl `classId` was already mod-qualified, masking the collision behind distinct HAS_METHOD sources. Qualify `className` (`a.Inner` / `b.Inner`) in the bare inherent-impl arm so the node id inherits the mod scope; symmetric with the call-resolution fallback, and the HAS_METHOD owner anchors on the unchanged qualified classId. New same-method-name fixture + sequential & worker-parity tests; holds on both legs. Also regenerate the rust scope-capture bench baseline: the new rust-nested-tail-collision-generic (#1992) + rust-generic-impl-same-method-name (F3) fixtures grow the rust-* corpus, so the order-independent fingerprint drifts (56ffc1c0 -> b00aea0f, fixture_count 127 -> 129). Pure fixture-corpus drift — no scope-extractor change; existing fixtures' captures byte-identical. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(rust): regenerate rust-captures golden for the F3 same-method-name fixture (#1992) The rust-* scope-capture corpus is fingerprinted by TWO gates: the bench baseline (bench/scope-capture/baselines.json, already updated) and the rust-captures-golden unit test (test/fixtures/rust-captures-golden/expected-captures.json). Adding the F3 fixture rust-generic-impl-same-method-name grew the corpus 128->129 entries, so the committed golden drifted too. Regenerated additively (UPDATE_GOLDEN=1) — only the new fixture's entry is added; existing fixtures' captures are byte-identical. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
404beadd1a
commit
dee9195b33
6 changed files with 265 additions and 13 deletions
|
|
@ -28,10 +28,10 @@
|
|||
"scaling_budget": 1.5
|
||||
},
|
||||
"rust": {
|
||||
"fingerprint": "56ffc1c069af10cac3c82a32f3d148322ea570e116ebaae67315445f05407fef",
|
||||
"fingerprint": "b00aea0f2dbff6a77d3aa709f7f90e8a70649f7e789a8de725d9b1958ebe12bc",
|
||||
"scaling_budget": 1.5,
|
||||
"_rebaselined": "#1956 tri-review U1: rust-qualified-trait fixture (scoped + generic-of-scoped impl trait paths); bareTypeIdentifier now resolves scoped_type_identifier bases by their name: tail (additive, no existing-fixture drift); linear (~1.04). #1975: + rust-scoped-impl fixture (impl a::Inner / b::Inner inherent scoped impls) — legacy @definition.impl scoped arm + findEnclosingClassInfo inherent-impl scoped target; rust scope-extractor captures byte-identical.",
|
||||
"_note": "PR #1934: F66/F68 let-binding pattern narrowing; F71 union (Struct-labeled, now materialized via legacy @definition.struct + resolvable); F72 macro FULLY WIRED — @declaration.macro/@reference.macro + MacroRegistry → USES edges to Macro nodes (never a same-named fn). + rust-macro / rust-union fixtures and merged with origin/main #1975 rust-scoped-impl; fingerprint re-baselined (scaling ~0.99, fixture_count 126)."
|
||||
"_note": "PR #1934: F66/F68 let-binding pattern narrowing; F71 union (Struct-labeled, now materialized via legacy @definition.struct + resolvable); F72 macro FULLY WIRED — @declaration.macro/@reference.macro + MacroRegistry → USES edges to Macro nodes (never a same-named fn). + rust-macro / rust-union fixtures and merged with origin/main #1975 rust-scoped-impl; fingerprint re-baselined (scaling ~0.99, fixture_count 126). #1992: + rust-nested-tail-collision-generic and rust-generic-impl-same-method-name (F3) fixtures — pure fixture-corpus drift, no scope-extractor change; fixture_count 127->129, fingerprint 56ffc1c0->b00aea0f."
|
||||
},
|
||||
"php": {
|
||||
"fingerprint": "f9c8eaf6d1084f9b95a9fb97ccce5e618a24d936c85fb8af4b96c73a560f7a7f",
|
||||
|
|
|
|||
|
|
@ -503,18 +503,50 @@ export const findEnclosingClassInfo = (
|
|||
// different mods own through DISTINCT nodes. The Impl-node
|
||||
// materialization (parsing-processor / parse-worker) mirrors this, so
|
||||
// the owner id == the Impl node id byte-for-byte (#1982).
|
||||
const firstType = children.find(
|
||||
(c: SyntaxNode) => c.type === 'type_identifier' || c.type === 'scoped_type_identifier',
|
||||
// - GENERIC (`impl<T> Inner<T>`, generic_type): the @definition.impl
|
||||
// node is materialized only when the generic base is a bare
|
||||
// `type_identifier` (tree-sitter-queries.ts), qualified the same way —
|
||||
// so drill into the base and mirror that gate, keeping the owner id ==
|
||||
// the node id byte-for-byte (#1992). A generic over a SCOPED base
|
||||
// (`impl<T> a::Inner<T>`) materializes NO node, so it must produce NO
|
||||
// owner (the method orphans — scoped-generic deferred, #1992).
|
||||
const implTarget = children.find(
|
||||
(c: SyntaxNode) =>
|
||||
c.type === 'type_identifier' ||
|
||||
c.type === 'scoped_type_identifier' ||
|
||||
c.type === 'generic_type',
|
||||
);
|
||||
if (firstType) {
|
||||
const ownerKey =
|
||||
firstType.type === 'type_identifier'
|
||||
? qualifyRustImplTargetByModScope(current, firstType.text)
|
||||
: firstType.text;
|
||||
return {
|
||||
classId: generateId('Impl', `${filePath}:${ownerKey}`),
|
||||
className: firstType.text,
|
||||
};
|
||||
if (implTarget) {
|
||||
const baseType =
|
||||
implTarget.type === 'generic_type'
|
||||
? (implTarget.childForFieldName?.('type') ?? null)
|
||||
: implTarget;
|
||||
if (baseType?.type === 'type_identifier') {
|
||||
// Bare target (`impl Inner` or `impl<T> Inner<T>`): qualify by mod scope.
|
||||
// #1992 follow-up: qualify `className` too (not just `classId`). The
|
||||
// method node id is keyed `${className}.${name}`, so a bare tail collapses
|
||||
// two same-tail bare impls that ALSO share a method name (`a::Inner::m` +
|
||||
// `b::Inner::m` both → `Inner.m`) onto one Method node (graph addNode is
|
||||
// first-write-wins). Qualifying className → `a.Inner.m` / `b.Inner.m` keeps
|
||||
// them distinct. Symmetric: the call-resolution fallback rebuilds the same
|
||||
// `${className}.${name}` from the same enclosing-impl walk, so def and call
|
||||
// ids still agree. Owner edge anchors on `classId` (already qualified).
|
||||
const qualified = qualifyRustImplTargetByModScope(current, baseType.text);
|
||||
return {
|
||||
classId: generateId('Impl', `${filePath}:${qualified}`),
|
||||
className: qualified,
|
||||
};
|
||||
}
|
||||
if (baseType?.type === 'scoped_type_identifier' && implTarget.type !== 'generic_type') {
|
||||
// Top-level scoped `impl a::Inner`: key by full raw text (#1975).
|
||||
return {
|
||||
classId: generateId('Impl', `${filePath}:${baseType.text}`),
|
||||
className: baseType.text,
|
||||
};
|
||||
}
|
||||
// generic-over-scoped (`impl<T> a::Inner<T>`) and any other base: fall
|
||||
// through with no owner — no @definition.impl node exists, so attributing
|
||||
// a method to a synthesized id would orphan it against a phantom owner.
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
24
gitnexus/test/fixtures/lang-resolution/rust-generic-impl-same-method-name/lib.rs
vendored
Normal file
24
gitnexus/test/fixtures/lang-resolution/rust-generic-impl-same-method-name/lib.rs
vendored
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
// #1992 follow-up (F3): two same-tail generic inherent impls that ALSO share a
|
||||
// method name. Pre-fix the Method node id keys `${className}.${name}` with the
|
||||
// BARE tail (`Inner.m`), so `a::Inner::m` and `b::Inner::m` collapse onto ONE
|
||||
// Method node (graph addNode is first-write-wins). Both HAS_METHOD edges then
|
||||
// point at the survivor, silently losing the second method. Qualifying
|
||||
// `className` (`a.Inner` / `b.Inner`) keys them as `a.Inner.m` / `b.Inner.m`, so
|
||||
// BOTH Method nodes survive and each owns through its own mod-qualified Impl node.
|
||||
pub mod a {
|
||||
pub struct Inner<T> {
|
||||
v: T,
|
||||
}
|
||||
impl<T> Inner<T> {
|
||||
pub fn m(&self) {}
|
||||
}
|
||||
}
|
||||
|
||||
pub mod b {
|
||||
pub struct Inner<T> {
|
||||
v: T,
|
||||
}
|
||||
impl<T> Inner<T> {
|
||||
pub fn m(&self) {}
|
||||
}
|
||||
}
|
||||
30
gitnexus/test/fixtures/lang-resolution/rust-nested-tail-collision-generic/lib.rs
vendored
Normal file
30
gitnexus/test/fixtures/lang-resolution/rust-nested-tail-collision-generic/lib.rs
vendored
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
// #1992: GENERIC inherent-impl ownership. Two same-tail `Inner<T>` types under
|
||||
// sibling mods, each with a generic inherent impl `impl<T> Inner<T>`. Their
|
||||
// methods must own through DISTINCT mod-qualified Impl nodes (`a.Inner` /
|
||||
// `b.Inner`), not orphan to File.
|
||||
pub mod a {
|
||||
pub struct Inner<T> { v: T }
|
||||
impl<T> Inner<T> {
|
||||
pub fn fa(&self) {}
|
||||
}
|
||||
}
|
||||
|
||||
pub mod b {
|
||||
pub struct Inner<T> { v: T }
|
||||
impl<T> Inner<T> {
|
||||
pub fn fb(&self) {}
|
||||
}
|
||||
}
|
||||
|
||||
// Scoped-generic inherent impl: `impl<T> crate::c::Scoped<T>` is a `generic_type`
|
||||
// wrapping a `scoped_type_identifier`. tree-sitter-queries materializes NO
|
||||
// @definition.impl node for this shape, so `fd` must stay orphaned (scoped-generic
|
||||
// deferred, #1992) — the owner walk must NOT mint a phantom `c.Scoped` owner.
|
||||
pub mod c {
|
||||
pub struct Scoped<T> { v: T }
|
||||
}
|
||||
pub mod d {
|
||||
impl<T> crate::c::Scoped<T> {
|
||||
pub fn fd(&self) {}
|
||||
}
|
||||
}
|
||||
|
|
@ -219,6 +219,10 @@
|
|||
"captureGroups": 9,
|
||||
"digest": "a8562a331eb945b4c099a7a9ec6c9b5eed8ff603c9359897b0445ce316a1424e"
|
||||
},
|
||||
"rust-generic-impl-same-method-name/lib.rs": {
|
||||
"captureGroups": 19,
|
||||
"digest": "494ee20e8d5ebff07a8358938b706e15a48c9a9fb21e0ce361c2a8c391080352"
|
||||
},
|
||||
"rust-grouped-imports/src/helpers/mod.rs": {
|
||||
"captureGroups": 14,
|
||||
"digest": "32bc4f57a1dc0ffe8e9cbf0f0d6ce2ac5b1f2f93e2e3de3f217c92636480de63"
|
||||
|
|
@ -319,6 +323,10 @@
|
|||
"captureGroups": 18,
|
||||
"digest": "3326eb4f82b1559b6afec497dc52cab734e6f3209501a4bd982bf5eab9ec6dba"
|
||||
},
|
||||
"rust-nested-tail-collision-generic/lib.rs": {
|
||||
"captureGroups": 29,
|
||||
"digest": "1bfdaaf207a83924fc25d2eec0a47e5807754bbd0de499b81adea48342c6e687"
|
||||
},
|
||||
"rust-nested-tail-collision/lib.rs": {
|
||||
"captureGroups": 17,
|
||||
"digest": "2fc1fe1eb4e8727a89ab283ae34a0ae8df0c421551a7bd5e6e7ffb9d4aa54189"
|
||||
|
|
|
|||
|
|
@ -2102,6 +2102,164 @@ describe('Rust inline mod-nested same-tail collision — distinct nodes (issue #
|
|||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// #1992: GENERIC inherent-impl ownership — `impl<T> Inner<T>` methods own through
|
||||
// the mod-qualified Impl node, not orphaned to File.
|
||||
//
|
||||
// PR #1981 / `bc4a560d` qualified the UNSCOPED bare `impl Inner` target. A GENERIC
|
||||
// inherent-impl target (`impl<T> Inner<T>`) is a `generic_type` node, which the
|
||||
// inherent-impl owner walk (ast-helpers `findEnclosingClassInfo`) did not match —
|
||||
// so the walk returned null and the method got `File -> DEFINES` with NO HAS_METHOD
|
||||
// (orphaned; invisible to findDanglingEdges). The Impl NODE was already correctly
|
||||
// mod-qualified (the @name capture drills into the inner type_identifier,
|
||||
// tree-sitter-queries.ts), so the fix is owner-walk-only and the owner id == the
|
||||
// node id (`a.Inner` / `b.Inner`) by construction. Holds on both resolver legs
|
||||
// (structure-phase).
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Rust generic inherent-impl same-tail ownership — distinct nodes (issue #1992)', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(
|
||||
path.join(FIXTURES, 'rust-nested-tail-collision-generic'),
|
||||
() => {},
|
||||
);
|
||||
}, 60000);
|
||||
|
||||
it('owns fa / fb through distinct mod-qualified Impl nodes (generic impl, no orphan)', () => {
|
||||
const hm = getRelationships(result, 'HAS_METHOD');
|
||||
const a = hm.find((e) => e.target === 'fa');
|
||||
const b = hm.find((e) => e.target === 'fb');
|
||||
// Pre-fix the generic-impl owner walk returns null, so fa/fb orphan to File
|
||||
// (File -> DEFINES, no HAS_METHOD) — toBeDefined() fails on the pre-fix base.
|
||||
expect(a, 'HAS_METHOD -> fa').toBeDefined();
|
||||
expect(b, 'HAS_METHOD -> fb').toBeDefined();
|
||||
// Owner id is the mod-qualified Impl node, byte-identical to the node id.
|
||||
expect(a!.rel.sourceId).not.toBe(b!.rel.sourceId);
|
||||
expect(a!.rel.sourceId).toContain('a.Inner');
|
||||
expect(b!.rel.sourceId).toContain('b.Inner');
|
||||
expect(findDanglingEdges(result, ['HAS_METHOD'])).toEqual([]);
|
||||
});
|
||||
|
||||
// R6: scoped-generic `impl<T> crate::c::Scoped<T>` materializes no Impl node, so
|
||||
// `fd` must NOT own through a phantom `c.Scoped` node — it stays orphaned
|
||||
// (deferred). Guards against the owner walk minting an owner id for an
|
||||
// unmaterialized node.
|
||||
it('does not mint a phantom owner for a scoped-generic impl (fd orphaned, deferred)', () => {
|
||||
const hm = getRelationships(result, 'HAS_METHOD');
|
||||
expect(hm.find((e) => e.target === 'fd')).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
// Same fixture forced through the WORKER pool (parse-worker.ts). The inherent-impl
|
||||
// owner walk is shared structure-phase logic, so generic-impl ownership must hold
|
||||
// on BOTH the sequential and worker paths.
|
||||
describe('Rust generic inherent-impl ownership — worker path parity (issue #1992)', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(
|
||||
path.join(FIXTURES, 'rust-nested-tail-collision-generic'),
|
||||
() => {},
|
||||
{ workerThresholdsForTest: { minFiles: 1, minBytes: 1 }, workerPoolSize: 2 },
|
||||
);
|
||||
}, 120000);
|
||||
|
||||
it('genuinely used the worker pool', () => {
|
||||
expect(result.usedWorkerPool).toBe(true);
|
||||
});
|
||||
|
||||
it('owns fa / fb through distinct mod-qualified Impl nodes on the worker path', () => {
|
||||
const hm = getRelationships(result, 'HAS_METHOD');
|
||||
const a = hm.find((e) => e.target === 'fa');
|
||||
const b = hm.find((e) => e.target === 'fb');
|
||||
expect(a, 'HAS_METHOD -> fa').toBeDefined();
|
||||
expect(b, 'HAS_METHOD -> fb').toBeDefined();
|
||||
expect(a!.rel.sourceId).not.toBe(b!.rel.sourceId);
|
||||
expect(a!.rel.sourceId).toContain('a.Inner');
|
||||
expect(b!.rel.sourceId).toContain('b.Inner');
|
||||
expect(findDanglingEdges(result, ['HAS_METHOD'])).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// F3 (#1992 follow-up) — same-tail generic impls that ALSO share a method name
|
||||
// must materialize DISTINCT method (Function) nodes.
|
||||
//
|
||||
// `${className}.${methodName}` keys the method node id (Rust `fn`s carry the
|
||||
// `Function` label). Before this fix the bare inherent-impl arm set `className` to
|
||||
// the bare tail (`Inner`), so two same-tail generic impls under sibling mods that
|
||||
// each define `fn m` both keyed `Function:…:Inner.m#0` and collapsed onto ONE node
|
||||
// (graph addNode is first-write-wins) — the second `m` was silently dropped and
|
||||
// both HAS_METHOD edges targeted the survivor. The owner `classId` was already
|
||||
// mod-qualified, so HAS_METHOD *sources* stayed distinct, which masked the
|
||||
// collision (sourceId-only assertions passed). Qualifying `className`
|
||||
// (`a.Inner` / `b.Inner`) keys `a.Inner.m` / `b.Inner.m`, so both nodes survive
|
||||
// with distinct ids. Structure-phase, so it holds on both resolver legs and the
|
||||
// worker path.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Rust same-tail generic impls with shared method name — distinct nodes (issue #1992)', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(
|
||||
path.join(FIXTURES, 'rust-generic-impl-same-method-name'),
|
||||
() => {},
|
||||
);
|
||||
}, 60000);
|
||||
|
||||
it('materializes two distinct `m` method nodes (no first-write-wins collapse)', () => {
|
||||
// Pre-fix: only one `m` Function node survives (the second is dropped on the
|
||||
// colliding id) — length is 1, so toBe(2) fails on the pre-fix base.
|
||||
const methods = getNodesByLabel(result, 'Function').filter((n) => n === 'm');
|
||||
expect(methods.length).toBe(2);
|
||||
});
|
||||
|
||||
it('owns each `m` through its own mod-qualified Impl node (distinct source AND target)', () => {
|
||||
const hm = getRelationships(result, 'HAS_METHOD').filter((e) => e.target === 'm');
|
||||
expect(hm.length).toBe(2);
|
||||
// Owner edges were always distinct (classId is mod-qualified)…
|
||||
expect(hm[0].rel.sourceId).not.toBe(hm[1].rel.sourceId);
|
||||
const sources = [hm[0].rel.sourceId, hm[1].rel.sourceId].sort();
|
||||
expect(sources[0]).toContain('a.Inner');
|
||||
expect(sources[1]).toContain('b.Inner');
|
||||
// …but the TARGET node collapsed pre-fix — this is the F3 assertion.
|
||||
expect(hm[0].rel.targetId).not.toBe(hm[1].rel.targetId);
|
||||
expect(findDanglingEdges(result, ['HAS_METHOD'])).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
// Same fixture forced through the WORKER pool — the impl owner walk + node-id
|
||||
// keying is shared structure-phase logic, so the distinct-node guarantee must hold
|
||||
// on the worker path too (parse-worker.ts mirrors parsing-processor.ts).
|
||||
describe('Rust same-tail generic impls with shared method name — worker path parity (issue #1992)', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(
|
||||
path.join(FIXTURES, 'rust-generic-impl-same-method-name'),
|
||||
() => {},
|
||||
{ workerThresholdsForTest: { minFiles: 1, minBytes: 1 }, workerPoolSize: 2 },
|
||||
);
|
||||
}, 120000);
|
||||
|
||||
it('genuinely used the worker pool', () => {
|
||||
expect(result.usedWorkerPool).toBe(true);
|
||||
});
|
||||
|
||||
it('materializes two distinct `m` method nodes on the worker path', () => {
|
||||
const methods = getNodesByLabel(result, 'Function').filter((n) => n === 'm');
|
||||
expect(methods.length).toBe(2);
|
||||
const hm = getRelationships(result, 'HAS_METHOD').filter((e) => e.target === 'm');
|
||||
expect(hm.length).toBe(2);
|
||||
expect(hm[0].rel.sourceId).not.toBe(hm[1].rel.sourceId);
|
||||
expect(hm[0].rel.targetId).not.toBe(hm[1].rel.targetId);
|
||||
expect(findDanglingEdges(result, ['HAS_METHOD'])).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// F71 — union declarations resolve as Struct nodes (issue #1934)
|
||||
//
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue