From 92d9e4bdc7293690be92790f241e443940621486 Mon Sep 17 00:00:00 2001 From: azizur100389 Date: Sat, 30 May 2026 14:40:31 +0100 Subject: [PATCH] fix(cpp): preserve helper calls in pack mixins --- .../core/ingestion/languages/cpp/captures.ts | 70 ++----------------- .../languages/cpp/two-phase-lookup.ts | 21 +++++- .../passes/free-call-fallback.ts | 15 ++++ .../main.cpp | 9 +++ .../test/integration/resolvers/cpp.test.ts | 16 +++++ 5 files changed, 65 insertions(+), 66 deletions(-) diff --git a/gitnexus/src/core/ingestion/languages/cpp/captures.ts b/gitnexus/src/core/ingestion/languages/cpp/captures.ts index 9841e492b..f0e5e9a88 100644 --- a/gitnexus/src/core/ingestion/languages/cpp/captures.ts +++ b/gitnexus/src/core/ingestion/languages/cpp/captures.ts @@ -332,13 +332,6 @@ 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); @@ -432,59 +425,6 @@ 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; @@ -528,8 +468,7 @@ function emitCppInheritanceCaptures(root: SyntaxNode, out: CaptureMatch[], fileP if (baseClause !== null) { for (const base of iterBaseClasses(baseClause)) { if (base.isPackExpansion) { - const className = getTypeIdentifierName(node); - if (className !== '') markCppDependentPackBase(filePath, className); + markClassWithPackExpandedBase(filePath, node); continue; } const baseName = extractBaseLookupName(base.node); @@ -579,7 +518,7 @@ function detectCppDependentBases(root: SyntaxNode, filePath: string): void { for (const base of iterBaseClasses(baseClause)) { if (base.isPackExpansion || isBaseDependent(base.node, params)) { if (base.isPackExpansion) { - markCppDependentPackBase(filePath, className); + markClassWithPackExpandedBase(filePath, classNode); } const baseName = extractBaseLookupName(base.node); const baseQualifier = extractBaseLookupQualifier(base.node); @@ -631,6 +570,11 @@ function collectTemplateParameterNames(templateDecl: SyntaxNode): Set { return names; } +function markClassWithPackExpandedBase(filePath: string, classNode: SyntaxNode): void { + const className = getTypeIdentifierName(classNode); + if (className !== '') markCppDependentPackBase(filePath, className); +} + interface CppBaseClassEntry { readonly node: SyntaxNode; readonly isPackExpansion: boolean; 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 487f27f84..7ec55bc9a 100644 --- a/gitnexus/src/core/ingestion/languages/cpp/two-phase-lookup.ts +++ b/gitnexus/src/core/ingestion/languages/cpp/two-phase-lookup.ts @@ -127,7 +127,7 @@ export function clearCppDependentBases(): void { * found (conservative: avoids false associations). */ export function populateCppDependentBases(parsedFiles: readonly ParsedFile[]): void { - if (dependentBasesByFile.size === 0) return; + if (dependentBasesByFile.size === 0 && dependentPackBaseClassesByFile.size === 0) return; // Build workspace-wide index: simpleName → {nodeId, nsPrefix}[] // nsPrefix is the dot-joined namespace path (qualifiedName without the @@ -303,10 +303,25 @@ export function isCppDependentBaseMember( 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(); + if (candidateDef.type !== 'Method' && candidateDef.type !== 'Constructor') return false; + const ownerName = getQualifiedParentName(candidateDef.qualifiedName); + const enclosingName = getQualifiedSimpleName(enclosing.qualifiedName); return ownerName !== undefined && ownerName !== enclosingName; } if (candidateDef.ownerId === undefined) return false; return bases.has(candidateDef.ownerId); } + +function getQualifiedParentName(qualifiedName: string | undefined): string | undefined { + if (qualifiedName === undefined) return undefined; + const lastDot = qualifiedName.lastIndexOf('.'); + if (lastDot < 0) return undefined; + const parent = qualifiedName.slice(0, lastDot); + return getQualifiedSimpleName(parent); +} + +function getQualifiedSimpleName(qualifiedName: string | undefined): string | undefined { + if (qualifiedName === undefined) return undefined; + const lastDot = qualifiedName.lastIndexOf('.'); + return lastDot >= 0 ? qualifiedName.slice(lastDot + 1) : qualifiedName; +} diff --git a/gitnexus/src/core/ingestion/scope-resolution/passes/free-call-fallback.ts b/gitnexus/src/core/ingestion/scope-resolution/passes/free-call-fallback.ts index 306bae73d..1160121ad 100644 --- a/gitnexus/src/core/ingestion/scope-resolution/passes/free-call-fallback.ts +++ b/gitnexus/src/core/ingestion/scope-resolution/passes/free-call-fallback.ts @@ -116,11 +116,13 @@ export function emitFreeCallFallback( // enclosing class. When the workspace has multiple methods of // the same name in a single class, choose the best match by // arity + argument types. + let fnDefFromImplicitThis = false; if (fnDef === undefined) { fnDef = pickImplicitThisOverload(site, scopes, workspaceIndex, model, { conversionRankFn: options.conversionRankFn, constraintCompatibility: options.constraintCompatibility, }); + fnDefFromImplicitThis = fnDef !== undefined; } // Scope-chain callable lookup. First-match preserves scope-chain // precedence (local shadows import). When a conversion-rank function @@ -336,6 +338,19 @@ export function emitFreeCallFallback( ); } if (fnDef === undefined) continue; + if ( + (fnDefFromImplicitThis || fnDef.type === 'Method' || fnDef.type === 'Constructor') && + options.isCallableVisibleFromCaller !== undefined && + !options.isCallableVisibleFromCaller({ + callerParsed: parsed, + candidate: fnDef, + callerScope: site.inScope, + scopes, + }) + ) { + handledSites.add(siteKey(parsed.filePath, site)); + continue; + } const callerGraphId = resolveCallerGraphId(site.inScope, scopes, nodeLookup); if (callerGraphId === undefined) continue; const tgtGraphId = resolveDefGraphId(fnDef.filePath, fnDef, nodeLookup); 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 index 6bfd0b05d..7b6f376c9 100644 --- a/gitnexus/test/fixtures/lang-resolution/cpp-variadic-dependent-resolution/main.cpp +++ b/gitnexus/test/fixtures/lang-resolution/cpp-variadic-dependent-resolution/main.cpp @@ -5,6 +5,13 @@ struct B { void sink(int value); void ambiguous(int value); void ambiguous(double value); +void helper(); + +namespace tools { +void namespaceHelper(); +} + +using tools::namespaceHelper; template void logMany(int, Ts... xs) { @@ -20,6 +27,8 @@ template struct Mix : B... { void run() { inherited(); + helper(); + namespaceHelper(); } }; diff --git a/gitnexus/test/integration/resolvers/cpp.test.ts b/gitnexus/test/integration/resolvers/cpp.test.ts index 5ee4097e9..43fcfd738 100644 --- a/gitnexus/test/integration/resolvers/cpp.test.ts +++ b/gitnexus/test/integration/resolvers/cpp.test.ts @@ -400,6 +400,22 @@ describe('C++ variadic packs and dependent-name resolution (#1894)', () => { expect(calls).toHaveLength(0); }); + it('preserves free helper calls inside a class with a pack-expanded dependent base', () => { + const calls = getRelationships(result, 'CALLS').filter( + (c) => c.source === 'run' && c.target === 'helper', + ); + + expect(calls).toHaveLength(1); + }); + + it('preserves using-declaration namespace helper calls inside a pack-base class', () => { + const calls = getRelationships(result, 'CALLS').filter( + (c) => c.source === 'run' && c.target === 'namespaceHelper', + ); + + expect(calls).toHaveLength(1); + }); + it('resolves current-instantiation unqualified member calls', () => { const calls = getRelationships(result, 'CALLS').filter( (c) => c.source === 'run' && c.target === 'own',