@optave/codegraph 3.1.0 → 3.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -5
- package/grammars/tree-sitter-go.wasm +0 -0
- package/package.json +8 -9
- package/src/ast-analysis/engine.js +365 -0
- package/src/ast-analysis/metrics.js +118 -0
- package/src/ast-analysis/rules/csharp.js +201 -0
- package/src/ast-analysis/rules/go.js +182 -0
- package/src/ast-analysis/rules/index.js +82 -0
- package/src/ast-analysis/rules/java.js +175 -0
- package/src/ast-analysis/rules/javascript.js +246 -0
- package/src/ast-analysis/rules/php.js +219 -0
- package/src/ast-analysis/rules/python.js +196 -0
- package/src/ast-analysis/rules/ruby.js +204 -0
- package/src/ast-analysis/rules/rust.js +173 -0
- package/src/ast-analysis/shared.js +223 -0
- package/src/ast-analysis/visitor-utils.js +176 -0
- package/src/ast-analysis/visitor.js +162 -0
- package/src/ast-analysis/visitors/ast-store-visitor.js +150 -0
- package/src/ast-analysis/visitors/cfg-visitor.js +792 -0
- package/src/ast-analysis/visitors/complexity-visitor.js +243 -0
- package/src/ast-analysis/visitors/dataflow-visitor.js +358 -0
- package/src/ast.js +26 -166
- package/src/audit.js +2 -88
- package/src/batch.js +0 -25
- package/src/boundaries.js +1 -1
- package/src/branch-compare.js +82 -172
- package/src/builder.js +48 -184
- package/src/cfg.js +148 -1174
- package/src/check.js +1 -84
- package/src/cli.js +118 -197
- package/src/cochange.js +1 -39
- package/src/commands/audit.js +88 -0
- package/src/commands/batch.js +26 -0
- package/src/commands/branch-compare.js +97 -0
- package/src/commands/cfg.js +55 -0
- package/src/commands/check.js +82 -0
- package/src/commands/cochange.js +37 -0
- package/src/commands/communities.js +69 -0
- package/src/commands/complexity.js +77 -0
- package/src/commands/dataflow.js +110 -0
- package/src/commands/flow.js +70 -0
- package/src/commands/manifesto.js +77 -0
- package/src/commands/owners.js +52 -0
- package/src/commands/query.js +21 -0
- package/src/commands/sequence.js +33 -0
- package/src/commands/structure.js +64 -0
- package/src/commands/triage.js +49 -0
- package/src/communities.js +22 -96
- package/src/complexity.js +234 -1591
- package/src/cycles.js +1 -1
- package/src/dataflow.js +274 -1352
- package/src/db/connection.js +88 -0
- package/src/db/migrations.js +312 -0
- package/src/db/query-builder.js +280 -0
- package/src/db/repository/build-stmts.js +104 -0
- package/src/db/repository/cfg.js +83 -0
- package/src/db/repository/cochange.js +41 -0
- package/src/db/repository/complexity.js +15 -0
- package/src/db/repository/dataflow.js +12 -0
- package/src/db/repository/edges.js +259 -0
- package/src/db/repository/embeddings.js +40 -0
- package/src/db/repository/graph-read.js +39 -0
- package/src/db/repository/index.js +42 -0
- package/src/db/repository/nodes.js +236 -0
- package/src/db.js +58 -399
- package/src/embedder.js +158 -174
- package/src/export.js +1 -1
- package/src/extractors/javascript.js +130 -5
- package/src/flow.js +153 -222
- package/src/index.js +53 -16
- package/src/infrastructure/result-formatter.js +21 -0
- package/src/infrastructure/test-filter.js +7 -0
- package/src/kinds.js +50 -0
- package/src/manifesto.js +1 -82
- package/src/mcp.js +37 -20
- package/src/owners.js +127 -182
- package/src/queries-cli.js +866 -0
- package/src/queries.js +1271 -2416
- package/src/sequence.js +179 -223
- package/src/structure.js +211 -269
- package/src/triage.js +117 -212
- package/src/viewer.js +1 -1
- package/src/watcher.js +7 -4
package/src/cfg.js
CHANGED
|
@@ -7,1030 +7,66 @@
|
|
|
7
7
|
|
|
8
8
|
import fs from 'node:fs';
|
|
9
9
|
import path from 'node:path';
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
10
|
+
import { CFG_RULES } from './ast-analysis/rules/index.js';
|
|
11
|
+
import {
|
|
12
|
+
makeCfgRules as _makeCfgRules,
|
|
13
|
+
buildExtensionSet,
|
|
14
|
+
buildExtToLangMap,
|
|
15
|
+
} from './ast-analysis/shared.js';
|
|
16
|
+
import { walkWithVisitors } from './ast-analysis/visitor.js';
|
|
17
|
+
import { createCfgVisitor } from './ast-analysis/visitors/cfg-visitor.js';
|
|
18
|
+
import {
|
|
19
|
+
deleteCfgForNode,
|
|
20
|
+
getCfgBlocks,
|
|
21
|
+
getCfgEdges,
|
|
22
|
+
getFunctionNodeId,
|
|
23
|
+
hasCfgTables,
|
|
24
|
+
openReadonlyOrFail,
|
|
25
|
+
} from './db.js';
|
|
26
|
+
import { isTestFile } from './infrastructure/test-filter.js';
|
|
12
27
|
import { info } from './logger.js';
|
|
13
|
-
import { paginateResult
|
|
14
|
-
import { LANGUAGE_REGISTRY } from './parser.js';
|
|
15
|
-
import { isTestFile } from './queries.js';
|
|
28
|
+
import { paginateResult } from './paginate.js';
|
|
16
29
|
|
|
17
|
-
//
|
|
30
|
+
// Re-export for backward compatibility
|
|
31
|
+
export { CFG_RULES };
|
|
32
|
+
export { _makeCfgRules as makeCfgRules };
|
|
18
33
|
|
|
19
|
-
const
|
|
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
|
-
}
|
|
34
|
+
const CFG_EXTENSIONS = buildExtensionSet(CFG_RULES);
|
|
269
35
|
|
|
270
36
|
// ─── Core Algorithm: AST → CFG ──────────────────────────────────────────
|
|
271
37
|
|
|
272
38
|
/**
|
|
273
39
|
* Build a control flow graph for a single function AST node.
|
|
274
40
|
*
|
|
41
|
+
* Thin wrapper around the CFG visitor — runs walkWithVisitors on the function
|
|
42
|
+
* node and returns the first result. All CFG construction logic lives in
|
|
43
|
+
* `ast-analysis/visitors/cfg-visitor.js`.
|
|
44
|
+
*
|
|
275
45
|
* @param {object} functionNode - tree-sitter function AST node
|
|
276
|
-
* @param {string} langId - language identifier
|
|
277
|
-
* @returns {{ blocks: object[], edges: object[] }} - CFG blocks and
|
|
46
|
+
* @param {string} langId - language identifier
|
|
47
|
+
* @returns {{ blocks: object[], edges: object[], cyclomatic: number }} - CFG blocks, edges, and derived cyclomatic
|
|
278
48
|
*/
|
|
279
49
|
export function buildFunctionCFG(functionNode, langId) {
|
|
280
50
|
const rules = CFG_RULES.get(langId);
|
|
281
|
-
if (!rules) return { blocks: [], edges: [] };
|
|
282
|
-
|
|
283
|
-
const
|
|
284
|
-
const
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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 };
|
|
51
|
+
if (!rules) return { blocks: [], edges: [], cyclomatic: 0 };
|
|
52
|
+
|
|
53
|
+
const visitor = createCfgVisitor(rules);
|
|
54
|
+
const walkerOpts = {
|
|
55
|
+
functionNodeTypes: new Set(rules.functionNodes),
|
|
56
|
+
nestingNodeTypes: new Set(),
|
|
57
|
+
getFunctionName: (node) => {
|
|
58
|
+
const nameNode = node.childForFieldName('name');
|
|
59
|
+
return nameNode ? nameNode.text : null;
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const results = walkWithVisitors(functionNode, [visitor], langId, walkerOpts);
|
|
64
|
+
const cfgResults = results.cfg || [];
|
|
65
|
+
if (cfgResults.length === 0) return { blocks: [], edges: [], cyclomatic: 0 };
|
|
66
|
+
|
|
67
|
+
const r = cfgResults.find((result) => result.funcNode === functionNode);
|
|
68
|
+
if (!r) return { blocks: [], edges: [], cyclomatic: 0 };
|
|
69
|
+
return { blocks: r.blocks, edges: r.edges, cyclomatic: r.cyclomatic };
|
|
1034
70
|
}
|
|
1035
71
|
|
|
1036
72
|
// ─── Build-Time: Compute CFG for Changed Files ─────────────────────────
|
|
@@ -1050,12 +86,7 @@ export async function buildCFGData(db, fileSymbols, rootDir, _engineOpts) {
|
|
|
1050
86
|
|
|
1051
87
|
// Always build ext→langId map so native-only builds (where _langId is unset)
|
|
1052
88
|
// can still derive the language from the file extension.
|
|
1053
|
-
const extToLang =
|
|
1054
|
-
for (const entry of LANGUAGE_REGISTRY) {
|
|
1055
|
-
for (const ext of entry.extensions) {
|
|
1056
|
-
extToLang.set(ext, entry.id);
|
|
1057
|
-
}
|
|
1058
|
-
}
|
|
89
|
+
const extToLang = buildExtToLangMap();
|
|
1059
90
|
|
|
1060
91
|
for (const [relPath, symbols] of fileSymbols) {
|
|
1061
92
|
if (!symbols._tree) {
|
|
@@ -1084,7 +115,7 @@ export async function buildCFGData(db, fileSymbols, rootDir, _engineOpts) {
|
|
|
1084
115
|
getParserFn = mod.getParser;
|
|
1085
116
|
}
|
|
1086
117
|
|
|
1087
|
-
|
|
118
|
+
// findFunctionNode imported from ./ast-analysis/shared.js at module level
|
|
1088
119
|
|
|
1089
120
|
const insertBlock = db.prepare(
|
|
1090
121
|
`INSERT INTO cfg_blocks (function_node_id, block_index, block_type, start_line, end_line, label)
|
|
@@ -1094,12 +125,6 @@ export async function buildCFGData(db, fileSymbols, rootDir, _engineOpts) {
|
|
|
1094
125
|
`INSERT INTO cfg_edges (function_node_id, source_block_id, target_block_id, kind)
|
|
1095
126
|
VALUES (?, ?, ?, ?)`,
|
|
1096
127
|
);
|
|
1097
|
-
const deleteBlocks = db.prepare('DELETE FROM cfg_blocks WHERE function_node_id = ?');
|
|
1098
|
-
const deleteEdges = db.prepare('DELETE FROM cfg_edges WHERE function_node_id = ?');
|
|
1099
|
-
const getNodeId = db.prepare(
|
|
1100
|
-
"SELECT id FROM nodes WHERE name = ? AND kind IN ('function','method') AND file = ? AND line = ?",
|
|
1101
|
-
);
|
|
1102
|
-
|
|
1103
128
|
let analyzed = 0;
|
|
1104
129
|
|
|
1105
130
|
const tx = db.transaction(() => {
|
|
@@ -1119,7 +144,7 @@ export async function buildCFGData(db, fileSymbols, rootDir, _engineOpts) {
|
|
|
1119
144
|
if (!tree && !allNative) {
|
|
1120
145
|
if (!getParserFn) continue;
|
|
1121
146
|
langId = extToLang.get(ext);
|
|
1122
|
-
if (!langId || !
|
|
147
|
+
if (!langId || !CFG_RULES.has(langId)) continue;
|
|
1123
148
|
|
|
1124
149
|
const absPath = path.join(rootDir, relPath);
|
|
1125
150
|
let code;
|
|
@@ -1147,39 +172,74 @@ export async function buildCFGData(db, fileSymbols, rootDir, _engineOpts) {
|
|
|
1147
172
|
const cfgRules = CFG_RULES.get(langId);
|
|
1148
173
|
if (!cfgRules) continue;
|
|
1149
174
|
|
|
1150
|
-
|
|
1151
|
-
//
|
|
175
|
+
// WASM fallback: run file-level visitor walk to compute CFG for all functions
|
|
176
|
+
// that don't already have pre-computed data (from native engine or unified walk)
|
|
177
|
+
let visitorCfgByLine = null;
|
|
178
|
+
const needsVisitor =
|
|
179
|
+
tree &&
|
|
180
|
+
symbols.definitions.some(
|
|
181
|
+
(d) =>
|
|
182
|
+
(d.kind === 'function' || d.kind === 'method') &&
|
|
183
|
+
d.line &&
|
|
184
|
+
d.cfg !== null &&
|
|
185
|
+
!d.cfg?.blocks?.length,
|
|
186
|
+
);
|
|
187
|
+
if (needsVisitor) {
|
|
188
|
+
const visitor = createCfgVisitor(cfgRules);
|
|
189
|
+
const walkerOpts = {
|
|
190
|
+
functionNodeTypes: new Set(cfgRules.functionNodes),
|
|
191
|
+
nestingNodeTypes: new Set(),
|
|
192
|
+
getFunctionName: (node) => {
|
|
193
|
+
const nameNode = node.childForFieldName('name');
|
|
194
|
+
return nameNode ? nameNode.text : null;
|
|
195
|
+
},
|
|
196
|
+
};
|
|
197
|
+
const walkResults = walkWithVisitors(tree.rootNode, [visitor], langId, walkerOpts);
|
|
198
|
+
const cfgResults = walkResults.cfg || [];
|
|
199
|
+
visitorCfgByLine = new Map();
|
|
200
|
+
for (const r of cfgResults) {
|
|
201
|
+
if (r.funcNode) {
|
|
202
|
+
const line = r.funcNode.startPosition.row + 1;
|
|
203
|
+
if (!visitorCfgByLine.has(line)) visitorCfgByLine.set(line, []);
|
|
204
|
+
visitorCfgByLine.get(line).push(r);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
1152
208
|
|
|
1153
209
|
for (const def of symbols.definitions) {
|
|
1154
210
|
if (def.kind !== 'function' && def.kind !== 'method') continue;
|
|
1155
211
|
if (!def.line) continue;
|
|
1156
212
|
|
|
1157
|
-
const
|
|
1158
|
-
if (!
|
|
213
|
+
const nodeId = getFunctionNodeId(db, def.name, relPath, def.line);
|
|
214
|
+
if (!nodeId) continue;
|
|
1159
215
|
|
|
1160
|
-
//
|
|
216
|
+
// Use pre-computed CFG (native engine or unified walk), then visitor fallback
|
|
1161
217
|
let cfg = null;
|
|
1162
218
|
if (def.cfg?.blocks?.length) {
|
|
1163
219
|
cfg = def.cfg;
|
|
1164
|
-
} else {
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
220
|
+
} else if (visitorCfgByLine) {
|
|
221
|
+
const candidates = visitorCfgByLine.get(def.line);
|
|
222
|
+
const r = !candidates
|
|
223
|
+
? undefined
|
|
224
|
+
: candidates.length === 1
|
|
225
|
+
? candidates[0]
|
|
226
|
+
: (candidates.find((c) => {
|
|
227
|
+
const n = c.funcNode.childForFieldName('name');
|
|
228
|
+
return n && n.text === def.name;
|
|
229
|
+
}) ?? candidates[0]);
|
|
230
|
+
if (r) cfg = { blocks: r.blocks, edges: r.edges };
|
|
1170
231
|
}
|
|
1171
232
|
|
|
1172
233
|
if (!cfg || cfg.blocks.length === 0) continue;
|
|
1173
234
|
|
|
1174
235
|
// Clear old CFG data for this function
|
|
1175
|
-
|
|
1176
|
-
deleteBlocks.run(row.id);
|
|
236
|
+
deleteCfgForNode(db, nodeId);
|
|
1177
237
|
|
|
1178
238
|
// Insert blocks and build index→dbId mapping
|
|
1179
239
|
const blockDbIds = new Map();
|
|
1180
240
|
for (const block of cfg.blocks) {
|
|
1181
241
|
const result = insertBlock.run(
|
|
1182
|
-
|
|
242
|
+
nodeId,
|
|
1183
243
|
block.index,
|
|
1184
244
|
block.type,
|
|
1185
245
|
block.startLine,
|
|
@@ -1194,7 +254,7 @@ export async function buildCFGData(db, fileSymbols, rootDir, _engineOpts) {
|
|
|
1194
254
|
const sourceDbId = blockDbIds.get(edge.sourceIndex);
|
|
1195
255
|
const targetDbId = blockDbIds.get(edge.targetIndex);
|
|
1196
256
|
if (sourceDbId && targetDbId) {
|
|
1197
|
-
insertEdge.run(
|
|
257
|
+
insertEdge.run(nodeId, sourceDbId, targetDbId, edge.kind);
|
|
1198
258
|
}
|
|
1199
259
|
}
|
|
1200
260
|
|
|
@@ -1214,15 +274,6 @@ export async function buildCFGData(db, fileSymbols, rootDir, _engineOpts) {
|
|
|
1214
274
|
|
|
1215
275
|
// ─── Query-Time Functions ───────────────────────────────────────────────
|
|
1216
276
|
|
|
1217
|
-
function hasCfgTables(db) {
|
|
1218
|
-
try {
|
|
1219
|
-
db.prepare('SELECT 1 FROM cfg_blocks LIMIT 0').get();
|
|
1220
|
-
return true;
|
|
1221
|
-
} catch {
|
|
1222
|
-
return false;
|
|
1223
|
-
}
|
|
1224
|
-
}
|
|
1225
|
-
|
|
1226
277
|
function findNodes(db, name, opts = {}) {
|
|
1227
278
|
const kinds = opts.kind ? [opts.kind] : ['function', 'method'];
|
|
1228
279
|
const placeholders = kinds.map(() => '?').join(', ');
|
|
@@ -1255,72 +306,57 @@ function findNodes(db, name, opts = {}) {
|
|
|
1255
306
|
*/
|
|
1256
307
|
export function cfgData(name, customDbPath, opts = {}) {
|
|
1257
308
|
const db = openReadonlyOrFail(customDbPath);
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
db
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
309
|
+
try {
|
|
310
|
+
const noTests = opts.noTests || false;
|
|
311
|
+
|
|
312
|
+
if (!hasCfgTables(db)) {
|
|
313
|
+
return {
|
|
314
|
+
name,
|
|
315
|
+
results: [],
|
|
316
|
+
warning:
|
|
317
|
+
'No CFG data found. Rebuild with `codegraph build` (CFG is now included by default).',
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const nodes = findNodes(db, name, { noTests, file: opts.file, kind: opts.kind });
|
|
322
|
+
if (nodes.length === 0) {
|
|
323
|
+
return { name, results: [] };
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const results = nodes.map((node) => {
|
|
327
|
+
const cfgBlocks = getCfgBlocks(db, node.id);
|
|
328
|
+
const cfgEdges = getCfgEdges(db, node.id);
|
|
329
|
+
|
|
330
|
+
return {
|
|
331
|
+
name: node.name,
|
|
332
|
+
kind: node.kind,
|
|
333
|
+
file: node.file,
|
|
334
|
+
line: node.line,
|
|
335
|
+
blocks: cfgBlocks.map((b) => ({
|
|
336
|
+
index: b.block_index,
|
|
337
|
+
type: b.block_type,
|
|
338
|
+
startLine: b.start_line,
|
|
339
|
+
endLine: b.end_line,
|
|
340
|
+
label: b.label,
|
|
341
|
+
})),
|
|
342
|
+
edges: cfgEdges.map((e) => ({
|
|
343
|
+
source: e.source_index,
|
|
344
|
+
sourceType: e.source_type,
|
|
345
|
+
target: e.target_index,
|
|
346
|
+
targetType: e.target_type,
|
|
347
|
+
kind: e.kind,
|
|
348
|
+
})),
|
|
349
|
+
summary: {
|
|
350
|
+
blockCount: cfgBlocks.length,
|
|
351
|
+
edgeCount: cfgEdges.length,
|
|
352
|
+
},
|
|
353
|
+
};
|
|
354
|
+
});
|
|
1269
355
|
|
|
1270
|
-
|
|
1271
|
-
|
|
356
|
+
return paginateResult({ name, results }, 'results', opts);
|
|
357
|
+
} finally {
|
|
1272
358
|
db.close();
|
|
1273
|
-
return { name, results: [] };
|
|
1274
359
|
}
|
|
1275
|
-
|
|
1276
|
-
const blockStmt = db.prepare(
|
|
1277
|
-
`SELECT id, block_index, block_type, start_line, end_line, label
|
|
1278
|
-
FROM cfg_blocks WHERE function_node_id = ?
|
|
1279
|
-
ORDER BY block_index`,
|
|
1280
|
-
);
|
|
1281
|
-
const edgeStmt = db.prepare(
|
|
1282
|
-
`SELECT e.kind,
|
|
1283
|
-
sb.block_index AS source_index, sb.block_type AS source_type,
|
|
1284
|
-
tb.block_index AS target_index, tb.block_type AS target_type
|
|
1285
|
-
FROM cfg_edges e
|
|
1286
|
-
JOIN cfg_blocks sb ON e.source_block_id = sb.id
|
|
1287
|
-
JOIN cfg_blocks tb ON e.target_block_id = tb.id
|
|
1288
|
-
WHERE e.function_node_id = ?
|
|
1289
|
-
ORDER BY sb.block_index, tb.block_index`,
|
|
1290
|
-
);
|
|
1291
|
-
|
|
1292
|
-
const results = nodes.map((node) => {
|
|
1293
|
-
const cfgBlocks = blockStmt.all(node.id);
|
|
1294
|
-
const cfgEdges = edgeStmt.all(node.id);
|
|
1295
|
-
|
|
1296
|
-
return {
|
|
1297
|
-
name: node.name,
|
|
1298
|
-
kind: node.kind,
|
|
1299
|
-
file: node.file,
|
|
1300
|
-
line: node.line,
|
|
1301
|
-
blocks: cfgBlocks.map((b) => ({
|
|
1302
|
-
index: b.block_index,
|
|
1303
|
-
type: b.block_type,
|
|
1304
|
-
startLine: b.start_line,
|
|
1305
|
-
endLine: b.end_line,
|
|
1306
|
-
label: b.label,
|
|
1307
|
-
})),
|
|
1308
|
-
edges: cfgEdges.map((e) => ({
|
|
1309
|
-
source: e.source_index,
|
|
1310
|
-
sourceType: e.source_type,
|
|
1311
|
-
target: e.target_index,
|
|
1312
|
-
targetType: e.target_type,
|
|
1313
|
-
kind: e.kind,
|
|
1314
|
-
})),
|
|
1315
|
-
summary: {
|
|
1316
|
-
blockCount: cfgBlocks.length,
|
|
1317
|
-
edgeCount: cfgEdges.length,
|
|
1318
|
-
},
|
|
1319
|
-
};
|
|
1320
|
-
});
|
|
1321
|
-
|
|
1322
|
-
db.close();
|
|
1323
|
-
return paginateResult({ name, results }, 'results', opts);
|
|
1324
360
|
}
|
|
1325
361
|
|
|
1326
362
|
// ─── Export Formats ─────────────────────────────────────────────────────
|
|
@@ -1409,65 +445,3 @@ function edgeStyle(kind) {
|
|
|
1409
445
|
if (kind === 'continue') return ', color=blue, style=dashed';
|
|
1410
446
|
return '';
|
|
1411
447
|
}
|
|
1412
|
-
|
|
1413
|
-
// ─── CLI Printer ────────────────────────────────────────────────────────
|
|
1414
|
-
|
|
1415
|
-
/**
|
|
1416
|
-
* CLI display for cfg command.
|
|
1417
|
-
*/
|
|
1418
|
-
export function cfg(name, customDbPath, opts = {}) {
|
|
1419
|
-
const data = cfgData(name, customDbPath, opts);
|
|
1420
|
-
|
|
1421
|
-
if (opts.json) {
|
|
1422
|
-
console.log(JSON.stringify(data, null, 2));
|
|
1423
|
-
return;
|
|
1424
|
-
}
|
|
1425
|
-
if (opts.ndjson) {
|
|
1426
|
-
printNdjson(data.results);
|
|
1427
|
-
return;
|
|
1428
|
-
}
|
|
1429
|
-
|
|
1430
|
-
if (data.warning) {
|
|
1431
|
-
console.log(`\u26A0 ${data.warning}`);
|
|
1432
|
-
return;
|
|
1433
|
-
}
|
|
1434
|
-
if (data.results.length === 0) {
|
|
1435
|
-
console.log(`No symbols matching "${name}".`);
|
|
1436
|
-
return;
|
|
1437
|
-
}
|
|
1438
|
-
|
|
1439
|
-
const format = opts.format || 'text';
|
|
1440
|
-
if (format === 'dot') {
|
|
1441
|
-
console.log(cfgToDOT(data));
|
|
1442
|
-
return;
|
|
1443
|
-
}
|
|
1444
|
-
if (format === 'mermaid') {
|
|
1445
|
-
console.log(cfgToMermaid(data));
|
|
1446
|
-
return;
|
|
1447
|
-
}
|
|
1448
|
-
|
|
1449
|
-
// Text format
|
|
1450
|
-
for (const r of data.results) {
|
|
1451
|
-
console.log(`\n${r.kind} ${r.name} (${r.file}:${r.line})`);
|
|
1452
|
-
console.log('\u2500'.repeat(60));
|
|
1453
|
-
console.log(` Blocks: ${r.summary.blockCount} Edges: ${r.summary.edgeCount}`);
|
|
1454
|
-
|
|
1455
|
-
if (r.blocks.length > 0) {
|
|
1456
|
-
console.log('\n Blocks:');
|
|
1457
|
-
for (const b of r.blocks) {
|
|
1458
|
-
const loc = b.startLine
|
|
1459
|
-
? ` L${b.startLine}${b.endLine && b.endLine !== b.startLine ? `-${b.endLine}` : ''}`
|
|
1460
|
-
: '';
|
|
1461
|
-
const label = b.label ? ` (${b.label})` : '';
|
|
1462
|
-
console.log(` [${b.index}] ${b.type}${label}${loc}`);
|
|
1463
|
-
}
|
|
1464
|
-
}
|
|
1465
|
-
|
|
1466
|
-
if (r.edges.length > 0) {
|
|
1467
|
-
console.log('\n Edges:');
|
|
1468
|
-
for (const e of r.edges) {
|
|
1469
|
-
console.log(` B${e.source} \u2192 B${e.target} [${e.kind}]`);
|
|
1470
|
-
}
|
|
1471
|
-
}
|
|
1472
|
-
}
|
|
1473
|
-
}
|