gitnexus 1.6.10-rc.164 → 1.6.10-rc.166

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.
@@ -629,8 +629,12 @@ export function emitTsScopeCaptures(sourceText, filePath, cachedTree) {
629
629
  * matching arms (member_expression for extends, nested_type_identifier plain +
630
630
  * generic-wrapped for implements).
631
631
  *
632
- * `interface_declaration` / `abstract_class_declaration` heritage is NOT emitted
633
- * by the synth. The EXTENDS-vs-IMPLEMENTS split is decided downstream from the
632
+ * `interface_declaration` and `abstract_class_declaration` heritage IS emitted
633
+ * (#2842 review; both were silently skipped before, so `interface B extends A`
634
+ * and `abstract class X implements I` produced no edge and every dispatch walk
635
+ * dead-ended on a bodiless declaration). They reach their bases by different
636
+ * shapes: an abstract class carries the same `class_heritage` child a concrete
637
+ * one does, while an interface's bases hang off `extends_type_clause` directly. The EXTENDS-vs-IMPLEMENTS split is decided downstream from the
634
638
  * resolved target's symbol kind in `preEmitInheritanceEdges` (class-extends →
635
639
  * EXTENDS, implements-interface / interface-target → IMPLEMENTS), so all bases
636
640
  * are emitted with the same `inherits` kind here. The base lookup name is
@@ -647,7 +651,26 @@ function synthesizeTsInheritanceReferences(root, out) {
647
651
  if (child !== null)
648
652
  stack.push(child);
649
653
  }
650
- if (node.type !== 'class_declaration')
654
+ // `interface B extends A, C` hangs its bases off an `extends_type_clause`
655
+ // DIRECTLY on the interface — there is no `class_heritage` wrapper, so the
656
+ // class path below cannot reach them (#2842 review). The clause's `type`
657
+ // field is `multiple: true`, so `childForFieldName('type')` would silently
658
+ // return only `A` and drop `C`; iterate the named children instead.
659
+ if (node.type === 'interface_declaration') {
660
+ for (const child of node.namedChildren) {
661
+ if (child === null || child.type !== 'extends_type_clause')
662
+ continue;
663
+ for (const base of child.namedChildren) {
664
+ emitTsInheritanceBase(base, out);
665
+ }
666
+ }
667
+ continue;
668
+ }
669
+ // `abstract class X implements I` carries an identical `class_heritage`
670
+ // child, so the existing body handles it once the node type is admitted.
671
+ // Omitting it severed the only link between an interface and the concrete
672
+ // classes below an abstract base — a whole subtree, not a leaf.
673
+ if (node.type !== 'class_declaration' && node.type !== 'abstract_class_declaration')
651
674
  continue;
652
675
  // Find the `class_heritage` child (holds extends / implements clauses).
653
676
  let heritage = null;
@@ -114,13 +114,23 @@
114
114
  * languages that set `resolveThisViaEnclosingClass === true`;
115
115
  * it intercepts every bare-`this` call/read/write site ahead of
116
116
  * Case 4 and does NOT emit the interface-dispatch fan-out that
117
- * Cases 0 and 4 both perform (Case 0 gained it in #2829), so
118
- * enabling the toggle for a language changes that language's
119
- * `this` dispatch semantics (see the toggle's doc below)
117
+ * Cases 0, 3b and 4 all perform (Case 0 gained it in #2829 and
118
+ * Case 3b in #2832, leaving 0.5 the only INSTANCE-receiver case
119
+ * that folds or walks to a receiver type without fanning out.
120
+ * Read that narrowly: Case 2 also walks an MRO and its binding
121
+ * admits `Interface`, but its receiver IS the type name, so the
122
+ * site is static dispatch and a fan-out would be wrong; Cases 3
123
+ * and 5 resolve by direct lookup rather than a fold or MRO walk,
124
+ * and no language is known to reach Case 3 with an Interface —
125
+ * every one that could strips the namespace qualifier first,
126
+ * sending it to Case 4), so enabling the toggle
127
+ * for a language changes that language's `this` dispatch
128
+ * semantics (see the toggle's doc below)
120
129
  * 4. Case 1 namespace-receiver
121
130
  * 5. Case 2 class-name receiver
122
131
  * 6. Case 3 dotted typeBinding for namespace prefix
123
- * 7. Case 3b chain-typebinding (compound resolver)
132
+ * 7. Case 3b chain-typebinding (compound resolver + interface-dispatch
133
+ * fan-out on an Interface fold, #2832)
124
134
  * 8. Case 4 simple typeBinding (MRO walk + findOwnedMember)
125
135
  * Reordering or merging cases changes resolution semantics. The
126
136
  * numbering is part of the contract — keep the comments.
@@ -114,13 +114,23 @@
114
114
  * languages that set `resolveThisViaEnclosingClass === true`;
115
115
  * it intercepts every bare-`this` call/read/write site ahead of
116
116
  * Case 4 and does NOT emit the interface-dispatch fan-out that
117
- * Cases 0 and 4 both perform (Case 0 gained it in #2829), so
118
- * enabling the toggle for a language changes that language's
119
- * `this` dispatch semantics (see the toggle's doc below)
117
+ * Cases 0, 3b and 4 all perform (Case 0 gained it in #2829 and
118
+ * Case 3b in #2832, leaving 0.5 the only INSTANCE-receiver case
119
+ * that folds or walks to a receiver type without fanning out.
120
+ * Read that narrowly: Case 2 also walks an MRO and its binding
121
+ * admits `Interface`, but its receiver IS the type name, so the
122
+ * site is static dispatch and a fan-out would be wrong; Cases 3
123
+ * and 5 resolve by direct lookup rather than a fold or MRO walk,
124
+ * and no language is known to reach Case 3 with an Interface —
125
+ * every one that could strips the namespace qualifier first,
126
+ * sending it to Case 4), so enabling the toggle
127
+ * for a language changes that language's `this` dispatch
128
+ * semantics (see the toggle's doc below)
120
129
  * 4. Case 1 namespace-receiver
121
130
  * 5. Case 2 class-name receiver
122
131
  * 6. Case 3 dotted typeBinding for namespace prefix
123
- * 7. Case 3b chain-typebinding (compound resolver)
132
+ * 7. Case 3b chain-typebinding (compound resolver + interface-dispatch
133
+ * fan-out on an Interface fold, #2832)
124
134
  * 8. Case 4 simple typeBinding (MRO walk + findOwnedMember)
125
135
  * Reordering or merging cases changes resolution semantics. The
126
136
  * numbering is part of the contract — keep the comments.
@@ -18,8 +18,8 @@
18
18
  * toggle unset skip this case entirely; their `this` sites fall
19
19
  * through to Case 4 via the synthesized `this` typeBinding (which
20
20
  * emits the interface-dispatch fan-out that this case does not —
21
- * as does Case 0 since #2829; Case 0.5 remains the only resolving
22
- * case without it).
21
+ * as do Cases 0 since #2829 and 3b since #2832; Case 0.5 remains
22
+ * the only fold-or-walk case without it).
23
23
  * 4. **Case 1 (namespace)** — receiver in `namespaceTargets` → exported def
24
24
  * 5. **Case 2 (class-name / static receiver)** — receiver resolves to a
25
25
  * class-like binding (Class/Interface/Struct/Record/Enum/Trait) → MRO
@@ -29,7 +29,9 @@
29
29
  * 6. **Case 3 (dotted typeBinding for namespace prefix)** —
30
30
  * `typeRef.rawName` like `models.User`
31
31
  * 7. **Case 3b (chain-typebinding)** — `typeRef.rawName` has a dot
32
- * but not a namespace prefix → compound resolver
32
+ * but not a namespace prefix → compound resolver. Also emits the
33
+ * interface-dispatch fan-out when the folded receiver type is an
34
+ * Interface (#2832) — same call Cases 0 and 4 make.
33
35
  * 8. **Case 4 (simple typeBinding)** — `typeRef.rawName` has no dot →
34
36
  * MRO walk + `findOwnedMember`
35
37
  * 9. **Case 5 (value-receiver bridge)** — receiver is a `Const`/`Variable`
@@ -18,8 +18,8 @@
18
18
  * toggle unset skip this case entirely; their `this` sites fall
19
19
  * through to Case 4 via the synthesized `this` typeBinding (which
20
20
  * emits the interface-dispatch fan-out that this case does not —
21
- * as does Case 0 since #2829; Case 0.5 remains the only resolving
22
- * case without it).
21
+ * as do Cases 0 since #2829 and 3b since #2832; Case 0.5 remains
22
+ * the only fold-or-walk case without it).
23
23
  * 4. **Case 1 (namespace)** — receiver in `namespaceTargets` → exported def
24
24
  * 5. **Case 2 (class-name / static receiver)** — receiver resolves to a
25
25
  * class-like binding (Class/Interface/Struct/Record/Enum/Trait) → MRO
@@ -29,7 +29,9 @@
29
29
  * 6. **Case 3 (dotted typeBinding for namespace prefix)** —
30
30
  * `typeRef.rawName` like `models.User`
31
31
  * 7. **Case 3b (chain-typebinding)** — `typeRef.rawName` has a dot
32
- * but not a namespace prefix → compound resolver
32
+ * but not a namespace prefix → compound resolver. Also emits the
33
+ * interface-dispatch fan-out when the folded receiver type is an
34
+ * Interface (#2832) — same call Cases 0 and 4 make.
33
35
  * 8. **Case 4 (simple typeBinding)** — `typeRef.rawName` has no dot →
34
36
  * MRO walk + `findOwnedMember`
35
37
  * 9. **Case 5 (value-receiver bridge)** — receiver is a `Const`/`Variable`
@@ -306,6 +308,49 @@ export function emitReceiverBoundCalls(graph, scopes, parsedFiles, nodeLookup, h
306
308
  return false;
307
309
  return graph.getNode(graphId)?.properties.isAbstract === true;
308
310
  };
311
+ /**
312
+ * Can an INSTANCE-typed receiver reach this member? A static member cannot be,
313
+ * ever — `class C implements I { static save() {} }` does not satisfy `I`
314
+ * (TypeScript rejects it outright as TS2420, "Property 'save' is missing"), so
315
+ * an edge to it from an `I`-typed receiver names a target no dispatch can
316
+ * produce. Every comparable tool draws the same line: tsserver partitions
317
+ * static from instance results, clangd gates on `isVirtual()` (C++ forbids
318
+ * virtual statics), jdtls filters abstract-or-static, and class-hierarchy
319
+ * analysis expands only VIRTUAL call sites — a static call already has exactly
320
+ * one target and needs no fan-out.
321
+ *
322
+ * Two sources answer this, and the order matters:
323
+ *
324
+ * 1. `provider.isStaticOnly` when the language declares it. It is the
325
+ * precise answer, because a language that needs the distinction defines
326
+ * it exactly — Kotlin marks only COMPANION-promoted defs, so a Kotlin
327
+ * `object Impl : Iface { override fun handle() }` is correctly kept: an
328
+ * `object` is a singleton INSTANCE and its members really are reachable
329
+ * through an `Iface`-typed receiver.
330
+ * 2. Otherwise the graph node's `isStatic`. For every language that does not
331
+ * declare the hook, that flag comes from the member's own modifier (or,
332
+ * for Ruby, from `singleton_class` — `def self.foo`, which is likewise
333
+ * unreachable through an instance), so it means what we need here.
334
+ *
335
+ * Getting that order wrong is a live regression, not a hypothetical: the
336
+ * method extractor derives `isStatic` from the OWNER type as well as the
337
+ * member (`method-extractors/generic.ts`, `staticOwnerTypes`), and the JVM
338
+ * config lists `object_declaration`. Reading the flag first would delete
339
+ * Kotlin object implementations from the fan-out. A language that needs
340
+ * precision declares the hook; that is the upgrade path.
341
+ *
342
+ * Unresolvable defs fail open (treated as reachable), matching
343
+ * `isDeclarationOnly` and the rest of this pass.
344
+ */
345
+ const isUnreachableByInstanceDispatch = (def) => {
346
+ const staticOnly = provider.isStaticOnly;
347
+ if (staticOnly !== undefined)
348
+ return staticOnly(def) === true;
349
+ const graphId = resolveDefGraphId(def.filePath, def, nodeLookup);
350
+ if (graphId === undefined)
351
+ return false;
352
+ return graph.getNode(graphId)?.properties.isStatic === true;
353
+ };
309
354
  /**
310
355
  * Emit secondary CALLS edges with reason='interface-dispatch' when the primary
311
356
  * receiver-typed edge targeted an Interface's method.
@@ -357,6 +402,10 @@ export function emitReceiverBoundCalls(graph, scopes, parsedFiles, nodeLookup, h
357
402
  // implementation — keep descending past it rather than emitting to it.
358
403
  if (isDeclarationOnly(implMember))
359
404
  continue;
405
+ // Nor is a static member: no instance-typed receiver can reach one, so
406
+ // an edge to it is a target dispatch cannot produce (#2842 review).
407
+ if (isUnreachableByInstanceDispatch(implMember))
408
+ continue;
360
409
  targets.push(implMember);
361
410
  }
362
411
  }
@@ -958,6 +1007,43 @@ export function emitReceiverBoundCalls(graph, scopes, parsedFiles, nodeLookup, h
958
1007
  const ok = tryEmitEdge(graph, scopes, nodeLookup, site, memberDef, memberDef.filePath !== parsed.filePath ? 'import-resolved' : 'global', seen, 0.85, collapse, calleeCapture);
959
1008
  if (ok)
960
1009
  emitted++;
1010
+ // Interface dispatch, exactly as Cases 0 and 4 do it (#2832). Case
1011
+ // 3b folds a chain to a receiver type through the SAME
1012
+ // `resolveCompoundReceiverClass` call and the same MRO walk Case 0
1013
+ // uses, so when that fold lands on an Interface the primary edge
1014
+ // above names the interface's own bodiless DECLARATION and nothing
1015
+ // reaches the implementations.
1016
+ //
1017
+ // Leaving 3b out made the fan-out a property of how the receiver
1018
+ // was SPELLED rather than of what it resolved to: `d.repo.save()`
1019
+ // took Case 0 and fanned out, while binding the identical field to
1020
+ // a local first (`const r = d.repo; r.save()`) took Case 3b and
1021
+ // did not. #2829 closed that gap for Case 0 and left this half of
1022
+ // it open (#2832).
1023
+ //
1024
+ // `ownerDef` is the receiver's own folded type — matching Case 0's
1025
+ // `currentClass` and Case 4's `ownerDef` — NOT the owner of the
1026
+ // member the MRO walk settled on. That distinction matters: a
1027
+ // receiver that folds to a concrete class merely INHERITING an
1028
+ // interface method must not fan out, because its runtime type is
1029
+ // that class. `emitInterfaceDispatchFor` self-gates on
1030
+ // `ownerDef.type !== 'Interface'`, so this is inert for every
1031
+ // concrete receiver and needs no language check of its own.
1032
+ //
1033
+ // That gate is deliberately narrower than "the primary landed on
1034
+ // something bodiless": a chain folding to an ABSTRACT class also
1035
+ // dead-ends on a declaration-only member and does NOT fan out
1036
+ // here. Widening it to `|| isDeclarationOnly(memberDef)` would
1037
+ // cover that, but it changes Cases 0 and 4 identically and for
1038
+ // every language, so it is not #2832's to make.
1039
+ //
1040
+ // Confidence mirrors THIS case's own primary emit above — the 0.85
1041
+ // literal — so a site's dispatch edges never claim more certainty
1042
+ // than the edge they hang off. Case 4 passes a site.kind-dependent
1043
+ // value instead because ITS primary varies that way; Case 3b's
1044
+ // primary, like Case 0's, does not, so there is no 1.0 arm here to
1045
+ // mirror.
1046
+ emitted += emitInterfaceDispatchFor(ownerDef, memberName, memberDef, site, 0.85, calleeCapture);
961
1047
  // Always mark handled when the site was resolved, even
962
1048
  // if the edge was deduplicated (collapse mode), so
963
1049
  // `emitReferencesViaLookup` doesn't re-emit from the
@@ -155,6 +155,7 @@ interface RepoHandle {
155
155
  * and cannot be changed mid-process.
156
156
  */
157
157
  export declare function resolveWorktreeCwd(repoPath: string, launchCwd: string): string;
158
+ export declare function buildDetectChangesDiffArgs(scope: string, baseRef?: string): string[] | null;
158
159
  /**
159
160
  * Length of the path-derived suffix appended to a colliding repo id.
160
161
  * Exported so tests can pin the suffix shape without re-deriving the
@@ -544,6 +544,20 @@ export function resolveWorktreeCwd(repoPath, launchCwd) {
544
544
  }
545
545
  return repoPath;
546
546
  }
547
+ export function buildDetectChangesDiffArgs(scope, baseRef) {
548
+ const args = ['diff', '--ignore-cr-at-eol'];
549
+ switch (scope) {
550
+ case 'staged':
551
+ return [...args, '--staged', '-U0'];
552
+ case 'all':
553
+ return [...args, 'HEAD', '-U0'];
554
+ case 'compare':
555
+ return baseRef ? [...args, baseRef, '-U0'] : null;
556
+ case 'unstaged':
557
+ default:
558
+ return [...args, '-U0'];
559
+ }
560
+ }
547
561
  /**
548
562
  * Length of the path-derived suffix appended to a colliding repo id.
549
563
  * Exported so tests can pin the suffix shape without re-deriving the
@@ -3942,25 +3956,11 @@ export class LocalBackend {
3942
3956
  await this.ensureInitialized(repo);
3943
3957
  const scope = params.scope || 'unstaged';
3944
3958
  const { execFileSync } = await import('child_process');
3945
- // Build git diff args based on scope (using execFileSync to avoid shell injection)
3946
- let diffArgs;
3947
- switch (scope) {
3948
- case 'staged':
3949
- diffArgs = ['diff', '--staged', '-U0'];
3950
- break;
3951
- case 'all':
3952
- diffArgs = ['diff', 'HEAD', '-U0'];
3953
- break;
3954
- case 'compare':
3955
- if (!params.base_ref)
3956
- return { error: 'base_ref is required for "compare" scope' };
3957
- diffArgs = ['diff', params.base_ref, '-U0'];
3958
- break;
3959
- case 'unstaged':
3960
- default:
3961
- diffArgs = ['diff', '-U0'];
3962
- break;
3963
- }
3959
+ // Ignore CR-only EOL differences, while preserving meaningful whitespace changes.
3960
+ // execFileSync receives an argv array, so refs never pass through a shell.
3961
+ const diffArgs = buildDetectChangesDiffArgs(scope, params.base_ref);
3962
+ if (!diffArgs)
3963
+ return { error: 'base_ref is required for "compare" scope' };
3964
3964
  let diffOutput;
3965
3965
  try {
3966
3966
  // Resolve the cwd for git diff.
@@ -219,7 +219,13 @@ import { fileURLToPath } from 'url';
219
219
  // value is unchanged by the rebase — the reason it was chosen is simply now
220
220
  // visible in the history above. RE-CHECK AGAINST origin/main IMMEDIATELY
221
221
  // BEFORE MERGING; this file records eight prior collisions, two EXACT.
222
- const SCHEMA_BUMP = 43;
222
+ // Moved 43 -> 44 for #2842's TypeScript heritage capture, which now emits
223
+ // `@reference.inherits` for `interface_declaration` and
224
+ // `abstract_class_declaration`. That is PARSE-TIME emission, so a v43 warm
225
+ // cache would serve entries that are missing those matches entirely — the
226
+ // exact failure a bump exists to prevent. Verified against origin/main at
227
+ // a857f4c5a, which is still on 43, so 44 is free. RE-CHECK BEFORE MERGE.
228
+ const SCHEMA_BUMP = 44;
223
229
  const GITNEXUS_PKG_VERSION = (() => {
224
230
  try {
225
231
  // package.json sits at gitnexus/package.json — two levels up from
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitnexus",
3
- "version": "1.6.10-rc.164",
3
+ "version": "1.6.10-rc.166",
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",