gitnexus 1.6.7-rc.4 → 1.6.7-rc.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -29,6 +29,7 @@ import type { ParsedFile } from '../../../../_shared/index.js';
29
29
  import { type CppAdlSideChannel } from './adl.js';
30
30
  import { type CppFileLocalSideChannel } from './file-local-linkage.js';
31
31
  import { type CppTwoPhaseSideChannel } from './two-phase-lookup.js';
32
+ import { type CppMemberLookupSideChannel } from './member-lookup.js';
32
33
  /**
33
34
  * Plain JSON-serializable composite of every C++ capture-time side-channel
34
35
  * slice for one file. Carried opaquely on `ParsedFile.captureSideChannel`.
@@ -48,6 +49,7 @@ export interface CppCaptureSideChannel {
48
49
  readonly inlineNamespaceRanges: readonly string[];
49
50
  readonly fileLocal: CppFileLocalSideChannel;
50
51
  readonly twoPhase: CppTwoPhaseSideChannel;
52
+ readonly memberLookup: CppMemberLookupSideChannel;
51
53
  }
52
54
  /**
53
55
  * `LanguageProvider.collectCaptureSideChannel` implementation for C++.
@@ -29,6 +29,7 @@ import { collectCppAdlSideChannel, applyCppAdlSideChannel } from './adl.js';
29
29
  import { collectCppInlineNamespaceSideChannel, applyCppInlineNamespaceSideChannel, } from './inline-namespaces.js';
30
30
  import { collectCppFileLocalSideChannel, applyCppFileLocalSideChannel, } from './file-local-linkage.js';
31
31
  import { collectCppTwoPhaseSideChannel, applyCppTwoPhaseSideChannel, } from './two-phase-lookup.js';
32
+ import { applyCppMemberLookupSideChannel, collectCppMemberLookupSideChannel, } from './member-lookup.js';
32
33
  /**
33
34
  * `LanguageProvider.collectCaptureSideChannel` implementation for C++.
34
35
  * Returns `undefined` when this file recorded no side-channel state at all, so
@@ -39,16 +40,19 @@ export function collectCppCaptureSideChannel(filePath) {
39
40
  const inlineNamespaceRanges = collectCppInlineNamespaceSideChannel(filePath);
40
41
  const fileLocal = collectCppFileLocalSideChannel(filePath);
41
42
  const twoPhase = collectCppTwoPhaseSideChannel(filePath);
43
+ const memberLookup = collectCppMemberLookupSideChannel(filePath);
42
44
  const isEmpty = adl.argInfoBySite.length === 0 &&
43
45
  adl.noAdlSites.length === 0 &&
44
46
  inlineNamespaceRanges.length === 0 &&
45
47
  fileLocal.fileLocalNames.length === 0 &&
46
48
  fileLocal.anonymousNamespaceRanges.length === 0 &&
47
49
  twoPhase.dependentBases.length === 0 &&
48
- twoPhase.dependentPackBaseClasses.length === 0;
50
+ twoPhase.dependentPackBaseClasses.length === 0 &&
51
+ memberLookup.baseEdges.length === 0 &&
52
+ memberLookup.memberUsings.length === 0;
49
53
  if (isEmpty)
50
54
  return undefined;
51
- return { kind: 'cpp', adl, inlineNamespaceRanges, fileLocal, twoPhase };
55
+ return { kind: 'cpp', adl, inlineNamespaceRanges, fileLocal, twoPhase, memberLookup };
52
56
  }
53
57
  /**
54
58
  * `ScopeResolver.applyCaptureSideChannel` implementation for C++. Reads the
@@ -75,4 +79,7 @@ export function applyCppCaptureSideChannel(parsed) {
75
79
  applyCppFileLocalSideChannel(parsed.filePath, data.fileLocal);
76
80
  if (data.twoPhase !== undefined)
77
81
  applyCppTwoPhaseSideChannel(parsed.filePath, data.twoPhase);
82
+ if (data.memberLookup !== undefined) {
83
+ applyCppMemberLookupSideChannel(parsed.filePath, data.memberLookup);
84
+ }
78
85
  }
@@ -10,6 +10,7 @@ import { markCppDependentBase, markCppDependentPackBase } from './two-phase-look
10
10
  import { markCppAdlSiteArgs, markCppAdlSiteNoAdl } from './adl.js';
11
11
  import { markCppInlineNamespaceRange } from './inline-namespaces.js';
12
12
  import { extractCppTemplateConstraints } from './constraint-extractor.js';
13
+ import { captureCppMemberLookupFacts } from './member-lookup.js';
13
14
  export function emitCppScopeCaptures(sourceText, filePath, cachedTree) {
14
15
  let tree = cachedTree;
15
16
  if (tree === undefined) {
@@ -354,6 +355,7 @@ export function emitCppScopeCaptures(sourceText, filePath, cachedTree) {
354
355
  // and the resolver can suppress unqualified-call binding to those
355
356
  // bases per ISO C++ two-phase lookup.
356
357
  detectCppDependentBases(tree.rootNode, filePath);
358
+ captureCppMemberLookupFacts(tree.rootNode, filePath);
357
359
  return out;
358
360
  }
359
361
  function extractCppDeclarationReturnType(fnNode) {
@@ -67,6 +67,13 @@ function buildIncludeCapture(node, pathNode) {
67
67
  export function splitCppUsingDecl(node) {
68
68
  if (node.type !== 'using_declaration')
69
69
  return null;
70
+ // A class-scope `using Base::member;` changes the derived class's member
71
+ // lookup set; it is not a namespace import. The C++ member-lookup sidecar
72
+ // captures it separately, so suppress import decomposition here.
73
+ for (let parent = node.parent; parent !== null; parent = parent.parent) {
74
+ if (parent.type === 'class_specifier' || parent.type === 'struct_specifier')
75
+ return null;
76
+ }
70
77
  // Check for "namespace" keyword among anonymous children
71
78
  let hasNamespaceKeyword = false;
72
79
  for (let i = 0; i < node.childCount; i++) {
@@ -0,0 +1,32 @@
1
+ import type { ParsedFile, ReferenceSite, SymbolDefinition } from '../../../../_shared/index.js';
2
+ import type { KnowledgeGraph } from '../../../graph/types.js';
3
+ import type { GraphNodeLookup } from '../../scope-resolution/graph-bridge/node-lookup.js';
4
+ import type { ScopeResolutionIndexes } from '../../model/scope-resolution-indexes.js';
5
+ import type { SemanticModel } from '../../model/semantic-model.js';
6
+ import type { ReceiverMemberResolution } from '../../scope-resolution/contract/scope-resolver.js';
7
+ import type { SyntaxNode } from '../../utils/ast-helpers.js';
8
+ interface CapturedBaseEdge {
9
+ readonly childName: string;
10
+ readonly childQualifiedName?: string;
11
+ readonly baseName: string;
12
+ readonly baseQualifiedName?: string;
13
+ readonly isVirtual: boolean;
14
+ }
15
+ interface CapturedMemberUsing {
16
+ readonly childName: string;
17
+ readonly childQualifiedName?: string;
18
+ readonly baseName: string;
19
+ readonly baseQualifiedName?: string;
20
+ readonly memberName: string;
21
+ }
22
+ export interface CppMemberLookupSideChannel {
23
+ readonly baseEdges: readonly CapturedBaseEdge[];
24
+ readonly memberUsings: readonly CapturedMemberUsing[];
25
+ }
26
+ export declare function clearCppMemberLookupState(): void;
27
+ export declare function captureCppMemberLookupFacts(root: SyntaxNode, filePath: string): void;
28
+ export declare function collectCppMemberLookupSideChannel(filePath: string): CppMemberLookupSideChannel;
29
+ export declare function applyCppMemberLookupSideChannel(filePath: string, data: CppMemberLookupSideChannel): void;
30
+ export declare function buildCppMemberLookupMro(graph: KnowledgeGraph, parsedFiles: readonly ParsedFile[], nodeLookup: GraphNodeLookup): Map<string, string[]>;
31
+ export declare function resolveCppReceiverMember(ownerDef: SymbolDefinition, memberName: string, callsite: ReferenceSite, _scopes: ScopeResolutionIndexes, model: SemanticModel): ReceiverMemberResolution | undefined;
32
+ export {};
@@ -0,0 +1,461 @@
1
+ import { resolveDefGraphId } from '../../scope-resolution/graph-bridge/ids.js';
2
+ import { buildMro, defaultLinearize } from '../../scope-resolution/passes/mro.js';
3
+ import { isOverloadAmbiguousAfterNormalization, narrowOverloadCandidates, } from '../../scope-resolution/passes/overload-narrowing.js';
4
+ import { isClassLike } from '../../scope-resolution/scope/walkers.js';
5
+ import { cppConstraintCompatibility } from './constraint-filter.js';
6
+ import { cppConversionRank } from './conversion-rank.js';
7
+ const capturedByFile = new Map();
8
+ let directParentsByDefId = new Map();
9
+ let virtualEdges = new Set();
10
+ let ancestorsByDefId = new Map();
11
+ let memberUsingsByDefId = new Map();
12
+ let inheritedLookupCache = new Map();
13
+ const MAX_INHERITANCE_VISITS = 4096;
14
+ export function clearCppMemberLookupState() {
15
+ capturedByFile.clear();
16
+ directParentsByDefId = new Map();
17
+ virtualEdges = new Set();
18
+ ancestorsByDefId = new Map();
19
+ memberUsingsByDefId = new Map();
20
+ inheritedLookupCache = new Map();
21
+ }
22
+ export function captureCppMemberLookupFacts(root, filePath) {
23
+ const baseEdges = [];
24
+ const memberUsings = [];
25
+ const stack = [root];
26
+ while (stack.length > 0) {
27
+ const node = stack.pop();
28
+ if (node.type === 'class_specifier' || node.type === 'struct_specifier') {
29
+ const childName = classNameOf(node);
30
+ const childQualifiedName = classQualifiedNameOf(node);
31
+ if (childName !== '') {
32
+ const baseClause = directChildOfType(node, 'base_class_clause');
33
+ if (baseClause !== null) {
34
+ captureBaseEdges(baseClause, childName, childQualifiedName, baseEdges);
35
+ }
36
+ const body = directChildOfType(node, 'field_declaration_list');
37
+ if (body !== null) {
38
+ for (let i = 0; i < body.namedChildCount; i++) {
39
+ const child = body.namedChild(i);
40
+ if (child?.type !== 'using_declaration')
41
+ continue;
42
+ const parsed = parseMemberUsing(child, childName, childQualifiedName);
43
+ if (parsed !== undefined)
44
+ memberUsings.push(parsed);
45
+ }
46
+ }
47
+ }
48
+ }
49
+ for (let i = 0; i < node.childCount; i++) {
50
+ const child = node.child(i);
51
+ if (child !== null)
52
+ stack.push(child);
53
+ }
54
+ }
55
+ if (baseEdges.length === 0 && memberUsings.length === 0) {
56
+ capturedByFile.delete(filePath);
57
+ }
58
+ else {
59
+ capturedByFile.set(filePath, { baseEdges, memberUsings });
60
+ }
61
+ }
62
+ export function collectCppMemberLookupSideChannel(filePath) {
63
+ return capturedByFile.get(filePath) ?? { baseEdges: [], memberUsings: [] };
64
+ }
65
+ export function applyCppMemberLookupSideChannel(filePath, data) {
66
+ if (!Array.isArray(data.baseEdges) || !Array.isArray(data.memberUsings))
67
+ return;
68
+ if (data.baseEdges.length === 0 && data.memberUsings.length === 0) {
69
+ capturedByFile.delete(filePath);
70
+ return;
71
+ }
72
+ capturedByFile.set(filePath, {
73
+ baseEdges: data.baseEdges.slice(),
74
+ memberUsings: data.memberUsings.slice(),
75
+ });
76
+ }
77
+ export function buildCppMemberLookupMro(graph, parsedFiles, nodeLookup) {
78
+ populateResolvedHierarchy(graph, parsedFiles, nodeLookup);
79
+ return buildMro(graph, parsedFiles, nodeLookup, defaultLinearize);
80
+ }
81
+ export function resolveCppReceiverMember(ownerDef, memberName, callsite, _scopes, model) {
82
+ if (callsite.kind !== 'call')
83
+ return undefined;
84
+ const ownMethods = model.methods.lookupAllByOwner(ownerDef.nodeId, memberName);
85
+ const introduced = introducedDefinitions(ownerDef.nodeId, memberName, model);
86
+ if (introduced.length > 0) {
87
+ return chooseOverload(uniqueDefinitions([...ownMethods, ...introduced]), callsite);
88
+ }
89
+ // Direct declarations hide every base declaration. Let the shared path
90
+ // retain its existing overload/static filtering for this common case.
91
+ if (ownMethods.length > 0)
92
+ return undefined;
93
+ const lookup = inheritedLookupSet(ownerDef.nodeId, memberName, model);
94
+ if (lookup.kind === 'none')
95
+ return undefined;
96
+ if (lookup.kind === 'ambiguous')
97
+ return lookup;
98
+ return chooseOverload(lookup.definitions, callsite);
99
+ }
100
+ function collectInheritedOccurrences(ownerDefId, memberName, model, path, virtualAnchor, active, budget) {
101
+ if (budget.remaining <= 0) {
102
+ budget.truncated = true;
103
+ return [];
104
+ }
105
+ budget.remaining--;
106
+ if (active.has(ownerDefId))
107
+ return [];
108
+ const nextActive = new Set(active);
109
+ nextActive.add(ownerDefId);
110
+ const definitions = uniqueDefinitions([
111
+ ...model.methods.lookupAllByOwner(ownerDefId, memberName),
112
+ ...introducedDefinitions(ownerDefId, memberName, model),
113
+ ]);
114
+ if (definitions.length > 0) {
115
+ return [{ ownerDefId, definitions, path, virtualAnchor }];
116
+ }
117
+ const results = [];
118
+ for (const parentDefId of directParentsByDefId.get(ownerDefId) ?? []) {
119
+ const edgeKey = `${ownerDefId}\0${parentDefId}`;
120
+ results.push(...collectInheritedOccurrences(parentDefId, memberName, model, [...path, parentDefId], virtualEdges.has(edgeKey) ? parentDefId : virtualAnchor, nextActive, budget));
121
+ }
122
+ return results;
123
+ }
124
+ function inheritedLookupSet(ownerDefId, memberName, model) {
125
+ const cacheKey = `${ownerDefId}\0${memberName}`;
126
+ const cached = inheritedLookupCache.get(cacheKey);
127
+ if (cached !== undefined)
128
+ return cached;
129
+ const budget = { remaining: MAX_INHERITANCE_VISITS, truncated: false };
130
+ const occurrences = collectInheritedOccurrences(ownerDefId, memberName, model, [], undefined, new Set(), budget);
131
+ if (budget.truncated) {
132
+ const conservative = {
133
+ kind: 'ambiguous',
134
+ candidateIds: uniqueDefinitions(occurrences.flatMap((entry) => entry.definitions)).map((definition) => definition.nodeId),
135
+ };
136
+ inheritedLookupCache.set(cacheKey, conservative);
137
+ return conservative;
138
+ }
139
+ if (occurrences.length === 0) {
140
+ const none = { kind: 'none' };
141
+ inheritedLookupCache.set(cacheKey, none);
142
+ return none;
143
+ }
144
+ // A declaration can dominate another lookup set only when the latter is
145
+ // reached through a shared virtual subobject. Ordinary ancestry alone is
146
+ // insufficient: declarations in one non-virtual branch do not hide members
147
+ // reached through a sibling base subobject.
148
+ const undominated = occurrences.filter((candidate) => !(candidate.virtualAnchor !== undefined &&
149
+ occurrences.some((other) => other.ownerDefId !== candidate.ownerDefId &&
150
+ isAncestor(candidate.ownerDefId, other.ownerDefId))));
151
+ const groups = new Map();
152
+ for (const occurrence of undominated) {
153
+ const key = occurrence.virtualAnchor !== undefined
154
+ ? `virtual:${occurrence.virtualAnchor}:${occurrence.ownerDefId}`
155
+ : `path:${occurrence.path.join('>')}:${occurrence.ownerDefId}`;
156
+ const bucket = groups.get(key);
157
+ if (bucket === undefined)
158
+ groups.set(key, [occurrence]);
159
+ else
160
+ bucket.push(occurrence);
161
+ }
162
+ let result;
163
+ if (groups.size !== 1) {
164
+ result = {
165
+ kind: 'ambiguous',
166
+ candidateIds: uniqueDefinitions(undominated.flatMap((entry) => entry.definitions)).map((definition) => definition.nodeId),
167
+ };
168
+ }
169
+ else {
170
+ result = {
171
+ kind: 'candidates',
172
+ definitions: groups.values().next().value?.[0]?.definitions ?? [],
173
+ };
174
+ }
175
+ inheritedLookupCache.set(cacheKey, result);
176
+ return result;
177
+ }
178
+ function introducedDefinitions(ownerDefId, memberName, model) {
179
+ const definitions = [];
180
+ for (const entry of memberUsingsByDefId.get(ownerDefId) ?? []) {
181
+ if (entry.memberName !== memberName)
182
+ continue;
183
+ definitions.push(...model.methods.lookupAllByOwner(entry.baseDefId, memberName));
184
+ }
185
+ return definitions;
186
+ }
187
+ function uniqueDefinitions(definitions) {
188
+ return [...new Map(definitions.map((definition) => [definition.nodeId, definition])).values()];
189
+ }
190
+ function chooseOverload(candidates, callsite) {
191
+ if (candidates.length === 0)
192
+ return undefined;
193
+ const narrowed = narrowOverloadCandidates(candidates, callsite.arity, callsite.argumentTypes, {
194
+ argumentTypeClasses: callsite.argumentTypeClasses,
195
+ conversionRankFn: cppConversionRank,
196
+ constraintCompatibility: cppConstraintCompatibility,
197
+ });
198
+ if (narrowed.length === 1)
199
+ return { kind: 'resolved', definition: narrowed[0] };
200
+ if (narrowed.length > 1 || isOverloadAmbiguousAfterNormalization(narrowed, callsite.arity)) {
201
+ return {
202
+ kind: 'ambiguous',
203
+ candidateIds: narrowed.map((candidate) => candidate.nodeId),
204
+ };
205
+ }
206
+ return undefined;
207
+ }
208
+ function populateResolvedHierarchy(graph, parsedFiles, nodeLookup) {
209
+ const defByGraphId = new Map();
210
+ const defById = new Map();
211
+ const defsByFileAndName = new Map();
212
+ for (const parsed of parsedFiles) {
213
+ for (const def of parsed.localDefs) {
214
+ if (!isClassLike(def.type))
215
+ continue;
216
+ const graphId = resolveDefGraphId(parsed.filePath, def, nodeLookup);
217
+ if (graphId === undefined)
218
+ continue;
219
+ defByGraphId.set(graphId, def);
220
+ defById.set(def.nodeId, def);
221
+ const names = new Set([simpleName(def), definitionQualifiedName(def)]);
222
+ for (const name of names) {
223
+ if (name === '')
224
+ continue;
225
+ const key = `${parsed.filePath}\0${name}`;
226
+ const bucket = defsByFileAndName.get(key);
227
+ if (bucket === undefined)
228
+ defsByFileAndName.set(key, [def]);
229
+ else
230
+ bucket.push(def);
231
+ }
232
+ }
233
+ }
234
+ const parents = new Map();
235
+ for (const rel of graph.iterRelationshipsByType('EXTENDS')) {
236
+ const child = defByGraphId.get(rel.sourceId);
237
+ const parent = defByGraphId.get(rel.targetId);
238
+ if (child === undefined || parent === undefined)
239
+ continue;
240
+ const bucket = parents.get(child.nodeId);
241
+ if (bucket === undefined)
242
+ parents.set(child.nodeId, [parent.nodeId]);
243
+ else
244
+ bucket.push(parent.nodeId);
245
+ }
246
+ directParentsByDefId = parents;
247
+ ancestorsByDefId = buildAncestorClosure(parents);
248
+ inheritedLookupCache = new Map();
249
+ const nextVirtualEdges = new Set();
250
+ const nextUsings = new Map();
251
+ for (const parsed of parsedFiles) {
252
+ const captured = capturedByFile.get(parsed.filePath);
253
+ if (captured === undefined)
254
+ continue;
255
+ for (const edge of captured.baseEdges) {
256
+ if (!edge.isVirtual)
257
+ continue;
258
+ for (const child of matchingChildren(parsed.filePath, edge.childName, edge.childQualifiedName, defsByFileAndName)) {
259
+ const parent = findCapturedParent(parents.get(child.nodeId) ?? [], edge.baseName, edge.baseQualifiedName, defById);
260
+ if (parent !== undefined)
261
+ nextVirtualEdges.add(`${child.nodeId}\0${parent.nodeId}`);
262
+ }
263
+ }
264
+ for (const using of captured.memberUsings) {
265
+ const children = matchingChildren(parsed.filePath, using.childName, using.childQualifiedName, defsByFileAndName);
266
+ for (const child of children) {
267
+ const baseDef = findCapturedParent(parents.get(child.nodeId) ?? [], using.baseName, using.baseQualifiedName, defById);
268
+ if (baseDef === undefined)
269
+ continue;
270
+ const bucket = nextUsings.get(child.nodeId);
271
+ const entry = { baseDefId: baseDef.nodeId, memberName: using.memberName };
272
+ if (bucket === undefined)
273
+ nextUsings.set(child.nodeId, [entry]);
274
+ else
275
+ bucket.push(entry);
276
+ }
277
+ }
278
+ }
279
+ virtualEdges = nextVirtualEdges;
280
+ memberUsingsByDefId = nextUsings;
281
+ }
282
+ function captureBaseEdges(baseClause, childName, childQualifiedName, output) {
283
+ let segmentStart = 0;
284
+ for (let i = 0; i < baseClause.childCount; i++) {
285
+ const child = baseClause.child(i);
286
+ if (child === null)
287
+ continue;
288
+ if (child.type === ',' || child.text === ',') {
289
+ segmentStart = i + 1;
290
+ continue;
291
+ }
292
+ if (child.type !== 'type_identifier' &&
293
+ child.type !== 'template_type' &&
294
+ child.type !== 'qualified_identifier') {
295
+ continue;
296
+ }
297
+ let isVirtual = false;
298
+ for (let j = segmentStart; j < i; j++) {
299
+ const modifier = baseClause.child(j);
300
+ if (modifier?.text === 'virtual')
301
+ isVirtual = true;
302
+ }
303
+ const baseQualifiedName = qualifiedTypeName(child.text);
304
+ const baseName = baseQualifiedName.split('.').at(-1) ?? '';
305
+ if (baseName !== '') {
306
+ output.push({
307
+ childName,
308
+ ...(childQualifiedName !== childName ? { childQualifiedName } : {}),
309
+ baseName,
310
+ ...(baseQualifiedName !== baseName ? { baseQualifiedName } : {}),
311
+ isVirtual,
312
+ });
313
+ }
314
+ }
315
+ }
316
+ function parseMemberUsing(node, childName, childQualifiedName) {
317
+ const qualified = node.namedChildren.find((child) => child.type === 'qualified_identifier');
318
+ if (qualified === undefined)
319
+ return undefined;
320
+ const parts = splitQualifiedSegments(qualified.text);
321
+ if (parts.length < 2)
322
+ return undefined;
323
+ const memberName = stripTemplateSuffix(parts.at(-1) ?? '');
324
+ const baseParts = parts.slice(0, -1).map(stripTemplateSuffix).filter(Boolean);
325
+ const baseName = baseParts.at(-1) ?? '';
326
+ const baseQualifiedName = baseParts.join('.');
327
+ if (baseName === '' || memberName === '')
328
+ return undefined;
329
+ return {
330
+ childName,
331
+ ...(childQualifiedName !== childName ? { childQualifiedName } : {}),
332
+ baseName,
333
+ ...(baseQualifiedName !== baseName ? { baseQualifiedName } : {}),
334
+ memberName,
335
+ };
336
+ }
337
+ function classNameOf(node) {
338
+ const name = node.childForFieldName?.('name');
339
+ return name === null || name === undefined ? '' : trailingIdentifier(name.text);
340
+ }
341
+ function classQualifiedNameOf(node) {
342
+ const parts = [classNameOf(node)];
343
+ let current = node.parent;
344
+ while (current !== null) {
345
+ if (current.type === 'class_specifier' || current.type === 'struct_specifier') {
346
+ const name = classNameOf(current);
347
+ if (name !== '')
348
+ parts.unshift(name);
349
+ }
350
+ else if (current.type === 'namespace_definition') {
351
+ const name = current.childForFieldName?.('name');
352
+ if (name !== null && name !== undefined) {
353
+ parts.unshift(...splitQualifiedSegments(name.text).map(stripTemplateSuffix).filter(Boolean));
354
+ }
355
+ }
356
+ current = current.parent;
357
+ }
358
+ return parts.filter(Boolean).join('.');
359
+ }
360
+ function directChildOfType(node, type) {
361
+ for (let i = 0; i < node.namedChildCount; i++) {
362
+ const child = node.namedChild(i);
363
+ if (child?.type === type)
364
+ return child;
365
+ }
366
+ return null;
367
+ }
368
+ function trailingIdentifier(value) {
369
+ return stripTemplateSuffix(splitQualifiedSegments(value).at(-1) ?? '');
370
+ }
371
+ function qualifiedTypeName(value) {
372
+ return splitQualifiedSegments(value).map(stripTemplateSuffix).filter(Boolean).join('.');
373
+ }
374
+ function splitQualifiedSegments(value) {
375
+ const parts = [];
376
+ let angleDepth = 0;
377
+ let segmentStart = 0;
378
+ for (let i = 0; i < value.length; i++) {
379
+ const char = value[i];
380
+ if (char === '<')
381
+ angleDepth++;
382
+ else if (char === '>' && angleDepth > 0)
383
+ angleDepth--;
384
+ else if (char === ':' && value[i + 1] === ':' && angleDepth === 0) {
385
+ const segment = value.slice(segmentStart, i).trim();
386
+ if (segment !== '')
387
+ parts.push(segment);
388
+ segmentStart = i + 2;
389
+ i++;
390
+ }
391
+ }
392
+ const tail = value.slice(segmentStart).trim();
393
+ if (tail !== '')
394
+ parts.push(tail);
395
+ return parts;
396
+ }
397
+ function stripTemplateSuffix(value) {
398
+ const templateStart = value.indexOf('<');
399
+ return (templateStart >= 0 ? value.slice(0, templateStart) : value).trim();
400
+ }
401
+ function simpleName(def) {
402
+ return def.qualifiedName?.split('.').at(-1) ?? '';
403
+ }
404
+ function definitionQualifiedName(def) {
405
+ const name = def.qualifiedName ?? '';
406
+ if (name === '' || def.namespacePrefix === undefined || def.namespacePrefix === '')
407
+ return name;
408
+ return name.startsWith(`${def.namespacePrefix}.`) ? name : `${def.namespacePrefix}.${name}`;
409
+ }
410
+ function matchingChildren(filePath, childName, childQualifiedName, defsByFileAndName) {
411
+ if (childQualifiedName !== undefined) {
412
+ const qualified = defsByFileAndName.get(`${filePath}\0${childQualifiedName}`) ?? [];
413
+ if (qualified.length > 0)
414
+ return qualified;
415
+ }
416
+ const simple = defsByFileAndName.get(`${filePath}\0${childName}`) ?? [];
417
+ return simple.length === 1 ? simple : [];
418
+ }
419
+ function findCapturedParent(parentIds, baseName, baseQualifiedName, defById) {
420
+ const candidates = parentIds
421
+ .map((id) => defById.get(id))
422
+ .filter((definition) => definition !== undefined);
423
+ if (baseQualifiedName !== undefined) {
424
+ const qualified = candidates.filter((definition) => {
425
+ const name = definitionQualifiedName(definition);
426
+ return name === baseQualifiedName || name.endsWith(`.${baseQualifiedName}`);
427
+ });
428
+ if (qualified.length === 1)
429
+ return qualified[0];
430
+ return undefined;
431
+ }
432
+ const simple = candidates.filter((definition) => simpleName(definition) === baseName);
433
+ return simple.length === 1 ? simple[0] : undefined;
434
+ }
435
+ function buildAncestorClosure(parents) {
436
+ const closure = new Map();
437
+ const visiting = new Set();
438
+ const ancestorsOf = (defId) => {
439
+ const cached = closure.get(defId);
440
+ if (cached !== undefined)
441
+ return cached;
442
+ if (visiting.has(defId))
443
+ return new Set();
444
+ visiting.add(defId);
445
+ const ancestors = new Set();
446
+ for (const parent of parents.get(defId) ?? []) {
447
+ ancestors.add(parent);
448
+ for (const ancestor of ancestorsOf(parent))
449
+ ancestors.add(ancestor);
450
+ }
451
+ visiting.delete(defId);
452
+ closure.set(defId, ancestors);
453
+ return ancestors;
454
+ };
455
+ for (const defId of parents.keys())
456
+ ancestorsOf(defId);
457
+ return closure;
458
+ }
459
+ function isAncestor(ancestorDefId, descendantDefId) {
460
+ return ancestorsByDefId.get(descendantDefId)?.has(ancestorDefId) === true;
461
+ }
@@ -1,6 +1,5 @@
1
1
  import { findClassBindingInScope, findEnclosingClassDef, } from '../../scope-resolution/scope/walkers.js';
2
2
  import { SupportedLanguages } from '../../../../_shared/index.js';
3
- import { buildMro, defaultLinearize } from '../../scope-resolution/passes/mro.js';
4
3
  import { populateClassOwnedMembers, tagNamespacePrefixes, } from '../../scope-resolution/scope/walkers.js';
5
4
  import { cppProvider } from '../c-cpp.js';
6
5
  import { cppArityCompatibility } from './arity.js';
@@ -16,6 +15,7 @@ import { clearCppInlineNamespaces, populateCppInlineNamespaceScopes, resolveCppQ
16
15
  import { populateCppRangeBindings } from './range-bindings.js';
17
16
  import { cppConstraintCompatibility } from './constraint-filter.js';
18
17
  import { clearCppUserDefinedConversions, populateCppUserDefinedConversions, } from './user-defined-conversions.js';
18
+ import { buildCppMemberLookupMro, clearCppMemberLookupState, resolveCppReceiverMember, } from './member-lookup.js';
19
19
  /**
20
20
  * Per-pass memo of the augmented `#include`-resolution file set
21
21
  * (`allFilePaths` ∪ header paths), keyed on the two stable source sets.
@@ -69,6 +69,7 @@ export const cppScopeResolver = {
69
69
  clearCppAdlState();
70
70
  clearCppInlineNamespaces();
71
71
  clearCppUserDefinedConversions();
72
+ clearCppMemberLookupState();
72
73
  return scanCppHeaderFiles(repoPath);
73
74
  },
74
75
  resolveImportTarget: (targetRaw, fromFile, allFilePaths, resolutionConfig) => {
@@ -91,7 +92,7 @@ export const cppScopeResolver = {
91
92
  // C++20 `requires P`) provably fail at the call site. Three-valued —
92
93
  // `'unknown'` keeps the candidate, preserving "degrade not lie".
93
94
  constraintCompatibility: cppConstraintCompatibility,
94
- buildMro: (graph, parsedFiles, nodeLookup) => buildMro(graph, parsedFiles, nodeLookup, defaultLinearize),
95
+ buildMro: buildCppMemberLookupMro,
95
96
  // Worker-boundary restore (see `ScopeResolver.applyCaptureSideChannel`).
96
97
  // `emitCppScopeCaptures` records per-file ADL call-site arg shapes
97
98
  // (`markCppAdlSiteArgs`/`markCppAdlSiteNoAdl`), inline-/anonymous-namespace
@@ -212,6 +213,7 @@ export const cppScopeResolver = {
212
213
  hoistTypeBindingsToModule: true,
213
214
  // Enable receiver-bound explicit-`this` fallback only for C++.
214
215
  resolveThisViaEnclosingClass: true,
216
+ resolveReceiverMember: resolveCppReceiverMember,
215
217
  // The `isFileLocalDef` hook on the global free-call fallback names
216
218
  // file-local linkage historically, but semantically gates "logically
217
219
  // invisible cross-file" defs. C++ extends this to also reject class-
@@ -261,7 +261,7 @@
261
261
  * Plan that introduced most of these invariants:
262
262
  * `docs/plans/2026-04-20-001-refactor-emit-pipeline-generalization-plan.md`.
263
263
  */
264
- import type { BindingRef, Callsite, ConstraintContext, ParsedFile, ScopeId, SupportedLanguages, SymbolDefinition } from '../../../../_shared/index.js';
264
+ import type { BindingRef, Callsite, ConstraintContext, ParsedFile, ReferenceSite, ScopeId, SupportedLanguages, SymbolDefinition } from '../../../../_shared/index.js';
265
265
  import type { KnowledgeGraph } from '../../../graph/types.js';
266
266
  import type { GraphNodeLookup } from '../graph-bridge/node-lookup.js';
267
267
  import { LanguageProvider } from '../../language-provider.js';
@@ -275,6 +275,13 @@ import type { ConversionRankFn } from '../passes/overload-narrowing.js';
275
275
  export type LinearizeStrategy = (classDefId: string, directParents: readonly string[], parentsByDefId: ReadonlyMap<string, readonly string[]>) => string[];
276
276
  /** Result of `ScopeResolver.arityCompatibility` — mirrors `RegistryProviders.arityCompatibility`. */
277
277
  export type ArityVerdict = 'compatible' | 'unknown' | 'incompatible';
278
+ export type ReceiverMemberResolution = {
279
+ readonly kind: 'resolved';
280
+ readonly definition: SymbolDefinition;
281
+ } | {
282
+ readonly kind: 'ambiguous';
283
+ readonly candidateIds: readonly string[];
284
+ };
278
285
  /** Re-exported for ScopeResolver consumers — same shape as
279
286
  * `RegistryProviders.constraintCompatibility`'s third parameter. */
280
287
  export type { ConstraintContext } from '../../../../_shared/index.js';
@@ -367,7 +374,7 @@ export interface ScopeResolver {
367
374
  * C++ is the first consumer; see `languages/cpp/constraint-filter.ts`
368
375
  * for the Tier-A predicate registry and Kleene 3-valued evaluator.
369
376
  */
370
- readonly constraintCompatibility?: (callsite: Callsite, def: SymbolDefinition, ctx: ConstraintContext) => ArityVerdict;
377
+ readonly constraintCompatibility?: (callsite: ReferenceSite, def: SymbolDefinition, ctx: ConstraintContext) => ArityVerdict;
371
378
  /**
372
379
  * Compute the method-dispatch order for every Class def in the
373
380
  * workspace. Python uses depth-first first-seen via
@@ -730,6 +737,14 @@ export interface ScopeResolver {
730
737
  * `undefined` to fall through to the remaining cases.
731
738
  */
732
739
  readonly resolveQualifiedReceiverMember?: (receiverName: string, memberName: string, callerScope: ScopeId, scopes: ScopeResolutionIndexes, parsedFiles: readonly ParsedFile[], callsite?: Callsite) => SymbolDefinition | 'ambiguous' | undefined;
740
+ /**
741
+ * Optional language-specific member-lattice lookup. Runs for a resolved
742
+ * simple receiver type before the generic flattened-MRO walk. Languages
743
+ * with lookup-set semantics that cannot be represented by one linear MRO
744
+ * may resolve a member, report ambiguity (which suppresses fallback), or
745
+ * return undefined to retain the shared behavior.
746
+ */
747
+ readonly resolveReceiverMember?: (ownerDef: SymbolDefinition, memberName: string, callsite: Callsite, scopes: ScopeResolutionIndexes, model: SemanticModel) => ReceiverMemberResolution | undefined;
733
748
  /**
734
749
  * Enable the receiver-bound Case 0.5 fallback for explicit `this`
735
750
  * receivers (`this->m()` / `this.m()`) that resolves against the