diff --git a/gitnexus/src/core/ingestion/languages/cpp/captures.ts b/gitnexus/src/core/ingestion/languages/cpp/captures.ts index de8cd058e9..1c669738dc 100644 --- a/gitnexus/src/core/ingestion/languages/cpp/captures.ts +++ b/gitnexus/src/core/ingestion/languages/cpp/captures.ts @@ -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); } } } @@ -553,10 +554,14 @@ function* iterBaseClasses(baseClause: SyntaxNode): IterableIterator */ function isBaseDependent(baseNode: SyntaxNode, templateParams: Set): 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`) 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- @@ -624,6 +629,27 @@ function extractBaseLookupName(baseNode: SyntaxNode): string { return ''; } +/** Extract the syntactic namespace qualifier from a base class node. + * For `detail::Inner`, returns `'detail'`. + * For unqualified bases (`Inner`, `Base`), returns `''`. + * Nested qualifiers (`a::b::Inner`) 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 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 ec95d77011..928138adeb 100644 --- a/gitnexus/src/core/ingestion/languages/cpp/two-phase-lookup.ts +++ b/gitnexus/src/core/ingestion/languages/cpp/two-phase-lookup.ts @@ -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`). * * Key: filePath - * Value: Map> + * Value: Map> + * qualifier is '' when the base was unqualified. */ -const dependentBasesByFile = new Map>>(); +const dependentBasesByFile = new Map>>>(); /** * Post-`populateOwners` resolution: per-class-nodeId, the set of @@ -55,12 +57,19 @@ const dependentBaseNodeIds = new Map>(); * 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`), 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(); @@ -68,10 +77,15 @@ export function markCppDependentBase(filePath: string, className: string, baseNa } 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`. */ @@ -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`), 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 { @@ -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(); + const entryByNodeId = new Map(); for (const parsed of parsedFiles) { for (const def of parsed.localDefs) { if (def.type !== 'Class' && def.type !== 'Struct' && def.type !== 'Interface') continue; @@ -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(); @@ -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`), 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; @@ -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 in ns::outer find Inner 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?