driftdetect-core 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/analyzers/ast-analyzer.d.ts +251 -0
- package/dist/analyzers/ast-analyzer.d.ts.map +1 -0
- package/dist/analyzers/ast-analyzer.js +548 -0
- package/dist/analyzers/ast-analyzer.js.map +1 -0
- package/dist/analyzers/flow-analyzer.d.ts +241 -0
- package/dist/analyzers/flow-analyzer.d.ts.map +1 -0
- package/dist/analyzers/flow-analyzer.js +1219 -0
- package/dist/analyzers/flow-analyzer.js.map +1 -0
- package/dist/analyzers/index.d.ts +18 -0
- package/dist/analyzers/index.d.ts.map +1 -0
- package/dist/analyzers/index.js +19 -0
- package/dist/analyzers/index.js.map +1 -0
- package/dist/analyzers/semantic-analyzer.d.ts +252 -0
- package/dist/analyzers/semantic-analyzer.d.ts.map +1 -0
- package/dist/analyzers/semantic-analyzer.js +1182 -0
- package/dist/analyzers/semantic-analyzer.js.map +1 -0
- package/dist/analyzers/type-analyzer.d.ts +289 -0
- package/dist/analyzers/type-analyzer.d.ts.map +1 -0
- package/dist/analyzers/type-analyzer.js +1269 -0
- package/dist/analyzers/type-analyzer.js.map +1 -0
- package/dist/analyzers/types.d.ts +537 -0
- package/dist/analyzers/types.d.ts.map +1 -0
- package/dist/analyzers/types.js +11 -0
- package/dist/analyzers/types.js.map +1 -0
- package/dist/config/config-loader.d.ts +166 -0
- package/dist/config/config-loader.d.ts.map +1 -0
- package/dist/config/config-loader.js +429 -0
- package/dist/config/config-loader.js.map +1 -0
- package/dist/config/config-validator.d.ts +204 -0
- package/dist/config/config-validator.d.ts.map +1 -0
- package/dist/config/config-validator.js +632 -0
- package/dist/config/config-validator.js.map +1 -0
- package/dist/config/defaults.d.ts +8 -0
- package/dist/config/defaults.d.ts.map +1 -0
- package/dist/config/defaults.js +26 -0
- package/dist/config/defaults.js.map +1 -0
- package/dist/config/index.d.ts +10 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +10 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config/types.d.ts +47 -0
- package/dist/config/types.d.ts.map +1 -0
- package/dist/config/types.js +7 -0
- package/dist/config/types.js.map +1 -0
- package/dist/index.d.ts +37 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +39 -0
- package/dist/index.js.map +1 -0
- package/dist/manifest/exporter.d.ts +21 -0
- package/dist/manifest/exporter.d.ts.map +1 -0
- package/dist/manifest/exporter.js +339 -0
- package/dist/manifest/exporter.js.map +1 -0
- package/dist/manifest/index.d.ts +14 -0
- package/dist/manifest/index.d.ts.map +1 -0
- package/dist/manifest/index.js +15 -0
- package/dist/manifest/index.js.map +1 -0
- package/dist/manifest/manifest-store.d.ts +111 -0
- package/dist/manifest/manifest-store.d.ts.map +1 -0
- package/dist/manifest/manifest-store.js +418 -0
- package/dist/manifest/manifest-store.js.map +1 -0
- package/dist/manifest/types.d.ts +238 -0
- package/dist/manifest/types.d.ts.map +1 -0
- package/dist/manifest/types.js +11 -0
- package/dist/manifest/types.js.map +1 -0
- package/dist/matcher/confidence-scorer.d.ts +188 -0
- package/dist/matcher/confidence-scorer.d.ts.map +1 -0
- package/dist/matcher/confidence-scorer.js +302 -0
- package/dist/matcher/confidence-scorer.js.map +1 -0
- package/dist/matcher/index.d.ts +24 -0
- package/dist/matcher/index.d.ts.map +1 -0
- package/dist/matcher/index.js +26 -0
- package/dist/matcher/index.js.map +1 -0
- package/dist/matcher/outlier-detector.d.ts +252 -0
- package/dist/matcher/outlier-detector.d.ts.map +1 -0
- package/dist/matcher/outlier-detector.js +544 -0
- package/dist/matcher/outlier-detector.js.map +1 -0
- package/dist/matcher/pattern-matcher.d.ts +169 -0
- package/dist/matcher/pattern-matcher.d.ts.map +1 -0
- package/dist/matcher/pattern-matcher.js +692 -0
- package/dist/matcher/pattern-matcher.js.map +1 -0
- package/dist/matcher/types.d.ts +476 -0
- package/dist/matcher/types.d.ts.map +1 -0
- package/dist/matcher/types.js +36 -0
- package/dist/matcher/types.js.map +1 -0
- package/dist/parsers/base-parser.d.ts +282 -0
- package/dist/parsers/base-parser.d.ts.map +1 -0
- package/dist/parsers/base-parser.js +421 -0
- package/dist/parsers/base-parser.js.map +1 -0
- package/dist/parsers/css-parser.d.ts +225 -0
- package/dist/parsers/css-parser.d.ts.map +1 -0
- package/dist/parsers/css-parser.js +477 -0
- package/dist/parsers/css-parser.js.map +1 -0
- package/dist/parsers/index.d.ts +15 -0
- package/dist/parsers/index.d.ts.map +1 -0
- package/dist/parsers/index.js +15 -0
- package/dist/parsers/index.js.map +1 -0
- package/dist/parsers/json-parser.d.ts +219 -0
- package/dist/parsers/json-parser.d.ts.map +1 -0
- package/dist/parsers/json-parser.js +602 -0
- package/dist/parsers/json-parser.js.map +1 -0
- package/dist/parsers/markdown-parser.d.ts +276 -0
- package/dist/parsers/markdown-parser.d.ts.map +1 -0
- package/dist/parsers/markdown-parser.js +731 -0
- package/dist/parsers/markdown-parser.js.map +1 -0
- package/dist/parsers/parser-manager.d.ts +294 -0
- package/dist/parsers/parser-manager.d.ts.map +1 -0
- package/dist/parsers/parser-manager.js +738 -0
- package/dist/parsers/parser-manager.js.map +1 -0
- package/dist/parsers/python-parser.d.ts +204 -0
- package/dist/parsers/python-parser.d.ts.map +1 -0
- package/dist/parsers/python-parser.js +517 -0
- package/dist/parsers/python-parser.js.map +1 -0
- package/dist/parsers/types.d.ts +43 -0
- package/dist/parsers/types.d.ts.map +1 -0
- package/dist/parsers/types.js +7 -0
- package/dist/parsers/types.js.map +1 -0
- package/dist/parsers/typescript-parser.d.ts +264 -0
- package/dist/parsers/typescript-parser.d.ts.map +1 -0
- package/dist/parsers/typescript-parser.js +658 -0
- package/dist/parsers/typescript-parser.js.map +1 -0
- package/dist/rules/evaluator.d.ts +305 -0
- package/dist/rules/evaluator.d.ts.map +1 -0
- package/dist/rules/evaluator.js +579 -0
- package/dist/rules/evaluator.js.map +1 -0
- package/dist/rules/index.d.ts +13 -0
- package/dist/rules/index.d.ts.map +1 -0
- package/dist/rules/index.js +13 -0
- package/dist/rules/index.js.map +1 -0
- package/dist/rules/quick-fix-generator.d.ts +334 -0
- package/dist/rules/quick-fix-generator.d.ts.map +1 -0
- package/dist/rules/quick-fix-generator.js +1075 -0
- package/dist/rules/quick-fix-generator.js.map +1 -0
- package/dist/rules/rule-engine.d.ts +241 -0
- package/dist/rules/rule-engine.d.ts.map +1 -0
- package/dist/rules/rule-engine.js +585 -0
- package/dist/rules/rule-engine.js.map +1 -0
- package/dist/rules/severity-manager.d.ts +394 -0
- package/dist/rules/severity-manager.d.ts.map +1 -0
- package/dist/rules/severity-manager.js +619 -0
- package/dist/rules/severity-manager.js.map +1 -0
- package/dist/rules/types.d.ts +370 -0
- package/dist/rules/types.d.ts.map +1 -0
- package/dist/rules/types.js +133 -0
- package/dist/rules/types.js.map +1 -0
- package/dist/rules/variant-manager.d.ts +388 -0
- package/dist/rules/variant-manager.d.ts.map +1 -0
- package/dist/rules/variant-manager.js +777 -0
- package/dist/rules/variant-manager.js.map +1 -0
- package/dist/scanner/change-detector.d.ts +164 -0
- package/dist/scanner/change-detector.d.ts.map +1 -0
- package/dist/scanner/change-detector.js +263 -0
- package/dist/scanner/change-detector.js.map +1 -0
- package/dist/scanner/dependency-graph.d.ts +270 -0
- package/dist/scanner/dependency-graph.d.ts.map +1 -0
- package/dist/scanner/dependency-graph.js +436 -0
- package/dist/scanner/dependency-graph.js.map +1 -0
- package/dist/scanner/file-walker.d.ts +127 -0
- package/dist/scanner/file-walker.d.ts.map +1 -0
- package/dist/scanner/file-walker.js +526 -0
- package/dist/scanner/file-walker.js.map +1 -0
- package/dist/scanner/index.d.ts +12 -0
- package/dist/scanner/index.d.ts.map +1 -0
- package/dist/scanner/index.js +12 -0
- package/dist/scanner/index.js.map +1 -0
- package/dist/scanner/types.d.ts +218 -0
- package/dist/scanner/types.d.ts.map +1 -0
- package/dist/scanner/types.js +10 -0
- package/dist/scanner/types.js.map +1 -0
- package/dist/scanner/worker-pool.d.ts +317 -0
- package/dist/scanner/worker-pool.d.ts.map +1 -0
- package/dist/scanner/worker-pool.js +571 -0
- package/dist/scanner/worker-pool.js.map +1 -0
- package/dist/store/cache-manager.d.ts +179 -0
- package/dist/store/cache-manager.d.ts.map +1 -0
- package/dist/store/cache-manager.js +391 -0
- package/dist/store/cache-manager.js.map +1 -0
- package/dist/store/history-store.d.ts +314 -0
- package/dist/store/history-store.d.ts.map +1 -0
- package/dist/store/history-store.js +707 -0
- package/dist/store/history-store.js.map +1 -0
- package/dist/store/index.d.ts +20 -0
- package/dist/store/index.d.ts.map +1 -0
- package/dist/store/index.js +26 -0
- package/dist/store/index.js.map +1 -0
- package/dist/store/lock-file-manager.d.ts +202 -0
- package/dist/store/lock-file-manager.d.ts.map +1 -0
- package/dist/store/lock-file-manager.js +475 -0
- package/dist/store/lock-file-manager.js.map +1 -0
- package/dist/store/pattern-store.d.ts +289 -0
- package/dist/store/pattern-store.d.ts.map +1 -0
- package/dist/store/pattern-store.js +936 -0
- package/dist/store/pattern-store.js.map +1 -0
- package/dist/store/schema-validator.d.ts +159 -0
- package/dist/store/schema-validator.d.ts.map +1 -0
- package/dist/store/schema-validator.js +1096 -0
- package/dist/store/schema-validator.js.map +1 -0
- package/dist/store/types.d.ts +585 -0
- package/dist/store/types.d.ts.map +1 -0
- package/dist/store/types.js +82 -0
- package/dist/store/types.js.map +1 -0
- package/dist/types/analysis.d.ts +19 -0
- package/dist/types/analysis.d.ts.map +1 -0
- package/dist/types/analysis.js +5 -0
- package/dist/types/analysis.js.map +1 -0
- package/dist/types/common.d.ts +7 -0
- package/dist/types/common.d.ts.map +1 -0
- package/dist/types/common.js +5 -0
- package/dist/types/common.js.map +1 -0
- package/dist/types/index.d.ts +12 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +10 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/patterns.d.ts +40 -0
- package/dist/types/patterns.d.ts.map +1 -0
- package/dist/types/patterns.js +7 -0
- package/dist/types/patterns.js.map +1 -0
- package/dist/types/violations.d.ts +7 -0
- package/dist/types/violations.d.ts.map +1 -0
- package/dist/types/violations.js +7 -0
- package/dist/types/violations.js.map +1 -0
- package/package.json +46 -0
|
@@ -0,0 +1,1219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Flow Analyzer - Control flow and data flow analysis
|
|
3
|
+
*
|
|
4
|
+
* Provides control flow graph (CFG) construction from AST,
|
|
5
|
+
* data flow analysis (reads, writes, captures), detection of
|
|
6
|
+
* unreachable code, infinite loops, missing return statements,
|
|
7
|
+
* null/undefined dereferences, unused variables, and uninitialized reads.
|
|
8
|
+
*
|
|
9
|
+
* @requirements 3.5 - Parser SHALL provide a unified AST query interface across all languages
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Flow Analyzer class for control flow and data flow analysis.
|
|
13
|
+
*
|
|
14
|
+
* Provides a unified interface for analyzing control flow and data flow
|
|
15
|
+
* across TypeScript and JavaScript code.
|
|
16
|
+
*
|
|
17
|
+
* @requirements 3.5 - Unified AST query interface across all languages
|
|
18
|
+
*/
|
|
19
|
+
export class FlowAnalyzer {
|
|
20
|
+
/** Counter for generating unique node IDs */
|
|
21
|
+
nodeIdCounter = 0;
|
|
22
|
+
/** All CFG nodes during construction */
|
|
23
|
+
nodes = new Map();
|
|
24
|
+
/** All CFG edges during construction */
|
|
25
|
+
edges = [];
|
|
26
|
+
/** Entry node ID */
|
|
27
|
+
entryNodeId = null;
|
|
28
|
+
/** Exit node IDs */
|
|
29
|
+
exitNodeIds = new Set();
|
|
30
|
+
/** Unreachable code locations */
|
|
31
|
+
unreachableCode = [];
|
|
32
|
+
/** Infinite loop locations */
|
|
33
|
+
infiniteLoops = [];
|
|
34
|
+
/** Missing return locations */
|
|
35
|
+
missingReturns = [];
|
|
36
|
+
/** Current analysis options */
|
|
37
|
+
options = {};
|
|
38
|
+
/**
|
|
39
|
+
* Analyze an AST and produce flow analysis results.
|
|
40
|
+
*
|
|
41
|
+
* @param ast - The AST to analyze
|
|
42
|
+
* @param options - Analysis options
|
|
43
|
+
* @returns Flow analysis result
|
|
44
|
+
*/
|
|
45
|
+
analyze(ast, options = {}) {
|
|
46
|
+
// Reset state for fresh analysis
|
|
47
|
+
this.reset();
|
|
48
|
+
this.options = {
|
|
49
|
+
detectUnreachable: true,
|
|
50
|
+
detectInfiniteLoops: true,
|
|
51
|
+
detectMissingReturns: true,
|
|
52
|
+
detectNullDereferences: true,
|
|
53
|
+
detectUnusedVariables: true,
|
|
54
|
+
detectUninitializedReads: true,
|
|
55
|
+
...options,
|
|
56
|
+
};
|
|
57
|
+
// Create entry and exit nodes
|
|
58
|
+
const entryNode = this.createNode('entry', {
|
|
59
|
+
start: ast.rootNode.startPosition,
|
|
60
|
+
end: ast.rootNode.startPosition,
|
|
61
|
+
});
|
|
62
|
+
this.entryNodeId = entryNode.id;
|
|
63
|
+
const exitNode = this.createNode('exit', {
|
|
64
|
+
start: ast.rootNode.endPosition,
|
|
65
|
+
end: ast.rootNode.endPosition,
|
|
66
|
+
});
|
|
67
|
+
this.exitNodeIds.add(exitNode.id);
|
|
68
|
+
// Create initial flow context
|
|
69
|
+
const context = this.createFlowContext(null);
|
|
70
|
+
// Build CFG from AST
|
|
71
|
+
const lastNodes = this.buildCFG(ast.rootNode, [entryNode.id], context);
|
|
72
|
+
// Connect remaining nodes to exit
|
|
73
|
+
for (const nodeId of lastNodes) {
|
|
74
|
+
this.addEdge(nodeId, exitNode.id);
|
|
75
|
+
}
|
|
76
|
+
// Mark reachable nodes
|
|
77
|
+
this.markReachableNodes();
|
|
78
|
+
// Detect unreachable code
|
|
79
|
+
if (this.options.detectUnreachable) {
|
|
80
|
+
this.detectUnreachableCode();
|
|
81
|
+
}
|
|
82
|
+
// Analyze data flow
|
|
83
|
+
const dataFlow = this.analyzeDataFlow(ast.rootNode, context);
|
|
84
|
+
// Build result
|
|
85
|
+
return this.buildResult(dataFlow);
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Analyze a single function/method for flow.
|
|
89
|
+
*
|
|
90
|
+
* @param node - The function AST node
|
|
91
|
+
* @param options - Analysis options
|
|
92
|
+
* @returns Flow analysis result for the function
|
|
93
|
+
*/
|
|
94
|
+
analyzeFunction(node, options = {}) {
|
|
95
|
+
// Reset state for fresh analysis
|
|
96
|
+
this.reset();
|
|
97
|
+
this.options = {
|
|
98
|
+
detectUnreachable: true,
|
|
99
|
+
detectInfiniteLoops: true,
|
|
100
|
+
detectMissingReturns: true,
|
|
101
|
+
detectNullDereferences: true,
|
|
102
|
+
detectUnusedVariables: true,
|
|
103
|
+
detectUninitializedReads: true,
|
|
104
|
+
...options,
|
|
105
|
+
};
|
|
106
|
+
// Create entry and exit nodes
|
|
107
|
+
const entryNode = this.createNode('entry', {
|
|
108
|
+
start: node.startPosition,
|
|
109
|
+
end: node.startPosition,
|
|
110
|
+
});
|
|
111
|
+
this.entryNodeId = entryNode.id;
|
|
112
|
+
const exitNode = this.createNode('exit', {
|
|
113
|
+
start: node.endPosition,
|
|
114
|
+
end: node.endPosition,
|
|
115
|
+
});
|
|
116
|
+
this.exitNodeIds.add(exitNode.id);
|
|
117
|
+
// Create initial flow context
|
|
118
|
+
const context = this.createFlowContext(null);
|
|
119
|
+
// Add function parameters to context
|
|
120
|
+
this.collectFunctionParameters(node, context);
|
|
121
|
+
// Find function body
|
|
122
|
+
const body = this.findChildByType(node, 'statement_block') ||
|
|
123
|
+
this.findChildByType(node, 'BlockStatement');
|
|
124
|
+
if (body) {
|
|
125
|
+
// Build CFG from function body
|
|
126
|
+
const lastNodes = this.buildCFG(body, [entryNode.id], context);
|
|
127
|
+
// Connect remaining nodes to exit (implicit return)
|
|
128
|
+
for (const nodeId of lastNodes) {
|
|
129
|
+
this.addEdge(nodeId, exitNode.id);
|
|
130
|
+
}
|
|
131
|
+
// Check for missing returns if function has return type
|
|
132
|
+
if (this.options.detectMissingReturns) {
|
|
133
|
+
this.checkMissingReturns(node, lastNodes);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
// Expression body (arrow function)
|
|
138
|
+
const lastNodes = this.buildCFG(node, [entryNode.id], context);
|
|
139
|
+
for (const nodeId of lastNodes) {
|
|
140
|
+
this.addEdge(nodeId, exitNode.id);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
// Mark reachable nodes
|
|
144
|
+
this.markReachableNodes();
|
|
145
|
+
// Detect unreachable code
|
|
146
|
+
if (this.options.detectUnreachable) {
|
|
147
|
+
this.detectUnreachableCode();
|
|
148
|
+
}
|
|
149
|
+
// Analyze data flow
|
|
150
|
+
const dataFlow = this.analyzeDataFlow(node, context);
|
|
151
|
+
// Build result
|
|
152
|
+
return this.buildResult(dataFlow);
|
|
153
|
+
}
|
|
154
|
+
// ============================================
|
|
155
|
+
// CFG Construction Methods
|
|
156
|
+
// ============================================
|
|
157
|
+
/**
|
|
158
|
+
* Build CFG from an AST node.
|
|
159
|
+
* Returns the IDs of nodes that flow out of this construct.
|
|
160
|
+
*/
|
|
161
|
+
buildCFG(node, predecessors, context) {
|
|
162
|
+
const nodeType = node.type;
|
|
163
|
+
switch (nodeType) {
|
|
164
|
+
case 'program':
|
|
165
|
+
case 'Program':
|
|
166
|
+
case 'statement_block':
|
|
167
|
+
case 'BlockStatement':
|
|
168
|
+
return this.buildBlockCFG(node, predecessors, context);
|
|
169
|
+
case 'if_statement':
|
|
170
|
+
case 'IfStatement':
|
|
171
|
+
return this.buildIfCFG(node, predecessors, context);
|
|
172
|
+
case 'for_statement':
|
|
173
|
+
case 'ForStatement':
|
|
174
|
+
return this.buildForCFG(node, predecessors, context);
|
|
175
|
+
case 'for_in_statement':
|
|
176
|
+
case 'ForInStatement':
|
|
177
|
+
case 'for_of_statement':
|
|
178
|
+
case 'ForOfStatement':
|
|
179
|
+
return this.buildForInOfCFG(node, predecessors, context);
|
|
180
|
+
case 'while_statement':
|
|
181
|
+
case 'WhileStatement':
|
|
182
|
+
return this.buildWhileCFG(node, predecessors, context);
|
|
183
|
+
case 'do_statement':
|
|
184
|
+
case 'DoWhileStatement':
|
|
185
|
+
return this.buildDoWhileCFG(node, predecessors, context);
|
|
186
|
+
case 'switch_statement':
|
|
187
|
+
case 'SwitchStatement':
|
|
188
|
+
return this.buildSwitchCFG(node, predecessors, context);
|
|
189
|
+
case 'try_statement':
|
|
190
|
+
case 'TryStatement':
|
|
191
|
+
return this.buildTryCFG(node, predecessors, context);
|
|
192
|
+
case 'return_statement':
|
|
193
|
+
case 'ReturnStatement':
|
|
194
|
+
return this.buildReturnCFG(node, predecessors, context);
|
|
195
|
+
case 'throw_statement':
|
|
196
|
+
case 'ThrowStatement':
|
|
197
|
+
return this.buildThrowCFG(node, predecessors, context);
|
|
198
|
+
case 'break_statement':
|
|
199
|
+
case 'BreakStatement':
|
|
200
|
+
return this.buildBreakCFG(node, predecessors, context);
|
|
201
|
+
case 'continue_statement':
|
|
202
|
+
case 'ContinueStatement':
|
|
203
|
+
return this.buildContinueCFG(node, predecessors, context);
|
|
204
|
+
case 'expression_statement':
|
|
205
|
+
case 'ExpressionStatement':
|
|
206
|
+
case 'variable_declaration':
|
|
207
|
+
case 'VariableDeclaration':
|
|
208
|
+
case 'lexical_declaration':
|
|
209
|
+
return this.buildStatementCFG(node, predecessors, context);
|
|
210
|
+
default:
|
|
211
|
+
// For other nodes, create a statement node and continue
|
|
212
|
+
if (this.isStatement(node)) {
|
|
213
|
+
return this.buildStatementCFG(node, predecessors, context);
|
|
214
|
+
}
|
|
215
|
+
// For non-statements, just pass through
|
|
216
|
+
return predecessors;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Build CFG for a block of statements.
|
|
221
|
+
*/
|
|
222
|
+
buildBlockCFG(node, predecessors, context) {
|
|
223
|
+
let currentPreds = predecessors;
|
|
224
|
+
for (const child of node.children) {
|
|
225
|
+
if (this.isStatement(child)) {
|
|
226
|
+
currentPreds = this.buildCFG(child, currentPreds, context);
|
|
227
|
+
// If no predecessors, remaining statements are unreachable
|
|
228
|
+
if (currentPreds.length === 0) {
|
|
229
|
+
break;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return currentPreds;
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Build CFG for an if statement.
|
|
237
|
+
*/
|
|
238
|
+
buildIfCFG(node, predecessors, context) {
|
|
239
|
+
// Create branch node for the condition
|
|
240
|
+
const branchNode = this.createNode('branch', {
|
|
241
|
+
start: node.startPosition,
|
|
242
|
+
end: node.startPosition,
|
|
243
|
+
}, node);
|
|
244
|
+
// Connect predecessors to branch
|
|
245
|
+
for (const pred of predecessors) {
|
|
246
|
+
this.addEdge(pred, branchNode.id);
|
|
247
|
+
}
|
|
248
|
+
// Find consequence and alternative
|
|
249
|
+
const consequence = this.findChildByType(node, 'statement_block') ||
|
|
250
|
+
this.findChildByType(node, 'BlockStatement') ||
|
|
251
|
+
node.children.find(c => this.isStatement(c) && c.type !== 'else_clause');
|
|
252
|
+
const elseClause = this.findChildByType(node, 'else_clause');
|
|
253
|
+
const alternative = elseClause?.children.find(c => this.isStatement(c));
|
|
254
|
+
const exitNodes = [];
|
|
255
|
+
// Build true branch
|
|
256
|
+
if (consequence) {
|
|
257
|
+
const trueExits = this.buildCFG(consequence, [branchNode.id], context);
|
|
258
|
+
exitNodes.push(...trueExits);
|
|
259
|
+
}
|
|
260
|
+
else {
|
|
261
|
+
exitNodes.push(branchNode.id);
|
|
262
|
+
}
|
|
263
|
+
// Build false branch
|
|
264
|
+
if (alternative) {
|
|
265
|
+
const falseExits = this.buildCFG(alternative, [branchNode.id], context);
|
|
266
|
+
exitNodes.push(...falseExits);
|
|
267
|
+
}
|
|
268
|
+
else {
|
|
269
|
+
exitNodes.push(branchNode.id);
|
|
270
|
+
}
|
|
271
|
+
// Create merge node if there are multiple exits
|
|
272
|
+
if (exitNodes.length > 1) {
|
|
273
|
+
const mergeNode = this.createNode('merge', {
|
|
274
|
+
start: node.endPosition,
|
|
275
|
+
end: node.endPosition,
|
|
276
|
+
});
|
|
277
|
+
for (const exit of exitNodes) {
|
|
278
|
+
this.addEdge(exit, mergeNode.id);
|
|
279
|
+
}
|
|
280
|
+
return [mergeNode.id];
|
|
281
|
+
}
|
|
282
|
+
return exitNodes;
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Build CFG for a for statement.
|
|
286
|
+
*/
|
|
287
|
+
buildForCFG(node, predecessors, context) {
|
|
288
|
+
// Create loop entry node
|
|
289
|
+
const loopNode = this.createNode('loop', {
|
|
290
|
+
start: node.startPosition,
|
|
291
|
+
end: node.startPosition,
|
|
292
|
+
}, node);
|
|
293
|
+
// Create exit node for break statements
|
|
294
|
+
const exitNode = this.createNode('merge', {
|
|
295
|
+
start: node.endPosition,
|
|
296
|
+
end: node.endPosition,
|
|
297
|
+
});
|
|
298
|
+
// Update context for loop
|
|
299
|
+
const loopContext = {
|
|
300
|
+
...context,
|
|
301
|
+
inLoop: true,
|
|
302
|
+
loopEntry: loopNode.id,
|
|
303
|
+
loopExit: exitNode.id,
|
|
304
|
+
};
|
|
305
|
+
// Connect predecessors to loop entry
|
|
306
|
+
for (const pred of predecessors) {
|
|
307
|
+
this.addEdge(pred, loopNode.id);
|
|
308
|
+
}
|
|
309
|
+
// Find loop body
|
|
310
|
+
const body = this.findChildByType(node, 'statement_block') ||
|
|
311
|
+
this.findChildByType(node, 'BlockStatement') ||
|
|
312
|
+
node.children.find(c => this.isStatement(c));
|
|
313
|
+
// Build loop body
|
|
314
|
+
let bodyExits = [loopNode.id];
|
|
315
|
+
if (body) {
|
|
316
|
+
bodyExits = this.buildCFG(body, [loopNode.id], loopContext);
|
|
317
|
+
}
|
|
318
|
+
// Connect body exits back to loop (back edge)
|
|
319
|
+
for (const exit of bodyExits) {
|
|
320
|
+
this.addEdge(exit, loopNode.id, undefined, true);
|
|
321
|
+
}
|
|
322
|
+
// Connect loop to exit (condition false)
|
|
323
|
+
this.addEdge(loopNode.id, exitNode.id);
|
|
324
|
+
// Check for infinite loop (no exit path from body)
|
|
325
|
+
if (this.options.detectInfiniteLoops) {
|
|
326
|
+
this.checkInfiniteLoop(node, bodyExits);
|
|
327
|
+
}
|
|
328
|
+
return [exitNode.id];
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* Build CFG for a for-in or for-of statement.
|
|
332
|
+
*/
|
|
333
|
+
buildForInOfCFG(node, predecessors, context) {
|
|
334
|
+
// Similar to for loop
|
|
335
|
+
const loopNode = this.createNode('loop', {
|
|
336
|
+
start: node.startPosition,
|
|
337
|
+
end: node.startPosition,
|
|
338
|
+
}, node);
|
|
339
|
+
const exitNode = this.createNode('merge', {
|
|
340
|
+
start: node.endPosition,
|
|
341
|
+
end: node.endPosition,
|
|
342
|
+
});
|
|
343
|
+
const loopContext = {
|
|
344
|
+
...context,
|
|
345
|
+
inLoop: true,
|
|
346
|
+
loopEntry: loopNode.id,
|
|
347
|
+
loopExit: exitNode.id,
|
|
348
|
+
};
|
|
349
|
+
for (const pred of predecessors) {
|
|
350
|
+
this.addEdge(pred, loopNode.id);
|
|
351
|
+
}
|
|
352
|
+
const body = this.findChildByType(node, 'statement_block') ||
|
|
353
|
+
this.findChildByType(node, 'BlockStatement') ||
|
|
354
|
+
node.children.find(c => this.isStatement(c));
|
|
355
|
+
let bodyExits = [loopNode.id];
|
|
356
|
+
if (body) {
|
|
357
|
+
bodyExits = this.buildCFG(body, [loopNode.id], loopContext);
|
|
358
|
+
}
|
|
359
|
+
for (const exit of bodyExits) {
|
|
360
|
+
this.addEdge(exit, loopNode.id, undefined, true);
|
|
361
|
+
}
|
|
362
|
+
this.addEdge(loopNode.id, exitNode.id);
|
|
363
|
+
return [exitNode.id];
|
|
364
|
+
}
|
|
365
|
+
/**
|
|
366
|
+
* Build CFG for a while statement.
|
|
367
|
+
*/
|
|
368
|
+
buildWhileCFG(node, predecessors, context) {
|
|
369
|
+
const loopNode = this.createNode('loop', {
|
|
370
|
+
start: node.startPosition,
|
|
371
|
+
end: node.startPosition,
|
|
372
|
+
}, node);
|
|
373
|
+
const exitNode = this.createNode('merge', {
|
|
374
|
+
start: node.endPosition,
|
|
375
|
+
end: node.endPosition,
|
|
376
|
+
});
|
|
377
|
+
const loopContext = {
|
|
378
|
+
...context,
|
|
379
|
+
inLoop: true,
|
|
380
|
+
loopEntry: loopNode.id,
|
|
381
|
+
loopExit: exitNode.id,
|
|
382
|
+
};
|
|
383
|
+
for (const pred of predecessors) {
|
|
384
|
+
this.addEdge(pred, loopNode.id);
|
|
385
|
+
}
|
|
386
|
+
const body = this.findChildByType(node, 'statement_block') ||
|
|
387
|
+
this.findChildByType(node, 'BlockStatement') ||
|
|
388
|
+
node.children.find(c => this.isStatement(c));
|
|
389
|
+
let bodyExits = [loopNode.id];
|
|
390
|
+
if (body) {
|
|
391
|
+
bodyExits = this.buildCFG(body, [loopNode.id], loopContext);
|
|
392
|
+
}
|
|
393
|
+
for (const exit of bodyExits) {
|
|
394
|
+
this.addEdge(exit, loopNode.id, undefined, true);
|
|
395
|
+
}
|
|
396
|
+
this.addEdge(loopNode.id, exitNode.id);
|
|
397
|
+
// Check for infinite loop
|
|
398
|
+
if (this.options.detectInfiniteLoops) {
|
|
399
|
+
this.checkInfiniteLoop(node, bodyExits);
|
|
400
|
+
}
|
|
401
|
+
return [exitNode.id];
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* Build CFG for a do-while statement.
|
|
405
|
+
*/
|
|
406
|
+
buildDoWhileCFG(node, predecessors, context) {
|
|
407
|
+
const loopNode = this.createNode('loop', {
|
|
408
|
+
start: node.startPosition,
|
|
409
|
+
end: node.startPosition,
|
|
410
|
+
}, node);
|
|
411
|
+
const exitNode = this.createNode('merge', {
|
|
412
|
+
start: node.endPosition,
|
|
413
|
+
end: node.endPosition,
|
|
414
|
+
});
|
|
415
|
+
const loopContext = {
|
|
416
|
+
...context,
|
|
417
|
+
inLoop: true,
|
|
418
|
+
loopEntry: loopNode.id,
|
|
419
|
+
loopExit: exitNode.id,
|
|
420
|
+
};
|
|
421
|
+
// Connect predecessors directly to body (do-while executes at least once)
|
|
422
|
+
for (const pred of predecessors) {
|
|
423
|
+
this.addEdge(pred, loopNode.id);
|
|
424
|
+
}
|
|
425
|
+
const body = this.findChildByType(node, 'statement_block') ||
|
|
426
|
+
this.findChildByType(node, 'BlockStatement') ||
|
|
427
|
+
node.children.find(c => this.isStatement(c));
|
|
428
|
+
let bodyExits = [loopNode.id];
|
|
429
|
+
if (body) {
|
|
430
|
+
bodyExits = this.buildCFG(body, [loopNode.id], loopContext);
|
|
431
|
+
}
|
|
432
|
+
// Body exits go to condition check, which can loop back or exit
|
|
433
|
+
for (const exit of bodyExits) {
|
|
434
|
+
this.addEdge(exit, loopNode.id, undefined, true);
|
|
435
|
+
}
|
|
436
|
+
this.addEdge(loopNode.id, exitNode.id);
|
|
437
|
+
return [exitNode.id];
|
|
438
|
+
}
|
|
439
|
+
/**
|
|
440
|
+
* Build CFG for a switch statement.
|
|
441
|
+
*/
|
|
442
|
+
buildSwitchCFG(node, predecessors, context) {
|
|
443
|
+
const branchNode = this.createNode('branch', {
|
|
444
|
+
start: node.startPosition,
|
|
445
|
+
end: node.startPosition,
|
|
446
|
+
}, node);
|
|
447
|
+
const exitNode = this.createNode('merge', {
|
|
448
|
+
start: node.endPosition,
|
|
449
|
+
end: node.endPosition,
|
|
450
|
+
});
|
|
451
|
+
const switchContext = {
|
|
452
|
+
...context,
|
|
453
|
+
switchExit: exitNode.id,
|
|
454
|
+
};
|
|
455
|
+
for (const pred of predecessors) {
|
|
456
|
+
this.addEdge(pred, branchNode.id);
|
|
457
|
+
}
|
|
458
|
+
// Find switch body
|
|
459
|
+
const switchBody = this.findChildByType(node, 'switch_body') ||
|
|
460
|
+
node.children.find(c => c.type === 'switch_body' || c.type === 'SwitchCase');
|
|
461
|
+
const caseExits = [];
|
|
462
|
+
let hasDefault = false;
|
|
463
|
+
let fallthrough = [branchNode.id];
|
|
464
|
+
if (switchBody) {
|
|
465
|
+
for (const caseNode of switchBody.children) {
|
|
466
|
+
if (caseNode.type === 'switch_case' || caseNode.type === 'SwitchCase' ||
|
|
467
|
+
caseNode.type === 'switch_default' || caseNode.type === 'default') {
|
|
468
|
+
if (caseNode.type === 'switch_default' || caseNode.type === 'default') {
|
|
469
|
+
hasDefault = true;
|
|
470
|
+
}
|
|
471
|
+
// Build case body
|
|
472
|
+
const caseBody = caseNode.children.filter(c => this.isStatement(c));
|
|
473
|
+
let caseCurrentPreds = [...fallthrough, branchNode.id];
|
|
474
|
+
for (const stmt of caseBody) {
|
|
475
|
+
caseCurrentPreds = this.buildCFG(stmt, caseCurrentPreds, switchContext);
|
|
476
|
+
if (caseCurrentPreds.length === 0)
|
|
477
|
+
break;
|
|
478
|
+
}
|
|
479
|
+
// Fallthrough to next case
|
|
480
|
+
fallthrough = caseCurrentPreds;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
// Connect remaining fallthrough to exit
|
|
485
|
+
caseExits.push(...fallthrough);
|
|
486
|
+
// If no default, branch can go directly to exit
|
|
487
|
+
if (!hasDefault) {
|
|
488
|
+
this.addEdge(branchNode.id, exitNode.id);
|
|
489
|
+
}
|
|
490
|
+
for (const exit of caseExits) {
|
|
491
|
+
this.addEdge(exit, exitNode.id);
|
|
492
|
+
}
|
|
493
|
+
return [exitNode.id];
|
|
494
|
+
}
|
|
495
|
+
/**
|
|
496
|
+
* Build CFG for a try statement.
|
|
497
|
+
*/
|
|
498
|
+
buildTryCFG(node, predecessors, context) {
|
|
499
|
+
const tryContext = {
|
|
500
|
+
...context,
|
|
501
|
+
inTry: true,
|
|
502
|
+
};
|
|
503
|
+
const exitNodes = [];
|
|
504
|
+
// Find try block
|
|
505
|
+
const tryBlock = this.findChildByType(node, 'statement_block') ||
|
|
506
|
+
this.findChildByType(node, 'BlockStatement');
|
|
507
|
+
// Build try block
|
|
508
|
+
let tryExits = predecessors;
|
|
509
|
+
if (tryBlock) {
|
|
510
|
+
tryExits = this.buildCFG(tryBlock, predecessors, tryContext);
|
|
511
|
+
}
|
|
512
|
+
exitNodes.push(...tryExits);
|
|
513
|
+
// Find catch clause
|
|
514
|
+
const catchClause = this.findChildByType(node, 'catch_clause') ||
|
|
515
|
+
this.findChildByType(node, 'CatchClause');
|
|
516
|
+
if (catchClause) {
|
|
517
|
+
// Catch can be entered from any point in try block
|
|
518
|
+
const catchExits = this.buildCFG(catchClause, predecessors, context);
|
|
519
|
+
exitNodes.push(...catchExits);
|
|
520
|
+
}
|
|
521
|
+
// Find finally clause
|
|
522
|
+
const finallyClause = this.findChildByType(node, 'finally_clause') ||
|
|
523
|
+
this.findChildByType(node, 'FinallyClause');
|
|
524
|
+
if (finallyClause) {
|
|
525
|
+
// Finally is always executed
|
|
526
|
+
const finallyBlock = this.findChildByType(finallyClause, 'statement_block') ||
|
|
527
|
+
this.findChildByType(finallyClause, 'BlockStatement');
|
|
528
|
+
if (finallyBlock) {
|
|
529
|
+
const finallyExits = this.buildCFG(finallyBlock, exitNodes, context);
|
|
530
|
+
return finallyExits;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
// Create merge node
|
|
534
|
+
if (exitNodes.length > 1) {
|
|
535
|
+
const mergeNode = this.createNode('merge', {
|
|
536
|
+
start: node.endPosition,
|
|
537
|
+
end: node.endPosition,
|
|
538
|
+
});
|
|
539
|
+
for (const exit of exitNodes) {
|
|
540
|
+
this.addEdge(exit, mergeNode.id);
|
|
541
|
+
}
|
|
542
|
+
return [mergeNode.id];
|
|
543
|
+
}
|
|
544
|
+
return exitNodes;
|
|
545
|
+
}
|
|
546
|
+
/**
|
|
547
|
+
* Build CFG for a return statement.
|
|
548
|
+
*/
|
|
549
|
+
buildReturnCFG(node, predecessors, _context) {
|
|
550
|
+
const returnNode = this.createNode('return', {
|
|
551
|
+
start: node.startPosition,
|
|
552
|
+
end: node.endPosition,
|
|
553
|
+
}, node);
|
|
554
|
+
for (const pred of predecessors) {
|
|
555
|
+
this.addEdge(pred, returnNode.id);
|
|
556
|
+
}
|
|
557
|
+
// Connect to exit
|
|
558
|
+
for (const exitId of this.exitNodeIds) {
|
|
559
|
+
this.addEdge(returnNode.id, exitId);
|
|
560
|
+
}
|
|
561
|
+
// Return terminates flow - no successors
|
|
562
|
+
return [];
|
|
563
|
+
}
|
|
564
|
+
/**
|
|
565
|
+
* Build CFG for a throw statement.
|
|
566
|
+
*/
|
|
567
|
+
buildThrowCFG(node, predecessors, _context) {
|
|
568
|
+
const throwNode = this.createNode('throw', {
|
|
569
|
+
start: node.startPosition,
|
|
570
|
+
end: node.endPosition,
|
|
571
|
+
}, node);
|
|
572
|
+
for (const pred of predecessors) {
|
|
573
|
+
this.addEdge(pred, throwNode.id);
|
|
574
|
+
}
|
|
575
|
+
// Throw terminates normal flow
|
|
576
|
+
// In a try block, it would go to catch, but we simplify here
|
|
577
|
+
return [];
|
|
578
|
+
}
|
|
579
|
+
/**
|
|
580
|
+
* Build CFG for a break statement.
|
|
581
|
+
*/
|
|
582
|
+
buildBreakCFG(node, predecessors, context) {
|
|
583
|
+
const breakNode = this.createNode('break', {
|
|
584
|
+
start: node.startPosition,
|
|
585
|
+
end: node.endPosition,
|
|
586
|
+
}, node);
|
|
587
|
+
for (const pred of predecessors) {
|
|
588
|
+
this.addEdge(pred, breakNode.id);
|
|
589
|
+
}
|
|
590
|
+
// Connect to loop/switch exit
|
|
591
|
+
const exitTarget = context.switchExit || context.loopExit;
|
|
592
|
+
if (exitTarget) {
|
|
593
|
+
this.addEdge(breakNode.id, exitTarget);
|
|
594
|
+
}
|
|
595
|
+
// Break terminates normal flow
|
|
596
|
+
return [];
|
|
597
|
+
}
|
|
598
|
+
/**
|
|
599
|
+
* Build CFG for a continue statement.
|
|
600
|
+
*/
|
|
601
|
+
buildContinueCFG(node, predecessors, context) {
|
|
602
|
+
const continueNode = this.createNode('continue', {
|
|
603
|
+
start: node.startPosition,
|
|
604
|
+
end: node.endPosition,
|
|
605
|
+
}, node);
|
|
606
|
+
for (const pred of predecessors) {
|
|
607
|
+
this.addEdge(pred, continueNode.id);
|
|
608
|
+
}
|
|
609
|
+
// Connect to loop entry
|
|
610
|
+
if (context.loopEntry) {
|
|
611
|
+
this.addEdge(continueNode.id, context.loopEntry, undefined, true);
|
|
612
|
+
}
|
|
613
|
+
// Continue terminates normal flow
|
|
614
|
+
return [];
|
|
615
|
+
}
|
|
616
|
+
/**
|
|
617
|
+
* Build CFG for a regular statement.
|
|
618
|
+
*/
|
|
619
|
+
buildStatementCFG(node, predecessors, context) {
|
|
620
|
+
const stmtNode = this.createNode('statement', {
|
|
621
|
+
start: node.startPosition,
|
|
622
|
+
end: node.endPosition,
|
|
623
|
+
}, node);
|
|
624
|
+
for (const pred of predecessors) {
|
|
625
|
+
this.addEdge(pred, stmtNode.id);
|
|
626
|
+
}
|
|
627
|
+
// Collect variable declarations
|
|
628
|
+
this.collectVariableDeclarations(node, context);
|
|
629
|
+
return [stmtNode.id];
|
|
630
|
+
}
|
|
631
|
+
// ============================================
|
|
632
|
+
// Data Flow Analysis Methods
|
|
633
|
+
// ============================================
|
|
634
|
+
/**
|
|
635
|
+
* Analyze data flow in an AST.
|
|
636
|
+
*/
|
|
637
|
+
analyzeDataFlow(node, context) {
|
|
638
|
+
const reads = [];
|
|
639
|
+
const writes = [];
|
|
640
|
+
const captures = [];
|
|
641
|
+
const nullDereferences = [];
|
|
642
|
+
const unusedVariables = [];
|
|
643
|
+
const uninitializedReads = [];
|
|
644
|
+
// Traverse AST to collect data flow information
|
|
645
|
+
this.traverseForDataFlow(node, context, reads, writes, captures);
|
|
646
|
+
// Detect unused variables
|
|
647
|
+
if (this.options.detectUnusedVariables) {
|
|
648
|
+
for (const [name, state] of context.variables) {
|
|
649
|
+
if (!state.isRead && state.isWritten) {
|
|
650
|
+
unusedVariables.push(name);
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
// Detect uninitialized reads
|
|
655
|
+
if (this.options.detectUninitializedReads) {
|
|
656
|
+
for (const read of reads) {
|
|
657
|
+
const state = context.variables.get(read.name);
|
|
658
|
+
if (state && !state.isInitialized) {
|
|
659
|
+
uninitializedReads.push(read);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
// Detect null dereferences
|
|
664
|
+
if (this.options.detectNullDereferences) {
|
|
665
|
+
this.detectNullDereferences(node, nullDereferences);
|
|
666
|
+
}
|
|
667
|
+
return {
|
|
668
|
+
reads,
|
|
669
|
+
writes,
|
|
670
|
+
captures,
|
|
671
|
+
nullDereferences,
|
|
672
|
+
unusedVariables,
|
|
673
|
+
uninitializedReads,
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
/**
|
|
677
|
+
* Traverse AST for data flow information.
|
|
678
|
+
*/
|
|
679
|
+
traverseForDataFlow(node, context, reads, writes, captures) {
|
|
680
|
+
const nodeType = node.type;
|
|
681
|
+
// Handle identifier reads
|
|
682
|
+
if (nodeType === 'identifier' || nodeType === 'Identifier') {
|
|
683
|
+
const name = node.text;
|
|
684
|
+
const state = context.variables.get(name);
|
|
685
|
+
if (state) {
|
|
686
|
+
// Check if this is a read or write context
|
|
687
|
+
const parent = this.getParentContext(node);
|
|
688
|
+
if (parent === 'read') {
|
|
689
|
+
reads.push({
|
|
690
|
+
name,
|
|
691
|
+
location: { start: node.startPosition, end: node.endPosition },
|
|
692
|
+
});
|
|
693
|
+
state.isRead = true;
|
|
694
|
+
state.readLocations.push({ start: node.startPosition, end: node.endPosition });
|
|
695
|
+
}
|
|
696
|
+
else if (parent === 'write') {
|
|
697
|
+
writes.push({
|
|
698
|
+
name,
|
|
699
|
+
location: { start: node.startPosition, end: node.endPosition },
|
|
700
|
+
});
|
|
701
|
+
state.isWritten = true;
|
|
702
|
+
state.isInitialized = true;
|
|
703
|
+
state.writeLocations.push({ start: node.startPosition, end: node.endPosition });
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
else if (context.parentContext) {
|
|
707
|
+
// Variable from outer scope - captured
|
|
708
|
+
const outerState = this.findVariableInParentContext(name, context.parentContext);
|
|
709
|
+
if (outerState) {
|
|
710
|
+
captures.push({
|
|
711
|
+
name,
|
|
712
|
+
location: { start: node.startPosition, end: node.endPosition },
|
|
713
|
+
});
|
|
714
|
+
outerState.isCaptured = true;
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
// Handle assignment expressions
|
|
719
|
+
if (nodeType === 'assignment_expression' || nodeType === 'AssignmentExpression') {
|
|
720
|
+
const left = node.children[0];
|
|
721
|
+
if (left && (left.type === 'identifier' || left.type === 'Identifier')) {
|
|
722
|
+
const name = left.text;
|
|
723
|
+
const state = context.variables.get(name);
|
|
724
|
+
if (state) {
|
|
725
|
+
writes.push({
|
|
726
|
+
name,
|
|
727
|
+
location: { start: left.startPosition, end: left.endPosition },
|
|
728
|
+
});
|
|
729
|
+
state.isWritten = true;
|
|
730
|
+
state.isInitialized = true;
|
|
731
|
+
state.writeLocations.push({ start: left.startPosition, end: left.endPosition });
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
// Handle update expressions (++, --)
|
|
736
|
+
if (nodeType === 'update_expression' || nodeType === 'UpdateExpression') {
|
|
737
|
+
const operand = node.children.find(c => c.type === 'identifier' || c.type === 'Identifier');
|
|
738
|
+
if (operand) {
|
|
739
|
+
const name = operand.text;
|
|
740
|
+
const state = context.variables.get(name);
|
|
741
|
+
if (state) {
|
|
742
|
+
// Update is both read and write
|
|
743
|
+
reads.push({
|
|
744
|
+
name,
|
|
745
|
+
location: { start: operand.startPosition, end: operand.endPosition },
|
|
746
|
+
});
|
|
747
|
+
writes.push({
|
|
748
|
+
name,
|
|
749
|
+
location: { start: operand.startPosition, end: operand.endPosition },
|
|
750
|
+
});
|
|
751
|
+
state.isRead = true;
|
|
752
|
+
state.isWritten = true;
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
// Recurse into children
|
|
757
|
+
for (const child of node.children) {
|
|
758
|
+
this.traverseForDataFlow(child, context, reads, writes, captures);
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
/**
|
|
762
|
+
* Detect potential null/undefined dereferences.
|
|
763
|
+
*/
|
|
764
|
+
detectNullDereferences(node, locations) {
|
|
765
|
+
// Look for member access on potentially null values
|
|
766
|
+
if (node.type === 'member_expression' || node.type === 'MemberExpression') {
|
|
767
|
+
const object = node.children[0];
|
|
768
|
+
if (object && this.isPotentiallyNull(object)) {
|
|
769
|
+
locations.push({
|
|
770
|
+
start: node.startPosition,
|
|
771
|
+
end: node.endPosition,
|
|
772
|
+
});
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
// Look for call expressions on potentially null values
|
|
776
|
+
if (node.type === 'call_expression' || node.type === 'CallExpression') {
|
|
777
|
+
const callee = node.children[0];
|
|
778
|
+
if (callee && this.isPotentiallyNull(callee)) {
|
|
779
|
+
locations.push({
|
|
780
|
+
start: node.startPosition,
|
|
781
|
+
end: node.endPosition,
|
|
782
|
+
});
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
// Recurse
|
|
786
|
+
for (const child of node.children) {
|
|
787
|
+
this.detectNullDereferences(child, locations);
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
/**
|
|
791
|
+
* Check if an expression is potentially null/undefined.
|
|
792
|
+
*/
|
|
793
|
+
isPotentiallyNull(node) {
|
|
794
|
+
// Check for null/undefined literals
|
|
795
|
+
if (node.type === 'null' || node.text === 'null' ||
|
|
796
|
+
node.type === 'undefined' || node.text === 'undefined') {
|
|
797
|
+
return true;
|
|
798
|
+
}
|
|
799
|
+
// Check for optional chaining result
|
|
800
|
+
if (node.type === 'optional_chain_expression' || node.type === 'OptionalMemberExpression') {
|
|
801
|
+
return true;
|
|
802
|
+
}
|
|
803
|
+
// Check for conditional expression that might be null
|
|
804
|
+
if (node.type === 'ternary_expression' || node.type === 'ConditionalExpression') {
|
|
805
|
+
const consequent = node.children[1];
|
|
806
|
+
const alternate = node.children[2];
|
|
807
|
+
const consequentNull = consequent ? this.isPotentiallyNull(consequent) : false;
|
|
808
|
+
const alternateNull = alternate ? this.isPotentiallyNull(alternate) : false;
|
|
809
|
+
return consequentNull || alternateNull;
|
|
810
|
+
}
|
|
811
|
+
return false;
|
|
812
|
+
}
|
|
813
|
+
// ============================================
|
|
814
|
+
// Detection Methods
|
|
815
|
+
// ============================================
|
|
816
|
+
/**
|
|
817
|
+
* Mark all reachable nodes starting from entry.
|
|
818
|
+
*/
|
|
819
|
+
markReachableNodes() {
|
|
820
|
+
if (!this.entryNodeId)
|
|
821
|
+
return;
|
|
822
|
+
const visited = new Set();
|
|
823
|
+
const queue = [this.entryNodeId];
|
|
824
|
+
while (queue.length > 0) {
|
|
825
|
+
const nodeId = queue.shift();
|
|
826
|
+
if (visited.has(nodeId))
|
|
827
|
+
continue;
|
|
828
|
+
visited.add(nodeId);
|
|
829
|
+
const node = this.nodes.get(nodeId);
|
|
830
|
+
if (node) {
|
|
831
|
+
node.isReachable = true;
|
|
832
|
+
for (const outgoing of node.outgoing) {
|
|
833
|
+
if (!visited.has(outgoing)) {
|
|
834
|
+
queue.push(outgoing);
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
/**
|
|
841
|
+
* Detect unreachable code.
|
|
842
|
+
*/
|
|
843
|
+
detectUnreachableCode() {
|
|
844
|
+
for (const node of this.nodes.values()) {
|
|
845
|
+
if (!node.isReachable && node.kind !== 'entry' && node.kind !== 'exit' &&
|
|
846
|
+
node.kind !== 'merge' && node.astNode) {
|
|
847
|
+
this.unreachableCode.push(node.location);
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
/**
|
|
852
|
+
* Check for infinite loops.
|
|
853
|
+
*/
|
|
854
|
+
checkInfiniteLoop(node, _bodyExits) {
|
|
855
|
+
// A loop is potentially infinite if:
|
|
856
|
+
// 1. The condition is always true (while(true))
|
|
857
|
+
// 2. There's no break/return in the body
|
|
858
|
+
const condition = this.findCondition(node);
|
|
859
|
+
if (condition) {
|
|
860
|
+
// Check for literal true
|
|
861
|
+
if (condition.text === 'true' || condition.text === '1') {
|
|
862
|
+
// Check if there's a break or return in the body
|
|
863
|
+
if (!this.hasBreakOrReturn(node)) {
|
|
864
|
+
this.infiniteLoops.push({
|
|
865
|
+
start: node.startPosition,
|
|
866
|
+
end: node.endPosition,
|
|
867
|
+
});
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
/**
|
|
873
|
+
* Check for missing return statements.
|
|
874
|
+
*/
|
|
875
|
+
checkMissingReturns(node, lastNodes) {
|
|
876
|
+
// Check if function has a return type annotation
|
|
877
|
+
const returnType = this.findChildByType(node, 'type_annotation') ||
|
|
878
|
+
this.findChildByType(node, 'return_type');
|
|
879
|
+
if (returnType) {
|
|
880
|
+
// Check if return type is void
|
|
881
|
+
const typeText = returnType.text;
|
|
882
|
+
if (typeText.includes('void') || typeText.includes('undefined')) {
|
|
883
|
+
return; // void functions don't need explicit return
|
|
884
|
+
}
|
|
885
|
+
// Check if all paths return
|
|
886
|
+
for (const nodeId of lastNodes) {
|
|
887
|
+
const cfgNode = this.nodes.get(nodeId);
|
|
888
|
+
if (cfgNode && cfgNode.kind !== 'return' && cfgNode.kind !== 'throw') {
|
|
889
|
+
this.missingReturns.push({
|
|
890
|
+
start: node.startPosition,
|
|
891
|
+
end: node.endPosition,
|
|
892
|
+
});
|
|
893
|
+
break;
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
/**
|
|
899
|
+
* Find the condition expression in a loop/if.
|
|
900
|
+
*/
|
|
901
|
+
findCondition(node) {
|
|
902
|
+
// Look for parenthesized expression or condition
|
|
903
|
+
for (const child of node.children) {
|
|
904
|
+
if (child.type === 'parenthesized_expression' ||
|
|
905
|
+
child.type === 'condition') {
|
|
906
|
+
return child.children[0] || child;
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
return null;
|
|
910
|
+
}
|
|
911
|
+
/**
|
|
912
|
+
* Check if a node contains break or return.
|
|
913
|
+
*/
|
|
914
|
+
hasBreakOrReturn(node) {
|
|
915
|
+
if (node.type === 'break_statement' || node.type === 'BreakStatement' ||
|
|
916
|
+
node.type === 'return_statement' || node.type === 'ReturnStatement') {
|
|
917
|
+
return true;
|
|
918
|
+
}
|
|
919
|
+
for (const child of node.children) {
|
|
920
|
+
if (this.hasBreakOrReturn(child)) {
|
|
921
|
+
return true;
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
return false;
|
|
925
|
+
}
|
|
926
|
+
// ============================================
|
|
927
|
+
// Helper Methods
|
|
928
|
+
// ============================================
|
|
929
|
+
/**
|
|
930
|
+
* Reset analyzer state.
|
|
931
|
+
*/
|
|
932
|
+
reset() {
|
|
933
|
+
this.nodeIdCounter = 0;
|
|
934
|
+
this.nodes.clear();
|
|
935
|
+
this.edges = [];
|
|
936
|
+
this.entryNodeId = null;
|
|
937
|
+
this.exitNodeIds.clear();
|
|
938
|
+
this.unreachableCode = [];
|
|
939
|
+
this.infiniteLoops = [];
|
|
940
|
+
this.missingReturns = [];
|
|
941
|
+
this.options = {};
|
|
942
|
+
}
|
|
943
|
+
/**
|
|
944
|
+
* Create a new CFG node.
|
|
945
|
+
*/
|
|
946
|
+
createNode(kind, location, astNode) {
|
|
947
|
+
const id = `cfg_${this.nodeIdCounter++}`;
|
|
948
|
+
const node = {
|
|
949
|
+
id,
|
|
950
|
+
kind,
|
|
951
|
+
location,
|
|
952
|
+
outgoing: new Set(),
|
|
953
|
+
incoming: new Set(),
|
|
954
|
+
isReachable: false,
|
|
955
|
+
};
|
|
956
|
+
if (astNode !== undefined) {
|
|
957
|
+
node.astNode = astNode;
|
|
958
|
+
}
|
|
959
|
+
this.nodes.set(id, node);
|
|
960
|
+
return node;
|
|
961
|
+
}
|
|
962
|
+
/**
|
|
963
|
+
* Add an edge between two nodes.
|
|
964
|
+
*/
|
|
965
|
+
addEdge(from, to, label, isBackEdge = false) {
|
|
966
|
+
const fromNode = this.nodes.get(from);
|
|
967
|
+
const toNode = this.nodes.get(to);
|
|
968
|
+
if (fromNode && toNode) {
|
|
969
|
+
fromNode.outgoing.add(to);
|
|
970
|
+
toNode.incoming.add(from);
|
|
971
|
+
const edge = {
|
|
972
|
+
from,
|
|
973
|
+
to,
|
|
974
|
+
isBackEdge,
|
|
975
|
+
};
|
|
976
|
+
if (label !== undefined) {
|
|
977
|
+
edge.label = label;
|
|
978
|
+
}
|
|
979
|
+
this.edges.push(edge);
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
/**
|
|
983
|
+
* Create a flow context.
|
|
984
|
+
*/
|
|
985
|
+
createFlowContext(parent) {
|
|
986
|
+
return {
|
|
987
|
+
variables: new Map(),
|
|
988
|
+
parentContext: parent,
|
|
989
|
+
inLoop: false,
|
|
990
|
+
inTry: false,
|
|
991
|
+
loopEntry: null,
|
|
992
|
+
loopExit: null,
|
|
993
|
+
switchExit: null,
|
|
994
|
+
};
|
|
995
|
+
}
|
|
996
|
+
/**
|
|
997
|
+
* Collect function parameters into context.
|
|
998
|
+
*/
|
|
999
|
+
collectFunctionParameters(node, context) {
|
|
1000
|
+
const params = this.findChildByType(node, 'formal_parameters') ||
|
|
1001
|
+
this.findChildByType(node, 'parameters');
|
|
1002
|
+
if (params) {
|
|
1003
|
+
for (const param of params.children) {
|
|
1004
|
+
const name = this.getParameterName(param);
|
|
1005
|
+
if (name) {
|
|
1006
|
+
context.variables.set(name, {
|
|
1007
|
+
name,
|
|
1008
|
+
isInitialized: true, // Parameters are initialized
|
|
1009
|
+
isRead: false,
|
|
1010
|
+
isWritten: false,
|
|
1011
|
+
isCaptured: false,
|
|
1012
|
+
declarationLocation: { start: param.startPosition, end: param.endPosition },
|
|
1013
|
+
readLocations: [],
|
|
1014
|
+
writeLocations: [],
|
|
1015
|
+
});
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
/**
|
|
1021
|
+
* Collect variable declarations into context.
|
|
1022
|
+
*/
|
|
1023
|
+
collectVariableDeclarations(node, context) {
|
|
1024
|
+
if (node.type === 'variable_declaration' || node.type === 'VariableDeclaration' ||
|
|
1025
|
+
node.type === 'lexical_declaration') {
|
|
1026
|
+
for (const child of node.children) {
|
|
1027
|
+
if (child.type === 'variable_declarator' || child.type === 'VariableDeclarator') {
|
|
1028
|
+
const nameNode = child.children[0];
|
|
1029
|
+
if (nameNode && (nameNode.type === 'identifier' || nameNode.type === 'Identifier')) {
|
|
1030
|
+
const name = nameNode.text;
|
|
1031
|
+
const hasInitializer = child.children.length > 1;
|
|
1032
|
+
context.variables.set(name, {
|
|
1033
|
+
name,
|
|
1034
|
+
isInitialized: hasInitializer,
|
|
1035
|
+
isRead: false,
|
|
1036
|
+
isWritten: hasInitializer,
|
|
1037
|
+
isCaptured: false,
|
|
1038
|
+
declarationLocation: { start: nameNode.startPosition, end: nameNode.endPosition },
|
|
1039
|
+
readLocations: [],
|
|
1040
|
+
writeLocations: hasInitializer
|
|
1041
|
+
? [{ start: nameNode.startPosition, end: nameNode.endPosition }]
|
|
1042
|
+
: [],
|
|
1043
|
+
});
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
// Recurse for nested declarations
|
|
1049
|
+
for (const child of node.children) {
|
|
1050
|
+
this.collectVariableDeclarations(child, context);
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
/**
|
|
1054
|
+
* Get parameter name from a parameter node.
|
|
1055
|
+
*/
|
|
1056
|
+
getParameterName(node) {
|
|
1057
|
+
if (node.type === 'identifier' || node.type === 'Identifier') {
|
|
1058
|
+
return node.text;
|
|
1059
|
+
}
|
|
1060
|
+
if (node.type === 'required_parameter' || node.type === 'optional_parameter') {
|
|
1061
|
+
const id = this.findChildByType(node, 'identifier') ||
|
|
1062
|
+
this.findChildByType(node, 'Identifier');
|
|
1063
|
+
return id?.text || null;
|
|
1064
|
+
}
|
|
1065
|
+
// Handle destructuring patterns
|
|
1066
|
+
if (node.type === 'object_pattern' || node.type === 'array_pattern') {
|
|
1067
|
+
return null; // Skip destructuring for now
|
|
1068
|
+
}
|
|
1069
|
+
return null;
|
|
1070
|
+
}
|
|
1071
|
+
/**
|
|
1072
|
+
* Find a child node by type.
|
|
1073
|
+
*/
|
|
1074
|
+
findChildByType(node, type) {
|
|
1075
|
+
for (const child of node.children) {
|
|
1076
|
+
if (child.type === type) {
|
|
1077
|
+
return child;
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
return null;
|
|
1081
|
+
}
|
|
1082
|
+
/**
|
|
1083
|
+
* Check if a node is a statement.
|
|
1084
|
+
*/
|
|
1085
|
+
isStatement(node) {
|
|
1086
|
+
const type = node.type;
|
|
1087
|
+
return type.includes('statement') ||
|
|
1088
|
+
type.includes('Statement') ||
|
|
1089
|
+
type.includes('declaration') ||
|
|
1090
|
+
type.includes('Declaration') ||
|
|
1091
|
+
type === 'if_statement' ||
|
|
1092
|
+
type === 'for_statement' ||
|
|
1093
|
+
type === 'while_statement' ||
|
|
1094
|
+
type === 'do_statement' ||
|
|
1095
|
+
type === 'switch_statement' ||
|
|
1096
|
+
type === 'try_statement' ||
|
|
1097
|
+
type === 'return_statement' ||
|
|
1098
|
+
type === 'throw_statement' ||
|
|
1099
|
+
type === 'break_statement' ||
|
|
1100
|
+
type === 'continue_statement' ||
|
|
1101
|
+
type === 'expression_statement' ||
|
|
1102
|
+
type === 'lexical_declaration';
|
|
1103
|
+
}
|
|
1104
|
+
/**
|
|
1105
|
+
* Get the parent context for an identifier (read or write).
|
|
1106
|
+
*/
|
|
1107
|
+
getParentContext(_node) {
|
|
1108
|
+
// Simplified: assume read unless in assignment left-hand side
|
|
1109
|
+
// A more complete implementation would track the AST path
|
|
1110
|
+
return 'read';
|
|
1111
|
+
}
|
|
1112
|
+
/**
|
|
1113
|
+
* Find a variable in parent contexts.
|
|
1114
|
+
*/
|
|
1115
|
+
findVariableInParentContext(name, context) {
|
|
1116
|
+
let current = context;
|
|
1117
|
+
while (current) {
|
|
1118
|
+
const state = current.variables.get(name);
|
|
1119
|
+
if (state) {
|
|
1120
|
+
return state;
|
|
1121
|
+
}
|
|
1122
|
+
current = current.parentContext;
|
|
1123
|
+
}
|
|
1124
|
+
return null;
|
|
1125
|
+
}
|
|
1126
|
+
/**
|
|
1127
|
+
* Build the final result.
|
|
1128
|
+
*/
|
|
1129
|
+
buildResult(dataFlow) {
|
|
1130
|
+
// Convert internal nodes to external format
|
|
1131
|
+
const nodes = [];
|
|
1132
|
+
const exits = [];
|
|
1133
|
+
for (const internal of this.nodes.values()) {
|
|
1134
|
+
const external = {
|
|
1135
|
+
id: internal.id,
|
|
1136
|
+
kind: internal.kind,
|
|
1137
|
+
location: internal.location,
|
|
1138
|
+
outgoing: Array.from(internal.outgoing),
|
|
1139
|
+
incoming: Array.from(internal.incoming),
|
|
1140
|
+
isReachable: internal.isReachable,
|
|
1141
|
+
};
|
|
1142
|
+
if (internal.astNode !== undefined) {
|
|
1143
|
+
external.astNode = internal.astNode;
|
|
1144
|
+
}
|
|
1145
|
+
nodes.push(external);
|
|
1146
|
+
if (this.exitNodeIds.has(internal.id)) {
|
|
1147
|
+
exits.push(external);
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
// Find entry node
|
|
1151
|
+
const entryNode = nodes.find(n => n.id === this.entryNodeId);
|
|
1152
|
+
if (!entryNode) {
|
|
1153
|
+
throw new Error('Entry node not found');
|
|
1154
|
+
}
|
|
1155
|
+
const controlFlow = {
|
|
1156
|
+
entry: entryNode,
|
|
1157
|
+
exits,
|
|
1158
|
+
nodes,
|
|
1159
|
+
edges: this.edges,
|
|
1160
|
+
};
|
|
1161
|
+
return {
|
|
1162
|
+
controlFlow,
|
|
1163
|
+
dataFlow,
|
|
1164
|
+
unreachableCode: this.unreachableCode,
|
|
1165
|
+
infiniteLoops: this.infiniteLoops,
|
|
1166
|
+
missingReturns: this.missingReturns,
|
|
1167
|
+
};
|
|
1168
|
+
}
|
|
1169
|
+
// ============================================
|
|
1170
|
+
// Public Utility Methods
|
|
1171
|
+
// ============================================
|
|
1172
|
+
/**
|
|
1173
|
+
* Get all nodes in the CFG.
|
|
1174
|
+
*/
|
|
1175
|
+
getNodes() {
|
|
1176
|
+
return Array.from(this.nodes.values()).map(internal => {
|
|
1177
|
+
const node = {
|
|
1178
|
+
id: internal.id,
|
|
1179
|
+
kind: internal.kind,
|
|
1180
|
+
location: internal.location,
|
|
1181
|
+
outgoing: Array.from(internal.outgoing),
|
|
1182
|
+
incoming: Array.from(internal.incoming),
|
|
1183
|
+
isReachable: internal.isReachable,
|
|
1184
|
+
};
|
|
1185
|
+
if (internal.astNode !== undefined) {
|
|
1186
|
+
node.astNode = internal.astNode;
|
|
1187
|
+
}
|
|
1188
|
+
return node;
|
|
1189
|
+
});
|
|
1190
|
+
}
|
|
1191
|
+
/**
|
|
1192
|
+
* Get all edges in the CFG.
|
|
1193
|
+
*/
|
|
1194
|
+
getEdges() {
|
|
1195
|
+
return [...this.edges];
|
|
1196
|
+
}
|
|
1197
|
+
/**
|
|
1198
|
+
* Check if a node is reachable.
|
|
1199
|
+
*/
|
|
1200
|
+
isNodeReachable(nodeId) {
|
|
1201
|
+
const node = this.nodes.get(nodeId);
|
|
1202
|
+
return node?.isReachable ?? false;
|
|
1203
|
+
}
|
|
1204
|
+
/**
|
|
1205
|
+
* Get predecessors of a node.
|
|
1206
|
+
*/
|
|
1207
|
+
getPredecessors(nodeId) {
|
|
1208
|
+
const node = this.nodes.get(nodeId);
|
|
1209
|
+
return node ? Array.from(node.incoming) : [];
|
|
1210
|
+
}
|
|
1211
|
+
/**
|
|
1212
|
+
* Get successors of a node.
|
|
1213
|
+
*/
|
|
1214
|
+
getSuccessors(nodeId) {
|
|
1215
|
+
const node = this.nodes.get(nodeId);
|
|
1216
|
+
return node ? Array.from(node.outgoing) : [];
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
//# sourceMappingURL=flow-analyzer.js.map
|