@nahisaho/musubix-dfg 1.8.5
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/dist/analyzers/index.d.ts +92 -0
- package/dist/analyzers/index.d.ts.map +1 -0
- package/dist/analyzers/index.js +902 -0
- package/dist/analyzers/index.js.map +1 -0
- package/dist/graph/index.d.ts +192 -0
- package/dist/graph/index.d.ts.map +1 -0
- package/dist/graph/index.js +552 -0
- package/dist/graph/index.js.map +1 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +39 -0
- package/dist/index.js.map +1 -0
- package/dist/types/index.d.ts +312 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +29 -0
- package/dist/types/index.js.map +1 -0
- package/dist/yata/index.d.ts +135 -0
- package/dist/yata/index.d.ts.map +1 -0
- package/dist/yata/index.js +557 -0
- package/dist/yata/index.js.map +1 -0
- package/package.json +82 -0
|
@@ -0,0 +1,902 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DFG/CFG Analyzers
|
|
3
|
+
*
|
|
4
|
+
* TypeScript AST to Data Flow Graph and Control Flow Graph conversion
|
|
5
|
+
*
|
|
6
|
+
* @packageDocumentation
|
|
7
|
+
* @module @nahisaho/musubix-dfg/analyzers
|
|
8
|
+
*/
|
|
9
|
+
import * as ts from 'typescript';
|
|
10
|
+
import { DFGBuilder, DFGAnalyzer, CFGBuilder, CFGAnalyzer, } from '../graph/index.js';
|
|
11
|
+
// ============================================================================
|
|
12
|
+
// Data Flow Analyzer
|
|
13
|
+
// ============================================================================
|
|
14
|
+
/**
|
|
15
|
+
* Data Flow Graph analyzer for TypeScript/JavaScript code
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```typescript
|
|
19
|
+
* const analyzer = new DataFlowAnalyzer();
|
|
20
|
+
* const dfg = await analyzer.analyze('src/user-service.ts');
|
|
21
|
+
*
|
|
22
|
+
* // Query dependencies
|
|
23
|
+
* const deps = dfg.getDataDependencies('userId');
|
|
24
|
+
* ```
|
|
25
|
+
*
|
|
26
|
+
* @traces REQ-DFG-001
|
|
27
|
+
*/
|
|
28
|
+
export class DataFlowAnalyzer {
|
|
29
|
+
options;
|
|
30
|
+
constructor(options = {}) {
|
|
31
|
+
this.options = {
|
|
32
|
+
interprocedural: options.interprocedural ?? false,
|
|
33
|
+
trackAliasing: options.trackAliasing ?? true,
|
|
34
|
+
includeTypes: options.includeTypes ?? true,
|
|
35
|
+
maxDepth: options.maxDepth ?? 10,
|
|
36
|
+
timeout: options.timeout ?? 30000,
|
|
37
|
+
includeExternal: options.includeExternal ?? false,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Analyze a TypeScript/JavaScript file
|
|
42
|
+
*/
|
|
43
|
+
async analyze(filePath) {
|
|
44
|
+
const fs = await import('fs');
|
|
45
|
+
const sourceCode = fs.readFileSync(filePath, 'utf-8');
|
|
46
|
+
return this.analyzeSource(sourceCode, filePath);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Analyze source code directly
|
|
50
|
+
*/
|
|
51
|
+
analyzeSource(sourceCode, filePath) {
|
|
52
|
+
const sourceFile = ts.createSourceFile(filePath, sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
|
|
53
|
+
const builder = new DFGBuilder(filePath);
|
|
54
|
+
const scopeStack = ['module'];
|
|
55
|
+
const variableMap = new Map(); // variable name -> node id
|
|
56
|
+
const visit = (node) => {
|
|
57
|
+
const location = this.getSourceLocation(sourceFile, node);
|
|
58
|
+
switch (node.kind) {
|
|
59
|
+
case ts.SyntaxKind.VariableDeclaration:
|
|
60
|
+
this.handleVariableDeclaration(node, builder, scopeStack, variableMap, location);
|
|
61
|
+
break;
|
|
62
|
+
case ts.SyntaxKind.Parameter:
|
|
63
|
+
this.handleParameter(node, builder, scopeStack, variableMap, location);
|
|
64
|
+
break;
|
|
65
|
+
case ts.SyntaxKind.FunctionDeclaration:
|
|
66
|
+
case ts.SyntaxKind.FunctionExpression:
|
|
67
|
+
case ts.SyntaxKind.ArrowFunction:
|
|
68
|
+
this.handleFunction(node, builder, scopeStack, variableMap, location, visit);
|
|
69
|
+
return; // Don't recurse automatically, handled in handleFunction
|
|
70
|
+
case ts.SyntaxKind.ClassDeclaration:
|
|
71
|
+
this.handleClass(node, builder, scopeStack, location, visit);
|
|
72
|
+
return;
|
|
73
|
+
case ts.SyntaxKind.CallExpression:
|
|
74
|
+
this.handleCallExpression(node, builder, scopeStack, variableMap, location);
|
|
75
|
+
break;
|
|
76
|
+
case ts.SyntaxKind.BinaryExpression:
|
|
77
|
+
this.handleBinaryExpression(node, builder, scopeStack, variableMap, location);
|
|
78
|
+
break;
|
|
79
|
+
case ts.SyntaxKind.Identifier:
|
|
80
|
+
this.handleIdentifier(node, builder, scopeStack, variableMap, location);
|
|
81
|
+
break;
|
|
82
|
+
case ts.SyntaxKind.ReturnStatement:
|
|
83
|
+
this.handleReturn(node, builder, scopeStack, variableMap, location);
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
ts.forEachChild(node, visit);
|
|
87
|
+
};
|
|
88
|
+
ts.forEachChild(sourceFile, visit);
|
|
89
|
+
return builder.build();
|
|
90
|
+
}
|
|
91
|
+
handleVariableDeclaration(node, builder, scopeStack, variableMap, location) {
|
|
92
|
+
const name = node.name.getText();
|
|
93
|
+
const nodeId = builder.generateNodeId('var');
|
|
94
|
+
const scope = scopeStack.join('.');
|
|
95
|
+
const dfgNode = {
|
|
96
|
+
id: nodeId,
|
|
97
|
+
type: 'variable',
|
|
98
|
+
name,
|
|
99
|
+
location,
|
|
100
|
+
scope,
|
|
101
|
+
metadata: {
|
|
102
|
+
isConst: node.parent.flags &
|
|
103
|
+
ts.NodeFlags.Const,
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
if (this.options.includeTypes && node.type) {
|
|
107
|
+
dfgNode.typeInfo = {
|
|
108
|
+
name: node.type.getText(),
|
|
109
|
+
fullType: node.type.getText(),
|
|
110
|
+
nullable: false,
|
|
111
|
+
isArray: ts.isArrayTypeNode(node.type),
|
|
112
|
+
isPromise: node.type.getText().startsWith('Promise'),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
builder.addNode(dfgNode);
|
|
116
|
+
variableMap.set(`${scope}.${name}`, nodeId);
|
|
117
|
+
// Handle initializer
|
|
118
|
+
if (node.initializer) {
|
|
119
|
+
this.createDataFlowEdges(node.initializer, nodeId, builder, scopeStack, variableMap);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
handleParameter(node, builder, scopeStack, variableMap, location) {
|
|
123
|
+
const name = node.name.getText();
|
|
124
|
+
const nodeId = builder.generateNodeId('param');
|
|
125
|
+
const scope = scopeStack.join('.');
|
|
126
|
+
const dfgNode = {
|
|
127
|
+
id: nodeId,
|
|
128
|
+
type: 'parameter',
|
|
129
|
+
name,
|
|
130
|
+
location,
|
|
131
|
+
scope,
|
|
132
|
+
metadata: {
|
|
133
|
+
isOptional: !!node.questionToken,
|
|
134
|
+
isRest: !!node.dotDotDotToken,
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
builder.addNode(dfgNode);
|
|
138
|
+
builder.addEntryPoint(nodeId);
|
|
139
|
+
variableMap.set(`${scope}.${name}`, nodeId);
|
|
140
|
+
}
|
|
141
|
+
handleFunction(node, builder, scopeStack, _variableMap, location, visit) {
|
|
142
|
+
const name = node.name?.getText() || `anonymous_${builder.generateNodeId('fn')}`;
|
|
143
|
+
const nodeId = builder.generateNodeId('fn');
|
|
144
|
+
const parentScope = scopeStack.join('.');
|
|
145
|
+
const dfgNode = {
|
|
146
|
+
id: nodeId,
|
|
147
|
+
type: 'function',
|
|
148
|
+
name,
|
|
149
|
+
location,
|
|
150
|
+
scope: parentScope,
|
|
151
|
+
metadata: {
|
|
152
|
+
isAsync: !!node.modifiers?.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword),
|
|
153
|
+
isGenerator: !!node.asteriskToken,
|
|
154
|
+
parameterCount: node.parameters.length,
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
builder.addNode(dfgNode);
|
|
158
|
+
builder.addEntryPoint(nodeId);
|
|
159
|
+
// Push function scope and visit children
|
|
160
|
+
scopeStack.push(name);
|
|
161
|
+
// Visit parameters
|
|
162
|
+
node.parameters.forEach((param) => visit(param));
|
|
163
|
+
// Visit body
|
|
164
|
+
if (node.body) {
|
|
165
|
+
ts.forEachChild(node.body, visit);
|
|
166
|
+
}
|
|
167
|
+
scopeStack.pop();
|
|
168
|
+
}
|
|
169
|
+
handleClass(node, builder, scopeStack, location, visit) {
|
|
170
|
+
const name = node.name?.getText() || 'AnonymousClass';
|
|
171
|
+
const nodeId = builder.generateNodeId('class');
|
|
172
|
+
const scope = scopeStack.join('.');
|
|
173
|
+
const dfgNode = {
|
|
174
|
+
id: nodeId,
|
|
175
|
+
type: 'class',
|
|
176
|
+
name,
|
|
177
|
+
location,
|
|
178
|
+
scope,
|
|
179
|
+
metadata: {
|
|
180
|
+
isAbstract: !!node.modifiers?.some((m) => m.kind === ts.SyntaxKind.AbstractKeyword),
|
|
181
|
+
memberCount: node.members.length,
|
|
182
|
+
},
|
|
183
|
+
};
|
|
184
|
+
builder.addNode(dfgNode);
|
|
185
|
+
scopeStack.push(name);
|
|
186
|
+
node.members.forEach((member) => visit(member));
|
|
187
|
+
scopeStack.pop();
|
|
188
|
+
}
|
|
189
|
+
handleCallExpression(node, builder, scopeStack, variableMap, location) {
|
|
190
|
+
const callName = node.expression.getText();
|
|
191
|
+
const nodeId = builder.generateNodeId('call');
|
|
192
|
+
const scope = scopeStack.join('.');
|
|
193
|
+
const dfgNode = {
|
|
194
|
+
id: nodeId,
|
|
195
|
+
type: 'call',
|
|
196
|
+
name: callName,
|
|
197
|
+
location,
|
|
198
|
+
scope,
|
|
199
|
+
metadata: {
|
|
200
|
+
argumentCount: node.arguments.length,
|
|
201
|
+
},
|
|
202
|
+
};
|
|
203
|
+
builder.addNode(dfgNode);
|
|
204
|
+
// Create edges from arguments
|
|
205
|
+
node.arguments.forEach((arg, _index) => {
|
|
206
|
+
this.createDataFlowEdges(arg, nodeId, builder, scopeStack, variableMap);
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
handleBinaryExpression(node, builder, scopeStack, variableMap, location) {
|
|
210
|
+
// Handle assignments
|
|
211
|
+
if (node.operatorToken.kind === ts.SyntaxKind.EqualsToken ||
|
|
212
|
+
node.operatorToken.kind === ts.SyntaxKind.PlusEqualsToken ||
|
|
213
|
+
node.operatorToken.kind === ts.SyntaxKind.MinusEqualsToken) {
|
|
214
|
+
const leftName = node.left.getText();
|
|
215
|
+
const scope = scopeStack.join('.');
|
|
216
|
+
const existingNodeId = variableMap.get(`${scope}.${leftName}`);
|
|
217
|
+
if (existingNodeId) {
|
|
218
|
+
// Create assignment node
|
|
219
|
+
const assignId = builder.generateNodeId('assign');
|
|
220
|
+
const assignNode = {
|
|
221
|
+
id: assignId,
|
|
222
|
+
type: 'assignment',
|
|
223
|
+
name: leftName,
|
|
224
|
+
location,
|
|
225
|
+
scope,
|
|
226
|
+
metadata: {
|
|
227
|
+
operator: node.operatorToken.getText(),
|
|
228
|
+
},
|
|
229
|
+
};
|
|
230
|
+
builder.addNode(assignNode);
|
|
231
|
+
// Edge from right side to assignment
|
|
232
|
+
this.createDataFlowEdges(node.right, assignId, builder, scopeStack, variableMap);
|
|
233
|
+
// Edge from assignment to variable
|
|
234
|
+
const edge = {
|
|
235
|
+
id: builder.generateEdgeId('e'),
|
|
236
|
+
type: 'def-use',
|
|
237
|
+
source: assignId,
|
|
238
|
+
target: existingNodeId,
|
|
239
|
+
weight: 1,
|
|
240
|
+
metadata: {},
|
|
241
|
+
};
|
|
242
|
+
builder.addEdge(edge);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
handleIdentifier(node, builder, scopeStack, variableMap, location) {
|
|
247
|
+
// Skip if parent is a declaration (handled separately)
|
|
248
|
+
const parent = node.parent;
|
|
249
|
+
if (ts.isVariableDeclaration(parent) ||
|
|
250
|
+
ts.isParameter(parent) ||
|
|
251
|
+
ts.isFunctionDeclaration(parent) ||
|
|
252
|
+
ts.isClassDeclaration(parent)) {
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
const name = node.getText();
|
|
256
|
+
const scope = scopeStack.join('.');
|
|
257
|
+
// Look up variable in current or parent scopes
|
|
258
|
+
let varNodeId;
|
|
259
|
+
const scopeParts = scope.split('.');
|
|
260
|
+
for (let i = scopeParts.length; i > 0; i--) {
|
|
261
|
+
const checkScope = scopeParts.slice(0, i).join('.');
|
|
262
|
+
varNodeId = variableMap.get(`${checkScope}.${name}`);
|
|
263
|
+
if (varNodeId)
|
|
264
|
+
break;
|
|
265
|
+
}
|
|
266
|
+
// If not found, might be external reference
|
|
267
|
+
if (!varNodeId && this.options.includeExternal) {
|
|
268
|
+
const externalId = builder.generateNodeId('ext');
|
|
269
|
+
const externalNode = {
|
|
270
|
+
id: externalId,
|
|
271
|
+
type: 'variable',
|
|
272
|
+
name,
|
|
273
|
+
location,
|
|
274
|
+
scope: 'external',
|
|
275
|
+
metadata: { isExternal: true },
|
|
276
|
+
};
|
|
277
|
+
builder.addNode(externalNode);
|
|
278
|
+
varNodeId = externalId;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
handleReturn(node, builder, scopeStack, variableMap, location) {
|
|
282
|
+
const nodeId = builder.generateNodeId('ret');
|
|
283
|
+
const scope = scopeStack.join('.');
|
|
284
|
+
const dfgNode = {
|
|
285
|
+
id: nodeId,
|
|
286
|
+
type: 'return',
|
|
287
|
+
name: 'return',
|
|
288
|
+
location,
|
|
289
|
+
scope,
|
|
290
|
+
metadata: {},
|
|
291
|
+
};
|
|
292
|
+
builder.addNode(dfgNode);
|
|
293
|
+
builder.addExitPoint(nodeId);
|
|
294
|
+
if (node.expression) {
|
|
295
|
+
this.createDataFlowEdges(node.expression, nodeId, builder, scopeStack, variableMap);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
createDataFlowEdges(expr, targetNodeId, builder, scopeStack, variableMap) {
|
|
299
|
+
const scope = scopeStack.join('.');
|
|
300
|
+
if (ts.isIdentifier(expr)) {
|
|
301
|
+
const name = expr.getText();
|
|
302
|
+
// Look up in scope chain
|
|
303
|
+
let sourceNodeId;
|
|
304
|
+
const scopeParts = scope.split('.');
|
|
305
|
+
for (let i = scopeParts.length; i > 0; i--) {
|
|
306
|
+
const checkScope = scopeParts.slice(0, i).join('.');
|
|
307
|
+
sourceNodeId = variableMap.get(`${checkScope}.${name}`);
|
|
308
|
+
if (sourceNodeId)
|
|
309
|
+
break;
|
|
310
|
+
}
|
|
311
|
+
if (sourceNodeId) {
|
|
312
|
+
const edge = {
|
|
313
|
+
id: builder.generateEdgeId('e'),
|
|
314
|
+
type: 'data-dep',
|
|
315
|
+
source: sourceNodeId,
|
|
316
|
+
target: targetNodeId,
|
|
317
|
+
weight: 1,
|
|
318
|
+
metadata: {},
|
|
319
|
+
};
|
|
320
|
+
builder.addEdge(edge);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
else if (ts.isBinaryExpression(expr)) {
|
|
324
|
+
this.createDataFlowEdges(expr.left, targetNodeId, builder, scopeStack, variableMap);
|
|
325
|
+
this.createDataFlowEdges(expr.right, targetNodeId, builder, scopeStack, variableMap);
|
|
326
|
+
}
|
|
327
|
+
else if (ts.isCallExpression(expr)) {
|
|
328
|
+
// Call result flows to target
|
|
329
|
+
const callNodeId = builder.generateNodeId('call');
|
|
330
|
+
const location = this.getSourceLocation(expr.getSourceFile(), expr);
|
|
331
|
+
const callNode = {
|
|
332
|
+
id: callNodeId,
|
|
333
|
+
type: 'call',
|
|
334
|
+
name: expr.expression.getText(),
|
|
335
|
+
location,
|
|
336
|
+
scope,
|
|
337
|
+
metadata: {},
|
|
338
|
+
};
|
|
339
|
+
builder.addNode(callNode);
|
|
340
|
+
const edge = {
|
|
341
|
+
id: builder.generateEdgeId('e'),
|
|
342
|
+
type: 'call-return',
|
|
343
|
+
source: callNodeId,
|
|
344
|
+
target: targetNodeId,
|
|
345
|
+
weight: 1,
|
|
346
|
+
metadata: {},
|
|
347
|
+
};
|
|
348
|
+
builder.addEdge(edge);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
getSourceLocation(sourceFile, node) {
|
|
352
|
+
const start = sourceFile.getLineAndCharacterOfPosition(node.getStart());
|
|
353
|
+
const end = sourceFile.getLineAndCharacterOfPosition(node.getEnd());
|
|
354
|
+
return {
|
|
355
|
+
filePath: sourceFile.fileName,
|
|
356
|
+
startLine: start.line + 1,
|
|
357
|
+
startColumn: start.character,
|
|
358
|
+
endLine: end.line + 1,
|
|
359
|
+
endColumn: end.character,
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Create analyzer for a built graph
|
|
364
|
+
*/
|
|
365
|
+
createAnalyzer(graph) {
|
|
366
|
+
return new DFGAnalyzer(graph);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
// ============================================================================
|
|
370
|
+
// Control Flow Analyzer
|
|
371
|
+
// ============================================================================
|
|
372
|
+
/**
|
|
373
|
+
* Control Flow Graph analyzer for TypeScript/JavaScript code
|
|
374
|
+
*
|
|
375
|
+
* @example
|
|
376
|
+
* ```typescript
|
|
377
|
+
* const analyzer = new ControlFlowAnalyzer();
|
|
378
|
+
* const cfg = await analyzer.analyze('src/user-service.ts', 'getUserById');
|
|
379
|
+
*
|
|
380
|
+
* // Get cyclomatic complexity
|
|
381
|
+
* const complexity = cfg.getCyclomaticComplexity();
|
|
382
|
+
* ```
|
|
383
|
+
*
|
|
384
|
+
* @traces REQ-DFG-002
|
|
385
|
+
*/
|
|
386
|
+
export class ControlFlowAnalyzer {
|
|
387
|
+
options;
|
|
388
|
+
constructor(options = {}) {
|
|
389
|
+
this.options = {
|
|
390
|
+
computeDominators: options.computeDominators ?? true,
|
|
391
|
+
computePostDominators: options.computePostDominators ?? true,
|
|
392
|
+
identifyLoops: options.identifyLoops ?? true,
|
|
393
|
+
includeExceptions: options.includeExceptions ?? true,
|
|
394
|
+
maxDepth: options.maxDepth ?? 10,
|
|
395
|
+
timeout: options.timeout ?? 30000,
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* Analyze a specific function in a file
|
|
400
|
+
*/
|
|
401
|
+
async analyze(filePath, functionName) {
|
|
402
|
+
const fs = await import('fs');
|
|
403
|
+
const sourceCode = fs.readFileSync(filePath, 'utf-8');
|
|
404
|
+
return this.analyzeSource(sourceCode, filePath, functionName);
|
|
405
|
+
}
|
|
406
|
+
/**
|
|
407
|
+
* Analyze source code directly
|
|
408
|
+
*/
|
|
409
|
+
analyzeSource(sourceCode, filePath, functionName) {
|
|
410
|
+
const sourceFile = ts.createSourceFile(filePath, sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
|
|
411
|
+
const graphs = [];
|
|
412
|
+
const visit = (node) => {
|
|
413
|
+
if (ts.isFunctionDeclaration(node) ||
|
|
414
|
+
ts.isMethodDeclaration(node) ||
|
|
415
|
+
ts.isArrowFunction(node) ||
|
|
416
|
+
ts.isFunctionExpression(node)) {
|
|
417
|
+
const name = this.getFunctionName(node);
|
|
418
|
+
if (!functionName || name === functionName) {
|
|
419
|
+
const cfg = this.analyzeFunction(node, sourceFile, filePath, name);
|
|
420
|
+
graphs.push(cfg);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
ts.forEachChild(node, visit);
|
|
424
|
+
};
|
|
425
|
+
ts.forEachChild(sourceFile, visit);
|
|
426
|
+
return graphs;
|
|
427
|
+
}
|
|
428
|
+
getFunctionName(node) {
|
|
429
|
+
if (ts.isFunctionDeclaration(node) && node.name) {
|
|
430
|
+
return node.name.getText();
|
|
431
|
+
}
|
|
432
|
+
if (ts.isMethodDeclaration(node) && node.name) {
|
|
433
|
+
return node.name.getText();
|
|
434
|
+
}
|
|
435
|
+
return `anonymous_${Math.random().toString(36).substr(2, 9)}`;
|
|
436
|
+
}
|
|
437
|
+
analyzeFunction(node, sourceFile, filePath, functionName) {
|
|
438
|
+
const builder = new CFGBuilder(functionName, filePath);
|
|
439
|
+
let loopDepth = 0;
|
|
440
|
+
// Create entry block
|
|
441
|
+
const entryId = builder.generateBlockId('entry');
|
|
442
|
+
const entryBlock = {
|
|
443
|
+
id: entryId,
|
|
444
|
+
type: 'entry',
|
|
445
|
+
label: 'entry',
|
|
446
|
+
statements: [],
|
|
447
|
+
predecessors: [],
|
|
448
|
+
successors: [],
|
|
449
|
+
loopDepth: 0,
|
|
450
|
+
location: this.getSourceLocation(sourceFile, node),
|
|
451
|
+
};
|
|
452
|
+
builder.addBlock(entryBlock);
|
|
453
|
+
builder.setEntryBlock(entryId);
|
|
454
|
+
// Create exit block
|
|
455
|
+
const exitId = builder.generateBlockId('exit');
|
|
456
|
+
const exitBlock = {
|
|
457
|
+
id: exitId,
|
|
458
|
+
type: 'exit',
|
|
459
|
+
label: 'exit',
|
|
460
|
+
statements: [],
|
|
461
|
+
predecessors: [],
|
|
462
|
+
successors: [],
|
|
463
|
+
loopDepth: 0,
|
|
464
|
+
location: this.getSourceLocation(sourceFile, node),
|
|
465
|
+
};
|
|
466
|
+
builder.addBlock(exitBlock);
|
|
467
|
+
builder.addExitBlock(exitId);
|
|
468
|
+
const processStatement = (stmt, blockId) => {
|
|
469
|
+
const location = this.getSourceLocation(sourceFile, stmt);
|
|
470
|
+
if (ts.isIfStatement(stmt)) {
|
|
471
|
+
return this.processIfStatement(stmt, blockId, exitId, builder, sourceFile, loopDepth, processStatement);
|
|
472
|
+
}
|
|
473
|
+
if (ts.isWhileStatement(stmt) || ts.isForStatement(stmt)) {
|
|
474
|
+
loopDepth++;
|
|
475
|
+
const result = this.processLoopStatement(stmt, blockId, exitId, builder, sourceFile, loopDepth, processStatement);
|
|
476
|
+
loopDepth--;
|
|
477
|
+
return result;
|
|
478
|
+
}
|
|
479
|
+
if (ts.isReturnStatement(stmt)) {
|
|
480
|
+
// Add return to current block and connect to exit
|
|
481
|
+
const block = builder.getBlock(blockId);
|
|
482
|
+
if (block) {
|
|
483
|
+
block.statements.push({
|
|
484
|
+
index: block.statements.length,
|
|
485
|
+
type: 'return',
|
|
486
|
+
text: stmt.getText(),
|
|
487
|
+
location,
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
const edgeId = builder.generateEdgeId('e');
|
|
491
|
+
builder.addEdge({
|
|
492
|
+
id: edgeId,
|
|
493
|
+
type: 'return',
|
|
494
|
+
source: blockId,
|
|
495
|
+
target: exitId,
|
|
496
|
+
isBackEdge: false,
|
|
497
|
+
});
|
|
498
|
+
return null; // No continuation
|
|
499
|
+
}
|
|
500
|
+
if (ts.isTryStatement(stmt) && this.options.includeExceptions) {
|
|
501
|
+
return this.processTryStatement(stmt, blockId, exitId, builder, sourceFile, loopDepth, processStatement);
|
|
502
|
+
}
|
|
503
|
+
// Regular statement - add to current block
|
|
504
|
+
const block = builder.getBlock(blockId);
|
|
505
|
+
if (block) {
|
|
506
|
+
block.statements.push({
|
|
507
|
+
index: block.statements.length,
|
|
508
|
+
type: stmt.kind.toString(),
|
|
509
|
+
text: stmt.getText().slice(0, 100), // Truncate long statements
|
|
510
|
+
location,
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
return blockId;
|
|
514
|
+
};
|
|
515
|
+
// Process function body
|
|
516
|
+
if (node.body) {
|
|
517
|
+
if (ts.isBlock(node.body)) {
|
|
518
|
+
let blockId = entryId;
|
|
519
|
+
for (const stmt of node.body.statements) {
|
|
520
|
+
if (blockId) {
|
|
521
|
+
blockId = processStatement(stmt, blockId);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
// Connect last block to exit if not already connected
|
|
525
|
+
if (blockId && blockId !== exitId) {
|
|
526
|
+
const block = builder.getBlock(blockId);
|
|
527
|
+
if (block && !block.successors.includes(exitId)) {
|
|
528
|
+
const edgeId = builder.generateEdgeId('e');
|
|
529
|
+
builder.addEdge({
|
|
530
|
+
id: edgeId,
|
|
531
|
+
type: 'sequential',
|
|
532
|
+
source: blockId,
|
|
533
|
+
target: exitId,
|
|
534
|
+
isBackEdge: false,
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
return builder.build();
|
|
541
|
+
}
|
|
542
|
+
processIfStatement(stmt, blockId, exitId, builder, sourceFile, loopDepth, processStatement) {
|
|
543
|
+
const location = this.getSourceLocation(sourceFile, stmt);
|
|
544
|
+
// Create conditional block
|
|
545
|
+
const condBlockId = builder.generateBlockId('cond');
|
|
546
|
+
const condBlock = {
|
|
547
|
+
id: condBlockId,
|
|
548
|
+
type: 'conditional',
|
|
549
|
+
label: `if (${stmt.expression.getText()})`,
|
|
550
|
+
statements: [],
|
|
551
|
+
predecessors: [],
|
|
552
|
+
successors: [],
|
|
553
|
+
loopDepth,
|
|
554
|
+
location,
|
|
555
|
+
};
|
|
556
|
+
builder.addBlock(condBlock);
|
|
557
|
+
// Connect from previous block
|
|
558
|
+
builder.addEdge({
|
|
559
|
+
id: builder.generateEdgeId('e'),
|
|
560
|
+
type: 'sequential',
|
|
561
|
+
source: blockId,
|
|
562
|
+
target: condBlockId,
|
|
563
|
+
isBackEdge: false,
|
|
564
|
+
});
|
|
565
|
+
// Create merge block
|
|
566
|
+
const mergeBlockId = builder.generateBlockId('merge');
|
|
567
|
+
const mergeBlock = {
|
|
568
|
+
id: mergeBlockId,
|
|
569
|
+
type: 'basic',
|
|
570
|
+
label: 'merge',
|
|
571
|
+
statements: [],
|
|
572
|
+
predecessors: [],
|
|
573
|
+
successors: [],
|
|
574
|
+
loopDepth,
|
|
575
|
+
location,
|
|
576
|
+
};
|
|
577
|
+
builder.addBlock(mergeBlock);
|
|
578
|
+
// Process then branch
|
|
579
|
+
const thenBlockId = builder.generateBlockId('then');
|
|
580
|
+
const thenBlock = {
|
|
581
|
+
id: thenBlockId,
|
|
582
|
+
type: 'basic',
|
|
583
|
+
label: 'then',
|
|
584
|
+
statements: [],
|
|
585
|
+
predecessors: [],
|
|
586
|
+
successors: [],
|
|
587
|
+
loopDepth,
|
|
588
|
+
location: this.getSourceLocation(sourceFile, stmt.thenStatement),
|
|
589
|
+
};
|
|
590
|
+
builder.addBlock(thenBlock);
|
|
591
|
+
builder.addEdge({
|
|
592
|
+
id: builder.generateEdgeId('e'),
|
|
593
|
+
type: 'conditional-true',
|
|
594
|
+
source: condBlockId,
|
|
595
|
+
target: thenBlockId,
|
|
596
|
+
condition: stmt.expression.getText(),
|
|
597
|
+
isBackEdge: false,
|
|
598
|
+
});
|
|
599
|
+
let thenEndBlock = thenBlockId;
|
|
600
|
+
if (ts.isBlock(stmt.thenStatement)) {
|
|
601
|
+
for (const s of stmt.thenStatement.statements) {
|
|
602
|
+
if (thenEndBlock) {
|
|
603
|
+
thenEndBlock = processStatement(s, thenEndBlock);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
if (thenEndBlock) {
|
|
608
|
+
builder.addEdge({
|
|
609
|
+
id: builder.generateEdgeId('e'),
|
|
610
|
+
type: 'sequential',
|
|
611
|
+
source: thenEndBlock,
|
|
612
|
+
target: mergeBlockId,
|
|
613
|
+
isBackEdge: false,
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
// Process else branch
|
|
617
|
+
if (stmt.elseStatement) {
|
|
618
|
+
const elseBlockId = builder.generateBlockId('else');
|
|
619
|
+
const elseBlock = {
|
|
620
|
+
id: elseBlockId,
|
|
621
|
+
type: 'basic',
|
|
622
|
+
label: 'else',
|
|
623
|
+
statements: [],
|
|
624
|
+
predecessors: [],
|
|
625
|
+
successors: [],
|
|
626
|
+
loopDepth,
|
|
627
|
+
location: this.getSourceLocation(sourceFile, stmt.elseStatement),
|
|
628
|
+
};
|
|
629
|
+
builder.addBlock(elseBlock);
|
|
630
|
+
builder.addEdge({
|
|
631
|
+
id: builder.generateEdgeId('e'),
|
|
632
|
+
type: 'conditional-false',
|
|
633
|
+
source: condBlockId,
|
|
634
|
+
target: elseBlockId,
|
|
635
|
+
condition: `!(${stmt.expression.getText()})`,
|
|
636
|
+
isBackEdge: false,
|
|
637
|
+
});
|
|
638
|
+
let elseEndBlock = elseBlockId;
|
|
639
|
+
if (ts.isBlock(stmt.elseStatement)) {
|
|
640
|
+
for (const s of stmt.elseStatement.statements) {
|
|
641
|
+
if (elseEndBlock) {
|
|
642
|
+
elseEndBlock = processStatement(s, elseEndBlock);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
else if (ts.isIfStatement(stmt.elseStatement)) {
|
|
647
|
+
elseEndBlock = this.processIfStatement(stmt.elseStatement, elseBlockId, exitId, builder, sourceFile, loopDepth, processStatement);
|
|
648
|
+
}
|
|
649
|
+
if (elseEndBlock) {
|
|
650
|
+
builder.addEdge({
|
|
651
|
+
id: builder.generateEdgeId('e'),
|
|
652
|
+
type: 'sequential',
|
|
653
|
+
source: elseEndBlock,
|
|
654
|
+
target: mergeBlockId,
|
|
655
|
+
isBackEdge: false,
|
|
656
|
+
});
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
else {
|
|
660
|
+
// No else - connect condition directly to merge
|
|
661
|
+
builder.addEdge({
|
|
662
|
+
id: builder.generateEdgeId('e'),
|
|
663
|
+
type: 'conditional-false',
|
|
664
|
+
source: condBlockId,
|
|
665
|
+
target: mergeBlockId,
|
|
666
|
+
isBackEdge: false,
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
return mergeBlockId;
|
|
670
|
+
}
|
|
671
|
+
processLoopStatement(stmt, blockId, _exitId, builder, sourceFile, loopDepth, processStatement) {
|
|
672
|
+
const location = this.getSourceLocation(sourceFile, stmt);
|
|
673
|
+
// Create loop header
|
|
674
|
+
const headerBlockId = builder.generateBlockId('loop_header');
|
|
675
|
+
const headerBlock = {
|
|
676
|
+
id: headerBlockId,
|
|
677
|
+
type: 'loop-header',
|
|
678
|
+
label: ts.isWhileStatement(stmt)
|
|
679
|
+
? `while (${stmt.expression.getText()})`
|
|
680
|
+
: `for (...)`,
|
|
681
|
+
statements: [],
|
|
682
|
+
predecessors: [],
|
|
683
|
+
successors: [],
|
|
684
|
+
loopDepth,
|
|
685
|
+
location,
|
|
686
|
+
};
|
|
687
|
+
builder.addBlock(headerBlock);
|
|
688
|
+
builder.addEdge({
|
|
689
|
+
id: builder.generateEdgeId('e'),
|
|
690
|
+
type: 'sequential',
|
|
691
|
+
source: blockId,
|
|
692
|
+
target: headerBlockId,
|
|
693
|
+
isBackEdge: false,
|
|
694
|
+
});
|
|
695
|
+
// Create loop body
|
|
696
|
+
const bodyBlockId = builder.generateBlockId('loop_body');
|
|
697
|
+
const bodyBlock = {
|
|
698
|
+
id: bodyBlockId,
|
|
699
|
+
type: 'loop-body',
|
|
700
|
+
label: 'loop body',
|
|
701
|
+
statements: [],
|
|
702
|
+
predecessors: [],
|
|
703
|
+
successors: [],
|
|
704
|
+
loopDepth,
|
|
705
|
+
location: this.getSourceLocation(sourceFile, stmt.statement),
|
|
706
|
+
};
|
|
707
|
+
builder.addBlock(bodyBlock);
|
|
708
|
+
builder.addEdge({
|
|
709
|
+
id: builder.generateEdgeId('e'),
|
|
710
|
+
type: 'conditional-true',
|
|
711
|
+
source: headerBlockId,
|
|
712
|
+
target: bodyBlockId,
|
|
713
|
+
condition: ts.isWhileStatement(stmt)
|
|
714
|
+
? stmt.expression.getText()
|
|
715
|
+
: 'condition',
|
|
716
|
+
isBackEdge: false,
|
|
717
|
+
});
|
|
718
|
+
// Process body statements
|
|
719
|
+
let bodyEndBlock = bodyBlockId;
|
|
720
|
+
if (ts.isBlock(stmt.statement)) {
|
|
721
|
+
for (const s of stmt.statement.statements) {
|
|
722
|
+
if (bodyEndBlock) {
|
|
723
|
+
bodyEndBlock = processStatement(s, bodyEndBlock);
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
// Back edge from body to header
|
|
728
|
+
if (bodyEndBlock) {
|
|
729
|
+
builder.addEdge({
|
|
730
|
+
id: builder.generateEdgeId('e'),
|
|
731
|
+
type: 'loop-back',
|
|
732
|
+
source: bodyEndBlock,
|
|
733
|
+
target: headerBlockId,
|
|
734
|
+
isBackEdge: true,
|
|
735
|
+
});
|
|
736
|
+
}
|
|
737
|
+
// Create loop exit
|
|
738
|
+
const exitBlockId = builder.generateBlockId('loop_exit');
|
|
739
|
+
const loopExitBlock = {
|
|
740
|
+
id: exitBlockId,
|
|
741
|
+
type: 'loop-exit',
|
|
742
|
+
label: 'loop exit',
|
|
743
|
+
statements: [],
|
|
744
|
+
predecessors: [],
|
|
745
|
+
successors: [],
|
|
746
|
+
loopDepth: loopDepth - 1,
|
|
747
|
+
location,
|
|
748
|
+
};
|
|
749
|
+
builder.addBlock(loopExitBlock);
|
|
750
|
+
builder.addEdge({
|
|
751
|
+
id: builder.generateEdgeId('e'),
|
|
752
|
+
type: 'loop-exit',
|
|
753
|
+
source: headerBlockId,
|
|
754
|
+
target: exitBlockId,
|
|
755
|
+
isBackEdge: false,
|
|
756
|
+
});
|
|
757
|
+
return exitBlockId;
|
|
758
|
+
}
|
|
759
|
+
processTryStatement(stmt, blockId, _exitId, builder, sourceFile, loopDepth, processStatement) {
|
|
760
|
+
const location = this.getSourceLocation(sourceFile, stmt);
|
|
761
|
+
// Create try block
|
|
762
|
+
const tryBlockId = builder.generateBlockId('try');
|
|
763
|
+
const tryBlock = {
|
|
764
|
+
id: tryBlockId,
|
|
765
|
+
type: 'try',
|
|
766
|
+
label: 'try',
|
|
767
|
+
statements: [],
|
|
768
|
+
predecessors: [],
|
|
769
|
+
successors: [],
|
|
770
|
+
loopDepth,
|
|
771
|
+
location,
|
|
772
|
+
};
|
|
773
|
+
builder.addBlock(tryBlock);
|
|
774
|
+
builder.addEdge({
|
|
775
|
+
id: builder.generateEdgeId('e'),
|
|
776
|
+
type: 'sequential',
|
|
777
|
+
source: blockId,
|
|
778
|
+
target: tryBlockId,
|
|
779
|
+
isBackEdge: false,
|
|
780
|
+
});
|
|
781
|
+
// Process try body
|
|
782
|
+
let tryEndBlock = tryBlockId;
|
|
783
|
+
for (const s of stmt.tryBlock.statements) {
|
|
784
|
+
if (tryEndBlock) {
|
|
785
|
+
tryEndBlock = processStatement(s, tryEndBlock);
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
// Create merge block after try-catch
|
|
789
|
+
const mergeBlockId = builder.generateBlockId('try_merge');
|
|
790
|
+
const mergeBlock = {
|
|
791
|
+
id: mergeBlockId,
|
|
792
|
+
type: 'basic',
|
|
793
|
+
label: 'try merge',
|
|
794
|
+
statements: [],
|
|
795
|
+
predecessors: [],
|
|
796
|
+
successors: [],
|
|
797
|
+
loopDepth,
|
|
798
|
+
location,
|
|
799
|
+
};
|
|
800
|
+
builder.addBlock(mergeBlock);
|
|
801
|
+
if (tryEndBlock) {
|
|
802
|
+
builder.addEdge({
|
|
803
|
+
id: builder.generateEdgeId('e'),
|
|
804
|
+
type: 'sequential',
|
|
805
|
+
source: tryEndBlock,
|
|
806
|
+
target: mergeBlockId,
|
|
807
|
+
isBackEdge: false,
|
|
808
|
+
});
|
|
809
|
+
}
|
|
810
|
+
// Process catch clause
|
|
811
|
+
if (stmt.catchClause) {
|
|
812
|
+
const catchBlockId = builder.generateBlockId('catch');
|
|
813
|
+
const catchBlock = {
|
|
814
|
+
id: catchBlockId,
|
|
815
|
+
type: 'catch',
|
|
816
|
+
label: 'catch',
|
|
817
|
+
statements: [],
|
|
818
|
+
predecessors: [],
|
|
819
|
+
successors: [],
|
|
820
|
+
loopDepth,
|
|
821
|
+
location: this.getSourceLocation(sourceFile, stmt.catchClause),
|
|
822
|
+
};
|
|
823
|
+
builder.addBlock(catchBlock);
|
|
824
|
+
// Exception edge from try to catch
|
|
825
|
+
builder.addEdge({
|
|
826
|
+
id: builder.generateEdgeId('e'),
|
|
827
|
+
type: 'exception',
|
|
828
|
+
source: tryBlockId,
|
|
829
|
+
target: catchBlockId,
|
|
830
|
+
isBackEdge: false,
|
|
831
|
+
});
|
|
832
|
+
let catchEndBlock = catchBlockId;
|
|
833
|
+
for (const s of stmt.catchClause.block.statements) {
|
|
834
|
+
if (catchEndBlock) {
|
|
835
|
+
catchEndBlock = processStatement(s, catchEndBlock);
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
if (catchEndBlock) {
|
|
839
|
+
builder.addEdge({
|
|
840
|
+
id: builder.generateEdgeId('e'),
|
|
841
|
+
type: 'sequential',
|
|
842
|
+
source: catchEndBlock,
|
|
843
|
+
target: mergeBlockId,
|
|
844
|
+
isBackEdge: false,
|
|
845
|
+
});
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
// Process finally clause
|
|
849
|
+
if (stmt.finallyBlock) {
|
|
850
|
+
const finallyBlockId = builder.generateBlockId('finally');
|
|
851
|
+
const finallyBlock = {
|
|
852
|
+
id: finallyBlockId,
|
|
853
|
+
type: 'finally',
|
|
854
|
+
label: 'finally',
|
|
855
|
+
statements: [],
|
|
856
|
+
predecessors: [],
|
|
857
|
+
successors: [],
|
|
858
|
+
loopDepth,
|
|
859
|
+
location: this.getSourceLocation(sourceFile, stmt.finallyBlock),
|
|
860
|
+
};
|
|
861
|
+
builder.addBlock(finallyBlock);
|
|
862
|
+
// Connect merge to finally
|
|
863
|
+
builder.addEdge({
|
|
864
|
+
id: builder.generateEdgeId('e'),
|
|
865
|
+
type: 'sequential',
|
|
866
|
+
source: mergeBlockId,
|
|
867
|
+
target: finallyBlockId,
|
|
868
|
+
isBackEdge: false,
|
|
869
|
+
});
|
|
870
|
+
let finallyEndBlock = finallyBlockId;
|
|
871
|
+
for (const s of stmt.finallyBlock.statements) {
|
|
872
|
+
if (finallyEndBlock) {
|
|
873
|
+
finallyEndBlock = processStatement(s, finallyEndBlock);
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
return finallyEndBlock;
|
|
877
|
+
}
|
|
878
|
+
return mergeBlockId;
|
|
879
|
+
}
|
|
880
|
+
getSourceLocation(sourceFile, node) {
|
|
881
|
+
const start = sourceFile.getLineAndCharacterOfPosition(node.getStart());
|
|
882
|
+
const end = sourceFile.getLineAndCharacterOfPosition(node.getEnd());
|
|
883
|
+
return {
|
|
884
|
+
filePath: sourceFile.fileName,
|
|
885
|
+
startLine: start.line + 1,
|
|
886
|
+
startColumn: start.character,
|
|
887
|
+
endLine: end.line + 1,
|
|
888
|
+
endColumn: end.character,
|
|
889
|
+
};
|
|
890
|
+
}
|
|
891
|
+
/**
|
|
892
|
+
* Create analyzer for a built graph
|
|
893
|
+
*/
|
|
894
|
+
createAnalyzer(graph) {
|
|
895
|
+
return new CFGAnalyzer(graph);
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
// Extend CFGBuilder prototype
|
|
899
|
+
CFGBuilder.prototype.getBlock = function (blockId) {
|
|
900
|
+
return this.blocks.get(blockId);
|
|
901
|
+
};
|
|
902
|
+
//# sourceMappingURL=index.js.map
|