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.
@@ -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 slash = rel.lastIndexOf('/');
542
- const file = slash >= 0 ? rel.slice(slash + 1) : rel;
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 noExt = rel.endsWith('.py') ? rel.slice(0, -3) : rel;
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
- import { getCsharpParser } from './query.js';
34
- import { getTreeSitterBufferSize } from '../../constants.js';
35
- import { parseSourceSafe } from '../../../tree-sitter/safe-parse.js';
36
- /** Build a structural view of a C# file by walking the tree-sitter
37
- * AST. Prefers `cachedTree` (handed in via `treeCache`) so we don't
38
- * re-parse files the orchestrator already parsed for `extractParsedFile`;
39
- * falls back to a fresh parse on cache miss. Parser singleton is
40
- * shared across calls. */
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
- const tree = cachedTree ??
43
- parseSourceSafe(getCsharpParser(), content, undefined, {
44
- bufferSize: getTreeSitterBufferSize(content),
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 = parsedFiles.find((p) => p.filePath === targetDef.filePath);
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
- if (bucketArr.some((b) => b.def.nodeId === memberDef.nodeId))
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
- if (bucketArr.some((b) => b.def.nodeId === def.nodeId))
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
- for (const [, bucket] of buckets) {
326
- // De-dup by (nodeId, filePath) across multiple declarations (e.g.
327
- // partial classes declaring the same name in two files — we take
328
- // both and leave de-dup to downstream consumers of bindings).
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
- const arr = [...(defsByName.get(key) ?? [])];
539
+ let arr = defsByName.get(key);
540
+ if (arr === undefined) {
541
+ arr = [];
542
+ defsByName.set(key, arr);
543
+ }
337
544
  arr.push(def);
338
- defsByName.set(key, arr);
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 = bucket.scopes.find((s) => s.filePath === filePath)?.scope.bindings.get(name);
589
+ const local = localScope?.bindings.get(name);
346
590
  if (local !== undefined && local.some((b) => b.origin === 'local'))
347
591
  continue;
348
- let bucketArr = null;
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 (bucketArr === null)
353
- bucketArr = getAugmentationBucket(augmentations, scopeId, name);
354
- if (bucketArr.some((b) => b.def.nodeId === def.nodeId))
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
- bucketArr.push({ def, origin: 'namespace' });
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 FQN binding lookup. Populated by PHP namespace-
67
- * siblings Step 3b as a shared map instead of per-scope duplication.
68
- * Consulted by `lookupBindingsAt` as a third source after finalized
69
- * and per-scope augmented bindings. Keys are backslash-separated FQNs
70
- * (e.g. `App\Models\User`). */
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 qKey = qualifiedKey(props.filePath, node.label, qualified);
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 && pTypes.length > 0 && node.label === 'Method') {
86
- const pKey = qualifiedKey(props.filePath, node.label, `${qualified}~${pTypes.join(',')}`);
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.set(pKey, node.id);
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, `${qualified}${templateConstraintsIdTag(tConstraints)}`);
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, `${qualified}~${props.templateArguments.join(',')}`);
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 two-channel binding lifecycle
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 two-channel binding lifecycle
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitnexus",
3
- "version": "1.6.6-rc.90",
3
+ "version": "1.6.6-rc.92",
4
4
  "description": "Graph-powered code intelligence for AI agents. Index any codebase, query via MCP or CLI.",
5
5
  "author": "Abhigyan Patwari",
6
6
  "license": "PolyForm-Noncommercial-1.0.0",