gitnexus 1.6.5-rc.32 → 1.6.5-rc.34
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.
- package/dist/core/ingestion/languages/cpp/captures.js +67 -9
- package/dist/core/ingestion/languages/cpp/query.js +19 -0
- package/dist/core/ingestion/languages/cpp/scope-resolver.js +8 -4
- package/dist/core/ingestion/languages/cpp/two-phase-lookup.d.ts +25 -9
- package/dist/core/ingestion/languages/cpp/two-phase-lookup.js +99 -30
- package/dist/core/ingestion/scope-resolution/contract/scope-resolver.d.ts +3 -0
- package/dist/core/ingestion/scope-resolution/contract/scope-resolver.js +3 -0
- package/dist/core/ingestion/scope-resolution/pipeline/run.js +58 -3
- package/package.json +1 -1
|
@@ -237,6 +237,12 @@ export function emitCppScopeCaptures(sourceText, filePath, cachedTree) {
|
|
|
237
237
|
}
|
|
238
238
|
out.push(grouped);
|
|
239
239
|
}
|
|
240
|
+
// ── Emit inheritance references for scope-resolution MRO / EXTENDS ──
|
|
241
|
+
// Walk every class/struct base list and synthesize `@reference.inherits`
|
|
242
|
+
// captures consumed by the registry-primary graph bridge. The lookup name
|
|
243
|
+
// is normalized to the bare class name so `Base<T>` / `outer::v1::Base<T>`
|
|
244
|
+
// resolve through V1's simple-name `findClassBindingInScope('Base')`.
|
|
245
|
+
emitCppInheritanceCaptures(tree.rootNode, out);
|
|
240
246
|
// ── Detect dependent-base relationships for two-phase template lookup ──
|
|
241
247
|
// Walk the tree once, finding every `template_declaration` whose
|
|
242
248
|
// child is a class/struct definition with a `base_class_clause` whose
|
|
@@ -248,6 +254,41 @@ export function emitCppScopeCaptures(sourceText, filePath, cachedTree) {
|
|
|
248
254
|
detectCppDependentBases(tree.rootNode, filePath);
|
|
249
255
|
return out;
|
|
250
256
|
}
|
|
257
|
+
/**
|
|
258
|
+
* Walk every C++ class/struct base clause and emit `@reference.inherits`
|
|
259
|
+
* captures for each base so scope resolution can resolve them into EXTENDS
|
|
260
|
+
* edges. Lookup names are normalized to bare class names (`Base<T>` → `Base`,
|
|
261
|
+
* `outer::v1::Base<T>` → `Base`) to match the V1 simple-name
|
|
262
|
+
* `findClassBindingInScope` contract. This intentionally preserves the
|
|
263
|
+
* existing scope-chain tradeoff: qualified namespace context is discarded
|
|
264
|
+
* here instead of introducing a C++-only name-resolution lane in shared
|
|
265
|
+
* ingestion infrastructure.
|
|
266
|
+
*/
|
|
267
|
+
function emitCppInheritanceCaptures(root, out) {
|
|
268
|
+
const stack = [root];
|
|
269
|
+
while (stack.length > 0) {
|
|
270
|
+
const node = stack.pop();
|
|
271
|
+
if (node.type === 'class_specifier' || node.type === 'struct_specifier') {
|
|
272
|
+
const baseClause = findChildOfType(node, ['base_class_clause']);
|
|
273
|
+
if (baseClause !== null) {
|
|
274
|
+
for (const base of iterBaseClasses(baseClause)) {
|
|
275
|
+
const baseName = extractBaseLookupName(base);
|
|
276
|
+
if (baseName.length === 0)
|
|
277
|
+
continue;
|
|
278
|
+
out.push({
|
|
279
|
+
'@reference.inherits': nodeToCapture('@reference.inherits', base),
|
|
280
|
+
'@reference.name': syntheticCapture('@reference.name', base, baseName),
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
286
|
+
const child = node.child(i);
|
|
287
|
+
if (child !== null)
|
|
288
|
+
stack.push(child);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
251
292
|
/**
|
|
252
293
|
* Walk the AST finding every template_declaration containing a class or
|
|
253
294
|
* struct definition with a dependent base. Records (className, baseName)
|
|
@@ -277,7 +318,7 @@ function detectCppDependentBases(root, filePath) {
|
|
|
277
318
|
if (baseClause !== null) {
|
|
278
319
|
for (const base of iterBaseClasses(baseClause)) {
|
|
279
320
|
if (isBaseDependent(base, params)) {
|
|
280
|
-
const baseName =
|
|
321
|
+
const baseName = extractBaseLookupName(base);
|
|
281
322
|
if (baseName !== '') {
|
|
282
323
|
markCppDependentBase(filePath, className, baseName);
|
|
283
324
|
}
|
|
@@ -393,23 +434,40 @@ function isBaseDependent(baseNode, templateParams) {
|
|
|
393
434
|
}
|
|
394
435
|
return false;
|
|
395
436
|
}
|
|
396
|
-
/**
|
|
397
|
-
|
|
398
|
-
|
|
437
|
+
/**
|
|
438
|
+
* Recursively extract the bare lookup name of a base class node.
|
|
439
|
+
* Examples: `Base` → `Base`, `Base<T>` → `Base`,
|
|
440
|
+
* `outer::v1::Base<T>` → `Base`. Namespace qualifiers are intentionally
|
|
441
|
+
* dropped to align with V1 scope-chain lookup everywhere else in the
|
|
442
|
+
* registry-primary pipeline.
|
|
443
|
+
*/
|
|
444
|
+
function extractBaseLookupName(baseNode) {
|
|
445
|
+
if (baseNode.type === 'type_identifier' || baseNode.type === 'identifier')
|
|
399
446
|
return baseNode.text;
|
|
400
447
|
if (baseNode.type === 'template_type') {
|
|
401
448
|
const nameNode = baseNode.childForFieldName('name');
|
|
402
449
|
if (nameNode !== null)
|
|
403
|
-
return nameNode
|
|
404
|
-
|
|
405
|
-
|
|
450
|
+
return extractBaseLookupName(nameNode);
|
|
451
|
+
const id = findFirstDescendantOfType(baseNode, 'type_identifier') ??
|
|
452
|
+
findFirstDescendantOfType(baseNode, 'identifier');
|
|
406
453
|
if (id !== null)
|
|
407
454
|
return id.text;
|
|
408
455
|
}
|
|
409
456
|
if (baseNode.type === 'qualified_identifier') {
|
|
410
457
|
const nameNode = baseNode.childForFieldName('name');
|
|
411
|
-
if (nameNode !== null)
|
|
412
|
-
|
|
458
|
+
if (nameNode !== null) {
|
|
459
|
+
const nested = extractBaseLookupName(nameNode);
|
|
460
|
+
if (nested.length > 0)
|
|
461
|
+
return nested;
|
|
462
|
+
}
|
|
463
|
+
for (let i = baseNode.childCount - 1; i >= 0; i--) {
|
|
464
|
+
const child = baseNode.child(i);
|
|
465
|
+
if (child === null)
|
|
466
|
+
continue;
|
|
467
|
+
const nested = extractBaseLookupName(child);
|
|
468
|
+
if (nested.length > 0)
|
|
469
|
+
return nested;
|
|
470
|
+
}
|
|
413
471
|
}
|
|
414
472
|
return '';
|
|
415
473
|
}
|
|
@@ -416,6 +416,25 @@ const CPP_SCOPE_QUERY = `
|
|
|
416
416
|
scope: (_) @reference.receiver
|
|
417
417
|
name: (identifier) @reference.name)) @reference.call.qualified
|
|
418
418
|
|
|
419
|
+
;; Nested qualified receiver: outer::v1::Base<T>::f()
|
|
420
|
+
;; tree-sitter-cpp nests this as qualified_identifier(name:
|
|
421
|
+
;; qualified_identifier(scope: qualified_identifier(...), name: identifier)).
|
|
422
|
+
;; Capturing the innermost receiver still gives isSuperReceiverInContext
|
|
423
|
+
;; enough text to strip qualifiers/template args down to Base.
|
|
424
|
+
(call_expression
|
|
425
|
+
function: (qualified_identifier
|
|
426
|
+
name: (qualified_identifier
|
|
427
|
+
scope: (_) @reference.receiver
|
|
428
|
+
name: (identifier) @reference.name))) @reference.call.qualified
|
|
429
|
+
|
|
430
|
+
;; Double-nested qualified receiver: outer::v1::Base<T>::f()
|
|
431
|
+
(call_expression
|
|
432
|
+
function: (qualified_identifier
|
|
433
|
+
name: (qualified_identifier
|
|
434
|
+
name: (qualified_identifier
|
|
435
|
+
scope: (_) @reference.receiver
|
|
436
|
+
name: (identifier) @reference.name)))) @reference.call.qualified
|
|
437
|
+
|
|
419
438
|
;; ─── References — member calls (obj.method() / ptr->method()) ───────
|
|
420
439
|
(call_expression
|
|
421
440
|
function: (field_expression
|
|
@@ -68,15 +68,19 @@ export const cppScopeResolver = {
|
|
|
68
68
|
// fallback and wildcard expansion can suppress them as unqualified
|
|
69
69
|
// cross-file callables.
|
|
70
70
|
populateCppNonGloballyVisible(parsed);
|
|
71
|
-
// Resolve recorded template-class → dependent-base simple names to
|
|
72
|
-
// class nodeIds for two-phase template lookup (U3 of plan
|
|
73
|
-
// 2026-05-13-001).
|
|
74
|
-
populateCppDependentBases(parsed);
|
|
75
71
|
// Build the class-def → enclosing-namespace-qualified-name map used
|
|
76
72
|
// by ADL (U2 of plan 2026-05-13-001) to identify each argument type's
|
|
77
73
|
// associated namespace for Koenig lookup.
|
|
78
74
|
populateCppAssociatedNamespaces(parsed);
|
|
79
75
|
},
|
|
76
|
+
// Resolve recorded template-class → dependent-base simple names to
|
|
77
|
+
// class nodeIds for two-phase template lookup (U3 of plan
|
|
78
|
+
// 2026-05-13-001). Runs AFTER all files have had `populateOwners`
|
|
79
|
+
// applied so that cross-file base classes (e.g. Base in base.h,
|
|
80
|
+
// Derived in derived.h) are reachable in the workspace index.
|
|
81
|
+
populateWorkspaceOwners: (parsedFiles) => {
|
|
82
|
+
populateCppDependentBases(parsedFiles);
|
|
83
|
+
},
|
|
80
84
|
// Simple `isSuperReceiver` returns false for C++. Real super
|
|
81
85
|
// classification is caller-context-dependent and lives in
|
|
82
86
|
// `isSuperReceiverInContext` below — without scope context the
|
|
@@ -11,8 +11,20 @@
|
|
|
11
11
|
* This module records — during `emitCppScopeCaptures` — which template
|
|
12
12
|
* class declarations have which dependent base class names (per file).
|
|
13
13
|
* `populateCppDependentBases` then resolves those names to class nodeIds
|
|
14
|
-
* using
|
|
15
|
-
* `
|
|
14
|
+
* using a workspace-wide registry, building the per-class set the
|
|
15
|
+
* `isCppDependentBaseMember` predicate consumes.
|
|
16
|
+
*
|
|
17
|
+
* Cross-file resolution: `Base<T>` may be declared in a different header
|
|
18
|
+
* than `Derived<T>`. `populateCppDependentBases` therefore runs as a
|
|
19
|
+
* workspace-wide pass (`populateWorkspaceOwners` hook) after every file
|
|
20
|
+
* has had `populateOwners` applied, so all class defs are reachable.
|
|
21
|
+
*
|
|
22
|
+
* Namespace disambiguation: when multiple classes share a simple name
|
|
23
|
+
* (e.g., `Box` in two namespaces), the resolver prefers the candidate
|
|
24
|
+
* whose qualified-name prefix (namespace path) matches the deriving
|
|
25
|
+
* class's prefix. If no namespace match is found, a unique simple-name
|
|
26
|
+
* match is accepted; ambiguous matches (multiple candidates, no
|
|
27
|
+
* namespace winner) are skipped conservatively.
|
|
16
28
|
*
|
|
17
29
|
* NOTE: module-level state, single-process-single-repo use only.
|
|
18
30
|
* `clearFileLocalNames()` clears this state alongside file-local linkage
|
|
@@ -33,15 +45,19 @@ export declare function markCppDependentBase(filePath: string, className: string
|
|
|
33
45
|
/** Clear two-phase-lookup state. Called from `clearFileLocalNames`. */
|
|
34
46
|
export declare function clearCppDependentBases(): void;
|
|
35
47
|
/**
|
|
36
|
-
* Resolve recorded dependent-base simple names to class nodeIds using
|
|
37
|
-
*
|
|
38
|
-
*
|
|
48
|
+
* Resolve recorded dependent-base simple names to class nodeIds using a
|
|
49
|
+
* workspace-wide index. Run as `populateWorkspaceOwners` after every
|
|
50
|
+
* file has had `populateOwners` applied, so class defs from ALL files
|
|
51
|
+
* are reachable.
|
|
39
52
|
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
53
|
+
* Disambiguation strategy (multiple classes sharing a simple name):
|
|
54
|
+
* 1. Prefer the candidate whose qualified-name namespace prefix matches
|
|
55
|
+
* the deriving class's namespace prefix (same-namespace bias).
|
|
56
|
+
* 2. Fall back to accepting a unique simple-name match.
|
|
57
|
+
* 3. Skip when multiple candidates exist and no namespace match is
|
|
58
|
+
* found (conservative: avoids false associations).
|
|
43
59
|
*/
|
|
44
|
-
export declare function populateCppDependentBases(
|
|
60
|
+
export declare function populateCppDependentBases(parsedFiles: readonly ParsedFile[]): void;
|
|
45
61
|
/**
|
|
46
62
|
* Two-phase lookup predicate: is the candidate def a member of a
|
|
47
63
|
* dependent base of the caller's enclosing template class?
|
|
@@ -11,8 +11,20 @@
|
|
|
11
11
|
* This module records — during `emitCppScopeCaptures` — which template
|
|
12
12
|
* class declarations have which dependent base class names (per file).
|
|
13
13
|
* `populateCppDependentBases` then resolves those names to class nodeIds
|
|
14
|
-
* using
|
|
15
|
-
* `
|
|
14
|
+
* using a workspace-wide registry, building the per-class set the
|
|
15
|
+
* `isCppDependentBaseMember` predicate consumes.
|
|
16
|
+
*
|
|
17
|
+
* Cross-file resolution: `Base<T>` may be declared in a different header
|
|
18
|
+
* than `Derived<T>`. `populateCppDependentBases` therefore runs as a
|
|
19
|
+
* workspace-wide pass (`populateWorkspaceOwners` hook) after every file
|
|
20
|
+
* has had `populateOwners` applied, so all class defs are reachable.
|
|
21
|
+
*
|
|
22
|
+
* Namespace disambiguation: when multiple classes share a simple name
|
|
23
|
+
* (e.g., `Box` in two namespaces), the resolver prefers the candidate
|
|
24
|
+
* whose qualified-name prefix (namespace path) matches the deriving
|
|
25
|
+
* class's prefix. If no namespace match is found, a unique simple-name
|
|
26
|
+
* match is accepted; ambiguous matches (multiple candidates, no
|
|
27
|
+
* namespace winner) are skipped conservatively.
|
|
16
28
|
*
|
|
17
29
|
* NOTE: module-level state, single-process-single-repo use only.
|
|
18
30
|
* `clearFileLocalNames()` clears this state alongside file-local linkage
|
|
@@ -61,40 +73,97 @@ export function clearCppDependentBases() {
|
|
|
61
73
|
dependentBaseNodeIds.clear();
|
|
62
74
|
}
|
|
63
75
|
/**
|
|
64
|
-
* Resolve recorded dependent-base simple names to class nodeIds using
|
|
65
|
-
*
|
|
66
|
-
*
|
|
76
|
+
* Resolve recorded dependent-base simple names to class nodeIds using a
|
|
77
|
+
* workspace-wide index. Run as `populateWorkspaceOwners` after every
|
|
78
|
+
* file has had `populateOwners` applied, so class defs from ALL files
|
|
79
|
+
* are reachable.
|
|
67
80
|
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
81
|
+
* Disambiguation strategy (multiple classes sharing a simple name):
|
|
82
|
+
* 1. Prefer the candidate whose qualified-name namespace prefix matches
|
|
83
|
+
* the deriving class's namespace prefix (same-namespace bias).
|
|
84
|
+
* 2. Fall back to accepting a unique simple-name match.
|
|
85
|
+
* 3. Skip when multiple candidates exist and no namespace match is
|
|
86
|
+
* found (conservative: avoids false associations).
|
|
71
87
|
*/
|
|
72
|
-
export function populateCppDependentBases(
|
|
73
|
-
|
|
74
|
-
if (perFile === undefined)
|
|
88
|
+
export function populateCppDependentBases(parsedFiles) {
|
|
89
|
+
if (dependentBasesByFile.size === 0)
|
|
75
90
|
return;
|
|
76
|
-
// Build
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
91
|
+
// Build workspace-wide index: simpleName → {nodeId, nsPrefix}[]
|
|
92
|
+
// nsPrefix is the dot-joined namespace path (qualifiedName without the
|
|
93
|
+
// last segment). Classes at global scope have nsPrefix = ''.
|
|
94
|
+
const classesBySimpleName = new Map();
|
|
95
|
+
for (const parsed of parsedFiles) {
|
|
96
|
+
for (const def of parsed.localDefs) {
|
|
97
|
+
if (def.type !== 'Class' && def.type !== 'Struct' && def.type !== 'Interface')
|
|
98
|
+
continue;
|
|
99
|
+
const qn = def.qualifiedName ?? '';
|
|
100
|
+
const lastDot = qn.lastIndexOf('.');
|
|
101
|
+
const simple = lastDot >= 0 ? qn.slice(lastDot + 1) : qn;
|
|
102
|
+
if (simple === '')
|
|
103
|
+
continue;
|
|
104
|
+
const nsPrefix = lastDot >= 0 ? qn.slice(0, lastDot) : '';
|
|
105
|
+
let entries = classesBySimpleName.get(simple);
|
|
106
|
+
if (entries === undefined) {
|
|
107
|
+
entries = [];
|
|
108
|
+
classesBySimpleName.set(simple, entries);
|
|
109
|
+
}
|
|
110
|
+
entries.push({ nodeId: def.nodeId, nsPrefix });
|
|
111
|
+
}
|
|
84
112
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
113
|
+
// Build a filePath → ParsedFile lookup for fast per-file access.
|
|
114
|
+
const parsedByFile = new Map();
|
|
115
|
+
for (const parsed of parsedFiles)
|
|
116
|
+
parsedByFile.set(parsed.filePath, parsed);
|
|
117
|
+
for (const [filePath, perFile] of dependentBasesByFile) {
|
|
118
|
+
const parsed = parsedByFile.get(filePath);
|
|
119
|
+
if (parsed === undefined)
|
|
88
120
|
continue;
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
121
|
+
// Build a simple-name → {nodeId, nsPrefix} map for THIS file's
|
|
122
|
+
// class-like defs so we can identify each template class precisely
|
|
123
|
+
// (avoids cross-file name collisions for the deriving class itself).
|
|
124
|
+
const localClassByName = new Map();
|
|
125
|
+
for (const def of parsed.localDefs) {
|
|
126
|
+
if (def.type !== 'Class' && def.type !== 'Struct' && def.type !== 'Interface')
|
|
127
|
+
continue;
|
|
128
|
+
const qn = def.qualifiedName ?? '';
|
|
129
|
+
const lastDot = qn.lastIndexOf('.');
|
|
130
|
+
const simple = lastDot >= 0 ? qn.slice(lastDot + 1) : qn;
|
|
131
|
+
if (simple === '')
|
|
132
|
+
continue;
|
|
133
|
+
const nsPrefix = lastDot >= 0 ? qn.slice(0, lastDot) : '';
|
|
134
|
+
localClassByName.set(simple, { nodeId: def.nodeId, nsPrefix });
|
|
93
135
|
}
|
|
94
|
-
for (const
|
|
95
|
-
const
|
|
96
|
-
if (
|
|
97
|
-
|
|
136
|
+
for (const [className, baseNames] of perFile) {
|
|
137
|
+
const classEntry = localClassByName.get(className);
|
|
138
|
+
if (classEntry === undefined)
|
|
139
|
+
continue;
|
|
140
|
+
let bases = dependentBaseNodeIds.get(classEntry.nodeId);
|
|
141
|
+
if (bases === undefined) {
|
|
142
|
+
bases = new Set();
|
|
143
|
+
dependentBaseNodeIds.set(classEntry.nodeId, bases);
|
|
144
|
+
}
|
|
145
|
+
for (const baseName of baseNames) {
|
|
146
|
+
const candidates = classesBySimpleName.get(baseName);
|
|
147
|
+
if (candidates === undefined || candidates.length === 0)
|
|
148
|
+
continue;
|
|
149
|
+
if (candidates.length === 1) {
|
|
150
|
+
// Unique simple-name match — accept regardless of namespace.
|
|
151
|
+
bases.add(candidates[0].nodeId);
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
// Multiple classes share the same simple name — prefer the one
|
|
155
|
+
// whose namespace matches the deriving class's namespace.
|
|
156
|
+
// V1: exact dot-prefix match only. Cross-namespace inheritance
|
|
157
|
+
// (e.g., `ns::outer::Derived` extending bare `Inner` defined in
|
|
158
|
+
// `ns::outer::inner`) and inline-namespace cases are deferred to
|
|
159
|
+
// V2; the conservative skip-on-ambiguity below avoids false
|
|
160
|
+
// associations in those edge cases.
|
|
161
|
+
const nsMatch = candidates.find((c) => c.nsPrefix === classEntry.nsPrefix);
|
|
162
|
+
if (nsMatch !== undefined) {
|
|
163
|
+
bases.add(nsMatch.nodeId);
|
|
164
|
+
}
|
|
165
|
+
// else: ambiguous (multiple candidates, no namespace match) → skip.
|
|
166
|
+
}
|
|
98
167
|
}
|
|
99
168
|
}
|
|
100
169
|
}
|
|
@@ -87,6 +87,9 @@
|
|
|
87
87
|
* attempting emission (even on dedup-collapse), because the
|
|
88
88
|
* per-(caller, target) collapse semantics require multiple call
|
|
89
89
|
* sites in the same caller body not produce multiple edges.
|
|
90
|
+
* `preEmitInheritanceEdges` also pre-marks every `inherits` site so
|
|
91
|
+
* the generic bridge cannot remap class heritage into method-owned
|
|
92
|
+
* EXTENDS edges via `resolveCallerGraphId`.
|
|
90
93
|
*
|
|
91
94
|
* - **I3 — `propagateImportedReturnTypes` mutation timing + ordering.**
|
|
92
95
|
* The pass mutates `Scope.typeBindings` (a plain `new Map(...)` from
|
|
@@ -87,6 +87,9 @@
|
|
|
87
87
|
* attempting emission (even on dedup-collapse), because the
|
|
88
88
|
* per-(caller, target) collapse semantics require multiple call
|
|
89
89
|
* sites in the same caller body not produce multiple edges.
|
|
90
|
+
* `preEmitInheritanceEdges` also pre-marks every `inherits` site so
|
|
91
|
+
* the generic bridge cannot remap class heritage into method-owned
|
|
92
|
+
* EXTENDS edges via `resolveCallerGraphId`.
|
|
90
93
|
*
|
|
91
94
|
* - **I3 — `propagateImportedReturnTypes` mutation timing + ordering.**
|
|
92
95
|
* The pass mutates `Scope.typeBindings` (a plain `new Map(...)` from
|
|
@@ -28,14 +28,68 @@ import { extractParsedFile } from '../../scope-extractor-bridge.js';
|
|
|
28
28
|
import { finalizeScopeModel } from '../../finalize-orchestrator.js';
|
|
29
29
|
import { resolveReferenceSites } from '../../resolve-references.js';
|
|
30
30
|
import { buildGraphNodeLookup } from '../graph-bridge/node-lookup.js';
|
|
31
|
+
import { resolveDefGraphId } from '../graph-bridge/ids.js';
|
|
31
32
|
import { buildPopulatedMethodDispatch } from '../graph-bridge/method-dispatch.js';
|
|
33
|
+
import { tryEmitEdge } from '../graph-bridge/edges.js';
|
|
32
34
|
import { propagateImportedReturnTypes } from '../passes/imported-return-types.js';
|
|
33
35
|
import { emitReceiverBoundCalls } from '../passes/receiver-bound-calls.js';
|
|
34
36
|
import { emitFreeCallFallback } from '../passes/free-call-fallback.js';
|
|
35
37
|
import { emitReferencesViaLookup } from '../graph-bridge/references-to-edges.js';
|
|
36
38
|
import { emitImportEdges } from '../graph-bridge/imports-to-edges.js';
|
|
39
|
+
import { findClassBindingInScope, findEnclosingClassDef } from '../scope/walkers.js';
|
|
37
40
|
import { buildWorkspaceResolutionIndex } from '../workspace-index.js';
|
|
38
41
|
import { logger } from '../../../logger.js';
|
|
42
|
+
/**
|
|
43
|
+
* Resolve inheritance reference sites early and pre-emit their EXTENDS edges
|
|
44
|
+
* before MRO construction. This lets template-base captures contribute to the
|
|
45
|
+
* graph in time for `buildMro`, while `handledSites` prevents the generic
|
|
46
|
+
* reference-edge bridge from re-emitting the same sites later.
|
|
47
|
+
*
|
|
48
|
+
* @returns Site keys to seed the downstream handled-site skip set.
|
|
49
|
+
*/
|
|
50
|
+
function preEmitInheritanceEdges(graph, scopes, nodeLookup) {
|
|
51
|
+
const handledSites = new Set();
|
|
52
|
+
const seen = new Set();
|
|
53
|
+
const existing = new Set();
|
|
54
|
+
for (const rel of graph.iterRelationshipsByType('EXTENDS')) {
|
|
55
|
+
existing.add(`${rel.sourceId}->${rel.targetId}`);
|
|
56
|
+
}
|
|
57
|
+
for (const site of scopes.referenceSites) {
|
|
58
|
+
if (site.kind !== 'inherits')
|
|
59
|
+
continue;
|
|
60
|
+
const scope = scopes.scopeTree.getScope(site.inScope);
|
|
61
|
+
const siteKey = scope?.filePath !== undefined
|
|
62
|
+
? `${scope.filePath}:${site.atRange.startLine}:${site.atRange.startCol}`
|
|
63
|
+
: undefined;
|
|
64
|
+
if (siteKey !== undefined) {
|
|
65
|
+
// Intentionally suppress every `inherits` site from the generic
|
|
66
|
+
// reference bridge, even when this pre-pass can't emit an EXTENDS
|
|
67
|
+
// edge. The shared bridge resolves the source via
|
|
68
|
+
// `resolveCallerGraphId`, which can degrade class-heritage sites into
|
|
69
|
+
// method-owned EXTENDS edges once methods exist on the class. This
|
|
70
|
+
// pre-pass is the authoritative inheritance emitter, so broad
|
|
71
|
+
// suppression keeps `buildMro` and the final graph class-owned.
|
|
72
|
+
handledSites.add(siteKey);
|
|
73
|
+
}
|
|
74
|
+
const targetDef = findClassBindingInScope(site.inScope, site.name, scopes);
|
|
75
|
+
if (targetDef === undefined)
|
|
76
|
+
continue;
|
|
77
|
+
const callerClass = findEnclosingClassDef(site.inScope, scopes);
|
|
78
|
+
if (callerClass === undefined)
|
|
79
|
+
continue;
|
|
80
|
+
const callerGraphId = resolveDefGraphId(callerClass.filePath, callerClass, nodeLookup);
|
|
81
|
+
const targetGraphId = resolveDefGraphId(targetDef.filePath, targetDef, nodeLookup);
|
|
82
|
+
if (callerGraphId === undefined || targetGraphId === undefined)
|
|
83
|
+
continue;
|
|
84
|
+
const edgeKey = `${callerGraphId}->${targetGraphId}`;
|
|
85
|
+
if (existing.has(edgeKey))
|
|
86
|
+
continue;
|
|
87
|
+
if (tryEmitEdge(graph, scopes, nodeLookup, site, targetDef, 'scope-resolution: inherits', seen, 0.85)) {
|
|
88
|
+
existing.add(edgeKey);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return handledSites;
|
|
92
|
+
}
|
|
39
93
|
export function runScopeResolution(input, provider) {
|
|
40
94
|
const { graph, files } = input;
|
|
41
95
|
const onWarn = input.onWarn ?? (() => { });
|
|
@@ -108,8 +162,6 @@ export function runScopeResolution(input, provider) {
|
|
|
108
162
|
// ── Phase 2: finalize → ScopeResolutionIndexes ─────────────────────────
|
|
109
163
|
const allFilePaths = new Set(parsedFiles.map((f) => f.filePath));
|
|
110
164
|
const nodeLookup = buildGraphNodeLookup(graph);
|
|
111
|
-
const mroByClassDefId = provider.buildMro(graph, parsedFiles, nodeLookup);
|
|
112
|
-
const extendsOnlyMroByClassDefId = provider.buildExtendsOnlyMro?.(graph, parsedFiles, nodeLookup);
|
|
113
165
|
const resolutionConfig = input.resolutionConfig;
|
|
114
166
|
const finalized = finalizeScopeModel(parsedFiles, {
|
|
115
167
|
hooks: {
|
|
@@ -118,6 +170,9 @@ export function runScopeResolution(input, provider) {
|
|
|
118
170
|
mergeBindings: (existing, incoming, scopeId) => provider.mergeBindings(existing, incoming, scopeId),
|
|
119
171
|
},
|
|
120
172
|
});
|
|
173
|
+
const preEmittedInheritanceSites = preEmitInheritanceEdges(graph, finalized, nodeLookup);
|
|
174
|
+
const mroByClassDefId = provider.buildMro(graph, parsedFiles, nodeLookup);
|
|
175
|
+
const extendsOnlyMroByClassDefId = provider.buildExtendsOnlyMro?.(graph, parsedFiles, nodeLookup);
|
|
121
176
|
// Replace the empty MethodDispatchIndex that finalizeScopeModel
|
|
122
177
|
// builds by design with the populated one derived from the
|
|
123
178
|
// language's MRO. Spread produces a fresh `ScopeResolutionIndexes`
|
|
@@ -184,7 +239,7 @@ export function runScopeResolution(input, provider) {
|
|
|
184
239
|
});
|
|
185
240
|
const tResolve = PROF ? process.hrtime.bigint() : 0n;
|
|
186
241
|
// ── Phase 4: emit graph edges (LOAD-BEARING ORDER — see I1) ────────────
|
|
187
|
-
const handledSites = new Set();
|
|
242
|
+
const handledSites = new Set(preEmittedInheritanceSites);
|
|
188
243
|
const receiverExtras = emitReceiverBoundCalls(graph, indexes, parsedFiles, nodeLookup, handledSites, provider, workspaceIndex, readonlyModel);
|
|
189
244
|
const unresolvedReceiverExtras = provider.emitUnresolvedReceiverEdges !== undefined
|
|
190
245
|
? provider.emitUnresolvedReceiverEdges(graph, indexes, parsedFiles, nodeLookup, handledSites, readonlyModel)
|
package/package.json
CHANGED