diff --git a/gitnexus/src/core/ingestion/languages/cpp/adl.ts b/gitnexus/src/core/ingestion/languages/cpp/adl.ts index 4c7b034b6..0eac14f05 100644 --- a/gitnexus/src/core/ingestion/languages/cpp/adl.ts +++ b/gitnexus/src/core/ingestion/languages/cpp/adl.ts @@ -15,14 +15,13 @@ * * ## 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 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. + * The current implementation covers class-typed arguments (value and pointer) + * and template specializations with explicit type arguments: + * - `audit::Event e`, `audit::Event* p`, `audit::Event** pp` + * - `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. * * The current implementation also short-circuits to ADL only when ordinary lookup is empty * (`findCallableBindingInScope` returned undefined). ISO C++ would @@ -67,13 +66,25 @@ 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; /** 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; + /** 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(); @@ -174,12 +185,28 @@ 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; if (arg.isReference) continue; - const classDef = findCppClassDefBySimpleName(arg.simpleClassName, scopes); - if (classDef === undefined) continue; - const nsQName = classToNamespaceQualifiedName.get(classDef.nodeId); - if (nsQName !== undefined) associatedNamespaces.add(nsQName); + + if (arg.simpleClassName !== '') { + const classDef = findCppClassDefBySimpleName(arg.simpleClassName, scopes); + if (classDef !== undefined) { + const nsQName = classToNamespaceQualifiedName.get(classDef.nodeId); + if (nsQName !== undefined) associatedNamespaces.add(nsQName); + } + } + + 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) { + if (className.length === 0) continue; + const classDef = findCppClassDefBySimpleName(className, scopes); + if (classDef === undefined) continue; + const nsQName = classToNamespaceQualifiedName.get(classDef.nodeId); + if (nsQName !== undefined) associatedNamespaces.add(nsQName); + } } if (associatedNamespaces.size === 0) return undefined; diff --git a/gitnexus/src/core/ingestion/languages/cpp/captures.ts b/gitnexus/src/core/ingestion/languages/cpp/captures.ts index 17b6962aa..fe6067dbe 100644 --- a/gitnexus/src/core/ingestion/languages/cpp/captures.ts +++ b/gitnexus/src/core/ingestion/languages/cpp/captures.ts @@ -743,7 +743,16 @@ function inferCppCallAdlArgs(callNode: SyntaxNode): CppAdlArgInfo[] { return out; } -const EMPTY_ADL_ARG: CppAdlArgInfo = { simpleClassName: '', isPointer: false, isReference: false }; +const ADL_TEMPLATE_RECURSION_MAX_DEPTH = 8; +const EMPTY_ADL_ARG: CppAdlArgInfo = { + simpleClassName: '', + isPointer: false, + isReference: false, + templateSimpleClassName: '', + templateNamespace: '', + templateArgClassNames: [], + templateArgNamespaces: [], +}; function classifyAdlArg(argNode: SyntaxNode): CppAdlArgInfo { // Literals and primitive-shaped expressions never have associated namespaces. @@ -848,29 +857,177 @@ function lookupAdlIdentifierType(identNode: SyntaxNode): CppAdlArgInfo { if (isFunctionPointer || nameText !== varName) continue; const simpleClassName = extractAdlSimpleTypeName(typeNode); - return { simpleClassName, isPointer, isReference }; + const { + templateSimpleClassName, + templateNamespace, + templateArgClassNames, + templateArgNamespaces, + } = extractAdlTemplateInfo(typeNode); + return { + simpleClassName, + isPointer, + isReference, + 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/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..91af89399 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/cpp-adl-template-args/app.cpp @@ -0,0 +1,13 @@ +#include "audit.h" + +namespace app { + void run() { + std::vector v; + apply(v); + } + + void runNested() { + std::map> m; + applyNested(m); + } +} 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..c229879e8 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/cpp-adl-template-args/audit.h @@ -0,0 +1,12 @@ +#pragma once + +#include +#include +#include + +namespace N { + struct T {}; + + void apply(std::vector v); + void applyNested(std::map> m); +} diff --git a/gitnexus/test/integration/resolvers/cpp.test.ts b/gitnexus/test/integration/resolvers/cpp.test.ts index cd95cfae7..fa92f9b92 100644 --- a/gitnexus/test/integration/resolvers/cpp.test.ts +++ b/gitnexus/test/integration/resolvers/cpp.test.ts @@ -2228,6 +2228,28 @@ 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'); + }); +}); + describe('C++ ADL — int/long-collision overloads suppress via OVERLOAD_AMBIGUOUS', () => { let result: PipelineResult;