From cdac8a691ae6c58fee9a64ae5e9442d776e5fa7c Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Thu, 14 May 2026 20:25:12 +0100 Subject: [PATCH] feat: C++ ADL V2: include class-typed reference args (incl. rvalue refs) in associated-namespace lookup (#1595) --- .../src/core/ingestion/languages/cpp/adl.ts | 24 ++++----- .../core/ingestion/languages/cpp/captures.ts | 15 +++--- .../cpp-adl-reference-arg-boundary/app.cpp | 21 ++++++++ .../cpp-adl-reference-arg-boundary/audit.h | 5 ++ .../cpp-adl-reference-arg-boundary/record.h | 8 +++ .../cpp-adl-rvalue-ref/app.cpp | 9 ++++ .../cpp-adl-rvalue-ref/audit.h | 5 ++ .../cpp-adl-rvalue-ref/record-rvalue.h | 7 +++ .../test/integration/resolvers/cpp.test.ts | 50 ++++++++++++++++++- 9 files changed, 119 insertions(+), 25 deletions(-) create mode 100644 gitnexus/test/fixtures/lang-resolution/cpp-adl-reference-arg-boundary/app.cpp create mode 100644 gitnexus/test/fixtures/lang-resolution/cpp-adl-reference-arg-boundary/audit.h create mode 100644 gitnexus/test/fixtures/lang-resolution/cpp-adl-reference-arg-boundary/record.h create mode 100644 gitnexus/test/fixtures/lang-resolution/cpp-adl-rvalue-ref/app.cpp create mode 100644 gitnexus/test/fixtures/lang-resolution/cpp-adl-rvalue-ref/audit.h create mode 100644 gitnexus/test/fixtures/lang-resolution/cpp-adl-rvalue-ref/record-rvalue.h diff --git a/gitnexus/src/core/ingestion/languages/cpp/adl.ts b/gitnexus/src/core/ingestion/languages/cpp/adl.ts index 4c7b034b6..8d6c1563d 100644 --- a/gitnexus/src/core/ingestion/languages/cpp/adl.ts +++ b/gitnexus/src/core/ingestion/languages/cpp/adl.ts @@ -18,11 +18,11 @@ * 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. 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, and the rest of the full closure are - * still deliberately excluded. + * pointer-typed and reference-typed class args (`audit::Event* p`, + * `audit::Event& r`, `audit::Event&& rr`): they contribute the pointee / + * referred class's enclosing namespace too. Function-pointer arguments, + * template specializations, 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 @@ -62,18 +62,13 @@ 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 * for primitives, literals, function pointers, template specs, etc. */ readonly simpleClassName: string; - /** 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; } const argInfoBySite = new Map(); @@ -152,8 +147,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: { @@ -175,7 +170,6 @@ export function pickCppAdlCandidates( const associatedNamespaces = new Set(); for (const arg of args) { if (arg.simpleClassName === '') 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 17b6962aa..75ef30804 100644 --- a/gitnexus/src/core/ingestion/languages/cpp/captures.ts +++ b/gitnexus/src/core/ingestion/languages/cpp/captures.ts @@ -743,7 +743,7 @@ function inferCppCallAdlArgs(callNode: SyntaxNode): CppAdlArgInfo[] { return out; } -const EMPTY_ADL_ARG: CppAdlArgInfo = { simpleClassName: '', isPointer: false, isReference: false }; +const EMPTY_ADL_ARG: CppAdlArgInfo = { simpleClassName: '' }; function classifyAdlArg(argNode: SyntaxNode): CppAdlArgInfo { // Literals and primitive-shaped expressions never have associated namespaces. @@ -794,12 +794,11 @@ 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 isPointer = false; - let isReference = false; let isFunctionPointer = false; let inner: SyntaxNode = declarator; let nameText: string | null = null; @@ -810,14 +809,12 @@ function lookupAdlIdentifierType(identNode: SyntaxNode): CppAdlArgInfo { isFunctionPointer = true; break; } - isPointer = true; const next = inner.childForFieldName('declarator'); if (next === null) break; inner = next; continue; } - if (inner.type === 'reference_declarator') { - isReference = true; + if (inner.type === 'reference_declarator' || inner.type === 'rvalue_reference_declarator') { // reference_declarator has a single child (the inner declarator). let next: SyntaxNode | null = null; for (let j = 0; j < inner.namedChildCount; j++) { @@ -848,7 +845,7 @@ function lookupAdlIdentifierType(identNode: SyntaxNode): CppAdlArgInfo { if (isFunctionPointer || nameText !== varName) continue; const simpleClassName = extractAdlSimpleTypeName(typeNode); - return { simpleClassName, isPointer, isReference }; + return { simpleClassName }; } return EMPTY_ADL_ARG; } 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 cd95cfae7..7bc040182 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;