gitnexus 1.6.6-rc.145 → 1.6.6-rc.146

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