@optave/codegraph 3.0.4 → 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 (49) hide show
  1. package/README.md +59 -52
  2. package/grammars/tree-sitter-go.wasm +0 -0
  3. package/package.json +9 -10
  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 +274 -159
  19. package/src/cfg.js +111 -341
  20. package/src/check.js +3 -3
  21. package/src/cli.js +122 -167
  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 +274 -697
  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 -392
  32. package/src/embedder.js +145 -141
  33. package/src/export.js +1 -1
  34. package/src/flow.js +160 -228
  35. package/src/index.js +36 -2
  36. package/src/kinds.js +49 -0
  37. package/src/manifesto.js +3 -8
  38. package/src/mcp.js +97 -20
  39. package/src/owners.js +132 -132
  40. package/src/parser.js +58 -131
  41. package/src/queries-cli.js +866 -0
  42. package/src/queries.js +1356 -2261
  43. package/src/resolve.js +11 -2
  44. package/src/result-formatter.js +21 -0
  45. package/src/sequence.js +364 -0
  46. package/src/structure.js +200 -199
  47. package/src/test-filter.js +7 -0
  48. package/src/triage.js +120 -162
  49. 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
 
@@ -1005,9 +603,12 @@ function collectIdentifiers(node, out, rules) {
1005
603
  export async function buildDataflowEdges(db, fileSymbols, rootDir, _engineOpts) {
1006
604
  // Lazily init WASM parsers if needed
1007
605
  let parsers = null;
1008
- let extToLang = null;
1009
606
  let needsFallback = false;
1010
607
 
608
+ // Always build ext→langId map so native-only builds (where _langId is unset)
609
+ // can still derive the language from the file extension.
610
+ const extToLang = buildExtToLangMap();
611
+
1011
612
  for (const [relPath, symbols] of fileSymbols) {
1012
613
  if (!symbols._tree && !symbols.dataflow) {
1013
614
  const ext = path.extname(relPath).toLowerCase();
@@ -1021,12 +622,6 @@ export async function buildDataflowEdges(db, fileSymbols, rootDir, _engineOpts)
1021
622
  if (needsFallback) {
1022
623
  const { createParsers } = await import('./parser.js');
1023
624
  parsers = await createParsers();
1024
- extToLang = new Map();
1025
- for (const entry of LANGUAGE_REGISTRY) {
1026
- for (const ext of entry.extensions) {
1027
- extToLang.set(ext, entry.id);
1028
- }
1029
- }
1030
625
  }
1031
626
 
1032
627
  let getParserFn = null;
@@ -1069,9 +664,9 @@ export async function buildDataflowEdges(db, fileSymbols, rootDir, _engineOpts)
1069
664
 
1070
665
  // WASM fallback if no cached tree
1071
666
  if (!tree) {
1072
- if (!extToLang || !getParserFn) continue;
667
+ if (!getParserFn) continue;
1073
668
  langId = extToLang.get(ext);
1074
- if (!langId || !DATAFLOW_LANG_IDS.has(langId)) continue;
669
+ if (!langId || !DATAFLOW_RULES.has(langId)) continue;
1075
670
 
1076
671
  const absPath = path.join(rootDir, relPath);
1077
672
  let code;
@@ -1092,7 +687,7 @@ export async function buildDataflowEdges(db, fileSymbols, rootDir, _engineOpts)
1092
687
  }
1093
688
 
1094
689
  if (!langId) {
1095
- langId = extToLang ? extToLang.get(ext) : null;
690
+ langId = extToLang.get(ext);
1096
691
  if (!langId) continue;
1097
692
  }
1098
693
 
@@ -1219,134 +814,135 @@ function hasDataflowTable(db) {
1219
814
  */
1220
815
  export function dataflowData(name, customDbPath, opts = {}) {
1221
816
  const db = openReadonlyOrFail(customDbPath);
1222
- const noTests = opts.noTests || false;
817
+ try {
818
+ const noTests = opts.noTests || false;
1223
819
 
1224
- if (!hasDataflowTable(db)) {
1225
- db.close();
1226
- return {
1227
- name,
1228
- results: [],
1229
- warning:
1230
- 'No dataflow data found. Rebuild with `codegraph build` (dataflow is now included by default).',
1231
- };
1232
- }
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
+ }
1233
828
 
1234
- const nodes = findNodes(db, name, { noTests, file: opts.file, kind: opts.kind });
1235
- if (nodes.length === 0) {
1236
- db.close();
1237
- return { name, results: [] };
1238
- }
829
+ const nodes = findNodes(db, name, { noTests, file: opts.file, kind: opts.kind });
830
+ if (nodes.length === 0) {
831
+ return { name, results: [] };
832
+ }
1239
833
 
1240
- const flowsToOut = db.prepare(
1241
- `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
1242
836
  FROM dataflow d JOIN nodes n ON d.target_id = n.id
1243
837
  WHERE d.source_id = ? AND d.kind = 'flows_to'`,
1244
- );
1245
- const flowsToIn = db.prepare(
1246
- `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
1247
841
  FROM dataflow d JOIN nodes n ON d.source_id = n.id
1248
842
  WHERE d.target_id = ? AND d.kind = 'flows_to'`,
1249
- );
1250
- const returnsOut = db.prepare(
1251
- `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
1252
846
  FROM dataflow d JOIN nodes n ON d.target_id = n.id
1253
847
  WHERE d.source_id = ? AND d.kind = 'returns'`,
1254
- );
1255
- const returnsIn = db.prepare(
1256
- `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
1257
851
  FROM dataflow d JOIN nodes n ON d.source_id = n.id
1258
852
  WHERE d.target_id = ? AND d.kind = 'returns'`,
1259
- );
1260
- const mutatesOut = db.prepare(
1261
- `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
1262
856
  FROM dataflow d JOIN nodes n ON d.target_id = n.id
1263
857
  WHERE d.source_id = ? AND d.kind = 'mutates'`,
1264
- );
1265
- const mutatesIn = db.prepare(
1266
- `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
1267
861
  FROM dataflow d JOIN nodes n ON d.source_id = n.id
1268
862
  WHERE d.target_id = ? AND d.kind = 'mutates'`,
1269
- );
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
+ }
1270
929
 
1271
- const hc = new Map();
1272
- const results = nodes.map((node) => {
1273
- const sym = normalizeSymbol(node, db, hc);
1274
-
1275
- const flowsTo = flowsToOut.all(node.id).map((r) => ({
1276
- target: r.target_name,
1277
- kind: r.target_kind,
1278
- file: r.target_file,
1279
- line: r.line,
1280
- paramIndex: r.param_index,
1281
- expression: r.expression,
1282
- confidence: r.confidence,
1283
- }));
1284
-
1285
- const flowsFrom = flowsToIn.all(node.id).map((r) => ({
1286
- source: r.source_name,
1287
- kind: r.source_kind,
1288
- file: r.source_file,
1289
- line: r.line,
1290
- paramIndex: r.param_index,
1291
- expression: r.expression,
1292
- confidence: r.confidence,
1293
- }));
1294
-
1295
- const returnConsumers = returnsOut.all(node.id).map((r) => ({
1296
- consumer: r.target_name,
1297
- kind: r.target_kind,
1298
- file: r.target_file,
1299
- line: r.line,
1300
- expression: r.expression,
1301
- }));
1302
-
1303
- const returnedBy = returnsIn.all(node.id).map((r) => ({
1304
- producer: r.source_name,
1305
- kind: r.source_kind,
1306
- file: r.source_file,
1307
- line: r.line,
1308
- expression: r.expression,
1309
- }));
1310
-
1311
- const mutatesTargets = mutatesOut.all(node.id).map((r) => ({
1312
- target: r.target_name,
1313
- expression: r.expression,
1314
- line: r.line,
1315
- }));
1316
-
1317
- const mutatedBy = mutatesIn.all(node.id).map((r) => ({
1318
- source: r.source_name,
1319
- expression: r.expression,
1320
- line: r.line,
1321
- }));
1322
-
1323
- if (noTests) {
1324
- const filter = (arr) => arr.filter((r) => !isTestFile(r.file));
1325
930
  return {
1326
931
  ...sym,
1327
- flowsTo: filter(flowsTo),
1328
- flowsFrom: filter(flowsFrom),
1329
- returns: returnConsumers.filter((r) => !isTestFile(r.file)),
1330
- returnedBy: returnedBy.filter((r) => !isTestFile(r.file)),
932
+ flowsTo,
933
+ flowsFrom,
934
+ returns: returnConsumers,
935
+ returnedBy,
1331
936
  mutates: mutatesTargets,
1332
937
  mutatedBy,
1333
938
  };
1334
- }
1335
-
1336
- return {
1337
- ...sym,
1338
- flowsTo,
1339
- flowsFrom,
1340
- returns: returnConsumers,
1341
- returnedBy,
1342
- mutates: mutatesTargets,
1343
- mutatedBy,
1344
- };
1345
- });
939
+ });
1346
940
 
1347
- db.close();
1348
- const base = { name, results };
1349
- 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
+ }
1350
946
  }
1351
947
 
1352
948
  /**
@@ -1360,125 +956,123 @@ export function dataflowData(name, customDbPath, opts = {}) {
1360
956
  */
1361
957
  export function dataflowPathData(from, to, customDbPath, opts = {}) {
1362
958
  const db = openReadonlyOrFail(customDbPath);
1363
- const noTests = opts.noTests || false;
1364
- const maxDepth = opts.maxDepth || 10;
959
+ try {
960
+ const noTests = opts.noTests || false;
961
+ const maxDepth = opts.maxDepth || 10;
1365
962
 
1366
- if (!hasDataflowTable(db)) {
1367
- db.close();
1368
- return {
1369
- from,
1370
- to,
1371
- found: false,
1372
- warning:
1373
- 'No dataflow data found. Rebuild with `codegraph build` (dataflow is now included by default).',
1374
- };
1375
- }
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
+ }
1376
972
 
1377
- const fromNodes = findNodes(db, from, { noTests, file: opts.fromFile, kind: opts.kind });
1378
- if (fromNodes.length === 0) {
1379
- db.close();
1380
- return { from, to, found: false, error: `No symbol matching "${from}"` };
1381
- }
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
+ }
1382
977
 
1383
- const toNodes = findNodes(db, to, { noTests, file: opts.toFile, kind: opts.kind });
1384
- if (toNodes.length === 0) {
1385
- db.close();
1386
- return { from, to, found: false, error: `No symbol matching "${to}"` };
1387
- }
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
+ }
1388
982
 
1389
- const sourceNode = fromNodes[0];
1390
- const targetNode = toNodes[0];
983
+ const sourceNode = fromNodes[0];
984
+ const targetNode = toNodes[0];
1391
985
 
1392
- if (sourceNode.id === targetNode.id) {
1393
- const hc = new Map();
1394
- const sym = normalizeSymbol(sourceNode, db, hc);
1395
- db.close();
1396
- return {
1397
- from,
1398
- to,
1399
- found: true,
1400
- hops: 0,
1401
- path: [{ ...sym, edgeKind: null }],
1402
- };
1403
- }
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
+ }
1404
997
 
1405
- // BFS through flows_to and returns edges
1406
- const neighborStmt = db.prepare(
1407
- `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
1408
1001
  FROM dataflow d JOIN nodes n ON d.target_id = n.id
1409
1002
  WHERE d.source_id = ? AND d.kind IN ('flows_to', 'returns')`,
1410
- );
1003
+ );
1411
1004
 
1412
- const visited = new Set([sourceNode.id]);
1413
- const parent = new Map();
1414
- let queue = [sourceNode.id];
1415
- let found = false;
1416
-
1417
- for (let depth = 1; depth <= maxDepth; depth++) {
1418
- const nextQueue = [];
1419
- for (const currentId of queue) {
1420
- const neighbors = neighborStmt.all(currentId);
1421
- for (const n of neighbors) {
1422
- if (noTests && isTestFile(n.file)) continue;
1423
- if (n.id === targetNode.id) {
1424
- if (!found) {
1425
- 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);
1426
1029
  parent.set(n.id, {
1427
1030
  parentId: currentId,
1428
1031
  edgeKind: n.edge_kind,
1429
1032
  expression: n.expression,
1430
1033
  });
1034
+ nextQueue.push(n.id);
1431
1035
  }
1432
- continue;
1433
- }
1434
- if (!visited.has(n.id)) {
1435
- visited.add(n.id);
1436
- parent.set(n.id, {
1437
- parentId: currentId,
1438
- edgeKind: n.edge_kind,
1439
- expression: n.expression,
1440
- });
1441
- nextQueue.push(n.id);
1442
1036
  }
1443
1037
  }
1038
+ if (found) break;
1039
+ queue = nextQueue;
1040
+ if (queue.length === 0) break;
1444
1041
  }
1445
- if (found) break;
1446
- queue = nextQueue;
1447
- if (queue.length === 0) break;
1448
- }
1449
1042
 
1450
- if (!found) {
1451
- db.close();
1452
- return { from, to, found: false };
1453
- }
1043
+ if (!found) {
1044
+ return { from, to, found: false };
1045
+ }
1454
1046
 
1455
- // Reconstruct path
1456
- const nodeById = db.prepare('SELECT * FROM nodes WHERE id = ?');
1457
- const hc = new Map();
1458
- const pathItems = [];
1459
- let cur = targetNode.id;
1460
- while (cur !== undefined) {
1461
- const nodeRow = nodeById.get(cur);
1462
- const parentInfo = parent.get(cur);
1463
- pathItems.unshift({
1464
- ...normalizeSymbol(nodeRow, db, hc),
1465
- edgeKind: parentInfo?.edgeKind ?? null,
1466
- expression: parentInfo?.expression ?? null,
1467
- });
1468
- cur = parentInfo?.parentId;
1469
- if (cur === sourceNode.id) {
1470
- 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);
1471
1055
  pathItems.unshift({
1472
- ...normalizeSymbol(srcRow, db, hc),
1473
- edgeKind: null,
1474
- expression: null,
1056
+ ...normalizeSymbol(nodeRow, db, hc),
1057
+ edgeKind: parentInfo?.edgeKind ?? null,
1058
+ expression: parentInfo?.expression ?? null,
1475
1059
  });
1476
- 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
+ }
1477
1070
  }
1478
- }
1479
1071
 
1480
- db.close();
1481
- 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
+ }
1482
1076
  }
1483
1077
 
1484
1078
  /**
@@ -1491,66 +1085,67 @@ export function dataflowPathData(from, to, customDbPath, opts = {}) {
1491
1085
  */
1492
1086
  export function dataflowImpactData(name, customDbPath, opts = {}) {
1493
1087
  const db = openReadonlyOrFail(customDbPath);
1494
- const maxDepth = opts.depth || 5;
1495
- const noTests = opts.noTests || false;
1088
+ try {
1089
+ const maxDepth = opts.depth || 5;
1090
+ const noTests = opts.noTests || false;
1496
1091
 
1497
- if (!hasDataflowTable(db)) {
1498
- db.close();
1499
- return {
1500
- name,
1501
- results: [],
1502
- warning:
1503
- 'No dataflow data found. Rebuild with `codegraph build` (dataflow is now included by default).',
1504
- };
1505
- }
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
+ }
1506
1100
 
1507
- const nodes = findNodes(db, name, { noTests, file: opts.file, kind: opts.kind });
1508
- if (nodes.length === 0) {
1509
- db.close();
1510
- return { name, results: [] };
1511
- }
1101
+ const nodes = findNodes(db, name, { noTests, file: opts.file, kind: opts.kind });
1102
+ if (nodes.length === 0) {
1103
+ return { name, results: [] };
1104
+ }
1512
1105
 
1513
- // Forward BFS: who consumes this function's return value (directly or transitively)?
1514
- const consumersStmt = db.prepare(
1515
- `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.*
1516
1109
  FROM dataflow d JOIN nodes n ON d.target_id = n.id
1517
1110
  WHERE d.source_id = ? AND d.kind = 'returns'`,
1518
- );
1111
+ );
1519
1112
 
1520
- const hc = new Map();
1521
- const results = nodes.map((node) => {
1522
- const sym = normalizeSymbol(node, db, hc);
1523
- const visited = new Set([node.id]);
1524
- const levels = {};
1525
- let frontier = [node.id];
1526
-
1527
- for (let d = 1; d <= maxDepth; d++) {
1528
- const nextFrontier = [];
1529
- for (const fid of frontier) {
1530
- const consumers = consumersStmt.all(fid);
1531
- for (const c of consumers) {
1532
- if (!visited.has(c.id) && (!noTests || !isTestFile(c.file))) {
1533
- visited.add(c.id);
1534
- nextFrontier.push(c.id);
1535
- if (!levels[d]) levels[d] = [];
1536
- 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
+ }
1537
1131
  }
1538
1132
  }
1133
+ frontier = nextFrontier;
1134
+ if (frontier.length === 0) break;
1539
1135
  }
1540
- frontier = nextFrontier;
1541
- if (frontier.length === 0) break;
1542
- }
1543
1136
 
1544
- return {
1545
- ...sym,
1546
- levels,
1547
- totalAffected: visited.size - 1,
1548
- };
1549
- });
1137
+ return {
1138
+ ...sym,
1139
+ levels,
1140
+ totalAffected: visited.size - 1,
1141
+ };
1142
+ });
1550
1143
 
1551
- db.close();
1552
- const base = { name, results };
1553
- 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
+ }
1554
1149
  }
1555
1150
 
1556
1151
  // ── Display formatters ──────────────────────────────────────────────────────
@@ -1565,16 +1160,7 @@ export function dataflow(name, customDbPath, opts = {}) {
1565
1160
 
1566
1161
  const data = dataflowData(name, customDbPath, opts);
1567
1162
 
1568
- if (opts.json) {
1569
- console.log(JSON.stringify(data, null, 2));
1570
- return;
1571
- }
1572
- if (opts.ndjson) {
1573
- for (const r of data.results) {
1574
- console.log(JSON.stringify(r));
1575
- }
1576
- return;
1577
- }
1163
+ if (outputResult(data, 'results', opts)) return;
1578
1164
 
1579
1165
  if (data.warning) {
1580
1166
  console.log(`⚠ ${data.warning}`);
@@ -1648,16 +1234,7 @@ function dataflowImpact(name, customDbPath, opts = {}) {
1648
1234
  offset: opts.offset,
1649
1235
  });
1650
1236
 
1651
- if (opts.json) {
1652
- console.log(JSON.stringify(data, null, 2));
1653
- return;
1654
- }
1655
- if (opts.ndjson) {
1656
- for (const r of data.results) {
1657
- console.log(JSON.stringify(r));
1658
- }
1659
- return;
1660
- }
1237
+ if (outputResult(data, 'results', opts)) return;
1661
1238
 
1662
1239
  if (data.warning) {
1663
1240
  console.log(`⚠ ${data.warning}`);