gitnexus 1.6.6-rc.90 → 1.6.6-rc.92
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/group/extractors/http-patterns/python.js +5 -3
- package/dist/core/ingestion/languages/csharp/namespace-siblings.d.ts +29 -1
- package/dist/core/ingestion/languages/csharp/namespace-siblings.js +300 -28
- package/dist/core/ingestion/method-extractors/configs/c-cpp.js +4 -0
- package/dist/core/ingestion/method-types.d.ts +2 -1
- package/dist/core/ingestion/model/scope-resolution-indexes.d.ts +9 -5
- package/dist/core/ingestion/parsing-processor.js +5 -2
- package/dist/core/ingestion/scope-resolution/graph-bridge/ids.d.ts +2 -1
- package/dist/core/ingestion/scope-resolution/graph-bridge/ids.js +13 -1
- package/dist/core/ingestion/scope-resolution/graph-bridge/node-lookup.js +22 -6
- package/dist/core/ingestion/scope-resolution/passes/overload-narrowing.d.ts +5 -0
- package/dist/core/ingestion/scope-resolution/passes/overload-narrowing.js +114 -0
- package/dist/core/ingestion/scope-resolution/pipeline/validate-bindings-immutability.d.ts +2 -2
- package/dist/core/ingestion/scope-resolution/pipeline/validate-bindings-immutability.js +15 -2
- package/dist/core/ingestion/scope-resolution/scope/walkers.d.ts +8 -0
- package/dist/core/ingestion/scope-resolution/scope/walkers.js +8 -0
- package/dist/core/ingestion/utils/method-props.d.ts +11 -1
- package/dist/core/ingestion/utils/method-props.js +42 -0
- package/dist/core/ingestion/workers/parse-worker.js +5 -2
- package/package.json +1 -1
|
@@ -538,8 +538,9 @@ const HTTPX_ASYNC_CLIENT_GENERIC_PATTERNS = compilePatterns({
|
|
|
538
538
|
});
|
|
539
539
|
/** Strip `.py` and return the bare basename (e.g. `api/users.py` → `users`). */
|
|
540
540
|
function fileShortKey(rel) {
|
|
541
|
-
const
|
|
542
|
-
const
|
|
541
|
+
const normalized = rel.replace(/\\/g, '/');
|
|
542
|
+
const slash = normalized.lastIndexOf('/');
|
|
543
|
+
const file = slash >= 0 ? normalized.slice(slash + 1) : normalized;
|
|
543
544
|
return file.endsWith('.py') ? file.slice(0, -3) : file;
|
|
544
545
|
}
|
|
545
546
|
/**
|
|
@@ -548,7 +549,8 @@ function fileShortKey(rel) {
|
|
|
548
549
|
* case callers should fall back to the short key.
|
|
549
550
|
*/
|
|
550
551
|
function fileLongKey(rel) {
|
|
551
|
-
const
|
|
552
|
+
const normalized = rel.replace(/\\/g, '/');
|
|
553
|
+
const noExt = normalized.endsWith('.py') ? normalized.slice(0, -3) : normalized;
|
|
552
554
|
const lastSlash = noExt.lastIndexOf('/');
|
|
553
555
|
if (lastSlash < 0)
|
|
554
556
|
return '';
|
|
@@ -28,10 +28,38 @@
|
|
|
28
28
|
* aliased `using static X = Y.Z;`, attributed namespace declarations,
|
|
29
29
|
* and preprocessor-guarded declarations correctly because the
|
|
30
30
|
* tree-sitter grammar parses them as real nodes (not textual
|
|
31
|
-
* coincidences).
|
|
31
|
+
* coincidences). When the orchestrator's `treeCache` has no Tree for a
|
|
32
|
+
* file — the worker path, where native Trees can't cross MessageChannels
|
|
33
|
+
* — `extractFileStructure` falls back to a line scanner rather than
|
|
34
|
+
* re-parsing every file from scratch (that re-parse dominated worker-mode
|
|
35
|
+
* scope-resolution time). See `extractCsharpStructureViaScanner`.
|
|
32
36
|
*/
|
|
33
37
|
import type { ParsedFile } from '../../../../_shared/index.js';
|
|
34
38
|
import type { ScopeResolutionIndexes } from '../../model/scope-resolution-indexes.js';
|
|
39
|
+
export interface CsharpFileStructure {
|
|
40
|
+
/** Declared namespace names in file source order. Empty array means
|
|
41
|
+
* the file has no `namespace X;` / `namespace X { }` declaration
|
|
42
|
+
* and sits in the default (global) namespace. */
|
|
43
|
+
readonly namespaces: readonly string[];
|
|
44
|
+
/** Dotted paths from `using static X.Y.Z;` (including
|
|
45
|
+
* `global using static` and aliased `using static A = X.Y.Z;`). */
|
|
46
|
+
readonly usingStaticPaths: readonly string[];
|
|
47
|
+
}
|
|
48
|
+
/** Line-scanner used when no cached tree is available (worker-parsed files
|
|
49
|
+
* can't transfer native tree-sitter Trees across MessageChannels, so
|
|
50
|
+
* `treeCache` is empty for them). Re-parsing every C# file here with
|
|
51
|
+
* tree-sitter was the dominant scope-resolution cost on large worker-mode
|
|
52
|
+
* runs — for a multi-thousand-file solution this loop alone re-parsed the
|
|
53
|
+
* whole repo a second time. The scanner extracts the same `namespaces` /
|
|
54
|
+
* `usingStaticPaths` the AST walk produces for line-anchored declarations,
|
|
55
|
+
* while tracking block-comment and string state across lines (via
|
|
56
|
+
* `advanceCsScanState`) so a `namespace` / `using static` keyword at the
|
|
57
|
+
* start of a line inside a block comment, verbatim string, or raw string
|
|
58
|
+
* literal is NOT mistaken for a declaration. The remaining trade-off vs the
|
|
59
|
+
* AST is a declaration whose keyword is not at the start of a code line
|
|
60
|
+
* (split across lines, or sharing a line with a comment/string closer).
|
|
61
|
+
* Mirrors PHP's `extractNamespaceViaScanner` (issue #1741). */
|
|
62
|
+
export declare function extractCsharpStructureViaScanner(content: string): CsharpFileStructure;
|
|
35
63
|
/** Content + (optional) pre-parsed tree-sitter trees keyed by filePath.
|
|
36
64
|
* The orchestrator builds `fileContents` from the pipeline's file list;
|
|
37
65
|
* `treeCache` is the same `scopeTreeCache` already populated by the
|
|
@@ -28,21 +28,188 @@
|
|
|
28
28
|
* aliased `using static X = Y.Z;`, attributed namespace declarations,
|
|
29
29
|
* and preprocessor-guarded declarations correctly because the
|
|
30
30
|
* tree-sitter grammar parses them as real nodes (not textual
|
|
31
|
-
* coincidences).
|
|
31
|
+
* coincidences). When the orchestrator's `treeCache` has no Tree for a
|
|
32
|
+
* file — the worker path, where native Trees can't cross MessageChannels
|
|
33
|
+
* — `extractFileStructure` falls back to a line scanner rather than
|
|
34
|
+
* re-parsing every file from scratch (that re-parse dominated worker-mode
|
|
35
|
+
* scope-resolution time). See `extractCsharpStructureViaScanner`.
|
|
32
36
|
*/
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
37
|
+
// Line-anchored matchers for the worker-path fallback (see
|
|
38
|
+
// `extractCsharpStructureViaScanner`). Anchored at line start (after
|
|
39
|
+
// indentation); the scanner additionally tracks block-comment / string
|
|
40
|
+
// state across lines so a keyword at the start of a line inside one of
|
|
41
|
+
// those regions is skipped.
|
|
42
|
+
const CS_NAMESPACE_RE = /^[ \t]*namespace[ \t]+([A-Za-z_@][A-Za-z0-9_.]*)/;
|
|
43
|
+
// `global using static`, plain `using static`, and the aliased
|
|
44
|
+
// `using static Alias = NS.Type;` form (the AST keeps the RHS path, so
|
|
45
|
+
// the optional `Alias =` is skipped and only the dotted path captured).
|
|
46
|
+
const CS_USING_STATIC_RE = /^[ \t]*(?:global[ \t]+)?using[ \t]+static[ \t]+(?:[A-Za-z_@][A-Za-z0-9_]*[ \t]*=[ \t]*)?([A-Za-z_@][A-Za-z0-9_.]*)/;
|
|
47
|
+
/** Advance the scanner's lexical state across one line, consuming block
|
|
48
|
+
* comments (slash-star), line comments (`//`), single-line regular /
|
|
49
|
+
* interpolated strings, verbatim strings (`@"…"`), and raw string literals
|
|
50
|
+
* (`"""…"""`, fence length tracked in `rawFence`). Returns the state and
|
|
51
|
+
* raw-fence length in effect at the START of the next line. Single-line
|
|
52
|
+
* strings and `//` comments resolve back to `code` before end of line; only
|
|
53
|
+
* block comments and multi-line strings carry state forward. */
|
|
54
|
+
function advanceCsScanState(line, state, rawFence) {
|
|
55
|
+
const n = line.length;
|
|
56
|
+
let i = 0;
|
|
57
|
+
while (i < n) {
|
|
58
|
+
if (state === 'block') {
|
|
59
|
+
const end = line.indexOf('*/', i);
|
|
60
|
+
if (end === -1)
|
|
61
|
+
return ['block', rawFence];
|
|
62
|
+
i = end + 2;
|
|
63
|
+
state = 'code';
|
|
64
|
+
}
|
|
65
|
+
else if (state === 'verbatim') {
|
|
66
|
+
// Ends at a `"` that is not doubled (`""` is an escaped quote).
|
|
67
|
+
while (i < n) {
|
|
68
|
+
if (line[i] === '"') {
|
|
69
|
+
if (line[i + 1] === '"') {
|
|
70
|
+
i += 2;
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
i++;
|
|
76
|
+
}
|
|
77
|
+
if (i >= n)
|
|
78
|
+
return ['verbatim', rawFence];
|
|
79
|
+
i += 1;
|
|
80
|
+
state = 'code';
|
|
81
|
+
}
|
|
82
|
+
else if (state === 'raw') {
|
|
83
|
+
// Ends at a run of `"` at least `rawFence` long.
|
|
84
|
+
let closed = false;
|
|
85
|
+
while (i < n) {
|
|
86
|
+
if (line[i] === '"') {
|
|
87
|
+
let k = i;
|
|
88
|
+
while (k < n && line[k] === '"')
|
|
89
|
+
k++;
|
|
90
|
+
if (k - i >= rawFence) {
|
|
91
|
+
i = k;
|
|
92
|
+
state = 'code';
|
|
93
|
+
rawFence = 0;
|
|
94
|
+
closed = true;
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
i = k;
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
i++;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (!closed)
|
|
104
|
+
return ['raw', rawFence];
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
const c = line[i];
|
|
108
|
+
const next = line[i + 1];
|
|
109
|
+
if (c === '/' && next === '/')
|
|
110
|
+
return ['code', rawFence]; // line comment to EOL
|
|
111
|
+
if (c === '/' && next === '*') {
|
|
112
|
+
state = 'block';
|
|
113
|
+
i += 2;
|
|
114
|
+
}
|
|
115
|
+
else if (c === '@' && next === '"') {
|
|
116
|
+
state = 'verbatim';
|
|
117
|
+
i += 2;
|
|
118
|
+
}
|
|
119
|
+
else if ((c === '$' && next === '@') || (c === '@' && next === '$')) {
|
|
120
|
+
if (line[i + 2] === '"') {
|
|
121
|
+
state = 'verbatim'; // interpolated verbatim ($@"…" / @$"…")
|
|
122
|
+
i += 3;
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
i++;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
else if (c === '"') {
|
|
129
|
+
let k = i;
|
|
130
|
+
while (k < n && line[k] === '"')
|
|
131
|
+
k++;
|
|
132
|
+
const run = k - i;
|
|
133
|
+
if (run >= 3) {
|
|
134
|
+
state = 'raw';
|
|
135
|
+
rawFence = run;
|
|
136
|
+
i = k;
|
|
137
|
+
}
|
|
138
|
+
else if (run === 2) {
|
|
139
|
+
i = k; // "" — empty string
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
// single-line regular / interpolated string; consume to closer
|
|
143
|
+
let j = i + 1;
|
|
144
|
+
while (j < n) {
|
|
145
|
+
if (line[j] === '\\') {
|
|
146
|
+
j += 2;
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
if (line[j] === '"')
|
|
150
|
+
break;
|
|
151
|
+
j++;
|
|
152
|
+
}
|
|
153
|
+
i = j >= n ? n : j + 1;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
i++;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return [state, rawFence];
|
|
162
|
+
}
|
|
163
|
+
/** Line-scanner used when no cached tree is available (worker-parsed files
|
|
164
|
+
* can't transfer native tree-sitter Trees across MessageChannels, so
|
|
165
|
+
* `treeCache` is empty for them). Re-parsing every C# file here with
|
|
166
|
+
* tree-sitter was the dominant scope-resolution cost on large worker-mode
|
|
167
|
+
* runs — for a multi-thousand-file solution this loop alone re-parsed the
|
|
168
|
+
* whole repo a second time. The scanner extracts the same `namespaces` /
|
|
169
|
+
* `usingStaticPaths` the AST walk produces for line-anchored declarations,
|
|
170
|
+
* while tracking block-comment and string state across lines (via
|
|
171
|
+
* `advanceCsScanState`) so a `namespace` / `using static` keyword at the
|
|
172
|
+
* start of a line inside a block comment, verbatim string, or raw string
|
|
173
|
+
* literal is NOT mistaken for a declaration. The remaining trade-off vs the
|
|
174
|
+
* AST is a declaration whose keyword is not at the start of a code line
|
|
175
|
+
* (split across lines, or sharing a line with a comment/string closer).
|
|
176
|
+
* Mirrors PHP's `extractNamespaceViaScanner` (issue #1741). */
|
|
177
|
+
export function extractCsharpStructureViaScanner(content) {
|
|
178
|
+
const namespaces = [];
|
|
179
|
+
const usingStaticPaths = [];
|
|
180
|
+
let state = 'code';
|
|
181
|
+
let rawFence = 0;
|
|
182
|
+
for (const line of content.split('\n')) {
|
|
183
|
+
// Only match when the line START is real code — keywords reached while
|
|
184
|
+
// inside a block comment / multi-line string are skipped.
|
|
185
|
+
if (state === 'code') {
|
|
186
|
+
const ns = CS_NAMESPACE_RE.exec(line);
|
|
187
|
+
if (ns !== null) {
|
|
188
|
+
namespaces.push(ns[1]);
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
const us = CS_USING_STATIC_RE.exec(line);
|
|
192
|
+
if (us !== null)
|
|
193
|
+
usingStaticPaths.push(us[1]);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
[state, rawFence] = advanceCsScanState(line, state, rawFence);
|
|
197
|
+
}
|
|
198
|
+
return { namespaces, usingStaticPaths };
|
|
199
|
+
}
|
|
200
|
+
/** Build a structural view of a C# file. Prefers `cachedTree` (handed in
|
|
201
|
+
* via `treeCache`) and walks the tree-sitter AST — the authoritative
|
|
202
|
+
* path that sees `global using static`, aliased `using static X = Y.Z;`,
|
|
203
|
+
* attributed namespace declarations, and preprocessor-guarded nodes
|
|
204
|
+
* correctly. On cache miss (worker-parsed files, whose native Trees
|
|
205
|
+
* can't cross MessageChannels) it falls back to the line scanner instead
|
|
206
|
+
* of a fresh tree-sitter parse — the parse here dominated worker-mode
|
|
207
|
+
* scope-resolution time. Parser singleton is shared across calls. */
|
|
41
208
|
function extractFileStructure(content, cachedTree) {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
209
|
+
if (!cachedTree) {
|
|
210
|
+
return extractCsharpStructureViaScanner(content);
|
|
211
|
+
}
|
|
212
|
+
const tree = cachedTree;
|
|
46
213
|
const namespaces = [];
|
|
47
214
|
const usingStaticPaths = [];
|
|
48
215
|
const visit = (node) => {
|
|
@@ -238,6 +405,9 @@ export function populateCsharpNamespaceSiblings(parsedFiles, indexes, inputs) {
|
|
|
238
405
|
// scope, so `Record(...)` (without `Logger.` qualifier) resolves
|
|
239
406
|
// to `Logger.Record`. AST walk above captured these (including
|
|
240
407
|
// `global using static` and aliased forms).
|
|
408
|
+
// Pre-index files by path once: the member-injection lookup below would
|
|
409
|
+
// otherwise be an O(files) scan per `using static` import.
|
|
410
|
+
const fileByPath = new Map(parsedFiles.map((p) => [p.filePath, p]));
|
|
241
411
|
for (const parsed of parsedFiles) {
|
|
242
412
|
const struct = structureByFile.get(parsed.filePath);
|
|
243
413
|
if (struct === undefined)
|
|
@@ -245,6 +415,9 @@ export function populateCsharpNamespaceSiblings(parsedFiles, indexes, inputs) {
|
|
|
245
415
|
const moduleScope = parsed.scopes.find((s) => s.kind === 'Module');
|
|
246
416
|
if (moduleScope === undefined)
|
|
247
417
|
continue;
|
|
418
|
+
// Per-file de-dup sets keyed by simple name, seeded lazily from the
|
|
419
|
+
// augmentation bucket — replaces the per-member O(A) `.some` scan below.
|
|
420
|
+
const seenByName = new Map();
|
|
248
421
|
for (const fullPath of struct.usingStaticPaths) {
|
|
249
422
|
const lastDot = fullPath.lastIndexOf('.');
|
|
250
423
|
if (lastDot === -1)
|
|
@@ -265,7 +438,7 @@ export function populateCsharpNamespaceSiblings(parsedFiles, indexes, inputs) {
|
|
|
265
438
|
// Inject the class's member methods into the importer's module
|
|
266
439
|
// scope. `memberByOwner` wasn't built yet here, so we walk the
|
|
267
440
|
// file's localDefs to find members with `ownerId === targetDef.nodeId`.
|
|
268
|
-
const targetFile =
|
|
441
|
+
const targetFile = fileByPath.get(targetDef.filePath);
|
|
269
442
|
if (targetFile === undefined)
|
|
270
443
|
continue;
|
|
271
444
|
for (const memberDef of targetFile.localDefs) {
|
|
@@ -282,8 +455,16 @@ export function populateCsharpNamespaceSiblings(parsedFiles, indexes, inputs) {
|
|
|
282
455
|
// `lookupBindingsAt`, which fans out across `bindings` +
|
|
283
456
|
// `bindingAugmentations`.
|
|
284
457
|
const bucketArr = getAugmentationBucket(augmentations, moduleScope.id, simpleName);
|
|
285
|
-
|
|
458
|
+
let seen = seenByName.get(simpleName);
|
|
459
|
+
if (seen === undefined) {
|
|
460
|
+
seen = new Set();
|
|
461
|
+
for (const b of bucketArr)
|
|
462
|
+
seen.add(b.def.nodeId);
|
|
463
|
+
seenByName.set(simpleName, seen);
|
|
464
|
+
}
|
|
465
|
+
if (seen.has(memberDef.nodeId))
|
|
286
466
|
continue;
|
|
467
|
+
seen.add(memberDef.nodeId);
|
|
287
468
|
bucketArr.push({ def: memberDef, origin: 'import' });
|
|
288
469
|
}
|
|
289
470
|
}
|
|
@@ -299,6 +480,9 @@ export function populateCsharpNamespaceSiblings(parsedFiles, indexes, inputs) {
|
|
|
299
480
|
const moduleScope = parsed.scopes.find((s) => s.kind === 'Module');
|
|
300
481
|
if (moduleScope === undefined)
|
|
301
482
|
continue;
|
|
483
|
+
// Per-file de-dup sets keyed by simple name, seeded lazily from the
|
|
484
|
+
// augmentation bucket — replaces the per-def O(A) `.some` scan below.
|
|
485
|
+
const seenByName = new Map();
|
|
302
486
|
for (const imp of parsed.parsedImports) {
|
|
303
487
|
if (imp.kind !== 'namespace')
|
|
304
488
|
continue;
|
|
@@ -316,16 +500,35 @@ export function populateCsharpNamespaceSiblings(parsedFiles, indexes, inputs) {
|
|
|
316
500
|
if (simpleName === '')
|
|
317
501
|
continue;
|
|
318
502
|
const bucketArr = getAugmentationBucket(augmentations, moduleScope.id, simpleName);
|
|
319
|
-
|
|
503
|
+
let seen = seenByName.get(simpleName);
|
|
504
|
+
if (seen === undefined) {
|
|
505
|
+
seen = new Set();
|
|
506
|
+
for (const b of bucketArr)
|
|
507
|
+
seen.add(b.def.nodeId);
|
|
508
|
+
seenByName.set(simpleName, seen);
|
|
509
|
+
}
|
|
510
|
+
if (seen.has(def.nodeId))
|
|
320
511
|
continue;
|
|
512
|
+
seen.add(def.nodeId);
|
|
321
513
|
bucketArr.push({ def, origin: 'namespace' });
|
|
322
514
|
}
|
|
323
515
|
}
|
|
324
516
|
}
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
517
|
+
// Workspace-level binding channel for global-namespace types (see the
|
|
518
|
+
// global fast-path below). `lookupBindingsAt` consults this as a third
|
|
519
|
+
// source after finalized + per-scope augmented bindings. Its inner arrays
|
|
520
|
+
// are mutable by contract (append-only, like `bindingAugmentations` — see
|
|
521
|
+
// the ScopeResolutionIndexes doc + validateBindingsImmutability), so the
|
|
522
|
+
// ReadonlyMap→Map cast is localized to this one line and all writes go
|
|
523
|
+
// through `getWorkspaceBucket`.
|
|
524
|
+
const workspace = indexes.workspaceFqnBindings;
|
|
525
|
+
for (const [nsName, bucket] of buckets) {
|
|
526
|
+
// Group sibling defs by simple name. Append in place — the previous
|
|
527
|
+
// `[...prev, def]` copy made this O(D²) per bucket, which on the
|
|
528
|
+
// global (`''`) namespace bucket of a large Unity solution (tens of
|
|
529
|
+
// thousands of type defs) was a primary slowness/OOM source. We keep
|
|
530
|
+
// every declaration (e.g. partial classes across files) and leave
|
|
531
|
+
// de-dup to downstream consumers.
|
|
329
532
|
const defsByName = new Map();
|
|
330
533
|
for (const def of bucket.classDefs) {
|
|
331
534
|
// Simple name = last segment of qualifiedName (e.g. `App.User` → `User`).
|
|
@@ -333,27 +536,81 @@ export function populateCsharpNamespaceSiblings(parsedFiles, indexes, inputs) {
|
|
|
333
536
|
const key = q.includes('.') ? q.slice(q.lastIndexOf('.') + 1) : q;
|
|
334
537
|
if (key === '')
|
|
335
538
|
continue;
|
|
336
|
-
|
|
539
|
+
let arr = defsByName.get(key);
|
|
540
|
+
if (arr === undefined) {
|
|
541
|
+
arr = [];
|
|
542
|
+
defsByName.set(key, arr);
|
|
543
|
+
}
|
|
337
544
|
arr.push(def);
|
|
338
|
-
|
|
545
|
+
}
|
|
546
|
+
// Global-namespace fast path (Unity OOM guard). Types declared in the
|
|
547
|
+
// default (global) namespace are visible from EVERY file in C# — the
|
|
548
|
+
// global namespace is always implicitly in scope — so one workspace-
|
|
549
|
+
// level entry per simple name is both semantically correct and O(D)
|
|
550
|
+
// instead of the O(S·D) per-scope augmentation that materialized
|
|
551
|
+
// billions of BindingRefs on large Unity solutions (tens of thousands
|
|
552
|
+
// of global types × tens of thousands of scopes). `walkScopeChain`
|
|
553
|
+
// checks local `scope.bindings` first, so local declarations still
|
|
554
|
+
// shadow these workspace entries; a file resolving its own global type
|
|
555
|
+
// hits the local binding before this map. Dedup by `def.nodeId` keeps
|
|
556
|
+
// partial-class / duplicate declarations from double-emitting.
|
|
557
|
+
if (nsName === '') {
|
|
558
|
+
for (const [name, defs] of defsByName) {
|
|
559
|
+
const bucket = getWorkspaceBucket(workspace, name);
|
|
560
|
+
const seen = new Set();
|
|
561
|
+
for (const b of bucket)
|
|
562
|
+
seen.add(b.def.nodeId);
|
|
563
|
+
for (const def of defs) {
|
|
564
|
+
if (seen.has(def.nodeId))
|
|
565
|
+
continue; // dedup by nodeId (keeps partials, drops re-emits)
|
|
566
|
+
seen.add(def.nodeId);
|
|
567
|
+
bucket.push({ def, origin: 'namespace' });
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
continue;
|
|
571
|
+
}
|
|
572
|
+
// Pre-index the first scope per file once (O(S)) instead of an
|
|
573
|
+
// O(S) `.find` re-run for every (scope, name) pair, which made the
|
|
574
|
+
// injection loop O(S²·D) and was the dominant cost on large buckets.
|
|
575
|
+
// Multiple scopes share a filePath (Module + Namespace); the local
|
|
576
|
+
// shadow check only needs that file's lexical `Scope.bindings`, which
|
|
577
|
+
// is identical regardless of which of those scopes we read.
|
|
578
|
+
const firstScopeByFile = new Map();
|
|
579
|
+
for (const s of bucket.scopes) {
|
|
580
|
+
if (!firstScopeByFile.has(s.filePath))
|
|
581
|
+
firstScopeByFile.set(s.filePath, s.scope);
|
|
339
582
|
}
|
|
340
583
|
for (const { scopeId, filePath } of bucket.scopes) {
|
|
584
|
+
const localScope = firstScopeByFile.get(filePath);
|
|
341
585
|
for (const [name, defs] of defsByName) {
|
|
342
586
|
// Skip names already present locally — `origin: 'local'` in
|
|
343
587
|
// scope.bindings would naturally shadow the cross-file
|
|
344
588
|
// namespace entry, but we also keep this index lean.
|
|
345
|
-
const local =
|
|
589
|
+
const local = localScope?.bindings.get(name);
|
|
346
590
|
if (local !== undefined && local.some((b) => b.origin === 'local'))
|
|
347
591
|
continue;
|
|
348
|
-
|
|
592
|
+
// Bind the augmentation bucket and its seeded de-dup set together
|
|
593
|
+
// under one nullable lifecycle, so neither needs a non-null
|
|
594
|
+
// assertion (they are always set or unset as a pair). Stays lazy:
|
|
595
|
+
// nothing is allocated for a name with no cross-file defs.
|
|
596
|
+
let inject = null;
|
|
349
597
|
for (const def of defs) {
|
|
350
598
|
if (def.filePath === filePath)
|
|
351
599
|
continue; // don't self-reference
|
|
352
|
-
if (
|
|
353
|
-
|
|
354
|
-
|
|
600
|
+
if (inject === null) {
|
|
601
|
+
const bucket = getAugmentationBucket(augmentations, scopeId, name);
|
|
602
|
+
// Seed the de-dup set from any entries an earlier pass
|
|
603
|
+
// (using-static / cross-namespace imports) already added,
|
|
604
|
+
// replacing the per-def O(A) `.some` scan.
|
|
605
|
+
const seen = new Set();
|
|
606
|
+
for (const b of bucket)
|
|
607
|
+
seen.add(b.def.nodeId);
|
|
608
|
+
inject = { bucket, seen };
|
|
609
|
+
}
|
|
610
|
+
if (inject.seen.has(def.nodeId))
|
|
355
611
|
continue;
|
|
356
|
-
|
|
612
|
+
inject.seen.add(def.nodeId);
|
|
613
|
+
inject.bucket.push({ def, origin: 'namespace' });
|
|
357
614
|
}
|
|
358
615
|
}
|
|
359
616
|
}
|
|
@@ -378,6 +635,21 @@ function getAugmentationBucket(augmentations, scopeId, name) {
|
|
|
378
635
|
}
|
|
379
636
|
return bucketArr;
|
|
380
637
|
}
|
|
638
|
+
/** Get-or-create a mutable inner bucket inside the `workspaceFqnBindings`
|
|
639
|
+
* channel (the scope-independent third channel; see
|
|
640
|
+
* `ScopeResolutionIndexes.workspaceFqnBindings`). Like
|
|
641
|
+
* `getAugmentationBucket`, the inner arrays are mutable by contract —
|
|
642
|
+
* callers `push` directly. Keeping the get-or-create here means the one
|
|
643
|
+
* ReadonlyMap→Map cast at the call site is the only place the mutable
|
|
644
|
+
* view is taken. */
|
|
645
|
+
function getWorkspaceBucket(workspace, name) {
|
|
646
|
+
let bucketArr = workspace.get(name);
|
|
647
|
+
if (bucketArr === undefined) {
|
|
648
|
+
bucketArr = [];
|
|
649
|
+
workspace.set(name, bucketArr);
|
|
650
|
+
}
|
|
651
|
+
return bucketArr;
|
|
652
|
+
}
|
|
381
653
|
function isTypeDef(def) {
|
|
382
654
|
return (def.type === 'Class' ||
|
|
383
655
|
def.type === 'Interface' ||
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// Verified against tree-sitter-cpp ^0.23.4
|
|
3
3
|
import { SupportedLanguages } from '../../../../_shared/index.js';
|
|
4
4
|
import { hasKeyword } from '../../field-extractors/configs/helpers.js';
|
|
5
|
+
import { classifyCppParameterType } from '../../languages/cpp/arity-metadata.js';
|
|
5
6
|
import { extractSimpleTypeName } from '../../type-extractors/shared.js';
|
|
6
7
|
// ---------------------------------------------------------------------------
|
|
7
8
|
// C/C++ helpers
|
|
@@ -140,6 +141,7 @@ function extractCppParameters(node) {
|
|
|
140
141
|
? (extractSimpleTypeName(typeNode) ?? typeNode.text?.trim() ?? null)
|
|
141
142
|
: null,
|
|
142
143
|
rawType: typeNode?.text?.trim() ?? null,
|
|
144
|
+
typeClass: classifyCppParameterType(typeNode?.text?.trim() ?? 'unknown', declNode?.text, param.text),
|
|
143
145
|
isOptional: false,
|
|
144
146
|
isVariadic: false,
|
|
145
147
|
});
|
|
@@ -155,6 +157,7 @@ function extractCppParameters(node) {
|
|
|
155
157
|
? (extractSimpleTypeName(typeNode) ?? typeNode.text?.trim() ?? null)
|
|
156
158
|
: null,
|
|
157
159
|
rawType: typeNode?.text?.trim() ?? null,
|
|
160
|
+
typeClass: classifyCppParameterType(typeNode?.text?.trim() ?? 'unknown', declNode?.text, param.text),
|
|
158
161
|
isOptional: true,
|
|
159
162
|
isVariadic: false,
|
|
160
163
|
});
|
|
@@ -171,6 +174,7 @@ function extractCppParameters(node) {
|
|
|
171
174
|
? (extractSimpleTypeName(typeNode) ?? typeNode.text?.trim() ?? null)
|
|
172
175
|
: null,
|
|
173
176
|
rawType: typeNode?.text?.trim() ?? null,
|
|
177
|
+
typeClass: classifyCppParameterType(typeNode?.text?.trim() ?? 'unknown', declNode?.text, param.text),
|
|
174
178
|
isOptional: false,
|
|
175
179
|
isVariadic: true,
|
|
176
180
|
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { SupportedLanguages } from '../../_shared/index.js';
|
|
1
|
+
import type { ParameterTypeClass, SupportedLanguages } from '../../_shared/index.js';
|
|
2
2
|
import type { FieldVisibility } from './field-types.js';
|
|
3
3
|
import type { SyntaxNode } from './utils/ast-helpers.js';
|
|
4
4
|
export type MethodVisibility = FieldVisibility;
|
|
@@ -9,6 +9,7 @@ export interface ParameterInfo {
|
|
|
9
9
|
* Used by typeTagForId for overload disambiguation where generic args matter.
|
|
10
10
|
* Falls back to `type` when not set. */
|
|
11
11
|
rawType?: string | null;
|
|
12
|
+
typeClass?: ParameterTypeClass;
|
|
12
13
|
isOptional: boolean;
|
|
13
14
|
isVariadic: boolean;
|
|
14
15
|
}
|
|
@@ -63,11 +63,15 @@ export interface ScopeResolutionIndexes {
|
|
|
63
63
|
* are returned first and win duplicate `def.nodeId` metadata, with
|
|
64
64
|
* unique augmentations appended after. See I8. */
|
|
65
65
|
readonly bindingAugmentations: ReadonlyMap<ScopeId, ReadonlyMap<string, readonly BindingRef[]>>;
|
|
66
|
-
/** Workspace-level
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
66
|
+
/** Workspace-level binding lookup, shared instead of per-scope
|
|
67
|
+
* duplication. Consulted by `lookupBindingsAt` as a third source after
|
|
68
|
+
* finalized and per-scope augmented bindings. Language-specific
|
|
69
|
+
* namespace-sibling hooks populate it with disjoint key formats that
|
|
70
|
+
* never collide — e.g. backslash-separated FQNs (`App\Models\User`) for
|
|
71
|
+
* backslash-namespace languages, and bare simple names (`User`) for
|
|
72
|
+
* global-/default-namespace types that are visible from every file. The
|
|
73
|
+
* shared map gives those workspace-wide names one entry each instead of
|
|
74
|
+
* O(scopes × defs) per-scope augmentation. */
|
|
71
75
|
readonly workspaceFqnBindings: ReadonlyMap<string, readonly BindingRef[]>;
|
|
72
76
|
/** Pre-resolution usage facts; consumed by the resolution phase. */
|
|
73
77
|
readonly referenceSites: readonly ReferenceSite[];
|
|
@@ -10,7 +10,7 @@ import { isVerboseIngestionEnabled } from './utils/verbose.js';
|
|
|
10
10
|
import { getDefinitionNodeFromCaptures, findEnclosingClassInfo, findObjectLiteralBindingInfo, getLabelFromCaptures, CLASS_CONTAINER_TYPES, } from './utils/ast-helpers.js';
|
|
11
11
|
import { detectFrameworkFromAST } from './framework-detection.js';
|
|
12
12
|
import { buildTypeEnv } from './type-env.js';
|
|
13
|
-
import { buildMethodProps, arityForIdFromInfo, typeTagForId, constTagForId, buildCollisionGroups, } from './utils/method-props.js';
|
|
13
|
+
import { buildMethodProps, arityForIdFromInfo, typeTagForId, constTagForId, buildCollisionGroups, parameterShapeIdTag, } from './utils/method-props.js';
|
|
14
14
|
import { extractTemplateArguments, templateArgumentsIdTag, templateConstraintsIdTag, } from './utils/template-arguments.js';
|
|
15
15
|
import { logger } from '../logger.js';
|
|
16
16
|
import { getTreeSitterBufferSize, getTreeSitterContentByteLength, TREE_SITTER_MAX_BUFFER, } from './constants.js';
|
|
@@ -504,6 +504,9 @@ const processParsingSequential = async (graph, files, symbolTable, astCache, sco
|
|
|
504
504
|
arityTag += typeTagForId(cached.map, nodeName, arityForId, seqDefMethodInfo, language, cached.groups);
|
|
505
505
|
arityTag += constTagForId(cached.map, nodeName, arityForId, seqDefMethodInfo, cached.groups);
|
|
506
506
|
}
|
|
507
|
+
const parameterShapeTag = nodeLabel === 'Function' || nodeLabel === 'Method'
|
|
508
|
+
? parameterShapeIdTag(methodProps.parameterTypes, methodProps.parameterTypeClasses)
|
|
509
|
+
: '';
|
|
507
510
|
const classTemplateArguments = extractedClassSymbol?.templateArguments ??
|
|
508
511
|
provider.classExtractor?.extractTemplateArgumentsFromCapture?.({
|
|
509
512
|
captureMap,
|
|
@@ -551,7 +554,7 @@ const processParsingSequential = async (graph, files, symbolTable, astCache, sco
|
|
|
551
554
|
constraintsTag = '';
|
|
552
555
|
}
|
|
553
556
|
}
|
|
554
|
-
const nodeId = generateId(nodeLabel, `${file.path}:${qualifiedName}${classTemplateTag}${arityTag}${constraintsTag}`);
|
|
557
|
+
const nodeId = generateId(nodeLabel, `${file.path}:${qualifiedName}${classTemplateTag}${arityTag}${constraintsTag}${parameterShapeTag}`);
|
|
555
558
|
const classNodeForSymbol = definitionNodeForRange || definitionNode || nameNode;
|
|
556
559
|
const qualifiedTypeName = extractedClassSymbol?.qualifiedName ??
|
|
557
560
|
(classNodeForSymbol && provider.classExtractor?.isTypeDeclaration(classNodeForSymbol)
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* a different file-level fallback — cross that bridge when they
|
|
17
17
|
* migrate.
|
|
18
18
|
*/
|
|
19
|
-
import type { NodeLabel, ScopeId, SymbolDefinition } from '../../../../_shared/index.js';
|
|
19
|
+
import type { NodeLabel, ParameterTypeClass, ScopeId, SymbolDefinition } from '../../../../_shared/index.js';
|
|
20
20
|
import type { ScopeResolutionIndexes } from '../../model/scope-resolution-indexes.js';
|
|
21
21
|
import { type GraphNodeLookup } from '../graph-bridge/node-lookup.js';
|
|
22
22
|
/**
|
|
@@ -40,6 +40,7 @@ export declare function resolveDefGraphId(filePath: string, def: {
|
|
|
40
40
|
qualifiedName?: string;
|
|
41
41
|
type?: NodeLabel;
|
|
42
42
|
parameterTypes?: readonly string[];
|
|
43
|
+
parameterTypeClasses?: readonly ParameterTypeClass[];
|
|
43
44
|
templateArguments?: readonly string[];
|
|
44
45
|
templateConstraints?: unknown;
|
|
45
46
|
}, nodeLookup: GraphNodeLookup): string | undefined;
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
import { generateId } from '../../../../lib/utils.js';
|
|
20
20
|
import { qualifiedKey, simpleKey } from '../graph-bridge/node-lookup.js';
|
|
21
21
|
import { templateConstraintsIdTag } from '../../utils/template-arguments.js';
|
|
22
|
+
import { parameterShapeIdTag } from '../../utils/method-props.js';
|
|
22
23
|
/**
|
|
23
24
|
* Labels that may legitimately ANCHOR a CALLS/ACCESSES edge as the
|
|
24
25
|
* source ("caller"). A Variable / Property can be the TARGET of an
|
|
@@ -82,10 +83,21 @@ export function resolveDefGraphId(filePath, def, nodeLookup) {
|
|
|
82
83
|
if (cHit !== undefined)
|
|
83
84
|
return cHit;
|
|
84
85
|
}
|
|
86
|
+
if ((def.type === 'Function' || def.type === 'Method') &&
|
|
87
|
+
def.parameterTypes !== undefined &&
|
|
88
|
+
def.parameterTypeClasses !== undefined) {
|
|
89
|
+
const shapeTag = parameterShapeIdTag(def.parameterTypes, def.parameterTypeClasses);
|
|
90
|
+
if (shapeTag !== '') {
|
|
91
|
+
const shapeKey = qualifiedKey(filePath, def.type, `${qn}${shapeTag}`);
|
|
92
|
+
const shapeHit = nodeLookup.get(shapeKey);
|
|
93
|
+
if (shapeHit !== undefined)
|
|
94
|
+
return shapeHit;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
85
97
|
// Overload disambiguation: when the def carries parameter types,
|
|
86
98
|
// try the parameter-typed key first so same-name same-arity
|
|
87
99
|
// overloads route to their distinct graph nodes.
|
|
88
|
-
if (def.type === 'Method' &&
|
|
100
|
+
if ((def.type === 'Function' || def.type === 'Method') &&
|
|
89
101
|
def.parameterTypes !== undefined &&
|
|
90
102
|
def.parameterTypes.length > 0) {
|
|
91
103
|
const pKey = qualifiedKey(filePath, def.type, `${qn}~${def.parameterTypes.join(',')}`);
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
* format that downstream consumers (queries, edges, MCP) expect.
|
|
19
19
|
*/
|
|
20
20
|
import { templateConstraintsIdTag } from '../../utils/template-arguments.js';
|
|
21
|
+
import { parameterShapeIdTag } from '../../utils/method-props.js';
|
|
21
22
|
/**
|
|
22
23
|
* Parse a qualified name out of a Function/Method node id.
|
|
23
24
|
*
|
|
@@ -37,6 +38,9 @@ function parseQualifiedFromId(id, label, filePath) {
|
|
|
37
38
|
const hash = suffix.indexOf('#');
|
|
38
39
|
return hash === -1 ? suffix : suffix.slice(0, hash);
|
|
39
40
|
}
|
|
41
|
+
function stripCallableDisambiguatorTags(qualifiedName) {
|
|
42
|
+
return qualifiedName.replace(/~shape:.*$/, '').replace(/~c:[a-z0-9]+$/, '');
|
|
43
|
+
}
|
|
40
44
|
/**
|
|
41
45
|
* Build a qualified-key string in a separate keyspace from simple-key
|
|
42
46
|
* strings. Prefix `<q>` can't appear in a valid filePath on any OS, so
|
|
@@ -72,7 +76,8 @@ export function buildGraphNodeLookup(graph) {
|
|
|
72
76
|
// `def save` vs `class User: def save`).
|
|
73
77
|
const qualified = props.qualifiedName ?? parseQualifiedFromId(node.id, node.label, props.filePath);
|
|
74
78
|
if (qualified !== undefined && qualified.length > 0) {
|
|
75
|
-
const
|
|
79
|
+
const keyQualified = stripCallableDisambiguatorTags(qualified);
|
|
80
|
+
const qKey = qualifiedKey(props.filePath, node.label, keyQualified);
|
|
76
81
|
if (!lookup.has(qKey))
|
|
77
82
|
lookup.set(qKey, node.id);
|
|
78
83
|
// Overload-disambiguating key: include parameter types so two
|
|
@@ -82,10 +87,21 @@ export function buildGraphNodeLookup(graph) {
|
|
|
82
87
|
// a parameter-types-suffixed key so resolveDefGraphId can find
|
|
83
88
|
// the right overload by matching its def's parameterTypes.
|
|
84
89
|
const pTypes = props.parameterTypes;
|
|
85
|
-
if (pTypes !== undefined &&
|
|
86
|
-
|
|
90
|
+
if (pTypes !== undefined &&
|
|
91
|
+
pTypes.length > 0 &&
|
|
92
|
+
(node.label === 'Function' || node.label === 'Method')) {
|
|
93
|
+
const pKey = qualifiedKey(props.filePath, node.label, `${keyQualified}~${pTypes.join(',')}`);
|
|
87
94
|
// Each overload is unique — set unconditionally.
|
|
88
|
-
lookup.
|
|
95
|
+
if (!lookup.has(pKey))
|
|
96
|
+
lookup.set(pKey, node.id);
|
|
97
|
+
}
|
|
98
|
+
const pClasses = props
|
|
99
|
+
.parameterTypeClasses;
|
|
100
|
+
const shapeTag = parameterShapeIdTag(pTypes, pClasses);
|
|
101
|
+
if (shapeTag !== '' && (node.label === 'Function' || node.label === 'Method')) {
|
|
102
|
+
const shapeKey = qualifiedKey(props.filePath, node.label, `${keyQualified}${shapeTag}`);
|
|
103
|
+
if (!lookup.has(shapeKey))
|
|
104
|
+
lookup.set(shapeKey, node.id);
|
|
89
105
|
}
|
|
90
106
|
// SFINAE / `requires`-clause disambiguation (issue #1579) — register
|
|
91
107
|
// a constraint-fingerprinted key so resolveDefGraphId can locate the
|
|
@@ -95,7 +111,7 @@ export function buildGraphNodeLookup(graph) {
|
|
|
95
111
|
// `parameterTypes=['T']` would otherwise collide.
|
|
96
112
|
const tConstraints = props.templateConstraints;
|
|
97
113
|
if (tConstraints !== undefined && (node.label === 'Function' || node.label === 'Method')) {
|
|
98
|
-
const cKey = qualifiedKey(props.filePath, node.label, `${
|
|
114
|
+
const cKey = qualifiedKey(props.filePath, node.label, `${keyQualified}${templateConstraintsIdTag(tConstraints)}`);
|
|
99
115
|
lookup.set(cKey, node.id);
|
|
100
116
|
}
|
|
101
117
|
if ((node.label === 'Class' ||
|
|
@@ -105,7 +121,7 @@ export function buildGraphNodeLookup(graph) {
|
|
|
105
121
|
node.label === 'Record') &&
|
|
106
122
|
props.templateArguments !== undefined &&
|
|
107
123
|
props.templateArguments.length > 0) {
|
|
108
|
-
const tKey = qualifiedKey(props.filePath, node.label, `${
|
|
124
|
+
const tKey = qualifiedKey(props.filePath, node.label, `${keyQualified}~${props.templateArguments.join(',')}`);
|
|
109
125
|
if (!lookup.has(tKey))
|
|
110
126
|
lookup.set(tKey, node.id);
|
|
111
127
|
}
|
|
@@ -35,6 +35,11 @@
|
|
|
35
35
|
* candidates whose template constraints provably fail at the
|
|
36
36
|
* call site. Three-valued; `'unknown'` keeps the candidate
|
|
37
37
|
* (monotonicity).
|
|
38
|
+
* 4d. Conservative C++ template partial-order approximation. When
|
|
39
|
+
* template-placeholder overloads remain tied, prefer a candidate
|
|
40
|
+
* whose parameter shape is more specialized for the observed
|
|
41
|
+
* argument shape (`T*` over `T`, `const T&` over `T`). Unknown or
|
|
42
|
+
* incomparable shapes are left ambiguous.
|
|
38
43
|
* 5. Empty input returns empty output.
|
|
39
44
|
*/
|
|
40
45
|
import type { ArityVerdict, Callsite, ConstraintContext, ParameterTypeClass, SymbolDefinition } from '../../../../_shared/index.js';
|
|
@@ -35,6 +35,11 @@
|
|
|
35
35
|
* candidates whose template constraints provably fail at the
|
|
36
36
|
* call site. Three-valued; `'unknown'` keeps the candidate
|
|
37
37
|
* (monotonicity).
|
|
38
|
+
* 4d. Conservative C++ template partial-order approximation. When
|
|
39
|
+
* template-placeholder overloads remain tied, prefer a candidate
|
|
40
|
+
* whose parameter shape is more specialized for the observed
|
|
41
|
+
* argument shape (`T*` over `T`, `const T&` over `T`). Unknown or
|
|
42
|
+
* incomparable shapes are left ambiguous.
|
|
38
43
|
* 5. Empty input returns empty output.
|
|
39
44
|
*/
|
|
40
45
|
export function narrowOverloadCandidates(overloads, argCount, argTypes, hookCtx) {
|
|
@@ -133,6 +138,11 @@ export function narrowOverloadCandidates(overloads, argCount, argTypes, hookCtx)
|
|
|
133
138
|
return hookCtx.constraintCompatibility(callsite, def, ctx) !== 'incompatible';
|
|
134
139
|
});
|
|
135
140
|
}
|
|
141
|
+
if (result.length > 1 && argTypes !== undefined && argTypes.length > 0) {
|
|
142
|
+
const partiallyOrdered = rankByTemplatePartialOrdering(result, argTypes, hookCtx?.argumentTypeClasses);
|
|
143
|
+
if (partiallyOrdered !== undefined)
|
|
144
|
+
result = partiallyOrdered;
|
|
145
|
+
}
|
|
136
146
|
return result;
|
|
137
147
|
}
|
|
138
148
|
function exactTypeSlotMatches(argType, paramType, argTypeClass, paramTypeClass) {
|
|
@@ -248,6 +258,110 @@ function pairwiseCompare(a, b) {
|
|
|
248
258
|
return 1;
|
|
249
259
|
return 0;
|
|
250
260
|
}
|
|
261
|
+
/**
|
|
262
|
+
* Closed-table approximation of C++ function-template partial ordering.
|
|
263
|
+
*
|
|
264
|
+
* Full `[temp.func.order]` requires template argument deduction. GitNexus
|
|
265
|
+
* keeps this graph-safe by recognizing only syntactic placeholder shapes
|
|
266
|
+
* that the C++ parameter sidecar already preserves:
|
|
267
|
+
* - `T*` is more specialized than `T` for pointer arguments.
|
|
268
|
+
*
|
|
269
|
+
* Anything with unknown argument shape, non-template parameter spelling, or
|
|
270
|
+
* incomparable specialized shapes stays ambiguous so callers suppress. The
|
|
271
|
+
* placeholder detector is intentionally narrow: lowercase template parameters
|
|
272
|
+
* are left ambiguous rather than guessed.
|
|
273
|
+
*/
|
|
274
|
+
function rankByTemplatePartialOrdering(candidates, argTypes, argTypeClasses) {
|
|
275
|
+
if (argTypeClasses === undefined)
|
|
276
|
+
return undefined;
|
|
277
|
+
const viable = [];
|
|
278
|
+
for (const def of candidates) {
|
|
279
|
+
const params = def.parameterTypes;
|
|
280
|
+
const paramClasses = def.parameterTypeClasses;
|
|
281
|
+
if (params === undefined || paramClasses === undefined)
|
|
282
|
+
continue;
|
|
283
|
+
const ranks = [];
|
|
284
|
+
let sawTemplateSlot = false;
|
|
285
|
+
let ok = true;
|
|
286
|
+
for (let i = 0; i < argTypes.length; i++) {
|
|
287
|
+
const paramType = parameterTypeAt(params, i);
|
|
288
|
+
const paramClass = parameterTypeClassAt(paramClasses, i);
|
|
289
|
+
const argClass = argTypeClasses[i];
|
|
290
|
+
if (paramType === undefined || paramClass === undefined || argClass === undefined) {
|
|
291
|
+
ok = false;
|
|
292
|
+
break;
|
|
293
|
+
}
|
|
294
|
+
const rank = templatePartialOrderSlotRank(paramType, paramClass, argClass);
|
|
295
|
+
if (rank === undefined) {
|
|
296
|
+
ok = false;
|
|
297
|
+
break;
|
|
298
|
+
}
|
|
299
|
+
sawTemplateSlot ||= isTemplatePlaceholder(paramType);
|
|
300
|
+
ranks.push(rank);
|
|
301
|
+
}
|
|
302
|
+
if (ok && sawTemplateSlot)
|
|
303
|
+
viable.push({ def, ranks });
|
|
304
|
+
}
|
|
305
|
+
if (viable.length === 0)
|
|
306
|
+
return undefined;
|
|
307
|
+
if (viable.length !== candidates.length)
|
|
308
|
+
return [];
|
|
309
|
+
if (viable.length <= 1)
|
|
310
|
+
return viable.map((v) => v.def);
|
|
311
|
+
const dominated = new Set();
|
|
312
|
+
for (let i = 0; i < viable.length; i++) {
|
|
313
|
+
if (dominated.has(i))
|
|
314
|
+
continue;
|
|
315
|
+
for (let j = i + 1; j < viable.length; j++) {
|
|
316
|
+
if (dominated.has(j))
|
|
317
|
+
continue;
|
|
318
|
+
const cmp = compareSpecializationRanks(viable[i].ranks, viable[j].ranks);
|
|
319
|
+
if (cmp < 0)
|
|
320
|
+
dominated.add(j);
|
|
321
|
+
else if (cmp > 0)
|
|
322
|
+
dominated.add(i);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
return viable.filter((_, idx) => !dominated.has(idx)).map((v) => v.def);
|
|
326
|
+
}
|
|
327
|
+
function templatePartialOrderSlotRank(paramType, paramClass, argClass) {
|
|
328
|
+
if (!isTemplatePlaceholder(paramType))
|
|
329
|
+
return undefined;
|
|
330
|
+
if (argClass.indirection === 'unknown' || paramClass.indirection === 'unknown') {
|
|
331
|
+
return undefined;
|
|
332
|
+
}
|
|
333
|
+
if (isPointerShape(paramClass)) {
|
|
334
|
+
return isPointerShape(argClass) ? 3 : undefined;
|
|
335
|
+
}
|
|
336
|
+
if (paramClass.indirection === 'value')
|
|
337
|
+
return 1;
|
|
338
|
+
return undefined;
|
|
339
|
+
}
|
|
340
|
+
function isTemplatePlaceholder(typeName) {
|
|
341
|
+
return /^[A-Z]\w*$/.test(typeName);
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* Higher specialization rank is better. Returns -1 when `a` dominates `b`,
|
|
345
|
+
* +1 when `b` dominates `a`, and 0 for ties / incomparable vectors.
|
|
346
|
+
*/
|
|
347
|
+
function compareSpecializationRanks(a, b) {
|
|
348
|
+
let aBetter = false;
|
|
349
|
+
let bBetter = false;
|
|
350
|
+
const len = Math.min(a.length, b.length);
|
|
351
|
+
for (let i = 0; i < len; i++) {
|
|
352
|
+
if (a[i] > b[i])
|
|
353
|
+
aBetter = true;
|
|
354
|
+
else if (b[i] > a[i])
|
|
355
|
+
bBetter = true;
|
|
356
|
+
if (aBetter && bBetter)
|
|
357
|
+
return 0;
|
|
358
|
+
}
|
|
359
|
+
if (aBetter && !bBetter)
|
|
360
|
+
return -1;
|
|
361
|
+
if (bBetter && !aBetter)
|
|
362
|
+
return 1;
|
|
363
|
+
return 0;
|
|
364
|
+
}
|
|
251
365
|
/**
|
|
252
366
|
* Detect when >1 candidate share identical `parameterTypes` after the
|
|
253
367
|
* per-language normalizer has collapsed distinct underlying types. This
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Dev-mode runtime validator for the
|
|
3
|
-
* (Contract Invariant I8 in `contract/scope-resolver.ts`).
|
|
2
|
+
* Dev-mode runtime validator for the post-finalize binding-channel
|
|
3
|
+
* lifecycle (Contract Invariant I8 in `contract/scope-resolver.ts`).
|
|
4
4
|
*
|
|
5
5
|
* The two channels:
|
|
6
6
|
* - `indexes.bindings` — finalize-output channel. After
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Dev-mode runtime validator for the
|
|
3
|
-
* (Contract Invariant I8 in `contract/scope-resolver.ts`).
|
|
2
|
+
* Dev-mode runtime validator for the post-finalize binding-channel
|
|
3
|
+
* lifecycle (Contract Invariant I8 in `contract/scope-resolver.ts`).
|
|
4
4
|
*
|
|
5
5
|
* The two channels:
|
|
6
6
|
* - `indexes.bindings` — finalize-output channel. After
|
|
@@ -61,5 +61,18 @@ export function validateBindingsImmutability(indexes, onWarn) {
|
|
|
61
61
|
}
|
|
62
62
|
}
|
|
63
63
|
}
|
|
64
|
+
// Third channel: `workspaceFqnBindings` (scope-independent, shared map
|
|
65
|
+
// populated by language namespace-sibling hooks — PHP FQN keys, C#
|
|
66
|
+
// global-namespace simple names). Like bindingAugmentations its inner
|
|
67
|
+
// arrays are mutable by contract (hooks `push()` directly), so freezing
|
|
68
|
+
// one is the same defect as freezing an augmentation bucket.
|
|
69
|
+
for (const [name, bucket] of indexes.workspaceFqnBindings) {
|
|
70
|
+
if (Object.isFrozen(bucket)) {
|
|
71
|
+
onWarn(`binding-immutability: indexes.workspaceFqnBindings[${name}] is FROZEN — ` +
|
|
72
|
+
`the workspace channel is mutable by contract; freezing it defeats the ` +
|
|
73
|
+
`append-only purpose. See ScopeResolver Invariant I8.`);
|
|
74
|
+
violations++;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
64
77
|
return violations;
|
|
65
78
|
}
|
|
@@ -55,6 +55,14 @@ export declare function lookupBindingsAt(scopeId: ScopeId, name: string, scopes:
|
|
|
55
55
|
* Fast paths (zero allocation) when at most one channel is populated:
|
|
56
56
|
* returns the underlying `Map.keys()` iterator directly. Only when both
|
|
57
57
|
* channels carry names do we materialize a `Set` for deduplication.
|
|
58
|
+
*
|
|
59
|
+
* Scope: enumerates only the per-scope `bindings` and `bindingAugmentations`
|
|
60
|
+
* channels. It deliberately EXCLUDES the scope-independent
|
|
61
|
+
* `workspaceFqnBindings` channel (PHP FQN keys, C# global-namespace simple
|
|
62
|
+
* names). `lookupBindingsAt` consults that third channel when resolving a
|
|
63
|
+
* specific name, but name *enumeration* here does not — those names apply at
|
|
64
|
+
* every scope and would flood per-scope callers. Callers that need
|
|
65
|
+
* workspace-level names must read `workspaceFqnBindings` directly.
|
|
58
66
|
*/
|
|
59
67
|
export declare function namesAtScope(scopeId: ScopeId, scopes: ScopeResolutionIndexes): Iterable<string>;
|
|
60
68
|
/**
|
|
@@ -92,6 +92,14 @@ const EMPTY_NAMES = Object.freeze([]);
|
|
|
92
92
|
* Fast paths (zero allocation) when at most one channel is populated:
|
|
93
93
|
* returns the underlying `Map.keys()` iterator directly. Only when both
|
|
94
94
|
* channels carry names do we materialize a `Set` for deduplication.
|
|
95
|
+
*
|
|
96
|
+
* Scope: enumerates only the per-scope `bindings` and `bindingAugmentations`
|
|
97
|
+
* channels. It deliberately EXCLUDES the scope-independent
|
|
98
|
+
* `workspaceFqnBindings` channel (PHP FQN keys, C# global-namespace simple
|
|
99
|
+
* names). `lookupBindingsAt` consults that third channel when resolving a
|
|
100
|
+
* specific name, but name *enumeration* here does not — those names apply at
|
|
101
|
+
* every scope and would flood per-scope callers. Callers that need
|
|
102
|
+
* workspace-level names must read `workspaceFqnBindings` directly.
|
|
95
103
|
*/
|
|
96
104
|
export function namesAtScope(scopeId, scopes) {
|
|
97
105
|
const finalized = scopes.bindings.get(scopeId);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { MethodInfo } from '../method-types.js';
|
|
2
|
-
import { SupportedLanguages } from '../../../_shared/index.js';
|
|
2
|
+
import { SupportedLanguages, type ParameterTypeClass } from '../../../_shared/index.js';
|
|
3
3
|
/**
|
|
4
4
|
* Compute arity for ID-generation purposes.
|
|
5
5
|
* Returns `undefined` when any parameter is variadic (arity is indeterminate).
|
|
@@ -28,5 +28,15 @@ collisionGroups?: Map<string, MethodInfo[]>): string;
|
|
|
28
28
|
export declare function constTagForId(methodMap: Map<string, MethodInfo>, methodName: string, arity: number | undefined, currentInfo: MethodInfo,
|
|
29
29
|
/** Pre-built collision groups from buildCollisionGroups(). Avoids O(N) scan per call. */
|
|
30
30
|
collisionGroups?: Map<string, MethodInfo[]>): string;
|
|
31
|
+
/**
|
|
32
|
+
* Disambiguate function-template overloads whose normalized parameter types
|
|
33
|
+
* intentionally collapse to the same placeholder token (`T`, `U`, ...), but
|
|
34
|
+
* whose C++ sidecar shape is semantically different (`T` vs `T*` / `T&`).
|
|
35
|
+
*
|
|
36
|
+
* Kept intentionally narrow: concrete types already use the existing raw-type
|
|
37
|
+
* overload tag, and non-template languages should not acquire sidecar-shaped
|
|
38
|
+
* IDs.
|
|
39
|
+
*/
|
|
40
|
+
export declare function parameterShapeIdTag(parameterTypes?: readonly string[], parameterTypeClasses?: readonly ParameterTypeClass[]): string;
|
|
31
41
|
/** Convert MethodInfo from methodExtractor into flat properties for a graph node. */
|
|
32
42
|
export declare function buildMethodProps(info: MethodInfo): Record<string, unknown>;
|
|
@@ -113,14 +113,53 @@ collisionGroups) {
|
|
|
113
113
|
}
|
|
114
114
|
return '';
|
|
115
115
|
}
|
|
116
|
+
/**
|
|
117
|
+
* Disambiguate function-template overloads whose normalized parameter types
|
|
118
|
+
* intentionally collapse to the same placeholder token (`T`, `U`, ...), but
|
|
119
|
+
* whose C++ sidecar shape is semantically different (`T` vs `T*` / `T&`).
|
|
120
|
+
*
|
|
121
|
+
* Kept intentionally narrow: concrete types already use the existing raw-type
|
|
122
|
+
* overload tag, and non-template languages should not acquire sidecar-shaped
|
|
123
|
+
* IDs.
|
|
124
|
+
*/
|
|
125
|
+
export function parameterShapeIdTag(parameterTypes, parameterTypeClasses) {
|
|
126
|
+
if (parameterTypes === undefined ||
|
|
127
|
+
parameterTypeClasses === undefined ||
|
|
128
|
+
parameterTypes.length === 0) {
|
|
129
|
+
return '';
|
|
130
|
+
}
|
|
131
|
+
let hasTemplatePlaceholder = false;
|
|
132
|
+
let hasDisambiguatingShape = false;
|
|
133
|
+
const parts = [];
|
|
134
|
+
for (let i = 0; i < parameterTypes.length; i++) {
|
|
135
|
+
const type = parameterTypes[i];
|
|
136
|
+
const typeClass = parameterTypeClasses[i];
|
|
137
|
+
if (typeClass === undefined)
|
|
138
|
+
return '';
|
|
139
|
+
if (/^[A-Z]\w*$/.test(type))
|
|
140
|
+
hasTemplatePlaceholder = true;
|
|
141
|
+
if (typeClass.indirection !== 'value' ||
|
|
142
|
+
typeClass.pointerDepth > 0 ||
|
|
143
|
+
(typeClass.cv !== 'none' && typeClass.cv !== 'unknown')) {
|
|
144
|
+
hasDisambiguatingShape = true;
|
|
145
|
+
}
|
|
146
|
+
parts.push(`${type}:${typeClass.cv}:${typeClass.indirection}:${typeClass.pointerDepth.toString()}`);
|
|
147
|
+
}
|
|
148
|
+
if (!hasTemplatePlaceholder || !hasDisambiguatingShape)
|
|
149
|
+
return '';
|
|
150
|
+
return `~shape:${parts.join('|')}`;
|
|
151
|
+
}
|
|
116
152
|
/** Convert MethodInfo from methodExtractor into flat properties for a graph node. */
|
|
117
153
|
export function buildMethodProps(info) {
|
|
118
154
|
const types = [];
|
|
155
|
+
const typeClasses = [];
|
|
119
156
|
let optionalCount = 0;
|
|
120
157
|
let hasVariadic = false;
|
|
121
158
|
for (const p of info.parameters) {
|
|
122
159
|
if (p.type !== null)
|
|
123
160
|
types.push(p.type);
|
|
161
|
+
if (p.typeClass !== undefined)
|
|
162
|
+
typeClasses.push(p.typeClass);
|
|
124
163
|
if (p.isOptional)
|
|
125
164
|
optionalCount++;
|
|
126
165
|
if (p.isVariadic)
|
|
@@ -132,6 +171,9 @@ export function buildMethodProps(info) {
|
|
|
132
171
|
? { requiredParameterCount: info.parameters.length - optionalCount }
|
|
133
172
|
: {}),
|
|
134
173
|
...(types.length > 0 ? { parameterTypes: types } : {}),
|
|
174
|
+
...(typeClasses.length === info.parameters.length && typeClasses.length > 0
|
|
175
|
+
? { parameterTypeClasses: typeClasses }
|
|
176
|
+
: {}),
|
|
135
177
|
returnType: info.returnType ?? undefined,
|
|
136
178
|
visibility: info.visibility,
|
|
137
179
|
isStatic: info.isStatic,
|
|
@@ -44,7 +44,7 @@ import { detectFrameworkFromAST } from '../framework-detection.js';
|
|
|
44
44
|
import { generateId } from '../../../lib/utils.js';
|
|
45
45
|
import { preprocessImportPath } from '../import-processor.js';
|
|
46
46
|
import { extractVueScript, extractTemplateComponents, isVueSetupTopLevel, } from '../vue-sfc-extractor.js';
|
|
47
|
-
import { buildMethodProps, arityForIdFromInfo, typeTagForId, constTagForId, buildCollisionGroups, } from '../utils/method-props.js';
|
|
47
|
+
import { buildMethodProps, arityForIdFromInfo, typeTagForId, constTagForId, buildCollisionGroups, parameterShapeIdTag, } from '../utils/method-props.js';
|
|
48
48
|
import { extractTemplateArguments, templateArgumentsIdTag } from '../utils/template-arguments.js';
|
|
49
49
|
import { extractParsedFile } from '../scope-extractor-bridge.js';
|
|
50
50
|
import { extractLaravelRoutes } from '../route-extractors/laravel.js';
|
|
@@ -1357,6 +1357,9 @@ const processFileGroup = (files, language, queryString, result, onFileProcessed)
|
|
|
1357
1357
|
arityTag += typeTagForId(defMethodMap, nodeName, arityForId, defMethodInfo, language, groups);
|
|
1358
1358
|
arityTag += constTagForId(defMethodMap, nodeName, arityForId, defMethodInfo, groups);
|
|
1359
1359
|
}
|
|
1360
|
+
const parameterShapeTag = nodeLabel === 'Function' || nodeLabel === 'Method'
|
|
1361
|
+
? parameterShapeIdTag(methodProps.parameterTypes, methodProps.parameterTypeClasses)
|
|
1362
|
+
: '';
|
|
1360
1363
|
const classTemplateArguments = extractedClassSymbol?.templateArguments ??
|
|
1361
1364
|
provider.classExtractor?.extractTemplateArgumentsFromCapture?.({
|
|
1362
1365
|
captureMap,
|
|
@@ -1376,7 +1379,7 @@ const processFileGroup = (files, language, queryString, result, onFileProcessed)
|
|
|
1376
1379
|
classTemplateArguments.length > 0
|
|
1377
1380
|
? templateArgumentsIdTag(classTemplateArguments)
|
|
1378
1381
|
: '';
|
|
1379
|
-
const nodeId = generateId(nodeLabel, `${file.path}:${qualifiedName}${classTemplateTag}${arityTag}`);
|
|
1382
|
+
const nodeId = generateId(nodeLabel, `${file.path}:${qualifiedName}${classTemplateTag}${arityTag}${parameterShapeTag}`);
|
|
1380
1383
|
const classNodeForSymbol = definitionNode || nameNode;
|
|
1381
1384
|
const qualifiedTypeName = extractedClassSymbol?.qualifiedName ??
|
|
1382
1385
|
(classNodeForSymbol && provider.classExtractor?.isTypeDeclaration(classNodeForSymbol)
|
package/package.json
CHANGED