@optave/codegraph 3.1.1 → 3.1.3

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.
Files changed (72) hide show
  1. package/README.md +6 -6
  2. package/package.json +7 -7
  3. package/src/ast-analysis/engine.js +365 -0
  4. package/src/ast-analysis/metrics.js +118 -0
  5. package/src/ast-analysis/visitor-utils.js +176 -0
  6. package/src/ast-analysis/visitor.js +162 -0
  7. package/src/ast-analysis/visitors/ast-store-visitor.js +150 -0
  8. package/src/ast-analysis/visitors/cfg-visitor.js +792 -0
  9. package/src/ast-analysis/visitors/complexity-visitor.js +243 -0
  10. package/src/ast-analysis/visitors/dataflow-visitor.js +358 -0
  11. package/src/ast.js +13 -140
  12. package/src/audit.js +2 -87
  13. package/src/batch.js +0 -25
  14. package/src/boundaries.js +1 -1
  15. package/src/branch-compare.js +1 -96
  16. package/src/builder.js +60 -178
  17. package/src/cfg.js +89 -883
  18. package/src/check.js +1 -84
  19. package/src/cli.js +31 -22
  20. package/src/cochange.js +1 -39
  21. package/src/commands/audit.js +88 -0
  22. package/src/commands/batch.js +26 -0
  23. package/src/commands/branch-compare.js +97 -0
  24. package/src/commands/cfg.js +55 -0
  25. package/src/commands/check.js +82 -0
  26. package/src/commands/cochange.js +37 -0
  27. package/src/commands/communities.js +69 -0
  28. package/src/commands/complexity.js +77 -0
  29. package/src/commands/dataflow.js +110 -0
  30. package/src/commands/flow.js +70 -0
  31. package/src/commands/manifesto.js +77 -0
  32. package/src/commands/owners.js +52 -0
  33. package/src/commands/query.js +21 -0
  34. package/src/commands/sequence.js +33 -0
  35. package/src/commands/structure.js +64 -0
  36. package/src/commands/triage.js +49 -0
  37. package/src/communities.js +12 -83
  38. package/src/complexity.js +43 -357
  39. package/src/cycles.js +1 -1
  40. package/src/dataflow.js +12 -665
  41. package/src/db/repository/build-stmts.js +104 -0
  42. package/src/db/repository/cached-stmt.js +19 -0
  43. package/src/db/repository/cfg.js +72 -0
  44. package/src/db/repository/cochange.js +54 -0
  45. package/src/db/repository/complexity.js +20 -0
  46. package/src/db/repository/dataflow.js +17 -0
  47. package/src/db/repository/edges.js +281 -0
  48. package/src/db/repository/embeddings.js +51 -0
  49. package/src/db/repository/graph-read.js +59 -0
  50. package/src/db/repository/index.js +43 -0
  51. package/src/db/repository/nodes.js +247 -0
  52. package/src/db.js +40 -1
  53. package/src/embedder.js +14 -34
  54. package/src/export.js +1 -1
  55. package/src/extractors/javascript.js +130 -5
  56. package/src/flow.js +2 -70
  57. package/src/index.js +30 -20
  58. package/src/{result-formatter.js → infrastructure/result-formatter.js} +1 -1
  59. package/src/kinds.js +1 -0
  60. package/src/manifesto.js +0 -76
  61. package/src/native.js +31 -9
  62. package/src/owners.js +1 -56
  63. package/src/parser.js +53 -2
  64. package/src/queries-cli.js +1 -1
  65. package/src/queries.js +79 -280
  66. package/src/sequence.js +5 -44
  67. package/src/structure.js +16 -75
  68. package/src/triage.js +1 -54
  69. package/src/viewer.js +1 -1
  70. package/src/watcher.js +7 -4
  71. package/src/db/repository.js +0 -134
  72. /package/src/{test-filter.js → infrastructure/test-filter.js} +0 -0
@@ -0,0 +1,243 @@
1
+ /**
2
+ * Visitor: Compute cognitive/cyclomatic complexity, max nesting, and Halstead metrics.
3
+ *
4
+ * Replaces the computeAllMetrics() DFS walk in complexity.js with a visitor that
5
+ * plugs into the unified walkWithVisitors framework. Operates per-function:
6
+ * resets accumulators on enterFunction, emits results on exitFunction.
7
+ */
8
+
9
+ import {
10
+ computeHalsteadDerived,
11
+ computeLOCMetrics,
12
+ computeMaintainabilityIndex,
13
+ } from '../metrics.js';
14
+
15
+ /**
16
+ * Create a complexity visitor for use with walkWithVisitors.
17
+ *
18
+ * When used in file-level mode (walking an entire file), this visitor collects
19
+ * per-function metrics using enterFunction/exitFunction hooks. When used in
20
+ * function-level mode (walking a single function node), it collects one result.
21
+ *
22
+ * @param {object} cRules - COMPLEXITY_RULES for the language
23
+ * @param {object} [hRules] - HALSTEAD_RULES for the language (null if unavailable)
24
+ * @param {object} [options]
25
+ * @param {boolean} [options.fileLevelWalk=false] - true when walking an entire file
26
+ * @returns {Visitor}
27
+ */
28
+ export function createComplexityVisitor(cRules, hRules, options = {}) {
29
+ const { fileLevelWalk = false, langId = null } = options;
30
+
31
+ // Per-function accumulators
32
+ let cognitive = 0;
33
+ let cyclomatic = 1;
34
+ let maxNesting = 0;
35
+ let operators = hRules ? new Map() : null;
36
+ let operands = hRules ? new Map() : null;
37
+ let halsteadSkipDepth = 0;
38
+
39
+ // In file-level mode, we only count when inside a function
40
+ let activeFuncNode = null;
41
+ let activeFuncName = null;
42
+ // Nesting depth relative to the active function (for nested functions)
43
+ let funcDepth = 0;
44
+
45
+ // Collected results (one per function)
46
+ const results = [];
47
+
48
+ function reset() {
49
+ cognitive = 0;
50
+ cyclomatic = 1;
51
+ maxNesting = 0;
52
+ operators = hRules ? new Map() : null;
53
+ operands = hRules ? new Map() : null;
54
+ halsteadSkipDepth = 0;
55
+ }
56
+
57
+ function collectResult(funcNode) {
58
+ const halstead =
59
+ hRules && operators && operands ? computeHalsteadDerived(operators, operands) : null;
60
+ const loc = computeLOCMetrics(funcNode, langId);
61
+ const volume = halstead ? halstead.volume : 0;
62
+ const commentRatio = loc.loc > 0 ? loc.commentLines / loc.loc : 0;
63
+ const mi = computeMaintainabilityIndex(volume, cyclomatic, loc.sloc, commentRatio);
64
+
65
+ return { cognitive, cyclomatic, maxNesting, halstead, loc, mi };
66
+ }
67
+
68
+ return {
69
+ name: 'complexity',
70
+ functionNodeTypes: cRules.functionNodes,
71
+
72
+ enterFunction(funcNode, funcName, _context) {
73
+ if (fileLevelWalk) {
74
+ if (!activeFuncNode) {
75
+ // Top-level function: start fresh
76
+ reset();
77
+ activeFuncNode = funcNode;
78
+ activeFuncName = funcName;
79
+ funcDepth = 0;
80
+ } else {
81
+ // Nested function: increase nesting for complexity
82
+ funcDepth++;
83
+ }
84
+ } else {
85
+ // Function-level mode: track nested functions for correct nesting depth
86
+ funcDepth++;
87
+ }
88
+ },
89
+
90
+ exitFunction(funcNode, _funcName, _context) {
91
+ if (fileLevelWalk) {
92
+ if (funcNode === activeFuncNode) {
93
+ // Leaving the top-level function: emit result
94
+ results.push({
95
+ funcNode,
96
+ funcName: activeFuncName,
97
+ metrics: collectResult(funcNode),
98
+ });
99
+ activeFuncNode = null;
100
+ activeFuncName = null;
101
+ } else {
102
+ funcDepth--;
103
+ }
104
+ } else {
105
+ funcDepth--;
106
+ }
107
+ },
108
+
109
+ enterNode(node, context) {
110
+ // In file-level mode, skip nodes outside any function
111
+ if (fileLevelWalk && !activeFuncNode) return;
112
+
113
+ const type = node.type;
114
+ const nestingLevel = fileLevelWalk ? context.nestingLevel + funcDepth : context.nestingLevel;
115
+
116
+ // ── Halstead classification ──
117
+ if (hRules) {
118
+ if (hRules.skipTypes.has(type)) halsteadSkipDepth++;
119
+ if (halsteadSkipDepth === 0) {
120
+ if (hRules.compoundOperators.has(type)) {
121
+ operators.set(type, (operators.get(type) || 0) + 1);
122
+ }
123
+ if (node.childCount === 0) {
124
+ if (hRules.operatorLeafTypes.has(type)) {
125
+ operators.set(type, (operators.get(type) || 0) + 1);
126
+ } else if (hRules.operandLeafTypes.has(type)) {
127
+ const text = node.text;
128
+ operands.set(text, (operands.get(text) || 0) + 1);
129
+ }
130
+ }
131
+ }
132
+ }
133
+
134
+ // ── Complexity: track nesting depth ──
135
+ if (nestingLevel > maxNesting) maxNesting = nestingLevel;
136
+
137
+ // Handle logical operators in binary expressions
138
+ if (type === cRules.logicalNodeType) {
139
+ const op = node.child(1)?.type;
140
+ if (op && cRules.logicalOperators.has(op)) {
141
+ cyclomatic++;
142
+ const parent = node.parent;
143
+ let sameSequence = false;
144
+ if (parent && parent.type === cRules.logicalNodeType) {
145
+ const parentOp = parent.child(1)?.type;
146
+ if (parentOp === op) sameSequence = true;
147
+ }
148
+ if (!sameSequence) cognitive++;
149
+ // Don't skip children — walker handles recursion
150
+ }
151
+ }
152
+
153
+ // Handle optional chaining (cyclomatic only)
154
+ if (type === cRules.optionalChainType) {
155
+ cyclomatic++;
156
+ }
157
+
158
+ // Handle branch/control flow nodes (skip keyword leaf tokens)
159
+ if (cRules.branchNodes.has(type) && node.childCount > 0) {
160
+ // Pattern A: else clause wraps if (JS/C#/Rust)
161
+ if (cRules.elseNodeType && type === cRules.elseNodeType) {
162
+ const firstChild = node.namedChild(0);
163
+ if (firstChild && firstChild.type === cRules.ifNodeType) {
164
+ // else-if: the if_statement child handles its own increment
165
+ return;
166
+ }
167
+ cognitive++;
168
+ return;
169
+ }
170
+
171
+ // Pattern B: explicit elif node (Python/Ruby/PHP)
172
+ if (cRules.elifNodeType && type === cRules.elifNodeType) {
173
+ cognitive++;
174
+ cyclomatic++;
175
+ return;
176
+ }
177
+
178
+ // Detect else-if via Pattern A or C
179
+ let isElseIf = false;
180
+ if (type === cRules.ifNodeType) {
181
+ if (cRules.elseViaAlternative) {
182
+ isElseIf =
183
+ node.parent?.type === cRules.ifNodeType &&
184
+ node.parent.childForFieldName('alternative')?.id === node.id;
185
+ } else if (cRules.elseNodeType) {
186
+ isElseIf = node.parent?.type === cRules.elseNodeType;
187
+ }
188
+ }
189
+
190
+ if (isElseIf) {
191
+ cognitive++;
192
+ cyclomatic++;
193
+ return;
194
+ }
195
+
196
+ // Regular branch node
197
+ cognitive += 1 + nestingLevel;
198
+ cyclomatic++;
199
+
200
+ if (cRules.switchLikeNodes?.has(type)) {
201
+ cyclomatic--;
202
+ }
203
+
204
+ // Nesting nodes are handled by the walker's nestingNodeTypes option
205
+ // But we still need them to count in complexity — they already do above
206
+ }
207
+
208
+ // Pattern C plain else: block that is the alternative of an if_statement (Go/Java)
209
+ if (
210
+ cRules.elseViaAlternative &&
211
+ type !== cRules.ifNodeType &&
212
+ node.parent?.type === cRules.ifNodeType &&
213
+ node.parent.childForFieldName('alternative')?.id === node.id
214
+ ) {
215
+ cognitive++;
216
+ }
217
+
218
+ // Handle case nodes (cyclomatic only, skip keyword leaves)
219
+ if (cRules.caseNodes.has(type) && node.childCount > 0) {
220
+ cyclomatic++;
221
+ }
222
+
223
+ // Handle nested function definitions (increase nesting)
224
+ // In file-level mode funcDepth handles this; in function-level mode the
225
+ // nestingNodeTypes option should include function nodes
226
+ },
227
+
228
+ exitNode(node) {
229
+ // Decrement skip depth when leaving a skip-type subtree
230
+ if (hRules?.skipTypes.has(node.type)) {
231
+ halsteadSkipDepth--;
232
+ }
233
+ },
234
+
235
+ finish() {
236
+ if (fileLevelWalk) {
237
+ return results;
238
+ }
239
+ // Function-level mode: return single result (no funcNode reference needed)
240
+ return collectResult({ text: '' });
241
+ },
242
+ };
243
+ }
@@ -0,0 +1,358 @@
1
+ /**
2
+ * Visitor: Extract dataflow information (define-use chains, arg flows, mutations).
3
+ *
4
+ * Replaces the standalone extractDataflow() visit logic in dataflow.js with a
5
+ * visitor that plugs into the unified walkWithVisitors framework.
6
+ *
7
+ * NOTE: The original dataflow walk uses `node.namedChildren` while the visitor
8
+ * framework uses `node.child(i)` (all children). This visitor handles both
9
+ * named and unnamed children correctly since the classification logic only
10
+ * cares about specific node types/fields, not about traversal order.
11
+ */
12
+
13
+ import {
14
+ collectIdentifiers,
15
+ extractParamNames,
16
+ extractParams,
17
+ functionName,
18
+ isIdent,
19
+ memberReceiver,
20
+ resolveCalleeName,
21
+ truncate,
22
+ } from '../visitor-utils.js';
23
+
24
+ /**
25
+ * Create a dataflow visitor for use with walkWithVisitors.
26
+ *
27
+ * @param {object} rules - DATAFLOW_RULES for the language
28
+ * @returns {Visitor}
29
+ */
30
+ export function createDataflowVisitor(rules) {
31
+ const isCallNode = rules.callNodes ? (t) => rules.callNodes.has(t) : (t) => t === rules.callNode;
32
+
33
+ const parameters = [];
34
+ const returns = [];
35
+ const assignments = [];
36
+ const argFlows = [];
37
+ const mutations = [];
38
+
39
+ const scopeStack = [];
40
+
41
+ function currentScope() {
42
+ return scopeStack.length > 0 ? scopeStack[scopeStack.length - 1] : null;
43
+ }
44
+
45
+ function findBinding(name) {
46
+ for (let i = scopeStack.length - 1; i >= 0; i--) {
47
+ const scope = scopeStack[i];
48
+ if (scope.params.has(name))
49
+ return { type: 'param', index: scope.params.get(name), funcName: scope.funcName };
50
+ if (scope.locals.has(name))
51
+ return { type: 'local', source: scope.locals.get(name), funcName: scope.funcName };
52
+ }
53
+ return null;
54
+ }
55
+
56
+ function bindingConfidence(binding) {
57
+ if (!binding) return 0.5;
58
+ if (binding.type === 'param') return 1.0;
59
+ if (binding.type === 'local') {
60
+ if (binding.source?.type === 'call_return') return 0.9;
61
+ if (binding.source?.type === 'destructured') return 0.8;
62
+ return 0.9;
63
+ }
64
+ return 0.5;
65
+ }
66
+
67
+ function unwrapAwait(node) {
68
+ if (rules.awaitNode && node.type === rules.awaitNode) {
69
+ return node.namedChildren[0] || node;
70
+ }
71
+ return node;
72
+ }
73
+
74
+ function isCall(node) {
75
+ return node && isCallNode(node.type);
76
+ }
77
+
78
+ function handleVarDeclarator(node) {
79
+ let nameNode = node.childForFieldName(rules.varNameField);
80
+ let valueNode = rules.varValueField ? node.childForFieldName(rules.varValueField) : null;
81
+
82
+ if (!valueNode && rules.equalsClauseType) {
83
+ for (const child of node.namedChildren) {
84
+ if (child.type === rules.equalsClauseType) {
85
+ valueNode = child.childForFieldName('value') || child.namedChildren[0];
86
+ break;
87
+ }
88
+ }
89
+ }
90
+
91
+ if (!valueNode) {
92
+ for (const child of node.namedChildren) {
93
+ if (child !== nameNode && isCall(unwrapAwait(child))) {
94
+ valueNode = child;
95
+ break;
96
+ }
97
+ }
98
+ }
99
+
100
+ if (rules.expressionListType) {
101
+ if (nameNode?.type === rules.expressionListType) nameNode = nameNode.namedChildren[0];
102
+ if (valueNode?.type === rules.expressionListType) valueNode = valueNode.namedChildren[0];
103
+ }
104
+
105
+ const scope = currentScope();
106
+ if (!nameNode || !valueNode || !scope) return;
107
+
108
+ const unwrapped = unwrapAwait(valueNode);
109
+ const callExpr = isCall(unwrapped) ? unwrapped : null;
110
+
111
+ if (callExpr) {
112
+ const callee = resolveCalleeName(callExpr, rules);
113
+ if (callee && scope.funcName) {
114
+ if (
115
+ (rules.objectDestructType && nameNode.type === rules.objectDestructType) ||
116
+ (rules.arrayDestructType && nameNode.type === rules.arrayDestructType)
117
+ ) {
118
+ const names = extractParamNames(nameNode, rules);
119
+ for (const n of names) {
120
+ assignments.push({
121
+ varName: n,
122
+ callerFunc: scope.funcName,
123
+ sourceCallName: callee,
124
+ expression: truncate(node.text),
125
+ line: node.startPosition.row + 1,
126
+ });
127
+ scope.locals.set(n, { type: 'destructured', callee });
128
+ }
129
+ } else {
130
+ const varName =
131
+ nameNode.type === 'identifier' || nameNode.type === rules.paramIdentifier
132
+ ? nameNode.text
133
+ : nameNode.text;
134
+ assignments.push({
135
+ varName,
136
+ callerFunc: scope.funcName,
137
+ sourceCallName: callee,
138
+ expression: truncate(node.text),
139
+ line: node.startPosition.row + 1,
140
+ });
141
+ scope.locals.set(varName, { type: 'call_return', callee });
142
+ }
143
+ }
144
+ }
145
+ }
146
+
147
+ function handleAssignment(node) {
148
+ const left = node.childForFieldName(rules.assignLeftField);
149
+ const right = node.childForFieldName(rules.assignRightField);
150
+ const scope = currentScope();
151
+ if (!scope?.funcName) return;
152
+
153
+ if (left && rules.memberNode && left.type === rules.memberNode) {
154
+ const receiver = memberReceiver(left, rules);
155
+ if (receiver) {
156
+ const binding = findBinding(receiver);
157
+ if (binding) {
158
+ mutations.push({
159
+ funcName: scope.funcName,
160
+ receiverName: receiver,
161
+ binding,
162
+ mutatingExpr: truncate(node.text),
163
+ line: node.startPosition.row + 1,
164
+ });
165
+ }
166
+ }
167
+ }
168
+
169
+ if (left && isIdent(left.type, rules) && right) {
170
+ const unwrapped = unwrapAwait(right);
171
+ const callExpr = isCall(unwrapped) ? unwrapped : null;
172
+ if (callExpr) {
173
+ const callee = resolveCalleeName(callExpr, rules);
174
+ if (callee) {
175
+ assignments.push({
176
+ varName: left.text,
177
+ callerFunc: scope.funcName,
178
+ sourceCallName: callee,
179
+ expression: truncate(node.text),
180
+ line: node.startPosition.row + 1,
181
+ });
182
+ scope.locals.set(left.text, { type: 'call_return', callee });
183
+ }
184
+ }
185
+ }
186
+ }
187
+
188
+ function handleCallExpr(node) {
189
+ const callee = resolveCalleeName(node, rules);
190
+ const argsNode = node.childForFieldName(rules.callArgsField);
191
+ const scope = currentScope();
192
+ if (!callee || !argsNode || !scope?.funcName) return;
193
+
194
+ let argIndex = 0;
195
+ for (let arg of argsNode.namedChildren) {
196
+ if (rules.argumentWrapperType && arg.type === rules.argumentWrapperType) {
197
+ arg = arg.namedChildren[0] || arg;
198
+ }
199
+ const unwrapped =
200
+ rules.spreadType && arg.type === rules.spreadType ? arg.namedChildren[0] || arg : arg;
201
+ if (!unwrapped) {
202
+ argIndex++;
203
+ continue;
204
+ }
205
+
206
+ const argName = isIdent(unwrapped.type, rules) ? unwrapped.text : null;
207
+ const argMember =
208
+ rules.memberNode && unwrapped.type === rules.memberNode
209
+ ? memberReceiver(unwrapped, rules)
210
+ : null;
211
+ const trackedName = argName || argMember;
212
+
213
+ if (trackedName) {
214
+ const binding = findBinding(trackedName);
215
+ if (binding) {
216
+ argFlows.push({
217
+ callerFunc: scope.funcName,
218
+ calleeName: callee,
219
+ argIndex,
220
+ argName: trackedName,
221
+ binding,
222
+ confidence: bindingConfidence(binding),
223
+ expression: truncate(arg.text),
224
+ line: node.startPosition.row + 1,
225
+ });
226
+ }
227
+ }
228
+ argIndex++;
229
+ }
230
+ }
231
+
232
+ function handleExprStmtMutation(node) {
233
+ if (rules.mutatingMethods.size === 0) return;
234
+ const expr = node.namedChildren[0];
235
+ if (!expr || !isCall(expr)) return;
236
+
237
+ let methodName = null;
238
+ let receiver = null;
239
+
240
+ const fn = expr.childForFieldName(rules.callFunctionField);
241
+ if (fn && fn.type === rules.memberNode) {
242
+ const prop = fn.childForFieldName(rules.memberPropertyField);
243
+ methodName = prop ? prop.text : null;
244
+ receiver = memberReceiver(fn, rules);
245
+ }
246
+
247
+ if (!receiver && rules.callObjectField) {
248
+ const obj = expr.childForFieldName(rules.callObjectField);
249
+ const name = expr.childForFieldName(rules.callFunctionField);
250
+ if (obj && name) {
251
+ methodName = name.text;
252
+ receiver = isIdent(obj.type, rules) ? obj.text : null;
253
+ }
254
+ }
255
+
256
+ if (!methodName || !rules.mutatingMethods.has(methodName)) return;
257
+
258
+ const scope = currentScope();
259
+ if (!receiver || !scope?.funcName) return;
260
+
261
+ const binding = findBinding(receiver);
262
+ if (binding) {
263
+ mutations.push({
264
+ funcName: scope.funcName,
265
+ receiverName: receiver,
266
+ binding,
267
+ mutatingExpr: truncate(expr.text),
268
+ line: node.startPosition.row + 1,
269
+ });
270
+ }
271
+ }
272
+
273
+ return {
274
+ name: 'dataflow',
275
+ functionNodeTypes: rules.functionNodes,
276
+
277
+ enterFunction(funcNode, _funcName, _context) {
278
+ const name = functionName(funcNode, rules);
279
+ const paramsNode = funcNode.childForFieldName(rules.paramListField);
280
+ const paramList = extractParams(paramsNode, rules);
281
+ const paramMap = new Map();
282
+ for (const p of paramList) {
283
+ paramMap.set(p.name, p.index);
284
+ if (name) {
285
+ parameters.push({
286
+ funcName: name,
287
+ paramName: p.name,
288
+ paramIndex: p.index,
289
+ line: (paramsNode?.startPosition?.row ?? funcNode.startPosition.row) + 1,
290
+ });
291
+ }
292
+ }
293
+ scopeStack.push({ funcName: name, funcNode, params: paramMap, locals: new Map() });
294
+ },
295
+
296
+ exitFunction(_funcNode, _funcName, _context) {
297
+ scopeStack.pop();
298
+ },
299
+
300
+ enterNode(node, _context) {
301
+ const t = node.type;
302
+
303
+ // Skip function nodes — handled by enterFunction/exitFunction
304
+ if (rules.functionNodes.has(t)) return;
305
+
306
+ // Return statements (skip keyword tokens inside return statements, e.g. Ruby's
307
+ // `return` node nests a `return` keyword child with the same type string)
308
+ if (rules.returnNode && t === rules.returnNode) {
309
+ if (node.parent?.type === rules.returnNode) return; // keyword token, not statement
310
+
311
+ const scope = currentScope();
312
+ if (scope?.funcName) {
313
+ const expr = node.namedChildren[0];
314
+ const referencedNames = [];
315
+ if (expr) collectIdentifiers(expr, referencedNames, rules);
316
+ returns.push({
317
+ funcName: scope.funcName,
318
+ expression: truncate(expr ? expr.text : ''),
319
+ referencedNames,
320
+ line: node.startPosition.row + 1,
321
+ });
322
+ }
323
+ return;
324
+ }
325
+
326
+ // Variable declarations
327
+ if (rules.varDeclaratorNode && t === rules.varDeclaratorNode) {
328
+ handleVarDeclarator(node);
329
+ return;
330
+ }
331
+ if (rules.varDeclaratorNodes?.has(t)) {
332
+ handleVarDeclarator(node);
333
+ return;
334
+ }
335
+
336
+ // Call expressions
337
+ if (isCallNode(t)) {
338
+ handleCallExpr(node);
339
+ return;
340
+ }
341
+
342
+ // Assignment expressions
343
+ if (rules.assignmentNode && t === rules.assignmentNode) {
344
+ handleAssignment(node);
345
+ return;
346
+ }
347
+
348
+ // Mutation detection via expression_statement
349
+ if (rules.expressionStmtNode && t === rules.expressionStmtNode) {
350
+ handleExprStmtMutation(node);
351
+ }
352
+ },
353
+
354
+ finish() {
355
+ return { parameters, returns, assignments, argFlows, mutations };
356
+ },
357
+ };
358
+ }