diff --git a/gitnexus/src/core/ingestion/languages/cpp/adl.ts b/gitnexus/src/core/ingestion/languages/cpp/adl.ts index 5fa380e92..a69801856 100644 --- a/gitnexus/src/core/ingestion/languages/cpp/adl.ts +++ b/gitnexus/src/core/ingestion/languages/cpp/adl.ts @@ -15,13 +15,14 @@ * * ## Current boundary * - * The current implementation covers class-typed arguments (value and pointer) - * and template specializations with explicit type arguments: + * The current implementation covers class-typed arguments (value, pointer, + * and reference) and template specializations with explicit type arguments: * - `audit::Event e`, `audit::Event* p`, `audit::Event** pp` + * - `audit::Event& r`, `audit::Event&& rr` * - `std::vector` (template namespace + template-arg namespaces) * - * Reference arguments, function-pointer arguments, base-class associated - * namespaces, and the rest of the full closure are still deliberately excluded. + * Function-pointer arguments, base-class associated namespaces, and the rest + * of the full closure are still deliberately excluded. * * The current implementation also short-circuits to ADL only when ordinary lookup is empty * (`findCallableBindingInScope` returned undefined). ISO C++ would @@ -61,8 +62,8 @@ import { /** * 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. + * arguments where `simpleClassName !== ''`, including class pointers and + * references whose declarator chain resolves to a named class type. */ export interface CppAdlArgInfo { /** Simple class-like type name (last segment of qualified name); empty @@ -163,8 +164,8 @@ export function populateCppAssociatedNamespaces(parsed: ParsedFile): void { * * Fires only when: * - the call site is not in `noAdlSites` (parenthesized form), AND - * - at least one argument resolves to a named class type (value or - * pointer, but not reference, function pointer, literal, or primitive). + * - at least one argument resolves to a named class type (value, + * pointer, or reference; but not function pointer, literal, or primitive). */ export function pickCppAdlCandidates( site: { @@ -185,8 +186,6 @@ export function pickCppAdlCandidates( // Collect associated namespace QNames from every participating class-typed arg. const associatedNamespaces = new Set(); for (const arg of args) { - if (arg.isReference) continue; - if (arg.simpleClassName !== '') { // For template args this may be the template name itself (e.g. // `vector`); simple-name lookup can match project classes with the diff --git a/gitnexus/src/core/ingestion/languages/cpp/captures.ts b/gitnexus/src/core/ingestion/languages/cpp/captures.ts index fe6067dbe..7c055feec 100644 --- a/gitnexus/src/core/ingestion/languages/cpp/captures.ts +++ b/gitnexus/src/core/ingestion/languages/cpp/captures.ts @@ -803,13 +803,14 @@ 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; repeated pointer wrappers still count as pointer - // typed; `init_declarator > reference_declarator > ...` means - // reference-typed; bare `init_declarator > identifier` is value. + // typed; `init_declarator > reference_declarator > ...` (or + // `rvalue_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 isFunctionPointer = false; 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 @@ -825,7 +826,7 @@ function lookupAdlIdentifierType(identNode: SyntaxNode): CppAdlArgInfo { inner = next; continue; } - if (inner.type === 'reference_declarator') { + if (inner.type === 'reference_declarator' || inner.type === 'rvalue_reference_declarator') { isReference = true; // reference_declarator has a single child (the inner declarator). let next: SyntaxNode | null = null; diff --git a/gitnexus/src/core/ingestion/languages/cpp/scope-resolver.ts b/gitnexus/src/core/ingestion/languages/cpp/scope-resolver.ts index a4d41e8b4..e91b14428 100644 --- a/gitnexus/src/core/ingestion/languages/cpp/scope-resolver.ts +++ b/gitnexus/src/core/ingestion/languages/cpp/scope-resolver.ts @@ -222,10 +222,10 @@ export const cppScopeResolver: ScopeResolver = { // C++ argument-dependent / Koenig lookup (U2 of plan 2026-05-13-001). // Fires after `findCallableBindingInScope` returns undefined; surfaces // candidates from the associated namespaces of class-typed arguments. - // Current boundary: class-typed value/pointer args and template + // Current boundary: class-typed value/pointer/reference args and template // specializations with explicit type arguments contribute associated - // namespaces. Reference args, function-pointer args, base-class - // associated namespaces, and full ordinary+ADL merge remain excluded. + // namespaces. Function-pointer args, base-class associated namespaces, + // and full ordinary+ADL merge remain 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 diff --git a/gitnexus/test/fixtures/lang-resolution/cpp-adl-reference-arg-boundary/app.cpp b/gitnexus/test/fixtures/lang-resolution/cpp-adl-reference-arg-boundary/app.cpp new file mode 100644 index 000000000..e32733d9b --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/cpp-adl-reference-arg-boundary/app.cpp @@ -0,0 +1,21 @@ +#include "audit.h" + +namespace app { + void runRef() { + audit::Event e; + audit::Event& s = e; + record(s); + } + + void runConstRef() { + audit::Event e; + const audit::Event& constEventRef = e; + recordConst(constEventRef); + } + + void runPrimitiveRef() { + int n = 0; + int& r = n; + note(r); + } +} diff --git a/gitnexus/test/fixtures/lang-resolution/cpp-adl-reference-arg-boundary/audit.h b/gitnexus/test/fixtures/lang-resolution/cpp-adl-reference-arg-boundary/audit.h new file mode 100644 index 000000000..4b4600381 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/cpp-adl-reference-arg-boundary/audit.h @@ -0,0 +1,5 @@ +#pragma once + +namespace audit { + struct Event {}; +} diff --git a/gitnexus/test/fixtures/lang-resolution/cpp-adl-reference-arg-boundary/record.h b/gitnexus/test/fixtures/lang-resolution/cpp-adl-reference-arg-boundary/record.h new file mode 100644 index 000000000..590ac5a87 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/cpp-adl-reference-arg-boundary/record.h @@ -0,0 +1,8 @@ +#pragma once + +#include "audit.h" + +namespace audit { + void record(Event& e); + void recordConst(const Event& e); +} diff --git a/gitnexus/test/fixtures/lang-resolution/cpp-adl-rvalue-ref/app.cpp b/gitnexus/test/fixtures/lang-resolution/cpp-adl-rvalue-ref/app.cpp new file mode 100644 index 000000000..8a5f57ba9 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/cpp-adl-rvalue-ref/app.cpp @@ -0,0 +1,9 @@ +#include "audit.h" + +namespace app { + void runRvalueRef() { + audit::Event e; + audit::Event&& rr = static_cast(e); + record(rr); + } +} diff --git a/gitnexus/test/fixtures/lang-resolution/cpp-adl-rvalue-ref/audit.h b/gitnexus/test/fixtures/lang-resolution/cpp-adl-rvalue-ref/audit.h new file mode 100644 index 000000000..4b4600381 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/cpp-adl-rvalue-ref/audit.h @@ -0,0 +1,5 @@ +#pragma once + +namespace audit { + struct Event {}; +} diff --git a/gitnexus/test/fixtures/lang-resolution/cpp-adl-rvalue-ref/record-rvalue.h b/gitnexus/test/fixtures/lang-resolution/cpp-adl-rvalue-ref/record-rvalue.h new file mode 100644 index 000000000..ae3cf7462 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/cpp-adl-rvalue-ref/record-rvalue.h @@ -0,0 +1,7 @@ +#pragma once + +#include "audit.h" + +namespace audit { + void record(Event&& e); +} diff --git a/gitnexus/test/integration/resolvers/cpp.test.ts b/gitnexus/test/integration/resolvers/cpp.test.ts index 0723f5533..7b9c2be9c 100644 --- a/gitnexus/test/integration/resolvers/cpp.test.ts +++ b/gitnexus/test/integration/resolvers/cpp.test.ts @@ -2103,7 +2103,7 @@ describe('C++ two-phase template lookup — cross-file namespace variant', () => // Free-function calls with class-typed arguments must consider candidates // declared in the argument's enclosing namespace (associated namespace). // V1 boundary: only direct enclosing-namespace closure for value class- -// typed args; pointer / reference / template-spec args excluded. +// typed args; pointer and reference args included, template-spec args excluded. // --------------------------------------------------------------------------- describe('C++ ADL — basic associated-namespace closure', () => { @@ -2161,6 +2161,54 @@ describe('C++ ADL — pointer arg unwrapping', () => { }); }); +describe('C++ ADL — reference arg unwrapping', () => { + let result: PipelineResult; + + beforeAll(async () => { + result = await runPipelineFromRepo( + path.join(FIXTURES, 'cpp-adl-reference-arg-boundary'), + () => {}, + ); + }, 60000); + + it('record(s) where s is audit::Event& resolves to audit::record via ADL', () => { + const calls = getRelationships(result, 'CALLS'); + const recordCalls = calls.filter((c) => c.source === 'runRef' && c.target === 'record'); + expect(recordCalls.length).toBe(1); + expect(recordCalls[0].targetFilePath).toContain('record.h'); + }); + + it('recordConst(cs) where cs is const audit::Event& resolves via ADL', () => { + const calls = getRelationships(result, 'CALLS'); + const recordCalls = calls.filter( + (c) => c.source === 'runConstRef' && c.target === 'recordConst', + ); + expect(recordCalls.length).toBe(1); + expect(recordCalls[0].targetFilePath).toContain('record.h'); + }); + + it('note(r) where r is int& emits zero CALLS edges (primitive ref)', () => { + const calls = getRelationships(result, 'CALLS'); + const noteCalls = calls.filter((c) => c.source === 'runPrimitiveRef' && c.target === 'note'); + expect(noteCalls.length).toBe(0); + }); +}); + +describe('C++ ADL — rvalue reference args participate', () => { + let result: PipelineResult; + + beforeAll(async () => { + result = await runPipelineFromRepo(path.join(FIXTURES, 'cpp-adl-rvalue-ref'), () => {}); + }, 60000); + + it('record(rr) where rr is audit::Event&& resolves to audit::record via ADL', () => { + const calls = getRelationships(result, 'CALLS'); + const recordCalls = calls.filter((c) => c.source === 'runRvalueRef' && c.target === 'record'); + expect(recordCalls.length).toBe(1); + expect(recordCalls[0].targetFilePath).toContain('record-rvalue.h'); + }); +}); + describe('C++ ADL — function pointer args do not participate', () => { let result: PipelineResult;