@grafema/core 0.1.0-alpha.5 → 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 (530) hide show
  1. package/README.md +0 -1
  2. package/dist/Orchestrator.d.ts +31 -2
  3. package/dist/Orchestrator.d.ts.map +1 -1
  4. package/dist/Orchestrator.js +222 -27
  5. package/dist/config/ConfigLoader.d.ts +90 -0
  6. package/dist/config/ConfigLoader.d.ts.map +1 -0
  7. package/dist/config/ConfigLoader.js +249 -0
  8. package/dist/config/index.d.ts +6 -0
  9. package/dist/config/index.d.ts.map +1 -0
  10. package/dist/config/index.js +4 -0
  11. package/dist/core/ASTWorker.d.ts +11 -36
  12. package/dist/core/ASTWorker.d.ts.map +1 -1
  13. package/dist/core/ASTWorker.js +93 -99
  14. package/dist/core/CoverageAnalyzer.d.ts +65 -0
  15. package/dist/core/CoverageAnalyzer.d.ts.map +1 -0
  16. package/dist/core/CoverageAnalyzer.js +198 -0
  17. package/dist/core/FileExplainer.d.ts +101 -0
  18. package/dist/core/FileExplainer.d.ts.map +1 -0
  19. package/dist/core/FileExplainer.js +139 -0
  20. package/dist/core/FileNodeManager.d.ts +40 -0
  21. package/dist/core/FileNodeManager.d.ts.map +1 -0
  22. package/dist/core/FileNodeManager.js +84 -0
  23. package/dist/core/GraphFreshnessChecker.d.ts +33 -0
  24. package/dist/core/GraphFreshnessChecker.d.ts.map +1 -0
  25. package/dist/core/GraphFreshnessChecker.js +101 -0
  26. package/dist/core/HashUtils.d.ts +24 -0
  27. package/dist/core/HashUtils.d.ts.map +1 -0
  28. package/dist/core/HashUtils.js +45 -0
  29. package/dist/core/IncrementalReanalyzer.d.ts +36 -0
  30. package/dist/core/IncrementalReanalyzer.d.ts.map +1 -0
  31. package/dist/core/IncrementalReanalyzer.js +132 -0
  32. package/dist/core/NodeFactory.d.ts +266 -19
  33. package/dist/core/NodeFactory.d.ts.map +1 -1
  34. package/dist/core/NodeFactory.js +256 -21
  35. package/dist/core/ScopeTracker.d.ts +84 -0
  36. package/dist/core/ScopeTracker.d.ts.map +1 -0
  37. package/dist/core/ScopeTracker.js +116 -0
  38. package/dist/core/SemanticId.d.ts +90 -0
  39. package/dist/core/SemanticId.d.ts.map +1 -0
  40. package/dist/core/SemanticId.js +115 -0
  41. package/dist/core/VersionManager.d.ts.map +1 -1
  42. package/dist/core/VersionManager.js +3 -2
  43. package/dist/core/nodes/ArgumentExpressionNode.d.ts +43 -0
  44. package/dist/core/nodes/ArgumentExpressionNode.d.ts.map +1 -0
  45. package/dist/core/nodes/ArgumentExpressionNode.js +60 -0
  46. package/dist/core/nodes/ArrayLiteralNode.d.ts +27 -0
  47. package/dist/core/nodes/ArrayLiteralNode.d.ts.map +1 -0
  48. package/dist/core/nodes/ArrayLiteralNode.js +43 -0
  49. package/dist/core/nodes/BranchNode.d.ts +41 -0
  50. package/dist/core/nodes/BranchNode.d.ts.map +1 -0
  51. package/dist/core/nodes/BranchNode.js +82 -0
  52. package/dist/core/nodes/CallSiteNode.d.ts +30 -2
  53. package/dist/core/nodes/CallSiteNode.d.ts.map +1 -1
  54. package/dist/core/nodes/CallSiteNode.js +54 -4
  55. package/dist/core/nodes/CaseNode.d.ts +43 -0
  56. package/dist/core/nodes/CaseNode.d.ts.map +1 -0
  57. package/dist/core/nodes/CaseNode.js +81 -0
  58. package/dist/core/nodes/ClassNode.d.ts +34 -2
  59. package/dist/core/nodes/ClassNode.d.ts.map +1 -1
  60. package/dist/core/nodes/ClassNode.js +52 -4
  61. package/dist/core/nodes/ConstantNode.d.ts +2 -2
  62. package/dist/core/nodes/ConstantNode.d.ts.map +1 -1
  63. package/dist/core/nodes/ConstantNode.js +6 -4
  64. package/dist/core/nodes/ConstructorCallNode.d.ts +51 -0
  65. package/dist/core/nodes/ConstructorCallNode.d.ts.map +1 -0
  66. package/dist/core/nodes/ConstructorCallNode.js +171 -0
  67. package/dist/core/nodes/DatabaseQueryNode.d.ts +3 -2
  68. package/dist/core/nodes/DatabaseQueryNode.d.ts.map +1 -1
  69. package/dist/core/nodes/DatabaseQueryNode.js +5 -2
  70. package/dist/core/nodes/DecoratorNode.d.ts +42 -0
  71. package/dist/core/nodes/DecoratorNode.d.ts.map +1 -0
  72. package/dist/core/nodes/DecoratorNode.js +64 -0
  73. package/dist/core/nodes/EnumNode.d.ts +42 -0
  74. package/dist/core/nodes/EnumNode.d.ts.map +1 -0
  75. package/dist/core/nodes/EnumNode.js +56 -0
  76. package/dist/core/nodes/EventListenerNode.d.ts +4 -4
  77. package/dist/core/nodes/EventListenerNode.d.ts.map +1 -1
  78. package/dist/core/nodes/EventListenerNode.js +7 -4
  79. package/dist/core/nodes/ExportNode.d.ts +38 -2
  80. package/dist/core/nodes/ExportNode.d.ts.map +1 -1
  81. package/dist/core/nodes/ExportNode.js +54 -4
  82. package/dist/core/nodes/ExpressionNode.d.ts +97 -0
  83. package/dist/core/nodes/ExpressionNode.d.ts.map +1 -0
  84. package/dist/core/nodes/ExpressionNode.js +180 -0
  85. package/dist/core/nodes/ExternalModuleNode.d.ts +32 -0
  86. package/dist/core/nodes/ExternalModuleNode.d.ts.map +1 -0
  87. package/dist/core/nodes/ExternalModuleNode.js +49 -0
  88. package/dist/core/nodes/ExternalStdioNode.d.ts +13 -6
  89. package/dist/core/nodes/ExternalStdioNode.d.ts.map +1 -1
  90. package/dist/core/nodes/ExternalStdioNode.js +15 -8
  91. package/dist/core/nodes/FunctionNode.d.ts +36 -0
  92. package/dist/core/nodes/FunctionNode.d.ts.map +1 -1
  93. package/dist/core/nodes/FunctionNode.js +80 -1
  94. package/dist/core/nodes/HttpRequestNode.d.ts +4 -4
  95. package/dist/core/nodes/HttpRequestNode.d.ts.map +1 -1
  96. package/dist/core/nodes/HttpRequestNode.js +7 -4
  97. package/dist/core/nodes/ImportNode.d.ts +28 -6
  98. package/dist/core/nodes/ImportNode.d.ts.map +1 -1
  99. package/dist/core/nodes/ImportNode.js +43 -8
  100. package/dist/core/nodes/InterfaceNode.d.ts +46 -0
  101. package/dist/core/nodes/InterfaceNode.d.ts.map +1 -0
  102. package/dist/core/nodes/InterfaceNode.js +57 -0
  103. package/dist/core/nodes/IssueNode.d.ts +73 -0
  104. package/dist/core/nodes/IssueNode.d.ts.map +1 -0
  105. package/dist/core/nodes/IssueNode.js +129 -0
  106. package/dist/core/nodes/LiteralNode.d.ts +2 -2
  107. package/dist/core/nodes/LiteralNode.d.ts.map +1 -1
  108. package/dist/core/nodes/LiteralNode.js +6 -4
  109. package/dist/core/nodes/MethodCallNode.d.ts +32 -2
  110. package/dist/core/nodes/MethodCallNode.d.ts.map +1 -1
  111. package/dist/core/nodes/MethodCallNode.js +57 -4
  112. package/dist/core/nodes/MethodNode.d.ts +34 -2
  113. package/dist/core/nodes/MethodNode.d.ts.map +1 -1
  114. package/dist/core/nodes/MethodNode.js +55 -3
  115. package/dist/core/nodes/ModuleNode.d.ts +31 -0
  116. package/dist/core/nodes/ModuleNode.d.ts.map +1 -1
  117. package/dist/core/nodes/ModuleNode.js +37 -0
  118. package/dist/core/nodes/NetworkRequestNode.d.ts +54 -0
  119. package/dist/core/nodes/NetworkRequestNode.d.ts.map +1 -0
  120. package/dist/core/nodes/NetworkRequestNode.js +65 -0
  121. package/dist/core/nodes/ObjectLiteralNode.d.ts +27 -0
  122. package/dist/core/nodes/ObjectLiteralNode.d.ts.map +1 -0
  123. package/dist/core/nodes/ObjectLiteralNode.js +43 -0
  124. package/dist/core/nodes/ParameterNode.d.ts +2 -2
  125. package/dist/core/nodes/ParameterNode.d.ts.map +1 -1
  126. package/dist/core/nodes/ParameterNode.js +5 -3
  127. package/dist/core/nodes/ScopeNode.d.ts +31 -0
  128. package/dist/core/nodes/ScopeNode.d.ts.map +1 -1
  129. package/dist/core/nodes/ScopeNode.js +49 -0
  130. package/dist/core/nodes/TypeNode.d.ts +36 -0
  131. package/dist/core/nodes/TypeNode.d.ts.map +1 -0
  132. package/dist/core/nodes/TypeNode.js +55 -0
  133. package/dist/core/nodes/VariableDeclarationNode.d.ts +29 -2
  134. package/dist/core/nodes/VariableDeclarationNode.d.ts.map +1 -1
  135. package/dist/core/nodes/VariableDeclarationNode.js +48 -4
  136. package/dist/core/nodes/index.d.ts +15 -1
  137. package/dist/core/nodes/index.d.ts.map +1 -1
  138. package/dist/core/nodes/index.js +17 -0
  139. package/dist/data/builtins/BuiltinRegistry.d.ts +78 -0
  140. package/dist/data/builtins/BuiltinRegistry.d.ts.map +1 -0
  141. package/dist/data/builtins/BuiltinRegistry.js +110 -0
  142. package/dist/data/builtins/definitions.d.ts +28 -0
  143. package/dist/data/builtins/definitions.d.ts.map +1 -0
  144. package/dist/data/builtins/definitions.js +250 -0
  145. package/dist/data/builtins/index.d.ts +10 -0
  146. package/dist/data/builtins/index.d.ts.map +1 -0
  147. package/dist/data/builtins/index.js +8 -0
  148. package/dist/data/builtins/jsGlobals.d.ts +18 -0
  149. package/dist/data/builtins/jsGlobals.d.ts.map +1 -0
  150. package/dist/data/builtins/jsGlobals.js +26 -0
  151. package/dist/data/builtins/types.d.ts +34 -0
  152. package/dist/data/builtins/types.d.ts.map +1 -0
  153. package/dist/data/builtins/types.js +7 -0
  154. package/dist/data/globals/definitions.d.ts +27 -0
  155. package/dist/data/globals/definitions.d.ts.map +1 -0
  156. package/dist/data/globals/definitions.js +117 -0
  157. package/dist/data/globals/index.d.ts +36 -0
  158. package/dist/data/globals/index.d.ts.map +1 -0
  159. package/dist/data/globals/index.js +52 -0
  160. package/dist/diagnostics/DiagnosticCollector.d.ts +98 -0
  161. package/dist/diagnostics/DiagnosticCollector.d.ts.map +1 -0
  162. package/dist/diagnostics/DiagnosticCollector.js +129 -0
  163. package/dist/diagnostics/DiagnosticReporter.d.ts +100 -0
  164. package/dist/diagnostics/DiagnosticReporter.d.ts.map +1 -0
  165. package/dist/diagnostics/DiagnosticReporter.js +247 -0
  166. package/dist/diagnostics/DiagnosticWriter.d.ts +31 -0
  167. package/dist/diagnostics/DiagnosticWriter.d.ts.map +1 -0
  168. package/dist/diagnostics/DiagnosticWriter.js +43 -0
  169. package/dist/diagnostics/index.d.ts +14 -0
  170. package/dist/diagnostics/index.d.ts.map +1 -0
  171. package/dist/diagnostics/index.js +11 -0
  172. package/dist/errors/GrafemaError.d.ts +161 -0
  173. package/dist/errors/GrafemaError.d.ts.map +1 -0
  174. package/dist/errors/GrafemaError.js +181 -0
  175. package/dist/index.d.ts +73 -1
  176. package/dist/index.d.ts.map +1 -1
  177. package/dist/index.js +70 -1
  178. package/dist/logging/Logger.d.ts +48 -0
  179. package/dist/logging/Logger.d.ts.map +1 -0
  180. package/dist/logging/Logger.js +134 -0
  181. package/dist/plugins/Plugin.d.ts +5 -1
  182. package/dist/plugins/Plugin.d.ts.map +1 -1
  183. package/dist/plugins/Plugin.js +33 -0
  184. package/dist/plugins/analysis/DatabaseAnalyzer.d.ts.map +1 -1
  185. package/dist/plugins/analysis/DatabaseAnalyzer.js +14 -6
  186. package/dist/plugins/analysis/ExpressAnalyzer.d.ts.map +1 -1
  187. package/dist/plugins/analysis/ExpressAnalyzer.js +29 -19
  188. package/dist/plugins/analysis/ExpressResponseAnalyzer.d.ts +148 -0
  189. package/dist/plugins/analysis/ExpressResponseAnalyzer.d.ts.map +1 -0
  190. package/dist/plugins/analysis/ExpressResponseAnalyzer.js +495 -0
  191. package/dist/plugins/analysis/ExpressRouteAnalyzer.d.ts.map +1 -1
  192. package/dist/plugins/analysis/ExpressRouteAnalyzer.js +71 -29
  193. package/dist/plugins/analysis/FetchAnalyzer.d.ts +41 -0
  194. package/dist/plugins/analysis/FetchAnalyzer.d.ts.map +1 -1
  195. package/dist/plugins/analysis/FetchAnalyzer.js +187 -19
  196. package/dist/plugins/analysis/IncrementalAnalysisPlugin.d.ts +6 -3
  197. package/dist/plugins/analysis/IncrementalAnalysisPlugin.d.ts.map +1 -1
  198. package/dist/plugins/analysis/IncrementalAnalysisPlugin.js +76 -80
  199. package/dist/plugins/analysis/JSASTAnalyzer.d.ts +313 -19
  200. package/dist/plugins/analysis/JSASTAnalyzer.d.ts.map +1 -1
  201. package/dist/plugins/analysis/JSASTAnalyzer.js +3430 -503
  202. package/dist/plugins/analysis/ReactAnalyzer.d.ts.map +1 -1
  203. package/dist/plugins/analysis/ReactAnalyzer.js +56 -57
  204. package/dist/plugins/analysis/RustAnalyzer.d.ts.map +1 -1
  205. package/dist/plugins/analysis/RustAnalyzer.js +16 -11
  206. package/dist/plugins/analysis/SQLiteAnalyzer.d.ts.map +1 -1
  207. package/dist/plugins/analysis/SQLiteAnalyzer.js +11 -7
  208. package/dist/plugins/analysis/ServiceLayerAnalyzer.d.ts.map +1 -1
  209. package/dist/plugins/analysis/ServiceLayerAnalyzer.js +21 -9
  210. package/dist/plugins/analysis/SocketIOAnalyzer.d.ts +9 -0
  211. package/dist/plugins/analysis/SocketIOAnalyzer.d.ts.map +1 -1
  212. package/dist/plugins/analysis/SocketIOAnalyzer.js +117 -21
  213. package/dist/plugins/analysis/SystemDbAnalyzer.d.ts.map +1 -1
  214. package/dist/plugins/analysis/SystemDbAnalyzer.js +15 -5
  215. package/dist/plugins/analysis/ast/GraphBuilder.d.ts +207 -4
  216. package/dist/plugins/analysis/ast/GraphBuilder.d.ts.map +1 -1
  217. package/dist/plugins/analysis/ast/GraphBuilder.js +1527 -316
  218. package/dist/plugins/analysis/ast/IdGenerator.d.ts +105 -0
  219. package/dist/plugins/analysis/ast/IdGenerator.d.ts.map +1 -0
  220. package/dist/plugins/analysis/ast/IdGenerator.js +116 -0
  221. package/dist/plugins/analysis/ast/types.d.ts +470 -5
  222. package/dist/plugins/analysis/ast/types.d.ts.map +1 -1
  223. package/dist/plugins/analysis/ast/utils/createParameterNodes.d.ts +33 -0
  224. package/dist/plugins/analysis/ast/utils/createParameterNodes.d.ts.map +1 -0
  225. package/dist/plugins/analysis/ast/utils/createParameterNodes.js +89 -0
  226. package/dist/plugins/analysis/ast/utils/index.d.ts +6 -0
  227. package/dist/plugins/analysis/ast/utils/index.d.ts.map +1 -0
  228. package/dist/plugins/analysis/ast/utils/index.js +5 -0
  229. package/dist/plugins/analysis/ast/utils/location.d.ts +87 -0
  230. package/dist/plugins/analysis/ast/utils/location.d.ts.map +1 -0
  231. package/dist/plugins/analysis/ast/utils/location.js +78 -0
  232. package/dist/plugins/analysis/ast/visitors/ASTVisitor.d.ts +14 -5
  233. package/dist/plugins/analysis/ast/visitors/ASTVisitor.d.ts.map +1 -1
  234. package/dist/plugins/analysis/ast/visitors/ASTVisitor.js +6 -5
  235. package/dist/plugins/analysis/ast/visitors/CallExpressionVisitor.d.ts +100 -9
  236. package/dist/plugins/analysis/ast/visitors/CallExpressionVisitor.d.ts.map +1 -1
  237. package/dist/plugins/analysis/ast/visitors/CallExpressionVisitor.js +674 -125
  238. package/dist/plugins/analysis/ast/visitors/ClassVisitor.d.ts +4 -1
  239. package/dist/plugins/analysis/ast/visitors/ClassVisitor.d.ts.map +1 -1
  240. package/dist/plugins/analysis/ast/visitors/ClassVisitor.js +72 -32
  241. package/dist/plugins/analysis/ast/visitors/FunctionVisitor.d.ts +14 -1
  242. package/dist/plugins/analysis/ast/visitors/FunctionVisitor.d.ts.map +1 -1
  243. package/dist/plugins/analysis/ast/visitors/FunctionVisitor.js +190 -63
  244. package/dist/plugins/analysis/ast/visitors/ImportExportVisitor.d.ts +4 -0
  245. package/dist/plugins/analysis/ast/visitors/ImportExportVisitor.d.ts.map +1 -1
  246. package/dist/plugins/analysis/ast/visitors/ImportExportVisitor.js +112 -8
  247. package/dist/plugins/analysis/ast/visitors/TypeScriptVisitor.d.ts +12 -1
  248. package/dist/plugins/analysis/ast/visitors/TypeScriptVisitor.d.ts.map +1 -1
  249. package/dist/plugins/analysis/ast/visitors/TypeScriptVisitor.js +36 -14
  250. package/dist/plugins/analysis/ast/visitors/VariableVisitor.d.ts +20 -2
  251. package/dist/plugins/analysis/ast/visitors/VariableVisitor.d.ts.map +1 -1
  252. package/dist/plugins/analysis/ast/visitors/VariableVisitor.js +243 -45
  253. package/dist/plugins/discovery/MonorepoServiceDiscovery.d.ts.map +1 -1
  254. package/dist/plugins/discovery/MonorepoServiceDiscovery.js +3 -2
  255. package/dist/plugins/discovery/SimpleProjectDiscovery.d.ts.map +1 -1
  256. package/dist/plugins/discovery/SimpleProjectDiscovery.js +5 -1
  257. package/dist/plugins/discovery/WorkspaceDiscovery.d.ts +22 -0
  258. package/dist/plugins/discovery/WorkspaceDiscovery.d.ts.map +1 -0
  259. package/dist/plugins/discovery/WorkspaceDiscovery.js +141 -0
  260. package/dist/plugins/discovery/resolveSourceEntrypoint.d.ts +46 -0
  261. package/dist/plugins/discovery/resolveSourceEntrypoint.d.ts.map +1 -0
  262. package/dist/plugins/discovery/resolveSourceEntrypoint.js +86 -0
  263. package/dist/plugins/discovery/workspaces/detector.d.ts +21 -0
  264. package/dist/plugins/discovery/workspaces/detector.d.ts.map +1 -0
  265. package/dist/plugins/discovery/workspaces/detector.js +49 -0
  266. package/dist/plugins/discovery/workspaces/globResolver.d.ts +35 -0
  267. package/dist/plugins/discovery/workspaces/globResolver.d.ts.map +1 -0
  268. package/dist/plugins/discovery/workspaces/globResolver.js +184 -0
  269. package/dist/plugins/discovery/workspaces/index.d.ts +9 -0
  270. package/dist/plugins/discovery/workspaces/index.d.ts.map +1 -0
  271. package/dist/plugins/discovery/workspaces/index.js +8 -0
  272. package/dist/plugins/discovery/workspaces/parsers.d.ts +38 -0
  273. package/dist/plugins/discovery/workspaces/parsers.d.ts.map +1 -0
  274. package/dist/plugins/discovery/workspaces/parsers.js +80 -0
  275. package/dist/plugins/enrichment/AliasTracker.d.ts.map +1 -1
  276. package/dist/plugins/enrichment/AliasTracker.js +29 -8
  277. package/dist/plugins/enrichment/ArgumentParameterLinker.d.ts +32 -0
  278. package/dist/plugins/enrichment/ArgumentParameterLinker.d.ts.map +1 -0
  279. package/dist/plugins/enrichment/ArgumentParameterLinker.js +175 -0
  280. package/dist/plugins/enrichment/ClosureCaptureEnricher.d.ts +51 -0
  281. package/dist/plugins/enrichment/ClosureCaptureEnricher.d.ts.map +1 -0
  282. package/dist/plugins/enrichment/ClosureCaptureEnricher.js +205 -0
  283. package/dist/plugins/enrichment/ExternalCallResolver.d.ts +42 -0
  284. package/dist/plugins/enrichment/ExternalCallResolver.d.ts.map +1 -0
  285. package/dist/plugins/enrichment/ExternalCallResolver.js +213 -0
  286. package/dist/plugins/enrichment/FunctionCallResolver.d.ts +58 -0
  287. package/dist/plugins/enrichment/FunctionCallResolver.d.ts.map +1 -0
  288. package/dist/plugins/enrichment/FunctionCallResolver.js +340 -0
  289. package/dist/plugins/enrichment/HTTPConnectionEnricher.d.ts +16 -3
  290. package/dist/plugins/enrichment/HTTPConnectionEnricher.d.ts.map +1 -1
  291. package/dist/plugins/enrichment/HTTPConnectionEnricher.js +78 -27
  292. package/dist/plugins/enrichment/ImportExportLinker.d.ts.map +1 -1
  293. package/dist/plugins/enrichment/ImportExportLinker.js +23 -6
  294. package/dist/plugins/enrichment/MethodCallResolver.d.ts.map +1 -1
  295. package/dist/plugins/enrichment/MethodCallResolver.js +33 -13
  296. package/dist/plugins/enrichment/MountPointResolver.d.ts +14 -12
  297. package/dist/plugins/enrichment/MountPointResolver.d.ts.map +1 -1
  298. package/dist/plugins/enrichment/MountPointResolver.js +173 -147
  299. package/dist/plugins/enrichment/NodejsBuiltinsResolver.d.ts +44 -0
  300. package/dist/plugins/enrichment/NodejsBuiltinsResolver.d.ts.map +1 -0
  301. package/dist/plugins/enrichment/NodejsBuiltinsResolver.js +271 -0
  302. package/dist/plugins/enrichment/PrefixEvaluator.d.ts.map +1 -1
  303. package/dist/plugins/enrichment/PrefixEvaluator.js +16 -7
  304. package/dist/plugins/enrichment/RustFFIEnricher.d.ts.map +1 -1
  305. package/dist/plugins/enrichment/RustFFIEnricher.js +6 -5
  306. package/dist/plugins/enrichment/ValueDomainAnalyzer.d.ts +22 -27
  307. package/dist/plugins/enrichment/ValueDomainAnalyzer.d.ts.map +1 -1
  308. package/dist/plugins/enrichment/ValueDomainAnalyzer.js +185 -143
  309. package/dist/plugins/indexing/IncrementalModuleIndexer.d.ts.map +1 -1
  310. package/dist/plugins/indexing/IncrementalModuleIndexer.js +23 -14
  311. package/dist/plugins/indexing/JSModuleIndexer.d.ts +15 -0
  312. package/dist/plugins/indexing/JSModuleIndexer.d.ts.map +1 -1
  313. package/dist/plugins/indexing/JSModuleIndexer.js +121 -31
  314. package/dist/plugins/indexing/RustModuleIndexer.d.ts +1 -1
  315. package/dist/plugins/indexing/RustModuleIndexer.d.ts.map +1 -1
  316. package/dist/plugins/indexing/RustModuleIndexer.js +8 -7
  317. package/dist/plugins/indexing/ServiceDetector.d.ts +10 -0
  318. package/dist/plugins/indexing/ServiceDetector.d.ts.map +1 -1
  319. package/dist/plugins/indexing/ServiceDetector.js +28 -15
  320. package/dist/plugins/validation/BrokenImportValidator.d.ts +31 -0
  321. package/dist/plugins/validation/BrokenImportValidator.d.ts.map +1 -0
  322. package/dist/plugins/validation/BrokenImportValidator.js +249 -0
  323. package/dist/plugins/validation/CallResolverValidator.d.ts +21 -10
  324. package/dist/plugins/validation/CallResolverValidator.d.ts.map +1 -1
  325. package/dist/plugins/validation/CallResolverValidator.js +103 -77
  326. package/dist/plugins/validation/DataFlowValidator.d.ts.map +1 -1
  327. package/dist/plugins/validation/DataFlowValidator.js +62 -49
  328. package/dist/plugins/validation/EvalBanValidator.d.ts.map +1 -1
  329. package/dist/plugins/validation/EvalBanValidator.js +17 -16
  330. package/dist/plugins/validation/GraphConnectivityValidator.d.ts.map +1 -1
  331. package/dist/plugins/validation/GraphConnectivityValidator.js +44 -24
  332. package/dist/plugins/validation/NodeCreationValidator.d.ts +85 -0
  333. package/dist/plugins/validation/NodeCreationValidator.d.ts.map +1 -0
  334. package/dist/plugins/validation/NodeCreationValidator.js +415 -0
  335. package/dist/plugins/validation/SQLInjectionValidator.d.ts.map +1 -1
  336. package/dist/plugins/validation/SQLInjectionValidator.js +61 -19
  337. package/dist/plugins/validation/ShadowingDetector.d.ts.map +1 -1
  338. package/dist/plugins/validation/ShadowingDetector.js +6 -5
  339. package/dist/plugins/validation/TypeScriptDeadCodeValidator.d.ts.map +1 -1
  340. package/dist/plugins/validation/TypeScriptDeadCodeValidator.js +12 -11
  341. package/dist/plugins/vcs/GitPlugin.d.ts.map +1 -1
  342. package/dist/plugins/vcs/GitPlugin.js +10 -12
  343. package/dist/plugins/vcs/VCSPlugin.d.ts +3 -2
  344. package/dist/plugins/vcs/VCSPlugin.d.ts.map +1 -1
  345. package/dist/plugins/vcs/VCSPlugin.js +5 -5
  346. package/dist/queries/findCallsInFunction.d.ts +52 -0
  347. package/dist/queries/findCallsInFunction.d.ts.map +1 -0
  348. package/dist/queries/findCallsInFunction.js +135 -0
  349. package/dist/queries/findContainingFunction.d.ts +45 -0
  350. package/dist/queries/findContainingFunction.d.ts.map +1 -0
  351. package/dist/queries/findContainingFunction.js +54 -0
  352. package/dist/queries/index.d.ts +14 -0
  353. package/dist/queries/index.d.ts.map +1 -0
  354. package/dist/queries/index.js +11 -0
  355. package/dist/queries/traceValues.d.ts +70 -0
  356. package/dist/queries/traceValues.d.ts.map +1 -0
  357. package/dist/queries/traceValues.js +299 -0
  358. package/dist/queries/types.d.ts +163 -0
  359. package/dist/queries/types.d.ts.map +1 -0
  360. package/dist/queries/types.js +9 -0
  361. package/dist/schema/GraphSchemaExtractor.d.ts +53 -0
  362. package/dist/schema/GraphSchemaExtractor.d.ts.map +1 -0
  363. package/dist/schema/GraphSchemaExtractor.js +124 -0
  364. package/dist/schema/InterfaceSchemaExtractor.d.ts +73 -0
  365. package/dist/schema/InterfaceSchemaExtractor.d.ts.map +1 -0
  366. package/dist/schema/InterfaceSchemaExtractor.js +112 -0
  367. package/dist/schema/index.d.ts +5 -0
  368. package/dist/schema/index.d.ts.map +1 -0
  369. package/dist/schema/index.js +2 -0
  370. package/dist/storage/backends/RFDBServerBackend.d.ts +21 -34
  371. package/dist/storage/backends/RFDBServerBackend.d.ts.map +1 -1
  372. package/dist/storage/backends/RFDBServerBackend.js +72 -62
  373. package/dist/storage/backends/typeValidation.d.ts.map +1 -1
  374. package/dist/storage/backends/typeValidation.js +1 -0
  375. package/dist/validation/PathValidator.d.ts +1 -2
  376. package/dist/validation/PathValidator.d.ts.map +1 -1
  377. package/package.json +3 -3
  378. package/src/Orchestrator.ts +272 -27
  379. package/src/config/ConfigLoader.ts +354 -0
  380. package/src/config/index.ts +5 -0
  381. package/src/core/ASTWorker.ts +143 -139
  382. package/src/core/CoverageAnalyzer.ts +243 -0
  383. package/src/core/FileExplainer.ts +179 -0
  384. package/src/core/FileNodeManager.ts +100 -0
  385. package/src/core/GraphFreshnessChecker.ts +143 -0
  386. package/src/core/HashUtils.ts +48 -0
  387. package/src/core/IncrementalReanalyzer.ts +192 -0
  388. package/src/core/NodeFactory.ts +470 -23
  389. package/src/core/ScopeTracker.ts +154 -0
  390. package/src/core/SemanticId.ts +192 -0
  391. package/src/core/VersionManager.ts +3 -2
  392. package/src/core/nodes/ArgumentExpressionNode.ts +89 -0
  393. package/src/core/nodes/ArrayLiteralNode.ts +66 -0
  394. package/src/core/nodes/BranchNode.ts +113 -0
  395. package/src/core/nodes/CallSiteNode.ts +64 -4
  396. package/src/core/nodes/CaseNode.ts +123 -0
  397. package/src/core/nodes/ClassNode.ts +67 -4
  398. package/src/core/nodes/ConstantNode.ts +5 -4
  399. package/src/core/nodes/ConstructorCallNode.ts +217 -0
  400. package/src/core/nodes/DatabaseQueryNode.ts +5 -1
  401. package/src/core/nodes/DecoratorNode.ts +92 -0
  402. package/src/core/nodes/EnumNode.ts +87 -0
  403. package/src/core/nodes/EventListenerNode.ts +7 -4
  404. package/src/core/nodes/ExportNode.ts +74 -4
  405. package/src/core/nodes/ExpressionNode.ts +232 -0
  406. package/src/core/nodes/ExternalModuleNode.ts +65 -0
  407. package/src/core/nodes/ExternalStdioNode.ts +17 -9
  408. package/src/core/nodes/FunctionNode.ts +101 -1
  409. package/src/core/nodes/HttpRequestNode.ts +7 -4
  410. package/src/core/nodes/ImportNode.ts +62 -13
  411. package/src/core/nodes/InterfaceNode.ts +92 -0
  412. package/src/core/nodes/IssueNode.ts +177 -0
  413. package/src/core/nodes/LiteralNode.ts +5 -4
  414. package/src/core/nodes/MethodCallNode.ts +70 -4
  415. package/src/core/nodes/MethodNode.ts +68 -3
  416. package/src/core/nodes/ModuleNode.ts +50 -0
  417. package/src/core/nodes/NetworkRequestNode.ts +77 -0
  418. package/src/core/nodes/ObjectLiteralNode.ts +66 -0
  419. package/src/core/nodes/ParameterNode.ts +4 -3
  420. package/src/core/nodes/ScopeNode.ts +65 -0
  421. package/src/core/nodes/TypeNode.ts +79 -0
  422. package/src/core/nodes/VariableDeclarationNode.ts +58 -4
  423. package/src/core/nodes/index.ts +21 -1
  424. package/src/data/builtins/BuiltinRegistry.ts +124 -0
  425. package/src/data/builtins/definitions.ts +267 -0
  426. package/src/data/builtins/index.ts +10 -0
  427. package/src/data/builtins/jsGlobals.ts +28 -0
  428. package/src/data/builtins/types.ts +36 -0
  429. package/src/data/globals/definitions.ts +156 -0
  430. package/src/data/globals/index.ts +66 -0
  431. package/src/diagnostics/DiagnosticCollector.ts +163 -0
  432. package/src/diagnostics/DiagnosticReporter.ts +324 -0
  433. package/src/diagnostics/DiagnosticWriter.ts +50 -0
  434. package/src/diagnostics/index.ts +16 -0
  435. package/src/errors/GrafemaError.ts +239 -0
  436. package/src/index.ts +193 -1
  437. package/src/logging/Logger.ts +152 -0
  438. package/src/plugins/Plugin.ts +42 -0
  439. package/src/plugins/analysis/DatabaseAnalyzer.ts +16 -8
  440. package/src/plugins/analysis/ExpressAnalyzer.ts +33 -19
  441. package/src/plugins/analysis/ExpressResponseAnalyzer.ts +636 -0
  442. package/src/plugins/analysis/ExpressRouteAnalyzer.ts +76 -36
  443. package/src/plugins/analysis/FetchAnalyzer.ts +232 -21
  444. package/src/plugins/analysis/IncrementalAnalysisPlugin.ts +84 -101
  445. package/src/plugins/analysis/JSASTAnalyzer.ts +4265 -587
  446. package/src/plugins/analysis/ReactAnalyzer.ts +57 -57
  447. package/src/plugins/analysis/RustAnalyzer.ts +16 -11
  448. package/src/plugins/analysis/SQLiteAnalyzer.ts +13 -7
  449. package/src/plugins/analysis/ServiceLayerAnalyzer.ts +22 -16
  450. package/src/plugins/analysis/SocketIOAnalyzer.ts +151 -28
  451. package/src/plugins/analysis/SystemDbAnalyzer.ts +16 -11
  452. package/src/plugins/analysis/ast/GraphBuilder.ts +1947 -327
  453. package/src/plugins/analysis/ast/IdGenerator.ts +177 -0
  454. package/src/plugins/analysis/ast/types.ts +596 -6
  455. package/src/plugins/analysis/ast/utils/createParameterNodes.ts +104 -0
  456. package/src/plugins/analysis/ast/utils/index.ts +12 -0
  457. package/src/plugins/analysis/ast/utils/location.ts +103 -0
  458. package/src/plugins/analysis/ast/visitors/ASTVisitor.ts +19 -8
  459. package/src/plugins/analysis/ast/visitors/CallExpressionVisitor.ts +924 -83
  460. package/src/plugins/analysis/ast/visitors/ClassVisitor.ts +97 -44
  461. package/src/plugins/analysis/ast/visitors/FunctionVisitor.ts +234 -93
  462. package/src/plugins/analysis/ast/visitors/ImportExportVisitor.ts +124 -9
  463. package/src/plugins/analysis/ast/visitors/TypeScriptVisitor.ts +41 -14
  464. package/src/plugins/analysis/ast/visitors/VariableVisitor.ts +294 -49
  465. package/src/plugins/discovery/MonorepoServiceDiscovery.ts +3 -2
  466. package/src/plugins/discovery/SimpleProjectDiscovery.ts +6 -1
  467. package/src/plugins/discovery/WorkspaceDiscovery.ts +184 -0
  468. package/src/plugins/discovery/resolveSourceEntrypoint.ts +103 -0
  469. package/src/plugins/discovery/workspaces/detector.ts +63 -0
  470. package/src/plugins/discovery/workspaces/globResolver.ts +229 -0
  471. package/src/plugins/discovery/workspaces/index.ts +23 -0
  472. package/src/plugins/discovery/workspaces/parsers.ts +99 -0
  473. package/src/plugins/enrichment/AliasTracker.ts +35 -8
  474. package/src/plugins/enrichment/ArgumentParameterLinker.ts +240 -0
  475. package/src/plugins/enrichment/ClosureCaptureEnricher.ts +267 -0
  476. package/src/plugins/enrichment/ExternalCallResolver.ts +262 -0
  477. package/src/plugins/enrichment/FunctionCallResolver.ts +456 -0
  478. package/src/plugins/enrichment/HTTPConnectionEnricher.ts +84 -27
  479. package/src/plugins/enrichment/ImportExportLinker.ts +24 -6
  480. package/src/plugins/enrichment/MethodCallResolver.ts +39 -13
  481. package/src/plugins/enrichment/MountPointResolver.ts +208 -195
  482. package/src/plugins/enrichment/NodejsBuiltinsResolver.ts +365 -0
  483. package/src/plugins/enrichment/PrefixEvaluator.ts +16 -7
  484. package/src/plugins/enrichment/RustFFIEnricher.ts +6 -5
  485. package/src/plugins/enrichment/ValueDomainAnalyzer.ts +209 -189
  486. package/src/plugins/indexing/IncrementalModuleIndexer.ts +23 -14
  487. package/src/plugins/indexing/JSModuleIndexer.ts +140 -34
  488. package/src/plugins/indexing/RustModuleIndexer.ts +8 -7
  489. package/src/plugins/validation/BrokenImportValidator.ts +325 -0
  490. package/src/plugins/validation/CallResolverValidator.ts +131 -110
  491. package/src/plugins/validation/DataFlowValidator.ts +88 -67
  492. package/src/plugins/validation/EvalBanValidator.ts +17 -16
  493. package/src/plugins/validation/GraphConnectivityValidator.ts +58 -24
  494. package/src/plugins/validation/NodeCreationValidator.ts +554 -0
  495. package/src/plugins/validation/SQLInjectionValidator.ts +63 -20
  496. package/src/plugins/validation/ShadowingDetector.ts +6 -5
  497. package/src/plugins/validation/TypeScriptDeadCodeValidator.ts +12 -11
  498. package/src/plugins/vcs/GitPlugin.ts +40 -12
  499. package/src/plugins/vcs/VCSPlugin.ts +7 -5
  500. package/src/queries/README.md +46 -0
  501. package/src/queries/findCallsInFunction.ts +206 -0
  502. package/src/queries/findContainingFunction.ts +83 -0
  503. package/src/queries/index.ts +23 -0
  504. package/src/queries/traceValues.ts +398 -0
  505. package/src/queries/types.ts +187 -0
  506. package/src/schema/GraphSchemaExtractor.ts +177 -0
  507. package/src/schema/InterfaceSchemaExtractor.ts +173 -0
  508. package/src/schema/index.ts +5 -0
  509. package/src/storage/backends/RFDBServerBackend.ts +100 -98
  510. package/src/storage/backends/typeValidation.ts +1 -0
  511. package/src/validation/PathValidator.ts +1 -1
  512. package/dist/core/AnalysisWorker.d.ts +0 -14
  513. package/dist/core/AnalysisWorker.d.ts.map +0 -1
  514. package/dist/core/AnalysisWorker.js +0 -307
  515. package/dist/core/ParallelAnalyzer.d.ts +0 -120
  516. package/dist/core/ParallelAnalyzer.d.ts.map +0 -1
  517. package/dist/core/ParallelAnalyzer.js +0 -331
  518. package/dist/core/QueueWorker.d.ts +0 -12
  519. package/dist/core/QueueWorker.d.ts.map +0 -1
  520. package/dist/core/QueueWorker.js +0 -567
  521. package/dist/core/RFDBClient.d.ts +0 -179
  522. package/dist/core/RFDBClient.d.ts.map +0 -1
  523. package/dist/core/RFDBClient.js +0 -429
  524. package/dist/plugins/discovery/ZonServiceDiscovery.d.ts +0 -19
  525. package/dist/plugins/discovery/ZonServiceDiscovery.d.ts.map +0 -1
  526. package/dist/plugins/discovery/ZonServiceDiscovery.js +0 -204
  527. package/src/core/AnalysisWorker.ts +0 -410
  528. package/src/core/ParallelAnalyzer.ts +0 -476
  529. package/src/core/QueueWorker.ts +0 -780
  530. package/src/plugins/indexing/ServiceDetector.ts +0 -230
@@ -4,6 +4,7 @@
4
4
  */
5
5
  import { readFileSync } from 'fs';
6
6
  import { createHash } from 'crypto';
7
+ import { basename } from 'path';
7
8
  import { parse } from '@babel/parser';
8
9
  import traverseModule from '@babel/traverse';
9
10
  import * as t from '@babel/types';
@@ -25,19 +26,25 @@ import { ImportExportVisitor, VariableVisitor, FunctionVisitor, ClassVisitor, Ca
25
26
  import { Task } from '../../core/Task.js';
26
27
  import { PriorityQueue } from '../../core/PriorityQueue.js';
27
28
  import { WorkerPool } from '../../core/WorkerPool.js';
29
+ import { ASTWorkerPool } from '../../core/ASTWorkerPool.js';
28
30
  import { ConditionParser } from './ast/ConditionParser.js';
31
+ import { getLine, getColumn } from './ast/utils/location.js';
29
32
  import { Profiler } from '../../core/Profiler.js';
33
+ import { ScopeTracker } from '../../core/ScopeTracker.js';
34
+ import { computeSemanticId } from '../../core/SemanticId.js';
35
+ import { ExpressionNode } from '../../core/nodes/ExpressionNode.js';
36
+ import { ConstructorCallNode } from '../../core/nodes/ConstructorCallNode.js';
37
+ import { ObjectLiteralNode } from '../../core/nodes/ObjectLiteralNode.js';
38
+ import { NodeFactory } from '../../core/NodeFactory.js';
30
39
  export class JSASTAnalyzer extends Plugin {
31
40
  graphBuilder;
32
41
  analyzedModules;
33
42
  profiler;
34
- _cacheCleared;
35
43
  constructor() {
36
44
  super();
37
45
  this.graphBuilder = new GraphBuilder();
38
46
  this.analyzedModules = new Set();
39
47
  this.profiler = new Profiler('JSASTAnalyzer');
40
- this._cacheCleared = false;
41
48
  }
42
49
  get metadata() {
43
50
  return {
@@ -57,7 +64,9 @@ export class JSASTAnalyzer extends Plugin {
57
64
  'WRITES_TO', 'IMPORTS', 'INSTANCE_OF', 'HANDLED_BY', 'HAS_CALLBACK',
58
65
  'PASSES_ARGUMENT', 'MAKES_REQUEST', 'IMPORTS_FROM', 'EXPORTS_TO', 'ASSIGNED_FROM',
59
66
  // TypeScript-specific edges
60
- 'IMPLEMENTS', 'EXTENDS', 'DECORATED_BY'
67
+ 'IMPLEMENTS', 'EXTENDS', 'DECORATED_BY',
68
+ // Promise data flow
69
+ 'RESOLVES_TO'
61
70
  ]
62
71
  },
63
72
  dependencies: ['JSModuleIndexer']
@@ -110,12 +119,12 @@ export class JSASTAnalyzer extends Plugin {
110
119
  return true;
111
120
  }
112
121
  async execute(context) {
122
+ const logger = this.log(context);
113
123
  try {
114
124
  const { manifest, graph, forceAnalysis = false } = context;
115
125
  const projectPath = manifest?.projectPath ?? '';
116
- if (forceAnalysis && !this._cacheCleared) {
126
+ if (forceAnalysis) {
117
127
  this.analyzedModules.clear();
118
- this._cacheCleared = true;
119
128
  }
120
129
  const allModules = await this.getModuleNodes(graph);
121
130
  const modulesToAnalyze = [];
@@ -132,11 +141,15 @@ export class JSASTAnalyzer extends Plugin {
132
141
  skippedCount++;
133
142
  }
134
143
  }
135
- console.log(`[JSASTAnalyzer] Starting parallel analysis of ${modulesToAnalyze.length} modules (${skippedCount} cached)...`);
144
+ logger.info('Starting module analysis', { toAnalyze: modulesToAnalyze.length, cached: skippedCount });
136
145
  if (modulesToAnalyze.length === 0) {
137
- console.log(`[JSASTAnalyzer] All modules are up-to-date, skipping analysis`);
146
+ logger.info('All modules up-to-date, skipping analysis');
138
147
  return createSuccessResult({ nodes: 0, edges: 0 });
139
148
  }
149
+ // Use ASTWorkerPool for true parallel parsing with worker_threads if enabled
150
+ if (context.parallelParsing) {
151
+ return await this.executeParallel(modulesToAnalyze, graph, projectPath, context);
152
+ }
140
153
  const queue = new PriorityQueue();
141
154
  const pool = new WorkerPool(context.workerCount || 10);
142
155
  pool.registerHandler('ANALYZE_MODULE', async (task) => {
@@ -171,7 +184,7 @@ export class JSASTAnalyzer extends Plugin {
171
184
  pool.on('worker:task:completed', () => {
172
185
  completed++;
173
186
  if (completed % 10 === 0 || completed === modulesToAnalyze.length) {
174
- console.log(`[JSASTAnalyzer] Progress: ${completed}/${modulesToAnalyze.length}`);
187
+ logger.debug('Analysis progress', { completed, total: modulesToAnalyze.length });
175
188
  }
176
189
  });
177
190
  await pool.processQueue(queue);
@@ -193,17 +206,86 @@ export class JSASTAnalyzer extends Plugin {
193
206
  edgesCreated += task.result.edges;
194
207
  }
195
208
  }
196
- console.log(`[JSASTAnalyzer] Analyzed ${modulesToAnalyze.length} modules, created ${nodesCreated} nodes`);
197
- console.log(`[JSASTAnalyzer] Stats:`, stats);
209
+ logger.info('Analysis complete', { modulesAnalyzed: modulesToAnalyze.length, nodesCreated });
210
+ logger.debug('Worker stats', { ...stats });
198
211
  this.profiler.printSummary();
199
212
  return createSuccessResult({ nodes: nodesCreated, edges: edgesCreated }, { modulesAnalyzed: modulesToAnalyze.length, workerStats: stats });
200
213
  }
201
214
  catch (error) {
202
- console.error(`[JSASTAnalyzer] Error:`, error);
215
+ logger.error('Analysis failed', { error: error instanceof Error ? error.message : String(error) });
203
216
  const err = error instanceof Error ? error : new Error(String(error));
204
217
  return createErrorResult(err);
205
218
  }
206
219
  }
220
+ /**
221
+ * Execute parallel analysis using ASTWorkerPool (worker_threads).
222
+ *
223
+ * This method uses actual OS threads for true parallel CPU-intensive parsing.
224
+ * Workers generate semantic IDs using ScopeTracker, matching sequential behavior.
225
+ *
226
+ * @param modules - Modules to analyze
227
+ * @param graph - Graph backend for writing results
228
+ * @param projectPath - Project root path
229
+ * @param context - Analysis context with options
230
+ * @returns Plugin result with node/edge counts
231
+ */
232
+ async executeParallel(modules, graph, projectPath, context) {
233
+ const logger = this.log(context);
234
+ const workerCount = context.workerCount || 4;
235
+ const pool = new ASTWorkerPool(workerCount);
236
+ logger.debug('Starting parallel parsing', { workerCount });
237
+ try {
238
+ await pool.init();
239
+ // Convert ModuleNode to ASTModuleInfo format
240
+ const moduleInfos = modules.map(m => ({
241
+ id: m.id,
242
+ file: m.file,
243
+ name: m.name
244
+ }));
245
+ // Parse all modules in parallel using worker threads
246
+ const results = await pool.parseModules(moduleInfos);
247
+ let nodesCreated = 0;
248
+ let edgesCreated = 0;
249
+ let errors = 0;
250
+ // Process results - collections already have semantic IDs from workers
251
+ for (const result of results) {
252
+ if (result.error) {
253
+ logger.warn('Parse error', { file: result.module.file, error: result.error.message });
254
+ errors++;
255
+ continue;
256
+ }
257
+ if (result.collections) {
258
+ // Find original module for metadata
259
+ const module = modules.find(m => m.id === result.module.id);
260
+ if (!module)
261
+ continue;
262
+ // Pass collections directly to GraphBuilder - IDs already semantic
263
+ // Cast is safe because ASTWorker.ASTCollections is structurally compatible
264
+ // with ast/types.ASTCollections (METHOD extends FUNCTION semantically)
265
+ const buildResult = await this.graphBuilder.build(module, graph, projectPath, result.collections);
266
+ if (typeof buildResult === 'object' && buildResult !== null) {
267
+ nodesCreated += buildResult.nodes || 0;
268
+ edgesCreated += buildResult.edges || 0;
269
+ }
270
+ }
271
+ // Report progress
272
+ if (context.onProgress) {
273
+ context.onProgress({
274
+ phase: 'analysis',
275
+ currentPlugin: 'JSASTAnalyzer',
276
+ message: `Processed ${result.module.file.replace(projectPath, '')}`,
277
+ totalFiles: modules.length,
278
+ processedFiles: results.indexOf(result) + 1
279
+ });
280
+ }
281
+ }
282
+ logger.info('Parallel parsing complete', { nodesCreated, edgesCreated, errors });
283
+ return createSuccessResult({ nodes: nodesCreated, edges: edgesCreated }, { modulesAnalyzed: modules.length - errors, parallelParsing: true });
284
+ }
285
+ finally {
286
+ await pool.terminate();
287
+ }
288
+ }
207
289
  /**
208
290
  * Extract variable names from destructuring patterns
209
291
  * Uses t.isX() type guards to avoid casts
@@ -278,14 +360,30 @@ export class JSASTAnalyzer extends Plugin {
278
360
  /**
279
361
  * Отслеживает присваивание переменной для data flow анализа
280
362
  */
281
- trackVariableAssignment(initNode, variableId, variableName, module, line, literals, variableAssignments, literalCounterRef) {
363
+ trackVariableAssignment(initNode, variableId, variableName, module, line, literals, variableAssignments, literalCounterRef, objectLiterals, objectProperties, objectLiteralCounterRef) {
282
364
  if (!initNode)
283
365
  return;
284
366
  // initNode is already typed as t.Expression
285
367
  const initExpression = initNode;
286
368
  // 0. AwaitExpression
287
369
  if (initExpression.type === 'AwaitExpression') {
288
- return this.trackVariableAssignment(initExpression.argument, variableId, variableName, module, line, literals, variableAssignments, literalCounterRef);
370
+ return this.trackVariableAssignment(initExpression.argument, variableId, variableName, module, line, literals, variableAssignments, literalCounterRef, objectLiterals, objectProperties, objectLiteralCounterRef);
371
+ }
372
+ // 0.5. ObjectExpression (REG-328) - must be before literal check
373
+ if (initExpression.type === 'ObjectExpression') {
374
+ const column = initExpression.loc?.start.column ?? 0;
375
+ const objectNode = ObjectLiteralNode.create(module.file, line, column, { counter: objectLiteralCounterRef.value++ });
376
+ // Add to objectLiterals collection for GraphBuilder to create the node
377
+ objectLiterals.push(objectNode);
378
+ // Extract properties from the object literal
379
+ this.extractObjectProperties(initExpression, objectNode.id, module, objectProperties, objectLiterals, objectLiteralCounterRef, literals, literalCounterRef);
380
+ // Create ASSIGNED_FROM edge: VARIABLE -> OBJECT_LITERAL
381
+ variableAssignments.push({
382
+ variableId,
383
+ sourceId: objectNode.id,
384
+ sourceType: 'OBJECT_LITERAL'
385
+ });
386
+ return;
289
387
  }
290
388
  // 1. Literal
291
389
  const literalValue = ExpressionEvaluator.extractLiteralValue(initExpression);
@@ -313,8 +411,8 @@ export class JSASTAnalyzer extends Plugin {
313
411
  sourceId: null,
314
412
  sourceType: 'CALL_SITE',
315
413
  callName: initExpression.callee.name,
316
- callLine: initExpression.loc.start.line,
317
- callColumn: initExpression.loc.start.column
414
+ callLine: getLine(initExpression),
415
+ callColumn: getColumn(initExpression)
318
416
  });
319
417
  return;
320
418
  }
@@ -324,7 +422,7 @@ export class JSASTAnalyzer extends Plugin {
324
422
  const objectName = callee.object.type === 'Identifier' ? callee.object.name : (callee.object.type === 'ThisExpression' ? 'this' : 'unknown');
325
423
  const methodName = callee.property.type === 'Identifier' ? callee.property.name : 'unknown';
326
424
  const fullName = `${objectName}.${methodName}`;
327
- const methodCallId = `CALL#${fullName}#${module.file}#${initExpression.loc.start.line}:${initExpression.loc.start.column}:inline`;
425
+ const methodCallId = `CALL#${fullName}#${module.file}#${getLine(initExpression)}:${getColumn(initExpression)}:inline`;
328
426
  const existing = variableAssignments.find(a => a.sourceId === methodCallId);
329
427
  if (!existing) {
330
428
  const extractedArgs = [];
@@ -332,15 +430,15 @@ export class JSASTAnalyzer extends Plugin {
332
430
  if (arg.type !== 'SpreadElement') {
333
431
  const argLiteralValue = ExpressionEvaluator.extractLiteralValue(arg);
334
432
  if (argLiteralValue !== null) {
335
- const literalId = `LITERAL#arg${index}#${module.file}#${initExpression.loc.start.line}:${initExpression.loc.start.column}:${literalCounterRef.value++}`;
433
+ const literalId = `LITERAL#arg${index}#${module.file}#${getLine(initExpression)}:${getColumn(initExpression)}:${literalCounterRef.value++}`;
336
434
  literals.push({
337
435
  id: literalId,
338
436
  type: 'LITERAL',
339
437
  value: argLiteralValue,
340
438
  valueType: typeof argLiteralValue,
341
439
  file: module.file,
342
- line: arg.loc?.start.line || initExpression.loc.start.line,
343
- column: arg.loc?.start.column || initExpression.loc.start.column,
440
+ line: arg.loc?.start.line || getLine(initExpression),
441
+ column: arg.loc?.start.column || getColumn(initExpression),
344
442
  parentCallId: methodCallId,
345
443
  argIndex: index
346
444
  });
@@ -359,8 +457,8 @@ export class JSASTAnalyzer extends Plugin {
359
457
  method: methodName,
360
458
  file: module.file,
361
459
  arguments: extractedArgs,
362
- line: initExpression.loc.start.line,
363
- column: initExpression.loc.start.column
460
+ line: getLine(initExpression),
461
+ column: getColumn(initExpression)
364
462
  });
365
463
  }
366
464
  variableAssignments.push({
@@ -380,17 +478,31 @@ export class JSASTAnalyzer extends Plugin {
380
478
  });
381
479
  return;
382
480
  }
383
- // 5. NewExpression
481
+ // 5. NewExpression -> CONSTRUCTOR_CALL
384
482
  if (initExpression.type === 'NewExpression') {
385
483
  const callee = initExpression.callee;
484
+ let className;
386
485
  if (callee.type === 'Identifier') {
387
- variableAssignments.push({
388
- variableId,
389
- sourceType: 'CLASS',
390
- className: callee.name,
391
- line: line
392
- });
486
+ className = callee.name;
487
+ }
488
+ else if (callee.type === 'MemberExpression' && callee.property.type === 'Identifier') {
489
+ // Handle: new module.ClassName()
490
+ className = callee.property.name;
393
491
  }
492
+ else {
493
+ // Unknown callee type, skip
494
+ return;
495
+ }
496
+ const callLine = initExpression.loc?.start.line ?? line;
497
+ const callColumn = initExpression.loc?.start.column ?? 0;
498
+ variableAssignments.push({
499
+ variableId,
500
+ sourceType: 'CONSTRUCTOR_CALL',
501
+ className,
502
+ file: module.file,
503
+ line: callLine,
504
+ column: callColumn
505
+ });
394
506
  return;
395
507
  }
396
508
  // 6. ArrowFunctionExpression or FunctionExpression
@@ -414,7 +526,8 @@ export class JSASTAnalyzer extends Plugin {
414
526
  const computedPropertyVar = initExpression.computed && initExpression.property.type === 'Identifier'
415
527
  ? initExpression.property.name
416
528
  : null;
417
- const expressionId = `EXPRESSION#${objectName}.${propertyName}#${module.file}#${line}:${initExpression.start}`;
529
+ const column = initExpression.start ?? 0;
530
+ const expressionId = ExpressionNode.generateId('MemberExpression', module.file, line, column);
418
531
  variableAssignments.push({
419
532
  variableId,
420
533
  sourceType: 'EXPRESSION',
@@ -426,13 +539,15 @@ export class JSASTAnalyzer extends Plugin {
426
539
  computedPropertyVar,
427
540
  objectSourceName: initExpression.object.type === 'Identifier' ? initExpression.object.name : null,
428
541
  file: module.file,
429
- line: line
542
+ line: line,
543
+ column: column
430
544
  });
431
545
  return;
432
546
  }
433
547
  // 8. BinaryExpression
434
548
  if (initExpression.type === 'BinaryExpression') {
435
- const expressionId = `EXPRESSION#binary#${module.file}#${line}:${initExpression.start}`;
549
+ const column = initExpression.start ?? 0;
550
+ const expressionId = ExpressionNode.generateId('BinaryExpression', module.file, line, column);
436
551
  variableAssignments.push({
437
552
  variableId,
438
553
  sourceType: 'EXPRESSION',
@@ -442,13 +557,15 @@ export class JSASTAnalyzer extends Plugin {
442
557
  leftSourceName: initExpression.left.type === 'Identifier' ? initExpression.left.name : null,
443
558
  rightSourceName: initExpression.right.type === 'Identifier' ? initExpression.right.name : null,
444
559
  file: module.file,
445
- line: line
560
+ line: line,
561
+ column: column
446
562
  });
447
563
  return;
448
564
  }
449
565
  // 9. ConditionalExpression
450
566
  if (initExpression.type === 'ConditionalExpression') {
451
- const expressionId = `EXPRESSION#conditional#${module.file}#${line}:${initExpression.start}`;
567
+ const column = initExpression.start ?? 0;
568
+ const expressionId = ExpressionNode.generateId('ConditionalExpression', module.file, line, column);
452
569
  variableAssignments.push({
453
570
  variableId,
454
571
  sourceType: 'EXPRESSION',
@@ -457,15 +574,17 @@ export class JSASTAnalyzer extends Plugin {
457
574
  consequentSourceName: initExpression.consequent.type === 'Identifier' ? initExpression.consequent.name : null,
458
575
  alternateSourceName: initExpression.alternate.type === 'Identifier' ? initExpression.alternate.name : null,
459
576
  file: module.file,
460
- line: line
577
+ line: line,
578
+ column: column
461
579
  });
462
- this.trackVariableAssignment(initExpression.consequent, variableId, variableName, module, line, literals, variableAssignments, literalCounterRef);
463
- this.trackVariableAssignment(initExpression.alternate, variableId, variableName, module, line, literals, variableAssignments, literalCounterRef);
580
+ this.trackVariableAssignment(initExpression.consequent, variableId, variableName, module, line, literals, variableAssignments, literalCounterRef, objectLiterals, objectProperties, objectLiteralCounterRef);
581
+ this.trackVariableAssignment(initExpression.alternate, variableId, variableName, module, line, literals, variableAssignments, literalCounterRef, objectLiterals, objectProperties, objectLiteralCounterRef);
464
582
  return;
465
583
  }
466
584
  // 10. LogicalExpression
467
585
  if (initExpression.type === 'LogicalExpression') {
468
- const expressionId = `EXPRESSION#logical#${module.file}#${line}:${initExpression.start}`;
586
+ const column = initExpression.start ?? 0;
587
+ const expressionId = ExpressionNode.generateId('LogicalExpression', module.file, line, column);
469
588
  variableAssignments.push({
470
589
  variableId,
471
590
  sourceType: 'EXPRESSION',
@@ -475,15 +594,17 @@ export class JSASTAnalyzer extends Plugin {
475
594
  leftSourceName: initExpression.left.type === 'Identifier' ? initExpression.left.name : null,
476
595
  rightSourceName: initExpression.right.type === 'Identifier' ? initExpression.right.name : null,
477
596
  file: module.file,
478
- line: line
597
+ line: line,
598
+ column: column
479
599
  });
480
- this.trackVariableAssignment(initExpression.left, variableId, variableName, module, line, literals, variableAssignments, literalCounterRef);
481
- this.trackVariableAssignment(initExpression.right, variableId, variableName, module, line, literals, variableAssignments, literalCounterRef);
600
+ this.trackVariableAssignment(initExpression.left, variableId, variableName, module, line, literals, variableAssignments, literalCounterRef, objectLiterals, objectProperties, objectLiteralCounterRef);
601
+ this.trackVariableAssignment(initExpression.right, variableId, variableName, module, line, literals, variableAssignments, literalCounterRef, objectLiterals, objectProperties, objectLiteralCounterRef);
482
602
  return;
483
603
  }
484
604
  // 11. TemplateLiteral
485
605
  if (initExpression.type === 'TemplateLiteral' && initExpression.expressions.length > 0) {
486
- const expressionId = `EXPRESSION#template#${module.file}#${line}:${initExpression.start}`;
606
+ const column = initExpression.start ?? 0;
607
+ const expressionId = ExpressionNode.generateId('TemplateLiteral', module.file, line, column);
487
608
  const expressionSourceNames = initExpression.expressions
488
609
  .filter((expr) => expr.type === 'Identifier')
489
610
  .map(expr => expr.name);
@@ -494,16 +615,365 @@ export class JSASTAnalyzer extends Plugin {
494
615
  expressionType: 'TemplateLiteral',
495
616
  expressionSourceNames,
496
617
  file: module.file,
497
- line: line
618
+ line: line,
619
+ column: column
498
620
  });
499
621
  for (const expr of initExpression.expressions) {
500
622
  // Filter out TSType nodes (only in TypeScript code)
501
623
  if (t.isExpression(expr)) {
502
- this.trackVariableAssignment(expr, variableId, variableName, module, line, literals, variableAssignments, literalCounterRef);
624
+ this.trackVariableAssignment(expr, variableId, variableName, module, line, literals, variableAssignments, literalCounterRef, objectLiterals, objectProperties, objectLiteralCounterRef);
625
+ }
626
+ }
627
+ return;
628
+ }
629
+ }
630
+ /**
631
+ * Extract object properties and create ObjectPropertyInfo records.
632
+ * Handles nested object/array literals recursively. (REG-328)
633
+ */
634
+ extractObjectProperties(objectExpr, objectId, module, objectProperties, objectLiterals, objectLiteralCounterRef, literals, literalCounterRef) {
635
+ for (const prop of objectExpr.properties) {
636
+ const propLine = prop.loc?.start.line || 0;
637
+ const propColumn = prop.loc?.start.column || 0;
638
+ // Handle spread properties: { ...other }
639
+ if (prop.type === 'SpreadElement') {
640
+ const spreadArg = prop.argument;
641
+ const propertyInfo = {
642
+ objectId,
643
+ propertyName: '<spread>',
644
+ valueType: 'SPREAD',
645
+ file: module.file,
646
+ line: propLine,
647
+ column: propColumn
648
+ };
649
+ if (spreadArg.type === 'Identifier') {
650
+ propertyInfo.valueName = spreadArg.name;
651
+ propertyInfo.valueType = 'VARIABLE';
652
+ }
653
+ objectProperties.push(propertyInfo);
654
+ continue;
655
+ }
656
+ // Handle regular properties
657
+ if (prop.type === 'ObjectProperty') {
658
+ let propertyName;
659
+ // Get property name
660
+ if (prop.key.type === 'Identifier') {
661
+ propertyName = prop.key.name;
662
+ }
663
+ else if (prop.key.type === 'StringLiteral') {
664
+ propertyName = prop.key.value;
503
665
  }
666
+ else if (prop.key.type === 'NumericLiteral') {
667
+ propertyName = String(prop.key.value);
668
+ }
669
+ else {
670
+ propertyName = '<computed>';
671
+ }
672
+ const propertyInfo = {
673
+ objectId,
674
+ propertyName,
675
+ file: module.file,
676
+ line: propLine,
677
+ column: propColumn,
678
+ valueType: 'EXPRESSION'
679
+ };
680
+ const value = prop.value;
681
+ // Nested object literal - check BEFORE extractLiteralValue
682
+ if (value.type === 'ObjectExpression') {
683
+ const nestedObjectNode = ObjectLiteralNode.create(module.file, value.loc?.start.line || 0, value.loc?.start.column || 0, { counter: objectLiteralCounterRef.value++ });
684
+ objectLiterals.push(nestedObjectNode);
685
+ const nestedObjectId = nestedObjectNode.id;
686
+ // Recursively extract nested properties
687
+ this.extractObjectProperties(value, nestedObjectId, module, objectProperties, objectLiterals, objectLiteralCounterRef, literals, literalCounterRef);
688
+ propertyInfo.valueType = 'OBJECT_LITERAL';
689
+ propertyInfo.nestedObjectId = nestedObjectId;
690
+ propertyInfo.valueNodeId = nestedObjectId;
691
+ }
692
+ // Literal value (primitives only - objects/arrays handled above)
693
+ else {
694
+ const literalValue = ExpressionEvaluator.extractLiteralValue(value);
695
+ // Handle both non-null literals AND explicit null literals (NullLiteral)
696
+ if (literalValue !== null || value.type === 'NullLiteral') {
697
+ const literalId = `LITERAL#${propertyName}#${module.file}#${propLine}:${propColumn}:${literalCounterRef.value++}`;
698
+ literals.push({
699
+ id: literalId,
700
+ type: 'LITERAL',
701
+ value: literalValue,
702
+ valueType: typeof literalValue,
703
+ file: module.file,
704
+ line: propLine,
705
+ column: propColumn,
706
+ parentCallId: objectId,
707
+ argIndex: 0
708
+ });
709
+ propertyInfo.valueType = 'LITERAL';
710
+ propertyInfo.valueNodeId = literalId;
711
+ propertyInfo.literalValue = literalValue;
712
+ }
713
+ // Variable reference
714
+ else if (value.type === 'Identifier') {
715
+ propertyInfo.valueType = 'VARIABLE';
716
+ propertyInfo.valueName = value.name;
717
+ }
718
+ // Call expression
719
+ else if (value.type === 'CallExpression') {
720
+ propertyInfo.valueType = 'CALL';
721
+ propertyInfo.callLine = value.loc?.start.line;
722
+ propertyInfo.callColumn = value.loc?.start.column;
723
+ }
724
+ // Other expressions
725
+ else {
726
+ propertyInfo.valueType = 'EXPRESSION';
727
+ }
728
+ }
729
+ objectProperties.push(propertyInfo);
730
+ }
731
+ // Handle object methods: { foo() {} }
732
+ else if (prop.type === 'ObjectMethod') {
733
+ const propertyName = prop.key.type === 'Identifier' ? prop.key.name : '<computed>';
734
+ objectProperties.push({
735
+ objectId,
736
+ propertyName,
737
+ valueType: 'EXPRESSION',
738
+ file: module.file,
739
+ line: propLine,
740
+ column: propColumn
741
+ });
504
742
  }
743
+ }
744
+ }
745
+ /**
746
+ * Recursively unwrap AwaitExpression to get the underlying expression.
747
+ * await await fetch() -> fetch()
748
+ */
749
+ unwrapAwaitExpression(node) {
750
+ if (node.type === 'AwaitExpression' && node.argument) {
751
+ return this.unwrapAwaitExpression(node.argument);
752
+ }
753
+ return node;
754
+ }
755
+ /**
756
+ * Extract call site information from CallExpression.
757
+ * Returns null if not a valid CallExpression.
758
+ */
759
+ extractCallInfo(node) {
760
+ if (node.type !== 'CallExpression') {
761
+ return null;
762
+ }
763
+ const callee = node.callee;
764
+ let name;
765
+ let isMethodCall = false;
766
+ // Direct call: fetchUser()
767
+ if (t.isIdentifier(callee)) {
768
+ name = callee.name;
769
+ }
770
+ // Method call: obj.fetchUser() or arr.map()
771
+ else if (t.isMemberExpression(callee)) {
772
+ isMethodCall = true;
773
+ const objectName = t.isIdentifier(callee.object)
774
+ ? callee.object.name
775
+ : (t.isThisExpression(callee.object) ? 'this' : 'unknown');
776
+ const methodName = t.isIdentifier(callee.property)
777
+ ? callee.property.name
778
+ : 'unknown';
779
+ name = `${objectName}.${methodName}`;
780
+ }
781
+ else {
782
+ return null;
783
+ }
784
+ return {
785
+ line: node.loc?.start.line ?? 0,
786
+ column: node.loc?.start.column ?? 0,
787
+ name,
788
+ isMethodCall
789
+ };
790
+ }
791
+ /**
792
+ * Check if expression is CallExpression or AwaitExpression wrapping a call.
793
+ */
794
+ isCallOrAwaitExpression(node) {
795
+ const unwrapped = this.unwrapAwaitExpression(node);
796
+ return unwrapped.type === 'CallExpression';
797
+ }
798
+ /**
799
+ * Tracks destructuring assignments for data flow analysis.
800
+ *
801
+ * For ObjectPattern: creates EXPRESSION nodes representing source.property
802
+ * For ArrayPattern: creates EXPRESSION nodes representing source[index]
803
+ *
804
+ * Supports:
805
+ * - Phase 1 (REG-201): Identifier init expressions (const { x } = obj)
806
+ * - Phase 2 (REG-223): CallExpression/AwaitExpression init (const { x } = getConfig())
807
+ *
808
+ * @param pattern - The destructuring pattern (ObjectPattern or ArrayPattern)
809
+ * @param initNode - The init expression (right-hand side)
810
+ * @param variables - Extracted variables with propertyPath/arrayIndex metadata and IDs
811
+ * @param module - Module context
812
+ * @param variableAssignments - Collection to push assignment info to
813
+ */
814
+ trackDestructuringAssignment(pattern, initNode, variables, module, variableAssignments) {
815
+ if (!initNode)
505
816
  return;
817
+ // Phase 1: Simple Identifier init expressions (REG-201)
818
+ // Examples: const { x } = obj, const [a] = arr
819
+ if (t.isIdentifier(initNode)) {
820
+ const sourceBaseName = initNode.name;
821
+ // Process each extracted variable
822
+ for (const varInfo of variables) {
823
+ const variableId = varInfo.id;
824
+ // Handle rest elements specially - create edge to whole source
825
+ if (varInfo.isRest) {
826
+ variableAssignments.push({
827
+ variableId,
828
+ sourceType: 'VARIABLE',
829
+ sourceName: sourceBaseName,
830
+ line: varInfo.loc.start.line
831
+ });
832
+ continue;
833
+ }
834
+ // ObjectPattern: const { headers } = req → headers ASSIGNED_FROM req.headers
835
+ if (t.isObjectPattern(pattern) && varInfo.propertyPath && varInfo.propertyPath.length > 0) {
836
+ const propertyPath = varInfo.propertyPath;
837
+ const expressionLine = varInfo.loc.start.line;
838
+ const expressionColumn = varInfo.loc.start.column;
839
+ // Build property path string (e.g., "req.headers.contentType" for nested)
840
+ const fullPath = [sourceBaseName, ...propertyPath].join('.');
841
+ const expressionId = ExpressionNode.generateId('MemberExpression', module.file, expressionLine, expressionColumn);
842
+ variableAssignments.push({
843
+ variableId,
844
+ sourceType: 'EXPRESSION',
845
+ sourceId: expressionId,
846
+ expressionType: 'MemberExpression',
847
+ object: sourceBaseName,
848
+ property: propertyPath[propertyPath.length - 1], // Last property for simple display
849
+ computed: false,
850
+ path: fullPath,
851
+ objectSourceName: sourceBaseName, // Use objectSourceName for DERIVES_FROM edge creation
852
+ propertyPath: propertyPath,
853
+ file: module.file,
854
+ line: expressionLine,
855
+ column: expressionColumn
856
+ });
857
+ }
858
+ // ArrayPattern: const [first, second] = arr → first ASSIGNED_FROM arr[0]
859
+ else if (t.isArrayPattern(pattern) && varInfo.arrayIndex !== undefined) {
860
+ const arrayIndex = varInfo.arrayIndex;
861
+ const expressionLine = varInfo.loc.start.line;
862
+ const expressionColumn = varInfo.loc.start.column;
863
+ // Check if we also have propertyPath (mixed destructuring: { items: [first] } = data)
864
+ const hasPropertyPath = varInfo.propertyPath && varInfo.propertyPath.length > 0;
865
+ const expressionId = ExpressionNode.generateId('MemberExpression', module.file, expressionLine, expressionColumn);
866
+ variableAssignments.push({
867
+ variableId,
868
+ sourceType: 'EXPRESSION',
869
+ sourceId: expressionId,
870
+ expressionType: 'MemberExpression',
871
+ object: sourceBaseName,
872
+ property: String(arrayIndex),
873
+ computed: true,
874
+ objectSourceName: sourceBaseName, // Use objectSourceName for DERIVES_FROM edge creation
875
+ arrayIndex: arrayIndex,
876
+ propertyPath: hasPropertyPath ? varInfo.propertyPath : undefined,
877
+ file: module.file,
878
+ line: expressionLine,
879
+ column: expressionColumn
880
+ });
881
+ }
882
+ }
883
+ }
884
+ // Phase 2: CallExpression or AwaitExpression (REG-223)
885
+ else if (this.isCallOrAwaitExpression(initNode)) {
886
+ const unwrapped = this.unwrapAwaitExpression(initNode);
887
+ const callInfo = this.extractCallInfo(unwrapped);
888
+ if (!callInfo) {
889
+ // Unsupported call pattern (computed callee, etc.)
890
+ return;
891
+ }
892
+ const callRepresentation = `${callInfo.name}()`;
893
+ // Process each extracted variable
894
+ for (const varInfo of variables) {
895
+ const variableId = varInfo.id;
896
+ // Handle rest elements - create direct CALL_SITE assignment
897
+ if (varInfo.isRest) {
898
+ variableAssignments.push({
899
+ variableId,
900
+ sourceType: 'CALL_SITE',
901
+ callName: callInfo.name,
902
+ callLine: callInfo.line,
903
+ callColumn: callInfo.column,
904
+ callSourceLine: callInfo.line,
905
+ callSourceColumn: callInfo.column,
906
+ callSourceFile: module.file,
907
+ callSourceName: callInfo.name,
908
+ line: varInfo.loc.start.line
909
+ });
910
+ continue;
911
+ }
912
+ // ObjectPattern: const { data } = fetchUser() → data ASSIGNED_FROM fetchUser().data
913
+ if (t.isObjectPattern(pattern) && varInfo.propertyPath && varInfo.propertyPath.length > 0) {
914
+ const propertyPath = varInfo.propertyPath;
915
+ const expressionLine = varInfo.loc.start.line;
916
+ const expressionColumn = varInfo.loc.start.column;
917
+ // Build property path string: "fetchUser().data" or "fetchUser().user.name"
918
+ const fullPath = [callRepresentation, ...propertyPath].join('.');
919
+ const expressionId = ExpressionNode.generateId('MemberExpression', module.file, expressionLine, expressionColumn);
920
+ variableAssignments.push({
921
+ variableId,
922
+ sourceType: 'EXPRESSION',
923
+ sourceId: expressionId,
924
+ expressionType: 'MemberExpression',
925
+ object: callRepresentation, // "fetchUser()" - display name
926
+ property: propertyPath[propertyPath.length - 1],
927
+ computed: false,
928
+ path: fullPath, // "fetchUser().data"
929
+ propertyPath: propertyPath, // ["data"]
930
+ // Call source for DERIVES_FROM lookup (REG-223)
931
+ callSourceLine: callInfo.line,
932
+ callSourceColumn: callInfo.column,
933
+ callSourceFile: module.file,
934
+ callSourceName: callInfo.name,
935
+ sourceMetadata: {
936
+ sourceType: callInfo.isMethodCall ? 'method-call' : 'call'
937
+ },
938
+ file: module.file,
939
+ line: expressionLine,
940
+ column: expressionColumn
941
+ });
942
+ }
943
+ // ArrayPattern: const [first] = arr.map(fn) → first ASSIGNED_FROM arr.map(fn)[0]
944
+ else if (t.isArrayPattern(pattern) && varInfo.arrayIndex !== undefined) {
945
+ const arrayIndex = varInfo.arrayIndex;
946
+ const expressionLine = varInfo.loc.start.line;
947
+ const expressionColumn = varInfo.loc.start.column;
948
+ const hasPropertyPath = varInfo.propertyPath && varInfo.propertyPath.length > 0;
949
+ const expressionId = ExpressionNode.generateId('MemberExpression', module.file, expressionLine, expressionColumn);
950
+ variableAssignments.push({
951
+ variableId,
952
+ sourceType: 'EXPRESSION',
953
+ sourceId: expressionId,
954
+ expressionType: 'MemberExpression',
955
+ object: callRepresentation,
956
+ property: String(arrayIndex),
957
+ computed: true,
958
+ arrayIndex: arrayIndex,
959
+ propertyPath: hasPropertyPath ? varInfo.propertyPath : undefined,
960
+ // Call source for DERIVES_FROM lookup (REG-223)
961
+ callSourceLine: callInfo.line,
962
+ callSourceColumn: callInfo.column,
963
+ callSourceFile: module.file,
964
+ callSourceName: callInfo.name,
965
+ sourceMetadata: {
966
+ sourceType: callInfo.isMethodCall ? 'method-call' : 'call'
967
+ },
968
+ file: module.file,
969
+ line: expressionLine,
970
+ column: expressionColumn
971
+ });
972
+ }
973
+ }
506
974
  }
975
+ // Unsupported init type (MemberExpression without call, etc.)
976
+ // else: do nothing - skip silently
507
977
  }
508
978
  /**
509
979
  * Получить все MODULE ноды из графа
@@ -531,14 +1001,23 @@ export class JSASTAnalyzer extends Plugin {
531
1001
  plugins: ['jsx', 'typescript']
532
1002
  });
533
1003
  this.profiler.end('babel_parse');
1004
+ // Create ScopeTracker for semantic ID generation
1005
+ // Use basename for shorter, more readable semantic IDs
1006
+ const scopeTracker = new ScopeTracker(basename(module.file));
534
1007
  const functions = [];
535
1008
  const parameters = [];
536
1009
  const scopes = [];
1010
+ // Branching (switch statements)
1011
+ const branches = [];
1012
+ const cases = [];
1013
+ // Control flow (loops)
1014
+ const loops = [];
537
1015
  const variableDeclarations = [];
538
1016
  const callSites = [];
539
1017
  const methodCalls = [];
540
1018
  const eventListeners = [];
541
1019
  const classInstantiations = [];
1020
+ const constructorCalls = [];
542
1021
  const classDeclarations = [];
543
1022
  const methodCallbacks = [];
544
1023
  const callArguments = [];
@@ -552,6 +1031,25 @@ export class JSASTAnalyzer extends Plugin {
552
1031
  const typeAliases = [];
553
1032
  const enums = [];
554
1033
  const decorators = [];
1034
+ // Object/Array literal tracking for data flow
1035
+ const objectLiterals = [];
1036
+ const objectProperties = [];
1037
+ const arrayLiterals = [];
1038
+ const arrayElements = [];
1039
+ // Array mutation tracking for FLOWS_INTO edges
1040
+ const arrayMutations = [];
1041
+ // Object mutation tracking for FLOWS_INTO edges
1042
+ const objectMutations = [];
1043
+ // Variable reassignment tracking for FLOWS_INTO edges (REG-290)
1044
+ const variableReassignments = [];
1045
+ // Return statement tracking for RETURNS edges
1046
+ const returnStatements = [];
1047
+ // Update expression tracking for MODIFIES edges (REG-288, REG-312)
1048
+ const updateExpressions = [];
1049
+ // Promise resolution tracking for RESOLVES_TO edges (REG-334)
1050
+ const promiseResolutions = [];
1051
+ // Promise executor contexts (REG-334) - keyed by executor function's start:end position
1052
+ const promiseExecutorContexts = new Map();
555
1053
  const ifScopeCounterRef = { value: 0 };
556
1054
  const scopeCounterRef = { value: 0 };
557
1055
  const varDeclCounterRef = { value: 0 };
@@ -560,6 +1058,10 @@ export class JSASTAnalyzer extends Plugin {
560
1058
  const httpRequestCounterRef = { value: 0 };
561
1059
  const literalCounterRef = { value: 0 };
562
1060
  const anonymousFunctionCounterRef = { value: 0 };
1061
+ const objectLiteralCounterRef = { value: 0 };
1062
+ const arrayLiteralCounterRef = { value: 0 };
1063
+ const branchCounterRef = { value: 0 };
1064
+ const caseCounterRef = { value: 0 };
563
1065
  const processedNodes = {
564
1066
  functions: new Set(),
565
1067
  classes: new Set(),
@@ -579,34 +1081,56 @@ export class JSASTAnalyzer extends Plugin {
579
1081
  this.profiler.end('traverse_imports');
580
1082
  // Variables
581
1083
  this.profiler.start('traverse_variables');
582
- const variableVisitor = new VariableVisitor(module, { variableDeclarations, classInstantiations, literals, variableAssignments, varDeclCounterRef, literalCounterRef }, this.extractVariableNamesFromPattern.bind(this), this.trackVariableAssignment.bind(this));
1084
+ const variableVisitor = new VariableVisitor(module, { variableDeclarations, classInstantiations, literals, variableAssignments, varDeclCounterRef, literalCounterRef, scopes, scopeCounterRef, objectLiterals, objectProperties, objectLiteralCounterRef }, this.extractVariableNamesFromPattern.bind(this), this.trackVariableAssignment.bind(this), scopeTracker // Pass ScopeTracker for semantic ID generation
1085
+ );
583
1086
  traverse(ast, variableVisitor.getHandlers());
584
1087
  this.profiler.end('traverse_variables');
585
- // Module-level scope context for consistent anonymous naming
586
- const moduleScopeCtx = {
587
- semanticPath: module.name,
588
- siblingCounters: new Map()
589
- };
590
1088
  const allCollections = {
591
- functions, parameters, scopes, variableDeclarations, callSites, methodCalls,
592
- eventListeners, methodCallbacks, callArguments, classInstantiations, classDeclarations,
1089
+ functions, parameters, scopes,
1090
+ // Branching (switch statements)
1091
+ branches, cases,
1092
+ // Control flow (loops)
1093
+ loops,
1094
+ variableDeclarations, callSites, methodCalls,
1095
+ eventListeners, methodCallbacks, callArguments, classInstantiations, constructorCalls, classDeclarations,
593
1096
  httpRequests, literals, variableAssignments,
594
1097
  // TypeScript-specific collections
595
1098
  interfaces, typeAliases, enums, decorators,
1099
+ // Object/Array literal tracking
1100
+ objectLiterals, objectProperties, arrayLiterals, arrayElements,
1101
+ // Array mutation tracking
1102
+ arrayMutations,
1103
+ // Object mutation tracking
1104
+ objectMutations,
1105
+ // Variable reassignment tracking (REG-290)
1106
+ variableReassignments,
1107
+ // Return statement tracking
1108
+ returnStatements,
1109
+ // Update expression tracking (REG-288, REG-312)
1110
+ updateExpressions,
1111
+ // Promise resolution tracking (REG-334)
1112
+ promiseResolutions,
1113
+ promiseExecutorContexts,
1114
+ objectLiteralCounterRef, arrayLiteralCounterRef,
596
1115
  ifScopeCounterRef, scopeCounterRef, varDeclCounterRef,
597
1116
  callSiteCounterRef, functionCounterRef, httpRequestCounterRef,
598
- literalCounterRef, anonymousFunctionCounterRef, processedNodes,
599
- imports, exports, moduleScopeCtx, code,
1117
+ literalCounterRef, anonymousFunctionCounterRef,
1118
+ branchCounterRef, caseCounterRef,
1119
+ processedNodes,
1120
+ imports, exports, code,
600
1121
  // VisitorCollections compatibility
601
1122
  classes: classDeclarations,
602
1123
  methods: [],
603
1124
  variables: variableDeclarations,
604
1125
  sideEffects: [],
605
- variableCounterRef: varDeclCounterRef
1126
+ variableCounterRef: varDeclCounterRef,
1127
+ // ScopeTracker for semantic ID generation
1128
+ scopeTracker
606
1129
  };
607
1130
  // Functions
608
1131
  this.profiler.start('traverse_functions');
609
- const functionVisitor = new FunctionVisitor(module, allCollections, this.analyzeFunctionBody.bind(this));
1132
+ const functionVisitor = new FunctionVisitor(module, allCollections, this.analyzeFunctionBody.bind(this), scopeTracker // Pass ScopeTracker for semantic ID generation
1133
+ );
610
1134
  traverse(ast, functionVisitor.getHandlers());
611
1135
  this.profiler.end('traverse_functions');
612
1136
  // AssignmentExpression (module-level function assignments)
@@ -631,20 +1155,20 @@ export class JSASTAnalyzer extends Plugin {
631
1155
  functionName = assignNode.left.name;
632
1156
  }
633
1157
  const funcNode = assignNode.right;
634
- const functionId = `FUNCTION#${functionName}#${module.file}#${assignNode.loc.start.line}:${assignNode.loc.start.column}`;
1158
+ // Use semantic ID as primary ID (matching FunctionVisitor pattern)
1159
+ const functionId = computeSemanticId('FUNCTION', functionName, scopeTracker.getContext());
635
1160
  functions.push({
636
1161
  id: functionId,
637
- stableId: functionId,
638
1162
  type: 'FUNCTION',
639
1163
  name: functionName,
640
1164
  file: module.file,
641
- line: assignNode.loc.start.line,
642
- column: assignNode.loc.start.column,
1165
+ line: getLine(assignNode),
1166
+ column: getColumn(assignNode),
643
1167
  async: funcNode.async || false,
644
1168
  generator: funcNode.type === 'FunctionExpression' ? funcNode.generator : false,
645
1169
  isAssignment: true
646
1170
  });
647
- const funcBodyScopeId = `SCOPE#${functionName}:body#${module.file}#${assignNode.loc.start.line}`;
1171
+ const funcBodyScopeId = `SCOPE#${functionName}:body#${module.file}#${getLine(assignNode)}`;
648
1172
  scopes.push({
649
1173
  id: funcBodyScopeId,
650
1174
  type: 'SCOPE',
@@ -653,28 +1177,56 @@ export class JSASTAnalyzer extends Plugin {
653
1177
  semanticId: `${functionName}:function_body[0]`,
654
1178
  conditional: false,
655
1179
  file: module.file,
656
- line: assignNode.loc.start.line,
1180
+ line: getLine(assignNode),
657
1181
  parentFunctionId: functionId
658
1182
  });
659
1183
  const funcPath = assignPath.get('right');
660
- // Create scope context for analyzing the function body
661
- const funcScopeCtx = {
662
- semanticPath: functionName,
663
- siblingCounters: new Map()
664
- };
665
- this.analyzeFunctionBody(funcPath, funcBodyScopeId, module, allCollections, funcScopeCtx);
1184
+ // Enter function scope for semantic ID generation and analyze
1185
+ scopeTracker.enterScope(functionName, 'function');
1186
+ this.analyzeFunctionBody(funcPath, funcBodyScopeId, module, allCollections);
1187
+ scopeTracker.exitScope();
1188
+ }
1189
+ // === VARIABLE REASSIGNMENT (REG-290) ===
1190
+ // Check if LHS is simple identifier (not obj.prop, not arr[i])
1191
+ // Must be checked at module level too
1192
+ if (assignNode.left.type === 'Identifier') {
1193
+ // Initialize collection if not exists
1194
+ if (!allCollections.variableReassignments) {
1195
+ allCollections.variableReassignments = [];
1196
+ }
1197
+ const variableReassignments = allCollections.variableReassignments;
1198
+ this.detectVariableReassignment(assignNode, module, variableReassignments, scopeTracker);
666
1199
  }
1200
+ // === END VARIABLE REASSIGNMENT ===
1201
+ // Check for indexed array assignment at module level: arr[i] = value
1202
+ this.detectIndexedArrayAssignment(assignNode, module, arrayMutations, scopeTracker);
1203
+ // Check for object property assignment at module level: obj.prop = value
1204
+ this.detectObjectPropertyAssignment(assignNode, module, objectMutations, scopeTracker);
667
1205
  }
668
1206
  });
669
1207
  this.profiler.end('traverse_assignments');
1208
+ // Module-level UpdateExpression (obj.count++, arr[i]++, i++) - REG-288/REG-312
1209
+ this.profiler.start('traverse_updates');
1210
+ traverse(ast, {
1211
+ UpdateExpression: (updatePath) => {
1212
+ // Skip if inside a function - analyzeFunctionBody handles those
1213
+ const functionParent = updatePath.getFunctionParent();
1214
+ if (functionParent)
1215
+ return;
1216
+ // Module-level update expression: no parentScopeId
1217
+ this.collectUpdateExpression(updatePath.node, module, updateExpressions, undefined, scopeTracker);
1218
+ }
1219
+ });
1220
+ this.profiler.end('traverse_updates');
670
1221
  // Classes
671
1222
  this.profiler.start('traverse_classes');
672
- const classVisitor = new ClassVisitor(module, allCollections, this.analyzeFunctionBody.bind(this));
1223
+ const classVisitor = new ClassVisitor(module, allCollections, this.analyzeFunctionBody.bind(this), scopeTracker // Pass ScopeTracker for semantic ID generation
1224
+ );
673
1225
  traverse(ast, classVisitor.getHandlers());
674
1226
  this.profiler.end('traverse_classes');
675
1227
  // TypeScript-specific constructs (interfaces, type aliases, enums)
676
1228
  this.profiler.start('traverse_typescript');
677
- const typescriptVisitor = new TypeScriptVisitor(module, allCollections);
1229
+ const typescriptVisitor = new TypeScriptVisitor(module, allCollections, scopeTracker);
678
1230
  traverse(ast, typescriptVisitor.getHandlers());
679
1231
  this.profiler.end('traverse_typescript');
680
1232
  // Module-level callbacks
@@ -686,22 +1238,22 @@ export class JSASTAnalyzer extends Plugin {
686
1238
  if (functionParent)
687
1239
  return;
688
1240
  if (funcPath.parent && funcPath.parent.type === 'CallExpression') {
689
- const funcName = funcNode.id ? funcNode.id.name : this.generateAnonymousName(moduleScopeCtx);
690
- const functionId = `FUNCTION#${funcName}#${module.file}#${funcNode.loc.start.line}:${funcNode.loc.start.column}`;
1241
+ const funcName = funcNode.id ? funcNode.id.name : this.generateAnonymousName(scopeTracker);
1242
+ // Use semantic ID as primary ID (matching FunctionVisitor pattern)
1243
+ const functionId = computeSemanticId('FUNCTION', funcName, scopeTracker.getContext());
691
1244
  functions.push({
692
1245
  id: functionId,
693
- stableId: functionId,
694
1246
  type: 'FUNCTION',
695
1247
  name: funcName,
696
1248
  file: module.file,
697
- line: funcNode.loc.start.line,
698
- column: funcNode.loc.start.column,
1249
+ line: getLine(funcNode),
1250
+ column: getColumn(funcNode),
699
1251
  async: funcNode.async || false,
700
1252
  generator: funcNode.generator || false,
701
1253
  isCallback: true,
702
1254
  parentScopeId: module.id
703
1255
  });
704
- const callbackScopeId = `SCOPE#${funcName}:body#${module.file}#${funcNode.loc.start.line}`;
1256
+ const callbackScopeId = `SCOPE#${funcName}:body#${module.file}#${getLine(funcNode)}`;
705
1257
  scopes.push({
706
1258
  id: callbackScopeId,
707
1259
  type: 'SCOPE',
@@ -710,15 +1262,13 @@ export class JSASTAnalyzer extends Plugin {
710
1262
  semanticId: `${funcName}:callback_body[0]`,
711
1263
  conditional: false,
712
1264
  file: module.file,
713
- line: funcNode.loc.start.line,
1265
+ line: getLine(funcNode),
714
1266
  parentFunctionId: functionId
715
1267
  });
716
- // Create scope context for analyzing the callback body
717
- const callbackScopeCtx = {
718
- semanticPath: funcName,
719
- siblingCounters: new Map()
720
- };
721
- this.analyzeFunctionBody(funcPath, callbackScopeId, module, allCollections, callbackScopeCtx);
1268
+ // Enter callback scope for semantic ID generation and analyze
1269
+ scopeTracker.enterScope(funcName, 'callback');
1270
+ this.analyzeFunctionBody(funcPath, callbackScopeId, module, allCollections);
1271
+ scopeTracker.exitScope();
722
1272
  funcPath.skip();
723
1273
  }
724
1274
  }
@@ -726,9 +1276,75 @@ export class JSASTAnalyzer extends Plugin {
726
1276
  this.profiler.end('traverse_callbacks');
727
1277
  // Call expressions
728
1278
  this.profiler.start('traverse_calls');
729
- const callExpressionVisitor = new CallExpressionVisitor(module, allCollections);
1279
+ const callExpressionVisitor = new CallExpressionVisitor(module, allCollections, scopeTracker);
730
1280
  traverse(ast, callExpressionVisitor.getHandlers());
731
1281
  this.profiler.end('traverse_calls');
1282
+ // Module-level NewExpression (constructor calls)
1283
+ // This handles top-level code like `const x = new Date()` that's not inside a function
1284
+ this.profiler.start('traverse_new');
1285
+ const processedConstructorCalls = new Set();
1286
+ traverse(ast, {
1287
+ NewExpression: (newPath) => {
1288
+ const newNode = newPath.node;
1289
+ const nodeKey = `constructor:new:${newNode.start}:${newNode.end}`;
1290
+ if (processedConstructorCalls.has(nodeKey)) {
1291
+ return;
1292
+ }
1293
+ processedConstructorCalls.add(nodeKey);
1294
+ // Determine className from callee
1295
+ let className = null;
1296
+ if (newNode.callee.type === 'Identifier') {
1297
+ className = newNode.callee.name;
1298
+ }
1299
+ else if (newNode.callee.type === 'MemberExpression' && newNode.callee.property.type === 'Identifier') {
1300
+ className = newNode.callee.property.name;
1301
+ }
1302
+ if (className) {
1303
+ const line = getLine(newNode);
1304
+ const column = getColumn(newNode);
1305
+ const constructorCallId = ConstructorCallNode.generateId(className, module.file, line, column);
1306
+ const isBuiltin = ConstructorCallNode.isBuiltinConstructor(className);
1307
+ constructorCalls.push({
1308
+ id: constructorCallId,
1309
+ type: 'CONSTRUCTOR_CALL',
1310
+ className,
1311
+ isBuiltin,
1312
+ file: module.file,
1313
+ line,
1314
+ column
1315
+ });
1316
+ // REG-334: If this is Promise constructor with executor callback,
1317
+ // register the context for resolve/reject detection
1318
+ if (className === 'Promise' && newNode.arguments.length > 0) {
1319
+ const executorArg = newNode.arguments[0];
1320
+ // Only handle inline function expressions (not variable references)
1321
+ if (t.isArrowFunctionExpression(executorArg) || t.isFunctionExpression(executorArg)) {
1322
+ // Extract resolve/reject parameter names
1323
+ let resolveName;
1324
+ let rejectName;
1325
+ if (executorArg.params.length > 0 && t.isIdentifier(executorArg.params[0])) {
1326
+ resolveName = executorArg.params[0].name;
1327
+ }
1328
+ if (executorArg.params.length > 1 && t.isIdentifier(executorArg.params[1])) {
1329
+ rejectName = executorArg.params[1].name;
1330
+ }
1331
+ if (resolveName) {
1332
+ // Key by function node position to allow nested Promise detection
1333
+ const funcKey = `${executorArg.start}:${executorArg.end}`;
1334
+ promiseExecutorContexts.set(funcKey, {
1335
+ constructorCallId,
1336
+ resolveName,
1337
+ rejectName,
1338
+ file: module.file,
1339
+ line
1340
+ });
1341
+ }
1342
+ }
1343
+ }
1344
+ }
1345
+ }
1346
+ });
1347
+ this.profiler.end('traverse_new');
732
1348
  // Module-level IfStatements
733
1349
  this.profiler.start('traverse_ifs');
734
1350
  traverse(ast, {
@@ -739,37 +1355,37 @@ export class JSASTAnalyzer extends Plugin {
739
1355
  const ifNode = ifPath.node;
740
1356
  const condition = code.substring(ifNode.test.start, ifNode.test.end) || 'condition';
741
1357
  const counterId = ifScopeCounterRef.value++;
742
- const ifScopeId = `SCOPE#if#${module.file}#${ifNode.loc.start.line}:${ifNode.loc.start.column}:${counterId}`;
1358
+ const ifScopeId = `SCOPE#if#${module.file}#${getLine(ifNode)}:${getColumn(ifNode)}:${counterId}`;
743
1359
  const constraints = ConditionParser.parse(ifNode.test);
744
- const ifSemanticId = this.generateSemanticId('if_statement', moduleScopeCtx);
1360
+ const ifSemanticId = this.generateSemanticId('if_statement', scopeTracker);
745
1361
  scopes.push({
746
1362
  id: ifScopeId,
747
1363
  type: 'SCOPE',
748
1364
  scopeType: 'if_statement',
749
- name: `if:${ifNode.loc.start.line}:${ifNode.loc.start.column}:${counterId}`,
1365
+ name: `if:${getLine(ifNode)}:${getColumn(ifNode)}:${counterId}`,
750
1366
  semanticId: ifSemanticId,
751
1367
  conditional: true,
752
1368
  condition,
753
1369
  constraints: constraints.length > 0 ? constraints : undefined,
754
1370
  file: module.file,
755
- line: ifNode.loc.start.line,
1371
+ line: getLine(ifNode),
756
1372
  parentScopeId: module.id
757
1373
  });
758
1374
  if (ifNode.alternate && ifNode.alternate.type !== 'IfStatement') {
759
1375
  const elseCounterId = ifScopeCounterRef.value++;
760
- const elseScopeId = `SCOPE#else#${module.file}#${ifNode.alternate.loc.start.line}:${ifNode.alternate.loc.start.column}:${elseCounterId}`;
1376
+ const elseScopeId = `SCOPE#else#${module.file}#${getLine(ifNode.alternate)}:${getColumn(ifNode.alternate)}:${elseCounterId}`;
761
1377
  const negatedConstraints = constraints.length > 0 ? ConditionParser.negate(constraints) : undefined;
762
- const elseSemanticId = this.generateSemanticId('else_statement', moduleScopeCtx);
1378
+ const elseSemanticId = this.generateSemanticId('else_statement', scopeTracker);
763
1379
  scopes.push({
764
1380
  id: elseScopeId,
765
1381
  type: 'SCOPE',
766
1382
  scopeType: 'else_statement',
767
- name: `else:${ifNode.alternate.loc.start.line}:${ifNode.alternate.loc.start.column}:${elseCounterId}`,
1383
+ name: `else:${getLine(ifNode.alternate)}:${getColumn(ifNode.alternate)}:${elseCounterId}`,
768
1384
  semanticId: elseSemanticId,
769
1385
  conditional: true,
770
1386
  constraints: negatedConstraints,
771
1387
  file: module.file,
772
- line: ifNode.alternate.loc.start.line,
1388
+ line: getLine(ifNode.alternate),
773
1389
  parentScopeId: module.id
774
1390
  });
775
1391
  }
@@ -781,14 +1397,25 @@ export class JSASTAnalyzer extends Plugin {
781
1397
  const result = await this.graphBuilder.build(module, graph, projectPath, {
782
1398
  functions,
783
1399
  scopes,
1400
+ // Branching (switch statements) - use allCollections refs as they're populated by analyzeFunctionBody
1401
+ branches: allCollections.branches || branches,
1402
+ cases: allCollections.cases || cases,
1403
+ // Control flow (loops) - use allCollections refs as they're populated by analyzeFunctionBody
1404
+ loops: allCollections.loops || loops,
1405
+ // Control flow (try/catch/finally) - Phase 4
1406
+ tryBlocks: allCollections.tryBlocks,
1407
+ catchBlocks: allCollections.catchBlocks,
1408
+ finallyBlocks: allCollections.finallyBlocks,
784
1409
  variableDeclarations,
785
1410
  callSites,
786
1411
  methodCalls,
787
1412
  eventListeners,
788
1413
  classInstantiations,
1414
+ constructorCalls,
789
1415
  classDeclarations,
790
1416
  methodCallbacks,
791
- callArguments,
1417
+ // REG-334: Use allCollections.callArguments to include function-level resolve/reject arguments
1418
+ callArguments: allCollections.callArguments || callArguments,
792
1419
  imports,
793
1420
  exports,
794
1421
  httpRequests,
@@ -799,308 +1426,1720 @@ export class JSASTAnalyzer extends Plugin {
799
1426
  interfaces,
800
1427
  typeAliases,
801
1428
  enums,
802
- decorators
1429
+ decorators,
1430
+ // Array mutation tracking
1431
+ arrayMutations,
1432
+ // Object mutation tracking
1433
+ objectMutations,
1434
+ // Variable reassignment tracking (REG-290)
1435
+ variableReassignments,
1436
+ // Return statement tracking
1437
+ returnStatements,
1438
+ // Update expression tracking (REG-288, REG-312)
1439
+ updateExpressions,
1440
+ // Promise resolution tracking (REG-334)
1441
+ promiseResolutions: allCollections.promiseResolutions || promiseResolutions,
1442
+ // Object/Array literal tracking - use allCollections refs as visitors may have created new arrays
1443
+ objectLiterals: allCollections.objectLiterals || objectLiterals,
1444
+ objectProperties: allCollections.objectProperties || objectProperties,
1445
+ arrayLiterals: allCollections.arrayLiterals || arrayLiterals
803
1446
  });
804
1447
  this.profiler.end('graph_build');
805
1448
  nodesCreated = result.nodes;
806
1449
  edgesCreated = result.edges;
807
1450
  }
808
- catch (error) {
809
- const err = error instanceof Error ? error : new Error(String(error));
810
- console.error(`[JSASTAnalyzer] Error analyzing ${module.file}:`, err.message);
811
- console.error(err.stack);
1451
+ catch {
1452
+ // Error analyzing module - silently skip, caller handles the result
812
1453
  }
813
1454
  return { nodes: nodesCreated, edges: edgesCreated };
814
1455
  }
815
1456
  /**
816
- * Helper to generate semantic ID for a scope and update counters
817
- */
818
- generateSemanticId(scopeType, scopeCtx) {
819
- if (!scopeCtx)
820
- return undefined;
821
- const siblingIndex = scopeCtx.siblingCounters.get(scopeType) || 0;
822
- scopeCtx.siblingCounters.set(scopeType, siblingIndex + 1);
823
- return `${scopeCtx.semanticPath}:${scopeType}[${siblingIndex}]`;
824
- }
825
- /**
826
- * Helper to create child scope context from a semantic ID
1457
+ * Helper to generate semantic ID for a scope using ScopeTracker.
1458
+ * Format: "scopePath:scopeType[index]" e.g. "MyClass->myMethod:if_statement[0]"
827
1459
  */
828
- createChildScopeContext(semanticId) {
829
- if (!semanticId)
1460
+ generateSemanticId(scopeType, scopeTracker) {
1461
+ if (!scopeTracker)
830
1462
  return undefined;
831
- return {
832
- semanticPath: semanticId,
833
- siblingCounters: new Map()
834
- };
1463
+ const scopePath = scopeTracker.getScopePath();
1464
+ const siblingIndex = scopeTracker.getItemCounter(`semanticId:${scopeType}`);
1465
+ return `${scopePath}:${scopeType}[${siblingIndex}]`;
835
1466
  }
836
1467
  /**
837
- * Generate a unique anonymous function name within the current scope
838
- * Uses scopeCtx.siblingCounters to ensure stability across JS/TS versions
1468
+ * Generate a unique anonymous function name within the current scope.
1469
+ * Uses ScopeTracker.getSiblingIndex() for stable naming.
839
1470
  */
840
- generateAnonymousName(scopeCtx) {
841
- if (!scopeCtx)
1471
+ generateAnonymousName(scopeTracker) {
1472
+ if (!scopeTracker)
842
1473
  return 'anonymous';
843
- const index = scopeCtx.siblingCounters.get('anonymous') || 0;
844
- scopeCtx.siblingCounters.set('anonymous', index + 1);
1474
+ const index = scopeTracker.getSiblingIndex('anonymous');
845
1475
  return `anonymous[${index}]`;
846
1476
  }
847
1477
  /**
848
- * Анализирует тело функции и извлекает переменные, вызовы, условные блоки
1478
+ * Factory method to create loop scope handlers.
1479
+ * All loop statements (for, for-in, for-of, while, do-while) follow the same pattern:
1480
+ * 1. Create scope with SCOPE#<scopeType>#file#line:counter
1481
+ * 2. Generate semantic ID
1482
+ * 3. Push to scopes array
1483
+ * 4. Enter/exit scope tracker
1484
+ *
1485
+ * @param trackerScopeType - Scope type for ScopeTracker (e.g., 'for', 'for-in', 'while')
1486
+ * @param scopeType - Scope type for the graph node (e.g., 'for-loop', 'for-in-loop')
1487
+ * @param parentScopeId - Parent scope ID for the scope node
1488
+ * @param module - Module context
1489
+ * @param scopes - Collection to push scope nodes to
1490
+ * @param scopeCounterRef - Counter for unique scope IDs
1491
+ * @param scopeTracker - Tracker for semantic ID generation
849
1492
  */
850
- analyzeFunctionBody(funcPath, parentScopeId, module, collections, scopeCtx) {
851
- // Extract with defaults for optional properties
852
- const functions = (collections.functions ?? []);
853
- const scopes = (collections.scopes ?? []);
854
- const variableDeclarations = (collections.variableDeclarations ?? []);
855
- const callSites = (collections.callSites ?? []);
856
- const methodCalls = (collections.methodCalls ?? []);
857
- const eventListeners = (collections.eventListeners ?? []);
858
- const methodCallbacks = (collections.methodCallbacks ?? []);
859
- const classInstantiations = (collections.classInstantiations ?? []);
860
- const httpRequests = (collections.httpRequests ?? []);
861
- const literals = (collections.literals ?? []);
862
- const variableAssignments = (collections.variableAssignments ?? []);
863
- const ifScopeCounterRef = (collections.ifScopeCounterRef ?? { value: 0 });
864
- const scopeCounterRef = (collections.scopeCounterRef ?? { value: 0 });
865
- const varDeclCounterRef = (collections.varDeclCounterRef ?? { value: 0 });
866
- const callSiteCounterRef = (collections.callSiteCounterRef ?? { value: 0 });
867
- const functionCounterRef = (collections.functionCounterRef ?? { value: 0 });
868
- const httpRequestCounterRef = (collections.httpRequestCounterRef ?? { value: 0 });
869
- const literalCounterRef = (collections.literalCounterRef ?? { value: 0 });
870
- const anonymousFunctionCounterRef = (collections.anonymousFunctionCounterRef ?? { value: 0 });
871
- const processedNodes = collections.processedNodes ?? {
872
- functions: new Set(),
873
- classes: new Set(),
874
- imports: new Set(),
875
- exports: new Set(),
876
- variables: new Set(),
877
- callSites: new Set(),
878
- methodCalls: new Set(),
879
- varDecls: new Set(),
880
- eventListeners: new Set()
881
- };
882
- const parentScopeVariables = new Set();
883
- const processedCallSites = processedNodes.callSites;
884
- const processedVarDecls = processedNodes.varDecls;
885
- const processedMethodCalls = processedNodes.methodCalls;
886
- const processedEventListeners = processedNodes.eventListeners;
887
- funcPath.traverse({
888
- VariableDeclaration: (varPath) => {
889
- const varNode = varPath.node;
890
- const isConst = varNode.kind === 'const';
891
- varNode.declarations.forEach(declarator => {
892
- const variables = this.extractVariableNamesFromPattern(declarator.id);
893
- variables.forEach(varInfo => {
894
- const literalValue = declarator.init ? ExpressionEvaluator.extractLiteralValue(declarator.init) : null;
895
- const isLiteral = literalValue !== null;
896
- const isNewExpression = declarator.init && declarator.init.type === 'NewExpression';
897
- const shouldBeConstant = isConst && (isLiteral || isNewExpression);
898
- const varId = shouldBeConstant
899
- ? `CONSTANT#${varInfo.name}#${module.file}#${varInfo.loc.start.line}:${varInfo.loc.start.column}:${varDeclCounterRef.value++}`
900
- : `VARIABLE#${varInfo.name}#${module.file}#${varInfo.loc.start.line}:${varInfo.loc.start.column}:${varDeclCounterRef.value++}`;
901
- parentScopeVariables.add({
902
- name: varInfo.name,
903
- id: varId,
904
- scopeId: parentScopeId
1493
+ /**
1494
+ * Handles VariableDeclaration nodes within function bodies.
1495
+ *
1496
+ * Extracts variable names from patterns (including destructuring), determines
1497
+ * if the variable should be CONSTANT or VARIABLE, generates semantic or legacy IDs,
1498
+ * and tracks class instantiations and variable assignments.
1499
+ *
1500
+ * @param varPath - The NodePath for the VariableDeclaration
1501
+ * @param parentScopeId - Parent scope ID for the variable
1502
+ * @param module - Module context with file info
1503
+ * @param variableDeclarations - Collection to push variable declarations to
1504
+ * @param classInstantiations - Collection to push class instantiations to
1505
+ * @param literals - Collection for literal tracking
1506
+ * @param variableAssignments - Collection for variable assignment tracking
1507
+ * @param varDeclCounterRef - Counter for unique variable declaration IDs
1508
+ * @param literalCounterRef - Counter for unique literal IDs
1509
+ * @param scopeTracker - Tracker for semantic ID generation
1510
+ * @param parentScopeVariables - Set to track variables for closure analysis
1511
+ * @param objectLiterals - Collection for object literal nodes (REG-328)
1512
+ * @param objectProperties - Collection for object property edges (REG-328)
1513
+ * @param objectLiteralCounterRef - Counter for unique object literal IDs (REG-328)
1514
+ */
1515
+ handleVariableDeclaration(varPath, parentScopeId, module, variableDeclarations, classInstantiations, literals, variableAssignments, varDeclCounterRef, literalCounterRef, scopeTracker, parentScopeVariables, objectLiterals, objectProperties, objectLiteralCounterRef) {
1516
+ const varNode = varPath.node;
1517
+ const isConst = varNode.kind === 'const';
1518
+ // Check if this is a loop variable (for...of or for...in)
1519
+ const parent = varPath.parent;
1520
+ const isLoopVariable = (t.isForOfStatement(parent) || t.isForInStatement(parent)) && parent.left === varNode;
1521
+ varNode.declarations.forEach(declarator => {
1522
+ const variables = this.extractVariableNamesFromPattern(declarator.id);
1523
+ const variablesWithIds = [];
1524
+ variables.forEach(varInfo => {
1525
+ const literalValue = declarator.init ? ExpressionEvaluator.extractLiteralValue(declarator.init) : null;
1526
+ const isLiteral = literalValue !== null;
1527
+ const isNewExpression = declarator.init && declarator.init.type === 'NewExpression';
1528
+ // Loop variables with const should be CONSTANT (they can't be reassigned in loop body)
1529
+ // Regular variables with const are CONSTANT only if initialized with literal or new expression
1530
+ const shouldBeConstant = isConst && (isLoopVariable || isLiteral || isNewExpression);
1531
+ const nodeType = shouldBeConstant ? 'CONSTANT' : 'VARIABLE';
1532
+ // Generate semantic ID (primary) or legacy ID (fallback)
1533
+ const legacyId = `${nodeType}#${varInfo.name}#${module.file}#${varInfo.loc.start.line}:${varInfo.loc.start.column}:${varDeclCounterRef.value++}`;
1534
+ const varId = scopeTracker
1535
+ ? computeSemanticId(nodeType, varInfo.name, scopeTracker.getContext())
1536
+ : legacyId;
1537
+ // Collect variable info with ID for destructuring tracking
1538
+ variablesWithIds.push({ ...varInfo, id: varId });
1539
+ parentScopeVariables.add({
1540
+ name: varInfo.name,
1541
+ id: varId,
1542
+ scopeId: parentScopeId
1543
+ });
1544
+ if (shouldBeConstant) {
1545
+ const constantData = {
1546
+ id: varId,
1547
+ type: 'CONSTANT',
1548
+ name: varInfo.name,
1549
+ file: module.file,
1550
+ line: varInfo.loc.start.line,
1551
+ parentScopeId
1552
+ };
1553
+ if (isLiteral) {
1554
+ constantData.value = literalValue;
1555
+ }
1556
+ variableDeclarations.push(constantData);
1557
+ const init = declarator.init;
1558
+ if (isNewExpression && t.isNewExpression(init) && t.isIdentifier(init.callee)) {
1559
+ const className = init.callee.name;
1560
+ classInstantiations.push({
1561
+ variableId: varId,
1562
+ variableName: varInfo.name,
1563
+ className: className,
1564
+ line: varInfo.loc.start.line,
1565
+ parentScopeId
905
1566
  });
906
- if (shouldBeConstant) {
907
- const constantData = {
908
- id: varId,
909
- type: 'CONSTANT',
910
- name: varInfo.name,
911
- file: module.file,
912
- line: varInfo.loc.start.line,
913
- parentScopeId
914
- };
915
- if (isLiteral) {
916
- constantData.value = literalValue;
917
- }
918
- variableDeclarations.push(constantData);
919
- const init = declarator.init;
920
- if (isNewExpression && t.isNewExpression(init) && t.isIdentifier(init.callee)) {
921
- const className = init.callee.name;
922
- classInstantiations.push({
923
- variableId: varId,
924
- variableName: varInfo.name,
925
- className: className,
926
- line: varInfo.loc.start.line,
927
- parentScopeId
928
- });
929
- }
930
- }
931
- else {
932
- variableDeclarations.push({
933
- id: varId,
934
- type: 'VARIABLE',
935
- name: varInfo.name,
1567
+ }
1568
+ }
1569
+ else {
1570
+ variableDeclarations.push({
1571
+ id: varId,
1572
+ type: 'VARIABLE',
1573
+ name: varInfo.name,
1574
+ file: module.file,
1575
+ line: varInfo.loc.start.line,
1576
+ parentScopeId
1577
+ });
1578
+ }
1579
+ });
1580
+ // Track assignments after all variables are created
1581
+ if (isLoopVariable) {
1582
+ // For loop variables, track assignment from the source collection (right side of for...of/for...in)
1583
+ const loopParent = parent;
1584
+ const sourceExpression = loopParent.right;
1585
+ if (t.isObjectPattern(declarator.id) || t.isArrayPattern(declarator.id)) {
1586
+ // Destructuring in loop: track each variable separately
1587
+ this.trackDestructuringAssignment(declarator.id, sourceExpression, variablesWithIds, module, variableAssignments);
1588
+ }
1589
+ else {
1590
+ // Simple loop variable: create DERIVES_FROM edges (not ASSIGNED_FROM)
1591
+ // Loop variables derive their values from the collection (semantic difference)
1592
+ variablesWithIds.forEach(varInfo => {
1593
+ if (t.isIdentifier(sourceExpression)) {
1594
+ variableAssignments.push({
1595
+ variableId: varInfo.id,
1596
+ sourceType: 'DERIVES_FROM_VARIABLE',
1597
+ sourceName: sourceExpression.name,
936
1598
  file: module.file,
937
- line: varInfo.loc.start.line,
938
- parentScopeId
1599
+ line: varInfo.loc.start.line
939
1600
  });
940
1601
  }
941
- if (declarator.init) {
942
- this.trackVariableAssignment(declarator.init, varId, varInfo.name, module, varInfo.loc.start.line, literals, variableAssignments, literalCounterRef);
1602
+ else {
1603
+ // Fallback to regular tracking for non-identifier expressions
1604
+ this.trackVariableAssignment(sourceExpression, varInfo.id, varInfo.name, module, varInfo.loc.start.line, literals, variableAssignments, literalCounterRef, objectLiterals, objectProperties, objectLiteralCounterRef);
943
1605
  }
944
1606
  });
1607
+ }
1608
+ }
1609
+ else if (declarator.init) {
1610
+ // Regular variable declaration with initializer
1611
+ if (t.isObjectPattern(declarator.id) || t.isArrayPattern(declarator.id)) {
1612
+ // Destructuring: use specialized tracking
1613
+ this.trackDestructuringAssignment(declarator.id, declarator.init, variablesWithIds, module, variableAssignments);
1614
+ }
1615
+ else {
1616
+ // Simple assignment: use existing tracking
1617
+ const varInfo = variablesWithIds[0];
1618
+ this.trackVariableAssignment(declarator.init, varInfo.id, varInfo.name, module, varInfo.loc.start.line, literals, variableAssignments, literalCounterRef, objectLiterals, objectProperties, objectLiteralCounterRef);
1619
+ }
1620
+ }
1621
+ });
1622
+ }
1623
+ createLoopScopeHandler(trackerScopeType, scopeType, loopType, parentScopeId, module, scopes, loops, scopeCounterRef, loopCounterRef, scopeTracker, scopeIdStack, controlFlowState) {
1624
+ return {
1625
+ enter: (path) => {
1626
+ const node = path.node;
1627
+ // Phase 6 (REG-267): Increment loop count for cyclomatic complexity
1628
+ if (controlFlowState) {
1629
+ controlFlowState.loopCount++;
1630
+ }
1631
+ // 1. Create LOOP node
1632
+ const loopCounter = loopCounterRef.value++;
1633
+ const legacyLoopId = `${module.file}:LOOP:${loopType}:${getLine(node)}:${loopCounter}`;
1634
+ const loopId = scopeTracker
1635
+ ? computeSemanticId('LOOP', loopType, scopeTracker.getContext(), { discriminator: loopCounter })
1636
+ : legacyLoopId;
1637
+ // 2. Extract iteration target for for-in/for-of
1638
+ let iteratesOverName;
1639
+ let iteratesOverLine;
1640
+ let iteratesOverColumn;
1641
+ if (loopType === 'for-in' || loopType === 'for-of') {
1642
+ const loopNode = node;
1643
+ if (t.isIdentifier(loopNode.right)) {
1644
+ iteratesOverName = loopNode.right.name;
1645
+ iteratesOverLine = getLine(loopNode.right);
1646
+ iteratesOverColumn = getColumn(loopNode.right);
1647
+ }
1648
+ else if (t.isMemberExpression(loopNode.right)) {
1649
+ iteratesOverName = this.memberExpressionToString(loopNode.right);
1650
+ iteratesOverLine = getLine(loopNode.right);
1651
+ iteratesOverColumn = getColumn(loopNode.right);
1652
+ }
1653
+ }
1654
+ // 2b. Extract init/test/update for classic for loops and test for while/do-while (REG-282)
1655
+ let initVariableName;
1656
+ let initLine;
1657
+ let testExpressionId;
1658
+ let testExpressionType;
1659
+ let testLine;
1660
+ let testColumn;
1661
+ let updateExpressionId;
1662
+ let updateExpressionType;
1663
+ let updateLine;
1664
+ let updateColumn;
1665
+ if (loopType === 'for') {
1666
+ const forNode = node;
1667
+ // Extract init: let i = 0
1668
+ if (forNode.init) {
1669
+ initLine = getLine(forNode.init);
1670
+ if (t.isVariableDeclaration(forNode.init)) {
1671
+ // Get name of first declared variable
1672
+ const firstDeclarator = forNode.init.declarations[0];
1673
+ if (t.isIdentifier(firstDeclarator.id)) {
1674
+ initVariableName = firstDeclarator.id.name;
1675
+ }
1676
+ }
1677
+ }
1678
+ // Extract test: i < 10
1679
+ if (forNode.test) {
1680
+ testLine = getLine(forNode.test);
1681
+ testColumn = getColumn(forNode.test);
1682
+ testExpressionType = forNode.test.type;
1683
+ testExpressionId = ExpressionNode.generateId(forNode.test.type, module.file, testLine, testColumn);
1684
+ }
1685
+ // Extract update: i++
1686
+ if (forNode.update) {
1687
+ updateLine = getLine(forNode.update);
1688
+ updateColumn = getColumn(forNode.update);
1689
+ updateExpressionType = forNode.update.type;
1690
+ updateExpressionId = ExpressionNode.generateId(forNode.update.type, module.file, updateLine, updateColumn);
1691
+ }
1692
+ }
1693
+ // Extract test condition for while and do-while loops
1694
+ if (loopType === 'while' || loopType === 'do-while') {
1695
+ const condLoop = node;
1696
+ if (condLoop.test) {
1697
+ testLine = getLine(condLoop.test);
1698
+ testColumn = getColumn(condLoop.test);
1699
+ testExpressionType = condLoop.test.type;
1700
+ testExpressionId = ExpressionNode.generateId(condLoop.test.type, module.file, testLine, testColumn);
1701
+ }
1702
+ }
1703
+ // Extract async flag for for-await-of (REG-284)
1704
+ let isAsync;
1705
+ if (loopType === 'for-of') {
1706
+ const forOfNode = node;
1707
+ isAsync = forOfNode.await === true ? true : undefined;
1708
+ }
1709
+ // 3. Determine actual parent - use stack for nested loops, otherwise original parentScopeId
1710
+ const actualParentScopeId = (scopeIdStack && scopeIdStack.length > 0)
1711
+ ? scopeIdStack[scopeIdStack.length - 1]
1712
+ : parentScopeId;
1713
+ // 3.5. Extract condition expression for while/do-while/for loops (REG-280)
1714
+ // Note: for-in and for-of don't have test expressions (they use ITERATES_OVER instead)
1715
+ let conditionExpressionId;
1716
+ let conditionExpressionType;
1717
+ let conditionLine;
1718
+ let conditionColumn;
1719
+ if (loopType === 'while' || loopType === 'do-while') {
1720
+ const testNode = node.test;
1721
+ if (testNode) {
1722
+ const condResult = this.extractDiscriminantExpression(testNode, module);
1723
+ conditionExpressionId = condResult.id;
1724
+ conditionExpressionType = condResult.expressionType;
1725
+ conditionLine = condResult.line;
1726
+ conditionColumn = condResult.column;
1727
+ }
1728
+ }
1729
+ else if (loopType === 'for') {
1730
+ const forNode = node;
1731
+ // for loop test may be null (infinite loop: for(;;))
1732
+ if (forNode.test) {
1733
+ const condResult = this.extractDiscriminantExpression(forNode.test, module);
1734
+ conditionExpressionId = condResult.id;
1735
+ conditionExpressionType = condResult.expressionType;
1736
+ conditionLine = condResult.line;
1737
+ conditionColumn = condResult.column;
1738
+ }
1739
+ }
1740
+ // 4. Push LOOP info
1741
+ loops.push({
1742
+ id: loopId,
1743
+ semanticId: loopId,
1744
+ type: 'LOOP',
1745
+ loopType,
1746
+ file: module.file,
1747
+ line: getLine(node),
1748
+ column: getColumn(node),
1749
+ parentScopeId: actualParentScopeId,
1750
+ iteratesOverName,
1751
+ iteratesOverLine,
1752
+ iteratesOverColumn,
1753
+ conditionExpressionId,
1754
+ conditionExpressionType,
1755
+ conditionLine,
1756
+ conditionColumn,
1757
+ // REG-282: init/test/update for classic for loops
1758
+ initVariableName,
1759
+ initLine,
1760
+ testExpressionId,
1761
+ testExpressionType,
1762
+ testLine,
1763
+ testColumn,
1764
+ updateExpressionId,
1765
+ updateExpressionType,
1766
+ updateLine,
1767
+ updateColumn,
1768
+ // REG-284: async flag for for-await-of
1769
+ async: isAsync
945
1770
  });
946
- },
947
- ForStatement: (forPath) => {
948
- const forNode = forPath.node;
949
- const scopeId = `SCOPE#for-loop#${module.file}#${forNode.loc.start.line}:${scopeCounterRef.value++}`;
950
- const semanticId = this.generateSemanticId('for-loop', scopeCtx);
1771
+ // 5. Create body SCOPE (backward compatibility)
1772
+ const scopeId = `SCOPE#${scopeType}#${module.file}#${getLine(node)}:${scopeCounterRef.value++}`;
1773
+ const semanticId = this.generateSemanticId(scopeType, scopeTracker);
951
1774
  scopes.push({
952
1775
  id: scopeId,
953
1776
  type: 'SCOPE',
954
- scopeType: 'for-loop',
1777
+ scopeType,
955
1778
  semanticId,
956
1779
  file: module.file,
957
- line: forNode.loc.start.line,
958
- parentScopeId
1780
+ line: getLine(node),
1781
+ parentScopeId: loopId // Parent is LOOP, not original parentScopeId
959
1782
  });
1783
+ // 6. Push body SCOPE to scopeIdStack (for CONTAINS edges to nested items)
1784
+ // The body scope is the container for nested loops, not the LOOP itself
1785
+ if (scopeIdStack) {
1786
+ scopeIdStack.push(scopeId);
1787
+ }
1788
+ // Enter scope for semantic ID generation
1789
+ if (scopeTracker) {
1790
+ scopeTracker.enterCountedScope(trackerScopeType);
1791
+ }
960
1792
  },
961
- ForInStatement: (forPath) => {
962
- const forNode = forPath.node;
963
- const scopeId = `SCOPE#for-in-loop#${module.file}#${forNode.loc.start.line}:${scopeCounterRef.value++}`;
964
- const semanticId = this.generateSemanticId('for-in-loop', scopeCtx);
965
- scopes.push({
966
- id: scopeId,
967
- type: 'SCOPE',
968
- scopeType: 'for-in-loop',
969
- semanticId,
1793
+ exit: () => {
1794
+ // Pop loop scope from stack
1795
+ if (scopeIdStack) {
1796
+ scopeIdStack.pop();
1797
+ }
1798
+ // Exit scope
1799
+ if (scopeTracker) {
1800
+ scopeTracker.exitScope();
1801
+ }
1802
+ }
1803
+ };
1804
+ }
1805
+ /**
1806
+ * Factory method to create TryStatement handler.
1807
+ * Creates TRY_BLOCK, CATCH_BLOCK, FINALLY_BLOCK nodes and body SCOPEs.
1808
+ * Does NOT use skip() - allows normal traversal for CallExpression/NewExpression visitors.
1809
+ *
1810
+ * Phase 4 (REG-267): Creates control flow nodes with HAS_CATCH and HAS_FINALLY edges.
1811
+ *
1812
+ * @param parentScopeId - Parent scope ID for the scope nodes
1813
+ * @param module - Module context
1814
+ * @param scopes - Collection to push scope nodes to
1815
+ * @param tryBlocks - Collection to push TRY_BLOCK nodes to
1816
+ * @param catchBlocks - Collection to push CATCH_BLOCK nodes to
1817
+ * @param finallyBlocks - Collection to push FINALLY_BLOCK nodes to
1818
+ * @param scopeCounterRef - Counter for unique scope IDs
1819
+ * @param tryBlockCounterRef - Counter for unique TRY_BLOCK IDs
1820
+ * @param catchBlockCounterRef - Counter for unique CATCH_BLOCK IDs
1821
+ * @param finallyBlockCounterRef - Counter for unique FINALLY_BLOCK IDs
1822
+ * @param scopeTracker - Tracker for semantic ID generation
1823
+ * @param tryScopeMap - Map to track try/catch/finally scope transitions
1824
+ * @param scopeIdStack - Stack for tracking current scope ID for CONTAINS edges
1825
+ */
1826
+ createTryStatementHandler(parentScopeId, module, scopes, tryBlocks, catchBlocks, finallyBlocks, scopeCounterRef, tryBlockCounterRef, catchBlockCounterRef, finallyBlockCounterRef, scopeTracker, tryScopeMap, scopeIdStack, controlFlowState) {
1827
+ return {
1828
+ enter: (tryPath) => {
1829
+ const tryNode = tryPath.node;
1830
+ // Phase 6 (REG-267): Mark that this function has try/catch
1831
+ if (controlFlowState) {
1832
+ controlFlowState.hasTryCatch = true;
1833
+ }
1834
+ // Determine actual parent - use stack for nested structures, otherwise original parentScopeId
1835
+ const actualParentScopeId = (scopeIdStack && scopeIdStack.length > 0)
1836
+ ? scopeIdStack[scopeIdStack.length - 1]
1837
+ : parentScopeId;
1838
+ // 1. Create TRY_BLOCK node
1839
+ const tryBlockCounter = tryBlockCounterRef.value++;
1840
+ const legacyTryBlockId = `${module.file}:TRY_BLOCK:${getLine(tryNode)}:${tryBlockCounter}`;
1841
+ const tryBlockId = scopeTracker
1842
+ ? computeSemanticId('TRY_BLOCK', 'try', scopeTracker.getContext(), { discriminator: tryBlockCounter })
1843
+ : legacyTryBlockId;
1844
+ tryBlocks.push({
1845
+ id: tryBlockId,
1846
+ semanticId: tryBlockId,
1847
+ type: 'TRY_BLOCK',
970
1848
  file: module.file,
971
- line: forNode.loc.start.line,
972
- parentScopeId
1849
+ line: getLine(tryNode),
1850
+ column: getColumn(tryNode),
1851
+ parentScopeId: actualParentScopeId
973
1852
  });
974
- },
975
- ForOfStatement: (forPath) => {
976
- const forNode = forPath.node;
977
- const scopeId = `SCOPE#for-of-loop#${module.file}#${forNode.loc.start.line}:${scopeCounterRef.value++}`;
978
- const semanticId = this.generateSemanticId('for-of-loop', scopeCtx);
979
- scopes.push({
980
- id: scopeId,
981
- type: 'SCOPE',
982
- scopeType: 'for-of-loop',
983
- semanticId,
984
- file: module.file,
985
- line: forNode.loc.start.line,
986
- parentScopeId
987
- });
988
- },
989
- WhileStatement: (whilePath) => {
990
- const whileNode = whilePath.node;
991
- const scopeId = `SCOPE#while-loop#${module.file}#${whileNode.loc.start.line}:${scopeCounterRef.value++}`;
992
- const semanticId = this.generateSemanticId('while-loop', scopeCtx);
993
- scopes.push({
994
- id: scopeId,
995
- type: 'SCOPE',
996
- scopeType: 'while-loop',
997
- semanticId,
998
- file: module.file,
999
- line: whileNode.loc.start.line,
1000
- parentScopeId
1001
- });
1002
- },
1003
- DoWhileStatement: (doPath) => {
1004
- const doNode = doPath.node;
1005
- const scopeId = `SCOPE#do-while-loop#${module.file}#${doNode.loc.start.line}:${scopeCounterRef.value++}`;
1006
- const semanticId = this.generateSemanticId('do-while-loop', scopeCtx);
1007
- scopes.push({
1008
- id: scopeId,
1009
- type: 'SCOPE',
1010
- scopeType: 'do-while-loop',
1011
- semanticId,
1012
- file: module.file,
1013
- line: doNode.loc.start.line,
1014
- parentScopeId
1015
- });
1016
- },
1017
- TryStatement: (tryPath) => {
1018
- const tryNode = tryPath.node;
1019
- const tryScopeId = `SCOPE#try-block#${module.file}#${tryNode.loc.start.line}:${scopeCounterRef.value++}`;
1020
- const trySemanticId = this.generateSemanticId('try-block', scopeCtx);
1853
+ // 2. Create try-body SCOPE (backward compatibility)
1854
+ // Parent is now TRY_BLOCK, not original parentScopeId
1855
+ const tryScopeId = `SCOPE#try-block#${module.file}#${getLine(tryNode)}:${scopeCounterRef.value++}`;
1856
+ const trySemanticId = this.generateSemanticId('try-block', scopeTracker);
1021
1857
  scopes.push({
1022
1858
  id: tryScopeId,
1023
1859
  type: 'SCOPE',
1024
1860
  scopeType: 'try-block',
1025
1861
  semanticId: trySemanticId,
1026
1862
  file: module.file,
1027
- line: tryNode.loc.start.line,
1028
- parentScopeId
1863
+ line: getLine(tryNode),
1864
+ parentScopeId: tryBlockId // Parent is TRY_BLOCK
1029
1865
  });
1866
+ // 3. Create CATCH_BLOCK and catch-body SCOPE if handler exists
1867
+ let catchBlockId = null;
1868
+ let catchScopeId = null;
1030
1869
  if (tryNode.handler) {
1031
- const catchBlock = tryNode.handler;
1032
- const catchScopeId = `SCOPE#catch-block#${module.file}#${catchBlock.loc.start.line}:${scopeCounterRef.value++}`;
1033
- const catchSemanticId = this.generateSemanticId('catch-block', scopeCtx);
1870
+ const catchClause = tryNode.handler;
1871
+ const catchBlockCounter = catchBlockCounterRef.value++;
1872
+ const legacyCatchBlockId = `${module.file}:CATCH_BLOCK:${getLine(catchClause)}:${catchBlockCounter}`;
1873
+ catchBlockId = scopeTracker
1874
+ ? computeSemanticId('CATCH_BLOCK', 'catch', scopeTracker.getContext(), { discriminator: catchBlockCounter })
1875
+ : legacyCatchBlockId;
1876
+ // Extract parameter name if present
1877
+ let parameterName;
1878
+ if (catchClause.param && t.isIdentifier(catchClause.param)) {
1879
+ parameterName = catchClause.param.name;
1880
+ }
1881
+ catchBlocks.push({
1882
+ id: catchBlockId,
1883
+ semanticId: catchBlockId,
1884
+ type: 'CATCH_BLOCK',
1885
+ file: module.file,
1886
+ line: getLine(catchClause),
1887
+ column: getColumn(catchClause),
1888
+ parentScopeId,
1889
+ parentTryBlockId: tryBlockId,
1890
+ parameterName
1891
+ });
1892
+ // Create catch-body SCOPE (backward compatibility)
1893
+ catchScopeId = `SCOPE#catch-block#${module.file}#${getLine(catchClause)}:${scopeCounterRef.value++}`;
1894
+ const catchSemanticId = this.generateSemanticId('catch-block', scopeTracker);
1034
1895
  scopes.push({
1035
1896
  id: catchScopeId,
1036
1897
  type: 'SCOPE',
1037
1898
  scopeType: 'catch-block',
1038
1899
  semanticId: catchSemanticId,
1039
1900
  file: module.file,
1040
- line: catchBlock.loc.start.line,
1041
- parentScopeId
1901
+ line: getLine(catchClause),
1902
+ parentScopeId: catchBlockId // Parent is CATCH_BLOCK
1042
1903
  });
1043
- if (catchBlock.param) {
1044
- const errorVarInfo = this.extractVariableNamesFromPattern(catchBlock.param);
1045
- errorVarInfo.forEach(varInfo => {
1046
- const varId = `VARIABLE#${varInfo.name}#${module.file}#${varInfo.loc.start.line}:${varInfo.loc.start.column}:${varDeclCounterRef.value++}`;
1047
- variableDeclarations.push({
1048
- id: varId,
1049
- type: 'VARIABLE',
1050
- name: varInfo.name,
1051
- file: module.file,
1052
- line: varInfo.loc.start.line,
1053
- parentScopeId: catchScopeId
1054
- });
1904
+ }
1905
+ // 4. Create FINALLY_BLOCK and finally-body SCOPE if finalizer exists
1906
+ let finallyBlockId = null;
1907
+ let finallyScopeId = null;
1908
+ if (tryNode.finalizer) {
1909
+ const finallyBlockCounter = finallyBlockCounterRef.value++;
1910
+ const legacyFinallyBlockId = `${module.file}:FINALLY_BLOCK:${getLine(tryNode.finalizer)}:${finallyBlockCounter}`;
1911
+ finallyBlockId = scopeTracker
1912
+ ? computeSemanticId('FINALLY_BLOCK', 'finally', scopeTracker.getContext(), { discriminator: finallyBlockCounter })
1913
+ : legacyFinallyBlockId;
1914
+ finallyBlocks.push({
1915
+ id: finallyBlockId,
1916
+ semanticId: finallyBlockId,
1917
+ type: 'FINALLY_BLOCK',
1918
+ file: module.file,
1919
+ line: getLine(tryNode.finalizer),
1920
+ column: getColumn(tryNode.finalizer),
1921
+ parentScopeId,
1922
+ parentTryBlockId: tryBlockId
1923
+ });
1924
+ // Create finally-body SCOPE (backward compatibility)
1925
+ finallyScopeId = `SCOPE#finally-block#${module.file}#${getLine(tryNode.finalizer)}:${scopeCounterRef.value++}`;
1926
+ const finallySemanticId = this.generateSemanticId('finally-block', scopeTracker);
1927
+ scopes.push({
1928
+ id: finallyScopeId,
1929
+ type: 'SCOPE',
1930
+ scopeType: 'finally-block',
1931
+ semanticId: finallySemanticId,
1932
+ file: module.file,
1933
+ line: getLine(tryNode.finalizer),
1934
+ parentScopeId: finallyBlockId // Parent is FINALLY_BLOCK
1935
+ });
1936
+ }
1937
+ // 5. Push try scope onto stack for CONTAINS edges
1938
+ if (scopeIdStack) {
1939
+ scopeIdStack.push(tryScopeId);
1940
+ }
1941
+ // Enter try scope for semantic ID generation
1942
+ if (scopeTracker) {
1943
+ scopeTracker.enterCountedScope('try');
1944
+ }
1945
+ // 6. Store scope info for catch/finally transitions
1946
+ tryScopeMap.set(tryNode, {
1947
+ tryScopeId,
1948
+ catchScopeId,
1949
+ finallyScopeId,
1950
+ currentBlock: 'try',
1951
+ tryBlockId,
1952
+ catchBlockId,
1953
+ finallyBlockId
1954
+ });
1955
+ },
1956
+ exit: (tryPath) => {
1957
+ const tryNode = tryPath.node;
1958
+ const scopeInfo = tryScopeMap.get(tryNode);
1959
+ // Pop the current scope from stack (could be try, catch, or finally)
1960
+ if (scopeIdStack) {
1961
+ scopeIdStack.pop();
1962
+ }
1963
+ // Exit the current scope
1964
+ if (scopeTracker) {
1965
+ scopeTracker.exitScope();
1966
+ }
1967
+ // Clean up
1968
+ tryScopeMap.delete(tryNode);
1969
+ }
1970
+ };
1971
+ }
1972
+ /**
1973
+ * Factory method to create CatchClause handler.
1974
+ * Handles scope transition from try to catch and processes catch parameter.
1975
+ *
1976
+ * @param module - Module context
1977
+ * @param variableDeclarations - Collection to push variable declarations to
1978
+ * @param varDeclCounterRef - Counter for unique variable declaration IDs
1979
+ * @param scopeTracker - Tracker for semantic ID generation
1980
+ * @param tryScopeMap - Map to track try/catch/finally scope transitions
1981
+ * @param scopeIdStack - Stack for tracking current scope ID for CONTAINS edges
1982
+ */
1983
+ createCatchClauseHandler(module, variableDeclarations, varDeclCounterRef, scopeTracker, tryScopeMap, scopeIdStack) {
1984
+ return {
1985
+ enter: (catchPath) => {
1986
+ const catchNode = catchPath.node;
1987
+ const parent = catchPath.parent;
1988
+ if (!t.isTryStatement(parent))
1989
+ return;
1990
+ const scopeInfo = tryScopeMap.get(parent);
1991
+ if (!scopeInfo || !scopeInfo.catchScopeId)
1992
+ return;
1993
+ // Transition from try scope to catch scope
1994
+ if (scopeInfo.currentBlock === 'try') {
1995
+ // Pop try scope, push catch scope
1996
+ if (scopeIdStack) {
1997
+ scopeIdStack.pop();
1998
+ scopeIdStack.push(scopeInfo.catchScopeId);
1999
+ }
2000
+ // Exit try scope, enter catch scope for semantic ID
2001
+ if (scopeTracker) {
2002
+ scopeTracker.exitScope();
2003
+ scopeTracker.enterCountedScope('catch');
2004
+ }
2005
+ scopeInfo.currentBlock = 'catch';
2006
+ }
2007
+ // Handle catch parameter (e.g., catch (e) or catch ({ message }))
2008
+ if (catchNode.param) {
2009
+ const errorVarInfo = this.extractVariableNamesFromPattern(catchNode.param);
2010
+ errorVarInfo.forEach(varInfo => {
2011
+ const legacyId = `VARIABLE#${varInfo.name}#${module.file}#${varInfo.loc.start.line}:${varInfo.loc.start.column}:${varDeclCounterRef.value++}`;
2012
+ const varId = scopeTracker
2013
+ ? computeSemanticId('VARIABLE', varInfo.name, scopeTracker.getContext())
2014
+ : legacyId;
2015
+ variableDeclarations.push({
2016
+ id: varId,
2017
+ type: 'VARIABLE',
2018
+ name: varInfo.name,
2019
+ file: module.file,
2020
+ line: varInfo.loc.start.line,
2021
+ parentScopeId: scopeInfo.catchScopeId
1055
2022
  });
2023
+ });
2024
+ }
2025
+ }
2026
+ };
2027
+ }
2028
+ /**
2029
+ * Handles SwitchStatement nodes.
2030
+ * Creates BRANCH node for switch, CASE nodes for each case clause,
2031
+ * and EXPRESSION node for discriminant.
2032
+ *
2033
+ * @param switchPath - The NodePath for the SwitchStatement
2034
+ * @param parentScopeId - Parent scope ID
2035
+ * @param module - Module context
2036
+ * @param collections - AST collections
2037
+ * @param scopeTracker - Tracker for semantic ID generation
2038
+ */
2039
+ handleSwitchStatement(switchPath, parentScopeId, module, collections, scopeTracker, controlFlowState) {
2040
+ const switchNode = switchPath.node;
2041
+ // Phase 6 (REG-267): Count branch and non-default cases for cyclomatic complexity
2042
+ if (controlFlowState) {
2043
+ controlFlowState.branchCount++; // switch itself is a branch
2044
+ // Count non-default cases
2045
+ for (const caseNode of switchNode.cases) {
2046
+ if (caseNode.test !== null) { // Not default case
2047
+ controlFlowState.caseCount++;
2048
+ }
2049
+ }
2050
+ }
2051
+ // Initialize collections if not exist
2052
+ if (!collections.branches) {
2053
+ collections.branches = [];
2054
+ }
2055
+ if (!collections.cases) {
2056
+ collections.cases = [];
2057
+ }
2058
+ if (!collections.branchCounterRef) {
2059
+ collections.branchCounterRef = { value: 0 };
2060
+ }
2061
+ if (!collections.caseCounterRef) {
2062
+ collections.caseCounterRef = { value: 0 };
2063
+ }
2064
+ const branches = collections.branches;
2065
+ const cases = collections.cases;
2066
+ const branchCounterRef = collections.branchCounterRef;
2067
+ const caseCounterRef = collections.caseCounterRef;
2068
+ // Create BRANCH node
2069
+ const branchCounter = branchCounterRef.value++;
2070
+ const legacyBranchId = `${module.file}:BRANCH:switch:${getLine(switchNode)}:${branchCounter}`;
2071
+ const branchId = scopeTracker
2072
+ ? computeSemanticId('BRANCH', 'switch', scopeTracker.getContext(), { discriminator: branchCounter })
2073
+ : legacyBranchId;
2074
+ // Handle discriminant expression - store metadata directly (Linus improvement)
2075
+ let discriminantExpressionId;
2076
+ let discriminantExpressionType;
2077
+ let discriminantLine;
2078
+ let discriminantColumn;
2079
+ if (switchNode.discriminant) {
2080
+ const discResult = this.extractDiscriminantExpression(switchNode.discriminant, module);
2081
+ discriminantExpressionId = discResult.id;
2082
+ discriminantExpressionType = discResult.expressionType;
2083
+ discriminantLine = discResult.line;
2084
+ discriminantColumn = discResult.column;
2085
+ }
2086
+ branches.push({
2087
+ id: branchId,
2088
+ semanticId: branchId,
2089
+ type: 'BRANCH',
2090
+ branchType: 'switch',
2091
+ file: module.file,
2092
+ line: getLine(switchNode),
2093
+ parentScopeId,
2094
+ discriminantExpressionId,
2095
+ discriminantExpressionType,
2096
+ discriminantLine,
2097
+ discriminantColumn
2098
+ });
2099
+ // Process each case clause
2100
+ for (let i = 0; i < switchNode.cases.length; i++) {
2101
+ const caseNode = switchNode.cases[i];
2102
+ const isDefault = caseNode.test === null;
2103
+ const isEmpty = caseNode.consequent.length === 0;
2104
+ // Detect fall-through: no break/return/throw at end of consequent
2105
+ const fallsThrough = isEmpty || !this.caseTerminates(caseNode);
2106
+ // Extract case value
2107
+ const value = isDefault ? null : this.extractCaseValue(caseNode.test ?? null);
2108
+ const caseCounter = caseCounterRef.value++;
2109
+ const valueName = isDefault ? 'default' : String(value);
2110
+ const legacyCaseId = `${module.file}:CASE:${valueName}:${getLine(caseNode)}:${caseCounter}`;
2111
+ const caseId = scopeTracker
2112
+ ? computeSemanticId('CASE', valueName, scopeTracker.getContext(), { discriminator: caseCounter })
2113
+ : legacyCaseId;
2114
+ cases.push({
2115
+ id: caseId,
2116
+ semanticId: caseId,
2117
+ type: 'CASE',
2118
+ value,
2119
+ isDefault,
2120
+ fallsThrough,
2121
+ isEmpty,
2122
+ file: module.file,
2123
+ line: getLine(caseNode),
2124
+ parentBranchId: branchId
2125
+ });
2126
+ }
2127
+ }
2128
+ /**
2129
+ * Extract EXPRESSION node ID and metadata for switch discriminant
2130
+ */
2131
+ extractDiscriminantExpression(discriminant, module) {
2132
+ const line = getLine(discriminant);
2133
+ const column = getColumn(discriminant);
2134
+ if (t.isIdentifier(discriminant)) {
2135
+ // Simple identifier: switch(x) - create EXPRESSION node
2136
+ return {
2137
+ id: ExpressionNode.generateId('Identifier', module.file, line, column),
2138
+ expressionType: 'Identifier',
2139
+ line,
2140
+ column
2141
+ };
2142
+ }
2143
+ else if (t.isMemberExpression(discriminant)) {
2144
+ // Member expression: switch(action.type)
2145
+ return {
2146
+ id: ExpressionNode.generateId('MemberExpression', module.file, line, column),
2147
+ expressionType: 'MemberExpression',
2148
+ line,
2149
+ column
2150
+ };
2151
+ }
2152
+ else if (t.isCallExpression(discriminant)) {
2153
+ // Call expression: switch(getType())
2154
+ const callee = t.isIdentifier(discriminant.callee) ? discriminant.callee.name : '<complex>';
2155
+ // Return CALL node ID instead of EXPRESSION (reuse existing call tracking)
2156
+ return {
2157
+ id: `${module.file}:CALL:${callee}:${line}:${column}`,
2158
+ expressionType: 'CallExpression',
2159
+ line,
2160
+ column
2161
+ };
2162
+ }
2163
+ // Default: create generic EXPRESSION
2164
+ return {
2165
+ id: ExpressionNode.generateId(discriminant.type, module.file, line, column),
2166
+ expressionType: discriminant.type,
2167
+ line,
2168
+ column
2169
+ };
2170
+ }
2171
+ /**
2172
+ * Extract case test value as a primitive
2173
+ */
2174
+ extractCaseValue(test) {
2175
+ if (!test)
2176
+ return null;
2177
+ if (t.isStringLiteral(test)) {
2178
+ return test.value;
2179
+ }
2180
+ else if (t.isNumericLiteral(test)) {
2181
+ return test.value;
2182
+ }
2183
+ else if (t.isBooleanLiteral(test)) {
2184
+ return test.value;
2185
+ }
2186
+ else if (t.isNullLiteral(test)) {
2187
+ return null;
2188
+ }
2189
+ else if (t.isIdentifier(test)) {
2190
+ // Constant reference: case CONSTANTS.ADD
2191
+ return test.name;
2192
+ }
2193
+ else if (t.isMemberExpression(test)) {
2194
+ // Member expression: case Action.ADD
2195
+ return this.memberExpressionToString(test);
2196
+ }
2197
+ return '<complex>';
2198
+ }
2199
+ /**
2200
+ * Check if case clause terminates (has break, return, throw)
2201
+ */
2202
+ caseTerminates(caseNode) {
2203
+ const statements = caseNode.consequent;
2204
+ if (statements.length === 0)
2205
+ return false;
2206
+ // Check last statement (or any statement for early returns)
2207
+ for (const stmt of statements) {
2208
+ if (t.isBreakStatement(stmt))
2209
+ return true;
2210
+ if (t.isReturnStatement(stmt))
2211
+ return true;
2212
+ if (t.isThrowStatement(stmt))
2213
+ return true;
2214
+ if (t.isContinueStatement(stmt))
2215
+ return true; // In switch inside loop
2216
+ // Check for nested blocks (if last statement is block, check inside)
2217
+ if (t.isBlockStatement(stmt)) {
2218
+ const lastInBlock = stmt.body[stmt.body.length - 1];
2219
+ if (lastInBlock && (t.isBreakStatement(lastInBlock) ||
2220
+ t.isReturnStatement(lastInBlock) ||
2221
+ t.isThrowStatement(lastInBlock))) {
2222
+ return true;
2223
+ }
2224
+ }
2225
+ // Check for if-else where both branches terminate
2226
+ if (t.isIfStatement(stmt) && stmt.alternate) {
2227
+ const ifTerminates = this.blockTerminates(stmt.consequent);
2228
+ const elseTerminates = this.blockTerminates(stmt.alternate);
2229
+ if (ifTerminates && elseTerminates)
2230
+ return true;
2231
+ }
2232
+ }
2233
+ return false;
2234
+ }
2235
+ /**
2236
+ * Check if a block/statement terminates
2237
+ */
2238
+ blockTerminates(node) {
2239
+ if (t.isBreakStatement(node))
2240
+ return true;
2241
+ if (t.isReturnStatement(node))
2242
+ return true;
2243
+ if (t.isThrowStatement(node))
2244
+ return true;
2245
+ if (t.isBlockStatement(node)) {
2246
+ const last = node.body[node.body.length - 1];
2247
+ return last ? this.blockTerminates(last) : false;
2248
+ }
2249
+ return false;
2250
+ }
2251
+ /**
2252
+ * Count logical operators (&& and ||) in a condition expression.
2253
+ * Used for cyclomatic complexity calculation (Phase 6 REG-267).
2254
+ *
2255
+ * @param node - The condition expression to analyze
2256
+ * @returns Number of logical operators found
2257
+ */
2258
+ countLogicalOperators(node) {
2259
+ let count = 0;
2260
+ const traverse = (expr) => {
2261
+ if (t.isLogicalExpression(expr)) {
2262
+ // Count && and || operators
2263
+ if (expr.operator === '&&' || expr.operator === '||') {
2264
+ count++;
2265
+ }
2266
+ traverse(expr.left);
2267
+ traverse(expr.right);
2268
+ }
2269
+ else if (t.isConditionalExpression(expr)) {
2270
+ // Handle ternary conditions: test ? consequent : alternate
2271
+ traverse(expr.test);
2272
+ traverse(expr.consequent);
2273
+ traverse(expr.alternate);
2274
+ }
2275
+ else if (t.isUnaryExpression(expr)) {
2276
+ traverse(expr.argument);
2277
+ }
2278
+ else if (t.isBinaryExpression(expr)) {
2279
+ traverse(expr.left);
2280
+ traverse(expr.right);
2281
+ }
2282
+ else if (t.isSequenceExpression(expr)) {
2283
+ for (const e of expr.expressions) {
2284
+ traverse(e);
2285
+ }
2286
+ }
2287
+ else if (t.isParenthesizedExpression(expr)) {
2288
+ traverse(expr.expression);
2289
+ }
2290
+ };
2291
+ traverse(node);
2292
+ return count;
2293
+ }
2294
+ /**
2295
+ * Convert MemberExpression to string representation
2296
+ */
2297
+ memberExpressionToString(expr) {
2298
+ const parts = [];
2299
+ let current = expr;
2300
+ while (t.isMemberExpression(current)) {
2301
+ if (t.isIdentifier(current.property)) {
2302
+ parts.unshift(current.property.name);
2303
+ }
2304
+ else {
2305
+ parts.unshift('<computed>');
2306
+ }
2307
+ current = current.object;
2308
+ }
2309
+ if (t.isIdentifier(current)) {
2310
+ parts.unshift(current.name);
2311
+ }
2312
+ return parts.join('.');
2313
+ }
2314
+ /**
2315
+ * Factory method to create IfStatement handler.
2316
+ * Creates BRANCH node for if statement and SCOPE nodes for if/else bodies.
2317
+ * Tracks if/else scope transitions via ifElseScopeMap.
2318
+ *
2319
+ * Phase 3 (REG-267): Creates BRANCH node with branchType='if' and
2320
+ * HAS_CONSEQUENT/HAS_ALTERNATE edges to body SCOPEs.
2321
+ *
2322
+ * @param parentScopeId - Parent scope ID for the scope nodes
2323
+ * @param module - Module context
2324
+ * @param scopes - Collection to push scope nodes to
2325
+ * @param branches - Collection to push BRANCH nodes to
2326
+ * @param ifScopeCounterRef - Counter for unique if scope IDs
2327
+ * @param branchCounterRef - Counter for unique BRANCH IDs
2328
+ * @param scopeTracker - Tracker for semantic ID generation
2329
+ * @param sourceCode - Source code for extracting condition text
2330
+ * @param ifElseScopeMap - Map to track if/else scope transitions
2331
+ * @param scopeIdStack - Stack for tracking current scope ID for CONTAINS edges
2332
+ */
2333
+ createIfStatementHandler(parentScopeId, module, scopes, branches, ifScopeCounterRef, branchCounterRef, scopeTracker, sourceCode, ifElseScopeMap, scopeIdStack, controlFlowState, countLogicalOperators) {
2334
+ return {
2335
+ enter: (ifPath) => {
2336
+ const ifNode = ifPath.node;
2337
+ const condition = sourceCode.substring(ifNode.test.start, ifNode.test.end) || 'condition';
2338
+ // Phase 6 (REG-267): Increment branch count and count logical operators
2339
+ if (controlFlowState) {
2340
+ controlFlowState.branchCount++;
2341
+ if (countLogicalOperators) {
2342
+ controlFlowState.logicalOpCount += countLogicalOperators(ifNode.test);
2343
+ }
2344
+ }
2345
+ // Check if this if-statement is an else-if (alternate of parent IfStatement)
2346
+ const isElseIf = t.isIfStatement(ifPath.parent) && ifPath.parentKey === 'alternate';
2347
+ // Determine actual parent scope
2348
+ let actualParentScopeId;
2349
+ if (isElseIf) {
2350
+ // For else-if, parent should be the outer BRANCH (stored in ifElseScopeMap)
2351
+ const parentIfInfo = ifElseScopeMap.get(ifPath.parent);
2352
+ if (parentIfInfo) {
2353
+ actualParentScopeId = parentIfInfo.branchId;
2354
+ }
2355
+ else {
2356
+ // Fallback to stack
2357
+ actualParentScopeId = (scopeIdStack && scopeIdStack.length > 0)
2358
+ ? scopeIdStack[scopeIdStack.length - 1]
2359
+ : parentScopeId;
2360
+ }
2361
+ }
2362
+ else {
2363
+ // For regular if statements, use stack or original parentScopeId
2364
+ actualParentScopeId = (scopeIdStack && scopeIdStack.length > 0)
2365
+ ? scopeIdStack[scopeIdStack.length - 1]
2366
+ : parentScopeId;
2367
+ }
2368
+ // 1. Create BRANCH node for if statement
2369
+ const branchCounter = branchCounterRef.value++;
2370
+ const legacyBranchId = `${module.file}:BRANCH:if:${getLine(ifNode)}:${branchCounter}`;
2371
+ const branchId = scopeTracker
2372
+ ? computeSemanticId('BRANCH', 'if', scopeTracker.getContext(), { discriminator: branchCounter })
2373
+ : legacyBranchId;
2374
+ // 2. Extract condition expression info for HAS_CONDITION edge
2375
+ const conditionResult = this.extractDiscriminantExpression(ifNode.test, module);
2376
+ // For else-if, get the parent branch ID
2377
+ const isAlternateOfBranchId = isElseIf
2378
+ ? ifElseScopeMap.get(ifPath.parent)?.branchId
2379
+ : undefined;
2380
+ branches.push({
2381
+ id: branchId,
2382
+ semanticId: branchId,
2383
+ type: 'BRANCH',
2384
+ branchType: 'if',
2385
+ file: module.file,
2386
+ line: getLine(ifNode),
2387
+ parentScopeId: actualParentScopeId,
2388
+ discriminantExpressionId: conditionResult.id,
2389
+ discriminantExpressionType: conditionResult.expressionType,
2390
+ discriminantLine: conditionResult.line,
2391
+ discriminantColumn: conditionResult.column,
2392
+ isAlternateOfBranchId
2393
+ });
2394
+ // 3. Create if-body SCOPE (backward compatibility)
2395
+ // Parent is now BRANCH, not original parentScopeId
2396
+ const counterId = ifScopeCounterRef.value++;
2397
+ const ifScopeId = `SCOPE#if#${module.file}#${getLine(ifNode)}:${getColumn(ifNode)}:${counterId}`;
2398
+ // Parse condition to extract constraints
2399
+ const constraints = ConditionParser.parse(ifNode.test);
2400
+ const ifSemanticId = this.generateSemanticId('if_statement', scopeTracker);
2401
+ scopes.push({
2402
+ id: ifScopeId,
2403
+ type: 'SCOPE',
2404
+ scopeType: 'if_statement',
2405
+ name: `if:${getLine(ifNode)}:${getColumn(ifNode)}:${counterId}`,
2406
+ semanticId: ifSemanticId,
2407
+ conditional: true,
2408
+ condition,
2409
+ constraints: constraints.length > 0 ? constraints : undefined,
2410
+ file: module.file,
2411
+ line: getLine(ifNode),
2412
+ parentScopeId: branchId // Parent is BRANCH, not original parentScopeId
2413
+ });
2414
+ // 4. Push if scope onto stack for CONTAINS edges
2415
+ if (scopeIdStack) {
2416
+ scopeIdStack.push(ifScopeId);
2417
+ }
2418
+ // Enter scope for semantic ID generation
2419
+ if (scopeTracker) {
2420
+ scopeTracker.enterCountedScope('if');
2421
+ }
2422
+ // 5. Handle else branch if present
2423
+ let elseScopeId = null;
2424
+ if (ifNode.alternate && !t.isIfStatement(ifNode.alternate)) {
2425
+ // Only create else scope for actual else block, not else-if
2426
+ const elseCounterId = ifScopeCounterRef.value++;
2427
+ elseScopeId = `SCOPE#else#${module.file}#${getLine(ifNode.alternate)}:${getColumn(ifNode.alternate)}:${elseCounterId}`;
2428
+ const negatedConstraints = constraints.length > 0 ? ConditionParser.negate(constraints) : undefined;
2429
+ const elseSemanticId = this.generateSemanticId('else_statement', scopeTracker);
2430
+ scopes.push({
2431
+ id: elseScopeId,
2432
+ type: 'SCOPE',
2433
+ scopeType: 'else_statement',
2434
+ name: `else:${getLine(ifNode.alternate)}:${getColumn(ifNode.alternate)}:${elseCounterId}`,
2435
+ semanticId: elseSemanticId,
2436
+ conditional: true,
2437
+ constraints: negatedConstraints,
2438
+ file: module.file,
2439
+ line: getLine(ifNode.alternate),
2440
+ parentScopeId: branchId // Parent is BRANCH, not original parentScopeId
2441
+ });
2442
+ // Store info to switch to else scope when we enter alternate
2443
+ ifElseScopeMap.set(ifNode, { inElse: false, hasElse: true, ifScopeId, elseScopeId, branchId });
2444
+ }
2445
+ else {
2446
+ ifElseScopeMap.set(ifNode, { inElse: false, hasElse: false, ifScopeId, elseScopeId: null, branchId });
2447
+ }
2448
+ },
2449
+ exit: (ifPath) => {
2450
+ const ifNode = ifPath.node;
2451
+ // Pop scope from stack (either if or else, depending on what we're exiting)
2452
+ if (scopeIdStack) {
2453
+ scopeIdStack.pop();
2454
+ }
2455
+ // Exit the current scope (either if or else)
2456
+ if (scopeTracker) {
2457
+ scopeTracker.exitScope();
2458
+ }
2459
+ // If we were in else, we already exited else scope
2460
+ // If we only had if, we exit if scope (done above)
2461
+ ifElseScopeMap.delete(ifNode);
2462
+ }
2463
+ };
2464
+ }
2465
+ /**
2466
+ * Factory method to create ConditionalExpression (ternary) handler.
2467
+ * Creates BRANCH nodes with branchType='ternary' and increments branchCount for cyclomatic complexity.
2468
+ *
2469
+ * Key difference from IfStatement: ternary has EXPRESSIONS as branches, not SCOPE blocks.
2470
+ * We store consequentExpressionId and alternateExpressionId in BranchInfo for HAS_CONSEQUENT/HAS_ALTERNATE edges.
2471
+ *
2472
+ * @param parentScopeId - Parent scope ID for the BRANCH node
2473
+ * @param module - Module context
2474
+ * @param branches - Collection to push BRANCH nodes to
2475
+ * @param branchCounterRef - Counter for unique BRANCH IDs
2476
+ * @param scopeTracker - Tracker for semantic ID generation
2477
+ * @param scopeIdStack - Stack for tracking current scope ID for CONTAINS edges
2478
+ * @param controlFlowState - State for tracking control flow metrics (complexity)
2479
+ * @param countLogicalOperators - Function to count logical operators in condition
2480
+ */
2481
+ createConditionalExpressionHandler(parentScopeId, module, branches, branchCounterRef, scopeTracker, scopeIdStack, controlFlowState, countLogicalOperators) {
2482
+ return (condPath) => {
2483
+ const condNode = condPath.node;
2484
+ // Increment branch count for cyclomatic complexity
2485
+ if (controlFlowState) {
2486
+ controlFlowState.branchCount++;
2487
+ // Count logical operators in the test condition (e.g., a && b ? x : y)
2488
+ if (countLogicalOperators) {
2489
+ controlFlowState.logicalOpCount += countLogicalOperators(condNode.test);
2490
+ }
2491
+ }
2492
+ // Determine parent scope from stack or fallback
2493
+ const actualParentScopeId = (scopeIdStack && scopeIdStack.length > 0)
2494
+ ? scopeIdStack[scopeIdStack.length - 1]
2495
+ : parentScopeId;
2496
+ // Create BRANCH node with branchType='ternary'
2497
+ const branchCounter = branchCounterRef.value++;
2498
+ const legacyBranchId = `${module.file}:BRANCH:ternary:${getLine(condNode)}:${branchCounter}`;
2499
+ const branchId = scopeTracker
2500
+ ? computeSemanticId('BRANCH', 'ternary', scopeTracker.getContext(), { discriminator: branchCounter })
2501
+ : legacyBranchId;
2502
+ // Extract condition expression info for HAS_CONDITION edge
2503
+ const conditionResult = this.extractDiscriminantExpression(condNode.test, module);
2504
+ // Generate expression IDs for consequent and alternate
2505
+ const consequentLine = getLine(condNode.consequent);
2506
+ const consequentColumn = getColumn(condNode.consequent);
2507
+ const consequentExpressionId = ExpressionNode.generateId(condNode.consequent.type, module.file, consequentLine, consequentColumn);
2508
+ const alternateLine = getLine(condNode.alternate);
2509
+ const alternateColumn = getColumn(condNode.alternate);
2510
+ const alternateExpressionId = ExpressionNode.generateId(condNode.alternate.type, module.file, alternateLine, alternateColumn);
2511
+ branches.push({
2512
+ id: branchId,
2513
+ semanticId: branchId,
2514
+ type: 'BRANCH',
2515
+ branchType: 'ternary',
2516
+ file: module.file,
2517
+ line: getLine(condNode),
2518
+ parentScopeId: actualParentScopeId,
2519
+ discriminantExpressionId: conditionResult.id,
2520
+ discriminantExpressionType: conditionResult.expressionType,
2521
+ discriminantLine: conditionResult.line,
2522
+ discriminantColumn: conditionResult.column,
2523
+ consequentExpressionId,
2524
+ alternateExpressionId
2525
+ });
2526
+ };
2527
+ }
2528
+ /**
2529
+ * Factory method to create BlockStatement handler for tracking if/else and try/finally transitions.
2530
+ * When entering an else block, switches scope from if to else.
2531
+ * When entering a finally block, switches scope from try/catch to finally.
2532
+ *
2533
+ * @param scopeTracker - Tracker for semantic ID generation
2534
+ * @param ifElseScopeMap - Map to track if/else scope transitions
2535
+ * @param tryScopeMap - Map to track try/catch/finally scope transitions
2536
+ * @param scopeIdStack - Stack for tracking current scope ID for CONTAINS edges
2537
+ */
2538
+ createBlockStatementHandler(scopeTracker, ifElseScopeMap, tryScopeMap, scopeIdStack) {
2539
+ return {
2540
+ enter: (blockPath) => {
2541
+ const parent = blockPath.parent;
2542
+ // Check if this block is the alternate of an IfStatement
2543
+ if (t.isIfStatement(parent) && parent.alternate === blockPath.node) {
2544
+ const scopeInfo = ifElseScopeMap.get(parent);
2545
+ if (scopeInfo && scopeInfo.hasElse && !scopeInfo.inElse) {
2546
+ // Swap if-scope for else-scope on the stack
2547
+ if (scopeIdStack && scopeInfo.elseScopeId) {
2548
+ scopeIdStack.pop(); // Remove if-scope
2549
+ scopeIdStack.push(scopeInfo.elseScopeId); // Push else-scope
2550
+ }
2551
+ // Exit if scope, enter else scope for semantic ID tracking
2552
+ if (scopeTracker) {
2553
+ scopeTracker.exitScope();
2554
+ scopeTracker.enterCountedScope('else');
2555
+ }
2556
+ scopeInfo.inElse = true;
2557
+ }
2558
+ }
2559
+ // Check if this block is the finalizer of a TryStatement
2560
+ if (t.isTryStatement(parent) && parent.finalizer === blockPath.node) {
2561
+ const scopeInfo = tryScopeMap.get(parent);
2562
+ if (scopeInfo && scopeInfo.finallyScopeId && scopeInfo.currentBlock !== 'finally') {
2563
+ // Pop current scope (try or catch), push finally scope
2564
+ if (scopeIdStack) {
2565
+ scopeIdStack.pop();
2566
+ scopeIdStack.push(scopeInfo.finallyScopeId);
2567
+ }
2568
+ // Exit current scope, enter finally scope for semantic ID tracking
2569
+ if (scopeTracker) {
2570
+ scopeTracker.exitScope();
2571
+ scopeTracker.enterCountedScope('finally');
2572
+ }
2573
+ scopeInfo.currentBlock = 'finally';
2574
+ }
2575
+ }
2576
+ }
2577
+ };
2578
+ }
2579
+ /**
2580
+ * Анализирует тело функции и извлекает переменные, вызовы, условные блоки.
2581
+ * Uses ScopeTracker from collections for semantic ID generation.
2582
+ */
2583
+ analyzeFunctionBody(funcPath, parentScopeId, module, collections) {
2584
+ // Extract with defaults for optional properties
2585
+ const functions = (collections.functions ?? []);
2586
+ const scopes = (collections.scopes ?? []);
2587
+ const variableDeclarations = (collections.variableDeclarations ?? []);
2588
+ const callSites = (collections.callSites ?? []);
2589
+ const methodCalls = (collections.methodCalls ?? []);
2590
+ const eventListeners = (collections.eventListeners ?? []);
2591
+ const methodCallbacks = (collections.methodCallbacks ?? []);
2592
+ const classInstantiations = (collections.classInstantiations ?? []);
2593
+ const constructorCalls = (collections.constructorCalls ?? []);
2594
+ const httpRequests = (collections.httpRequests ?? []);
2595
+ const literals = (collections.literals ?? []);
2596
+ const variableAssignments = (collections.variableAssignments ?? []);
2597
+ const ifScopeCounterRef = (collections.ifScopeCounterRef ?? { value: 0 });
2598
+ const scopeCounterRef = (collections.scopeCounterRef ?? { value: 0 });
2599
+ const varDeclCounterRef = (collections.varDeclCounterRef ?? { value: 0 });
2600
+ const callSiteCounterRef = (collections.callSiteCounterRef ?? { value: 0 });
2601
+ const functionCounterRef = (collections.functionCounterRef ?? { value: 0 });
2602
+ const httpRequestCounterRef = (collections.httpRequestCounterRef ?? { value: 0 });
2603
+ const literalCounterRef = (collections.literalCounterRef ?? { value: 0 });
2604
+ const anonymousFunctionCounterRef = (collections.anonymousFunctionCounterRef ?? { value: 0 });
2605
+ const scopeTracker = collections.scopeTracker;
2606
+ // Object literal tracking (REG-328)
2607
+ if (!collections.objectLiterals) {
2608
+ collections.objectLiterals = [];
2609
+ }
2610
+ if (!collections.objectProperties) {
2611
+ collections.objectProperties = [];
2612
+ }
2613
+ if (!collections.objectLiteralCounterRef) {
2614
+ collections.objectLiteralCounterRef = { value: 0 };
2615
+ }
2616
+ const objectLiterals = collections.objectLiterals;
2617
+ const objectProperties = collections.objectProperties;
2618
+ const objectLiteralCounterRef = collections.objectLiteralCounterRef;
2619
+ const returnStatements = (collections.returnStatements ?? []);
2620
+ const parameters = (collections.parameters ?? []);
2621
+ // Control flow collections (Phase 2: LOOP nodes)
2622
+ // Initialize if not exist to ensure nested function calls share same arrays
2623
+ if (!collections.loops) {
2624
+ collections.loops = [];
2625
+ }
2626
+ if (!collections.loopCounterRef) {
2627
+ collections.loopCounterRef = { value: 0 };
2628
+ }
2629
+ const loops = collections.loops;
2630
+ const loopCounterRef = collections.loopCounterRef;
2631
+ const updateExpressions = (collections.updateExpressions ?? []);
2632
+ const processedNodes = collections.processedNodes ?? {
2633
+ functions: new Set(),
2634
+ classes: new Set(),
2635
+ imports: new Set(),
2636
+ exports: new Set(),
2637
+ variables: new Set(),
2638
+ callSites: new Set(),
2639
+ methodCalls: new Set(),
2640
+ varDecls: new Set(),
2641
+ eventListeners: new Set()
2642
+ };
2643
+ const parentScopeVariables = new Set();
2644
+ const processedCallSites = processedNodes.callSites;
2645
+ const processedVarDecls = processedNodes.varDecls;
2646
+ const processedMethodCalls = processedNodes.methodCalls;
2647
+ const processedEventListeners = processedNodes.eventListeners;
2648
+ // Track if/else scope transitions (Phase 3: extended with branchId)
2649
+ const ifElseScopeMap = new Map();
2650
+ // Ensure branches and branchCounterRef are initialized (used by IfStatement and SwitchStatement)
2651
+ if (!collections.branches) {
2652
+ collections.branches = [];
2653
+ }
2654
+ if (!collections.branchCounterRef) {
2655
+ collections.branchCounterRef = { value: 0 };
2656
+ }
2657
+ const branches = collections.branches;
2658
+ const branchCounterRef = collections.branchCounterRef;
2659
+ // Phase 4: Initialize try/catch/finally collections and counters
2660
+ if (!collections.tryBlocks) {
2661
+ collections.tryBlocks = [];
2662
+ }
2663
+ if (!collections.catchBlocks) {
2664
+ collections.catchBlocks = [];
2665
+ }
2666
+ if (!collections.finallyBlocks) {
2667
+ collections.finallyBlocks = [];
2668
+ }
2669
+ if (!collections.tryBlockCounterRef) {
2670
+ collections.tryBlockCounterRef = { value: 0 };
2671
+ }
2672
+ if (!collections.catchBlockCounterRef) {
2673
+ collections.catchBlockCounterRef = { value: 0 };
2674
+ }
2675
+ if (!collections.finallyBlockCounterRef) {
2676
+ collections.finallyBlockCounterRef = { value: 0 };
2677
+ }
2678
+ const tryBlocks = collections.tryBlocks;
2679
+ const catchBlocks = collections.catchBlocks;
2680
+ const finallyBlocks = collections.finallyBlocks;
2681
+ const tryBlockCounterRef = collections.tryBlockCounterRef;
2682
+ const catchBlockCounterRef = collections.catchBlockCounterRef;
2683
+ const finallyBlockCounterRef = collections.finallyBlockCounterRef;
2684
+ // Track try/catch/finally scope transitions
2685
+ const tryScopeMap = new Map();
2686
+ // REG-334: Use shared Promise executor contexts from collections.
2687
+ // These are populated by module-level NewExpression handler and function-level NewExpression handler.
2688
+ if (!collections.promiseExecutorContexts) {
2689
+ collections.promiseExecutorContexts = new Map();
2690
+ }
2691
+ const promiseExecutorContexts = collections.promiseExecutorContexts;
2692
+ // Initialize promiseResolutions array if not exists
2693
+ if (!collections.promiseResolutions) {
2694
+ collections.promiseResolutions = [];
2695
+ }
2696
+ const promiseResolutions = collections.promiseResolutions;
2697
+ // Dynamic scope ID stack for CONTAINS edges
2698
+ // Starts with the function body scope, gets updated as we enter/exit conditional scopes
2699
+ const scopeIdStack = [parentScopeId];
2700
+ const getCurrentScopeId = () => scopeIdStack[scopeIdStack.length - 1];
2701
+ // Determine the ID of the function we're analyzing for RETURNS edges
2702
+ // Find by matching file/line/column in functions collection (it was just added by the visitor)
2703
+ const funcNode = funcPath.node;
2704
+ const funcLine = getLine(funcNode);
2705
+ const funcColumn = getColumn(funcNode);
2706
+ let currentFunctionId = null;
2707
+ const matchingFunction = functions.find(f => f.file === module.file &&
2708
+ f.line === funcLine &&
2709
+ (f.column === undefined || f.column === funcColumn));
2710
+ if (matchingFunction) {
2711
+ currentFunctionId = matchingFunction.id;
2712
+ }
2713
+ // Phase 6 (REG-267): Control flow tracking state for cyclomatic complexity
2714
+ const controlFlowState = {
2715
+ branchCount: 0, // if/switch statements
2716
+ loopCount: 0, // for/while/do-while/for-in/for-of
2717
+ caseCount: 0, // switch cases (excluding default)
2718
+ logicalOpCount: 0, // && and || in conditions
2719
+ hasTryCatch: false,
2720
+ hasEarlyReturn: false,
2721
+ hasThrow: false,
2722
+ returnCount: 0, // Track total return count for early return detection
2723
+ totalStatements: 0 // Track if there are statements after returns
2724
+ };
2725
+ // Handle implicit return for THIS arrow function if it has an expression body
2726
+ // e.g., `const double = x => x * 2;` - the function we're analyzing IS an arrow with expression body
2727
+ if (t.isArrowFunctionExpression(funcNode) && !t.isBlockStatement(funcNode.body) && currentFunctionId) {
2728
+ const bodyExpr = funcNode.body;
2729
+ const bodyLine = getLine(bodyExpr);
2730
+ const bodyColumn = getColumn(bodyExpr);
2731
+ const returnInfo = {
2732
+ parentFunctionId: currentFunctionId,
2733
+ file: module.file,
2734
+ line: bodyLine,
2735
+ column: bodyColumn,
2736
+ returnValueType: 'NONE',
2737
+ isImplicitReturn: true
2738
+ };
2739
+ // Apply type detection logic for the implicit return
2740
+ if (t.isIdentifier(bodyExpr)) {
2741
+ returnInfo.returnValueType = 'VARIABLE';
2742
+ returnInfo.returnValueName = bodyExpr.name;
2743
+ }
2744
+ // TemplateLiteral must come BEFORE isLiteral (TemplateLiteral extends Literal)
2745
+ else if (t.isTemplateLiteral(bodyExpr)) {
2746
+ returnInfo.returnValueType = 'EXPRESSION';
2747
+ returnInfo.expressionType = 'TemplateLiteral';
2748
+ returnInfo.returnValueLine = getLine(bodyExpr);
2749
+ returnInfo.returnValueColumn = getColumn(bodyExpr);
2750
+ returnInfo.returnValueId = NodeFactory.generateExpressionId('TemplateLiteral', module.file, getLine(bodyExpr), getColumn(bodyExpr));
2751
+ const sourceNames = [];
2752
+ for (const expr of bodyExpr.expressions) {
2753
+ if (t.isIdentifier(expr))
2754
+ sourceNames.push(expr.name);
2755
+ }
2756
+ if (sourceNames.length > 0)
2757
+ returnInfo.expressionSourceNames = sourceNames;
2758
+ }
2759
+ else if (t.isLiteral(bodyExpr)) {
2760
+ returnInfo.returnValueType = 'LITERAL';
2761
+ const literalId = `LITERAL#implicit_return#${module.file}#${funcLine}:${funcColumn}:${literalCounterRef.value++}`;
2762
+ returnInfo.returnValueId = literalId;
2763
+ literals.push({
2764
+ id: literalId,
2765
+ type: 'LITERAL',
2766
+ value: ExpressionEvaluator.extractLiteralValue(bodyExpr),
2767
+ valueType: typeof ExpressionEvaluator.extractLiteralValue(bodyExpr),
2768
+ file: module.file,
2769
+ line: bodyLine,
2770
+ column: bodyColumn
2771
+ });
2772
+ }
2773
+ else if (t.isCallExpression(bodyExpr) && t.isIdentifier(bodyExpr.callee)) {
2774
+ returnInfo.returnValueType = 'CALL_SITE';
2775
+ returnInfo.returnValueLine = getLine(bodyExpr);
2776
+ returnInfo.returnValueColumn = getColumn(bodyExpr);
2777
+ returnInfo.returnValueCallName = bodyExpr.callee.name;
2778
+ }
2779
+ else if (t.isCallExpression(bodyExpr) && t.isMemberExpression(bodyExpr.callee)) {
2780
+ returnInfo.returnValueType = 'METHOD_CALL';
2781
+ returnInfo.returnValueLine = getLine(bodyExpr);
2782
+ returnInfo.returnValueColumn = getColumn(bodyExpr);
2783
+ if (t.isIdentifier(bodyExpr.callee.property)) {
2784
+ returnInfo.returnValueCallName = bodyExpr.callee.property.name;
2785
+ }
2786
+ }
2787
+ // REG-276: Detailed EXPRESSION handling for implicit arrow returns
2788
+ else if (t.isBinaryExpression(bodyExpr)) {
2789
+ returnInfo.returnValueType = 'EXPRESSION';
2790
+ returnInfo.expressionType = 'BinaryExpression';
2791
+ returnInfo.returnValueLine = getLine(bodyExpr);
2792
+ returnInfo.returnValueColumn = getColumn(bodyExpr);
2793
+ returnInfo.operator = bodyExpr.operator;
2794
+ returnInfo.returnValueId = NodeFactory.generateExpressionId('BinaryExpression', module.file, getLine(bodyExpr), getColumn(bodyExpr));
2795
+ if (t.isIdentifier(bodyExpr.left))
2796
+ returnInfo.leftSourceName = bodyExpr.left.name;
2797
+ if (t.isIdentifier(bodyExpr.right))
2798
+ returnInfo.rightSourceName = bodyExpr.right.name;
2799
+ }
2800
+ else if (t.isLogicalExpression(bodyExpr)) {
2801
+ returnInfo.returnValueType = 'EXPRESSION';
2802
+ returnInfo.expressionType = 'LogicalExpression';
2803
+ returnInfo.returnValueLine = getLine(bodyExpr);
2804
+ returnInfo.returnValueColumn = getColumn(bodyExpr);
2805
+ returnInfo.operator = bodyExpr.operator;
2806
+ returnInfo.returnValueId = NodeFactory.generateExpressionId('LogicalExpression', module.file, getLine(bodyExpr), getColumn(bodyExpr));
2807
+ if (t.isIdentifier(bodyExpr.left))
2808
+ returnInfo.leftSourceName = bodyExpr.left.name;
2809
+ if (t.isIdentifier(bodyExpr.right))
2810
+ returnInfo.rightSourceName = bodyExpr.right.name;
2811
+ }
2812
+ else if (t.isConditionalExpression(bodyExpr)) {
2813
+ returnInfo.returnValueType = 'EXPRESSION';
2814
+ returnInfo.expressionType = 'ConditionalExpression';
2815
+ returnInfo.returnValueLine = getLine(bodyExpr);
2816
+ returnInfo.returnValueColumn = getColumn(bodyExpr);
2817
+ returnInfo.returnValueId = NodeFactory.generateExpressionId('ConditionalExpression', module.file, getLine(bodyExpr), getColumn(bodyExpr));
2818
+ if (t.isIdentifier(bodyExpr.consequent))
2819
+ returnInfo.consequentSourceName = bodyExpr.consequent.name;
2820
+ if (t.isIdentifier(bodyExpr.alternate))
2821
+ returnInfo.alternateSourceName = bodyExpr.alternate.name;
2822
+ }
2823
+ else if (t.isUnaryExpression(bodyExpr)) {
2824
+ returnInfo.returnValueType = 'EXPRESSION';
2825
+ returnInfo.expressionType = 'UnaryExpression';
2826
+ returnInfo.returnValueLine = getLine(bodyExpr);
2827
+ returnInfo.returnValueColumn = getColumn(bodyExpr);
2828
+ returnInfo.operator = bodyExpr.operator;
2829
+ returnInfo.returnValueId = NodeFactory.generateExpressionId('UnaryExpression', module.file, getLine(bodyExpr), getColumn(bodyExpr));
2830
+ if (t.isIdentifier(bodyExpr.argument))
2831
+ returnInfo.unaryArgSourceName = bodyExpr.argument.name;
2832
+ }
2833
+ else if (t.isMemberExpression(bodyExpr)) {
2834
+ returnInfo.returnValueType = 'EXPRESSION';
2835
+ returnInfo.expressionType = 'MemberExpression';
2836
+ returnInfo.returnValueLine = getLine(bodyExpr);
2837
+ returnInfo.returnValueColumn = getColumn(bodyExpr);
2838
+ returnInfo.returnValueId = NodeFactory.generateExpressionId('MemberExpression', module.file, getLine(bodyExpr), getColumn(bodyExpr));
2839
+ if (t.isIdentifier(bodyExpr.object)) {
2840
+ returnInfo.object = bodyExpr.object.name;
2841
+ returnInfo.objectSourceName = bodyExpr.object.name;
2842
+ }
2843
+ if (t.isIdentifier(bodyExpr.property))
2844
+ returnInfo.property = bodyExpr.property.name;
2845
+ returnInfo.computed = bodyExpr.computed;
2846
+ }
2847
+ else {
2848
+ // Fallback: any other expression type
2849
+ returnInfo.returnValueType = 'EXPRESSION';
2850
+ returnInfo.expressionType = bodyExpr.type;
2851
+ returnInfo.returnValueLine = getLine(bodyExpr);
2852
+ returnInfo.returnValueColumn = getColumn(bodyExpr);
2853
+ returnInfo.returnValueId = NodeFactory.generateExpressionId(bodyExpr.type, module.file, getLine(bodyExpr), getColumn(bodyExpr));
2854
+ }
2855
+ returnStatements.push(returnInfo);
2856
+ }
2857
+ funcPath.traverse({
2858
+ VariableDeclaration: (varPath) => {
2859
+ this.handleVariableDeclaration(varPath, getCurrentScopeId(), module, variableDeclarations, classInstantiations, literals, variableAssignments, varDeclCounterRef, literalCounterRef, scopeTracker, parentScopeVariables, objectLiterals, objectProperties, objectLiteralCounterRef);
2860
+ },
2861
+ // Detect indexed array assignments: arr[i] = value
2862
+ AssignmentExpression: (assignPath) => {
2863
+ const assignNode = assignPath.node;
2864
+ // === VARIABLE REASSIGNMENT (REG-290) ===
2865
+ // Check if LHS is simple identifier (not obj.prop, not arr[i])
2866
+ // Must be checked FIRST before array/object mutation handlers
2867
+ if (assignNode.left.type === 'Identifier') {
2868
+ // Initialize collection if not exists
2869
+ if (!collections.variableReassignments) {
2870
+ collections.variableReassignments = [];
2871
+ }
2872
+ const variableReassignments = collections.variableReassignments;
2873
+ this.detectVariableReassignment(assignNode, module, variableReassignments, scopeTracker);
2874
+ }
2875
+ // === END VARIABLE REASSIGNMENT ===
2876
+ // Initialize collection if not exists
2877
+ if (!collections.arrayMutations) {
2878
+ collections.arrayMutations = [];
2879
+ }
2880
+ const arrayMutations = collections.arrayMutations;
2881
+ // Check for indexed array assignment: arr[i] = value
2882
+ this.detectIndexedArrayAssignment(assignNode, module, arrayMutations, scopeTracker);
2883
+ // Initialize object mutations collection if not exists
2884
+ if (!collections.objectMutations) {
2885
+ collections.objectMutations = [];
2886
+ }
2887
+ const objectMutations = collections.objectMutations;
2888
+ // Check for object property assignment: obj.prop = value
2889
+ this.detectObjectPropertyAssignment(assignNode, module, objectMutations, scopeTracker);
2890
+ },
2891
+ // Handle return statements for RETURNS edges
2892
+ ReturnStatement: (returnPath) => {
2893
+ // Skip if we couldn't determine the function ID
2894
+ if (!currentFunctionId) {
2895
+ return;
2896
+ }
2897
+ // Skip if this return is inside a nested function (not the function we're analyzing)
2898
+ // Check if there's a function ancestor BETWEEN us and funcNode
2899
+ // Stop checking once we reach funcNode - parents above funcNode are outside scope
2900
+ let parent = returnPath.parentPath;
2901
+ let isInsideConditional = false;
2902
+ while (parent) {
2903
+ // If we've reached funcNode, we're done checking - this return belongs to funcNode
2904
+ if (parent.node === funcNode) {
2905
+ break;
2906
+ }
2907
+ if (t.isFunction(parent.node)) {
2908
+ // Found a function between returnPath and funcNode - this return is inside a nested function
2909
+ return;
2910
+ }
2911
+ // Track if return is inside a conditional block (if/else, switch case, loop, try/catch)
2912
+ if (t.isIfStatement(parent.node) ||
2913
+ t.isSwitchCase(parent.node) ||
2914
+ t.isLoop(parent.node) ||
2915
+ t.isTryStatement(parent.node) ||
2916
+ t.isCatchClause(parent.node)) {
2917
+ isInsideConditional = true;
2918
+ }
2919
+ parent = parent.parentPath;
2920
+ }
2921
+ // Phase 6 (REG-267): Track return count and early return detection
2922
+ controlFlowState.returnCount++;
2923
+ // A return is "early" if it's inside a conditional structure
2924
+ // (More returns after this one indicate the function doesn't always end here)
2925
+ if (isInsideConditional) {
2926
+ controlFlowState.hasEarlyReturn = true;
2927
+ }
2928
+ const returnNode = returnPath.node;
2929
+ const returnLine = getLine(returnNode);
2930
+ const returnColumn = getColumn(returnNode);
2931
+ // Handle bare return; (no value)
2932
+ if (!returnNode.argument) {
2933
+ // Skip - no data flow value
2934
+ return;
2935
+ }
2936
+ const arg = returnNode.argument;
2937
+ // Determine return value type and extract relevant info
2938
+ const returnInfo = {
2939
+ parentFunctionId: currentFunctionId,
2940
+ file: module.file,
2941
+ line: returnLine,
2942
+ column: returnColumn,
2943
+ returnValueType: 'NONE'
2944
+ };
2945
+ // Identifier (variable reference)
2946
+ if (t.isIdentifier(arg)) {
2947
+ returnInfo.returnValueType = 'VARIABLE';
2948
+ returnInfo.returnValueName = arg.name;
2949
+ }
2950
+ // TemplateLiteral must come BEFORE isLiteral (TemplateLiteral extends Literal)
2951
+ else if (t.isTemplateLiteral(arg)) {
2952
+ returnInfo.returnValueType = 'EXPRESSION';
2953
+ returnInfo.expressionType = 'TemplateLiteral';
2954
+ returnInfo.returnValueLine = getLine(arg);
2955
+ returnInfo.returnValueColumn = getColumn(arg);
2956
+ returnInfo.returnValueId = NodeFactory.generateExpressionId('TemplateLiteral', module.file, getLine(arg), getColumn(arg));
2957
+ // Extract all embedded expression identifiers
2958
+ const sourceNames = [];
2959
+ for (const expr of arg.expressions) {
2960
+ if (t.isIdentifier(expr)) {
2961
+ sourceNames.push(expr.name);
2962
+ }
2963
+ }
2964
+ if (sourceNames.length > 0) {
2965
+ returnInfo.expressionSourceNames = sourceNames;
2966
+ }
2967
+ }
2968
+ // Literal values (after TemplateLiteral check)
2969
+ else if (t.isLiteral(arg)) {
2970
+ returnInfo.returnValueType = 'LITERAL';
2971
+ // Create a LITERAL node ID for this return value
2972
+ const literalId = `LITERAL#return#${module.file}#${returnLine}:${returnColumn}:${literalCounterRef.value++}`;
2973
+ returnInfo.returnValueId = literalId;
2974
+ // Also add to literals collection for node creation
2975
+ literals.push({
2976
+ id: literalId,
2977
+ type: 'LITERAL',
2978
+ value: ExpressionEvaluator.extractLiteralValue(arg),
2979
+ valueType: typeof ExpressionEvaluator.extractLiteralValue(arg),
2980
+ file: module.file,
2981
+ line: returnLine,
2982
+ column: returnColumn
2983
+ });
2984
+ }
2985
+ // Direct function call: return foo()
2986
+ else if (t.isCallExpression(arg) && t.isIdentifier(arg.callee)) {
2987
+ returnInfo.returnValueType = 'CALL_SITE';
2988
+ returnInfo.returnValueLine = getLine(arg);
2989
+ returnInfo.returnValueColumn = getColumn(arg);
2990
+ returnInfo.returnValueCallName = arg.callee.name;
2991
+ }
2992
+ // Method call: return obj.method()
2993
+ else if (t.isCallExpression(arg) && t.isMemberExpression(arg.callee)) {
2994
+ returnInfo.returnValueType = 'METHOD_CALL';
2995
+ returnInfo.returnValueLine = getLine(arg);
2996
+ returnInfo.returnValueColumn = getColumn(arg);
2997
+ // Extract method name for lookup
2998
+ if (t.isIdentifier(arg.callee.property)) {
2999
+ returnInfo.returnValueCallName = arg.callee.property.name;
3000
+ }
3001
+ }
3002
+ // BinaryExpression: return a + b
3003
+ else if (t.isBinaryExpression(arg)) {
3004
+ returnInfo.returnValueType = 'EXPRESSION';
3005
+ returnInfo.expressionType = 'BinaryExpression';
3006
+ returnInfo.returnValueLine = getLine(arg);
3007
+ returnInfo.returnValueColumn = getColumn(arg);
3008
+ returnInfo.operator = arg.operator;
3009
+ // Generate stable ID for the EXPRESSION node
3010
+ returnInfo.returnValueId = NodeFactory.generateExpressionId('BinaryExpression', module.file, getLine(arg), getColumn(arg));
3011
+ // Extract left operand source
3012
+ if (t.isIdentifier(arg.left)) {
3013
+ returnInfo.leftSourceName = arg.left.name;
3014
+ }
3015
+ // Extract right operand source
3016
+ if (t.isIdentifier(arg.right)) {
3017
+ returnInfo.rightSourceName = arg.right.name;
3018
+ }
3019
+ }
3020
+ // LogicalExpression: return a && b, return a || b
3021
+ else if (t.isLogicalExpression(arg)) {
3022
+ returnInfo.returnValueType = 'EXPRESSION';
3023
+ returnInfo.expressionType = 'LogicalExpression';
3024
+ returnInfo.returnValueLine = getLine(arg);
3025
+ returnInfo.returnValueColumn = getColumn(arg);
3026
+ returnInfo.operator = arg.operator;
3027
+ returnInfo.returnValueId = NodeFactory.generateExpressionId('LogicalExpression', module.file, getLine(arg), getColumn(arg));
3028
+ if (t.isIdentifier(arg.left)) {
3029
+ returnInfo.leftSourceName = arg.left.name;
3030
+ }
3031
+ if (t.isIdentifier(arg.right)) {
3032
+ returnInfo.rightSourceName = arg.right.name;
3033
+ }
3034
+ }
3035
+ // ConditionalExpression: return condition ? a : b
3036
+ else if (t.isConditionalExpression(arg)) {
3037
+ returnInfo.returnValueType = 'EXPRESSION';
3038
+ returnInfo.expressionType = 'ConditionalExpression';
3039
+ returnInfo.returnValueLine = getLine(arg);
3040
+ returnInfo.returnValueColumn = getColumn(arg);
3041
+ returnInfo.returnValueId = NodeFactory.generateExpressionId('ConditionalExpression', module.file, getLine(arg), getColumn(arg));
3042
+ // Extract consequent (then branch) source
3043
+ if (t.isIdentifier(arg.consequent)) {
3044
+ returnInfo.consequentSourceName = arg.consequent.name;
3045
+ }
3046
+ // Extract alternate (else branch) source
3047
+ if (t.isIdentifier(arg.alternate)) {
3048
+ returnInfo.alternateSourceName = arg.alternate.name;
3049
+ }
3050
+ }
3051
+ // UnaryExpression: return !x, return -x
3052
+ else if (t.isUnaryExpression(arg)) {
3053
+ returnInfo.returnValueType = 'EXPRESSION';
3054
+ returnInfo.expressionType = 'UnaryExpression';
3055
+ returnInfo.returnValueLine = getLine(arg);
3056
+ returnInfo.returnValueColumn = getColumn(arg);
3057
+ returnInfo.operator = arg.operator;
3058
+ returnInfo.returnValueId = NodeFactory.generateExpressionId('UnaryExpression', module.file, getLine(arg), getColumn(arg));
3059
+ if (t.isIdentifier(arg.argument)) {
3060
+ returnInfo.unaryArgSourceName = arg.argument.name;
3061
+ }
3062
+ }
3063
+ // MemberExpression (property access): return obj.prop
3064
+ else if (t.isMemberExpression(arg)) {
3065
+ returnInfo.returnValueType = 'EXPRESSION';
3066
+ returnInfo.expressionType = 'MemberExpression';
3067
+ returnInfo.returnValueLine = getLine(arg);
3068
+ returnInfo.returnValueColumn = getColumn(arg);
3069
+ returnInfo.returnValueId = NodeFactory.generateExpressionId('MemberExpression', module.file, getLine(arg), getColumn(arg));
3070
+ // Extract object.property info
3071
+ if (t.isIdentifier(arg.object)) {
3072
+ returnInfo.object = arg.object.name;
3073
+ returnInfo.objectSourceName = arg.object.name;
1056
3074
  }
3075
+ if (t.isIdentifier(arg.property)) {
3076
+ returnInfo.property = arg.property.name;
3077
+ }
3078
+ returnInfo.computed = arg.computed;
1057
3079
  }
1058
- if (tryNode.finalizer) {
1059
- const finallyScopeId = `SCOPE#finally-block#${module.file}#${tryNode.finalizer.loc.start.line}:${scopeCounterRef.value++}`;
1060
- const finallySemanticId = this.generateSemanticId('finally-block', scopeCtx);
1061
- scopes.push({
1062
- id: finallyScopeId,
1063
- type: 'SCOPE',
1064
- scopeType: 'finally-block',
1065
- semanticId: finallySemanticId,
1066
- file: module.file,
1067
- line: tryNode.finalizer.loc.start.line,
1068
- parentScopeId
1069
- });
3080
+ // NewExpression: return new Foo()
3081
+ else if (t.isNewExpression(arg)) {
3082
+ returnInfo.returnValueType = 'EXPRESSION';
3083
+ returnInfo.expressionType = 'NewExpression';
3084
+ returnInfo.returnValueLine = getLine(arg);
3085
+ returnInfo.returnValueColumn = getColumn(arg);
3086
+ returnInfo.returnValueId = NodeFactory.generateExpressionId('NewExpression', module.file, getLine(arg), getColumn(arg));
3087
+ }
3088
+ // Fallback for other expression types
3089
+ else {
3090
+ returnInfo.returnValueType = 'EXPRESSION';
3091
+ returnInfo.expressionType = arg.type;
3092
+ returnInfo.returnValueLine = getLine(arg);
3093
+ returnInfo.returnValueColumn = getColumn(arg);
3094
+ returnInfo.returnValueId = NodeFactory.generateExpressionId(arg.type, module.file, getLine(arg), getColumn(arg));
3095
+ }
3096
+ returnStatements.push(returnInfo);
3097
+ },
3098
+ // Phase 6 (REG-267): Track throw statements for control flow metadata
3099
+ ThrowStatement: (throwPath) => {
3100
+ // Skip if this throw is inside a nested function (not the function we're analyzing)
3101
+ let parent = throwPath.parentPath;
3102
+ while (parent) {
3103
+ if (t.isFunction(parent.node) && parent.node !== funcNode) {
3104
+ // This throw is inside a nested function - skip it
3105
+ return;
3106
+ }
3107
+ parent = parent.parentPath;
1070
3108
  }
3109
+ controlFlowState.hasThrow = true;
1071
3110
  },
3111
+ ForStatement: this.createLoopScopeHandler('for', 'for-loop', 'for', parentScopeId, module, scopes, loops, scopeCounterRef, loopCounterRef, scopeTracker, scopeIdStack, controlFlowState),
3112
+ ForInStatement: this.createLoopScopeHandler('for-in', 'for-in-loop', 'for-in', parentScopeId, module, scopes, loops, scopeCounterRef, loopCounterRef, scopeTracker, scopeIdStack, controlFlowState),
3113
+ ForOfStatement: this.createLoopScopeHandler('for-of', 'for-of-loop', 'for-of', parentScopeId, module, scopes, loops, scopeCounterRef, loopCounterRef, scopeTracker, scopeIdStack, controlFlowState),
3114
+ WhileStatement: this.createLoopScopeHandler('while', 'while-loop', 'while', parentScopeId, module, scopes, loops, scopeCounterRef, loopCounterRef, scopeTracker, scopeIdStack, controlFlowState),
3115
+ DoWhileStatement: this.createLoopScopeHandler('do-while', 'do-while-loop', 'do-while', parentScopeId, module, scopes, loops, scopeCounterRef, loopCounterRef, scopeTracker, scopeIdStack, controlFlowState),
3116
+ // Phase 4 (REG-267): Now creates TRY_BLOCK, CATCH_BLOCK, FINALLY_BLOCK nodes
3117
+ TryStatement: this.createTryStatementHandler(parentScopeId, module, scopes, tryBlocks, catchBlocks, finallyBlocks, scopeCounterRef, tryBlockCounterRef, catchBlockCounterRef, finallyBlockCounterRef, scopeTracker, tryScopeMap, scopeIdStack, controlFlowState),
3118
+ CatchClause: this.createCatchClauseHandler(module, variableDeclarations, varDeclCounterRef, scopeTracker, tryScopeMap, scopeIdStack),
1072
3119
  SwitchStatement: (switchPath) => {
1073
- const switchNode = switchPath.node;
1074
- const scopeId = `SCOPE#switch-case#${module.file}#${switchNode.loc.start.line}:${scopeCounterRef.value++}`;
1075
- const semanticId = this.generateSemanticId('switch-case', scopeCtx);
1076
- scopes.push({
1077
- id: scopeId,
1078
- type: 'SCOPE',
1079
- scopeType: 'switch-case',
1080
- semanticId,
1081
- file: module.file,
1082
- line: switchNode.loc.start.line,
1083
- parentScopeId
1084
- });
3120
+ this.handleSwitchStatement(switchPath, parentScopeId, module, collections, scopeTracker, controlFlowState);
1085
3121
  },
1086
3122
  FunctionExpression: (funcPath) => {
1087
3123
  const node = funcPath.node;
1088
- const funcName = node.id ? node.id.name : this.generateAnonymousName(scopeCtx);
1089
- const functionId = `FUNCTION#${funcName}#${module.file}#${node.loc.start.line}:${node.loc.start.column}:${functionCounterRef.value++}`;
3124
+ const funcName = node.id ? node.id.name : this.generateAnonymousName(scopeTracker);
3125
+ // Use semantic ID as primary ID when scopeTracker available
3126
+ const legacyId = `FUNCTION#${funcName}#${module.file}#${getLine(node)}:${getColumn(node)}:${functionCounterRef.value++}`;
3127
+ const functionId = scopeTracker
3128
+ ? computeSemanticId('FUNCTION', funcName, scopeTracker.getContext())
3129
+ : legacyId;
1090
3130
  functions.push({
1091
3131
  id: functionId,
1092
- stableId: functionId,
1093
3132
  type: 'FUNCTION',
1094
3133
  name: funcName,
1095
3134
  file: module.file,
1096
- line: node.loc.start.line,
1097
- column: node.loc.start.column,
3135
+ line: getLine(node),
3136
+ column: getColumn(node),
1098
3137
  async: node.async || false,
1099
3138
  generator: node.generator || false,
1100
3139
  parentScopeId
1101
3140
  });
1102
- const nestedScopeId = `SCOPE#${funcName}:body#${module.file}#${node.loc.start.line}`;
1103
- const closureSemanticId = this.generateSemanticId('closure', scopeCtx);
3141
+ const nestedScopeId = `SCOPE#${funcName}:body#${module.file}#${getLine(node)}`;
3142
+ const closureSemanticId = this.generateSemanticId('closure', scopeTracker);
1104
3143
  scopes.push({
1105
3144
  id: nestedScopeId,
1106
3145
  type: 'SCOPE',
@@ -1109,22 +3148,24 @@ export class JSASTAnalyzer extends Plugin {
1109
3148
  semanticId: closureSemanticId,
1110
3149
  conditional: false,
1111
3150
  file: module.file,
1112
- line: node.loc.start.line,
3151
+ line: getLine(node),
1113
3152
  parentFunctionId: functionId,
1114
3153
  capturesFrom: parentScopeId
1115
3154
  });
1116
- // For nested function, create new context with function name as semantic path
1117
- const nestedFuncCtx = {
1118
- semanticPath: scopeCtx ? `${scopeCtx.semanticPath}.${funcName}` : funcName,
1119
- siblingCounters: new Map()
1120
- };
1121
- this.analyzeFunctionBody(funcPath, nestedScopeId, module, collections, nestedFuncCtx);
3155
+ // Enter nested function scope for semantic ID generation
3156
+ if (scopeTracker) {
3157
+ scopeTracker.enterScope(funcName, 'function');
3158
+ }
3159
+ this.analyzeFunctionBody(funcPath, nestedScopeId, module, collections);
3160
+ if (scopeTracker) {
3161
+ scopeTracker.exitScope();
3162
+ }
1122
3163
  funcPath.skip();
1123
3164
  },
1124
3165
  ArrowFunctionExpression: (arrowPath) => {
1125
3166
  const node = arrowPath.node;
1126
- const line = node.loc.start.line;
1127
- const column = node.loc.start.column;
3167
+ const line = getLine(node);
3168
+ const column = getColumn(node);
1128
3169
  // Определяем имя (anonymous если не присвоено переменной)
1129
3170
  const parent = arrowPath.parent;
1130
3171
  let funcName;
@@ -1133,12 +3174,15 @@ export class JSASTAnalyzer extends Plugin {
1133
3174
  }
1134
3175
  else {
1135
3176
  // Используем scope-level счётчик для стабильного semanticId
1136
- funcName = this.generateAnonymousName(scopeCtx);
3177
+ funcName = this.generateAnonymousName(scopeTracker);
1137
3178
  }
1138
- const functionId = `FUNCTION#${funcName}:${line}:${column}:${functionCounterRef.value++}`;
3179
+ // Use semantic ID as primary ID when scopeTracker available
3180
+ const legacyId = `FUNCTION#${funcName}:${line}:${column}:${functionCounterRef.value++}`;
3181
+ const functionId = scopeTracker
3182
+ ? computeSemanticId('FUNCTION', funcName, scopeTracker.getContext())
3183
+ : legacyId;
1139
3184
  functions.push({
1140
3185
  id: functionId,
1141
- stableId: functionId,
1142
3186
  type: 'FUNCTION',
1143
3187
  name: funcName,
1144
3188
  file: module.file,
@@ -1150,7 +3194,7 @@ export class JSASTAnalyzer extends Plugin {
1150
3194
  });
1151
3195
  if (node.body.type === 'BlockStatement') {
1152
3196
  const nestedScopeId = `SCOPE#${funcName}:body#${module.file}#${line}`;
1153
- const arrowSemanticId = this.generateSemanticId('arrow_body', scopeCtx);
3197
+ const arrowSemanticId = this.generateSemanticId('arrow_body', scopeTracker);
1154
3198
  scopes.push({
1155
3199
  id: nestedScopeId,
1156
3200
  type: 'SCOPE',
@@ -1163,17 +3207,153 @@ export class JSASTAnalyzer extends Plugin {
1163
3207
  parentFunctionId: functionId,
1164
3208
  capturesFrom: parentScopeId
1165
3209
  });
1166
- // For arrow function, create new context with function name as semantic path
1167
- const arrowFuncCtx = {
1168
- semanticPath: scopeCtx ? `${scopeCtx.semanticPath}.${funcName}` : funcName,
1169
- siblingCounters: new Map()
3210
+ // Enter arrow function scope for semantic ID generation
3211
+ if (scopeTracker) {
3212
+ scopeTracker.enterScope(funcName, 'arrow');
3213
+ }
3214
+ this.analyzeFunctionBody(arrowPath, nestedScopeId, module, collections);
3215
+ if (scopeTracker) {
3216
+ scopeTracker.exitScope();
3217
+ }
3218
+ }
3219
+ else {
3220
+ // Arrow function with expression body (implicit return)
3221
+ // e.g., x => x * 2, () => 42
3222
+ const bodyExpr = node.body;
3223
+ const bodyLine = getLine(bodyExpr);
3224
+ const bodyColumn = getColumn(bodyExpr);
3225
+ const returnInfo = {
3226
+ parentFunctionId: functionId,
3227
+ file: module.file,
3228
+ line: bodyLine,
3229
+ column: bodyColumn,
3230
+ returnValueType: 'NONE',
3231
+ isImplicitReturn: true
1170
3232
  };
1171
- this.analyzeFunctionBody(arrowPath, nestedScopeId, module, collections, arrowFuncCtx);
3233
+ // Apply same type detection logic as ReturnStatement handler
3234
+ if (t.isIdentifier(bodyExpr)) {
3235
+ returnInfo.returnValueType = 'VARIABLE';
3236
+ returnInfo.returnValueName = bodyExpr.name;
3237
+ }
3238
+ // TemplateLiteral must come BEFORE isLiteral (TemplateLiteral extends Literal)
3239
+ else if (t.isTemplateLiteral(bodyExpr)) {
3240
+ returnInfo.returnValueType = 'EXPRESSION';
3241
+ returnInfo.expressionType = 'TemplateLiteral';
3242
+ returnInfo.returnValueLine = getLine(bodyExpr);
3243
+ returnInfo.returnValueColumn = getColumn(bodyExpr);
3244
+ returnInfo.returnValueId = NodeFactory.generateExpressionId('TemplateLiteral', module.file, getLine(bodyExpr), getColumn(bodyExpr));
3245
+ const sourceNames = [];
3246
+ for (const expr of bodyExpr.expressions) {
3247
+ if (t.isIdentifier(expr))
3248
+ sourceNames.push(expr.name);
3249
+ }
3250
+ if (sourceNames.length > 0)
3251
+ returnInfo.expressionSourceNames = sourceNames;
3252
+ }
3253
+ else if (t.isLiteral(bodyExpr)) {
3254
+ returnInfo.returnValueType = 'LITERAL';
3255
+ const literalId = `LITERAL#implicit_return#${module.file}#${line}:${column}:${literalCounterRef.value++}`;
3256
+ returnInfo.returnValueId = literalId;
3257
+ literals.push({
3258
+ id: literalId,
3259
+ type: 'LITERAL',
3260
+ value: ExpressionEvaluator.extractLiteralValue(bodyExpr),
3261
+ valueType: typeof ExpressionEvaluator.extractLiteralValue(bodyExpr),
3262
+ file: module.file,
3263
+ line: bodyLine,
3264
+ column: bodyColumn
3265
+ });
3266
+ }
3267
+ else if (t.isCallExpression(bodyExpr) && t.isIdentifier(bodyExpr.callee)) {
3268
+ returnInfo.returnValueType = 'CALL_SITE';
3269
+ returnInfo.returnValueLine = getLine(bodyExpr);
3270
+ returnInfo.returnValueColumn = getColumn(bodyExpr);
3271
+ returnInfo.returnValueCallName = bodyExpr.callee.name;
3272
+ }
3273
+ else if (t.isCallExpression(bodyExpr) && t.isMemberExpression(bodyExpr.callee)) {
3274
+ returnInfo.returnValueType = 'METHOD_CALL';
3275
+ returnInfo.returnValueLine = getLine(bodyExpr);
3276
+ returnInfo.returnValueColumn = getColumn(bodyExpr);
3277
+ if (t.isIdentifier(bodyExpr.callee.property)) {
3278
+ returnInfo.returnValueCallName = bodyExpr.callee.property.name;
3279
+ }
3280
+ }
3281
+ // REG-276: Detailed EXPRESSION handling for nested implicit arrow returns
3282
+ else if (t.isBinaryExpression(bodyExpr)) {
3283
+ returnInfo.returnValueType = 'EXPRESSION';
3284
+ returnInfo.expressionType = 'BinaryExpression';
3285
+ returnInfo.returnValueLine = getLine(bodyExpr);
3286
+ returnInfo.returnValueColumn = getColumn(bodyExpr);
3287
+ returnInfo.operator = bodyExpr.operator;
3288
+ returnInfo.returnValueId = NodeFactory.generateExpressionId('BinaryExpression', module.file, getLine(bodyExpr), getColumn(bodyExpr));
3289
+ if (t.isIdentifier(bodyExpr.left))
3290
+ returnInfo.leftSourceName = bodyExpr.left.name;
3291
+ if (t.isIdentifier(bodyExpr.right))
3292
+ returnInfo.rightSourceName = bodyExpr.right.name;
3293
+ }
3294
+ else if (t.isLogicalExpression(bodyExpr)) {
3295
+ returnInfo.returnValueType = 'EXPRESSION';
3296
+ returnInfo.expressionType = 'LogicalExpression';
3297
+ returnInfo.returnValueLine = getLine(bodyExpr);
3298
+ returnInfo.returnValueColumn = getColumn(bodyExpr);
3299
+ returnInfo.operator = bodyExpr.operator;
3300
+ returnInfo.returnValueId = NodeFactory.generateExpressionId('LogicalExpression', module.file, getLine(bodyExpr), getColumn(bodyExpr));
3301
+ if (t.isIdentifier(bodyExpr.left))
3302
+ returnInfo.leftSourceName = bodyExpr.left.name;
3303
+ if (t.isIdentifier(bodyExpr.right))
3304
+ returnInfo.rightSourceName = bodyExpr.right.name;
3305
+ }
3306
+ else if (t.isConditionalExpression(bodyExpr)) {
3307
+ returnInfo.returnValueType = 'EXPRESSION';
3308
+ returnInfo.expressionType = 'ConditionalExpression';
3309
+ returnInfo.returnValueLine = getLine(bodyExpr);
3310
+ returnInfo.returnValueColumn = getColumn(bodyExpr);
3311
+ returnInfo.returnValueId = NodeFactory.generateExpressionId('ConditionalExpression', module.file, getLine(bodyExpr), getColumn(bodyExpr));
3312
+ if (t.isIdentifier(bodyExpr.consequent))
3313
+ returnInfo.consequentSourceName = bodyExpr.consequent.name;
3314
+ if (t.isIdentifier(bodyExpr.alternate))
3315
+ returnInfo.alternateSourceName = bodyExpr.alternate.name;
3316
+ }
3317
+ else if (t.isUnaryExpression(bodyExpr)) {
3318
+ returnInfo.returnValueType = 'EXPRESSION';
3319
+ returnInfo.expressionType = 'UnaryExpression';
3320
+ returnInfo.returnValueLine = getLine(bodyExpr);
3321
+ returnInfo.returnValueColumn = getColumn(bodyExpr);
3322
+ returnInfo.operator = bodyExpr.operator;
3323
+ returnInfo.returnValueId = NodeFactory.generateExpressionId('UnaryExpression', module.file, getLine(bodyExpr), getColumn(bodyExpr));
3324
+ if (t.isIdentifier(bodyExpr.argument))
3325
+ returnInfo.unaryArgSourceName = bodyExpr.argument.name;
3326
+ }
3327
+ else if (t.isMemberExpression(bodyExpr)) {
3328
+ returnInfo.returnValueType = 'EXPRESSION';
3329
+ returnInfo.expressionType = 'MemberExpression';
3330
+ returnInfo.returnValueLine = getLine(bodyExpr);
3331
+ returnInfo.returnValueColumn = getColumn(bodyExpr);
3332
+ returnInfo.returnValueId = NodeFactory.generateExpressionId('MemberExpression', module.file, getLine(bodyExpr), getColumn(bodyExpr));
3333
+ if (t.isIdentifier(bodyExpr.object)) {
3334
+ returnInfo.object = bodyExpr.object.name;
3335
+ returnInfo.objectSourceName = bodyExpr.object.name;
3336
+ }
3337
+ if (t.isIdentifier(bodyExpr.property))
3338
+ returnInfo.property = bodyExpr.property.name;
3339
+ returnInfo.computed = bodyExpr.computed;
3340
+ }
3341
+ else {
3342
+ returnInfo.returnValueType = 'EXPRESSION';
3343
+ returnInfo.expressionType = bodyExpr.type;
3344
+ returnInfo.returnValueLine = getLine(bodyExpr);
3345
+ returnInfo.returnValueColumn = getColumn(bodyExpr);
3346
+ returnInfo.returnValueId = NodeFactory.generateExpressionId(bodyExpr.type, module.file, getLine(bodyExpr), getColumn(bodyExpr));
3347
+ }
3348
+ returnStatements.push(returnInfo);
1172
3349
  }
1173
3350
  arrowPath.skip();
1174
3351
  },
1175
3352
  UpdateExpression: (updatePath) => {
1176
3353
  const updateNode = updatePath.node;
3354
+ // REG-288/REG-312: Collect update expression info for graph building
3355
+ this.collectUpdateExpression(updateNode, module, updateExpressions, getCurrentScopeId(), scopeTracker);
3356
+ // Legacy behavior: update scope.modifies for IDENTIFIER targets
1177
3357
  if (updateNode.argument.type === 'Identifier') {
1178
3358
  const varName = updateNode.argument.name;
1179
3359
  // Find variable by name - could be from parent scope or declarations
@@ -1188,191 +3368,938 @@ export class JSASTAnalyzer extends Plugin {
1188
3368
  scope.modifies.push({
1189
3369
  variableId: variable.id,
1190
3370
  variableName: varName,
1191
- line: updateNode.loc.start.line
3371
+ line: getLine(updateNode)
1192
3372
  });
1193
3373
  }
1194
3374
  }
1195
3375
  }
1196
3376
  },
1197
3377
  // IF statements - создаём условные scope и обходим содержимое для CALL узлов
1198
- IfStatement: (ifPath) => {
1199
- const ifNode = ifPath.node;
1200
- const sourceCode = collections.code ?? '';
1201
- const condition = sourceCode.substring(ifNode.test.start, ifNode.test.end) || 'condition';
1202
- const counterId = ifScopeCounterRef.value++;
1203
- const ifScopeId = `SCOPE#if#${module.file}#${ifNode.loc.start.line}:${ifNode.loc.start.column}:${counterId}`;
1204
- // Parse condition to extract constraints
1205
- const constraints = ConditionParser.parse(ifNode.test);
1206
- const ifSemanticId = this.generateSemanticId('if_statement', scopeCtx);
1207
- scopes.push({
1208
- id: ifScopeId,
1209
- type: 'SCOPE',
1210
- scopeType: 'if_statement',
1211
- name: `if:${ifNode.loc.start.line}:${ifNode.loc.start.column}:${counterId}`,
1212
- semanticId: ifSemanticId,
1213
- conditional: true,
1214
- condition,
1215
- constraints: constraints.length > 0 ? constraints : undefined,
1216
- file: module.file,
1217
- line: ifNode.loc.start.line,
1218
- parentScopeId
1219
- });
1220
- // Обходим содержимое if-блока (consequent)
1221
- ifPath.get('consequent').traverse({
1222
- CallExpression: (callPath) => {
1223
- const callNode = callPath.node;
1224
- if (callNode.callee.type === 'Identifier') {
1225
- const nodeKey = `${callNode.start}:${callNode.end}`;
1226
- if (processedCallSites.has(nodeKey)) {
1227
- return;
1228
- }
1229
- processedCallSites.add(nodeKey);
1230
- callSites.push({
1231
- id: `CALL#${callNode.callee.name}#${module.file}#${callNode.loc.start.line}:${callNode.loc.start.column}:${callSiteCounterRef.value++}`,
1232
- type: 'CALL',
1233
- name: callNode.callee.name,
1234
- file: module.file,
1235
- line: callNode.loc.start.line,
1236
- parentScopeId: ifScopeId,
1237
- targetFunctionName: callNode.callee.name
1238
- });
1239
- }
1240
- },
1241
- NewExpression: (newPath) => {
1242
- const newNode = newPath.node;
1243
- if (newNode.callee.type === 'Identifier') {
1244
- const nodeKey = `new:${newNode.start}:${newNode.end}`;
1245
- if (processedCallSites.has(nodeKey)) {
1246
- return;
3378
+ // Phase 3 (REG-267): Now creates BRANCH nodes with branchType='if'
3379
+ IfStatement: this.createIfStatementHandler(parentScopeId, module, scopes, branches, ifScopeCounterRef, branchCounterRef, scopeTracker, collections.code ?? '', ifElseScopeMap, scopeIdStack, controlFlowState, this.countLogicalOperators.bind(this)),
3380
+ // Ternary expressions (REG-287): Creates BRANCH nodes with branchType='ternary'
3381
+ ConditionalExpression: this.createConditionalExpressionHandler(parentScopeId, module, branches, branchCounterRef, scopeTracker, scopeIdStack, controlFlowState, this.countLogicalOperators.bind(this)),
3382
+ // Track when we enter the alternate (else) block of an IfStatement
3383
+ BlockStatement: this.createBlockStatementHandler(scopeTracker, ifElseScopeMap, tryScopeMap, scopeIdStack),
3384
+ // Function call expressions
3385
+ CallExpression: (callPath) => {
3386
+ this.handleCallExpression(callPath.node, processedCallSites, processedMethodCalls, callSites, methodCalls, module, callSiteCounterRef, scopeTracker, getCurrentScopeId(), collections);
3387
+ // REG-334: Check for resolve/reject calls inside Promise executors
3388
+ const callNode = callPath.node;
3389
+ if (t.isIdentifier(callNode.callee)) {
3390
+ const calleeName = callNode.callee.name;
3391
+ // Walk up function parents to find Promise executor context
3392
+ // This handles nested callbacks like: new Promise((resolve) => { db.query((err, data) => { resolve(data); }); });
3393
+ let funcParent = callPath.getFunctionParent();
3394
+ while (funcParent) {
3395
+ const funcNode = funcParent.node;
3396
+ const funcKey = `${funcNode.start}:${funcNode.end}`;
3397
+ const context = promiseExecutorContexts.get(funcKey);
3398
+ if (context) {
3399
+ const isResolve = calleeName === context.resolveName;
3400
+ const isReject = calleeName === context.rejectName;
3401
+ if (isResolve || isReject) {
3402
+ // Find the CALL node ID for this resolve/reject call
3403
+ // It was just added by handleCallExpression
3404
+ const callLine = getLine(callNode);
3405
+ const callColumn = getColumn(callNode);
3406
+ // Find matching call site that was just added
3407
+ const resolveCall = callSites.find(cs => cs.name === calleeName &&
3408
+ cs.file === module.file &&
3409
+ cs.line === callLine &&
3410
+ cs.column === callColumn);
3411
+ if (resolveCall) {
3412
+ promiseResolutions.push({
3413
+ callId: resolveCall.id,
3414
+ constructorCallId: context.constructorCallId,
3415
+ isReject,
3416
+ file: module.file,
3417
+ line: callLine
3418
+ });
3419
+ // REG-334: Collect arguments for resolve/reject calls
3420
+ // This enables traceValues to follow PASSES_ARGUMENT edges
3421
+ if (!collections.callArguments) {
3422
+ collections.callArguments = [];
3423
+ }
3424
+ const callArgumentsArr = collections.callArguments;
3425
+ // Process arguments (typically just one: resolve(value))
3426
+ callNode.arguments.forEach((arg, argIndex) => {
3427
+ const argInfo = {
3428
+ callId: resolveCall.id,
3429
+ argIndex,
3430
+ file: module.file,
3431
+ line: getLine(arg),
3432
+ column: getColumn(arg)
3433
+ };
3434
+ // Handle different argument types
3435
+ if (t.isIdentifier(arg)) {
3436
+ argInfo.targetType = 'VARIABLE';
3437
+ argInfo.targetName = arg.name;
3438
+ }
3439
+ else if (t.isLiteral(arg) && !t.isTemplateLiteral(arg)) {
3440
+ // Create LITERAL node for the argument value
3441
+ const literalValue = ExpressionEvaluator.extractLiteralValue(arg);
3442
+ if (literalValue !== null) {
3443
+ const argLine = getLine(arg);
3444
+ const argColumn = getColumn(arg);
3445
+ const literalId = `LITERAL#arg${argIndex}#${module.file}#${argLine}:${argColumn}:${literalCounterRef.value++}`;
3446
+ literals.push({
3447
+ id: literalId,
3448
+ type: 'LITERAL',
3449
+ value: literalValue,
3450
+ valueType: typeof literalValue,
3451
+ file: module.file,
3452
+ line: argLine,
3453
+ column: argColumn,
3454
+ parentCallId: resolveCall.id,
3455
+ argIndex
3456
+ });
3457
+ argInfo.targetType = 'LITERAL';
3458
+ argInfo.targetId = literalId;
3459
+ argInfo.literalValue = literalValue;
3460
+ }
3461
+ }
3462
+ else if (t.isCallExpression(arg)) {
3463
+ argInfo.targetType = 'CALL';
3464
+ argInfo.nestedCallLine = getLine(arg);
3465
+ argInfo.nestedCallColumn = getColumn(arg);
3466
+ }
3467
+ else {
3468
+ argInfo.targetType = 'EXPRESSION';
3469
+ argInfo.expressionType = arg.type;
3470
+ }
3471
+ callArgumentsArr.push(argInfo);
3472
+ });
3473
+ }
3474
+ break; // Found context, stop searching
1247
3475
  }
1248
- processedCallSites.add(nodeKey);
1249
- callSites.push({
1250
- id: `CALL#new:${newNode.callee.name}#${module.file}#${newNode.loc.start.line}:${newNode.loc.start.column}:${callSiteCounterRef.value++}`,
1251
- type: 'CALL',
1252
- name: newNode.callee.name,
1253
- file: module.file,
1254
- line: newNode.loc.start.line,
1255
- parentScopeId: ifScopeId,
1256
- targetFunctionName: newNode.callee.name,
1257
- isNew: true
1258
- });
1259
3476
  }
3477
+ funcParent = funcParent.getFunctionParent();
1260
3478
  }
1261
- });
1262
- // Handle else branch if present
1263
- if (ifNode.alternate) {
1264
- const elseCounterId = ifScopeCounterRef.value++;
1265
- const elseScopeId = `SCOPE#else#${module.file}#${ifNode.alternate.loc.start.line}:${ifNode.alternate.loc.start.column}:${elseCounterId}`;
1266
- // Negate constraints for else branch
1267
- const negatedConstraints = constraints.length > 0 ? ConditionParser.negate(constraints) : undefined;
1268
- const elseSemanticId = this.generateSemanticId('else_statement', scopeCtx);
1269
- scopes.push({
1270
- id: elseScopeId,
1271
- type: 'SCOPE',
1272
- scopeType: 'else_statement',
1273
- name: `else:${ifNode.alternate.loc.start.line}:${ifNode.alternate.loc.start.column}:${elseCounterId}`,
1274
- semanticId: elseSemanticId,
1275
- conditional: true,
1276
- constraints: negatedConstraints,
1277
- file: module.file,
1278
- line: ifNode.alternate.loc.start.line,
1279
- parentScopeId
1280
- });
1281
- // Traverse else block
1282
- ifPath.get('alternate').traverse({
1283
- CallExpression: (callPath) => {
1284
- const callNode = callPath.node;
1285
- if (callNode.callee.type === 'Identifier') {
1286
- const nodeKey = `${callNode.start}:${callNode.end}`;
1287
- if (processedCallSites.has(nodeKey)) {
1288
- return;
3479
+ }
3480
+ },
3481
+ // NewExpression (constructor calls)
3482
+ NewExpression: (newPath) => {
3483
+ const newNode = newPath.node;
3484
+ const nodeKey = `new:${newNode.start}:${newNode.end}`;
3485
+ // Determine className from callee
3486
+ let className = null;
3487
+ if (newNode.callee.type === 'Identifier') {
3488
+ className = newNode.callee.name;
3489
+ }
3490
+ else if (newNode.callee.type === 'MemberExpression' && newNode.callee.property.type === 'Identifier') {
3491
+ className = newNode.callee.property.name;
3492
+ }
3493
+ // Create CONSTRUCTOR_CALL node (always, for all NewExpressions)
3494
+ if (className) {
3495
+ const constructorKey = `constructor:${nodeKey}`;
3496
+ if (!processedCallSites.has(constructorKey)) {
3497
+ processedCallSites.add(constructorKey);
3498
+ const line = getLine(newNode);
3499
+ const column = getColumn(newNode);
3500
+ const constructorCallId = ConstructorCallNode.generateId(className, module.file, line, column);
3501
+ const isBuiltin = ConstructorCallNode.isBuiltinConstructor(className);
3502
+ constructorCalls.push({
3503
+ id: constructorCallId,
3504
+ type: 'CONSTRUCTOR_CALL',
3505
+ className,
3506
+ isBuiltin,
3507
+ file: module.file,
3508
+ line,
3509
+ column
3510
+ });
3511
+ // REG-334: If this is Promise constructor with executor callback,
3512
+ // register the context for resolve/reject detection
3513
+ if (className === 'Promise' && newNode.arguments.length > 0) {
3514
+ const executorArg = newNode.arguments[0];
3515
+ // Only handle inline function expressions (not variable references)
3516
+ if (t.isArrowFunctionExpression(executorArg) || t.isFunctionExpression(executorArg)) {
3517
+ // Extract resolve/reject parameter names
3518
+ let resolveName;
3519
+ let rejectName;
3520
+ if (executorArg.params.length > 0 && t.isIdentifier(executorArg.params[0])) {
3521
+ resolveName = executorArg.params[0].name;
1289
3522
  }
1290
- processedCallSites.add(nodeKey);
1291
- callSites.push({
1292
- id: `CALL#${callNode.callee.name}#${module.file}#${callNode.loc.start.line}:${callNode.loc.start.column}:${callSiteCounterRef.value++}`,
1293
- type: 'CALL',
1294
- name: callNode.callee.name,
1295
- file: module.file,
1296
- line: callNode.loc.start.line,
1297
- parentScopeId: elseScopeId,
1298
- targetFunctionName: callNode.callee.name
1299
- });
1300
- }
1301
- },
1302
- NewExpression: (newPath) => {
1303
- const newNode = newPath.node;
1304
- if (newNode.callee.type === 'Identifier') {
1305
- const nodeKey = `new:${newNode.start}:${newNode.end}`;
1306
- if (processedCallSites.has(nodeKey)) {
1307
- return;
3523
+ if (executorArg.params.length > 1 && t.isIdentifier(executorArg.params[1])) {
3524
+ rejectName = executorArg.params[1].name;
3525
+ }
3526
+ if (resolveName) {
3527
+ // Key by function node position to allow nested Promise detection
3528
+ const funcKey = `${executorArg.start}:${executorArg.end}`;
3529
+ promiseExecutorContexts.set(funcKey, {
3530
+ constructorCallId,
3531
+ resolveName,
3532
+ rejectName,
3533
+ file: module.file,
3534
+ line
3535
+ });
1308
3536
  }
1309
- processedCallSites.add(nodeKey);
1310
- callSites.push({
1311
- id: `CALL#new:${newNode.callee.name}#${module.file}#${newNode.loc.start.line}:${newNode.loc.start.column}:${callSiteCounterRef.value++}`,
1312
- type: 'CALL',
1313
- name: newNode.callee.name,
1314
- file: module.file,
1315
- line: newNode.loc.start.line,
1316
- parentScopeId: elseScopeId,
1317
- targetFunctionName: newNode.callee.name,
1318
- isNew: true
1319
- });
1320
3537
  }
1321
3538
  }
3539
+ }
3540
+ }
3541
+ // Handle simple constructor: new Foo()
3542
+ if (newNode.callee.type === 'Identifier') {
3543
+ if (processedCallSites.has(nodeKey)) {
3544
+ return;
3545
+ }
3546
+ processedCallSites.add(nodeKey);
3547
+ // Generate semantic ID (primary) or legacy ID (fallback)
3548
+ const constructorName = newNode.callee.name;
3549
+ const legacyId = `CALL#new:${constructorName}#${module.file}#${getLine(newNode)}:${getColumn(newNode)}:${callSiteCounterRef.value++}`;
3550
+ let newCallId = legacyId;
3551
+ if (scopeTracker) {
3552
+ const discriminator = scopeTracker.getItemCounter(`CALL:new:${constructorName}`);
3553
+ newCallId = computeSemanticId('CALL', `new:${constructorName}`, scopeTracker.getContext(), { discriminator });
3554
+ }
3555
+ callSites.push({
3556
+ id: newCallId,
3557
+ type: 'CALL',
3558
+ name: constructorName,
3559
+ file: module.file,
3560
+ line: getLine(newNode),
3561
+ parentScopeId: getCurrentScopeId(),
3562
+ targetFunctionName: constructorName,
3563
+ isNew: true
1322
3564
  });
1323
3565
  }
1324
- // Останавливаем дальнейший обход, чтобы не обрабатывать вызовы дважды
1325
- ifPath.skip();
1326
- },
1327
- // Вызовы функций на безусловном уровне (вне if/for/while)
1328
- CallExpression: (callPath) => {
1329
- // Проверяем что вызов не внутри if/for/while (их мы обрабатываем отдельно)
1330
- const parent = callPath.parent;
1331
- if (parent.type !== 'IfStatement' && parent.type !== 'ForStatement' && parent.type !== 'WhileStatement') {
1332
- const callNode = callPath.node;
1333
- // Обычные вызовы функций (greet(), main())
1334
- if (callNode.callee.type === 'Identifier') {
1335
- const nodeKey = `${callNode.start}:${callNode.end}`;
1336
- if (processedCallSites.has(nodeKey)) {
3566
+ // Handle namespaced constructor: new ns.Constructor()
3567
+ else if (newNode.callee.type === 'MemberExpression') {
3568
+ const memberCallee = newNode.callee;
3569
+ const object = memberCallee.object;
3570
+ const property = memberCallee.property;
3571
+ if (object.type === 'Identifier' && property.type === 'Identifier') {
3572
+ if (processedMethodCalls.has(nodeKey)) {
1337
3573
  return;
1338
3574
  }
1339
- processedCallSites.add(nodeKey);
1340
- callSites.push({
1341
- id: `CALL#${callNode.callee.name}#${module.file}#${callNode.loc.start.line}:${callNode.loc.start.column}:${callSiteCounterRef.value++}`,
3575
+ processedMethodCalls.add(nodeKey);
3576
+ const objectName = object.name;
3577
+ const constructorName = property.name;
3578
+ const fullName = `${objectName}.${constructorName}`;
3579
+ // Generate semantic ID for method-style constructor call
3580
+ const legacyId = `CALL#new:${fullName}#${module.file}#${getLine(newNode)}:${getColumn(newNode)}:${callSiteCounterRef.value++}`;
3581
+ let newMethodCallId = legacyId;
3582
+ if (scopeTracker) {
3583
+ const discriminator = scopeTracker.getItemCounter(`CALL:new:${fullName}`);
3584
+ newMethodCallId = computeSemanticId('CALL', `new:${fullName}`, scopeTracker.getContext(), { discriminator });
3585
+ }
3586
+ methodCalls.push({
3587
+ id: newMethodCallId,
1342
3588
  type: 'CALL',
1343
- name: callNode.callee.name,
3589
+ name: fullName,
3590
+ object: objectName,
3591
+ method: constructorName,
1344
3592
  file: module.file,
1345
- line: callNode.loc.start.line,
1346
- parentScopeId,
1347
- targetFunctionName: callNode.callee.name
3593
+ line: getLine(newNode),
3594
+ column: getColumn(newNode),
3595
+ parentScopeId: getCurrentScopeId(),
3596
+ isNew: true
1348
3597
  });
1349
3598
  }
1350
3599
  }
1351
- },
1352
- // NewExpression на безусловном уровне
1353
- NewExpression: (newPath) => {
1354
- const parent = newPath.parent;
1355
- if (parent.type !== 'IfStatement' && parent.type !== 'ForStatement' && parent.type !== 'WhileStatement') {
1356
- const newNode = newPath.node;
1357
- if (newNode.callee.type === 'Identifier') {
1358
- const nodeKey = `new:${newNode.start}:${newNode.end}`;
1359
- if (processedCallSites.has(nodeKey)) {
1360
- return;
3600
+ }
3601
+ });
3602
+ // Phase 6 (REG-267): Attach control flow metadata to the function node
3603
+ if (matchingFunction) {
3604
+ const cyclomaticComplexity = 1 +
3605
+ controlFlowState.branchCount +
3606
+ controlFlowState.loopCount +
3607
+ controlFlowState.caseCount +
3608
+ controlFlowState.logicalOpCount;
3609
+ matchingFunction.controlFlow = {
3610
+ hasBranches: controlFlowState.branchCount > 0,
3611
+ hasLoops: controlFlowState.loopCount > 0,
3612
+ hasTryCatch: controlFlowState.hasTryCatch,
3613
+ hasEarlyReturn: controlFlowState.hasEarlyReturn,
3614
+ hasThrow: controlFlowState.hasThrow,
3615
+ cyclomaticComplexity
3616
+ };
3617
+ }
3618
+ }
3619
+ /**
3620
+ * Handle CallExpression nodes: direct function calls (greet(), main())
3621
+ * and method calls (obj.method(), data.process()).
3622
+ *
3623
+ * Handles:
3624
+ * - Direct function calls (Identifier callee) → callSites collection
3625
+ * - Method calls (MemberExpression callee) → methodCalls collection
3626
+ * - Array mutation detection (push, unshift, splice)
3627
+ * - Object.assign() detection
3628
+ *
3629
+ * @param callNode - The call expression AST node
3630
+ * @param processedCallSites - Set of already processed call site keys to avoid duplicates
3631
+ * @param processedMethodCalls - Set of already processed method call keys to avoid duplicates
3632
+ * @param callSites - Collection for direct function calls
3633
+ * @param methodCalls - Collection for method calls
3634
+ * @param module - Current module being analyzed
3635
+ * @param callSiteCounterRef - Counter for legacy ID generation
3636
+ * @param scopeTracker - Optional scope tracker for semantic ID generation
3637
+ * @param parentScopeId - ID of the parent scope containing this call
3638
+ * @param collections - Full collections object for array/object mutations
3639
+ */
3640
+ handleCallExpression(callNode, processedCallSites, processedMethodCalls, callSites, methodCalls, module, callSiteCounterRef, scopeTracker, parentScopeId, collections) {
3641
+ // Handle direct function calls (greet(), main())
3642
+ if (callNode.callee.type === 'Identifier') {
3643
+ const nodeKey = `${callNode.start}:${callNode.end}`;
3644
+ if (processedCallSites.has(nodeKey)) {
3645
+ return;
3646
+ }
3647
+ processedCallSites.add(nodeKey);
3648
+ // Generate semantic ID (primary) or legacy ID (fallback)
3649
+ const calleeName = callNode.callee.name;
3650
+ const legacyId = `CALL#${calleeName}#${module.file}#${getLine(callNode)}:${getColumn(callNode)}:${callSiteCounterRef.value++}`;
3651
+ let callId = legacyId;
3652
+ if (scopeTracker) {
3653
+ const discriminator = scopeTracker.getItemCounter(`CALL:${calleeName}`);
3654
+ callId = computeSemanticId('CALL', calleeName, scopeTracker.getContext(), { discriminator });
3655
+ }
3656
+ callSites.push({
3657
+ id: callId,
3658
+ type: 'CALL',
3659
+ name: calleeName,
3660
+ file: module.file,
3661
+ line: getLine(callNode),
3662
+ column: getColumn(callNode), // REG-223: Add column for coordinate-based lookup
3663
+ parentScopeId,
3664
+ targetFunctionName: calleeName
3665
+ });
3666
+ }
3667
+ // Handle method calls (obj.method(), data.process())
3668
+ else if (callNode.callee.type === 'MemberExpression') {
3669
+ const memberCallee = callNode.callee;
3670
+ const object = memberCallee.object;
3671
+ const property = memberCallee.property;
3672
+ const isComputed = memberCallee.computed;
3673
+ if ((object.type === 'Identifier' || object.type === 'ThisExpression') && property.type === 'Identifier') {
3674
+ const nodeKey = `${callNode.start}:${callNode.end}`;
3675
+ if (processedMethodCalls.has(nodeKey)) {
3676
+ return;
3677
+ }
3678
+ processedMethodCalls.add(nodeKey);
3679
+ const objectName = object.type === 'Identifier' ? object.name : 'this';
3680
+ const methodName = isComputed ? '<computed>' : property.name;
3681
+ const fullName = `${objectName}.${methodName}`;
3682
+ // Generate semantic ID (primary) or legacy ID (fallback)
3683
+ const legacyId = `CALL#${fullName}#${module.file}#${getLine(callNode)}:${getColumn(callNode)}:${callSiteCounterRef.value++}`;
3684
+ let methodCallId = legacyId;
3685
+ if (scopeTracker) {
3686
+ const discriminator = scopeTracker.getItemCounter(`CALL:${fullName}`);
3687
+ methodCallId = computeSemanticId('CALL', fullName, scopeTracker.getContext(), { discriminator });
3688
+ }
3689
+ methodCalls.push({
3690
+ id: methodCallId,
3691
+ type: 'CALL',
3692
+ name: fullName,
3693
+ object: objectName,
3694
+ method: methodName,
3695
+ computed: isComputed,
3696
+ computedPropertyVar: isComputed ? property.name : null,
3697
+ file: module.file,
3698
+ line: getLine(callNode),
3699
+ column: getColumn(callNode),
3700
+ parentScopeId
3701
+ });
3702
+ // Check for array mutation methods (push, unshift, splice)
3703
+ const ARRAY_MUTATION_METHODS = ['push', 'unshift', 'splice'];
3704
+ if (ARRAY_MUTATION_METHODS.includes(methodName)) {
3705
+ // Initialize collection if not exists
3706
+ if (!collections.arrayMutations) {
3707
+ collections.arrayMutations = [];
3708
+ }
3709
+ const arrayMutations = collections.arrayMutations;
3710
+ this.detectArrayMutationInFunction(callNode, objectName, methodName, module, arrayMutations, scopeTracker);
3711
+ }
3712
+ // Check for Object.assign() calls
3713
+ if (objectName === 'Object' && methodName === 'assign') {
3714
+ // Initialize collection if not exists
3715
+ if (!collections.objectMutations) {
3716
+ collections.objectMutations = [];
3717
+ }
3718
+ const objectMutations = collections.objectMutations;
3719
+ this.detectObjectAssignInFunction(callNode, module, objectMutations, scopeTracker);
3720
+ }
3721
+ }
3722
+ // REG-117: Nested array mutations like obj.arr.push(item)
3723
+ // object is MemberExpression, property is the method name
3724
+ else if (object.type === 'MemberExpression' && property.type === 'Identifier') {
3725
+ const nestedMember = object;
3726
+ const methodName = property.name;
3727
+ const ARRAY_MUTATION_METHODS = ['push', 'unshift', 'splice'];
3728
+ if (ARRAY_MUTATION_METHODS.includes(methodName)) {
3729
+ // Extract base object and property from nested MemberExpression
3730
+ const base = nestedMember.object;
3731
+ const prop = nestedMember.property;
3732
+ // Only handle single-level nesting: obj.arr.push() or this.items.push()
3733
+ if ((base.type === 'Identifier' || base.type === 'ThisExpression') &&
3734
+ !nestedMember.computed &&
3735
+ prop.type === 'Identifier') {
3736
+ const baseObjectName = base.type === 'Identifier' ? base.name : 'this';
3737
+ const propertyName = prop.name;
3738
+ // Initialize collection if not exists
3739
+ if (!collections.arrayMutations) {
3740
+ collections.arrayMutations = [];
1361
3741
  }
1362
- processedCallSites.add(nodeKey);
1363
- callSites.push({
1364
- id: `CALL#new:${newNode.callee.name}#${module.file}#${newNode.loc.start.line}:${newNode.loc.start.column}:${callSiteCounterRef.value++}`,
1365
- type: 'CALL',
1366
- name: newNode.callee.name,
1367
- file: module.file,
1368
- line: newNode.loc.start.line,
1369
- parentScopeId,
1370
- targetFunctionName: newNode.callee.name,
1371
- isNew: true
1372
- });
3742
+ const arrayMutations = collections.arrayMutations;
3743
+ this.detectArrayMutationInFunction(callNode, `${baseObjectName}.${propertyName}`, // arrayName for ID purposes
3744
+ methodName, module, arrayMutations, scopeTracker, true, // isNested
3745
+ baseObjectName, propertyName);
3746
+ }
3747
+ }
3748
+ }
3749
+ }
3750
+ }
3751
+ /**
3752
+ * Detect array mutation calls (push, unshift, splice) inside functions
3753
+ * and collect mutation info for FLOWS_INTO edge creation in GraphBuilder
3754
+ *
3755
+ * REG-117: Added isNested, baseObjectName, propertyName for nested mutations
3756
+ *
3757
+ * @param callNode - The call expression node
3758
+ * @param arrayName - Name of the array being mutated
3759
+ * @param method - The mutation method (push, unshift, splice)
3760
+ * @param module - Current module being analyzed
3761
+ * @param arrayMutations - Collection to push mutation info into
3762
+ * @param scopeTracker - Optional scope tracker for semantic IDs
3763
+ * @param isNested - REG-117: true if this is a nested mutation (obj.arr.push)
3764
+ * @param baseObjectName - REG-117: base object name for nested mutations
3765
+ * @param propertyName - REG-117: property name for nested mutations
3766
+ */
3767
+ detectArrayMutationInFunction(callNode, arrayName, method, module, arrayMutations, scopeTracker, isNested, baseObjectName, propertyName) {
3768
+ const mutationArgs = [];
3769
+ // For splice, only arguments from index 2 onwards are insertions
3770
+ // splice(start, deleteCount, item1, item2, ...)
3771
+ callNode.arguments.forEach((arg, index) => {
3772
+ // Skip start and deleteCount for splice
3773
+ if (method === 'splice' && index < 2)
3774
+ return;
3775
+ const argInfo = {
3776
+ argIndex: method === 'splice' ? index - 2 : index,
3777
+ isSpread: arg.type === 'SpreadElement',
3778
+ valueType: 'EXPRESSION' // Default
3779
+ };
3780
+ let actualArg = arg;
3781
+ if (arg.type === 'SpreadElement') {
3782
+ actualArg = arg.argument;
3783
+ }
3784
+ // Determine value type
3785
+ const literalValue = ExpressionEvaluator.extractLiteralValue(actualArg);
3786
+ if (literalValue !== null) {
3787
+ argInfo.valueType = 'LITERAL';
3788
+ argInfo.literalValue = literalValue;
3789
+ }
3790
+ else if (actualArg.type === 'Identifier') {
3791
+ argInfo.valueType = 'VARIABLE';
3792
+ argInfo.valueName = actualArg.name;
3793
+ }
3794
+ else if (actualArg.type === 'ObjectExpression') {
3795
+ argInfo.valueType = 'OBJECT_LITERAL';
3796
+ }
3797
+ else if (actualArg.type === 'ArrayExpression') {
3798
+ argInfo.valueType = 'ARRAY_LITERAL';
3799
+ }
3800
+ else if (actualArg.type === 'CallExpression') {
3801
+ argInfo.valueType = 'CALL';
3802
+ argInfo.callLine = actualArg.loc?.start.line;
3803
+ argInfo.callColumn = actualArg.loc?.start.column;
3804
+ }
3805
+ mutationArgs.push(argInfo);
3806
+ });
3807
+ // Only record if there are actual insertions
3808
+ if (mutationArgs.length > 0) {
3809
+ const line = callNode.loc?.start.line ?? 0;
3810
+ const column = callNode.loc?.start.column ?? 0;
3811
+ // Generate semantic ID for array mutation if scopeTracker available
3812
+ let mutationId;
3813
+ if (scopeTracker) {
3814
+ const discriminator = scopeTracker.getItemCounter(`ARRAY_MUTATION:${arrayName}.${method}`);
3815
+ mutationId = computeSemanticId('ARRAY_MUTATION', `${arrayName}.${method}`, scopeTracker.getContext(), { discriminator });
3816
+ }
3817
+ arrayMutations.push({
3818
+ id: mutationId,
3819
+ arrayName,
3820
+ mutationMethod: method,
3821
+ file: module.file,
3822
+ line,
3823
+ column,
3824
+ insertedValues: mutationArgs,
3825
+ // REG-117: Nested mutation fields
3826
+ isNested,
3827
+ baseObjectName,
3828
+ propertyName
3829
+ });
3830
+ }
3831
+ }
3832
+ /**
3833
+ * Detect indexed array assignment: arr[i] = value
3834
+ * Creates ArrayMutationInfo for FLOWS_INTO edge generation in GraphBuilder
3835
+ *
3836
+ * @param assignNode - The assignment expression node
3837
+ * @param module - Current module being analyzed
3838
+ * @param arrayMutations - Collection to push mutation info into
3839
+ */
3840
+ detectIndexedArrayAssignment(assignNode, module, arrayMutations, scopeTracker) {
3841
+ // Check for indexed array assignment: arr[i] = value
3842
+ if (assignNode.left.type === 'MemberExpression' && assignNode.left.computed) {
3843
+ const memberExpr = assignNode.left;
3844
+ // Only process NumericLiteral keys - those are clearly array indexed assignments
3845
+ // e.g., arr[0] = value, arr[1] = value
3846
+ // All other computed keys (StringLiteral, Identifier, expressions) are handled as object mutations
3847
+ // This avoids duplicate edge creation for ambiguous cases like obj[key] = value
3848
+ if (memberExpr.property.type !== 'NumericLiteral') {
3849
+ return;
3850
+ }
3851
+ // Get array name (only simple identifiers for now)
3852
+ if (memberExpr.object.type === 'Identifier') {
3853
+ const arrayName = memberExpr.object.name;
3854
+ const value = assignNode.right;
3855
+ const argInfo = {
3856
+ argIndex: 0,
3857
+ isSpread: false,
3858
+ valueType: 'EXPRESSION'
3859
+ };
3860
+ // Determine value type
3861
+ const literalValue = ExpressionEvaluator.extractLiteralValue(value);
3862
+ if (literalValue !== null) {
3863
+ argInfo.valueType = 'LITERAL';
3864
+ argInfo.literalValue = literalValue;
3865
+ }
3866
+ else if (value.type === 'Identifier') {
3867
+ argInfo.valueType = 'VARIABLE';
3868
+ argInfo.valueName = value.name;
3869
+ }
3870
+ else if (value.type === 'ObjectExpression') {
3871
+ argInfo.valueType = 'OBJECT_LITERAL';
3872
+ }
3873
+ else if (value.type === 'ArrayExpression') {
3874
+ argInfo.valueType = 'ARRAY_LITERAL';
3875
+ }
3876
+ else if (value.type === 'CallExpression') {
3877
+ argInfo.valueType = 'CALL';
3878
+ argInfo.callLine = value.loc?.start.line;
3879
+ argInfo.callColumn = value.loc?.start.column;
3880
+ }
3881
+ // Use defensive loc checks instead of ! assertions
3882
+ const line = assignNode.loc?.start.line ?? 0;
3883
+ const column = assignNode.loc?.start.column ?? 0;
3884
+ // Capture scope path for scope-aware lookup (REG-309)
3885
+ const scopePath = scopeTracker?.getContext().scopePath ?? [];
3886
+ arrayMutations.push({
3887
+ arrayName,
3888
+ mutationScopePath: scopePath,
3889
+ mutationMethod: 'indexed',
3890
+ file: module.file,
3891
+ line: line,
3892
+ column: column,
3893
+ insertedValues: [argInfo]
3894
+ });
3895
+ }
3896
+ }
3897
+ }
3898
+ /**
3899
+ * Detect object property assignment: obj.prop = value, obj['prop'] = value
3900
+ * Creates ObjectMutationInfo for FLOWS_INTO edge generation in GraphBuilder
3901
+ *
3902
+ * @param assignNode - The assignment expression node
3903
+ * @param module - Current module being analyzed
3904
+ * @param objectMutations - Collection to push mutation info into
3905
+ * @param scopeTracker - Optional scope tracker for semantic IDs
3906
+ */
3907
+ detectObjectPropertyAssignment(assignNode, module, objectMutations, scopeTracker) {
3908
+ // Check for property assignment: obj.prop = value or obj['prop'] = value
3909
+ if (assignNode.left.type !== 'MemberExpression')
3910
+ return;
3911
+ const memberExpr = assignNode.left;
3912
+ // Skip NumericLiteral indexed assignment (handled by array mutation handler)
3913
+ // Array mutation handler processes: arr[0] (numeric literal index)
3914
+ // Object mutation handler processes: obj.prop, obj['prop'], obj[key], obj[expr]
3915
+ if (memberExpr.computed && memberExpr.property.type === 'NumericLiteral') {
3916
+ return; // Let array mutation handler deal with this
3917
+ }
3918
+ // Get object name and enclosing class context for 'this'
3919
+ let objectName;
3920
+ let enclosingClassName;
3921
+ if (memberExpr.object.type === 'Identifier') {
3922
+ objectName = memberExpr.object.name;
3923
+ }
3924
+ else if (memberExpr.object.type === 'ThisExpression') {
3925
+ objectName = 'this';
3926
+ // REG-152: Extract enclosing class name from scope context
3927
+ if (scopeTracker) {
3928
+ enclosingClassName = scopeTracker.getEnclosingScope('CLASS');
3929
+ }
3930
+ }
3931
+ else {
3932
+ // Complex expressions like obj.nested.prop = value
3933
+ // For now, skip these (documented limitation)
3934
+ return;
3935
+ }
3936
+ // Get property name
3937
+ let propertyName;
3938
+ let mutationType;
3939
+ let computedPropertyVar;
3940
+ if (!memberExpr.computed) {
3941
+ // obj.prop
3942
+ if (memberExpr.property.type === 'Identifier') {
3943
+ propertyName = memberExpr.property.name;
3944
+ mutationType = 'property';
3945
+ }
3946
+ else {
3947
+ return; // Unexpected property type
3948
+ }
3949
+ }
3950
+ else {
3951
+ // obj['prop'] or obj[key]
3952
+ if (memberExpr.property.type === 'StringLiteral') {
3953
+ propertyName = memberExpr.property.value;
3954
+ mutationType = 'property'; // String literal is effectively a property name
3955
+ }
3956
+ else {
3957
+ propertyName = '<computed>';
3958
+ mutationType = 'computed';
3959
+ // Capture variable name for later resolution in enrichment phase
3960
+ if (memberExpr.property.type === 'Identifier') {
3961
+ computedPropertyVar = memberExpr.property.name;
3962
+ }
3963
+ }
3964
+ }
3965
+ // Extract value info
3966
+ const value = assignNode.right;
3967
+ const valueInfo = this.extractMutationValue(value);
3968
+ // Use defensive loc checks
3969
+ const line = assignNode.loc?.start.line ?? 0;
3970
+ const column = assignNode.loc?.start.column ?? 0;
3971
+ // Capture scope path for scope-aware lookup (REG-309)
3972
+ const scopePath = scopeTracker?.getContext().scopePath ?? [];
3973
+ // Generate semantic ID if scopeTracker available
3974
+ let mutationId;
3975
+ if (scopeTracker) {
3976
+ const discriminator = scopeTracker.getItemCounter(`OBJECT_MUTATION:${objectName}.${propertyName}`);
3977
+ mutationId = computeSemanticId('OBJECT_MUTATION', `${objectName}.${propertyName}`, scopeTracker.getContext(), { discriminator });
3978
+ }
3979
+ objectMutations.push({
3980
+ id: mutationId,
3981
+ objectName,
3982
+ mutationScopePath: scopePath,
3983
+ enclosingClassName, // REG-152: Class name for 'this' mutations
3984
+ propertyName,
3985
+ mutationType,
3986
+ computedPropertyVar,
3987
+ file: module.file,
3988
+ line,
3989
+ column,
3990
+ value: valueInfo
3991
+ });
3992
+ }
3993
+ /**
3994
+ * Collect update expression info for graph building (i++, obj.prop++, arr[i]++).
3995
+ *
3996
+ * REG-288: Simple identifiers (i++, --count)
3997
+ * REG-312: Member expressions (obj.prop++, arr[i]++, this.count++)
3998
+ *
3999
+ * Creates UpdateExpressionInfo entries that GraphBuilder uses to create:
4000
+ * - UPDATE_EXPRESSION nodes
4001
+ * - MODIFIES edges to target variables/objects
4002
+ * - READS_FROM self-loops
4003
+ * - CONTAINS edges for scope hierarchy
4004
+ */
4005
+ collectUpdateExpression(updateNode, module, updateExpressions, parentScopeId, scopeTracker) {
4006
+ const operator = updateNode.operator;
4007
+ const prefix = updateNode.prefix;
4008
+ const line = getLine(updateNode);
4009
+ const column = getColumn(updateNode);
4010
+ // CASE 1: Simple identifier (i++, --count) - REG-288 behavior
4011
+ if (updateNode.argument.type === 'Identifier') {
4012
+ const variableName = updateNode.argument.name;
4013
+ updateExpressions.push({
4014
+ targetType: 'IDENTIFIER',
4015
+ variableName,
4016
+ variableLine: getLine(updateNode.argument),
4017
+ operator,
4018
+ prefix,
4019
+ file: module.file,
4020
+ line,
4021
+ column,
4022
+ parentScopeId
4023
+ });
4024
+ return;
4025
+ }
4026
+ // CASE 2: Member expression (obj.prop++, arr[i]++) - REG-312 new
4027
+ if (updateNode.argument.type === 'MemberExpression') {
4028
+ const memberExpr = updateNode.argument;
4029
+ // Extract object name (reuses detectObjectPropertyAssignment pattern)
4030
+ let objectName;
4031
+ let enclosingClassName;
4032
+ if (memberExpr.object.type === 'Identifier') {
4033
+ objectName = memberExpr.object.name;
4034
+ }
4035
+ else if (memberExpr.object.type === 'ThisExpression') {
4036
+ objectName = 'this';
4037
+ // REG-152: Extract enclosing class name from scope context
4038
+ if (scopeTracker) {
4039
+ enclosingClassName = scopeTracker.getEnclosingScope('CLASS');
4040
+ }
4041
+ }
4042
+ else {
4043
+ // Complex expressions: obj.nested.prop++, (obj || fallback).count++
4044
+ // Skip for now (documented limitation, same as detectObjectPropertyAssignment)
4045
+ return;
4046
+ }
4047
+ // Extract property name (reuses detectObjectPropertyAssignment pattern)
4048
+ let propertyName;
4049
+ let mutationType;
4050
+ let computedPropertyVar;
4051
+ if (!memberExpr.computed) {
4052
+ // obj.prop++
4053
+ if (memberExpr.property.type === 'Identifier') {
4054
+ propertyName = memberExpr.property.name;
4055
+ mutationType = 'property';
4056
+ }
4057
+ else {
4058
+ return; // Unexpected property type
4059
+ }
4060
+ }
4061
+ else {
4062
+ // obj['prop']++ or obj[key]++
4063
+ if (memberExpr.property.type === 'StringLiteral') {
4064
+ // obj['prop']++ - static string
4065
+ propertyName = memberExpr.property.value;
4066
+ mutationType = 'property';
4067
+ }
4068
+ else {
4069
+ // obj[key]++, arr[i]++ - computed property
4070
+ propertyName = '<computed>';
4071
+ mutationType = 'computed';
4072
+ if (memberExpr.property.type === 'Identifier') {
4073
+ computedPropertyVar = memberExpr.property.name;
1373
4074
  }
1374
4075
  }
1375
4076
  }
4077
+ updateExpressions.push({
4078
+ targetType: 'MEMBER_EXPRESSION',
4079
+ objectName,
4080
+ objectLine: getLine(memberExpr.object),
4081
+ enclosingClassName,
4082
+ propertyName,
4083
+ mutationType,
4084
+ computedPropertyVar,
4085
+ operator,
4086
+ prefix,
4087
+ file: module.file,
4088
+ line,
4089
+ column,
4090
+ parentScopeId
4091
+ });
4092
+ }
4093
+ }
4094
+ /**
4095
+ * Detect variable reassignment for FLOWS_INTO edge creation.
4096
+ * Handles all assignment operators: =, +=, -=, *=, /=, etc.
4097
+ *
4098
+ * Captures COMPLETE metadata for:
4099
+ * - LITERAL values (literalValue field)
4100
+ * - EXPRESSION nodes (expressionType, expressionMetadata fields)
4101
+ * - VARIABLE, CALL_SITE, METHOD_CALL references
4102
+ *
4103
+ * REG-290: No deferred functionality - all value types captured.
4104
+ */
4105
+ detectVariableReassignment(assignNode, module, variableReassignments, scopeTracker) {
4106
+ // LHS must be simple identifier (checked by caller)
4107
+ const leftId = assignNode.left;
4108
+ const variableName = leftId.name;
4109
+ const operator = assignNode.operator; // '=', '+=', '-=', etc.
4110
+ // Get RHS value info
4111
+ const rightExpr = assignNode.right;
4112
+ const line = getLine(assignNode);
4113
+ const column = getColumn(assignNode);
4114
+ // Extract value source (similar to VariableVisitor pattern)
4115
+ let valueType;
4116
+ let valueName;
4117
+ let valueId = null;
4118
+ let callLine;
4119
+ let callColumn;
4120
+ // Complete metadata for node creation
4121
+ let literalValue;
4122
+ let expressionType;
4123
+ let expressionMetadata;
4124
+ // 1. Literal value
4125
+ const extractedLiteralValue = ExpressionEvaluator.extractLiteralValue(rightExpr);
4126
+ if (extractedLiteralValue !== null) {
4127
+ valueType = 'LITERAL';
4128
+ valueId = `LITERAL#${line}:${rightExpr.start}#${module.file}`;
4129
+ literalValue = extractedLiteralValue; // Store for GraphBuilder
4130
+ }
4131
+ // 2. Simple identifier (variable reference)
4132
+ else if (rightExpr.type === 'Identifier') {
4133
+ valueType = 'VARIABLE';
4134
+ valueName = rightExpr.name;
4135
+ }
4136
+ // 3. CallExpression (function call)
4137
+ else if (rightExpr.type === 'CallExpression' && rightExpr.callee.type === 'Identifier') {
4138
+ valueType = 'CALL_SITE';
4139
+ valueName = rightExpr.callee.name;
4140
+ callLine = getLine(rightExpr);
4141
+ callColumn = getColumn(rightExpr);
4142
+ }
4143
+ // 4. MemberExpression (method call: obj.method())
4144
+ else if (rightExpr.type === 'CallExpression' && rightExpr.callee.type === 'MemberExpression') {
4145
+ valueType = 'METHOD_CALL';
4146
+ callLine = getLine(rightExpr);
4147
+ callColumn = getColumn(rightExpr);
4148
+ }
4149
+ // 5. Everything else is EXPRESSION
4150
+ else {
4151
+ valueType = 'EXPRESSION';
4152
+ expressionType = rightExpr.type; // Store AST node type
4153
+ // Use correct EXPRESSION ID format: {file}:EXPRESSION:{type}:{line}:{column}
4154
+ valueId = `${module.file}:EXPRESSION:${expressionType}:${line}:${column}`;
4155
+ // Extract type-specific metadata (matches VariableAssignmentInfo pattern)
4156
+ expressionMetadata = {};
4157
+ // MemberExpression: obj.prop or obj[key]
4158
+ if (rightExpr.type === 'MemberExpression') {
4159
+ const objName = rightExpr.object.type === 'Identifier' ? rightExpr.object.name : undefined;
4160
+ const propName = rightExpr.property.type === 'Identifier' ? rightExpr.property.name : undefined;
4161
+ const computed = rightExpr.computed;
4162
+ expressionMetadata.object = objName;
4163
+ expressionMetadata.property = propName;
4164
+ expressionMetadata.computed = computed;
4165
+ // Computed property variable: obj[varName]
4166
+ if (computed && rightExpr.property.type === 'Identifier') {
4167
+ expressionMetadata.computedPropertyVar = rightExpr.property.name;
4168
+ }
4169
+ }
4170
+ // BinaryExpression: a + b, a - b, etc.
4171
+ else if (rightExpr.type === 'BinaryExpression' || rightExpr.type === 'LogicalExpression') {
4172
+ expressionMetadata.operator = rightExpr.operator;
4173
+ expressionMetadata.leftSourceName = rightExpr.left.type === 'Identifier' ? rightExpr.left.name : undefined;
4174
+ expressionMetadata.rightSourceName = rightExpr.right.type === 'Identifier' ? rightExpr.right.name : undefined;
4175
+ }
4176
+ // ConditionalExpression: condition ? a : b
4177
+ else if (rightExpr.type === 'ConditionalExpression') {
4178
+ expressionMetadata.consequentSourceName = rightExpr.consequent.type === 'Identifier' ? rightExpr.consequent.name : undefined;
4179
+ expressionMetadata.alternateSourceName = rightExpr.alternate.type === 'Identifier' ? rightExpr.alternate.name : undefined;
4180
+ }
4181
+ // Add more expression types as needed
4182
+ }
4183
+ // Capture scope path for scope-aware lookup (REG-309)
4184
+ const scopePath = scopeTracker?.getContext().scopePath ?? [];
4185
+ // Push reassignment info to collection
4186
+ variableReassignments.push({
4187
+ variableName,
4188
+ variableLine: getLine(leftId),
4189
+ mutationScopePath: scopePath,
4190
+ valueType,
4191
+ valueName,
4192
+ valueId,
4193
+ callLine,
4194
+ callColumn,
4195
+ operator,
4196
+ // Complete metadata
4197
+ literalValue,
4198
+ expressionType,
4199
+ expressionMetadata,
4200
+ file: module.file,
4201
+ line,
4202
+ column
1376
4203
  });
1377
4204
  }
4205
+ /**
4206
+ * Extract value information from an expression for mutation tracking
4207
+ */
4208
+ extractMutationValue(value) {
4209
+ const valueInfo = {
4210
+ valueType: 'EXPRESSION' // Default
4211
+ };
4212
+ const literalValue = ExpressionEvaluator.extractLiteralValue(value);
4213
+ if (literalValue !== null) {
4214
+ valueInfo.valueType = 'LITERAL';
4215
+ valueInfo.literalValue = literalValue;
4216
+ }
4217
+ else if (value.type === 'Identifier') {
4218
+ valueInfo.valueType = 'VARIABLE';
4219
+ valueInfo.valueName = value.name;
4220
+ }
4221
+ else if (value.type === 'ObjectExpression') {
4222
+ valueInfo.valueType = 'OBJECT_LITERAL';
4223
+ }
4224
+ else if (value.type === 'ArrayExpression') {
4225
+ valueInfo.valueType = 'ARRAY_LITERAL';
4226
+ }
4227
+ else if (value.type === 'CallExpression') {
4228
+ valueInfo.valueType = 'CALL';
4229
+ valueInfo.callLine = value.loc?.start.line;
4230
+ valueInfo.callColumn = value.loc?.start.column;
4231
+ }
4232
+ return valueInfo;
4233
+ }
4234
+ /**
4235
+ * Detect Object.assign() calls inside functions
4236
+ * Creates ObjectMutationInfo for FLOWS_INTO edge generation in GraphBuilder
4237
+ */
4238
+ detectObjectAssignInFunction(callNode, module, objectMutations, scopeTracker) {
4239
+ // Need at least 2 arguments: target and at least one source
4240
+ if (callNode.arguments.length < 2)
4241
+ return;
4242
+ // First argument is target
4243
+ const targetArg = callNode.arguments[0];
4244
+ let targetName;
4245
+ if (targetArg.type === 'Identifier') {
4246
+ targetName = targetArg.name;
4247
+ }
4248
+ else if (targetArg.type === 'ObjectExpression') {
4249
+ targetName = '<anonymous>';
4250
+ }
4251
+ else {
4252
+ return;
4253
+ }
4254
+ const line = callNode.loc?.start.line ?? 0;
4255
+ const column = callNode.loc?.start.column ?? 0;
4256
+ for (let i = 1; i < callNode.arguments.length; i++) {
4257
+ let arg = callNode.arguments[i];
4258
+ let isSpread = false;
4259
+ if (arg.type === 'SpreadElement') {
4260
+ isSpread = true;
4261
+ arg = arg.argument;
4262
+ }
4263
+ const valueInfo = {
4264
+ valueType: 'EXPRESSION',
4265
+ argIndex: i - 1,
4266
+ isSpread
4267
+ };
4268
+ const literalValue = ExpressionEvaluator.extractLiteralValue(arg);
4269
+ if (literalValue !== null) {
4270
+ valueInfo.valueType = 'LITERAL';
4271
+ valueInfo.literalValue = literalValue;
4272
+ }
4273
+ else if (arg.type === 'Identifier') {
4274
+ valueInfo.valueType = 'VARIABLE';
4275
+ valueInfo.valueName = arg.name;
4276
+ }
4277
+ else if (arg.type === 'ObjectExpression') {
4278
+ valueInfo.valueType = 'OBJECT_LITERAL';
4279
+ }
4280
+ else if (arg.type === 'ArrayExpression') {
4281
+ valueInfo.valueType = 'ARRAY_LITERAL';
4282
+ }
4283
+ else if (arg.type === 'CallExpression') {
4284
+ valueInfo.valueType = 'CALL';
4285
+ valueInfo.callLine = arg.loc?.start.line;
4286
+ valueInfo.callColumn = arg.loc?.start.column;
4287
+ }
4288
+ let mutationId;
4289
+ if (scopeTracker) {
4290
+ const discriminator = scopeTracker.getItemCounter(`OBJECT_MUTATION:Object.assign:${targetName}`);
4291
+ mutationId = computeSemanticId('OBJECT_MUTATION', `Object.assign:${targetName}`, scopeTracker.getContext(), { discriminator });
4292
+ }
4293
+ objectMutations.push({
4294
+ id: mutationId,
4295
+ objectName: targetName,
4296
+ propertyName: '<assign>',
4297
+ mutationType: 'assign',
4298
+ file: module.file,
4299
+ line,
4300
+ column,
4301
+ value: valueInfo
4302
+ });
4303
+ }
4304
+ }
1378
4305
  }