@optave/codegraph 3.1.0 → 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 (83) 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/engine.js +365 -0
  5. package/src/ast-analysis/metrics.js +118 -0
  6. package/src/ast-analysis/rules/csharp.js +201 -0
  7. package/src/ast-analysis/rules/go.js +182 -0
  8. package/src/ast-analysis/rules/index.js +82 -0
  9. package/src/ast-analysis/rules/java.js +175 -0
  10. package/src/ast-analysis/rules/javascript.js +246 -0
  11. package/src/ast-analysis/rules/php.js +219 -0
  12. package/src/ast-analysis/rules/python.js +196 -0
  13. package/src/ast-analysis/rules/ruby.js +204 -0
  14. package/src/ast-analysis/rules/rust.js +173 -0
  15. package/src/ast-analysis/shared.js +223 -0
  16. package/src/ast-analysis/visitor-utils.js +176 -0
  17. package/src/ast-analysis/visitor.js +162 -0
  18. package/src/ast-analysis/visitors/ast-store-visitor.js +150 -0
  19. package/src/ast-analysis/visitors/cfg-visitor.js +792 -0
  20. package/src/ast-analysis/visitors/complexity-visitor.js +243 -0
  21. package/src/ast-analysis/visitors/dataflow-visitor.js +358 -0
  22. package/src/ast.js +26 -166
  23. package/src/audit.js +2 -88
  24. package/src/batch.js +0 -25
  25. package/src/boundaries.js +1 -1
  26. package/src/branch-compare.js +82 -172
  27. package/src/builder.js +48 -184
  28. package/src/cfg.js +148 -1174
  29. package/src/check.js +1 -84
  30. package/src/cli.js +118 -197
  31. package/src/cochange.js +1 -39
  32. package/src/commands/audit.js +88 -0
  33. package/src/commands/batch.js +26 -0
  34. package/src/commands/branch-compare.js +97 -0
  35. package/src/commands/cfg.js +55 -0
  36. package/src/commands/check.js +82 -0
  37. package/src/commands/cochange.js +37 -0
  38. package/src/commands/communities.js +69 -0
  39. package/src/commands/complexity.js +77 -0
  40. package/src/commands/dataflow.js +110 -0
  41. package/src/commands/flow.js +70 -0
  42. package/src/commands/manifesto.js +77 -0
  43. package/src/commands/owners.js +52 -0
  44. package/src/commands/query.js +21 -0
  45. package/src/commands/sequence.js +33 -0
  46. package/src/commands/structure.js +64 -0
  47. package/src/commands/triage.js +49 -0
  48. package/src/communities.js +22 -96
  49. package/src/complexity.js +234 -1591
  50. package/src/cycles.js +1 -1
  51. package/src/dataflow.js +274 -1352
  52. package/src/db/connection.js +88 -0
  53. package/src/db/migrations.js +312 -0
  54. package/src/db/query-builder.js +280 -0
  55. package/src/db/repository/build-stmts.js +104 -0
  56. package/src/db/repository/cfg.js +83 -0
  57. package/src/db/repository/cochange.js +41 -0
  58. package/src/db/repository/complexity.js +15 -0
  59. package/src/db/repository/dataflow.js +12 -0
  60. package/src/db/repository/edges.js +259 -0
  61. package/src/db/repository/embeddings.js +40 -0
  62. package/src/db/repository/graph-read.js +39 -0
  63. package/src/db/repository/index.js +42 -0
  64. package/src/db/repository/nodes.js +236 -0
  65. package/src/db.js +58 -399
  66. package/src/embedder.js +158 -174
  67. package/src/export.js +1 -1
  68. package/src/extractors/javascript.js +130 -5
  69. package/src/flow.js +153 -222
  70. package/src/index.js +53 -16
  71. package/src/infrastructure/result-formatter.js +21 -0
  72. package/src/infrastructure/test-filter.js +7 -0
  73. package/src/kinds.js +50 -0
  74. package/src/manifesto.js +1 -82
  75. package/src/mcp.js +37 -20
  76. package/src/owners.js +127 -182
  77. package/src/queries-cli.js +866 -0
  78. package/src/queries.js +1271 -2416
  79. package/src/sequence.js +179 -223
  80. package/src/structure.js +211 -269
  81. package/src/triage.js +117 -212
  82. package/src/viewer.js +1 -1
  83. package/src/watcher.js +7 -4
package/src/cfg.js CHANGED
@@ -7,1030 +7,66 @@
7
7
 
8
8
  import fs from 'node:fs';
9
9
  import path from 'node:path';
10
- import { COMPLEXITY_RULES } from './complexity.js';
11
- import { openReadonlyOrFail } from './db.js';
10
+ import { CFG_RULES } from './ast-analysis/rules/index.js';
11
+ import {
12
+ makeCfgRules as _makeCfgRules,
13
+ buildExtensionSet,
14
+ buildExtToLangMap,
15
+ } from './ast-analysis/shared.js';
16
+ import { walkWithVisitors } from './ast-analysis/visitor.js';
17
+ import { createCfgVisitor } from './ast-analysis/visitors/cfg-visitor.js';
18
+ import {
19
+ deleteCfgForNode,
20
+ getCfgBlocks,
21
+ getCfgEdges,
22
+ getFunctionNodeId,
23
+ hasCfgTables,
24
+ openReadonlyOrFail,
25
+ } from './db.js';
26
+ import { isTestFile } from './infrastructure/test-filter.js';
12
27
  import { info } from './logger.js';
13
- import { paginateResult, printNdjson } from './paginate.js';
14
- import { LANGUAGE_REGISTRY } from './parser.js';
15
- import { isTestFile } from './queries.js';
28
+ import { paginateResult } from './paginate.js';
16
29
 
17
- // ─── CFG Node Type Rules (extends COMPLEXITY_RULES) ──────────────────────
30
+ // Re-export for backward compatibility
31
+ export { CFG_RULES };
32
+ export { _makeCfgRules as makeCfgRules };
18
33
 
19
- const CFG_DEFAULTS = {
20
- ifNode: null,
21
- ifNodes: null,
22
- elifNode: null,
23
- elseClause: null,
24
- elseViaAlternative: false,
25
- ifConsequentField: null,
26
- forNodes: new Set(),
27
- whileNode: null,
28
- whileNodes: null,
29
- doNode: null,
30
- infiniteLoopNode: null,
31
- unlessNode: null,
32
- untilNode: null,
33
- switchNode: null,
34
- switchNodes: null,
35
- caseNode: null,
36
- caseNodes: null,
37
- defaultNode: null,
38
- tryNode: null,
39
- catchNode: null,
40
- finallyNode: null,
41
- returnNode: null,
42
- throwNode: null,
43
- breakNode: null,
44
- continueNode: null,
45
- blockNode: null,
46
- blockNodes: null,
47
- labeledNode: null,
48
- functionNodes: new Set(),
49
- };
50
-
51
- const CFG_RULE_KEYS = new Set(Object.keys(CFG_DEFAULTS));
52
-
53
- export function makeCfgRules(overrides) {
54
- for (const key of Object.keys(overrides)) {
55
- if (!CFG_RULE_KEYS.has(key)) {
56
- throw new Error(`CFG rules: unknown key "${key}"`);
57
- }
58
- }
59
- const rules = { ...CFG_DEFAULTS, ...overrides };
60
- if (!(rules.functionNodes instanceof Set) || rules.functionNodes.size === 0) {
61
- throw new Error('CFG rules: functionNodes must be a non-empty Set');
62
- }
63
- if (!(rules.forNodes instanceof Set)) {
64
- throw new Error('CFG rules: forNodes must be a Set');
65
- }
66
- return rules;
67
- }
68
-
69
- const JS_TS_CFG = makeCfgRules({
70
- ifNode: 'if_statement',
71
- elseClause: 'else_clause',
72
- forNodes: new Set(['for_statement', 'for_in_statement']),
73
- whileNode: 'while_statement',
74
- doNode: 'do_statement',
75
- switchNode: 'switch_statement',
76
- caseNode: 'switch_case',
77
- defaultNode: 'switch_default',
78
- tryNode: 'try_statement',
79
- catchNode: 'catch_clause',
80
- finallyNode: 'finally_clause',
81
- returnNode: 'return_statement',
82
- throwNode: 'throw_statement',
83
- breakNode: 'break_statement',
84
- continueNode: 'continue_statement',
85
- blockNode: 'statement_block',
86
- labeledNode: 'labeled_statement',
87
- functionNodes: new Set([
88
- 'function_declaration',
89
- 'function_expression',
90
- 'arrow_function',
91
- 'method_definition',
92
- 'generator_function',
93
- 'generator_function_declaration',
94
- ]),
95
- });
96
-
97
- const PYTHON_CFG = makeCfgRules({
98
- ifNode: 'if_statement',
99
- elifNode: 'elif_clause',
100
- elseClause: 'else_clause',
101
- forNodes: new Set(['for_statement']),
102
- whileNode: 'while_statement',
103
- switchNode: 'match_statement',
104
- caseNode: 'case_clause',
105
- tryNode: 'try_statement',
106
- catchNode: 'except_clause',
107
- finallyNode: 'finally_clause',
108
- returnNode: 'return_statement',
109
- throwNode: 'raise_statement',
110
- breakNode: 'break_statement',
111
- continueNode: 'continue_statement',
112
- blockNode: 'block',
113
- functionNodes: new Set(['function_definition']),
114
- });
115
-
116
- const GO_CFG = makeCfgRules({
117
- ifNode: 'if_statement',
118
- elseViaAlternative: true,
119
- forNodes: new Set(['for_statement']),
120
- switchNodes: new Set([
121
- 'expression_switch_statement',
122
- 'type_switch_statement',
123
- 'select_statement',
124
- ]),
125
- caseNode: 'expression_case',
126
- caseNodes: new Set(['type_case', 'communication_case']),
127
- defaultNode: 'default_case',
128
- returnNode: 'return_statement',
129
- breakNode: 'break_statement',
130
- continueNode: 'continue_statement',
131
- blockNode: 'block',
132
- labeledNode: 'labeled_statement',
133
- functionNodes: new Set(['function_declaration', 'method_declaration', 'func_literal']),
134
- });
135
-
136
- const RUST_CFG = makeCfgRules({
137
- ifNode: 'if_expression',
138
- ifNodes: new Set(['if_let_expression']),
139
- elseClause: 'else_clause',
140
- forNodes: new Set(['for_expression']),
141
- whileNode: 'while_expression',
142
- whileNodes: new Set(['while_let_expression']),
143
- infiniteLoopNode: 'loop_expression',
144
- switchNode: 'match_expression',
145
- caseNode: 'match_arm',
146
- returnNode: 'return_expression',
147
- breakNode: 'break_expression',
148
- continueNode: 'continue_expression',
149
- blockNode: 'block',
150
- functionNodes: new Set(['function_item', 'closure_expression']),
151
- });
152
-
153
- const JAVA_CFG = makeCfgRules({
154
- ifNode: 'if_statement',
155
- elseViaAlternative: true,
156
- forNodes: new Set(['for_statement', 'enhanced_for_statement']),
157
- whileNode: 'while_statement',
158
- doNode: 'do_statement',
159
- switchNode: 'switch_expression',
160
- caseNode: 'switch_block_statement_group',
161
- caseNodes: new Set(['switch_rule']),
162
- tryNode: 'try_statement',
163
- catchNode: 'catch_clause',
164
- finallyNode: 'finally_clause',
165
- returnNode: 'return_statement',
166
- throwNode: 'throw_statement',
167
- breakNode: 'break_statement',
168
- continueNode: 'continue_statement',
169
- blockNode: 'block',
170
- labeledNode: 'labeled_statement',
171
- functionNodes: new Set(['method_declaration', 'constructor_declaration', 'lambda_expression']),
172
- });
173
-
174
- const CSHARP_CFG = makeCfgRules({
175
- ifNode: 'if_statement',
176
- elseViaAlternative: true,
177
- forNodes: new Set(['for_statement', 'foreach_statement']),
178
- whileNode: 'while_statement',
179
- doNode: 'do_statement',
180
- switchNode: 'switch_statement',
181
- caseNode: 'switch_section',
182
- tryNode: 'try_statement',
183
- catchNode: 'catch_clause',
184
- finallyNode: 'finally_clause',
185
- returnNode: 'return_statement',
186
- throwNode: 'throw_statement',
187
- breakNode: 'break_statement',
188
- continueNode: 'continue_statement',
189
- blockNode: 'block',
190
- labeledNode: 'labeled_statement',
191
- functionNodes: new Set([
192
- 'method_declaration',
193
- 'constructor_declaration',
194
- 'lambda_expression',
195
- 'local_function_statement',
196
- ]),
197
- });
198
-
199
- const RUBY_CFG = makeCfgRules({
200
- ifNode: 'if',
201
- elifNode: 'elsif',
202
- elseClause: 'else',
203
- forNodes: new Set(['for']),
204
- whileNode: 'while',
205
- unlessNode: 'unless',
206
- untilNode: 'until',
207
- switchNode: 'case',
208
- caseNode: 'when',
209
- defaultNode: 'else',
210
- tryNode: 'begin',
211
- catchNode: 'rescue',
212
- finallyNode: 'ensure',
213
- returnNode: 'return',
214
- breakNode: 'break',
215
- continueNode: 'next',
216
- blockNodes: new Set(['then', 'do', 'body_statement']),
217
- functionNodes: new Set(['method', 'singleton_method']),
218
- });
219
-
220
- const PHP_CFG = makeCfgRules({
221
- ifNode: 'if_statement',
222
- elifNode: 'else_if_clause',
223
- elseClause: 'else_clause',
224
- ifConsequentField: 'body',
225
- forNodes: new Set(['for_statement', 'foreach_statement']),
226
- whileNode: 'while_statement',
227
- doNode: 'do_statement',
228
- switchNode: 'switch_statement',
229
- caseNode: 'case_statement',
230
- defaultNode: 'default_statement',
231
- tryNode: 'try_statement',
232
- catchNode: 'catch_clause',
233
- finallyNode: 'finally_clause',
234
- returnNode: 'return_statement',
235
- throwNode: 'throw_expression',
236
- breakNode: 'break_statement',
237
- continueNode: 'continue_statement',
238
- blockNode: 'compound_statement',
239
- functionNodes: new Set([
240
- 'function_definition',
241
- 'method_declaration',
242
- 'anonymous_function_creation_expression',
243
- 'arrow_function',
244
- ]),
245
- });
246
-
247
- export const CFG_RULES = new Map([
248
- ['javascript', JS_TS_CFG],
249
- ['typescript', JS_TS_CFG],
250
- ['tsx', JS_TS_CFG],
251
- ['python', PYTHON_CFG],
252
- ['go', GO_CFG],
253
- ['rust', RUST_CFG],
254
- ['java', JAVA_CFG],
255
- ['csharp', CSHARP_CFG],
256
- ['ruby', RUBY_CFG],
257
- ['php', PHP_CFG],
258
- ]);
259
-
260
- const CFG_LANG_IDS = new Set(CFG_RULES.keys());
261
-
262
- // JS/TS extensions
263
- const CFG_EXTENSIONS = new Set();
264
- for (const entry of LANGUAGE_REGISTRY) {
265
- if (CFG_LANG_IDS.has(entry.id)) {
266
- for (const ext of entry.extensions) CFG_EXTENSIONS.add(ext);
267
- }
268
- }
34
+ const CFG_EXTENSIONS = buildExtensionSet(CFG_RULES);
269
35
 
270
36
  // ─── Core Algorithm: AST → CFG ──────────────────────────────────────────
271
37
 
272
38
  /**
273
39
  * Build a control flow graph for a single function AST node.
274
40
  *
41
+ * Thin wrapper around the CFG visitor — runs walkWithVisitors on the function
42
+ * node and returns the first result. All CFG construction logic lives in
43
+ * `ast-analysis/visitors/cfg-visitor.js`.
44
+ *
275
45
  * @param {object} functionNode - tree-sitter function AST node
276
- * @param {string} langId - language identifier (javascript, typescript, tsx)
277
- * @returns {{ blocks: object[], edges: object[] }} - CFG blocks and edges
46
+ * @param {string} langId - language identifier
47
+ * @returns {{ blocks: object[], edges: object[], cyclomatic: number }} - CFG blocks, edges, and derived cyclomatic
278
48
  */
279
49
  export function buildFunctionCFG(functionNode, langId) {
280
50
  const rules = CFG_RULES.get(langId);
281
- if (!rules) return { blocks: [], edges: [] };
282
-
283
- const blocks = [];
284
- const edges = [];
285
- let nextIndex = 0;
286
-
287
- function makeBlock(type, startLine = null, endLine = null, label = null) {
288
- const block = {
289
- index: nextIndex++,
290
- type,
291
- startLine,
292
- endLine,
293
- label,
294
- };
295
- blocks.push(block);
296
- return block;
297
- }
298
-
299
- function addEdge(source, target, kind) {
300
- edges.push({
301
- sourceIndex: source.index,
302
- targetIndex: target.index,
303
- kind,
304
- });
305
- }
306
-
307
- const entryBlock = makeBlock('entry');
308
- const exitBlock = makeBlock('exit');
309
-
310
- // Loop context stack for break/continue resolution
311
- const loopStack = [];
312
-
313
- // Label map for labeled break/continue
314
- const labelMap = new Map();
315
-
316
- /**
317
- * Get the body node of a function (handles arrow functions with expression bodies).
318
- */
319
- function getFunctionBody(fnNode) {
320
- const body = fnNode.childForFieldName('body');
321
- if (!body) return null;
322
- return body;
323
- }
324
-
325
- /**
326
- * Get statement children from a block or statement list.
327
- */
328
- function getStatements(node) {
329
- if (!node) return [];
330
- // Block-like nodes: extract named children
331
- if (node.type === rules.blockNode || rules.blockNodes?.has(node.type)) {
332
- const stmts = [];
333
- for (let i = 0; i < node.namedChildCount; i++) {
334
- stmts.push(node.namedChild(i));
335
- }
336
- return stmts;
337
- }
338
- // Single statement (e.g., arrow fn with expression body, or unbraced if body)
339
- return [node];
340
- }
341
-
342
- /**
343
- * Process a list of statements, creating blocks and edges.
344
- * Returns the last "current" block after processing, or null if all paths terminated.
345
- */
346
- function processStatements(stmts, currentBlock) {
347
- let cur = currentBlock;
348
-
349
- for (const stmt of stmts) {
350
- if (!cur) {
351
- // Dead code after return/break/continue/throw — skip remaining
352
- break;
353
- }
354
- cur = processStatement(stmt, cur);
355
- }
356
-
357
- return cur;
358
- }
359
-
360
- /**
361
- * Process a single statement, returns the new current block or null if terminated.
362
- */
363
- function processStatement(stmt, currentBlock) {
364
- if (!stmt || !currentBlock) return currentBlock;
365
-
366
- // Unwrap expression_statement (Rust uses expressions for control flow)
367
- if (stmt.type === 'expression_statement' && stmt.namedChildCount === 1) {
368
- const inner = stmt.namedChild(0);
369
- const t = inner.type;
370
- if (
371
- t === rules.ifNode ||
372
- rules.ifNodes?.has(t) ||
373
- rules.forNodes?.has(t) ||
374
- t === rules.whileNode ||
375
- rules.whileNodes?.has(t) ||
376
- t === rules.doNode ||
377
- t === rules.infiniteLoopNode ||
378
- t === rules.switchNode ||
379
- rules.switchNodes?.has(t) ||
380
- t === rules.returnNode ||
381
- t === rules.throwNode ||
382
- t === rules.breakNode ||
383
- t === rules.continueNode ||
384
- t === rules.unlessNode ||
385
- t === rules.untilNode
386
- ) {
387
- return processStatement(inner, currentBlock);
388
- }
389
- }
390
-
391
- const type = stmt.type;
392
-
393
- // Labeled statement: register label then process inner statement
394
- if (type === rules.labeledNode) {
395
- const labelNode = stmt.childForFieldName('label');
396
- const labelName = labelNode ? labelNode.text : null;
397
- const body = stmt.childForFieldName('body');
398
- if (body && labelName) {
399
- // Will be filled when we encounter the loop
400
- const labelCtx = { headerBlock: null, exitBlock: null };
401
- labelMap.set(labelName, labelCtx);
402
- const result = processStatement(body, currentBlock);
403
- labelMap.delete(labelName);
404
- return result;
405
- }
406
- return currentBlock;
407
- }
408
-
409
- // If statement (including language variants like if_let_expression)
410
- if (type === rules.ifNode || rules.ifNodes?.has(type)) {
411
- return processIf(stmt, currentBlock);
412
- }
413
-
414
- // Unless (Ruby) — same CFG shape as if
415
- if (rules.unlessNode && type === rules.unlessNode) {
416
- return processIf(stmt, currentBlock);
417
- }
418
-
419
- // For / for-in loops
420
- if (rules.forNodes.has(type)) {
421
- return processForLoop(stmt, currentBlock);
422
- }
423
-
424
- // While loop (including language variants like while_let_expression)
425
- if (type === rules.whileNode || rules.whileNodes?.has(type)) {
426
- return processWhileLoop(stmt, currentBlock);
427
- }
428
-
429
- // Until (Ruby) — same CFG shape as while
430
- if (rules.untilNode && type === rules.untilNode) {
431
- return processWhileLoop(stmt, currentBlock);
432
- }
433
-
434
- // Do-while loop
435
- if (rules.doNode && type === rules.doNode) {
436
- return processDoWhileLoop(stmt, currentBlock);
437
- }
438
-
439
- // Infinite loop (Rust's loop {})
440
- if (rules.infiniteLoopNode && type === rules.infiniteLoopNode) {
441
- return processInfiniteLoop(stmt, currentBlock);
442
- }
443
-
444
- // Switch / match statement
445
- if (type === rules.switchNode || rules.switchNodes?.has(type)) {
446
- return processSwitch(stmt, currentBlock);
447
- }
448
-
449
- // Try/catch/finally
450
- if (rules.tryNode && type === rules.tryNode) {
451
- return processTryCatch(stmt, currentBlock);
452
- }
453
-
454
- // Return statement
455
- if (type === rules.returnNode) {
456
- currentBlock.endLine = stmt.startPosition.row + 1;
457
- addEdge(currentBlock, exitBlock, 'return');
458
- return null; // path terminated
459
- }
460
-
461
- // Throw statement
462
- if (type === rules.throwNode) {
463
- currentBlock.endLine = stmt.startPosition.row + 1;
464
- addEdge(currentBlock, exitBlock, 'exception');
465
- return null; // path terminated
466
- }
467
-
468
- // Break statement
469
- if (type === rules.breakNode) {
470
- const labelNode = stmt.childForFieldName('label');
471
- const labelName = labelNode ? labelNode.text : null;
472
-
473
- let target = null;
474
- if (labelName && labelMap.has(labelName)) {
475
- target = labelMap.get(labelName).exitBlock;
476
- } else if (loopStack.length > 0) {
477
- target = loopStack[loopStack.length - 1].exitBlock;
478
- }
479
-
480
- if (target) {
481
- currentBlock.endLine = stmt.startPosition.row + 1;
482
- addEdge(currentBlock, target, 'break');
483
- return null; // path terminated
484
- }
485
- // break with no enclosing loop/switch — treat as no-op
486
- return currentBlock;
487
- }
488
-
489
- // Continue statement
490
- if (type === rules.continueNode) {
491
- const labelNode = stmt.childForFieldName('label');
492
- const labelName = labelNode ? labelNode.text : null;
493
-
494
- let target = null;
495
- if (labelName && labelMap.has(labelName)) {
496
- target = labelMap.get(labelName).headerBlock;
497
- } else if (loopStack.length > 0) {
498
- target = loopStack[loopStack.length - 1].headerBlock;
499
- }
500
-
501
- if (target) {
502
- currentBlock.endLine = stmt.startPosition.row + 1;
503
- addEdge(currentBlock, target, 'continue');
504
- return null; // path terminated
505
- }
506
- return currentBlock;
507
- }
508
-
509
- // Regular statement — extend current block
510
- if (!currentBlock.startLine) {
511
- currentBlock.startLine = stmt.startPosition.row + 1;
512
- }
513
- currentBlock.endLine = stmt.endPosition.row + 1;
514
- return currentBlock;
515
- }
516
-
517
- /**
518
- * Process an if/else-if/else chain.
519
- * Handles three patterns:
520
- * A) Wrapper: alternative → else_clause → nested if or block (JS/TS, Rust)
521
- * B) Siblings: elif/elsif/else_if as sibling children (Python, Ruby, PHP)
522
- * C) Direct: alternative → if_statement or block directly (Go, Java, C#)
523
- */
524
- function processIf(ifStmt, currentBlock) {
525
- // Terminate current block at condition
526
- currentBlock.endLine = ifStmt.startPosition.row + 1;
527
-
528
- const condBlock = makeBlock(
529
- 'condition',
530
- ifStmt.startPosition.row + 1,
531
- ifStmt.startPosition.row + 1,
532
- 'if',
533
- );
534
- addEdge(currentBlock, condBlock, 'fallthrough');
535
-
536
- const joinBlock = makeBlock('body');
537
-
538
- // True branch (consequent)
539
- const consequentField = rules.ifConsequentField || 'consequence';
540
- const consequent = ifStmt.childForFieldName(consequentField);
541
- const trueBlock = makeBlock('branch_true', null, null, 'then');
542
- addEdge(condBlock, trueBlock, 'branch_true');
543
- const trueStmts = getStatements(consequent);
544
- const trueEnd = processStatements(trueStmts, trueBlock);
545
- if (trueEnd) {
546
- addEdge(trueEnd, joinBlock, 'fallthrough');
547
- }
548
-
549
- // False branch — depends on language pattern
550
- if (rules.elifNode) {
551
- // Pattern B: elif/else as siblings of the if node
552
- processElifSiblings(ifStmt, condBlock, joinBlock);
553
- } else {
554
- const alternative = ifStmt.childForFieldName('alternative');
555
- if (alternative) {
556
- if (rules.elseViaAlternative && alternative.type !== rules.elseClause) {
557
- // Pattern C: alternative points directly to if or block
558
- if (alternative.type === rules.ifNode || rules.ifNodes?.has(alternative.type)) {
559
- // else-if: recurse
560
- const falseBlock = makeBlock('branch_false', null, null, 'else-if');
561
- addEdge(condBlock, falseBlock, 'branch_false');
562
- const elseIfEnd = processIf(alternative, falseBlock);
563
- if (elseIfEnd) {
564
- addEdge(elseIfEnd, joinBlock, 'fallthrough');
565
- }
566
- } else {
567
- // else block
568
- const falseBlock = makeBlock('branch_false', null, null, 'else');
569
- addEdge(condBlock, falseBlock, 'branch_false');
570
- const falseStmts = getStatements(alternative);
571
- const falseEnd = processStatements(falseStmts, falseBlock);
572
- if (falseEnd) {
573
- addEdge(falseEnd, joinBlock, 'fallthrough');
574
- }
575
- }
576
- } else if (alternative.type === rules.elseClause) {
577
- // Pattern A: else_clause wrapper — may contain another if (else-if) or a block
578
- const elseChildren = [];
579
- for (let i = 0; i < alternative.namedChildCount; i++) {
580
- elseChildren.push(alternative.namedChild(i));
581
- }
582
- if (
583
- elseChildren.length === 1 &&
584
- (elseChildren[0].type === rules.ifNode || rules.ifNodes?.has(elseChildren[0].type))
585
- ) {
586
- // else-if: recurse
587
- const falseBlock = makeBlock('branch_false', null, null, 'else-if');
588
- addEdge(condBlock, falseBlock, 'branch_false');
589
- const elseIfEnd = processIf(elseChildren[0], falseBlock);
590
- if (elseIfEnd) {
591
- addEdge(elseIfEnd, joinBlock, 'fallthrough');
592
- }
593
- } else {
594
- // else block
595
- const falseBlock = makeBlock('branch_false', null, null, 'else');
596
- addEdge(condBlock, falseBlock, 'branch_false');
597
- const falseEnd = processStatements(elseChildren, falseBlock);
598
- if (falseEnd) {
599
- addEdge(falseEnd, joinBlock, 'fallthrough');
600
- }
601
- }
602
- }
603
- } else {
604
- // No else: condition-false goes directly to join
605
- addEdge(condBlock, joinBlock, 'branch_false');
606
- }
607
- }
608
-
609
- return joinBlock;
610
- }
611
-
612
- /**
613
- * Handle Pattern B: elif/elsif/else_if as sibling children of the if node.
614
- */
615
- function processElifSiblings(ifStmt, firstCondBlock, joinBlock) {
616
- let lastCondBlock = firstCondBlock;
617
- let foundElse = false;
618
-
619
- for (let i = 0; i < ifStmt.namedChildCount; i++) {
620
- const child = ifStmt.namedChild(i);
621
-
622
- if (child.type === rules.elifNode) {
623
- // Create condition block for elif
624
- const elifCondBlock = makeBlock(
625
- 'condition',
626
- child.startPosition.row + 1,
627
- child.startPosition.row + 1,
628
- 'else-if',
629
- );
630
- addEdge(lastCondBlock, elifCondBlock, 'branch_false');
631
-
632
- // True branch of elif
633
- const elifConsequentField = rules.ifConsequentField || 'consequence';
634
- const elifConsequent = child.childForFieldName(elifConsequentField);
635
- const elifTrueBlock = makeBlock('branch_true', null, null, 'then');
636
- addEdge(elifCondBlock, elifTrueBlock, 'branch_true');
637
- const elifTrueStmts = getStatements(elifConsequent);
638
- const elifTrueEnd = processStatements(elifTrueStmts, elifTrueBlock);
639
- if (elifTrueEnd) {
640
- addEdge(elifTrueEnd, joinBlock, 'fallthrough');
641
- }
642
-
643
- lastCondBlock = elifCondBlock;
644
- } else if (child.type === rules.elseClause) {
645
- // Else body
646
- const elseBlock = makeBlock('branch_false', null, null, 'else');
647
- addEdge(lastCondBlock, elseBlock, 'branch_false');
648
-
649
- // Try field access first, then collect children
650
- const elseBody = child.childForFieldName('body');
651
- let elseStmts;
652
- if (elseBody) {
653
- elseStmts = getStatements(elseBody);
654
- } else {
655
- elseStmts = [];
656
- for (let j = 0; j < child.namedChildCount; j++) {
657
- elseStmts.push(child.namedChild(j));
658
- }
659
- }
660
- const elseEnd = processStatements(elseStmts, elseBlock);
661
- if (elseEnd) {
662
- addEdge(elseEnd, joinBlock, 'fallthrough');
663
- }
664
-
665
- foundElse = true;
666
- }
667
- }
668
-
669
- // If no else clause, last condition's false goes to join
670
- if (!foundElse) {
671
- addEdge(lastCondBlock, joinBlock, 'branch_false');
672
- }
673
- }
674
-
675
- /**
676
- * Process a for/for-in loop.
677
- */
678
- function processForLoop(forStmt, currentBlock) {
679
- const headerBlock = makeBlock(
680
- 'loop_header',
681
- forStmt.startPosition.row + 1,
682
- forStmt.startPosition.row + 1,
683
- 'for',
684
- );
685
- addEdge(currentBlock, headerBlock, 'fallthrough');
686
-
687
- const loopExitBlock = makeBlock('body');
688
-
689
- // Register loop context
690
- const loopCtx = { headerBlock, exitBlock: loopExitBlock };
691
- loopStack.push(loopCtx);
692
-
693
- // Update label map if this is inside a labeled statement
694
- for (const [, ctx] of labelMap) {
695
- if (!ctx.headerBlock) {
696
- ctx.headerBlock = headerBlock;
697
- ctx.exitBlock = loopExitBlock;
698
- }
699
- }
700
-
701
- // Loop body
702
- const body = forStmt.childForFieldName('body');
703
- const bodyBlock = makeBlock('loop_body');
704
- addEdge(headerBlock, bodyBlock, 'branch_true');
705
-
706
- const bodyStmts = getStatements(body);
707
- const bodyEnd = processStatements(bodyStmts, bodyBlock);
708
-
709
- if (bodyEnd) {
710
- addEdge(bodyEnd, headerBlock, 'loop_back');
711
- }
712
-
713
- // Loop exit
714
- addEdge(headerBlock, loopExitBlock, 'loop_exit');
715
-
716
- loopStack.pop();
717
- return loopExitBlock;
718
- }
719
-
720
- /**
721
- * Process a while loop.
722
- */
723
- function processWhileLoop(whileStmt, currentBlock) {
724
- const headerBlock = makeBlock(
725
- 'loop_header',
726
- whileStmt.startPosition.row + 1,
727
- whileStmt.startPosition.row + 1,
728
- 'while',
729
- );
730
- addEdge(currentBlock, headerBlock, 'fallthrough');
731
-
732
- const loopExitBlock = makeBlock('body');
733
-
734
- const loopCtx = { headerBlock, exitBlock: loopExitBlock };
735
- loopStack.push(loopCtx);
736
-
737
- for (const [, ctx] of labelMap) {
738
- if (!ctx.headerBlock) {
739
- ctx.headerBlock = headerBlock;
740
- ctx.exitBlock = loopExitBlock;
741
- }
742
- }
743
-
744
- const body = whileStmt.childForFieldName('body');
745
- const bodyBlock = makeBlock('loop_body');
746
- addEdge(headerBlock, bodyBlock, 'branch_true');
747
-
748
- const bodyStmts = getStatements(body);
749
- const bodyEnd = processStatements(bodyStmts, bodyBlock);
750
-
751
- if (bodyEnd) {
752
- addEdge(bodyEnd, headerBlock, 'loop_back');
753
- }
754
-
755
- addEdge(headerBlock, loopExitBlock, 'loop_exit');
756
-
757
- loopStack.pop();
758
- return loopExitBlock;
759
- }
760
-
761
- /**
762
- * Process a do-while loop.
763
- */
764
- function processDoWhileLoop(doStmt, currentBlock) {
765
- const bodyBlock = makeBlock('loop_body', doStmt.startPosition.row + 1, null, 'do');
766
- addEdge(currentBlock, bodyBlock, 'fallthrough');
767
-
768
- const condBlock = makeBlock('loop_header', null, null, 'do-while');
769
- const loopExitBlock = makeBlock('body');
770
-
771
- const loopCtx = { headerBlock: condBlock, exitBlock: loopExitBlock };
772
- loopStack.push(loopCtx);
773
-
774
- for (const [, ctx] of labelMap) {
775
- if (!ctx.headerBlock) {
776
- ctx.headerBlock = condBlock;
777
- ctx.exitBlock = loopExitBlock;
778
- }
779
- }
780
-
781
- const body = doStmt.childForFieldName('body');
782
- const bodyStmts = getStatements(body);
783
- const bodyEnd = processStatements(bodyStmts, bodyBlock);
784
-
785
- if (bodyEnd) {
786
- addEdge(bodyEnd, condBlock, 'fallthrough');
787
- }
788
-
789
- // Condition: loop_back or exit
790
- addEdge(condBlock, bodyBlock, 'loop_back');
791
- addEdge(condBlock, loopExitBlock, 'loop_exit');
792
-
793
- loopStack.pop();
794
- return loopExitBlock;
795
- }
796
-
797
- /**
798
- * Process an infinite loop (Rust's `loop {}`).
799
- * No condition — body always executes. Exit only via break.
800
- */
801
- function processInfiniteLoop(loopStmt, currentBlock) {
802
- const headerBlock = makeBlock(
803
- 'loop_header',
804
- loopStmt.startPosition.row + 1,
805
- loopStmt.startPosition.row + 1,
806
- 'loop',
807
- );
808
- addEdge(currentBlock, headerBlock, 'fallthrough');
809
-
810
- const loopExitBlock = makeBlock('body');
811
-
812
- const loopCtx = { headerBlock, exitBlock: loopExitBlock };
813
- loopStack.push(loopCtx);
814
-
815
- for (const [, ctx] of labelMap) {
816
- if (!ctx.headerBlock) {
817
- ctx.headerBlock = headerBlock;
818
- ctx.exitBlock = loopExitBlock;
819
- }
820
- }
821
-
822
- const body = loopStmt.childForFieldName('body');
823
- const bodyBlock = makeBlock('loop_body');
824
- addEdge(headerBlock, bodyBlock, 'branch_true');
825
-
826
- const bodyStmts = getStatements(body);
827
- const bodyEnd = processStatements(bodyStmts, bodyBlock);
828
-
829
- if (bodyEnd) {
830
- addEdge(bodyEnd, headerBlock, 'loop_back');
831
- }
832
-
833
- // No loop_exit from header — can only exit via break
834
-
835
- loopStack.pop();
836
- return loopExitBlock;
837
- }
838
-
839
- /**
840
- * Process a switch statement.
841
- */
842
- function processSwitch(switchStmt, currentBlock) {
843
- currentBlock.endLine = switchStmt.startPosition.row + 1;
844
-
845
- const switchHeader = makeBlock(
846
- 'condition',
847
- switchStmt.startPosition.row + 1,
848
- switchStmt.startPosition.row + 1,
849
- 'switch',
850
- );
851
- addEdge(currentBlock, switchHeader, 'fallthrough');
852
-
853
- const joinBlock = makeBlock('body');
854
-
855
- // Switch acts like a break target for contained break statements
856
- const switchCtx = { headerBlock: switchHeader, exitBlock: joinBlock };
857
- loopStack.push(switchCtx);
858
-
859
- // Get case children from body field or direct children
860
- const switchBody = switchStmt.childForFieldName('body');
861
- const container = switchBody || switchStmt;
862
-
863
- let hasDefault = false;
864
- for (let i = 0; i < container.namedChildCount; i++) {
865
- const caseClause = container.namedChild(i);
866
-
867
- const isDefault = caseClause.type === rules.defaultNode;
868
- const isCase =
869
- isDefault || caseClause.type === rules.caseNode || rules.caseNodes?.has(caseClause.type);
870
-
871
- if (!isCase) continue;
872
-
873
- const caseLabel = isDefault ? 'default' : 'case';
874
- const caseBlock = makeBlock('case', caseClause.startPosition.row + 1, null, caseLabel);
875
- addEdge(switchHeader, caseBlock, isDefault ? 'branch_false' : 'branch_true');
876
- if (isDefault) hasDefault = true;
877
-
878
- // Extract case body: try field access, then collect non-header children
879
- const caseBodyNode =
880
- caseClause.childForFieldName('body') || caseClause.childForFieldName('consequence');
881
- let caseStmts;
882
- if (caseBodyNode) {
883
- caseStmts = getStatements(caseBodyNode);
884
- } else {
885
- caseStmts = [];
886
- const valueNode = caseClause.childForFieldName('value');
887
- const patternNode = caseClause.childForFieldName('pattern');
888
- for (let j = 0; j < caseClause.namedChildCount; j++) {
889
- const child = caseClause.namedChild(j);
890
- if (child !== valueNode && child !== patternNode && child.type !== 'switch_label') {
891
- caseStmts.push(child);
892
- }
893
- }
894
- }
895
-
896
- const caseEnd = processStatements(caseStmts, caseBlock);
897
- if (caseEnd) {
898
- addEdge(caseEnd, joinBlock, 'fallthrough');
899
- }
900
- }
901
-
902
- // If no default case, switch header can skip to join
903
- if (!hasDefault) {
904
- addEdge(switchHeader, joinBlock, 'branch_false');
905
- }
906
-
907
- loopStack.pop();
908
- return joinBlock;
909
- }
910
-
911
- /**
912
- * Process try/catch/finally.
913
- */
914
- function processTryCatch(tryStmt, currentBlock) {
915
- currentBlock.endLine = tryStmt.startPosition.row + 1;
916
-
917
- const joinBlock = makeBlock('body');
918
-
919
- // Try body — field access or collect non-handler children (e.g., Ruby's begin)
920
- const tryBody = tryStmt.childForFieldName('body');
921
- let tryBodyStart;
922
- let tryStmts;
923
- if (tryBody) {
924
- tryBodyStart = tryBody.startPosition.row + 1;
925
- tryStmts = getStatements(tryBody);
926
- } else {
927
- tryBodyStart = tryStmt.startPosition.row + 1;
928
- tryStmts = [];
929
- for (let i = 0; i < tryStmt.namedChildCount; i++) {
930
- const child = tryStmt.namedChild(i);
931
- if (rules.catchNode && child.type === rules.catchNode) continue;
932
- if (rules.finallyNode && child.type === rules.finallyNode) continue;
933
- tryStmts.push(child);
934
- }
935
- }
936
-
937
- const tryBlock = makeBlock('body', tryBodyStart, null, 'try');
938
- addEdge(currentBlock, tryBlock, 'fallthrough');
939
- const tryEnd = processStatements(tryStmts, tryBlock);
940
-
941
- // Catch handler
942
- let catchHandler = null;
943
- let finallyHandler = null;
944
- for (let i = 0; i < tryStmt.namedChildCount; i++) {
945
- const child = tryStmt.namedChild(i);
946
- if (rules.catchNode && child.type === rules.catchNode) catchHandler = child;
947
- if (rules.finallyNode && child.type === rules.finallyNode) finallyHandler = child;
948
- }
949
-
950
- if (catchHandler) {
951
- const catchBlock = makeBlock('catch', catchHandler.startPosition.row + 1, null, 'catch');
952
- // Exception edge from try to catch
953
- addEdge(tryBlock, catchBlock, 'exception');
954
-
955
- // Catch body — try field access, then collect children
956
- const catchBodyNode = catchHandler.childForFieldName('body');
957
- let catchStmts;
958
- if (catchBodyNode) {
959
- catchStmts = getStatements(catchBodyNode);
960
- } else {
961
- catchStmts = [];
962
- for (let i = 0; i < catchHandler.namedChildCount; i++) {
963
- catchStmts.push(catchHandler.namedChild(i));
964
- }
965
- }
966
- const catchEnd = processStatements(catchStmts, catchBlock);
967
-
968
- if (finallyHandler) {
969
- const finallyBlock = makeBlock(
970
- 'finally',
971
- finallyHandler.startPosition.row + 1,
972
- null,
973
- 'finally',
974
- );
975
- if (tryEnd) addEdge(tryEnd, finallyBlock, 'fallthrough');
976
- if (catchEnd) addEdge(catchEnd, finallyBlock, 'fallthrough');
977
-
978
- const finallyBodyNode = finallyHandler.childForFieldName('body');
979
- const finallyStmts = finallyBodyNode
980
- ? getStatements(finallyBodyNode)
981
- : getStatements(finallyHandler);
982
- const finallyEnd = processStatements(finallyStmts, finallyBlock);
983
- if (finallyEnd) addEdge(finallyEnd, joinBlock, 'fallthrough');
984
- } else {
985
- if (tryEnd) addEdge(tryEnd, joinBlock, 'fallthrough');
986
- if (catchEnd) addEdge(catchEnd, joinBlock, 'fallthrough');
987
- }
988
- } else if (finallyHandler) {
989
- const finallyBlock = makeBlock(
990
- 'finally',
991
- finallyHandler.startPosition.row + 1,
992
- null,
993
- 'finally',
994
- );
995
- if (tryEnd) addEdge(tryEnd, finallyBlock, 'fallthrough');
996
-
997
- const finallyBodyNode = finallyHandler.childForFieldName('body');
998
- const finallyStmts = finallyBodyNode
999
- ? getStatements(finallyBodyNode)
1000
- : getStatements(finallyHandler);
1001
- const finallyEnd = processStatements(finallyStmts, finallyBlock);
1002
- if (finallyEnd) addEdge(finallyEnd, joinBlock, 'fallthrough');
1003
- } else {
1004
- if (tryEnd) addEdge(tryEnd, joinBlock, 'fallthrough');
1005
- }
1006
-
1007
- return joinBlock;
1008
- }
1009
-
1010
- // ── Main entry point ──────────────────────────────────────────────────
1011
-
1012
- const body = getFunctionBody(functionNode);
1013
- if (!body) {
1014
- // Empty function or expression body
1015
- addEdge(entryBlock, exitBlock, 'fallthrough');
1016
- return { blocks, edges };
1017
- }
1018
-
1019
- const stmts = getStatements(body);
1020
- if (stmts.length === 0) {
1021
- addEdge(entryBlock, exitBlock, 'fallthrough');
1022
- return { blocks, edges };
1023
- }
1024
-
1025
- const firstBlock = makeBlock('body');
1026
- addEdge(entryBlock, firstBlock, 'fallthrough');
1027
-
1028
- const lastBlock = processStatements(stmts, firstBlock);
1029
- if (lastBlock) {
1030
- addEdge(lastBlock, exitBlock, 'fallthrough');
1031
- }
1032
-
1033
- return { blocks, edges };
51
+ if (!rules) return { blocks: [], edges: [], cyclomatic: 0 };
52
+
53
+ const visitor = createCfgVisitor(rules);
54
+ const walkerOpts = {
55
+ functionNodeTypes: new Set(rules.functionNodes),
56
+ nestingNodeTypes: new Set(),
57
+ getFunctionName: (node) => {
58
+ const nameNode = node.childForFieldName('name');
59
+ return nameNode ? nameNode.text : null;
60
+ },
61
+ };
62
+
63
+ const results = walkWithVisitors(functionNode, [visitor], langId, walkerOpts);
64
+ const cfgResults = results.cfg || [];
65
+ if (cfgResults.length === 0) return { blocks: [], edges: [], cyclomatic: 0 };
66
+
67
+ const r = cfgResults.find((result) => result.funcNode === functionNode);
68
+ if (!r) return { blocks: [], edges: [], cyclomatic: 0 };
69
+ return { blocks: r.blocks, edges: r.edges, cyclomatic: r.cyclomatic };
1034
70
  }
1035
71
 
1036
72
  // ─── Build-Time: Compute CFG for Changed Files ─────────────────────────
@@ -1050,12 +86,7 @@ export async function buildCFGData(db, fileSymbols, rootDir, _engineOpts) {
1050
86
 
1051
87
  // Always build ext→langId map so native-only builds (where _langId is unset)
1052
88
  // can still derive the language from the file extension.
1053
- const extToLang = new Map();
1054
- for (const entry of LANGUAGE_REGISTRY) {
1055
- for (const ext of entry.extensions) {
1056
- extToLang.set(ext, entry.id);
1057
- }
1058
- }
89
+ const extToLang = buildExtToLangMap();
1059
90
 
1060
91
  for (const [relPath, symbols] of fileSymbols) {
1061
92
  if (!symbols._tree) {
@@ -1084,7 +115,7 @@ export async function buildCFGData(db, fileSymbols, rootDir, _engineOpts) {
1084
115
  getParserFn = mod.getParser;
1085
116
  }
1086
117
 
1087
- const { findFunctionNode } = await import('./complexity.js');
118
+ // findFunctionNode imported from ./ast-analysis/shared.js at module level
1088
119
 
1089
120
  const insertBlock = db.prepare(
1090
121
  `INSERT INTO cfg_blocks (function_node_id, block_index, block_type, start_line, end_line, label)
@@ -1094,12 +125,6 @@ export async function buildCFGData(db, fileSymbols, rootDir, _engineOpts) {
1094
125
  `INSERT INTO cfg_edges (function_node_id, source_block_id, target_block_id, kind)
1095
126
  VALUES (?, ?, ?, ?)`,
1096
127
  );
1097
- const deleteBlocks = db.prepare('DELETE FROM cfg_blocks WHERE function_node_id = ?');
1098
- const deleteEdges = db.prepare('DELETE FROM cfg_edges WHERE function_node_id = ?');
1099
- const getNodeId = db.prepare(
1100
- "SELECT id FROM nodes WHERE name = ? AND kind IN ('function','method') AND file = ? AND line = ?",
1101
- );
1102
-
1103
128
  let analyzed = 0;
1104
129
 
1105
130
  const tx = db.transaction(() => {
@@ -1119,7 +144,7 @@ export async function buildCFGData(db, fileSymbols, rootDir, _engineOpts) {
1119
144
  if (!tree && !allNative) {
1120
145
  if (!getParserFn) continue;
1121
146
  langId = extToLang.get(ext);
1122
- if (!langId || !CFG_LANG_IDS.has(langId)) continue;
147
+ if (!langId || !CFG_RULES.has(langId)) continue;
1123
148
 
1124
149
  const absPath = path.join(rootDir, relPath);
1125
150
  let code;
@@ -1147,39 +172,74 @@ export async function buildCFGData(db, fileSymbols, rootDir, _engineOpts) {
1147
172
  const cfgRules = CFG_RULES.get(langId);
1148
173
  if (!cfgRules) continue;
1149
174
 
1150
- const complexityRules = COMPLEXITY_RULES.get(langId);
1151
- // complexityRules only needed for WASM fallback path
175
+ // WASM fallback: run file-level visitor walk to compute CFG for all functions
176
+ // that don't already have pre-computed data (from native engine or unified walk)
177
+ let visitorCfgByLine = null;
178
+ const needsVisitor =
179
+ tree &&
180
+ symbols.definitions.some(
181
+ (d) =>
182
+ (d.kind === 'function' || d.kind === 'method') &&
183
+ d.line &&
184
+ d.cfg !== null &&
185
+ !d.cfg?.blocks?.length,
186
+ );
187
+ if (needsVisitor) {
188
+ const visitor = createCfgVisitor(cfgRules);
189
+ const walkerOpts = {
190
+ functionNodeTypes: new Set(cfgRules.functionNodes),
191
+ nestingNodeTypes: new Set(),
192
+ getFunctionName: (node) => {
193
+ const nameNode = node.childForFieldName('name');
194
+ return nameNode ? nameNode.text : null;
195
+ },
196
+ };
197
+ const walkResults = walkWithVisitors(tree.rootNode, [visitor], langId, walkerOpts);
198
+ const cfgResults = walkResults.cfg || [];
199
+ visitorCfgByLine = new Map();
200
+ for (const r of cfgResults) {
201
+ if (r.funcNode) {
202
+ const line = r.funcNode.startPosition.row + 1;
203
+ if (!visitorCfgByLine.has(line)) visitorCfgByLine.set(line, []);
204
+ visitorCfgByLine.get(line).push(r);
205
+ }
206
+ }
207
+ }
1152
208
 
1153
209
  for (const def of symbols.definitions) {
1154
210
  if (def.kind !== 'function' && def.kind !== 'method') continue;
1155
211
  if (!def.line) continue;
1156
212
 
1157
- const row = getNodeId.get(def.name, relPath, def.line);
1158
- if (!row) continue;
213
+ const nodeId = getFunctionNodeId(db, def.name, relPath, def.line);
214
+ if (!nodeId) continue;
1159
215
 
1160
- // Native path: use pre-computed CFG from Rust engine
216
+ // Use pre-computed CFG (native engine or unified walk), then visitor fallback
1161
217
  let cfg = null;
1162
218
  if (def.cfg?.blocks?.length) {
1163
219
  cfg = def.cfg;
1164
- } else {
1165
- // WASM fallback: compute CFG from tree-sitter AST
1166
- if (!tree || !complexityRules) continue;
1167
- const funcNode = findFunctionNode(tree.rootNode, def.line, def.endLine, complexityRules);
1168
- if (!funcNode) continue;
1169
- cfg = buildFunctionCFG(funcNode, langId);
220
+ } else if (visitorCfgByLine) {
221
+ const candidates = visitorCfgByLine.get(def.line);
222
+ const r = !candidates
223
+ ? undefined
224
+ : candidates.length === 1
225
+ ? candidates[0]
226
+ : (candidates.find((c) => {
227
+ const n = c.funcNode.childForFieldName('name');
228
+ return n && n.text === def.name;
229
+ }) ?? candidates[0]);
230
+ if (r) cfg = { blocks: r.blocks, edges: r.edges };
1170
231
  }
1171
232
 
1172
233
  if (!cfg || cfg.blocks.length === 0) continue;
1173
234
 
1174
235
  // Clear old CFG data for this function
1175
- deleteEdges.run(row.id);
1176
- deleteBlocks.run(row.id);
236
+ deleteCfgForNode(db, nodeId);
1177
237
 
1178
238
  // Insert blocks and build index→dbId mapping
1179
239
  const blockDbIds = new Map();
1180
240
  for (const block of cfg.blocks) {
1181
241
  const result = insertBlock.run(
1182
- row.id,
242
+ nodeId,
1183
243
  block.index,
1184
244
  block.type,
1185
245
  block.startLine,
@@ -1194,7 +254,7 @@ export async function buildCFGData(db, fileSymbols, rootDir, _engineOpts) {
1194
254
  const sourceDbId = blockDbIds.get(edge.sourceIndex);
1195
255
  const targetDbId = blockDbIds.get(edge.targetIndex);
1196
256
  if (sourceDbId && targetDbId) {
1197
- insertEdge.run(row.id, sourceDbId, targetDbId, edge.kind);
257
+ insertEdge.run(nodeId, sourceDbId, targetDbId, edge.kind);
1198
258
  }
1199
259
  }
1200
260
 
@@ -1214,15 +274,6 @@ export async function buildCFGData(db, fileSymbols, rootDir, _engineOpts) {
1214
274
 
1215
275
  // ─── Query-Time Functions ───────────────────────────────────────────────
1216
276
 
1217
- function hasCfgTables(db) {
1218
- try {
1219
- db.prepare('SELECT 1 FROM cfg_blocks LIMIT 0').get();
1220
- return true;
1221
- } catch {
1222
- return false;
1223
- }
1224
- }
1225
-
1226
277
  function findNodes(db, name, opts = {}) {
1227
278
  const kinds = opts.kind ? [opts.kind] : ['function', 'method'];
1228
279
  const placeholders = kinds.map(() => '?').join(', ');
@@ -1255,72 +306,57 @@ function findNodes(db, name, opts = {}) {
1255
306
  */
1256
307
  export function cfgData(name, customDbPath, opts = {}) {
1257
308
  const db = openReadonlyOrFail(customDbPath);
1258
- const noTests = opts.noTests || false;
1259
-
1260
- if (!hasCfgTables(db)) {
1261
- db.close();
1262
- return {
1263
- name,
1264
- results: [],
1265
- warning:
1266
- 'No CFG data found. Rebuild with `codegraph build` (CFG is now included by default).',
1267
- };
1268
- }
309
+ try {
310
+ const noTests = opts.noTests || false;
311
+
312
+ if (!hasCfgTables(db)) {
313
+ return {
314
+ name,
315
+ results: [],
316
+ warning:
317
+ 'No CFG data found. Rebuild with `codegraph build` (CFG is now included by default).',
318
+ };
319
+ }
320
+
321
+ const nodes = findNodes(db, name, { noTests, file: opts.file, kind: opts.kind });
322
+ if (nodes.length === 0) {
323
+ return { name, results: [] };
324
+ }
325
+
326
+ const results = nodes.map((node) => {
327
+ const cfgBlocks = getCfgBlocks(db, node.id);
328
+ const cfgEdges = getCfgEdges(db, node.id);
329
+
330
+ return {
331
+ name: node.name,
332
+ kind: node.kind,
333
+ file: node.file,
334
+ line: node.line,
335
+ blocks: cfgBlocks.map((b) => ({
336
+ index: b.block_index,
337
+ type: b.block_type,
338
+ startLine: b.start_line,
339
+ endLine: b.end_line,
340
+ label: b.label,
341
+ })),
342
+ edges: cfgEdges.map((e) => ({
343
+ source: e.source_index,
344
+ sourceType: e.source_type,
345
+ target: e.target_index,
346
+ targetType: e.target_type,
347
+ kind: e.kind,
348
+ })),
349
+ summary: {
350
+ blockCount: cfgBlocks.length,
351
+ edgeCount: cfgEdges.length,
352
+ },
353
+ };
354
+ });
1269
355
 
1270
- const nodes = findNodes(db, name, { noTests, file: opts.file, kind: opts.kind });
1271
- if (nodes.length === 0) {
356
+ return paginateResult({ name, results }, 'results', opts);
357
+ } finally {
1272
358
  db.close();
1273
- return { name, results: [] };
1274
359
  }
1275
-
1276
- const blockStmt = db.prepare(
1277
- `SELECT id, block_index, block_type, start_line, end_line, label
1278
- FROM cfg_blocks WHERE function_node_id = ?
1279
- ORDER BY block_index`,
1280
- );
1281
- const edgeStmt = db.prepare(
1282
- `SELECT e.kind,
1283
- sb.block_index AS source_index, sb.block_type AS source_type,
1284
- tb.block_index AS target_index, tb.block_type AS target_type
1285
- FROM cfg_edges e
1286
- JOIN cfg_blocks sb ON e.source_block_id = sb.id
1287
- JOIN cfg_blocks tb ON e.target_block_id = tb.id
1288
- WHERE e.function_node_id = ?
1289
- ORDER BY sb.block_index, tb.block_index`,
1290
- );
1291
-
1292
- const results = nodes.map((node) => {
1293
- const cfgBlocks = blockStmt.all(node.id);
1294
- const cfgEdges = edgeStmt.all(node.id);
1295
-
1296
- return {
1297
- name: node.name,
1298
- kind: node.kind,
1299
- file: node.file,
1300
- line: node.line,
1301
- blocks: cfgBlocks.map((b) => ({
1302
- index: b.block_index,
1303
- type: b.block_type,
1304
- startLine: b.start_line,
1305
- endLine: b.end_line,
1306
- label: b.label,
1307
- })),
1308
- edges: cfgEdges.map((e) => ({
1309
- source: e.source_index,
1310
- sourceType: e.source_type,
1311
- target: e.target_index,
1312
- targetType: e.target_type,
1313
- kind: e.kind,
1314
- })),
1315
- summary: {
1316
- blockCount: cfgBlocks.length,
1317
- edgeCount: cfgEdges.length,
1318
- },
1319
- };
1320
- });
1321
-
1322
- db.close();
1323
- return paginateResult({ name, results }, 'results', opts);
1324
360
  }
1325
361
 
1326
362
  // ─── Export Formats ─────────────────────────────────────────────────────
@@ -1409,65 +445,3 @@ function edgeStyle(kind) {
1409
445
  if (kind === 'continue') return ', color=blue, style=dashed';
1410
446
  return '';
1411
447
  }
1412
-
1413
- // ─── CLI Printer ────────────────────────────────────────────────────────
1414
-
1415
- /**
1416
- * CLI display for cfg command.
1417
- */
1418
- export function cfg(name, customDbPath, opts = {}) {
1419
- const data = cfgData(name, customDbPath, opts);
1420
-
1421
- if (opts.json) {
1422
- console.log(JSON.stringify(data, null, 2));
1423
- return;
1424
- }
1425
- if (opts.ndjson) {
1426
- printNdjson(data.results);
1427
- return;
1428
- }
1429
-
1430
- if (data.warning) {
1431
- console.log(`\u26A0 ${data.warning}`);
1432
- return;
1433
- }
1434
- if (data.results.length === 0) {
1435
- console.log(`No symbols matching "${name}".`);
1436
- return;
1437
- }
1438
-
1439
- const format = opts.format || 'text';
1440
- if (format === 'dot') {
1441
- console.log(cfgToDOT(data));
1442
- return;
1443
- }
1444
- if (format === 'mermaid') {
1445
- console.log(cfgToMermaid(data));
1446
- return;
1447
- }
1448
-
1449
- // Text format
1450
- for (const r of data.results) {
1451
- console.log(`\n${r.kind} ${r.name} (${r.file}:${r.line})`);
1452
- console.log('\u2500'.repeat(60));
1453
- console.log(` Blocks: ${r.summary.blockCount} Edges: ${r.summary.edgeCount}`);
1454
-
1455
- if (r.blocks.length > 0) {
1456
- console.log('\n Blocks:');
1457
- for (const b of r.blocks) {
1458
- const loc = b.startLine
1459
- ? ` L${b.startLine}${b.endLine && b.endLine !== b.startLine ? `-${b.endLine}` : ''}`
1460
- : '';
1461
- const label = b.label ? ` (${b.label})` : '';
1462
- console.log(` [${b.index}] ${b.type}${label}${loc}`);
1463
- }
1464
- }
1465
-
1466
- if (r.edges.length > 0) {
1467
- console.log('\n Edges:');
1468
- for (const e of r.edges) {
1469
- console.log(` B${e.source} \u2192 B${e.target} [${e.kind}]`);
1470
- }
1471
- }
1472
- }
1473
- }