Skip to content
4 changes: 2 additions & 2 deletions gitnexus/bench/scope-capture/baselines.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
54 changes: 43 additions & 11 deletions gitnexus/src/core/ingestion/utils/ast-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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) {}
}
}
Original file line number Diff line number Diff line change
@@ -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) {}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down
158 changes: 158 additions & 0 deletions gitnexus/test/integration/resolvers/rust.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
//
Expand Down
Loading