fix(cpp): handle variadic pack dependent lookup

This commit is contained in:
azizur100389 2026-05-29 23:05:38 +01:00
parent e234dac849
commit 18406be8f2
5 changed files with 247 additions and 12 deletions

View file

@ -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<T>` / `outer::v1::Base<T>`
// 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<string> {
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<SyntaxNode> {
function* iterBaseClasses(baseClause: SyntaxNode): IterableIterator<CppBaseClassEntry> {
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<SyntaxNode>
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

View file

@ -46,6 +46,13 @@ import { findEnclosingClassDef } from '../../scope-resolution/scope/walkers.js';
*/
const dependentBasesByFile = new Map<string, Map<string, Map<string, Set<string>>>>();
/**
* 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<string, Set<string>>();
/**
* 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<T>`), 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);
}

View file

@ -0,0 +1,46 @@
struct B {
void inherited();
};
void sink(int value);
void ambiguous(int value);
void ambiguous(double value);
template <class... Ts>
void logMany(int, Ts... xs) {
(sink(xs), ...);
}
template <class... Ts>
void foldAmbiguous(Ts... xs) {
(ambiguous(xs), ...);
}
template <class... B>
struct Mix : B... {
void run() {
inherited();
}
};
template <class T>
struct Current {
void own();
void run() {
own();
}
};
template <class T>
struct UnknownSpecialization {
typename T::value_type value;
void run() {
value.use();
}
};
void callVariadic() {
logMany(1, 2, 3);
}

View file

@ -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
// ---------------------------------------------------------------------------

View file

@ -313,6 +313,11 @@ const LEGACY_RESOLVER_PARITY_EXPECTED_FAILURES: Readonly<Record<string, Readonly
'g(1, 2) resolves to fixed-arity g(int, int), not g(int, ...)',
"h(1, 'a') resolves to h(int, double), not h(int, ...)",
'k(1, 2, 3) keeps the ellipsis overload viable when it is the only match',
// Pack-expanded dependent bases (`struct Mix : B...`) are suppressed
// at C++ scope-capture time in the registry-primary path. The legacy
// DAG still sees same-file class-owned methods by simple name and
// over-emits `Mix::run -> 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.