@optave/codegraph 3.1.1 → 3.1.3
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.
- package/README.md +6 -6
- package/package.json +7 -7
- package/src/ast-analysis/engine.js +365 -0
- package/src/ast-analysis/metrics.js +118 -0
- package/src/ast-analysis/visitor-utils.js +176 -0
- package/src/ast-analysis/visitor.js +162 -0
- package/src/ast-analysis/visitors/ast-store-visitor.js +150 -0
- package/src/ast-analysis/visitors/cfg-visitor.js +792 -0
- package/src/ast-analysis/visitors/complexity-visitor.js +243 -0
- package/src/ast-analysis/visitors/dataflow-visitor.js +358 -0
- package/src/ast.js +13 -140
- package/src/audit.js +2 -87
- package/src/batch.js +0 -25
- package/src/boundaries.js +1 -1
- package/src/branch-compare.js +1 -96
- package/src/builder.js +60 -178
- package/src/cfg.js +89 -883
- package/src/check.js +1 -84
- package/src/cli.js +31 -22
- package/src/cochange.js +1 -39
- package/src/commands/audit.js +88 -0
- package/src/commands/batch.js +26 -0
- package/src/commands/branch-compare.js +97 -0
- package/src/commands/cfg.js +55 -0
- package/src/commands/check.js +82 -0
- package/src/commands/cochange.js +37 -0
- package/src/commands/communities.js +69 -0
- package/src/commands/complexity.js +77 -0
- package/src/commands/dataflow.js +110 -0
- package/src/commands/flow.js +70 -0
- package/src/commands/manifesto.js +77 -0
- package/src/commands/owners.js +52 -0
- package/src/commands/query.js +21 -0
- package/src/commands/sequence.js +33 -0
- package/src/commands/structure.js +64 -0
- package/src/commands/triage.js +49 -0
- package/src/communities.js +12 -83
- package/src/complexity.js +43 -357
- package/src/cycles.js +1 -1
- package/src/dataflow.js +12 -665
- package/src/db/repository/build-stmts.js +104 -0
- package/src/db/repository/cached-stmt.js +19 -0
- package/src/db/repository/cfg.js +72 -0
- package/src/db/repository/cochange.js +54 -0
- package/src/db/repository/complexity.js +20 -0
- package/src/db/repository/dataflow.js +17 -0
- package/src/db/repository/edges.js +281 -0
- package/src/db/repository/embeddings.js +51 -0
- package/src/db/repository/graph-read.js +59 -0
- package/src/db/repository/index.js +43 -0
- package/src/db/repository/nodes.js +247 -0
- package/src/db.js +40 -1
- package/src/embedder.js +14 -34
- package/src/export.js +1 -1
- package/src/extractors/javascript.js +130 -5
- package/src/flow.js +2 -70
- package/src/index.js +30 -20
- package/src/{result-formatter.js → infrastructure/result-formatter.js} +1 -1
- package/src/kinds.js +1 -0
- package/src/manifesto.js +0 -76
- package/src/native.js +31 -9
- package/src/owners.js +1 -56
- package/src/parser.js +53 -2
- package/src/queries-cli.js +1 -1
- package/src/queries.js +79 -280
- package/src/sequence.js +5 -44
- package/src/structure.js +16 -75
- package/src/triage.js +1 -54
- package/src/viewer.js +1 -1
- package/src/watcher.js +7 -4
- package/src/db/repository.js +0 -134
- /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
|
+
}
|