From 18406be8f2b02d7be09a1da70e0459c045380ead Mon Sep 17 00:00:00 2001 From: azizur100389 Date: Fri, 29 May 2026 23:05:38 +0100 Subject: [PATCH] fix(cpp): handle variadic pack dependent lookup --- .../core/ingestion/languages/cpp/captures.ts | 106 ++++++++++++++++-- .../languages/cpp/two-phase-lookup.ts | 35 +++++- .../main.cpp | 46 ++++++++ .../test/integration/resolvers/cpp.test.ts | 67 +++++++++++ .../test/integration/resolvers/helpers.ts | 5 + 5 files changed, 247 insertions(+), 12 deletions(-) create mode 100644 gitnexus/test/fixtures/lang-resolution/cpp-variadic-dependent-resolution/main.cpp diff --git a/gitnexus/src/core/ingestion/languages/cpp/captures.ts b/gitnexus/src/core/ingestion/languages/cpp/captures.ts index 86384a759..9841e492b 100644 --- a/gitnexus/src/core/ingestion/languages/cpp/captures.ts +++ b/gitnexus/src/core/ingestion/languages/cpp/captures.ts @@ -15,7 +15,7 @@ import { computeCppCallArity, } from './arity-metadata.js'; import { markCppAnonymousNamespaceRange, markFileLocal } from './file-local-linkage.js'; -import { markCppDependentBase } from './two-phase-lookup.js'; +import { markCppDependentBase, markCppDependentPackBase } from './two-phase-lookup.js'; import { markCppAdlSiteArgs, markCppAdlSiteNoAdl, type CppAdlArgInfo } from './adl.js'; import { markCppInlineNamespaceRange } from './inline-namespaces.js'; import { extractCppTemplateConstraints } from './constraint-extractor.js'; @@ -332,6 +332,13 @@ export function emitCppScopeCaptures( 'call_expression', ); if (freeCallNode !== null) { + const callName = grouped['@reference.name']?.text; + if ( + callName !== undefined && + isUnqualifiedCallSuppressedByPackBase(freeCallNode, callName) + ) { + continue; + } const adlAnchorRange = grouped['@reference.call.free']!.range; if (isParenthesizedFunctionCall(freeCallNode)) { markCppAdlSiteNoAdl(filePath, adlAnchorRange.startLine, adlAnchorRange.startCol); @@ -410,7 +417,7 @@ export function emitCppScopeCaptures( // captures consumed by the registry-primary graph bridge. The lookup name // is normalized to the bare class name so `Base` / `outer::v1::Base` // resolve through V1's simple-name `findClassBindingInScope('Base')`. - emitCppInheritanceCaptures(tree.rootNode, out); + emitCppInheritanceCaptures(tree.rootNode, out, filePath); // ── Detect dependent-base relationships for two-phase template lookup ── // Walk the tree once, finding every `template_declaration` whose @@ -425,6 +432,59 @@ export function emitCppScopeCaptures( return out; } +function isUnqualifiedCallSuppressedByPackBase(callNode: SyntaxNode, callName: string): boolean { + const classNode = findEnclosingClassLike(callNode); + if (classNode === null) return false; + const baseClause = findChildOfType(classNode, ['base_class_clause']); + if (baseClause === null) return false; + const hasPackBase = [...iterBaseClasses(baseClause)].some((base) => base.isPackExpansion); + if (!hasPackBase) return false; + return !classDeclaresMemberNamed(classNode, callName); +} + +function findEnclosingClassLike(node: SyntaxNode): SyntaxNode | null { + let cur = node.parent; + while (cur !== null) { + if (cur.type === 'class_specifier' || cur.type === 'struct_specifier') return cur; + cur = cur.parent; + } + return null; +} + +function classDeclaresMemberNamed(classNode: SyntaxNode, memberName: string): boolean { + const body = findChildOfType(classNode, ['field_declaration_list']); + if (body === null) return false; + const stack: SyntaxNode[] = [body]; + while (stack.length > 0) { + const node = stack.pop()!; + if ( + (node.type === 'function_declarator' || node.type === 'field_declaration') && + getFunctionDeclaratorName(node) === memberName + ) { + return true; + } + for (let i = 0; i < node.namedChildCount; i++) { + const child = node.namedChild(i); + if (child !== null) stack.push(child); + } + } + return false; +} + +function getFunctionDeclaratorName(node: SyntaxNode): string { + const nameNode = node.childForFieldName('declarator') ?? node.childForFieldName('name'); + if (nameNode?.type === 'field_identifier' || nameNode?.type === 'identifier') { + return nameNode.text; + } + for (let i = 0; i < node.namedChildCount; i++) { + const child = node.namedChild(i); + if (child === null) continue; + const nested = getFunctionDeclaratorName(child); + if (nested !== '') return nested; + } + return ''; +} + function extractCppDeclarationReturnType(fnNode: SyntaxNode): string | undefined { const typeNode = fnNode.childForFieldName('type'); if (typeNode === null) return undefined; @@ -459,7 +519,7 @@ function isCppUnsupportedReturnTypeDeclarator(funcDeclarator: SyntaxNode): boole * here instead of introducing a C++-only name-resolution lane in shared * ingestion infrastructure. */ -function emitCppInheritanceCaptures(root: SyntaxNode, out: CaptureMatch[]): void { +function emitCppInheritanceCaptures(root: SyntaxNode, out: CaptureMatch[], filePath: string): void { const stack: SyntaxNode[] = [root]; while (stack.length > 0) { const node = stack.pop()!; @@ -467,11 +527,16 @@ function emitCppInheritanceCaptures(root: SyntaxNode, out: CaptureMatch[]): void const baseClause = findChildOfType(node, ['base_class_clause']); if (baseClause !== null) { for (const base of iterBaseClasses(baseClause)) { - const baseName = extractBaseLookupName(base); + if (base.isPackExpansion) { + const className = getTypeIdentifierName(node); + if (className !== '') markCppDependentPackBase(filePath, className); + continue; + } + const baseName = extractBaseLookupName(base.node); if (baseName.length === 0) continue; out.push({ - '@reference.inherits': nodeToCapture('@reference.inherits', base), - '@reference.name': syntheticCapture('@reference.name', base, baseName), + '@reference.inherits': nodeToCapture('@reference.inherits', base.node), + '@reference.name': syntheticCapture('@reference.name', base.node, baseName), }); } } @@ -512,9 +577,12 @@ function detectCppDependentBases(root: SyntaxNode, filePath: string): void { const baseClause = findChildOfType(classNode, ['base_class_clause']); if (baseClause !== null) { for (const base of iterBaseClasses(baseClause)) { - if (isBaseDependent(base, params)) { - const baseName = extractBaseLookupName(base); - const baseQualifier = extractBaseLookupQualifier(base); + if (base.isPackExpansion || isBaseDependent(base.node, params)) { + if (base.isPackExpansion) { + markCppDependentPackBase(filePath, className); + } + const baseName = extractBaseLookupName(base.node); + const baseQualifier = extractBaseLookupQualifier(base.node); if (baseName !== '') { markCppDependentBase(filePath, className, baseName, baseQualifier); } @@ -563,8 +631,13 @@ function collectTemplateParameterNames(templateDecl: SyntaxNode): Set { return names; } +interface CppBaseClassEntry { + readonly node: SyntaxNode; + readonly isPackExpansion: boolean; +} + /** Yield each base-class entry from a `base_class_clause`. */ -function* iterBaseClasses(baseClause: SyntaxNode): IterableIterator { +function* iterBaseClasses(baseClause: SyntaxNode): IterableIterator { for (let i = 0; i < baseClause.childCount; i++) { const child = baseClause.child(i); if (child === null) continue; @@ -575,11 +648,22 @@ function* iterBaseClasses(baseClause: SyntaxNode): IterableIterator child.type === 'template_type' || child.type === 'qualified_identifier' ) { - yield child; + yield { node: child, isPackExpansion: isFollowedByPackExpansion(baseClause, i) }; } } } +function isFollowedByPackExpansion(baseClause: SyntaxNode, childIndex: number): boolean { + for (let i = childIndex + 1; i < baseClause.childCount; i++) { + const sibling = baseClause.child(i); + if (sibling === null) continue; + if (sibling.type === '...' || (!sibling.isNamed && sibling.text === '...')) return true; + if (sibling.type === ',' || sibling.type === 'access_specifier') return false; + if (sibling.isNamed) return false; + } + return false; +} + /** * A base is dependent when: * - it's a `template_type` and its argument list contains a diff --git a/gitnexus/src/core/ingestion/languages/cpp/two-phase-lookup.ts b/gitnexus/src/core/ingestion/languages/cpp/two-phase-lookup.ts index 928138ade..487f27f84 100644 --- a/gitnexus/src/core/ingestion/languages/cpp/two-phase-lookup.ts +++ b/gitnexus/src/core/ingestion/languages/cpp/two-phase-lookup.ts @@ -46,6 +46,13 @@ import { findEnclosingClassDef } from '../../scope-resolution/scope/walkers.js'; */ const dependentBasesByFile = new Map>>>(); +/** + * Class templates with pack-expanded bases (`struct Mix : Bases...`) have + * an unknown set of base classes. Unqualified member lookup inside the class + * cannot safely bind to class-owned methods outside the current class. + */ +const dependentPackBaseClassesByFile = new Map>(); + /** * Post-`populateOwners` resolution: per-class-nodeId, the set of * dependent-base-class nodeIds. Built by `populateCppDependentBases` @@ -88,9 +95,19 @@ export function markCppDependentBase( quals.add(qualifier); } +export function markCppDependentPackBase(filePath: string, className: string): void { + let perFile = dependentPackBaseClassesByFile.get(filePath); + if (perFile === undefined) { + perFile = new Set(); + dependentPackBaseClassesByFile.set(filePath, perFile); + } + perFile.add(className); +} + /** Clear two-phase-lookup state. Called from `clearFileLocalNames`. */ export function clearCppDependentBases(): void { dependentBasesByFile.clear(); + dependentPackBaseClassesByFile.clear(); dependentBaseNodeIds.clear(); } @@ -164,6 +181,16 @@ export function populateCppDependentBases(parsedFiles: readonly ParsedFile[]): v localClassByName.set(simple, { nodeId: def.nodeId, nsPrefix }); } + const packBaseClasses = dependentPackBaseClassesByFile.get(filePath); + if (packBaseClasses !== undefined) { + for (const className of packBaseClasses) { + const classEntry = localClassByName.get(className); + if (classEntry !== undefined) { + dependentBaseNodeIds.set(classEntry.nodeId, new Set(['*pack-expansion*'])); + } + } + } + // V3: qualifier-based exact targeting. When the base specifier carries // a syntactic qualifier (e.g., `detail` in `detail::Inner`), compute // the expected namespace prefix and use exact (===) match. Falls back to @@ -270,10 +297,16 @@ export function isCppDependentBaseMember( candidateDef: SymbolDefinition, scopes: ScopeResolutionIndexes, ): boolean { - if (candidateDef.ownerId === undefined) return false; const enclosing = findEnclosingClassDef(callerScopeId, scopes); if (enclosing === undefined) return false; const bases = dependentBaseNodeIds.get(enclosing.nodeId); if (bases === undefined) return false; + if (bases.has('*pack-expansion*')) { + if (candidateDef.ownerId !== undefined) return candidateDef.ownerId !== enclosing.nodeId; + const ownerName = candidateDef.qualifiedName?.split('.').slice(-2, -1)[0]; + const enclosingName = enclosing.qualifiedName?.split('.').pop(); + return ownerName !== undefined && ownerName !== enclosingName; + } + if (candidateDef.ownerId === undefined) return false; return bases.has(candidateDef.ownerId); } diff --git a/gitnexus/test/fixtures/lang-resolution/cpp-variadic-dependent-resolution/main.cpp b/gitnexus/test/fixtures/lang-resolution/cpp-variadic-dependent-resolution/main.cpp new file mode 100644 index 000000000..6bfd0b05d --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/cpp-variadic-dependent-resolution/main.cpp @@ -0,0 +1,46 @@ +struct B { + void inherited(); +}; + +void sink(int value); +void ambiguous(int value); +void ambiguous(double value); + +template +void logMany(int, Ts... xs) { + (sink(xs), ...); +} + +template +void foldAmbiguous(Ts... xs) { + (ambiguous(xs), ...); +} + +template +struct Mix : B... { + void run() { + inherited(); + } +}; + +template +struct Current { + void own(); + + void run() { + own(); + } +}; + +template +struct UnknownSpecialization { + typename T::value_type value; + + void run() { + value.use(); + } +}; + +void callVariadic() { + logMany(1, 2, 3); +} diff --git a/gitnexus/test/integration/resolvers/cpp.test.ts b/gitnexus/test/integration/resolvers/cpp.test.ts index 57fadebb1..5ee4097e9 100644 --- a/gitnexus/test/integration/resolvers/cpp.test.ts +++ b/gitnexus/test/integration/resolvers/cpp.test.ts @@ -350,6 +350,73 @@ describe('C++ variadic call resolution', () => { }); }); +describe('C++ variadic packs and dependent-name resolution (#1894)', () => { + let result: PipelineResult; + + beforeAll(async () => { + result = await runPipelineFromRepo( + path.join(FIXTURES, 'cpp-variadic-dependent-resolution'), + () => {}, + ); + }, 60000); + + it('keeps parameter-pack functions viable when call arity exceeds the fixed prefix', () => { + const calls = getRelationships(result, 'CALLS').filter( + (c) => c.source === 'callVariadic' && c.target === 'logMany', + ); + + expect(calls).toHaveLength(1); + }); + + it('emits one fold-expression edge when the folded callee is unambiguous', () => { + const calls = getRelationships(result, 'CALLS').filter( + (c) => c.source === 'logMany' && c.target === 'sink', + ); + + expect(calls).toHaveLength(1); + }); + + it('emits zero fold-expression edges when overload resolution remains ambiguous', () => { + const calls = getRelationships(result, 'CALLS').filter( + (c) => c.source === 'foldAmbiguous' && c.target === 'ambiguous', + ); + + expect(calls).toHaveLength(0); + }); + + it('does not emit a concrete EXTENDS edge for a pack-expanded base', () => { + const extendsEdges = getRelationships(result, 'EXTENDS').filter( + (e) => e.source === 'Mix' && e.target === 'B', + ); + + expect(extendsEdges).toHaveLength(0); + }); + + it('does not bind unqualified member lookup through a pack-expanded dependent base', () => { + const calls = getRelationships(result, 'CALLS').filter( + (c) => c.source === 'run' && c.target === 'inherited', + ); + + expect(calls).toHaveLength(0); + }); + + it('resolves current-instantiation unqualified member calls', () => { + const calls = getRelationships(result, 'CALLS').filter( + (c) => c.source === 'run' && c.target === 'own', + ); + + expect(calls).toHaveLength(1); + }); + + it('keeps unknown-specialization member types unresolved', () => { + const calls = getRelationships(result, 'CALLS').filter( + (c) => c.source === 'run' && c.target === 'use', + ); + + expect(calls).toHaveLength(0); + }); +}); + // --------------------------------------------------------------------------- // Local shadow: same-file definition takes priority over imported name // --------------------------------------------------------------------------- diff --git a/gitnexus/test/integration/resolvers/helpers.ts b/gitnexus/test/integration/resolvers/helpers.ts index d4722d2e7..d6b1a59cd 100644 --- a/gitnexus/test/integration/resolvers/helpers.ts +++ b/gitnexus/test/integration/resolvers/helpers.ts @@ -313,6 +313,11 @@ const LEGACY_RESOLVER_PARITY_EXPECTED_FAILURES: Readonly B::inherited`. + 'does not bind unqualified member lookup through a pack-expanded dependent base', // User-defined conversion ranking (#1631) builds on the C++ // conversion-rank hook and the registry-primary C++ owner sidecars. // Legacy DAG has no user-defined-conversion sidecar or ranking path.