diff --git a/gitnexus/src/core/ingestion/languages/cpp/adl.ts b/gitnexus/src/core/ingestion/languages/cpp/adl.ts index 8d6c1563d..bb28c3853 100644 --- a/gitnexus/src/core/ingestion/languages/cpp/adl.ts +++ b/gitnexus/src/core/ingestion/languages/cpp/adl.ts @@ -15,14 +15,14 @@ * * ## Current boundary * - * 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 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 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) + * + * 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 @@ -67,8 +67,20 @@ import { */ export interface CppAdlArgInfo { /** Simple class-like type name (last segment of qualified name); empty - * for primitives, literals, function pointers, template specs, etc. */ + * for primitives, literals, function pointers, etc. */ readonly simpleClassName: string; + /** Template's own simple class-like name (e.g. `vector` for + * `std::vector`), empty when arg type is not a template spec. */ + readonly templateSimpleClassName: string; + /** Template's own enclosing namespace (dot-qualified, e.g. `std`), empty + * when unavailable / unqualified. */ + readonly templateNamespace: string; + /** Class-like names extracted from explicit type template arguments, + * recursively bounded. */ + readonly templateArgClassNames: readonly string[]; + /** Enclosing namespaces extracted from explicit type template arguments, + * recursively bounded. */ + readonly templateArgNamespaces: readonly string[]; } const argInfoBySite = new Map(); @@ -169,11 +181,7 @@ export function pickCppAdlCandidates( // Collect associated namespace QNames from every participating class-typed arg. const associatedNamespaces = new Set(); for (const arg of args) { - if (arg.simpleClassName === '') continue; - const classDef = findCppClassDefBySimpleName(arg.simpleClassName, scopes); - if (classDef === undefined) continue; - const nsQName = classToNamespaceQualifiedName.get(classDef.nodeId); - if (nsQName !== undefined) associatedNamespaces.add(nsQName); + collectAssociatedNamespacesForAdlArg(arg, scopes, associatedNamespaces); } if (associatedNamespaces.size === 0) return undefined; @@ -222,6 +230,41 @@ export function pickCppAdlCandidates( return ADL_AMBIGUOUS; } +function collectAssociatedNamespacesForAdlArg( + arg: CppAdlArgInfo, + scopes: ScopeResolutionIndexes, + associatedNamespaces: Set, +): void { + // For template args this may be the template name itself (e.g. `vector`); + // simple-name lookup can match project classes with the same name (known + // V1/V2 simplification). + addAssociatedNamespaceForClassName(arg.simpleClassName, scopes, associatedNamespaces); + + // Includes template-owner namespaces (e.g. `std` in std::vector). If + // that surfaces extra candidates, ADL_AMBIGUOUS suppression below prevents + // arbitrary edge emission. + if (arg.templateNamespace.length > 0) associatedNamespaces.add(arg.templateNamespace); + + for (const ns of arg.templateArgNamespaces) { + if (ns.length > 0) associatedNamespaces.add(ns); + } + for (const className of arg.templateArgClassNames) { + addAssociatedNamespaceForClassName(className, scopes, associatedNamespaces); + } +} + +function addAssociatedNamespaceForClassName( + simpleClassName: string, + scopes: ScopeResolutionIndexes, + associatedNamespaces: Set, +): void { + if (simpleClassName.length === 0) return; + const classDef = findCppClassDefBySimpleName(simpleClassName, scopes); + if (classDef === undefined) return; + const nsQName = classToNamespaceQualifiedName.get(classDef.nodeId); + if (nsQName !== undefined) associatedNamespaces.add(nsQName); +} + /** Walk upward from a Class scope, finding the innermost enclosing * Namespace scope, and return that namespace's qualified name (dot- * joined, outermost-first). Returns '' when the class has no enclosing diff --git a/gitnexus/src/core/ingestion/languages/cpp/captures.ts b/gitnexus/src/core/ingestion/languages/cpp/captures.ts index 75ef30804..338e9992b 100644 --- a/gitnexus/src/core/ingestion/languages/cpp/captures.ts +++ b/gitnexus/src/core/ingestion/languages/cpp/captures.ts @@ -720,12 +720,14 @@ function isParenthesizedFunctionCall(callNode: SyntaxNode): boolean { /** * Per-argument ADL classification: walk each argument of a free call and - * 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. + * classify its declared type for associated-namespace lookup. * - * Class-typed values and class pointers (`N::S`, `N::S*`, `N::S**`) all - * preserve the pointee class name for associated-namespace lookup. + * Value/pointer/reference class-typed args and template specializations + * with explicit type arguments contribute; function pointers, primitives, + * literals, and other unsupported shapes produce an empty result. + * + * Class-typed values/pointers/references (`N::S`, `N::S*`, `N::S&`) all + * preserve the 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. @@ -743,7 +745,14 @@ function inferCppCallAdlArgs(callNode: SyntaxNode): CppAdlArgInfo[] { return out; } -const EMPTY_ADL_ARG: CppAdlArgInfo = { simpleClassName: '' }; +const ADL_TEMPLATE_RECURSION_MAX_DEPTH = 8; +const EMPTY_ADL_ARG: CppAdlArgInfo = { + simpleClassName: '', + templateSimpleClassName: '', + templateNamespace: '', + templateArgClassNames: [], + templateArgNamespaces: [], +}; function classifyAdlArg(argNode: SyntaxNode): CppAdlArgInfo { // Literals and primitive-shaped expressions never have associated namespaces. @@ -845,29 +854,175 @@ function lookupAdlIdentifierType(identNode: SyntaxNode): CppAdlArgInfo { if (isFunctionPointer || nameText !== varName) continue; const simpleClassName = extractAdlSimpleTypeName(typeNode); - return { simpleClassName }; + const { + templateSimpleClassName, + templateNamespace, + templateArgClassNames, + templateArgNamespaces, + } = extractAdlTemplateInfo(typeNode); + return { + simpleClassName, + templateSimpleClassName, + templateNamespace, + templateArgClassNames, + templateArgNamespaces, + }; } return EMPTY_ADL_ARG; } /** Extract the simple class-like type name from a `type:` field node. - * Returns '' for primitives, template specializations, and any other + * Returns '' for primitives 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 === 'type_descriptor') { + const innerType = typeNode.childForFieldName('type'); + if (innerType !== null) return extractAdlSimpleTypeName(innerType); + for (let i = 0; i < typeNode.childCount; i++) { + const child = typeNode.child(i); + if (child === null) continue; + if ( + child.type === 'type_identifier' || + child.type === 'qualified_identifier' || + child.type === 'template_type' + ) { + return extractAdlSimpleTypeName(child); + } + } + return ''; + } if (typeNode.type === 'primitive_type') return ''; if (typeNode.type === 'sized_type_specifier') return ''; if (typeNode.type === 'type_identifier') return typeNode.text; + if (typeNode.type === 'template_type') { + const nameNode = typeNode.childForFieldName('name'); + if (nameNode !== null) return extractAdlSimpleTypeName(nameNode); + const id = findFirstDescendantOfType(typeNode, 'type_identifier'); + return id !== null ? id.text : ''; + } if (typeNode.type === 'qualified_identifier') { const nameNode = typeNode.childForFieldName('name'); if (nameNode !== null) return extractAdlSimpleTypeName(nameNode); const id = findFirstDescendantOfType(typeNode, 'type_identifier'); return id !== null ? id.text : ''; } - // template_type (e.g. `vector`), function pointers, decltype — V1 excludes. + // Function pointers, decltype, etc — unsupported for ADL participation. return ''; } +function extractAdlTypeNamespace(typeNode: SyntaxNode): string { + if (typeNode.type === 'type_descriptor') { + const innerType = typeNode.childForFieldName('type'); + if (innerType !== null) return extractAdlTypeNamespace(innerType); + for (let i = 0; i < typeNode.childCount; i++) { + const child = typeNode.child(i); + if (child === null) continue; + if ( + child.type === 'qualified_identifier' || + child.type === 'template_type' || + child.type === 'type_identifier' + ) { + return extractAdlTypeNamespace(child); + } + } + return ''; + } + if (typeNode.type === 'template_type') { + const nameNode = typeNode.childForFieldName('name'); + return nameNode !== null ? extractAdlTypeNamespace(nameNode) : ''; + } + if (typeNode.type === 'qualified_identifier') { + const scope = typeNode.childForFieldName('scope'); + if (scope !== null) return normalizeCppNamespaceQName(scope.text); + return extractNamespaceFromQualifiedText(typeNode.text); + } + return ''; +} + +function extractAdlTemplateInfo(typeNode: SyntaxNode): { + templateSimpleClassName: string; + templateNamespace: string; + templateArgClassNames: string[]; + templateArgNamespaces: string[]; +} { + const templateTypeNode = findTemplateTypeNode(typeNode); + if (templateTypeNode === null) { + return { + templateSimpleClassName: '', + templateNamespace: '', + templateArgClassNames: [], + templateArgNamespaces: [], + }; + } + const templateArgClassNames: string[] = []; + const templateArgNamespaces: string[] = []; + collectAdlTemplateArgs(templateTypeNode, 0, templateArgClassNames, templateArgNamespaces); + return { + templateSimpleClassName: extractAdlSimpleTypeName(templateTypeNode), + templateNamespace: extractAdlTypeNamespace(typeNode), + templateArgClassNames, + templateArgNamespaces, + }; +} + +function collectAdlTemplateArgs( + templateTypeNode: SyntaxNode, + depth: number, + outClassNames: string[], + outNamespaces: string[], +): void { + if (depth >= ADL_TEMPLATE_RECURSION_MAX_DEPTH) return; + if (templateTypeNode.type !== 'template_type') return; + + const argList = + templateTypeNode.childForFieldName('arguments') ?? + findChildOfType(templateTypeNode, ['template_argument_list']); + if (argList === null) return; + + for (let i = 0; i < argList.namedChildCount; i++) { + const arg = argList.namedChild(i); + if (arg === null || arg.type !== 'type_descriptor') continue; + const simpleClassName = extractAdlSimpleTypeName(arg); + if (simpleClassName.length > 0) outClassNames.push(simpleClassName); + const ns = extractAdlTypeNamespace(arg); + if (ns.length > 0) outNamespaces.push(ns); + + const nestedType = arg.childForFieldName('type'); + const nestedTemplate = nestedType !== null ? findTemplateTypeNode(nestedType) : null; + if (nestedTemplate !== null) { + collectAdlTemplateArgs(nestedTemplate, depth + 1, outClassNames, outNamespaces); + } + } +} + +function findTemplateTypeNode(typeNode: SyntaxNode): SyntaxNode | null { + if (typeNode.type === 'template_type') return typeNode; + if (typeNode.type === 'type_descriptor') { + const innerType = typeNode.childForFieldName('type'); + if (innerType !== null) return findTemplateTypeNode(innerType); + return null; + } + if (typeNode.type === 'qualified_identifier') { + const nameNode = typeNode.childForFieldName('name'); + if (nameNode !== null) return findTemplateTypeNode(nameNode); + return null; + } + return null; +} + +function normalizeCppNamespaceQName(text: string): string { + const normalized = text.replace(/^::/, '').replace(/::$/, '').replace(/::/g, '.'); + return normalized; +} + +function extractNamespaceFromQualifiedText(text: string): string { + const cleaned = text.replace(/\s+/g, ''); + const idx = cleaned.lastIndexOf('::'); + if (idx <= 0) return ''; + return normalizeCppNamespaceQName(cleaned.slice(0, idx)); +} + /** * Check if a C++ function_definition or declaration has `static` storage class. */ diff --git a/gitnexus/src/core/ingestion/languages/cpp/scope-resolver.ts b/gitnexus/src/core/ingestion/languages/cpp/scope-resolver.ts index 431717d27..e91b14428 100644 --- a/gitnexus/src/core/ingestion/languages/cpp/scope-resolver.ts +++ b/gitnexus/src/core/ingestion/languages/cpp/scope-resolver.ts @@ -222,8 +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. - // V1 limitation: only direct enclosing-namespace closure for value - // class-typed args; pointer/reference/template-spec args excluded. + // Current boundary: class-typed value/pointer/reference args and template + // specializations with explicit type arguments contribute associated + // 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-template-args/app.cpp b/gitnexus/test/fixtures/lang-resolution/cpp-adl-template-args/app.cpp new file mode 100644 index 000000000..5673ee229 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/cpp-adl-template-args/app.cpp @@ -0,0 +1,23 @@ +#include "audit.h" + +namespace app { + void run() { + std::vector v; + apply(v); + } + + void runNested() { + std::map> m; + applyNested(m); + } + + void runArray() { + std::array a; + applyArray(a); + } + + void runStdConflict() { + std::vector v; + applyStdConflict(v); + } +} diff --git a/gitnexus/test/fixtures/lang-resolution/cpp-adl-template-args/audit.h b/gitnexus/test/fixtures/lang-resolution/cpp-adl-template-args/audit.h new file mode 100644 index 000000000..e59392f89 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/cpp-adl-template-args/audit.h @@ -0,0 +1,19 @@ +#pragma once + +#include +#include +#include +#include + +namespace N { + struct T {}; + + void apply(std::vector v); + void applyNested(std::map> m); + void applyArray(std::array a); + void applyStdConflict(std::vector v); +} + +namespace std { + void applyStdConflict(vector v); +} diff --git a/gitnexus/test/integration/resolvers/cpp.test.ts b/gitnexus/test/integration/resolvers/cpp.test.ts index 7bc040182..0c818b1f0 100644 --- a/gitnexus/test/integration/resolvers/cpp.test.ts +++ b/gitnexus/test/integration/resolvers/cpp.test.ts @@ -2103,7 +2103,9 @@ 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 and reference args included, template-spec args excluded. +// typed args; pointer/reference args and template specializations with +// explicit type arguments included. Function pointers and base-class +// associated namespaces remain excluded. // --------------------------------------------------------------------------- describe('C++ ADL — basic associated-namespace closure', () => { @@ -2276,6 +2278,43 @@ describe('C++ ADL — pointer-to-pointer args participate', () => { }); }); +describe('C++ ADL — template specialization args contribute associated namespaces', () => { + let result: PipelineResult; + + beforeAll(async () => { + result = await runPipelineFromRepo(path.join(FIXTURES, 'cpp-adl-template-args'), () => {}); + }, 60000); + + it('apply(v) where v is std::vector resolves to N::apply via ADL template-arg namespace', () => { + const calls = getRelationships(result, 'CALLS'); + const applyCalls = calls.filter((c) => c.source === 'run' && c.target === 'apply'); + expect(applyCalls.length).toBe(1); + expect(applyCalls[0].targetFilePath).toContain('audit.h'); + }); + + it('applyNested(m) where m is std::map> resolves via nested template-arg namespace', () => { + const calls = getRelationships(result, 'CALLS'); + const applyCalls = calls.filter((c) => c.source === 'runNested' && c.target === 'applyNested'); + expect(applyCalls.length).toBe(1); + expect(applyCalls[0].targetFilePath).toContain('audit.h'); + }); + + it('applyArray(a) where a is std::array resolves to N::applyArray (non-type arg ignored)', () => { + const calls = getRelationships(result, 'CALLS'); + const applyCalls = calls.filter((c) => c.source === 'runArray' && c.target === 'applyArray'); + expect(applyCalls.length).toBe(1); + expect(applyCalls[0].targetFilePath).toContain('audit.h'); + }); + + it('applyStdConflict(v) is suppressed when ADL surfaces both N and std candidates', () => { + const calls = getRelationships(result, 'CALLS'); + const applyCalls = calls.filter( + (c) => c.source === 'runStdConflict' && c.target === 'applyStdConflict', + ); + expect(applyCalls.length).toBe(0); + }); +}); + describe('C++ ADL — int/long-collision overloads suppress via OVERLOAD_AMBIGUOUS', () => { let result: PipelineResult;