mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-08-28 05:25:25 +00:00
* fix(cpp): resolve cross-namespace same-tail inheritance bases bridge-held (#1993) PR #1981's bridge fixed within-namespace same-tail heritage (NS::A::Inner vs NS::B::Inner). The residual: a cross-namespace same-tail base (NS1::A::Inner vs NS2::A::Inner) both key the namespace-omitted `A.Inner` in the qualifiedNames index, so resolveQualifiedInheritanceBase couldn't pick a winner and the deriving classes cross-wired (DB's EXTENDS bound to NS1's A::Inner). Fixed bridge-held via the existing `namespacePrefix` sidecar — no qualifiedName invariant flip, no resolution-index re-keying: (1) tagNamespacePrefixes also tags defs declared directly in a namespace (the deriving NS1::DA), composed identically to the class-nested path; (2) resolveQualifiedInheritanceBase breaks a same-tail tie by preferring the candidate whose namespacePrefix matches the deriving class's. Two-phase lookup, UDC, brace-init, file-local linkage untouched (def.qualifiedName + index keys unchanged). New cpp-cross-namespace-same-tail fixture + registry-primary test (in the cpp parity expected-failures). Verified: cpp suite 287/287 primary, 209 + 78 skips legacy — no regression; tsc + prettier clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(cpp): worker-path parity for #1993 cross-namespace tie-break + correct narrative Add the missing parse-worker.ts parity describe for the #1993 cross-namespace same-tail heritage tie-break, mirroring the #1982/#1995 worker siblings (workerThresholdsForTest minFiles:1/minBytes:1, workerPoolSize:2, usedWorkerPool guard, and the same NS1.DA→NS1.A.Inner / NS2.DB→NS2.A.Inner base assertions), and register both worker test names in LEGACY_RESOLVER_PARITY_EXPECTED_FAILURES['cpp'] (registry-primary-only, like the sequential entry). Closes the DoD sequential≡worker gap flagged in the tri-review of PR #2005. Also correct the fixture/test narrative: the pre-fix failure is a CROSS-WIRE (DB's EXTENDS binds NS1::A::Inner via the refuse-on-tie scope-walk fallback), not a silent miss — the empirical pre-fix run shows the edge exists but points at the wrong target. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(scope-resolution): type the namespacePrefix sidecar; regen cpp bench baseline (#1993) F4 follow-up to #1993: declare `namespacePrefix?: string` on SymbolDefinition (gitnexus-shared) and drop the six `as { namespacePrefix?: string }` casts in walkers.ts / graph-bridge/ids.ts that #1993 introduced. Pure type-level — the `as` assertions erase at compile time, runtime is byte-identical, and the field stays a sidecar (no graph-node identity; the qualifiedName-keyed index is untouched). Also regenerate the cpp scope-capture bench baseline: rebased onto main (now carrying #1995's cpp fixtures), #1993 adds cpp-cross-namespace-same-tail, growing the cpp-* corpus 272->273 and drifting the fingerprint d63ded6->6d6207ae. Pure fixture-corpus drift — no scope-extractor change. 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
e316222cd5
commit
9f3bcee7fc
7 changed files with 158 additions and 5 deletions
|
|
@ -59,4 +59,13 @@ export interface SymbolDefinition {
|
|||
isExplicit?: boolean;
|
||||
/** Links Method/Constructor/Property to owning Class/Struct/Trait nodeId */
|
||||
ownerId?: string;
|
||||
/** #1982/#1993: bridge-held enclosing-namespace path (e.g. `NS1`, `Outer.Inner`)
|
||||
* tagged during the C++ resolution phase. Lets the graph bridge retry a
|
||||
* namespace-prefixed node-lookup key and lets the qualified-base resolver
|
||||
* break same-tail cross-namespace inheritance ties. A deliberate sidecar,
|
||||
* separate from `qualifiedName`: it does NOT participate in graph node
|
||||
* identity (node keys derive from filePath/type/qualifiedName) and leaves the
|
||||
* qualifiedName-keyed resolution index untouched. Absent for the common case
|
||||
* (non-namespace-nested defs and all non-C++ languages). */
|
||||
namespacePrefix?: string;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,11 +16,11 @@
|
|||
"_added": "#1956: c added to the scope-capture bench (was UNBENCHED). C has no inheritance \u2014 flat scale source. Adding it exposed + fixed a pre-existing O(n^2) findNodeAtRange root-walk in c/captures.ts (threaded c.node, byte-identical over c-* fixtures); scaling 3.475 -> 0.96."
|
||||
},
|
||||
"cpp": {
|
||||
"fingerprint": "d63ded6251a89d42cc63941ac3fdb093bf5b59ae483b135786766f925cdc91c5",
|
||||
"fingerprint": "6d6207ae1df3943c5fae28983e0c294e55225456e7cf39af1d46fda21b6787c4",
|
||||
"scaling_budget": 1.5,
|
||||
"_added": "#1956: cpp added to the scope-capture bench (was UNBENCHED). Heritage-bearing scale source (: public Base, public Mixin) drives emitCppInheritanceCaptures at scale. Adding it exposed + fixed a pre-existing O(n^2) findNodeAtRange root-walk in cpp/captures.ts (~12 sites, threaded c.node, byte-identical over 263 cpp-* fixtures); scaling 2.30 -> 1.12.",
|
||||
"_rebaselined": "#1965 / #1923 F4: uninitialized non-leading multi-declarators now emit @declaration.variable captures; cpp-adl-inner-callable-outer-noncallable data::Pair a, b adds the legitimate fixture drift. Linear (~1.06).",
|
||||
"_note": "#1975: + cpp-out-of-line-class fixture, fixture_count 263->265. #1990: + cpp-adl-ns-plus-hidden-friend-same-name fixture (ADL hidden-friend + namespace-callable merge parity test). Pure fixture-corpus drift — no scope-extractor change; existing fixtures' captures byte-identical. fixture_count 265->267. #1995: + cpp-union-nested-tail-collision and cpp-anon-ns-tail-collision fixtures — pure fixture-corpus drift; fixture_count 270->272, fingerprint 538e8be->d63ded6."
|
||||
"_note": "#1975: + cpp-out-of-line-class fixture, fixture_count 263->265. #1990: + cpp-adl-ns-plus-hidden-friend-same-name fixture (ADL hidden-friend + namespace-callable merge parity test). Pure fixture-corpus drift — no scope-extractor change; existing fixtures' captures byte-identical. fixture_count 265->267. #1995: + cpp-union-nested-tail-collision and cpp-anon-ns-tail-collision fixtures — pure fixture-corpus drift; fixture_count 270->272, fingerprint 538e8be->d63ded6. #1993: + cpp-cross-namespace-same-tail fixture — pure fixture-corpus drift; fixture_count 272->273, fingerprint d63ded6->6d6207ae."
|
||||
},
|
||||
"csharp": {
|
||||
"_rebaselined": "#1956 synth-widening: + csharp-qualified-base fixture; the synth now walks record_declaration + struct_declaration base_lists and handles alias_qualified_name (matching the #1940 legacy leg), so record/struct heritage now emits. csharp-record-base gains a record inherits capture. (record->record SAME-namespace EXTENDS is a separate registry resolution gap, tracked as follow-up.) Linear (~1.00). (Earlier #1956: heritage-bearing scale source.)",
|
||||
|
|
|
|||
|
|
@ -80,6 +80,8 @@ export function resolveDefGraphId(
|
|||
parameterTypeClasses?: readonly ParameterTypeClass[];
|
||||
templateArguments?: readonly string[];
|
||||
templateConstraints?: unknown;
|
||||
/** #1982 bridge-held namespace path; see `SymbolDefinition.namespacePrefix`. */
|
||||
namespacePrefix?: string;
|
||||
},
|
||||
nodeLookup: GraphNodeLookup,
|
||||
): string | undefined {
|
||||
|
|
@ -149,7 +151,7 @@ export function resolveDefGraphId(
|
|||
// namespace-prefixed key (tagged by `tagNamespacePrefixes`) BEFORE the
|
||||
// simple-name fallback, so same-tail nested bases don't collapse across
|
||||
// sibling namespace members via `simpleKey`.
|
||||
const nsPrefix = (def as { namespacePrefix?: string }).namespacePrefix;
|
||||
const nsPrefix = def.namespacePrefix;
|
||||
if (nsPrefix !== undefined && nsPrefix.length > 0) {
|
||||
const nsHit = nodeLookup.get(qualifiedKey(filePath, def.type, `${nsPrefix}.${qn}`));
|
||||
if (nsHit !== undefined) return nsHit;
|
||||
|
|
|
|||
|
|
@ -385,7 +385,27 @@ function resolveQualifiedInheritanceBase(
|
|||
}
|
||||
}
|
||||
if (count === 1) return unique;
|
||||
if (count > 1) return undefined; // genuine tie at this key → refuse, don't guess
|
||||
if (count > 1) {
|
||||
// #1993: same-tail bases collide at this namespace-omitted key (`NS1::A::Inner`
|
||||
// and `NS2::A::Inner` both key `A.Inner`). Break the tie with the bridge's
|
||||
// `namespacePrefix` sidecar — prefer the candidate in the SAME enclosing
|
||||
// namespace as the deriving class. Bridge-held: `def.qualifiedName` and the
|
||||
// index keys are untouched; still refuse when the sidecar can't pick a unique.
|
||||
const childPrefix = enclosingClassDef?.namespacePrefix;
|
||||
if (childPrefix !== undefined && childPrefix.length > 0) {
|
||||
let nsUnique: SymbolDefinition | undefined;
|
||||
let nsCount = 0;
|
||||
for (const id of ids) {
|
||||
const def = scopes.defs.get(id);
|
||||
if (def !== undefined && isClassLike(def.type) && def.namespacePrefix === childPrefix) {
|
||||
nsUnique = def;
|
||||
nsCount++;
|
||||
}
|
||||
}
|
||||
if (nsCount === 1) return nsUnique;
|
||||
}
|
||||
return undefined; // genuine tie → refuse, don't guess
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
|
@ -853,7 +873,32 @@ export function tagNamespacePrefixes(parsed: ParsedFile): void {
|
|||
const q = def.qualifiedName;
|
||||
if (q === undefined || q.length === 0) continue;
|
||||
if (q === prefix || q.startsWith(`${prefix}.`)) continue; // already namespaced
|
||||
(def as { namespacePrefix?: string }).namespacePrefix = prefix;
|
||||
def.namespacePrefix = prefix;
|
||||
}
|
||||
}
|
||||
|
||||
// #1993: also tag defs declared DIRECTLY in a Namespace scope with that
|
||||
// namespace's OWN full path. The loop above only reaches class-nested defs
|
||||
// (`A::Inner`); a deriving class like `NS1::DA` lives in the namespace scope and
|
||||
// is skipped, so it would carry no prefix and a same-tail cross-namespace base
|
||||
// tie (`NS1::A::Inner` vs `NS2::A::Inner`) could not be broken by the deriving
|
||||
// side. Composed identically to the class-nested path (enclosing tails + own
|
||||
// tail) so the two agree; still sidecar-only (`qualifiedName` untouched).
|
||||
for (const scope of parsed.scopes) {
|
||||
if (scope.kind !== 'Namespace') continue;
|
||||
const ownNsDef = scope.ownedDefs.find((d) => d.type === 'Namespace');
|
||||
const ownQ = ownNsDef?.qualifiedName;
|
||||
if (ownQ === undefined || ownQ.length === 0) continue;
|
||||
const ownTail = ownQ.slice(ownQ.lastIndexOf('.') + 1);
|
||||
const parentPrefix = namespacePrefixOf(scope);
|
||||
const fullPrefix = parentPrefix.length > 0 ? `${parentPrefix}.${ownTail}` : ownTail;
|
||||
for (const def of scope.ownedDefs) {
|
||||
if (def.type === 'Namespace') continue;
|
||||
const q = def.qualifiedName;
|
||||
if (q === undefined || q.length === 0) continue;
|
||||
if (q === fullPrefix || q.startsWith(`${fullPrefix}.`)) continue; // already namespaced
|
||||
if (def.namespacePrefix !== undefined) continue;
|
||||
def.namespacePrefix = fullPrefix;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
23
gitnexus/test/fixtures/lang-resolution/cpp-cross-namespace-same-tail/main.cpp
vendored
Normal file
23
gitnexus/test/fixtures/lang-resolution/cpp-cross-namespace-same-tail/main.cpp
vendored
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
// Cross-namespace same-tail nested heritage (#1993).
|
||||
//
|
||||
// NS1::A::Inner and NS2::A::Inner are distinct nested types whose scope-model
|
||||
// def.qualifiedName both drops the enclosing namespace and reads `A.Inner`. They
|
||||
// collide in the qualifiedNames resolution index, so resolveQualifiedInheritanceBase
|
||||
// hit refuse-on-tie and the scope-walk fallback first-won to NS1's Inner — DB
|
||||
// CROSS-WIRED its EXTENDS to NS1::A::Inner (DA resolved correctly only by that
|
||||
// first-wins luck). The cross-wire still lands on a real node, so findDanglingEdges
|
||||
// stays blind to it. The `namespacePrefix` sidecar breaks the tie (bridge-held):
|
||||
// DA's enclosing namespace NS1 selects NS1::A::Inner.
|
||||
namespace NS1 {
|
||||
struct A {
|
||||
struct Inner {};
|
||||
};
|
||||
struct DA : A::Inner {};
|
||||
} // namespace NS1
|
||||
|
||||
namespace NS2 {
|
||||
struct A {
|
||||
struct Inner {};
|
||||
};
|
||||
struct DB : A::Inner {};
|
||||
} // namespace NS2
|
||||
|
|
@ -4188,6 +4188,73 @@ describe('C++ namespaced same-tail nested heritage — worker path parity (issue
|
|||
// `A.Inner` key is tried → the global type. Registry-primary only.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Cross-namespace same-tail nested heritage — bridge-held tie-break (issue #1993)
|
||||
//
|
||||
// NS1::A::Inner and NS2::A::Inner both key the namespace-omitted `A.Inner` in the
|
||||
// qualifiedNames index, so resolveQualifiedInheritanceBase refused-on-tie and the
|
||||
// scope-walk fallback first-wins to NS1's Inner — DB CROSS-WIRES its EXTENDS to
|
||||
// NS1::A::Inner (DA resolves correctly only by that first-wins luck). The cross-wire
|
||||
// still resolves to a real node, so findDanglingEdges can't catch it, and the #1982
|
||||
// bridge can't reach it either (it rescues the structure-phase node lookup, not the
|
||||
// resolution-index tie). The `namespacePrefix` sidecar breaks the tie: DA's enclosing
|
||||
// namespace NS1 selects NS1::A::Inner. Bridge-held — def.qualifiedName and the index
|
||||
// keys are unchanged. Registry-primary only (the qualified-base resolver is the bridge).
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('C++ cross-namespace same-tail nested heritage — bridge-held tie-break (issue #1993)', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(
|
||||
path.join(FIXTURES, 'cpp-cross-namespace-same-tail'),
|
||||
() => {},
|
||||
);
|
||||
}, 60000);
|
||||
|
||||
it('routes NS1.DA EXTENDS NS1.A.Inner and NS2.DB EXTENDS NS2.A.Inner (no cross-ns tie)', () => {
|
||||
const extendsEdges = getRelationships(result, 'EXTENDS');
|
||||
const baseQnOf = (derivedQn: string) => {
|
||||
const e = extendsEdges.find(
|
||||
(x) => result.graph.getNode(x.rel.sourceId)?.properties.qualifiedName === derivedQn,
|
||||
);
|
||||
expect(e, `EXTENDS from ${derivedQn}`).toBeDefined();
|
||||
return result.graph.getNode(e!.rel.targetId)?.properties.qualifiedName;
|
||||
};
|
||||
expect(baseQnOf('NS1.DA')).toBe('NS1.A.Inner');
|
||||
expect(baseQnOf('NS2.DB')).toBe('NS2.A.Inner');
|
||||
});
|
||||
});
|
||||
|
||||
describe('C++ cross-namespace same-tail nested heritage — worker path parity (issue #1993)', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(
|
||||
path.join(FIXTURES, 'cpp-cross-namespace-same-tail'),
|
||||
() => {},
|
||||
{ workerThresholdsForTest: { minFiles: 1, minBytes: 1 }, workerPoolSize: 2 },
|
||||
);
|
||||
}, 120000);
|
||||
|
||||
it('genuinely used the worker pool for the cross-namespace fixture', () => {
|
||||
expect(result.usedWorkerPool).toBe(true);
|
||||
});
|
||||
|
||||
it('routes NS1.DA / NS2.DB to their own namespaced base on the worker path (no cross-ns tie)', () => {
|
||||
const extendsEdges = getRelationships(result, 'EXTENDS');
|
||||
const baseQnOf = (derivedQn: string) => {
|
||||
const e = extendsEdges.find(
|
||||
(x) => result.graph.getNode(x.rel.sourceId)?.properties.qualifiedName === derivedQn,
|
||||
);
|
||||
expect(e, `EXTENDS from ${derivedQn} (worker)`).toBeDefined();
|
||||
return result.graph.getNode(e!.rel.targetId)?.properties.qualifiedName;
|
||||
};
|
||||
expect(baseQnOf('NS1.DA')).toBe('NS1.A.Inner');
|
||||
expect(baseQnOf('NS2.DB')).toBe('NS2.A.Inner');
|
||||
});
|
||||
});
|
||||
|
||||
describe('C++ root-anchored base ignores enclosing-relative type (issue #1982)', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
|
|
|
|||
|
|
@ -621,6 +621,13 @@ const LEGACY_RESOLVER_PARITY_EXPECTED_FAILURES: Readonly<Record<string, Readonly
|
|||
'resolves NS::DB : B::Inner → EXTENDS the NS.B.Inner node (not NS.A.Inner)',
|
||||
'genuinely used the worker pool for the namespaced fixture',
|
||||
'resolves NS::DA / NS::DB to their own namespaced base on the worker path',
|
||||
// #1993: cross-namespace same-tail base, broken by the namespacePrefix sidecar
|
||||
// tie-break in the registry-primary resolveQualifiedInheritanceBase. Legacy only.
|
||||
'routes NS1.DA EXTENDS NS1.A.Inner and NS2.DB EXTENDS NS2.A.Inner (no cross-ns tie)',
|
||||
// #1993 worker-path parity (mirrors the sequential entry above): the sidecar
|
||||
// tie-break must resolve identically on the parse-worker path. Registry-primary only.
|
||||
'genuinely used the worker pool for the cross-namespace fixture',
|
||||
'routes NS1.DA / NS2.DB to their own namespaced base on the worker path (no cross-ns tie)',
|
||||
// #1982 follow-up: C++ worker-path DerivedA parity + duplicate guard.
|
||||
'resolves DerivedA : Outer::Inner → EXTENDS Outer.Inner on the worker path (parity + no duplicate)',
|
||||
// #1982 follow-up: root-anchored base must not bind to an enclosing-relative
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue