@optave/codegraph 3.1.0 → 3.1.1

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 (47) hide show
  1. package/README.md +5 -5
  2. package/grammars/tree-sitter-go.wasm +0 -0
  3. package/package.json +8 -9
  4. package/src/ast-analysis/rules/csharp.js +201 -0
  5. package/src/ast-analysis/rules/go.js +182 -0
  6. package/src/ast-analysis/rules/index.js +82 -0
  7. package/src/ast-analysis/rules/java.js +175 -0
  8. package/src/ast-analysis/rules/javascript.js +246 -0
  9. package/src/ast-analysis/rules/php.js +219 -0
  10. package/src/ast-analysis/rules/python.js +196 -0
  11. package/src/ast-analysis/rules/ruby.js +204 -0
  12. package/src/ast-analysis/rules/rust.js +173 -0
  13. package/src/ast-analysis/shared.js +223 -0
  14. package/src/ast.js +15 -28
  15. package/src/audit.js +4 -5
  16. package/src/boundaries.js +1 -1
  17. package/src/branch-compare.js +84 -79
  18. package/src/builder.js +0 -5
  19. package/src/cfg.js +106 -338
  20. package/src/check.js +3 -3
  21. package/src/cli.js +99 -179
  22. package/src/cochange.js +1 -1
  23. package/src/communities.js +13 -16
  24. package/src/complexity.js +196 -1239
  25. package/src/cycles.js +1 -1
  26. package/src/dataflow.js +269 -694
  27. package/src/db/connection.js +88 -0
  28. package/src/db/migrations.js +312 -0
  29. package/src/db/query-builder.js +280 -0
  30. package/src/db/repository.js +134 -0
  31. package/src/db.js +19 -399
  32. package/src/embedder.js +145 -141
  33. package/src/export.js +1 -1
  34. package/src/flow.js +161 -162
  35. package/src/index.js +34 -1
  36. package/src/kinds.js +49 -0
  37. package/src/manifesto.js +3 -8
  38. package/src/mcp.js +37 -20
  39. package/src/owners.js +132 -132
  40. package/src/queries-cli.js +866 -0
  41. package/src/queries.js +1323 -2267
  42. package/src/result-formatter.js +21 -0
  43. package/src/sequence.js +177 -182
  44. package/src/structure.js +200 -199
  45. package/src/test-filter.js +7 -0
  46. package/src/triage.js +120 -162
  47. package/src/viewer.js +1 -1
package/src/dataflow.js CHANGED
@@ -11,427 +11,25 @@
11
11
 
12
12
  import fs from 'node:fs';
13
13
  import path from 'node:path';
14
+ import { DATAFLOW_RULES } from './ast-analysis/rules/index.js';
15
+ import {
16
+ makeDataflowRules as _makeDataflowRules,
17
+ buildExtensionSet,
18
+ buildExtToLangMap,
19
+ } from './ast-analysis/shared.js';
14
20
  import { openReadonlyOrFail } from './db.js';
15
21
  import { info } from './logger.js';
16
22
  import { paginateResult } from './paginate.js';
17
- import { LANGUAGE_REGISTRY } from './parser.js';
18
- import { ALL_SYMBOL_KINDS, isTestFile, normalizeSymbol } from './queries.js';
19
-
20
- // ─── Language-Specific Dataflow Rules ────────────────────────────────────
21
-
22
- const DATAFLOW_DEFAULTS = {
23
- // Scope entry
24
- functionNodes: new Set(), // REQUIRED: non-empty
25
-
26
- // Function name extraction
27
- nameField: 'name',
28
- varAssignedFnParent: null, // parent type for `const fn = ...` (JS only)
29
- assignmentFnParent: null, // parent type for `x = function...` (JS only)
30
- pairFnParent: null, // parent type for `{ key: function }` (JS only)
31
-
32
- // Parameters
33
- paramListField: 'parameters',
34
- paramIdentifier: 'identifier',
35
- paramWrapperTypes: new Set(),
36
- defaultParamType: null,
37
- restParamType: null,
38
- objectDestructType: null,
39
- arrayDestructType: null,
40
- shorthandPropPattern: null,
41
- pairPatternType: null,
42
- extractParamName: null, // override: (node) => string[]
43
-
44
- // Return
45
- returnNode: null,
46
-
47
- // Variable declarations
48
- varDeclaratorNode: null,
49
- varDeclaratorNodes: null,
50
- varNameField: 'name',
51
- varValueField: 'value',
52
- assignmentNode: null,
53
- assignLeftField: 'left',
54
- assignRightField: 'right',
55
-
56
- // Calls
57
- callNode: null,
58
- callNodes: null,
59
- callFunctionField: 'function',
60
- callArgsField: 'arguments',
61
- spreadType: null,
62
-
63
- // Member access
64
- memberNode: null,
65
- memberObjectField: 'object',
66
- memberPropertyField: 'property',
67
- optionalChainNode: null,
68
-
69
- // Await
70
- awaitNode: null,
71
-
72
- // Mutation
73
- mutatingMethods: new Set(),
74
- expressionStmtNode: 'expression_statement',
75
- callObjectField: null, // Java: combined call+member has [object] field on call node
76
-
77
- // Structural wrappers
78
- expressionListType: null, // Go: expression_list wraps LHS/RHS of short_var_declaration
79
- equalsClauseType: null, // C#: equals_value_clause wraps variable initializer
80
- argumentWrapperType: null, // PHP: individual args wrapped in 'argument' nodes
81
- extraIdentifierTypes: null, // Set of additional identifier-like types (PHP: variable_name, name)
82
- };
83
-
84
- const DATAFLOW_RULE_KEYS = new Set(Object.keys(DATAFLOW_DEFAULTS));
85
-
86
- export function makeDataflowRules(overrides) {
87
- for (const key of Object.keys(overrides)) {
88
- if (!DATAFLOW_RULE_KEYS.has(key)) {
89
- throw new Error(`Dataflow rules: unknown key "${key}"`);
90
- }
91
- }
92
- const rules = { ...DATAFLOW_DEFAULTS, ...overrides };
93
- if (!(rules.functionNodes instanceof Set) || rules.functionNodes.size === 0) {
94
- throw new Error('Dataflow rules: functionNodes must be a non-empty Set');
95
- }
96
- return rules;
97
- }
98
23
 
99
- // ── JS / TS / TSX ────────────────────────────────────────────────────────
100
-
101
- const JS_TS_MUTATING = new Set([
102
- 'push',
103
- 'pop',
104
- 'shift',
105
- 'unshift',
106
- 'splice',
107
- 'sort',
108
- 'reverse',
109
- 'fill',
110
- 'set',
111
- 'delete',
112
- 'add',
113
- 'clear',
114
- ]);
115
-
116
- const JS_TS_DATAFLOW = makeDataflowRules({
117
- functionNodes: new Set([
118
- 'function_declaration',
119
- 'method_definition',
120
- 'arrow_function',
121
- 'function_expression',
122
- 'function',
123
- ]),
124
- varAssignedFnParent: 'variable_declarator',
125
- assignmentFnParent: 'assignment_expression',
126
- pairFnParent: 'pair',
127
- paramWrapperTypes: new Set(['required_parameter', 'optional_parameter']),
128
- defaultParamType: 'assignment_pattern',
129
- restParamType: 'rest_pattern',
130
- objectDestructType: 'object_pattern',
131
- arrayDestructType: 'array_pattern',
132
- shorthandPropPattern: 'shorthand_property_identifier_pattern',
133
- pairPatternType: 'pair_pattern',
134
- returnNode: 'return_statement',
135
- varDeclaratorNode: 'variable_declarator',
136
- assignmentNode: 'assignment_expression',
137
- callNode: 'call_expression',
138
- spreadType: 'spread_element',
139
- memberNode: 'member_expression',
140
- optionalChainNode: 'optional_chain_expression',
141
- awaitNode: 'await_expression',
142
- mutatingMethods: JS_TS_MUTATING,
143
- });
144
-
145
- // ── Python ───────────────────────────────────────────────────────────────
146
-
147
- const PYTHON_DATAFLOW = makeDataflowRules({
148
- functionNodes: new Set(['function_definition', 'lambda']),
149
- defaultParamType: 'default_parameter',
150
- restParamType: 'list_splat_pattern',
151
- returnNode: 'return_statement',
152
- varDeclaratorNode: null,
153
- assignmentNode: 'assignment',
154
- assignLeftField: 'left',
155
- assignRightField: 'right',
156
- callNode: 'call',
157
- callFunctionField: 'function',
158
- callArgsField: 'arguments',
159
- spreadType: 'list_splat',
160
- memberNode: 'attribute',
161
- memberObjectField: 'object',
162
- memberPropertyField: 'attribute',
163
- awaitNode: 'await',
164
- mutatingMethods: new Set([
165
- 'append',
166
- 'extend',
167
- 'insert',
168
- 'pop',
169
- 'remove',
170
- 'clear',
171
- 'sort',
172
- 'reverse',
173
- 'add',
174
- 'discard',
175
- 'update',
176
- ]),
177
- extractParamName(node) {
178
- // typed_parameter / typed_default_parameter: first identifier child is the name
179
- if (node.type === 'typed_parameter' || node.type === 'typed_default_parameter') {
180
- for (const c of node.namedChildren) {
181
- if (c.type === 'identifier') return [c.text];
182
- }
183
- return null;
184
- }
185
- if (node.type === 'default_parameter') {
186
- const nameNode = node.childForFieldName('name');
187
- return nameNode ? [nameNode.text] : null;
188
- }
189
- if (node.type === 'list_splat_pattern' || node.type === 'dictionary_splat_pattern') {
190
- for (const c of node.namedChildren) {
191
- if (c.type === 'identifier') return [c.text];
192
- }
193
- return null;
194
- }
195
- return null;
196
- },
197
- });
198
-
199
- // ── Go ───────────────────────────────────────────────────────────────────
200
-
201
- const GO_DATAFLOW = makeDataflowRules({
202
- functionNodes: new Set(['function_declaration', 'method_declaration', 'func_literal']),
203
- returnNode: 'return_statement',
204
- varDeclaratorNodes: new Set(['short_var_declaration', 'var_declaration']),
205
- varNameField: 'left',
206
- varValueField: 'right',
207
- assignmentNode: 'assignment_statement',
208
- assignLeftField: 'left',
209
- assignRightField: 'right',
210
- callNode: 'call_expression',
211
- callFunctionField: 'function',
212
- callArgsField: 'arguments',
213
- memberNode: 'selector_expression',
214
- memberObjectField: 'operand',
215
- memberPropertyField: 'field',
216
- mutatingMethods: new Set(),
217
- expressionListType: 'expression_list',
218
- extractParamName(node) {
219
- // Go: parameter_declaration has name(s) + type; e.g. `a, b int`
220
- if (node.type === 'parameter_declaration') {
221
- const names = [];
222
- for (const c of node.namedChildren) {
223
- if (c.type === 'identifier') names.push(c.text);
224
- }
225
- return names.length > 0 ? names : null;
226
- }
227
- if (node.type === 'variadic_parameter_declaration') {
228
- const nameNode = node.childForFieldName('name');
229
- return nameNode ? [nameNode.text] : null;
230
- }
231
- return null;
232
- },
233
- });
234
-
235
- // ── Rust ─────────────────────────────────────────────────────────────────
236
-
237
- const RUST_DATAFLOW = makeDataflowRules({
238
- functionNodes: new Set(['function_item', 'closure_expression']),
239
- returnNode: 'return_expression',
240
- varDeclaratorNode: 'let_declaration',
241
- varNameField: 'pattern',
242
- varValueField: 'value',
243
- assignmentNode: 'assignment_expression',
244
- callNode: 'call_expression',
245
- callFunctionField: 'function',
246
- callArgsField: 'arguments',
247
- memberNode: 'field_expression',
248
- memberObjectField: 'value',
249
- memberPropertyField: 'field',
250
- awaitNode: 'await_expression',
251
- mutatingMethods: new Set(['push', 'pop', 'insert', 'remove', 'clear', 'sort', 'reverse']),
252
- extractParamName(node) {
253
- if (node.type === 'parameter') {
254
- const pat = node.childForFieldName('pattern');
255
- if (pat?.type === 'identifier') return [pat.text];
256
- return null;
257
- }
258
- if (node.type === 'identifier') return [node.text];
259
- return null;
260
- },
261
- });
262
-
263
- // ── Java ─────────────────────────────────────────────────────────────────
264
-
265
- const JAVA_DATAFLOW = makeDataflowRules({
266
- functionNodes: new Set(['method_declaration', 'constructor_declaration', 'lambda_expression']),
267
- returnNode: 'return_statement',
268
- varDeclaratorNode: 'variable_declarator',
269
- assignmentNode: 'assignment_expression',
270
- callNodes: new Set(['method_invocation', 'object_creation_expression']),
271
- callFunctionField: 'name',
272
- callArgsField: 'arguments',
273
- memberNode: 'field_access',
274
- memberObjectField: 'object',
275
- memberPropertyField: 'field',
276
- callObjectField: 'object',
277
- argumentWrapperType: 'argument',
278
- mutatingMethods: new Set(['add', 'remove', 'clear', 'put', 'set', 'push', 'pop', 'sort']),
279
- extractParamName(node) {
280
- if (node.type === 'formal_parameter' || node.type === 'spread_parameter') {
281
- const nameNode = node.childForFieldName('name');
282
- return nameNode ? [nameNode.text] : null;
283
- }
284
- if (node.type === 'identifier') return [node.text];
285
- return null;
286
- },
287
- });
288
-
289
- // ── C# ───────────────────────────────────────────────────────────────────
290
-
291
- const CSHARP_DATAFLOW = makeDataflowRules({
292
- functionNodes: new Set([
293
- 'method_declaration',
294
- 'constructor_declaration',
295
- 'lambda_expression',
296
- 'local_function_statement',
297
- ]),
298
- returnNode: 'return_statement',
299
- varDeclaratorNode: 'variable_declarator',
300
- varNameField: 'name',
301
- assignmentNode: 'assignment_expression',
302
- callNode: 'invocation_expression',
303
- callFunctionField: 'function',
304
- callArgsField: 'arguments',
305
- memberNode: 'member_access_expression',
306
- memberObjectField: 'expression',
307
- memberPropertyField: 'name',
308
- awaitNode: 'await_expression',
309
- argumentWrapperType: 'argument',
310
- mutatingMethods: new Set(['Add', 'Remove', 'Clear', 'Insert', 'Sort', 'Reverse', 'Push', 'Pop']),
311
- extractParamName(node) {
312
- if (node.type === 'parameter') {
313
- const nameNode = node.childForFieldName('name');
314
- return nameNode ? [nameNode.text] : null;
315
- }
316
- if (node.type === 'identifier') return [node.text];
317
- return null;
318
- },
319
- });
320
-
321
- // ── PHP ──────────────────────────────────────────────────────────────────
322
-
323
- const PHP_DATAFLOW = makeDataflowRules({
324
- functionNodes: new Set([
325
- 'function_definition',
326
- 'method_declaration',
327
- 'anonymous_function_creation_expression',
328
- 'arrow_function',
329
- ]),
330
- paramListField: 'parameters',
331
- paramIdentifier: 'variable_name',
332
- returnNode: 'return_statement',
333
- varDeclaratorNode: null,
334
- assignmentNode: 'assignment_expression',
335
- assignLeftField: 'left',
336
- assignRightField: 'right',
337
- callNodes: new Set([
338
- 'function_call_expression',
339
- 'member_call_expression',
340
- 'scoped_call_expression',
341
- ]),
342
- callFunctionField: 'function',
343
- callArgsField: 'arguments',
344
- spreadType: 'spread_expression',
345
- memberNode: 'member_access_expression',
346
- memberObjectField: 'object',
347
- memberPropertyField: 'name',
348
- argumentWrapperType: 'argument',
349
- extraIdentifierTypes: new Set(['variable_name', 'name']),
350
- mutatingMethods: new Set(['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse']),
351
- extractParamName(node) {
352
- // PHP: simple_parameter → $name or &$name
353
- if (node.type === 'simple_parameter' || node.type === 'variadic_parameter') {
354
- const nameNode = node.childForFieldName('name');
355
- return nameNode ? [nameNode.text] : null;
356
- }
357
- if (node.type === 'variable_name') return [node.text];
358
- return null;
359
- },
360
- });
361
-
362
- // ── Ruby ─────────────────────────────────────────────────────────────────
363
-
364
- const RUBY_DATAFLOW = makeDataflowRules({
365
- functionNodes: new Set(['method', 'singleton_method', 'lambda']),
366
- paramListField: 'parameters',
367
- returnNode: 'return',
368
- varDeclaratorNode: null,
369
- assignmentNode: 'assignment',
370
- assignLeftField: 'left',
371
- assignRightField: 'right',
372
- callNode: 'call',
373
- callFunctionField: 'method',
374
- callArgsField: 'arguments',
375
- spreadType: 'splat_parameter',
376
- memberNode: 'call',
377
- memberObjectField: 'receiver',
378
- memberPropertyField: 'method',
379
- mutatingMethods: new Set([
380
- 'push',
381
- 'pop',
382
- 'shift',
383
- 'unshift',
384
- 'delete',
385
- 'clear',
386
- 'sort!',
387
- 'reverse!',
388
- 'map!',
389
- 'select!',
390
- 'reject!',
391
- 'compact!',
392
- 'flatten!',
393
- 'concat',
394
- 'replace',
395
- 'insert',
396
- ]),
397
- extractParamName(node) {
398
- if (node.type === 'identifier') return [node.text];
399
- if (
400
- node.type === 'optional_parameter' ||
401
- node.type === 'keyword_parameter' ||
402
- node.type === 'splat_parameter' ||
403
- node.type === 'hash_splat_parameter'
404
- ) {
405
- const nameNode = node.childForFieldName('name');
406
- return nameNode ? [nameNode.text] : null;
407
- }
408
- return null;
409
- },
410
- });
411
-
412
- // ── Rules Map + Extensions Set ───────────────────────────────────────────
413
-
414
- export const DATAFLOW_RULES = new Map([
415
- ['javascript', JS_TS_DATAFLOW],
416
- ['typescript', JS_TS_DATAFLOW],
417
- ['tsx', JS_TS_DATAFLOW],
418
- ['python', PYTHON_DATAFLOW],
419
- ['go', GO_DATAFLOW],
420
- ['rust', RUST_DATAFLOW],
421
- ['java', JAVA_DATAFLOW],
422
- ['csharp', CSHARP_DATAFLOW],
423
- ['php', PHP_DATAFLOW],
424
- ['ruby', RUBY_DATAFLOW],
425
- ]);
426
-
427
- const DATAFLOW_LANG_IDS = new Set(DATAFLOW_RULES.keys());
428
-
429
- export const DATAFLOW_EXTENSIONS = new Set();
430
- for (const entry of LANGUAGE_REGISTRY) {
431
- if (DATAFLOW_RULES.has(entry.id)) {
432
- for (const ext of entry.extensions) DATAFLOW_EXTENSIONS.add(ext);
433
- }
434
- }
24
+ import { ALL_SYMBOL_KINDS, normalizeSymbol } from './queries.js';
25
+ import { outputResult } from './result-formatter.js';
26
+ import { isTestFile } from './test-filter.js';
27
+
28
+ // Re-export for backward compatibility
29
+ export { DATAFLOW_RULES };
30
+ export { _makeDataflowRules as makeDataflowRules };
31
+
32
+ export const DATAFLOW_EXTENSIONS = buildExtensionSet(DATAFLOW_RULES);
435
33
 
436
34
  // ── AST helpers ──────────────────────────────────────────────────────────────
437
35
 
@@ -1009,12 +607,7 @@ export async function buildDataflowEdges(db, fileSymbols, rootDir, _engineOpts)
1009
607
 
1010
608
  // Always build ext→langId map so native-only builds (where _langId is unset)
1011
609
  // can still derive the language from the file extension.
1012
- const extToLang = new Map();
1013
- for (const entry of LANGUAGE_REGISTRY) {
1014
- for (const ext of entry.extensions) {
1015
- extToLang.set(ext, entry.id);
1016
- }
1017
- }
610
+ const extToLang = buildExtToLangMap();
1018
611
 
1019
612
  for (const [relPath, symbols] of fileSymbols) {
1020
613
  if (!symbols._tree && !symbols.dataflow) {
@@ -1073,7 +666,7 @@ export async function buildDataflowEdges(db, fileSymbols, rootDir, _engineOpts)
1073
666
  if (!tree) {
1074
667
  if (!getParserFn) continue;
1075
668
  langId = extToLang.get(ext);
1076
- if (!langId || !DATAFLOW_LANG_IDS.has(langId)) continue;
669
+ if (!langId || !DATAFLOW_RULES.has(langId)) continue;
1077
670
 
1078
671
  const absPath = path.join(rootDir, relPath);
1079
672
  let code;
@@ -1221,134 +814,135 @@ function hasDataflowTable(db) {
1221
814
  */
1222
815
  export function dataflowData(name, customDbPath, opts = {}) {
1223
816
  const db = openReadonlyOrFail(customDbPath);
1224
- const noTests = opts.noTests || false;
817
+ try {
818
+ const noTests = opts.noTests || false;
1225
819
 
1226
- if (!hasDataflowTable(db)) {
1227
- db.close();
1228
- return {
1229
- name,
1230
- results: [],
1231
- warning:
1232
- 'No dataflow data found. Rebuild with `codegraph build` (dataflow is now included by default).',
1233
- };
1234
- }
820
+ if (!hasDataflowTable(db)) {
821
+ return {
822
+ name,
823
+ results: [],
824
+ warning:
825
+ 'No dataflow data found. Rebuild with `codegraph build` (dataflow is now included by default).',
826
+ };
827
+ }
1235
828
 
1236
- const nodes = findNodes(db, name, { noTests, file: opts.file, kind: opts.kind });
1237
- if (nodes.length === 0) {
1238
- db.close();
1239
- return { name, results: [] };
1240
- }
829
+ const nodes = findNodes(db, name, { noTests, file: opts.file, kind: opts.kind });
830
+ if (nodes.length === 0) {
831
+ return { name, results: [] };
832
+ }
1241
833
 
1242
- const flowsToOut = db.prepare(
1243
- `SELECT d.*, n.name AS target_name, n.kind AS target_kind, n.file AS target_file, n.line AS target_line
834
+ const flowsToOut = db.prepare(
835
+ `SELECT d.*, n.name AS target_name, n.kind AS target_kind, n.file AS target_file, n.line AS target_line
1244
836
  FROM dataflow d JOIN nodes n ON d.target_id = n.id
1245
837
  WHERE d.source_id = ? AND d.kind = 'flows_to'`,
1246
- );
1247
- const flowsToIn = db.prepare(
1248
- `SELECT d.*, n.name AS source_name, n.kind AS source_kind, n.file AS source_file, n.line AS source_line
838
+ );
839
+ const flowsToIn = db.prepare(
840
+ `SELECT d.*, n.name AS source_name, n.kind AS source_kind, n.file AS source_file, n.line AS source_line
1249
841
  FROM dataflow d JOIN nodes n ON d.source_id = n.id
1250
842
  WHERE d.target_id = ? AND d.kind = 'flows_to'`,
1251
- );
1252
- const returnsOut = db.prepare(
1253
- `SELECT d.*, n.name AS target_name, n.kind AS target_kind, n.file AS target_file, n.line AS target_line
843
+ );
844
+ const returnsOut = db.prepare(
845
+ `SELECT d.*, n.name AS target_name, n.kind AS target_kind, n.file AS target_file, n.line AS target_line
1254
846
  FROM dataflow d JOIN nodes n ON d.target_id = n.id
1255
847
  WHERE d.source_id = ? AND d.kind = 'returns'`,
1256
- );
1257
- const returnsIn = db.prepare(
1258
- `SELECT d.*, n.name AS source_name, n.kind AS source_kind, n.file AS source_file, n.line AS source_line
848
+ );
849
+ const returnsIn = db.prepare(
850
+ `SELECT d.*, n.name AS source_name, n.kind AS source_kind, n.file AS source_file, n.line AS source_line
1259
851
  FROM dataflow d JOIN nodes n ON d.source_id = n.id
1260
852
  WHERE d.target_id = ? AND d.kind = 'returns'`,
1261
- );
1262
- const mutatesOut = db.prepare(
1263
- `SELECT d.*, n.name AS target_name, n.kind AS target_kind, n.file AS target_file, n.line AS target_line
853
+ );
854
+ const mutatesOut = db.prepare(
855
+ `SELECT d.*, n.name AS target_name, n.kind AS target_kind, n.file AS target_file, n.line AS target_line
1264
856
  FROM dataflow d JOIN nodes n ON d.target_id = n.id
1265
857
  WHERE d.source_id = ? AND d.kind = 'mutates'`,
1266
- );
1267
- const mutatesIn = db.prepare(
1268
- `SELECT d.*, n.name AS source_name, n.kind AS source_kind, n.file AS source_file, n.line AS source_line
858
+ );
859
+ const mutatesIn = db.prepare(
860
+ `SELECT d.*, n.name AS source_name, n.kind AS source_kind, n.file AS source_file, n.line AS source_line
1269
861
  FROM dataflow d JOIN nodes n ON d.source_id = n.id
1270
862
  WHERE d.target_id = ? AND d.kind = 'mutates'`,
1271
- );
863
+ );
864
+
865
+ const hc = new Map();
866
+ const results = nodes.map((node) => {
867
+ const sym = normalizeSymbol(node, db, hc);
868
+
869
+ const flowsTo = flowsToOut.all(node.id).map((r) => ({
870
+ target: r.target_name,
871
+ kind: r.target_kind,
872
+ file: r.target_file,
873
+ line: r.line,
874
+ paramIndex: r.param_index,
875
+ expression: r.expression,
876
+ confidence: r.confidence,
877
+ }));
878
+
879
+ const flowsFrom = flowsToIn.all(node.id).map((r) => ({
880
+ source: r.source_name,
881
+ kind: r.source_kind,
882
+ file: r.source_file,
883
+ line: r.line,
884
+ paramIndex: r.param_index,
885
+ expression: r.expression,
886
+ confidence: r.confidence,
887
+ }));
888
+
889
+ const returnConsumers = returnsOut.all(node.id).map((r) => ({
890
+ consumer: r.target_name,
891
+ kind: r.target_kind,
892
+ file: r.target_file,
893
+ line: r.line,
894
+ expression: r.expression,
895
+ }));
896
+
897
+ const returnedBy = returnsIn.all(node.id).map((r) => ({
898
+ producer: r.source_name,
899
+ kind: r.source_kind,
900
+ file: r.source_file,
901
+ line: r.line,
902
+ expression: r.expression,
903
+ }));
904
+
905
+ const mutatesTargets = mutatesOut.all(node.id).map((r) => ({
906
+ target: r.target_name,
907
+ expression: r.expression,
908
+ line: r.line,
909
+ }));
910
+
911
+ const mutatedBy = mutatesIn.all(node.id).map((r) => ({
912
+ source: r.source_name,
913
+ expression: r.expression,
914
+ line: r.line,
915
+ }));
916
+
917
+ if (noTests) {
918
+ const filter = (arr) => arr.filter((r) => !isTestFile(r.file));
919
+ return {
920
+ ...sym,
921
+ flowsTo: filter(flowsTo),
922
+ flowsFrom: filter(flowsFrom),
923
+ returns: returnConsumers.filter((r) => !isTestFile(r.file)),
924
+ returnedBy: returnedBy.filter((r) => !isTestFile(r.file)),
925
+ mutates: mutatesTargets,
926
+ mutatedBy,
927
+ };
928
+ }
1272
929
 
1273
- const hc = new Map();
1274
- const results = nodes.map((node) => {
1275
- const sym = normalizeSymbol(node, db, hc);
1276
-
1277
- const flowsTo = flowsToOut.all(node.id).map((r) => ({
1278
- target: r.target_name,
1279
- kind: r.target_kind,
1280
- file: r.target_file,
1281
- line: r.line,
1282
- paramIndex: r.param_index,
1283
- expression: r.expression,
1284
- confidence: r.confidence,
1285
- }));
1286
-
1287
- const flowsFrom = flowsToIn.all(node.id).map((r) => ({
1288
- source: r.source_name,
1289
- kind: r.source_kind,
1290
- file: r.source_file,
1291
- line: r.line,
1292
- paramIndex: r.param_index,
1293
- expression: r.expression,
1294
- confidence: r.confidence,
1295
- }));
1296
-
1297
- const returnConsumers = returnsOut.all(node.id).map((r) => ({
1298
- consumer: r.target_name,
1299
- kind: r.target_kind,
1300
- file: r.target_file,
1301
- line: r.line,
1302
- expression: r.expression,
1303
- }));
1304
-
1305
- const returnedBy = returnsIn.all(node.id).map((r) => ({
1306
- producer: r.source_name,
1307
- kind: r.source_kind,
1308
- file: r.source_file,
1309
- line: r.line,
1310
- expression: r.expression,
1311
- }));
1312
-
1313
- const mutatesTargets = mutatesOut.all(node.id).map((r) => ({
1314
- target: r.target_name,
1315
- expression: r.expression,
1316
- line: r.line,
1317
- }));
1318
-
1319
- const mutatedBy = mutatesIn.all(node.id).map((r) => ({
1320
- source: r.source_name,
1321
- expression: r.expression,
1322
- line: r.line,
1323
- }));
1324
-
1325
- if (noTests) {
1326
- const filter = (arr) => arr.filter((r) => !isTestFile(r.file));
1327
930
  return {
1328
931
  ...sym,
1329
- flowsTo: filter(flowsTo),
1330
- flowsFrom: filter(flowsFrom),
1331
- returns: returnConsumers.filter((r) => !isTestFile(r.file)),
1332
- returnedBy: returnedBy.filter((r) => !isTestFile(r.file)),
932
+ flowsTo,
933
+ flowsFrom,
934
+ returns: returnConsumers,
935
+ returnedBy,
1333
936
  mutates: mutatesTargets,
1334
937
  mutatedBy,
1335
938
  };
1336
- }
1337
-
1338
- return {
1339
- ...sym,
1340
- flowsTo,
1341
- flowsFrom,
1342
- returns: returnConsumers,
1343
- returnedBy,
1344
- mutates: mutatesTargets,
1345
- mutatedBy,
1346
- };
1347
- });
939
+ });
1348
940
 
1349
- db.close();
1350
- const base = { name, results };
1351
- return paginateResult(base, 'results', { limit: opts.limit, offset: opts.offset });
941
+ const base = { name, results };
942
+ return paginateResult(base, 'results', { limit: opts.limit, offset: opts.offset });
943
+ } finally {
944
+ db.close();
945
+ }
1352
946
  }
1353
947
 
1354
948
  /**
@@ -1362,125 +956,123 @@ export function dataflowData(name, customDbPath, opts = {}) {
1362
956
  */
1363
957
  export function dataflowPathData(from, to, customDbPath, opts = {}) {
1364
958
  const db = openReadonlyOrFail(customDbPath);
1365
- const noTests = opts.noTests || false;
1366
- const maxDepth = opts.maxDepth || 10;
959
+ try {
960
+ const noTests = opts.noTests || false;
961
+ const maxDepth = opts.maxDepth || 10;
1367
962
 
1368
- if (!hasDataflowTable(db)) {
1369
- db.close();
1370
- return {
1371
- from,
1372
- to,
1373
- found: false,
1374
- warning:
1375
- 'No dataflow data found. Rebuild with `codegraph build` (dataflow is now included by default).',
1376
- };
1377
- }
963
+ if (!hasDataflowTable(db)) {
964
+ return {
965
+ from,
966
+ to,
967
+ found: false,
968
+ warning:
969
+ 'No dataflow data found. Rebuild with `codegraph build` (dataflow is now included by default).',
970
+ };
971
+ }
1378
972
 
1379
- const fromNodes = findNodes(db, from, { noTests, file: opts.fromFile, kind: opts.kind });
1380
- if (fromNodes.length === 0) {
1381
- db.close();
1382
- return { from, to, found: false, error: `No symbol matching "${from}"` };
1383
- }
973
+ const fromNodes = findNodes(db, from, { noTests, file: opts.fromFile, kind: opts.kind });
974
+ if (fromNodes.length === 0) {
975
+ return { from, to, found: false, error: `No symbol matching "${from}"` };
976
+ }
1384
977
 
1385
- const toNodes = findNodes(db, to, { noTests, file: opts.toFile, kind: opts.kind });
1386
- if (toNodes.length === 0) {
1387
- db.close();
1388
- return { from, to, found: false, error: `No symbol matching "${to}"` };
1389
- }
978
+ const toNodes = findNodes(db, to, { noTests, file: opts.toFile, kind: opts.kind });
979
+ if (toNodes.length === 0) {
980
+ return { from, to, found: false, error: `No symbol matching "${to}"` };
981
+ }
1390
982
 
1391
- const sourceNode = fromNodes[0];
1392
- const targetNode = toNodes[0];
983
+ const sourceNode = fromNodes[0];
984
+ const targetNode = toNodes[0];
1393
985
 
1394
- if (sourceNode.id === targetNode.id) {
1395
- const hc = new Map();
1396
- const sym = normalizeSymbol(sourceNode, db, hc);
1397
- db.close();
1398
- return {
1399
- from,
1400
- to,
1401
- found: true,
1402
- hops: 0,
1403
- path: [{ ...sym, edgeKind: null }],
1404
- };
1405
- }
986
+ if (sourceNode.id === targetNode.id) {
987
+ const hc = new Map();
988
+ const sym = normalizeSymbol(sourceNode, db, hc);
989
+ return {
990
+ from,
991
+ to,
992
+ found: true,
993
+ hops: 0,
994
+ path: [{ ...sym, edgeKind: null }],
995
+ };
996
+ }
1406
997
 
1407
- // BFS through flows_to and returns edges
1408
- const neighborStmt = db.prepare(
1409
- `SELECT n.id, n.name, n.kind, n.file, n.line, d.kind AS edge_kind, d.expression
998
+ // BFS through flows_to and returns edges
999
+ const neighborStmt = db.prepare(
1000
+ `SELECT n.id, n.name, n.kind, n.file, n.line, d.kind AS edge_kind, d.expression
1410
1001
  FROM dataflow d JOIN nodes n ON d.target_id = n.id
1411
1002
  WHERE d.source_id = ? AND d.kind IN ('flows_to', 'returns')`,
1412
- );
1003
+ );
1413
1004
 
1414
- const visited = new Set([sourceNode.id]);
1415
- const parent = new Map();
1416
- let queue = [sourceNode.id];
1417
- let found = false;
1418
-
1419
- for (let depth = 1; depth <= maxDepth; depth++) {
1420
- const nextQueue = [];
1421
- for (const currentId of queue) {
1422
- const neighbors = neighborStmt.all(currentId);
1423
- for (const n of neighbors) {
1424
- if (noTests && isTestFile(n.file)) continue;
1425
- if (n.id === targetNode.id) {
1426
- if (!found) {
1427
- found = true;
1005
+ const visited = new Set([sourceNode.id]);
1006
+ const parent = new Map();
1007
+ let queue = [sourceNode.id];
1008
+ let found = false;
1009
+
1010
+ for (let depth = 1; depth <= maxDepth; depth++) {
1011
+ const nextQueue = [];
1012
+ for (const currentId of queue) {
1013
+ const neighbors = neighborStmt.all(currentId);
1014
+ for (const n of neighbors) {
1015
+ if (noTests && isTestFile(n.file)) continue;
1016
+ if (n.id === targetNode.id) {
1017
+ if (!found) {
1018
+ found = true;
1019
+ parent.set(n.id, {
1020
+ parentId: currentId,
1021
+ edgeKind: n.edge_kind,
1022
+ expression: n.expression,
1023
+ });
1024
+ }
1025
+ continue;
1026
+ }
1027
+ if (!visited.has(n.id)) {
1028
+ visited.add(n.id);
1428
1029
  parent.set(n.id, {
1429
1030
  parentId: currentId,
1430
1031
  edgeKind: n.edge_kind,
1431
1032
  expression: n.expression,
1432
1033
  });
1034
+ nextQueue.push(n.id);
1433
1035
  }
1434
- continue;
1435
- }
1436
- if (!visited.has(n.id)) {
1437
- visited.add(n.id);
1438
- parent.set(n.id, {
1439
- parentId: currentId,
1440
- edgeKind: n.edge_kind,
1441
- expression: n.expression,
1442
- });
1443
- nextQueue.push(n.id);
1444
1036
  }
1445
1037
  }
1038
+ if (found) break;
1039
+ queue = nextQueue;
1040
+ if (queue.length === 0) break;
1446
1041
  }
1447
- if (found) break;
1448
- queue = nextQueue;
1449
- if (queue.length === 0) break;
1450
- }
1451
1042
 
1452
- if (!found) {
1453
- db.close();
1454
- return { from, to, found: false };
1455
- }
1043
+ if (!found) {
1044
+ return { from, to, found: false };
1045
+ }
1456
1046
 
1457
- // Reconstruct path
1458
- const nodeById = db.prepare('SELECT * FROM nodes WHERE id = ?');
1459
- const hc = new Map();
1460
- const pathItems = [];
1461
- let cur = targetNode.id;
1462
- while (cur !== undefined) {
1463
- const nodeRow = nodeById.get(cur);
1464
- const parentInfo = parent.get(cur);
1465
- pathItems.unshift({
1466
- ...normalizeSymbol(nodeRow, db, hc),
1467
- edgeKind: parentInfo?.edgeKind ?? null,
1468
- expression: parentInfo?.expression ?? null,
1469
- });
1470
- cur = parentInfo?.parentId;
1471
- if (cur === sourceNode.id) {
1472
- const srcRow = nodeById.get(cur);
1047
+ // Reconstruct path
1048
+ const nodeById = db.prepare('SELECT * FROM nodes WHERE id = ?');
1049
+ const hc = new Map();
1050
+ const pathItems = [];
1051
+ let cur = targetNode.id;
1052
+ while (cur !== undefined) {
1053
+ const nodeRow = nodeById.get(cur);
1054
+ const parentInfo = parent.get(cur);
1473
1055
  pathItems.unshift({
1474
- ...normalizeSymbol(srcRow, db, hc),
1475
- edgeKind: null,
1476
- expression: null,
1056
+ ...normalizeSymbol(nodeRow, db, hc),
1057
+ edgeKind: parentInfo?.edgeKind ?? null,
1058
+ expression: parentInfo?.expression ?? null,
1477
1059
  });
1478
- break;
1060
+ cur = parentInfo?.parentId;
1061
+ if (cur === sourceNode.id) {
1062
+ const srcRow = nodeById.get(cur);
1063
+ pathItems.unshift({
1064
+ ...normalizeSymbol(srcRow, db, hc),
1065
+ edgeKind: null,
1066
+ expression: null,
1067
+ });
1068
+ break;
1069
+ }
1479
1070
  }
1480
- }
1481
1071
 
1482
- db.close();
1483
- return { from, to, found: true, hops: pathItems.length - 1, path: pathItems };
1072
+ return { from, to, found: true, hops: pathItems.length - 1, path: pathItems };
1073
+ } finally {
1074
+ db.close();
1075
+ }
1484
1076
  }
1485
1077
 
1486
1078
  /**
@@ -1493,66 +1085,67 @@ export function dataflowPathData(from, to, customDbPath, opts = {}) {
1493
1085
  */
1494
1086
  export function dataflowImpactData(name, customDbPath, opts = {}) {
1495
1087
  const db = openReadonlyOrFail(customDbPath);
1496
- const maxDepth = opts.depth || 5;
1497
- const noTests = opts.noTests || false;
1088
+ try {
1089
+ const maxDepth = opts.depth || 5;
1090
+ const noTests = opts.noTests || false;
1498
1091
 
1499
- if (!hasDataflowTable(db)) {
1500
- db.close();
1501
- return {
1502
- name,
1503
- results: [],
1504
- warning:
1505
- 'No dataflow data found. Rebuild with `codegraph build` (dataflow is now included by default).',
1506
- };
1507
- }
1092
+ if (!hasDataflowTable(db)) {
1093
+ return {
1094
+ name,
1095
+ results: [],
1096
+ warning:
1097
+ 'No dataflow data found. Rebuild with `codegraph build` (dataflow is now included by default).',
1098
+ };
1099
+ }
1508
1100
 
1509
- const nodes = findNodes(db, name, { noTests, file: opts.file, kind: opts.kind });
1510
- if (nodes.length === 0) {
1511
- db.close();
1512
- return { name, results: [] };
1513
- }
1101
+ const nodes = findNodes(db, name, { noTests, file: opts.file, kind: opts.kind });
1102
+ if (nodes.length === 0) {
1103
+ return { name, results: [] };
1104
+ }
1514
1105
 
1515
- // Forward BFS: who consumes this function's return value (directly or transitively)?
1516
- const consumersStmt = db.prepare(
1517
- `SELECT DISTINCT n.*
1106
+ // Forward BFS: who consumes this function's return value (directly or transitively)?
1107
+ const consumersStmt = db.prepare(
1108
+ `SELECT DISTINCT n.*
1518
1109
  FROM dataflow d JOIN nodes n ON d.target_id = n.id
1519
1110
  WHERE d.source_id = ? AND d.kind = 'returns'`,
1520
- );
1111
+ );
1521
1112
 
1522
- const hc = new Map();
1523
- const results = nodes.map((node) => {
1524
- const sym = normalizeSymbol(node, db, hc);
1525
- const visited = new Set([node.id]);
1526
- const levels = {};
1527
- let frontier = [node.id];
1528
-
1529
- for (let d = 1; d <= maxDepth; d++) {
1530
- const nextFrontier = [];
1531
- for (const fid of frontier) {
1532
- const consumers = consumersStmt.all(fid);
1533
- for (const c of consumers) {
1534
- if (!visited.has(c.id) && (!noTests || !isTestFile(c.file))) {
1535
- visited.add(c.id);
1536
- nextFrontier.push(c.id);
1537
- if (!levels[d]) levels[d] = [];
1538
- levels[d].push(normalizeSymbol(c, db, hc));
1113
+ const hc = new Map();
1114
+ const results = nodes.map((node) => {
1115
+ const sym = normalizeSymbol(node, db, hc);
1116
+ const visited = new Set([node.id]);
1117
+ const levels = {};
1118
+ let frontier = [node.id];
1119
+
1120
+ for (let d = 1; d <= maxDepth; d++) {
1121
+ const nextFrontier = [];
1122
+ for (const fid of frontier) {
1123
+ const consumers = consumersStmt.all(fid);
1124
+ for (const c of consumers) {
1125
+ if (!visited.has(c.id) && (!noTests || !isTestFile(c.file))) {
1126
+ visited.add(c.id);
1127
+ nextFrontier.push(c.id);
1128
+ if (!levels[d]) levels[d] = [];
1129
+ levels[d].push(normalizeSymbol(c, db, hc));
1130
+ }
1539
1131
  }
1540
1132
  }
1133
+ frontier = nextFrontier;
1134
+ if (frontier.length === 0) break;
1541
1135
  }
1542
- frontier = nextFrontier;
1543
- if (frontier.length === 0) break;
1544
- }
1545
1136
 
1546
- return {
1547
- ...sym,
1548
- levels,
1549
- totalAffected: visited.size - 1,
1550
- };
1551
- });
1137
+ return {
1138
+ ...sym,
1139
+ levels,
1140
+ totalAffected: visited.size - 1,
1141
+ };
1142
+ });
1552
1143
 
1553
- db.close();
1554
- const base = { name, results };
1555
- return paginateResult(base, 'results', { limit: opts.limit, offset: opts.offset });
1144
+ const base = { name, results };
1145
+ return paginateResult(base, 'results', { limit: opts.limit, offset: opts.offset });
1146
+ } finally {
1147
+ db.close();
1148
+ }
1556
1149
  }
1557
1150
 
1558
1151
  // ── Display formatters ──────────────────────────────────────────────────────
@@ -1567,16 +1160,7 @@ export function dataflow(name, customDbPath, opts = {}) {
1567
1160
 
1568
1161
  const data = dataflowData(name, customDbPath, opts);
1569
1162
 
1570
- if (opts.json) {
1571
- console.log(JSON.stringify(data, null, 2));
1572
- return;
1573
- }
1574
- if (opts.ndjson) {
1575
- for (const r of data.results) {
1576
- console.log(JSON.stringify(r));
1577
- }
1578
- return;
1579
- }
1163
+ if (outputResult(data, 'results', opts)) return;
1580
1164
 
1581
1165
  if (data.warning) {
1582
1166
  console.log(`⚠ ${data.warning}`);
@@ -1650,16 +1234,7 @@ function dataflowImpact(name, customDbPath, opts = {}) {
1650
1234
  offset: opts.offset,
1651
1235
  });
1652
1236
 
1653
- if (opts.json) {
1654
- console.log(JSON.stringify(data, null, 2));
1655
- return;
1656
- }
1657
- if (opts.ndjson) {
1658
- for (const r of data.results) {
1659
- console.log(JSON.stringify(r));
1660
- }
1661
- return;
1662
- }
1237
+ if (outputResult(data, 'results', opts)) return;
1663
1238
 
1664
1239
  if (data.warning) {
1665
1240
  console.log(`⚠ ${data.warning}`);