@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.
@@ -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