@optave/codegraph 3.1.1 → 3.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/package.json +7 -7
  2. package/src/ast-analysis/engine.js +365 -0
  3. package/src/ast-analysis/metrics.js +118 -0
  4. package/src/ast-analysis/visitor-utils.js +176 -0
  5. package/src/ast-analysis/visitor.js +162 -0
  6. package/src/ast-analysis/visitors/ast-store-visitor.js +150 -0
  7. package/src/ast-analysis/visitors/cfg-visitor.js +792 -0
  8. package/src/ast-analysis/visitors/complexity-visitor.js +243 -0
  9. package/src/ast-analysis/visitors/dataflow-visitor.js +358 -0
  10. package/src/ast.js +13 -140
  11. package/src/audit.js +2 -87
  12. package/src/batch.js +0 -25
  13. package/src/boundaries.js +1 -1
  14. package/src/branch-compare.js +1 -96
  15. package/src/builder.js +48 -179
  16. package/src/cfg.js +89 -883
  17. package/src/check.js +1 -84
  18. package/src/cli.js +20 -19
  19. package/src/cochange.js +1 -39
  20. package/src/commands/audit.js +88 -0
  21. package/src/commands/batch.js +26 -0
  22. package/src/commands/branch-compare.js +97 -0
  23. package/src/commands/cfg.js +55 -0
  24. package/src/commands/check.js +82 -0
  25. package/src/commands/cochange.js +37 -0
  26. package/src/commands/communities.js +69 -0
  27. package/src/commands/complexity.js +77 -0
  28. package/src/commands/dataflow.js +110 -0
  29. package/src/commands/flow.js +70 -0
  30. package/src/commands/manifesto.js +77 -0
  31. package/src/commands/owners.js +52 -0
  32. package/src/commands/query.js +21 -0
  33. package/src/commands/sequence.js +33 -0
  34. package/src/commands/structure.js +64 -0
  35. package/src/commands/triage.js +49 -0
  36. package/src/communities.js +12 -83
  37. package/src/complexity.js +42 -356
  38. package/src/cycles.js +1 -1
  39. package/src/dataflow.js +12 -665
  40. package/src/db/repository/build-stmts.js +104 -0
  41. package/src/db/repository/cfg.js +83 -0
  42. package/src/db/repository/cochange.js +41 -0
  43. package/src/db/repository/complexity.js +15 -0
  44. package/src/db/repository/dataflow.js +12 -0
  45. package/src/db/repository/edges.js +259 -0
  46. package/src/db/repository/embeddings.js +40 -0
  47. package/src/db/repository/graph-read.js +39 -0
  48. package/src/db/repository/index.js +42 -0
  49. package/src/db/repository/nodes.js +236 -0
  50. package/src/db.js +40 -1
  51. package/src/embedder.js +14 -34
  52. package/src/export.js +1 -1
  53. package/src/extractors/javascript.js +130 -5
  54. package/src/flow.js +2 -70
  55. package/src/index.js +23 -19
  56. package/src/{result-formatter.js → infrastructure/result-formatter.js} +1 -1
  57. package/src/kinds.js +1 -0
  58. package/src/manifesto.js +0 -76
  59. package/src/owners.js +1 -56
  60. package/src/queries-cli.js +1 -1
  61. package/src/queries.js +79 -280
  62. package/src/sequence.js +5 -44
  63. package/src/structure.js +16 -75
  64. package/src/triage.js +1 -54
  65. package/src/viewer.js +1 -1
  66. package/src/watcher.js +7 -4
  67. package/src/db/repository.js +0 -134
  68. /package/src/{test-filter.js → infrastructure/test-filter.js} +0 -0
@@ -0,0 +1,792 @@
1
+ /**
2
+ * Visitor: Build intraprocedural Control Flow Graphs (CFGs) from tree-sitter AST.
3
+ *
4
+ * Replaces the statement-level traversal in cfg.js (buildFunctionCFG) with a
5
+ * node-level visitor that plugs into the unified walkWithVisitors framework.
6
+ * This eliminates the last redundant tree traversal (Mode B) in engine.js,
7
+ * unifying all 4 analyses into a single DFS walk.
8
+ *
9
+ * The visitor builds basic blocks and edges incrementally via enterNode/exitNode
10
+ * hooks, using a control-flow frame stack to track branch/loop/switch context.
11
+ */
12
+
13
+ /**
14
+ * Create a CFG visitor for use with walkWithVisitors.
15
+ *
16
+ * @param {object} cfgRules - CFG_RULES for the language
17
+ * @returns {Visitor}
18
+ */
19
+ export function createCfgVisitor(cfgRules) {
20
+ // ── Per-function state ──────────────────────────────────────────────
21
+ // Pushed/popped on enterFunction/exitFunction for nested function support.
22
+
23
+ /** @type {Array<object>} Stack of per-function CFG state */
24
+ const funcStateStack = [];
25
+
26
+ /** @type {object|null} Active per-function state */
27
+ let S = null;
28
+
29
+ // Collected results (one per top-level function)
30
+ const results = [];
31
+
32
+ function makeFuncState() {
33
+ const blocks = [];
34
+ const edges = [];
35
+ let nextIndex = 0;
36
+
37
+ function makeBlock(type, startLine = null, endLine = null, label = null) {
38
+ const block = { index: nextIndex++, type, startLine, endLine, label };
39
+ blocks.push(block);
40
+ return block;
41
+ }
42
+
43
+ function addEdge(source, target, kind) {
44
+ edges.push({ sourceIndex: source.index, targetIndex: target.index, kind });
45
+ }
46
+
47
+ const entry = makeBlock('entry');
48
+ const exit = makeBlock('exit');
49
+ const firstBody = makeBlock('body');
50
+ addEdge(entry, firstBody, 'fallthrough');
51
+
52
+ return {
53
+ blocks,
54
+ edges,
55
+ makeBlock,
56
+ addEdge,
57
+ entryBlock: entry,
58
+ exitBlock: exit,
59
+ currentBlock: firstBody,
60
+ loopStack: [],
61
+ labelMap: new Map(),
62
+ /** Control-flow frame stack for nested if/switch/try/loop/labeled */
63
+ cfgStack: [],
64
+ funcNode: null,
65
+ };
66
+ }
67
+
68
+ // ── Helpers ─────────────────────────────────────────────────────────
69
+
70
+ function isIfNode(type) {
71
+ return type === cfgRules.ifNode || cfgRules.ifNodes?.has(type);
72
+ }
73
+
74
+ function isForNode(type) {
75
+ return cfgRules.forNodes.has(type);
76
+ }
77
+
78
+ function isWhileNode(type) {
79
+ return type === cfgRules.whileNode || cfgRules.whileNodes?.has(type);
80
+ }
81
+
82
+ function isSwitchNode(type) {
83
+ return type === cfgRules.switchNode || cfgRules.switchNodes?.has(type);
84
+ }
85
+
86
+ function isCaseNode(type) {
87
+ return (
88
+ type === cfgRules.caseNode || type === cfgRules.defaultNode || cfgRules.caseNodes?.has(type)
89
+ );
90
+ }
91
+
92
+ function isBlockNode(type) {
93
+ return (
94
+ type === 'statement_list' || type === cfgRules.blockNode || cfgRules.blockNodes?.has(type)
95
+ );
96
+ }
97
+
98
+ /** Check if a node is a control-flow statement that we handle specially */
99
+ function isControlFlow(type) {
100
+ return (
101
+ isIfNode(type) ||
102
+ (cfgRules.unlessNode && type === cfgRules.unlessNode) ||
103
+ isForNode(type) ||
104
+ isWhileNode(type) ||
105
+ (cfgRules.untilNode && type === cfgRules.untilNode) ||
106
+ (cfgRules.doNode && type === cfgRules.doNode) ||
107
+ (cfgRules.infiniteLoopNode && type === cfgRules.infiniteLoopNode) ||
108
+ isSwitchNode(type) ||
109
+ (cfgRules.tryNode && type === cfgRules.tryNode) ||
110
+ type === cfgRules.returnNode ||
111
+ type === cfgRules.throwNode ||
112
+ type === cfgRules.breakNode ||
113
+ type === cfgRules.continueNode ||
114
+ type === cfgRules.labeledNode
115
+ );
116
+ }
117
+
118
+ /**
119
+ * Get the actual control-flow node (unwrapping expression_statement if needed).
120
+ */
121
+ function effectiveNode(node) {
122
+ if (node.type === 'expression_statement' && node.namedChildCount === 1) {
123
+ const inner = node.namedChild(0);
124
+ if (isControlFlow(inner.type)) return inner;
125
+ }
126
+ return node;
127
+ }
128
+
129
+ /**
130
+ * Register a loop/switch in label map for labeled break/continue.
131
+ */
132
+ function registerLabelCtx(headerBlock, exitBlock) {
133
+ for (const [, ctx] of S.labelMap) {
134
+ if (!ctx.headerBlock) {
135
+ ctx.headerBlock = headerBlock;
136
+ ctx.exitBlock = exitBlock;
137
+ }
138
+ }
139
+ }
140
+
141
+ /**
142
+ * Get statements from a body node (block or single statement).
143
+ * Returns effective (unwrapped) nodes.
144
+ */
145
+ function getBodyStatements(bodyNode) {
146
+ if (!bodyNode) return [];
147
+ if (isBlockNode(bodyNode.type)) {
148
+ const stmts = [];
149
+ for (let i = 0; i < bodyNode.namedChildCount; i++) {
150
+ const child = bodyNode.namedChild(i);
151
+ if (child.type === 'statement_list') {
152
+ for (let j = 0; j < child.namedChildCount; j++) {
153
+ stmts.push(child.namedChild(j));
154
+ }
155
+ } else {
156
+ stmts.push(child);
157
+ }
158
+ }
159
+ return stmts;
160
+ }
161
+ return [bodyNode];
162
+ }
163
+
164
+ // ── Statement-level processing (replicates buildFunctionCFG logic) ──
165
+ // The visitor delegates to these for each control-flow construct,
166
+ // processing the body statements sequentially just like the original.
167
+
168
+ function processStatements(stmts, currentBlock) {
169
+ let cur = currentBlock;
170
+ for (const stmt of stmts) {
171
+ if (!cur) break;
172
+ cur = processStatement(stmt, cur);
173
+ }
174
+ return cur;
175
+ }
176
+
177
+ function processStatement(stmt, currentBlock) {
178
+ if (!stmt || !currentBlock) return currentBlock;
179
+
180
+ // Unwrap expression_statement for Rust-style control flow expressions
181
+ const effNode = effectiveNode(stmt);
182
+ const type = effNode.type;
183
+
184
+ // Labeled statement
185
+ if (type === cfgRules.labeledNode) {
186
+ return processLabeled(effNode, currentBlock);
187
+ }
188
+
189
+ // If / unless
190
+ if (isIfNode(type) || (cfgRules.unlessNode && type === cfgRules.unlessNode)) {
191
+ return processIf(effNode, currentBlock);
192
+ }
193
+
194
+ // For loops
195
+ if (isForNode(type)) {
196
+ return processForLoop(effNode, currentBlock);
197
+ }
198
+
199
+ // While / until
200
+ if (isWhileNode(type) || (cfgRules.untilNode && type === cfgRules.untilNode)) {
201
+ return processWhileLoop(effNode, currentBlock);
202
+ }
203
+
204
+ // Do-while
205
+ if (cfgRules.doNode && type === cfgRules.doNode) {
206
+ return processDoWhileLoop(effNode, currentBlock);
207
+ }
208
+
209
+ // Infinite loop (Rust)
210
+ if (cfgRules.infiniteLoopNode && type === cfgRules.infiniteLoopNode) {
211
+ return processInfiniteLoop(effNode, currentBlock);
212
+ }
213
+
214
+ // Switch / match
215
+ if (isSwitchNode(type)) {
216
+ return processSwitch(effNode, currentBlock);
217
+ }
218
+
219
+ // Try/catch/finally
220
+ if (cfgRules.tryNode && type === cfgRules.tryNode) {
221
+ return processTryCatch(effNode, currentBlock);
222
+ }
223
+
224
+ // Return
225
+ if (type === cfgRules.returnNode) {
226
+ currentBlock.endLine = effNode.startPosition.row + 1;
227
+ S.addEdge(currentBlock, S.exitBlock, 'return');
228
+ return null;
229
+ }
230
+
231
+ // Throw
232
+ if (type === cfgRules.throwNode) {
233
+ currentBlock.endLine = effNode.startPosition.row + 1;
234
+ S.addEdge(currentBlock, S.exitBlock, 'exception');
235
+ return null;
236
+ }
237
+
238
+ // Break
239
+ if (type === cfgRules.breakNode) {
240
+ return processBreak(effNode, currentBlock);
241
+ }
242
+
243
+ // Continue
244
+ if (type === cfgRules.continueNode) {
245
+ return processContinue(effNode, currentBlock);
246
+ }
247
+
248
+ // Regular statement — extend current block
249
+ if (!currentBlock.startLine) {
250
+ currentBlock.startLine = stmt.startPosition.row + 1;
251
+ }
252
+ currentBlock.endLine = stmt.endPosition.row + 1;
253
+ return currentBlock;
254
+ }
255
+
256
+ function processLabeled(node, currentBlock) {
257
+ const labelNode = node.childForFieldName('label');
258
+ const labelName = labelNode ? labelNode.text : null;
259
+ const body = node.childForFieldName('body');
260
+ if (body && labelName) {
261
+ const labelCtx = { headerBlock: null, exitBlock: null };
262
+ S.labelMap.set(labelName, labelCtx);
263
+ const result = processStatement(body, currentBlock);
264
+ S.labelMap.delete(labelName);
265
+ return result;
266
+ }
267
+ return currentBlock;
268
+ }
269
+
270
+ function processBreak(node, currentBlock) {
271
+ const labelNode = node.childForFieldName('label');
272
+ const labelName = labelNode ? labelNode.text : null;
273
+
274
+ let target = null;
275
+ if (labelName && S.labelMap.has(labelName)) {
276
+ target = S.labelMap.get(labelName).exitBlock;
277
+ } else if (S.loopStack.length > 0) {
278
+ target = S.loopStack[S.loopStack.length - 1].exitBlock;
279
+ }
280
+
281
+ if (target) {
282
+ currentBlock.endLine = node.startPosition.row + 1;
283
+ S.addEdge(currentBlock, target, 'break');
284
+ return null;
285
+ }
286
+ return currentBlock;
287
+ }
288
+
289
+ function processContinue(node, currentBlock) {
290
+ const labelNode = node.childForFieldName('label');
291
+ const labelName = labelNode ? labelNode.text : null;
292
+
293
+ let target = null;
294
+ if (labelName && S.labelMap.has(labelName)) {
295
+ target = S.labelMap.get(labelName).headerBlock;
296
+ } else if (S.loopStack.length > 0) {
297
+ target = S.loopStack[S.loopStack.length - 1].headerBlock;
298
+ }
299
+
300
+ if (target) {
301
+ currentBlock.endLine = node.startPosition.row + 1;
302
+ S.addEdge(currentBlock, target, 'continue');
303
+ return null;
304
+ }
305
+ return currentBlock;
306
+ }
307
+
308
+ // ── If/else-if/else ─────────────────────────────────────────────────
309
+
310
+ function processIf(ifStmt, currentBlock) {
311
+ currentBlock.endLine = ifStmt.startPosition.row + 1;
312
+
313
+ const condBlock = S.makeBlock(
314
+ 'condition',
315
+ ifStmt.startPosition.row + 1,
316
+ ifStmt.startPosition.row + 1,
317
+ 'if',
318
+ );
319
+ S.addEdge(currentBlock, condBlock, 'fallthrough');
320
+
321
+ const joinBlock = S.makeBlock('body');
322
+
323
+ // True branch
324
+ const consequentField = cfgRules.ifConsequentField || 'consequence';
325
+ const consequent = ifStmt.childForFieldName(consequentField);
326
+ const trueBlock = S.makeBlock('branch_true', null, null, 'then');
327
+ S.addEdge(condBlock, trueBlock, 'branch_true');
328
+ const trueStmts = getBodyStatements(consequent);
329
+ const trueEnd = processStatements(trueStmts, trueBlock);
330
+ if (trueEnd) {
331
+ S.addEdge(trueEnd, joinBlock, 'fallthrough');
332
+ }
333
+
334
+ // False branch
335
+ if (cfgRules.elifNode) {
336
+ processElifSiblings(ifStmt, condBlock, joinBlock);
337
+ } else {
338
+ const alternative = ifStmt.childForFieldName('alternative');
339
+ if (alternative) {
340
+ if (cfgRules.elseViaAlternative && alternative.type !== cfgRules.elseClause) {
341
+ // Pattern C: direct alternative (Go, Java, C#)
342
+ if (isIfNode(alternative.type)) {
343
+ const falseBlock = S.makeBlock('branch_false', null, null, 'else-if');
344
+ S.addEdge(condBlock, falseBlock, 'branch_false');
345
+ const elseIfEnd = processIf(alternative, falseBlock);
346
+ if (elseIfEnd) S.addEdge(elseIfEnd, joinBlock, 'fallthrough');
347
+ } else {
348
+ const falseBlock = S.makeBlock('branch_false', null, null, 'else');
349
+ S.addEdge(condBlock, falseBlock, 'branch_false');
350
+ const falseStmts = getBodyStatements(alternative);
351
+ const falseEnd = processStatements(falseStmts, falseBlock);
352
+ if (falseEnd) S.addEdge(falseEnd, joinBlock, 'fallthrough');
353
+ }
354
+ } else if (alternative.type === cfgRules.elseClause) {
355
+ // Pattern A: else_clause wrapper (JS/TS, Rust)
356
+ const elseChildren = [];
357
+ for (let i = 0; i < alternative.namedChildCount; i++) {
358
+ elseChildren.push(alternative.namedChild(i));
359
+ }
360
+ if (elseChildren.length === 1 && isIfNode(elseChildren[0].type)) {
361
+ const falseBlock = S.makeBlock('branch_false', null, null, 'else-if');
362
+ S.addEdge(condBlock, falseBlock, 'branch_false');
363
+ const elseIfEnd = processIf(elseChildren[0], falseBlock);
364
+ if (elseIfEnd) S.addEdge(elseIfEnd, joinBlock, 'fallthrough');
365
+ } else {
366
+ const falseBlock = S.makeBlock('branch_false', null, null, 'else');
367
+ S.addEdge(condBlock, falseBlock, 'branch_false');
368
+ const falseEnd = processStatements(elseChildren, falseBlock);
369
+ if (falseEnd) S.addEdge(falseEnd, joinBlock, 'fallthrough');
370
+ }
371
+ }
372
+ } else {
373
+ // No else
374
+ S.addEdge(condBlock, joinBlock, 'branch_false');
375
+ }
376
+ }
377
+
378
+ return joinBlock;
379
+ }
380
+
381
+ function processElifSiblings(ifStmt, firstCondBlock, joinBlock) {
382
+ let lastCondBlock = firstCondBlock;
383
+ let foundElse = false;
384
+
385
+ for (let i = 0; i < ifStmt.namedChildCount; i++) {
386
+ const child = ifStmt.namedChild(i);
387
+
388
+ if (child.type === cfgRules.elifNode) {
389
+ const elifCondBlock = S.makeBlock(
390
+ 'condition',
391
+ child.startPosition.row + 1,
392
+ child.startPosition.row + 1,
393
+ 'else-if',
394
+ );
395
+ S.addEdge(lastCondBlock, elifCondBlock, 'branch_false');
396
+
397
+ const elifConsequentField = cfgRules.ifConsequentField || 'consequence';
398
+ const elifConsequent = child.childForFieldName(elifConsequentField);
399
+ const elifTrueBlock = S.makeBlock('branch_true', null, null, 'then');
400
+ S.addEdge(elifCondBlock, elifTrueBlock, 'branch_true');
401
+ const elifTrueStmts = getBodyStatements(elifConsequent);
402
+ const elifTrueEnd = processStatements(elifTrueStmts, elifTrueBlock);
403
+ if (elifTrueEnd) S.addEdge(elifTrueEnd, joinBlock, 'fallthrough');
404
+
405
+ lastCondBlock = elifCondBlock;
406
+ } else if (child.type === cfgRules.elseClause) {
407
+ const elseBlock = S.makeBlock('branch_false', null, null, 'else');
408
+ S.addEdge(lastCondBlock, elseBlock, 'branch_false');
409
+
410
+ const elseBody = child.childForFieldName('body');
411
+ let elseStmts;
412
+ if (elseBody) {
413
+ elseStmts = getBodyStatements(elseBody);
414
+ } else {
415
+ elseStmts = [];
416
+ for (let j = 0; j < child.namedChildCount; j++) {
417
+ elseStmts.push(child.namedChild(j));
418
+ }
419
+ }
420
+ const elseEnd = processStatements(elseStmts, elseBlock);
421
+ if (elseEnd) S.addEdge(elseEnd, joinBlock, 'fallthrough');
422
+
423
+ foundElse = true;
424
+ }
425
+ }
426
+
427
+ if (!foundElse) {
428
+ S.addEdge(lastCondBlock, joinBlock, 'branch_false');
429
+ }
430
+ }
431
+
432
+ // ── Loops ───────────────────────────────────────────────────────────
433
+
434
+ function processForLoop(forStmt, currentBlock) {
435
+ const headerBlock = S.makeBlock(
436
+ 'loop_header',
437
+ forStmt.startPosition.row + 1,
438
+ forStmt.startPosition.row + 1,
439
+ 'for',
440
+ );
441
+ S.addEdge(currentBlock, headerBlock, 'fallthrough');
442
+
443
+ const loopExitBlock = S.makeBlock('body');
444
+ const loopCtx = { headerBlock, exitBlock: loopExitBlock };
445
+ S.loopStack.push(loopCtx);
446
+ registerLabelCtx(headerBlock, loopExitBlock);
447
+
448
+ const body = forStmt.childForFieldName('body');
449
+ const bodyBlock = S.makeBlock('loop_body');
450
+ S.addEdge(headerBlock, bodyBlock, 'branch_true');
451
+
452
+ const bodyStmts = getBodyStatements(body);
453
+ const bodyEnd = processStatements(bodyStmts, bodyBlock);
454
+ if (bodyEnd) S.addEdge(bodyEnd, headerBlock, 'loop_back');
455
+
456
+ S.addEdge(headerBlock, loopExitBlock, 'loop_exit');
457
+ S.loopStack.pop();
458
+ return loopExitBlock;
459
+ }
460
+
461
+ function processWhileLoop(whileStmt, currentBlock) {
462
+ const headerBlock = S.makeBlock(
463
+ 'loop_header',
464
+ whileStmt.startPosition.row + 1,
465
+ whileStmt.startPosition.row + 1,
466
+ 'while',
467
+ );
468
+ S.addEdge(currentBlock, headerBlock, 'fallthrough');
469
+
470
+ const loopExitBlock = S.makeBlock('body');
471
+ const loopCtx = { headerBlock, exitBlock: loopExitBlock };
472
+ S.loopStack.push(loopCtx);
473
+ registerLabelCtx(headerBlock, loopExitBlock);
474
+
475
+ const body = whileStmt.childForFieldName('body');
476
+ const bodyBlock = S.makeBlock('loop_body');
477
+ S.addEdge(headerBlock, bodyBlock, 'branch_true');
478
+
479
+ const bodyStmts = getBodyStatements(body);
480
+ const bodyEnd = processStatements(bodyStmts, bodyBlock);
481
+ if (bodyEnd) S.addEdge(bodyEnd, headerBlock, 'loop_back');
482
+
483
+ S.addEdge(headerBlock, loopExitBlock, 'loop_exit');
484
+ S.loopStack.pop();
485
+ return loopExitBlock;
486
+ }
487
+
488
+ function processDoWhileLoop(doStmt, currentBlock) {
489
+ const bodyBlock = S.makeBlock('loop_body', doStmt.startPosition.row + 1, null, 'do');
490
+ S.addEdge(currentBlock, bodyBlock, 'fallthrough');
491
+
492
+ const condBlock = S.makeBlock('loop_header', null, null, 'do-while');
493
+ const loopExitBlock = S.makeBlock('body');
494
+
495
+ const loopCtx = { headerBlock: condBlock, exitBlock: loopExitBlock };
496
+ S.loopStack.push(loopCtx);
497
+ registerLabelCtx(condBlock, loopExitBlock);
498
+
499
+ const body = doStmt.childForFieldName('body');
500
+ const bodyStmts = getBodyStatements(body);
501
+ const bodyEnd = processStatements(bodyStmts, bodyBlock);
502
+ if (bodyEnd) S.addEdge(bodyEnd, condBlock, 'fallthrough');
503
+
504
+ S.addEdge(condBlock, bodyBlock, 'loop_back');
505
+ S.addEdge(condBlock, loopExitBlock, 'loop_exit');
506
+
507
+ S.loopStack.pop();
508
+ return loopExitBlock;
509
+ }
510
+
511
+ function processInfiniteLoop(loopStmt, currentBlock) {
512
+ const headerBlock = S.makeBlock(
513
+ 'loop_header',
514
+ loopStmt.startPosition.row + 1,
515
+ loopStmt.startPosition.row + 1,
516
+ 'loop',
517
+ );
518
+ S.addEdge(currentBlock, headerBlock, 'fallthrough');
519
+
520
+ const loopExitBlock = S.makeBlock('body');
521
+ const loopCtx = { headerBlock, exitBlock: loopExitBlock };
522
+ S.loopStack.push(loopCtx);
523
+ registerLabelCtx(headerBlock, loopExitBlock);
524
+
525
+ const body = loopStmt.childForFieldName('body');
526
+ const bodyBlock = S.makeBlock('loop_body');
527
+ S.addEdge(headerBlock, bodyBlock, 'branch_true');
528
+
529
+ const bodyStmts = getBodyStatements(body);
530
+ const bodyEnd = processStatements(bodyStmts, bodyBlock);
531
+ if (bodyEnd) S.addEdge(bodyEnd, headerBlock, 'loop_back');
532
+
533
+ // No loop_exit from header — only via break
534
+ S.loopStack.pop();
535
+ return loopExitBlock;
536
+ }
537
+
538
+ // ── Switch / match ──────────────────────────────────────────────────
539
+
540
+ function processSwitch(switchStmt, currentBlock) {
541
+ currentBlock.endLine = switchStmt.startPosition.row + 1;
542
+
543
+ const switchHeader = S.makeBlock(
544
+ 'condition',
545
+ switchStmt.startPosition.row + 1,
546
+ switchStmt.startPosition.row + 1,
547
+ 'switch',
548
+ );
549
+ S.addEdge(currentBlock, switchHeader, 'fallthrough');
550
+
551
+ const joinBlock = S.makeBlock('body');
552
+ const switchCtx = { headerBlock: switchHeader, exitBlock: joinBlock };
553
+ S.loopStack.push(switchCtx);
554
+
555
+ const switchBody = switchStmt.childForFieldName('body');
556
+ const container = switchBody || switchStmt;
557
+
558
+ let hasDefault = false;
559
+ for (let i = 0; i < container.namedChildCount; i++) {
560
+ const caseClause = container.namedChild(i);
561
+
562
+ const isDefault = caseClause.type === cfgRules.defaultNode;
563
+ const isCase = isDefault || isCaseNode(caseClause.type);
564
+ if (!isCase) continue;
565
+
566
+ const caseLabel = isDefault ? 'default' : 'case';
567
+ const caseBlock = S.makeBlock('case', caseClause.startPosition.row + 1, null, caseLabel);
568
+ S.addEdge(switchHeader, caseBlock, isDefault ? 'branch_false' : 'branch_true');
569
+ if (isDefault) hasDefault = true;
570
+
571
+ // Extract case body
572
+ const caseBodyNode =
573
+ caseClause.childForFieldName('body') || caseClause.childForFieldName('consequence');
574
+ let caseStmts;
575
+ if (caseBodyNode) {
576
+ caseStmts = getBodyStatements(caseBodyNode);
577
+ } else {
578
+ caseStmts = [];
579
+ const valueNode = caseClause.childForFieldName('value');
580
+ const patternNode = caseClause.childForFieldName('pattern');
581
+ for (let j = 0; j < caseClause.namedChildCount; j++) {
582
+ const child = caseClause.namedChild(j);
583
+ if (child !== valueNode && child !== patternNode && child.type !== 'switch_label') {
584
+ if (child.type === 'statement_list') {
585
+ for (let k = 0; k < child.namedChildCount; k++) {
586
+ caseStmts.push(child.namedChild(k));
587
+ }
588
+ } else {
589
+ caseStmts.push(child);
590
+ }
591
+ }
592
+ }
593
+ }
594
+
595
+ const caseEnd = processStatements(caseStmts, caseBlock);
596
+ if (caseEnd) S.addEdge(caseEnd, joinBlock, 'fallthrough');
597
+ }
598
+
599
+ if (!hasDefault) {
600
+ S.addEdge(switchHeader, joinBlock, 'branch_false');
601
+ }
602
+
603
+ S.loopStack.pop();
604
+ return joinBlock;
605
+ }
606
+
607
+ // ── Try/catch/finally ───────────────────────────────────────────────
608
+
609
+ function processTryCatch(tryStmt, currentBlock) {
610
+ currentBlock.endLine = tryStmt.startPosition.row + 1;
611
+
612
+ const joinBlock = S.makeBlock('body');
613
+
614
+ // Try body
615
+ const tryBody = tryStmt.childForFieldName('body');
616
+ let tryBodyStart;
617
+ let tryStmts;
618
+ if (tryBody) {
619
+ tryBodyStart = tryBody.startPosition.row + 1;
620
+ tryStmts = getBodyStatements(tryBody);
621
+ } else {
622
+ tryBodyStart = tryStmt.startPosition.row + 1;
623
+ tryStmts = [];
624
+ for (let i = 0; i < tryStmt.namedChildCount; i++) {
625
+ const child = tryStmt.namedChild(i);
626
+ if (cfgRules.catchNode && child.type === cfgRules.catchNode) continue;
627
+ if (cfgRules.finallyNode && child.type === cfgRules.finallyNode) continue;
628
+ tryStmts.push(child);
629
+ }
630
+ }
631
+
632
+ const tryBlock = S.makeBlock('body', tryBodyStart, null, 'try');
633
+ S.addEdge(currentBlock, tryBlock, 'fallthrough');
634
+ const tryEnd = processStatements(tryStmts, tryBlock);
635
+
636
+ // Find catch and finally handlers
637
+ let catchHandler = null;
638
+ let finallyHandler = null;
639
+ for (let i = 0; i < tryStmt.namedChildCount; i++) {
640
+ const child = tryStmt.namedChild(i);
641
+ if (cfgRules.catchNode && child.type === cfgRules.catchNode) catchHandler = child;
642
+ if (cfgRules.finallyNode && child.type === cfgRules.finallyNode) finallyHandler = child;
643
+ }
644
+
645
+ if (catchHandler) {
646
+ const catchBlock = S.makeBlock('catch', catchHandler.startPosition.row + 1, null, 'catch');
647
+ S.addEdge(tryBlock, catchBlock, 'exception');
648
+
649
+ const catchBodyNode = catchHandler.childForFieldName('body');
650
+ let catchStmts;
651
+ if (catchBodyNode) {
652
+ catchStmts = getBodyStatements(catchBodyNode);
653
+ } else {
654
+ catchStmts = [];
655
+ for (let i = 0; i < catchHandler.namedChildCount; i++) {
656
+ catchStmts.push(catchHandler.namedChild(i));
657
+ }
658
+ }
659
+ const catchEnd = processStatements(catchStmts, catchBlock);
660
+
661
+ if (finallyHandler) {
662
+ const finallyBlock = S.makeBlock(
663
+ 'finally',
664
+ finallyHandler.startPosition.row + 1,
665
+ null,
666
+ 'finally',
667
+ );
668
+ if (tryEnd) S.addEdge(tryEnd, finallyBlock, 'fallthrough');
669
+ if (catchEnd) S.addEdge(catchEnd, finallyBlock, 'fallthrough');
670
+
671
+ const finallyBodyNode = finallyHandler.childForFieldName('body');
672
+ const finallyStmts = finallyBodyNode
673
+ ? getBodyStatements(finallyBodyNode)
674
+ : getBodyStatements(finallyHandler);
675
+ const finallyEnd = processStatements(finallyStmts, finallyBlock);
676
+ if (finallyEnd) S.addEdge(finallyEnd, joinBlock, 'fallthrough');
677
+ } else {
678
+ if (tryEnd) S.addEdge(tryEnd, joinBlock, 'fallthrough');
679
+ if (catchEnd) S.addEdge(catchEnd, joinBlock, 'fallthrough');
680
+ }
681
+ } else if (finallyHandler) {
682
+ const finallyBlock = S.makeBlock(
683
+ 'finally',
684
+ finallyHandler.startPosition.row + 1,
685
+ null,
686
+ 'finally',
687
+ );
688
+ if (tryEnd) S.addEdge(tryEnd, finallyBlock, 'fallthrough');
689
+
690
+ const finallyBodyNode = finallyHandler.childForFieldName('body');
691
+ const finallyStmts = finallyBodyNode
692
+ ? getBodyStatements(finallyBodyNode)
693
+ : getBodyStatements(finallyHandler);
694
+ const finallyEnd = processStatements(finallyStmts, finallyBlock);
695
+ if (finallyEnd) S.addEdge(finallyEnd, joinBlock, 'fallthrough');
696
+ } else {
697
+ if (tryEnd) S.addEdge(tryEnd, joinBlock, 'fallthrough');
698
+ }
699
+
700
+ return joinBlock;
701
+ }
702
+
703
+ // ── Visitor interface ───────────────────────────────────────────────
704
+
705
+ return {
706
+ name: 'cfg',
707
+ functionNodeTypes: cfgRules.functionNodes,
708
+
709
+ enterFunction(funcNode, _funcName, _context) {
710
+ if (S) {
711
+ // Nested function — push current state
712
+ funcStateStack.push(S);
713
+ }
714
+ S = makeFuncState();
715
+ S.funcNode = funcNode;
716
+
717
+ // Check for expression body (arrow functions): no block body
718
+ const body = funcNode.childForFieldName('body');
719
+ if (!body) {
720
+ // No body at all — entry → exit
721
+ // Remove the firstBody block and its edge
722
+ S.blocks.length = 2; // keep entry + exit
723
+ S.edges.length = 0;
724
+ S.addEdge(S.entryBlock, S.exitBlock, 'fallthrough');
725
+ S.currentBlock = null;
726
+ return;
727
+ }
728
+
729
+ if (!isBlockNode(body.type)) {
730
+ // Expression body (e.g., arrow function `(x) => x + 1`)
731
+ // entry → body → exit (body is the expression)
732
+ const bodyBlock = S.blocks[2]; // the firstBody we already created
733
+ bodyBlock.startLine = body.startPosition.row + 1;
734
+ bodyBlock.endLine = body.endPosition.row + 1;
735
+ S.addEdge(bodyBlock, S.exitBlock, 'fallthrough');
736
+ S.currentBlock = null; // no further processing needed
737
+ return;
738
+ }
739
+
740
+ // Block body — process statements
741
+ const stmts = getBodyStatements(body);
742
+ if (stmts.length === 0) {
743
+ // Empty function
744
+ S.blocks.length = 2;
745
+ S.edges.length = 0;
746
+ S.addEdge(S.entryBlock, S.exitBlock, 'fallthrough');
747
+ S.currentBlock = null;
748
+ return;
749
+ }
750
+
751
+ // Process all body statements using the statement-level processor
752
+ const firstBody = S.blocks[2]; // the firstBody block
753
+ const lastBlock = processStatements(stmts, firstBody);
754
+ if (lastBlock) {
755
+ S.addEdge(lastBlock, S.exitBlock, 'fallthrough');
756
+ }
757
+ S.currentBlock = null; // done processing
758
+ },
759
+
760
+ exitFunction(funcNode, _funcName, _context) {
761
+ if (S && S.funcNode === funcNode) {
762
+ // Derive cyclomatic complexity from CFG: E - N + 2
763
+ const cyclomatic = S.edges.length - S.blocks.length + 2;
764
+ results.push({
765
+ funcNode: S.funcNode,
766
+ blocks: S.blocks,
767
+ edges: S.edges,
768
+ cyclomatic: Math.max(cyclomatic, 1),
769
+ });
770
+ }
771
+
772
+ // Pop to parent function state (if nested)
773
+ S = funcStateStack.length > 0 ? funcStateStack.pop() : null;
774
+ },
775
+
776
+ enterNode(_node, _context) {
777
+ // No-op — all CFG construction is done in enterFunction via
778
+ // processStatements. We intentionally do NOT return skipChildren here
779
+ // so that the walker still recurses into children, allowing nested
780
+ // function definitions to trigger enterFunction/exitFunction and get
781
+ // their own CFG computed via the funcStateStack.
782
+ },
783
+
784
+ exitNode(_node, _context) {
785
+ // No-op — all work done in enterFunction/exitFunction
786
+ },
787
+
788
+ finish() {
789
+ return results;
790
+ },
791
+ };
792
+ }