@optave/codegraph 3.1.1 → 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/package.json +7 -7
- package/src/ast-analysis/engine.js +365 -0
- package/src/ast-analysis/metrics.js +118 -0
- package/src/ast-analysis/visitor-utils.js +176 -0
- package/src/ast-analysis/visitor.js +162 -0
- package/src/ast-analysis/visitors/ast-store-visitor.js +150 -0
- package/src/ast-analysis/visitors/cfg-visitor.js +792 -0
- package/src/ast-analysis/visitors/complexity-visitor.js +243 -0
- package/src/ast-analysis/visitors/dataflow-visitor.js +358 -0
- package/src/ast.js +13 -140
- package/src/audit.js +2 -87
- package/src/batch.js +0 -25
- package/src/boundaries.js +1 -1
- package/src/branch-compare.js +1 -96
- package/src/builder.js +48 -179
- package/src/cfg.js +89 -883
- package/src/check.js +1 -84
- package/src/cli.js +20 -19
- package/src/cochange.js +1 -39
- package/src/commands/audit.js +88 -0
- package/src/commands/batch.js +26 -0
- package/src/commands/branch-compare.js +97 -0
- package/src/commands/cfg.js +55 -0
- package/src/commands/check.js +82 -0
- package/src/commands/cochange.js +37 -0
- package/src/commands/communities.js +69 -0
- package/src/commands/complexity.js +77 -0
- package/src/commands/dataflow.js +110 -0
- package/src/commands/flow.js +70 -0
- package/src/commands/manifesto.js +77 -0
- package/src/commands/owners.js +52 -0
- package/src/commands/query.js +21 -0
- package/src/commands/sequence.js +33 -0
- package/src/commands/structure.js +64 -0
- package/src/commands/triage.js +49 -0
- package/src/communities.js +12 -83
- package/src/complexity.js +42 -356
- package/src/cycles.js +1 -1
- package/src/dataflow.js +12 -665
- package/src/db/repository/build-stmts.js +104 -0
- package/src/db/repository/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 +40 -1
- package/src/embedder.js +14 -34
- package/src/export.js +1 -1
- package/src/extractors/javascript.js +130 -5
- package/src/flow.js +2 -70
- package/src/index.js +23 -19
- package/src/{result-formatter.js → infrastructure/result-formatter.js} +1 -1
- package/src/kinds.js +1 -0
- package/src/manifesto.js +0 -76
- package/src/owners.js +1 -56
- package/src/queries-cli.js +1 -1
- package/src/queries.js +79 -280
- package/src/sequence.js +5 -44
- package/src/structure.js +16 -75
- package/src/triage.js +1 -54
- package/src/viewer.js +1 -1
- package/src/watcher.js +7 -4
- package/src/db/repository.js +0 -134
- /package/src/{test-filter.js → infrastructure/test-filter.js} +0 -0
package/src/cfg.js
CHANGED
|
@@ -7,20 +7,26 @@
|
|
|
7
7
|
|
|
8
8
|
import fs from 'node:fs';
|
|
9
9
|
import path from 'node:path';
|
|
10
|
-
import { CFG_RULES
|
|
10
|
+
import { CFG_RULES } from './ast-analysis/rules/index.js';
|
|
11
11
|
import {
|
|
12
12
|
makeCfgRules as _makeCfgRules,
|
|
13
13
|
buildExtensionSet,
|
|
14
14
|
buildExtToLangMap,
|
|
15
|
-
findFunctionNode,
|
|
16
15
|
} from './ast-analysis/shared.js';
|
|
17
|
-
import {
|
|
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';
|
|
18
27
|
import { info } from './logger.js';
|
|
19
28
|
import { paginateResult } from './paginate.js';
|
|
20
29
|
|
|
21
|
-
import { outputResult } from './result-formatter.js';
|
|
22
|
-
import { isTestFile } from './test-filter.js';
|
|
23
|
-
|
|
24
30
|
// Re-export for backward compatibility
|
|
25
31
|
export { CFG_RULES };
|
|
26
32
|
export { _makeCfgRules as makeCfgRules };
|
|
@@ -32,784 +38,35 @@ const CFG_EXTENSIONS = buildExtensionSet(CFG_RULES);
|
|
|
32
38
|
/**
|
|
33
39
|
* Build a control flow graph for a single function AST node.
|
|
34
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
|
+
*
|
|
35
45
|
* @param {object} functionNode - tree-sitter function AST node
|
|
36
|
-
* @param {string} langId - language identifier
|
|
37
|
-
* @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
|
|
38
48
|
*/
|
|
39
49
|
export function buildFunctionCFG(functionNode, langId) {
|
|
40
50
|
const rules = CFG_RULES.get(langId);
|
|
41
|
-
if (!rules) return { blocks: [], edges: [] };
|
|
42
|
-
|
|
43
|
-
const
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
edges.push({
|
|
61
|
-
sourceIndex: source.index,
|
|
62
|
-
targetIndex: target.index,
|
|
63
|
-
kind,
|
|
64
|
-
});
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const entryBlock = makeBlock('entry');
|
|
68
|
-
const exitBlock = makeBlock('exit');
|
|
69
|
-
|
|
70
|
-
// Loop context stack for break/continue resolution
|
|
71
|
-
const loopStack = [];
|
|
72
|
-
|
|
73
|
-
// Label map for labeled break/continue
|
|
74
|
-
const labelMap = new Map();
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Get the body node of a function (handles arrow functions with expression bodies).
|
|
78
|
-
*/
|
|
79
|
-
function getFunctionBody(fnNode) {
|
|
80
|
-
const body = fnNode.childForFieldName('body');
|
|
81
|
-
if (!body) return null;
|
|
82
|
-
return body;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* Get statement children from a block or statement list.
|
|
87
|
-
*/
|
|
88
|
-
function getStatements(node) {
|
|
89
|
-
if (!node) return [];
|
|
90
|
-
// Block-like nodes (including statement_list wrappers from tree-sitter-go 0.25+)
|
|
91
|
-
if (
|
|
92
|
-
node.type === 'statement_list' ||
|
|
93
|
-
node.type === rules.blockNode ||
|
|
94
|
-
rules.blockNodes?.has(node.type)
|
|
95
|
-
) {
|
|
96
|
-
const stmts = [];
|
|
97
|
-
for (let i = 0; i < node.namedChildCount; i++) {
|
|
98
|
-
const child = node.namedChild(i);
|
|
99
|
-
if (child.type === 'statement_list') {
|
|
100
|
-
// Unwrap nested statement_list (block → statement_list → stmts)
|
|
101
|
-
for (let j = 0; j < child.namedChildCount; j++) {
|
|
102
|
-
stmts.push(child.namedChild(j));
|
|
103
|
-
}
|
|
104
|
-
} else {
|
|
105
|
-
stmts.push(child);
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
return stmts;
|
|
109
|
-
}
|
|
110
|
-
// Single statement (e.g., arrow fn with expression body, or unbraced if body)
|
|
111
|
-
return [node];
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Process a list of statements, creating blocks and edges.
|
|
116
|
-
* Returns the last "current" block after processing, or null if all paths terminated.
|
|
117
|
-
*/
|
|
118
|
-
function processStatements(stmts, currentBlock) {
|
|
119
|
-
let cur = currentBlock;
|
|
120
|
-
|
|
121
|
-
for (const stmt of stmts) {
|
|
122
|
-
if (!cur) {
|
|
123
|
-
// Dead code after return/break/continue/throw — skip remaining
|
|
124
|
-
break;
|
|
125
|
-
}
|
|
126
|
-
cur = processStatement(stmt, cur);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
return cur;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
/**
|
|
133
|
-
* Process a single statement, returns the new current block or null if terminated.
|
|
134
|
-
*/
|
|
135
|
-
function processStatement(stmt, currentBlock) {
|
|
136
|
-
if (!stmt || !currentBlock) return currentBlock;
|
|
137
|
-
|
|
138
|
-
// Unwrap expression_statement (Rust uses expressions for control flow)
|
|
139
|
-
if (stmt.type === 'expression_statement' && stmt.namedChildCount === 1) {
|
|
140
|
-
const inner = stmt.namedChild(0);
|
|
141
|
-
const t = inner.type;
|
|
142
|
-
if (
|
|
143
|
-
t === rules.ifNode ||
|
|
144
|
-
rules.ifNodes?.has(t) ||
|
|
145
|
-
rules.forNodes?.has(t) ||
|
|
146
|
-
t === rules.whileNode ||
|
|
147
|
-
rules.whileNodes?.has(t) ||
|
|
148
|
-
t === rules.doNode ||
|
|
149
|
-
t === rules.infiniteLoopNode ||
|
|
150
|
-
t === rules.switchNode ||
|
|
151
|
-
rules.switchNodes?.has(t) ||
|
|
152
|
-
t === rules.returnNode ||
|
|
153
|
-
t === rules.throwNode ||
|
|
154
|
-
t === rules.breakNode ||
|
|
155
|
-
t === rules.continueNode ||
|
|
156
|
-
t === rules.unlessNode ||
|
|
157
|
-
t === rules.untilNode
|
|
158
|
-
) {
|
|
159
|
-
return processStatement(inner, currentBlock);
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
const type = stmt.type;
|
|
164
|
-
|
|
165
|
-
// Labeled statement: register label then process inner statement
|
|
166
|
-
if (type === rules.labeledNode) {
|
|
167
|
-
const labelNode = stmt.childForFieldName('label');
|
|
168
|
-
const labelName = labelNode ? labelNode.text : null;
|
|
169
|
-
const body = stmt.childForFieldName('body');
|
|
170
|
-
if (body && labelName) {
|
|
171
|
-
// Will be filled when we encounter the loop
|
|
172
|
-
const labelCtx = { headerBlock: null, exitBlock: null };
|
|
173
|
-
labelMap.set(labelName, labelCtx);
|
|
174
|
-
const result = processStatement(body, currentBlock);
|
|
175
|
-
labelMap.delete(labelName);
|
|
176
|
-
return result;
|
|
177
|
-
}
|
|
178
|
-
return currentBlock;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
// If statement (including language variants like if_let_expression)
|
|
182
|
-
if (type === rules.ifNode || rules.ifNodes?.has(type)) {
|
|
183
|
-
return processIf(stmt, currentBlock);
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
// Unless (Ruby) — same CFG shape as if
|
|
187
|
-
if (rules.unlessNode && type === rules.unlessNode) {
|
|
188
|
-
return processIf(stmt, currentBlock);
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
// For / for-in loops
|
|
192
|
-
if (rules.forNodes.has(type)) {
|
|
193
|
-
return processForLoop(stmt, currentBlock);
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
// While loop (including language variants like while_let_expression)
|
|
197
|
-
if (type === rules.whileNode || rules.whileNodes?.has(type)) {
|
|
198
|
-
return processWhileLoop(stmt, currentBlock);
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
// Until (Ruby) — same CFG shape as while
|
|
202
|
-
if (rules.untilNode && type === rules.untilNode) {
|
|
203
|
-
return processWhileLoop(stmt, currentBlock);
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
// Do-while loop
|
|
207
|
-
if (rules.doNode && type === rules.doNode) {
|
|
208
|
-
return processDoWhileLoop(stmt, currentBlock);
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
// Infinite loop (Rust's loop {})
|
|
212
|
-
if (rules.infiniteLoopNode && type === rules.infiniteLoopNode) {
|
|
213
|
-
return processInfiniteLoop(stmt, currentBlock);
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
// Switch / match statement
|
|
217
|
-
if (type === rules.switchNode || rules.switchNodes?.has(type)) {
|
|
218
|
-
return processSwitch(stmt, currentBlock);
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
// Try/catch/finally
|
|
222
|
-
if (rules.tryNode && type === rules.tryNode) {
|
|
223
|
-
return processTryCatch(stmt, currentBlock);
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
// Return statement
|
|
227
|
-
if (type === rules.returnNode) {
|
|
228
|
-
currentBlock.endLine = stmt.startPosition.row + 1;
|
|
229
|
-
addEdge(currentBlock, exitBlock, 'return');
|
|
230
|
-
return null; // path terminated
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
// Throw statement
|
|
234
|
-
if (type === rules.throwNode) {
|
|
235
|
-
currentBlock.endLine = stmt.startPosition.row + 1;
|
|
236
|
-
addEdge(currentBlock, exitBlock, 'exception');
|
|
237
|
-
return null; // path terminated
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
// Break statement
|
|
241
|
-
if (type === rules.breakNode) {
|
|
242
|
-
const labelNode = stmt.childForFieldName('label');
|
|
243
|
-
const labelName = labelNode ? labelNode.text : null;
|
|
244
|
-
|
|
245
|
-
let target = null;
|
|
246
|
-
if (labelName && labelMap.has(labelName)) {
|
|
247
|
-
target = labelMap.get(labelName).exitBlock;
|
|
248
|
-
} else if (loopStack.length > 0) {
|
|
249
|
-
target = loopStack[loopStack.length - 1].exitBlock;
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
if (target) {
|
|
253
|
-
currentBlock.endLine = stmt.startPosition.row + 1;
|
|
254
|
-
addEdge(currentBlock, target, 'break');
|
|
255
|
-
return null; // path terminated
|
|
256
|
-
}
|
|
257
|
-
// break with no enclosing loop/switch — treat as no-op
|
|
258
|
-
return currentBlock;
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
// Continue statement
|
|
262
|
-
if (type === rules.continueNode) {
|
|
263
|
-
const labelNode = stmt.childForFieldName('label');
|
|
264
|
-
const labelName = labelNode ? labelNode.text : null;
|
|
265
|
-
|
|
266
|
-
let target = null;
|
|
267
|
-
if (labelName && labelMap.has(labelName)) {
|
|
268
|
-
target = labelMap.get(labelName).headerBlock;
|
|
269
|
-
} else if (loopStack.length > 0) {
|
|
270
|
-
target = loopStack[loopStack.length - 1].headerBlock;
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
if (target) {
|
|
274
|
-
currentBlock.endLine = stmt.startPosition.row + 1;
|
|
275
|
-
addEdge(currentBlock, target, 'continue');
|
|
276
|
-
return null; // path terminated
|
|
277
|
-
}
|
|
278
|
-
return currentBlock;
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
// Regular statement — extend current block
|
|
282
|
-
if (!currentBlock.startLine) {
|
|
283
|
-
currentBlock.startLine = stmt.startPosition.row + 1;
|
|
284
|
-
}
|
|
285
|
-
currentBlock.endLine = stmt.endPosition.row + 1;
|
|
286
|
-
return currentBlock;
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
/**
|
|
290
|
-
* Process an if/else-if/else chain.
|
|
291
|
-
* Handles three patterns:
|
|
292
|
-
* A) Wrapper: alternative → else_clause → nested if or block (JS/TS, Rust)
|
|
293
|
-
* B) Siblings: elif/elsif/else_if as sibling children (Python, Ruby, PHP)
|
|
294
|
-
* C) Direct: alternative → if_statement or block directly (Go, Java, C#)
|
|
295
|
-
*/
|
|
296
|
-
function processIf(ifStmt, currentBlock) {
|
|
297
|
-
// Terminate current block at condition
|
|
298
|
-
currentBlock.endLine = ifStmt.startPosition.row + 1;
|
|
299
|
-
|
|
300
|
-
const condBlock = makeBlock(
|
|
301
|
-
'condition',
|
|
302
|
-
ifStmt.startPosition.row + 1,
|
|
303
|
-
ifStmt.startPosition.row + 1,
|
|
304
|
-
'if',
|
|
305
|
-
);
|
|
306
|
-
addEdge(currentBlock, condBlock, 'fallthrough');
|
|
307
|
-
|
|
308
|
-
const joinBlock = makeBlock('body');
|
|
309
|
-
|
|
310
|
-
// True branch (consequent)
|
|
311
|
-
const consequentField = rules.ifConsequentField || 'consequence';
|
|
312
|
-
const consequent = ifStmt.childForFieldName(consequentField);
|
|
313
|
-
const trueBlock = makeBlock('branch_true', null, null, 'then');
|
|
314
|
-
addEdge(condBlock, trueBlock, 'branch_true');
|
|
315
|
-
const trueStmts = getStatements(consequent);
|
|
316
|
-
const trueEnd = processStatements(trueStmts, trueBlock);
|
|
317
|
-
if (trueEnd) {
|
|
318
|
-
addEdge(trueEnd, joinBlock, 'fallthrough');
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
// False branch — depends on language pattern
|
|
322
|
-
if (rules.elifNode) {
|
|
323
|
-
// Pattern B: elif/else as siblings of the if node
|
|
324
|
-
processElifSiblings(ifStmt, condBlock, joinBlock);
|
|
325
|
-
} else {
|
|
326
|
-
const alternative = ifStmt.childForFieldName('alternative');
|
|
327
|
-
if (alternative) {
|
|
328
|
-
if (rules.elseViaAlternative && alternative.type !== rules.elseClause) {
|
|
329
|
-
// Pattern C: alternative points directly to if or block
|
|
330
|
-
if (alternative.type === rules.ifNode || rules.ifNodes?.has(alternative.type)) {
|
|
331
|
-
// else-if: recurse
|
|
332
|
-
const falseBlock = makeBlock('branch_false', null, null, 'else-if');
|
|
333
|
-
addEdge(condBlock, falseBlock, 'branch_false');
|
|
334
|
-
const elseIfEnd = processIf(alternative, falseBlock);
|
|
335
|
-
if (elseIfEnd) {
|
|
336
|
-
addEdge(elseIfEnd, joinBlock, 'fallthrough');
|
|
337
|
-
}
|
|
338
|
-
} else {
|
|
339
|
-
// else block
|
|
340
|
-
const falseBlock = makeBlock('branch_false', null, null, 'else');
|
|
341
|
-
addEdge(condBlock, falseBlock, 'branch_false');
|
|
342
|
-
const falseStmts = getStatements(alternative);
|
|
343
|
-
const falseEnd = processStatements(falseStmts, falseBlock);
|
|
344
|
-
if (falseEnd) {
|
|
345
|
-
addEdge(falseEnd, joinBlock, 'fallthrough');
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
} else if (alternative.type === rules.elseClause) {
|
|
349
|
-
// Pattern A: else_clause wrapper — may contain another if (else-if) or a block
|
|
350
|
-
const elseChildren = [];
|
|
351
|
-
for (let i = 0; i < alternative.namedChildCount; i++) {
|
|
352
|
-
elseChildren.push(alternative.namedChild(i));
|
|
353
|
-
}
|
|
354
|
-
if (
|
|
355
|
-
elseChildren.length === 1 &&
|
|
356
|
-
(elseChildren[0].type === rules.ifNode || rules.ifNodes?.has(elseChildren[0].type))
|
|
357
|
-
) {
|
|
358
|
-
// else-if: recurse
|
|
359
|
-
const falseBlock = makeBlock('branch_false', null, null, 'else-if');
|
|
360
|
-
addEdge(condBlock, falseBlock, 'branch_false');
|
|
361
|
-
const elseIfEnd = processIf(elseChildren[0], falseBlock);
|
|
362
|
-
if (elseIfEnd) {
|
|
363
|
-
addEdge(elseIfEnd, joinBlock, 'fallthrough');
|
|
364
|
-
}
|
|
365
|
-
} else {
|
|
366
|
-
// else block
|
|
367
|
-
const falseBlock = makeBlock('branch_false', null, null, 'else');
|
|
368
|
-
addEdge(condBlock, falseBlock, 'branch_false');
|
|
369
|
-
const falseEnd = processStatements(elseChildren, falseBlock);
|
|
370
|
-
if (falseEnd) {
|
|
371
|
-
addEdge(falseEnd, joinBlock, 'fallthrough');
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
} else {
|
|
376
|
-
// No else: condition-false goes directly to join
|
|
377
|
-
addEdge(condBlock, joinBlock, 'branch_false');
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
return joinBlock;
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
/**
|
|
385
|
-
* Handle Pattern B: elif/elsif/else_if as sibling children of the if node.
|
|
386
|
-
*/
|
|
387
|
-
function processElifSiblings(ifStmt, firstCondBlock, joinBlock) {
|
|
388
|
-
let lastCondBlock = firstCondBlock;
|
|
389
|
-
let foundElse = false;
|
|
390
|
-
|
|
391
|
-
for (let i = 0; i < ifStmt.namedChildCount; i++) {
|
|
392
|
-
const child = ifStmt.namedChild(i);
|
|
393
|
-
|
|
394
|
-
if (child.type === rules.elifNode) {
|
|
395
|
-
// Create condition block for elif
|
|
396
|
-
const elifCondBlock = makeBlock(
|
|
397
|
-
'condition',
|
|
398
|
-
child.startPosition.row + 1,
|
|
399
|
-
child.startPosition.row + 1,
|
|
400
|
-
'else-if',
|
|
401
|
-
);
|
|
402
|
-
addEdge(lastCondBlock, elifCondBlock, 'branch_false');
|
|
403
|
-
|
|
404
|
-
// True branch of elif
|
|
405
|
-
const elifConsequentField = rules.ifConsequentField || 'consequence';
|
|
406
|
-
const elifConsequent = child.childForFieldName(elifConsequentField);
|
|
407
|
-
const elifTrueBlock = makeBlock('branch_true', null, null, 'then');
|
|
408
|
-
addEdge(elifCondBlock, elifTrueBlock, 'branch_true');
|
|
409
|
-
const elifTrueStmts = getStatements(elifConsequent);
|
|
410
|
-
const elifTrueEnd = processStatements(elifTrueStmts, elifTrueBlock);
|
|
411
|
-
if (elifTrueEnd) {
|
|
412
|
-
addEdge(elifTrueEnd, joinBlock, 'fallthrough');
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
lastCondBlock = elifCondBlock;
|
|
416
|
-
} else if (child.type === rules.elseClause) {
|
|
417
|
-
// Else body
|
|
418
|
-
const elseBlock = makeBlock('branch_false', null, null, 'else');
|
|
419
|
-
addEdge(lastCondBlock, elseBlock, 'branch_false');
|
|
420
|
-
|
|
421
|
-
// Try field access first, then collect children
|
|
422
|
-
const elseBody = child.childForFieldName('body');
|
|
423
|
-
let elseStmts;
|
|
424
|
-
if (elseBody) {
|
|
425
|
-
elseStmts = getStatements(elseBody);
|
|
426
|
-
} else {
|
|
427
|
-
elseStmts = [];
|
|
428
|
-
for (let j = 0; j < child.namedChildCount; j++) {
|
|
429
|
-
elseStmts.push(child.namedChild(j));
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
const elseEnd = processStatements(elseStmts, elseBlock);
|
|
433
|
-
if (elseEnd) {
|
|
434
|
-
addEdge(elseEnd, joinBlock, 'fallthrough');
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
foundElse = true;
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
// If no else clause, last condition's false goes to join
|
|
442
|
-
if (!foundElse) {
|
|
443
|
-
addEdge(lastCondBlock, joinBlock, 'branch_false');
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
/**
|
|
448
|
-
* Process a for/for-in loop.
|
|
449
|
-
*/
|
|
450
|
-
function processForLoop(forStmt, currentBlock) {
|
|
451
|
-
const headerBlock = makeBlock(
|
|
452
|
-
'loop_header',
|
|
453
|
-
forStmt.startPosition.row + 1,
|
|
454
|
-
forStmt.startPosition.row + 1,
|
|
455
|
-
'for',
|
|
456
|
-
);
|
|
457
|
-
addEdge(currentBlock, headerBlock, 'fallthrough');
|
|
458
|
-
|
|
459
|
-
const loopExitBlock = makeBlock('body');
|
|
460
|
-
|
|
461
|
-
// Register loop context
|
|
462
|
-
const loopCtx = { headerBlock, exitBlock: loopExitBlock };
|
|
463
|
-
loopStack.push(loopCtx);
|
|
464
|
-
|
|
465
|
-
// Update label map if this is inside a labeled statement
|
|
466
|
-
for (const [, ctx] of labelMap) {
|
|
467
|
-
if (!ctx.headerBlock) {
|
|
468
|
-
ctx.headerBlock = headerBlock;
|
|
469
|
-
ctx.exitBlock = loopExitBlock;
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
// Loop body
|
|
474
|
-
const body = forStmt.childForFieldName('body');
|
|
475
|
-
const bodyBlock = makeBlock('loop_body');
|
|
476
|
-
addEdge(headerBlock, bodyBlock, 'branch_true');
|
|
477
|
-
|
|
478
|
-
const bodyStmts = getStatements(body);
|
|
479
|
-
const bodyEnd = processStatements(bodyStmts, bodyBlock);
|
|
480
|
-
|
|
481
|
-
if (bodyEnd) {
|
|
482
|
-
addEdge(bodyEnd, headerBlock, 'loop_back');
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
// Loop exit
|
|
486
|
-
addEdge(headerBlock, loopExitBlock, 'loop_exit');
|
|
487
|
-
|
|
488
|
-
loopStack.pop();
|
|
489
|
-
return loopExitBlock;
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
/**
|
|
493
|
-
* Process a while loop.
|
|
494
|
-
*/
|
|
495
|
-
function processWhileLoop(whileStmt, currentBlock) {
|
|
496
|
-
const headerBlock = makeBlock(
|
|
497
|
-
'loop_header',
|
|
498
|
-
whileStmt.startPosition.row + 1,
|
|
499
|
-
whileStmt.startPosition.row + 1,
|
|
500
|
-
'while',
|
|
501
|
-
);
|
|
502
|
-
addEdge(currentBlock, headerBlock, 'fallthrough');
|
|
503
|
-
|
|
504
|
-
const loopExitBlock = makeBlock('body');
|
|
505
|
-
|
|
506
|
-
const loopCtx = { headerBlock, exitBlock: loopExitBlock };
|
|
507
|
-
loopStack.push(loopCtx);
|
|
508
|
-
|
|
509
|
-
for (const [, ctx] of labelMap) {
|
|
510
|
-
if (!ctx.headerBlock) {
|
|
511
|
-
ctx.headerBlock = headerBlock;
|
|
512
|
-
ctx.exitBlock = loopExitBlock;
|
|
513
|
-
}
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
const body = whileStmt.childForFieldName('body');
|
|
517
|
-
const bodyBlock = makeBlock('loop_body');
|
|
518
|
-
addEdge(headerBlock, bodyBlock, 'branch_true');
|
|
519
|
-
|
|
520
|
-
const bodyStmts = getStatements(body);
|
|
521
|
-
const bodyEnd = processStatements(bodyStmts, bodyBlock);
|
|
522
|
-
|
|
523
|
-
if (bodyEnd) {
|
|
524
|
-
addEdge(bodyEnd, headerBlock, 'loop_back');
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
addEdge(headerBlock, loopExitBlock, 'loop_exit');
|
|
528
|
-
|
|
529
|
-
loopStack.pop();
|
|
530
|
-
return loopExitBlock;
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
/**
|
|
534
|
-
* Process a do-while loop.
|
|
535
|
-
*/
|
|
536
|
-
function processDoWhileLoop(doStmt, currentBlock) {
|
|
537
|
-
const bodyBlock = makeBlock('loop_body', doStmt.startPosition.row + 1, null, 'do');
|
|
538
|
-
addEdge(currentBlock, bodyBlock, 'fallthrough');
|
|
539
|
-
|
|
540
|
-
const condBlock = makeBlock('loop_header', null, null, 'do-while');
|
|
541
|
-
const loopExitBlock = makeBlock('body');
|
|
542
|
-
|
|
543
|
-
const loopCtx = { headerBlock: condBlock, exitBlock: loopExitBlock };
|
|
544
|
-
loopStack.push(loopCtx);
|
|
545
|
-
|
|
546
|
-
for (const [, ctx] of labelMap) {
|
|
547
|
-
if (!ctx.headerBlock) {
|
|
548
|
-
ctx.headerBlock = condBlock;
|
|
549
|
-
ctx.exitBlock = loopExitBlock;
|
|
550
|
-
}
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
const body = doStmt.childForFieldName('body');
|
|
554
|
-
const bodyStmts = getStatements(body);
|
|
555
|
-
const bodyEnd = processStatements(bodyStmts, bodyBlock);
|
|
556
|
-
|
|
557
|
-
if (bodyEnd) {
|
|
558
|
-
addEdge(bodyEnd, condBlock, 'fallthrough');
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
// Condition: loop_back or exit
|
|
562
|
-
addEdge(condBlock, bodyBlock, 'loop_back');
|
|
563
|
-
addEdge(condBlock, loopExitBlock, 'loop_exit');
|
|
564
|
-
|
|
565
|
-
loopStack.pop();
|
|
566
|
-
return loopExitBlock;
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
/**
|
|
570
|
-
* Process an infinite loop (Rust's `loop {}`).
|
|
571
|
-
* No condition — body always executes. Exit only via break.
|
|
572
|
-
*/
|
|
573
|
-
function processInfiniteLoop(loopStmt, currentBlock) {
|
|
574
|
-
const headerBlock = makeBlock(
|
|
575
|
-
'loop_header',
|
|
576
|
-
loopStmt.startPosition.row + 1,
|
|
577
|
-
loopStmt.startPosition.row + 1,
|
|
578
|
-
'loop',
|
|
579
|
-
);
|
|
580
|
-
addEdge(currentBlock, headerBlock, 'fallthrough');
|
|
581
|
-
|
|
582
|
-
const loopExitBlock = makeBlock('body');
|
|
583
|
-
|
|
584
|
-
const loopCtx = { headerBlock, exitBlock: loopExitBlock };
|
|
585
|
-
loopStack.push(loopCtx);
|
|
586
|
-
|
|
587
|
-
for (const [, ctx] of labelMap) {
|
|
588
|
-
if (!ctx.headerBlock) {
|
|
589
|
-
ctx.headerBlock = headerBlock;
|
|
590
|
-
ctx.exitBlock = loopExitBlock;
|
|
591
|
-
}
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
const body = loopStmt.childForFieldName('body');
|
|
595
|
-
const bodyBlock = makeBlock('loop_body');
|
|
596
|
-
addEdge(headerBlock, bodyBlock, 'branch_true');
|
|
597
|
-
|
|
598
|
-
const bodyStmts = getStatements(body);
|
|
599
|
-
const bodyEnd = processStatements(bodyStmts, bodyBlock);
|
|
600
|
-
|
|
601
|
-
if (bodyEnd) {
|
|
602
|
-
addEdge(bodyEnd, headerBlock, 'loop_back');
|
|
603
|
-
}
|
|
604
|
-
|
|
605
|
-
// No loop_exit from header — can only exit via break
|
|
606
|
-
|
|
607
|
-
loopStack.pop();
|
|
608
|
-
return loopExitBlock;
|
|
609
|
-
}
|
|
610
|
-
|
|
611
|
-
/**
|
|
612
|
-
* Process a switch statement.
|
|
613
|
-
*/
|
|
614
|
-
function processSwitch(switchStmt, currentBlock) {
|
|
615
|
-
currentBlock.endLine = switchStmt.startPosition.row + 1;
|
|
616
|
-
|
|
617
|
-
const switchHeader = makeBlock(
|
|
618
|
-
'condition',
|
|
619
|
-
switchStmt.startPosition.row + 1,
|
|
620
|
-
switchStmt.startPosition.row + 1,
|
|
621
|
-
'switch',
|
|
622
|
-
);
|
|
623
|
-
addEdge(currentBlock, switchHeader, 'fallthrough');
|
|
624
|
-
|
|
625
|
-
const joinBlock = makeBlock('body');
|
|
626
|
-
|
|
627
|
-
// Switch acts like a break target for contained break statements
|
|
628
|
-
const switchCtx = { headerBlock: switchHeader, exitBlock: joinBlock };
|
|
629
|
-
loopStack.push(switchCtx);
|
|
630
|
-
|
|
631
|
-
// Get case children from body field or direct children
|
|
632
|
-
const switchBody = switchStmt.childForFieldName('body');
|
|
633
|
-
const container = switchBody || switchStmt;
|
|
634
|
-
|
|
635
|
-
let hasDefault = false;
|
|
636
|
-
for (let i = 0; i < container.namedChildCount; i++) {
|
|
637
|
-
const caseClause = container.namedChild(i);
|
|
638
|
-
|
|
639
|
-
const isDefault = caseClause.type === rules.defaultNode;
|
|
640
|
-
const isCase =
|
|
641
|
-
isDefault || caseClause.type === rules.caseNode || rules.caseNodes?.has(caseClause.type);
|
|
642
|
-
|
|
643
|
-
if (!isCase) continue;
|
|
644
|
-
|
|
645
|
-
const caseLabel = isDefault ? 'default' : 'case';
|
|
646
|
-
const caseBlock = makeBlock('case', caseClause.startPosition.row + 1, null, caseLabel);
|
|
647
|
-
addEdge(switchHeader, caseBlock, isDefault ? 'branch_false' : 'branch_true');
|
|
648
|
-
if (isDefault) hasDefault = true;
|
|
649
|
-
|
|
650
|
-
// Extract case body: try field access, then collect non-header children
|
|
651
|
-
const caseBodyNode =
|
|
652
|
-
caseClause.childForFieldName('body') || caseClause.childForFieldName('consequence');
|
|
653
|
-
let caseStmts;
|
|
654
|
-
if (caseBodyNode) {
|
|
655
|
-
caseStmts = getStatements(caseBodyNode);
|
|
656
|
-
} else {
|
|
657
|
-
caseStmts = [];
|
|
658
|
-
const valueNode = caseClause.childForFieldName('value');
|
|
659
|
-
const patternNode = caseClause.childForFieldName('pattern');
|
|
660
|
-
for (let j = 0; j < caseClause.namedChildCount; j++) {
|
|
661
|
-
const child = caseClause.namedChild(j);
|
|
662
|
-
if (child !== valueNode && child !== patternNode && child.type !== 'switch_label') {
|
|
663
|
-
if (child.type === 'statement_list') {
|
|
664
|
-
// Unwrap statement_list (tree-sitter-go 0.25+)
|
|
665
|
-
for (let k = 0; k < child.namedChildCount; k++) {
|
|
666
|
-
caseStmts.push(child.namedChild(k));
|
|
667
|
-
}
|
|
668
|
-
} else {
|
|
669
|
-
caseStmts.push(child);
|
|
670
|
-
}
|
|
671
|
-
}
|
|
672
|
-
}
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
const caseEnd = processStatements(caseStmts, caseBlock);
|
|
676
|
-
if (caseEnd) {
|
|
677
|
-
addEdge(caseEnd, joinBlock, 'fallthrough');
|
|
678
|
-
}
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
// If no default case, switch header can skip to join
|
|
682
|
-
if (!hasDefault) {
|
|
683
|
-
addEdge(switchHeader, joinBlock, 'branch_false');
|
|
684
|
-
}
|
|
685
|
-
|
|
686
|
-
loopStack.pop();
|
|
687
|
-
return joinBlock;
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
/**
|
|
691
|
-
* Process try/catch/finally.
|
|
692
|
-
*/
|
|
693
|
-
function processTryCatch(tryStmt, currentBlock) {
|
|
694
|
-
currentBlock.endLine = tryStmt.startPosition.row + 1;
|
|
695
|
-
|
|
696
|
-
const joinBlock = makeBlock('body');
|
|
697
|
-
|
|
698
|
-
// Try body — field access or collect non-handler children (e.g., Ruby's begin)
|
|
699
|
-
const tryBody = tryStmt.childForFieldName('body');
|
|
700
|
-
let tryBodyStart;
|
|
701
|
-
let tryStmts;
|
|
702
|
-
if (tryBody) {
|
|
703
|
-
tryBodyStart = tryBody.startPosition.row + 1;
|
|
704
|
-
tryStmts = getStatements(tryBody);
|
|
705
|
-
} else {
|
|
706
|
-
tryBodyStart = tryStmt.startPosition.row + 1;
|
|
707
|
-
tryStmts = [];
|
|
708
|
-
for (let i = 0; i < tryStmt.namedChildCount; i++) {
|
|
709
|
-
const child = tryStmt.namedChild(i);
|
|
710
|
-
if (rules.catchNode && child.type === rules.catchNode) continue;
|
|
711
|
-
if (rules.finallyNode && child.type === rules.finallyNode) continue;
|
|
712
|
-
tryStmts.push(child);
|
|
713
|
-
}
|
|
714
|
-
}
|
|
715
|
-
|
|
716
|
-
const tryBlock = makeBlock('body', tryBodyStart, null, 'try');
|
|
717
|
-
addEdge(currentBlock, tryBlock, 'fallthrough');
|
|
718
|
-
const tryEnd = processStatements(tryStmts, tryBlock);
|
|
719
|
-
|
|
720
|
-
// Catch handler
|
|
721
|
-
let catchHandler = null;
|
|
722
|
-
let finallyHandler = null;
|
|
723
|
-
for (let i = 0; i < tryStmt.namedChildCount; i++) {
|
|
724
|
-
const child = tryStmt.namedChild(i);
|
|
725
|
-
if (rules.catchNode && child.type === rules.catchNode) catchHandler = child;
|
|
726
|
-
if (rules.finallyNode && child.type === rules.finallyNode) finallyHandler = child;
|
|
727
|
-
}
|
|
728
|
-
|
|
729
|
-
if (catchHandler) {
|
|
730
|
-
const catchBlock = makeBlock('catch', catchHandler.startPosition.row + 1, null, 'catch');
|
|
731
|
-
// Exception edge from try to catch
|
|
732
|
-
addEdge(tryBlock, catchBlock, 'exception');
|
|
733
|
-
|
|
734
|
-
// Catch body — try field access, then collect children
|
|
735
|
-
const catchBodyNode = catchHandler.childForFieldName('body');
|
|
736
|
-
let catchStmts;
|
|
737
|
-
if (catchBodyNode) {
|
|
738
|
-
catchStmts = getStatements(catchBodyNode);
|
|
739
|
-
} else {
|
|
740
|
-
catchStmts = [];
|
|
741
|
-
for (let i = 0; i < catchHandler.namedChildCount; i++) {
|
|
742
|
-
catchStmts.push(catchHandler.namedChild(i));
|
|
743
|
-
}
|
|
744
|
-
}
|
|
745
|
-
const catchEnd = processStatements(catchStmts, catchBlock);
|
|
746
|
-
|
|
747
|
-
if (finallyHandler) {
|
|
748
|
-
const finallyBlock = makeBlock(
|
|
749
|
-
'finally',
|
|
750
|
-
finallyHandler.startPosition.row + 1,
|
|
751
|
-
null,
|
|
752
|
-
'finally',
|
|
753
|
-
);
|
|
754
|
-
if (tryEnd) addEdge(tryEnd, finallyBlock, 'fallthrough');
|
|
755
|
-
if (catchEnd) addEdge(catchEnd, finallyBlock, 'fallthrough');
|
|
756
|
-
|
|
757
|
-
const finallyBodyNode = finallyHandler.childForFieldName('body');
|
|
758
|
-
const finallyStmts = finallyBodyNode
|
|
759
|
-
? getStatements(finallyBodyNode)
|
|
760
|
-
: getStatements(finallyHandler);
|
|
761
|
-
const finallyEnd = processStatements(finallyStmts, finallyBlock);
|
|
762
|
-
if (finallyEnd) addEdge(finallyEnd, joinBlock, 'fallthrough');
|
|
763
|
-
} else {
|
|
764
|
-
if (tryEnd) addEdge(tryEnd, joinBlock, 'fallthrough');
|
|
765
|
-
if (catchEnd) addEdge(catchEnd, joinBlock, 'fallthrough');
|
|
766
|
-
}
|
|
767
|
-
} else if (finallyHandler) {
|
|
768
|
-
const finallyBlock = makeBlock(
|
|
769
|
-
'finally',
|
|
770
|
-
finallyHandler.startPosition.row + 1,
|
|
771
|
-
null,
|
|
772
|
-
'finally',
|
|
773
|
-
);
|
|
774
|
-
if (tryEnd) addEdge(tryEnd, finallyBlock, 'fallthrough');
|
|
775
|
-
|
|
776
|
-
const finallyBodyNode = finallyHandler.childForFieldName('body');
|
|
777
|
-
const finallyStmts = finallyBodyNode
|
|
778
|
-
? getStatements(finallyBodyNode)
|
|
779
|
-
: getStatements(finallyHandler);
|
|
780
|
-
const finallyEnd = processStatements(finallyStmts, finallyBlock);
|
|
781
|
-
if (finallyEnd) addEdge(finallyEnd, joinBlock, 'fallthrough');
|
|
782
|
-
} else {
|
|
783
|
-
if (tryEnd) addEdge(tryEnd, joinBlock, 'fallthrough');
|
|
784
|
-
}
|
|
785
|
-
|
|
786
|
-
return joinBlock;
|
|
787
|
-
}
|
|
788
|
-
|
|
789
|
-
// ── Main entry point ──────────────────────────────────────────────────
|
|
790
|
-
|
|
791
|
-
const body = getFunctionBody(functionNode);
|
|
792
|
-
if (!body) {
|
|
793
|
-
// Empty function or expression body
|
|
794
|
-
addEdge(entryBlock, exitBlock, 'fallthrough');
|
|
795
|
-
return { blocks, edges };
|
|
796
|
-
}
|
|
797
|
-
|
|
798
|
-
const stmts = getStatements(body);
|
|
799
|
-
if (stmts.length === 0) {
|
|
800
|
-
addEdge(entryBlock, exitBlock, 'fallthrough');
|
|
801
|
-
return { blocks, edges };
|
|
802
|
-
}
|
|
803
|
-
|
|
804
|
-
const firstBlock = makeBlock('body');
|
|
805
|
-
addEdge(entryBlock, firstBlock, 'fallthrough');
|
|
806
|
-
|
|
807
|
-
const lastBlock = processStatements(stmts, firstBlock);
|
|
808
|
-
if (lastBlock) {
|
|
809
|
-
addEdge(lastBlock, exitBlock, 'fallthrough');
|
|
810
|
-
}
|
|
811
|
-
|
|
812
|
-
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 };
|
|
813
70
|
}
|
|
814
71
|
|
|
815
72
|
// ─── Build-Time: Compute CFG for Changed Files ─────────────────────────
|
|
@@ -868,12 +125,6 @@ export async function buildCFGData(db, fileSymbols, rootDir, _engineOpts) {
|
|
|
868
125
|
`INSERT INTO cfg_edges (function_node_id, source_block_id, target_block_id, kind)
|
|
869
126
|
VALUES (?, ?, ?, ?)`,
|
|
870
127
|
);
|
|
871
|
-
const deleteBlocks = db.prepare('DELETE FROM cfg_blocks WHERE function_node_id = ?');
|
|
872
|
-
const deleteEdges = db.prepare('DELETE FROM cfg_edges WHERE function_node_id = ?');
|
|
873
|
-
const getNodeId = db.prepare(
|
|
874
|
-
"SELECT id FROM nodes WHERE name = ? AND kind IN ('function','method') AND file = ? AND line = ?",
|
|
875
|
-
);
|
|
876
|
-
|
|
877
128
|
let analyzed = 0;
|
|
878
129
|
|
|
879
130
|
const tx = db.transaction(() => {
|
|
@@ -921,39 +172,74 @@ export async function buildCFGData(db, fileSymbols, rootDir, _engineOpts) {
|
|
|
921
172
|
const cfgRules = CFG_RULES.get(langId);
|
|
922
173
|
if (!cfgRules) continue;
|
|
923
174
|
|
|
924
|
-
|
|
925
|
-
//
|
|
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
|
+
}
|
|
926
208
|
|
|
927
209
|
for (const def of symbols.definitions) {
|
|
928
210
|
if (def.kind !== 'function' && def.kind !== 'method') continue;
|
|
929
211
|
if (!def.line) continue;
|
|
930
212
|
|
|
931
|
-
const
|
|
932
|
-
if (!
|
|
213
|
+
const nodeId = getFunctionNodeId(db, def.name, relPath, def.line);
|
|
214
|
+
if (!nodeId) continue;
|
|
933
215
|
|
|
934
|
-
//
|
|
216
|
+
// Use pre-computed CFG (native engine or unified walk), then visitor fallback
|
|
935
217
|
let cfg = null;
|
|
936
218
|
if (def.cfg?.blocks?.length) {
|
|
937
219
|
cfg = def.cfg;
|
|
938
|
-
} else {
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
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 };
|
|
944
231
|
}
|
|
945
232
|
|
|
946
233
|
if (!cfg || cfg.blocks.length === 0) continue;
|
|
947
234
|
|
|
948
235
|
// Clear old CFG data for this function
|
|
949
|
-
|
|
950
|
-
deleteBlocks.run(row.id);
|
|
236
|
+
deleteCfgForNode(db, nodeId);
|
|
951
237
|
|
|
952
238
|
// Insert blocks and build index→dbId mapping
|
|
953
239
|
const blockDbIds = new Map();
|
|
954
240
|
for (const block of cfg.blocks) {
|
|
955
241
|
const result = insertBlock.run(
|
|
956
|
-
|
|
242
|
+
nodeId,
|
|
957
243
|
block.index,
|
|
958
244
|
block.type,
|
|
959
245
|
block.startLine,
|
|
@@ -968,7 +254,7 @@ export async function buildCFGData(db, fileSymbols, rootDir, _engineOpts) {
|
|
|
968
254
|
const sourceDbId = blockDbIds.get(edge.sourceIndex);
|
|
969
255
|
const targetDbId = blockDbIds.get(edge.targetIndex);
|
|
970
256
|
if (sourceDbId && targetDbId) {
|
|
971
|
-
insertEdge.run(
|
|
257
|
+
insertEdge.run(nodeId, sourceDbId, targetDbId, edge.kind);
|
|
972
258
|
}
|
|
973
259
|
}
|
|
974
260
|
|
|
@@ -988,15 +274,6 @@ export async function buildCFGData(db, fileSymbols, rootDir, _engineOpts) {
|
|
|
988
274
|
|
|
989
275
|
// ─── Query-Time Functions ───────────────────────────────────────────────
|
|
990
276
|
|
|
991
|
-
function hasCfgTables(db) {
|
|
992
|
-
try {
|
|
993
|
-
db.prepare('SELECT 1 FROM cfg_blocks LIMIT 0').get();
|
|
994
|
-
return true;
|
|
995
|
-
} catch {
|
|
996
|
-
return false;
|
|
997
|
-
}
|
|
998
|
-
}
|
|
999
|
-
|
|
1000
277
|
function findNodes(db, name, opts = {}) {
|
|
1001
278
|
const kinds = opts.kind ? [opts.kind] : ['function', 'method'];
|
|
1002
279
|
const placeholders = kinds.map(() => '?').join(', ');
|
|
@@ -1046,25 +323,9 @@ export function cfgData(name, customDbPath, opts = {}) {
|
|
|
1046
323
|
return { name, results: [] };
|
|
1047
324
|
}
|
|
1048
325
|
|
|
1049
|
-
const blockStmt = db.prepare(
|
|
1050
|
-
`SELECT id, block_index, block_type, start_line, end_line, label
|
|
1051
|
-
FROM cfg_blocks WHERE function_node_id = ?
|
|
1052
|
-
ORDER BY block_index`,
|
|
1053
|
-
);
|
|
1054
|
-
const edgeStmt = db.prepare(
|
|
1055
|
-
`SELECT e.kind,
|
|
1056
|
-
sb.block_index AS source_index, sb.block_type AS source_type,
|
|
1057
|
-
tb.block_index AS target_index, tb.block_type AS target_type
|
|
1058
|
-
FROM cfg_edges e
|
|
1059
|
-
JOIN cfg_blocks sb ON e.source_block_id = sb.id
|
|
1060
|
-
JOIN cfg_blocks tb ON e.target_block_id = tb.id
|
|
1061
|
-
WHERE e.function_node_id = ?
|
|
1062
|
-
ORDER BY sb.block_index, tb.block_index`,
|
|
1063
|
-
);
|
|
1064
|
-
|
|
1065
326
|
const results = nodes.map((node) => {
|
|
1066
|
-
const cfgBlocks =
|
|
1067
|
-
const cfgEdges =
|
|
327
|
+
const cfgBlocks = getCfgBlocks(db, node.id);
|
|
328
|
+
const cfgEdges = getCfgEdges(db, node.id);
|
|
1068
329
|
|
|
1069
330
|
return {
|
|
1070
331
|
name: node.name,
|
|
@@ -1184,58 +445,3 @@ function edgeStyle(kind) {
|
|
|
1184
445
|
if (kind === 'continue') return ', color=blue, style=dashed';
|
|
1185
446
|
return '';
|
|
1186
447
|
}
|
|
1187
|
-
|
|
1188
|
-
// ─── CLI Printer ────────────────────────────────────────────────────────
|
|
1189
|
-
|
|
1190
|
-
/**
|
|
1191
|
-
* CLI display for cfg command.
|
|
1192
|
-
*/
|
|
1193
|
-
export function cfg(name, customDbPath, opts = {}) {
|
|
1194
|
-
const data = cfgData(name, customDbPath, opts);
|
|
1195
|
-
|
|
1196
|
-
if (outputResult(data, 'results', opts)) return;
|
|
1197
|
-
|
|
1198
|
-
if (data.warning) {
|
|
1199
|
-
console.log(`\u26A0 ${data.warning}`);
|
|
1200
|
-
return;
|
|
1201
|
-
}
|
|
1202
|
-
if (data.results.length === 0) {
|
|
1203
|
-
console.log(`No symbols matching "${name}".`);
|
|
1204
|
-
return;
|
|
1205
|
-
}
|
|
1206
|
-
|
|
1207
|
-
const format = opts.format || 'text';
|
|
1208
|
-
if (format === 'dot') {
|
|
1209
|
-
console.log(cfgToDOT(data));
|
|
1210
|
-
return;
|
|
1211
|
-
}
|
|
1212
|
-
if (format === 'mermaid') {
|
|
1213
|
-
console.log(cfgToMermaid(data));
|
|
1214
|
-
return;
|
|
1215
|
-
}
|
|
1216
|
-
|
|
1217
|
-
// Text format
|
|
1218
|
-
for (const r of data.results) {
|
|
1219
|
-
console.log(`\n${r.kind} ${r.name} (${r.file}:${r.line})`);
|
|
1220
|
-
console.log('\u2500'.repeat(60));
|
|
1221
|
-
console.log(` Blocks: ${r.summary.blockCount} Edges: ${r.summary.edgeCount}`);
|
|
1222
|
-
|
|
1223
|
-
if (r.blocks.length > 0) {
|
|
1224
|
-
console.log('\n Blocks:');
|
|
1225
|
-
for (const b of r.blocks) {
|
|
1226
|
-
const loc = b.startLine
|
|
1227
|
-
? ` L${b.startLine}${b.endLine && b.endLine !== b.startLine ? `-${b.endLine}` : ''}`
|
|
1228
|
-
: '';
|
|
1229
|
-
const label = b.label ? ` (${b.label})` : '';
|
|
1230
|
-
console.log(` [${b.index}] ${b.type}${label}${loc}`);
|
|
1231
|
-
}
|
|
1232
|
-
}
|
|
1233
|
-
|
|
1234
|
-
if (r.edges.length > 0) {
|
|
1235
|
-
console.log('\n Edges:');
|
|
1236
|
-
for (const e of r.edges) {
|
|
1237
|
-
console.log(` B${e.source} \u2192 B${e.target} [${e.kind}]`);
|
|
1238
|
-
}
|
|
1239
|
-
}
|
|
1240
|
-
}
|
|
1241
|
-
}
|