@optave/codegraph 3.1.5 → 3.2.0

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