Skip to content
36 changes: 31 additions & 5 deletions gitnexus/src/core/ingestion/languages/cpp/captures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -475,8 +475,9 @@ function detectCppDependentBases(root: SyntaxNode, filePath: string): void {
for (const base of iterBaseClasses(baseClause)) {
if (isBaseDependent(base, params)) {
const baseName = extractBaseLookupName(base);
const baseQualifier = extractBaseLookupQualifier(base);
if (baseName !== '') {
markCppDependentBase(filePath, className, baseName);
markCppDependentBase(filePath, className, baseName, baseQualifier);
}
}
}
Expand Down Expand Up @@ -553,10 +554,14 @@ function* iterBaseClasses(baseClause: SyntaxNode): IterableIterator<SyntaxNode>
*/
function isBaseDependent(baseNode: SyntaxNode, templateParams: Set<string>): boolean {
if (baseNode.type !== 'template_type') {
// Bare `type_identifier` or `qualified_identifier` bases — not
// dependent (the base name itself doesn't reference a template
// parameter at this level).
return false;
if (baseNode.type === 'qualified_identifier') {
// Qualified identifier bases (e.g. `detail::Inner<T>`) may contain
// template_type children — descend into them for template param check.
// Fall through to the stack walk below.
} else {
// Bare `type_identifier` bases — not dependent.
return false;
}
}
// Walk all descendants of the template_argument_list looking for any
// type_identifier matching a template parameter, or any conservative-
Expand Down Expand Up @@ -624,6 +629,27 @@ function extractBaseLookupName(baseNode: SyntaxNode): string {
return '';
}

/** Extract the syntactic namespace qualifier from a base class node.
* For `detail::Inner<T>`, returns `'detail'`.
* For unqualified bases (`Inner<T>`, `Base<int>`), returns `''`.
* Nested qualifiers (`a::b::Inner<T>`) return the full scope text.
*/
function extractBaseLookupQualifier(baseNode: SyntaxNode): string {
if (baseNode.type === 'qualified_identifier') {
const scopeNode = baseNode.childForFieldName('scope');
if (scopeNode !== null) return scopeNode.text;
}
// template_type nodes may have a qualified_identifier as their name child
if (baseNode.type === 'template_type') {
const nameNode = baseNode.childForFieldName('name');
if (nameNode !== null && nameNode.type === 'qualified_identifier') {
const scopeNode = nameNode.childForFieldName('scope');
if (scopeNode !== null) return scopeNode.text;
}
}
return '';
}

/**
* Walk parent chain from a function_definition / declaration / field_declaration
* to find the enclosing `template_declaration`. Returns null when the function
Expand Down
161 changes: 107 additions & 54 deletions gitnexus/src/core/ingestion/languages/cpp/two-phase-lookup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,14 @@ import { findEnclosingClassDef } from '../../scope-resolution/scope/walkers.js';

/**
* Capture-time record: for each template class declaration in a file,
* the simple names of its dependent base classes.
* the simple names of its dependent base classes and their syntactic
* qualifiers (e.g., `detail` for `detail::Inner<T>`).
*
* Key: filePath
* Value: Map<className, Set<dependentBaseSimpleName>>
* Value: Map<className, Map<baseName, qualifier>>
* qualifier is '' when the base was unqualified.
*/
const dependentBasesByFile = new Map<string, Map<string, Set<string>>>();
const dependentBasesByFile = new Map<string, Map<string, Map<string, Set<string>>>>();

/**
* Post-`populateOwners` resolution: per-class-nodeId, the set of
Expand All @@ -55,23 +57,35 @@ const dependentBaseNodeIds = new Map<string, Set<string>>();
* Record a dependent-base relationship discovered during scope-capture
* emission. `className` is the simple name of the template class;
* `baseName` is the simple name of the dependent base class.
* `qualifier` is the syntactic namespace qualifier (e.g. `detail` for
* `detail::Inner<T>`), or '' for unqualified bases.
*
* The capture-time recorder uses simple names because the registry
* resolution that maps names → nodeIds runs later (in
* `populateCppDependentBases`).
*/
export function markCppDependentBase(filePath: string, className: string, baseName: string): void {
export function markCppDependentBase(
filePath: string,
className: string,
baseName: string,
qualifier = '',
): void {
let perFile = dependentBasesByFile.get(filePath);
if (perFile === undefined) {
perFile = new Map();
dependentBasesByFile.set(filePath, perFile);
}
let bases = perFile.get(className);
if (bases === undefined) {
bases = new Set();
bases = new Map();
perFile.set(className, bases);
}
bases.add(baseName);
let quals = bases.get(baseName);
if (quals === undefined) {
quals = new Set();
bases.set(baseName, quals);
}
quals.add(qualifier);
}

/** Clear two-phase-lookup state. Called from `clearFileLocalNames`. */
Expand All @@ -89,8 +103,10 @@ export function clearCppDependentBases(): void {
* Disambiguation strategy (multiple classes sharing a simple name):
* 1. Prefer the candidate whose qualified-name namespace prefix matches
* the deriving class's namespace prefix (same-namespace bias).
* 2. Fall back to accepting a unique simple-name match.
* 3. Skip when multiple candidates exist and no namespace match is
* 2. When a syntactic qualifier is available (`detail` in
* `detail::Inner<T>`), target the exact namespace derived from it.
* 3. Fall back to accepting a unique simple-name match.
* 4. Skip when multiple candidates exist and no namespace match is
* found (conservative: avoids false associations).
*/
export function populateCppDependentBases(parsedFiles: readonly ParsedFile[]): void {
Expand All @@ -99,7 +115,13 @@ export function populateCppDependentBases(parsedFiles: readonly ParsedFile[]): v
// Build workspace-wide index: simpleName → {nodeId, nsPrefix}[]
// nsPrefix is the dot-joined namespace path (qualifiedName without the
// last segment). Classes at global scope have nsPrefix = ''.
// Dedup by nodeId, keeping the LAST occurrence: parsed.localDefs may
// list the same class def multiple times — the scope-extractor creates
// a def with simple-name qualifiedName first, then the class extractor
// replaces it with the correct fully-qualified qualifiedName. Keeping
// the later entry ensures we capture the full namespace path.
const classesBySimpleName = new Map<string, { nodeId: string; nsPrefix: string }[]>();
const entryByNodeId = new Map<string, { nodeId: string; nsPrefix: string; simple: string }>();
for (const parsed of parsedFiles) {
for (const def of parsed.localDefs) {
if (def.type !== 'Class' && def.type !== 'Struct' && def.type !== 'Interface') continue;
Expand All @@ -108,14 +130,17 @@ export function populateCppDependentBases(parsedFiles: readonly ParsedFile[]): v
const simple = lastDot >= 0 ? qn.slice(lastDot + 1) : qn;
if (simple === '') continue;
const nsPrefix = lastDot >= 0 ? qn.slice(0, lastDot) : '';
let entries = classesBySimpleName.get(simple);
if (entries === undefined) {
entries = [];
classesBySimpleName.set(simple, entries);
}
entries.push({ nodeId: def.nodeId, nsPrefix });
entryByNodeId.set(def.nodeId, { nodeId: def.nodeId, nsPrefix, simple });
}
}
for (const entry of entryByNodeId.values()) {
let entries = classesBySimpleName.get(entry.simple);
if (entries === undefined) {
entries = [];
classesBySimpleName.set(entry.simple, entries);
}
entries.push({ nodeId: entry.nodeId, nsPrefix: entry.nsPrefix });
}

// Build a filePath → ParsedFile lookup for fast per-file access.
const parsedByFile = new Map<string, ParsedFile>();
Expand All @@ -139,7 +164,12 @@ export function populateCppDependentBases(parsedFiles: readonly ParsedFile[]): v
localClassByName.set(simple, { nodeId: def.nodeId, nsPrefix });
}

for (const [className, baseNames] of perFile) {
// 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
// the V2 prefix-contains heuristic when the qualifier isn't available or
// the exact match fails (absolute qualifier edge cases like `::std`).
for (const [className, baseEntries] of perFile) {
const classEntry = localClassByName.get(className);
if (classEntry === undefined) continue;

Expand All @@ -149,54 +179,77 @@ export function populateCppDependentBases(parsedFiles: readonly ParsedFile[]): v
dependentBaseNodeIds.set(classEntry.nodeId, bases);
}

for (const baseName of baseNames) {
const candidates = classesBySimpleName.get(baseName);
if (candidates === undefined || candidates.length === 0) continue;
for (const [baseName, qualsSet] of baseEntries) {
for (const baseQualifier of qualsSet) {
const candidates = classesBySimpleName.get(baseName);
if (candidates === undefined || candidates.length === 0) continue;

if (candidates.length === 1) {
// Unique simple-name match — accept regardless of namespace.
bases.add(candidates[0].nodeId);
continue;
}
// Compute the expected namespace prefix from the qualifier.
// Relative qualifier (e.g. `inner`): prepend deriving class's prefix.
// Absolute qualifiers (`::std`, `ns::other`) will fail the relative
// lookup and fall through to the prefix-heuristic below.
const normalizedQualifier = baseQualifier.replace(/::/g, '.');
const expectedNs =
baseQualifier && classEntry.nsPrefix
? classEntry.nsPrefix + '.' + normalizedQualifier
: normalizedQualifier;

// Multiple classes share the same simple name — prefer the one
// whose namespace matches the deriving class's namespace.
// V2: filter by prefix-match capped at one level deeper, then
// accept only if exactly one candidate survives. This lets
// Derived<T> in ns::outer find Inner<T> in ns::outer::inner
// (or ns::v1 for inline-namespace variants) while rejecting
// sibling collisions (e.g. detail::Inner vs public_api::Inner).
//
// The one-segment cap limits walk depth: ns → ns.a ✓, ns → ns.a.b ✗.
// Global-scope deriving classes match any single-segment namespace.
//
// LIMITATION: True ISO behavior would use the base specifier's
// syntactic qualifier (available at captures.ts:611 as
// qualified_identifier scope) to navigate from the current scope,
// which would resolve `detail::Inner` vs `public_api::Inner`
// unambiguously. Threading the qualifier is tracked in #1815.
// Until then, sibling collisions correctly suppress.
const nsMatches = candidates.filter((c) => {
if (c.nsPrefix === classEntry.nsPrefix) return true;
if (classEntry.nsPrefix === '') {
return c.nsPrefix !== '' && !c.nsPrefix.includes('.');
if (candidates.length === 1) {
// Unqualified base: accept unique match (pre-existing behavior).
if (!baseQualifier) {
bases.add(candidates[0].nodeId);
continue;
}
// Qualified base: verify namespace before accepting.
if (
candidates[0].nsPrefix === expectedNs ||
candidates[0].nsPrefix === normalizedQualifier
) {
bases.add(candidates[0].nodeId);
}
// else: suppress — qualifier doesn't match. #1564 policy.
continue;
}
if (c.nsPrefix.startsWith(classEntry.nsPrefix + '.')) {
const suffix = c.nsPrefix.slice(classEntry.nsPrefix.length + 1);
return !suffix.includes('.');

// V3: qualifier-based exact targeting. When the base specifier
// carries a syntactic qualifier, compute the expected namespace
// prefix and attempt an exact (===) match using the deduplicated
// nsPrefix. Dedup by nodeId removes broken entries from the
// classesBySimpleName index, making the surviving nsPrefix reliable.
if (baseQualifier) {
const qualifierMatch = candidates.find(
(c) => c.nsPrefix === expectedNs || c.nsPrefix === normalizedQualifier,
);
if (qualifierMatch !== undefined) {
bases.add(qualifierMatch.nodeId);
continue;
}
continue; // qualifier was explicit but no match — suppress, don't fall through to V2
}

// V2 fallback: filter by prefix-match capped at one level deeper,
// then accept only if exactly one candidate survives.
const nsMatches = candidates.filter((c) => {
if (c.nsPrefix === classEntry.nsPrefix) return true;
if (classEntry.nsPrefix === '') {
return c.nsPrefix !== '' && !c.nsPrefix.includes('.');
}
if (c.nsPrefix.startsWith(classEntry.nsPrefix + '.')) {
const suffix = c.nsPrefix.slice(classEntry.nsPrefix.length + 1);
return !suffix.includes('.');
}
return false;
});
const nsMatch = nsMatches.length === 1 ? nsMatches[0] : undefined;
if (nsMatch !== undefined) {
bases.add(nsMatch.nodeId);
}
return false;
});
const nsMatch = nsMatches.length === 1 ? nsMatches[0] : undefined;
if (nsMatch !== undefined) {
bases.add(nsMatch.nodeId);
// else: ambiguous (multiple candidates, no namespace match) → skip.
}
// else: ambiguous (multiple candidates, no namespace match) → skip.
}
}
}
}

/**
* Two-phase lookup predicate: is the candidate def a member of a
* dependent base of the caller's enclosing template class?
Expand Down
Loading