@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.
- package/README.md +3 -2
- package/package.json +7 -7
- package/src/ast-analysis/engine.js +252 -258
- package/src/ast-analysis/shared.js +0 -12
- package/src/ast-analysis/visitors/cfg-visitor.js +635 -649
- package/src/ast-analysis/visitors/complexity-visitor.js +135 -139
- package/src/ast-analysis/visitors/dataflow-visitor.js +230 -224
- package/src/cli/commands/ast.js +2 -1
- package/src/cli/commands/audit.js +2 -1
- package/src/cli/commands/batch.js +2 -1
- package/src/cli/commands/brief.js +12 -0
- package/src/cli/commands/cfg.js +2 -1
- package/src/cli/commands/check.js +20 -23
- package/src/cli/commands/children.js +6 -1
- package/src/cli/commands/complexity.js +2 -1
- package/src/cli/commands/context.js +6 -1
- package/src/cli/commands/dataflow.js +2 -1
- package/src/cli/commands/deps.js +8 -3
- package/src/cli/commands/flow.js +2 -1
- package/src/cli/commands/fn-impact.js +6 -1
- package/src/cli/commands/owners.js +4 -2
- package/src/cli/commands/query.js +6 -1
- package/src/cli/commands/roles.js +2 -1
- package/src/cli/commands/search.js +8 -2
- package/src/cli/commands/sequence.js +2 -1
- package/src/cli/commands/triage.js +38 -27
- package/src/db/connection.js +18 -12
- package/src/db/migrations.js +41 -64
- package/src/db/query-builder.js +60 -4
- package/src/db/repository/in-memory-repository.js +27 -16
- package/src/db/repository/nodes.js +8 -10
- package/src/domain/analysis/brief.js +155 -0
- package/src/domain/analysis/context.js +174 -190
- package/src/domain/analysis/dependencies.js +200 -146
- package/src/domain/analysis/exports.js +3 -2
- package/src/domain/analysis/impact.js +267 -152
- package/src/domain/analysis/module-map.js +247 -221
- package/src/domain/analysis/roles.js +8 -5
- package/src/domain/analysis/symbol-lookup.js +7 -5
- package/src/domain/graph/builder/helpers.js +1 -1
- package/src/domain/graph/builder/incremental.js +116 -90
- package/src/domain/graph/builder/pipeline.js +106 -80
- package/src/domain/graph/builder/stages/build-edges.js +318 -239
- package/src/domain/graph/builder/stages/detect-changes.js +198 -177
- package/src/domain/graph/builder/stages/insert-nodes.js +147 -139
- package/src/domain/graph/watcher.js +2 -2
- package/src/domain/parser.js +20 -11
- package/src/domain/queries.js +1 -0
- package/src/domain/search/search/filters.js +9 -5
- package/src/domain/search/search/keyword.js +12 -5
- package/src/domain/search/search/prepare.js +13 -5
- package/src/extractors/csharp.js +224 -207
- package/src/extractors/go.js +176 -172
- package/src/extractors/hcl.js +94 -78
- package/src/extractors/java.js +213 -207
- package/src/extractors/javascript.js +274 -304
- package/src/extractors/php.js +234 -221
- package/src/extractors/python.js +252 -250
- package/src/extractors/ruby.js +192 -185
- package/src/extractors/rust.js +182 -167
- package/src/features/ast.js +5 -3
- package/src/features/audit.js +4 -2
- package/src/features/boundaries.js +98 -83
- package/src/features/cfg.js +134 -143
- package/src/features/communities.js +68 -53
- package/src/features/complexity.js +143 -132
- package/src/features/dataflow.js +146 -149
- package/src/features/export.js +3 -3
- package/src/features/graph-enrichment.js +2 -2
- package/src/features/manifesto.js +9 -6
- package/src/features/owners.js +4 -3
- package/src/features/sequence.js +152 -141
- package/src/features/shared/find-nodes.js +31 -0
- package/src/features/structure.js +130 -99
- package/src/features/triage.js +83 -68
- package/src/graph/classifiers/risk.js +3 -2
- package/src/graph/classifiers/roles.js +6 -3
- package/src/index.js +1 -0
- package/src/mcp/server.js +65 -56
- package/src/mcp/tool-registry.js +13 -0
- package/src/mcp/tools/brief.js +8 -0
- package/src/mcp/tools/index.js +2 -0
- package/src/presentation/brief.js +51 -0
- package/src/presentation/queries-cli/exports.js +21 -14
- package/src/presentation/queries-cli/impact.js +55 -39
- package/src/presentation/queries-cli/inspect.js +184 -189
- package/src/presentation/queries-cli/overview.js +57 -58
- package/src/presentation/queries-cli/path.js +36 -29
- package/src/presentation/table.js +0 -8
- package/src/shared/generators.js +7 -3
- 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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
19
|
+
function isForNode(type, cfgRules) {
|
|
20
|
+
return cfgRules.forNodes.has(type);
|
|
21
|
+
}
|
|
73
22
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
23
|
+
function isWhileNode(type, cfgRules) {
|
|
24
|
+
return type === cfgRules.whileNode || cfgRules.whileNodes?.has(type);
|
|
25
|
+
}
|
|
77
26
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
27
|
+
function isSwitchNode(type, cfgRules) {
|
|
28
|
+
return type === cfgRules.switchNode || cfgRules.switchNodes?.has(type);
|
|
29
|
+
}
|
|
81
30
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
31
|
+
function isCaseNode(type, cfgRules) {
|
|
32
|
+
return (
|
|
33
|
+
type === cfgRules.caseNode || type === cfgRules.defaultNode || cfgRules.caseNodes?.has(type)
|
|
34
|
+
);
|
|
35
|
+
}
|
|
85
36
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
);
|
|
90
|
-
}
|
|
37
|
+
function isBlockNode(type, cfgRules) {
|
|
38
|
+
return type === 'statement_list' || type === cfgRules.blockNode || cfgRules.blockNodes?.has(type);
|
|
39
|
+
}
|
|
91
40
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
|
104
|
+
return stmts;
|
|
162
105
|
}
|
|
106
|
+
return [bodyNode];
|
|
107
|
+
}
|
|
163
108
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
109
|
+
function makeFuncState() {
|
|
110
|
+
const blocks = [];
|
|
111
|
+
const edges = [];
|
|
112
|
+
let nextIndex = 0;
|
|
167
113
|
|
|
168
|
-
function
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
|
178
|
-
|
|
120
|
+
function addEdge(source, target, kind) {
|
|
121
|
+
edges.push({ sourceIndex: source.index, targetIndex: target.index, kind });
|
|
122
|
+
}
|
|
179
123
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
124
|
+
const entry = makeBlock('entry');
|
|
125
|
+
const exit = makeBlock('exit');
|
|
126
|
+
const firstBody = makeBlock('body');
|
|
127
|
+
addEdge(entry, firstBody, 'fallthrough');
|
|
183
128
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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
|
-
|
|
190
|
-
if (isIfNode(type) || (cfgRules.unlessNode && type === cfgRules.unlessNode)) {
|
|
191
|
-
return processIf(effNode, currentBlock);
|
|
192
|
-
}
|
|
144
|
+
// ── Statement processors ────────────────────────────────────────────────
|
|
193
145
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
-
|
|
200
|
-
|
|
201
|
-
return processWhileLoop(effNode, currentBlock);
|
|
202
|
-
}
|
|
155
|
+
function processStatement(stmt, currentBlock, S, cfgRules) {
|
|
156
|
+
if (!stmt || !currentBlock) return currentBlock;
|
|
203
157
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
return processDoWhileLoop(effNode, currentBlock);
|
|
207
|
-
}
|
|
158
|
+
const effNode = effectiveNode(stmt, cfgRules);
|
|
159
|
+
const type = effNode.type;
|
|
208
160
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
}
|
|
245
|
+
function processContinue(node, currentBlock, S) {
|
|
246
|
+
const labelNode = node.childForFieldName('label');
|
|
247
|
+
const labelName = labelNode ? labelNode.text : null;
|
|
247
248
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
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
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
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
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
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
|
-
|
|
290
|
-
|
|
291
|
-
const labelName = labelNode ? labelNode.text : null;
|
|
297
|
+
return joinBlock;
|
|
298
|
+
}
|
|
292
299
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
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
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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
|
-
|
|
309
|
-
|
|
310
|
-
|
|
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
|
-
|
|
314
|
-
|
|
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
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
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
|
-
|
|
374
|
-
|
|
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
|
-
|
|
428
|
-
S.addEdge(lastCondBlock, joinBlock, 'branch_false');
|
|
383
|
+
foundElse = true;
|
|
429
384
|
}
|
|
430
385
|
}
|
|
431
386
|
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
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
|
-
|
|
449
|
-
|
|
450
|
-
|
|
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
|
-
|
|
453
|
-
|
|
454
|
-
|
|
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
|
-
|
|
457
|
-
|
|
458
|
-
|
|
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
|
-
|
|
462
|
-
|
|
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
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
registerLabelCtx(headerBlock, loopExitBlock);
|
|
455
|
+
const loopCtx = { headerBlock: condBlock, exitBlock: loopExitBlock };
|
|
456
|
+
S.loopStack.push(loopCtx);
|
|
457
|
+
registerLabelCtx(S, condBlock, loopExitBlock);
|
|
474
458
|
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
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
|
-
|
|
480
|
-
|
|
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
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
}
|
|
467
|
+
S.loopStack.pop();
|
|
468
|
+
return loopExitBlock;
|
|
469
|
+
}
|
|
487
470
|
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
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
|
-
|
|
493
|
-
const loopExitBlock = S.makeBlock('body');
|
|
498
|
+
// ── Switch / match ──────────────────────────────────────────────────────
|
|
494
499
|
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
registerLabelCtx(condBlock, loopExitBlock);
|
|
500
|
+
function processSwitch(switchStmt, currentBlock, S, cfgRules) {
|
|
501
|
+
currentBlock.endLine = switchStmt.startPosition.row + 1;
|
|
498
502
|
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
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
|
-
|
|
505
|
-
|
|
511
|
+
const joinBlock = S.makeBlock('body');
|
|
512
|
+
const switchCtx = { headerBlock: switchHeader, exitBlock: joinBlock };
|
|
513
|
+
S.loopStack.push(switchCtx);
|
|
506
514
|
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
}
|
|
515
|
+
const switchBody = switchStmt.childForFieldName('body');
|
|
516
|
+
const container = switchBody || switchStmt;
|
|
510
517
|
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
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
|
|
521
|
-
const
|
|
522
|
-
|
|
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
|
|
526
|
-
const
|
|
527
|
-
S.addEdge(
|
|
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
|
|
530
|
-
const
|
|
531
|
-
if (
|
|
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
|
-
|
|
534
|
-
S.
|
|
535
|
-
return loopExitBlock;
|
|
536
|
+
if (!hasDefault) {
|
|
537
|
+
S.addEdge(switchHeader, joinBlock, 'branch_false');
|
|
536
538
|
}
|
|
537
539
|
|
|
538
|
-
|
|
540
|
+
S.loopStack.pop();
|
|
541
|
+
return joinBlock;
|
|
542
|
+
}
|
|
539
543
|
|
|
540
|
-
|
|
541
|
-
|
|
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
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
)
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
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
|
-
|
|
600
|
-
|
|
601
|
-
|
|
569
|
+
// ── Try / catch / finally ───────────────────────────────────────────────
|
|
570
|
+
|
|
571
|
+
function processTryCatch(tryStmt, currentBlock, S, cfgRules) {
|
|
572
|
+
currentBlock.endLine = tryStmt.startPosition.row + 1;
|
|
602
573
|
|
|
603
|
-
|
|
604
|
-
|
|
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
|
-
|
|
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
|
-
|
|
610
|
-
|
|
598
|
+
// Find catch and finally handlers
|
|
599
|
+
const { catchHandler, finallyHandler } = findTryHandlers(tryStmt, cfgRules);
|
|
611
600
|
|
|
612
|
-
|
|
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
|
-
|
|
615
|
-
|
|
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
|
-
|
|
633
|
-
|
|
634
|
-
|
|
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
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
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
|
-
|
|
646
|
-
|
|
647
|
-
|
|
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
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
779
|
-
//
|
|
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
|
|
771
|
+
// No-op
|
|
786
772
|
},
|
|
787
773
|
|
|
788
774
|
finish() {
|