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