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.
Files changed (221) hide show
  1. package/dist/analyzers/ast-analyzer.d.ts +251 -0
  2. package/dist/analyzers/ast-analyzer.d.ts.map +1 -0
  3. package/dist/analyzers/ast-analyzer.js +548 -0
  4. package/dist/analyzers/ast-analyzer.js.map +1 -0
  5. package/dist/analyzers/flow-analyzer.d.ts +241 -0
  6. package/dist/analyzers/flow-analyzer.d.ts.map +1 -0
  7. package/dist/analyzers/flow-analyzer.js +1219 -0
  8. package/dist/analyzers/flow-analyzer.js.map +1 -0
  9. package/dist/analyzers/index.d.ts +18 -0
  10. package/dist/analyzers/index.d.ts.map +1 -0
  11. package/dist/analyzers/index.js +19 -0
  12. package/dist/analyzers/index.js.map +1 -0
  13. package/dist/analyzers/semantic-analyzer.d.ts +252 -0
  14. package/dist/analyzers/semantic-analyzer.d.ts.map +1 -0
  15. package/dist/analyzers/semantic-analyzer.js +1182 -0
  16. package/dist/analyzers/semantic-analyzer.js.map +1 -0
  17. package/dist/analyzers/type-analyzer.d.ts +289 -0
  18. package/dist/analyzers/type-analyzer.d.ts.map +1 -0
  19. package/dist/analyzers/type-analyzer.js +1269 -0
  20. package/dist/analyzers/type-analyzer.js.map +1 -0
  21. package/dist/analyzers/types.d.ts +537 -0
  22. package/dist/analyzers/types.d.ts.map +1 -0
  23. package/dist/analyzers/types.js +11 -0
  24. package/dist/analyzers/types.js.map +1 -0
  25. package/dist/config/config-loader.d.ts +166 -0
  26. package/dist/config/config-loader.d.ts.map +1 -0
  27. package/dist/config/config-loader.js +429 -0
  28. package/dist/config/config-loader.js.map +1 -0
  29. package/dist/config/config-validator.d.ts +204 -0
  30. package/dist/config/config-validator.d.ts.map +1 -0
  31. package/dist/config/config-validator.js +632 -0
  32. package/dist/config/config-validator.js.map +1 -0
  33. package/dist/config/defaults.d.ts +8 -0
  34. package/dist/config/defaults.d.ts.map +1 -0
  35. package/dist/config/defaults.js +26 -0
  36. package/dist/config/defaults.js.map +1 -0
  37. package/dist/config/index.d.ts +10 -0
  38. package/dist/config/index.d.ts.map +1 -0
  39. package/dist/config/index.js +10 -0
  40. package/dist/config/index.js.map +1 -0
  41. package/dist/config/types.d.ts +47 -0
  42. package/dist/config/types.d.ts.map +1 -0
  43. package/dist/config/types.js +7 -0
  44. package/dist/config/types.js.map +1 -0
  45. package/dist/index.d.ts +37 -0
  46. package/dist/index.d.ts.map +1 -0
  47. package/dist/index.js +39 -0
  48. package/dist/index.js.map +1 -0
  49. package/dist/manifest/exporter.d.ts +21 -0
  50. package/dist/manifest/exporter.d.ts.map +1 -0
  51. package/dist/manifest/exporter.js +339 -0
  52. package/dist/manifest/exporter.js.map +1 -0
  53. package/dist/manifest/index.d.ts +14 -0
  54. package/dist/manifest/index.d.ts.map +1 -0
  55. package/dist/manifest/index.js +15 -0
  56. package/dist/manifest/index.js.map +1 -0
  57. package/dist/manifest/manifest-store.d.ts +111 -0
  58. package/dist/manifest/manifest-store.d.ts.map +1 -0
  59. package/dist/manifest/manifest-store.js +418 -0
  60. package/dist/manifest/manifest-store.js.map +1 -0
  61. package/dist/manifest/types.d.ts +238 -0
  62. package/dist/manifest/types.d.ts.map +1 -0
  63. package/dist/manifest/types.js +11 -0
  64. package/dist/manifest/types.js.map +1 -0
  65. package/dist/matcher/confidence-scorer.d.ts +188 -0
  66. package/dist/matcher/confidence-scorer.d.ts.map +1 -0
  67. package/dist/matcher/confidence-scorer.js +302 -0
  68. package/dist/matcher/confidence-scorer.js.map +1 -0
  69. package/dist/matcher/index.d.ts +24 -0
  70. package/dist/matcher/index.d.ts.map +1 -0
  71. package/dist/matcher/index.js +26 -0
  72. package/dist/matcher/index.js.map +1 -0
  73. package/dist/matcher/outlier-detector.d.ts +252 -0
  74. package/dist/matcher/outlier-detector.d.ts.map +1 -0
  75. package/dist/matcher/outlier-detector.js +544 -0
  76. package/dist/matcher/outlier-detector.js.map +1 -0
  77. package/dist/matcher/pattern-matcher.d.ts +169 -0
  78. package/dist/matcher/pattern-matcher.d.ts.map +1 -0
  79. package/dist/matcher/pattern-matcher.js +692 -0
  80. package/dist/matcher/pattern-matcher.js.map +1 -0
  81. package/dist/matcher/types.d.ts +476 -0
  82. package/dist/matcher/types.d.ts.map +1 -0
  83. package/dist/matcher/types.js +36 -0
  84. package/dist/matcher/types.js.map +1 -0
  85. package/dist/parsers/base-parser.d.ts +282 -0
  86. package/dist/parsers/base-parser.d.ts.map +1 -0
  87. package/dist/parsers/base-parser.js +421 -0
  88. package/dist/parsers/base-parser.js.map +1 -0
  89. package/dist/parsers/css-parser.d.ts +225 -0
  90. package/dist/parsers/css-parser.d.ts.map +1 -0
  91. package/dist/parsers/css-parser.js +477 -0
  92. package/dist/parsers/css-parser.js.map +1 -0
  93. package/dist/parsers/index.d.ts +15 -0
  94. package/dist/parsers/index.d.ts.map +1 -0
  95. package/dist/parsers/index.js +15 -0
  96. package/dist/parsers/index.js.map +1 -0
  97. package/dist/parsers/json-parser.d.ts +219 -0
  98. package/dist/parsers/json-parser.d.ts.map +1 -0
  99. package/dist/parsers/json-parser.js +602 -0
  100. package/dist/parsers/json-parser.js.map +1 -0
  101. package/dist/parsers/markdown-parser.d.ts +276 -0
  102. package/dist/parsers/markdown-parser.d.ts.map +1 -0
  103. package/dist/parsers/markdown-parser.js +731 -0
  104. package/dist/parsers/markdown-parser.js.map +1 -0
  105. package/dist/parsers/parser-manager.d.ts +294 -0
  106. package/dist/parsers/parser-manager.d.ts.map +1 -0
  107. package/dist/parsers/parser-manager.js +738 -0
  108. package/dist/parsers/parser-manager.js.map +1 -0
  109. package/dist/parsers/python-parser.d.ts +204 -0
  110. package/dist/parsers/python-parser.d.ts.map +1 -0
  111. package/dist/parsers/python-parser.js +517 -0
  112. package/dist/parsers/python-parser.js.map +1 -0
  113. package/dist/parsers/types.d.ts +43 -0
  114. package/dist/parsers/types.d.ts.map +1 -0
  115. package/dist/parsers/types.js +7 -0
  116. package/dist/parsers/types.js.map +1 -0
  117. package/dist/parsers/typescript-parser.d.ts +264 -0
  118. package/dist/parsers/typescript-parser.d.ts.map +1 -0
  119. package/dist/parsers/typescript-parser.js +658 -0
  120. package/dist/parsers/typescript-parser.js.map +1 -0
  121. package/dist/rules/evaluator.d.ts +305 -0
  122. package/dist/rules/evaluator.d.ts.map +1 -0
  123. package/dist/rules/evaluator.js +579 -0
  124. package/dist/rules/evaluator.js.map +1 -0
  125. package/dist/rules/index.d.ts +13 -0
  126. package/dist/rules/index.d.ts.map +1 -0
  127. package/dist/rules/index.js +13 -0
  128. package/dist/rules/index.js.map +1 -0
  129. package/dist/rules/quick-fix-generator.d.ts +334 -0
  130. package/dist/rules/quick-fix-generator.d.ts.map +1 -0
  131. package/dist/rules/quick-fix-generator.js +1075 -0
  132. package/dist/rules/quick-fix-generator.js.map +1 -0
  133. package/dist/rules/rule-engine.d.ts +241 -0
  134. package/dist/rules/rule-engine.d.ts.map +1 -0
  135. package/dist/rules/rule-engine.js +585 -0
  136. package/dist/rules/rule-engine.js.map +1 -0
  137. package/dist/rules/severity-manager.d.ts +394 -0
  138. package/dist/rules/severity-manager.d.ts.map +1 -0
  139. package/dist/rules/severity-manager.js +619 -0
  140. package/dist/rules/severity-manager.js.map +1 -0
  141. package/dist/rules/types.d.ts +370 -0
  142. package/dist/rules/types.d.ts.map +1 -0
  143. package/dist/rules/types.js +133 -0
  144. package/dist/rules/types.js.map +1 -0
  145. package/dist/rules/variant-manager.d.ts +388 -0
  146. package/dist/rules/variant-manager.d.ts.map +1 -0
  147. package/dist/rules/variant-manager.js +777 -0
  148. package/dist/rules/variant-manager.js.map +1 -0
  149. package/dist/scanner/change-detector.d.ts +164 -0
  150. package/dist/scanner/change-detector.d.ts.map +1 -0
  151. package/dist/scanner/change-detector.js +263 -0
  152. package/dist/scanner/change-detector.js.map +1 -0
  153. package/dist/scanner/dependency-graph.d.ts +270 -0
  154. package/dist/scanner/dependency-graph.d.ts.map +1 -0
  155. package/dist/scanner/dependency-graph.js +436 -0
  156. package/dist/scanner/dependency-graph.js.map +1 -0
  157. package/dist/scanner/file-walker.d.ts +127 -0
  158. package/dist/scanner/file-walker.d.ts.map +1 -0
  159. package/dist/scanner/file-walker.js +526 -0
  160. package/dist/scanner/file-walker.js.map +1 -0
  161. package/dist/scanner/index.d.ts +12 -0
  162. package/dist/scanner/index.d.ts.map +1 -0
  163. package/dist/scanner/index.js +12 -0
  164. package/dist/scanner/index.js.map +1 -0
  165. package/dist/scanner/types.d.ts +218 -0
  166. package/dist/scanner/types.d.ts.map +1 -0
  167. package/dist/scanner/types.js +10 -0
  168. package/dist/scanner/types.js.map +1 -0
  169. package/dist/scanner/worker-pool.d.ts +317 -0
  170. package/dist/scanner/worker-pool.d.ts.map +1 -0
  171. package/dist/scanner/worker-pool.js +571 -0
  172. package/dist/scanner/worker-pool.js.map +1 -0
  173. package/dist/store/cache-manager.d.ts +179 -0
  174. package/dist/store/cache-manager.d.ts.map +1 -0
  175. package/dist/store/cache-manager.js +391 -0
  176. package/dist/store/cache-manager.js.map +1 -0
  177. package/dist/store/history-store.d.ts +314 -0
  178. package/dist/store/history-store.d.ts.map +1 -0
  179. package/dist/store/history-store.js +707 -0
  180. package/dist/store/history-store.js.map +1 -0
  181. package/dist/store/index.d.ts +20 -0
  182. package/dist/store/index.d.ts.map +1 -0
  183. package/dist/store/index.js +26 -0
  184. package/dist/store/index.js.map +1 -0
  185. package/dist/store/lock-file-manager.d.ts +202 -0
  186. package/dist/store/lock-file-manager.d.ts.map +1 -0
  187. package/dist/store/lock-file-manager.js +475 -0
  188. package/dist/store/lock-file-manager.js.map +1 -0
  189. package/dist/store/pattern-store.d.ts +289 -0
  190. package/dist/store/pattern-store.d.ts.map +1 -0
  191. package/dist/store/pattern-store.js +936 -0
  192. package/dist/store/pattern-store.js.map +1 -0
  193. package/dist/store/schema-validator.d.ts +159 -0
  194. package/dist/store/schema-validator.d.ts.map +1 -0
  195. package/dist/store/schema-validator.js +1096 -0
  196. package/dist/store/schema-validator.js.map +1 -0
  197. package/dist/store/types.d.ts +585 -0
  198. package/dist/store/types.d.ts.map +1 -0
  199. package/dist/store/types.js +82 -0
  200. package/dist/store/types.js.map +1 -0
  201. package/dist/types/analysis.d.ts +19 -0
  202. package/dist/types/analysis.d.ts.map +1 -0
  203. package/dist/types/analysis.js +5 -0
  204. package/dist/types/analysis.js.map +1 -0
  205. package/dist/types/common.d.ts +7 -0
  206. package/dist/types/common.d.ts.map +1 -0
  207. package/dist/types/common.js +5 -0
  208. package/dist/types/common.js.map +1 -0
  209. package/dist/types/index.d.ts +12 -0
  210. package/dist/types/index.d.ts.map +1 -0
  211. package/dist/types/index.js +10 -0
  212. package/dist/types/index.js.map +1 -0
  213. package/dist/types/patterns.d.ts +40 -0
  214. package/dist/types/patterns.d.ts.map +1 -0
  215. package/dist/types/patterns.js +7 -0
  216. package/dist/types/patterns.js.map +1 -0
  217. package/dist/types/violations.d.ts +7 -0
  218. package/dist/types/violations.d.ts.map +1 -0
  219. package/dist/types/violations.js +7 -0
  220. package/dist/types/violations.js.map +1 -0
  221. 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