fix(cpp): preserve helper calls in pack mixins

This commit is contained in:
azizur100389 2026-05-30 14:40:31 +01:00
parent 463f0701fc
commit 92d9e4bdc7
5 changed files with 65 additions and 66 deletions

View file

@ -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<string> {
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;

View file

@ -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;
}

View file

@ -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);

View file

@ -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 <class... Ts>
void logMany(int, Ts... xs) {
@ -20,6 +27,8 @@ template <class... B>
struct Mix : B... {
void run() {
inherited();
helper();
namespaceHelper();
}
};

View file

@ -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',