mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-09-29 01:41:42 +00:00
fix(cpp): handle variadic pack dependent lookup
This commit is contained in:
parent
e234dac849
commit
18406be8f2
5 changed files with 247 additions and 12 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
46
gitnexus/test/fixtures/lang-resolution/cpp-variadic-dependent-resolution/main.cpp
vendored
Normal file
46
gitnexus/test/fixtures/lang-resolution/cpp-variadic-dependent-resolution/main.cpp
vendored
Normal 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);
|
||||
}
|
||||
|
|
@ -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
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue