mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-09-06 08:16:02 +00:00
* perf(cpp): index qualified namespace members once per pipeline run (#2788)
`resolveCppQualifiedNamespaceMember` walked every parsed file — rebuilding a
per-file `scopesById` map each time — once per qualified `ns::member()` call
site, so the scope-resolution emit phase cost O(callsites x scopes). On a
1,473-file C++ repo that was 25.3 min of a 33-min analyze, with 75% of total
self-time in this one function. Its inner `findMemberInNamespaceTransitive`
compounded it: each recursion step filtered `scopesById.values()` by parent,
O(scopes^2) per file on its own.
This is the same bug #1990 fixed in the sibling ADL path (`pickCppAdlCandidates`
-> `AdlCandidateIndex`), so it gets the same fix: a `QualifiedNsMemberIndex`
(receiver simple name -> member simple name -> callable defs) built lazily once
per `parsedFiles` identity and reset by `clearCppInlineNamespaces`, which runs
from `cppScopeResolver.loadResolutionConfig` at the start of every pass. Per
call site the work drops to two Map lookups.
Ordering is preserved exactly — file-major, `parsed.scopes` declaration order,
a namespace's own `ownedDefs` before its inline-namespace children, depth-first
— because the caller takes `allHits[0]` for the single-hit case and
`narrowOverloadCandidates` is first-wins. Non-inline nested namespaces are
still not descended into, and same-name hits across inline children still
report `'ambiguous'` (#1564).
Measured with `PROF_SCOPE_RESOLUTION=1 analyze --force --index-only` on a
synthetic corpus (`namespace ns_i { inline namespace v1 { ... } }` plus 20
`ns_j::fn()` call sites per file):
| files | emit before | emit after |
|-------|-------------|------------|
| 100 | 153ms | 16ms |
| 200 | 704ms | 24ms |
| 400 | 3,293ms | 42ms |
| 800 | 16,898ms | 78ms |
Before, doubling the file count quadrupled emit; now it doubles. At 800 files
total scope resolution goes 17.2s -> 394ms.
Output is unchanged, verified rather than assumed: a full graph dump (sorted
nodes + relationships) from a baseline build at the parent commit and from this
one are byte-identical on all 134 `cpp-*` fixtures merged into a single repo
(1573 nodes / 1997 relationships) and on the 400-file synthetic corpus.
`test/integration/resolvers/cpp.test.ts` passes 334/334.
#1990 shipped its ADL fix without a scaling gate, which is how the bug class
came straight back here, so this adds one: `bench/cpp-qualified-ns` measures
`(t_large/t_small)/(1600/400)` — 0.93-1.21 indexed versus 3.45 for the old
per-call-site scan — alongside a fingerprint over every
`receiver::member -> outcome` the corpus resolves, and CI runs it with
`--check`. `test/unit/cpp-qualified-ns-index.test.ts` covers the cache
invalidation the index introduces.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(cpp): address tri-review findings on the qualified-namespace index (#2788)
Multi-engine review of this PR (Claude swarm + ce-code-review, Codex
gpt-5.6-sol swarm + ce + adversarial) returned two P1s and five smaller
findings. All are fixed here.
P1 — the index defeated the pipeline's post-language memory release.
`scope-resolution/pipeline/phase.ts` evicts each language's files and then
calls `forceGc()`, on a stated premise that "This language's ParsedFiles are
now unreachable", sizing C/C++ at ~17-20GB on the Linux kernel. The
module-level `let qualifiedNsIndexSource` falsified that: it pinned the whole
`parsedFiles` array, and the index held defs reaching into those files'
scopes, until the *next* C++ pass cleared it — which in a single analyze never
comes. C++ is 7th of 16 in SCOPE_RESOLVERS, so the set survived nine later
language passes plus emit. Replaced with a
`WeakMap<readonly ParsedFile[], QualifiedNsMemberIndex>`, the pattern already
used by `moduleScopeIndexByPass` in `cpp/file-local-linkage.ts`.
`clearCppInlineNamespaces` still swaps in a fresh WeakMap, because the index
has a second input (`inlineNamespaceScopeIds`) the key cannot observe.
Measured with `--expose-gc`: 61.2MB retained after the caller drops the array
before, 0.1MB after.
The ADL twin (`adl.ts`) has the same pattern, so the hazard predates this PR —
but `pickCppAdlCandidates` returns early before `ensureAdlIndex` on
`noAdlSites`/empty `argInfoBySite`, so it rarely arms, whereas a qualified
`ns::member()` index arms on almost every C++ workspace. Moving the ADL twin
to a WeakMap is left as a follow-up.
P1 — the new bench could not see the regression class it exists to gate.
`callSites()` drew every receiver from `ns_${...}`, so the receiver lookup
never missed; production is the opposite, since Case 1.5 in
`receiver-bound-calls.ts` is reached by every plain-identifier receiver call
and misses on most. A rescan reintroduced only on the receiver-bucket-absent
path scored 1.279 and PASSED the old bench. The corpus now mirrors production
(~1 in 5 receivers name a declared namespace) and adds a namespace reopened
across files, a same-name inline nest, a member declared at both namespace and
inline-child level, and call sites carrying a real `Callsite` so
`narrowOverloadCandidates`/`cppConversionRank`/
`isOverloadAmbiguousAfterNormalization` are inside the fingerprinted surface at
all. That same rescan now measures 4.538 and FAILS; defeating the dedup now
fails the fingerprint arm where it previously passed byte-identical. The
fingerprint moved once, deliberately, for the corpus expansion — recorded in
`_rebaseline_2788_review`, explicitly not precedent.
Also fixed:
- Unbounded recursion aborted analyze. `collectNamespaceMembers` recursed per
inline child with no bound and threw an uncontained `RangeError` at inline
depth 8000 (`phase.ts`'s try has a `finally`, no `catch`), and a receiver
*miss* paid full recursion where the deleted walker skipped on a name
mismatch. An explicit work-stack alone would only have converted that into
an OOM at depth 6000, because the eager table was quadratic in memory too:
for a depth-D chain it legitimately holds D(D+1)/2 entries, since `v2::foo()`
is a valid receiver at every level. Replaced with a lazily-queried node graph
(per-scope own-member buckets plus direct child links, resolved on demand and
memoized per receiver+member). Build is now linear; depth 100000 costs 133ms
where 8000 previously threw.
- "#1990 shipped without a scaling gate" was false. #1990 did ship
`test/integration/cpp-adl-benchmark.test.ts` (f1b843838). Corrected in the
bench header and the CI step comment. The accurate point is narrower and
stronger: that bench asserts `callsResolved === 0`, so it never drives the
qualified-receiver path, and it is `skipIf(!GITNEXUS_BENCH)` while the only
step setting that variable lists neither C++ bench — so it has never run in
CI. Wiring it in is a follow-up.
- "Ordering is load-bearing" was not a live property: `allHits[0]` is only
reached at length 1, and both tail branches return `'ambiguous'`. Order is
still preserved for byte-identity with the pre-#2788 walker; the comment now
says that instead, and the test named for ordering is renamed to the
parent/inline-child visibility it actually asserts.
- "Same contract as `ensureAdlIndex`" overstated parity — the sibling ships a
`validateAdlSeqCoverage` guard because it reads
`seqByNodeId.get(...) ?? 0`, which can silently collapse candidates. This
index has no analogous defaulting read, so no guard is added; the comment now
says why.
- Two coverage gaps closed, both mutation-verified: cross-file merge of one
namespace reopened in two files (no existing test covered it — confirmed by
making each file clobber the previous and watching only the new test fail),
and the same-name inline nest whose dedup, when defeated, flips a resolved
def to `'ambiguous'`.
- `resolveCppQualifiedNamespaceMember`'s JSDoc now names both production call
sites, including the callsite-less `resolveAdlCandidates` path.
- Memoized candidate buckets are frozen, so a future in-place sort in
`overload-narrowing.ts` throws instead of silently corrupting later
resolutions now that the array is shared across call sites.
- `_scaling_note`'s "measured 0.93-1.21" band did not reproduce; it is now the
honestly observed 1.28-1.45, with the small arm widened to ~14ms (halves the
spread) and a triage line saying a scaling failure is a timing signal to
re-run, unlike the deterministic fingerprint arm.
Verification: 840,000 differential probes against the pre-#2788 walker
extracted from base, 0 mismatches, plus 48,000 candidate-order comparisons,
0 mismatches. cpp resolver integration suite 334/334. Unit suite 10/10.
tsc, eslint, prettier clean. Bench --check PASS.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(bench): escape the NUL separator so measure.mjs stays a text file
The fingerprint key separator was written as a literal NUL byte instead of the
`\u0000` escape. Git classifies any file containing a NUL as binary, so the
whole bench showed as `Bin` with no diff on GitHub and could not be reviewed —
the same defect this branch already fixed once before the tri-review.
Escaping it is byte-for-byte equivalent at runtime (both produce U+0000), so
the committed fingerprint is unchanged and `--check` still passes.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* refactor(cpp): quality cleanups from a four-angle review of the #2788 series
Reuse, simplification, efficiency and altitude passes over the diff. No
resolution behaviour changes: the bench fingerprint is unchanged and the C++
resolver integration suite still passes in full.
Efficiency
- The `Object.freeze` added in the review-fix commit to close an aliasing
residual costs 4.6x on the narrowing path — V8 moves frozen arrays to
PACKED_FROZEN_ELEMENTS, off the fast path for the `.filter`/`.map`/`.some`
runs `narrowOverloadCandidates` does at every multi-candidate call site.
The hazard it guards is already a compile error (both the memo and the
parameter are `readonly SymbolDefinition[]`), so it is now a dev-only
tripwire. Gated on `isSemanticModelValidatorEnabled()` — the repo's opt-IN
form used by `phase.ts` and `validate-bindings-immutability.ts` — not
`adl.ts`'s opt-out `NODE_ENV !== 'production'`, which would keep paying the
cost in CLI runs where `NODE_ENV` is unset. Large bench arm 100.3 -> 70.6ms.
- The index build scanned every scope in every file twice; pass 1 now collects
`[scope, node]` pairs for pass 2 to iterate. 800k scopes 14.40 -> 8.21ms.
- `simpleNameOf` uses `lastIndexOf('.')` + `slice` instead of
`split('.').pop()` (83 -> 24 ns/call), semantics verified byte-equivalent
over 12 edge cases including `undefined`, `''`, `'a.'`, `'.b'` and `'a..b'`.
- The per-call `hookCtx` literal is hoisted to a module const.
- The bench's fingerprint pass resolved 960k call sites to produce 5,800
distinct outcomes; it now dedups on the key it already builds. Bench wall
time 2.4 -> 1.9s, fingerprint byte-identical.
Reuse
- `bucketOwnMembers` used an inlined `Function | Method | Constructor` compare;
it now calls the canonical `isOverloadableCallable`. `graph-bridge/ids.ts`
already carries a note that inlining this list recreated twin-list drift once.
Altitude
- `adl.ts` had the same retention defect this series just fixed next door: a
module-level `let adlIndex` + `let adlIndexSource` strongly pinning the whole
`parsedFiles` array until the next C++ pass, which in a single analyze never
comes. Converted to the same `WeakMap` shape. Measured with `--expose-gc`:
89.11MB retained after the caller drops the array before, 0.17MB after. Six
file-local helpers now take the index as a parameter; no exported signature
changed.
- The index's freshness depended on `clearCppInlineNamespaces()` being called
from another file, guarded only by a warning paragraph. An epoch bumped in
both `populateCppInlineNamespaceScopes` and the clear is now stored with the
memo, so a missed clear degrades to a rebuild instead of a stale answer —
confirmed by driving inline state mid-pass without the clear. Roughly line
neutral, since it replaces most of the paragraph.
- `test/integration/cpp-adl-benchmark.test.ts` is wired into the
`GITNEXUS_BENCH` step. It is `skipIf`-gated and was absent from that step's
explicit file list, so #1990's ADL emit-scaling guard had never executed in
CI. It passes; ~50s added to a 25-minute job. `cpp-pipeline-benchmark.test.ts`
is deliberately NOT wired: it costs 115s for guards covering generic
per-language pipeline scaling that nothing here touches.
Simplification
- Deleted a comment referencing a `inlineChildrenByParent` map that only ever
existed inside this branch's own first commit, so "the legacy map" pointed a
reader at code that never shipped.
- Dropped three unreachable `undefined` guards (`strict: false`, no
`noUncheckedIndexedAccess`), keeping the load-bearing `visited` check.
- Compressed the `rootsByReceiver` doc from 17 lines to 7 — it was the longest
comment in the file and guarded the least consequential property — the
`validateAdlSeqCoverage` paragraph from 7 lines to 3, and turned three
restatements of the uncaught-throw and dedup arguments into pointers.
- Test fixtures: dropped the dead `'Module'` union arm, added a one-line `ns()`
builder for the nine hand-written scope literals, and moved the file to
`test/unit/scope-resolution/cpp/` where every other C++ scope-resolution unit
test lives. 403 -> 337 lines, same 10 tests, and the cross-file mutation check
still fails exactly one test.
Not done, and why: merging this index into `AdlCandidateIndex` (they key on
different names with different inline-transparency depth — a refactor with
correctness risk, not a cleanup); `ScopeTree.getChildren` (trades in-memory
`parsed.scopes` for store hits on a hot path); a shared `cpp/` util for the five
pre-existing `simpleName` copies; sharing fixtures across the bench/test
boundary (no precedent in this repo); and an exact-count arm for the bench,
which is a gate redesign worth its own change.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
337 lines
13 KiB
TypeScript
337 lines
13 KiB
TypeScript
/**
|
||
* #2788 — `resolveCppQualifiedNamespaceMember` serves qualified `ns::member()`
|
||
* lookups from a per-pipeline index instead of rescanning every parsed file per
|
||
* call site. These tests pin the properties the index must not lose:
|
||
*
|
||
* 1. Transitive inline-namespace collection — parent- and child-level member
|
||
* visibility through one receiver — and same-name ambiguity (#1564): the
|
||
* semantics the old linear scan provided.
|
||
* 2. Cross-file accumulation — C++ namespaces are open, so one receiver's
|
||
* members are spread over however many files reopen it. The legacy scan
|
||
* re-derived its hits per call site and got this for free; the index has
|
||
* to merge across the whole `parsedFiles` array to match it.
|
||
* 3. Cache invalidation — a new `parsedFiles` array, or a
|
||
* `clearCppInlineNamespaces()` between passes, must not serve stale hits.
|
||
* This is the failure mode the index introduces; nothing else covers it.
|
||
*/
|
||
|
||
import type {
|
||
ParsedFile,
|
||
ScopeId,
|
||
ScopeResolutionIndexes,
|
||
SymbolDefinition,
|
||
} from 'gitnexus-shared';
|
||
import { beforeEach, describe, expect, it } from 'vitest';
|
||
import {
|
||
clearCppInlineNamespaces,
|
||
markCppInlineNamespaceRange,
|
||
populateCppInlineNamespaceScopes,
|
||
resolveCppQualifiedNamespaceMember,
|
||
} from '../../../../src/core/ingestion/languages/cpp/inline-namespaces.js';
|
||
|
||
const NO_SCOPES = {} as unknown as ScopeResolutionIndexes;
|
||
|
||
interface ScopeSpec {
|
||
readonly id: string;
|
||
readonly parent: string | null;
|
||
readonly defs: readonly SymbolDefinition[];
|
||
/** Distinguishes each scope's range so inline marking targets exactly one. */
|
||
readonly line: number;
|
||
}
|
||
|
||
function def(nodeId: string, type: string, qualifiedName: string): SymbolDefinition {
|
||
return { nodeId, type, qualifiedName } as unknown as SymbolDefinition;
|
||
}
|
||
|
||
function nsDef(nodeId: string, qualifiedName: string): SymbolDefinition {
|
||
return def(nodeId, 'Namespace', qualifiedName);
|
||
}
|
||
|
||
function fnDef(nodeId: string, qualifiedName: string): SymbolDefinition {
|
||
return def(nodeId, 'Function', qualifiedName);
|
||
}
|
||
|
||
function ns(
|
||
id: string,
|
||
parent: string | null,
|
||
defs: readonly SymbolDefinition[],
|
||
line: number,
|
||
): ScopeSpec {
|
||
return { id, parent, defs, line };
|
||
}
|
||
|
||
function range(line: number): {
|
||
startLine: number;
|
||
startCol: number;
|
||
endLine: number;
|
||
endCol: number;
|
||
} {
|
||
return { startLine: line, startCol: 0, endLine: line + 1, endCol: 0 };
|
||
}
|
||
|
||
/** Build one `ParsedFile` from scope specs, marking the scopes named in
|
||
* `inlineIds` as inline namespaces (capture-time range mark +
|
||
* `populateOwners`-time scope-id resolution, same order as the pipeline).
|
||
* Separate from {@link makeParsedFiles} so a test can compose a genuinely
|
||
* MULTI-file `parsedFiles` array, which is the only way to exercise the
|
||
* index's cross-file accumulation. */
|
||
function makeParsedFile(
|
||
filePath: string,
|
||
specs: readonly ScopeSpec[],
|
||
inlineIds: readonly string[],
|
||
): ParsedFile {
|
||
const parsed = {
|
||
filePath,
|
||
scopes: specs.map((s) => ({
|
||
id: s.id as unknown as ScopeId,
|
||
kind: 'Namespace',
|
||
parent: s.parent as unknown as ScopeId | null,
|
||
ownedDefs: s.defs,
|
||
range: range(s.line),
|
||
})),
|
||
} as unknown as ParsedFile;
|
||
markInline(parsed, specs, inlineIds);
|
||
return parsed;
|
||
}
|
||
|
||
/** Single-file `parsedFiles` array — the shape most of these tests need. */
|
||
function makeParsedFiles(
|
||
filePath: string,
|
||
specs: readonly ScopeSpec[],
|
||
inlineIds: readonly string[],
|
||
): readonly ParsedFile[] {
|
||
return [makeParsedFile(filePath, specs, inlineIds)];
|
||
}
|
||
|
||
/** Capture-time inline marking + `populateOwners`-time scope-id resolution,
|
||
* in the same order the pipeline runs them. Spec lookup is a Map, not
|
||
* `Array.find`, so marking a deep chain stays linear in `specs`. */
|
||
function markInline(
|
||
parsed: ParsedFile,
|
||
specs: readonly ScopeSpec[],
|
||
inlineIds: readonly string[],
|
||
): void {
|
||
const byId = new Map(specs.map((s) => [s.id, s]));
|
||
for (const id of inlineIds) {
|
||
const spec = byId.get(id);
|
||
if (spec === undefined) throw new Error(`inline scope ${id} must exist`);
|
||
markCppInlineNamespaceRange(parsed.filePath, range(spec.line));
|
||
}
|
||
populateCppInlineNamespaceScopes(parsed);
|
||
}
|
||
|
||
/** `namespace outer { <ownDefs> inline namespace v1 { <inlineDefs> } }` */
|
||
function outerWithInlineChild(
|
||
filePath: string,
|
||
ownDefs: readonly SymbolDefinition[],
|
||
inlineDefs: readonly SymbolDefinition[],
|
||
): readonly ParsedFile[] {
|
||
return makeParsedFiles(
|
||
filePath,
|
||
[
|
||
ns('sc:outer', null, [nsDef('n:outer', 'outer'), ...ownDefs], 1),
|
||
ns('sc:v1', 'sc:outer', [nsDef('n:v1', 'outer.v1'), ...inlineDefs], 10),
|
||
],
|
||
['sc:v1'],
|
||
);
|
||
}
|
||
|
||
/** `namespace n0 { inline namespace n1 { … inline namespace n<depth-1> {
|
||
* void leaf(); } … } }` — one function, declared in the innermost namespace,
|
||
* reachable by qualified lookup from every level above it. */
|
||
function inlineChain(filePath: string, depth: number): readonly ParsedFile[] {
|
||
const specs: ScopeSpec[] = [];
|
||
for (let d = 0; d < depth; d++) {
|
||
const self = nsDef(`n:ns${d}`, `n${d}`);
|
||
specs.push(
|
||
ns(
|
||
`sc:n${d}`,
|
||
d === 0 ? null : `sc:n${d - 1}`,
|
||
d === depth - 1 ? [self, fnDef('n:leaf', `n${d}.leaf`)] : [self],
|
||
d * 2 + 1,
|
||
),
|
||
);
|
||
}
|
||
// Every level below the outermost is `inline`, so the whole chain is one
|
||
// transitively-visible run of namespaces.
|
||
return makeParsedFiles(
|
||
filePath,
|
||
specs,
|
||
specs.slice(1).map((s) => s.id),
|
||
);
|
||
}
|
||
|
||
describe('C++ qualified-namespace member index (#2788)', () => {
|
||
beforeEach(() => {
|
||
clearCppInlineNamespaces();
|
||
});
|
||
|
||
it('resolves outer::foo through an inline-namespace child', () => {
|
||
const files = outerWithInlineChild('a.cpp', [], [fnDef('n:foo@v1', 'outer.v1.foo')]);
|
||
expect(resolveCppQualifiedNamespaceMember('outer', 'foo', files, NO_SCOPES)).toMatchObject({
|
||
nodeId: 'n:foo@v1',
|
||
});
|
||
});
|
||
|
||
it('returns undefined for an unknown namespace or member', () => {
|
||
const files = outerWithInlineChild('a.cpp', [], [fnDef('n:foo@v1', 'outer.v1.foo')]);
|
||
expect(resolveCppQualifiedNamespaceMember('nope', 'foo', files, NO_SCOPES)).toBeUndefined();
|
||
expect(resolveCppQualifiedNamespaceMember('outer', 'nope', files, NO_SCOPES)).toBeUndefined();
|
||
});
|
||
|
||
it('does not descend into a non-inline nested namespace', () => {
|
||
const files = makeParsedFiles(
|
||
'a.cpp',
|
||
[
|
||
ns('sc:outer', null, [nsDef('n:outer', 'outer')], 1),
|
||
ns(
|
||
'sc:nested',
|
||
'sc:outer',
|
||
[nsDef('n:nested', 'outer.nested'), fnDef('n:foo@nested', 'outer.nested.foo')],
|
||
10,
|
||
),
|
||
],
|
||
[],
|
||
);
|
||
expect(resolveCppQualifiedNamespaceMember('outer', 'foo', files, NO_SCOPES)).toBeUndefined();
|
||
expect(resolveCppQualifiedNamespaceMember('nested', 'foo', files, NO_SCOPES)).toMatchObject({
|
||
nodeId: 'n:foo@nested',
|
||
});
|
||
});
|
||
|
||
it('reports same-name hits across two inline children as ambiguous (#1564)', () => {
|
||
const files = makeParsedFiles(
|
||
'a.cpp',
|
||
[
|
||
ns('sc:outer', null, [nsDef('n:outer', 'outer')], 1),
|
||
ns('sc:v1', 'sc:outer', [nsDef('n:v1', 'outer.v1'), fnDef('n:foo@v1', 'outer.v1.foo')], 10),
|
||
ns('sc:v2', 'sc:outer', [nsDef('n:v2', 'outer.v2'), fnDef('n:foo@v2', 'outer.v2.foo')], 20),
|
||
],
|
||
['sc:v1', 'sc:v2'],
|
||
);
|
||
expect(resolveCppQualifiedNamespaceMember('outer', 'foo', files, NO_SCOPES)).toBe('ambiguous');
|
||
});
|
||
|
||
it('resolves through an inline namespace that reuses its parent’s name', () => {
|
||
// Pins the `visited` dedup: `namespace ns { inline namespace ns { … } }`
|
||
// registers both scopes under receiver `ns`, and without dedup the one
|
||
// `foo` is collected twice and degrades to 'ambiguous', dropping the CALLS
|
||
// edge. Why node identity is the right dedup key: see
|
||
// `gatherQualifiedNsMember` in `inline-namespaces.ts`.
|
||
const files = makeParsedFiles(
|
||
'a.cpp',
|
||
[
|
||
ns('sc:ns@outer', null, [nsDef('n:ns@outer', 'ns')], 1),
|
||
ns(
|
||
'sc:ns@inner',
|
||
'sc:ns@outer',
|
||
[nsDef('n:ns@inner', 'ns.ns'), fnDef('n:foo', 'ns.ns.foo')],
|
||
10,
|
||
),
|
||
],
|
||
['sc:ns@inner'],
|
||
);
|
||
expect(resolveCppQualifiedNamespaceMember('ns', 'foo', files, NO_SCOPES)).toMatchObject({
|
||
nodeId: 'n:foo',
|
||
});
|
||
});
|
||
|
||
it('finds both a namespace-owned member and its inline child member', () => {
|
||
// Both levels are visible through the same receiver: `foo` is owned by
|
||
// `outer` itself, `bar` only by its inline child `v1`. Distinct member
|
||
// names per level on purpose — two same-named candidates with no call-site
|
||
// info collapse to 'ambiguous', which would assert nothing about either.
|
||
const files = outerWithInlineChild(
|
||
'a.cpp',
|
||
[fnDef('n:foo@outer', 'outer.foo')],
|
||
[fnDef('n:bar@v1', 'outer.v1.bar')],
|
||
);
|
||
expect(resolveCppQualifiedNamespaceMember('outer', 'foo', files, NO_SCOPES)).toMatchObject({
|
||
nodeId: 'n:foo@outer',
|
||
});
|
||
expect(resolveCppQualifiedNamespaceMember('outer', 'bar', files, NO_SCOPES)).toMatchObject({
|
||
nodeId: 'n:bar@v1',
|
||
});
|
||
});
|
||
|
||
it('resolves through a 20,000-deep inline chain without exhausting the stack', () => {
|
||
// Pins that a deep chain neither overflows the stack nor blows the heap —
|
||
// the two failure modes a recursive build and an eager member table had.
|
||
// Why an uncaught throw here aborts the whole `analyze`: see
|
||
// `QualifiedNsMemberIndex` in `inline-namespaces.ts`. 20,000 is ~2.5x the
|
||
// depth that overflowed in this same runner, for margin over per-platform
|
||
// stack sizes.
|
||
const files = inlineChain('deep.cpp', 20_000);
|
||
// From the outermost namespace: the full chain is one transitive walk.
|
||
expect(resolveCppQualifiedNamespaceMember('n0', 'leaf', files, NO_SCOPES)).toMatchObject({
|
||
nodeId: 'n:leaf',
|
||
});
|
||
// Every inline namespace is a legal qualified receiver of its own, so
|
||
// mid-chain and innermost receivers must resolve too.
|
||
expect(resolveCppQualifiedNamespaceMember('n10000', 'leaf', files, NO_SCOPES)).toMatchObject({
|
||
nodeId: 'n:leaf',
|
||
});
|
||
expect(resolveCppQualifiedNamespaceMember('n19999', 'leaf', files, NO_SCOPES)).toMatchObject({
|
||
nodeId: 'n:leaf',
|
||
});
|
||
// A miss down the same chain stays a miss rather than becoming a throw.
|
||
expect(resolveCppQualifiedNamespaceMember('n0', 'nosuch', files, NO_SCOPES)).toBeUndefined();
|
||
});
|
||
|
||
it('merges one namespace’s members across every file that reopens it', () => {
|
||
// C++ namespaces are open: `namespace outer { void a(); }` in one file and
|
||
// `namespace outer { void b(); }` in another are the SAME namespace, and
|
||
// `outer::a()` / `outer::b()` must both resolve. The legacy scan re-derived
|
||
// its hits from all of `parsedFiles` per call site; the index accumulates
|
||
// once, so the per-receiver state has to outlive the file loop. Distinct
|
||
// scope ids per file, as the real pipeline mints them.
|
||
const files: readonly ParsedFile[] = [
|
||
makeParsedFile(
|
||
'a.cpp',
|
||
[ns('sc:outer@a', null, [nsDef('n:outer@a', 'outer'), fnDef('n:a', 'outer.a')], 1)],
|
||
[],
|
||
),
|
||
makeParsedFile(
|
||
'b.cpp',
|
||
[ns('sc:outer@b', null, [nsDef('n:outer@b', 'outer'), fnDef('n:b', 'outer.b')], 1)],
|
||
[],
|
||
),
|
||
];
|
||
expect(resolveCppQualifiedNamespaceMember('outer', 'a', files, NO_SCOPES)).toMatchObject({
|
||
nodeId: 'n:a',
|
||
});
|
||
expect(resolveCppQualifiedNamespaceMember('outer', 'b', files, NO_SCOPES)).toMatchObject({
|
||
nodeId: 'n:b',
|
||
});
|
||
});
|
||
|
||
it('does not serve one parsedFiles array’s index to another', () => {
|
||
const first = outerWithInlineChild('a.cpp', [], [fnDef('n:foo@a', 'outer.v1.foo')]);
|
||
expect(resolveCppQualifiedNamespaceMember('outer', 'foo', first, NO_SCOPES)).toMatchObject({
|
||
nodeId: 'n:foo@a',
|
||
});
|
||
const second = outerWithInlineChild('b.cpp', [], [fnDef('n:foo@b', 'outer.v1.foo')]);
|
||
expect(resolveCppQualifiedNamespaceMember('outer', 'foo', second, NO_SCOPES)).toMatchObject({
|
||
nodeId: 'n:foo@b',
|
||
});
|
||
});
|
||
|
||
it('rebuilds after clearCppInlineNamespaces even when parsedFiles is reused', () => {
|
||
// Pass 1: `v1` is inline, so `outer::foo` reaches through it.
|
||
const specs: readonly ScopeSpec[] = [
|
||
ns('sc:outer', null, [nsDef('n:outer', 'outer')], 1),
|
||
ns('sc:v1', 'sc:outer', [nsDef('n:v1', 'outer.v1'), fnDef('n:foo@v1', 'outer.v1.foo')], 10),
|
||
];
|
||
const files = makeParsedFiles('a.cpp', specs, ['sc:v1']);
|
||
expect(resolveCppQualifiedNamespaceMember('outer', 'foo', files, NO_SCOPES)).toMatchObject({
|
||
nodeId: 'n:foo@v1',
|
||
});
|
||
|
||
// Pass 2: SAME `parsedFiles` reference (so identity alone would serve the
|
||
// cached index), but `v1` is no longer inline. Without the index reset in
|
||
// `clearCppInlineNamespaces` the stale pass-1 hit survives.
|
||
clearCppInlineNamespaces();
|
||
markInline(files[0], specs, []);
|
||
expect(resolveCppQualifiedNamespaceMember('outer', 'foo', files, NO_SCOPES)).toBeUndefined();
|
||
});
|
||
});
|