gitnexus 1.6.6-rc.145 → 1.6.6-rc.146
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/csharp/captures.js +108 -0
- package/dist/core/ingestion/languages/csharp/interpret.js +17 -3
- package/dist/core/ingestion/languages/csharp/qualified-type-names.d.ts +14 -0
- package/dist/core/ingestion/languages/csharp/qualified-type-names.js +59 -0
- package/dist/core/ingestion/languages/csharp/query.js +13 -0
- package/dist/core/ingestion/languages/csharp/scope-resolver.js +8 -1
- package/dist/core/ingestion/scope-resolution/graph-bridge/edges.js +2 -2
- package/dist/core/ingestion/scope-resolution/graph-bridge/ids.d.ts +4 -1
- package/dist/core/ingestion/scope-resolution/graph-bridge/ids.js +32 -14
- package/dist/core/ingestion/scope-resolution/graph-bridge/node-lookup.js +1 -11
- package/dist/core/ingestion/scope-resolution/passes/free-call-fallback.js +22 -12
- package/dist/core/ingestion/scope-resolution/scope/walkers.js +44 -4
- package/dist/core/ingestion/utils/callable-labels.d.ts +13 -0
- package/dist/core/ingestion/utils/callable-labels.js +14 -0
- package/dist/core/ingestion/utils/qualified-name.d.ts +6 -0
- package/dist/core/ingestion/utils/qualified-name.js +27 -0
- package/package.json +1 -1
|
@@ -163,6 +163,26 @@ export function emitCsharpScopeCaptures(sourceText, _filePath, cachedTree) {
|
|
|
163
163
|
}
|
|
164
164
|
}
|
|
165
165
|
}
|
|
166
|
+
// Qualified constructor calls — `new Ns.Foo()`, `new A.B.Foo()`,
|
|
167
|
+
// `new Ns.Box<int>()` — bind only `@reference.call.constructor.qualified`
|
|
168
|
+
// with NO `@reference.name`, so the central extractor falls back to the
|
|
169
|
+
// whole-expression anchor and the reference name becomes the raw
|
|
170
|
+
// `new Ns.Foo()` text (never resolves). Derive the bare simple-name tail via
|
|
171
|
+
// the same `terminalTypeNameNode` helper the inheritance synth uses — it
|
|
172
|
+
// handles qualified_name, a generic tail (`Ns.Box<int>` → `Box`), and
|
|
173
|
+
// alias_qualified_name (`global::Ns.Foo`). Mirrors Java F35 (#1928).
|
|
174
|
+
if (grouped['@reference.call.constructor.qualified'] !== undefined &&
|
|
175
|
+
grouped['@reference.name'] === undefined) {
|
|
176
|
+
const qNode = nodeMap['@reference.call.constructor.qualified'];
|
|
177
|
+
const nameNode = qNode === undefined ? null : terminalTypeNameNode(qNode);
|
|
178
|
+
if (nameNode !== null) {
|
|
179
|
+
grouped['@reference.name'] = nodeToCapture('@reference.name', nameNode);
|
|
180
|
+
const qText = qNode.text.trim();
|
|
181
|
+
if (qText.length > 0 && qText !== nameNode.text) {
|
|
182
|
+
grouped['@reference.qualified-name'] = syntheticCapture('@reference.qualified-name', qNode, qText);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
166
186
|
// Synthesize `@reference.arity` on every callsite so the
|
|
167
187
|
// registry's arity filter can narrow overloads. Count the
|
|
168
188
|
// `argument` named children of the backing `argument_list`.
|
|
@@ -206,8 +226,96 @@ export function emitCsharpScopeCaptures(sourceText, _filePath, cachedTree) {
|
|
|
206
226
|
}
|
|
207
227
|
out.push(...synthesizeGenericTypeArgumentReferences(tree.rootNode));
|
|
208
228
|
out.push(...synthesizeCsharpInheritanceReferences(tree.rootNode));
|
|
229
|
+
out.push(...synthesizeCsharpConstructorInitializerReferences(tree.rootNode));
|
|
209
230
|
return out;
|
|
210
231
|
}
|
|
232
|
+
/**
|
|
233
|
+
* Synthesize `@reference.call.constructor` captures for C# constructor
|
|
234
|
+
* initializers — `: base(...)` and `: this(...)` (F38 analog of Java #1928).
|
|
235
|
+
* tree-sitter-c-sharp models these as `constructor_initializer` nodes the scope
|
|
236
|
+
* query never matched, so the chained-constructor CALLS edges (derived ctor →
|
|
237
|
+
* base ctor; ctor → sibling overload) were silently dropped.
|
|
238
|
+
*
|
|
239
|
+
* The initializer carries no constructor name (the `base`/`this` child is a bare
|
|
240
|
+
* keyword token), so the target is resolved structurally:
|
|
241
|
+
* - `this(...)` → the enclosing type's own simple name.
|
|
242
|
+
* - `base(...)` → the enclosing class/record's base type, reduced to its bare
|
|
243
|
+
* simple name via `terminalTypeNameNode`. C# requires the base class first in
|
|
244
|
+
* a mixed list (`class C : Base, IFoo`); interface-only lists (`class C : IFoo`)
|
|
245
|
+
* imply implicit `System.Object` — no `@reference` is emitted when the first
|
|
246
|
+
* non-builtin base would be an interface-only target (resolution also drops
|
|
247
|
+
* Interface-typed constructor targets in `free-call-fallback`).
|
|
248
|
+
* Arity is attached for overload disambiguation, mirroring `new X(...)`.
|
|
249
|
+
*/
|
|
250
|
+
function synthesizeCsharpConstructorInitializerReferences(root) {
|
|
251
|
+
const out = [];
|
|
252
|
+
walkNamedTree(root, (node) => {
|
|
253
|
+
if (node.type !== 'constructor_initializer')
|
|
254
|
+
return;
|
|
255
|
+
let kind = null;
|
|
256
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
257
|
+
const t = node.child(i)?.type;
|
|
258
|
+
if (t === 'base' || t === 'this') {
|
|
259
|
+
kind = t;
|
|
260
|
+
break;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
if (kind === null)
|
|
264
|
+
return;
|
|
265
|
+
const enclosingType = findEnclosingTypeDeclaration(node);
|
|
266
|
+
if (enclosingType === null)
|
|
267
|
+
return;
|
|
268
|
+
let targetNameNode = null;
|
|
269
|
+
if (kind === 'this') {
|
|
270
|
+
targetNameNode = enclosingType.childForFieldName('name');
|
|
271
|
+
}
|
|
272
|
+
else {
|
|
273
|
+
const baseList = findNamedChild(enclosingType, 'base_list');
|
|
274
|
+
if (baseList === null)
|
|
275
|
+
return;
|
|
276
|
+
// Prefer the first non-builtin entry (idiomatically the base class). When
|
|
277
|
+
// the list is interface-only (`class C : IFoo`), do not synthesize a
|
|
278
|
+
// `base(...)` ref — valid C# chains to implicit Object, not IFoo (#2046).
|
|
279
|
+
let sawNonBuiltin = false;
|
|
280
|
+
for (const base of baseList.namedChildren) {
|
|
281
|
+
if (base === null)
|
|
282
|
+
continue;
|
|
283
|
+
const n = terminalTypeNameNode(base);
|
|
284
|
+
if (n === null || BUILTIN_TYPE_NAMES.has(n.text))
|
|
285
|
+
continue;
|
|
286
|
+
sawNonBuiltin = true;
|
|
287
|
+
targetNameNode = n;
|
|
288
|
+
break;
|
|
289
|
+
}
|
|
290
|
+
if (!sawNonBuiltin)
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
if (targetNameNode === null)
|
|
294
|
+
return;
|
|
295
|
+
const argList = findNamedChild(node, 'argument_list');
|
|
296
|
+
const arity = argList === null
|
|
297
|
+
? 0
|
|
298
|
+
: argList.namedChildren.filter((c) => c !== null && c.type === 'argument').length;
|
|
299
|
+
out.push({
|
|
300
|
+
'@reference.call.constructor': nodeToCapture('@reference.call.constructor', node),
|
|
301
|
+
'@reference.name': nodeToCapture('@reference.name', targetNameNode),
|
|
302
|
+
'@reference.arity': syntheticCapture('@reference.arity', node, String(arity)),
|
|
303
|
+
});
|
|
304
|
+
});
|
|
305
|
+
return out;
|
|
306
|
+
}
|
|
307
|
+
function findEnclosingTypeDeclaration(node) {
|
|
308
|
+
let cur = node.parent;
|
|
309
|
+
while (cur !== null) {
|
|
310
|
+
if (cur.type === 'class_declaration' ||
|
|
311
|
+
cur.type === 'struct_declaration' ||
|
|
312
|
+
cur.type === 'record_declaration') {
|
|
313
|
+
return cur;
|
|
314
|
+
}
|
|
315
|
+
cur = cur.parent;
|
|
316
|
+
}
|
|
317
|
+
return null;
|
|
318
|
+
}
|
|
211
319
|
/**
|
|
212
320
|
* Synthesize `@reference.inherits` captures from C# base lists so the
|
|
213
321
|
* registry-primary scope-resolution path emits EXTENDS / IMPLEMENTS edges
|
|
@@ -122,10 +122,24 @@ function stripGeneric(text) {
|
|
|
122
122
|
* receiver's generic type based on the suffix — `data.Values` →
|
|
123
123
|
* element type of `data`'s Dictionary<K,V>. */
|
|
124
124
|
function stripQualifier(text) {
|
|
125
|
-
|
|
126
|
-
|
|
125
|
+
// Strip only the outermost qualifier: the last `.` at generic nesting depth 0.
|
|
126
|
+
// This preserves F41 (never cut inside `Dictionary<string, Ns.User>`) AND
|
|
127
|
+
// nested types through a generic outer (`Ns.Outer<int>.Inner` → `Inner`, not
|
|
128
|
+
// `Outer<int>.Inner` — #2046 P3).
|
|
129
|
+
let depth = 0;
|
|
130
|
+
let lastDotAtDepth0 = -1;
|
|
131
|
+
for (let i = 0; i < text.length; i++) {
|
|
132
|
+
const c = text[i];
|
|
133
|
+
if (c === '<')
|
|
134
|
+
depth++;
|
|
135
|
+
else if (c === '>')
|
|
136
|
+
depth = Math.max(0, depth - 1);
|
|
137
|
+
else if (c === '.' && depth === 0)
|
|
138
|
+
lastDotAtDepth0 = i;
|
|
139
|
+
}
|
|
140
|
+
if (lastDotAtDepth0 === -1)
|
|
127
141
|
return text;
|
|
128
|
-
const tail = text.slice(
|
|
142
|
+
const tail = text.slice(lastDotAtDepth0 + 1);
|
|
129
143
|
if (COLLECTION_ACCESSOR_SUFFIXES.has(tail))
|
|
130
144
|
return text;
|
|
131
145
|
return tail;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tag C# file-level type defs with their enclosing-namespace path on the
|
|
3
|
+
* sidecar `namespacePrefix` field — WITHOUT touching `qualifiedName` (mutating
|
|
4
|
+
* it corrupts simple-name heritage / base resolution; #2046 regression).
|
|
5
|
+
*
|
|
6
|
+
* `tagNamespacePrefixes` (shared) only reaches defs whose scope chain includes
|
|
7
|
+
* a Namespace scope. C# file-scoped `namespace X;` gives the Namespace scope a
|
|
8
|
+
* 1-line range, so top-level types land under the Module scope and are missed.
|
|
9
|
+
* This pass covers both block- and file-scoped namespaces so the qualified
|
|
10
|
+
* constructor resolver can break a same-tail collision (`new B.Foo()` with both
|
|
11
|
+
* `A.Foo` and `B.Foo`) by matching the explicit qualifier against the sidecar.
|
|
12
|
+
*/
|
|
13
|
+
import type { ParsedFile } from '../../../../_shared/index.js';
|
|
14
|
+
export declare function populateCsharpNamespacePrefixes(parsed: ParsedFile): void;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { isClassLike } from '../../scope-resolution/scope/walkers.js';
|
|
2
|
+
function isTypeDef(def) {
|
|
3
|
+
return isClassLike(def.type) || def.type === 'Enum';
|
|
4
|
+
}
|
|
5
|
+
export function populateCsharpNamespacePrefixes(parsed) {
|
|
6
|
+
const scopesById = new Map();
|
|
7
|
+
for (const scope of parsed.scopes)
|
|
8
|
+
scopesById.set(scope.id, scope);
|
|
9
|
+
// The file's declared namespace (file-scoped `namespace X;`). First Namespace
|
|
10
|
+
// scope's own def qualifiedName; undefined when the file is namespace-free.
|
|
11
|
+
const fileNamespace = (() => {
|
|
12
|
+
for (const scope of parsed.scopes) {
|
|
13
|
+
if (scope.kind !== 'Namespace')
|
|
14
|
+
continue;
|
|
15
|
+
const nsDef = scope.ownedDefs.find((d) => d.type === 'Namespace');
|
|
16
|
+
const q = nsDef?.qualifiedName;
|
|
17
|
+
if (q !== undefined && q.length > 0)
|
|
18
|
+
return q;
|
|
19
|
+
}
|
|
20
|
+
return undefined;
|
|
21
|
+
})();
|
|
22
|
+
// Enclosing namespace path for a scope: nearest ancestor Namespace scope's
|
|
23
|
+
// full qualifiedName, else the file-scoped namespace (Module-parented types).
|
|
24
|
+
const namespaceOf = (scope) => {
|
|
25
|
+
let parentId = scope.parent;
|
|
26
|
+
while (parentId !== null) {
|
|
27
|
+
const parent = scopesById.get(parentId);
|
|
28
|
+
if (parent === undefined)
|
|
29
|
+
break;
|
|
30
|
+
if (parent.kind === 'Namespace') {
|
|
31
|
+
const nsDef = parent.ownedDefs.find((d) => d.type === 'Namespace');
|
|
32
|
+
const q = nsDef?.qualifiedName;
|
|
33
|
+
if (q !== undefined && q.length > 0)
|
|
34
|
+
return q;
|
|
35
|
+
}
|
|
36
|
+
if (parent.kind === 'Module')
|
|
37
|
+
return fileNamespace;
|
|
38
|
+
parentId = parent.parent;
|
|
39
|
+
}
|
|
40
|
+
return fileNamespace;
|
|
41
|
+
};
|
|
42
|
+
for (const scope of parsed.scopes) {
|
|
43
|
+
if (scope.kind !== 'Class')
|
|
44
|
+
continue;
|
|
45
|
+
const prefix = namespaceOf(scope);
|
|
46
|
+
if (prefix === undefined || prefix.length === 0)
|
|
47
|
+
continue;
|
|
48
|
+
for (const def of scope.ownedDefs) {
|
|
49
|
+
if (!isTypeDef(def))
|
|
50
|
+
continue;
|
|
51
|
+
if (def.namespacePrefix !== undefined)
|
|
52
|
+
continue;
|
|
53
|
+
const q = def.qualifiedName;
|
|
54
|
+
if (q === prefix || (q !== undefined && q.startsWith(`${prefix}.`)))
|
|
55
|
+
continue;
|
|
56
|
+
def.namespacePrefix = prefix;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -39,6 +39,12 @@ const CSHARP_SCOPE_QUERY = `
|
|
|
39
39
|
(namespace_declaration) @scope.namespace
|
|
40
40
|
(file_scoped_namespace_declaration) @scope.namespace
|
|
41
41
|
|
|
42
|
+
(namespace_declaration
|
|
43
|
+
name: (_) @declaration.name) @declaration.namespace
|
|
44
|
+
|
|
45
|
+
(file_scoped_namespace_declaration
|
|
46
|
+
name: (_) @declaration.name) @declaration.namespace
|
|
47
|
+
|
|
42
48
|
(class_declaration) @scope.class
|
|
43
49
|
(interface_declaration) @scope.class
|
|
44
50
|
(struct_declaration) @scope.class
|
|
@@ -480,6 +486,13 @@ const CSHARP_SCOPE_QUERY = `
|
|
|
480
486
|
(object_creation_expression
|
|
481
487
|
type: (qualified_name) @reference.call.constructor.qualified) @reference.call.constructor
|
|
482
488
|
|
|
489
|
+
;; Alias-qualified constructor: \`new MyAlias::Foo()\`, \`new global::Foo()\`. The
|
|
490
|
+
;; top-level type is an alias_qualified_name (a \`global::Ns.Foo\` qualifier nests
|
|
491
|
+
;; under qualified_name instead, covered above). No @reference.name here —
|
|
492
|
+
;; captures.ts derives the simple-name tail via terminalTypeNameNode.
|
|
493
|
+
(object_creation_expression
|
|
494
|
+
type: (alias_qualified_name) @reference.call.constructor.qualified) @reference.call.constructor
|
|
495
|
+
|
|
483
496
|
;; References — field/property writes: \`obj.Name = "x"\` emits a write
|
|
484
497
|
;; ACCESSES edge from the enclosing method to the field/property on
|
|
485
498
|
;; obj's class.
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import { SupportedLanguages } from '../../../../_shared/index.js';
|
|
9
9
|
import { buildMro, defaultLinearize } from '../../scope-resolution/passes/mro.js';
|
|
10
10
|
import { populateClassOwnedMembers } from '../../scope-resolution/scope/walkers.js';
|
|
11
|
+
import { populateCsharpNamespacePrefixes } from './qualified-type-names.js';
|
|
11
12
|
import { csharpProvider } from '../csharp.js';
|
|
12
13
|
import { csharpArityCompatibility, csharpMergeBindings, resolveCsharpImportTarget, } from './index.js';
|
|
13
14
|
import { populateCsharpNamespaceSiblings } from './namespace-siblings.js';
|
|
@@ -38,7 +39,13 @@ const csharpScopeResolver = {
|
|
|
38
39
|
// contract is (callsite, def).
|
|
39
40
|
arityCompatibility: (callsite, def) => csharpArityCompatibility(def, callsite),
|
|
40
41
|
buildMro: (graph, parsedFiles, nodeLookup) => buildMro(graph, parsedFiles, nodeLookup, defaultLinearize),
|
|
41
|
-
populateOwners: (parsed) =>
|
|
42
|
+
populateOwners: (parsed) => {
|
|
43
|
+
populateClassOwnedMembers(parsed);
|
|
44
|
+
// Sidecar-only namespace tagging (does NOT touch qualifiedName) so the
|
|
45
|
+
// qualified constructor resolver can break same-tail collisions like
|
|
46
|
+
// `new B.Foo()` by matching the explicit qualifier (#2046).
|
|
47
|
+
populateCsharpNamespacePrefixes(parsed);
|
|
48
|
+
},
|
|
42
49
|
// C# uses `base` for super-class dispatch, not `super`. Match as a
|
|
43
50
|
// plain identifier (no `()` call like Python's `super(...)`) — `base`
|
|
44
51
|
// is a keyword-like receiver, not a callable.
|
|
@@ -55,7 +55,7 @@ export function tryEmitEdge(graph, scopes, nodeLookup, site, targetDef, reason,
|
|
|
55
55
|
// Inheritance edges are emitted directly by `preEmitInheritanceEdges` (which
|
|
56
56
|
// owns the enclosing-class caller and the EXTENDS-vs-IMPLEMENTS type), so this
|
|
57
57
|
// generic bridge derives caller + edge type purely from the site.
|
|
58
|
-
const callerGraphId = resolveCallerGraphId(site.inScope, scopes, nodeLookup);
|
|
58
|
+
const callerGraphId = resolveCallerGraphId(site.inScope, scopes, nodeLookup, site.atRange);
|
|
59
59
|
const targetGraphId = resolveDefGraphId(targetDef.filePath, targetDef, nodeLookup);
|
|
60
60
|
const edgeType = mapReferenceKindToEdgeType(site.kind);
|
|
61
61
|
if (callerGraphId === undefined)
|
|
@@ -99,7 +99,7 @@ export function tryEmitEdge(graph, scopes, nodeLookup, site, targetDef, reason,
|
|
|
99
99
|
* flag honoring, edge-type mapping, caller-id resolution.
|
|
100
100
|
*/
|
|
101
101
|
export function tryEmitEdgeWithExplicitTargetId(graph, scopes, nodeLookup, site, targetGraphId, reason, seen, confidence = 0.85, collapseByCallerTarget = false) {
|
|
102
|
-
const callerGraphId = resolveCallerGraphId(site.inScope, scopes, nodeLookup);
|
|
102
|
+
const callerGraphId = resolveCallerGraphId(site.inScope, scopes, nodeLookup, site.atRange);
|
|
103
103
|
const edgeType = mapReferenceKindToEdgeType(site.kind);
|
|
104
104
|
if (callerGraphId === undefined)
|
|
105
105
|
return false;
|
|
@@ -59,4 +59,7 @@ export declare function simpleQualifiedName(def: SymbolDefinition): string | und
|
|
|
59
59
|
* edge source. Matches legacy DAG behavior where module-level CALLS
|
|
60
60
|
* edges originate from the file symbol.
|
|
61
61
|
*/
|
|
62
|
-
export declare function resolveCallerGraphId(startScope: ScopeId, scopes: ScopeResolutionIndexes, nodeLookup: GraphNodeLookup
|
|
62
|
+
export declare function resolveCallerGraphId(startScope: ScopeId, scopes: ScopeResolutionIndexes, nodeLookup: GraphNodeLookup, atRange?: {
|
|
63
|
+
startLine: number;
|
|
64
|
+
startCol: number;
|
|
65
|
+
}): string | undefined;
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
*/
|
|
19
19
|
import { generateId } from '../../../../lib/utils.js';
|
|
20
20
|
import { qualifiedKey, simpleKey } from '../graph-bridge/node-lookup.js';
|
|
21
|
+
import { isOverloadableCallable } from '../../utils/callable-labels.js';
|
|
21
22
|
import { templateConstraintsIdTag } from '../../utils/template-arguments.js';
|
|
22
23
|
import { parameterShapeIdTag } from '../../utils/method-props.js';
|
|
23
24
|
/**
|
|
@@ -48,14 +49,30 @@ function isCallerAnchorLabel(label) {
|
|
|
48
49
|
label === 'Struct' ||
|
|
49
50
|
label === 'Enum');
|
|
50
51
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
return
|
|
52
|
+
function rangeContainsPoint(range, at) {
|
|
53
|
+
if (at.startLine < range.startLine || at.startLine > range.endLine)
|
|
54
|
+
return false;
|
|
55
|
+
if (at.startLine === range.startLine && at.startCol < range.startCol)
|
|
56
|
+
return false;
|
|
57
|
+
if (at.startLine === range.endLine && at.startCol > range.endCol)
|
|
58
|
+
return false;
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
/** Pick the callable that owns `atRange` when multiple overloads share a class scope. */
|
|
62
|
+
function pickCallerCallableDef(scope, scopes, atRange) {
|
|
63
|
+
if (atRange !== undefined) {
|
|
64
|
+
for (const childId of scopes.scopeTree.getChildren(scope.id)) {
|
|
65
|
+
const child = scopes.scopeTree.getScope(childId);
|
|
66
|
+
if (child === undefined || child.kind !== 'Function')
|
|
67
|
+
continue;
|
|
68
|
+
if (!rangeContainsPoint(child.range, atRange))
|
|
69
|
+
continue;
|
|
70
|
+
const childCallable = child.ownedDefs.find((d) => d.type === 'Function' || d.type === 'Method' || d.type === 'Constructor');
|
|
71
|
+
if (childCallable !== undefined)
|
|
72
|
+
return childCallable;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return scope.ownedDefs.find((d) => d.type === 'Function' || d.type === 'Method' || d.type === 'Constructor');
|
|
59
76
|
}
|
|
60
77
|
/**
|
|
61
78
|
* Look up a `SymbolDefinition` in the graph node lookup.
|
|
@@ -106,9 +123,10 @@ export function resolveDefGraphId(filePath, def, nodeLookup) {
|
|
|
106
123
|
// Overload disambiguation: when the def carries parameter types,
|
|
107
124
|
// try the parameter-typed key first so same-name same-arity
|
|
108
125
|
// overloads route to their distinct graph nodes. Constructors are
|
|
109
|
-
// included so a
|
|
110
|
-
//
|
|
111
|
-
//
|
|
126
|
+
// included so a C# `: this(int)` / `: base(int)` chain, a Java
|
|
127
|
+
// `this(int)`/`super(int)` chain, or `new Foo(int)` resolves to the
|
|
128
|
+
// matching ctor overload instead of first-wins collapsing onto
|
|
129
|
+
// another `Foo` ctor (a self-loop) — #1928 F38 / #2046.
|
|
112
130
|
if (isOverloadableCallable(def.type) &&
|
|
113
131
|
def.parameterTypes !== undefined &&
|
|
114
132
|
def.parameterTypes.length > 0) {
|
|
@@ -167,7 +185,7 @@ export function simpleQualifiedName(def) {
|
|
|
167
185
|
* edge source. Matches legacy DAG behavior where module-level CALLS
|
|
168
186
|
* edges originate from the file symbol.
|
|
169
187
|
*/
|
|
170
|
-
export function resolveCallerGraphId(startScope, scopes, nodeLookup) {
|
|
188
|
+
export function resolveCallerGraphId(startScope, scopes, nodeLookup, atRange) {
|
|
171
189
|
let current = startScope;
|
|
172
190
|
const visited = new Set();
|
|
173
191
|
let lastFilePath;
|
|
@@ -182,9 +200,9 @@ export function resolveCallerGraphId(startScope, scopes, nodeLookup) {
|
|
|
182
200
|
// Prefer Function/Method/Constructor anchors; fall back to
|
|
183
201
|
// Class/Interface/Struct/Enum. Variable/Property are NOT valid
|
|
184
202
|
// caller anchors — see `isCallerAnchorLabel` for why.
|
|
185
|
-
const fnDef = scope
|
|
203
|
+
const fnDef = pickCallerCallableDef(scope, scopes, atRange);
|
|
186
204
|
if (fnDef !== undefined) {
|
|
187
|
-
const id = resolveDefGraphId(
|
|
205
|
+
const id = resolveDefGraphId(fnDef.filePath, fnDef, nodeLookup);
|
|
188
206
|
if (id !== undefined)
|
|
189
207
|
return id;
|
|
190
208
|
}
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
* `SymbolDefinition.nodeId` values into the legacy graph-node ID
|
|
18
18
|
* format that downstream consumers (queries, edges, MCP) expect.
|
|
19
19
|
*/
|
|
20
|
+
import { isOverloadableCallable } from '../../utils/callable-labels.js';
|
|
20
21
|
import { templateConstraintsIdTag } from '../../utils/template-arguments.js';
|
|
21
22
|
import { parameterShapeIdTag } from '../../utils/method-props.js';
|
|
22
23
|
/**
|
|
@@ -134,17 +135,6 @@ export function buildGraphNodeLookup(graph) {
|
|
|
134
135
|
}
|
|
135
136
|
return lookup;
|
|
136
137
|
}
|
|
137
|
-
/**
|
|
138
|
-
* Callables whose same-name overloads must route to distinct graph nodes via
|
|
139
|
-
* the parameter-types / shape key. Constructors belong here too: a class with
|
|
140
|
-
* `Foo()` and `Foo(int)` mints distinct `#0`/`#1` Constructor nodes, and a
|
|
141
|
-
* `this(...)`/`super(...)` edge (or any `new Foo(args)`) must reach the right
|
|
142
|
-
* one. Without the overload key both ctor nodes collapse onto the first-wins
|
|
143
|
-
* qualified/simple key, turning a `this()` chain into a self-loop (#1928 F38).
|
|
144
|
-
*/
|
|
145
|
-
function isOverloadableCallable(label) {
|
|
146
|
-
return label === 'Function' || label === 'Method' || label === 'Constructor';
|
|
147
|
-
}
|
|
148
138
|
export function isLinkableLabel(label) {
|
|
149
139
|
return (label === 'Function' ||
|
|
150
140
|
label === 'Method' ||
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* generalization plan.
|
|
18
18
|
*/
|
|
19
19
|
import { resolveCallerGraphId, resolveDefGraphId } from '../graph-bridge/ids.js';
|
|
20
|
-
import { findAllCallableBindingsInScope, findCallableBindingInScope, findCallableBindingsAndAdlBlocker,
|
|
20
|
+
import { findAllCallableBindingsInScope, findCallableBindingInScope, findCallableBindingsAndAdlBlocker, resolveInheritanceBaseInScope, } from '../scope/walkers.js';
|
|
21
21
|
import { isOverloadAmbiguousAfterNormalization, narrowOverloadCandidates, } from './overload-narrowing.js';
|
|
22
22
|
export function emitFreeCallFallback(graph, scopes, parsedFiles, nodeLookup, _referenceIndex, handledSites, model, workspaceIndex, options = {}) {
|
|
23
23
|
let emitted = 0;
|
|
@@ -44,8 +44,8 @@ export function emitFreeCallFallback(graph, scopes, parsedFiles, nodeLookup, _re
|
|
|
44
44
|
// the same two targets; see test expectations.
|
|
45
45
|
let fnDef;
|
|
46
46
|
if (site.callForm === 'constructor') {
|
|
47
|
-
const classDef =
|
|
48
|
-
if (classDef !== undefined) {
|
|
47
|
+
const classDef = resolveInheritanceBaseInScope(site.inScope, site.name, scopes, site.rawQualifiedName);
|
|
48
|
+
if (classDef !== undefined && classDef.type !== 'Interface') {
|
|
49
49
|
// Most languages link `Type(...)` to the explicit Constructor def
|
|
50
50
|
// when one exists (else the Class). Languages that model the call
|
|
51
51
|
// as a reference to the type itself opt into
|
|
@@ -53,7 +53,7 @@ export function emitFreeCallFallback(graph, scopes, parsedFiles, nodeLookup, _re
|
|
|
53
53
|
fnDef =
|
|
54
54
|
options.constructorCallTargetsClass === true
|
|
55
55
|
? classDef
|
|
56
|
-
: pickConstructorOrClass(classDef, workspaceIndex, scopes);
|
|
56
|
+
: pickConstructorOrClass(classDef, workspaceIndex, scopes, site.arity);
|
|
57
57
|
}
|
|
58
58
|
else if (options.allowGlobalFallback === true) {
|
|
59
59
|
// The constructed type may live in a sibling/imported file that is
|
|
@@ -64,9 +64,11 @@ export function emitFreeCallFallback(graph, scopes, parsedFiles, nodeLookup, _re
|
|
|
64
64
|
const globalClass = pickUniqueGlobalClass(site.name, globalClassesBySimpleName);
|
|
65
65
|
if (globalClass !== undefined) {
|
|
66
66
|
fnDef =
|
|
67
|
-
|
|
68
|
-
?
|
|
69
|
-
:
|
|
67
|
+
globalClass.type === 'Interface'
|
|
68
|
+
? undefined
|
|
69
|
+
: options.constructorCallTargetsClass === true
|
|
70
|
+
? globalClass
|
|
71
|
+
: pickConstructorOrClass(globalClass, workspaceIndex, scopes, site.arity);
|
|
70
72
|
}
|
|
71
73
|
}
|
|
72
74
|
}
|
|
@@ -290,7 +292,7 @@ export function emitFreeCallFallback(graph, scopes, parsedFiles, nodeLookup, _re
|
|
|
290
292
|
handledSites.add(siteKey(parsed.filePath, site));
|
|
291
293
|
continue;
|
|
292
294
|
}
|
|
293
|
-
const callerGraphId = resolveCallerGraphId(site.inScope, scopes, nodeLookup);
|
|
295
|
+
const callerGraphId = resolveCallerGraphId(site.inScope, scopes, nodeLookup, site.atRange);
|
|
294
296
|
if (callerGraphId === undefined)
|
|
295
297
|
continue;
|
|
296
298
|
const tgtGraphId = resolveDefGraphId(fnDef.filePath, fnDef, nodeLookup);
|
|
@@ -529,13 +531,14 @@ function logicalCallableKey(def) {
|
|
|
529
531
|
* Class def itself when no explicit Constructor exists. Matches
|
|
530
532
|
* legacy behavior — tests assert targetLabel === 'Class' for implicit
|
|
531
533
|
* ctors and targetLabel === 'Constructor' for explicit ones. */
|
|
532
|
-
function pickConstructorOrClass(classDef, workspaceIndex, scopes) {
|
|
534
|
+
function pickConstructorOrClass(classDef, workspaceIndex, scopes, callArity) {
|
|
533
535
|
const classScope = workspaceIndex.classScopeByDefId.get(classDef.nodeId);
|
|
534
536
|
if (classScope === undefined)
|
|
535
537
|
return classDef;
|
|
538
|
+
const ctors = [];
|
|
536
539
|
for (const def of classScope.ownedDefs) {
|
|
537
540
|
if (def.type === 'Constructor')
|
|
538
|
-
|
|
541
|
+
ctors.push(def);
|
|
539
542
|
}
|
|
540
543
|
if (scopes !== undefined) {
|
|
541
544
|
for (const childId of scopes.scopeTree.getChildren(classScope.id)) {
|
|
@@ -544,11 +547,18 @@ function pickConstructorOrClass(classDef, workspaceIndex, scopes) {
|
|
|
544
547
|
continue;
|
|
545
548
|
for (const def of childScope.ownedDefs) {
|
|
546
549
|
if (def.type === 'Constructor')
|
|
547
|
-
|
|
550
|
+
ctors.push(def);
|
|
548
551
|
}
|
|
549
552
|
}
|
|
550
553
|
}
|
|
551
|
-
|
|
554
|
+
if (ctors.length === 0)
|
|
555
|
+
return classDef;
|
|
556
|
+
if (callArity !== undefined) {
|
|
557
|
+
const narrowed = narrowByArity(ctors, callArity);
|
|
558
|
+
if (narrowed !== undefined)
|
|
559
|
+
return narrowed;
|
|
560
|
+
}
|
|
561
|
+
return ctors[0];
|
|
552
562
|
}
|
|
553
563
|
/** Find a unique workspace-wide class-like def by simple name, for a
|
|
554
564
|
* constructor-form call `Type(...)` whose type lives outside the call
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* "resolve member on owner with MRO" pattern. All four are reusable
|
|
20
20
|
* as-is for TypeScript, Java, Kotlin, Ruby, etc.
|
|
21
21
|
*/
|
|
22
|
-
import { normalizeQualifiedName } from '../../utils/qualified-name.js';
|
|
22
|
+
import { normalizeQualifiedName, splitQualifiedName, stripTrailingTypeArguments, } from '../../utils/qualified-name.js';
|
|
23
23
|
const EMPTY_BINDINGS = Object.freeze([]);
|
|
24
24
|
/**
|
|
25
25
|
* Look up binding refs at `scopeId` for `name`, consulting both the
|
|
@@ -321,7 +321,7 @@ export function resolveInheritanceBaseInScope(startScope, baseName, scopes, rawQ
|
|
|
321
321
|
* wrong EXTENDS edge silently corrupts impact analysis).
|
|
322
322
|
*/
|
|
323
323
|
function resolveQualifiedInheritanceBase(startScope, rawQualifiedName, scopes, enclosingClassDef) {
|
|
324
|
-
const normalized = normalizeQualifiedName(rawQualifiedName);
|
|
324
|
+
const normalized = stripTrailingTypeArguments(normalizeQualifiedName(rawQualifiedName));
|
|
325
325
|
// No qualifier after normalization → nothing the simple-tail walk doesn't do.
|
|
326
326
|
if (normalized.length === 0 || !normalized.includes('.'))
|
|
327
327
|
return undefined;
|
|
@@ -333,12 +333,25 @@ function resolveQualifiedInheritanceBase(startScope, rawQualifiedName, scopes, e
|
|
|
333
333
|
const enclosing = isRootAnchored
|
|
334
334
|
? []
|
|
335
335
|
: enclosingScopeSegments(startScope, scopes, enclosingClassDef);
|
|
336
|
-
// Candidate keys: longest enclosing prefix first
|
|
336
|
+
// Candidate keys: longest enclosing prefix first for *relative* qualified
|
|
337
|
+
// bases (`Outer.Inner` inside `NS.Outer.Derived` → `NS.Outer.Inner`). When the
|
|
338
|
+
// qualifier names a *different* namespace than the enclosing scope (`new B.Foo()`
|
|
339
|
+
// inside `namespace A` → `B.Foo`, not `A.Foo`), try the raw normalized key
|
|
340
|
+
// FIRST so same-tail local bindings don't win (#2046 / #1991).
|
|
341
|
+
const normParts = splitQualifiedName(normalized);
|
|
342
|
+
const isRelativeToEnclosing = enclosing.length > 0 &&
|
|
343
|
+
normParts.length > 0 &&
|
|
344
|
+
normParts[0] === enclosing[enclosing.length - 1];
|
|
337
345
|
const keys = [];
|
|
346
|
+
if (!isRelativeToEnclosing) {
|
|
347
|
+
keys.push(normalized);
|
|
348
|
+
}
|
|
338
349
|
for (let i = enclosing.length; i >= 1; i--) {
|
|
339
350
|
keys.push([...enclosing.slice(0, i), normalized].join('.'));
|
|
340
351
|
}
|
|
341
|
-
keys.
|
|
352
|
+
if (!keys.includes(normalized)) {
|
|
353
|
+
keys.push(normalized);
|
|
354
|
+
}
|
|
342
355
|
for (const key of keys) {
|
|
343
356
|
const ids = scopes.qualifiedNames.get(key);
|
|
344
357
|
if (ids.length === 0)
|
|
@@ -377,6 +390,33 @@ function resolveQualifiedInheritanceBase(startScope, rawQualifiedName, scopes, e
|
|
|
377
390
|
return undefined; // genuine tie → refuse, don't guess
|
|
378
391
|
}
|
|
379
392
|
}
|
|
393
|
+
// Qualifier-vs-sidecar fallback (#2046). Languages whose class `qualifiedName`
|
|
394
|
+
// is the SIMPLE name (C#) never populate a qualified key in the index, so the
|
|
395
|
+
// keyed loop above can't see `B.Foo`. Resolve the simple TAIL and break the
|
|
396
|
+
// same-tail collision by matching the explicit qualifier (`B`) against each
|
|
397
|
+
// candidate's `namespacePrefix` sidecar. Commit only on a unique match — a
|
|
398
|
+
// still-ambiguous qualifier refuses (never guesses a wrong EXTENDS/CALLS edge).
|
|
399
|
+
const tail = normParts[normParts.length - 1];
|
|
400
|
+
const qualifier = normParts.slice(0, -1).join('.');
|
|
401
|
+
if (tail !== undefined && qualifier.length > 0) {
|
|
402
|
+
const tailIds = scopes.qualifiedNames.get(tail);
|
|
403
|
+
let qUnique;
|
|
404
|
+
let qCount = 0;
|
|
405
|
+
for (const id of tailIds) {
|
|
406
|
+
const def = scopes.defs.get(id);
|
|
407
|
+
if (def === undefined || !isClassLike(def.type))
|
|
408
|
+
continue;
|
|
409
|
+
const np = def.namespacePrefix;
|
|
410
|
+
if (np === undefined || np.length === 0)
|
|
411
|
+
continue;
|
|
412
|
+
if (np === qualifier || np.endsWith(`.${qualifier}`)) {
|
|
413
|
+
qUnique = def;
|
|
414
|
+
qCount++;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
if (qCount === 1)
|
|
418
|
+
return qUnique;
|
|
419
|
+
}
|
|
380
420
|
return undefined;
|
|
381
421
|
}
|
|
382
422
|
/**
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { NodeLabel } from '../../../_shared/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* Callables whose same-name overloads occupy distinct graph nodes keyed by
|
|
4
|
+
* parameter types / shape. Shared by graph-bridge registration (`node-lookup.ts`)
|
|
5
|
+
* and resolution (`ids.ts`) so overload keys stay aligned.
|
|
6
|
+
*
|
|
7
|
+
* Constructors belong here: a class with `Foo()` and `Foo(int)` mints distinct
|
|
8
|
+
* `#0`/`#1` Constructor nodes, and a `: this(...)` / `: base(...)` (C#),
|
|
9
|
+
* `this(...)`/`super(...)` (Java), or `new Foo(args)` edge must reach the
|
|
10
|
+
* right overload — otherwise both ctor nodes collapse onto the first-wins
|
|
11
|
+
* qualified/simple key and a ctor chain becomes a self-loop (#1928 F38 / #2046).
|
|
12
|
+
*/
|
|
13
|
+
export declare function isOverloadableCallable(label: NodeLabel | undefined): boolean;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Callables whose same-name overloads occupy distinct graph nodes keyed by
|
|
3
|
+
* parameter types / shape. Shared by graph-bridge registration (`node-lookup.ts`)
|
|
4
|
+
* and resolution (`ids.ts`) so overload keys stay aligned.
|
|
5
|
+
*
|
|
6
|
+
* Constructors belong here: a class with `Foo()` and `Foo(int)` mints distinct
|
|
7
|
+
* `#0`/`#1` Constructor nodes, and a `: this(...)` / `: base(...)` (C#),
|
|
8
|
+
* `this(...)`/`super(...)` (Java), or `new Foo(args)` edge must reach the
|
|
9
|
+
* right overload — otherwise both ctor nodes collapse onto the first-wins
|
|
10
|
+
* qualified/simple key and a ctor chain becomes a self-loop (#1928 F38 / #2046).
|
|
11
|
+
*/
|
|
12
|
+
export function isOverloadableCallable(label) {
|
|
13
|
+
return label === 'Function' || label === 'Method' || label === 'Constructor';
|
|
14
|
+
}
|
|
@@ -29,3 +29,9 @@ export declare const normalizeQualifiedName: (value: string) => string;
|
|
|
29
29
|
* separator-only input.
|
|
30
30
|
*/
|
|
31
31
|
export declare const splitQualifiedName: (value: string) => string[];
|
|
32
|
+
/**
|
|
33
|
+
* Strip a trailing generic argument list at angle-bracket depth 0
|
|
34
|
+
* (`Models.Box<int>` → `Models.Box`). Constructor and inheritance sites
|
|
35
|
+
* often carry type arguments that are not part of the indexed def key.
|
|
36
|
+
*/
|
|
37
|
+
export declare function stripTrailingTypeArguments(value: string): string;
|
|
@@ -38,3 +38,30 @@ export const splitQualifiedName = (value) => {
|
|
|
38
38
|
const normalized = normalizeQualifiedName(value);
|
|
39
39
|
return normalized ? normalized.split('.').filter(Boolean) : [];
|
|
40
40
|
};
|
|
41
|
+
/**
|
|
42
|
+
* Strip a trailing generic argument list at angle-bracket depth 0
|
|
43
|
+
* (`Models.Box<int>` → `Models.Box`). Constructor and inheritance sites
|
|
44
|
+
* often carry type arguments that are not part of the indexed def key.
|
|
45
|
+
*/
|
|
46
|
+
export function stripTrailingTypeArguments(value) {
|
|
47
|
+
let depth = 0;
|
|
48
|
+
let ltAt0 = -1;
|
|
49
|
+
for (let i = 0; i < value.length; i++) {
|
|
50
|
+
const c = value[i];
|
|
51
|
+
if (c === '<') {
|
|
52
|
+
if (depth === 0)
|
|
53
|
+
ltAt0 = i;
|
|
54
|
+
depth++;
|
|
55
|
+
}
|
|
56
|
+
else if (c === '>') {
|
|
57
|
+
depth = Math.max(0, depth - 1);
|
|
58
|
+
if (depth === 0 && ltAt0 >= 0) {
|
|
59
|
+
const after = value.slice(i + 1).trim();
|
|
60
|
+
if (after.length === 0)
|
|
61
|
+
return value.slice(0, ltAt0);
|
|
62
|
+
ltAt0 = -1;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return value;
|
|
67
|
+
}
|
package/package.json
CHANGED