gitnexus 1.6.13-rc.37 → 1.6.13-rc.38

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.
@@ -3,9 +3,10 @@
3
3
  *
4
4
  * Kotlin's tree-sitter grammar (vendored, NOT an npm package — loaded via
5
5
  * `requireVendoredGrammar('tree-sitter-kotlin')`, exactly like tree-sitter-swift)
6
- * is field-less for control flow: NONE of the control-flow nodes expose
7
- * `childForFieldName` fields (verified by a real parse — every `fieldNameForChild`
8
- * came back null), so this visitor navigates purely by child TYPE and position.
6
+ * is mostly field-less for control flow (`when` / loops / `try` / `elvis_expression`
7
+ * expose no fields), so this visitor navigates purely by child TYPE and position.
8
+ * `if_expression` is the exception: it DOES field `condition` / `consequence` /
9
+ * `alternative` (#3354 — callable-flow capture reads them); position still works.
9
10
  * Every node-type literal below was grammar-validated against the vendored
10
11
  * tree-sitter-kotlin via the introspection probe before use (mandatory pre-step —
11
12
  * the grammar-literal CI gate maps `kotlin.ts → Kotlin` and fails on a wrong
@@ -46,8 +46,8 @@
46
46
  * - reads: `attribute` (fields `object`/`attribute`), `subscript` (fields
47
47
  * `value`/`subscript`), `call` (fields `function`/`arguments`),
48
48
  * `boolean_operator` (fields `left`/`operator`/`right`),
49
- * `conditional_expression` (ternary: consequent / condition / alternative in
50
- * source order), `parenthesized_expression`.
49
+ * `conditional_expression` (ternary, FIELDLESS: consequent, condition,
50
+ * alternative as positional children), `parenthesized_expression`.
51
51
  *
52
52
  * TWO-PHASE, ORDER-INDEPENDENT (load-bearing — mirrors the TS / Go harvesters):
53
53
  * the CFG walk is NOT source-order, so resolving names against a scope stack
@@ -109,6 +109,23 @@ const DART_CALLABLE_CAPTURE_OPTIONS = {
109
109
  extractCallCallee: (node) => dartCallableCallee(node) ?? undefined,
110
110
  callSiteNode: (node) => dartCallableCallee(node) ?? undefined,
111
111
  callableProtocolMethods: new Set(['call']),
112
+ // tree-sitter-dart spells `a ?? b` as `if_null_expression(first:, second:)`
113
+ // and `c ? a : b` as a `conditional_expression` with `consequence` /
114
+ // `alternative` but NO `condition` field, so neither matches the shared
115
+ // field-based branch rule and only the last operand flowed (#3354).
116
+ valueAlternatives: (node) => {
117
+ if (node.type === 'if_null_expression') {
118
+ const first = node.childForFieldName('first');
119
+ const second = node.childForFieldName('second');
120
+ return first !== null && second !== null ? [first, second] : undefined;
121
+ }
122
+ if (node.type === 'conditional_expression') {
123
+ const consequence = node.childForFieldName('consequence');
124
+ const alternative = node.childForFieldName('alternative');
125
+ return consequence !== null && alternative !== null ? [consequence, alternative] : undefined;
126
+ }
127
+ return undefined;
128
+ },
112
129
  };
113
130
  function dartLexicalFunctionOwner(input) {
114
131
  let node = input;
@@ -57,7 +57,44 @@ const KOTLIN_CALLABLE_CAPTURE_OPTIONS = {
57
57
  return destination === undefined || source === undefined ? undefined : { destination, source };
58
58
  },
59
59
  normalizeQualifiedName: (raw) => raw.replaceAll('::', '.'),
60
+ valueAlternatives: (node) => kotlinValueAlternatives(node),
60
61
  };
62
+ const isKotlinComment = (node) => node.type === 'line_comment' || node.type === 'multiline_comment';
63
+ const namedNonComments = (node) => node.namedChildren.filter((child) => child !== null && !isKotlinComment(child));
64
+ /**
65
+ * Branches of a Kotlin value-selecting expression (#3354). `a ?: b` is a
66
+ * FIELDLESS `elvis_expression` (positional operands), so the shared
67
+ * field-based rule never saw it and only the last operand flowed. An
68
+ * `if_expression` fields its branches as `control_structure_body` wrappers:
69
+ * a bare branch (`if (c) ::f`) holds the expression directly, a braced one
70
+ * (`if (c) { ::f }`) nests it one level deeper in a `statements` node. A
71
+ * branch holding exactly one expression is that expression's value; an
72
+ * empty or multi-statement block, or an `if` without `else`, keeps the
73
+ * whole `if` one opaque source.
74
+ */
75
+ function kotlinValueAlternatives(node) {
76
+ if (node.type === 'elvis_expression') {
77
+ const operands = namedNonComments(node);
78
+ return operands.length === 2 ? operands : undefined;
79
+ }
80
+ if (node.type !== 'if_expression')
81
+ return undefined;
82
+ const branches = [];
83
+ for (const field of ['consequence', 'alternative']) {
84
+ const body = node.childForFieldName(field);
85
+ if (body === null)
86
+ return [node];
87
+ let values = namedNonComments(body);
88
+ const [block] = values;
89
+ if (values.length === 1 && block?.type === 'statements')
90
+ values = namedNonComments(block);
91
+ const [value] = values;
92
+ if (values.length !== 1 || value === undefined)
93
+ return [node];
94
+ branches.push(value);
95
+ }
96
+ return branches;
97
+ }
61
98
  export function emitKotlinScopeCaptures(sourceText, filePath, cachedTree) {
62
99
  let tree = cachedTree;
63
100
  if (tree === undefined) {
@@ -42,6 +42,19 @@ const PYTHON_CALLABLE_CAPTURE_OPTIONS = {
42
42
  assignmentNodeTypes: new Set(['assignment', 'named_expression']),
43
43
  identifierNodeTypes: new Set(['identifier']),
44
44
  functionScopedValueBindings: true,
45
+ // `a if c else b` is a FIELDLESS `conditional_expression` (positional
46
+ // value, condition, value), so the shared condition/consequence/alternative
47
+ // rule never sees its branches and only the last operand flowed (#3354).
48
+ // `a or b` is a fielded `boolean_operator` the shared rule already handles.
49
+ valueAlternatives: (node) => {
50
+ if (node.type !== 'conditional_expression')
51
+ return undefined;
52
+ const named = node.namedChildren.filter((child) => child !== null && child.type !== 'comment');
53
+ const [value, , alternative] = named;
54
+ return named.length === 3 && value !== undefined && alternative !== undefined
55
+ ? [value, alternative]
56
+ : undefined;
57
+ },
45
58
  };
46
59
  export function emitPythonScopeCaptures(sourceText, _filePath, cachedTree) {
47
60
  // Skip the parse when the caller (the scope-resolution orchestrator's
@@ -41,6 +41,7 @@ const RUBY_CALLABLE_CAPTURE_OPTIONS = {
41
41
  // Ruby (`action = process` stores process's RETURN value) — only explicit
42
42
  // reference forms (method(:x), &:x, lambda/proc) reference the callable.
43
43
  bareNamesAreCalls: true,
44
+ valueAlternatives: (node) => rubyValueAlternatives(node),
44
45
  extractCallableReference: (node) => {
45
46
  if (node.type !== 'call')
46
47
  return undefined;
@@ -55,6 +56,40 @@ const RUBY_CALLABLE_CAPTURE_OPTIONS = {
55
56
  return name.length === 0 ? undefined : { name, anchor: symbol };
56
57
  },
57
58
  };
59
+ /**
60
+ * Branches of a statement-bodied `if` / `unless` / `elsif` (#3354). They field
61
+ * `condition` / `consequence` / `alternative` like a ternary, but their
62
+ * branches are `then` / `else` STATEMENT LISTS, so the shared ternary rule
63
+ * would dig an identifier out of whichever statement it found (`g = h; 0`
64
+ * flowed `h`, although the branch evaluates to `0`). A branch holding one
65
+ * statement is that statement's value. A longer branch is skipped: it adds
66
+ * no alternative, while the other branches still flow. Every alternative
67
+ * emitted is a value the conditional really evaluates to, so skipping cannot
68
+ * add an edge. It only drops the skipped branch's own value. The `elsif`
69
+ * chain is walked here, so one multi-statement `elsif` no longer hides the
70
+ * branches after it. When no branch is a single statement, the whole
71
+ * conditional stays one opaque source. The `c ? a : b` ternary
72
+ * (`conditional`) is left to the shared rule.
73
+ */
74
+ function rubyValueAlternatives(node) {
75
+ if (node.type !== 'if' && node.type !== 'unless' && node.type !== 'elsif')
76
+ return undefined;
77
+ const branches = [];
78
+ for (let link = node; link !== null;) {
79
+ const consequence = link.childForFieldName('consequence');
80
+ const alternative = link.childForFieldName('alternative');
81
+ link = alternative?.type === 'elsif' ? alternative : null;
82
+ for (const branch of [consequence, link === null ? alternative : null]) {
83
+ if (branch === null)
84
+ continue;
85
+ const statements = branch.namedChildren.filter((child) => child !== null && child.type !== 'comment');
86
+ const [statement] = statements;
87
+ if (statements.length === 1 && statement !== undefined)
88
+ branches.push(statement);
89
+ }
90
+ }
91
+ return branches.length > 0 ? branches : [node];
92
+ }
58
93
  /**
59
94
  * Build the full `.`-joined qualified owner name for a heritage/attr call by
60
95
  * walking ALL enclosing class/module ancestors (not just the immediate one),
@@ -94,6 +94,17 @@ const SWIFT_CALLABLE_CAPTURE_OPTIONS = {
94
94
  assignmentNodeTypes: new Set(['assignment']),
95
95
  identifierNodeTypes: new Set(['simple_identifier', 'type_identifier']),
96
96
  extractFunctionParameters: (node) => node.namedChildren.filter((child) => child !== null && child.type === 'parameter'),
97
+ // tree-sitter-swift fields `a ?? b` as `value` / `if_nil` and `c ? a : b` as
98
+ // `if_true` / `if_false`, neither of which the shared field-based branch
99
+ // rule knows, so only the last operand flowed (#3354).
100
+ valueAlternatives: (node) => {
101
+ const [first, second] = node.type === 'nil_coalescing_expression'
102
+ ? [node.childForFieldName('value'), node.childForFieldName('if_nil')]
103
+ : node.type === 'ternary_expression'
104
+ ? [node.childForFieldName('if_true'), node.childForFieldName('if_false')]
105
+ : [null, null];
106
+ return first !== null && second !== null ? [first, second] : undefined;
107
+ },
97
108
  };
98
109
  /** tree-sitter-swift node types that carry arity. */
99
110
  const FUNCTION_NODE_TYPES = [
@@ -47,6 +47,12 @@
47
47
  * `extractAssignment` (Kotlin's `assignment`, Dart's
48
48
  * `initialized_identifier`). Returning `undefined` falls back to the shared
49
49
  * path, so one callback can handle the odd node and leave the rest alone.
50
+ * - A **fieldless or oddly-fielded** `??`/`?:`/elvis/ternary node is invisible
51
+ * to the shared branch expansion, so only one operand (if any) flows.
52
+ * Supply `valueAlternatives` (Kotlin's `elvis_expression`, Swift's
53
+ * `nil_coalescing_expression`, Dart's `if_null_expression`, Python's
54
+ * `conditional_expression`); return `[node]` to keep a statement-bodied
55
+ * `if` opaque (Ruby).
50
56
  * - A binding needs a `SymbolDefinition` for the pass to attach to. Captures
51
57
  * alone are not enough: without a `@declaration.*` for the bound name, the
52
58
  * seed has no cell to key on.
@@ -139,6 +145,20 @@ export interface CallableFlowCaptureOptions {
139
145
  readonly anchor: SyntaxNode;
140
146
  readonly qualifiedName?: string;
141
147
  } | undefined;
148
+ /**
149
+ * Provider-owned branches of a value-selecting expression (#3354). The
150
+ * shared rule only knows the field shapes `left`/`operator`/`right` (with a
151
+ * `??`/`||`/`or` operator) and `condition`/`consequence`/`alternative`; a
152
+ * grammar that spells the same construct differently (fieldless children,
153
+ * `value`/`if_nil`, `first`/`second`, a ternary without a `condition`
154
+ * field) supplies the branches here. Each returned branch is expanded again,
155
+ * so chains work. Returning `[node]` means "recognized, but opaque": the
156
+ * whole expression stays one source, which is how a provider keeps the
157
+ * shared ternary rule off a statement-bodied `if` whose branches are
158
+ * statement lists, not values. `undefined` falls back to the shared rule
159
+ * (mirrors `extractAssignment`).
160
+ */
161
+ readonly valueAlternatives?: (node: SyntaxNode) => readonly SyntaxNode[] | undefined;
142
162
  }
143
163
  /**
144
164
  * Emit normalized flow captures in deterministic source order.
@@ -146,6 +166,10 @@ export interface CallableFlowCaptureOptions {
146
166
  * One explicit DFS supplies all phases below. Query-backed emitters may still
147
167
  * perform their existing query walk; this helper never reparses and remains
148
168
  * linear in AST size (the scope-capture benchmark guards the scaling ratio).
169
+ * That includes a value-selecting source: `valueAlternatives` expands a
170
+ * chain iteratively into disjoint leaves, and each leaf's visibility walk
171
+ * reuses the memoized anchor spine instead of re-walking to the root (the
172
+ * `typescript-deep-chain` benchmark case guards that one).
149
173
  */
150
174
  export declare function synthesizeCallableFlowCaptures(root: SyntaxNode, options: CallableFlowCaptureOptions): readonly CaptureMatch[];
151
175
  /**
@@ -47,6 +47,12 @@
47
47
  * `extractAssignment` (Kotlin's `assignment`, Dart's
48
48
  * `initialized_identifier`). Returning `undefined` falls back to the shared
49
49
  * path, so one callback can handle the odd node and leave the rest alone.
50
+ * - A **fieldless or oddly-fielded** `??`/`?:`/elvis/ternary node is invisible
51
+ * to the shared branch expansion, so only one operand (if any) flows.
52
+ * Supply `valueAlternatives` (Kotlin's `elvis_expression`, Swift's
53
+ * `nil_coalescing_expression`, Dart's `if_null_expression`, Python's
54
+ * `conditional_expression`); return `[node]` to keep a statement-bodied
55
+ * `if` opaque (Ruby).
50
56
  * - A binding needs a `SymbolDefinition` for the pass to attach to. Captures
51
57
  * alone are not enough: without a `@declaration.*` for the bound name, the
52
58
  * seed has no cell to key on.
@@ -61,16 +67,23 @@ import { nodeToCapture, syntheticCapture } from './ast-helpers.js';
61
67
  * One explicit DFS supplies all phases below. Query-backed emitters may still
62
68
  * perform their existing query walk; this helper never reparses and remains
63
69
  * linear in AST size (the scope-capture benchmark guards the scaling ratio).
70
+ * That includes a value-selecting source: `valueAlternatives` expands a
71
+ * chain iteratively into disjoint leaves, and each leaf's visibility walk
72
+ * reuses the memoized anchor spine instead of re-walking to the root (the
73
+ * `typescript-deep-chain` benchmark case guards that one).
64
74
  */
65
75
  export function synthesizeCallableFlowCaptures(root, options) {
66
- const nodes = collectNodes(root);
76
+ const parentById = new Map();
77
+ const nodes = collectNodes(root, parentById);
67
78
  const functions = collectFunctions(nodes, options);
68
79
  const knownCallableNames = new Set(functions.map((fn) => fn.name));
69
80
  const assignments = collectAssignments(nodes, options);
70
- const valueBindings = buildValueBindingIndex(nodes, assignments, functions, options);
81
+ const valueBindings = buildValueBindingIndex(nodes, parentById, assignments, functions, options);
71
82
  const out = [];
72
83
  for (const assignment of assignments) {
73
- emitAssignmentFact(assignment, knownCallableNames, valueBindings, options, out);
84
+ for (const source of valueAlternatives(assignment.source, options)) {
85
+ emitAssignmentFact({ ...assignment, source }, knownCallableNames, valueBindings, options, out);
86
+ }
74
87
  }
75
88
  for (const fn of functions)
76
89
  emitFormalFacts(fn, options, out);
@@ -83,7 +96,9 @@ export function synthesizeCallableFlowCaptures(root, options) {
83
96
  out.sort((a, b) => compareCaptures(firstCapture(a), firstCapture(b)));
84
97
  return out;
85
98
  }
86
- function collectNodes(root) {
99
+ /** Every named node in document order; also records each one's parent into
100
+ * `parentById` (see `ValueBindingIndex.parentById`). */
101
+ function collectNodes(root, parentById) {
87
102
  const out = [];
88
103
  const stack = [root];
89
104
  while (stack.length > 0) {
@@ -92,8 +107,10 @@ function collectNodes(root) {
92
107
  const children = node.namedChildren;
93
108
  for (let i = children.length - 1; i >= 0; i--) {
94
109
  const child = children[i];
95
- if (child !== null)
96
- stack.push(child);
110
+ if (child === null)
111
+ continue;
112
+ parentById.set(child.id, node);
113
+ stack.push(child);
97
114
  }
98
115
  }
99
116
  return out;
@@ -163,7 +180,7 @@ function collectAssignments(nodes, options) {
163
180
  }
164
181
  return out;
165
182
  }
166
- function buildValueBindingIndex(nodes, assignments, functions, options) {
183
+ function buildValueBindingIndex(nodes, parentById, assignments, functions, options) {
167
184
  const assignmentRegionIdsByName = new Map();
168
185
  const memberStoreRegionIdsByName = new Map();
169
186
  const formalByOwner = new Map();
@@ -250,13 +267,59 @@ function buildValueBindingIndex(nodes, assignments, functions, options) {
250
267
  byRegion.set(functionOwner.id, signature);
251
268
  }
252
269
  }
270
+ const anchorIds = new Set();
271
+ for (const index of [assignmentRegionIdsByName, memberStoreRegionIdsByName]) {
272
+ for (const regionIds of index.values())
273
+ for (const id of regionIds)
274
+ anchorIds.add(id);
275
+ }
276
+ for (const byRegion of signatureByNameAndRegion.values()) {
277
+ for (const id of byRegion.keys())
278
+ anchorIds.add(id);
279
+ }
280
+ for (const owner of formalByOwner.keys())
281
+ if (owner !== undefined)
282
+ anchorIds.add(owner);
253
283
  return {
254
284
  assignmentRegionIdsByName,
255
285
  memberStoreRegionIdsByName,
256
286
  formalByOwner,
257
287
  signatureByNameAndRegion,
288
+ anchorIds,
289
+ parentById,
290
+ nearestAnchorById: new Map(),
258
291
  };
259
292
  }
293
+ /** A node the DFS did not reach (none in practice) falls back to tree-sitter. */
294
+ function parentOf(node, bindings) {
295
+ return bindings.parentById.get(node.id) ?? node.parent;
296
+ }
297
+ /**
298
+ * The nearest anchor at-or-above `start` (see `ValueBindingIndex.anchorIds`),
299
+ * or null when none is. Every node visited on the way is memoized, so across
300
+ * one file each node's `parent` is taken at most once by these walks.
301
+ */
302
+ function nearestAnchor(start, bindings) {
303
+ const visited = [];
304
+ let node = start;
305
+ let found = null;
306
+ while (node !== null) {
307
+ const cached = bindings.nearestAnchorById.get(node.id);
308
+ if (cached !== undefined) {
309
+ found = cached;
310
+ break;
311
+ }
312
+ visited.push(node.id);
313
+ if (bindings.anchorIds.has(node.id)) {
314
+ found = node;
315
+ break;
316
+ }
317
+ node = parentOf(node, bindings);
318
+ }
319
+ for (const id of visited)
320
+ bindings.nearestAnchorById.set(id, found);
321
+ return found;
322
+ }
260
323
  /** True when a pointer/parenthesized declarator sits between the declaration
261
324
  * and its binding identifier — the shape of a callable-typed variable, never
262
325
  * of a plain prototype. Only C/C++ supply signature-declaration node types,
@@ -307,15 +370,13 @@ function isVisibleValueBinding(input, name, bindings, options) {
307
370
  bindings.formalByOwner.get(providerOwner.id)?.has(name) === true)) {
308
371
  return true;
309
372
  }
310
- let node = input;
311
- while (node !== null) {
373
+ for (let node = nearestAnchor(input, bindings); node !== null; node = nearestAnchor(parentOf(node, bindings), bindings)) {
312
374
  if (assignmentRegionIds?.has(node.id) === true)
313
375
  return true;
314
376
  if (options.functionNodeTypes.has(node.type) &&
315
377
  bindings.formalByOwner.get(node.id)?.has(name) === true) {
316
378
  return true;
317
379
  }
318
- node = node.parent;
319
380
  }
320
381
  if (bindings.formalByOwner.get(undefined)?.has(name) === true)
321
382
  return true;
@@ -341,11 +402,9 @@ function isVisibleMemberStore(input, name, bindings, options) {
341
402
  const providerOwner = options.lexicalFunctionOwner?.(input);
342
403
  if (providerOwner !== undefined && regionIds.has(providerOwner.id))
343
404
  return true;
344
- let node = input;
345
- while (node !== null) {
405
+ for (let node = nearestAnchor(input, bindings); node !== null; node = nearestAnchor(parentOf(node, bindings), bindings)) {
346
406
  if (regionIds.has(node.id))
347
407
  return true;
348
- node = node.parent;
349
408
  }
350
409
  }
351
410
  return visibleCallableSignature(input, name, bindings, options) !== undefined;
@@ -360,12 +419,10 @@ function visibleCallableSignature(input, name, bindings, options) {
360
419
  if (signature !== undefined)
361
420
  return signature;
362
421
  }
363
- let node = input;
364
- while (node !== null) {
422
+ for (let node = nearestAnchor(input, bindings); node !== null; node = nearestAnchor(parentOf(node, bindings), bindings)) {
365
423
  const signature = byRegion.get(node.id);
366
424
  if (signature !== undefined)
367
425
  return signature;
368
- node = node.parent;
369
426
  }
370
427
  return undefined;
371
428
  }
@@ -402,6 +459,108 @@ function assignmentParts(node, options) {
402
459
  return [];
403
460
  return [{ container: node, destination, source }];
404
461
  }
462
+ /** Operators whose result is one of their operands, not a computed value. */
463
+ const VALUE_SELECTING_OPERATORS = new Set(['??', '||', 'or']);
464
+ /**
465
+ * Operators that yield their left operand when it is falsy and their right
466
+ * operand otherwise. A falsy value is never a callable, so only the RIGHT
467
+ * operand can be the callable that is later invoked. Where `&&` / `and`
468
+ * yields a boolean instead (Java, C#, Go, Rust, C, C++, PHP, Zig), the
469
+ * destination is not callable, so the flow never meets an invoke.
470
+ */
471
+ const RIGHT_SELECTING_OPERATORS = new Set(['&&', 'and']);
472
+ /**
473
+ * The operands a value-selecting expression can evaluate to (#3354):
474
+ * `a ?? b`, `a || b`, `a or b`, and `c ? a : b` each yield one of their
475
+ * branches, so each branch flows into the destination. `a && b` / `a and b`
476
+ * can only yield a callable through `b`, so `x and f or g` reaches `f` and `g`.
477
+ * Anything else is its own single alternative, which leaves every other
478
+ * source shape untouched. A branch that is itself an operator expression
479
+ * (`x.kind === f || g`) yields a computed value, so it contributes nothing.
480
+ * `options.valueAlternatives` is consulted first for grammars whose shape the
481
+ * field-based rule below cannot see.
482
+ */
483
+ function valueAlternatives(node, options) {
484
+ // Explicit stack, not recursion: `a || b || …` nests one level per operand,
485
+ // so a generated keyword table thousands of operands long overflowed the
486
+ // call stack (and re-copied every partial result at each level).
487
+ const out = [];
488
+ const pending = [node];
489
+ for (let current = pending.pop(); current !== undefined; current = pending.pop()) {
490
+ const branches = valueBranches(current, options);
491
+ if (branches === undefined) {
492
+ // An operator branch is opaque, as the whole compound source was before
493
+ // the fan-out: emitted alone, `x.kind === Handlers.run` becomes a seed
494
+ // whose qualified text slices to receiver `Handlers`, member `run`.
495
+ if (current === node || !isBinaryOperatorExpression(unwrapParentheses(current))) {
496
+ out.push(current);
497
+ }
498
+ continue;
499
+ }
500
+ // Reverse push keeps the left-to-right branch order on output.
501
+ for (let i = branches.length - 1; i >= 0; i--) {
502
+ const branch = branches[i];
503
+ if (branch !== undefined)
504
+ pending.push(branch);
505
+ }
506
+ }
507
+ return out;
508
+ }
509
+ /** One level of `valueAlternatives`: the branches `node` selects between, or
510
+ * undefined when it is opaque (its own single alternative). */
511
+ function valueBranches(node, options) {
512
+ const inner = unwrapParentheses(node);
513
+ const provided = options.valueAlternatives?.(inner);
514
+ if (provided !== undefined) {
515
+ return provided.length === 1 && provided[0]?.id === inner.id ? undefined : provided;
516
+ }
517
+ const consequence = inner.childForFieldName('consequence');
518
+ const alternative = inner.childForFieldName('alternative');
519
+ if (consequence !== null && alternative !== null && inner.childForFieldName('condition')) {
520
+ return [consequence, alternative];
521
+ }
522
+ const left = inner.childForFieldName('left');
523
+ const right = inner.childForFieldName('right');
524
+ const operator = inner.childForFieldName('operator')?.type;
525
+ if (left === null || right === null || !operator)
526
+ return undefined;
527
+ if (VALUE_SELECTING_OPERATORS.has(operator))
528
+ return [left, right];
529
+ if (RIGHT_SELECTING_OPERATORS.has(operator))
530
+ return [right];
531
+ return undefined;
532
+ }
533
+ function unwrapParentheses(node) {
534
+ let inner = node;
535
+ while (inner.type.includes('parenthesized') && inner.namedChildCount === 1) {
536
+ const child = inner.namedChild(0);
537
+ if (child === null)
538
+ break;
539
+ inner = child;
540
+ }
541
+ return inner;
542
+ }
543
+ /**
544
+ * True for an expression that computes a value from two operands (`a === b`,
545
+ * `a + b`, `a is b`), which designates neither operand. Read from the same
546
+ * field vocabulary as `valueBranches` rather than grammar type names: a
547
+ * `left`/`right` pair, or an operator token (`operator`, Python's
548
+ * `operators`, Swift's `op`) that follows the expression's start. A member
549
+ * access that fields its `.` / `->` as `operator` (Ruby `call`, C/C++
550
+ * `field_expression`) also fields its member name, so it stays a designator;
551
+ * a unary `&f` / `*fp` leads with its operator and stays one too.
552
+ */
553
+ function isBinaryOperatorExpression(node) {
554
+ if (node.childForFieldName('left') !== null && node.childForFieldName('right') !== null) {
555
+ return true;
556
+ }
557
+ if (memberNameNode(node) !== null)
558
+ return false;
559
+ const operator = node.childForFieldName('operator') ??
560
+ node.childForFieldName('operators') ??
561
+ node.childForFieldName('op');
562
+ return operator !== null && operator.startIndex > node.startIndex;
563
+ }
405
564
  function emitAssignmentFact(assignment, knownCallableNames, valueBindings, options, out) {
406
565
  const destination = operandSyntax(assignment.destination, options, options.bindingNodeTypes.has(assignment.container.type));
407
566
  const source = operandSyntax(assignment.source, options);
@@ -699,10 +858,7 @@ function memberParts(input, options) {
699
858
  // stay unaffected. Without it every `x.f(arg)` in such a grammar collapsed
700
859
  // to a DIRECT call named `f` and the flow solver fanned the argument out to
701
860
  // every same-named callable.
702
- const memberNode = node.childForFieldName('property') ??
703
- node.childForFieldName('field') ??
704
- node.childForFieldName('method') ??
705
- node.childForFieldName('member');
861
+ const memberNode = memberNameNode(node);
706
862
  if (receiverNode === null || memberNode === null)
707
863
  return undefined;
708
864
  const receiver = operandSyntax(receiverNode, options);
@@ -713,6 +869,14 @@ function memberParts(input, options) {
713
869
  options.memberPointerOperators?.has(child.text) === true);
714
870
  return { receiver, member, ...(operator !== undefined ? { operator: operator.text } : {}) };
715
871
  }
872
+ /** The member-name child of a member access, under the field names the
873
+ * grammars use for it. */
874
+ function memberNameNode(node) {
875
+ return (node.childForFieldName('property') ??
876
+ node.childForFieldName('field') ??
877
+ node.childForFieldName('method') ??
878
+ node.childForFieldName('member'));
879
+ }
716
880
  function operandSyntax(input, options, declarationBinding = false) {
717
881
  if (declarationBinding) {
718
882
  const binding = bindingIdentifier(input, options);
@@ -178,6 +178,14 @@ function resolveAliasString(canonical, legacy) {
178
178
  }
179
179
  return undefined;
180
180
  }
181
+ /**
182
+ * A `*_uid` param as a lookup key: trimmed, or `undefined` when it is blank or
183
+ * not a string (#3354). Strict adapters send `" "`/`""` for an omitted optional
184
+ * string, and the MCP envelope is not type-validated.
185
+ */
186
+ function nonBlankUid(value) {
187
+ return typeof value === 'string' ? value.trim() || undefined : undefined;
188
+ }
181
189
  const TOOL_STRING_ALIASES = {
182
190
  impact: [{ canonical: 'target', aliases: ['name', 'symbol'] }],
183
191
  context: [{ canonical: 'file_path', aliases: ['file'] }],
@@ -3510,7 +3518,11 @@ export class LocalBackend {
3510
3518
  * same filePath), the Class wins silently.
3511
3519
  */
3512
3520
  async resolveSymbolCandidates(repo, query, hints) {
3513
- const { uid, name, include_content } = query;
3521
+ const { name, include_content } = query;
3522
+ // A blank or non-string uid is omitted, not a lookup key — fall through to
3523
+ // the name instead of `not_found`, matching normalizeToolParams' impact
3524
+ // target_uid check.
3525
+ const uid = nonBlankUid(query.uid);
3514
3526
  const selectClause = `n.id AS id, n.name AS name, labels(n)[0] AS type, n.filePath AS filePath, n.startLine AS startLine, n.endLine AS endLine${include_content ? ', n.content AS content' : ''}`;
3515
3527
  // Direct UID — zero-ambiguity path.
3516
3528
  if (uid) {
@@ -5510,7 +5522,7 @@ export class LocalBackend {
5510
5522
  if (fromOutcome.kind === 'not_found') {
5511
5523
  return {
5512
5524
  status: 'not_found',
5513
- error: `Source symbol '${params.from_uid ?? params.from}' not found.`,
5525
+ error: `Source symbol '${nonBlankUid(params.from_uid) ?? params.from}' not found.`,
5514
5526
  suggestion: 'Check the symbol name or use --from-uid for zero-ambiguity.',
5515
5527
  };
5516
5528
  }
@@ -5528,7 +5540,7 @@ export class LocalBackend {
5528
5540
  if (toOutcome.kind === 'not_found') {
5529
5541
  return {
5530
5542
  status: 'not_found',
5531
- error: `Target symbol '${params.to_uid ?? params.to}' not found.`,
5543
+ error: `Target symbol '${nonBlankUid(params.to_uid) ?? params.to}' not found.`,
5532
5544
  suggestion: 'Check the symbol name or use --to-uid for zero-ambiguity.',
5533
5545
  };
5534
5546
  }
@@ -5852,7 +5864,7 @@ export class LocalBackend {
5852
5864
  // selected silently.
5853
5865
  const outcome = await this.resolveSymbolCandidates(repo, { uid: params.target_uid, name: target }, { file_path: params.file_path, kind: params.kind });
5854
5866
  if (outcome.kind === 'not_found') {
5855
- const missing = params.target_uid ?? target;
5867
+ const missing = nonBlankUid(params.target_uid) ?? target;
5856
5868
  // not_found = no resolved symbol, so the envelope keeps the partial-but-
5857
5869
  // typed target (typed PdgImpactTarget — there is no id/type/filePath yet).
5858
5870
  const notFoundTarget = { name: target };
@@ -6549,6 +6561,12 @@ export class LocalBackend {
6549
6561
  const safeMinConfidence = Number.isFinite(minConfidence) ? minConfidence : 0;
6550
6562
  const confidenceFilter = safeMinConfidence > 0 ? ' AND r.confidence >= $minConfidence' : '';
6551
6563
  const symId = sym.id || sym[0];
6564
+ // #3354: a walk with no anchor id cannot say anything about THIS symbol,
6565
+ // yet it still ships a normal-looking `exact` result with the target's
6566
+ // name echoed back. Throw so every caller's catch reports UNKNOWN instead.
6567
+ if (!symId) {
6568
+ throw new Error(`Impact target '${sym.name || sym[1] || '?'}' resolved without a node id; refusing to report a blast radius`);
6569
+ }
6552
6570
  // #1858 — kick off the epistemic boundary probe concurrently with the BFS.
6553
6571
  // It depends only on symId/symType/symName (all known now) and touches no
6554
6572
  // shared state, so its extra round-trip overlaps the traversal instead of
@@ -773,7 +773,14 @@ import { copyV8CacheIfPresent, tryLoadV8Cache, writeV8CacheFile } from './v8-sid
773
773
  // v104 (#3339 review): TS/JS pair-HOC queries now name object-pair
774
774
  // `mutation(withAuth(arrow))` handlers. Warm caches replay the pre-fix
775
775
  // capture set (anonymous arrows, no Function name), so both stores re-extract.
776
- const SCHEMA_BUMP = 104;
776
+ // v112 (#3354): callable-value flow now follows each branch of `a ?? f`,
777
+ // `a || f`, and `c ? f : g`. Warm caches replay the pre-fix flow facts, which
778
+ // have no flow for those assignments, so both stores re-extract. 105-111 are
779
+ // claimed by open PR #3326 (Elixir).
780
+ // Same v112: the `valueAlternatives` provider hook extends it to Kotlin
781
+ // `?:`/`if`, Swift/Dart `??`/`?:`, and Python `x if c else y`, and keeps a
782
+ // Ruby multi-statement `if` one opaque source.
783
+ const SCHEMA_BUMP = 112;
777
784
  const GITNEXUS_PKG_VERSION = (() => {
778
785
  try {
779
786
  // 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.13-rc.37",
3
+ "version": "1.6.13-rc.38",
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",