mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-29 06:32:28 +00:00
* fix(cpp): resolve qualified receiver types by namespace, not by tail
Cause: a C++ receiver declared with a qualified type (`rp::C x = ...`,
`void f(a::Config c)`, `rp::Box<int> b`) typed nothing. The capture keeps
the qualifier, but the shared receiver lookups read a qualifier only as a
`.` namespace-import handle, and C++ has neither: `#include` binds no
handle and the index keys `rp::C` under `C`. The two places that did type
qualified spellings — the `auto x = ns::T{}` brace-init rule and qualified
member fields — reduced them to their tail, which binds the NEAREST
same-named class: `auto x = rp::C{}` reached a global `C` through another
include, and `a::Config` inside a `namespace b` declaring its own Config
reached `b::Config` (both confident wrong CALLS edges).
Fix: a ScopeResolver opt-in, `resolveQualifiedTypeSpellings`, that C++
sets. When a qualified declared or return type binds nothing,
`resolveClassBindingForName` and the compound resolver's return-type lookup
fall back to `resolveQualifiedInheritanceBase` — the route qualified base
classes and qualified type references already take: the qualifier is
matched against the candidate's namespace and a tie resolves nothing. It
runs after every existing lookup and after the template-argument match, so
it adds edges and never retargets one. The brace-init rule now captures the
whole qualified_identifier, and non-generic qualified member fields keep
their qualifier, so both resolve through the same route; generic members
stay tail-reduced because specialization matching keys on the bare base
name.
Corpus A/B (lang-resolution, all languages): no edge outside the new
fixture moved; drops unchanged. bench/scope-capture cpp re-baselined (one
brace-init capture text, plus the new fixture).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(cpp): bind qualified declarations that have no initializer
Cause: `rp::C x;` and `rp::C* x = nullptr;` emitted no type binding at
all. The plain-declaration annotation rules accept only a bare
`type_identifier` or `template_type`, and tree-sitter-cpp parses a
qualified type as a `qualified_identifier` wrapping one of those, so the
receiver of `x.m()` was untyped (no edge, and no drop either). The
initialized form `rp::C x = ...` was already captured by the `type: (_)`
assignment rule.
Fix: two @type-binding.annotation rules on the OUTER qualified_identifier
(plain declarator, and pointer inside an init_declarator) — the qualified
twins of the bare rules, one pattern per declarator shape like the
qualified member-field rules, with no node-type alternation. The spelling
is kept as written and resolved by the qualifier-aware route from the
previous commit.
Capture diff over the cpp corpus: 29 added @type-binding.annotation
matches on qualified locals, nothing removed or changed. Corpus A/B: no
edge or drop outside the new fixture moved. bench/scope-capture cpp
re-baselined.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(cpp): record return types of header function prototypes
Cause: only function DEFINITIONS (and in-class method prototypes)
recorded a return type. A free-function prototype — `D makeD();`,
`namespace rp { C make(); }` — recorded none, and the usual layout is
"header declares, .cpp defines": the .cpp is never included, so an
includer saw no return fact at all and `auto x = makeD(); x.m()` /
`makeD().m()` typed nothing (the inline form recorded a drop, the bound
form not even that).
Fix: @type-binding.return rules for a prototype with a plain or pointer
return. In a function body the same syntax declares a block-scope
function (the most vexing parse, `D x();`), whose binding would be
hoisted to the module and type every undeclared `x` in the file, so
captures.ts keeps a prototype match only at namespace scope — the
translation unit or a namespace / extern "C" body, through preprocessor
conditionals (include guards), single-declaration extern "C" and
template prefixes. A prototype declared in a namespace keeps its binding
in that namespace instead of the module: the binding's scope is also
where its type is looked up, and from the module a global `Widget` in
the same header won over `rp::Widget`. Callers read namespace members by
definition identity, which joins in any scope, and namespace members are
never mirrored to includers by bare name anyway.
A reference return (`D& f();`) gets no rule: such a function is not
declared as a Function at all (no declaration pattern), so a return fact
would have nothing to attach to.
Capture diff over the cpp corpus: one added match outside the new
fixture (cpp-return-type's header prototype, same type as its
definition); the gate drops only the fixture's deliberate `E y();`.
Corpus A/B: no edge or drop outside the fixture moved.
bench/scope-capture cpp re-baselined.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(scope): type an inline call receiver from its callee's own return type
Cause: the compound resolver's free-call branch (`make().m()`) read the
callee's return type BY NAME from the caller's scope chain, while the
chain follow that types the bound form (`auto x = make(); x.m()`) reads
it BY DEFINITION IDENTITY (`producerTypeRef`). A callee that arrives by a
channel nothing mirrors a return binding through — a C++ using-directive
— therefore typed the bound form and dropped the inline one.
Fix: when the name read finds nothing, fall back to the return type the
callee's definition declares, via `producerTypeRef` (now exported from
imported-return-types.ts; the only change there). Only for a callee the
name actually BINDS at the call: `findExportedDefByName` falls back to the
first workspace callable of that name, and reading a return type off that
guess is not evidence. Measured: without the binding check the corpus
gained 15 non-C++ edges (Go range/chain factories, Swift call results),
all reached through that workspace-name guess and one of them provably
wrong (swift-return-type's `user.save()` bound to another fixture's
User); with it, no edge outside the new fixture moves.
Corpus A/B: edges and drops outside the fixture identical to main.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(cpp): type receivers produced by a qualified factory callee
Cause: `auto x = rp::make(); x.m()` bound `x` to the literal type `auto`
— the bare `auto x = f()` constructor rule matches only an identifier
callee, so the generic assignment rule captured the placeholder — and
`rp::make().m()` reached the compound resolver as the free call
`rp::make`, whose name binds nothing. Neither had any route from the
qualified callee to its definition, so both typed nothing (`auto x =
rp::C()` likewise).
Fix:
- captures.ts promotes `auto x = ns::f()` / `auto x = ns::T()` (a
qualified_identifier whose innermost name is a plain identifier) to
@type-binding.constructor with the whole callee as the type, the
qualified twin of the bare rule; template factories keep their own
rule. interpret.ts drops any leftover `auto` / `decltype(auto)` binding,
which named no type.
- New ScopeResolver hook `splitQualifiedCallee` maps a callee spelling onto
the receiver/member pair the language's qualified CALL captures use (C++:
innermost scope segment, `a:🅱️:make` -> `b` / `make`), so shared code
never splits on `::`. The compound resolver's free-call branch, when the
callee's name binds nothing, resolves the definition through the existing
`resolveQualifiedReceiverMember` hook — the answer the call itself got —
and reads that definition's own return type by identity
(`producerTypeRef`). `auto x = rp::C()` resolves through the
qualified-type route instead.
Capture diff over the cpp corpus: one changed match outside the new
fixture (cpp-method-enrichment `auto category = Animal::classify(...)`
now binds the callee instead of `auto`; a class static method, which the
namespace lookup does not answer, so it stays untyped as before). Corpus
A/B: no edge or drop outside the fixture moved. bench/scope-capture cpp
re-baselined. The new cpp-captures rows also pin the namespace-scope gate
on prototype return types from the earlier commit.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(cpp): bind qualified receiver owners
Preserve complete namespace qualifiers and declaration-owned return facts. Resolve inline namespace and chained alias types without borrowing unrelated functions or classes; cover review collision and header-order cases.
* fix(cpp): honor qualified type visibility
Carry header alias activation positions through include chains. Refuse unresolved type-owned qualifiers without rejecting value names, and align the lexical-ownership fixture with the scope-index contract.
* style(cpp): format inline namespace lookup
* fix(cpp): preserve factory lookup position
Keep later local type declarations from hiding an earlier namespace
factory. Name overload and class-factory recall limits explicitly.
Refs #3656
* fix(cpp): expand factory return aliases
Apply the provider qualified-type hook in producer-return lookup.
Keep authoritative refusals and the original declaration context.
Refs #3656
---------
Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
349 lines
14 KiB
TypeScript
349 lines
14 KiB
TypeScript
import type { ScopeResolutionIndexes } from '../../../../src/core/ingestion/model/scope-resolution-indexes.js';
|
||
/**
|
||
* #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, SymbolDefinition } from 'gitnexus-shared';
|
||
import { beforeEach, describe, expect, it } from 'vitest';
|
||
import {
|
||
clearCppInlineNamespaces,
|
||
markCppInlineNamespaceRange,
|
||
populateCppInlineNamespaceScopes,
|
||
resolveCppQualifiedNamespaceMember,
|
||
splitCppQualifiedCallee,
|
||
} 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();
|
||
});
|
||
});
|
||
|
||
describe('splitCppQualifiedCallee', () => {
|
||
// Keep the written namespace path so a factory result cannot borrow a
|
||
// same-named nested namespace from another outer namespace.
|
||
it.each<[string, { receiver: string; member: string } | undefined]>([
|
||
['rp::make', { receiver: 'rp', member: 'make' }],
|
||
['a::b::make', { receiver: 'a::b', member: 'make' }],
|
||
['ns::Box<a::B>::make', { receiver: 'ns::Box<a::B>', member: 'make' }],
|
||
['make', undefined],
|
||
['::make', undefined],
|
||
['Box<a::B>', undefined],
|
||
])('%s -> %j', (callee, expected) => {
|
||
expect(splitCppQualifiedCallee(callee)).toEqual(expected);
|
||
});
|
||
});
|