@grafema/core 0.1.1-alpha → 0.2.0-beta

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 (319) hide show
  1. package/dist/Orchestrator.d.ts +7 -0
  2. package/dist/Orchestrator.d.ts.map +1 -1
  3. package/dist/Orchestrator.js +25 -3
  4. package/dist/config/ConfigLoader.d.ts +18 -0
  5. package/dist/config/ConfigLoader.d.ts.map +1 -1
  6. package/dist/config/ConfigLoader.js +65 -3
  7. package/dist/core/FileExplainer.d.ts +101 -0
  8. package/dist/core/FileExplainer.d.ts.map +1 -0
  9. package/dist/core/FileExplainer.js +139 -0
  10. package/dist/core/NodeFactory.d.ts +44 -5
  11. package/dist/core/NodeFactory.d.ts.map +1 -1
  12. package/dist/core/NodeFactory.js +52 -7
  13. package/dist/core/nodes/ArrayLiteralNode.d.ts.map +1 -1
  14. package/dist/core/nodes/ArrayLiteralNode.js +4 -2
  15. package/dist/core/nodes/BranchNode.d.ts +41 -0
  16. package/dist/core/nodes/BranchNode.d.ts.map +1 -0
  17. package/dist/core/nodes/BranchNode.js +82 -0
  18. package/dist/core/nodes/CallSiteNode.d.ts +2 -2
  19. package/dist/core/nodes/CallSiteNode.d.ts.map +1 -1
  20. package/dist/core/nodes/CallSiteNode.js +9 -5
  21. package/dist/core/nodes/CaseNode.d.ts +43 -0
  22. package/dist/core/nodes/CaseNode.d.ts.map +1 -0
  23. package/dist/core/nodes/CaseNode.js +81 -0
  24. package/dist/core/nodes/ClassNode.d.ts +2 -2
  25. package/dist/core/nodes/ClassNode.d.ts.map +1 -1
  26. package/dist/core/nodes/ClassNode.js +8 -4
  27. package/dist/core/nodes/ConstantNode.d.ts +2 -2
  28. package/dist/core/nodes/ConstantNode.d.ts.map +1 -1
  29. package/dist/core/nodes/ConstantNode.js +6 -4
  30. package/dist/core/nodes/ConstructorCallNode.d.ts +51 -0
  31. package/dist/core/nodes/ConstructorCallNode.d.ts.map +1 -0
  32. package/dist/core/nodes/ConstructorCallNode.js +171 -0
  33. package/dist/core/nodes/DatabaseQueryNode.d.ts +3 -2
  34. package/dist/core/nodes/DatabaseQueryNode.d.ts.map +1 -1
  35. package/dist/core/nodes/DatabaseQueryNode.js +5 -2
  36. package/dist/core/nodes/DecoratorNode.d.ts +2 -2
  37. package/dist/core/nodes/DecoratorNode.d.ts.map +1 -1
  38. package/dist/core/nodes/DecoratorNode.js +5 -3
  39. package/dist/core/nodes/EnumNode.d.ts +2 -2
  40. package/dist/core/nodes/EnumNode.d.ts.map +1 -1
  41. package/dist/core/nodes/EnumNode.js +5 -3
  42. package/dist/core/nodes/EventListenerNode.d.ts +4 -4
  43. package/dist/core/nodes/EventListenerNode.d.ts.map +1 -1
  44. package/dist/core/nodes/EventListenerNode.js +7 -4
  45. package/dist/core/nodes/ExportNode.d.ts +2 -2
  46. package/dist/core/nodes/ExportNode.d.ts.map +1 -1
  47. package/dist/core/nodes/ExportNode.js +8 -4
  48. package/dist/core/nodes/ExpressionNode.d.ts +2 -2
  49. package/dist/core/nodes/ExpressionNode.d.ts.map +1 -1
  50. package/dist/core/nodes/ExpressionNode.js +6 -4
  51. package/dist/core/nodes/ExternalModuleNode.d.ts +4 -0
  52. package/dist/core/nodes/ExternalModuleNode.d.ts.map +1 -1
  53. package/dist/core/nodes/ExternalModuleNode.js +10 -2
  54. package/dist/core/nodes/HttpRequestNode.d.ts +4 -4
  55. package/dist/core/nodes/HttpRequestNode.d.ts.map +1 -1
  56. package/dist/core/nodes/HttpRequestNode.js +7 -4
  57. package/dist/core/nodes/ImportNode.d.ts +10 -2
  58. package/dist/core/nodes/ImportNode.d.ts.map +1 -1
  59. package/dist/core/nodes/ImportNode.js +21 -4
  60. package/dist/core/nodes/InterfaceNode.d.ts +2 -2
  61. package/dist/core/nodes/InterfaceNode.d.ts.map +1 -1
  62. package/dist/core/nodes/InterfaceNode.js +5 -3
  63. package/dist/core/nodes/LiteralNode.d.ts +2 -2
  64. package/dist/core/nodes/LiteralNode.d.ts.map +1 -1
  65. package/dist/core/nodes/LiteralNode.js +6 -4
  66. package/dist/core/nodes/MethodCallNode.d.ts +2 -2
  67. package/dist/core/nodes/MethodCallNode.d.ts.map +1 -1
  68. package/dist/core/nodes/MethodCallNode.js +9 -5
  69. package/dist/core/nodes/MethodNode.d.ts +2 -2
  70. package/dist/core/nodes/MethodNode.d.ts.map +1 -1
  71. package/dist/core/nodes/MethodNode.js +8 -4
  72. package/dist/core/nodes/ObjectLiteralNode.d.ts.map +1 -1
  73. package/dist/core/nodes/ObjectLiteralNode.js +4 -2
  74. package/dist/core/nodes/ParameterNode.d.ts +2 -2
  75. package/dist/core/nodes/ParameterNode.d.ts.map +1 -1
  76. package/dist/core/nodes/ParameterNode.js +5 -3
  77. package/dist/core/nodes/TypeNode.d.ts +2 -2
  78. package/dist/core/nodes/TypeNode.d.ts.map +1 -1
  79. package/dist/core/nodes/TypeNode.js +5 -3
  80. package/dist/core/nodes/VariableDeclarationNode.d.ts +2 -2
  81. package/dist/core/nodes/VariableDeclarationNode.d.ts.map +1 -1
  82. package/dist/core/nodes/VariableDeclarationNode.js +9 -5
  83. package/dist/core/nodes/index.d.ts +3 -0
  84. package/dist/core/nodes/index.d.ts.map +1 -1
  85. package/dist/core/nodes/index.js +3 -0
  86. package/dist/data/builtins/BuiltinRegistry.d.ts +78 -0
  87. package/dist/data/builtins/BuiltinRegistry.d.ts.map +1 -0
  88. package/dist/data/builtins/BuiltinRegistry.js +110 -0
  89. package/dist/data/builtins/definitions.d.ts +28 -0
  90. package/dist/data/builtins/definitions.d.ts.map +1 -0
  91. package/dist/data/builtins/definitions.js +250 -0
  92. package/dist/data/builtins/index.d.ts +10 -0
  93. package/dist/data/builtins/index.d.ts.map +1 -0
  94. package/dist/data/builtins/index.js +8 -0
  95. package/dist/data/builtins/jsGlobals.d.ts +18 -0
  96. package/dist/data/builtins/jsGlobals.d.ts.map +1 -0
  97. package/dist/data/builtins/jsGlobals.js +26 -0
  98. package/dist/data/builtins/types.d.ts +34 -0
  99. package/dist/data/builtins/types.d.ts.map +1 -0
  100. package/dist/data/builtins/types.js +7 -0
  101. package/dist/data/globals/definitions.d.ts +27 -0
  102. package/dist/data/globals/definitions.d.ts.map +1 -0
  103. package/dist/data/globals/definitions.js +117 -0
  104. package/dist/data/globals/index.d.ts +36 -0
  105. package/dist/data/globals/index.d.ts.map +1 -0
  106. package/dist/data/globals/index.js +52 -0
  107. package/dist/diagnostics/DiagnosticReporter.d.ts +23 -0
  108. package/dist/diagnostics/DiagnosticReporter.d.ts.map +1 -1
  109. package/dist/diagnostics/DiagnosticReporter.js +88 -0
  110. package/dist/diagnostics/index.d.ts +1 -1
  111. package/dist/diagnostics/index.d.ts.map +1 -1
  112. package/dist/errors/GrafemaError.d.ts +43 -0
  113. package/dist/errors/GrafemaError.d.ts.map +1 -1
  114. package/dist/errors/GrafemaError.js +50 -0
  115. package/dist/index.d.ts +17 -1
  116. package/dist/index.d.ts.map +1 -1
  117. package/dist/index.js +17 -1
  118. package/dist/plugins/analysis/DatabaseAnalyzer.d.ts.map +1 -1
  119. package/dist/plugins/analysis/DatabaseAnalyzer.js +3 -2
  120. package/dist/plugins/analysis/ExpressAnalyzer.d.ts.map +1 -1
  121. package/dist/plugins/analysis/ExpressAnalyzer.js +3 -1
  122. package/dist/plugins/analysis/ExpressResponseAnalyzer.d.ts +148 -0
  123. package/dist/plugins/analysis/ExpressResponseAnalyzer.d.ts.map +1 -0
  124. package/dist/plugins/analysis/ExpressResponseAnalyzer.js +495 -0
  125. package/dist/plugins/analysis/ExpressRouteAnalyzer.d.ts.map +1 -1
  126. package/dist/plugins/analysis/ExpressRouteAnalyzer.js +53 -18
  127. package/dist/plugins/analysis/FetchAnalyzer.d.ts +40 -0
  128. package/dist/plugins/analysis/FetchAnalyzer.d.ts.map +1 -1
  129. package/dist/plugins/analysis/FetchAnalyzer.js +163 -15
  130. package/dist/plugins/analysis/JSASTAnalyzer.d.ts +157 -26
  131. package/dist/plugins/analysis/JSASTAnalyzer.d.ts.map +1 -1
  132. package/dist/plugins/analysis/JSASTAnalyzer.js +2418 -191
  133. package/dist/plugins/analysis/RustAnalyzer.js +4 -4
  134. package/dist/plugins/analysis/SQLiteAnalyzer.d.ts.map +1 -1
  135. package/dist/plugins/analysis/SQLiteAnalyzer.js +4 -2
  136. package/dist/plugins/analysis/SocketIOAnalyzer.d.ts +9 -0
  137. package/dist/plugins/analysis/SocketIOAnalyzer.d.ts.map +1 -1
  138. package/dist/plugins/analysis/SocketIOAnalyzer.js +91 -7
  139. package/dist/plugins/analysis/ast/GraphBuilder.d.ts +173 -0
  140. package/dist/plugins/analysis/ast/GraphBuilder.d.ts.map +1 -1
  141. package/dist/plugins/analysis/ast/GraphBuilder.js +1256 -65
  142. package/dist/plugins/analysis/ast/types.d.ts +294 -0
  143. package/dist/plugins/analysis/ast/types.d.ts.map +1 -1
  144. package/dist/plugins/analysis/ast/visitors/ASTVisitor.d.ts +5 -1
  145. package/dist/plugins/analysis/ast/visitors/ASTVisitor.d.ts.map +1 -1
  146. package/dist/plugins/analysis/ast/visitors/CallExpressionVisitor.d.ts +1 -0
  147. package/dist/plugins/analysis/ast/visitors/CallExpressionVisitor.d.ts.map +1 -1
  148. package/dist/plugins/analysis/ast/visitors/CallExpressionVisitor.js +12 -1
  149. package/dist/plugins/analysis/ast/visitors/FunctionVisitor.d.ts +10 -0
  150. package/dist/plugins/analysis/ast/visitors/FunctionVisitor.d.ts.map +1 -1
  151. package/dist/plugins/analysis/ast/visitors/FunctionVisitor.js +62 -0
  152. package/dist/plugins/analysis/ast/visitors/ImportExportVisitor.d.ts +4 -0
  153. package/dist/plugins/analysis/ast/visitors/ImportExportVisitor.d.ts.map +1 -1
  154. package/dist/plugins/analysis/ast/visitors/ImportExportVisitor.js +101 -0
  155. package/dist/plugins/analysis/ast/visitors/VariableVisitor.d.ts +16 -1
  156. package/dist/plugins/analysis/ast/visitors/VariableVisitor.d.ts.map +1 -1
  157. package/dist/plugins/analysis/ast/visitors/VariableVisitor.js +233 -39
  158. package/dist/plugins/discovery/WorkspaceDiscovery.d.ts.map +1 -1
  159. package/dist/plugins/discovery/WorkspaceDiscovery.js +9 -4
  160. package/dist/plugins/enrichment/AliasTracker.d.ts.map +1 -1
  161. package/dist/plugins/enrichment/AliasTracker.js +16 -1
  162. package/dist/plugins/enrichment/ArgumentParameterLinker.d.ts +32 -0
  163. package/dist/plugins/enrichment/ArgumentParameterLinker.d.ts.map +1 -0
  164. package/dist/plugins/enrichment/ArgumentParameterLinker.js +175 -0
  165. package/dist/plugins/enrichment/ClosureCaptureEnricher.d.ts +51 -0
  166. package/dist/plugins/enrichment/ClosureCaptureEnricher.d.ts.map +1 -0
  167. package/dist/plugins/enrichment/ClosureCaptureEnricher.js +205 -0
  168. package/dist/plugins/enrichment/ExternalCallResolver.d.ts +42 -0
  169. package/dist/plugins/enrichment/ExternalCallResolver.d.ts.map +1 -0
  170. package/dist/plugins/enrichment/ExternalCallResolver.js +213 -0
  171. package/dist/plugins/enrichment/FunctionCallResolver.d.ts +58 -0
  172. package/dist/plugins/enrichment/FunctionCallResolver.d.ts.map +1 -0
  173. package/dist/plugins/enrichment/FunctionCallResolver.js +340 -0
  174. package/dist/plugins/enrichment/HTTPConnectionEnricher.d.ts +16 -3
  175. package/dist/plugins/enrichment/HTTPConnectionEnricher.d.ts.map +1 -1
  176. package/dist/plugins/enrichment/HTTPConnectionEnricher.js +64 -20
  177. package/dist/plugins/enrichment/MethodCallResolver.d.ts.map +1 -1
  178. package/dist/plugins/enrichment/MethodCallResolver.js +15 -1
  179. package/dist/plugins/enrichment/MountPointResolver.d.ts +14 -12
  180. package/dist/plugins/enrichment/MountPointResolver.d.ts.map +1 -1
  181. package/dist/plugins/enrichment/MountPointResolver.js +172 -151
  182. package/dist/plugins/enrichment/NodejsBuiltinsResolver.d.ts +44 -0
  183. package/dist/plugins/enrichment/NodejsBuiltinsResolver.d.ts.map +1 -0
  184. package/dist/plugins/enrichment/NodejsBuiltinsResolver.js +271 -0
  185. package/dist/plugins/enrichment/ValueDomainAnalyzer.d.ts +5 -27
  186. package/dist/plugins/enrichment/ValueDomainAnalyzer.d.ts.map +1 -1
  187. package/dist/plugins/enrichment/ValueDomainAnalyzer.js +62 -139
  188. package/dist/plugins/indexing/JSModuleIndexer.d.ts +15 -0
  189. package/dist/plugins/indexing/JSModuleIndexer.d.ts.map +1 -1
  190. package/dist/plugins/indexing/JSModuleIndexer.js +58 -0
  191. package/dist/plugins/indexing/RustModuleIndexer.d.ts +1 -1
  192. package/dist/plugins/indexing/RustModuleIndexer.js +4 -4
  193. package/dist/plugins/validation/BrokenImportValidator.d.ts +31 -0
  194. package/dist/plugins/validation/BrokenImportValidator.d.ts.map +1 -0
  195. package/dist/plugins/validation/BrokenImportValidator.js +249 -0
  196. package/dist/plugins/validation/CallResolverValidator.d.ts +21 -10
  197. package/dist/plugins/validation/CallResolverValidator.d.ts.map +1 -1
  198. package/dist/plugins/validation/CallResolverValidator.js +101 -76
  199. package/dist/plugins/validation/DataFlowValidator.d.ts.map +1 -1
  200. package/dist/plugins/validation/DataFlowValidator.js +49 -41
  201. package/dist/plugins/validation/GraphConnectivityValidator.d.ts.map +1 -1
  202. package/dist/plugins/validation/GraphConnectivityValidator.js +25 -1
  203. package/dist/plugins/validation/SQLInjectionValidator.d.ts.map +1 -1
  204. package/dist/plugins/validation/SQLInjectionValidator.js +2 -3
  205. package/dist/queries/findCallsInFunction.d.ts +52 -0
  206. package/dist/queries/findCallsInFunction.d.ts.map +1 -0
  207. package/dist/queries/findCallsInFunction.js +135 -0
  208. package/dist/queries/findContainingFunction.d.ts +45 -0
  209. package/dist/queries/findContainingFunction.d.ts.map +1 -0
  210. package/dist/queries/findContainingFunction.js +54 -0
  211. package/dist/queries/index.d.ts +14 -0
  212. package/dist/queries/index.d.ts.map +1 -0
  213. package/dist/queries/index.js +11 -0
  214. package/dist/queries/traceValues.d.ts +70 -0
  215. package/dist/queries/traceValues.d.ts.map +1 -0
  216. package/dist/queries/traceValues.js +299 -0
  217. package/dist/queries/types.d.ts +163 -0
  218. package/dist/queries/types.d.ts.map +1 -0
  219. package/dist/queries/types.js +9 -0
  220. package/dist/schema/GraphSchemaExtractor.d.ts +53 -0
  221. package/dist/schema/GraphSchemaExtractor.d.ts.map +1 -0
  222. package/dist/schema/GraphSchemaExtractor.js +124 -0
  223. package/dist/schema/InterfaceSchemaExtractor.d.ts +73 -0
  224. package/dist/schema/InterfaceSchemaExtractor.d.ts.map +1 -0
  225. package/dist/schema/InterfaceSchemaExtractor.js +112 -0
  226. package/dist/schema/index.d.ts +5 -0
  227. package/dist/schema/index.d.ts.map +1 -0
  228. package/dist/schema/index.js +2 -0
  229. package/dist/storage/backends/RFDBServerBackend.d.ts +12 -18
  230. package/dist/storage/backends/RFDBServerBackend.d.ts.map +1 -1
  231. package/dist/storage/backends/RFDBServerBackend.js +41 -52
  232. package/dist/storage/backends/typeValidation.d.ts.map +1 -1
  233. package/dist/storage/backends/typeValidation.js +1 -0
  234. package/package.json +3 -3
  235. package/src/Orchestrator.ts +35 -3
  236. package/src/config/ConfigLoader.ts +94 -3
  237. package/src/core/FileExplainer.ts +179 -0
  238. package/src/core/NodeFactory.ts +72 -8
  239. package/src/core/nodes/ArrayLiteralNode.ts +3 -2
  240. package/src/core/nodes/BranchNode.ts +113 -0
  241. package/src/core/nodes/CallSiteNode.ts +7 -5
  242. package/src/core/nodes/CaseNode.ts +123 -0
  243. package/src/core/nodes/ClassNode.ts +6 -4
  244. package/src/core/nodes/ConstantNode.ts +5 -4
  245. package/src/core/nodes/ConstructorCallNode.ts +217 -0
  246. package/src/core/nodes/DatabaseQueryNode.ts +5 -1
  247. package/src/core/nodes/DecoratorNode.ts +4 -3
  248. package/src/core/nodes/EnumNode.ts +4 -3
  249. package/src/core/nodes/EventListenerNode.ts +7 -4
  250. package/src/core/nodes/ExportNode.ts +6 -4
  251. package/src/core/nodes/ExpressionNode.ts +5 -4
  252. package/src/core/nodes/ExternalModuleNode.ts +11 -2
  253. package/src/core/nodes/HttpRequestNode.ts +7 -4
  254. package/src/core/nodes/ImportNode.ts +31 -4
  255. package/src/core/nodes/InterfaceNode.ts +4 -3
  256. package/src/core/nodes/LiteralNode.ts +5 -4
  257. package/src/core/nodes/MethodCallNode.ts +7 -5
  258. package/src/core/nodes/MethodNode.ts +6 -4
  259. package/src/core/nodes/ObjectLiteralNode.ts +3 -2
  260. package/src/core/nodes/ParameterNode.ts +4 -3
  261. package/src/core/nodes/TypeNode.ts +4 -3
  262. package/src/core/nodes/VariableDeclarationNode.ts +7 -5
  263. package/src/core/nodes/index.ts +3 -0
  264. package/src/data/builtins/BuiltinRegistry.ts +124 -0
  265. package/src/data/builtins/definitions.ts +267 -0
  266. package/src/data/builtins/index.ts +10 -0
  267. package/src/data/builtins/jsGlobals.ts +28 -0
  268. package/src/data/builtins/types.ts +36 -0
  269. package/src/data/globals/definitions.ts +156 -0
  270. package/src/data/globals/index.ts +66 -0
  271. package/src/diagnostics/DiagnosticReporter.ts +120 -0
  272. package/src/diagnostics/index.ts +1 -1
  273. package/src/errors/GrafemaError.ts +65 -0
  274. package/src/index.ts +45 -0
  275. package/src/plugins/analysis/DatabaseAnalyzer.ts +4 -2
  276. package/src/plugins/analysis/ExpressAnalyzer.ts +5 -1
  277. package/src/plugins/analysis/ExpressResponseAnalyzer.ts +636 -0
  278. package/src/plugins/analysis/ExpressRouteAnalyzer.ts +57 -18
  279. package/src/plugins/analysis/FetchAnalyzer.ts +204 -16
  280. package/src/plugins/analysis/JSASTAnalyzer.ts +2958 -260
  281. package/src/plugins/analysis/RustAnalyzer.ts +4 -4
  282. package/src/plugins/analysis/SQLiteAnalyzer.ts +5 -2
  283. package/src/plugins/analysis/SocketIOAnalyzer.ts +121 -7
  284. package/src/plugins/analysis/ast/GraphBuilder.ts +1578 -70
  285. package/src/plugins/analysis/ast/types.ts +387 -0
  286. package/src/plugins/analysis/ast/visitors/ASTVisitor.ts +8 -0
  287. package/src/plugins/analysis/ast/visitors/CallExpressionVisitor.ts +16 -1
  288. package/src/plugins/analysis/ast/visitors/FunctionVisitor.ts +77 -2
  289. package/src/plugins/analysis/ast/visitors/ImportExportVisitor.ts +112 -1
  290. package/src/plugins/analysis/ast/visitors/VariableVisitor.ts +272 -47
  291. package/src/plugins/discovery/WorkspaceDiscovery.ts +11 -4
  292. package/src/plugins/enrichment/AliasTracker.ts +22 -1
  293. package/src/plugins/enrichment/ArgumentParameterLinker.ts +240 -0
  294. package/src/plugins/enrichment/ClosureCaptureEnricher.ts +267 -0
  295. package/src/plugins/enrichment/ExternalCallResolver.ts +262 -0
  296. package/src/plugins/enrichment/FunctionCallResolver.ts +456 -0
  297. package/src/plugins/enrichment/HTTPConnectionEnricher.ts +70 -20
  298. package/src/plugins/enrichment/MethodCallResolver.ts +21 -1
  299. package/src/plugins/enrichment/MountPointResolver.ts +206 -198
  300. package/src/plugins/enrichment/NodejsBuiltinsResolver.ts +365 -0
  301. package/src/plugins/enrichment/ValueDomainAnalyzer.ts +67 -184
  302. package/src/plugins/indexing/JSModuleIndexer.ts +66 -0
  303. package/src/plugins/indexing/RustModuleIndexer.ts +4 -4
  304. package/src/plugins/validation/BrokenImportValidator.ts +325 -0
  305. package/src/plugins/validation/CallResolverValidator.ts +129 -109
  306. package/src/plugins/validation/DataFlowValidator.ts +75 -58
  307. package/src/plugins/validation/GraphConnectivityValidator.ts +39 -1
  308. package/src/plugins/validation/SQLInjectionValidator.ts +2 -5
  309. package/src/queries/README.md +46 -0
  310. package/src/queries/findCallsInFunction.ts +206 -0
  311. package/src/queries/findContainingFunction.ts +83 -0
  312. package/src/queries/index.ts +23 -0
  313. package/src/queries/traceValues.ts +398 -0
  314. package/src/queries/types.ts +187 -0
  315. package/src/schema/GraphSchemaExtractor.ts +177 -0
  316. package/src/schema/InterfaceSchemaExtractor.ts +173 -0
  317. package/src/schema/index.ts +5 -0
  318. package/src/storage/backends/RFDBServerBackend.ts +58 -70
  319. package/src/storage/backends/typeValidation.ts +1 -0
@@ -54,17 +54,27 @@ import { Profiler } from '../../core/Profiler.js';
54
54
  import { ScopeTracker } from '../../core/ScopeTracker.js';
55
55
  import { computeSemanticId } from '../../core/SemanticId.js';
56
56
  import { ExpressionNode } from '../../core/nodes/ExpressionNode.js';
57
+ import { ConstructorCallNode } from '../../core/nodes/ConstructorCallNode.js';
58
+ import { ObjectLiteralNode } from '../../core/nodes/ObjectLiteralNode.js';
59
+ import { NodeFactory } from '../../core/NodeFactory.js';
57
60
  import type { PluginContext, PluginResult, PluginMetadata, GraphBackend } from '@grafema/types';
58
61
  import type {
59
62
  ModuleNode,
60
63
  FunctionInfo,
61
64
  ParameterInfo,
62
65
  ScopeInfo,
66
+ BranchInfo,
67
+ CaseInfo,
68
+ LoopInfo,
69
+ TryBlockInfo,
70
+ CatchBlockInfo,
71
+ FinallyBlockInfo,
63
72
  VariableDeclarationInfo,
64
73
  CallSiteInfo,
65
74
  MethodCallInfo,
66
75
  EventListenerInfo,
67
76
  ClassInstantiationInfo,
77
+ ConstructorCallInfo,
68
78
  ClassDeclarationInfo,
69
79
  MethodCallbackInfo,
70
80
  CallArgumentInfo,
@@ -85,6 +95,11 @@ import type {
85
95
  ArrayMutationArgument,
86
96
  ObjectMutationInfo,
87
97
  ObjectMutationValue,
98
+ VariableReassignmentInfo,
99
+ ReturnStatementInfo,
100
+ UpdateExpressionInfo,
101
+ PromiseResolutionInfo,
102
+ PromiseExecutorContext,
88
103
  CounterRef,
89
104
  ProcessedNodes,
90
105
  ASTCollections,
@@ -101,6 +116,18 @@ interface Collections {
101
116
  functions: FunctionInfo[];
102
117
  parameters: ParameterInfo[];
103
118
  scopes: ScopeInfo[];
119
+ // Branching (switch statements)
120
+ branches: BranchInfo[];
121
+ cases: CaseInfo[];
122
+ // Control flow (loops)
123
+ loops: LoopInfo[];
124
+ // Control flow (try/catch/finally) - Phase 4
125
+ tryBlocks?: TryBlockInfo[];
126
+ catchBlocks?: CatchBlockInfo[];
127
+ finallyBlocks?: FinallyBlockInfo[];
128
+ tryBlockCounterRef?: CounterRef;
129
+ catchBlockCounterRef?: CounterRef;
130
+ finallyBlockCounterRef?: CounterRef;
104
131
  variableDeclarations: VariableDeclarationInfo[];
105
132
  callSites: CallSiteInfo[];
106
133
  methodCalls: MethodCallInfo[];
@@ -128,6 +155,16 @@ interface Collections {
128
155
  arrayMutations: ArrayMutationInfo[];
129
156
  // Object mutation tracking for FLOWS_INTO edges
130
157
  objectMutations: ObjectMutationInfo[];
158
+ // Variable reassignment tracking for FLOWS_INTO edges (REG-290)
159
+ variableReassignments: VariableReassignmentInfo[];
160
+ // Return statement tracking for RETURNS edges
161
+ returnStatements: ReturnStatementInfo[];
162
+ // Update expression tracking for MODIFIES edges (REG-288, REG-312)
163
+ updateExpressions: UpdateExpressionInfo[];
164
+ // Promise resolution tracking for RESOLVES_TO edges (REG-334)
165
+ promiseResolutions: PromiseResolutionInfo[];
166
+ // Promise executor contexts (REG-334) - keyed by executor function's start:end position
167
+ promiseExecutorContexts: Map<string, PromiseExecutorContext>;
131
168
  objectLiteralCounterRef: CounterRef;
132
169
  arrayLiteralCounterRef: CounterRef;
133
170
  ifScopeCounterRef: CounterRef;
@@ -138,6 +175,8 @@ interface Collections {
138
175
  httpRequestCounterRef: CounterRef;
139
176
  literalCounterRef: CounterRef;
140
177
  anonymousFunctionCounterRef: CounterRef;
178
+ branchCounterRef: CounterRef;
179
+ caseCounterRef: CounterRef;
141
180
  processedNodes: ProcessedNodes;
142
181
  code?: string;
143
182
  // VisitorCollections compatibility
@@ -151,6 +190,35 @@ interface Collections {
151
190
  [key: string]: unknown;
152
191
  }
153
192
 
193
+ /**
194
+ * Tracks try/catch/finally scope transitions during traversal.
195
+ * Used by createTryStatementHandler and createBlockStatementHandler.
196
+ */
197
+ interface TryScopeInfo {
198
+ tryScopeId: string;
199
+ catchScopeId: string | null;
200
+ finallyScopeId: string | null;
201
+ currentBlock: 'try' | 'catch' | 'finally';
202
+ // Phase 4: Control flow node IDs
203
+ tryBlockId: string;
204
+ catchBlockId: string | null;
205
+ finallyBlockId: string | null;
206
+ }
207
+
208
+ /**
209
+ * Tracks if/else scope transitions during traversal.
210
+ * Used by createIfStatementHandler and createBlockStatementHandler.
211
+ * Phase 3: Extended to include branchId for control flow BRANCH nodes.
212
+ */
213
+ interface IfElseScopeInfo {
214
+ inElse: boolean;
215
+ hasElse: boolean;
216
+ ifScopeId: string;
217
+ elseScopeId: string | null;
218
+ // Phase 3: Control flow BRANCH node ID
219
+ branchId: string;
220
+ }
221
+
154
222
  interface AnalysisManifest {
155
223
  projectPath: string;
156
224
  [key: string]: unknown;
@@ -196,7 +264,9 @@ export class JSASTAnalyzer extends Plugin {
196
264
  'WRITES_TO', 'IMPORTS', 'INSTANCE_OF', 'HANDLED_BY', 'HAS_CALLBACK',
197
265
  'PASSES_ARGUMENT', 'MAKES_REQUEST', 'IMPORTS_FROM', 'EXPORTS_TO', 'ASSIGNED_FROM',
198
266
  // TypeScript-specific edges
199
- 'IMPLEMENTS', 'EXTENDS', 'DECORATED_BY'
267
+ 'IMPLEMENTS', 'EXTENDS', 'DECORATED_BY',
268
+ // Promise data flow
269
+ 'RESOLVES_TO'
200
270
  ]
201
271
  },
202
272
  dependencies: ['JSModuleIndexer']
@@ -553,7 +623,10 @@ export class JSASTAnalyzer extends Plugin {
553
623
  line: number,
554
624
  literals: LiteralInfo[],
555
625
  variableAssignments: VariableAssignmentInfo[],
556
- literalCounterRef: CounterRef
626
+ literalCounterRef: CounterRef,
627
+ objectLiterals: ObjectLiteralInfo[],
628
+ objectProperties: ObjectPropertyInfo[],
629
+ objectLiteralCounterRef: CounterRef
557
630
  ): void {
558
631
  if (!initNode) return;
559
632
  // initNode is already typed as t.Expression
@@ -561,7 +634,41 @@ export class JSASTAnalyzer extends Plugin {
561
634
 
562
635
  // 0. AwaitExpression
563
636
  if (initExpression.type === 'AwaitExpression') {
564
- return this.trackVariableAssignment(initExpression.argument, variableId, variableName, module, line, literals, variableAssignments, literalCounterRef);
637
+ return this.trackVariableAssignment(initExpression.argument, variableId, variableName, module, line, literals, variableAssignments, literalCounterRef, objectLiterals, objectProperties, objectLiteralCounterRef);
638
+ }
639
+
640
+ // 0.5. ObjectExpression (REG-328) - must be before literal check
641
+ if (initExpression.type === 'ObjectExpression') {
642
+ const column = initExpression.loc?.start.column ?? 0;
643
+ const objectNode = ObjectLiteralNode.create(
644
+ module.file,
645
+ line,
646
+ column,
647
+ { counter: objectLiteralCounterRef.value++ }
648
+ );
649
+
650
+ // Add to objectLiterals collection for GraphBuilder to create the node
651
+ objectLiterals.push(objectNode as unknown as ObjectLiteralInfo);
652
+
653
+ // Extract properties from the object literal
654
+ this.extractObjectProperties(
655
+ initExpression,
656
+ objectNode.id,
657
+ module,
658
+ objectProperties,
659
+ objectLiterals,
660
+ objectLiteralCounterRef,
661
+ literals,
662
+ literalCounterRef
663
+ );
664
+
665
+ // Create ASSIGNED_FROM edge: VARIABLE -> OBJECT_LITERAL
666
+ variableAssignments.push({
667
+ variableId,
668
+ sourceId: objectNode.id,
669
+ sourceType: 'OBJECT_LITERAL'
670
+ });
671
+ return;
565
672
  }
566
673
 
567
674
  // 1. Literal
@@ -665,17 +772,32 @@ export class JSASTAnalyzer extends Plugin {
665
772
  return;
666
773
  }
667
774
 
668
- // 5. NewExpression
775
+ // 5. NewExpression -> CONSTRUCTOR_CALL
669
776
  if (initExpression.type === 'NewExpression') {
670
777
  const callee = initExpression.callee;
778
+ let className: string;
779
+
671
780
  if (callee.type === 'Identifier') {
672
- variableAssignments.push({
673
- variableId,
674
- sourceType: 'CLASS',
675
- className: callee.name,
676
- line: line
677
- });
781
+ className = callee.name;
782
+ } else if (callee.type === 'MemberExpression' && callee.property.type === 'Identifier') {
783
+ // Handle: new module.ClassName()
784
+ className = callee.property.name;
785
+ } else {
786
+ // Unknown callee type, skip
787
+ return;
678
788
  }
789
+
790
+ const callLine = initExpression.loc?.start.line ?? line;
791
+ const callColumn = initExpression.loc?.start.column ?? 0;
792
+
793
+ variableAssignments.push({
794
+ variableId,
795
+ sourceType: 'CONSTRUCTOR_CALL',
796
+ className,
797
+ file: module.file,
798
+ line: callLine,
799
+ column: callColumn
800
+ });
679
801
  return;
680
802
  }
681
803
 
@@ -760,8 +882,8 @@ export class JSASTAnalyzer extends Plugin {
760
882
  column: column
761
883
  });
762
884
 
763
- this.trackVariableAssignment(initExpression.consequent, variableId, variableName, module, line, literals, variableAssignments, literalCounterRef);
764
- this.trackVariableAssignment(initExpression.alternate, variableId, variableName, module, line, literals, variableAssignments, literalCounterRef);
885
+ this.trackVariableAssignment(initExpression.consequent, variableId, variableName, module, line, literals, variableAssignments, literalCounterRef, objectLiterals, objectProperties, objectLiteralCounterRef);
886
+ this.trackVariableAssignment(initExpression.alternate, variableId, variableName, module, line, literals, variableAssignments, literalCounterRef, objectLiterals, objectProperties, objectLiteralCounterRef);
765
887
  return;
766
888
  }
767
889
 
@@ -783,8 +905,8 @@ export class JSASTAnalyzer extends Plugin {
783
905
  column: column
784
906
  });
785
907
 
786
- this.trackVariableAssignment(initExpression.left, variableId, variableName, module, line, literals, variableAssignments, literalCounterRef);
787
- this.trackVariableAssignment(initExpression.right, variableId, variableName, module, line, literals, variableAssignments, literalCounterRef);
908
+ this.trackVariableAssignment(initExpression.left, variableId, variableName, module, line, literals, variableAssignments, literalCounterRef, objectLiterals, objectProperties, objectLiteralCounterRef);
909
+ this.trackVariableAssignment(initExpression.right, variableId, variableName, module, line, literals, variableAssignments, literalCounterRef, objectLiterals, objectProperties, objectLiteralCounterRef);
788
910
  return;
789
911
  }
790
912
 
@@ -811,13 +933,451 @@ export class JSASTAnalyzer extends Plugin {
811
933
  for (const expr of initExpression.expressions) {
812
934
  // Filter out TSType nodes (only in TypeScript code)
813
935
  if (t.isExpression(expr)) {
814
- this.trackVariableAssignment(expr, variableId, variableName, module, line, literals, variableAssignments, literalCounterRef);
936
+ this.trackVariableAssignment(expr, variableId, variableName, module, line, literals, variableAssignments, literalCounterRef, objectLiterals, objectProperties, objectLiteralCounterRef);
815
937
  }
816
938
  }
817
939
  return;
818
940
  }
819
941
  }
820
942
 
943
+ /**
944
+ * Extract object properties and create ObjectPropertyInfo records.
945
+ * Handles nested object/array literals recursively. (REG-328)
946
+ */
947
+ private extractObjectProperties(
948
+ objectExpr: t.ObjectExpression,
949
+ objectId: string,
950
+ module: VisitorModule,
951
+ objectProperties: ObjectPropertyInfo[],
952
+ objectLiterals: ObjectLiteralInfo[],
953
+ objectLiteralCounterRef: CounterRef,
954
+ literals: LiteralInfo[],
955
+ literalCounterRef: CounterRef
956
+ ): void {
957
+ for (const prop of objectExpr.properties) {
958
+ const propLine = prop.loc?.start.line || 0;
959
+ const propColumn = prop.loc?.start.column || 0;
960
+
961
+ // Handle spread properties: { ...other }
962
+ if (prop.type === 'SpreadElement') {
963
+ const spreadArg = prop.argument;
964
+ const propertyInfo: ObjectPropertyInfo = {
965
+ objectId,
966
+ propertyName: '<spread>',
967
+ valueType: 'SPREAD',
968
+ file: module.file,
969
+ line: propLine,
970
+ column: propColumn
971
+ };
972
+
973
+ if (spreadArg.type === 'Identifier') {
974
+ propertyInfo.valueName = spreadArg.name;
975
+ propertyInfo.valueType = 'VARIABLE';
976
+ }
977
+
978
+ objectProperties.push(propertyInfo);
979
+ continue;
980
+ }
981
+
982
+ // Handle regular properties
983
+ if (prop.type === 'ObjectProperty') {
984
+ let propertyName: string;
985
+
986
+ // Get property name
987
+ if (prop.key.type === 'Identifier') {
988
+ propertyName = prop.key.name;
989
+ } else if (prop.key.type === 'StringLiteral') {
990
+ propertyName = prop.key.value;
991
+ } else if (prop.key.type === 'NumericLiteral') {
992
+ propertyName = String(prop.key.value);
993
+ } else {
994
+ propertyName = '<computed>';
995
+ }
996
+
997
+ const propertyInfo: ObjectPropertyInfo = {
998
+ objectId,
999
+ propertyName,
1000
+ file: module.file,
1001
+ line: propLine,
1002
+ column: propColumn,
1003
+ valueType: 'EXPRESSION'
1004
+ };
1005
+
1006
+ const value = prop.value;
1007
+
1008
+ // Nested object literal - check BEFORE extractLiteralValue
1009
+ if (value.type === 'ObjectExpression') {
1010
+ const nestedObjectNode = ObjectLiteralNode.create(
1011
+ module.file,
1012
+ value.loc?.start.line || 0,
1013
+ value.loc?.start.column || 0,
1014
+ { counter: objectLiteralCounterRef.value++ }
1015
+ );
1016
+ objectLiterals.push(nestedObjectNode as unknown as ObjectLiteralInfo);
1017
+ const nestedObjectId = nestedObjectNode.id;
1018
+
1019
+ // Recursively extract nested properties
1020
+ this.extractObjectProperties(
1021
+ value,
1022
+ nestedObjectId,
1023
+ module,
1024
+ objectProperties,
1025
+ objectLiterals,
1026
+ objectLiteralCounterRef,
1027
+ literals,
1028
+ literalCounterRef
1029
+ );
1030
+
1031
+ propertyInfo.valueType = 'OBJECT_LITERAL';
1032
+ propertyInfo.nestedObjectId = nestedObjectId;
1033
+ propertyInfo.valueNodeId = nestedObjectId;
1034
+ }
1035
+ // Literal value (primitives only - objects/arrays handled above)
1036
+ else {
1037
+ const literalValue = ExpressionEvaluator.extractLiteralValue(value);
1038
+ // Handle both non-null literals AND explicit null literals (NullLiteral)
1039
+ if (literalValue !== null || value.type === 'NullLiteral') {
1040
+ const literalId = `LITERAL#${propertyName}#${module.file}#${propLine}:${propColumn}:${literalCounterRef.value++}`;
1041
+ literals.push({
1042
+ id: literalId,
1043
+ type: 'LITERAL',
1044
+ value: literalValue,
1045
+ valueType: typeof literalValue,
1046
+ file: module.file,
1047
+ line: propLine,
1048
+ column: propColumn,
1049
+ parentCallId: objectId,
1050
+ argIndex: 0
1051
+ });
1052
+ propertyInfo.valueType = 'LITERAL';
1053
+ propertyInfo.valueNodeId = literalId;
1054
+ propertyInfo.literalValue = literalValue;
1055
+ }
1056
+ // Variable reference
1057
+ else if (value.type === 'Identifier') {
1058
+ propertyInfo.valueType = 'VARIABLE';
1059
+ propertyInfo.valueName = value.name;
1060
+ }
1061
+ // Call expression
1062
+ else if (value.type === 'CallExpression') {
1063
+ propertyInfo.valueType = 'CALL';
1064
+ propertyInfo.callLine = value.loc?.start.line;
1065
+ propertyInfo.callColumn = value.loc?.start.column;
1066
+ }
1067
+ // Other expressions
1068
+ else {
1069
+ propertyInfo.valueType = 'EXPRESSION';
1070
+ }
1071
+ }
1072
+
1073
+ objectProperties.push(propertyInfo);
1074
+ }
1075
+ // Handle object methods: { foo() {} }
1076
+ else if (prop.type === 'ObjectMethod') {
1077
+ const propertyName = prop.key.type === 'Identifier' ? prop.key.name : '<computed>';
1078
+ objectProperties.push({
1079
+ objectId,
1080
+ propertyName,
1081
+ valueType: 'EXPRESSION',
1082
+ file: module.file,
1083
+ line: propLine,
1084
+ column: propColumn
1085
+ });
1086
+ }
1087
+ }
1088
+ }
1089
+
1090
+ /**
1091
+ * Recursively unwrap AwaitExpression to get the underlying expression.
1092
+ * await await fetch() -> fetch()
1093
+ */
1094
+ private unwrapAwaitExpression(node: t.Expression): t.Expression {
1095
+ if (node.type === 'AwaitExpression' && node.argument) {
1096
+ return this.unwrapAwaitExpression(node.argument);
1097
+ }
1098
+ return node;
1099
+ }
1100
+
1101
+ /**
1102
+ * Extract call site information from CallExpression.
1103
+ * Returns null if not a valid CallExpression.
1104
+ */
1105
+ private extractCallInfo(node: t.Expression): {
1106
+ line: number;
1107
+ column: number;
1108
+ name: string;
1109
+ isMethodCall: boolean;
1110
+ } | null {
1111
+ if (node.type !== 'CallExpression') {
1112
+ return null;
1113
+ }
1114
+
1115
+ const callee = node.callee;
1116
+ let name: string;
1117
+ let isMethodCall = false;
1118
+
1119
+ // Direct call: fetchUser()
1120
+ if (t.isIdentifier(callee)) {
1121
+ name = callee.name;
1122
+ }
1123
+ // Method call: obj.fetchUser() or arr.map()
1124
+ else if (t.isMemberExpression(callee)) {
1125
+ isMethodCall = true;
1126
+ const objectName = t.isIdentifier(callee.object)
1127
+ ? callee.object.name
1128
+ : (t.isThisExpression(callee.object) ? 'this' : 'unknown');
1129
+ const methodName = t.isIdentifier(callee.property)
1130
+ ? callee.property.name
1131
+ : 'unknown';
1132
+ name = `${objectName}.${methodName}`;
1133
+ }
1134
+ else {
1135
+ return null;
1136
+ }
1137
+
1138
+ return {
1139
+ line: node.loc?.start.line ?? 0,
1140
+ column: node.loc?.start.column ?? 0,
1141
+ name,
1142
+ isMethodCall
1143
+ };
1144
+ }
1145
+
1146
+ /**
1147
+ * Check if expression is CallExpression or AwaitExpression wrapping a call.
1148
+ */
1149
+ private isCallOrAwaitExpression(node: t.Expression): boolean {
1150
+ const unwrapped = this.unwrapAwaitExpression(node);
1151
+ return unwrapped.type === 'CallExpression';
1152
+ }
1153
+
1154
+ /**
1155
+ * Tracks destructuring assignments for data flow analysis.
1156
+ *
1157
+ * For ObjectPattern: creates EXPRESSION nodes representing source.property
1158
+ * For ArrayPattern: creates EXPRESSION nodes representing source[index]
1159
+ *
1160
+ * Supports:
1161
+ * - Phase 1 (REG-201): Identifier init expressions (const { x } = obj)
1162
+ * - Phase 2 (REG-223): CallExpression/AwaitExpression init (const { x } = getConfig())
1163
+ *
1164
+ * @param pattern - The destructuring pattern (ObjectPattern or ArrayPattern)
1165
+ * @param initNode - The init expression (right-hand side)
1166
+ * @param variables - Extracted variables with propertyPath/arrayIndex metadata and IDs
1167
+ * @param module - Module context
1168
+ * @param variableAssignments - Collection to push assignment info to
1169
+ */
1170
+ private trackDestructuringAssignment(
1171
+ pattern: t.ObjectPattern | t.ArrayPattern,
1172
+ initNode: t.Expression | null | undefined,
1173
+ variables: Array<ExtractedVariable & { id: string }>,
1174
+ module: VisitorModule,
1175
+ variableAssignments: VariableAssignmentInfo[]
1176
+ ): void {
1177
+ if (!initNode) return;
1178
+
1179
+ // Phase 1: Simple Identifier init expressions (REG-201)
1180
+ // Examples: const { x } = obj, const [a] = arr
1181
+ if (t.isIdentifier(initNode)) {
1182
+ const sourceBaseName = initNode.name;
1183
+
1184
+ // Process each extracted variable
1185
+ for (const varInfo of variables) {
1186
+ const variableId = varInfo.id;
1187
+
1188
+ // Handle rest elements specially - create edge to whole source
1189
+ if (varInfo.isRest) {
1190
+ variableAssignments.push({
1191
+ variableId,
1192
+ sourceType: 'VARIABLE',
1193
+ sourceName: sourceBaseName,
1194
+ line: varInfo.loc.start.line
1195
+ });
1196
+ continue;
1197
+ }
1198
+
1199
+ // ObjectPattern: const { headers } = req → headers ASSIGNED_FROM req.headers
1200
+ if (t.isObjectPattern(pattern) && varInfo.propertyPath && varInfo.propertyPath.length > 0) {
1201
+ const propertyPath = varInfo.propertyPath;
1202
+ const expressionLine = varInfo.loc.start.line;
1203
+ const expressionColumn = varInfo.loc.start.column;
1204
+
1205
+ // Build property path string (e.g., "req.headers.contentType" for nested)
1206
+ const fullPath = [sourceBaseName, ...propertyPath].join('.');
1207
+
1208
+ const expressionId = ExpressionNode.generateId(
1209
+ 'MemberExpression',
1210
+ module.file,
1211
+ expressionLine,
1212
+ expressionColumn
1213
+ );
1214
+
1215
+ variableAssignments.push({
1216
+ variableId,
1217
+ sourceType: 'EXPRESSION',
1218
+ sourceId: expressionId,
1219
+ expressionType: 'MemberExpression',
1220
+ object: sourceBaseName,
1221
+ property: propertyPath[propertyPath.length - 1], // Last property for simple display
1222
+ computed: false,
1223
+ path: fullPath,
1224
+ objectSourceName: sourceBaseName, // Use objectSourceName for DERIVES_FROM edge creation
1225
+ propertyPath: propertyPath,
1226
+ file: module.file,
1227
+ line: expressionLine,
1228
+ column: expressionColumn
1229
+ });
1230
+ }
1231
+ // ArrayPattern: const [first, second] = arr → first ASSIGNED_FROM arr[0]
1232
+ else if (t.isArrayPattern(pattern) && varInfo.arrayIndex !== undefined) {
1233
+ const arrayIndex = varInfo.arrayIndex;
1234
+ const expressionLine = varInfo.loc.start.line;
1235
+ const expressionColumn = varInfo.loc.start.column;
1236
+
1237
+ // Check if we also have propertyPath (mixed destructuring: { items: [first] } = data)
1238
+ const hasPropertyPath = varInfo.propertyPath && varInfo.propertyPath.length > 0;
1239
+
1240
+ const expressionId = ExpressionNode.generateId(
1241
+ 'MemberExpression',
1242
+ module.file,
1243
+ expressionLine,
1244
+ expressionColumn
1245
+ );
1246
+
1247
+ variableAssignments.push({
1248
+ variableId,
1249
+ sourceType: 'EXPRESSION',
1250
+ sourceId: expressionId,
1251
+ expressionType: 'MemberExpression',
1252
+ object: sourceBaseName,
1253
+ property: String(arrayIndex),
1254
+ computed: true,
1255
+ objectSourceName: sourceBaseName, // Use objectSourceName for DERIVES_FROM edge creation
1256
+ arrayIndex: arrayIndex,
1257
+ propertyPath: hasPropertyPath ? varInfo.propertyPath : undefined,
1258
+ file: module.file,
1259
+ line: expressionLine,
1260
+ column: expressionColumn
1261
+ });
1262
+ }
1263
+ }
1264
+ }
1265
+ // Phase 2: CallExpression or AwaitExpression (REG-223)
1266
+ else if (this.isCallOrAwaitExpression(initNode)) {
1267
+ const unwrapped = this.unwrapAwaitExpression(initNode);
1268
+ const callInfo = this.extractCallInfo(unwrapped);
1269
+
1270
+ if (!callInfo) {
1271
+ // Unsupported call pattern (computed callee, etc.)
1272
+ return;
1273
+ }
1274
+
1275
+ const callRepresentation = `${callInfo.name}()`;
1276
+
1277
+ // Process each extracted variable
1278
+ for (const varInfo of variables) {
1279
+ const variableId = varInfo.id;
1280
+
1281
+ // Handle rest elements - create direct CALL_SITE assignment
1282
+ if (varInfo.isRest) {
1283
+ variableAssignments.push({
1284
+ variableId,
1285
+ sourceType: 'CALL_SITE',
1286
+ callName: callInfo.name,
1287
+ callLine: callInfo.line,
1288
+ callColumn: callInfo.column,
1289
+ callSourceLine: callInfo.line,
1290
+ callSourceColumn: callInfo.column,
1291
+ callSourceFile: module.file,
1292
+ callSourceName: callInfo.name,
1293
+ line: varInfo.loc.start.line
1294
+ });
1295
+ continue;
1296
+ }
1297
+
1298
+ // ObjectPattern: const { data } = fetchUser() → data ASSIGNED_FROM fetchUser().data
1299
+ if (t.isObjectPattern(pattern) && varInfo.propertyPath && varInfo.propertyPath.length > 0) {
1300
+ const propertyPath = varInfo.propertyPath;
1301
+ const expressionLine = varInfo.loc.start.line;
1302
+ const expressionColumn = varInfo.loc.start.column;
1303
+
1304
+ // Build property path string: "fetchUser().data" or "fetchUser().user.name"
1305
+ const fullPath = [callRepresentation, ...propertyPath].join('.');
1306
+
1307
+ const expressionId = ExpressionNode.generateId(
1308
+ 'MemberExpression',
1309
+ module.file,
1310
+ expressionLine,
1311
+ expressionColumn
1312
+ );
1313
+
1314
+ variableAssignments.push({
1315
+ variableId,
1316
+ sourceType: 'EXPRESSION',
1317
+ sourceId: expressionId,
1318
+ expressionType: 'MemberExpression',
1319
+ object: callRepresentation, // "fetchUser()" - display name
1320
+ property: propertyPath[propertyPath.length - 1],
1321
+ computed: false,
1322
+ path: fullPath, // "fetchUser().data"
1323
+ propertyPath: propertyPath, // ["data"]
1324
+ // Call source for DERIVES_FROM lookup (REG-223)
1325
+ callSourceLine: callInfo.line,
1326
+ callSourceColumn: callInfo.column,
1327
+ callSourceFile: module.file,
1328
+ callSourceName: callInfo.name,
1329
+ sourceMetadata: {
1330
+ sourceType: callInfo.isMethodCall ? 'method-call' : 'call'
1331
+ },
1332
+ file: module.file,
1333
+ line: expressionLine,
1334
+ column: expressionColumn
1335
+ });
1336
+ }
1337
+ // ArrayPattern: const [first] = arr.map(fn) → first ASSIGNED_FROM arr.map(fn)[0]
1338
+ else if (t.isArrayPattern(pattern) && varInfo.arrayIndex !== undefined) {
1339
+ const arrayIndex = varInfo.arrayIndex;
1340
+ const expressionLine = varInfo.loc.start.line;
1341
+ const expressionColumn = varInfo.loc.start.column;
1342
+
1343
+ const hasPropertyPath = varInfo.propertyPath && varInfo.propertyPath.length > 0;
1344
+
1345
+ const expressionId = ExpressionNode.generateId(
1346
+ 'MemberExpression',
1347
+ module.file,
1348
+ expressionLine,
1349
+ expressionColumn
1350
+ );
1351
+
1352
+ variableAssignments.push({
1353
+ variableId,
1354
+ sourceType: 'EXPRESSION',
1355
+ sourceId: expressionId,
1356
+ expressionType: 'MemberExpression',
1357
+ object: callRepresentation,
1358
+ property: String(arrayIndex),
1359
+ computed: true,
1360
+ arrayIndex: arrayIndex,
1361
+ propertyPath: hasPropertyPath ? varInfo.propertyPath : undefined,
1362
+ // Call source for DERIVES_FROM lookup (REG-223)
1363
+ callSourceLine: callInfo.line,
1364
+ callSourceColumn: callInfo.column,
1365
+ callSourceFile: module.file,
1366
+ callSourceName: callInfo.name,
1367
+ sourceMetadata: {
1368
+ sourceType: callInfo.isMethodCall ? 'method-call' : 'call'
1369
+ },
1370
+ file: module.file,
1371
+ line: expressionLine,
1372
+ column: expressionColumn
1373
+ });
1374
+ }
1375
+ }
1376
+ }
1377
+ // Unsupported init type (MemberExpression without call, etc.)
1378
+ // else: do nothing - skip silently
1379
+ }
1380
+
821
1381
  /**
822
1382
  * Получить все MODULE ноды из графа
823
1383
  */
@@ -855,11 +1415,17 @@ export class JSASTAnalyzer extends Plugin {
855
1415
  const functions: FunctionInfo[] = [];
856
1416
  const parameters: ParameterInfo[] = [];
857
1417
  const scopes: ScopeInfo[] = [];
1418
+ // Branching (switch statements)
1419
+ const branches: BranchInfo[] = [];
1420
+ const cases: CaseInfo[] = [];
1421
+ // Control flow (loops)
1422
+ const loops: LoopInfo[] = [];
858
1423
  const variableDeclarations: VariableDeclarationInfo[] = [];
859
1424
  const callSites: CallSiteInfo[] = [];
860
1425
  const methodCalls: MethodCallInfo[] = [];
861
1426
  const eventListeners: EventListenerInfo[] = [];
862
1427
  const classInstantiations: ClassInstantiationInfo[] = [];
1428
+ const constructorCalls: ConstructorCallInfo[] = [];
863
1429
  const classDeclarations: ClassDeclarationInfo[] = [];
864
1430
  const methodCallbacks: MethodCallbackInfo[] = [];
865
1431
  const callArguments: CallArgumentInfo[] = [];
@@ -882,6 +1448,16 @@ export class JSASTAnalyzer extends Plugin {
882
1448
  const arrayMutations: ArrayMutationInfo[] = [];
883
1449
  // Object mutation tracking for FLOWS_INTO edges
884
1450
  const objectMutations: ObjectMutationInfo[] = [];
1451
+ // Variable reassignment tracking for FLOWS_INTO edges (REG-290)
1452
+ const variableReassignments: VariableReassignmentInfo[] = [];
1453
+ // Return statement tracking for RETURNS edges
1454
+ const returnStatements: ReturnStatementInfo[] = [];
1455
+ // Update expression tracking for MODIFIES edges (REG-288, REG-312)
1456
+ const updateExpressions: UpdateExpressionInfo[] = [];
1457
+ // Promise resolution tracking for RESOLVES_TO edges (REG-334)
1458
+ const promiseResolutions: PromiseResolutionInfo[] = [];
1459
+ // Promise executor contexts (REG-334) - keyed by executor function's start:end position
1460
+ const promiseExecutorContexts = new Map<string, PromiseExecutorContext>();
885
1461
 
886
1462
  const ifScopeCounterRef: CounterRef = { value: 0 };
887
1463
  const scopeCounterRef: CounterRef = { value: 0 };
@@ -893,6 +1469,8 @@ export class JSASTAnalyzer extends Plugin {
893
1469
  const anonymousFunctionCounterRef: CounterRef = { value: 0 };
894
1470
  const objectLiteralCounterRef: CounterRef = { value: 0 };
895
1471
  const arrayLiteralCounterRef: CounterRef = { value: 0 };
1472
+ const branchCounterRef: CounterRef = { value: 0 };
1473
+ const caseCounterRef: CounterRef = { value: 0 };
896
1474
 
897
1475
  const processedNodes: ProcessedNodes = {
898
1476
  functions: new Set(),
@@ -921,7 +1499,7 @@ export class JSASTAnalyzer extends Plugin {
921
1499
  this.profiler.start('traverse_variables');
922
1500
  const variableVisitor = new VariableVisitor(
923
1501
  module,
924
- { variableDeclarations, classInstantiations, literals, variableAssignments, varDeclCounterRef, literalCounterRef },
1502
+ { variableDeclarations, classInstantiations, literals, variableAssignments, varDeclCounterRef, literalCounterRef, scopes, scopeCounterRef, objectLiterals, objectProperties, objectLiteralCounterRef },
925
1503
  this.extractVariableNamesFromPattern.bind(this),
926
1504
  this.trackVariableAssignment.bind(this) as TrackVariableAssignmentCallback,
927
1505
  scopeTracker // Pass ScopeTracker for semantic ID generation
@@ -930,8 +1508,13 @@ export class JSASTAnalyzer extends Plugin {
930
1508
  this.profiler.end('traverse_variables');
931
1509
 
932
1510
  const allCollections: Collections = {
933
- functions, parameters, scopes, variableDeclarations, callSites, methodCalls,
934
- eventListeners, methodCallbacks, callArguments, classInstantiations, classDeclarations,
1511
+ functions, parameters, scopes,
1512
+ // Branching (switch statements)
1513
+ branches, cases,
1514
+ // Control flow (loops)
1515
+ loops,
1516
+ variableDeclarations, callSites, methodCalls,
1517
+ eventListeners, methodCallbacks, callArguments, classInstantiations, constructorCalls, classDeclarations,
935
1518
  httpRequests, literals, variableAssignments,
936
1519
  // TypeScript-specific collections
937
1520
  interfaces, typeAliases, enums, decorators,
@@ -941,10 +1524,21 @@ export class JSASTAnalyzer extends Plugin {
941
1524
  arrayMutations,
942
1525
  // Object mutation tracking
943
1526
  objectMutations,
1527
+ // Variable reassignment tracking (REG-290)
1528
+ variableReassignments,
1529
+ // Return statement tracking
1530
+ returnStatements,
1531
+ // Update expression tracking (REG-288, REG-312)
1532
+ updateExpressions,
1533
+ // Promise resolution tracking (REG-334)
1534
+ promiseResolutions,
1535
+ promiseExecutorContexts,
944
1536
  objectLiteralCounterRef, arrayLiteralCounterRef,
945
1537
  ifScopeCounterRef, scopeCounterRef, varDeclCounterRef,
946
1538
  callSiteCounterRef, functionCounterRef, httpRequestCounterRef,
947
- literalCounterRef, anonymousFunctionCounterRef, processedNodes,
1539
+ literalCounterRef, anonymousFunctionCounterRef,
1540
+ branchCounterRef, caseCounterRef,
1541
+ processedNodes,
948
1542
  imports, exports, code,
949
1543
  // VisitorCollections compatibility
950
1544
  classes: classDeclarations,
@@ -1025,8 +1619,22 @@ export class JSASTAnalyzer extends Plugin {
1025
1619
  scopeTracker.exitScope();
1026
1620
  }
1027
1621
 
1622
+ // === VARIABLE REASSIGNMENT (REG-290) ===
1623
+ // Check if LHS is simple identifier (not obj.prop, not arr[i])
1624
+ // Must be checked at module level too
1625
+ if (assignNode.left.type === 'Identifier') {
1626
+ // Initialize collection if not exists
1627
+ if (!allCollections.variableReassignments) {
1628
+ allCollections.variableReassignments = [];
1629
+ }
1630
+ const variableReassignments = allCollections.variableReassignments as VariableReassignmentInfo[];
1631
+
1632
+ this.detectVariableReassignment(assignNode, module, variableReassignments, scopeTracker);
1633
+ }
1634
+ // === END VARIABLE REASSIGNMENT ===
1635
+
1028
1636
  // Check for indexed array assignment at module level: arr[i] = value
1029
- this.detectIndexedArrayAssignment(assignNode, module, arrayMutations);
1637
+ this.detectIndexedArrayAssignment(assignNode, module, arrayMutations, scopeTracker);
1030
1638
 
1031
1639
  // Check for object property assignment at module level: obj.prop = value
1032
1640
  this.detectObjectPropertyAssignment(assignNode, module, objectMutations, scopeTracker);
@@ -1034,6 +1642,20 @@ export class JSASTAnalyzer extends Plugin {
1034
1642
  });
1035
1643
  this.profiler.end('traverse_assignments');
1036
1644
 
1645
+ // Module-level UpdateExpression (obj.count++, arr[i]++, i++) - REG-288/REG-312
1646
+ this.profiler.start('traverse_updates');
1647
+ traverse(ast, {
1648
+ UpdateExpression: (updatePath: NodePath<t.UpdateExpression>) => {
1649
+ // Skip if inside a function - analyzeFunctionBody handles those
1650
+ const functionParent = updatePath.getFunctionParent();
1651
+ if (functionParent) return;
1652
+
1653
+ // Module-level update expression: no parentScopeId
1654
+ this.collectUpdateExpression(updatePath.node, module, updateExpressions, undefined, scopeTracker);
1655
+ }
1656
+ });
1657
+ this.profiler.end('traverse_updates');
1658
+
1037
1659
  // Classes
1038
1660
  this.profiler.start('traverse_classes');
1039
1661
  const classVisitor = new ClassVisitor(
@@ -1106,6 +1728,79 @@ export class JSASTAnalyzer extends Plugin {
1106
1728
  traverse(ast, callExpressionVisitor.getHandlers());
1107
1729
  this.profiler.end('traverse_calls');
1108
1730
 
1731
+ // Module-level NewExpression (constructor calls)
1732
+ // This handles top-level code like `const x = new Date()` that's not inside a function
1733
+ this.profiler.start('traverse_new');
1734
+ const processedConstructorCalls = new Set<string>();
1735
+ traverse(ast, {
1736
+ NewExpression: (newPath: NodePath<t.NewExpression>) => {
1737
+ const newNode = newPath.node;
1738
+ const nodeKey = `constructor:new:${newNode.start}:${newNode.end}`;
1739
+ if (processedConstructorCalls.has(nodeKey)) {
1740
+ return;
1741
+ }
1742
+ processedConstructorCalls.add(nodeKey);
1743
+
1744
+ // Determine className from callee
1745
+ let className: string | null = null;
1746
+ if (newNode.callee.type === 'Identifier') {
1747
+ className = newNode.callee.name;
1748
+ } else if (newNode.callee.type === 'MemberExpression' && newNode.callee.property.type === 'Identifier') {
1749
+ className = newNode.callee.property.name;
1750
+ }
1751
+
1752
+ if (className) {
1753
+ const line = getLine(newNode);
1754
+ const column = getColumn(newNode);
1755
+ const constructorCallId = ConstructorCallNode.generateId(className, module.file, line, column);
1756
+ const isBuiltin = ConstructorCallNode.isBuiltinConstructor(className);
1757
+
1758
+ constructorCalls.push({
1759
+ id: constructorCallId,
1760
+ type: 'CONSTRUCTOR_CALL',
1761
+ className,
1762
+ isBuiltin,
1763
+ file: module.file,
1764
+ line,
1765
+ column
1766
+ });
1767
+
1768
+ // REG-334: If this is Promise constructor with executor callback,
1769
+ // register the context for resolve/reject detection
1770
+ if (className === 'Promise' && newNode.arguments.length > 0) {
1771
+ const executorArg = newNode.arguments[0];
1772
+
1773
+ // Only handle inline function expressions (not variable references)
1774
+ if (t.isArrowFunctionExpression(executorArg) || t.isFunctionExpression(executorArg)) {
1775
+ // Extract resolve/reject parameter names
1776
+ let resolveName: string | undefined;
1777
+ let rejectName: string | undefined;
1778
+
1779
+ if (executorArg.params.length > 0 && t.isIdentifier(executorArg.params[0])) {
1780
+ resolveName = executorArg.params[0].name;
1781
+ }
1782
+ if (executorArg.params.length > 1 && t.isIdentifier(executorArg.params[1])) {
1783
+ rejectName = executorArg.params[1].name;
1784
+ }
1785
+
1786
+ if (resolveName) {
1787
+ // Key by function node position to allow nested Promise detection
1788
+ const funcKey = `${executorArg.start}:${executorArg.end}`;
1789
+ promiseExecutorContexts.set(funcKey, {
1790
+ constructorCallId,
1791
+ resolveName,
1792
+ rejectName,
1793
+ file: module.file,
1794
+ line
1795
+ });
1796
+ }
1797
+ }
1798
+ }
1799
+ }
1800
+ }
1801
+ });
1802
+ this.profiler.end('traverse_new');
1803
+
1109
1804
  // Module-level IfStatements
1110
1805
  this.profiler.start('traverse_ifs');
1111
1806
  traverse(ast, {
@@ -1164,14 +1859,25 @@ export class JSASTAnalyzer extends Plugin {
1164
1859
  const result = await this.graphBuilder.build(module, graph, projectPath, {
1165
1860
  functions,
1166
1861
  scopes,
1862
+ // Branching (switch statements) - use allCollections refs as they're populated by analyzeFunctionBody
1863
+ branches: allCollections.branches || branches,
1864
+ cases: allCollections.cases || cases,
1865
+ // Control flow (loops) - use allCollections refs as they're populated by analyzeFunctionBody
1866
+ loops: allCollections.loops || loops,
1867
+ // Control flow (try/catch/finally) - Phase 4
1868
+ tryBlocks: allCollections.tryBlocks,
1869
+ catchBlocks: allCollections.catchBlocks,
1870
+ finallyBlocks: allCollections.finallyBlocks,
1167
1871
  variableDeclarations,
1168
1872
  callSites,
1169
1873
  methodCalls,
1170
1874
  eventListeners,
1171
1875
  classInstantiations,
1876
+ constructorCalls,
1172
1877
  classDeclarations,
1173
1878
  methodCallbacks,
1174
- callArguments,
1879
+ // REG-334: Use allCollections.callArguments to include function-level resolve/reject arguments
1880
+ callArguments: allCollections.callArguments || callArguments,
1175
1881
  imports,
1176
1882
  exports,
1177
1883
  httpRequests,
@@ -1187,8 +1893,17 @@ export class JSASTAnalyzer extends Plugin {
1187
1893
  arrayMutations,
1188
1894
  // Object mutation tracking
1189
1895
  objectMutations,
1896
+ // Variable reassignment tracking (REG-290)
1897
+ variableReassignments,
1898
+ // Return statement tracking
1899
+ returnStatements,
1900
+ // Update expression tracking (REG-288, REG-312)
1901
+ updateExpressions,
1902
+ // Promise resolution tracking (REG-334)
1903
+ promiseResolutions: allCollections.promiseResolutions || promiseResolutions,
1190
1904
  // Object/Array literal tracking - use allCollections refs as visitors may have created new arrays
1191
1905
  objectLiterals: allCollections.objectLiterals || objectLiterals,
1906
+ objectProperties: allCollections.objectProperties || objectProperties,
1192
1907
  arrayLiterals: allCollections.arrayLiterals || arrayLiterals
1193
1908
  });
1194
1909
  this.profiler.end('graph_build');
@@ -1263,6 +1978,9 @@ export class JSASTAnalyzer extends Plugin {
1263
1978
  * @param literalCounterRef - Counter for unique literal IDs
1264
1979
  * @param scopeTracker - Tracker for semantic ID generation
1265
1980
  * @param parentScopeVariables - Set to track variables for closure analysis
1981
+ * @param objectLiterals - Collection for object literal nodes (REG-328)
1982
+ * @param objectProperties - Collection for object property edges (REG-328)
1983
+ * @param objectLiteralCounterRef - Counter for unique object literal IDs (REG-328)
1266
1984
  */
1267
1985
  private handleVariableDeclaration(
1268
1986
  varPath: NodePath<t.VariableDeclaration>,
@@ -1275,20 +1993,30 @@ export class JSASTAnalyzer extends Plugin {
1275
1993
  varDeclCounterRef: CounterRef,
1276
1994
  literalCounterRef: CounterRef,
1277
1995
  scopeTracker: ScopeTracker | undefined,
1278
- parentScopeVariables: Set<{ name: string; id: string; scopeId: string }>
1996
+ parentScopeVariables: Set<{ name: string; id: string; scopeId: string }>,
1997
+ objectLiterals: ObjectLiteralInfo[],
1998
+ objectProperties: ObjectPropertyInfo[],
1999
+ objectLiteralCounterRef: CounterRef
1279
2000
  ): void {
1280
2001
  const varNode = varPath.node;
1281
2002
  const isConst = varNode.kind === 'const';
1282
2003
 
2004
+ // Check if this is a loop variable (for...of or for...in)
2005
+ const parent = varPath.parent;
2006
+ const isLoopVariable = (t.isForOfStatement(parent) || t.isForInStatement(parent)) && parent.left === varNode;
2007
+
1283
2008
  varNode.declarations.forEach(declarator => {
1284
2009
  const variables = this.extractVariableNamesFromPattern(declarator.id);
2010
+ const variablesWithIds: Array<ExtractedVariable & { id: string }> = [];
1285
2011
 
1286
2012
  variables.forEach(varInfo => {
1287
2013
  const literalValue = declarator.init ? ExpressionEvaluator.extractLiteralValue(declarator.init) : null;
1288
2014
  const isLiteral = literalValue !== null;
1289
2015
  const isNewExpression = declarator.init && declarator.init.type === 'NewExpression';
1290
2016
 
1291
- const shouldBeConstant = isConst && (isLiteral || isNewExpression);
2017
+ // Loop variables with const should be CONSTANT (they can't be reassigned in loop body)
2018
+ // Regular variables with const are CONSTANT only if initialized with literal or new expression
2019
+ const shouldBeConstant = isConst && (isLoopVariable || isLiteral || isNewExpression);
1292
2020
  const nodeType = shouldBeConstant ? 'CONSTANT' : 'VARIABLE';
1293
2021
 
1294
2022
  // Generate semantic ID (primary) or legacy ID (fallback)
@@ -1298,6 +2026,9 @@ export class JSASTAnalyzer extends Plugin {
1298
2026
  ? computeSemanticId(nodeType, varInfo.name, scopeTracker.getContext())
1299
2027
  : legacyId;
1300
2028
 
2029
+ // Collect variable info with ID for destructuring tracking
2030
+ variablesWithIds.push({ ...varInfo, id: varId });
2031
+
1301
2032
  parentScopeVariables.add({
1302
2033
  name: varInfo.name,
1303
2034
  id: varId,
@@ -1341,26 +2072,263 @@ export class JSASTAnalyzer extends Plugin {
1341
2072
  parentScopeId
1342
2073
  });
1343
2074
  }
2075
+ });
1344
2076
 
1345
- if (declarator.init) {
1346
- this.trackVariableAssignment(declarator.init, varId, varInfo.name, module, varInfo.loc.start.line, literals, variableAssignments, literalCounterRef);
2077
+ // Track assignments after all variables are created
2078
+ if (isLoopVariable) {
2079
+ // For loop variables, track assignment from the source collection (right side of for...of/for...in)
2080
+ const loopParent = parent as t.ForOfStatement | t.ForInStatement;
2081
+ const sourceExpression = loopParent.right;
2082
+
2083
+ if (t.isObjectPattern(declarator.id) || t.isArrayPattern(declarator.id)) {
2084
+ // Destructuring in loop: track each variable separately
2085
+ this.trackDestructuringAssignment(
2086
+ declarator.id,
2087
+ sourceExpression,
2088
+ variablesWithIds,
2089
+ module,
2090
+ variableAssignments
2091
+ );
2092
+ } else {
2093
+ // Simple loop variable: create DERIVES_FROM edges (not ASSIGNED_FROM)
2094
+ // Loop variables derive their values from the collection (semantic difference)
2095
+ variablesWithIds.forEach(varInfo => {
2096
+ if (t.isIdentifier(sourceExpression)) {
2097
+ variableAssignments.push({
2098
+ variableId: varInfo.id,
2099
+ sourceType: 'DERIVES_FROM_VARIABLE',
2100
+ sourceName: sourceExpression.name,
2101
+ file: module.file,
2102
+ line: varInfo.loc.start.line
2103
+ });
2104
+ } else {
2105
+ // Fallback to regular tracking for non-identifier expressions
2106
+ this.trackVariableAssignment(
2107
+ sourceExpression,
2108
+ varInfo.id,
2109
+ varInfo.name,
2110
+ module,
2111
+ varInfo.loc.start.line,
2112
+ literals,
2113
+ variableAssignments,
2114
+ literalCounterRef,
2115
+ objectLiterals,
2116
+ objectProperties,
2117
+ objectLiteralCounterRef
2118
+ );
2119
+ }
2120
+ });
1347
2121
  }
1348
- });
2122
+ } else if (declarator.init) {
2123
+ // Regular variable declaration with initializer
2124
+ if (t.isObjectPattern(declarator.id) || t.isArrayPattern(declarator.id)) {
2125
+ // Destructuring: use specialized tracking
2126
+ this.trackDestructuringAssignment(
2127
+ declarator.id,
2128
+ declarator.init,
2129
+ variablesWithIds,
2130
+ module,
2131
+ variableAssignments
2132
+ );
2133
+ } else {
2134
+ // Simple assignment: use existing tracking
2135
+ const varInfo = variablesWithIds[0];
2136
+ this.trackVariableAssignment(
2137
+ declarator.init,
2138
+ varInfo.id,
2139
+ varInfo.name,
2140
+ module,
2141
+ varInfo.loc.start.line,
2142
+ literals,
2143
+ variableAssignments,
2144
+ literalCounterRef,
2145
+ objectLiterals,
2146
+ objectProperties,
2147
+ objectLiteralCounterRef
2148
+ );
2149
+ }
2150
+ }
1349
2151
  });
1350
2152
  }
1351
2153
 
1352
2154
  private createLoopScopeHandler(
1353
2155
  trackerScopeType: string,
1354
2156
  scopeType: string,
2157
+ loopType: 'for' | 'for-in' | 'for-of' | 'while' | 'do-while',
1355
2158
  parentScopeId: string,
1356
2159
  module: VisitorModule,
1357
2160
  scopes: ScopeInfo[],
2161
+ loops: LoopInfo[],
1358
2162
  scopeCounterRef: CounterRef,
1359
- scopeTracker: ScopeTracker | undefined
2163
+ loopCounterRef: CounterRef,
2164
+ scopeTracker: ScopeTracker | undefined,
2165
+ scopeIdStack?: string[],
2166
+ controlFlowState?: { loopCount: number }
1360
2167
  ): { enter: (path: NodePath<t.Loop>) => void; exit: () => void } {
1361
2168
  return {
1362
2169
  enter: (path: NodePath<t.Loop>) => {
1363
2170
  const node = path.node;
2171
+
2172
+ // Phase 6 (REG-267): Increment loop count for cyclomatic complexity
2173
+ if (controlFlowState) {
2174
+ controlFlowState.loopCount++;
2175
+ }
2176
+
2177
+ // 1. Create LOOP node
2178
+ const loopCounter = loopCounterRef.value++;
2179
+ const legacyLoopId = `${module.file}:LOOP:${loopType}:${getLine(node)}:${loopCounter}`;
2180
+ const loopId = scopeTracker
2181
+ ? computeSemanticId('LOOP', loopType, scopeTracker.getContext(), { discriminator: loopCounter })
2182
+ : legacyLoopId;
2183
+
2184
+ // 2. Extract iteration target for for-in/for-of
2185
+ let iteratesOverName: string | undefined;
2186
+ let iteratesOverLine: number | undefined;
2187
+ let iteratesOverColumn: number | undefined;
2188
+
2189
+ if (loopType === 'for-in' || loopType === 'for-of') {
2190
+ const loopNode = node as t.ForInStatement | t.ForOfStatement;
2191
+ if (t.isIdentifier(loopNode.right)) {
2192
+ iteratesOverName = loopNode.right.name;
2193
+ iteratesOverLine = getLine(loopNode.right);
2194
+ iteratesOverColumn = getColumn(loopNode.right);
2195
+ } else if (t.isMemberExpression(loopNode.right)) {
2196
+ iteratesOverName = this.memberExpressionToString(loopNode.right);
2197
+ iteratesOverLine = getLine(loopNode.right);
2198
+ iteratesOverColumn = getColumn(loopNode.right);
2199
+ }
2200
+ }
2201
+
2202
+ // 2b. Extract init/test/update for classic for loops and test for while/do-while (REG-282)
2203
+ let initVariableName: string | undefined;
2204
+ let initLine: number | undefined;
2205
+
2206
+ let testExpressionId: string | undefined;
2207
+ let testExpressionType: string | undefined;
2208
+ let testLine: number | undefined;
2209
+ let testColumn: number | undefined;
2210
+
2211
+ let updateExpressionId: string | undefined;
2212
+ let updateExpressionType: string | undefined;
2213
+ let updateLine: number | undefined;
2214
+ let updateColumn: number | undefined;
2215
+
2216
+ if (loopType === 'for') {
2217
+ const forNode = node as t.ForStatement;
2218
+
2219
+ // Extract init: let i = 0
2220
+ if (forNode.init) {
2221
+ initLine = getLine(forNode.init);
2222
+ if (t.isVariableDeclaration(forNode.init)) {
2223
+ // Get name of first declared variable
2224
+ const firstDeclarator = forNode.init.declarations[0];
2225
+ if (t.isIdentifier(firstDeclarator.id)) {
2226
+ initVariableName = firstDeclarator.id.name;
2227
+ }
2228
+ }
2229
+ }
2230
+
2231
+ // Extract test: i < 10
2232
+ if (forNode.test) {
2233
+ testLine = getLine(forNode.test);
2234
+ testColumn = getColumn(forNode.test);
2235
+ testExpressionType = forNode.test.type;
2236
+ testExpressionId = ExpressionNode.generateId(forNode.test.type, module.file, testLine, testColumn);
2237
+ }
2238
+
2239
+ // Extract update: i++
2240
+ if (forNode.update) {
2241
+ updateLine = getLine(forNode.update);
2242
+ updateColumn = getColumn(forNode.update);
2243
+ updateExpressionType = forNode.update.type;
2244
+ updateExpressionId = ExpressionNode.generateId(forNode.update.type, module.file, updateLine, updateColumn);
2245
+ }
2246
+ }
2247
+
2248
+ // Extract test condition for while and do-while loops
2249
+ if (loopType === 'while' || loopType === 'do-while') {
2250
+ const condLoop = node as t.WhileStatement | t.DoWhileStatement;
2251
+ if (condLoop.test) {
2252
+ testLine = getLine(condLoop.test);
2253
+ testColumn = getColumn(condLoop.test);
2254
+ testExpressionType = condLoop.test.type;
2255
+ testExpressionId = ExpressionNode.generateId(condLoop.test.type, module.file, testLine, testColumn);
2256
+ }
2257
+ }
2258
+
2259
+ // Extract async flag for for-await-of (REG-284)
2260
+ let isAsync: boolean | undefined;
2261
+ if (loopType === 'for-of') {
2262
+ const forOfNode = node as t.ForOfStatement;
2263
+ isAsync = forOfNode.await === true ? true : undefined;
2264
+ }
2265
+
2266
+ // 3. Determine actual parent - use stack for nested loops, otherwise original parentScopeId
2267
+ const actualParentScopeId = (scopeIdStack && scopeIdStack.length > 0)
2268
+ ? scopeIdStack[scopeIdStack.length - 1]
2269
+ : parentScopeId;
2270
+
2271
+ // 3.5. Extract condition expression for while/do-while/for loops (REG-280)
2272
+ // Note: for-in and for-of don't have test expressions (they use ITERATES_OVER instead)
2273
+ let conditionExpressionId: string | undefined;
2274
+ let conditionExpressionType: string | undefined;
2275
+ let conditionLine: number | undefined;
2276
+ let conditionColumn: number | undefined;
2277
+
2278
+ if (loopType === 'while' || loopType === 'do-while') {
2279
+ const testNode = (node as t.WhileStatement | t.DoWhileStatement).test;
2280
+ if (testNode) {
2281
+ const condResult = this.extractDiscriminantExpression(testNode, module);
2282
+ conditionExpressionId = condResult.id;
2283
+ conditionExpressionType = condResult.expressionType;
2284
+ conditionLine = condResult.line;
2285
+ conditionColumn = condResult.column;
2286
+ }
2287
+ } else if (loopType === 'for') {
2288
+ const forNode = node as t.ForStatement;
2289
+ // for loop test may be null (infinite loop: for(;;))
2290
+ if (forNode.test) {
2291
+ const condResult = this.extractDiscriminantExpression(forNode.test, module);
2292
+ conditionExpressionId = condResult.id;
2293
+ conditionExpressionType = condResult.expressionType;
2294
+ conditionLine = condResult.line;
2295
+ conditionColumn = condResult.column;
2296
+ }
2297
+ }
2298
+
2299
+ // 4. Push LOOP info
2300
+ loops.push({
2301
+ id: loopId,
2302
+ semanticId: loopId,
2303
+ type: 'LOOP',
2304
+ loopType,
2305
+ file: module.file,
2306
+ line: getLine(node),
2307
+ column: getColumn(node),
2308
+ parentScopeId: actualParentScopeId,
2309
+ iteratesOverName,
2310
+ iteratesOverLine,
2311
+ iteratesOverColumn,
2312
+ conditionExpressionId,
2313
+ conditionExpressionType,
2314
+ conditionLine,
2315
+ conditionColumn,
2316
+ // REG-282: init/test/update for classic for loops
2317
+ initVariableName,
2318
+ initLine,
2319
+ testExpressionId,
2320
+ testExpressionType,
2321
+ testLine,
2322
+ testColumn,
2323
+ updateExpressionId,
2324
+ updateExpressionType,
2325
+ updateLine,
2326
+ updateColumn,
2327
+ // REG-284: async flag for for-await-of
2328
+ async: isAsync
2329
+ });
2330
+
2331
+ // 5. Create body SCOPE (backward compatibility)
1364
2332
  const scopeId = `SCOPE#${scopeType}#${module.file}#${getLine(node)}:${scopeCounterRef.value++}`;
1365
2333
  const semanticId = this.generateSemanticId(scopeType, scopeTracker);
1366
2334
  scopes.push({
@@ -1370,15 +2338,26 @@ export class JSASTAnalyzer extends Plugin {
1370
2338
  semanticId,
1371
2339
  file: module.file,
1372
2340
  line: getLine(node),
1373
- parentScopeId
2341
+ parentScopeId: loopId // Parent is LOOP, not original parentScopeId
1374
2342
  });
1375
2343
 
2344
+ // 6. Push body SCOPE to scopeIdStack (for CONTAINS edges to nested items)
2345
+ // The body scope is the container for nested loops, not the LOOP itself
2346
+ if (scopeIdStack) {
2347
+ scopeIdStack.push(scopeId);
2348
+ }
2349
+
1376
2350
  // Enter scope for semantic ID generation
1377
2351
  if (scopeTracker) {
1378
2352
  scopeTracker.enterCountedScope(trackerScopeType);
1379
2353
  }
1380
2354
  },
1381
2355
  exit: () => {
2356
+ // Pop loop scope from stack
2357
+ if (scopeIdStack) {
2358
+ scopeIdStack.pop();
2359
+ }
2360
+
1382
2361
  // Exit scope
1383
2362
  if (scopeTracker) {
1384
2363
  scopeTracker.exitScope();
@@ -1388,257 +2367,687 @@ export class JSASTAnalyzer extends Plugin {
1388
2367
  }
1389
2368
 
1390
2369
  /**
1391
- * Process VariableDeclarations within a try/catch/finally block.
1392
- * This is a simplified version that doesn't track parentScopeVariables or class instantiations.
2370
+ * Factory method to create TryStatement handler.
2371
+ * Creates TRY_BLOCK, CATCH_BLOCK, FINALLY_BLOCK nodes and body SCOPEs.
2372
+ * Does NOT use skip() - allows normal traversal for CallExpression/NewExpression visitors.
1393
2373
  *
1394
- * @param blockPath - The NodePath for the block to process
1395
- * @param blockScopeId - The scope ID for variables in this block
2374
+ * Phase 4 (REG-267): Creates control flow nodes with HAS_CATCH and HAS_FINALLY edges.
2375
+ *
2376
+ * @param parentScopeId - Parent scope ID for the scope nodes
1396
2377
  * @param module - Module context
1397
- * @param variableDeclarations - Collection to push variable declarations to
1398
- * @param literals - Collection for literal tracking
1399
- * @param variableAssignments - Collection for variable assignment tracking
1400
- * @param varDeclCounterRef - Counter for unique variable declaration IDs
1401
- * @param literalCounterRef - Counter for unique literal IDs
2378
+ * @param scopes - Collection to push scope nodes to
2379
+ * @param tryBlocks - Collection to push TRY_BLOCK nodes to
2380
+ * @param catchBlocks - Collection to push CATCH_BLOCK nodes to
2381
+ * @param finallyBlocks - Collection to push FINALLY_BLOCK nodes to
2382
+ * @param scopeCounterRef - Counter for unique scope IDs
2383
+ * @param tryBlockCounterRef - Counter for unique TRY_BLOCK IDs
2384
+ * @param catchBlockCounterRef - Counter for unique CATCH_BLOCK IDs
2385
+ * @param finallyBlockCounterRef - Counter for unique FINALLY_BLOCK IDs
1402
2386
  * @param scopeTracker - Tracker for semantic ID generation
2387
+ * @param tryScopeMap - Map to track try/catch/finally scope transitions
2388
+ * @param scopeIdStack - Stack for tracking current scope ID for CONTAINS edges
1403
2389
  */
1404
- private processBlockVariables(
1405
- blockPath: NodePath,
1406
- blockScopeId: string,
2390
+ private createTryStatementHandler(
2391
+ parentScopeId: string,
1407
2392
  module: VisitorModule,
1408
- variableDeclarations: VariableDeclarationInfo[],
1409
- literals: LiteralInfo[],
1410
- variableAssignments: VariableAssignmentInfo[],
1411
- varDeclCounterRef: CounterRef,
1412
- literalCounterRef: CounterRef,
1413
- scopeTracker: ScopeTracker | undefined
1414
- ): void {
1415
- blockPath.traverse({
1416
- VariableDeclaration: (varPath: NodePath<t.VariableDeclaration>) => {
1417
- const varNode = varPath.node;
1418
- const isConst = varNode.kind === 'const';
2393
+ scopes: ScopeInfo[],
2394
+ tryBlocks: TryBlockInfo[],
2395
+ catchBlocks: CatchBlockInfo[],
2396
+ finallyBlocks: FinallyBlockInfo[],
2397
+ scopeCounterRef: CounterRef,
2398
+ tryBlockCounterRef: CounterRef,
2399
+ catchBlockCounterRef: CounterRef,
2400
+ finallyBlockCounterRef: CounterRef,
2401
+ scopeTracker: ScopeTracker | undefined,
2402
+ tryScopeMap: Map<t.TryStatement, TryScopeInfo>,
2403
+ scopeIdStack?: string[],
2404
+ controlFlowState?: { hasTryCatch: boolean }
2405
+ ): { enter: (tryPath: NodePath<t.TryStatement>) => void; exit: (tryPath: NodePath<t.TryStatement>) => void } {
2406
+ return {
2407
+ enter: (tryPath: NodePath<t.TryStatement>) => {
2408
+ const tryNode = tryPath.node;
1419
2409
 
1420
- varNode.declarations.forEach(declarator => {
1421
- const variables = this.extractVariableNamesFromPattern(declarator.id);
2410
+ // Phase 6 (REG-267): Mark that this function has try/catch
2411
+ if (controlFlowState) {
2412
+ controlFlowState.hasTryCatch = true;
2413
+ }
2414
+
2415
+ // Determine actual parent - use stack for nested structures, otherwise original parentScopeId
2416
+ const actualParentScopeId = (scopeIdStack && scopeIdStack.length > 0)
2417
+ ? scopeIdStack[scopeIdStack.length - 1]
2418
+ : parentScopeId;
2419
+
2420
+ // 1. Create TRY_BLOCK node
2421
+ const tryBlockCounter = tryBlockCounterRef.value++;
2422
+ const legacyTryBlockId = `${module.file}:TRY_BLOCK:${getLine(tryNode)}:${tryBlockCounter}`;
2423
+ const tryBlockId = scopeTracker
2424
+ ? computeSemanticId('TRY_BLOCK', 'try', scopeTracker.getContext(), { discriminator: tryBlockCounter })
2425
+ : legacyTryBlockId;
2426
+
2427
+ tryBlocks.push({
2428
+ id: tryBlockId,
2429
+ semanticId: tryBlockId,
2430
+ type: 'TRY_BLOCK',
2431
+ file: module.file,
2432
+ line: getLine(tryNode),
2433
+ column: getColumn(tryNode),
2434
+ parentScopeId: actualParentScopeId
2435
+ });
2436
+
2437
+ // 2. Create try-body SCOPE (backward compatibility)
2438
+ // Parent is now TRY_BLOCK, not original parentScopeId
2439
+ const tryScopeId = `SCOPE#try-block#${module.file}#${getLine(tryNode)}:${scopeCounterRef.value++}`;
2440
+ const trySemanticId = this.generateSemanticId('try-block', scopeTracker);
2441
+ scopes.push({
2442
+ id: tryScopeId,
2443
+ type: 'SCOPE',
2444
+ scopeType: 'try-block',
2445
+ semanticId: trySemanticId,
2446
+ file: module.file,
2447
+ line: getLine(tryNode),
2448
+ parentScopeId: tryBlockId // Parent is TRY_BLOCK
2449
+ });
2450
+
2451
+ // 3. Create CATCH_BLOCK and catch-body SCOPE if handler exists
2452
+ let catchBlockId: string | null = null;
2453
+ let catchScopeId: string | null = null;
2454
+ if (tryNode.handler) {
2455
+ const catchClause = tryNode.handler;
2456
+ const catchBlockCounter = catchBlockCounterRef.value++;
2457
+ const legacyCatchBlockId = `${module.file}:CATCH_BLOCK:${getLine(catchClause)}:${catchBlockCounter}`;
2458
+ catchBlockId = scopeTracker
2459
+ ? computeSemanticId('CATCH_BLOCK', 'catch', scopeTracker.getContext(), { discriminator: catchBlockCounter })
2460
+ : legacyCatchBlockId;
2461
+
2462
+ // Extract parameter name if present
2463
+ let parameterName: string | undefined;
2464
+ if (catchClause.param && t.isIdentifier(catchClause.param)) {
2465
+ parameterName = catchClause.param.name;
2466
+ }
2467
+
2468
+ catchBlocks.push({
2469
+ id: catchBlockId,
2470
+ semanticId: catchBlockId,
2471
+ type: 'CATCH_BLOCK',
2472
+ file: module.file,
2473
+ line: getLine(catchClause),
2474
+ column: getColumn(catchClause),
2475
+ parentScopeId,
2476
+ parentTryBlockId: tryBlockId,
2477
+ parameterName
2478
+ });
2479
+
2480
+ // Create catch-body SCOPE (backward compatibility)
2481
+ catchScopeId = `SCOPE#catch-block#${module.file}#${getLine(catchClause)}:${scopeCounterRef.value++}`;
2482
+ const catchSemanticId = this.generateSemanticId('catch-block', scopeTracker);
2483
+ scopes.push({
2484
+ id: catchScopeId,
2485
+ type: 'SCOPE',
2486
+ scopeType: 'catch-block',
2487
+ semanticId: catchSemanticId,
2488
+ file: module.file,
2489
+ line: getLine(catchClause),
2490
+ parentScopeId: catchBlockId // Parent is CATCH_BLOCK
2491
+ });
2492
+ }
2493
+
2494
+ // 4. Create FINALLY_BLOCK and finally-body SCOPE if finalizer exists
2495
+ let finallyBlockId: string | null = null;
2496
+ let finallyScopeId: string | null = null;
2497
+ if (tryNode.finalizer) {
2498
+ const finallyBlockCounter = finallyBlockCounterRef.value++;
2499
+ const legacyFinallyBlockId = `${module.file}:FINALLY_BLOCK:${getLine(tryNode.finalizer)}:${finallyBlockCounter}`;
2500
+ finallyBlockId = scopeTracker
2501
+ ? computeSemanticId('FINALLY_BLOCK', 'finally', scopeTracker.getContext(), { discriminator: finallyBlockCounter })
2502
+ : legacyFinallyBlockId;
2503
+
2504
+ finallyBlocks.push({
2505
+ id: finallyBlockId,
2506
+ semanticId: finallyBlockId,
2507
+ type: 'FINALLY_BLOCK',
2508
+ file: module.file,
2509
+ line: getLine(tryNode.finalizer),
2510
+ column: getColumn(tryNode.finalizer),
2511
+ parentScopeId,
2512
+ parentTryBlockId: tryBlockId
2513
+ });
2514
+
2515
+ // Create finally-body SCOPE (backward compatibility)
2516
+ finallyScopeId = `SCOPE#finally-block#${module.file}#${getLine(tryNode.finalizer)}:${scopeCounterRef.value++}`;
2517
+ const finallySemanticId = this.generateSemanticId('finally-block', scopeTracker);
2518
+ scopes.push({
2519
+ id: finallyScopeId,
2520
+ type: 'SCOPE',
2521
+ scopeType: 'finally-block',
2522
+ semanticId: finallySemanticId,
2523
+ file: module.file,
2524
+ line: getLine(tryNode.finalizer),
2525
+ parentScopeId: finallyBlockId // Parent is FINALLY_BLOCK
2526
+ });
2527
+ }
2528
+
2529
+ // 5. Push try scope onto stack for CONTAINS edges
2530
+ if (scopeIdStack) {
2531
+ scopeIdStack.push(tryScopeId);
2532
+ }
2533
+
2534
+ // Enter try scope for semantic ID generation
2535
+ if (scopeTracker) {
2536
+ scopeTracker.enterCountedScope('try');
2537
+ }
2538
+
2539
+ // 6. Store scope info for catch/finally transitions
2540
+ tryScopeMap.set(tryNode, {
2541
+ tryScopeId,
2542
+ catchScopeId,
2543
+ finallyScopeId,
2544
+ currentBlock: 'try',
2545
+ tryBlockId,
2546
+ catchBlockId,
2547
+ finallyBlockId
2548
+ });
2549
+ },
2550
+ exit: (tryPath: NodePath<t.TryStatement>) => {
2551
+ const tryNode = tryPath.node;
2552
+ const scopeInfo = tryScopeMap.get(tryNode);
2553
+
2554
+ // Pop the current scope from stack (could be try, catch, or finally)
2555
+ if (scopeIdStack) {
2556
+ scopeIdStack.pop();
2557
+ }
2558
+
2559
+ // Exit the current scope
2560
+ if (scopeTracker) {
2561
+ scopeTracker.exitScope();
2562
+ }
1422
2563
 
1423
- variables.forEach(varInfo => {
1424
- const literalValue = declarator.init ? ExpressionEvaluator.extractLiteralValue(declarator.init) : null;
1425
- const isLiteral = literalValue !== null;
1426
- const isNewExpression = declarator.init && declarator.init.type === 'NewExpression';
1427
- const shouldBeConstant = isConst && (isLiteral || isNewExpression);
1428
- const nodeType = shouldBeConstant ? 'CONSTANT' : 'VARIABLE';
2564
+ // Clean up
2565
+ tryScopeMap.delete(tryNode);
2566
+ }
2567
+ };
2568
+ }
2569
+
2570
+ /**
2571
+ * Factory method to create CatchClause handler.
2572
+ * Handles scope transition from try to catch and processes catch parameter.
2573
+ *
2574
+ * @param module - Module context
2575
+ * @param variableDeclarations - Collection to push variable declarations to
2576
+ * @param varDeclCounterRef - Counter for unique variable declaration IDs
2577
+ * @param scopeTracker - Tracker for semantic ID generation
2578
+ * @param tryScopeMap - Map to track try/catch/finally scope transitions
2579
+ * @param scopeIdStack - Stack for tracking current scope ID for CONTAINS edges
2580
+ */
2581
+ private createCatchClauseHandler(
2582
+ module: VisitorModule,
2583
+ variableDeclarations: VariableDeclarationInfo[],
2584
+ varDeclCounterRef: CounterRef,
2585
+ scopeTracker: ScopeTracker | undefined,
2586
+ tryScopeMap: Map<t.TryStatement, TryScopeInfo>,
2587
+ scopeIdStack?: string[]
2588
+ ): { enter: (catchPath: NodePath<t.CatchClause>) => void } {
2589
+ return {
2590
+ enter: (catchPath: NodePath<t.CatchClause>) => {
2591
+ const catchNode = catchPath.node;
2592
+ const parent = catchPath.parent;
2593
+
2594
+ if (!t.isTryStatement(parent)) return;
2595
+
2596
+ const scopeInfo = tryScopeMap.get(parent);
2597
+ if (!scopeInfo || !scopeInfo.catchScopeId) return;
2598
+
2599
+ // Transition from try scope to catch scope
2600
+ if (scopeInfo.currentBlock === 'try') {
2601
+ // Pop try scope, push catch scope
2602
+ if (scopeIdStack) {
2603
+ scopeIdStack.pop();
2604
+ scopeIdStack.push(scopeInfo.catchScopeId);
2605
+ }
2606
+
2607
+ // Exit try scope, enter catch scope for semantic ID
2608
+ if (scopeTracker) {
2609
+ scopeTracker.exitScope();
2610
+ scopeTracker.enterCountedScope('catch');
2611
+ }
1429
2612
 
1430
- const legacyId = `${nodeType}#${varInfo.name}#${module.file}#${varInfo.loc.start.line}:${varInfo.loc.start.column}:${varDeclCounterRef.value++}`;
2613
+ scopeInfo.currentBlock = 'catch';
2614
+ }
2615
+
2616
+ // Handle catch parameter (e.g., catch (e) or catch ({ message }))
2617
+ if (catchNode.param) {
2618
+ const errorVarInfo = this.extractVariableNamesFromPattern(catchNode.param);
2619
+
2620
+ errorVarInfo.forEach(varInfo => {
2621
+ const legacyId = `VARIABLE#${varInfo.name}#${module.file}#${varInfo.loc.start.line}:${varInfo.loc.start.column}:${varDeclCounterRef.value++}`;
1431
2622
  const varId = scopeTracker
1432
- ? computeSemanticId(nodeType, varInfo.name, scopeTracker.getContext())
2623
+ ? computeSemanticId('VARIABLE', varInfo.name, scopeTracker.getContext())
1433
2624
  : legacyId;
1434
2625
 
1435
2626
  variableDeclarations.push({
1436
2627
  id: varId,
1437
- type: nodeType,
2628
+ type: 'VARIABLE',
1438
2629
  name: varInfo.name,
1439
2630
  file: module.file,
1440
2631
  line: varInfo.loc.start.line,
1441
- parentScopeId: blockScopeId
2632
+ parentScopeId: scopeInfo.catchScopeId!
1442
2633
  });
1443
-
1444
- if (declarator.init) {
1445
- this.trackVariableAssignment(declarator.init, varId, varInfo.name, module, varInfo.loc.start.line, literals, variableAssignments, literalCounterRef);
1446
- }
1447
2634
  });
1448
- });
2635
+ }
1449
2636
  }
1450
- });
2637
+ };
1451
2638
  }
1452
2639
 
1453
2640
  /**
1454
- * Handles TryStatement nodes within function bodies.
1455
- * Creates try, catch (with optional error parameter), and finally scopes,
1456
- * and processes variable declarations within each block.
2641
+ * Handles SwitchStatement nodes.
2642
+ * Creates BRANCH node for switch, CASE nodes for each case clause,
2643
+ * and EXPRESSION node for discriminant.
1457
2644
  *
1458
- * @param tryPath - The NodePath for the TryStatement
1459
- * @param parentScopeId - Parent scope ID for the scope nodes
2645
+ * @param switchPath - The NodePath for the SwitchStatement
2646
+ * @param parentScopeId - Parent scope ID
1460
2647
  * @param module - Module context
1461
- * @param scopes - Collection to push scope nodes to
1462
- * @param variableDeclarations - Collection to push variable declarations to
1463
- * @param literals - Collection for literal tracking
1464
- * @param variableAssignments - Collection for variable assignment tracking
1465
- * @param scopeCounterRef - Counter for unique scope IDs
1466
- * @param varDeclCounterRef - Counter for unique variable declaration IDs
1467
- * @param literalCounterRef - Counter for unique literal IDs
2648
+ * @param collections - AST collections
1468
2649
  * @param scopeTracker - Tracker for semantic ID generation
1469
2650
  */
1470
- private handleTryStatement(
1471
- tryPath: NodePath<t.TryStatement>,
2651
+ private handleSwitchStatement(
2652
+ switchPath: NodePath<t.SwitchStatement>,
1472
2653
  parentScopeId: string,
1473
2654
  module: VisitorModule,
1474
- scopes: ScopeInfo[],
1475
- variableDeclarations: VariableDeclarationInfo[],
1476
- literals: LiteralInfo[],
1477
- variableAssignments: VariableAssignmentInfo[],
1478
- scopeCounterRef: CounterRef,
1479
- varDeclCounterRef: CounterRef,
1480
- literalCounterRef: CounterRef,
1481
- scopeTracker: ScopeTracker | undefined
2655
+ collections: VisitorCollections,
2656
+ scopeTracker: ScopeTracker | undefined,
2657
+ controlFlowState?: { branchCount: number; caseCount: number }
1482
2658
  ): void {
1483
- const tryNode = tryPath.node;
1484
-
1485
- // Create and process try block
1486
- const tryScopeId = `SCOPE#try-block#${module.file}#${getLine(tryNode)}:${scopeCounterRef.value++}`;
1487
- const trySemanticId = this.generateSemanticId('try-block', scopeTracker);
1488
- scopes.push({
1489
- id: tryScopeId,
1490
- type: 'SCOPE',
1491
- scopeType: 'try-block',
1492
- semanticId: trySemanticId,
1493
- file: module.file,
1494
- line: getLine(tryNode),
1495
- parentScopeId
1496
- });
2659
+ const switchNode = switchPath.node;
2660
+
2661
+ // Phase 6 (REG-267): Count branch and non-default cases for cyclomatic complexity
2662
+ if (controlFlowState) {
2663
+ controlFlowState.branchCount++; // switch itself is a branch
2664
+ // Count non-default cases
2665
+ for (const caseNode of switchNode.cases) {
2666
+ if (caseNode.test !== null) { // Not default case
2667
+ controlFlowState.caseCount++;
2668
+ }
2669
+ }
2670
+ }
1497
2671
 
1498
- if (scopeTracker) {
1499
- scopeTracker.enterCountedScope('try');
1500
- }
1501
- this.processBlockVariables(
1502
- tryPath.get('block'),
1503
- tryScopeId,
1504
- module,
1505
- variableDeclarations,
1506
- literals,
1507
- variableAssignments,
1508
- varDeclCounterRef,
1509
- literalCounterRef,
1510
- scopeTracker
1511
- );
1512
- if (scopeTracker) {
1513
- scopeTracker.exitScope();
2672
+ // Initialize collections if not exist
2673
+ if (!collections.branches) {
2674
+ collections.branches = [];
2675
+ }
2676
+ if (!collections.cases) {
2677
+ collections.cases = [];
2678
+ }
2679
+ if (!collections.branchCounterRef) {
2680
+ collections.branchCounterRef = { value: 0 };
2681
+ }
2682
+ if (!collections.caseCounterRef) {
2683
+ collections.caseCounterRef = { value: 0 };
1514
2684
  }
1515
2685
 
1516
- // Create and process catch block if present
1517
- if (tryNode.handler) {
1518
- const catchBlock = tryNode.handler;
1519
- const catchScopeId = `SCOPE#catch-block#${module.file}#${getLine(catchBlock)}:${scopeCounterRef.value++}`;
1520
- const catchSemanticId = this.generateSemanticId('catch-block', scopeTracker);
2686
+ const branches = collections.branches as BranchInfo[];
2687
+ const cases = collections.cases as CaseInfo[];
2688
+ const branchCounterRef = collections.branchCounterRef as CounterRef;
2689
+ const caseCounterRef = collections.caseCounterRef as CounterRef;
2690
+
2691
+ // Create BRANCH node
2692
+ const branchCounter = branchCounterRef.value++;
2693
+ const legacyBranchId = `${module.file}:BRANCH:switch:${getLine(switchNode)}:${branchCounter}`;
2694
+ const branchId = scopeTracker
2695
+ ? computeSemanticId('BRANCH', 'switch', scopeTracker.getContext(), { discriminator: branchCounter })
2696
+ : legacyBranchId;
2697
+
2698
+ // Handle discriminant expression - store metadata directly (Linus improvement)
2699
+ let discriminantExpressionId: string | undefined;
2700
+ let discriminantExpressionType: string | undefined;
2701
+ let discriminantLine: number | undefined;
2702
+ let discriminantColumn: number | undefined;
2703
+
2704
+ if (switchNode.discriminant) {
2705
+ const discResult = this.extractDiscriminantExpression(
2706
+ switchNode.discriminant,
2707
+ module
2708
+ );
2709
+ discriminantExpressionId = discResult.id;
2710
+ discriminantExpressionType = discResult.expressionType;
2711
+ discriminantLine = discResult.line;
2712
+ discriminantColumn = discResult.column;
2713
+ }
1521
2714
 
1522
- scopes.push({
1523
- id: catchScopeId,
1524
- type: 'SCOPE',
1525
- scopeType: 'catch-block',
1526
- semanticId: catchSemanticId,
2715
+ branches.push({
2716
+ id: branchId,
2717
+ semanticId: branchId,
2718
+ type: 'BRANCH',
2719
+ branchType: 'switch',
2720
+ file: module.file,
2721
+ line: getLine(switchNode),
2722
+ parentScopeId,
2723
+ discriminantExpressionId,
2724
+ discriminantExpressionType,
2725
+ discriminantLine,
2726
+ discriminantColumn
2727
+ });
2728
+
2729
+ // Process each case clause
2730
+ for (let i = 0; i < switchNode.cases.length; i++) {
2731
+ const caseNode = switchNode.cases[i];
2732
+ const isDefault = caseNode.test === null;
2733
+ const isEmpty = caseNode.consequent.length === 0;
2734
+
2735
+ // Detect fall-through: no break/return/throw at end of consequent
2736
+ const fallsThrough = isEmpty || !this.caseTerminates(caseNode);
2737
+
2738
+ // Extract case value
2739
+ const value = isDefault ? null : this.extractCaseValue(caseNode.test ?? null);
2740
+
2741
+ const caseCounter = caseCounterRef.value++;
2742
+ const valueName = isDefault ? 'default' : String(value);
2743
+ const legacyCaseId = `${module.file}:CASE:${valueName}:${getLine(caseNode)}:${caseCounter}`;
2744
+ const caseId = scopeTracker
2745
+ ? computeSemanticId('CASE', valueName, scopeTracker.getContext(), { discriminator: caseCounter })
2746
+ : legacyCaseId;
2747
+
2748
+ cases.push({
2749
+ id: caseId,
2750
+ semanticId: caseId,
2751
+ type: 'CASE',
2752
+ value,
2753
+ isDefault,
2754
+ fallsThrough,
2755
+ isEmpty,
1527
2756
  file: module.file,
1528
- line: getLine(catchBlock),
1529
- parentScopeId
2757
+ line: getLine(caseNode),
2758
+ parentBranchId: branchId
1530
2759
  });
2760
+ }
2761
+ }
1531
2762
 
1532
- if (scopeTracker) {
1533
- scopeTracker.enterCountedScope('catch');
1534
- }
2763
+ /**
2764
+ * Extract EXPRESSION node ID and metadata for switch discriminant
2765
+ */
2766
+ private extractDiscriminantExpression(
2767
+ discriminant: t.Expression,
2768
+ module: VisitorModule
2769
+ ): { id: string; expressionType: string; line: number; column: number } {
2770
+ const line = getLine(discriminant);
2771
+ const column = getColumn(discriminant);
2772
+
2773
+ if (t.isIdentifier(discriminant)) {
2774
+ // Simple identifier: switch(x) - create EXPRESSION node
2775
+ return {
2776
+ id: ExpressionNode.generateId('Identifier', module.file, line, column),
2777
+ expressionType: 'Identifier',
2778
+ line,
2779
+ column
2780
+ };
2781
+ } else if (t.isMemberExpression(discriminant)) {
2782
+ // Member expression: switch(action.type)
2783
+ return {
2784
+ id: ExpressionNode.generateId('MemberExpression', module.file, line, column),
2785
+ expressionType: 'MemberExpression',
2786
+ line,
2787
+ column
2788
+ };
2789
+ } else if (t.isCallExpression(discriminant)) {
2790
+ // Call expression: switch(getType())
2791
+ const callee = t.isIdentifier(discriminant.callee) ? discriminant.callee.name : '<complex>';
2792
+ // Return CALL node ID instead of EXPRESSION (reuse existing call tracking)
2793
+ return {
2794
+ id: `${module.file}:CALL:${callee}:${line}:${column}`,
2795
+ expressionType: 'CallExpression',
2796
+ line,
2797
+ column
2798
+ };
2799
+ }
1535
2800
 
1536
- // Handle catch parameter (e.g., catch (e))
1537
- if (catchBlock.param) {
1538
- const errorVarInfo = this.extractVariableNamesFromPattern(catchBlock.param);
2801
+ // Default: create generic EXPRESSION
2802
+ return {
2803
+ id: ExpressionNode.generateId(discriminant.type, module.file, line, column),
2804
+ expressionType: discriminant.type,
2805
+ line,
2806
+ column
2807
+ };
2808
+ }
1539
2809
 
1540
- errorVarInfo.forEach(varInfo => {
1541
- const legacyId = `VARIABLE#${varInfo.name}#${module.file}#${varInfo.loc.start.line}:${varInfo.loc.start.column}:${varDeclCounterRef.value++}`;
1542
- const varId = scopeTracker
1543
- ? computeSemanticId('VARIABLE', varInfo.name, scopeTracker.getContext())
1544
- : legacyId;
2810
+ /**
2811
+ * Extract case test value as a primitive
2812
+ */
2813
+ private extractCaseValue(test: t.Expression | null): unknown {
2814
+ if (!test) return null;
2815
+
2816
+ if (t.isStringLiteral(test)) {
2817
+ return test.value;
2818
+ } else if (t.isNumericLiteral(test)) {
2819
+ return test.value;
2820
+ } else if (t.isBooleanLiteral(test)) {
2821
+ return test.value;
2822
+ } else if (t.isNullLiteral(test)) {
2823
+ return null;
2824
+ } else if (t.isIdentifier(test)) {
2825
+ // Constant reference: case CONSTANTS.ADD
2826
+ return test.name;
2827
+ } else if (t.isMemberExpression(test)) {
2828
+ // Member expression: case Action.ADD
2829
+ return this.memberExpressionToString(test);
2830
+ }
1545
2831
 
1546
- variableDeclarations.push({
1547
- id: varId,
1548
- type: 'VARIABLE',
1549
- name: varInfo.name,
1550
- file: module.file,
1551
- line: varInfo.loc.start.line,
1552
- parentScopeId: catchScopeId
1553
- });
1554
- });
1555
- }
2832
+ return '<complex>';
2833
+ }
1556
2834
 
1557
- this.processBlockVariables(
1558
- tryPath.get('handler.body'),
1559
- catchScopeId,
1560
- module,
1561
- variableDeclarations,
1562
- literals,
1563
- variableAssignments,
1564
- varDeclCounterRef,
1565
- literalCounterRef,
1566
- scopeTracker
1567
- );
2835
+ /**
2836
+ * Check if case clause terminates (has break, return, throw)
2837
+ */
2838
+ private caseTerminates(caseNode: t.SwitchCase): boolean {
2839
+ const statements = caseNode.consequent;
2840
+ if (statements.length === 0) return false;
2841
+
2842
+ // Check last statement (or any statement for early returns)
2843
+ for (const stmt of statements) {
2844
+ if (t.isBreakStatement(stmt)) return true;
2845
+ if (t.isReturnStatement(stmt)) return true;
2846
+ if (t.isThrowStatement(stmt)) return true;
2847
+ if (t.isContinueStatement(stmt)) return true; // In switch inside loop
2848
+
2849
+ // Check for nested blocks (if last statement is block, check inside)
2850
+ if (t.isBlockStatement(stmt)) {
2851
+ const lastInBlock = stmt.body[stmt.body.length - 1];
2852
+ if (lastInBlock && (
2853
+ t.isBreakStatement(lastInBlock) ||
2854
+ t.isReturnStatement(lastInBlock) ||
2855
+ t.isThrowStatement(lastInBlock)
2856
+ )) {
2857
+ return true;
2858
+ }
2859
+ }
1568
2860
 
1569
- if (scopeTracker) {
1570
- scopeTracker.exitScope();
2861
+ // Check for if-else where both branches terminate
2862
+ if (t.isIfStatement(stmt) && stmt.alternate) {
2863
+ const ifTerminates = this.blockTerminates(stmt.consequent);
2864
+ const elseTerminates = this.blockTerminates(stmt.alternate);
2865
+ if (ifTerminates && elseTerminates) return true;
1571
2866
  }
1572
2867
  }
1573
2868
 
1574
- // Create and process finally block if present
1575
- if (tryNode.finalizer) {
1576
- const finallyScopeId = `SCOPE#finally-block#${module.file}#${getLine(tryNode.finalizer)}:${scopeCounterRef.value++}`;
1577
- const finallySemanticId = this.generateSemanticId('finally-block', scopeTracker);
2869
+ return false;
2870
+ }
1578
2871
 
1579
- scopes.push({
1580
- id: finallyScopeId,
1581
- type: 'SCOPE',
1582
- scopeType: 'finally-block',
1583
- semanticId: finallySemanticId,
1584
- file: module.file,
1585
- line: getLine(tryNode.finalizer),
1586
- parentScopeId
1587
- });
2872
+ /**
2873
+ * Check if a block/statement terminates
2874
+ */
2875
+ private blockTerminates(node: t.Statement): boolean {
2876
+ if (t.isBreakStatement(node)) return true;
2877
+ if (t.isReturnStatement(node)) return true;
2878
+ if (t.isThrowStatement(node)) return true;
2879
+ if (t.isBlockStatement(node)) {
2880
+ const last = node.body[node.body.length - 1];
2881
+ return last ? this.blockTerminates(last) : false;
2882
+ }
2883
+ return false;
2884
+ }
1588
2885
 
1589
- if (scopeTracker) {
1590
- scopeTracker.enterCountedScope('finally');
2886
+ /**
2887
+ * Count logical operators (&& and ||) in a condition expression.
2888
+ * Used for cyclomatic complexity calculation (Phase 6 REG-267).
2889
+ *
2890
+ * @param node - The condition expression to analyze
2891
+ * @returns Number of logical operators found
2892
+ */
2893
+ private countLogicalOperators(node: t.Expression): number {
2894
+ let count = 0;
2895
+
2896
+ const traverse = (expr: t.Expression | t.Node): void => {
2897
+ if (t.isLogicalExpression(expr)) {
2898
+ // Count && and || operators
2899
+ if (expr.operator === '&&' || expr.operator === '||') {
2900
+ count++;
2901
+ }
2902
+ traverse(expr.left);
2903
+ traverse(expr.right);
2904
+ } else if (t.isConditionalExpression(expr)) {
2905
+ // Handle ternary conditions: test ? consequent : alternate
2906
+ traverse(expr.test);
2907
+ traverse(expr.consequent);
2908
+ traverse(expr.alternate);
2909
+ } else if (t.isUnaryExpression(expr)) {
2910
+ traverse(expr.argument);
2911
+ } else if (t.isBinaryExpression(expr)) {
2912
+ traverse(expr.left);
2913
+ traverse(expr.right);
2914
+ } else if (t.isSequenceExpression(expr)) {
2915
+ for (const e of expr.expressions) {
2916
+ traverse(e);
2917
+ }
2918
+ } else if (t.isParenthesizedExpression(expr)) {
2919
+ traverse(expr.expression);
1591
2920
  }
2921
+ };
1592
2922
 
1593
- const finalizerPath = tryPath.get('finalizer');
1594
- if (finalizerPath.node) {
1595
- this.processBlockVariables(
1596
- finalizerPath as NodePath,
1597
- finallyScopeId,
1598
- module,
1599
- variableDeclarations,
1600
- literals,
1601
- variableAssignments,
1602
- varDeclCounterRef,
1603
- literalCounterRef,
1604
- scopeTracker
1605
- );
1606
- }
2923
+ traverse(node);
2924
+ return count;
2925
+ }
1607
2926
 
1608
- if (scopeTracker) {
1609
- scopeTracker.exitScope();
2927
+ /**
2928
+ * Convert MemberExpression to string representation
2929
+ */
2930
+ private memberExpressionToString(expr: t.MemberExpression): string {
2931
+ const parts: string[] = [];
2932
+
2933
+ let current: t.Expression = expr;
2934
+ while (t.isMemberExpression(current)) {
2935
+ if (t.isIdentifier(current.property)) {
2936
+ parts.unshift(current.property.name);
2937
+ } else {
2938
+ parts.unshift('<computed>');
1610
2939
  }
2940
+ current = current.object;
1611
2941
  }
1612
2942
 
1613
- tryPath.skip();
2943
+ if (t.isIdentifier(current)) {
2944
+ parts.unshift(current.name);
2945
+ }
2946
+
2947
+ return parts.join('.');
1614
2948
  }
1615
2949
 
1616
2950
  /**
1617
2951
  * Factory method to create IfStatement handler.
1618
- * Creates if scope with condition parsing and optional else scope.
2952
+ * Creates BRANCH node for if statement and SCOPE nodes for if/else bodies.
1619
2953
  * Tracks if/else scope transitions via ifElseScopeMap.
1620
2954
  *
2955
+ * Phase 3 (REG-267): Creates BRANCH node with branchType='if' and
2956
+ * HAS_CONSEQUENT/HAS_ALTERNATE edges to body SCOPEs.
2957
+ *
1621
2958
  * @param parentScopeId - Parent scope ID for the scope nodes
1622
2959
  * @param module - Module context
1623
2960
  * @param scopes - Collection to push scope nodes to
2961
+ * @param branches - Collection to push BRANCH nodes to
1624
2962
  * @param ifScopeCounterRef - Counter for unique if scope IDs
2963
+ * @param branchCounterRef - Counter for unique BRANCH IDs
1625
2964
  * @param scopeTracker - Tracker for semantic ID generation
1626
2965
  * @param sourceCode - Source code for extracting condition text
1627
2966
  * @param ifElseScopeMap - Map to track if/else scope transitions
2967
+ * @param scopeIdStack - Stack for tracking current scope ID for CONTAINS edges
1628
2968
  */
1629
2969
  private createIfStatementHandler(
1630
2970
  parentScopeId: string,
1631
2971
  module: VisitorModule,
1632
2972
  scopes: ScopeInfo[],
2973
+ branches: BranchInfo[],
1633
2974
  ifScopeCounterRef: CounterRef,
2975
+ branchCounterRef: CounterRef,
1634
2976
  scopeTracker: ScopeTracker | undefined,
1635
2977
  sourceCode: string,
1636
- ifElseScopeMap: Map<t.IfStatement, { inElse: boolean; hasElse: boolean }>
2978
+ ifElseScopeMap: Map<t.IfStatement, IfElseScopeInfo>,
2979
+ scopeIdStack?: string[],
2980
+ controlFlowState?: { branchCount: number; logicalOpCount: number },
2981
+ countLogicalOperators?: (node: t.Expression) => number
1637
2982
  ): { enter: (ifPath: NodePath<t.IfStatement>) => void; exit: (ifPath: NodePath<t.IfStatement>) => void } {
1638
2983
  return {
1639
2984
  enter: (ifPath: NodePath<t.IfStatement>) => {
1640
2985
  const ifNode = ifPath.node;
1641
2986
  const condition = sourceCode.substring(ifNode.test.start!, ifNode.test.end!) || 'condition';
2987
+
2988
+ // Phase 6 (REG-267): Increment branch count and count logical operators
2989
+ if (controlFlowState) {
2990
+ controlFlowState.branchCount++;
2991
+ if (countLogicalOperators) {
2992
+ controlFlowState.logicalOpCount += countLogicalOperators(ifNode.test);
2993
+ }
2994
+ }
2995
+
2996
+ // Check if this if-statement is an else-if (alternate of parent IfStatement)
2997
+ const isElseIf = t.isIfStatement(ifPath.parent) && ifPath.parentKey === 'alternate';
2998
+
2999
+ // Determine actual parent scope
3000
+ let actualParentScopeId: string;
3001
+ if (isElseIf) {
3002
+ // For else-if, parent should be the outer BRANCH (stored in ifElseScopeMap)
3003
+ const parentIfInfo = ifElseScopeMap.get(ifPath.parent as t.IfStatement);
3004
+ if (parentIfInfo) {
3005
+ actualParentScopeId = parentIfInfo.branchId;
3006
+ } else {
3007
+ // Fallback to stack
3008
+ actualParentScopeId = (scopeIdStack && scopeIdStack.length > 0)
3009
+ ? scopeIdStack[scopeIdStack.length - 1]
3010
+ : parentScopeId;
3011
+ }
3012
+ } else {
3013
+ // For regular if statements, use stack or original parentScopeId
3014
+ actualParentScopeId = (scopeIdStack && scopeIdStack.length > 0)
3015
+ ? scopeIdStack[scopeIdStack.length - 1]
3016
+ : parentScopeId;
3017
+ }
3018
+
3019
+ // 1. Create BRANCH node for if statement
3020
+ const branchCounter = branchCounterRef.value++;
3021
+ const legacyBranchId = `${module.file}:BRANCH:if:${getLine(ifNode)}:${branchCounter}`;
3022
+ const branchId = scopeTracker
3023
+ ? computeSemanticId('BRANCH', 'if', scopeTracker.getContext(), { discriminator: branchCounter })
3024
+ : legacyBranchId;
3025
+
3026
+ // 2. Extract condition expression info for HAS_CONDITION edge
3027
+ const conditionResult = this.extractDiscriminantExpression(ifNode.test, module);
3028
+
3029
+ // For else-if, get the parent branch ID
3030
+ const isAlternateOfBranchId = isElseIf
3031
+ ? ifElseScopeMap.get(ifPath.parent as t.IfStatement)?.branchId
3032
+ : undefined;
3033
+
3034
+ branches.push({
3035
+ id: branchId,
3036
+ semanticId: branchId,
3037
+ type: 'BRANCH',
3038
+ branchType: 'if',
3039
+ file: module.file,
3040
+ line: getLine(ifNode),
3041
+ parentScopeId: actualParentScopeId,
3042
+ discriminantExpressionId: conditionResult.id,
3043
+ discriminantExpressionType: conditionResult.expressionType,
3044
+ discriminantLine: conditionResult.line,
3045
+ discriminantColumn: conditionResult.column,
3046
+ isAlternateOfBranchId
3047
+ });
3048
+
3049
+ // 3. Create if-body SCOPE (backward compatibility)
3050
+ // Parent is now BRANCH, not original parentScopeId
1642
3051
  const counterId = ifScopeCounterRef.value++;
1643
3052
  const ifScopeId = `SCOPE#if#${module.file}#${getLine(ifNode)}:${getColumn(ifNode)}:${counterId}`;
1644
3053
 
@@ -1657,19 +3066,25 @@ export class JSASTAnalyzer extends Plugin {
1657
3066
  constraints: constraints.length > 0 ? constraints : undefined,
1658
3067
  file: module.file,
1659
3068
  line: getLine(ifNode),
1660
- parentScopeId
3069
+ parentScopeId: branchId // Parent is BRANCH, not original parentScopeId
1661
3070
  });
1662
3071
 
3072
+ // 4. Push if scope onto stack for CONTAINS edges
3073
+ if (scopeIdStack) {
3074
+ scopeIdStack.push(ifScopeId);
3075
+ }
3076
+
1663
3077
  // Enter scope for semantic ID generation
1664
3078
  if (scopeTracker) {
1665
3079
  scopeTracker.enterCountedScope('if');
1666
3080
  }
1667
3081
 
1668
- // Handle else branch if present
3082
+ // 5. Handle else branch if present
3083
+ let elseScopeId: string | null = null;
1669
3084
  if (ifNode.alternate && !t.isIfStatement(ifNode.alternate)) {
1670
3085
  // Only create else scope for actual else block, not else-if
1671
3086
  const elseCounterId = ifScopeCounterRef.value++;
1672
- const elseScopeId = `SCOPE#else#${module.file}#${getLine(ifNode.alternate)}:${getColumn(ifNode.alternate)}:${elseCounterId}`;
3087
+ elseScopeId = `SCOPE#else#${module.file}#${getLine(ifNode.alternate)}:${getColumn(ifNode.alternate)}:${elseCounterId}`;
1673
3088
 
1674
3089
  const negatedConstraints = constraints.length > 0 ? ConditionParser.negate(constraints) : undefined;
1675
3090
  const elseSemanticId = this.generateSemanticId('else_statement', scopeTracker);
@@ -1684,18 +3099,23 @@ export class JSASTAnalyzer extends Plugin {
1684
3099
  constraints: negatedConstraints,
1685
3100
  file: module.file,
1686
3101
  line: getLine(ifNode.alternate),
1687
- parentScopeId
3102
+ parentScopeId: branchId // Parent is BRANCH, not original parentScopeId
1688
3103
  });
1689
3104
 
1690
3105
  // Store info to switch to else scope when we enter alternate
1691
- ifElseScopeMap.set(ifNode, { inElse: false, hasElse: true });
3106
+ ifElseScopeMap.set(ifNode, { inElse: false, hasElse: true, ifScopeId, elseScopeId, branchId });
1692
3107
  } else {
1693
- ifElseScopeMap.set(ifNode, { inElse: false, hasElse: false });
3108
+ ifElseScopeMap.set(ifNode, { inElse: false, hasElse: false, ifScopeId, elseScopeId: null, branchId });
1694
3109
  }
1695
3110
  },
1696
3111
  exit: (ifPath: NodePath<t.IfStatement>) => {
1697
3112
  const ifNode = ifPath.node;
1698
3113
 
3114
+ // Pop scope from stack (either if or else, depending on what we're exiting)
3115
+ if (scopeIdStack) {
3116
+ scopeIdStack.pop();
3117
+ }
3118
+
1699
3119
  // Exit the current scope (either if or else)
1700
3120
  if (scopeTracker) {
1701
3121
  scopeTracker.exitScope();
@@ -1709,29 +3129,152 @@ export class JSASTAnalyzer extends Plugin {
1709
3129
  }
1710
3130
 
1711
3131
  /**
1712
- * Factory method to create BlockStatement handler for tracking if/else transitions.
3132
+ * Factory method to create ConditionalExpression (ternary) handler.
3133
+ * Creates BRANCH nodes with branchType='ternary' and increments branchCount for cyclomatic complexity.
3134
+ *
3135
+ * Key difference from IfStatement: ternary has EXPRESSIONS as branches, not SCOPE blocks.
3136
+ * We store consequentExpressionId and alternateExpressionId in BranchInfo for HAS_CONSEQUENT/HAS_ALTERNATE edges.
3137
+ *
3138
+ * @param parentScopeId - Parent scope ID for the BRANCH node
3139
+ * @param module - Module context
3140
+ * @param branches - Collection to push BRANCH nodes to
3141
+ * @param branchCounterRef - Counter for unique BRANCH IDs
3142
+ * @param scopeTracker - Tracker for semantic ID generation
3143
+ * @param scopeIdStack - Stack for tracking current scope ID for CONTAINS edges
3144
+ * @param controlFlowState - State for tracking control flow metrics (complexity)
3145
+ * @param countLogicalOperators - Function to count logical operators in condition
3146
+ */
3147
+ private createConditionalExpressionHandler(
3148
+ parentScopeId: string,
3149
+ module: VisitorModule,
3150
+ branches: BranchInfo[],
3151
+ branchCounterRef: CounterRef,
3152
+ scopeTracker: ScopeTracker | undefined,
3153
+ scopeIdStack?: string[],
3154
+ controlFlowState?: { branchCount: number; logicalOpCount: number },
3155
+ countLogicalOperators?: (node: t.Expression) => number
3156
+ ): (condPath: NodePath<t.ConditionalExpression>) => void {
3157
+ return (condPath: NodePath<t.ConditionalExpression>) => {
3158
+ const condNode = condPath.node;
3159
+
3160
+ // Increment branch count for cyclomatic complexity
3161
+ if (controlFlowState) {
3162
+ controlFlowState.branchCount++;
3163
+ // Count logical operators in the test condition (e.g., a && b ? x : y)
3164
+ if (countLogicalOperators) {
3165
+ controlFlowState.logicalOpCount += countLogicalOperators(condNode.test);
3166
+ }
3167
+ }
3168
+
3169
+ // Determine parent scope from stack or fallback
3170
+ const actualParentScopeId = (scopeIdStack && scopeIdStack.length > 0)
3171
+ ? scopeIdStack[scopeIdStack.length - 1]
3172
+ : parentScopeId;
3173
+
3174
+ // Create BRANCH node with branchType='ternary'
3175
+ const branchCounter = branchCounterRef.value++;
3176
+ const legacyBranchId = `${module.file}:BRANCH:ternary:${getLine(condNode)}:${branchCounter}`;
3177
+ const branchId = scopeTracker
3178
+ ? computeSemanticId('BRANCH', 'ternary', scopeTracker.getContext(), { discriminator: branchCounter })
3179
+ : legacyBranchId;
3180
+
3181
+ // Extract condition expression info for HAS_CONDITION edge
3182
+ const conditionResult = this.extractDiscriminantExpression(condNode.test, module);
3183
+
3184
+ // Generate expression IDs for consequent and alternate
3185
+ const consequentLine = getLine(condNode.consequent);
3186
+ const consequentColumn = getColumn(condNode.consequent);
3187
+ const consequentExpressionId = ExpressionNode.generateId(
3188
+ condNode.consequent.type,
3189
+ module.file,
3190
+ consequentLine,
3191
+ consequentColumn
3192
+ );
3193
+
3194
+ const alternateLine = getLine(condNode.alternate);
3195
+ const alternateColumn = getColumn(condNode.alternate);
3196
+ const alternateExpressionId = ExpressionNode.generateId(
3197
+ condNode.alternate.type,
3198
+ module.file,
3199
+ alternateLine,
3200
+ alternateColumn
3201
+ );
3202
+
3203
+ branches.push({
3204
+ id: branchId,
3205
+ semanticId: branchId,
3206
+ type: 'BRANCH',
3207
+ branchType: 'ternary',
3208
+ file: module.file,
3209
+ line: getLine(condNode),
3210
+ parentScopeId: actualParentScopeId,
3211
+ discriminantExpressionId: conditionResult.id,
3212
+ discriminantExpressionType: conditionResult.expressionType,
3213
+ discriminantLine: conditionResult.line,
3214
+ discriminantColumn: conditionResult.column,
3215
+ consequentExpressionId,
3216
+ alternateExpressionId
3217
+ });
3218
+ };
3219
+ }
3220
+
3221
+ /**
3222
+ * Factory method to create BlockStatement handler for tracking if/else and try/finally transitions.
1713
3223
  * When entering an else block, switches scope from if to else.
3224
+ * When entering a finally block, switches scope from try/catch to finally.
1714
3225
  *
1715
3226
  * @param scopeTracker - Tracker for semantic ID generation
1716
3227
  * @param ifElseScopeMap - Map to track if/else scope transitions
3228
+ * @param tryScopeMap - Map to track try/catch/finally scope transitions
3229
+ * @param scopeIdStack - Stack for tracking current scope ID for CONTAINS edges
1717
3230
  */
1718
- private createIfElseBlockStatementHandler(
3231
+ private createBlockStatementHandler(
1719
3232
  scopeTracker: ScopeTracker | undefined,
1720
- ifElseScopeMap: Map<t.IfStatement, { inElse: boolean; hasElse: boolean }>
3233
+ ifElseScopeMap: Map<t.IfStatement, IfElseScopeInfo>,
3234
+ tryScopeMap: Map<t.TryStatement, TryScopeInfo>,
3235
+ scopeIdStack?: string[]
1721
3236
  ): { enter: (blockPath: NodePath<t.BlockStatement>) => void } {
1722
3237
  return {
1723
3238
  enter: (blockPath: NodePath<t.BlockStatement>) => {
1724
- // Check if this block is the alternate of an IfStatement
1725
3239
  const parent = blockPath.parent;
3240
+
3241
+ // Check if this block is the alternate of an IfStatement
1726
3242
  if (t.isIfStatement(parent) && parent.alternate === blockPath.node) {
1727
3243
  const scopeInfo = ifElseScopeMap.get(parent);
1728
- if (scopeInfo && scopeInfo.hasElse && !scopeInfo.inElse && scopeTracker) {
1729
- // Exit if scope, enter else scope
1730
- scopeTracker.exitScope();
1731
- scopeTracker.enterCountedScope('else');
3244
+ if (scopeInfo && scopeInfo.hasElse && !scopeInfo.inElse) {
3245
+ // Swap if-scope for else-scope on the stack
3246
+ if (scopeIdStack && scopeInfo.elseScopeId) {
3247
+ scopeIdStack.pop(); // Remove if-scope
3248
+ scopeIdStack.push(scopeInfo.elseScopeId); // Push else-scope
3249
+ }
3250
+
3251
+ // Exit if scope, enter else scope for semantic ID tracking
3252
+ if (scopeTracker) {
3253
+ scopeTracker.exitScope();
3254
+ scopeTracker.enterCountedScope('else');
3255
+ }
1732
3256
  scopeInfo.inElse = true;
1733
3257
  }
1734
3258
  }
3259
+
3260
+ // Check if this block is the finalizer of a TryStatement
3261
+ if (t.isTryStatement(parent) && parent.finalizer === blockPath.node) {
3262
+ const scopeInfo = tryScopeMap.get(parent);
3263
+ if (scopeInfo && scopeInfo.finallyScopeId && scopeInfo.currentBlock !== 'finally') {
3264
+ // Pop current scope (try or catch), push finally scope
3265
+ if (scopeIdStack) {
3266
+ scopeIdStack.pop();
3267
+ scopeIdStack.push(scopeInfo.finallyScopeId);
3268
+ }
3269
+
3270
+ // Exit current scope, enter finally scope for semantic ID tracking
3271
+ if (scopeTracker) {
3272
+ scopeTracker.exitScope();
3273
+ scopeTracker.enterCountedScope('finally');
3274
+ }
3275
+ scopeInfo.currentBlock = 'finally';
3276
+ }
3277
+ }
1735
3278
  }
1736
3279
  };
1737
3280
  }
@@ -1755,6 +3298,7 @@ export class JSASTAnalyzer extends Plugin {
1755
3298
  const eventListeners = (collections.eventListeners ?? []) as EventListenerInfo[];
1756
3299
  const methodCallbacks = (collections.methodCallbacks ?? []) as MethodCallbackInfo[];
1757
3300
  const classInstantiations = (collections.classInstantiations ?? []) as ClassInstantiationInfo[];
3301
+ const constructorCalls = (collections.constructorCalls ?? []) as ConstructorCallInfo[];
1758
3302
  const httpRequests = (collections.httpRequests ?? []) as HttpRequestInfo[];
1759
3303
  const literals = (collections.literals ?? []) as LiteralInfo[];
1760
3304
  const variableAssignments = (collections.variableAssignments ?? []) as VariableAssignmentInfo[];
@@ -1767,6 +3311,32 @@ export class JSASTAnalyzer extends Plugin {
1767
3311
  const literalCounterRef = (collections.literalCounterRef ?? { value: 0 }) as CounterRef;
1768
3312
  const anonymousFunctionCounterRef = (collections.anonymousFunctionCounterRef ?? { value: 0 }) as CounterRef;
1769
3313
  const scopeTracker = collections.scopeTracker as ScopeTracker | undefined;
3314
+ // Object literal tracking (REG-328)
3315
+ if (!collections.objectLiterals) {
3316
+ collections.objectLiterals = [];
3317
+ }
3318
+ if (!collections.objectProperties) {
3319
+ collections.objectProperties = [];
3320
+ }
3321
+ if (!collections.objectLiteralCounterRef) {
3322
+ collections.objectLiteralCounterRef = { value: 0 };
3323
+ }
3324
+ const objectLiterals = collections.objectLiterals as ObjectLiteralInfo[];
3325
+ const objectProperties = collections.objectProperties as ObjectPropertyInfo[];
3326
+ const objectLiteralCounterRef = collections.objectLiteralCounterRef as CounterRef;
3327
+ const returnStatements = (collections.returnStatements ?? []) as ReturnStatementInfo[];
3328
+ const parameters = (collections.parameters ?? []) as ParameterInfo[];
3329
+ // Control flow collections (Phase 2: LOOP nodes)
3330
+ // Initialize if not exist to ensure nested function calls share same arrays
3331
+ if (!collections.loops) {
3332
+ collections.loops = [];
3333
+ }
3334
+ if (!collections.loopCounterRef) {
3335
+ collections.loopCounterRef = { value: 0 };
3336
+ }
3337
+ const loops = collections.loops as LoopInfo[];
3338
+ const loopCounterRef = collections.loopCounterRef as CounterRef;
3339
+ const updateExpressions = (collections.updateExpressions ?? []) as UpdateExpressionInfo[];
1770
3340
  const processedNodes = collections.processedNodes ?? {
1771
3341
  functions: new Set<string>(),
1772
3342
  classes: new Set<string>(),
@@ -1786,14 +3356,240 @@ export class JSASTAnalyzer extends Plugin {
1786
3356
  const processedMethodCalls = processedNodes.methodCalls;
1787
3357
  const processedEventListeners = processedNodes.eventListeners;
1788
3358
 
1789
- // Track if/else scope transitions
1790
- const ifElseScopeMap = new Map<t.IfStatement, { inElse: boolean; hasElse: boolean }>();
3359
+ // Track if/else scope transitions (Phase 3: extended with branchId)
3360
+ const ifElseScopeMap = new Map<t.IfStatement, IfElseScopeInfo>();
3361
+
3362
+ // Ensure branches and branchCounterRef are initialized (used by IfStatement and SwitchStatement)
3363
+ if (!collections.branches) {
3364
+ collections.branches = [];
3365
+ }
3366
+ if (!collections.branchCounterRef) {
3367
+ collections.branchCounterRef = { value: 0 };
3368
+ }
3369
+ const branches = collections.branches as BranchInfo[];
3370
+ const branchCounterRef = collections.branchCounterRef as CounterRef;
3371
+
3372
+ // Phase 4: Initialize try/catch/finally collections and counters
3373
+ if (!collections.tryBlocks) {
3374
+ collections.tryBlocks = [];
3375
+ }
3376
+ if (!collections.catchBlocks) {
3377
+ collections.catchBlocks = [];
3378
+ }
3379
+ if (!collections.finallyBlocks) {
3380
+ collections.finallyBlocks = [];
3381
+ }
3382
+ if (!collections.tryBlockCounterRef) {
3383
+ collections.tryBlockCounterRef = { value: 0 };
3384
+ }
3385
+ if (!collections.catchBlockCounterRef) {
3386
+ collections.catchBlockCounterRef = { value: 0 };
3387
+ }
3388
+ if (!collections.finallyBlockCounterRef) {
3389
+ collections.finallyBlockCounterRef = { value: 0 };
3390
+ }
3391
+ const tryBlocks = collections.tryBlocks as TryBlockInfo[];
3392
+ const catchBlocks = collections.catchBlocks as CatchBlockInfo[];
3393
+ const finallyBlocks = collections.finallyBlocks as FinallyBlockInfo[];
3394
+ const tryBlockCounterRef = collections.tryBlockCounterRef as CounterRef;
3395
+ const catchBlockCounterRef = collections.catchBlockCounterRef as CounterRef;
3396
+ const finallyBlockCounterRef = collections.finallyBlockCounterRef as CounterRef;
3397
+
3398
+ // Track try/catch/finally scope transitions
3399
+ const tryScopeMap = new Map<t.TryStatement, TryScopeInfo>();
3400
+
3401
+ // REG-334: Use shared Promise executor contexts from collections.
3402
+ // These are populated by module-level NewExpression handler and function-level NewExpression handler.
3403
+ if (!collections.promiseExecutorContexts) {
3404
+ collections.promiseExecutorContexts = new Map<string, PromiseExecutorContext>();
3405
+ }
3406
+ const promiseExecutorContexts = collections.promiseExecutorContexts as Map<string, PromiseExecutorContext>;
3407
+
3408
+ // Initialize promiseResolutions array if not exists
3409
+ if (!collections.promiseResolutions) {
3410
+ collections.promiseResolutions = [];
3411
+ }
3412
+ const promiseResolutions = collections.promiseResolutions as PromiseResolutionInfo[];
3413
+
3414
+ // Dynamic scope ID stack for CONTAINS edges
3415
+ // Starts with the function body scope, gets updated as we enter/exit conditional scopes
3416
+ const scopeIdStack: string[] = [parentScopeId];
3417
+ const getCurrentScopeId = (): string => scopeIdStack[scopeIdStack.length - 1];
3418
+
3419
+ // Determine the ID of the function we're analyzing for RETURNS edges
3420
+ // Find by matching file/line/column in functions collection (it was just added by the visitor)
3421
+ const funcNode = funcPath.node;
3422
+ const funcLine = getLine(funcNode);
3423
+ const funcColumn = getColumn(funcNode);
3424
+ let currentFunctionId: string | null = null;
3425
+
3426
+ const matchingFunction = functions.find(f =>
3427
+ f.file === module.file &&
3428
+ f.line === funcLine &&
3429
+ (f.column === undefined || f.column === funcColumn)
3430
+ );
3431
+ if (matchingFunction) {
3432
+ currentFunctionId = matchingFunction.id;
3433
+ }
3434
+
3435
+ // Phase 6 (REG-267): Control flow tracking state for cyclomatic complexity
3436
+ const controlFlowState = {
3437
+ branchCount: 0, // if/switch statements
3438
+ loopCount: 0, // for/while/do-while/for-in/for-of
3439
+ caseCount: 0, // switch cases (excluding default)
3440
+ logicalOpCount: 0, // && and || in conditions
3441
+ hasTryCatch: false,
3442
+ hasEarlyReturn: false,
3443
+ hasThrow: false,
3444
+ returnCount: 0, // Track total return count for early return detection
3445
+ totalStatements: 0 // Track if there are statements after returns
3446
+ };
3447
+
3448
+ // Handle implicit return for THIS arrow function if it has an expression body
3449
+ // e.g., `const double = x => x * 2;` - the function we're analyzing IS an arrow with expression body
3450
+ if (t.isArrowFunctionExpression(funcNode) && !t.isBlockStatement(funcNode.body) && currentFunctionId) {
3451
+ const bodyExpr = funcNode.body;
3452
+ const bodyLine = getLine(bodyExpr);
3453
+ const bodyColumn = getColumn(bodyExpr);
3454
+
3455
+ const returnInfo: ReturnStatementInfo = {
3456
+ parentFunctionId: currentFunctionId,
3457
+ file: module.file,
3458
+ line: bodyLine,
3459
+ column: bodyColumn,
3460
+ returnValueType: 'NONE',
3461
+ isImplicitReturn: true
3462
+ };
3463
+
3464
+ // Apply type detection logic for the implicit return
3465
+ if (t.isIdentifier(bodyExpr)) {
3466
+ returnInfo.returnValueType = 'VARIABLE';
3467
+ returnInfo.returnValueName = bodyExpr.name;
3468
+ }
3469
+ // TemplateLiteral must come BEFORE isLiteral (TemplateLiteral extends Literal)
3470
+ else if (t.isTemplateLiteral(bodyExpr)) {
3471
+ returnInfo.returnValueType = 'EXPRESSION';
3472
+ returnInfo.expressionType = 'TemplateLiteral';
3473
+ returnInfo.returnValueLine = getLine(bodyExpr);
3474
+ returnInfo.returnValueColumn = getColumn(bodyExpr);
3475
+ returnInfo.returnValueId = NodeFactory.generateExpressionId(
3476
+ 'TemplateLiteral', module.file, getLine(bodyExpr), getColumn(bodyExpr)
3477
+ );
3478
+ const sourceNames: string[] = [];
3479
+ for (const expr of bodyExpr.expressions) {
3480
+ if (t.isIdentifier(expr)) sourceNames.push(expr.name);
3481
+ }
3482
+ if (sourceNames.length > 0) returnInfo.expressionSourceNames = sourceNames;
3483
+ }
3484
+ else if (t.isLiteral(bodyExpr)) {
3485
+ returnInfo.returnValueType = 'LITERAL';
3486
+ const literalId = `LITERAL#implicit_return#${module.file}#${funcLine}:${funcColumn}:${literalCounterRef.value++}`;
3487
+ returnInfo.returnValueId = literalId;
3488
+ literals.push({
3489
+ id: literalId,
3490
+ type: 'LITERAL',
3491
+ value: ExpressionEvaluator.extractLiteralValue(bodyExpr),
3492
+ valueType: typeof ExpressionEvaluator.extractLiteralValue(bodyExpr),
3493
+ file: module.file,
3494
+ line: bodyLine,
3495
+ column: bodyColumn
3496
+ });
3497
+ }
3498
+ else if (t.isCallExpression(bodyExpr) && t.isIdentifier(bodyExpr.callee)) {
3499
+ returnInfo.returnValueType = 'CALL_SITE';
3500
+ returnInfo.returnValueLine = getLine(bodyExpr);
3501
+ returnInfo.returnValueColumn = getColumn(bodyExpr);
3502
+ returnInfo.returnValueCallName = bodyExpr.callee.name;
3503
+ }
3504
+ else if (t.isCallExpression(bodyExpr) && t.isMemberExpression(bodyExpr.callee)) {
3505
+ returnInfo.returnValueType = 'METHOD_CALL';
3506
+ returnInfo.returnValueLine = getLine(bodyExpr);
3507
+ returnInfo.returnValueColumn = getColumn(bodyExpr);
3508
+ if (t.isIdentifier(bodyExpr.callee.property)) {
3509
+ returnInfo.returnValueCallName = bodyExpr.callee.property.name;
3510
+ }
3511
+ }
3512
+ // REG-276: Detailed EXPRESSION handling for implicit arrow returns
3513
+ else if (t.isBinaryExpression(bodyExpr)) {
3514
+ returnInfo.returnValueType = 'EXPRESSION';
3515
+ returnInfo.expressionType = 'BinaryExpression';
3516
+ returnInfo.returnValueLine = getLine(bodyExpr);
3517
+ returnInfo.returnValueColumn = getColumn(bodyExpr);
3518
+ returnInfo.operator = bodyExpr.operator;
3519
+ returnInfo.returnValueId = NodeFactory.generateExpressionId(
3520
+ 'BinaryExpression', module.file, getLine(bodyExpr), getColumn(bodyExpr)
3521
+ );
3522
+ if (t.isIdentifier(bodyExpr.left)) returnInfo.leftSourceName = bodyExpr.left.name;
3523
+ if (t.isIdentifier(bodyExpr.right)) returnInfo.rightSourceName = bodyExpr.right.name;
3524
+ }
3525
+ else if (t.isLogicalExpression(bodyExpr)) {
3526
+ returnInfo.returnValueType = 'EXPRESSION';
3527
+ returnInfo.expressionType = 'LogicalExpression';
3528
+ returnInfo.returnValueLine = getLine(bodyExpr);
3529
+ returnInfo.returnValueColumn = getColumn(bodyExpr);
3530
+ returnInfo.operator = bodyExpr.operator;
3531
+ returnInfo.returnValueId = NodeFactory.generateExpressionId(
3532
+ 'LogicalExpression', module.file, getLine(bodyExpr), getColumn(bodyExpr)
3533
+ );
3534
+ if (t.isIdentifier(bodyExpr.left)) returnInfo.leftSourceName = bodyExpr.left.name;
3535
+ if (t.isIdentifier(bodyExpr.right)) returnInfo.rightSourceName = bodyExpr.right.name;
3536
+ }
3537
+ else if (t.isConditionalExpression(bodyExpr)) {
3538
+ returnInfo.returnValueType = 'EXPRESSION';
3539
+ returnInfo.expressionType = 'ConditionalExpression';
3540
+ returnInfo.returnValueLine = getLine(bodyExpr);
3541
+ returnInfo.returnValueColumn = getColumn(bodyExpr);
3542
+ returnInfo.returnValueId = NodeFactory.generateExpressionId(
3543
+ 'ConditionalExpression', module.file, getLine(bodyExpr), getColumn(bodyExpr)
3544
+ );
3545
+ if (t.isIdentifier(bodyExpr.consequent)) returnInfo.consequentSourceName = bodyExpr.consequent.name;
3546
+ if (t.isIdentifier(bodyExpr.alternate)) returnInfo.alternateSourceName = bodyExpr.alternate.name;
3547
+ }
3548
+ else if (t.isUnaryExpression(bodyExpr)) {
3549
+ returnInfo.returnValueType = 'EXPRESSION';
3550
+ returnInfo.expressionType = 'UnaryExpression';
3551
+ returnInfo.returnValueLine = getLine(bodyExpr);
3552
+ returnInfo.returnValueColumn = getColumn(bodyExpr);
3553
+ returnInfo.operator = bodyExpr.operator;
3554
+ returnInfo.returnValueId = NodeFactory.generateExpressionId(
3555
+ 'UnaryExpression', module.file, getLine(bodyExpr), getColumn(bodyExpr)
3556
+ );
3557
+ if (t.isIdentifier(bodyExpr.argument)) returnInfo.unaryArgSourceName = bodyExpr.argument.name;
3558
+ }
3559
+ else if (t.isMemberExpression(bodyExpr)) {
3560
+ returnInfo.returnValueType = 'EXPRESSION';
3561
+ returnInfo.expressionType = 'MemberExpression';
3562
+ returnInfo.returnValueLine = getLine(bodyExpr);
3563
+ returnInfo.returnValueColumn = getColumn(bodyExpr);
3564
+ returnInfo.returnValueId = NodeFactory.generateExpressionId(
3565
+ 'MemberExpression', module.file, getLine(bodyExpr), getColumn(bodyExpr)
3566
+ );
3567
+ if (t.isIdentifier(bodyExpr.object)) {
3568
+ returnInfo.object = bodyExpr.object.name;
3569
+ returnInfo.objectSourceName = bodyExpr.object.name;
3570
+ }
3571
+ if (t.isIdentifier(bodyExpr.property)) returnInfo.property = bodyExpr.property.name;
3572
+ returnInfo.computed = bodyExpr.computed;
3573
+ }
3574
+ else {
3575
+ // Fallback: any other expression type
3576
+ returnInfo.returnValueType = 'EXPRESSION';
3577
+ returnInfo.expressionType = bodyExpr.type;
3578
+ returnInfo.returnValueLine = getLine(bodyExpr);
3579
+ returnInfo.returnValueColumn = getColumn(bodyExpr);
3580
+ returnInfo.returnValueId = NodeFactory.generateExpressionId(
3581
+ bodyExpr.type, module.file, getLine(bodyExpr), getColumn(bodyExpr)
3582
+ );
3583
+ }
3584
+
3585
+ returnStatements.push(returnInfo);
3586
+ }
1791
3587
 
1792
3588
  funcPath.traverse({
1793
3589
  VariableDeclaration: (varPath: NodePath<t.VariableDeclaration>) => {
1794
3590
  this.handleVariableDeclaration(
1795
3591
  varPath,
1796
- parentScopeId,
3592
+ getCurrentScopeId(),
1797
3593
  module,
1798
3594
  variableDeclarations,
1799
3595
  classInstantiations,
@@ -1802,7 +3598,10 @@ export class JSASTAnalyzer extends Plugin {
1802
3598
  varDeclCounterRef,
1803
3599
  literalCounterRef,
1804
3600
  scopeTracker,
1805
- parentScopeVariables
3601
+ parentScopeVariables,
3602
+ objectLiterals,
3603
+ objectProperties,
3604
+ objectLiteralCounterRef
1806
3605
  );
1807
3606
  },
1808
3607
 
@@ -1810,6 +3609,20 @@ export class JSASTAnalyzer extends Plugin {
1810
3609
  AssignmentExpression: (assignPath: NodePath<t.AssignmentExpression>) => {
1811
3610
  const assignNode = assignPath.node;
1812
3611
 
3612
+ // === VARIABLE REASSIGNMENT (REG-290) ===
3613
+ // Check if LHS is simple identifier (not obj.prop, not arr[i])
3614
+ // Must be checked FIRST before array/object mutation handlers
3615
+ if (assignNode.left.type === 'Identifier') {
3616
+ // Initialize collection if not exists
3617
+ if (!collections.variableReassignments) {
3618
+ collections.variableReassignments = [];
3619
+ }
3620
+ const variableReassignments = collections.variableReassignments as VariableReassignmentInfo[];
3621
+
3622
+ this.detectVariableReassignment(assignNode, module, variableReassignments, scopeTracker);
3623
+ }
3624
+ // === END VARIABLE REASSIGNMENT ===
3625
+
1813
3626
  // Initialize collection if not exists
1814
3627
  if (!collections.arrayMutations) {
1815
3628
  collections.arrayMutations = [];
@@ -1817,7 +3630,7 @@ export class JSASTAnalyzer extends Plugin {
1817
3630
  const arrayMutations = collections.arrayMutations as ArrayMutationInfo[];
1818
3631
 
1819
3632
  // Check for indexed array assignment: arr[i] = value
1820
- this.detectIndexedArrayAssignment(assignNode, module, arrayMutations);
3633
+ this.detectIndexedArrayAssignment(assignNode, module, arrayMutations, scopeTracker);
1821
3634
 
1822
3635
  // Initialize object mutations collection if not exists
1823
3636
  if (!collections.objectMutations) {
@@ -1829,42 +3642,335 @@ export class JSASTAnalyzer extends Plugin {
1829
3642
  this.detectObjectPropertyAssignment(assignNode, module, objectMutations, scopeTracker);
1830
3643
  },
1831
3644
 
1832
- ForStatement: this.createLoopScopeHandler('for', 'for-loop', parentScopeId, module, scopes, scopeCounterRef, scopeTracker),
1833
- ForInStatement: this.createLoopScopeHandler('for-in', 'for-in-loop', parentScopeId, module, scopes, scopeCounterRef, scopeTracker),
1834
- ForOfStatement: this.createLoopScopeHandler('for-of', 'for-of-loop', parentScopeId, module, scopes, scopeCounterRef, scopeTracker),
1835
- WhileStatement: this.createLoopScopeHandler('while', 'while-loop', parentScopeId, module, scopes, scopeCounterRef, scopeTracker),
1836
- DoWhileStatement: this.createLoopScopeHandler('do-while', 'do-while-loop', parentScopeId, module, scopes, scopeCounterRef, scopeTracker),
3645
+ // Handle return statements for RETURNS edges
3646
+ ReturnStatement: (returnPath: NodePath<t.ReturnStatement>) => {
3647
+ // Skip if we couldn't determine the function ID
3648
+ if (!currentFunctionId) {
3649
+ return;
3650
+ }
1837
3651
 
1838
- TryStatement: (tryPath: NodePath<t.TryStatement>) => {
1839
- this.handleTryStatement(
1840
- tryPath,
1841
- parentScopeId,
1842
- module,
1843
- scopes,
1844
- variableDeclarations,
1845
- literals,
1846
- variableAssignments,
1847
- scopeCounterRef,
1848
- varDeclCounterRef,
1849
- literalCounterRef,
1850
- scopeTracker
1851
- );
1852
- },
3652
+ // Skip if this return is inside a nested function (not the function we're analyzing)
3653
+ // Check if there's a function ancestor BETWEEN us and funcNode
3654
+ // Stop checking once we reach funcNode - parents above funcNode are outside scope
3655
+ let parent: NodePath | null = returnPath.parentPath;
3656
+ let isInsideConditional = false;
3657
+ while (parent) {
3658
+ // If we've reached funcNode, we're done checking - this return belongs to funcNode
3659
+ if (parent.node === funcNode) {
3660
+ break;
3661
+ }
3662
+ if (t.isFunction(parent.node)) {
3663
+ // Found a function between returnPath and funcNode - this return is inside a nested function
3664
+ return;
3665
+ }
3666
+ // Track if return is inside a conditional block (if/else, switch case, loop, try/catch)
3667
+ if (t.isIfStatement(parent.node) ||
3668
+ t.isSwitchCase(parent.node) ||
3669
+ t.isLoop(parent.node) ||
3670
+ t.isTryStatement(parent.node) ||
3671
+ t.isCatchClause(parent.node)) {
3672
+ isInsideConditional = true;
3673
+ }
3674
+ parent = parent.parentPath;
3675
+ }
1853
3676
 
1854
- SwitchStatement: (switchPath: NodePath<t.SwitchStatement>) => {
1855
- const switchNode = switchPath.node;
1856
- const scopeId = `SCOPE#switch-case#${module.file}#${getLine(switchNode)}:${scopeCounterRef.value++}`;
1857
- const semanticId = this.generateSemanticId('switch-case', scopeTracker);
3677
+ // Phase 6 (REG-267): Track return count and early return detection
3678
+ controlFlowState.returnCount++;
1858
3679
 
1859
- scopes.push({
1860
- id: scopeId,
1861
- type: 'SCOPE',
1862
- scopeType: 'switch-case',
1863
- semanticId,
3680
+ // A return is "early" if it's inside a conditional structure
3681
+ // (More returns after this one indicate the function doesn't always end here)
3682
+ if (isInsideConditional) {
3683
+ controlFlowState.hasEarlyReturn = true;
3684
+ }
3685
+
3686
+ const returnNode = returnPath.node;
3687
+ const returnLine = getLine(returnNode);
3688
+ const returnColumn = getColumn(returnNode);
3689
+
3690
+ // Handle bare return; (no value)
3691
+ if (!returnNode.argument) {
3692
+ // Skip - no data flow value
3693
+ return;
3694
+ }
3695
+
3696
+ const arg = returnNode.argument;
3697
+
3698
+ // Determine return value type and extract relevant info
3699
+ const returnInfo: ReturnStatementInfo = {
3700
+ parentFunctionId: currentFunctionId,
1864
3701
  file: module.file,
1865
- line: getLine(switchNode),
1866
- parentScopeId
1867
- });
3702
+ line: returnLine,
3703
+ column: returnColumn,
3704
+ returnValueType: 'NONE'
3705
+ };
3706
+
3707
+ // Identifier (variable reference)
3708
+ if (t.isIdentifier(arg)) {
3709
+ returnInfo.returnValueType = 'VARIABLE';
3710
+ returnInfo.returnValueName = arg.name;
3711
+ }
3712
+ // TemplateLiteral must come BEFORE isLiteral (TemplateLiteral extends Literal)
3713
+ else if (t.isTemplateLiteral(arg)) {
3714
+ returnInfo.returnValueType = 'EXPRESSION';
3715
+ returnInfo.expressionType = 'TemplateLiteral';
3716
+ returnInfo.returnValueLine = getLine(arg);
3717
+ returnInfo.returnValueColumn = getColumn(arg);
3718
+
3719
+ returnInfo.returnValueId = NodeFactory.generateExpressionId(
3720
+ 'TemplateLiteral',
3721
+ module.file,
3722
+ getLine(arg),
3723
+ getColumn(arg)
3724
+ );
3725
+
3726
+ // Extract all embedded expression identifiers
3727
+ const sourceNames: string[] = [];
3728
+ for (const expr of arg.expressions) {
3729
+ if (t.isIdentifier(expr)) {
3730
+ sourceNames.push(expr.name);
3731
+ }
3732
+ }
3733
+ if (sourceNames.length > 0) {
3734
+ returnInfo.expressionSourceNames = sourceNames;
3735
+ }
3736
+ }
3737
+ // Literal values (after TemplateLiteral check)
3738
+ else if (t.isLiteral(arg)) {
3739
+ returnInfo.returnValueType = 'LITERAL';
3740
+ // Create a LITERAL node ID for this return value
3741
+ const literalId = `LITERAL#return#${module.file}#${returnLine}:${returnColumn}:${literalCounterRef.value++}`;
3742
+ returnInfo.returnValueId = literalId;
3743
+
3744
+ // Also add to literals collection for node creation
3745
+ literals.push({
3746
+ id: literalId,
3747
+ type: 'LITERAL',
3748
+ value: ExpressionEvaluator.extractLiteralValue(arg),
3749
+ valueType: typeof ExpressionEvaluator.extractLiteralValue(arg),
3750
+ file: module.file,
3751
+ line: returnLine,
3752
+ column: returnColumn
3753
+ });
3754
+ }
3755
+ // Direct function call: return foo()
3756
+ else if (t.isCallExpression(arg) && t.isIdentifier(arg.callee)) {
3757
+ returnInfo.returnValueType = 'CALL_SITE';
3758
+ returnInfo.returnValueLine = getLine(arg);
3759
+ returnInfo.returnValueColumn = getColumn(arg);
3760
+ returnInfo.returnValueCallName = arg.callee.name;
3761
+ }
3762
+ // Method call: return obj.method()
3763
+ else if (t.isCallExpression(arg) && t.isMemberExpression(arg.callee)) {
3764
+ returnInfo.returnValueType = 'METHOD_CALL';
3765
+ returnInfo.returnValueLine = getLine(arg);
3766
+ returnInfo.returnValueColumn = getColumn(arg);
3767
+ // Extract method name for lookup
3768
+ if (t.isIdentifier(arg.callee.property)) {
3769
+ returnInfo.returnValueCallName = arg.callee.property.name;
3770
+ }
3771
+ }
3772
+ // BinaryExpression: return a + b
3773
+ else if (t.isBinaryExpression(arg)) {
3774
+ returnInfo.returnValueType = 'EXPRESSION';
3775
+ returnInfo.expressionType = 'BinaryExpression';
3776
+ returnInfo.returnValueLine = getLine(arg);
3777
+ returnInfo.returnValueColumn = getColumn(arg);
3778
+ returnInfo.operator = arg.operator;
3779
+
3780
+ // Generate stable ID for the EXPRESSION node
3781
+ returnInfo.returnValueId = NodeFactory.generateExpressionId(
3782
+ 'BinaryExpression',
3783
+ module.file,
3784
+ getLine(arg),
3785
+ getColumn(arg)
3786
+ );
3787
+
3788
+ // Extract left operand source
3789
+ if (t.isIdentifier(arg.left)) {
3790
+ returnInfo.leftSourceName = arg.left.name;
3791
+ }
3792
+ // Extract right operand source
3793
+ if (t.isIdentifier(arg.right)) {
3794
+ returnInfo.rightSourceName = arg.right.name;
3795
+ }
3796
+ }
3797
+ // LogicalExpression: return a && b, return a || b
3798
+ else if (t.isLogicalExpression(arg)) {
3799
+ returnInfo.returnValueType = 'EXPRESSION';
3800
+ returnInfo.expressionType = 'LogicalExpression';
3801
+ returnInfo.returnValueLine = getLine(arg);
3802
+ returnInfo.returnValueColumn = getColumn(arg);
3803
+ returnInfo.operator = arg.operator;
3804
+
3805
+ returnInfo.returnValueId = NodeFactory.generateExpressionId(
3806
+ 'LogicalExpression',
3807
+ module.file,
3808
+ getLine(arg),
3809
+ getColumn(arg)
3810
+ );
3811
+
3812
+ if (t.isIdentifier(arg.left)) {
3813
+ returnInfo.leftSourceName = arg.left.name;
3814
+ }
3815
+ if (t.isIdentifier(arg.right)) {
3816
+ returnInfo.rightSourceName = arg.right.name;
3817
+ }
3818
+ }
3819
+ // ConditionalExpression: return condition ? a : b
3820
+ else if (t.isConditionalExpression(arg)) {
3821
+ returnInfo.returnValueType = 'EXPRESSION';
3822
+ returnInfo.expressionType = 'ConditionalExpression';
3823
+ returnInfo.returnValueLine = getLine(arg);
3824
+ returnInfo.returnValueColumn = getColumn(arg);
3825
+
3826
+ returnInfo.returnValueId = NodeFactory.generateExpressionId(
3827
+ 'ConditionalExpression',
3828
+ module.file,
3829
+ getLine(arg),
3830
+ getColumn(arg)
3831
+ );
3832
+
3833
+ // Extract consequent (then branch) source
3834
+ if (t.isIdentifier(arg.consequent)) {
3835
+ returnInfo.consequentSourceName = arg.consequent.name;
3836
+ }
3837
+ // Extract alternate (else branch) source
3838
+ if (t.isIdentifier(arg.alternate)) {
3839
+ returnInfo.alternateSourceName = arg.alternate.name;
3840
+ }
3841
+ }
3842
+ // UnaryExpression: return !x, return -x
3843
+ else if (t.isUnaryExpression(arg)) {
3844
+ returnInfo.returnValueType = 'EXPRESSION';
3845
+ returnInfo.expressionType = 'UnaryExpression';
3846
+ returnInfo.returnValueLine = getLine(arg);
3847
+ returnInfo.returnValueColumn = getColumn(arg);
3848
+ returnInfo.operator = arg.operator;
3849
+
3850
+ returnInfo.returnValueId = NodeFactory.generateExpressionId(
3851
+ 'UnaryExpression',
3852
+ module.file,
3853
+ getLine(arg),
3854
+ getColumn(arg)
3855
+ );
3856
+
3857
+ if (t.isIdentifier(arg.argument)) {
3858
+ returnInfo.unaryArgSourceName = arg.argument.name;
3859
+ }
3860
+ }
3861
+ // MemberExpression (property access): return obj.prop
3862
+ else if (t.isMemberExpression(arg)) {
3863
+ returnInfo.returnValueType = 'EXPRESSION';
3864
+ returnInfo.expressionType = 'MemberExpression';
3865
+ returnInfo.returnValueLine = getLine(arg);
3866
+ returnInfo.returnValueColumn = getColumn(arg);
3867
+
3868
+ returnInfo.returnValueId = NodeFactory.generateExpressionId(
3869
+ 'MemberExpression',
3870
+ module.file,
3871
+ getLine(arg),
3872
+ getColumn(arg)
3873
+ );
3874
+
3875
+ // Extract object.property info
3876
+ if (t.isIdentifier(arg.object)) {
3877
+ returnInfo.object = arg.object.name;
3878
+ returnInfo.objectSourceName = arg.object.name;
3879
+ }
3880
+ if (t.isIdentifier(arg.property)) {
3881
+ returnInfo.property = arg.property.name;
3882
+ }
3883
+ returnInfo.computed = arg.computed;
3884
+ }
3885
+ // NewExpression: return new Foo()
3886
+ else if (t.isNewExpression(arg)) {
3887
+ returnInfo.returnValueType = 'EXPRESSION';
3888
+ returnInfo.expressionType = 'NewExpression';
3889
+ returnInfo.returnValueLine = getLine(arg);
3890
+ returnInfo.returnValueColumn = getColumn(arg);
3891
+
3892
+ returnInfo.returnValueId = NodeFactory.generateExpressionId(
3893
+ 'NewExpression',
3894
+ module.file,
3895
+ getLine(arg),
3896
+ getColumn(arg)
3897
+ );
3898
+ }
3899
+ // Fallback for other expression types
3900
+ else {
3901
+ returnInfo.returnValueType = 'EXPRESSION';
3902
+ returnInfo.expressionType = arg.type;
3903
+ returnInfo.returnValueLine = getLine(arg);
3904
+ returnInfo.returnValueColumn = getColumn(arg);
3905
+
3906
+ returnInfo.returnValueId = NodeFactory.generateExpressionId(
3907
+ arg.type,
3908
+ module.file,
3909
+ getLine(arg),
3910
+ getColumn(arg)
3911
+ );
3912
+ }
3913
+
3914
+ returnStatements.push(returnInfo);
3915
+ },
3916
+
3917
+ // Phase 6 (REG-267): Track throw statements for control flow metadata
3918
+ ThrowStatement: (throwPath: NodePath<t.ThrowStatement>) => {
3919
+ // Skip if this throw is inside a nested function (not the function we're analyzing)
3920
+ let parent: NodePath | null = throwPath.parentPath;
3921
+ while (parent) {
3922
+ if (t.isFunction(parent.node) && parent.node !== funcNode) {
3923
+ // This throw is inside a nested function - skip it
3924
+ return;
3925
+ }
3926
+ parent = parent.parentPath;
3927
+ }
3928
+
3929
+ controlFlowState.hasThrow = true;
3930
+ },
3931
+
3932
+ ForStatement: this.createLoopScopeHandler('for', 'for-loop', 'for', parentScopeId, module, scopes, loops, scopeCounterRef, loopCounterRef, scopeTracker, scopeIdStack, controlFlowState),
3933
+ ForInStatement: this.createLoopScopeHandler('for-in', 'for-in-loop', 'for-in', parentScopeId, module, scopes, loops, scopeCounterRef, loopCounterRef, scopeTracker, scopeIdStack, controlFlowState),
3934
+ ForOfStatement: this.createLoopScopeHandler('for-of', 'for-of-loop', 'for-of', parentScopeId, module, scopes, loops, scopeCounterRef, loopCounterRef, scopeTracker, scopeIdStack, controlFlowState),
3935
+ WhileStatement: this.createLoopScopeHandler('while', 'while-loop', 'while', parentScopeId, module, scopes, loops, scopeCounterRef, loopCounterRef, scopeTracker, scopeIdStack, controlFlowState),
3936
+ DoWhileStatement: this.createLoopScopeHandler('do-while', 'do-while-loop', 'do-while', parentScopeId, module, scopes, loops, scopeCounterRef, loopCounterRef, scopeTracker, scopeIdStack, controlFlowState),
3937
+
3938
+ // Phase 4 (REG-267): Now creates TRY_BLOCK, CATCH_BLOCK, FINALLY_BLOCK nodes
3939
+ TryStatement: this.createTryStatementHandler(
3940
+ parentScopeId,
3941
+ module,
3942
+ scopes,
3943
+ tryBlocks,
3944
+ catchBlocks,
3945
+ finallyBlocks,
3946
+ scopeCounterRef,
3947
+ tryBlockCounterRef,
3948
+ catchBlockCounterRef,
3949
+ finallyBlockCounterRef,
3950
+ scopeTracker,
3951
+ tryScopeMap,
3952
+ scopeIdStack,
3953
+ controlFlowState
3954
+ ),
3955
+
3956
+ CatchClause: this.createCatchClauseHandler(
3957
+ module,
3958
+ variableDeclarations,
3959
+ varDeclCounterRef,
3960
+ scopeTracker,
3961
+ tryScopeMap,
3962
+ scopeIdStack
3963
+ ),
3964
+
3965
+ SwitchStatement: (switchPath: NodePath<t.SwitchStatement>) => {
3966
+ this.handleSwitchStatement(
3967
+ switchPath,
3968
+ parentScopeId,
3969
+ module,
3970
+ collections,
3971
+ scopeTracker,
3972
+ controlFlowState
3973
+ );
1868
3974
  },
1869
3975
 
1870
3976
  FunctionExpression: (funcPath: NodePath<t.FunctionExpression>) => {
@@ -1971,6 +4077,143 @@ export class JSASTAnalyzer extends Plugin {
1971
4077
  if (scopeTracker) {
1972
4078
  scopeTracker.exitScope();
1973
4079
  }
4080
+ } else {
4081
+ // Arrow function with expression body (implicit return)
4082
+ // e.g., x => x * 2, () => 42
4083
+ const bodyExpr = node.body;
4084
+ const bodyLine = getLine(bodyExpr);
4085
+ const bodyColumn = getColumn(bodyExpr);
4086
+
4087
+ const returnInfo: ReturnStatementInfo = {
4088
+ parentFunctionId: functionId,
4089
+ file: module.file,
4090
+ line: bodyLine,
4091
+ column: bodyColumn,
4092
+ returnValueType: 'NONE',
4093
+ isImplicitReturn: true
4094
+ };
4095
+
4096
+ // Apply same type detection logic as ReturnStatement handler
4097
+ if (t.isIdentifier(bodyExpr)) {
4098
+ returnInfo.returnValueType = 'VARIABLE';
4099
+ returnInfo.returnValueName = bodyExpr.name;
4100
+ }
4101
+ // TemplateLiteral must come BEFORE isLiteral (TemplateLiteral extends Literal)
4102
+ else if (t.isTemplateLiteral(bodyExpr)) {
4103
+ returnInfo.returnValueType = 'EXPRESSION';
4104
+ returnInfo.expressionType = 'TemplateLiteral';
4105
+ returnInfo.returnValueLine = getLine(bodyExpr);
4106
+ returnInfo.returnValueColumn = getColumn(bodyExpr);
4107
+ returnInfo.returnValueId = NodeFactory.generateExpressionId(
4108
+ 'TemplateLiteral', module.file, getLine(bodyExpr), getColumn(bodyExpr)
4109
+ );
4110
+ const sourceNames: string[] = [];
4111
+ for (const expr of bodyExpr.expressions) {
4112
+ if (t.isIdentifier(expr)) sourceNames.push(expr.name);
4113
+ }
4114
+ if (sourceNames.length > 0) returnInfo.expressionSourceNames = sourceNames;
4115
+ }
4116
+ else if (t.isLiteral(bodyExpr)) {
4117
+ returnInfo.returnValueType = 'LITERAL';
4118
+ const literalId = `LITERAL#implicit_return#${module.file}#${line}:${column}:${literalCounterRef.value++}`;
4119
+ returnInfo.returnValueId = literalId;
4120
+ literals.push({
4121
+ id: literalId,
4122
+ type: 'LITERAL',
4123
+ value: ExpressionEvaluator.extractLiteralValue(bodyExpr),
4124
+ valueType: typeof ExpressionEvaluator.extractLiteralValue(bodyExpr),
4125
+ file: module.file,
4126
+ line: bodyLine,
4127
+ column: bodyColumn
4128
+ });
4129
+ }
4130
+ else if (t.isCallExpression(bodyExpr) && t.isIdentifier(bodyExpr.callee)) {
4131
+ returnInfo.returnValueType = 'CALL_SITE';
4132
+ returnInfo.returnValueLine = getLine(bodyExpr);
4133
+ returnInfo.returnValueColumn = getColumn(bodyExpr);
4134
+ returnInfo.returnValueCallName = bodyExpr.callee.name;
4135
+ }
4136
+ else if (t.isCallExpression(bodyExpr) && t.isMemberExpression(bodyExpr.callee)) {
4137
+ returnInfo.returnValueType = 'METHOD_CALL';
4138
+ returnInfo.returnValueLine = getLine(bodyExpr);
4139
+ returnInfo.returnValueColumn = getColumn(bodyExpr);
4140
+ if (t.isIdentifier(bodyExpr.callee.property)) {
4141
+ returnInfo.returnValueCallName = bodyExpr.callee.property.name;
4142
+ }
4143
+ }
4144
+ // REG-276: Detailed EXPRESSION handling for nested implicit arrow returns
4145
+ else if (t.isBinaryExpression(bodyExpr)) {
4146
+ returnInfo.returnValueType = 'EXPRESSION';
4147
+ returnInfo.expressionType = 'BinaryExpression';
4148
+ returnInfo.returnValueLine = getLine(bodyExpr);
4149
+ returnInfo.returnValueColumn = getColumn(bodyExpr);
4150
+ returnInfo.operator = bodyExpr.operator;
4151
+ returnInfo.returnValueId = NodeFactory.generateExpressionId(
4152
+ 'BinaryExpression', module.file, getLine(bodyExpr), getColumn(bodyExpr)
4153
+ );
4154
+ if (t.isIdentifier(bodyExpr.left)) returnInfo.leftSourceName = bodyExpr.left.name;
4155
+ if (t.isIdentifier(bodyExpr.right)) returnInfo.rightSourceName = bodyExpr.right.name;
4156
+ }
4157
+ else if (t.isLogicalExpression(bodyExpr)) {
4158
+ returnInfo.returnValueType = 'EXPRESSION';
4159
+ returnInfo.expressionType = 'LogicalExpression';
4160
+ returnInfo.returnValueLine = getLine(bodyExpr);
4161
+ returnInfo.returnValueColumn = getColumn(bodyExpr);
4162
+ returnInfo.operator = bodyExpr.operator;
4163
+ returnInfo.returnValueId = NodeFactory.generateExpressionId(
4164
+ 'LogicalExpression', module.file, getLine(bodyExpr), getColumn(bodyExpr)
4165
+ );
4166
+ if (t.isIdentifier(bodyExpr.left)) returnInfo.leftSourceName = bodyExpr.left.name;
4167
+ if (t.isIdentifier(bodyExpr.right)) returnInfo.rightSourceName = bodyExpr.right.name;
4168
+ }
4169
+ else if (t.isConditionalExpression(bodyExpr)) {
4170
+ returnInfo.returnValueType = 'EXPRESSION';
4171
+ returnInfo.expressionType = 'ConditionalExpression';
4172
+ returnInfo.returnValueLine = getLine(bodyExpr);
4173
+ returnInfo.returnValueColumn = getColumn(bodyExpr);
4174
+ returnInfo.returnValueId = NodeFactory.generateExpressionId(
4175
+ 'ConditionalExpression', module.file, getLine(bodyExpr), getColumn(bodyExpr)
4176
+ );
4177
+ if (t.isIdentifier(bodyExpr.consequent)) returnInfo.consequentSourceName = bodyExpr.consequent.name;
4178
+ if (t.isIdentifier(bodyExpr.alternate)) returnInfo.alternateSourceName = bodyExpr.alternate.name;
4179
+ }
4180
+ else if (t.isUnaryExpression(bodyExpr)) {
4181
+ returnInfo.returnValueType = 'EXPRESSION';
4182
+ returnInfo.expressionType = 'UnaryExpression';
4183
+ returnInfo.returnValueLine = getLine(bodyExpr);
4184
+ returnInfo.returnValueColumn = getColumn(bodyExpr);
4185
+ returnInfo.operator = bodyExpr.operator;
4186
+ returnInfo.returnValueId = NodeFactory.generateExpressionId(
4187
+ 'UnaryExpression', module.file, getLine(bodyExpr), getColumn(bodyExpr)
4188
+ );
4189
+ if (t.isIdentifier(bodyExpr.argument)) returnInfo.unaryArgSourceName = bodyExpr.argument.name;
4190
+ }
4191
+ else if (t.isMemberExpression(bodyExpr)) {
4192
+ returnInfo.returnValueType = 'EXPRESSION';
4193
+ returnInfo.expressionType = 'MemberExpression';
4194
+ returnInfo.returnValueLine = getLine(bodyExpr);
4195
+ returnInfo.returnValueColumn = getColumn(bodyExpr);
4196
+ returnInfo.returnValueId = NodeFactory.generateExpressionId(
4197
+ 'MemberExpression', module.file, getLine(bodyExpr), getColumn(bodyExpr)
4198
+ );
4199
+ if (t.isIdentifier(bodyExpr.object)) {
4200
+ returnInfo.object = bodyExpr.object.name;
4201
+ returnInfo.objectSourceName = bodyExpr.object.name;
4202
+ }
4203
+ if (t.isIdentifier(bodyExpr.property)) returnInfo.property = bodyExpr.property.name;
4204
+ returnInfo.computed = bodyExpr.computed;
4205
+ }
4206
+ else {
4207
+ returnInfo.returnValueType = 'EXPRESSION';
4208
+ returnInfo.expressionType = bodyExpr.type;
4209
+ returnInfo.returnValueLine = getLine(bodyExpr);
4210
+ returnInfo.returnValueColumn = getColumn(bodyExpr);
4211
+ returnInfo.returnValueId = NodeFactory.generateExpressionId(
4212
+ bodyExpr.type, module.file, getLine(bodyExpr), getColumn(bodyExpr)
4213
+ );
4214
+ }
4215
+
4216
+ returnStatements.push(returnInfo);
1974
4217
  }
1975
4218
 
1976
4219
  arrowPath.skip();
@@ -1978,6 +4221,11 @@ export class JSASTAnalyzer extends Plugin {
1978
4221
 
1979
4222
  UpdateExpression: (updatePath: NodePath<t.UpdateExpression>) => {
1980
4223
  const updateNode = updatePath.node;
4224
+
4225
+ // REG-288/REG-312: Collect update expression info for graph building
4226
+ this.collectUpdateExpression(updateNode, module, updateExpressions, getCurrentScopeId(), scopeTracker);
4227
+
4228
+ // Legacy behavior: update scope.modifies for IDENTIFIER targets
1981
4229
  if (updateNode.argument.type === 'Identifier') {
1982
4230
  const varName = updateNode.argument.name;
1983
4231
 
@@ -2001,18 +4249,36 @@ export class JSASTAnalyzer extends Plugin {
2001
4249
  },
2002
4250
 
2003
4251
  // IF statements - создаём условные scope и обходим содержимое для CALL узлов
4252
+ // Phase 3 (REG-267): Now creates BRANCH nodes with branchType='if'
2004
4253
  IfStatement: this.createIfStatementHandler(
2005
4254
  parentScopeId,
2006
4255
  module,
2007
4256
  scopes,
4257
+ branches,
2008
4258
  ifScopeCounterRef,
4259
+ branchCounterRef,
2009
4260
  scopeTracker,
2010
4261
  collections.code ?? '',
2011
- ifElseScopeMap
4262
+ ifElseScopeMap,
4263
+ scopeIdStack,
4264
+ controlFlowState,
4265
+ this.countLogicalOperators.bind(this)
4266
+ ),
4267
+
4268
+ // Ternary expressions (REG-287): Creates BRANCH nodes with branchType='ternary'
4269
+ ConditionalExpression: this.createConditionalExpressionHandler(
4270
+ parentScopeId,
4271
+ module,
4272
+ branches,
4273
+ branchCounterRef,
4274
+ scopeTracker,
4275
+ scopeIdStack,
4276
+ controlFlowState,
4277
+ this.countLogicalOperators.bind(this)
2012
4278
  ),
2013
4279
 
2014
4280
  // Track when we enter the alternate (else) block of an IfStatement
2015
- BlockStatement: this.createIfElseBlockStatementHandler(scopeTracker, ifElseScopeMap),
4281
+ BlockStatement: this.createBlockStatementHandler(scopeTracker, ifElseScopeMap, tryScopeMap, scopeIdStack),
2016
4282
 
2017
4283
  // Function call expressions
2018
4284
  CallExpression: (callPath: NodePath<t.CallExpression>) => {
@@ -2025,18 +4291,185 @@ export class JSASTAnalyzer extends Plugin {
2025
4291
  module,
2026
4292
  callSiteCounterRef,
2027
4293
  scopeTracker,
2028
- parentScopeId,
4294
+ getCurrentScopeId(),
2029
4295
  collections
2030
4296
  );
4297
+
4298
+ // REG-334: Check for resolve/reject calls inside Promise executors
4299
+ const callNode = callPath.node;
4300
+ if (t.isIdentifier(callNode.callee)) {
4301
+ const calleeName = callNode.callee.name;
4302
+
4303
+ // Walk up function parents to find Promise executor context
4304
+ // This handles nested callbacks like: new Promise((resolve) => { db.query((err, data) => { resolve(data); }); });
4305
+ let funcParent = callPath.getFunctionParent();
4306
+ while (funcParent) {
4307
+ const funcNode = funcParent.node;
4308
+ const funcKey = `${funcNode.start}:${funcNode.end}`;
4309
+ const context = promiseExecutorContexts.get(funcKey);
4310
+
4311
+ if (context) {
4312
+ const isResolve = calleeName === context.resolveName;
4313
+ const isReject = calleeName === context.rejectName;
4314
+
4315
+ if (isResolve || isReject) {
4316
+ // Find the CALL node ID for this resolve/reject call
4317
+ // It was just added by handleCallExpression
4318
+ const callLine = getLine(callNode);
4319
+ const callColumn = getColumn(callNode);
4320
+
4321
+ // Find matching call site that was just added
4322
+ const resolveCall = callSites.find(cs =>
4323
+ cs.name === calleeName &&
4324
+ cs.file === module.file &&
4325
+ cs.line === callLine &&
4326
+ cs.column === callColumn
4327
+ );
4328
+
4329
+ if (resolveCall) {
4330
+ promiseResolutions.push({
4331
+ callId: resolveCall.id,
4332
+ constructorCallId: context.constructorCallId,
4333
+ isReject,
4334
+ file: module.file,
4335
+ line: callLine
4336
+ });
4337
+
4338
+ // REG-334: Collect arguments for resolve/reject calls
4339
+ // This enables traceValues to follow PASSES_ARGUMENT edges
4340
+ if (!collections.callArguments) {
4341
+ collections.callArguments = [];
4342
+ }
4343
+ const callArgumentsArr = collections.callArguments as CallArgumentInfo[];
4344
+
4345
+ // Process arguments (typically just one: resolve(value))
4346
+ callNode.arguments.forEach((arg, argIndex) => {
4347
+ const argInfo: CallArgumentInfo = {
4348
+ callId: resolveCall.id,
4349
+ argIndex,
4350
+ file: module.file,
4351
+ line: getLine(arg),
4352
+ column: getColumn(arg)
4353
+ };
4354
+
4355
+ // Handle different argument types
4356
+ if (t.isIdentifier(arg)) {
4357
+ argInfo.targetType = 'VARIABLE';
4358
+ argInfo.targetName = arg.name;
4359
+ } else if (t.isLiteral(arg) && !t.isTemplateLiteral(arg)) {
4360
+ // Create LITERAL node for the argument value
4361
+ const literalValue = ExpressionEvaluator.extractLiteralValue(arg as t.Literal);
4362
+ if (literalValue !== null) {
4363
+ const argLine = getLine(arg);
4364
+ const argColumn = getColumn(arg);
4365
+ const literalId = `LITERAL#arg${argIndex}#${module.file}#${argLine}:${argColumn}:${literalCounterRef.value++}`;
4366
+ literals.push({
4367
+ id: literalId,
4368
+ type: 'LITERAL',
4369
+ value: literalValue,
4370
+ valueType: typeof literalValue,
4371
+ file: module.file,
4372
+ line: argLine,
4373
+ column: argColumn,
4374
+ parentCallId: resolveCall.id,
4375
+ argIndex
4376
+ });
4377
+ argInfo.targetType = 'LITERAL';
4378
+ argInfo.targetId = literalId;
4379
+ argInfo.literalValue = literalValue;
4380
+ }
4381
+ } else if (t.isCallExpression(arg)) {
4382
+ argInfo.targetType = 'CALL';
4383
+ argInfo.nestedCallLine = getLine(arg);
4384
+ argInfo.nestedCallColumn = getColumn(arg);
4385
+ } else {
4386
+ argInfo.targetType = 'EXPRESSION';
4387
+ argInfo.expressionType = arg.type;
4388
+ }
4389
+
4390
+ callArgumentsArr.push(argInfo);
4391
+ });
4392
+ }
4393
+
4394
+ break; // Found context, stop searching
4395
+ }
4396
+ }
4397
+
4398
+ funcParent = funcParent.getFunctionParent();
4399
+ }
4400
+ }
2031
4401
  },
2032
4402
 
2033
4403
  // NewExpression (constructor calls)
2034
4404
  NewExpression: (newPath: NodePath<t.NewExpression>) => {
2035
4405
  const newNode = newPath.node;
4406
+ const nodeKey = `new:${newNode.start}:${newNode.end}`;
4407
+
4408
+ // Determine className from callee
4409
+ let className: string | null = null;
4410
+ if (newNode.callee.type === 'Identifier') {
4411
+ className = newNode.callee.name;
4412
+ } else if (newNode.callee.type === 'MemberExpression' && newNode.callee.property.type === 'Identifier') {
4413
+ className = newNode.callee.property.name;
4414
+ }
4415
+
4416
+ // Create CONSTRUCTOR_CALL node (always, for all NewExpressions)
4417
+ if (className) {
4418
+ const constructorKey = `constructor:${nodeKey}`;
4419
+ if (!processedCallSites.has(constructorKey)) {
4420
+ processedCallSites.add(constructorKey);
4421
+
4422
+ const line = getLine(newNode);
4423
+ const column = getColumn(newNode);
4424
+ const constructorCallId = ConstructorCallNode.generateId(className, module.file, line, column);
4425
+ const isBuiltin = ConstructorCallNode.isBuiltinConstructor(className);
4426
+
4427
+ constructorCalls.push({
4428
+ id: constructorCallId,
4429
+ type: 'CONSTRUCTOR_CALL',
4430
+ className,
4431
+ isBuiltin,
4432
+ file: module.file,
4433
+ line,
4434
+ column
4435
+ });
4436
+
4437
+ // REG-334: If this is Promise constructor with executor callback,
4438
+ // register the context for resolve/reject detection
4439
+ if (className === 'Promise' && newNode.arguments.length > 0) {
4440
+ const executorArg = newNode.arguments[0];
4441
+
4442
+ // Only handle inline function expressions (not variable references)
4443
+ if (t.isArrowFunctionExpression(executorArg) || t.isFunctionExpression(executorArg)) {
4444
+ // Extract resolve/reject parameter names
4445
+ let resolveName: string | undefined;
4446
+ let rejectName: string | undefined;
4447
+
4448
+ if (executorArg.params.length > 0 && t.isIdentifier(executorArg.params[0])) {
4449
+ resolveName = executorArg.params[0].name;
4450
+ }
4451
+ if (executorArg.params.length > 1 && t.isIdentifier(executorArg.params[1])) {
4452
+ rejectName = executorArg.params[1].name;
4453
+ }
4454
+
4455
+ if (resolveName) {
4456
+ // Key by function node position to allow nested Promise detection
4457
+ const funcKey = `${executorArg.start}:${executorArg.end}`;
4458
+ promiseExecutorContexts.set(funcKey, {
4459
+ constructorCallId,
4460
+ resolveName,
4461
+ rejectName,
4462
+ file: module.file,
4463
+ line
4464
+ });
4465
+ }
4466
+ }
4467
+ }
4468
+ }
4469
+ }
2036
4470
 
2037
4471
  // Handle simple constructor: new Foo()
2038
4472
  if (newNode.callee.type === 'Identifier') {
2039
- const nodeKey = `new:${newNode.start}:${newNode.end}`;
2040
4473
  if (processedCallSites.has(nodeKey)) {
2041
4474
  return;
2042
4475
  }
@@ -2058,7 +4491,7 @@ export class JSASTAnalyzer extends Plugin {
2058
4491
  name: constructorName,
2059
4492
  file: module.file,
2060
4493
  line: getLine(newNode),
2061
- parentScopeId,
4494
+ parentScopeId: getCurrentScopeId(),
2062
4495
  targetFunctionName: constructorName,
2063
4496
  isNew: true
2064
4497
  });
@@ -2070,7 +4503,6 @@ export class JSASTAnalyzer extends Plugin {
2070
4503
  const property = memberCallee.property;
2071
4504
 
2072
4505
  if (object.type === 'Identifier' && property.type === 'Identifier') {
2073
- const nodeKey = `new:${newNode.start}:${newNode.end}`;
2074
4506
  if (processedMethodCalls.has(nodeKey)) {
2075
4507
  return;
2076
4508
  }
@@ -2098,13 +4530,31 @@ export class JSASTAnalyzer extends Plugin {
2098
4530
  file: module.file,
2099
4531
  line: getLine(newNode),
2100
4532
  column: getColumn(newNode),
2101
- parentScopeId,
4533
+ parentScopeId: getCurrentScopeId(),
2102
4534
  isNew: true
2103
4535
  });
2104
4536
  }
2105
4537
  }
2106
4538
  }
2107
4539
  });
4540
+
4541
+ // Phase 6 (REG-267): Attach control flow metadata to the function node
4542
+ if (matchingFunction) {
4543
+ const cyclomaticComplexity = 1 +
4544
+ controlFlowState.branchCount +
4545
+ controlFlowState.loopCount +
4546
+ controlFlowState.caseCount +
4547
+ controlFlowState.logicalOpCount;
4548
+
4549
+ matchingFunction.controlFlow = {
4550
+ hasBranches: controlFlowState.branchCount > 0,
4551
+ hasLoops: controlFlowState.loopCount > 0,
4552
+ hasTryCatch: controlFlowState.hasTryCatch,
4553
+ hasEarlyReturn: controlFlowState.hasEarlyReturn,
4554
+ hasThrow: controlFlowState.hasThrow,
4555
+ cyclomaticComplexity
4556
+ };
4557
+ }
2108
4558
  }
2109
4559
 
2110
4560
  /**
@@ -2164,6 +4614,7 @@ export class JSASTAnalyzer extends Plugin {
2164
4614
  name: calleeName,
2165
4615
  file: module.file,
2166
4616
  line: getLine(callNode),
4617
+ column: getColumn(callNode), // REG-223: Add column for coordinate-based lookup
2167
4618
  parentScopeId,
2168
4619
  targetFunctionName: calleeName
2169
4620
  });
@@ -2390,7 +4841,8 @@ export class JSASTAnalyzer extends Plugin {
2390
4841
  private detectIndexedArrayAssignment(
2391
4842
  assignNode: t.AssignmentExpression,
2392
4843
  module: VisitorModule,
2393
- arrayMutations: ArrayMutationInfo[]
4844
+ arrayMutations: ArrayMutationInfo[],
4845
+ scopeTracker?: ScopeTracker
2394
4846
  ): void {
2395
4847
  // Check for indexed array assignment: arr[i] = value
2396
4848
  if (assignNode.left.type === 'MemberExpression' && assignNode.left.computed) {
@@ -2437,8 +4889,12 @@ export class JSASTAnalyzer extends Plugin {
2437
4889
  const line = assignNode.loc?.start.line ?? 0;
2438
4890
  const column = assignNode.loc?.start.column ?? 0;
2439
4891
 
4892
+ // Capture scope path for scope-aware lookup (REG-309)
4893
+ const scopePath = scopeTracker?.getContext().scopePath ?? [];
4894
+
2440
4895
  arrayMutations.push({
2441
4896
  arrayName,
4897
+ mutationScopePath: scopePath,
2442
4898
  mutationMethod: 'indexed',
2443
4899
  file: module.file,
2444
4900
  line: line,
@@ -2530,6 +4986,9 @@ export class JSASTAnalyzer extends Plugin {
2530
4986
  const line = assignNode.loc?.start.line ?? 0;
2531
4987
  const column = assignNode.loc?.start.column ?? 0;
2532
4988
 
4989
+ // Capture scope path for scope-aware lookup (REG-309)
4990
+ const scopePath = scopeTracker?.getContext().scopePath ?? [];
4991
+
2533
4992
  // Generate semantic ID if scopeTracker available
2534
4993
  let mutationId: string | undefined;
2535
4994
  if (scopeTracker) {
@@ -2540,6 +4999,7 @@ export class JSASTAnalyzer extends Plugin {
2540
4999
  objectMutations.push({
2541
5000
  id: mutationId,
2542
5001
  objectName,
5002
+ mutationScopePath: scopePath,
2543
5003
  enclosingClassName, // REG-152: Class name for 'this' mutations
2544
5004
  propertyName,
2545
5005
  mutationType,
@@ -2551,6 +5011,244 @@ export class JSASTAnalyzer extends Plugin {
2551
5011
  });
2552
5012
  }
2553
5013
 
5014
+ /**
5015
+ * Collect update expression info for graph building (i++, obj.prop++, arr[i]++).
5016
+ *
5017
+ * REG-288: Simple identifiers (i++, --count)
5018
+ * REG-312: Member expressions (obj.prop++, arr[i]++, this.count++)
5019
+ *
5020
+ * Creates UpdateExpressionInfo entries that GraphBuilder uses to create:
5021
+ * - UPDATE_EXPRESSION nodes
5022
+ * - MODIFIES edges to target variables/objects
5023
+ * - READS_FROM self-loops
5024
+ * - CONTAINS edges for scope hierarchy
5025
+ */
5026
+ private collectUpdateExpression(
5027
+ updateNode: t.UpdateExpression,
5028
+ module: VisitorModule,
5029
+ updateExpressions: UpdateExpressionInfo[],
5030
+ parentScopeId: string | undefined,
5031
+ scopeTracker?: ScopeTracker
5032
+ ): void {
5033
+ const operator = updateNode.operator as '++' | '--';
5034
+ const prefix = updateNode.prefix;
5035
+ const line = getLine(updateNode);
5036
+ const column = getColumn(updateNode);
5037
+
5038
+ // CASE 1: Simple identifier (i++, --count) - REG-288 behavior
5039
+ if (updateNode.argument.type === 'Identifier') {
5040
+ const variableName = updateNode.argument.name;
5041
+
5042
+ updateExpressions.push({
5043
+ targetType: 'IDENTIFIER',
5044
+ variableName,
5045
+ variableLine: getLine(updateNode.argument),
5046
+ operator,
5047
+ prefix,
5048
+ file: module.file,
5049
+ line,
5050
+ column,
5051
+ parentScopeId
5052
+ });
5053
+ return;
5054
+ }
5055
+
5056
+ // CASE 2: Member expression (obj.prop++, arr[i]++) - REG-312 new
5057
+ if (updateNode.argument.type === 'MemberExpression') {
5058
+ const memberExpr = updateNode.argument;
5059
+
5060
+ // Extract object name (reuses detectObjectPropertyAssignment pattern)
5061
+ let objectName: string;
5062
+ let enclosingClassName: string | undefined;
5063
+
5064
+ if (memberExpr.object.type === 'Identifier') {
5065
+ objectName = memberExpr.object.name;
5066
+ } else if (memberExpr.object.type === 'ThisExpression') {
5067
+ objectName = 'this';
5068
+ // REG-152: Extract enclosing class name from scope context
5069
+ if (scopeTracker) {
5070
+ enclosingClassName = scopeTracker.getEnclosingScope('CLASS');
5071
+ }
5072
+ } else {
5073
+ // Complex expressions: obj.nested.prop++, (obj || fallback).count++
5074
+ // Skip for now (documented limitation, same as detectObjectPropertyAssignment)
5075
+ return;
5076
+ }
5077
+
5078
+ // Extract property name (reuses detectObjectPropertyAssignment pattern)
5079
+ let propertyName: string;
5080
+ let mutationType: 'property' | 'computed';
5081
+ let computedPropertyVar: string | undefined;
5082
+
5083
+ if (!memberExpr.computed) {
5084
+ // obj.prop++
5085
+ if (memberExpr.property.type === 'Identifier') {
5086
+ propertyName = memberExpr.property.name;
5087
+ mutationType = 'property';
5088
+ } else {
5089
+ return; // Unexpected property type
5090
+ }
5091
+ } else {
5092
+ // obj['prop']++ or obj[key]++
5093
+ if (memberExpr.property.type === 'StringLiteral') {
5094
+ // obj['prop']++ - static string
5095
+ propertyName = memberExpr.property.value;
5096
+ mutationType = 'property';
5097
+ } else {
5098
+ // obj[key]++, arr[i]++ - computed property
5099
+ propertyName = '<computed>';
5100
+ mutationType = 'computed';
5101
+ if (memberExpr.property.type === 'Identifier') {
5102
+ computedPropertyVar = memberExpr.property.name;
5103
+ }
5104
+ }
5105
+ }
5106
+
5107
+ updateExpressions.push({
5108
+ targetType: 'MEMBER_EXPRESSION',
5109
+ objectName,
5110
+ objectLine: getLine(memberExpr.object),
5111
+ enclosingClassName,
5112
+ propertyName,
5113
+ mutationType,
5114
+ computedPropertyVar,
5115
+ operator,
5116
+ prefix,
5117
+ file: module.file,
5118
+ line,
5119
+ column,
5120
+ parentScopeId
5121
+ });
5122
+ }
5123
+ }
5124
+
5125
+ /**
5126
+ * Detect variable reassignment for FLOWS_INTO edge creation.
5127
+ * Handles all assignment operators: =, +=, -=, *=, /=, etc.
5128
+ *
5129
+ * Captures COMPLETE metadata for:
5130
+ * - LITERAL values (literalValue field)
5131
+ * - EXPRESSION nodes (expressionType, expressionMetadata fields)
5132
+ * - VARIABLE, CALL_SITE, METHOD_CALL references
5133
+ *
5134
+ * REG-290: No deferred functionality - all value types captured.
5135
+ */
5136
+ private detectVariableReassignment(
5137
+ assignNode: t.AssignmentExpression,
5138
+ module: VisitorModule,
5139
+ variableReassignments: VariableReassignmentInfo[],
5140
+ scopeTracker?: ScopeTracker
5141
+ ): void {
5142
+ // LHS must be simple identifier (checked by caller)
5143
+ const leftId = assignNode.left as t.Identifier;
5144
+ const variableName = leftId.name;
5145
+ const operator = assignNode.operator; // '=', '+=', '-=', etc.
5146
+
5147
+ // Get RHS value info
5148
+ const rightExpr = assignNode.right;
5149
+ const line = getLine(assignNode);
5150
+ const column = getColumn(assignNode);
5151
+
5152
+ // Extract value source (similar to VariableVisitor pattern)
5153
+ let valueType: 'VARIABLE' | 'CALL_SITE' | 'METHOD_CALL' | 'LITERAL' | 'EXPRESSION';
5154
+ let valueName: string | undefined;
5155
+ let valueId: string | null = null;
5156
+ let callLine: number | undefined;
5157
+ let callColumn: number | undefined;
5158
+
5159
+ // Complete metadata for node creation
5160
+ let literalValue: unknown;
5161
+ let expressionType: string | undefined;
5162
+ let expressionMetadata: VariableReassignmentInfo['expressionMetadata'];
5163
+
5164
+ // 1. Literal value
5165
+ const extractedLiteralValue = ExpressionEvaluator.extractLiteralValue(rightExpr);
5166
+ if (extractedLiteralValue !== null) {
5167
+ valueType = 'LITERAL';
5168
+ valueId = `LITERAL#${line}:${rightExpr.start}#${module.file}`;
5169
+ literalValue = extractedLiteralValue; // Store for GraphBuilder
5170
+ }
5171
+ // 2. Simple identifier (variable reference)
5172
+ else if (rightExpr.type === 'Identifier') {
5173
+ valueType = 'VARIABLE';
5174
+ valueName = rightExpr.name;
5175
+ }
5176
+ // 3. CallExpression (function call)
5177
+ else if (rightExpr.type === 'CallExpression' && rightExpr.callee.type === 'Identifier') {
5178
+ valueType = 'CALL_SITE';
5179
+ valueName = rightExpr.callee.name;
5180
+ callLine = getLine(rightExpr);
5181
+ callColumn = getColumn(rightExpr);
5182
+ }
5183
+ // 4. MemberExpression (method call: obj.method())
5184
+ else if (rightExpr.type === 'CallExpression' && rightExpr.callee.type === 'MemberExpression') {
5185
+ valueType = 'METHOD_CALL';
5186
+ callLine = getLine(rightExpr);
5187
+ callColumn = getColumn(rightExpr);
5188
+ }
5189
+ // 5. Everything else is EXPRESSION
5190
+ else {
5191
+ valueType = 'EXPRESSION';
5192
+ expressionType = rightExpr.type; // Store AST node type
5193
+ // Use correct EXPRESSION ID format: {file}:EXPRESSION:{type}:{line}:{column}
5194
+ valueId = `${module.file}:EXPRESSION:${expressionType}:${line}:${column}`;
5195
+
5196
+ // Extract type-specific metadata (matches VariableAssignmentInfo pattern)
5197
+ expressionMetadata = {};
5198
+
5199
+ // MemberExpression: obj.prop or obj[key]
5200
+ if (rightExpr.type === 'MemberExpression') {
5201
+ const objName = rightExpr.object.type === 'Identifier' ? rightExpr.object.name : undefined;
5202
+ const propName = rightExpr.property.type === 'Identifier' ? rightExpr.property.name : undefined;
5203
+ const computed = rightExpr.computed;
5204
+
5205
+ expressionMetadata.object = objName;
5206
+ expressionMetadata.property = propName;
5207
+ expressionMetadata.computed = computed;
5208
+
5209
+ // Computed property variable: obj[varName]
5210
+ if (computed && rightExpr.property.type === 'Identifier') {
5211
+ expressionMetadata.computedPropertyVar = rightExpr.property.name;
5212
+ }
5213
+ }
5214
+ // BinaryExpression: a + b, a - b, etc.
5215
+ else if (rightExpr.type === 'BinaryExpression' || rightExpr.type === 'LogicalExpression') {
5216
+ expressionMetadata.operator = rightExpr.operator;
5217
+ expressionMetadata.leftSourceName = rightExpr.left.type === 'Identifier' ? rightExpr.left.name : undefined;
5218
+ expressionMetadata.rightSourceName = rightExpr.right.type === 'Identifier' ? rightExpr.right.name : undefined;
5219
+ }
5220
+ // ConditionalExpression: condition ? a : b
5221
+ else if (rightExpr.type === 'ConditionalExpression') {
5222
+ expressionMetadata.consequentSourceName = rightExpr.consequent.type === 'Identifier' ? rightExpr.consequent.name : undefined;
5223
+ expressionMetadata.alternateSourceName = rightExpr.alternate.type === 'Identifier' ? rightExpr.alternate.name : undefined;
5224
+ }
5225
+ // Add more expression types as needed
5226
+ }
5227
+
5228
+ // Capture scope path for scope-aware lookup (REG-309)
5229
+ const scopePath = scopeTracker?.getContext().scopePath ?? [];
5230
+
5231
+ // Push reassignment info to collection
5232
+ variableReassignments.push({
5233
+ variableName,
5234
+ variableLine: getLine(leftId),
5235
+ mutationScopePath: scopePath,
5236
+ valueType,
5237
+ valueName,
5238
+ valueId,
5239
+ callLine,
5240
+ callColumn,
5241
+ operator,
5242
+ // Complete metadata
5243
+ literalValue,
5244
+ expressionType,
5245
+ expressionMetadata,
5246
+ file: module.file,
5247
+ line,
5248
+ column
5249
+ });
5250
+ }
5251
+
2554
5252
  /**
2555
5253
  * Extract value information from an expression for mutation tracking
2556
5254
  */