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.
- package/dist/core/ingestion/cfg/visitors/kotlin.d.ts +4 -3
- package/dist/core/ingestion/cfg/visitors/python-harvest.d.ts +2 -2
- package/dist/core/ingestion/languages/dart/captures.js +17 -0
- package/dist/core/ingestion/languages/kotlin/captures.js +37 -0
- package/dist/core/ingestion/languages/python/captures.js +13 -0
- package/dist/core/ingestion/languages/ruby/captures.js +35 -0
- package/dist/core/ingestion/languages/swift/captures.js +11 -0
- package/dist/core/ingestion/utils/callable-flow-captures.d.ts +24 -0
- package/dist/core/ingestion/utils/callable-flow-captures.js +184 -20
- package/dist/mcp/local/local-backend.js +22 -4
- package/dist/storage/parse-cache.js +8 -1
- package/package.json +1 -1
|
@@ -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
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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
|
|
50
|
-
*
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
96
|
-
|
|
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
|
|
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 {
|
|
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
|
-
|
|
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