@optave/codegraph 3.1.1 → 3.1.2

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 (68) hide show
  1. package/package.json +7 -7
  2. package/src/ast-analysis/engine.js +365 -0
  3. package/src/ast-analysis/metrics.js +118 -0
  4. package/src/ast-analysis/visitor-utils.js +176 -0
  5. package/src/ast-analysis/visitor.js +162 -0
  6. package/src/ast-analysis/visitors/ast-store-visitor.js +150 -0
  7. package/src/ast-analysis/visitors/cfg-visitor.js +792 -0
  8. package/src/ast-analysis/visitors/complexity-visitor.js +243 -0
  9. package/src/ast-analysis/visitors/dataflow-visitor.js +358 -0
  10. package/src/ast.js +13 -140
  11. package/src/audit.js +2 -87
  12. package/src/batch.js +0 -25
  13. package/src/boundaries.js +1 -1
  14. package/src/branch-compare.js +1 -96
  15. package/src/builder.js +48 -179
  16. package/src/cfg.js +89 -883
  17. package/src/check.js +1 -84
  18. package/src/cli.js +20 -19
  19. package/src/cochange.js +1 -39
  20. package/src/commands/audit.js +88 -0
  21. package/src/commands/batch.js +26 -0
  22. package/src/commands/branch-compare.js +97 -0
  23. package/src/commands/cfg.js +55 -0
  24. package/src/commands/check.js +82 -0
  25. package/src/commands/cochange.js +37 -0
  26. package/src/commands/communities.js +69 -0
  27. package/src/commands/complexity.js +77 -0
  28. package/src/commands/dataflow.js +110 -0
  29. package/src/commands/flow.js +70 -0
  30. package/src/commands/manifesto.js +77 -0
  31. package/src/commands/owners.js +52 -0
  32. package/src/commands/query.js +21 -0
  33. package/src/commands/sequence.js +33 -0
  34. package/src/commands/structure.js +64 -0
  35. package/src/commands/triage.js +49 -0
  36. package/src/communities.js +12 -83
  37. package/src/complexity.js +42 -356
  38. package/src/cycles.js +1 -1
  39. package/src/dataflow.js +12 -665
  40. package/src/db/repository/build-stmts.js +104 -0
  41. package/src/db/repository/cfg.js +83 -0
  42. package/src/db/repository/cochange.js +41 -0
  43. package/src/db/repository/complexity.js +15 -0
  44. package/src/db/repository/dataflow.js +12 -0
  45. package/src/db/repository/edges.js +259 -0
  46. package/src/db/repository/embeddings.js +40 -0
  47. package/src/db/repository/graph-read.js +39 -0
  48. package/src/db/repository/index.js +42 -0
  49. package/src/db/repository/nodes.js +236 -0
  50. package/src/db.js +40 -1
  51. package/src/embedder.js +14 -34
  52. package/src/export.js +1 -1
  53. package/src/extractors/javascript.js +130 -5
  54. package/src/flow.js +2 -70
  55. package/src/index.js +23 -19
  56. package/src/{result-formatter.js → infrastructure/result-formatter.js} +1 -1
  57. package/src/kinds.js +1 -0
  58. package/src/manifesto.js +0 -76
  59. package/src/owners.js +1 -56
  60. package/src/queries-cli.js +1 -1
  61. package/src/queries.js +79 -280
  62. package/src/sequence.js +5 -44
  63. package/src/structure.js +16 -75
  64. package/src/triage.js +1 -54
  65. package/src/viewer.js +1 -1
  66. package/src/watcher.js +7 -4
  67. package/src/db/repository.js +0 -134
  68. /package/src/{test-filter.js → infrastructure/test-filter.js} +0 -0
package/src/dataflow.js CHANGED
@@ -17,13 +17,13 @@ import {
17
17
  buildExtensionSet,
18
18
  buildExtToLangMap,
19
19
  } from './ast-analysis/shared.js';
20
- import { openReadonlyOrFail } from './db.js';
20
+ import { walkWithVisitors } from './ast-analysis/visitor.js';
21
+ import { createDataflowVisitor } from './ast-analysis/visitors/dataflow-visitor.js';
22
+ import { hasDataflowTable, openReadonlyOrFail } from './db.js';
23
+ import { isTestFile } from './infrastructure/test-filter.js';
21
24
  import { info } from './logger.js';
22
25
  import { paginateResult } from './paginate.js';
23
-
24
26
  import { ALL_SYMBOL_KINDS, normalizeSymbol } from './queries.js';
25
- import { outputResult } from './result-formatter.js';
26
- import { isTestFile } from './test-filter.js';
27
27
 
28
28
  // Re-export for backward compatibility
29
29
  export { DATAFLOW_RULES };
@@ -31,172 +31,13 @@ export { _makeDataflowRules as makeDataflowRules };
31
31
 
32
32
  export const DATAFLOW_EXTENSIONS = buildExtensionSet(DATAFLOW_RULES);
33
33
 
34
- // ── AST helpers ──────────────────────────────────────────────────────────────
35
-
36
- function truncate(str, max = 120) {
37
- if (!str) return '';
38
- return str.length > max ? `${str.slice(0, max)}…` : str;
39
- }
40
-
41
- /**
42
- * Get the name of a function node from the AST using rules.
43
- */
44
- function functionName(fnNode, rules) {
45
- if (!fnNode) return null;
46
- // Try the standard name field first (works for most languages)
47
- const nameNode = fnNode.childForFieldName(rules.nameField);
48
- if (nameNode) return nameNode.text;
49
-
50
- // JS-specific: arrow_function/function_expression assigned to variable, pair, or assignment
51
- const parent = fnNode.parent;
52
- if (parent) {
53
- if (rules.varAssignedFnParent && parent.type === rules.varAssignedFnParent) {
54
- const n = parent.childForFieldName('name');
55
- return n ? n.text : null;
56
- }
57
- if (rules.pairFnParent && parent.type === rules.pairFnParent) {
58
- const keyNode = parent.childForFieldName('key');
59
- return keyNode ? keyNode.text : null;
60
- }
61
- if (rules.assignmentFnParent && parent.type === rules.assignmentFnParent) {
62
- const left = parent.childForFieldName(rules.assignLeftField);
63
- return left ? left.text : null;
64
- }
65
- }
66
- return null;
67
- }
68
-
69
- /**
70
- * Extract parameter names and indices from a formal_parameters node.
71
- */
72
- function extractParams(paramsNode, rules) {
73
- if (!paramsNode) return [];
74
- const result = [];
75
- let index = 0;
76
- for (const child of paramsNode.namedChildren) {
77
- const names = extractParamNames(child, rules);
78
- for (const name of names) {
79
- result.push({ name, index });
80
- }
81
- index++;
82
- }
83
- return result;
84
- }
85
-
86
- function extractParamNames(node, rules) {
87
- if (!node) return [];
88
- const t = node.type;
89
-
90
- // Language-specific override (Go, Rust, Java, C#, PHP, Ruby)
91
- if (rules.extractParamName) {
92
- const result = rules.extractParamName(node);
93
- if (result) return result;
94
- }
95
-
96
- // Leaf identifier
97
- if (t === rules.paramIdentifier) return [node.text];
98
-
99
- // Wrapper types (TS required_parameter, Python typed_parameter, etc.)
100
- if (rules.paramWrapperTypes.has(t)) {
101
- const pattern = node.childForFieldName('pattern') || node.childForFieldName('name');
102
- return pattern ? extractParamNames(pattern, rules) : [];
103
- }
104
-
105
- // Default parameter (assignment_pattern / default_parameter)
106
- if (rules.defaultParamType && t === rules.defaultParamType) {
107
- const left = node.childForFieldName('left') || node.childForFieldName('name');
108
- return left ? extractParamNames(left, rules) : [];
109
- }
110
-
111
- // Rest / splat parameter
112
- if (rules.restParamType && t === rules.restParamType) {
113
- // Try name field first, then fall back to scanning children
114
- const nameNode = node.childForFieldName('name');
115
- if (nameNode) return [nameNode.text];
116
- for (const child of node.namedChildren) {
117
- if (child.type === rules.paramIdentifier) return [child.text];
118
- }
119
- return [];
120
- }
121
-
122
- // Object destructuring (JS only)
123
- if (rules.objectDestructType && t === rules.objectDestructType) {
124
- const names = [];
125
- for (const child of node.namedChildren) {
126
- if (rules.shorthandPropPattern && child.type === rules.shorthandPropPattern) {
127
- names.push(child.text);
128
- } else if (rules.pairPatternType && child.type === rules.pairPatternType) {
129
- const value = child.childForFieldName('value');
130
- if (value) names.push(...extractParamNames(value, rules));
131
- } else if (rules.restParamType && child.type === rules.restParamType) {
132
- names.push(...extractParamNames(child, rules));
133
- }
134
- }
135
- return names;
136
- }
137
-
138
- // Array destructuring (JS only)
139
- if (rules.arrayDestructType && t === rules.arrayDestructType) {
140
- const names = [];
141
- for (const child of node.namedChildren) {
142
- names.push(...extractParamNames(child, rules));
143
- }
144
- return names;
145
- }
146
-
147
- return [];
148
- }
149
-
150
- /** Check if a node type is identifier-like for this language. */
151
- function isIdent(nodeType, rules) {
152
- if (nodeType === 'identifier' || nodeType === rules.paramIdentifier) return true;
153
- return rules.extraIdentifierTypes ? rules.extraIdentifierTypes.has(nodeType) : false;
154
- }
155
-
156
- /**
157
- * Resolve the name a call expression is calling using rules.
158
- */
159
- function resolveCalleeName(callNode, rules) {
160
- const fn = callNode.childForFieldName(rules.callFunctionField);
161
- if (!fn) {
162
- // Some languages (Java method_invocation, Ruby call) use 'name' field directly
163
- const nameNode = callNode.childForFieldName('name') || callNode.childForFieldName('method');
164
- return nameNode ? nameNode.text : null;
165
- }
166
- if (isIdent(fn.type, rules)) return fn.text;
167
- if (fn.type === rules.memberNode) {
168
- const prop = fn.childForFieldName(rules.memberPropertyField);
169
- return prop ? prop.text : null;
170
- }
171
- if (rules.optionalChainNode && fn.type === rules.optionalChainNode) {
172
- const target = fn.namedChildren[0];
173
- if (!target) return null;
174
- if (target.type === rules.memberNode) {
175
- const prop = target.childForFieldName(rules.memberPropertyField);
176
- return prop ? prop.text : null;
177
- }
178
- if (target.type === 'identifier') return target.text;
179
- const prop = fn.childForFieldName(rules.memberPropertyField);
180
- return prop ? prop.text : null;
181
- }
182
- return null;
183
- }
184
-
185
- /**
186
- * Get the receiver (object) of a member expression using rules.
187
- */
188
- function memberReceiver(memberExpr, rules) {
189
- const obj = memberExpr.childForFieldName(rules.memberObjectField);
190
- if (!obj) return null;
191
- if (isIdent(obj.type, rules)) return obj.text;
192
- if (obj.type === rules.memberNode) return memberReceiver(obj, rules);
193
- return null;
194
- }
34
+ // ── AST helpers (now in ast-analysis/visitor-utils.js, kept as re-exports) ──
195
35
 
196
36
  // ── extractDataflow ──────────────────────────────────────────────────────────
197
37
 
198
38
  /**
199
39
  * Extract dataflow information from a parsed AST.
40
+ * Delegates to the dataflow visitor via the unified walker.
200
41
  *
201
42
  * @param {object} tree - tree-sitter parse tree
202
43
  * @param {string} filePath - relative file path
@@ -208,385 +49,13 @@ export function extractDataflow(tree, _filePath, _definitions, langId = 'javascr
208
49
  const rules = DATAFLOW_RULES.get(langId);
209
50
  if (!rules) return { parameters: [], returns: [], assignments: [], argFlows: [], mutations: [] };
210
51
 
211
- const isCallNode = rules.callNodes ? (t) => rules.callNodes.has(t) : (t) => t === rules.callNode;
212
-
213
- const parameters = [];
214
- const returns = [];
215
- const assignments = [];
216
- const argFlows = [];
217
- const mutations = [];
218
-
219
- const scopeStack = [];
220
-
221
- function currentScope() {
222
- return scopeStack.length > 0 ? scopeStack[scopeStack.length - 1] : null;
223
- }
224
-
225
- function findBinding(name) {
226
- for (let i = scopeStack.length - 1; i >= 0; i--) {
227
- const scope = scopeStack[i];
228
- if (scope.params.has(name))
229
- return { type: 'param', index: scope.params.get(name), funcName: scope.funcName };
230
- if (scope.locals.has(name))
231
- return { type: 'local', source: scope.locals.get(name), funcName: scope.funcName };
232
- }
233
- return null;
234
- }
235
-
236
- function enterScope(fnNode) {
237
- const name = functionName(fnNode, rules);
238
- const paramsNode = fnNode.childForFieldName(rules.paramListField);
239
- const paramList = extractParams(paramsNode, rules);
240
- const paramMap = new Map();
241
- for (const p of paramList) {
242
- paramMap.set(p.name, p.index);
243
- if (name) {
244
- parameters.push({
245
- funcName: name,
246
- paramName: p.name,
247
- paramIndex: p.index,
248
- line: (paramsNode?.startPosition?.row ?? fnNode.startPosition.row) + 1,
249
- });
250
- }
251
- }
252
- scopeStack.push({ funcName: name, funcNode: fnNode, params: paramMap, locals: new Map() });
253
- }
254
-
255
- function exitScope() {
256
- scopeStack.pop();
257
- }
258
-
259
- function bindingConfidence(binding) {
260
- if (!binding) return 0.5;
261
- if (binding.type === 'param') return 1.0;
262
- if (binding.type === 'local') {
263
- if (binding.source?.type === 'call_return') return 0.9;
264
- if (binding.source?.type === 'destructured') return 0.8;
265
- return 0.9;
266
- }
267
- return 0.5;
268
- }
269
-
270
- /** Unwrap await if present, returning the inner expression. */
271
- function unwrapAwait(node) {
272
- if (rules.awaitNode && node.type === rules.awaitNode) {
273
- return node.namedChildren[0] || node;
274
- }
275
- return node;
276
- }
277
-
278
- /** Check if a node is a call expression (single or multi-type). */
279
- function isCall(node) {
280
- return node && isCallNode(node.type);
281
- }
282
-
283
- /** Handle a variable declarator / short_var_declaration node. */
284
- function handleVarDeclarator(node) {
285
- let nameNode = node.childForFieldName(rules.varNameField);
286
- let valueNode = rules.varValueField ? node.childForFieldName(rules.varValueField) : null;
287
-
288
- // C#: initializer is inside equals_value_clause child
289
- if (!valueNode && rules.equalsClauseType) {
290
- for (const child of node.namedChildren) {
291
- if (child.type === rules.equalsClauseType) {
292
- valueNode = child.childForFieldName('value') || child.namedChildren[0];
293
- break;
294
- }
295
- }
296
- }
297
-
298
- // Fallback: initializer is a direct unnamed child (C# variable_declarator)
299
- if (!valueNode) {
300
- for (const child of node.namedChildren) {
301
- if (child !== nameNode && isCall(unwrapAwait(child))) {
302
- valueNode = child;
303
- break;
304
- }
305
- }
306
- }
307
-
308
- // Go: expression_list wraps LHS/RHS — unwrap to first named child
309
- if (rules.expressionListType) {
310
- if (nameNode?.type === rules.expressionListType) nameNode = nameNode.namedChildren[0];
311
- if (valueNode?.type === rules.expressionListType) valueNode = valueNode.namedChildren[0];
312
- }
313
-
314
- const scope = currentScope();
315
- if (!nameNode || !valueNode || !scope) return;
316
-
317
- const unwrapped = unwrapAwait(valueNode);
318
- const callExpr = isCall(unwrapped) ? unwrapped : null;
319
-
320
- if (callExpr) {
321
- const callee = resolveCalleeName(callExpr, rules);
322
- if (callee && scope.funcName) {
323
- // Destructuring: const { a, b } = foo()
324
- if (
325
- (rules.objectDestructType && nameNode.type === rules.objectDestructType) ||
326
- (rules.arrayDestructType && nameNode.type === rules.arrayDestructType)
327
- ) {
328
- const names = extractParamNames(nameNode, rules);
329
- for (const n of names) {
330
- assignments.push({
331
- varName: n,
332
- callerFunc: scope.funcName,
333
- sourceCallName: callee,
334
- expression: truncate(node.text),
335
- line: node.startPosition.row + 1,
336
- });
337
- scope.locals.set(n, { type: 'destructured', callee });
338
- }
339
- } else {
340
- const varName =
341
- nameNode.type === 'identifier' || nameNode.type === rules.paramIdentifier
342
- ? nameNode.text
343
- : nameNode.text;
344
- assignments.push({
345
- varName,
346
- callerFunc: scope.funcName,
347
- sourceCallName: callee,
348
- expression: truncate(node.text),
349
- line: node.startPosition.row + 1,
350
- });
351
- scope.locals.set(varName, { type: 'call_return', callee });
352
- }
353
- }
354
- }
355
- }
356
-
357
- /** Handle assignment expressions (mutation detection + call captures). */
358
- function handleAssignment(node) {
359
- const left = node.childForFieldName(rules.assignLeftField);
360
- const right = node.childForFieldName(rules.assignRightField);
361
- const scope = currentScope();
362
- if (!scope?.funcName) return;
363
-
364
- // Mutation: obj.prop = value
365
- if (left && rules.memberNode && left.type === rules.memberNode) {
366
- const receiver = memberReceiver(left, rules);
367
- if (receiver) {
368
- const binding = findBinding(receiver);
369
- if (binding) {
370
- mutations.push({
371
- funcName: scope.funcName,
372
- receiverName: receiver,
373
- binding,
374
- mutatingExpr: truncate(node.text),
375
- line: node.startPosition.row + 1,
376
- });
377
- }
378
- }
379
- }
380
-
381
- // Non-declaration assignment: x = foo()
382
- if (left && isIdent(left.type, rules) && right) {
383
- const unwrapped = unwrapAwait(right);
384
- const callExpr = isCall(unwrapped) ? unwrapped : null;
385
- if (callExpr) {
386
- const callee = resolveCalleeName(callExpr, rules);
387
- if (callee) {
388
- assignments.push({
389
- varName: left.text,
390
- callerFunc: scope.funcName,
391
- sourceCallName: callee,
392
- expression: truncate(node.text),
393
- line: node.startPosition.row + 1,
394
- });
395
- scope.locals.set(left.text, { type: 'call_return', callee });
396
- }
397
- }
398
- }
399
- }
400
-
401
- /** Handle call expressions: track argument flows. */
402
- function handleCallExpr(node) {
403
- const callee = resolveCalleeName(node, rules);
404
- const argsNode = node.childForFieldName(rules.callArgsField);
405
- const scope = currentScope();
406
- if (!callee || !argsNode || !scope?.funcName) return;
407
-
408
- let argIndex = 0;
409
- for (let arg of argsNode.namedChildren) {
410
- // PHP/Java: unwrap argument wrapper
411
- if (rules.argumentWrapperType && arg.type === rules.argumentWrapperType) {
412
- arg = arg.namedChildren[0] || arg;
413
- }
414
- const unwrapped =
415
- rules.spreadType && arg.type === rules.spreadType ? arg.namedChildren[0] || arg : arg;
416
- if (!unwrapped) {
417
- argIndex++;
418
- continue;
419
- }
420
-
421
- const argName = isIdent(unwrapped.type, rules) ? unwrapped.text : null;
422
- const argMember =
423
- rules.memberNode && unwrapped.type === rules.memberNode
424
- ? memberReceiver(unwrapped, rules)
425
- : null;
426
- const trackedName = argName || argMember;
427
-
428
- if (trackedName) {
429
- const binding = findBinding(trackedName);
430
- if (binding) {
431
- argFlows.push({
432
- callerFunc: scope.funcName,
433
- calleeName: callee,
434
- argIndex,
435
- argName: trackedName,
436
- binding,
437
- confidence: bindingConfidence(binding),
438
- expression: truncate(arg.text),
439
- line: node.startPosition.row + 1,
440
- });
441
- }
442
- }
443
- argIndex++;
444
- }
445
- }
446
-
447
- /** Detect mutating method calls in expression statements. */
448
- function handleExprStmtMutation(node) {
449
- if (rules.mutatingMethods.size === 0) return;
450
- const expr = node.namedChildren[0];
451
- if (!expr || !isCall(expr)) return;
452
-
453
- let methodName = null;
454
- let receiver = null;
455
-
456
- // Standard pattern: call(fn: member(obj, prop))
457
- const fn = expr.childForFieldName(rules.callFunctionField);
458
- if (fn && fn.type === rules.memberNode) {
459
- const prop = fn.childForFieldName(rules.memberPropertyField);
460
- methodName = prop ? prop.text : null;
461
- receiver = memberReceiver(fn, rules);
462
- }
463
-
464
- // Java/combined pattern: call node itself has object + name fields
465
- if (!receiver && rules.callObjectField) {
466
- const obj = expr.childForFieldName(rules.callObjectField);
467
- const name = expr.childForFieldName(rules.callFunctionField);
468
- if (obj && name) {
469
- methodName = name.text;
470
- receiver = isIdent(obj.type, rules) ? obj.text : null;
471
- }
472
- }
473
-
474
- if (!methodName || !rules.mutatingMethods.has(methodName)) return;
475
-
476
- const scope = currentScope();
477
- if (!receiver || !scope?.funcName) return;
478
-
479
- const binding = findBinding(receiver);
480
- if (binding) {
481
- mutations.push({
482
- funcName: scope.funcName,
483
- receiverName: receiver,
484
- binding,
485
- mutatingExpr: truncate(expr.text),
486
- line: node.startPosition.row + 1,
487
- });
488
- }
489
- }
490
-
491
- // Recursive AST walk
492
- function visit(node) {
493
- if (!node) return;
494
- const t = node.type;
495
-
496
- // Enter function scopes
497
- if (rules.functionNodes.has(t)) {
498
- enterScope(node);
499
- for (const child of node.namedChildren) {
500
- visit(child);
501
- }
502
- exitScope();
503
- return;
504
- }
505
-
506
- // Return statements
507
- if (rules.returnNode && t === rules.returnNode) {
508
- const scope = currentScope();
509
- if (scope?.funcName) {
510
- const expr = node.namedChildren[0];
511
- const referencedNames = [];
512
- if (expr) collectIdentifiers(expr, referencedNames, rules);
513
- returns.push({
514
- funcName: scope.funcName,
515
- expression: truncate(expr ? expr.text : ''),
516
- referencedNames,
517
- line: node.startPosition.row + 1,
518
- });
519
- }
520
- for (const child of node.namedChildren) {
521
- visit(child);
522
- }
523
- return;
524
- }
525
-
526
- // Variable declarations
527
- if (rules.varDeclaratorNode && t === rules.varDeclaratorNode) {
528
- handleVarDeclarator(node);
529
- for (const child of node.namedChildren) {
530
- visit(child);
531
- }
532
- return;
533
- }
534
- if (rules.varDeclaratorNodes?.has(t)) {
535
- handleVarDeclarator(node);
536
- for (const child of node.namedChildren) {
537
- visit(child);
538
- }
539
- return;
540
- }
541
-
542
- // Call expressions
543
- if (isCallNode(t)) {
544
- handleCallExpr(node);
545
- for (const child of node.namedChildren) {
546
- visit(child);
547
- }
548
- return;
549
- }
550
-
551
- // Assignment expressions
552
- if (rules.assignmentNode && t === rules.assignmentNode) {
553
- handleAssignment(node);
554
- for (const child of node.namedChildren) {
555
- visit(child);
556
- }
557
- return;
558
- }
559
-
560
- // Mutation detection via expression_statement
561
- if (rules.expressionStmtNode && t === rules.expressionStmtNode) {
562
- handleExprStmtMutation(node);
563
- }
564
-
565
- // Default: visit all children
566
- for (const child of node.namedChildren) {
567
- visit(child);
568
- }
569
- }
570
-
571
- visit(tree.rootNode);
572
-
573
- return { parameters, returns, assignments, argFlows, mutations };
574
- }
52
+ const visitor = createDataflowVisitor(rules);
53
+ const results = walkWithVisitors(tree.rootNode, [visitor], langId, {
54
+ functionNodeTypes: rules.functionNodes,
55
+ getFunctionName: () => null, // dataflow visitor handles its own name extraction
56
+ });
575
57
 
576
- /**
577
- * Collect all identifier names referenced within a node.
578
- * Uses isIdent() to support language-specific identifier node types
579
- * (e.g. PHP's `variable_name`).
580
- */
581
- function collectIdentifiers(node, out, rules) {
582
- if (!node) return;
583
- if (isIdent(node.type, rules)) {
584
- out.push(node.text);
585
- return;
586
- }
587
- for (const child of node.namedChildren) {
588
- collectIdentifiers(child, out, rules);
589
- }
58
+ return results.dataflow;
590
59
  }
591
60
 
592
61
  // ── buildDataflowEdges ──────────────────────────────────────────────────────
@@ -792,18 +261,6 @@ function findNodes(db, name, opts = {}) {
792
261
  return opts.noTests ? rows.filter((n) => !isTestFile(n.file)) : rows;
793
262
  }
794
263
 
795
- /**
796
- * Check if the dataflow table exists and has data.
797
- */
798
- function hasDataflowTable(db) {
799
- try {
800
- const row = db.prepare('SELECT COUNT(*) as c FROM dataflow').get();
801
- return row.c > 0;
802
- } catch {
803
- return false;
804
- }
805
- }
806
-
807
264
  /**
808
265
  * Return all dataflow edges for a symbol.
809
266
  *
@@ -1147,113 +604,3 @@ export function dataflowImpactData(name, customDbPath, opts = {}) {
1147
604
  db.close();
1148
605
  }
1149
606
  }
1150
-
1151
- // ── Display formatters ──────────────────────────────────────────────────────
1152
-
1153
- /**
1154
- * CLI display for dataflow command.
1155
- */
1156
- export function dataflow(name, customDbPath, opts = {}) {
1157
- if (opts.impact) {
1158
- return dataflowImpact(name, customDbPath, opts);
1159
- }
1160
-
1161
- const data = dataflowData(name, customDbPath, opts);
1162
-
1163
- if (outputResult(data, 'results', opts)) return;
1164
-
1165
- if (data.warning) {
1166
- console.log(`⚠ ${data.warning}`);
1167
- return;
1168
- }
1169
- if (data.results.length === 0) {
1170
- console.log(`No symbols matching "${name}".`);
1171
- return;
1172
- }
1173
-
1174
- for (const r of data.results) {
1175
- console.log(`\n${r.kind} ${r.name} (${r.file}:${r.line})`);
1176
- console.log('─'.repeat(60));
1177
-
1178
- if (r.flowsTo.length > 0) {
1179
- console.log('\n Data flows TO:');
1180
- for (const f of r.flowsTo) {
1181
- const conf = f.confidence < 1.0 ? ` [${(f.confidence * 100).toFixed(0)}%]` : '';
1182
- console.log(` → ${f.target} (${f.file}:${f.line}) arg[${f.paramIndex}]${conf}`);
1183
- }
1184
- }
1185
-
1186
- if (r.flowsFrom.length > 0) {
1187
- console.log('\n Data flows FROM:');
1188
- for (const f of r.flowsFrom) {
1189
- const conf = f.confidence < 1.0 ? ` [${(f.confidence * 100).toFixed(0)}%]` : '';
1190
- console.log(` ← ${f.source} (${f.file}:${f.line}) arg[${f.paramIndex}]${conf}`);
1191
- }
1192
- }
1193
-
1194
- if (r.returns.length > 0) {
1195
- console.log('\n Return value consumed by:');
1196
- for (const c of r.returns) {
1197
- console.log(` → ${c.consumer} (${c.file}:${c.line}) ${c.expression}`);
1198
- }
1199
- }
1200
-
1201
- if (r.returnedBy.length > 0) {
1202
- console.log('\n Uses return value of:');
1203
- for (const p of r.returnedBy) {
1204
- console.log(` ← ${p.producer} (${p.file}:${p.line}) ${p.expression}`);
1205
- }
1206
- }
1207
-
1208
- if (r.mutates.length > 0) {
1209
- console.log('\n Mutates:');
1210
- for (const m of r.mutates) {
1211
- console.log(` ✎ ${m.expression} (line ${m.line})`);
1212
- }
1213
- }
1214
-
1215
- if (r.mutatedBy.length > 0) {
1216
- console.log('\n Mutated by:');
1217
- for (const m of r.mutatedBy) {
1218
- console.log(` ✎ ${m.source} — ${m.expression} (line ${m.line})`);
1219
- }
1220
- }
1221
- }
1222
- }
1223
-
1224
- /**
1225
- * CLI display for dataflow --impact.
1226
- */
1227
- function dataflowImpact(name, customDbPath, opts = {}) {
1228
- const data = dataflowImpactData(name, customDbPath, {
1229
- noTests: opts.noTests,
1230
- depth: opts.depth ? Number(opts.depth) : 5,
1231
- file: opts.file,
1232
- kind: opts.kind,
1233
- limit: opts.limit,
1234
- offset: opts.offset,
1235
- });
1236
-
1237
- if (outputResult(data, 'results', opts)) return;
1238
-
1239
- if (data.warning) {
1240
- console.log(`⚠ ${data.warning}`);
1241
- return;
1242
- }
1243
- if (data.results.length === 0) {
1244
- console.log(`No symbols matching "${name}".`);
1245
- return;
1246
- }
1247
-
1248
- for (const r of data.results) {
1249
- console.log(
1250
- `\n${r.kind} ${r.name} (${r.file}:${r.line}) — ${r.totalAffected} data-dependent consumer${r.totalAffected !== 1 ? 's' : ''}`,
1251
- );
1252
- for (const [level, items] of Object.entries(r.levels)) {
1253
- console.log(` Level ${level}:`);
1254
- for (const item of items) {
1255
- console.log(` ${item.name} (${item.file}:${item.line})`);
1256
- }
1257
- }
1258
- }
1259
- }