diff --git a/gitnexus/src/core/ingestion/languages/cpp/adl.ts b/gitnexus/src/core/ingestion/languages/cpp/adl.ts index 112502a74..4c7b034b6 100644 --- a/gitnexus/src/core/ingestion/languages/cpp/adl.ts +++ b/gitnexus/src/core/ingestion/languages/cpp/adl.ts @@ -1,5 +1,5 @@ /** - * C++ argument-dependent lookup (ADL / Koenig lookup) — V1. + * C++ argument-dependent lookup (ADL / Koenig lookup). * * When ordinary unqualified lookup fails for a free-call site, ADL also * considers candidates declared in the **associated namespaces** of the @@ -13,17 +13,18 @@ * `using` anything. With V1 ADL: `audit::record` is discovered via * `audit::Event`'s associated namespace. * - * ## V1 boundary + * ## Current boundary * - * V1 covers ONE associated-entity rule: an argument that's a directly-named + * The current implementation covers ONE associated-entity rule: an argument that's a directly-named * class type (`audit::Event e`) contributes its **direct enclosing - * namespace** to the candidate set. Anything else — pointer/reference + * namespace** to the candidate set. V2 extends that one step to + * pointer-typed class args (`audit::Event* p`, `audit::Event** pp`): + * they contribute the pointee class's enclosing namespace too. Reference * arguments, function-pointer arguments, template specializations, - * base-class associated namespaces — is V2 closure work and is - * deliberately excluded. The `cpp-adl-pointer-arg-boundary` fixture - * locks the exclusion in CI. + * base-class associated namespaces, and the rest of the full closure are + * still deliberately excluded. * - * V1 also short-circuits to ADL only when ordinary lookup is empty + * The current implementation also short-circuits to ADL only when ordinary lookup is empty * (`findCallableBindingInScope` returned undefined). ISO C++ would * normally merge ADL candidates with ordinary-lookup candidates and * run overload resolution over the union; V1 defers that merge to V2. @@ -60,16 +61,16 @@ import { } from '../../scope-resolution/passes/overload-narrowing.js'; /** - * Per-argument shape information collected at capture time. ADL only - * fires for arguments where `simpleClassName !== ''` AND `!isPointer` - * AND `!isReference` — i.e., directly-named class-type values. + * Per-argument shape information collected at capture time. ADL fires for + * arguments where `simpleClassName !== ''` AND `!isReference`, including + * class pointers whose declarator chain resolves to a named class type. */ export interface CppAdlArgInfo { /** Simple class-like type name (last segment of qualified name); empty * for primitives, literals, function pointers, template specs, etc. */ readonly simpleClassName: string; - /** True when the variable's declarator was a `pointer_declarator`. V1 - * excludes pointer-typed args (closure rules deferred to V2). */ + /** True when the variable's declarator contained one or more + * `pointer_declarator` wrappers. */ readonly isPointer: boolean; /** True when the variable's declarator was a `reference_declarator`. */ readonly isReference: boolean; @@ -151,8 +152,8 @@ export function populateCppAssociatedNamespaces(parsed: ParsedFile): void { * * Fires only when: * - the call site is not in `noAdlSites` (parenthesized form), AND - * - at least one argument is a directly-named class type (not pointer, - * not reference, not literal/primitive). + * - at least one argument resolves to a named class type (value or + * pointer, but not reference, function pointer, literal, or primitive). */ export function pickCppAdlCandidates( site: { @@ -170,11 +171,11 @@ export function pickCppAdlCandidates( const args = argInfoBySite.get(key); if (args === undefined || args.length === 0) return undefined; - // Collect associated namespace QNames from every value-class-typed arg. + // Collect associated namespace QNames from every participating class-typed arg. const associatedNamespaces = new Set(); for (const arg of args) { if (arg.simpleClassName === '') continue; - if (arg.isPointer || arg.isReference) continue; + if (arg.isReference) continue; const classDef = findCppClassDefBySimpleName(arg.simpleClassName, scopes); if (classDef === undefined) continue; const nsQName = classToNamespaceQualifiedName.get(classDef.nodeId); diff --git a/gitnexus/src/core/ingestion/languages/cpp/captures.ts b/gitnexus/src/core/ingestion/languages/cpp/captures.ts index 325742bfb..17b6962aa 100644 --- a/gitnexus/src/core/ingestion/languages/cpp/captures.ts +++ b/gitnexus/src/core/ingestion/languages/cpp/captures.ts @@ -720,14 +720,15 @@ function isParenthesizedFunctionCall(callNode: SyntaxNode): boolean { /** * Per-argument ADL classification: walk each argument of a free call and - * decide whether it's a directly-named class type (V1 ADL fires) or - * something V1 excludes (pointer, reference, primitive, literal, function - * pointer, template specialization). + * decide whether it resolves to a directly-named class or class-pointer + * type (ADL fires) or to an excluded shape such as a reference, function + * pointer, primitive, literal, or template specialization. * - * V1 only fires for value class-typed args: `void f(N::S); N::S s; f(s);`. - * Pointer args (`N::S* p; f(p);`) intentionally return `simpleClassName=''` - * to lock the V1 boundary — the `cpp-adl-pointer-arg-boundary` fixture - * regression-tests this. + * Class-typed values and class pointers (`N::S`, `N::S*`, `N::S**`) all + * preserve the pointee class name for associated-namespace lookup. + * Function pointers remain excluded even when their return type names a + * class, because the associated entity is the pointed-to function type, + * not the return type. */ function inferCppCallAdlArgs(callNode: SyntaxNode): CppAdlArgInfo[] { const argList = callNode.childForFieldName('arguments'); @@ -792,15 +793,23 @@ function lookupAdlIdentifierType(identNode: SyntaxNode): CppAdlArgInfo { // Unwrap declarator chain to find pointer/reference markers and the // variable name. `init_declarator > pointer_declarator > identifier` - // means pointer-typed; `init_declarator > reference_declarator > ...` - // means reference-typed; bare `init_declarator > identifier` is value. + // means pointer-typed; repeated pointer wrappers still count as pointer + // typed; `init_declarator > reference_declarator > ...` means + // reference-typed; bare `init_declarator > identifier` is value. + // Function-pointer wrappers (`pointer_declarator > function_declarator`) + // must not contribute ADL associated namespaces. let isPointer = false; let isReference = false; + let isFunctionPointer = false; let inner: SyntaxNode = declarator; let nameText: string | null = null; let safety = 16; // bound walk depth defensively while (safety-- > 0) { if (inner.type === 'pointer_declarator') { + if (findFirstDescendantOfType(inner, 'function_declarator') !== null) { + isFunctionPointer = true; + break; + } isPointer = true; const next = inner.childForFieldName('declarator'); if (next === null) break; @@ -828,11 +837,15 @@ function lookupAdlIdentifierType(identNode: SyntaxNode): CppAdlArgInfo { inner = next; continue; } + if (inner.type === 'function_declarator') { + isFunctionPointer = true; + break; + } // Reached the leaf — usually `identifier`. Take its text. nameText = inner.text; break; } - if (nameText !== varName) continue; + if (isFunctionPointer || nameText !== varName) continue; const simpleClassName = extractAdlSimpleTypeName(typeNode); return { simpleClassName, isPointer, isReference }; @@ -841,9 +854,9 @@ function lookupAdlIdentifierType(identNode: SyntaxNode): CppAdlArgInfo { } /** Extract the simple class-like type name from a `type:` field node. - * Returns '' for primitives, template specializations, function pointers, - * and any other shape V1 ADL doesn't support — those args are excluded - * from associated-namespace closure. */ + * Returns '' for primitives, template specializations, and any other + * unsupported type-only shape. Function pointers are filtered at the + * declarator level in `lookupAdlIdentifierType`. */ function extractAdlSimpleTypeName(typeNode: SyntaxNode): string { if (typeNode.type === 'primitive_type') return ''; if (typeNode.type === 'sized_type_specifier') return ''; diff --git a/gitnexus/src/core/ingestion/languages/cpp/scope-resolver.ts b/gitnexus/src/core/ingestion/languages/cpp/scope-resolver.ts index 02aaff3bf..431717d27 100644 --- a/gitnexus/src/core/ingestion/languages/cpp/scope-resolver.ts +++ b/gitnexus/src/core/ingestion/languages/cpp/scope-resolver.ts @@ -36,6 +36,10 @@ import { resolveCppQualifiedNamespaceMember, } from './inline-namespaces.js'; import { populateCppRangeBindings } from './range-bindings.js'; +import { + isOverloadAmbiguousAfterNormalization, + narrowOverloadCandidates, +} from '../../scope-resolution/passes/overload-narrowing.js'; /** * C++ `ScopeResolver` registered in `SCOPE_RESOLVERS` and consumed by @@ -178,6 +182,8 @@ export const cppScopeResolver: ScopeResolver = { // for cross-file propagation and compound-receiver chain resolution. // cppBindingScopeFor hoists @type-binding.return to Module scope. hoistTypeBindingsToModule: true, + // Enable receiver-bound explicit-`this` fallback only for C++. + resolveThisViaEnclosingClass: true, // The `isFileLocalDef` hook on the global free-call fallback names // file-local linkage historically, but semantically gates "logically // invisible cross-file" defs. C++ extends this to also reject class- @@ -219,6 +225,33 @@ export const cppScopeResolver: ScopeResolver = { // V1 limitation: only direct enclosing-namespace closure for value // class-typed args; pointer/reference/template-spec args excluded. resolveAdlCandidates: (site, callerParsed, scopes, parsedFiles) => { + // `using ns::name;` introduces `name` into ordinary unqualified lookup. + // For template-class method bodies, lexical scope walks can miss this + // named-using visibility; recover by resolving the imported namespace + // member directly when the local call name matches a named using import. + const usingNamedHits: SymbolDefinition[] = []; + const seenUsing = new Set(); + for (const imp of callerParsed.parsedImports) { + if (imp.kind !== 'named') continue; + if (imp.localName !== site.name) continue; + const member = resolveCppQualifiedNamespaceMember( + imp.targetRaw, + imp.importedName, + parsedFiles, + scopes, + ); + if (member === undefined) continue; + if (seenUsing.has(member.nodeId)) continue; + seenUsing.add(member.nodeId); + usingNamedHits.push(member); + } + if (usingNamedHits.length > 0) { + const narrowed = narrowOverloadCandidates(usingNamedHits, site.arity, site.argumentTypes); + if (isOverloadAmbiguousAfterNormalization(narrowed, site.arity)) return 'ambiguous'; + if (narrowed.length === 1) return narrowed[0]; + if (narrowed.length > 1) return 'ambiguous'; + } + const result = pickCppAdlCandidates(site, callerParsed, scopes, parsedFiles); if (result === ADL_AMBIGUOUS) return 'ambiguous'; return result; diff --git a/gitnexus/src/core/ingestion/scope-resolution/contract/scope-resolver.ts b/gitnexus/src/core/ingestion/scope-resolution/contract/scope-resolver.ts index d29b6efa8..36856e6a8 100644 --- a/gitnexus/src/core/ingestion/scope-resolution/contract/scope-resolver.ts +++ b/gitnexus/src/core/ingestion/scope-resolution/contract/scope-resolver.ts @@ -627,6 +627,18 @@ export interface ScopeResolver { parsedFiles: readonly ParsedFile[], ) => SymbolDefinition | undefined; + /** + * Enable the receiver-bound Case 0.5 fallback for explicit `this` + * receivers (`this->m()` / `this.m()`) that resolves against the + * enclosing class + MRO even when no explicit `this` typeBinding is + * present in scope. + * + * Keep disabled for languages where the existing type-binding path + * (Case 4) already handles `this` correctly and overload ambiguity + * suppression must remain unchanged. + */ + readonly resolveThisViaEnclosingClass?: boolean; + /** * Optional post-finalize hook to inject cross-file bindings that * aren't modeled via explicit imports. Runs after diff --git a/gitnexus/src/core/ingestion/scope-resolution/passes/receiver-bound-calls.ts b/gitnexus/src/core/ingestion/scope-resolution/passes/receiver-bound-calls.ts index 80ff3a200..5aff78261 100644 --- a/gitnexus/src/core/ingestion/scope-resolution/passes/receiver-bound-calls.ts +++ b/gitnexus/src/core/ingestion/scope-resolution/passes/receiver-bound-calls.ts @@ -72,6 +72,7 @@ type ReceiverBoundProviderSubset = Pick< | 'unwrapCollectionAccessor' | 'hoistTypeBindingsToModule' | 'resolveQualifiedReceiverMember' + | 'resolveThisViaEnclosingClass' >; function normalizeTemplateArgToken(value: string): string { @@ -321,6 +322,88 @@ export function emitReceiverBoundCalls( } } + // ── Case 0.5: implicit `this` receiver ─────────────────────── + // C++ `this->member()` (and same-shape receivers in other OO + // languages) should resolve against the enclosing class + MRO + // even when there is no explicit `this` typeBinding in scope. + if (provider.resolveThisViaEnclosingClass === true && receiverName === 'this') { + const enclosingClass = findEnclosingClassDef(site.inScope, scopes); + if (enclosingClass !== undefined) { + const chain = [ + enclosingClass.nodeId, + ...scopes.methodDispatch.mroFor(enclosingClass.nodeId), + ]; + let memberDef: SymbolDefinition | undefined; + let ambiguous = false; + let hiddenByName = false; + for (const ownerId of chain) { + const methodOverloads = model.methods.lookupAllByOwner(ownerId, memberName); + if (methodOverloads.length > 0) { + const narrowed = narrowOverloadCandidates( + methodOverloads, + site.arity, + site.argumentTypes, + ); + if (isOverloadAmbiguousAfterNormalization(narrowed, site.arity)) { + ambiguous = true; + break; + } + if (narrowed.length === 0) { + // C++ name hiding: if the derived class declares `f`, base-class + // overloads named `f` are hidden for member lookup + // ([basic.lookup.classref]). A non-viable derived overload set + // therefore terminates lookup instead of falling through to base. + hiddenByName = true; + break; + } + memberDef = narrowed[0] ?? methodOverloads[0]; + break; + } + + // Field/property lookup intentionally runs only after the method + // lookup above: in C++ member-name lookup, functions with this + // name hide same-named base members; we therefore prefer method + // candidates first and only target a field when no methods with + // this name exist on the current owner. + memberDef = model.fields.lookupFieldByOwner(ownerId, memberName); + if (memberDef !== undefined) { + break; + } + } + if (ambiguous) { + handledSites.add(siteKey); + continue; + } + if (hiddenByName) { + handledSites.add(siteKey); + continue; + } + if (memberDef !== undefined) { + const reason = + site.kind === 'write' || site.kind === 'read' + ? site.kind + : memberDef.filePath !== parsed.filePath + ? 'import-resolved' + : 'global'; + const confidence = site.kind === 'write' || site.kind === 'read' ? 1.0 : 0.85; + const ok = tryEmitEdge( + graph, + scopes, + nodeLookup, + site, + memberDef, + reason, + seen, + confidence, + collapse, + ); + if (ok) emitted++; + handledSites.add(siteKey); + continue; + } + } + } + // ── Case 1: namespace receiver ─────────────────────────────── const targetFiles = namespaceTargets.get(receiverName); if (targetFiles !== undefined) { diff --git a/gitnexus/test/fixtures/lang-resolution/cpp-adl-function-pointer-arg/app.cpp b/gitnexus/test/fixtures/lang-resolution/cpp-adl-function-pointer-arg/app.cpp new file mode 100644 index 000000000..b9f99d201 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/cpp-adl-function-pointer-arg/app.cpp @@ -0,0 +1,8 @@ +#include "audit.h" + +namespace app { + void run() { + void (*g)(); + record(g); + } +} diff --git a/gitnexus/test/fixtures/lang-resolution/cpp-adl-function-pointer-arg/audit.h b/gitnexus/test/fixtures/lang-resolution/cpp-adl-function-pointer-arg/audit.h new file mode 100644 index 000000000..a0283cabc --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/cpp-adl-function-pointer-arg/audit.h @@ -0,0 +1,5 @@ +#pragma once + +namespace audit { + void record(void (*g)()); +} diff --git a/gitnexus/test/fixtures/lang-resolution/cpp-adl-function-pointer-before-class-arg/app.cpp b/gitnexus/test/fixtures/lang-resolution/cpp-adl-function-pointer-before-class-arg/app.cpp new file mode 100644 index 000000000..9e6cfd188 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/cpp-adl-function-pointer-before-class-arg/app.cpp @@ -0,0 +1,9 @@ +#include "audit.h" + +namespace app { + void run() { + void (*fp)(); + audit::Event e; + record(e); + } +} diff --git a/gitnexus/test/fixtures/lang-resolution/cpp-adl-function-pointer-before-class-arg/audit.h b/gitnexus/test/fixtures/lang-resolution/cpp-adl-function-pointer-before-class-arg/audit.h new file mode 100644 index 000000000..2c9803a3d --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/cpp-adl-function-pointer-before-class-arg/audit.h @@ -0,0 +1,6 @@ +#pragma once + +namespace audit { + struct Event {}; + void record(Event e); +} diff --git a/gitnexus/test/fixtures/lang-resolution/cpp-adl-function-pointer-class-return-arg/app.cpp b/gitnexus/test/fixtures/lang-resolution/cpp-adl-function-pointer-class-return-arg/app.cpp new file mode 100644 index 000000000..0cf4ee3bb --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/cpp-adl-function-pointer-class-return-arg/app.cpp @@ -0,0 +1,8 @@ +#include "audit.h" + +namespace app { + void run() { + audit::Event (*factory)(); + record(factory); + } +} diff --git a/gitnexus/test/fixtures/lang-resolution/cpp-adl-function-pointer-class-return-arg/audit.h b/gitnexus/test/fixtures/lang-resolution/cpp-adl-function-pointer-class-return-arg/audit.h new file mode 100644 index 000000000..a2438fe5e --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/cpp-adl-function-pointer-class-return-arg/audit.h @@ -0,0 +1,6 @@ +#pragma once + +namespace audit { + struct Event {}; + void record(Event (*factory)()); +} diff --git a/gitnexus/test/fixtures/lang-resolution/cpp-adl-pointer-to-pointer/app.cpp b/gitnexus/test/fixtures/lang-resolution/cpp-adl-pointer-to-pointer/app.cpp new file mode 100644 index 000000000..33508836e --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/cpp-adl-pointer-to-pointer/app.cpp @@ -0,0 +1,8 @@ +#include "audit.h" + +namespace app { + void run() { + audit::Event** pp; + record(pp); + } +} diff --git a/gitnexus/test/fixtures/lang-resolution/cpp-adl-pointer-to-pointer/audit.h b/gitnexus/test/fixtures/lang-resolution/cpp-adl-pointer-to-pointer/audit.h new file mode 100644 index 000000000..2f719362c --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/cpp-adl-pointer-to-pointer/audit.h @@ -0,0 +1,6 @@ +#pragma once + +namespace audit { + struct Event {}; + void record(Event** e); +} diff --git a/gitnexus/test/fixtures/lang-resolution/cpp-two-phase-namespace-free-call-inside-template/derived.h b/gitnexus/test/fixtures/lang-resolution/cpp-two-phase-namespace-free-call-inside-template/derived.h index ef57810fc..e3ea61977 100644 --- a/gitnexus/test/fixtures/lang-resolution/cpp-two-phase-namespace-free-call-inside-template/derived.h +++ b/gitnexus/test/fixtures/lang-resolution/cpp-two-phase-namespace-free-call-inside-template/derived.h @@ -3,9 +3,12 @@ #include "base.h" #include "helpers.h" +using utils::ns_helper_2; + template struct D : Base { void g() { utils::ns_helper(); + ns_helper_2(); } }; diff --git a/gitnexus/test/fixtures/lang-resolution/cpp-two-phase-namespace-free-call-inside-template/helpers.h b/gitnexus/test/fixtures/lang-resolution/cpp-two-phase-namespace-free-call-inside-template/helpers.h index 5e291aba6..4bd7a9833 100644 --- a/gitnexus/test/fixtures/lang-resolution/cpp-two-phase-namespace-free-call-inside-template/helpers.h +++ b/gitnexus/test/fixtures/lang-resolution/cpp-two-phase-namespace-free-call-inside-template/helpers.h @@ -2,4 +2,5 @@ namespace utils { void ns_helper(); + void ns_helper_2(); } diff --git a/gitnexus/test/fixtures/lang-resolution/cpp-two-phase-paired/base.h b/gitnexus/test/fixtures/lang-resolution/cpp-two-phase-paired/base.h new file mode 100644 index 000000000..84bc1a954 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/cpp-two-phase-paired/base.h @@ -0,0 +1,6 @@ +#pragma once + +template +struct Base { + void f(); +}; diff --git a/gitnexus/test/fixtures/lang-resolution/cpp-two-phase-paired/derived.h b/gitnexus/test/fixtures/lang-resolution/cpp-two-phase-paired/derived.h new file mode 100644 index 000000000..c7cc1a53b --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/cpp-two-phase-paired/derived.h @@ -0,0 +1,14 @@ +#pragma once + +#include "base.h" + +template +struct Derived : Base { + void g_unqualified() { + f(); + } + + void g_this() { + this->f(); + } +}; diff --git a/gitnexus/test/fixtures/lang-resolution/cpp-two-phase-this-name-hiding-arity/base.h b/gitnexus/test/fixtures/lang-resolution/cpp-two-phase-this-name-hiding-arity/base.h new file mode 100644 index 000000000..84bc1a954 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/cpp-two-phase-this-name-hiding-arity/base.h @@ -0,0 +1,6 @@ +#pragma once + +template +struct Base { + void f(); +}; diff --git a/gitnexus/test/fixtures/lang-resolution/cpp-two-phase-this-name-hiding-arity/derived.h b/gitnexus/test/fixtures/lang-resolution/cpp-two-phase-this-name-hiding-arity/derived.h new file mode 100644 index 000000000..ad867e820 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/cpp-two-phase-this-name-hiding-arity/derived.h @@ -0,0 +1,16 @@ +#pragma once + +#include "base.h" + +template +struct Derived : Base { + void f(int); + + void g() { + this->f(); + } + + void g_ok() { + this->f(42); + } +}; diff --git a/gitnexus/test/fixtures/lang-resolution/cpp-two-phase-this-qualified/base.h b/gitnexus/test/fixtures/lang-resolution/cpp-two-phase-this-qualified/base.h index 1c7084ee6..286f877a1 100644 --- a/gitnexus/test/fixtures/lang-resolution/cpp-two-phase-this-qualified/base.h +++ b/gitnexus/test/fixtures/lang-resolution/cpp-two-phase-this-qualified/base.h @@ -3,5 +3,6 @@ template struct Base { void f(); + void base_method(); int i; }; diff --git a/gitnexus/test/fixtures/lang-resolution/cpp-two-phase-this-qualified/derived.h b/gitnexus/test/fixtures/lang-resolution/cpp-two-phase-this-qualified/derived.h index 5c13c1737..9b154a429 100644 --- a/gitnexus/test/fixtures/lang-resolution/cpp-two-phase-this-qualified/derived.h +++ b/gitnexus/test/fixtures/lang-resolution/cpp-two-phase-this-qualified/derived.h @@ -7,6 +7,9 @@ struct Derived : Base { void g() { this->f(); } + void k() { + this->base_method(); + } int h() { return this->i; } diff --git a/gitnexus/test/integration/resolvers/cpp.test.ts b/gitnexus/test/integration/resolvers/cpp.test.ts index 25a7f9de7..cd95cfae7 100644 --- a/gitnexus/test/integration/resolvers/cpp.test.ts +++ b/gitnexus/test/integration/resolvers/cpp.test.ts @@ -1972,14 +1972,100 @@ describe('C++ two-phase template lookup — dependent base suppression', () => { }); }); -// NOTE: positive guards (this->f() resolves, non-dependent-base unqualified -// f() resolves, namespace-qualified utils::ns_helper() resolves) inside -// template bodies are documented gaps in C++ template-context resolution -// independent of U3's dependent-base suppression. The U3 core asserts only -// the negative behavior (dependent-base members are NOT bound by unqualified -// calls); the positive cases would require additional `this` type-binding -// and template-body member-lookup work tracked separately. See plan -// 2026-05-13-001 follow-ups. +describe('C++ two-phase template lookup — positive this-qualified calls', () => { + let result: PipelineResult; + + beforeAll(async () => { + result = await runPipelineFromRepo( + path.join(FIXTURES, 'cpp-two-phase-this-qualified'), + () => {}, + ); + }, 60000); + + it('Derived::g() -> this->f() resolves to f (1 edge)', () => { + const calls = getRelationships(result, 'CALLS'); + const thisCalls = calls.filter((c) => c.source === 'g' && c.target === 'f'); + expect(thisCalls.length).toBe(1); + expect(thisCalls[0].targetFilePath).toContain('base.h'); + }); + + it('Derived::k() -> this->base_method() resolves via EXTENDS chain (1 edge)', () => { + const calls = getRelationships(result, 'CALLS'); + const inheritedCalls = calls.filter((c) => c.source === 'k' && c.target === 'base_method'); + expect(inheritedCalls.length).toBe(1); + expect(inheritedCalls[0].targetFilePath).toContain('base.h'); + }); +}); + +describe('C++ two-phase template lookup — paired unqualified + this-qualified in one fixture', () => { + let result: PipelineResult; + + beforeAll(async () => { + result = await runPipelineFromRepo(path.join(FIXTURES, 'cpp-two-phase-paired'), () => {}); + }, 60000); + + it('Derived::g_unqualified() -> f() does NOT bind to Base::f', () => { + const calls = getRelationships(result, 'CALLS'); + const leaks = calls.filter((c) => c.source === 'g_unqualified' && c.target === 'f'); + expect(leaks.length).toBe(0); + }); + + it('Derived::g_this() -> this->f() resolves to Base::f (1 edge)', () => { + const calls = getRelationships(result, 'CALLS'); + const resolved = calls.filter((c) => c.source === 'g_this' && c.target === 'f'); + expect(resolved.length).toBe(1); + expect(resolved[0].targetFilePath).toContain('base.h'); + }); +}); + +describe('C++ two-phase template lookup — namespace calls inside template body', () => { + let result: PipelineResult; + + beforeAll(async () => { + result = await runPipelineFromRepo( + path.join(FIXTURES, 'cpp-two-phase-namespace-free-call-inside-template'), + () => {}, + ); + }, 60000); + + it('D::g() -> utils::ns_helper() resolves (1 edge)', () => { + const calls = getRelationships(result, 'CALLS'); + const qualifiedCalls = calls.filter((c) => c.source === 'g' && c.target === 'ns_helper'); + expect(qualifiedCalls.length).toBe(1); + expect(qualifiedCalls[0].targetFilePath).toContain('helpers.h'); + }); + + it('D::g() -> ns_helper_2() resolves after using-declaration (1 edge)', () => { + const calls = getRelationships(result, 'CALLS'); + const usingCalls = calls.filter((c) => c.source === 'g' && c.target === 'ns_helper_2'); + expect(usingCalls.length).toBe(1); + expect(usingCalls[0].targetFilePath).toContain('helpers.h'); + }); +}); + +describe('C++ two-phase template lookup — this-> name-hiding arity mismatch', () => { + let result: PipelineResult; + + beforeAll(async () => { + result = await runPipelineFromRepo( + path.join(FIXTURES, 'cpp-two-phase-this-name-hiding-arity'), + () => {}, + ); + }, 60000); + + it('Derived::g() -> this->f() emits zero CALLS edges when only hidden derived overload is arity-incompatible', () => { + const calls = getRelationships(result, 'CALLS'); + const fCalls = calls.filter((c) => c.source === 'g' && c.target === 'f'); + expect(fCalls.length).toBe(0); + }); + + it('Derived::g_ok() -> this->f(42) resolves to derived overload (1 edge)', () => { + const calls = getRelationships(result, 'CALLS'); + const fCalls = calls.filter((c) => c.source === 'g_ok' && c.target === 'f'); + expect(fCalls.length).toBe(1); + expect(fCalls[0].targetFilePath).toContain('derived.h'); + }); +}); // --------------------------------------------------------------------------- // U3 cross-file namespace variant: Base lives in a different file AND @@ -2057,7 +2143,7 @@ describe('C++ ADL — parenthesized name suppresses ADL', () => { }); }); -describe('C++ ADL — pointer-arg V1 boundary', () => { +describe('C++ ADL — pointer arg unwrapping', () => { let result: PipelineResult; beforeAll(async () => { @@ -2067,19 +2153,81 @@ describe('C++ ADL — pointer-arg V1 boundary', () => { ); }, 60000); - it('record(p) where p is audit::Event* emits zero CALLS — V1 ADL excludes pointer args', () => { + it('record(p) where p is audit::Event* resolves to audit::record via ADL', () => { + const calls = getRelationships(result, 'CALLS'); + const recordCalls = calls.filter((c) => c.source === 'run' && c.target === 'record'); + expect(recordCalls.length).toBe(1); + expect(recordCalls[0].targetFilePath).toContain('audit.h'); + }); +}); + +describe('C++ ADL — function pointer args do not participate', () => { + let result: PipelineResult; + + beforeAll(async () => { + result = await runPipelineFromRepo( + path.join(FIXTURES, 'cpp-adl-function-pointer-arg'), + () => {}, + ); + }, 60000); + + it('record(g) where g is void (*)() emits zero CALLS edges', () => { const calls = getRelationships(result, 'CALLS'); const recordCalls = calls.filter((c) => c.source === 'run' && c.target === 'record'); - // Exact .toBe(0): V1 ADL covers only directly-named class-type values - // (per plan 2026-05-13-001 R4). Pointer-typed args fall under - // associated-entity closure rules deferred to V2. This fixture locks - // the boundary in CI so the implementer cannot accidentally extend - // V1 to include pointer types. Real ISO C++ would resolve via V2 - // closure; matching that requires the V2 follow-up plan. expect(recordCalls.length).toBe(0); }); }); +describe('C++ ADL — preceding function-pointer declarations do not block class args', () => { + let result: PipelineResult; + + beforeAll(async () => { + result = await runPipelineFromRepo( + path.join(FIXTURES, 'cpp-adl-function-pointer-before-class-arg'), + () => {}, + ); + }, 60000); + + it('record(e) still resolves via ADL when an earlier declaration is void (*)()', () => { + const calls = getRelationships(result, 'CALLS'); + const recordCalls = calls.filter((c) => c.source === 'run' && c.target === 'record'); + expect(recordCalls.length).toBe(1); + expect(recordCalls[0].targetFilePath).toContain('audit.h'); + }); +}); + +describe('C++ ADL — class-returning function pointer args do not participate', () => { + let result: PipelineResult; + + beforeAll(async () => { + result = await runPipelineFromRepo( + path.join(FIXTURES, 'cpp-adl-function-pointer-class-return-arg'), + () => {}, + ); + }, 60000); + + it('record(factory) where factory is audit::Event (*)() emits zero CALLS edges', () => { + const calls = getRelationships(result, 'CALLS'); + const recordCalls = calls.filter((c) => c.source === 'run' && c.target === 'record'); + expect(recordCalls.length).toBe(0); + }); +}); + +describe('C++ ADL — pointer-to-pointer args participate', () => { + let result: PipelineResult; + + beforeAll(async () => { + result = await runPipelineFromRepo(path.join(FIXTURES, 'cpp-adl-pointer-to-pointer'), () => {}); + }, 60000); + + it('record(pp) where pp is audit::Event** resolves to audit::record via ADL', () => { + const calls = getRelationships(result, 'CALLS'); + const recordCalls = calls.filter((c) => c.source === 'run' && c.target === 'record'); + expect(recordCalls.length).toBe(1); + expect(recordCalls[0].targetFilePath).toContain('audit.h'); + }); +}); + describe('C++ ADL — int/long-collision overloads suppress via OVERLOAD_AMBIGUOUS', () => { let result: PipelineResult; diff --git a/gitnexus/test/integration/resolvers/helpers.ts b/gitnexus/test/integration/resolvers/helpers.ts index 5149e3e69..396e7905b 100644 --- a/gitnexus/test/integration/resolvers/helpers.ts +++ b/gitnexus/test/integration/resolvers/helpers.ts @@ -168,6 +168,15 @@ const LEGACY_RESOLVER_PARITY_EXPECTED_FAILURES: Readonly and List', 'callSave() in each specialization resolves to its own save()', 'save specialization bodies route to their own sibling method', + // PR #1590 follow-up: explicit `this->` resolution in template class + // bodies and paired two-phase assertions are scope-resolver-only. + // Legacy DAG lacks this receiver-bound template semantics and + // dependent-base suppression parity for these shapes. + 'Derived::g() -> this->f() resolves to f (1 edge)', + 'Derived::k() -> this->base_method() resolves via EXTENDS chain (1 edge)', + 'Derived::g_unqualified() -> f() does NOT bind to Base::f', + 'Derived::g_this() -> this->f() resolves to Base::f (1 edge)', + 'Derived::g() -> this->f() emits zero CALLS edges when only hidden derived overload is arity-incompatible', ]), };