mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-08-28 05:25:25 +00:00
feat(cpp): resolve inheritance-lattice member lookup (#2077)
* feat(cpp): resolve inheritance-lattice member lookup * fix(cpp): harden inheritance-lattice lookup --------- Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
This commit is contained in:
parent
774cd4d568
commit
3a4247ec36
14 changed files with 1066 additions and 11 deletions
|
|
@ -18,11 +18,11 @@
|
|||
"_rebaselined": "#1919 open-language coverage: new lang-resolution fixtures + intended capture additions (F5/F9 c-cpp, F26/F28/F29 dart, F47/F48/F49/F51/F52 kotlin, F75/F79 swift). Fingerprint-only drift; scaling_ratio ~1.0 (linear, no perf regression)."
|
||||
},
|
||||
"cpp": {
|
||||
"fingerprint": "fd3d3768cdebbb4767d7cf18b8d2df19d61de969c816d7f4d6b599f947811356",
|
||||
"fingerprint": "f56625342f73e182170e2c964d538e316c079fa6e9466a7f076bff2ebcf8aac4",
|
||||
"scaling_budget": 1.5,
|
||||
"_added": "#1956: cpp added to the scope-capture bench (was UNBENCHED). Heritage-bearing scale source (: public Base, public Mixin) drives emitCppInheritanceCaptures at scale. Adding it exposed + fixed a pre-existing O(n^2) findNodeAtRange root-walk in cpp/captures.ts (~12 sites, threaded c.node, byte-identical over 263 cpp-* fixtures); scaling 2.30 -> 1.12.",
|
||||
"_rebaselined": "#1919 open-language coverage: new lang-resolution fixtures + intended capture additions (F5/F9 c-cpp, F26/F28/F29 dart, F47/F48/F49/F51/F52 kotlin, F75/F79 swift). Fingerprint-only drift; scaling_ratio ~1.0 (linear, no perf regression).",
|
||||
"_note": "#1975: + cpp-out-of-line-class fixture, fixture_count 263->265. #1990: + cpp-adl-ns-plus-hidden-friend-same-name fixture (ADL hidden-friend + namespace-callable merge parity test). Pure fixture-corpus drift — no scope-extractor change; existing fixtures' captures byte-identical. fixture_count 265->267. #1995: + cpp-union-nested-tail-collision and cpp-anon-ns-tail-collision fixtures — pure fixture-corpus drift; fixture_count 270->272, fingerprint 538e8be->d63ded6. #1993: + cpp-cross-namespace-same-tail fixture — pure fixture-corpus drift; fixture_count 272->273, fingerprint d63ded6->6d6207ae."
|
||||
"_note": "#1975: + cpp-out-of-line-class fixture, fixture_count 263->265. #1990: + cpp-adl-ns-plus-hidden-friend-same-name fixture (ADL hidden-friend + namespace-callable merge parity test). Pure fixture-corpus drift — no scope-extractor change; existing fixtures' captures byte-identical. fixture_count 265->267. #1995: + cpp-union-nested-tail-collision and cpp-anon-ns-tail-collision fixtures — pure fixture-corpus drift; fixture_count 270->272, fingerprint 538e8be->d63ded6. #1993: + cpp-cross-namespace-same-tail fixture — pure fixture-corpus drift; fixture_count 272->273, fingerprint d63ded6->6d6207ae. #2077 review follow-up: cpp-member-lattice adds cross-file, qualified-base, nested-template, inherited-using, this-receiver, and non-virtual-override regressions; fixture_count 274->275. Capture scaling remains linear (1.134 < 1.5)."
|
||||
},
|
||||
"csharp": {
|
||||
"_rebaselined": "#1956 synth-widening: + csharp-qualified-base fixture; the synth now walks record_declaration + struct_declaration base_lists and handles alias_qualified_name (matching the #1940 legacy leg), so record/struct heritage now emits. csharp-record-base gains a record inherits capture. (record->record SAME-namespace EXTENDS is a separate registry resolution gap, tracked as follow-up.) Linear (~1.00). (Earlier #1956: heritage-bearing scale source.) | #942: scope-resolution-only cleanup reworded fixture comments; capture byte-positions shift, capture LOGIC unchanged. | #1924 F16: record primary-constructor base bindings now exclude constructor arguments; capture fingerprint changes, scaling remains linear. | #2036 review follow-up: csharp-record-base now exercises primary-constructor base dispatch end to end; +2 capture groups, scaling remains linear.",
|
||||
|
|
|
|||
|
|
@ -42,6 +42,11 @@ import {
|
|||
applyCppTwoPhaseSideChannel,
|
||||
type CppTwoPhaseSideChannel,
|
||||
} from './two-phase-lookup.js';
|
||||
import {
|
||||
applyCppMemberLookupSideChannel,
|
||||
collectCppMemberLookupSideChannel,
|
||||
type CppMemberLookupSideChannel,
|
||||
} from './member-lookup.js';
|
||||
|
||||
/**
|
||||
* Plain JSON-serializable composite of every C++ capture-time side-channel
|
||||
|
|
@ -62,6 +67,7 @@ export interface CppCaptureSideChannel {
|
|||
readonly inlineNamespaceRanges: readonly string[];
|
||||
readonly fileLocal: CppFileLocalSideChannel;
|
||||
readonly twoPhase: CppTwoPhaseSideChannel;
|
||||
readonly memberLookup: CppMemberLookupSideChannel;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -74,6 +80,7 @@ export function collectCppCaptureSideChannel(filePath: string): CppCaptureSideCh
|
|||
const inlineNamespaceRanges = collectCppInlineNamespaceSideChannel(filePath);
|
||||
const fileLocal = collectCppFileLocalSideChannel(filePath);
|
||||
const twoPhase = collectCppTwoPhaseSideChannel(filePath);
|
||||
const memberLookup = collectCppMemberLookupSideChannel(filePath);
|
||||
|
||||
const isEmpty =
|
||||
adl.argInfoBySite.length === 0 &&
|
||||
|
|
@ -82,10 +89,12 @@ export function collectCppCaptureSideChannel(filePath: string): CppCaptureSideCh
|
|||
fileLocal.fileLocalNames.length === 0 &&
|
||||
fileLocal.anonymousNamespaceRanges.length === 0 &&
|
||||
twoPhase.dependentBases.length === 0 &&
|
||||
twoPhase.dependentPackBaseClasses.length === 0;
|
||||
twoPhase.dependentPackBaseClasses.length === 0 &&
|
||||
memberLookup.baseEdges.length === 0 &&
|
||||
memberLookup.memberUsings.length === 0;
|
||||
if (isEmpty) return undefined;
|
||||
|
||||
return { kind: 'cpp', adl, inlineNamespaceRanges, fileLocal, twoPhase };
|
||||
return { kind: 'cpp', adl, inlineNamespaceRanges, fileLocal, twoPhase, memberLookup };
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -108,4 +117,7 @@ export function applyCppCaptureSideChannel(parsed: ParsedFile): void {
|
|||
}
|
||||
if (data.fileLocal !== undefined) applyCppFileLocalSideChannel(parsed.filePath, data.fileLocal);
|
||||
if (data.twoPhase !== undefined) applyCppTwoPhaseSideChannel(parsed.filePath, data.twoPhase);
|
||||
if (data.memberLookup !== undefined) {
|
||||
applyCppMemberLookupSideChannel(parsed.filePath, data.memberLookup);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import { markCppDependentBase, markCppDependentPackBase } from './two-phase-look
|
|||
import { markCppAdlSiteArgs, markCppAdlSiteNoAdl, type CppAdlArgInfo } from './adl.js';
|
||||
import { markCppInlineNamespaceRange } from './inline-namespaces.js';
|
||||
import { extractCppTemplateConstraints } from './constraint-extractor.js';
|
||||
import { captureCppMemberLookupFacts } from './member-lookup.js';
|
||||
|
||||
export function emitCppScopeCaptures(
|
||||
sourceText: string,
|
||||
|
|
@ -464,6 +465,7 @@ export function emitCppScopeCaptures(
|
|||
// and the resolver can suppress unqualified-call binding to those
|
||||
// bases per ISO C++ two-phase lookup.
|
||||
detectCppDependentBases(tree.rootNode, filePath);
|
||||
captureCppMemberLookupFacts(tree.rootNode, filePath);
|
||||
|
||||
return out;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -73,6 +73,12 @@ function buildIncludeCapture(node: SyntaxNode, pathNode: SyntaxNode): CaptureMat
|
|||
*/
|
||||
export function splitCppUsingDecl(node: SyntaxNode): CaptureMatch | null {
|
||||
if (node.type !== 'using_declaration') return null;
|
||||
// A class-scope `using Base::member;` changes the derived class's member
|
||||
// lookup set; it is not a namespace import. The C++ member-lookup sidecar
|
||||
// captures it separately, so suppress import decomposition here.
|
||||
for (let parent = node.parent; parent !== null; parent = parent.parent) {
|
||||
if (parent.type === 'class_specifier' || parent.type === 'struct_specifier') return null;
|
||||
}
|
||||
|
||||
// Check for "namespace" keyword among anonymous children
|
||||
let hasNamespaceKeyword = false;
|
||||
|
|
|
|||
616
gitnexus/src/core/ingestion/languages/cpp/member-lookup.ts
Normal file
616
gitnexus/src/core/ingestion/languages/cpp/member-lookup.ts
Normal file
|
|
@ -0,0 +1,616 @@
|
|||
import type { ParsedFile, ReferenceSite, SymbolDefinition } from 'gitnexus-shared';
|
||||
import type { KnowledgeGraph } from '../../../graph/types.js';
|
||||
import type { GraphNodeLookup } from '../../scope-resolution/graph-bridge/node-lookup.js';
|
||||
import { resolveDefGraphId } from '../../scope-resolution/graph-bridge/ids.js';
|
||||
import type { ScopeResolutionIndexes } from '../../model/scope-resolution-indexes.js';
|
||||
import type { SemanticModel } from '../../model/semantic-model.js';
|
||||
import type { ReceiverMemberResolution } from '../../scope-resolution/contract/scope-resolver.js';
|
||||
import { buildMro, defaultLinearize } from '../../scope-resolution/passes/mro.js';
|
||||
import {
|
||||
isOverloadAmbiguousAfterNormalization,
|
||||
narrowOverloadCandidates,
|
||||
} from '../../scope-resolution/passes/overload-narrowing.js';
|
||||
import { isClassLike } from '../../scope-resolution/scope/walkers.js';
|
||||
import type { SyntaxNode } from '../../utils/ast-helpers.js';
|
||||
import { cppConstraintCompatibility } from './constraint-filter.js';
|
||||
import { cppConversionRank } from './conversion-rank.js';
|
||||
|
||||
interface CapturedBaseEdge {
|
||||
readonly childName: string;
|
||||
readonly childQualifiedName?: string;
|
||||
readonly baseName: string;
|
||||
readonly baseQualifiedName?: string;
|
||||
readonly isVirtual: boolean;
|
||||
}
|
||||
|
||||
interface CapturedMemberUsing {
|
||||
readonly childName: string;
|
||||
readonly childQualifiedName?: string;
|
||||
readonly baseName: string;
|
||||
readonly baseQualifiedName?: string;
|
||||
readonly memberName: string;
|
||||
}
|
||||
|
||||
export interface CppMemberLookupSideChannel {
|
||||
readonly baseEdges: readonly CapturedBaseEdge[];
|
||||
readonly memberUsings: readonly CapturedMemberUsing[];
|
||||
}
|
||||
|
||||
const capturedByFile = new Map<string, CppMemberLookupSideChannel>();
|
||||
let directParentsByDefId = new Map<string, readonly string[]>();
|
||||
let virtualEdges = new Set<string>();
|
||||
let ancestorsByDefId = new Map<string, ReadonlySet<string>>();
|
||||
let memberUsingsByDefId = new Map<
|
||||
string,
|
||||
readonly { readonly baseDefId: string; readonly memberName: string }[]
|
||||
>();
|
||||
let inheritedLookupCache = new Map<string, CachedInheritedLookup>();
|
||||
|
||||
const MAX_INHERITANCE_VISITS = 4096;
|
||||
|
||||
type CachedInheritedLookup =
|
||||
| { readonly kind: 'none' }
|
||||
| { readonly kind: 'candidates'; readonly definitions: readonly SymbolDefinition[] }
|
||||
| { readonly kind: 'ambiguous'; readonly candidateIds: readonly string[] };
|
||||
|
||||
export function clearCppMemberLookupState(): void {
|
||||
capturedByFile.clear();
|
||||
directParentsByDefId = new Map();
|
||||
virtualEdges = new Set();
|
||||
ancestorsByDefId = new Map();
|
||||
memberUsingsByDefId = new Map();
|
||||
inheritedLookupCache = new Map();
|
||||
}
|
||||
|
||||
export function captureCppMemberLookupFacts(root: SyntaxNode, filePath: string): void {
|
||||
const baseEdges: CapturedBaseEdge[] = [];
|
||||
const memberUsings: CapturedMemberUsing[] = [];
|
||||
const stack: SyntaxNode[] = [root];
|
||||
|
||||
while (stack.length > 0) {
|
||||
const node = stack.pop()!;
|
||||
if (node.type === 'class_specifier' || node.type === 'struct_specifier') {
|
||||
const childName = classNameOf(node);
|
||||
const childQualifiedName = classQualifiedNameOf(node);
|
||||
if (childName !== '') {
|
||||
const baseClause = directChildOfType(node, 'base_class_clause');
|
||||
if (baseClause !== null) {
|
||||
captureBaseEdges(baseClause, childName, childQualifiedName, baseEdges);
|
||||
}
|
||||
const body = directChildOfType(node, 'field_declaration_list');
|
||||
if (body !== null) {
|
||||
for (let i = 0; i < body.namedChildCount; i++) {
|
||||
const child = body.namedChild(i);
|
||||
if (child?.type !== 'using_declaration') continue;
|
||||
const parsed = parseMemberUsing(child, childName, childQualifiedName);
|
||||
if (parsed !== undefined) memberUsings.push(parsed);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < node.childCount; i++) {
|
||||
const child = node.child(i);
|
||||
if (child !== null) stack.push(child);
|
||||
}
|
||||
}
|
||||
|
||||
if (baseEdges.length === 0 && memberUsings.length === 0) {
|
||||
capturedByFile.delete(filePath);
|
||||
} else {
|
||||
capturedByFile.set(filePath, { baseEdges, memberUsings });
|
||||
}
|
||||
}
|
||||
|
||||
export function collectCppMemberLookupSideChannel(filePath: string): CppMemberLookupSideChannel {
|
||||
return capturedByFile.get(filePath) ?? { baseEdges: [], memberUsings: [] };
|
||||
}
|
||||
|
||||
export function applyCppMemberLookupSideChannel(
|
||||
filePath: string,
|
||||
data: CppMemberLookupSideChannel,
|
||||
): void {
|
||||
if (!Array.isArray(data.baseEdges) || !Array.isArray(data.memberUsings)) return;
|
||||
if (data.baseEdges.length === 0 && data.memberUsings.length === 0) {
|
||||
capturedByFile.delete(filePath);
|
||||
return;
|
||||
}
|
||||
capturedByFile.set(filePath, {
|
||||
baseEdges: data.baseEdges.slice(),
|
||||
memberUsings: data.memberUsings.slice(),
|
||||
});
|
||||
}
|
||||
|
||||
export function buildCppMemberLookupMro(
|
||||
graph: KnowledgeGraph,
|
||||
parsedFiles: readonly ParsedFile[],
|
||||
nodeLookup: GraphNodeLookup,
|
||||
): Map<string, string[]> {
|
||||
populateResolvedHierarchy(graph, parsedFiles, nodeLookup);
|
||||
return buildMro(graph, parsedFiles, nodeLookup, defaultLinearize);
|
||||
}
|
||||
|
||||
export function resolveCppReceiverMember(
|
||||
ownerDef: SymbolDefinition,
|
||||
memberName: string,
|
||||
callsite: ReferenceSite,
|
||||
_scopes: ScopeResolutionIndexes,
|
||||
model: SemanticModel,
|
||||
): ReceiverMemberResolution | undefined {
|
||||
if (callsite.kind !== 'call') return undefined;
|
||||
const ownMethods = model.methods.lookupAllByOwner(ownerDef.nodeId, memberName);
|
||||
const introduced = introducedDefinitions(ownerDef.nodeId, memberName, model);
|
||||
|
||||
if (introduced.length > 0) {
|
||||
return chooseOverload(uniqueDefinitions([...ownMethods, ...introduced]), callsite);
|
||||
}
|
||||
|
||||
// Direct declarations hide every base declaration. Let the shared path
|
||||
// retain its existing overload/static filtering for this common case.
|
||||
if (ownMethods.length > 0) return undefined;
|
||||
|
||||
const lookup = inheritedLookupSet(ownerDef.nodeId, memberName, model);
|
||||
if (lookup.kind === 'none') return undefined;
|
||||
if (lookup.kind === 'ambiguous') return lookup;
|
||||
return chooseOverload(lookup.definitions, callsite);
|
||||
}
|
||||
|
||||
interface MemberOccurrence {
|
||||
readonly ownerDefId: string;
|
||||
readonly definitions: readonly SymbolDefinition[];
|
||||
readonly path: readonly string[];
|
||||
readonly virtualAnchor?: string;
|
||||
}
|
||||
|
||||
function collectInheritedOccurrences(
|
||||
ownerDefId: string,
|
||||
memberName: string,
|
||||
model: SemanticModel,
|
||||
path: readonly string[],
|
||||
virtualAnchor: string | undefined,
|
||||
active: Set<string>,
|
||||
budget: { remaining: number; truncated: boolean },
|
||||
): MemberOccurrence[] {
|
||||
if (budget.remaining <= 0) {
|
||||
budget.truncated = true;
|
||||
return [];
|
||||
}
|
||||
budget.remaining--;
|
||||
if (active.has(ownerDefId)) return [];
|
||||
const nextActive = new Set(active);
|
||||
nextActive.add(ownerDefId);
|
||||
|
||||
const definitions = uniqueDefinitions([
|
||||
...model.methods.lookupAllByOwner(ownerDefId, memberName),
|
||||
...introducedDefinitions(ownerDefId, memberName, model),
|
||||
]);
|
||||
if (definitions.length > 0) {
|
||||
return [{ ownerDefId, definitions, path, virtualAnchor }];
|
||||
}
|
||||
|
||||
const results: MemberOccurrence[] = [];
|
||||
for (const parentDefId of directParentsByDefId.get(ownerDefId) ?? []) {
|
||||
const edgeKey = `${ownerDefId}\0${parentDefId}`;
|
||||
results.push(
|
||||
...collectInheritedOccurrences(
|
||||
parentDefId,
|
||||
memberName,
|
||||
model,
|
||||
[...path, parentDefId],
|
||||
virtualEdges.has(edgeKey) ? parentDefId : virtualAnchor,
|
||||
nextActive,
|
||||
budget,
|
||||
),
|
||||
);
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
function inheritedLookupSet(
|
||||
ownerDefId: string,
|
||||
memberName: string,
|
||||
model: SemanticModel,
|
||||
): CachedInheritedLookup {
|
||||
const cacheKey = `${ownerDefId}\0${memberName}`;
|
||||
const cached = inheritedLookupCache.get(cacheKey);
|
||||
if (cached !== undefined) return cached;
|
||||
|
||||
const budget = { remaining: MAX_INHERITANCE_VISITS, truncated: false };
|
||||
const occurrences = collectInheritedOccurrences(
|
||||
ownerDefId,
|
||||
memberName,
|
||||
model,
|
||||
[],
|
||||
undefined,
|
||||
new Set(),
|
||||
budget,
|
||||
);
|
||||
if (budget.truncated) {
|
||||
const conservative: CachedInheritedLookup = {
|
||||
kind: 'ambiguous',
|
||||
candidateIds: uniqueDefinitions(occurrences.flatMap((entry) => entry.definitions)).map(
|
||||
(definition) => definition.nodeId,
|
||||
),
|
||||
};
|
||||
inheritedLookupCache.set(cacheKey, conservative);
|
||||
return conservative;
|
||||
}
|
||||
if (occurrences.length === 0) {
|
||||
const none: CachedInheritedLookup = { kind: 'none' };
|
||||
inheritedLookupCache.set(cacheKey, none);
|
||||
return none;
|
||||
}
|
||||
|
||||
// A declaration can dominate another lookup set only when the latter is
|
||||
// reached through a shared virtual subobject. Ordinary ancestry alone is
|
||||
// insufficient: declarations in one non-virtual branch do not hide members
|
||||
// reached through a sibling base subobject.
|
||||
const undominated = occurrences.filter(
|
||||
(candidate) =>
|
||||
!(
|
||||
candidate.virtualAnchor !== undefined &&
|
||||
occurrences.some(
|
||||
(other) =>
|
||||
other.ownerDefId !== candidate.ownerDefId &&
|
||||
isAncestor(candidate.ownerDefId, other.ownerDefId),
|
||||
)
|
||||
),
|
||||
);
|
||||
const groups = new Map<string, MemberOccurrence[]>();
|
||||
for (const occurrence of undominated) {
|
||||
const key =
|
||||
occurrence.virtualAnchor !== undefined
|
||||
? `virtual:${occurrence.virtualAnchor}:${occurrence.ownerDefId}`
|
||||
: `path:${occurrence.path.join('>')}:${occurrence.ownerDefId}`;
|
||||
const bucket = groups.get(key);
|
||||
if (bucket === undefined) groups.set(key, [occurrence]);
|
||||
else bucket.push(occurrence);
|
||||
}
|
||||
|
||||
let result: CachedInheritedLookup;
|
||||
if (groups.size !== 1) {
|
||||
result = {
|
||||
kind: 'ambiguous',
|
||||
candidateIds: uniqueDefinitions(undominated.flatMap((entry) => entry.definitions)).map(
|
||||
(definition) => definition.nodeId,
|
||||
),
|
||||
};
|
||||
} else {
|
||||
result = {
|
||||
kind: 'candidates',
|
||||
definitions: groups.values().next().value?.[0]?.definitions ?? [],
|
||||
};
|
||||
}
|
||||
inheritedLookupCache.set(cacheKey, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
function introducedDefinitions(
|
||||
ownerDefId: string,
|
||||
memberName: string,
|
||||
model: SemanticModel,
|
||||
): SymbolDefinition[] {
|
||||
const definitions: SymbolDefinition[] = [];
|
||||
for (const entry of memberUsingsByDefId.get(ownerDefId) ?? []) {
|
||||
if (entry.memberName !== memberName) continue;
|
||||
definitions.push(...model.methods.lookupAllByOwner(entry.baseDefId, memberName));
|
||||
}
|
||||
return definitions;
|
||||
}
|
||||
|
||||
function uniqueDefinitions(definitions: readonly SymbolDefinition[]): SymbolDefinition[] {
|
||||
return [...new Map(definitions.map((definition) => [definition.nodeId, definition])).values()];
|
||||
}
|
||||
|
||||
function chooseOverload(
|
||||
candidates: readonly SymbolDefinition[],
|
||||
callsite: ReferenceSite,
|
||||
): ReceiverMemberResolution | undefined {
|
||||
if (candidates.length === 0) return undefined;
|
||||
const narrowed = narrowOverloadCandidates(candidates, callsite.arity, callsite.argumentTypes, {
|
||||
argumentTypeClasses: callsite.argumentTypeClasses,
|
||||
conversionRankFn: cppConversionRank,
|
||||
constraintCompatibility: cppConstraintCompatibility,
|
||||
});
|
||||
if (narrowed.length === 1) return { kind: 'resolved', definition: narrowed[0]! };
|
||||
if (narrowed.length > 1 || isOverloadAmbiguousAfterNormalization(narrowed, callsite.arity)) {
|
||||
return {
|
||||
kind: 'ambiguous',
|
||||
candidateIds: narrowed.map((candidate) => candidate.nodeId),
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function populateResolvedHierarchy(
|
||||
graph: KnowledgeGraph,
|
||||
parsedFiles: readonly ParsedFile[],
|
||||
nodeLookup: GraphNodeLookup,
|
||||
): void {
|
||||
const defByGraphId = new Map<string, SymbolDefinition>();
|
||||
const defById = new Map<string, SymbolDefinition>();
|
||||
const defsByFileAndName = new Map<string, SymbolDefinition[]>();
|
||||
|
||||
for (const parsed of parsedFiles) {
|
||||
for (const def of parsed.localDefs) {
|
||||
if (!isClassLike(def.type)) continue;
|
||||
const graphId = resolveDefGraphId(parsed.filePath, def, nodeLookup);
|
||||
if (graphId === undefined) continue;
|
||||
defByGraphId.set(graphId, def);
|
||||
defById.set(def.nodeId, def);
|
||||
const names = new Set([simpleName(def), definitionQualifiedName(def)]);
|
||||
for (const name of names) {
|
||||
if (name === '') continue;
|
||||
const key = `${parsed.filePath}\0${name}`;
|
||||
const bucket = defsByFileAndName.get(key);
|
||||
if (bucket === undefined) defsByFileAndName.set(key, [def]);
|
||||
else bucket.push(def);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const parents = new Map<string, string[]>();
|
||||
for (const rel of graph.iterRelationshipsByType('EXTENDS')) {
|
||||
const child = defByGraphId.get(rel.sourceId);
|
||||
const parent = defByGraphId.get(rel.targetId);
|
||||
if (child === undefined || parent === undefined) continue;
|
||||
const bucket = parents.get(child.nodeId);
|
||||
if (bucket === undefined) parents.set(child.nodeId, [parent.nodeId]);
|
||||
else bucket.push(parent.nodeId);
|
||||
}
|
||||
directParentsByDefId = parents;
|
||||
ancestorsByDefId = buildAncestorClosure(parents);
|
||||
inheritedLookupCache = new Map();
|
||||
|
||||
const nextVirtualEdges = new Set<string>();
|
||||
const nextUsings = new Map<
|
||||
string,
|
||||
{ readonly baseDefId: string; readonly memberName: string }[]
|
||||
>();
|
||||
for (const parsed of parsedFiles) {
|
||||
const captured = capturedByFile.get(parsed.filePath);
|
||||
if (captured === undefined) continue;
|
||||
for (const edge of captured.baseEdges) {
|
||||
if (!edge.isVirtual) continue;
|
||||
for (const child of matchingChildren(
|
||||
parsed.filePath,
|
||||
edge.childName,
|
||||
edge.childQualifiedName,
|
||||
defsByFileAndName,
|
||||
)) {
|
||||
const parent = findCapturedParent(
|
||||
parents.get(child.nodeId) ?? [],
|
||||
edge.baseName,
|
||||
edge.baseQualifiedName,
|
||||
defById,
|
||||
);
|
||||
if (parent !== undefined) nextVirtualEdges.add(`${child.nodeId}\0${parent.nodeId}`);
|
||||
}
|
||||
}
|
||||
for (const using of captured.memberUsings) {
|
||||
const children = matchingChildren(
|
||||
parsed.filePath,
|
||||
using.childName,
|
||||
using.childQualifiedName,
|
||||
defsByFileAndName,
|
||||
);
|
||||
for (const child of children) {
|
||||
const baseDef = findCapturedParent(
|
||||
parents.get(child.nodeId) ?? [],
|
||||
using.baseName,
|
||||
using.baseQualifiedName,
|
||||
defById,
|
||||
);
|
||||
if (baseDef === undefined) continue;
|
||||
const bucket = nextUsings.get(child.nodeId);
|
||||
const entry = { baseDefId: baseDef.nodeId, memberName: using.memberName };
|
||||
if (bucket === undefined) nextUsings.set(child.nodeId, [entry]);
|
||||
else bucket.push(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
virtualEdges = nextVirtualEdges;
|
||||
memberUsingsByDefId = nextUsings;
|
||||
}
|
||||
|
||||
function captureBaseEdges(
|
||||
baseClause: SyntaxNode,
|
||||
childName: string,
|
||||
childQualifiedName: string,
|
||||
output: CapturedBaseEdge[],
|
||||
): void {
|
||||
let segmentStart = 0;
|
||||
for (let i = 0; i < baseClause.childCount; i++) {
|
||||
const child = baseClause.child(i);
|
||||
if (child === null) continue;
|
||||
if (child.type === ',' || child.text === ',') {
|
||||
segmentStart = i + 1;
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
child.type !== 'type_identifier' &&
|
||||
child.type !== 'template_type' &&
|
||||
child.type !== 'qualified_identifier'
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
let isVirtual = false;
|
||||
for (let j = segmentStart; j < i; j++) {
|
||||
const modifier = baseClause.child(j);
|
||||
if (modifier?.text === 'virtual') isVirtual = true;
|
||||
}
|
||||
const baseQualifiedName = qualifiedTypeName(child.text);
|
||||
const baseName = baseQualifiedName.split('.').at(-1) ?? '';
|
||||
if (baseName !== '') {
|
||||
output.push({
|
||||
childName,
|
||||
...(childQualifiedName !== childName ? { childQualifiedName } : {}),
|
||||
baseName,
|
||||
...(baseQualifiedName !== baseName ? { baseQualifiedName } : {}),
|
||||
isVirtual,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function parseMemberUsing(
|
||||
node: SyntaxNode,
|
||||
childName: string,
|
||||
childQualifiedName: string,
|
||||
): CapturedMemberUsing | undefined {
|
||||
const qualified = node.namedChildren.find((child) => child.type === 'qualified_identifier');
|
||||
if (qualified === undefined) return undefined;
|
||||
const parts = splitQualifiedSegments(qualified.text);
|
||||
if (parts.length < 2) return undefined;
|
||||
const memberName = stripTemplateSuffix(parts.at(-1) ?? '');
|
||||
const baseParts = parts.slice(0, -1).map(stripTemplateSuffix).filter(Boolean);
|
||||
const baseName = baseParts.at(-1) ?? '';
|
||||
const baseQualifiedName = baseParts.join('.');
|
||||
if (baseName === '' || memberName === '') return undefined;
|
||||
return {
|
||||
childName,
|
||||
...(childQualifiedName !== childName ? { childQualifiedName } : {}),
|
||||
baseName,
|
||||
...(baseQualifiedName !== baseName ? { baseQualifiedName } : {}),
|
||||
memberName,
|
||||
};
|
||||
}
|
||||
|
||||
function classNameOf(node: SyntaxNode): string {
|
||||
const name = node.childForFieldName?.('name');
|
||||
return name === null || name === undefined ? '' : trailingIdentifier(name.text);
|
||||
}
|
||||
|
||||
function classQualifiedNameOf(node: SyntaxNode): string {
|
||||
const parts = [classNameOf(node)];
|
||||
let current = node.parent;
|
||||
while (current !== null) {
|
||||
if (current.type === 'class_specifier' || current.type === 'struct_specifier') {
|
||||
const name = classNameOf(current);
|
||||
if (name !== '') parts.unshift(name);
|
||||
} else if (current.type === 'namespace_definition') {
|
||||
const name = current.childForFieldName?.('name');
|
||||
if (name !== null && name !== undefined) {
|
||||
parts.unshift(
|
||||
...splitQualifiedSegments(name.text).map(stripTemplateSuffix).filter(Boolean),
|
||||
);
|
||||
}
|
||||
}
|
||||
current = current.parent;
|
||||
}
|
||||
return parts.filter(Boolean).join('.');
|
||||
}
|
||||
|
||||
function directChildOfType(node: SyntaxNode, type: string): SyntaxNode | null {
|
||||
for (let i = 0; i < node.namedChildCount; i++) {
|
||||
const child = node.namedChild(i);
|
||||
if (child?.type === type) return child;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function trailingIdentifier(value: string): string {
|
||||
return stripTemplateSuffix(splitQualifiedSegments(value).at(-1) ?? '');
|
||||
}
|
||||
|
||||
function qualifiedTypeName(value: string): string {
|
||||
return splitQualifiedSegments(value).map(stripTemplateSuffix).filter(Boolean).join('.');
|
||||
}
|
||||
|
||||
function splitQualifiedSegments(value: string): string[] {
|
||||
const parts: string[] = [];
|
||||
let angleDepth = 0;
|
||||
let segmentStart = 0;
|
||||
for (let i = 0; i < value.length; i++) {
|
||||
const char = value[i];
|
||||
if (char === '<') angleDepth++;
|
||||
else if (char === '>' && angleDepth > 0) angleDepth--;
|
||||
else if (char === ':' && value[i + 1] === ':' && angleDepth === 0) {
|
||||
const segment = value.slice(segmentStart, i).trim();
|
||||
if (segment !== '') parts.push(segment);
|
||||
segmentStart = i + 2;
|
||||
i++;
|
||||
}
|
||||
}
|
||||
const tail = value.slice(segmentStart).trim();
|
||||
if (tail !== '') parts.push(tail);
|
||||
return parts;
|
||||
}
|
||||
|
||||
function stripTemplateSuffix(value: string): string {
|
||||
const templateStart = value.indexOf('<');
|
||||
return (templateStart >= 0 ? value.slice(0, templateStart) : value).trim();
|
||||
}
|
||||
|
||||
function simpleName(def: SymbolDefinition): string {
|
||||
return def.qualifiedName?.split('.').at(-1) ?? '';
|
||||
}
|
||||
|
||||
function definitionQualifiedName(def: SymbolDefinition): string {
|
||||
const name = def.qualifiedName ?? '';
|
||||
if (name === '' || def.namespacePrefix === undefined || def.namespacePrefix === '') return name;
|
||||
return name.startsWith(`${def.namespacePrefix}.`) ? name : `${def.namespacePrefix}.${name}`;
|
||||
}
|
||||
|
||||
function matchingChildren(
|
||||
filePath: string,
|
||||
childName: string,
|
||||
childQualifiedName: string | undefined,
|
||||
defsByFileAndName: ReadonlyMap<string, readonly SymbolDefinition[]>,
|
||||
): readonly SymbolDefinition[] {
|
||||
if (childQualifiedName !== undefined) {
|
||||
const qualified = defsByFileAndName.get(`${filePath}\0${childQualifiedName}`) ?? [];
|
||||
if (qualified.length > 0) return qualified;
|
||||
}
|
||||
const simple = defsByFileAndName.get(`${filePath}\0${childName}`) ?? [];
|
||||
return simple.length === 1 ? simple : [];
|
||||
}
|
||||
|
||||
function findCapturedParent(
|
||||
parentIds: readonly string[],
|
||||
baseName: string,
|
||||
baseQualifiedName: string | undefined,
|
||||
defById: ReadonlyMap<string, SymbolDefinition>,
|
||||
): SymbolDefinition | undefined {
|
||||
const candidates = parentIds
|
||||
.map((id) => defById.get(id))
|
||||
.filter((definition): definition is SymbolDefinition => definition !== undefined);
|
||||
if (baseQualifiedName !== undefined) {
|
||||
const qualified = candidates.filter((definition) => {
|
||||
const name = definitionQualifiedName(definition);
|
||||
return name === baseQualifiedName || name.endsWith(`.${baseQualifiedName}`);
|
||||
});
|
||||
if (qualified.length === 1) return qualified[0];
|
||||
return undefined;
|
||||
}
|
||||
const simple = candidates.filter((definition) => simpleName(definition) === baseName);
|
||||
return simple.length === 1 ? simple[0] : undefined;
|
||||
}
|
||||
|
||||
function buildAncestorClosure(
|
||||
parents: ReadonlyMap<string, readonly string[]>,
|
||||
): Map<string, ReadonlySet<string>> {
|
||||
const closure = new Map<string, ReadonlySet<string>>();
|
||||
const visiting = new Set<string>();
|
||||
|
||||
const ancestorsOf = (defId: string): ReadonlySet<string> => {
|
||||
const cached = closure.get(defId);
|
||||
if (cached !== undefined) return cached;
|
||||
if (visiting.has(defId)) return new Set();
|
||||
visiting.add(defId);
|
||||
const ancestors = new Set<string>();
|
||||
for (const parent of parents.get(defId) ?? []) {
|
||||
ancestors.add(parent);
|
||||
for (const ancestor of ancestorsOf(parent)) ancestors.add(ancestor);
|
||||
}
|
||||
visiting.delete(defId);
|
||||
closure.set(defId, ancestors);
|
||||
return ancestors;
|
||||
};
|
||||
|
||||
for (const defId of parents.keys()) ancestorsOf(defId);
|
||||
return closure;
|
||||
}
|
||||
|
||||
function isAncestor(ancestorDefId: string, descendantDefId: string): boolean {
|
||||
return ancestorsByDefId.get(descendantDefId)?.has(ancestorDefId) === true;
|
||||
}
|
||||
|
|
@ -4,7 +4,6 @@ import {
|
|||
findEnclosingClassDef,
|
||||
} from '../../scope-resolution/scope/walkers.js';
|
||||
import { SupportedLanguages } from 'gitnexus-shared';
|
||||
import { buildMro, defaultLinearize } from '../../scope-resolution/passes/mro.js';
|
||||
import {
|
||||
populateClassOwnedMembers,
|
||||
tagNamespacePrefixes,
|
||||
|
|
@ -42,6 +41,11 @@ import {
|
|||
clearCppUserDefinedConversions,
|
||||
populateCppUserDefinedConversions,
|
||||
} from './user-defined-conversions.js';
|
||||
import {
|
||||
buildCppMemberLookupMro,
|
||||
clearCppMemberLookupState,
|
||||
resolveCppReceiverMember,
|
||||
} from './member-lookup.js';
|
||||
|
||||
/**
|
||||
* Per-pass memo of the augmented `#include`-resolution file set
|
||||
|
|
@ -104,6 +108,7 @@ export const cppScopeResolver: ScopeResolver = {
|
|||
clearCppAdlState();
|
||||
clearCppInlineNamespaces();
|
||||
clearCppUserDefinedConversions();
|
||||
clearCppMemberLookupState();
|
||||
return scanCppHeaderFiles(repoPath);
|
||||
},
|
||||
|
||||
|
|
@ -137,8 +142,7 @@ export const cppScopeResolver: ScopeResolver = {
|
|||
// `'unknown'` keeps the candidate, preserving "degrade not lie".
|
||||
constraintCompatibility: cppConstraintCompatibility,
|
||||
|
||||
buildMro: (graph, parsedFiles, nodeLookup) =>
|
||||
buildMro(graph, parsedFiles, nodeLookup, defaultLinearize),
|
||||
buildMro: buildCppMemberLookupMro,
|
||||
|
||||
// Worker-boundary restore (see `ScopeResolver.applyCaptureSideChannel`).
|
||||
// `emitCppScopeCaptures` records per-file ADL call-site arg shapes
|
||||
|
|
@ -261,6 +265,7 @@ export const cppScopeResolver: ScopeResolver = {
|
|||
hoistTypeBindingsToModule: true,
|
||||
// Enable receiver-bound explicit-`this` fallback only for C++.
|
||||
resolveThisViaEnclosingClass: true,
|
||||
resolveReceiverMember: resolveCppReceiverMember,
|
||||
// The `isFileLocalDef` hook on the global free-call fallback names
|
||||
// file-local linkage historically, but semantically gates "logically
|
||||
// invisible cross-file" defs. C++ extends this to also reject class-
|
||||
|
|
|
|||
|
|
@ -267,6 +267,7 @@ import type {
|
|||
Callsite,
|
||||
ConstraintContext,
|
||||
ParsedFile,
|
||||
ReferenceSite,
|
||||
ScopeId,
|
||||
SupportedLanguages,
|
||||
SymbolDefinition,
|
||||
|
|
@ -291,6 +292,10 @@ export type LinearizeStrategy = (
|
|||
/** Result of `ScopeResolver.arityCompatibility` — mirrors `RegistryProviders.arityCompatibility`. */
|
||||
export type ArityVerdict = 'compatible' | 'unknown' | 'incompatible';
|
||||
|
||||
export type ReceiverMemberResolution =
|
||||
| { readonly kind: 'resolved'; readonly definition: SymbolDefinition }
|
||||
| { readonly kind: 'ambiguous'; readonly candidateIds: readonly string[] };
|
||||
|
||||
/** Re-exported for ScopeResolver consumers — same shape as
|
||||
* `RegistryProviders.constraintCompatibility`'s third parameter. */
|
||||
export type { ConstraintContext } from 'gitnexus-shared';
|
||||
|
|
@ -407,7 +412,7 @@ export interface ScopeResolver {
|
|||
* for the Tier-A predicate registry and Kleene 3-valued evaluator.
|
||||
*/
|
||||
readonly constraintCompatibility?: (
|
||||
callsite: Callsite,
|
||||
callsite: ReferenceSite,
|
||||
def: SymbolDefinition,
|
||||
ctx: ConstraintContext,
|
||||
) => ArityVerdict;
|
||||
|
|
@ -834,6 +839,21 @@ export interface ScopeResolver {
|
|||
callsite?: Callsite,
|
||||
) => SymbolDefinition | 'ambiguous' | undefined;
|
||||
|
||||
/**
|
||||
* Optional language-specific member-lattice lookup. Runs for a resolved
|
||||
* simple receiver type before the generic flattened-MRO walk. Languages
|
||||
* with lookup-set semantics that cannot be represented by one linear MRO
|
||||
* may resolve a member, report ambiguity (which suppresses fallback), or
|
||||
* return undefined to retain the shared behavior.
|
||||
*/
|
||||
readonly resolveReceiverMember?: (
|
||||
ownerDef: SymbolDefinition,
|
||||
memberName: string,
|
||||
callsite: Callsite,
|
||||
scopes: ScopeResolutionIndexes,
|
||||
model: SemanticModel,
|
||||
) => ReceiverMemberResolution | undefined;
|
||||
|
||||
/**
|
||||
* Enable the receiver-bound Case 0.5 fallback for explicit `this`
|
||||
* receivers (`this->m()` / `this.m()`) that resolves against the
|
||||
|
|
|
|||
|
|
@ -82,6 +82,7 @@ type ReceiverBoundProviderSubset = Pick<
|
|||
| 'unwrapCollectionAccessor'
|
||||
| 'hoistTypeBindingsToModule'
|
||||
| 'resolveQualifiedReceiverMember'
|
||||
| 'resolveReceiverMember'
|
||||
| 'resolveThisViaEnclosingClass'
|
||||
| 'conversionRankFn'
|
||||
| 'constraintCompatibility'
|
||||
|
|
@ -375,6 +376,51 @@ export function emitReceiverBoundCalls(
|
|||
if (provider.resolveThisViaEnclosingClass === true && receiverName === 'this') {
|
||||
const enclosingClass = findEnclosingClassDef(site.inScope, scopes);
|
||||
if (enclosingClass !== undefined) {
|
||||
const languageResolution = provider.resolveReceiverMember?.(
|
||||
enclosingClass,
|
||||
memberName,
|
||||
site,
|
||||
scopes,
|
||||
model,
|
||||
);
|
||||
if (languageResolution?.kind === 'ambiguous') {
|
||||
options.recordResolutionOutcome?.({
|
||||
kind: 'suppressed',
|
||||
phase: 'receiver-bound-calls',
|
||||
filePath: parsed.filePath,
|
||||
name: site.name,
|
||||
range: site.atRange,
|
||||
reason: 'member-lookup-ambiguous',
|
||||
candidateIds: languageResolution.candidateIds,
|
||||
});
|
||||
handledSites.add(siteKey);
|
||||
continue;
|
||||
}
|
||||
if (languageResolution?.kind === 'resolved') {
|
||||
const memberDef = languageResolution.definition;
|
||||
const reason =
|
||||
site.kind === 'write' || site.kind === 'read'
|
||||
? site.kind
|
||||
: memberDef.filePath !== parsed.filePath
|
||||
? 'import-resolved'
|
||||
: 'global';
|
||||
const confidence = site.kind === 'write' || site.kind === 'read' ? 1.0 : 0.85;
|
||||
const ok = tryEmitEdge(
|
||||
graph,
|
||||
scopes,
|
||||
nodeLookup,
|
||||
site,
|
||||
memberDef,
|
||||
reason,
|
||||
seen,
|
||||
confidence,
|
||||
collapse,
|
||||
);
|
||||
if (ok) emitted++;
|
||||
handledSites.add(siteKey);
|
||||
continue;
|
||||
}
|
||||
|
||||
const chain = [
|
||||
enclosingClass.nodeId,
|
||||
...scopes.methodDispatch.mroFor(enclosingClass.nodeId),
|
||||
|
|
@ -722,6 +768,51 @@ export function emitReceiverBoundCalls(
|
|||
);
|
||||
}
|
||||
if (ownerDef !== undefined) {
|
||||
const languageResolution = provider.resolveReceiverMember?.(
|
||||
ownerDef,
|
||||
memberName,
|
||||
site,
|
||||
scopes,
|
||||
model,
|
||||
);
|
||||
if (languageResolution?.kind === 'ambiguous') {
|
||||
options.recordResolutionOutcome?.({
|
||||
kind: 'suppressed',
|
||||
phase: 'receiver-bound-calls',
|
||||
filePath: parsed.filePath,
|
||||
name: site.name,
|
||||
range: site.atRange,
|
||||
reason: 'member-lookup-ambiguous',
|
||||
candidateIds: languageResolution.candidateIds,
|
||||
});
|
||||
handledSites.add(siteKey);
|
||||
continue;
|
||||
}
|
||||
if (languageResolution?.kind === 'resolved') {
|
||||
const memberDef = languageResolution.definition;
|
||||
const reason =
|
||||
site.kind === 'write' || site.kind === 'read'
|
||||
? site.kind
|
||||
: memberDef.filePath !== parsed.filePath
|
||||
? 'import-resolved'
|
||||
: 'global';
|
||||
const confidence = site.kind === 'write' || site.kind === 'read' ? 1.0 : 0.85;
|
||||
const ok = tryEmitEdge(
|
||||
graph,
|
||||
scopes,
|
||||
nodeLookup,
|
||||
site,
|
||||
memberDef,
|
||||
reason,
|
||||
seen,
|
||||
confidence,
|
||||
collapse,
|
||||
);
|
||||
if (ok) emitted++;
|
||||
handledSites.add(siteKey);
|
||||
continue;
|
||||
}
|
||||
|
||||
const chain = [ownerDef.nodeId, ...scopes.methodDispatch.mroFor(ownerDef.nodeId)];
|
||||
let memberDef: SymbolDefinition | undefined;
|
||||
let ambiguous = false;
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ export type ResolutionSuppressionReason =
|
|||
| 'adl-ordinary-lookup-blocked'
|
||||
| 'conversion-rank-tied'
|
||||
| 'inline-ns-ambiguous'
|
||||
| 'member-lookup-ambiguous'
|
||||
| 'overload-ambiguous'
|
||||
| 'overload-ambiguous-normalization';
|
||||
|
||||
|
|
|
|||
5
gitnexus/test/fixtures/lang-resolution/cpp-member-lattice/base.h
vendored
Normal file
5
gitnexus/test/fixtures/lang-resolution/cpp-member-lattice/base.h
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
#pragma once
|
||||
|
||||
struct CrossFileBase {
|
||||
void crossFile();
|
||||
};
|
||||
142
gitnexus/test/fixtures/lang-resolution/cpp-member-lattice/main.cpp
vendored
Normal file
142
gitnexus/test/fixtures/lang-resolution/cpp-member-lattice/main.cpp
vendored
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
#include "base.h"
|
||||
|
||||
struct Left {
|
||||
void collide();
|
||||
};
|
||||
|
||||
struct Right {
|
||||
void collide();
|
||||
};
|
||||
|
||||
struct Ambiguous : Left, Right {
|
||||
void callThis();
|
||||
};
|
||||
|
||||
void ambiguousCall() {
|
||||
Ambiguous value;
|
||||
value.collide();
|
||||
}
|
||||
|
||||
void Ambiguous::callThis() {
|
||||
this->collide();
|
||||
}
|
||||
|
||||
struct Dominant : Left, Right {
|
||||
void collide();
|
||||
};
|
||||
|
||||
void dominantCall() {
|
||||
Dominant value;
|
||||
value.collide();
|
||||
}
|
||||
|
||||
struct Root {
|
||||
void shared();
|
||||
};
|
||||
|
||||
struct VirtualLeft : virtual Root {};
|
||||
struct VirtualRight : virtual Root {};
|
||||
struct VirtualDiamond : VirtualLeft, VirtualRight {};
|
||||
|
||||
void virtualDiamondCall() {
|
||||
VirtualDiamond value;
|
||||
value.shared();
|
||||
}
|
||||
|
||||
struct PlainLeft : Root {};
|
||||
struct PlainRight : Root {};
|
||||
struct PlainDiamond : PlainLeft, PlainRight {};
|
||||
|
||||
void plainDiamondCall() {
|
||||
PlainDiamond value;
|
||||
value.shared();
|
||||
}
|
||||
|
||||
struct Base {
|
||||
void select(int);
|
||||
};
|
||||
|
||||
struct Derived : Base {
|
||||
using Base::select;
|
||||
void select(double);
|
||||
};
|
||||
|
||||
void usingCall() {
|
||||
Derived value;
|
||||
value.select(1);
|
||||
}
|
||||
|
||||
struct OverrideRoot {
|
||||
void overrideMember();
|
||||
};
|
||||
|
||||
struct OverrideLeft : OverrideRoot {
|
||||
void overrideMember();
|
||||
};
|
||||
|
||||
struct OverrideRight : OverrideRoot {};
|
||||
struct OverrideDiamond : OverrideLeft, OverrideRight {};
|
||||
|
||||
void nonVirtualOverrideCall() {
|
||||
OverrideDiamond value;
|
||||
value.overrideMember();
|
||||
}
|
||||
|
||||
struct UsingRoot {
|
||||
void inheritedUsing(int);
|
||||
};
|
||||
|
||||
struct UsingMiddle : UsingRoot {
|
||||
using UsingRoot::inheritedUsing;
|
||||
void inheritedUsing(double);
|
||||
};
|
||||
|
||||
struct UsingLeaf : UsingMiddle {};
|
||||
|
||||
void inheritedUsingCall() {
|
||||
UsingLeaf value;
|
||||
value.inheritedUsing(1);
|
||||
}
|
||||
|
||||
namespace alpha {
|
||||
struct SameNameBase {
|
||||
void qualified(int);
|
||||
};
|
||||
}
|
||||
|
||||
namespace beta {
|
||||
struct SameNameBase {
|
||||
void qualified(double);
|
||||
};
|
||||
}
|
||||
|
||||
struct QualifiedBases : alpha::SameNameBase, beta::SameNameBase {
|
||||
using alpha::SameNameBase::qualified;
|
||||
};
|
||||
|
||||
void qualifiedUsingCall() {
|
||||
QualifiedBases value;
|
||||
value.qualified(1);
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
struct TemplatedOuter {
|
||||
template <typename U>
|
||||
struct NestedBase {
|
||||
void nestedTemplate();
|
||||
};
|
||||
};
|
||||
|
||||
struct TemplatedDerived : TemplatedOuter<int>::NestedBase<double> {};
|
||||
|
||||
void nestedTemplateCall() {
|
||||
TemplatedDerived value;
|
||||
value.nestedTemplate();
|
||||
}
|
||||
|
||||
struct CrossFileDerived : CrossFileBase {};
|
||||
|
||||
void crossFileCall() {
|
||||
CrossFileDerived value;
|
||||
value.crossFile();
|
||||
}
|
||||
|
|
@ -1851,6 +1851,109 @@ describe('C++ Derived : A, B — diamond inheritance via leftmost-base MRO (SM-1
|
|||
});
|
||||
});
|
||||
|
||||
describe('C++ inheritance-lattice member lookup (#1891)', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(path.join(FIXTURES, 'cpp-member-lattice'), () => {});
|
||||
}, 60000);
|
||||
|
||||
it('suppresses same-name members inherited from unrelated bases', () => {
|
||||
const calls = getRelationships(result, 'CALLS').filter(
|
||||
(call) => call.source === 'ambiguousCall' && call.target === 'collide',
|
||||
);
|
||||
expect(calls).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('lets a derived declaration hide both base declarations', () => {
|
||||
const calls = getRelationships(result, 'CALLS').filter(
|
||||
(call) => call.source === 'dominantCall' && call.target === 'collide',
|
||||
);
|
||||
expect(calls).toHaveLength(1);
|
||||
expect(calls[0]?.targetFilePath).toBe('main.cpp');
|
||||
});
|
||||
|
||||
it('merges a shared virtual base into one member subobject', () => {
|
||||
const calls = getRelationships(result, 'CALLS').filter(
|
||||
(call) => call.source === 'virtualDiamondCall' && call.target === 'shared',
|
||||
);
|
||||
expect(calls).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('suppresses the same declaration reached through two non-virtual base subobjects', () => {
|
||||
const calls = getRelationships(result, 'CALLS').filter(
|
||||
(call) => call.source === 'plainDiamondCall' && call.target === 'shared',
|
||||
);
|
||||
expect(calls).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('adds a member using-declaration to the derived overload set', () => {
|
||||
const calls = getRelationships(result, 'CALLS').filter(
|
||||
(call) => call.source === 'usingCall' && call.target === 'select',
|
||||
);
|
||||
expect(calls).toHaveLength(1);
|
||||
const target = result.graph.getNode(calls[0]!.rel.targetId);
|
||||
expect(target?.properties.parameterTypes).toEqual(['int']);
|
||||
});
|
||||
|
||||
it('records both conservative ambiguity suppressions', () => {
|
||||
const outcomes = getResolutionOutcomes(result).filter(
|
||||
(outcome) => outcome.kind === 'suppressed' && outcome.reason === 'member-lookup-ambiguous',
|
||||
);
|
||||
const names = outcomes.map((outcome) => outcome.name);
|
||||
expect(names).toContain('collide');
|
||||
expect(names).toContain('overrideMember');
|
||||
expect(names).toContain('shared');
|
||||
});
|
||||
|
||||
it('keeps sibling non-virtual subobjects ambiguous when one branch overrides the member', () => {
|
||||
const calls = getRelationships(result, 'CALLS').filter(
|
||||
(call) => call.source === 'nonVirtualOverrideCall' && call.target === 'overrideMember',
|
||||
);
|
||||
expect(calls).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('merges inherited using-declarations with methods declared by the same intermediate class', () => {
|
||||
const calls = getRelationships(result, 'CALLS').filter(
|
||||
(call) => call.source === 'inheritedUsingCall' && call.target === 'inheritedUsing',
|
||||
);
|
||||
expect(calls).toHaveLength(1);
|
||||
const target = result.graph.getNode(calls[0]!.rel.targetId);
|
||||
expect(target?.properties.parameterTypes).toEqual(['int']);
|
||||
});
|
||||
|
||||
it('uses qualified base identities when same-simple-name direct bases collide', () => {
|
||||
const calls = getRelationships(result, 'CALLS').filter(
|
||||
(call) => call.source === 'qualifiedUsingCall' && call.target === 'qualified',
|
||||
);
|
||||
expect(calls).toHaveLength(1);
|
||||
const target = result.graph.getNode(calls[0]!.rel.targetId);
|
||||
expect(target?.properties.parameterTypes).toEqual(['int']);
|
||||
});
|
||||
|
||||
it('normalizes every segment of a nested templated base name', () => {
|
||||
const calls = getRelationships(result, 'CALLS').filter(
|
||||
(call) => call.source === 'nestedTemplateCall' && call.target === 'nestedTemplate',
|
||||
);
|
||||
expect(calls).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('applies lattice ambiguity suppression to explicit this receivers', () => {
|
||||
const calls = getRelationships(result, 'CALLS').filter(
|
||||
(call) => call.source === 'callThis' && call.target === 'collide',
|
||||
);
|
||||
expect(calls).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('resolves inherited members across files', () => {
|
||||
const calls = getRelationships(result, 'CALLS').filter(
|
||||
(call) => call.source === 'crossFileCall' && call.target === 'crossFile',
|
||||
);
|
||||
expect(calls).toHaveLength(1);
|
||||
expect(calls[0]?.targetFilePath).toBe('base.h');
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// U1: `#include` must not leak class-owned methods as unqualified bindings
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -14,9 +14,14 @@ import type { SyntaxNode } from '../../../../src/core/ingestion/utils/ast-helper
|
|||
|
||||
function parseNode(src: string, type: string): SyntaxNode | null {
|
||||
const tree = getCppParser().parse(src);
|
||||
for (let i = 0; i < tree.rootNode.namedChildCount; i++) {
|
||||
const child = tree.rootNode.namedChild(i);
|
||||
if (child?.type === type) return child as SyntaxNode;
|
||||
const stack: SyntaxNode[] = [tree.rootNode as SyntaxNode];
|
||||
while (stack.length > 0) {
|
||||
const node = stack.pop()!;
|
||||
if (node.type === type) return node;
|
||||
for (let i = node.namedChildCount - 1; i >= 0; i--) {
|
||||
const child = node.namedChild(i);
|
||||
if (child !== null) stack.push(child as SyntaxNode);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
|
@ -59,6 +64,12 @@ describe('C++ include decomposition (splitCppInclude)', () => {
|
|||
// ── using declaration decomposition ─────────────────────────────────────────
|
||||
|
||||
describe('C++ using declaration decomposition (splitCppUsingDecl)', () => {
|
||||
it('does not treat a class-scope member using-declaration as an import', () => {
|
||||
const node = parseNode('struct Derived : Base { using Base::run; };', 'using_declaration');
|
||||
expect(node).not.toBeNull();
|
||||
expect(splitCppUsingDecl(node!)).toBeNull();
|
||||
});
|
||||
|
||||
it('decomposes "using namespace std;" as wildcard import', () => {
|
||||
const node = parseNode('using namespace std;', 'using_declaration');
|
||||
expect(node).not.toBeNull();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,41 @@
|
|||
import { beforeEach, describe, expect, it } from 'vitest';
|
||||
import {
|
||||
applyCppMemberLookupSideChannel,
|
||||
clearCppMemberLookupState,
|
||||
collectCppMemberLookupSideChannel,
|
||||
type CppMemberLookupSideChannel,
|
||||
} from '../../../../src/core/ingestion/languages/cpp/member-lookup.js';
|
||||
|
||||
describe('C++ member-lookup capture side-channel', () => {
|
||||
beforeEach(() => {
|
||||
clearCppMemberLookupState();
|
||||
});
|
||||
|
||||
it('preserves qualified base identities through a worker-style JSON round trip', () => {
|
||||
const snapshot: CppMemberLookupSideChannel = {
|
||||
baseEdges: [
|
||||
{
|
||||
childName: 'Derived',
|
||||
childQualifiedName: 'app.Derived',
|
||||
baseName: 'Base',
|
||||
baseQualifiedName: 'detail.Base',
|
||||
isVirtual: true,
|
||||
},
|
||||
],
|
||||
memberUsings: [
|
||||
{
|
||||
childName: 'Derived',
|
||||
childQualifiedName: 'app.Derived',
|
||||
baseName: 'Base',
|
||||
baseQualifiedName: 'detail.Base',
|
||||
memberName: 'select',
|
||||
},
|
||||
],
|
||||
};
|
||||
const throughWorker = JSON.parse(JSON.stringify(snapshot)) as CppMemberLookupSideChannel;
|
||||
|
||||
applyCppMemberLookupSideChannel('main.cpp', throughWorker);
|
||||
|
||||
expect(collectCppMemberLookupSideChannel('main.cpp')).toEqual(snapshot);
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue