@grafema/core 0.1.0-alpha.5 → 0.1.1-alpha

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 (391) hide show
  1. package/README.md +0 -1
  2. package/dist/Orchestrator.d.ts +24 -2
  3. package/dist/Orchestrator.d.ts.map +1 -1
  4. package/dist/Orchestrator.js +197 -24
  5. package/dist/config/ConfigLoader.d.ts +72 -0
  6. package/dist/config/ConfigLoader.d.ts.map +1 -0
  7. package/dist/config/ConfigLoader.js +187 -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/FileNodeManager.d.ts +40 -0
  18. package/dist/core/FileNodeManager.d.ts.map +1 -0
  19. package/dist/core/FileNodeManager.js +84 -0
  20. package/dist/core/GraphFreshnessChecker.d.ts +33 -0
  21. package/dist/core/GraphFreshnessChecker.d.ts.map +1 -0
  22. package/dist/core/GraphFreshnessChecker.js +101 -0
  23. package/dist/core/HashUtils.d.ts +24 -0
  24. package/dist/core/HashUtils.d.ts.map +1 -0
  25. package/dist/core/HashUtils.js +45 -0
  26. package/dist/core/IncrementalReanalyzer.d.ts +36 -0
  27. package/dist/core/IncrementalReanalyzer.d.ts.map +1 -0
  28. package/dist/core/IncrementalReanalyzer.js +132 -0
  29. package/dist/core/NodeFactory.d.ts +225 -17
  30. package/dist/core/NodeFactory.d.ts.map +1 -1
  31. package/dist/core/NodeFactory.js +208 -18
  32. package/dist/core/ScopeTracker.d.ts +84 -0
  33. package/dist/core/ScopeTracker.d.ts.map +1 -0
  34. package/dist/core/ScopeTracker.js +116 -0
  35. package/dist/core/SemanticId.d.ts +90 -0
  36. package/dist/core/SemanticId.d.ts.map +1 -0
  37. package/dist/core/SemanticId.js +115 -0
  38. package/dist/core/VersionManager.d.ts.map +1 -1
  39. package/dist/core/VersionManager.js +3 -2
  40. package/dist/core/nodes/ArgumentExpressionNode.d.ts +43 -0
  41. package/dist/core/nodes/ArgumentExpressionNode.d.ts.map +1 -0
  42. package/dist/core/nodes/ArgumentExpressionNode.js +60 -0
  43. package/dist/core/nodes/ArrayLiteralNode.d.ts +27 -0
  44. package/dist/core/nodes/ArrayLiteralNode.d.ts.map +1 -0
  45. package/dist/core/nodes/ArrayLiteralNode.js +41 -0
  46. package/dist/core/nodes/CallSiteNode.d.ts +28 -0
  47. package/dist/core/nodes/CallSiteNode.d.ts.map +1 -1
  48. package/dist/core/nodes/CallSiteNode.js +46 -0
  49. package/dist/core/nodes/ClassNode.d.ts +33 -1
  50. package/dist/core/nodes/ClassNode.d.ts.map +1 -1
  51. package/dist/core/nodes/ClassNode.js +46 -2
  52. package/dist/core/nodes/DecoratorNode.d.ts +42 -0
  53. package/dist/core/nodes/DecoratorNode.d.ts.map +1 -0
  54. package/dist/core/nodes/DecoratorNode.js +62 -0
  55. package/dist/core/nodes/EnumNode.d.ts +42 -0
  56. package/dist/core/nodes/EnumNode.d.ts.map +1 -0
  57. package/dist/core/nodes/EnumNode.js +54 -0
  58. package/dist/core/nodes/ExportNode.d.ts +37 -1
  59. package/dist/core/nodes/ExportNode.d.ts.map +1 -1
  60. package/dist/core/nodes/ExportNode.js +48 -2
  61. package/dist/core/nodes/ExpressionNode.d.ts +97 -0
  62. package/dist/core/nodes/ExpressionNode.d.ts.map +1 -0
  63. package/dist/core/nodes/ExpressionNode.js +178 -0
  64. package/dist/core/nodes/ExternalModuleNode.d.ts +28 -0
  65. package/dist/core/nodes/ExternalModuleNode.d.ts.map +1 -0
  66. package/dist/core/nodes/ExternalModuleNode.js +41 -0
  67. package/dist/core/nodes/ExternalStdioNode.d.ts +13 -6
  68. package/dist/core/nodes/ExternalStdioNode.d.ts.map +1 -1
  69. package/dist/core/nodes/ExternalStdioNode.js +15 -8
  70. package/dist/core/nodes/FunctionNode.d.ts +36 -0
  71. package/dist/core/nodes/FunctionNode.d.ts.map +1 -1
  72. package/dist/core/nodes/FunctionNode.js +80 -1
  73. package/dist/core/nodes/ImportNode.d.ts +19 -5
  74. package/dist/core/nodes/ImportNode.d.ts.map +1 -1
  75. package/dist/core/nodes/ImportNode.js +23 -5
  76. package/dist/core/nodes/InterfaceNode.d.ts +46 -0
  77. package/dist/core/nodes/InterfaceNode.d.ts.map +1 -0
  78. package/dist/core/nodes/InterfaceNode.js +55 -0
  79. package/dist/core/nodes/IssueNode.d.ts +73 -0
  80. package/dist/core/nodes/IssueNode.d.ts.map +1 -0
  81. package/dist/core/nodes/IssueNode.js +129 -0
  82. package/dist/core/nodes/MethodCallNode.d.ts +30 -0
  83. package/dist/core/nodes/MethodCallNode.d.ts.map +1 -1
  84. package/dist/core/nodes/MethodCallNode.js +49 -0
  85. package/dist/core/nodes/MethodNode.d.ts +32 -0
  86. package/dist/core/nodes/MethodNode.d.ts.map +1 -1
  87. package/dist/core/nodes/MethodNode.js +48 -0
  88. package/dist/core/nodes/ModuleNode.d.ts +31 -0
  89. package/dist/core/nodes/ModuleNode.d.ts.map +1 -1
  90. package/dist/core/nodes/ModuleNode.js +37 -0
  91. package/dist/core/nodes/NetworkRequestNode.d.ts +54 -0
  92. package/dist/core/nodes/NetworkRequestNode.d.ts.map +1 -0
  93. package/dist/core/nodes/NetworkRequestNode.js +65 -0
  94. package/dist/core/nodes/ObjectLiteralNode.d.ts +27 -0
  95. package/dist/core/nodes/ObjectLiteralNode.d.ts.map +1 -0
  96. package/dist/core/nodes/ObjectLiteralNode.js +41 -0
  97. package/dist/core/nodes/ScopeNode.d.ts +31 -0
  98. package/dist/core/nodes/ScopeNode.d.ts.map +1 -1
  99. package/dist/core/nodes/ScopeNode.js +49 -0
  100. package/dist/core/nodes/TypeNode.d.ts +36 -0
  101. package/dist/core/nodes/TypeNode.d.ts.map +1 -0
  102. package/dist/core/nodes/TypeNode.js +53 -0
  103. package/dist/core/nodes/VariableDeclarationNode.d.ts +27 -0
  104. package/dist/core/nodes/VariableDeclarationNode.d.ts.map +1 -1
  105. package/dist/core/nodes/VariableDeclarationNode.js +40 -0
  106. package/dist/core/nodes/index.d.ts +12 -1
  107. package/dist/core/nodes/index.d.ts.map +1 -1
  108. package/dist/core/nodes/index.js +14 -0
  109. package/dist/diagnostics/DiagnosticCollector.d.ts +98 -0
  110. package/dist/diagnostics/DiagnosticCollector.d.ts.map +1 -0
  111. package/dist/diagnostics/DiagnosticCollector.js +129 -0
  112. package/dist/diagnostics/DiagnosticReporter.d.ts +77 -0
  113. package/dist/diagnostics/DiagnosticReporter.d.ts.map +1 -0
  114. package/dist/diagnostics/DiagnosticReporter.js +159 -0
  115. package/dist/diagnostics/DiagnosticWriter.d.ts +31 -0
  116. package/dist/diagnostics/DiagnosticWriter.d.ts.map +1 -0
  117. package/dist/diagnostics/DiagnosticWriter.js +43 -0
  118. package/dist/diagnostics/index.d.ts +14 -0
  119. package/dist/diagnostics/index.d.ts.map +1 -0
  120. package/dist/diagnostics/index.js +11 -0
  121. package/dist/errors/GrafemaError.d.ts +118 -0
  122. package/dist/errors/GrafemaError.d.ts.map +1 -0
  123. package/dist/errors/GrafemaError.js +131 -0
  124. package/dist/index.d.ts +57 -1
  125. package/dist/index.d.ts.map +1 -1
  126. package/dist/index.js +54 -1
  127. package/dist/logging/Logger.d.ts +48 -0
  128. package/dist/logging/Logger.d.ts.map +1 -0
  129. package/dist/logging/Logger.js +134 -0
  130. package/dist/plugins/Plugin.d.ts +5 -1
  131. package/dist/plugins/Plugin.d.ts.map +1 -1
  132. package/dist/plugins/Plugin.js +33 -0
  133. package/dist/plugins/analysis/DatabaseAnalyzer.d.ts.map +1 -1
  134. package/dist/plugins/analysis/DatabaseAnalyzer.js +13 -6
  135. package/dist/plugins/analysis/ExpressAnalyzer.d.ts.map +1 -1
  136. package/dist/plugins/analysis/ExpressAnalyzer.js +27 -19
  137. package/dist/plugins/analysis/ExpressRouteAnalyzer.d.ts.map +1 -1
  138. package/dist/plugins/analysis/ExpressRouteAnalyzer.js +21 -14
  139. package/dist/plugins/analysis/FetchAnalyzer.d.ts +1 -0
  140. package/dist/plugins/analysis/FetchAnalyzer.d.ts.map +1 -1
  141. package/dist/plugins/analysis/FetchAnalyzer.js +34 -14
  142. package/dist/plugins/analysis/IncrementalAnalysisPlugin.d.ts +6 -3
  143. package/dist/plugins/analysis/IncrementalAnalysisPlugin.d.ts.map +1 -1
  144. package/dist/plugins/analysis/IncrementalAnalysisPlugin.js +76 -80
  145. package/dist/plugins/analysis/JSASTAnalyzer.d.ts +180 -17
  146. package/dist/plugins/analysis/JSASTAnalyzer.d.ts.map +1 -1
  147. package/dist/plugins/analysis/JSASTAnalyzer.js +1171 -471
  148. package/dist/plugins/analysis/ReactAnalyzer.d.ts.map +1 -1
  149. package/dist/plugins/analysis/ReactAnalyzer.js +56 -57
  150. package/dist/plugins/analysis/RustAnalyzer.d.ts.map +1 -1
  151. package/dist/plugins/analysis/RustAnalyzer.js +15 -10
  152. package/dist/plugins/analysis/SQLiteAnalyzer.d.ts.map +1 -1
  153. package/dist/plugins/analysis/SQLiteAnalyzer.js +9 -7
  154. package/dist/plugins/analysis/ServiceLayerAnalyzer.d.ts.map +1 -1
  155. package/dist/plugins/analysis/ServiceLayerAnalyzer.js +21 -9
  156. package/dist/plugins/analysis/SocketIOAnalyzer.d.ts.map +1 -1
  157. package/dist/plugins/analysis/SocketIOAnalyzer.js +27 -15
  158. package/dist/plugins/analysis/SystemDbAnalyzer.d.ts.map +1 -1
  159. package/dist/plugins/analysis/SystemDbAnalyzer.js +15 -5
  160. package/dist/plugins/analysis/ast/GraphBuilder.d.ts +34 -4
  161. package/dist/plugins/analysis/ast/GraphBuilder.d.ts.map +1 -1
  162. package/dist/plugins/analysis/ast/GraphBuilder.js +318 -298
  163. package/dist/plugins/analysis/ast/IdGenerator.d.ts +105 -0
  164. package/dist/plugins/analysis/ast/IdGenerator.d.ts.map +1 -0
  165. package/dist/plugins/analysis/ast/IdGenerator.js +116 -0
  166. package/dist/plugins/analysis/ast/types.d.ts +176 -5
  167. package/dist/plugins/analysis/ast/types.d.ts.map +1 -1
  168. package/dist/plugins/analysis/ast/utils/createParameterNodes.d.ts +33 -0
  169. package/dist/plugins/analysis/ast/utils/createParameterNodes.d.ts.map +1 -0
  170. package/dist/plugins/analysis/ast/utils/createParameterNodes.js +89 -0
  171. package/dist/plugins/analysis/ast/utils/index.d.ts +6 -0
  172. package/dist/plugins/analysis/ast/utils/index.d.ts.map +1 -0
  173. package/dist/plugins/analysis/ast/utils/index.js +5 -0
  174. package/dist/plugins/analysis/ast/utils/location.d.ts +87 -0
  175. package/dist/plugins/analysis/ast/utils/location.d.ts.map +1 -0
  176. package/dist/plugins/analysis/ast/utils/location.js +78 -0
  177. package/dist/plugins/analysis/ast/visitors/ASTVisitor.d.ts +9 -4
  178. package/dist/plugins/analysis/ast/visitors/ASTVisitor.d.ts.map +1 -1
  179. package/dist/plugins/analysis/ast/visitors/ASTVisitor.js +6 -5
  180. package/dist/plugins/analysis/ast/visitors/CallExpressionVisitor.d.ts +99 -9
  181. package/dist/plugins/analysis/ast/visitors/CallExpressionVisitor.d.ts.map +1 -1
  182. package/dist/plugins/analysis/ast/visitors/CallExpressionVisitor.js +663 -125
  183. package/dist/plugins/analysis/ast/visitors/ClassVisitor.d.ts +4 -1
  184. package/dist/plugins/analysis/ast/visitors/ClassVisitor.d.ts.map +1 -1
  185. package/dist/plugins/analysis/ast/visitors/ClassVisitor.js +72 -32
  186. package/dist/plugins/analysis/ast/visitors/FunctionVisitor.d.ts +4 -1
  187. package/dist/plugins/analysis/ast/visitors/FunctionVisitor.d.ts.map +1 -1
  188. package/dist/plugins/analysis/ast/visitors/FunctionVisitor.js +128 -63
  189. package/dist/plugins/analysis/ast/visitors/ImportExportVisitor.d.ts.map +1 -1
  190. package/dist/plugins/analysis/ast/visitors/ImportExportVisitor.js +11 -8
  191. package/dist/plugins/analysis/ast/visitors/TypeScriptVisitor.d.ts +12 -1
  192. package/dist/plugins/analysis/ast/visitors/TypeScriptVisitor.d.ts.map +1 -1
  193. package/dist/plugins/analysis/ast/visitors/TypeScriptVisitor.js +36 -14
  194. package/dist/plugins/analysis/ast/visitors/VariableVisitor.d.ts +4 -1
  195. package/dist/plugins/analysis/ast/visitors/VariableVisitor.d.ts.map +1 -1
  196. package/dist/plugins/analysis/ast/visitors/VariableVisitor.js +17 -13
  197. package/dist/plugins/discovery/MonorepoServiceDiscovery.d.ts.map +1 -1
  198. package/dist/plugins/discovery/MonorepoServiceDiscovery.js +3 -2
  199. package/dist/plugins/discovery/SimpleProjectDiscovery.d.ts.map +1 -1
  200. package/dist/plugins/discovery/SimpleProjectDiscovery.js +5 -1
  201. package/dist/plugins/discovery/WorkspaceDiscovery.d.ts +22 -0
  202. package/dist/plugins/discovery/WorkspaceDiscovery.d.ts.map +1 -0
  203. package/dist/plugins/discovery/WorkspaceDiscovery.js +136 -0
  204. package/dist/plugins/discovery/resolveSourceEntrypoint.d.ts +46 -0
  205. package/dist/plugins/discovery/resolveSourceEntrypoint.d.ts.map +1 -0
  206. package/dist/plugins/discovery/resolveSourceEntrypoint.js +86 -0
  207. package/dist/plugins/discovery/workspaces/detector.d.ts +21 -0
  208. package/dist/plugins/discovery/workspaces/detector.d.ts.map +1 -0
  209. package/dist/plugins/discovery/workspaces/detector.js +49 -0
  210. package/dist/plugins/discovery/workspaces/globResolver.d.ts +35 -0
  211. package/dist/plugins/discovery/workspaces/globResolver.d.ts.map +1 -0
  212. package/dist/plugins/discovery/workspaces/globResolver.js +184 -0
  213. package/dist/plugins/discovery/workspaces/index.d.ts +9 -0
  214. package/dist/plugins/discovery/workspaces/index.d.ts.map +1 -0
  215. package/dist/plugins/discovery/workspaces/index.js +8 -0
  216. package/dist/plugins/discovery/workspaces/parsers.d.ts +38 -0
  217. package/dist/plugins/discovery/workspaces/parsers.d.ts.map +1 -0
  218. package/dist/plugins/discovery/workspaces/parsers.js +80 -0
  219. package/dist/plugins/enrichment/AliasTracker.d.ts.map +1 -1
  220. package/dist/plugins/enrichment/AliasTracker.js +14 -8
  221. package/dist/plugins/enrichment/HTTPConnectionEnricher.d.ts.map +1 -1
  222. package/dist/plugins/enrichment/HTTPConnectionEnricher.js +14 -7
  223. package/dist/plugins/enrichment/ImportExportLinker.d.ts.map +1 -1
  224. package/dist/plugins/enrichment/ImportExportLinker.js +23 -6
  225. package/dist/plugins/enrichment/MethodCallResolver.d.ts.map +1 -1
  226. package/dist/plugins/enrichment/MethodCallResolver.js +18 -12
  227. package/dist/plugins/enrichment/MountPointResolver.d.ts.map +1 -1
  228. package/dist/plugins/enrichment/MountPointResolver.js +8 -3
  229. package/dist/plugins/enrichment/PrefixEvaluator.d.ts.map +1 -1
  230. package/dist/plugins/enrichment/PrefixEvaluator.js +16 -7
  231. package/dist/plugins/enrichment/RustFFIEnricher.d.ts.map +1 -1
  232. package/dist/plugins/enrichment/RustFFIEnricher.js +6 -5
  233. package/dist/plugins/enrichment/ValueDomainAnalyzer.d.ts +17 -0
  234. package/dist/plugins/enrichment/ValueDomainAnalyzer.d.ts.map +1 -1
  235. package/dist/plugins/enrichment/ValueDomainAnalyzer.js +129 -10
  236. package/dist/plugins/indexing/IncrementalModuleIndexer.d.ts.map +1 -1
  237. package/dist/plugins/indexing/IncrementalModuleIndexer.js +23 -14
  238. package/dist/plugins/indexing/JSModuleIndexer.d.ts.map +1 -1
  239. package/dist/plugins/indexing/JSModuleIndexer.js +63 -31
  240. package/dist/plugins/indexing/RustModuleIndexer.d.ts.map +1 -1
  241. package/dist/plugins/indexing/RustModuleIndexer.js +5 -4
  242. package/dist/plugins/indexing/ServiceDetector.d.ts +10 -0
  243. package/dist/plugins/indexing/ServiceDetector.d.ts.map +1 -1
  244. package/dist/plugins/indexing/ServiceDetector.js +28 -15
  245. package/dist/plugins/validation/CallResolverValidator.d.ts.map +1 -1
  246. package/dist/plugins/validation/CallResolverValidator.js +8 -7
  247. package/dist/plugins/validation/DataFlowValidator.d.ts.map +1 -1
  248. package/dist/plugins/validation/DataFlowValidator.js +17 -12
  249. package/dist/plugins/validation/EvalBanValidator.d.ts.map +1 -1
  250. package/dist/plugins/validation/EvalBanValidator.js +17 -16
  251. package/dist/plugins/validation/GraphConnectivityValidator.d.ts.map +1 -1
  252. package/dist/plugins/validation/GraphConnectivityValidator.js +19 -23
  253. package/dist/plugins/validation/NodeCreationValidator.d.ts +85 -0
  254. package/dist/plugins/validation/NodeCreationValidator.d.ts.map +1 -0
  255. package/dist/plugins/validation/NodeCreationValidator.js +415 -0
  256. package/dist/plugins/validation/SQLInjectionValidator.d.ts.map +1 -1
  257. package/dist/plugins/validation/SQLInjectionValidator.js +59 -16
  258. package/dist/plugins/validation/ShadowingDetector.d.ts.map +1 -1
  259. package/dist/plugins/validation/ShadowingDetector.js +6 -5
  260. package/dist/plugins/validation/TypeScriptDeadCodeValidator.d.ts.map +1 -1
  261. package/dist/plugins/validation/TypeScriptDeadCodeValidator.js +12 -11
  262. package/dist/plugins/vcs/GitPlugin.d.ts.map +1 -1
  263. package/dist/plugins/vcs/GitPlugin.js +10 -12
  264. package/dist/plugins/vcs/VCSPlugin.d.ts +3 -2
  265. package/dist/plugins/vcs/VCSPlugin.d.ts.map +1 -1
  266. package/dist/plugins/vcs/VCSPlugin.js +5 -5
  267. package/dist/storage/backends/RFDBServerBackend.d.ts +10 -17
  268. package/dist/storage/backends/RFDBServerBackend.d.ts.map +1 -1
  269. package/dist/storage/backends/RFDBServerBackend.js +31 -10
  270. package/dist/validation/PathValidator.d.ts +1 -2
  271. package/dist/validation/PathValidator.d.ts.map +1 -1
  272. package/package.json +3 -3
  273. package/src/Orchestrator.ts +237 -24
  274. package/src/config/ConfigLoader.ts +263 -0
  275. package/src/config/index.ts +5 -0
  276. package/src/core/ASTWorker.ts +143 -139
  277. package/src/core/CoverageAnalyzer.ts +243 -0
  278. package/src/core/FileNodeManager.ts +100 -0
  279. package/src/core/GraphFreshnessChecker.ts +143 -0
  280. package/src/core/HashUtils.ts +48 -0
  281. package/src/core/IncrementalReanalyzer.ts +192 -0
  282. package/src/core/NodeFactory.ts +401 -18
  283. package/src/core/ScopeTracker.ts +154 -0
  284. package/src/core/SemanticId.ts +192 -0
  285. package/src/core/VersionManager.ts +3 -2
  286. package/src/core/nodes/ArgumentExpressionNode.ts +89 -0
  287. package/src/core/nodes/ArrayLiteralNode.ts +65 -0
  288. package/src/core/nodes/CallSiteNode.ts +58 -0
  289. package/src/core/nodes/ClassNode.ts +63 -2
  290. package/src/core/nodes/DecoratorNode.ts +91 -0
  291. package/src/core/nodes/EnumNode.ts +86 -0
  292. package/src/core/nodes/ExportNode.ts +70 -2
  293. package/src/core/nodes/ExpressionNode.ts +231 -0
  294. package/src/core/nodes/ExternalModuleNode.ts +56 -0
  295. package/src/core/nodes/ExternalStdioNode.ts +17 -9
  296. package/src/core/nodes/FunctionNode.ts +101 -1
  297. package/src/core/nodes/ImportNode.ts +32 -10
  298. package/src/core/nodes/InterfaceNode.ts +91 -0
  299. package/src/core/nodes/IssueNode.ts +177 -0
  300. package/src/core/nodes/MethodCallNode.ts +64 -0
  301. package/src/core/nodes/MethodNode.ts +63 -0
  302. package/src/core/nodes/ModuleNode.ts +50 -0
  303. package/src/core/nodes/NetworkRequestNode.ts +77 -0
  304. package/src/core/nodes/ObjectLiteralNode.ts +65 -0
  305. package/src/core/nodes/ScopeNode.ts +65 -0
  306. package/src/core/nodes/TypeNode.ts +78 -0
  307. package/src/core/nodes/VariableDeclarationNode.ts +52 -0
  308. package/src/core/nodes/index.ts +18 -1
  309. package/src/diagnostics/DiagnosticCollector.ts +163 -0
  310. package/src/diagnostics/DiagnosticReporter.ts +204 -0
  311. package/src/diagnostics/DiagnosticWriter.ts +50 -0
  312. package/src/diagnostics/index.ts +16 -0
  313. package/src/errors/GrafemaError.ts +174 -0
  314. package/src/index.ts +148 -1
  315. package/src/logging/Logger.ts +152 -0
  316. package/src/plugins/Plugin.ts +42 -0
  317. package/src/plugins/analysis/DatabaseAnalyzer.ts +14 -8
  318. package/src/plugins/analysis/ExpressAnalyzer.ts +29 -19
  319. package/src/plugins/analysis/ExpressRouteAnalyzer.ts +22 -21
  320. package/src/plugins/analysis/FetchAnalyzer.ts +39 -16
  321. package/src/plugins/analysis/IncrementalAnalysisPlugin.ts +84 -101
  322. package/src/plugins/analysis/JSASTAnalyzer.ts +1483 -503
  323. package/src/plugins/analysis/ReactAnalyzer.ts +57 -57
  324. package/src/plugins/analysis/RustAnalyzer.ts +15 -10
  325. package/src/plugins/analysis/SQLiteAnalyzer.ts +10 -7
  326. package/src/plugins/analysis/ServiceLayerAnalyzer.ts +22 -16
  327. package/src/plugins/analysis/SocketIOAnalyzer.ts +31 -22
  328. package/src/plugins/analysis/SystemDbAnalyzer.ts +16 -11
  329. package/src/plugins/analysis/ast/GraphBuilder.ts +439 -327
  330. package/src/plugins/analysis/ast/IdGenerator.ts +177 -0
  331. package/src/plugins/analysis/ast/types.ts +209 -6
  332. package/src/plugins/analysis/ast/utils/createParameterNodes.ts +104 -0
  333. package/src/plugins/analysis/ast/utils/index.ts +12 -0
  334. package/src/plugins/analysis/ast/utils/location.ts +103 -0
  335. package/src/plugins/analysis/ast/visitors/ASTVisitor.ts +11 -8
  336. package/src/plugins/analysis/ast/visitors/CallExpressionVisitor.ts +909 -83
  337. package/src/plugins/analysis/ast/visitors/ClassVisitor.ts +97 -44
  338. package/src/plugins/analysis/ast/visitors/FunctionVisitor.ts +159 -93
  339. package/src/plugins/analysis/ast/visitors/ImportExportVisitor.ts +12 -8
  340. package/src/plugins/analysis/ast/visitors/TypeScriptVisitor.ts +41 -14
  341. package/src/plugins/analysis/ast/visitors/VariableVisitor.ts +37 -17
  342. package/src/plugins/discovery/MonorepoServiceDiscovery.ts +3 -2
  343. package/src/plugins/discovery/SimpleProjectDiscovery.ts +6 -1
  344. package/src/plugins/discovery/WorkspaceDiscovery.ts +177 -0
  345. package/src/plugins/discovery/resolveSourceEntrypoint.ts +103 -0
  346. package/src/plugins/discovery/workspaces/detector.ts +63 -0
  347. package/src/plugins/discovery/workspaces/globResolver.ts +229 -0
  348. package/src/plugins/discovery/workspaces/index.ts +23 -0
  349. package/src/plugins/discovery/workspaces/parsers.ts +99 -0
  350. package/src/plugins/enrichment/AliasTracker.ts +14 -8
  351. package/src/plugins/enrichment/HTTPConnectionEnricher.ts +14 -7
  352. package/src/plugins/enrichment/ImportExportLinker.ts +24 -6
  353. package/src/plugins/enrichment/MethodCallResolver.ts +18 -12
  354. package/src/plugins/enrichment/MountPointResolver.ts +8 -3
  355. package/src/plugins/enrichment/PrefixEvaluator.ts +16 -7
  356. package/src/plugins/enrichment/RustFFIEnricher.ts +6 -5
  357. package/src/plugins/enrichment/ValueDomainAnalyzer.ts +149 -12
  358. package/src/plugins/indexing/IncrementalModuleIndexer.ts +23 -14
  359. package/src/plugins/indexing/JSModuleIndexer.ts +74 -34
  360. package/src/plugins/indexing/RustModuleIndexer.ts +5 -4
  361. package/src/plugins/validation/CallResolverValidator.ts +8 -7
  362. package/src/plugins/validation/DataFlowValidator.ts +16 -12
  363. package/src/plugins/validation/EvalBanValidator.ts +17 -16
  364. package/src/plugins/validation/GraphConnectivityValidator.ts +19 -23
  365. package/src/plugins/validation/NodeCreationValidator.ts +554 -0
  366. package/src/plugins/validation/SQLInjectionValidator.ts +61 -15
  367. package/src/plugins/validation/ShadowingDetector.ts +6 -5
  368. package/src/plugins/validation/TypeScriptDeadCodeValidator.ts +12 -11
  369. package/src/plugins/vcs/GitPlugin.ts +40 -12
  370. package/src/plugins/vcs/VCSPlugin.ts +7 -5
  371. package/src/storage/backends/RFDBServerBackend.ts +43 -29
  372. package/src/validation/PathValidator.ts +1 -1
  373. package/dist/core/AnalysisWorker.d.ts +0 -14
  374. package/dist/core/AnalysisWorker.d.ts.map +0 -1
  375. package/dist/core/AnalysisWorker.js +0 -307
  376. package/dist/core/ParallelAnalyzer.d.ts +0 -120
  377. package/dist/core/ParallelAnalyzer.d.ts.map +0 -1
  378. package/dist/core/ParallelAnalyzer.js +0 -331
  379. package/dist/core/QueueWorker.d.ts +0 -12
  380. package/dist/core/QueueWorker.d.ts.map +0 -1
  381. package/dist/core/QueueWorker.js +0 -567
  382. package/dist/core/RFDBClient.d.ts +0 -179
  383. package/dist/core/RFDBClient.d.ts.map +0 -1
  384. package/dist/core/RFDBClient.js +0 -429
  385. package/dist/plugins/discovery/ZonServiceDiscovery.d.ts +0 -19
  386. package/dist/plugins/discovery/ZonServiceDiscovery.d.ts.map +0 -1
  387. package/dist/plugins/discovery/ZonServiceDiscovery.js +0 -204
  388. package/src/core/AnalysisWorker.ts +0 -410
  389. package/src/core/ParallelAnalyzer.ts +0 -476
  390. package/src/core/QueueWorker.ts +0 -780
  391. package/src/plugins/indexing/ServiceDetector.ts +0 -230
@@ -5,6 +5,7 @@
5
5
 
6
6
  import { readFileSync } from 'fs';
7
7
  import { createHash } from 'crypto';
8
+ import { basename } from 'path';
8
9
  import { parse } from '@babel/parser';
9
10
  import traverseModule from '@babel/traverse';
10
11
  import type { NodePath, TraverseOptions } from '@babel/traverse';
@@ -46,8 +47,13 @@ import {
46
47
  import { Task } from '../../core/Task.js';
47
48
  import { PriorityQueue } from '../../core/PriorityQueue.js';
48
49
  import { WorkerPool } from '../../core/WorkerPool.js';
50
+ import { ASTWorkerPool, type ModuleInfo as ASTModuleInfo, type ParseResult } from '../../core/ASTWorkerPool.js';
49
51
  import { ConditionParser } from './ast/ConditionParser.js';
52
+ import { getLine, getColumn } from './ast/utils/location.js';
50
53
  import { Profiler } from '../../core/Profiler.js';
54
+ import { ScopeTracker } from '../../core/ScopeTracker.js';
55
+ import { computeSemanticId } from '../../core/SemanticId.js';
56
+ import { ExpressionNode } from '../../core/nodes/ExpressionNode.js';
51
57
  import type { PluginContext, PluginResult, PluginMetadata, GraphBackend } from '@grafema/types';
52
58
  import type {
53
59
  ModuleNode,
@@ -71,6 +77,14 @@ import type {
71
77
  TypeAliasInfo,
72
78
  EnumDeclarationInfo,
73
79
  DecoratorInfo,
80
+ ObjectLiteralInfo,
81
+ ObjectPropertyInfo,
82
+ ArrayLiteralInfo,
83
+ ArrayElementInfo,
84
+ ArrayMutationInfo,
85
+ ArrayMutationArgument,
86
+ ObjectMutationInfo,
87
+ ObjectMutationValue,
74
88
  CounterRef,
75
89
  ProcessedNodes,
76
90
  ASTCollections,
@@ -79,14 +93,8 @@ import type {
79
93
 
80
94
  // === LOCAL TYPES ===
81
95
 
82
- /**
83
- * Context for tracking semantic IDs within a scope hierarchy.
84
- * Used to generate stable, line-number-independent IDs for SCOPE nodes.
85
- */
86
- interface ScopeContext {
87
- semanticPath: string; // "ClassName.method" or "funcName"
88
- siblingCounters: Map<string, number>; // scopeType → count for this level
89
- }
96
+ // Note: Legacy ScopeContext interface removed in REG-141
97
+ // Semantic ID generation now uses ScopeTracker exclusively
90
98
 
91
99
  // Internal Collections with required fields (ASTCollections has optional for GraphBuilder)
92
100
  interface Collections {
@@ -111,6 +119,17 @@ interface Collections {
111
119
  typeAliases: TypeAliasInfo[];
112
120
  enums: EnumDeclarationInfo[];
113
121
  decorators: DecoratorInfo[];
122
+ // Object/Array literal tracking
123
+ objectLiterals: ObjectLiteralInfo[];
124
+ objectProperties: ObjectPropertyInfo[];
125
+ arrayLiterals: ArrayLiteralInfo[];
126
+ arrayElements: ArrayElementInfo[];
127
+ // Array mutation tracking for FLOWS_INTO edges
128
+ arrayMutations: ArrayMutationInfo[];
129
+ // Object mutation tracking for FLOWS_INTO edges
130
+ objectMutations: ObjectMutationInfo[];
131
+ objectLiteralCounterRef: CounterRef;
132
+ arrayLiteralCounterRef: CounterRef;
114
133
  ifScopeCounterRef: CounterRef;
115
134
  scopeCounterRef: CounterRef;
116
135
  varDeclCounterRef: CounterRef;
@@ -120,7 +139,6 @@ interface Collections {
120
139
  literalCounterRef: CounterRef;
121
140
  anonymousFunctionCounterRef: CounterRef;
122
141
  processedNodes: ProcessedNodes;
123
- moduleScopeCtx?: ScopeContext;
124
142
  code?: string;
125
143
  // VisitorCollections compatibility
126
144
  classes: ClassDeclarationInfo[];
@@ -128,6 +146,8 @@ interface Collections {
128
146
  variables: VariableDeclarationInfo[];
129
147
  sideEffects: unknown[]; // TODO: define SideEffectInfo
130
148
  variableCounterRef: CounterRef;
149
+ // ScopeTracker for semantic ID generation
150
+ scopeTracker?: ScopeTracker;
131
151
  [key: string]: unknown;
132
152
  }
133
153
 
@@ -140,6 +160,8 @@ interface AnalyzeContext extends PluginContext {
140
160
  manifest?: AnalysisManifest;
141
161
  forceAnalysis?: boolean;
142
162
  workerCount?: number;
163
+ /** Enable parallel parsing using ASTWorkerPool (worker_threads) */
164
+ parallelParsing?: boolean;
143
165
  // Use base onProgress type for compatibility
144
166
  onProgress?: (info: Record<string, unknown>) => void;
145
167
  }
@@ -148,14 +170,12 @@ export class JSASTAnalyzer extends Plugin {
148
170
  private graphBuilder: GraphBuilder;
149
171
  private analyzedModules: Set<string>;
150
172
  private profiler: Profiler;
151
- private _cacheCleared: boolean;
152
173
 
153
174
  constructor() {
154
175
  super();
155
176
  this.graphBuilder = new GraphBuilder();
156
177
  this.analyzedModules = new Set();
157
178
  this.profiler = new Profiler('JSASTAnalyzer');
158
- this._cacheCleared = false;
159
179
  }
160
180
 
161
181
  get metadata(): PluginMetadata {
@@ -235,13 +255,14 @@ export class JSASTAnalyzer extends Plugin {
235
255
  }
236
256
 
237
257
  async execute(context: AnalyzeContext): Promise<PluginResult> {
258
+ const logger = this.log(context);
259
+
238
260
  try {
239
261
  const { manifest, graph, forceAnalysis = false } = context;
240
262
  const projectPath = manifest?.projectPath ?? '';
241
263
 
242
- if (forceAnalysis && !this._cacheCleared) {
264
+ if (forceAnalysis) {
243
265
  this.analyzedModules.clear();
244
- this._cacheCleared = true;
245
266
  }
246
267
 
247
268
  const allModules = await this.getModuleNodes(graph);
@@ -262,13 +283,18 @@ export class JSASTAnalyzer extends Plugin {
262
283
  }
263
284
  }
264
285
 
265
- console.log(`[JSASTAnalyzer] Starting parallel analysis of ${modulesToAnalyze.length} modules (${skippedCount} cached)...`);
286
+ logger.info('Starting module analysis', { toAnalyze: modulesToAnalyze.length, cached: skippedCount });
266
287
 
267
288
  if (modulesToAnalyze.length === 0) {
268
- console.log(`[JSASTAnalyzer] All modules are up-to-date, skipping analysis`);
289
+ logger.info('All modules up-to-date, skipping analysis');
269
290
  return createSuccessResult({ nodes: 0, edges: 0 });
270
291
  }
271
292
 
293
+ // Use ASTWorkerPool for true parallel parsing with worker_threads if enabled
294
+ if (context.parallelParsing) {
295
+ return await this.executeParallel(modulesToAnalyze, graph, projectPath, context);
296
+ }
297
+
272
298
  const queue = new PriorityQueue();
273
299
  const pool = new WorkerPool(context.workerCount || 10);
274
300
 
@@ -311,7 +337,7 @@ export class JSASTAnalyzer extends Plugin {
311
337
  completed++;
312
338
 
313
339
  if (completed % 10 === 0 || completed === modulesToAnalyze.length) {
314
- console.log(`[JSASTAnalyzer] Progress: ${completed}/${modulesToAnalyze.length}`);
340
+ logger.debug('Analysis progress', { completed, total: modulesToAnalyze.length });
315
341
  }
316
342
  });
317
343
 
@@ -339,8 +365,8 @@ export class JSASTAnalyzer extends Plugin {
339
365
  }
340
366
  }
341
367
 
342
- console.log(`[JSASTAnalyzer] Analyzed ${modulesToAnalyze.length} modules, created ${nodesCreated} nodes`);
343
- console.log(`[JSASTAnalyzer] Stats:`, stats);
368
+ logger.info('Analysis complete', { modulesAnalyzed: modulesToAnalyze.length, nodesCreated });
369
+ logger.debug('Worker stats', { ...stats });
344
370
 
345
371
  this.profiler.printSummary();
346
372
 
@@ -350,12 +376,105 @@ export class JSASTAnalyzer extends Plugin {
350
376
  );
351
377
 
352
378
  } catch (error) {
353
- console.error(`[JSASTAnalyzer] Error:`, error);
379
+ logger.error('Analysis failed', { error: error instanceof Error ? error.message : String(error) });
354
380
  const err = error instanceof Error ? error : new Error(String(error));
355
381
  return createErrorResult(err);
356
382
  }
357
383
  }
358
384
 
385
+ /**
386
+ * Execute parallel analysis using ASTWorkerPool (worker_threads).
387
+ *
388
+ * This method uses actual OS threads for true parallel CPU-intensive parsing.
389
+ * Workers generate semantic IDs using ScopeTracker, matching sequential behavior.
390
+ *
391
+ * @param modules - Modules to analyze
392
+ * @param graph - Graph backend for writing results
393
+ * @param projectPath - Project root path
394
+ * @param context - Analysis context with options
395
+ * @returns Plugin result with node/edge counts
396
+ */
397
+ private async executeParallel(
398
+ modules: ModuleNode[],
399
+ graph: GraphBackend,
400
+ projectPath: string,
401
+ context: AnalyzeContext
402
+ ): Promise<PluginResult> {
403
+ const logger = this.log(context);
404
+ const workerCount = context.workerCount || 4;
405
+ const pool = new ASTWorkerPool(workerCount);
406
+
407
+ logger.debug('Starting parallel parsing', { workerCount });
408
+
409
+ try {
410
+ await pool.init();
411
+
412
+ // Convert ModuleNode to ASTModuleInfo format
413
+ const moduleInfos: ASTModuleInfo[] = modules.map(m => ({
414
+ id: m.id,
415
+ file: m.file,
416
+ name: m.name
417
+ }));
418
+
419
+ // Parse all modules in parallel using worker threads
420
+ const results: ParseResult[] = await pool.parseModules(moduleInfos);
421
+
422
+ let nodesCreated = 0;
423
+ let edgesCreated = 0;
424
+ let errors = 0;
425
+
426
+ // Process results - collections already have semantic IDs from workers
427
+ for (const result of results) {
428
+ if (result.error) {
429
+ logger.warn('Parse error', { file: result.module.file, error: result.error.message });
430
+ errors++;
431
+ continue;
432
+ }
433
+
434
+ if (result.collections) {
435
+ // Find original module for metadata
436
+ const module = modules.find(m => m.id === result.module.id);
437
+ if (!module) continue;
438
+
439
+ // Pass collections directly to GraphBuilder - IDs already semantic
440
+ // Cast is safe because ASTWorker.ASTCollections is structurally compatible
441
+ // with ast/types.ASTCollections (METHOD extends FUNCTION semantically)
442
+ const buildResult = await this.graphBuilder.build(
443
+ module,
444
+ graph,
445
+ projectPath,
446
+ result.collections as unknown as ASTCollections
447
+ );
448
+
449
+ if (typeof buildResult === 'object' && buildResult !== null) {
450
+ nodesCreated += (buildResult as { nodes: number }).nodes || 0;
451
+ edgesCreated += (buildResult as { edges: number }).edges || 0;
452
+ }
453
+ }
454
+
455
+ // Report progress
456
+ if (context.onProgress) {
457
+ context.onProgress({
458
+ phase: 'analysis',
459
+ currentPlugin: 'JSASTAnalyzer',
460
+ message: `Processed ${result.module.file.replace(projectPath, '')}`,
461
+ totalFiles: modules.length,
462
+ processedFiles: results.indexOf(result) + 1
463
+ });
464
+ }
465
+ }
466
+
467
+ logger.info('Parallel parsing complete', { nodesCreated, edgesCreated, errors });
468
+
469
+ return createSuccessResult(
470
+ { nodes: nodesCreated, edges: edgesCreated },
471
+ { modulesAnalyzed: modules.length - errors, parallelParsing: true }
472
+ );
473
+ } finally {
474
+ await pool.terminate();
475
+ }
476
+ }
477
+
359
478
  /**
360
479
  * Extract variable names from destructuring patterns
361
480
  * Uses t.isX() type guards to avoid casts
@@ -473,8 +592,8 @@ export class JSASTAnalyzer extends Plugin {
473
592
  sourceId: null,
474
593
  sourceType: 'CALL_SITE',
475
594
  callName: initExpression.callee.name,
476
- callLine: initExpression.loc!.start.line,
477
- callColumn: initExpression.loc!.start.column
595
+ callLine: getLine(initExpression),
596
+ callColumn: getColumn(initExpression)
478
597
  });
479
598
  return;
480
599
  }
@@ -486,7 +605,7 @@ export class JSASTAnalyzer extends Plugin {
486
605
  const methodName = callee.property.type === 'Identifier' ? callee.property.name : 'unknown';
487
606
 
488
607
  const fullName = `${objectName}.${methodName}`;
489
- const methodCallId = `CALL#${fullName}#${module.file}#${initExpression.loc!.start.line}:${initExpression.loc!.start.column}:inline`;
608
+ const methodCallId = `CALL#${fullName}#${module.file}#${getLine(initExpression)}:${getColumn(initExpression)}:inline`;
490
609
 
491
610
  const existing = variableAssignments.find(a => a.sourceId === methodCallId);
492
611
  if (!existing) {
@@ -495,15 +614,15 @@ export class JSASTAnalyzer extends Plugin {
495
614
  if (arg.type !== 'SpreadElement') {
496
615
  const argLiteralValue = ExpressionEvaluator.extractLiteralValue(arg);
497
616
  if (argLiteralValue !== null) {
498
- const literalId = `LITERAL#arg${index}#${module.file}#${initExpression.loc!.start.line}:${initExpression.loc!.start.column}:${literalCounterRef.value++}`;
617
+ const literalId = `LITERAL#arg${index}#${module.file}#${getLine(initExpression)}:${getColumn(initExpression)}:${literalCounterRef.value++}`;
499
618
  literals.push({
500
619
  id: literalId,
501
620
  type: 'LITERAL',
502
621
  value: argLiteralValue,
503
622
  valueType: typeof argLiteralValue,
504
623
  file: module.file,
505
- line: arg.loc?.start.line || initExpression.loc!.start.line,
506
- column: arg.loc?.start.column || initExpression.loc!.start.column,
624
+ line: arg.loc?.start.line || getLine(initExpression),
625
+ column: arg.loc?.start.column || getColumn(initExpression),
507
626
  parentCallId: methodCallId,
508
627
  argIndex: index
509
628
  });
@@ -522,8 +641,8 @@ export class JSASTAnalyzer extends Plugin {
522
641
  method: methodName,
523
642
  file: module.file,
524
643
  arguments: extractedArgs,
525
- line: initExpression.loc!.start.line,
526
- column: initExpression.loc!.start.column
644
+ line: getLine(initExpression),
645
+ column: getColumn(initExpression)
527
646
  });
528
647
  }
529
648
 
@@ -584,7 +703,8 @@ export class JSASTAnalyzer extends Plugin {
584
703
  ? initExpression.property.name
585
704
  : null;
586
705
 
587
- const expressionId = `EXPRESSION#${objectName}.${propertyName}#${module.file}#${line}:${initExpression.start}`;
706
+ const column = initExpression.start ?? 0;
707
+ const expressionId = ExpressionNode.generateId('MemberExpression', module.file, line, column);
588
708
 
589
709
  variableAssignments.push({
590
710
  variableId,
@@ -597,14 +717,16 @@ export class JSASTAnalyzer extends Plugin {
597
717
  computedPropertyVar,
598
718
  objectSourceName: initExpression.object.type === 'Identifier' ? initExpression.object.name : null,
599
719
  file: module.file,
600
- line: line
720
+ line: line,
721
+ column: column
601
722
  });
602
723
  return;
603
724
  }
604
725
 
605
726
  // 8. BinaryExpression
606
727
  if (initExpression.type === 'BinaryExpression') {
607
- const expressionId = `EXPRESSION#binary#${module.file}#${line}:${initExpression.start}`;
728
+ const column = initExpression.start ?? 0;
729
+ const expressionId = ExpressionNode.generateId('BinaryExpression', module.file, line, column);
608
730
 
609
731
  variableAssignments.push({
610
732
  variableId,
@@ -615,14 +737,16 @@ export class JSASTAnalyzer extends Plugin {
615
737
  leftSourceName: initExpression.left.type === 'Identifier' ? initExpression.left.name : null,
616
738
  rightSourceName: initExpression.right.type === 'Identifier' ? initExpression.right.name : null,
617
739
  file: module.file,
618
- line: line
740
+ line: line,
741
+ column: column
619
742
  });
620
743
  return;
621
744
  }
622
745
 
623
746
  // 9. ConditionalExpression
624
747
  if (initExpression.type === 'ConditionalExpression') {
625
- const expressionId = `EXPRESSION#conditional#${module.file}#${line}:${initExpression.start}`;
748
+ const column = initExpression.start ?? 0;
749
+ const expressionId = ExpressionNode.generateId('ConditionalExpression', module.file, line, column);
626
750
 
627
751
  variableAssignments.push({
628
752
  variableId,
@@ -632,7 +756,8 @@ export class JSASTAnalyzer extends Plugin {
632
756
  consequentSourceName: initExpression.consequent.type === 'Identifier' ? initExpression.consequent.name : null,
633
757
  alternateSourceName: initExpression.alternate.type === 'Identifier' ? initExpression.alternate.name : null,
634
758
  file: module.file,
635
- line: line
759
+ line: line,
760
+ column: column
636
761
  });
637
762
 
638
763
  this.trackVariableAssignment(initExpression.consequent, variableId, variableName, module, line, literals, variableAssignments, literalCounterRef);
@@ -642,7 +767,8 @@ export class JSASTAnalyzer extends Plugin {
642
767
 
643
768
  // 10. LogicalExpression
644
769
  if (initExpression.type === 'LogicalExpression') {
645
- const expressionId = `EXPRESSION#logical#${module.file}#${line}:${initExpression.start}`;
770
+ const column = initExpression.start ?? 0;
771
+ const expressionId = ExpressionNode.generateId('LogicalExpression', module.file, line, column);
646
772
 
647
773
  variableAssignments.push({
648
774
  variableId,
@@ -653,7 +779,8 @@ export class JSASTAnalyzer extends Plugin {
653
779
  leftSourceName: initExpression.left.type === 'Identifier' ? initExpression.left.name : null,
654
780
  rightSourceName: initExpression.right.type === 'Identifier' ? initExpression.right.name : null,
655
781
  file: module.file,
656
- line: line
782
+ line: line,
783
+ column: column
657
784
  });
658
785
 
659
786
  this.trackVariableAssignment(initExpression.left, variableId, variableName, module, line, literals, variableAssignments, literalCounterRef);
@@ -663,7 +790,8 @@ export class JSASTAnalyzer extends Plugin {
663
790
 
664
791
  // 11. TemplateLiteral
665
792
  if (initExpression.type === 'TemplateLiteral' && initExpression.expressions.length > 0) {
666
- const expressionId = `EXPRESSION#template#${module.file}#${line}:${initExpression.start}`;
793
+ const column = initExpression.start ?? 0;
794
+ const expressionId = ExpressionNode.generateId('TemplateLiteral', module.file, line, column);
667
795
 
668
796
  const expressionSourceNames = initExpression.expressions
669
797
  .filter((expr): expr is t.Identifier => expr.type === 'Identifier')
@@ -676,7 +804,8 @@ export class JSASTAnalyzer extends Plugin {
676
804
  expressionType: 'TemplateLiteral',
677
805
  expressionSourceNames,
678
806
  file: module.file,
679
- line: line
807
+ line: line,
808
+ column: column
680
809
  });
681
810
 
682
811
  for (const expr of initExpression.expressions) {
@@ -719,6 +848,10 @@ export class JSASTAnalyzer extends Plugin {
719
848
  });
720
849
  this.profiler.end('babel_parse');
721
850
 
851
+ // Create ScopeTracker for semantic ID generation
852
+ // Use basename for shorter, more readable semantic IDs
853
+ const scopeTracker = new ScopeTracker(basename(module.file));
854
+
722
855
  const functions: FunctionInfo[] = [];
723
856
  const parameters: ParameterInfo[] = [];
724
857
  const scopes: ScopeInfo[] = [];
@@ -740,6 +873,15 @@ export class JSASTAnalyzer extends Plugin {
740
873
  const typeAliases: TypeAliasInfo[] = [];
741
874
  const enums: EnumDeclarationInfo[] = [];
742
875
  const decorators: DecoratorInfo[] = [];
876
+ // Object/Array literal tracking for data flow
877
+ const objectLiterals: ObjectLiteralInfo[] = [];
878
+ const objectProperties: ObjectPropertyInfo[] = [];
879
+ const arrayLiterals: ArrayLiteralInfo[] = [];
880
+ const arrayElements: ArrayElementInfo[] = [];
881
+ // Array mutation tracking for FLOWS_INTO edges
882
+ const arrayMutations: ArrayMutationInfo[] = [];
883
+ // Object mutation tracking for FLOWS_INTO edges
884
+ const objectMutations: ObjectMutationInfo[] = [];
743
885
 
744
886
  const ifScopeCounterRef: CounterRef = { value: 0 };
745
887
  const scopeCounterRef: CounterRef = { value: 0 };
@@ -749,6 +891,8 @@ export class JSASTAnalyzer extends Plugin {
749
891
  const httpRequestCounterRef: CounterRef = { value: 0 };
750
892
  const literalCounterRef: CounterRef = { value: 0 };
751
893
  const anonymousFunctionCounterRef: CounterRef = { value: 0 };
894
+ const objectLiteralCounterRef: CounterRef = { value: 0 };
895
+ const arrayLiteralCounterRef: CounterRef = { value: 0 };
752
896
 
753
897
  const processedNodes: ProcessedNodes = {
754
898
  functions: new Set(),
@@ -779,33 +923,37 @@ export class JSASTAnalyzer extends Plugin {
779
923
  module,
780
924
  { variableDeclarations, classInstantiations, literals, variableAssignments, varDeclCounterRef, literalCounterRef },
781
925
  this.extractVariableNamesFromPattern.bind(this),
782
- this.trackVariableAssignment.bind(this) as TrackVariableAssignmentCallback
926
+ this.trackVariableAssignment.bind(this) as TrackVariableAssignmentCallback,
927
+ scopeTracker // Pass ScopeTracker for semantic ID generation
783
928
  );
784
929
  traverse(ast, variableVisitor.getHandlers());
785
930
  this.profiler.end('traverse_variables');
786
931
 
787
- // Module-level scope context for consistent anonymous naming
788
- const moduleScopeCtx: ScopeContext = {
789
- semanticPath: module.name!,
790
- siblingCounters: new Map()
791
- };
792
-
793
932
  const allCollections: Collections = {
794
933
  functions, parameters, scopes, variableDeclarations, callSites, methodCalls,
795
934
  eventListeners, methodCallbacks, callArguments, classInstantiations, classDeclarations,
796
935
  httpRequests, literals, variableAssignments,
797
936
  // TypeScript-specific collections
798
937
  interfaces, typeAliases, enums, decorators,
938
+ // Object/Array literal tracking
939
+ objectLiterals, objectProperties, arrayLiterals, arrayElements,
940
+ // Array mutation tracking
941
+ arrayMutations,
942
+ // Object mutation tracking
943
+ objectMutations,
944
+ objectLiteralCounterRef, arrayLiteralCounterRef,
799
945
  ifScopeCounterRef, scopeCounterRef, varDeclCounterRef,
800
946
  callSiteCounterRef, functionCounterRef, httpRequestCounterRef,
801
947
  literalCounterRef, anonymousFunctionCounterRef, processedNodes,
802
- imports, exports, moduleScopeCtx, code,
948
+ imports, exports, code,
803
949
  // VisitorCollections compatibility
804
950
  classes: classDeclarations,
805
951
  methods: [],
806
952
  variables: variableDeclarations,
807
953
  sideEffects: [],
808
- variableCounterRef: varDeclCounterRef
954
+ variableCounterRef: varDeclCounterRef,
955
+ // ScopeTracker for semantic ID generation
956
+ scopeTracker
809
957
  };
810
958
 
811
959
  // Functions
@@ -813,7 +961,8 @@ export class JSASTAnalyzer extends Plugin {
813
961
  const functionVisitor = new FunctionVisitor(
814
962
  module,
815
963
  allCollections,
816
- this.analyzeFunctionBody.bind(this)
964
+ this.analyzeFunctionBody.bind(this),
965
+ scopeTracker // Pass ScopeTracker for semantic ID generation
817
966
  );
818
967
  traverse(ast, functionVisitor.getHandlers());
819
968
  this.profiler.end('traverse_functions');
@@ -841,22 +990,22 @@ export class JSASTAnalyzer extends Plugin {
841
990
  }
842
991
 
843
992
  const funcNode = assignNode.right;
844
- const functionId = `FUNCTION#${functionName}#${module.file}#${assignNode.loc!.start.line}:${assignNode.loc!.start.column}`;
993
+ // Use semantic ID as primary ID (matching FunctionVisitor pattern)
994
+ const functionId = computeSemanticId('FUNCTION', functionName, scopeTracker.getContext());
845
995
 
846
996
  functions.push({
847
997
  id: functionId,
848
- stableId: functionId,
849
998
  type: 'FUNCTION',
850
999
  name: functionName,
851
1000
  file: module.file,
852
- line: assignNode.loc!.start.line,
853
- column: assignNode.loc!.start.column,
1001
+ line: getLine(assignNode),
1002
+ column: getColumn(assignNode),
854
1003
  async: funcNode.async || false,
855
1004
  generator: funcNode.type === 'FunctionExpression' ? funcNode.generator : false,
856
1005
  isAssignment: true
857
1006
  });
858
1007
 
859
- const funcBodyScopeId = `SCOPE#${functionName}:body#${module.file}#${assignNode.loc!.start.line}`;
1008
+ const funcBodyScopeId = `SCOPE#${functionName}:body#${module.file}#${getLine(assignNode)}`;
860
1009
  scopes.push({
861
1010
  id: funcBodyScopeId,
862
1011
  type: 'SCOPE',
@@ -865,18 +1014,22 @@ export class JSASTAnalyzer extends Plugin {
865
1014
  semanticId: `${functionName}:function_body[0]`,
866
1015
  conditional: false,
867
1016
  file: module.file,
868
- line: assignNode.loc!.start.line,
1017
+ line: getLine(assignNode),
869
1018
  parentFunctionId: functionId
870
1019
  });
871
1020
 
872
1021
  const funcPath = assignPath.get('right') as NodePath<t.FunctionExpression | t.ArrowFunctionExpression>;
873
- // Create scope context for analyzing the function body
874
- const funcScopeCtx: ScopeContext = {
875
- semanticPath: functionName,
876
- siblingCounters: new Map()
877
- };
878
- this.analyzeFunctionBody(funcPath, funcBodyScopeId, module, allCollections, funcScopeCtx);
1022
+ // Enter function scope for semantic ID generation and analyze
1023
+ scopeTracker.enterScope(functionName, 'function');
1024
+ this.analyzeFunctionBody(funcPath, funcBodyScopeId, module, allCollections);
1025
+ scopeTracker.exitScope();
879
1026
  }
1027
+
1028
+ // Check for indexed array assignment at module level: arr[i] = value
1029
+ this.detectIndexedArrayAssignment(assignNode, module, arrayMutations);
1030
+
1031
+ // Check for object property assignment at module level: obj.prop = value
1032
+ this.detectObjectPropertyAssignment(assignNode, module, objectMutations, scopeTracker);
880
1033
  }
881
1034
  });
882
1035
  this.profiler.end('traverse_assignments');
@@ -886,14 +1039,15 @@ export class JSASTAnalyzer extends Plugin {
886
1039
  const classVisitor = new ClassVisitor(
887
1040
  module,
888
1041
  allCollections,
889
- this.analyzeFunctionBody.bind(this)
1042
+ this.analyzeFunctionBody.bind(this),
1043
+ scopeTracker // Pass ScopeTracker for semantic ID generation
890
1044
  );
891
1045
  traverse(ast, classVisitor.getHandlers());
892
1046
  this.profiler.end('traverse_classes');
893
1047
 
894
1048
  // TypeScript-specific constructs (interfaces, type aliases, enums)
895
1049
  this.profiler.start('traverse_typescript');
896
- const typescriptVisitor = new TypeScriptVisitor(module, allCollections);
1050
+ const typescriptVisitor = new TypeScriptVisitor(module, allCollections, scopeTracker);
897
1051
  traverse(ast, typescriptVisitor.getHandlers());
898
1052
  this.profiler.end('traverse_typescript');
899
1053
 
@@ -906,24 +1060,24 @@ export class JSASTAnalyzer extends Plugin {
906
1060
  if (functionParent) return;
907
1061
 
908
1062
  if (funcPath.parent && funcPath.parent.type === 'CallExpression') {
909
- const funcName = funcNode.id ? funcNode.id.name : this.generateAnonymousName(moduleScopeCtx);
910
- const functionId = `FUNCTION#${funcName}#${module.file}#${funcNode.loc!.start.line}:${funcNode.loc!.start.column}`;
1063
+ const funcName = funcNode.id ? funcNode.id.name : this.generateAnonymousName(scopeTracker);
1064
+ // Use semantic ID as primary ID (matching FunctionVisitor pattern)
1065
+ const functionId = computeSemanticId('FUNCTION', funcName, scopeTracker.getContext());
911
1066
 
912
1067
  functions.push({
913
1068
  id: functionId,
914
- stableId: functionId,
915
1069
  type: 'FUNCTION',
916
1070
  name: funcName,
917
1071
  file: module.file,
918
- line: funcNode.loc!.start.line,
919
- column: funcNode.loc!.start.column,
1072
+ line: getLine(funcNode),
1073
+ column: getColumn(funcNode),
920
1074
  async: funcNode.async || false,
921
1075
  generator: funcNode.generator || false,
922
1076
  isCallback: true,
923
1077
  parentScopeId: module.id
924
1078
  });
925
1079
 
926
- const callbackScopeId = `SCOPE#${funcName}:body#${module.file}#${funcNode.loc!.start.line}`;
1080
+ const callbackScopeId = `SCOPE#${funcName}:body#${module.file}#${getLine(funcNode)}`;
927
1081
  scopes.push({
928
1082
  id: callbackScopeId,
929
1083
  type: 'SCOPE',
@@ -932,16 +1086,14 @@ export class JSASTAnalyzer extends Plugin {
932
1086
  semanticId: `${funcName}:callback_body[0]`,
933
1087
  conditional: false,
934
1088
  file: module.file,
935
- line: funcNode.loc!.start.line,
1089
+ line: getLine(funcNode),
936
1090
  parentFunctionId: functionId
937
1091
  });
938
1092
 
939
- // Create scope context for analyzing the callback body
940
- const callbackScopeCtx: ScopeContext = {
941
- semanticPath: funcName,
942
- siblingCounters: new Map()
943
- };
944
- this.analyzeFunctionBody(funcPath, callbackScopeId, module, allCollections, callbackScopeCtx);
1093
+ // Enter callback scope for semantic ID generation and analyze
1094
+ scopeTracker.enterScope(funcName, 'callback');
1095
+ this.analyzeFunctionBody(funcPath, callbackScopeId, module, allCollections);
1096
+ scopeTracker.exitScope();
945
1097
  funcPath.skip();
946
1098
  }
947
1099
  }
@@ -950,7 +1102,7 @@ export class JSASTAnalyzer extends Plugin {
950
1102
 
951
1103
  // Call expressions
952
1104
  this.profiler.start('traverse_calls');
953
- const callExpressionVisitor = new CallExpressionVisitor(module, allCollections);
1105
+ const callExpressionVisitor = new CallExpressionVisitor(module, allCollections, scopeTracker);
954
1106
  traverse(ast, callExpressionVisitor.getHandlers());
955
1107
  this.profiler.end('traverse_calls');
956
1108
 
@@ -964,42 +1116,42 @@ export class JSASTAnalyzer extends Plugin {
964
1116
  const ifNode = ifPath.node;
965
1117
  const condition = code.substring(ifNode.test.start!, ifNode.test.end!) || 'condition';
966
1118
  const counterId = ifScopeCounterRef.value++;
967
- const ifScopeId = `SCOPE#if#${module.file}#${ifNode.loc!.start.line}:${ifNode.loc!.start.column}:${counterId}`;
1119
+ const ifScopeId = `SCOPE#if#${module.file}#${getLine(ifNode)}:${getColumn(ifNode)}:${counterId}`;
968
1120
 
969
1121
  const constraints = ConditionParser.parse(ifNode.test);
970
- const ifSemanticId = this.generateSemanticId('if_statement', moduleScopeCtx);
1122
+ const ifSemanticId = this.generateSemanticId('if_statement', scopeTracker);
971
1123
 
972
1124
  scopes.push({
973
1125
  id: ifScopeId,
974
1126
  type: 'SCOPE',
975
1127
  scopeType: 'if_statement',
976
- name: `if:${ifNode.loc!.start.line}:${ifNode.loc!.start.column}:${counterId}`,
1128
+ name: `if:${getLine(ifNode)}:${getColumn(ifNode)}:${counterId}`,
977
1129
  semanticId: ifSemanticId,
978
1130
  conditional: true,
979
1131
  condition,
980
1132
  constraints: constraints.length > 0 ? constraints : undefined,
981
1133
  file: module.file,
982
- line: ifNode.loc!.start.line,
1134
+ line: getLine(ifNode),
983
1135
  parentScopeId: module.id
984
1136
  });
985
1137
 
986
1138
  if (ifNode.alternate && ifNode.alternate.type !== 'IfStatement') {
987
1139
  const elseCounterId = ifScopeCounterRef.value++;
988
- const elseScopeId = `SCOPE#else#${module.file}#${ifNode.alternate.loc!.start.line}:${ifNode.alternate.loc!.start.column}:${elseCounterId}`;
1140
+ const elseScopeId = `SCOPE#else#${module.file}#${getLine(ifNode.alternate)}:${getColumn(ifNode.alternate)}:${elseCounterId}`;
989
1141
 
990
1142
  const negatedConstraints = constraints.length > 0 ? ConditionParser.negate(constraints) : undefined;
991
- const elseSemanticId = this.generateSemanticId('else_statement', moduleScopeCtx);
1143
+ const elseSemanticId = this.generateSemanticId('else_statement', scopeTracker);
992
1144
 
993
1145
  scopes.push({
994
1146
  id: elseScopeId,
995
1147
  type: 'SCOPE',
996
1148
  scopeType: 'else_statement',
997
- name: `else:${ifNode.alternate.loc!.start.line}:${ifNode.alternate.loc!.start.column}:${elseCounterId}`,
1149
+ name: `else:${getLine(ifNode.alternate)}:${getColumn(ifNode.alternate)}:${elseCounterId}`,
998
1150
  semanticId: elseSemanticId,
999
1151
  conditional: true,
1000
1152
  constraints: negatedConstraints,
1001
1153
  file: module.file,
1002
- line: ifNode.alternate.loc!.start.line,
1154
+ line: getLine(ifNode.alternate),
1003
1155
  parentScopeId: module.id
1004
1156
  });
1005
1157
  }
@@ -1030,67 +1182,569 @@ export class JSASTAnalyzer extends Plugin {
1030
1182
  interfaces,
1031
1183
  typeAliases,
1032
1184
  enums,
1033
- decorators
1185
+ decorators,
1186
+ // Array mutation tracking
1187
+ arrayMutations,
1188
+ // Object mutation tracking
1189
+ objectMutations,
1190
+ // Object/Array literal tracking - use allCollections refs as visitors may have created new arrays
1191
+ objectLiterals: allCollections.objectLiterals || objectLiterals,
1192
+ arrayLiterals: allCollections.arrayLiterals || arrayLiterals
1034
1193
  });
1035
1194
  this.profiler.end('graph_build');
1036
1195
 
1037
1196
  nodesCreated = result.nodes;
1038
1197
  edgesCreated = result.edges;
1039
1198
 
1040
- } catch (error) {
1041
- const err = error instanceof Error ? error : new Error(String(error));
1042
- console.error(`[JSASTAnalyzer] Error analyzing ${module.file}:`, err.message);
1043
- console.error(err.stack);
1199
+ } catch {
1200
+ // Error analyzing module - silently skip, caller handles the result
1044
1201
  }
1045
1202
 
1046
1203
  return { nodes: nodesCreated, edges: edgesCreated };
1047
1204
  }
1048
1205
 
1049
1206
  /**
1050
- * Helper to generate semantic ID for a scope and update counters
1207
+ * Helper to generate semantic ID for a scope using ScopeTracker.
1208
+ * Format: "scopePath:scopeType[index]" e.g. "MyClass->myMethod:if_statement[0]"
1051
1209
  */
1052
1210
  private generateSemanticId(
1053
1211
  scopeType: string,
1054
- scopeCtx: ScopeContext | undefined
1212
+ scopeTracker: ScopeTracker | undefined
1055
1213
  ): string | undefined {
1056
- if (!scopeCtx) return undefined;
1214
+ if (!scopeTracker) return undefined;
1215
+
1216
+ const scopePath = scopeTracker.getScopePath();
1217
+ const siblingIndex = scopeTracker.getItemCounter(`semanticId:${scopeType}`);
1218
+ return `${scopePath}:${scopeType}[${siblingIndex}]`;
1219
+ }
1057
1220
 
1058
- const siblingIndex = scopeCtx.siblingCounters.get(scopeType) || 0;
1059
- scopeCtx.siblingCounters.set(scopeType, siblingIndex + 1);
1060
- return `${scopeCtx.semanticPath}:${scopeType}[${siblingIndex}]`;
1221
+ /**
1222
+ * Generate a unique anonymous function name within the current scope.
1223
+ * Uses ScopeTracker.getSiblingIndex() for stable naming.
1224
+ */
1225
+ private generateAnonymousName(scopeTracker: ScopeTracker | undefined): string {
1226
+ if (!scopeTracker) return 'anonymous';
1227
+ const index = scopeTracker.getSiblingIndex('anonymous');
1228
+ return `anonymous[${index}]`;
1061
1229
  }
1062
1230
 
1063
1231
  /**
1064
- * Helper to create child scope context from a semantic ID
1232
+ * Factory method to create loop scope handlers.
1233
+ * All loop statements (for, for-in, for-of, while, do-while) follow the same pattern:
1234
+ * 1. Create scope with SCOPE#<scopeType>#file#line:counter
1235
+ * 2. Generate semantic ID
1236
+ * 3. Push to scopes array
1237
+ * 4. Enter/exit scope tracker
1238
+ *
1239
+ * @param trackerScopeType - Scope type for ScopeTracker (e.g., 'for', 'for-in', 'while')
1240
+ * @param scopeType - Scope type for the graph node (e.g., 'for-loop', 'for-in-loop')
1241
+ * @param parentScopeId - Parent scope ID for the scope node
1242
+ * @param module - Module context
1243
+ * @param scopes - Collection to push scope nodes to
1244
+ * @param scopeCounterRef - Counter for unique scope IDs
1245
+ * @param scopeTracker - Tracker for semantic ID generation
1246
+ */
1247
+
1248
+ /**
1249
+ * Handles VariableDeclaration nodes within function bodies.
1250
+ *
1251
+ * Extracts variable names from patterns (including destructuring), determines
1252
+ * if the variable should be CONSTANT or VARIABLE, generates semantic or legacy IDs,
1253
+ * and tracks class instantiations and variable assignments.
1254
+ *
1255
+ * @param varPath - The NodePath for the VariableDeclaration
1256
+ * @param parentScopeId - Parent scope ID for the variable
1257
+ * @param module - Module context with file info
1258
+ * @param variableDeclarations - Collection to push variable declarations to
1259
+ * @param classInstantiations - Collection to push class instantiations to
1260
+ * @param literals - Collection for literal tracking
1261
+ * @param variableAssignments - Collection for variable assignment tracking
1262
+ * @param varDeclCounterRef - Counter for unique variable declaration IDs
1263
+ * @param literalCounterRef - Counter for unique literal IDs
1264
+ * @param scopeTracker - Tracker for semantic ID generation
1265
+ * @param parentScopeVariables - Set to track variables for closure analysis
1065
1266
  */
1066
- private createChildScopeContext(semanticId: string | undefined): ScopeContext | undefined {
1067
- if (!semanticId) return undefined;
1267
+ private handleVariableDeclaration(
1268
+ varPath: NodePath<t.VariableDeclaration>,
1269
+ parentScopeId: string,
1270
+ module: VisitorModule,
1271
+ variableDeclarations: VariableDeclarationInfo[],
1272
+ classInstantiations: ClassInstantiationInfo[],
1273
+ literals: LiteralInfo[],
1274
+ variableAssignments: VariableAssignmentInfo[],
1275
+ varDeclCounterRef: CounterRef,
1276
+ literalCounterRef: CounterRef,
1277
+ scopeTracker: ScopeTracker | undefined,
1278
+ parentScopeVariables: Set<{ name: string; id: string; scopeId: string }>
1279
+ ): void {
1280
+ const varNode = varPath.node;
1281
+ const isConst = varNode.kind === 'const';
1282
+
1283
+ varNode.declarations.forEach(declarator => {
1284
+ const variables = this.extractVariableNamesFromPattern(declarator.id);
1285
+
1286
+ variables.forEach(varInfo => {
1287
+ const literalValue = declarator.init ? ExpressionEvaluator.extractLiteralValue(declarator.init) : null;
1288
+ const isLiteral = literalValue !== null;
1289
+ const isNewExpression = declarator.init && declarator.init.type === 'NewExpression';
1290
+
1291
+ const shouldBeConstant = isConst && (isLiteral || isNewExpression);
1292
+ const nodeType = shouldBeConstant ? 'CONSTANT' : 'VARIABLE';
1293
+
1294
+ // Generate semantic ID (primary) or legacy ID (fallback)
1295
+ const legacyId = `${nodeType}#${varInfo.name}#${module.file}#${varInfo.loc.start.line}:${varInfo.loc.start.column}:${varDeclCounterRef.value++}`;
1296
+
1297
+ const varId = scopeTracker
1298
+ ? computeSemanticId(nodeType, varInfo.name, scopeTracker.getContext())
1299
+ : legacyId;
1300
+
1301
+ parentScopeVariables.add({
1302
+ name: varInfo.name,
1303
+ id: varId,
1304
+ scopeId: parentScopeId
1305
+ });
1306
+
1307
+ if (shouldBeConstant) {
1308
+ const constantData: VariableDeclarationInfo = {
1309
+ id: varId,
1310
+ type: 'CONSTANT',
1311
+ name: varInfo.name,
1312
+ file: module.file,
1313
+ line: varInfo.loc.start.line,
1314
+ parentScopeId
1315
+ };
1316
+
1317
+ if (isLiteral) {
1318
+ constantData.value = literalValue;
1319
+ }
1320
+
1321
+ variableDeclarations.push(constantData);
1322
+
1323
+ const init = declarator.init;
1324
+ if (isNewExpression && t.isNewExpression(init) && t.isIdentifier(init.callee)) {
1325
+ const className = init.callee.name;
1326
+ classInstantiations.push({
1327
+ variableId: varId,
1328
+ variableName: varInfo.name,
1329
+ className: className,
1330
+ line: varInfo.loc.start.line,
1331
+ parentScopeId
1332
+ });
1333
+ }
1334
+ } else {
1335
+ variableDeclarations.push({
1336
+ id: varId,
1337
+ type: 'VARIABLE',
1338
+ name: varInfo.name,
1339
+ file: module.file,
1340
+ line: varInfo.loc.start.line,
1341
+ parentScopeId
1342
+ });
1343
+ }
1344
+
1345
+ if (declarator.init) {
1346
+ this.trackVariableAssignment(declarator.init, varId, varInfo.name, module, varInfo.loc.start.line, literals, variableAssignments, literalCounterRef);
1347
+ }
1348
+ });
1349
+ });
1350
+ }
1351
+
1352
+ private createLoopScopeHandler(
1353
+ trackerScopeType: string,
1354
+ scopeType: string,
1355
+ parentScopeId: string,
1356
+ module: VisitorModule,
1357
+ scopes: ScopeInfo[],
1358
+ scopeCounterRef: CounterRef,
1359
+ scopeTracker: ScopeTracker | undefined
1360
+ ): { enter: (path: NodePath<t.Loop>) => void; exit: () => void } {
1068
1361
  return {
1069
- semanticPath: semanticId,
1070
- siblingCounters: new Map()
1362
+ enter: (path: NodePath<t.Loop>) => {
1363
+ const node = path.node;
1364
+ const scopeId = `SCOPE#${scopeType}#${module.file}#${getLine(node)}:${scopeCounterRef.value++}`;
1365
+ const semanticId = this.generateSemanticId(scopeType, scopeTracker);
1366
+ scopes.push({
1367
+ id: scopeId,
1368
+ type: 'SCOPE',
1369
+ scopeType,
1370
+ semanticId,
1371
+ file: module.file,
1372
+ line: getLine(node),
1373
+ parentScopeId
1374
+ });
1375
+
1376
+ // Enter scope for semantic ID generation
1377
+ if (scopeTracker) {
1378
+ scopeTracker.enterCountedScope(trackerScopeType);
1379
+ }
1380
+ },
1381
+ exit: () => {
1382
+ // Exit scope
1383
+ if (scopeTracker) {
1384
+ scopeTracker.exitScope();
1385
+ }
1386
+ }
1071
1387
  };
1072
1388
  }
1073
1389
 
1074
1390
  /**
1075
- * Generate a unique anonymous function name within the current scope
1076
- * Uses scopeCtx.siblingCounters to ensure stability across JS/TS versions
1391
+ * Process VariableDeclarations within a try/catch/finally block.
1392
+ * This is a simplified version that doesn't track parentScopeVariables or class instantiations.
1393
+ *
1394
+ * @param blockPath - The NodePath for the block to process
1395
+ * @param blockScopeId - The scope ID for variables in this block
1396
+ * @param module - Module context
1397
+ * @param variableDeclarations - Collection to push variable declarations to
1398
+ * @param literals - Collection for literal tracking
1399
+ * @param variableAssignments - Collection for variable assignment tracking
1400
+ * @param varDeclCounterRef - Counter for unique variable declaration IDs
1401
+ * @param literalCounterRef - Counter for unique literal IDs
1402
+ * @param scopeTracker - Tracker for semantic ID generation
1077
1403
  */
1078
- private generateAnonymousName(scopeCtx: ScopeContext | undefined): string {
1079
- if (!scopeCtx) return 'anonymous';
1080
- const index = scopeCtx.siblingCounters.get('anonymous') || 0;
1081
- scopeCtx.siblingCounters.set('anonymous', index + 1);
1082
- return `anonymous[${index}]`;
1404
+ private processBlockVariables(
1405
+ blockPath: NodePath,
1406
+ blockScopeId: string,
1407
+ module: VisitorModule,
1408
+ variableDeclarations: VariableDeclarationInfo[],
1409
+ literals: LiteralInfo[],
1410
+ variableAssignments: VariableAssignmentInfo[],
1411
+ varDeclCounterRef: CounterRef,
1412
+ literalCounterRef: CounterRef,
1413
+ scopeTracker: ScopeTracker | undefined
1414
+ ): void {
1415
+ blockPath.traverse({
1416
+ VariableDeclaration: (varPath: NodePath<t.VariableDeclaration>) => {
1417
+ const varNode = varPath.node;
1418
+ const isConst = varNode.kind === 'const';
1419
+
1420
+ varNode.declarations.forEach(declarator => {
1421
+ const variables = this.extractVariableNamesFromPattern(declarator.id);
1422
+
1423
+ variables.forEach(varInfo => {
1424
+ const literalValue = declarator.init ? ExpressionEvaluator.extractLiteralValue(declarator.init) : null;
1425
+ const isLiteral = literalValue !== null;
1426
+ const isNewExpression = declarator.init && declarator.init.type === 'NewExpression';
1427
+ const shouldBeConstant = isConst && (isLiteral || isNewExpression);
1428
+ const nodeType = shouldBeConstant ? 'CONSTANT' : 'VARIABLE';
1429
+
1430
+ const legacyId = `${nodeType}#${varInfo.name}#${module.file}#${varInfo.loc.start.line}:${varInfo.loc.start.column}:${varDeclCounterRef.value++}`;
1431
+ const varId = scopeTracker
1432
+ ? computeSemanticId(nodeType, varInfo.name, scopeTracker.getContext())
1433
+ : legacyId;
1434
+
1435
+ variableDeclarations.push({
1436
+ id: varId,
1437
+ type: nodeType,
1438
+ name: varInfo.name,
1439
+ file: module.file,
1440
+ line: varInfo.loc.start.line,
1441
+ parentScopeId: blockScopeId
1442
+ });
1443
+
1444
+ if (declarator.init) {
1445
+ this.trackVariableAssignment(declarator.init, varId, varInfo.name, module, varInfo.loc.start.line, literals, variableAssignments, literalCounterRef);
1446
+ }
1447
+ });
1448
+ });
1449
+ }
1450
+ });
1451
+ }
1452
+
1453
+ /**
1454
+ * Handles TryStatement nodes within function bodies.
1455
+ * Creates try, catch (with optional error parameter), and finally scopes,
1456
+ * and processes variable declarations within each block.
1457
+ *
1458
+ * @param tryPath - The NodePath for the TryStatement
1459
+ * @param parentScopeId - Parent scope ID for the scope nodes
1460
+ * @param module - Module context
1461
+ * @param scopes - Collection to push scope nodes to
1462
+ * @param variableDeclarations - Collection to push variable declarations to
1463
+ * @param literals - Collection for literal tracking
1464
+ * @param variableAssignments - Collection for variable assignment tracking
1465
+ * @param scopeCounterRef - Counter for unique scope IDs
1466
+ * @param varDeclCounterRef - Counter for unique variable declaration IDs
1467
+ * @param literalCounterRef - Counter for unique literal IDs
1468
+ * @param scopeTracker - Tracker for semantic ID generation
1469
+ */
1470
+ private handleTryStatement(
1471
+ tryPath: NodePath<t.TryStatement>,
1472
+ parentScopeId: string,
1473
+ module: VisitorModule,
1474
+ scopes: ScopeInfo[],
1475
+ variableDeclarations: VariableDeclarationInfo[],
1476
+ literals: LiteralInfo[],
1477
+ variableAssignments: VariableAssignmentInfo[],
1478
+ scopeCounterRef: CounterRef,
1479
+ varDeclCounterRef: CounterRef,
1480
+ literalCounterRef: CounterRef,
1481
+ scopeTracker: ScopeTracker | undefined
1482
+ ): void {
1483
+ const tryNode = tryPath.node;
1484
+
1485
+ // Create and process try block
1486
+ const tryScopeId = `SCOPE#try-block#${module.file}#${getLine(tryNode)}:${scopeCounterRef.value++}`;
1487
+ const trySemanticId = this.generateSemanticId('try-block', scopeTracker);
1488
+ scopes.push({
1489
+ id: tryScopeId,
1490
+ type: 'SCOPE',
1491
+ scopeType: 'try-block',
1492
+ semanticId: trySemanticId,
1493
+ file: module.file,
1494
+ line: getLine(tryNode),
1495
+ parentScopeId
1496
+ });
1497
+
1498
+ if (scopeTracker) {
1499
+ scopeTracker.enterCountedScope('try');
1500
+ }
1501
+ this.processBlockVariables(
1502
+ tryPath.get('block'),
1503
+ tryScopeId,
1504
+ module,
1505
+ variableDeclarations,
1506
+ literals,
1507
+ variableAssignments,
1508
+ varDeclCounterRef,
1509
+ literalCounterRef,
1510
+ scopeTracker
1511
+ );
1512
+ if (scopeTracker) {
1513
+ scopeTracker.exitScope();
1514
+ }
1515
+
1516
+ // Create and process catch block if present
1517
+ if (tryNode.handler) {
1518
+ const catchBlock = tryNode.handler;
1519
+ const catchScopeId = `SCOPE#catch-block#${module.file}#${getLine(catchBlock)}:${scopeCounterRef.value++}`;
1520
+ const catchSemanticId = this.generateSemanticId('catch-block', scopeTracker);
1521
+
1522
+ scopes.push({
1523
+ id: catchScopeId,
1524
+ type: 'SCOPE',
1525
+ scopeType: 'catch-block',
1526
+ semanticId: catchSemanticId,
1527
+ file: module.file,
1528
+ line: getLine(catchBlock),
1529
+ parentScopeId
1530
+ });
1531
+
1532
+ if (scopeTracker) {
1533
+ scopeTracker.enterCountedScope('catch');
1534
+ }
1535
+
1536
+ // Handle catch parameter (e.g., catch (e))
1537
+ if (catchBlock.param) {
1538
+ const errorVarInfo = this.extractVariableNamesFromPattern(catchBlock.param);
1539
+
1540
+ errorVarInfo.forEach(varInfo => {
1541
+ const legacyId = `VARIABLE#${varInfo.name}#${module.file}#${varInfo.loc.start.line}:${varInfo.loc.start.column}:${varDeclCounterRef.value++}`;
1542
+ const varId = scopeTracker
1543
+ ? computeSemanticId('VARIABLE', varInfo.name, scopeTracker.getContext())
1544
+ : legacyId;
1545
+
1546
+ variableDeclarations.push({
1547
+ id: varId,
1548
+ type: 'VARIABLE',
1549
+ name: varInfo.name,
1550
+ file: module.file,
1551
+ line: varInfo.loc.start.line,
1552
+ parentScopeId: catchScopeId
1553
+ });
1554
+ });
1555
+ }
1556
+
1557
+ this.processBlockVariables(
1558
+ tryPath.get('handler.body'),
1559
+ catchScopeId,
1560
+ module,
1561
+ variableDeclarations,
1562
+ literals,
1563
+ variableAssignments,
1564
+ varDeclCounterRef,
1565
+ literalCounterRef,
1566
+ scopeTracker
1567
+ );
1568
+
1569
+ if (scopeTracker) {
1570
+ scopeTracker.exitScope();
1571
+ }
1572
+ }
1573
+
1574
+ // Create and process finally block if present
1575
+ if (tryNode.finalizer) {
1576
+ const finallyScopeId = `SCOPE#finally-block#${module.file}#${getLine(tryNode.finalizer)}:${scopeCounterRef.value++}`;
1577
+ const finallySemanticId = this.generateSemanticId('finally-block', scopeTracker);
1578
+
1579
+ scopes.push({
1580
+ id: finallyScopeId,
1581
+ type: 'SCOPE',
1582
+ scopeType: 'finally-block',
1583
+ semanticId: finallySemanticId,
1584
+ file: module.file,
1585
+ line: getLine(tryNode.finalizer),
1586
+ parentScopeId
1587
+ });
1588
+
1589
+ if (scopeTracker) {
1590
+ scopeTracker.enterCountedScope('finally');
1591
+ }
1592
+
1593
+ const finalizerPath = tryPath.get('finalizer');
1594
+ if (finalizerPath.node) {
1595
+ this.processBlockVariables(
1596
+ finalizerPath as NodePath,
1597
+ finallyScopeId,
1598
+ module,
1599
+ variableDeclarations,
1600
+ literals,
1601
+ variableAssignments,
1602
+ varDeclCounterRef,
1603
+ literalCounterRef,
1604
+ scopeTracker
1605
+ );
1606
+ }
1607
+
1608
+ if (scopeTracker) {
1609
+ scopeTracker.exitScope();
1610
+ }
1611
+ }
1612
+
1613
+ tryPath.skip();
1614
+ }
1615
+
1616
+ /**
1617
+ * Factory method to create IfStatement handler.
1618
+ * Creates if scope with condition parsing and optional else scope.
1619
+ * Tracks if/else scope transitions via ifElseScopeMap.
1620
+ *
1621
+ * @param parentScopeId - Parent scope ID for the scope nodes
1622
+ * @param module - Module context
1623
+ * @param scopes - Collection to push scope nodes to
1624
+ * @param ifScopeCounterRef - Counter for unique if scope IDs
1625
+ * @param scopeTracker - Tracker for semantic ID generation
1626
+ * @param sourceCode - Source code for extracting condition text
1627
+ * @param ifElseScopeMap - Map to track if/else scope transitions
1628
+ */
1629
+ private createIfStatementHandler(
1630
+ parentScopeId: string,
1631
+ module: VisitorModule,
1632
+ scopes: ScopeInfo[],
1633
+ ifScopeCounterRef: CounterRef,
1634
+ scopeTracker: ScopeTracker | undefined,
1635
+ sourceCode: string,
1636
+ ifElseScopeMap: Map<t.IfStatement, { inElse: boolean; hasElse: boolean }>
1637
+ ): { enter: (ifPath: NodePath<t.IfStatement>) => void; exit: (ifPath: NodePath<t.IfStatement>) => void } {
1638
+ return {
1639
+ enter: (ifPath: NodePath<t.IfStatement>) => {
1640
+ const ifNode = ifPath.node;
1641
+ const condition = sourceCode.substring(ifNode.test.start!, ifNode.test.end!) || 'condition';
1642
+ const counterId = ifScopeCounterRef.value++;
1643
+ const ifScopeId = `SCOPE#if#${module.file}#${getLine(ifNode)}:${getColumn(ifNode)}:${counterId}`;
1644
+
1645
+ // Parse condition to extract constraints
1646
+ const constraints = ConditionParser.parse(ifNode.test);
1647
+ const ifSemanticId = this.generateSemanticId('if_statement', scopeTracker);
1648
+
1649
+ scopes.push({
1650
+ id: ifScopeId,
1651
+ type: 'SCOPE',
1652
+ scopeType: 'if_statement',
1653
+ name: `if:${getLine(ifNode)}:${getColumn(ifNode)}:${counterId}`,
1654
+ semanticId: ifSemanticId,
1655
+ conditional: true,
1656
+ condition,
1657
+ constraints: constraints.length > 0 ? constraints : undefined,
1658
+ file: module.file,
1659
+ line: getLine(ifNode),
1660
+ parentScopeId
1661
+ });
1662
+
1663
+ // Enter scope for semantic ID generation
1664
+ if (scopeTracker) {
1665
+ scopeTracker.enterCountedScope('if');
1666
+ }
1667
+
1668
+ // Handle else branch if present
1669
+ if (ifNode.alternate && !t.isIfStatement(ifNode.alternate)) {
1670
+ // Only create else scope for actual else block, not else-if
1671
+ const elseCounterId = ifScopeCounterRef.value++;
1672
+ const elseScopeId = `SCOPE#else#${module.file}#${getLine(ifNode.alternate)}:${getColumn(ifNode.alternate)}:${elseCounterId}`;
1673
+
1674
+ const negatedConstraints = constraints.length > 0 ? ConditionParser.negate(constraints) : undefined;
1675
+ const elseSemanticId = this.generateSemanticId('else_statement', scopeTracker);
1676
+
1677
+ scopes.push({
1678
+ id: elseScopeId,
1679
+ type: 'SCOPE',
1680
+ scopeType: 'else_statement',
1681
+ name: `else:${getLine(ifNode.alternate)}:${getColumn(ifNode.alternate)}:${elseCounterId}`,
1682
+ semanticId: elseSemanticId,
1683
+ conditional: true,
1684
+ constraints: negatedConstraints,
1685
+ file: module.file,
1686
+ line: getLine(ifNode.alternate),
1687
+ parentScopeId
1688
+ });
1689
+
1690
+ // Store info to switch to else scope when we enter alternate
1691
+ ifElseScopeMap.set(ifNode, { inElse: false, hasElse: true });
1692
+ } else {
1693
+ ifElseScopeMap.set(ifNode, { inElse: false, hasElse: false });
1694
+ }
1695
+ },
1696
+ exit: (ifPath: NodePath<t.IfStatement>) => {
1697
+ const ifNode = ifPath.node;
1698
+
1699
+ // Exit the current scope (either if or else)
1700
+ if (scopeTracker) {
1701
+ scopeTracker.exitScope();
1702
+ }
1703
+
1704
+ // If we were in else, we already exited else scope
1705
+ // If we only had if, we exit if scope (done above)
1706
+ ifElseScopeMap.delete(ifNode);
1707
+ }
1708
+ };
1709
+ }
1710
+
1711
+ /**
1712
+ * Factory method to create BlockStatement handler for tracking if/else transitions.
1713
+ * When entering an else block, switches scope from if to else.
1714
+ *
1715
+ * @param scopeTracker - Tracker for semantic ID generation
1716
+ * @param ifElseScopeMap - Map to track if/else scope transitions
1717
+ */
1718
+ private createIfElseBlockStatementHandler(
1719
+ scopeTracker: ScopeTracker | undefined,
1720
+ ifElseScopeMap: Map<t.IfStatement, { inElse: boolean; hasElse: boolean }>
1721
+ ): { enter: (blockPath: NodePath<t.BlockStatement>) => void } {
1722
+ return {
1723
+ enter: (blockPath: NodePath<t.BlockStatement>) => {
1724
+ // Check if this block is the alternate of an IfStatement
1725
+ const parent = blockPath.parent;
1726
+ if (t.isIfStatement(parent) && parent.alternate === blockPath.node) {
1727
+ const scopeInfo = ifElseScopeMap.get(parent);
1728
+ if (scopeInfo && scopeInfo.hasElse && !scopeInfo.inElse && scopeTracker) {
1729
+ // Exit if scope, enter else scope
1730
+ scopeTracker.exitScope();
1731
+ scopeTracker.enterCountedScope('else');
1732
+ scopeInfo.inElse = true;
1733
+ }
1734
+ }
1735
+ }
1736
+ };
1083
1737
  }
1084
1738
 
1085
1739
  /**
1086
- * Анализирует тело функции и извлекает переменные, вызовы, условные блоки
1740
+ * Анализирует тело функции и извлекает переменные, вызовы, условные блоки.
1741
+ * Uses ScopeTracker from collections for semantic ID generation.
1087
1742
  */
1088
1743
  analyzeFunctionBody(
1089
1744
  funcPath: NodePath<t.Function>,
1090
1745
  parentScopeId: string,
1091
1746
  module: VisitorModule,
1092
- collections: VisitorCollections,
1093
- scopeCtx?: ScopeContext
1747
+ collections: VisitorCollections
1094
1748
  ): void {
1095
1749
  // Extract with defaults for optional properties
1096
1750
  const functions = (collections.functions ?? []) as FunctionInfo[];
@@ -1112,6 +1766,7 @@ export class JSASTAnalyzer extends Plugin {
1112
1766
  const httpRequestCounterRef = (collections.httpRequestCounterRef ?? { value: 0 }) as CounterRef;
1113
1767
  const literalCounterRef = (collections.literalCounterRef ?? { value: 0 }) as CounterRef;
1114
1768
  const anonymousFunctionCounterRef = (collections.anonymousFunctionCounterRef ?? { value: 0 }) as CounterRef;
1769
+ const scopeTracker = collections.scopeTracker as ScopeTracker | undefined;
1115
1770
  const processedNodes = collections.processedNodes ?? {
1116
1771
  functions: new Set<string>(),
1117
1772
  classes: new Set<string>(),
@@ -1131,219 +1786,75 @@ export class JSASTAnalyzer extends Plugin {
1131
1786
  const processedMethodCalls = processedNodes.methodCalls;
1132
1787
  const processedEventListeners = processedNodes.eventListeners;
1133
1788
 
1789
+ // Track if/else scope transitions
1790
+ const ifElseScopeMap = new Map<t.IfStatement, { inElse: boolean; hasElse: boolean }>();
1791
+
1134
1792
  funcPath.traverse({
1135
1793
  VariableDeclaration: (varPath: NodePath<t.VariableDeclaration>) => {
1136
- const varNode = varPath.node;
1137
- const isConst = varNode.kind === 'const';
1794
+ this.handleVariableDeclaration(
1795
+ varPath,
1796
+ parentScopeId,
1797
+ module,
1798
+ variableDeclarations,
1799
+ classInstantiations,
1800
+ literals,
1801
+ variableAssignments,
1802
+ varDeclCounterRef,
1803
+ literalCounterRef,
1804
+ scopeTracker,
1805
+ parentScopeVariables
1806
+ );
1807
+ },
1138
1808
 
1139
- varNode.declarations.forEach(declarator => {
1140
- const variables = this.extractVariableNamesFromPattern(declarator.id);
1809
+ // Detect indexed array assignments: arr[i] = value
1810
+ AssignmentExpression: (assignPath: NodePath<t.AssignmentExpression>) => {
1811
+ const assignNode = assignPath.node;
1141
1812
 
1142
- variables.forEach(varInfo => {
1143
- const literalValue = declarator.init ? ExpressionEvaluator.extractLiteralValue(declarator.init) : null;
1144
- const isLiteral = literalValue !== null;
1145
- const isNewExpression = declarator.init && declarator.init.type === 'NewExpression';
1813
+ // Initialize collection if not exists
1814
+ if (!collections.arrayMutations) {
1815
+ collections.arrayMutations = [];
1816
+ }
1817
+ const arrayMutations = collections.arrayMutations as ArrayMutationInfo[];
1146
1818
 
1147
- const shouldBeConstant = isConst && (isLiteral || isNewExpression);
1819
+ // Check for indexed array assignment: arr[i] = value
1820
+ this.detectIndexedArrayAssignment(assignNode, module, arrayMutations);
1148
1821
 
1149
- const varId = shouldBeConstant
1150
- ? `CONSTANT#${varInfo.name}#${module.file}#${varInfo.loc.start.line}:${varInfo.loc.start.column}:${varDeclCounterRef.value++}`
1151
- : `VARIABLE#${varInfo.name}#${module.file}#${varInfo.loc.start.line}:${varInfo.loc.start.column}:${varDeclCounterRef.value++}`;
1822
+ // Initialize object mutations collection if not exists
1823
+ if (!collections.objectMutations) {
1824
+ collections.objectMutations = [];
1825
+ }
1826
+ const objectMutations = collections.objectMutations as ObjectMutationInfo[];
1152
1827
 
1153
- parentScopeVariables.add({
1154
- name: varInfo.name,
1155
- id: varId,
1156
- scopeId: parentScopeId
1157
- });
1158
-
1159
- if (shouldBeConstant) {
1160
- const constantData: VariableDeclarationInfo = {
1161
- id: varId,
1162
- type: 'CONSTANT',
1163
- name: varInfo.name,
1164
- file: module.file,
1165
- line: varInfo.loc.start.line,
1166
- parentScopeId
1167
- };
1168
-
1169
- if (isLiteral) {
1170
- constantData.value = literalValue;
1171
- }
1172
-
1173
- variableDeclarations.push(constantData);
1174
-
1175
- const init = declarator.init;
1176
- if (isNewExpression && t.isNewExpression(init) && t.isIdentifier(init.callee)) {
1177
- const className = init.callee.name;
1178
- classInstantiations.push({
1179
- variableId: varId,
1180
- variableName: varInfo.name,
1181
- className: className,
1182
- line: varInfo.loc.start.line,
1183
- parentScopeId
1184
- });
1185
- }
1186
- } else {
1187
- variableDeclarations.push({
1188
- id: varId,
1189
- type: 'VARIABLE',
1190
- name: varInfo.name,
1191
- file: module.file,
1192
- line: varInfo.loc.start.line,
1193
- parentScopeId
1194
- });
1195
- }
1196
-
1197
- if (declarator.init) {
1198
- this.trackVariableAssignment(declarator.init, varId, varInfo.name, module, varInfo.loc.start.line, literals, variableAssignments, literalCounterRef);
1199
- }
1200
- });
1201
- });
1202
- },
1203
-
1204
- ForStatement: (forPath: NodePath<t.ForStatement>) => {
1205
- const forNode = forPath.node;
1206
- const scopeId = `SCOPE#for-loop#${module.file}#${forNode.loc!.start.line}:${scopeCounterRef.value++}`;
1207
- const semanticId = this.generateSemanticId('for-loop', scopeCtx);
1208
- scopes.push({
1209
- id: scopeId,
1210
- type: 'SCOPE',
1211
- scopeType: 'for-loop',
1212
- semanticId,
1213
- file: module.file,
1214
- line: forNode.loc!.start.line,
1215
- parentScopeId
1216
- });
1217
- },
1218
-
1219
- ForInStatement: (forPath: NodePath<t.ForInStatement>) => {
1220
- const forNode = forPath.node;
1221
- const scopeId = `SCOPE#for-in-loop#${module.file}#${forNode.loc!.start.line}:${scopeCounterRef.value++}`;
1222
- const semanticId = this.generateSemanticId('for-in-loop', scopeCtx);
1223
- scopes.push({
1224
- id: scopeId,
1225
- type: 'SCOPE',
1226
- scopeType: 'for-in-loop',
1227
- semanticId,
1228
- file: module.file,
1229
- line: forNode.loc!.start.line,
1230
- parentScopeId
1231
- });
1828
+ // Check for object property assignment: obj.prop = value
1829
+ this.detectObjectPropertyAssignment(assignNode, module, objectMutations, scopeTracker);
1232
1830
  },
1233
1831
 
1234
- ForOfStatement: (forPath: NodePath<t.ForOfStatement>) => {
1235
- const forNode = forPath.node;
1236
- const scopeId = `SCOPE#for-of-loop#${module.file}#${forNode.loc!.start.line}:${scopeCounterRef.value++}`;
1237
- const semanticId = this.generateSemanticId('for-of-loop', scopeCtx);
1238
- scopes.push({
1239
- id: scopeId,
1240
- type: 'SCOPE',
1241
- scopeType: 'for-of-loop',
1242
- semanticId,
1243
- file: module.file,
1244
- line: forNode.loc!.start.line,
1245
- parentScopeId
1246
- });
1247
- },
1248
-
1249
- WhileStatement: (whilePath: NodePath<t.WhileStatement>) => {
1250
- const whileNode = whilePath.node;
1251
- const scopeId = `SCOPE#while-loop#${module.file}#${whileNode.loc!.start.line}:${scopeCounterRef.value++}`;
1252
- const semanticId = this.generateSemanticId('while-loop', scopeCtx);
1253
- scopes.push({
1254
- id: scopeId,
1255
- type: 'SCOPE',
1256
- scopeType: 'while-loop',
1257
- semanticId,
1258
- file: module.file,
1259
- line: whileNode.loc!.start.line,
1260
- parentScopeId
1261
- });
1262
- },
1263
-
1264
- DoWhileStatement: (doPath: NodePath<t.DoWhileStatement>) => {
1265
- const doNode = doPath.node;
1266
- const scopeId = `SCOPE#do-while-loop#${module.file}#${doNode.loc!.start.line}:${scopeCounterRef.value++}`;
1267
- const semanticId = this.generateSemanticId('do-while-loop', scopeCtx);
1268
- scopes.push({
1269
- id: scopeId,
1270
- type: 'SCOPE',
1271
- scopeType: 'do-while-loop',
1272
- semanticId,
1273
- file: module.file,
1274
- line: doNode.loc!.start.line,
1275
- parentScopeId
1276
- });
1277
- },
1832
+ ForStatement: this.createLoopScopeHandler('for', 'for-loop', parentScopeId, module, scopes, scopeCounterRef, scopeTracker),
1833
+ ForInStatement: this.createLoopScopeHandler('for-in', 'for-in-loop', parentScopeId, module, scopes, scopeCounterRef, scopeTracker),
1834
+ ForOfStatement: this.createLoopScopeHandler('for-of', 'for-of-loop', parentScopeId, module, scopes, scopeCounterRef, scopeTracker),
1835
+ WhileStatement: this.createLoopScopeHandler('while', 'while-loop', parentScopeId, module, scopes, scopeCounterRef, scopeTracker),
1836
+ DoWhileStatement: this.createLoopScopeHandler('do-while', 'do-while-loop', parentScopeId, module, scopes, scopeCounterRef, scopeTracker),
1278
1837
 
1279
1838
  TryStatement: (tryPath: NodePath<t.TryStatement>) => {
1280
- const tryNode = tryPath.node;
1281
-
1282
- const tryScopeId = `SCOPE#try-block#${module.file}#${tryNode.loc!.start.line}:${scopeCounterRef.value++}`;
1283
- const trySemanticId = this.generateSemanticId('try-block', scopeCtx);
1284
- scopes.push({
1285
- id: tryScopeId,
1286
- type: 'SCOPE',
1287
- scopeType: 'try-block',
1288
- semanticId: trySemanticId,
1289
- file: module.file,
1290
- line: tryNode.loc!.start.line,
1291
- parentScopeId
1292
- });
1293
-
1294
- if (tryNode.handler) {
1295
- const catchBlock = tryNode.handler;
1296
- const catchScopeId = `SCOPE#catch-block#${module.file}#${catchBlock.loc!.start.line}:${scopeCounterRef.value++}`;
1297
- const catchSemanticId = this.generateSemanticId('catch-block', scopeCtx);
1298
-
1299
- scopes.push({
1300
- id: catchScopeId,
1301
- type: 'SCOPE',
1302
- scopeType: 'catch-block',
1303
- semanticId: catchSemanticId,
1304
- file: module.file,
1305
- line: catchBlock.loc!.start.line,
1306
- parentScopeId
1307
- });
1308
-
1309
- if (catchBlock.param) {
1310
- const errorVarInfo = this.extractVariableNamesFromPattern(catchBlock.param);
1311
-
1312
- errorVarInfo.forEach(varInfo => {
1313
- const varId = `VARIABLE#${varInfo.name}#${module.file}#${varInfo.loc.start.line}:${varInfo.loc.start.column}:${varDeclCounterRef.value++}`;
1314
-
1315
- variableDeclarations.push({
1316
- id: varId,
1317
- type: 'VARIABLE',
1318
- name: varInfo.name,
1319
- file: module.file,
1320
- line: varInfo.loc.start.line,
1321
- parentScopeId: catchScopeId
1322
- });
1323
- });
1324
- }
1325
- }
1326
-
1327
- if (tryNode.finalizer) {
1328
- const finallyScopeId = `SCOPE#finally-block#${module.file}#${tryNode.finalizer.loc!.start.line}:${scopeCounterRef.value++}`;
1329
- const finallySemanticId = this.generateSemanticId('finally-block', scopeCtx);
1330
-
1331
- scopes.push({
1332
- id: finallyScopeId,
1333
- type: 'SCOPE',
1334
- scopeType: 'finally-block',
1335
- semanticId: finallySemanticId,
1336
- file: module.file,
1337
- line: tryNode.finalizer.loc!.start.line,
1338
- parentScopeId
1339
- });
1340
- }
1839
+ this.handleTryStatement(
1840
+ tryPath,
1841
+ parentScopeId,
1842
+ module,
1843
+ scopes,
1844
+ variableDeclarations,
1845
+ literals,
1846
+ variableAssignments,
1847
+ scopeCounterRef,
1848
+ varDeclCounterRef,
1849
+ literalCounterRef,
1850
+ scopeTracker
1851
+ );
1341
1852
  },
1342
1853
 
1343
1854
  SwitchStatement: (switchPath: NodePath<t.SwitchStatement>) => {
1344
1855
  const switchNode = switchPath.node;
1345
- const scopeId = `SCOPE#switch-case#${module.file}#${switchNode.loc!.start.line}:${scopeCounterRef.value++}`;
1346
- const semanticId = this.generateSemanticId('switch-case', scopeCtx);
1856
+ const scopeId = `SCOPE#switch-case#${module.file}#${getLine(switchNode)}:${scopeCounterRef.value++}`;
1857
+ const semanticId = this.generateSemanticId('switch-case', scopeTracker);
1347
1858
 
1348
1859
  scopes.push({
1349
1860
  id: scopeId,
@@ -1351,31 +1862,34 @@ export class JSASTAnalyzer extends Plugin {
1351
1862
  scopeType: 'switch-case',
1352
1863
  semanticId,
1353
1864
  file: module.file,
1354
- line: switchNode.loc!.start.line,
1865
+ line: getLine(switchNode),
1355
1866
  parentScopeId
1356
1867
  });
1357
1868
  },
1358
1869
 
1359
1870
  FunctionExpression: (funcPath: NodePath<t.FunctionExpression>) => {
1360
1871
  const node = funcPath.node;
1361
- const funcName = node.id ? node.id.name : this.generateAnonymousName(scopeCtx);
1362
- const functionId = `FUNCTION#${funcName}#${module.file}#${node.loc!.start.line}:${node.loc!.start.column}:${functionCounterRef.value++}`;
1872
+ const funcName = node.id ? node.id.name : this.generateAnonymousName(scopeTracker);
1873
+ // Use semantic ID as primary ID when scopeTracker available
1874
+ const legacyId = `FUNCTION#${funcName}#${module.file}#${getLine(node)}:${getColumn(node)}:${functionCounterRef.value++}`;
1875
+ const functionId = scopeTracker
1876
+ ? computeSemanticId('FUNCTION', funcName, scopeTracker.getContext())
1877
+ : legacyId;
1363
1878
 
1364
1879
  functions.push({
1365
1880
  id: functionId,
1366
- stableId: functionId,
1367
1881
  type: 'FUNCTION',
1368
1882
  name: funcName,
1369
1883
  file: module.file,
1370
- line: node.loc!.start.line,
1371
- column: node.loc!.start.column,
1884
+ line: getLine(node),
1885
+ column: getColumn(node),
1372
1886
  async: node.async || false,
1373
1887
  generator: node.generator || false,
1374
1888
  parentScopeId
1375
1889
  });
1376
1890
 
1377
- const nestedScopeId = `SCOPE#${funcName}:body#${module.file}#${node.loc!.start.line}`;
1378
- const closureSemanticId = this.generateSemanticId('closure', scopeCtx);
1891
+ const nestedScopeId = `SCOPE#${funcName}:body#${module.file}#${getLine(node)}`;
1892
+ const closureSemanticId = this.generateSemanticId('closure', scopeTracker);
1379
1893
  scopes.push({
1380
1894
  id: nestedScopeId,
1381
1895
  type: 'SCOPE',
@@ -1384,24 +1898,26 @@ export class JSASTAnalyzer extends Plugin {
1384
1898
  semanticId: closureSemanticId,
1385
1899
  conditional: false,
1386
1900
  file: module.file,
1387
- line: node.loc!.start.line,
1901
+ line: getLine(node),
1388
1902
  parentFunctionId: functionId,
1389
1903
  capturesFrom: parentScopeId
1390
1904
  });
1391
1905
 
1392
- // For nested function, create new context with function name as semantic path
1393
- const nestedFuncCtx: ScopeContext = {
1394
- semanticPath: scopeCtx ? `${scopeCtx.semanticPath}.${funcName}` : funcName,
1395
- siblingCounters: new Map()
1396
- };
1397
- this.analyzeFunctionBody(funcPath, nestedScopeId, module, collections, nestedFuncCtx);
1906
+ // Enter nested function scope for semantic ID generation
1907
+ if (scopeTracker) {
1908
+ scopeTracker.enterScope(funcName, 'function');
1909
+ }
1910
+ this.analyzeFunctionBody(funcPath, nestedScopeId, module, collections);
1911
+ if (scopeTracker) {
1912
+ scopeTracker.exitScope();
1913
+ }
1398
1914
  funcPath.skip();
1399
1915
  },
1400
1916
 
1401
1917
  ArrowFunctionExpression: (arrowPath: NodePath<t.ArrowFunctionExpression>) => {
1402
1918
  const node = arrowPath.node;
1403
- const line = node.loc!.start.line;
1404
- const column = node.loc!.start.column;
1919
+ const line = getLine(node);
1920
+ const column = getColumn(node);
1405
1921
 
1406
1922
  // Определяем имя (anonymous если не присвоено переменной)
1407
1923
  const parent = arrowPath.parent;
@@ -1410,14 +1926,17 @@ export class JSASTAnalyzer extends Plugin {
1410
1926
  funcName = parent.id.name;
1411
1927
  } else {
1412
1928
  // Используем scope-level счётчик для стабильного semanticId
1413
- funcName = this.generateAnonymousName(scopeCtx);
1929
+ funcName = this.generateAnonymousName(scopeTracker);
1414
1930
  }
1415
1931
 
1416
- const functionId = `FUNCTION#${funcName}:${line}:${column}:${functionCounterRef.value++}`;
1932
+ // Use semantic ID as primary ID when scopeTracker available
1933
+ const legacyId = `FUNCTION#${funcName}:${line}:${column}:${functionCounterRef.value++}`;
1934
+ const functionId = scopeTracker
1935
+ ? computeSemanticId('FUNCTION', funcName, scopeTracker.getContext())
1936
+ : legacyId;
1417
1937
 
1418
1938
  functions.push({
1419
1939
  id: functionId,
1420
- stableId: functionId,
1421
1940
  type: 'FUNCTION',
1422
1941
  name: funcName,
1423
1942
  file: module.file,
@@ -1430,7 +1949,7 @@ export class JSASTAnalyzer extends Plugin {
1430
1949
 
1431
1950
  if (node.body.type === 'BlockStatement') {
1432
1951
  const nestedScopeId = `SCOPE#${funcName}:body#${module.file}#${line}`;
1433
- const arrowSemanticId = this.generateSemanticId('arrow_body', scopeCtx);
1952
+ const arrowSemanticId = this.generateSemanticId('arrow_body', scopeTracker);
1434
1953
  scopes.push({
1435
1954
  id: nestedScopeId,
1436
1955
  type: 'SCOPE',
@@ -1444,12 +1963,14 @@ export class JSASTAnalyzer extends Plugin {
1444
1963
  capturesFrom: parentScopeId
1445
1964
  });
1446
1965
 
1447
- // For arrow function, create new context with function name as semantic path
1448
- const arrowFuncCtx: ScopeContext = {
1449
- semanticPath: scopeCtx ? `${scopeCtx.semanticPath}.${funcName}` : funcName,
1450
- siblingCounters: new Map()
1451
- };
1452
- this.analyzeFunctionBody(arrowPath, nestedScopeId, module, collections, arrowFuncCtx);
1966
+ // Enter arrow function scope for semantic ID generation
1967
+ if (scopeTracker) {
1968
+ scopeTracker.enterScope(funcName, 'arrow');
1969
+ }
1970
+ this.analyzeFunctionBody(arrowPath, nestedScopeId, module, collections);
1971
+ if (scopeTracker) {
1972
+ scopeTracker.exitScope();
1973
+ }
1453
1974
  }
1454
1975
 
1455
1976
  arrowPath.skip();
@@ -1472,7 +1993,7 @@ export class JSASTAnalyzer extends Plugin {
1472
1993
  scope.modifies.push({
1473
1994
  variableId: variable.id,
1474
1995
  variableName: varName,
1475
- line: updateNode.loc!.start.line
1996
+ line: getLine(updateNode)
1476
1997
  });
1477
1998
  }
1478
1999
  }
@@ -1480,201 +2001,660 @@ export class JSASTAnalyzer extends Plugin {
1480
2001
  },
1481
2002
 
1482
2003
  // IF statements - создаём условные scope и обходим содержимое для CALL узлов
1483
- IfStatement: (ifPath: NodePath<t.IfStatement>) => {
1484
- const ifNode = ifPath.node;
1485
- const sourceCode = collections.code ?? '';
1486
- const condition = sourceCode.substring(ifNode.test.start!, ifNode.test.end!) || 'condition';
1487
- const counterId = ifScopeCounterRef.value++;
1488
- const ifScopeId = `SCOPE#if#${module.file}#${ifNode.loc!.start.line}:${ifNode.loc!.start.column}:${counterId}`;
2004
+ IfStatement: this.createIfStatementHandler(
2005
+ parentScopeId,
2006
+ module,
2007
+ scopes,
2008
+ ifScopeCounterRef,
2009
+ scopeTracker,
2010
+ collections.code ?? '',
2011
+ ifElseScopeMap
2012
+ ),
1489
2013
 
1490
- // Parse condition to extract constraints
1491
- const constraints = ConditionParser.parse(ifNode.test);
1492
- const ifSemanticId = this.generateSemanticId('if_statement', scopeCtx);
2014
+ // Track when we enter the alternate (else) block of an IfStatement
2015
+ BlockStatement: this.createIfElseBlockStatementHandler(scopeTracker, ifElseScopeMap),
1493
2016
 
1494
- scopes.push({
1495
- id: ifScopeId,
1496
- type: 'SCOPE',
1497
- scopeType: 'if_statement',
1498
- name: `if:${ifNode.loc!.start.line}:${ifNode.loc!.start.column}:${counterId}`,
1499
- semanticId: ifSemanticId,
1500
- conditional: true,
1501
- condition,
1502
- constraints: constraints.length > 0 ? constraints : undefined,
1503
- file: module.file,
1504
- line: ifNode.loc!.start.line,
1505
- parentScopeId
1506
- });
1507
-
1508
- // Обходим содержимое if-блока (consequent)
1509
- ifPath.get('consequent').traverse({
1510
- CallExpression: (callPath: NodePath<t.CallExpression>) => {
1511
- const callNode = callPath.node;
1512
- if (callNode.callee.type === 'Identifier') {
1513
- const nodeKey = `${callNode.start}:${callNode.end}`;
1514
- if (processedCallSites.has(nodeKey)) {
1515
- return;
1516
- }
1517
- processedCallSites.add(nodeKey);
2017
+ // Function call expressions
2018
+ CallExpression: (callPath: NodePath<t.CallExpression>) => {
2019
+ this.handleCallExpression(
2020
+ callPath.node,
2021
+ processedCallSites,
2022
+ processedMethodCalls,
2023
+ callSites,
2024
+ methodCalls,
2025
+ module,
2026
+ callSiteCounterRef,
2027
+ scopeTracker,
2028
+ parentScopeId,
2029
+ collections
2030
+ );
2031
+ },
1518
2032
 
1519
- callSites.push({
1520
- id: `CALL#${callNode.callee.name}#${module.file}#${callNode.loc!.start.line}:${callNode.loc!.start.column}:${callSiteCounterRef.value++}`,
1521
- type: 'CALL',
1522
- name: callNode.callee.name,
1523
- file: module.file,
1524
- line: callNode.loc!.start.line,
1525
- parentScopeId: ifScopeId,
1526
- targetFunctionName: callNode.callee.name
1527
- });
1528
- }
1529
- },
1530
- NewExpression: (newPath: NodePath<t.NewExpression>) => {
1531
- const newNode = newPath.node;
1532
- if (newNode.callee.type === 'Identifier') {
1533
- const nodeKey = `new:${newNode.start}:${newNode.end}`;
1534
- if (processedCallSites.has(nodeKey)) {
1535
- return;
1536
- }
1537
- processedCallSites.add(nodeKey);
2033
+ // NewExpression (constructor calls)
2034
+ NewExpression: (newPath: NodePath<t.NewExpression>) => {
2035
+ const newNode = newPath.node;
1538
2036
 
1539
- callSites.push({
1540
- id: `CALL#new:${newNode.callee.name}#${module.file}#${newNode.loc!.start.line}:${newNode.loc!.start.column}:${callSiteCounterRef.value++}`,
1541
- type: 'CALL',
1542
- name: newNode.callee.name,
1543
- file: module.file,
1544
- line: newNode.loc!.start.line,
1545
- parentScopeId: ifScopeId,
1546
- targetFunctionName: newNode.callee.name,
1547
- isNew: true
1548
- });
1549
- }
2037
+ // Handle simple constructor: new Foo()
2038
+ if (newNode.callee.type === 'Identifier') {
2039
+ const nodeKey = `new:${newNode.start}:${newNode.end}`;
2040
+ if (processedCallSites.has(nodeKey)) {
2041
+ return;
1550
2042
  }
1551
- });
2043
+ processedCallSites.add(nodeKey);
1552
2044
 
1553
- // Handle else branch if present
1554
- if (ifNode.alternate) {
1555
- const elseCounterId = ifScopeCounterRef.value++;
1556
- const elseScopeId = `SCOPE#else#${module.file}#${ifNode.alternate.loc!.start.line}:${ifNode.alternate.loc!.start.column}:${elseCounterId}`;
2045
+ // Generate semantic ID (primary) or legacy ID (fallback)
2046
+ const constructorName = newNode.callee.name;
2047
+ const legacyId = `CALL#new:${constructorName}#${module.file}#${getLine(newNode)}:${getColumn(newNode)}:${callSiteCounterRef.value++}`;
1557
2048
 
1558
- // Negate constraints for else branch
1559
- const negatedConstraints = constraints.length > 0 ? ConditionParser.negate(constraints) : undefined;
1560
- const elseSemanticId = this.generateSemanticId('else_statement', scopeCtx);
2049
+ let newCallId = legacyId;
2050
+ if (scopeTracker) {
2051
+ const discriminator = scopeTracker.getItemCounter(`CALL:new:${constructorName}`);
2052
+ newCallId = computeSemanticId('CALL', `new:${constructorName}`, scopeTracker.getContext(), { discriminator });
2053
+ }
1561
2054
 
1562
- scopes.push({
1563
- id: elseScopeId,
1564
- type: 'SCOPE',
1565
- scopeType: 'else_statement',
1566
- name: `else:${ifNode.alternate.loc!.start.line}:${ifNode.alternate.loc!.start.column}:${elseCounterId}`,
1567
- semanticId: elseSemanticId,
1568
- conditional: true,
1569
- constraints: negatedConstraints,
2055
+ callSites.push({
2056
+ id: newCallId,
2057
+ type: 'CALL',
2058
+ name: constructorName,
1570
2059
  file: module.file,
1571
- line: ifNode.alternate.loc!.start.line,
1572
- parentScopeId
2060
+ line: getLine(newNode),
2061
+ parentScopeId,
2062
+ targetFunctionName: constructorName,
2063
+ isNew: true
1573
2064
  });
2065
+ }
2066
+ // Handle namespaced constructor: new ns.Constructor()
2067
+ else if (newNode.callee.type === 'MemberExpression') {
2068
+ const memberCallee = newNode.callee;
2069
+ const object = memberCallee.object;
2070
+ const property = memberCallee.property;
1574
2071
 
1575
- // Traverse else block
1576
- ifPath.get('alternate').traverse({
1577
- CallExpression: (callPath: NodePath<t.CallExpression>) => {
1578
- const callNode = callPath.node;
1579
- if (callNode.callee.type === 'Identifier') {
1580
- const nodeKey = `${callNode.start}:${callNode.end}`;
1581
- if (processedCallSites.has(nodeKey)) {
1582
- return;
1583
- }
1584
- processedCallSites.add(nodeKey);
1585
-
1586
- callSites.push({
1587
- id: `CALL#${callNode.callee.name}#${module.file}#${callNode.loc!.start.line}:${callNode.loc!.start.column}:${callSiteCounterRef.value++}`,
1588
- type: 'CALL',
1589
- name: callNode.callee.name,
1590
- file: module.file,
1591
- line: callNode.loc!.start.line,
1592
- parentScopeId: elseScopeId,
1593
- targetFunctionName: callNode.callee.name
1594
- });
1595
- }
1596
- },
1597
- NewExpression: (newPath: NodePath<t.NewExpression>) => {
1598
- const newNode = newPath.node;
1599
- if (newNode.callee.type === 'Identifier') {
1600
- const nodeKey = `new:${newNode.start}:${newNode.end}`;
1601
- if (processedCallSites.has(nodeKey)) {
1602
- return;
1603
- }
1604
- processedCallSites.add(nodeKey);
1605
-
1606
- callSites.push({
1607
- id: `CALL#new:${newNode.callee.name}#${module.file}#${newNode.loc!.start.line}:${newNode.loc!.start.column}:${callSiteCounterRef.value++}`,
1608
- type: 'CALL',
1609
- name: newNode.callee.name,
1610
- file: module.file,
1611
- line: newNode.loc!.start.line,
1612
- parentScopeId: elseScopeId,
1613
- targetFunctionName: newNode.callee.name,
1614
- isNew: true
1615
- });
1616
- }
2072
+ if (object.type === 'Identifier' && property.type === 'Identifier') {
2073
+ const nodeKey = `new:${newNode.start}:${newNode.end}`;
2074
+ if (processedMethodCalls.has(nodeKey)) {
2075
+ return;
1617
2076
  }
1618
- });
1619
- }
2077
+ processedMethodCalls.add(nodeKey);
1620
2078
 
1621
- // Останавливаем дальнейший обход, чтобы не обрабатывать вызовы дважды
1622
- ifPath.skip();
1623
- },
2079
+ const objectName = object.name;
2080
+ const constructorName = property.name;
2081
+ const fullName = `${objectName}.${constructorName}`;
1624
2082
 
1625
- // Вызовы функций на безусловном уровне (вне if/for/while)
1626
- CallExpression: (callPath: NodePath<t.CallExpression>) => {
1627
- // Проверяем что вызов не внутри if/for/while (их мы обрабатываем отдельно)
1628
- const parent = callPath.parent;
1629
- if (parent.type !== 'IfStatement' && parent.type !== 'ForStatement' && parent.type !== 'WhileStatement') {
1630
- const callNode = callPath.node;
1631
-
1632
- // Обычные вызовы функций (greet(), main())
1633
- if (callNode.callee.type === 'Identifier') {
1634
- const nodeKey = `${callNode.start}:${callNode.end}`;
1635
- if (processedCallSites.has(nodeKey)) {
1636
- return;
2083
+ // Generate semantic ID for method-style constructor call
2084
+ const legacyId = `CALL#new:${fullName}#${module.file}#${getLine(newNode)}:${getColumn(newNode)}:${callSiteCounterRef.value++}`;
2085
+
2086
+ let newMethodCallId = legacyId;
2087
+ if (scopeTracker) {
2088
+ const discriminator = scopeTracker.getItemCounter(`CALL:new:${fullName}`);
2089
+ newMethodCallId = computeSemanticId('CALL', `new:${fullName}`, scopeTracker.getContext(), { discriminator });
1637
2090
  }
1638
- processedCallSites.add(nodeKey);
1639
2091
 
1640
- callSites.push({
1641
- id: `CALL#${callNode.callee.name}#${module.file}#${callNode.loc!.start.line}:${callNode.loc!.start.column}:${callSiteCounterRef.value++}`,
2092
+ methodCalls.push({
2093
+ id: newMethodCallId,
1642
2094
  type: 'CALL',
1643
- name: callNode.callee.name,
2095
+ name: fullName,
2096
+ object: objectName,
2097
+ method: constructorName,
1644
2098
  file: module.file,
1645
- line: callNode.loc!.start.line,
2099
+ line: getLine(newNode),
2100
+ column: getColumn(newNode),
1646
2101
  parentScopeId,
1647
- targetFunctionName: callNode.callee.name
2102
+ isNew: true
1648
2103
  });
1649
2104
  }
1650
2105
  }
1651
- },
2106
+ }
2107
+ });
2108
+ }
1652
2109
 
1653
- // NewExpression на безусловном уровне
1654
- NewExpression: (newPath: NodePath<t.NewExpression>) => {
1655
- const parent = newPath.parent;
1656
- if (parent.type !== 'IfStatement' && parent.type !== 'ForStatement' && parent.type !== 'WhileStatement') {
1657
- const newNode = newPath.node;
1658
- if (newNode.callee.type === 'Identifier') {
1659
- const nodeKey = `new:${newNode.start}:${newNode.end}`;
1660
- if (processedCallSites.has(nodeKey)) {
1661
- return;
1662
- }
1663
- processedCallSites.add(nodeKey);
2110
+ /**
2111
+ * Handle CallExpression nodes: direct function calls (greet(), main())
2112
+ * and method calls (obj.method(), data.process()).
2113
+ *
2114
+ * Handles:
2115
+ * - Direct function calls (Identifier callee) → callSites collection
2116
+ * - Method calls (MemberExpression callee) → methodCalls collection
2117
+ * - Array mutation detection (push, unshift, splice)
2118
+ * - Object.assign() detection
2119
+ *
2120
+ * @param callNode - The call expression AST node
2121
+ * @param processedCallSites - Set of already processed call site keys to avoid duplicates
2122
+ * @param processedMethodCalls - Set of already processed method call keys to avoid duplicates
2123
+ * @param callSites - Collection for direct function calls
2124
+ * @param methodCalls - Collection for method calls
2125
+ * @param module - Current module being analyzed
2126
+ * @param callSiteCounterRef - Counter for legacy ID generation
2127
+ * @param scopeTracker - Optional scope tracker for semantic ID generation
2128
+ * @param parentScopeId - ID of the parent scope containing this call
2129
+ * @param collections - Full collections object for array/object mutations
2130
+ */
2131
+ private handleCallExpression(
2132
+ callNode: t.CallExpression,
2133
+ processedCallSites: Set<string>,
2134
+ processedMethodCalls: Set<string>,
2135
+ callSites: CallSiteInfo[],
2136
+ methodCalls: MethodCallInfo[],
2137
+ module: VisitorModule,
2138
+ callSiteCounterRef: CounterRef,
2139
+ scopeTracker: ScopeTracker | undefined,
2140
+ parentScopeId: string,
2141
+ collections: VisitorCollections
2142
+ ): void {
2143
+ // Handle direct function calls (greet(), main())
2144
+ if (callNode.callee.type === 'Identifier') {
2145
+ const nodeKey = `${callNode.start}:${callNode.end}`;
2146
+ if (processedCallSites.has(nodeKey)) {
2147
+ return;
2148
+ }
2149
+ processedCallSites.add(nodeKey);
1664
2150
 
1665
- callSites.push({
1666
- id: `CALL#new:${newNode.callee.name}#${module.file}#${newNode.loc!.start.line}:${newNode.loc!.start.column}:${callSiteCounterRef.value++}`,
1667
- type: 'CALL',
1668
- name: newNode.callee.name,
1669
- file: module.file,
1670
- line: newNode.loc!.start.line,
1671
- parentScopeId,
1672
- targetFunctionName: newNode.callee.name,
1673
- isNew: true
1674
- });
2151
+ // Generate semantic ID (primary) or legacy ID (fallback)
2152
+ const calleeName = callNode.callee.name;
2153
+ const legacyId = `CALL#${calleeName}#${module.file}#${getLine(callNode)}:${getColumn(callNode)}:${callSiteCounterRef.value++}`;
2154
+
2155
+ let callId = legacyId;
2156
+ if (scopeTracker) {
2157
+ const discriminator = scopeTracker.getItemCounter(`CALL:${calleeName}`);
2158
+ callId = computeSemanticId('CALL', calleeName, scopeTracker.getContext(), { discriminator });
2159
+ }
2160
+
2161
+ callSites.push({
2162
+ id: callId,
2163
+ type: 'CALL',
2164
+ name: calleeName,
2165
+ file: module.file,
2166
+ line: getLine(callNode),
2167
+ parentScopeId,
2168
+ targetFunctionName: calleeName
2169
+ });
2170
+ }
2171
+ // Handle method calls (obj.method(), data.process())
2172
+ else if (callNode.callee.type === 'MemberExpression') {
2173
+ const memberCallee = callNode.callee;
2174
+ const object = memberCallee.object;
2175
+ const property = memberCallee.property;
2176
+ const isComputed = memberCallee.computed;
2177
+
2178
+ if ((object.type === 'Identifier' || object.type === 'ThisExpression') && property.type === 'Identifier') {
2179
+ const nodeKey = `${callNode.start}:${callNode.end}`;
2180
+ if (processedMethodCalls.has(nodeKey)) {
2181
+ return;
2182
+ }
2183
+ processedMethodCalls.add(nodeKey);
2184
+
2185
+ const objectName = object.type === 'Identifier' ? object.name : 'this';
2186
+ const methodName = isComputed ? '<computed>' : property.name;
2187
+ const fullName = `${objectName}.${methodName}`;
2188
+
2189
+ // Generate semantic ID (primary) or legacy ID (fallback)
2190
+ const legacyId = `CALL#${fullName}#${module.file}#${getLine(callNode)}:${getColumn(callNode)}:${callSiteCounterRef.value++}`;
2191
+
2192
+ let methodCallId = legacyId;
2193
+ if (scopeTracker) {
2194
+ const discriminator = scopeTracker.getItemCounter(`CALL:${fullName}`);
2195
+ methodCallId = computeSemanticId('CALL', fullName, scopeTracker.getContext(), { discriminator });
2196
+ }
2197
+
2198
+ methodCalls.push({
2199
+ id: methodCallId,
2200
+ type: 'CALL',
2201
+ name: fullName,
2202
+ object: objectName,
2203
+ method: methodName,
2204
+ computed: isComputed,
2205
+ computedPropertyVar: isComputed ? property.name : null,
2206
+ file: module.file,
2207
+ line: getLine(callNode),
2208
+ column: getColumn(callNode),
2209
+ parentScopeId
2210
+ });
2211
+
2212
+ // Check for array mutation methods (push, unshift, splice)
2213
+ const ARRAY_MUTATION_METHODS = ['push', 'unshift', 'splice'];
2214
+ if (ARRAY_MUTATION_METHODS.includes(methodName)) {
2215
+ // Initialize collection if not exists
2216
+ if (!collections.arrayMutations) {
2217
+ collections.arrayMutations = [];
1675
2218
  }
2219
+ const arrayMutations = collections.arrayMutations as ArrayMutationInfo[];
2220
+ this.detectArrayMutationInFunction(
2221
+ callNode,
2222
+ objectName,
2223
+ methodName as 'push' | 'unshift' | 'splice',
2224
+ module,
2225
+ arrayMutations,
2226
+ scopeTracker
2227
+ );
2228
+ }
2229
+
2230
+ // Check for Object.assign() calls
2231
+ if (objectName === 'Object' && methodName === 'assign') {
2232
+ // Initialize collection if not exists
2233
+ if (!collections.objectMutations) {
2234
+ collections.objectMutations = [];
2235
+ }
2236
+ const objectMutations = collections.objectMutations as ObjectMutationInfo[];
2237
+ this.detectObjectAssignInFunction(
2238
+ callNode,
2239
+ module,
2240
+ objectMutations,
2241
+ scopeTracker
2242
+ );
2243
+ }
2244
+ }
2245
+ // REG-117: Nested array mutations like obj.arr.push(item)
2246
+ // object is MemberExpression, property is the method name
2247
+ else if (object.type === 'MemberExpression' && property.type === 'Identifier') {
2248
+ const nestedMember = object;
2249
+ const methodName = property.name;
2250
+ const ARRAY_MUTATION_METHODS = ['push', 'unshift', 'splice'];
2251
+
2252
+ if (ARRAY_MUTATION_METHODS.includes(methodName)) {
2253
+ // Extract base object and property from nested MemberExpression
2254
+ const base = nestedMember.object;
2255
+ const prop = nestedMember.property;
2256
+
2257
+ // Only handle single-level nesting: obj.arr.push() or this.items.push()
2258
+ if ((base.type === 'Identifier' || base.type === 'ThisExpression') &&
2259
+ !nestedMember.computed &&
2260
+ prop.type === 'Identifier') {
2261
+ const baseObjectName = base.type === 'Identifier' ? base.name : 'this';
2262
+ const propertyName = prop.name;
2263
+
2264
+ // Initialize collection if not exists
2265
+ if (!collections.arrayMutations) {
2266
+ collections.arrayMutations = [];
2267
+ }
2268
+ const arrayMutations = collections.arrayMutations as ArrayMutationInfo[];
2269
+
2270
+ this.detectArrayMutationInFunction(
2271
+ callNode,
2272
+ `${baseObjectName}.${propertyName}`, // arrayName for ID purposes
2273
+ methodName as 'push' | 'unshift' | 'splice',
2274
+ module,
2275
+ arrayMutations,
2276
+ scopeTracker,
2277
+ true, // isNested
2278
+ baseObjectName,
2279
+ propertyName
2280
+ );
2281
+ }
2282
+ }
2283
+ }
2284
+ }
2285
+ }
2286
+
2287
+ /**
2288
+ * Detect array mutation calls (push, unshift, splice) inside functions
2289
+ * and collect mutation info for FLOWS_INTO edge creation in GraphBuilder
2290
+ *
2291
+ * REG-117: Added isNested, baseObjectName, propertyName for nested mutations
2292
+ *
2293
+ * @param callNode - The call expression node
2294
+ * @param arrayName - Name of the array being mutated
2295
+ * @param method - The mutation method (push, unshift, splice)
2296
+ * @param module - Current module being analyzed
2297
+ * @param arrayMutations - Collection to push mutation info into
2298
+ * @param scopeTracker - Optional scope tracker for semantic IDs
2299
+ * @param isNested - REG-117: true if this is a nested mutation (obj.arr.push)
2300
+ * @param baseObjectName - REG-117: base object name for nested mutations
2301
+ * @param propertyName - REG-117: property name for nested mutations
2302
+ */
2303
+ private detectArrayMutationInFunction(
2304
+ callNode: t.CallExpression,
2305
+ arrayName: string,
2306
+ method: 'push' | 'unshift' | 'splice',
2307
+ module: VisitorModule,
2308
+ arrayMutations: ArrayMutationInfo[],
2309
+ scopeTracker?: ScopeTracker,
2310
+ isNested?: boolean,
2311
+ baseObjectName?: string,
2312
+ propertyName?: string
2313
+ ): void {
2314
+ const mutationArgs: ArrayMutationArgument[] = [];
2315
+
2316
+ // For splice, only arguments from index 2 onwards are insertions
2317
+ // splice(start, deleteCount, item1, item2, ...)
2318
+ callNode.arguments.forEach((arg, index) => {
2319
+ // Skip start and deleteCount for splice
2320
+ if (method === 'splice' && index < 2) return;
2321
+
2322
+ const argInfo: ArrayMutationArgument = {
2323
+ argIndex: method === 'splice' ? index - 2 : index,
2324
+ isSpread: arg.type === 'SpreadElement',
2325
+ valueType: 'EXPRESSION' // Default
2326
+ };
2327
+
2328
+ let actualArg: t.Node = arg;
2329
+ if (arg.type === 'SpreadElement') {
2330
+ actualArg = arg.argument;
2331
+ }
2332
+
2333
+ // Determine value type
2334
+ const literalValue = ExpressionEvaluator.extractLiteralValue(actualArg);
2335
+ if (literalValue !== null) {
2336
+ argInfo.valueType = 'LITERAL';
2337
+ argInfo.literalValue = literalValue;
2338
+ } else if (actualArg.type === 'Identifier') {
2339
+ argInfo.valueType = 'VARIABLE';
2340
+ argInfo.valueName = actualArg.name;
2341
+ } else if (actualArg.type === 'ObjectExpression') {
2342
+ argInfo.valueType = 'OBJECT_LITERAL';
2343
+ } else if (actualArg.type === 'ArrayExpression') {
2344
+ argInfo.valueType = 'ARRAY_LITERAL';
2345
+ } else if (actualArg.type === 'CallExpression') {
2346
+ argInfo.valueType = 'CALL';
2347
+ argInfo.callLine = actualArg.loc?.start.line;
2348
+ argInfo.callColumn = actualArg.loc?.start.column;
2349
+ }
2350
+
2351
+ mutationArgs.push(argInfo);
2352
+ });
2353
+
2354
+ // Only record if there are actual insertions
2355
+ if (mutationArgs.length > 0) {
2356
+ const line = callNode.loc?.start.line ?? 0;
2357
+ const column = callNode.loc?.start.column ?? 0;
2358
+
2359
+ // Generate semantic ID for array mutation if scopeTracker available
2360
+ let mutationId: string | undefined;
2361
+ if (scopeTracker) {
2362
+ const discriminator = scopeTracker.getItemCounter(`ARRAY_MUTATION:${arrayName}.${method}`);
2363
+ mutationId = computeSemanticId('ARRAY_MUTATION', `${arrayName}.${method}`, scopeTracker.getContext(), { discriminator });
2364
+ }
2365
+
2366
+ arrayMutations.push({
2367
+ id: mutationId,
2368
+ arrayName,
2369
+ mutationMethod: method,
2370
+ file: module.file,
2371
+ line,
2372
+ column,
2373
+ insertedValues: mutationArgs,
2374
+ // REG-117: Nested mutation fields
2375
+ isNested,
2376
+ baseObjectName,
2377
+ propertyName
2378
+ });
2379
+ }
2380
+ }
2381
+
2382
+ /**
2383
+ * Detect indexed array assignment: arr[i] = value
2384
+ * Creates ArrayMutationInfo for FLOWS_INTO edge generation in GraphBuilder
2385
+ *
2386
+ * @param assignNode - The assignment expression node
2387
+ * @param module - Current module being analyzed
2388
+ * @param arrayMutations - Collection to push mutation info into
2389
+ */
2390
+ private detectIndexedArrayAssignment(
2391
+ assignNode: t.AssignmentExpression,
2392
+ module: VisitorModule,
2393
+ arrayMutations: ArrayMutationInfo[]
2394
+ ): void {
2395
+ // Check for indexed array assignment: arr[i] = value
2396
+ if (assignNode.left.type === 'MemberExpression' && assignNode.left.computed) {
2397
+ const memberExpr = assignNode.left;
2398
+
2399
+ // Only process NumericLiteral keys - those are clearly array indexed assignments
2400
+ // e.g., arr[0] = value, arr[1] = value
2401
+ // All other computed keys (StringLiteral, Identifier, expressions) are handled as object mutations
2402
+ // This avoids duplicate edge creation for ambiguous cases like obj[key] = value
2403
+ if (memberExpr.property.type !== 'NumericLiteral') {
2404
+ return;
2405
+ }
2406
+
2407
+ // Get array name (only simple identifiers for now)
2408
+ if (memberExpr.object.type === 'Identifier') {
2409
+ const arrayName = memberExpr.object.name;
2410
+ const value = assignNode.right;
2411
+
2412
+ const argInfo: ArrayMutationArgument = {
2413
+ argIndex: 0,
2414
+ isSpread: false,
2415
+ valueType: 'EXPRESSION'
2416
+ };
2417
+
2418
+ // Determine value type
2419
+ const literalValue = ExpressionEvaluator.extractLiteralValue(value);
2420
+ if (literalValue !== null) {
2421
+ argInfo.valueType = 'LITERAL';
2422
+ argInfo.literalValue = literalValue;
2423
+ } else if (value.type === 'Identifier') {
2424
+ argInfo.valueType = 'VARIABLE';
2425
+ argInfo.valueName = value.name;
2426
+ } else if (value.type === 'ObjectExpression') {
2427
+ argInfo.valueType = 'OBJECT_LITERAL';
2428
+ } else if (value.type === 'ArrayExpression') {
2429
+ argInfo.valueType = 'ARRAY_LITERAL';
2430
+ } else if (value.type === 'CallExpression') {
2431
+ argInfo.valueType = 'CALL';
2432
+ argInfo.callLine = value.loc?.start.line;
2433
+ argInfo.callColumn = value.loc?.start.column;
1676
2434
  }
2435
+
2436
+ // Use defensive loc checks instead of ! assertions
2437
+ const line = assignNode.loc?.start.line ?? 0;
2438
+ const column = assignNode.loc?.start.column ?? 0;
2439
+
2440
+ arrayMutations.push({
2441
+ arrayName,
2442
+ mutationMethod: 'indexed',
2443
+ file: module.file,
2444
+ line: line,
2445
+ column: column,
2446
+ insertedValues: [argInfo]
2447
+ });
1677
2448
  }
2449
+ }
2450
+ }
2451
+
2452
+ /**
2453
+ * Detect object property assignment: obj.prop = value, obj['prop'] = value
2454
+ * Creates ObjectMutationInfo for FLOWS_INTO edge generation in GraphBuilder
2455
+ *
2456
+ * @param assignNode - The assignment expression node
2457
+ * @param module - Current module being analyzed
2458
+ * @param objectMutations - Collection to push mutation info into
2459
+ * @param scopeTracker - Optional scope tracker for semantic IDs
2460
+ */
2461
+ private detectObjectPropertyAssignment(
2462
+ assignNode: t.AssignmentExpression,
2463
+ module: VisitorModule,
2464
+ objectMutations: ObjectMutationInfo[],
2465
+ scopeTracker?: ScopeTracker
2466
+ ): void {
2467
+ // Check for property assignment: obj.prop = value or obj['prop'] = value
2468
+ if (assignNode.left.type !== 'MemberExpression') return;
2469
+
2470
+ const memberExpr = assignNode.left;
2471
+
2472
+ // Skip NumericLiteral indexed assignment (handled by array mutation handler)
2473
+ // Array mutation handler processes: arr[0] (numeric literal index)
2474
+ // Object mutation handler processes: obj.prop, obj['prop'], obj[key], obj[expr]
2475
+ if (memberExpr.computed && memberExpr.property.type === 'NumericLiteral') {
2476
+ return; // Let array mutation handler deal with this
2477
+ }
2478
+
2479
+ // Get object name and enclosing class context for 'this'
2480
+ let objectName: string;
2481
+ let enclosingClassName: string | undefined;
2482
+
2483
+ if (memberExpr.object.type === 'Identifier') {
2484
+ objectName = memberExpr.object.name;
2485
+ } else if (memberExpr.object.type === 'ThisExpression') {
2486
+ objectName = 'this';
2487
+ // REG-152: Extract enclosing class name from scope context
2488
+ if (scopeTracker) {
2489
+ enclosingClassName = scopeTracker.getEnclosingScope('CLASS');
2490
+ }
2491
+ } else {
2492
+ // Complex expressions like obj.nested.prop = value
2493
+ // For now, skip these (documented limitation)
2494
+ return;
2495
+ }
2496
+
2497
+ // Get property name
2498
+ let propertyName: string;
2499
+ let mutationType: 'property' | 'computed';
2500
+ let computedPropertyVar: string | undefined;
2501
+
2502
+ if (!memberExpr.computed) {
2503
+ // obj.prop
2504
+ if (memberExpr.property.type === 'Identifier') {
2505
+ propertyName = memberExpr.property.name;
2506
+ mutationType = 'property';
2507
+ } else {
2508
+ return; // Unexpected property type
2509
+ }
2510
+ } else {
2511
+ // obj['prop'] or obj[key]
2512
+ if (memberExpr.property.type === 'StringLiteral') {
2513
+ propertyName = memberExpr.property.value;
2514
+ mutationType = 'property'; // String literal is effectively a property name
2515
+ } else {
2516
+ propertyName = '<computed>';
2517
+ mutationType = 'computed';
2518
+ // Capture variable name for later resolution in enrichment phase
2519
+ if (memberExpr.property.type === 'Identifier') {
2520
+ computedPropertyVar = memberExpr.property.name;
2521
+ }
2522
+ }
2523
+ }
2524
+
2525
+ // Extract value info
2526
+ const value = assignNode.right;
2527
+ const valueInfo = this.extractMutationValue(value);
2528
+
2529
+ // Use defensive loc checks
2530
+ const line = assignNode.loc?.start.line ?? 0;
2531
+ const column = assignNode.loc?.start.column ?? 0;
2532
+
2533
+ // Generate semantic ID if scopeTracker available
2534
+ let mutationId: string | undefined;
2535
+ if (scopeTracker) {
2536
+ const discriminator = scopeTracker.getItemCounter(`OBJECT_MUTATION:${objectName}.${propertyName}`);
2537
+ mutationId = computeSemanticId('OBJECT_MUTATION', `${objectName}.${propertyName}`, scopeTracker.getContext(), { discriminator });
2538
+ }
2539
+
2540
+ objectMutations.push({
2541
+ id: mutationId,
2542
+ objectName,
2543
+ enclosingClassName, // REG-152: Class name for 'this' mutations
2544
+ propertyName,
2545
+ mutationType,
2546
+ computedPropertyVar,
2547
+ file: module.file,
2548
+ line,
2549
+ column,
2550
+ value: valueInfo
1678
2551
  });
1679
2552
  }
2553
+
2554
+ /**
2555
+ * Extract value information from an expression for mutation tracking
2556
+ */
2557
+ private extractMutationValue(value: t.Expression): ObjectMutationValue {
2558
+ const valueInfo: ObjectMutationValue = {
2559
+ valueType: 'EXPRESSION' // Default
2560
+ };
2561
+
2562
+ const literalValue = ExpressionEvaluator.extractLiteralValue(value);
2563
+ if (literalValue !== null) {
2564
+ valueInfo.valueType = 'LITERAL';
2565
+ valueInfo.literalValue = literalValue;
2566
+ } else if (value.type === 'Identifier') {
2567
+ valueInfo.valueType = 'VARIABLE';
2568
+ valueInfo.valueName = value.name;
2569
+ } else if (value.type === 'ObjectExpression') {
2570
+ valueInfo.valueType = 'OBJECT_LITERAL';
2571
+ } else if (value.type === 'ArrayExpression') {
2572
+ valueInfo.valueType = 'ARRAY_LITERAL';
2573
+ } else if (value.type === 'CallExpression') {
2574
+ valueInfo.valueType = 'CALL';
2575
+ valueInfo.callLine = value.loc?.start.line;
2576
+ valueInfo.callColumn = value.loc?.start.column;
2577
+ }
2578
+
2579
+ return valueInfo;
2580
+ }
2581
+
2582
+ /**
2583
+ * Detect Object.assign() calls inside functions
2584
+ * Creates ObjectMutationInfo for FLOWS_INTO edge generation in GraphBuilder
2585
+ */
2586
+ private detectObjectAssignInFunction(
2587
+ callNode: t.CallExpression,
2588
+ module: VisitorModule,
2589
+ objectMutations: ObjectMutationInfo[],
2590
+ scopeTracker?: ScopeTracker
2591
+ ): void {
2592
+ // Need at least 2 arguments: target and at least one source
2593
+ if (callNode.arguments.length < 2) return;
2594
+
2595
+ // First argument is target
2596
+ const targetArg = callNode.arguments[0];
2597
+ let targetName: string;
2598
+
2599
+ if (targetArg.type === 'Identifier') {
2600
+ targetName = targetArg.name;
2601
+ } else if (targetArg.type === 'ObjectExpression') {
2602
+ targetName = '<anonymous>';
2603
+ } else {
2604
+ return;
2605
+ }
2606
+
2607
+ const line = callNode.loc?.start.line ?? 0;
2608
+ const column = callNode.loc?.start.column ?? 0;
2609
+
2610
+ for (let i = 1; i < callNode.arguments.length; i++) {
2611
+ let arg = callNode.arguments[i];
2612
+ let isSpread = false;
2613
+
2614
+ if (arg.type === 'SpreadElement') {
2615
+ isSpread = true;
2616
+ arg = arg.argument;
2617
+ }
2618
+
2619
+ const valueInfo: ObjectMutationValue = {
2620
+ valueType: 'EXPRESSION',
2621
+ argIndex: i - 1,
2622
+ isSpread
2623
+ };
2624
+
2625
+ const literalValue = ExpressionEvaluator.extractLiteralValue(arg);
2626
+ if (literalValue !== null) {
2627
+ valueInfo.valueType = 'LITERAL';
2628
+ valueInfo.literalValue = literalValue;
2629
+ } else if (arg.type === 'Identifier') {
2630
+ valueInfo.valueType = 'VARIABLE';
2631
+ valueInfo.valueName = arg.name;
2632
+ } else if (arg.type === 'ObjectExpression') {
2633
+ valueInfo.valueType = 'OBJECT_LITERAL';
2634
+ } else if (arg.type === 'ArrayExpression') {
2635
+ valueInfo.valueType = 'ARRAY_LITERAL';
2636
+ } else if (arg.type === 'CallExpression') {
2637
+ valueInfo.valueType = 'CALL';
2638
+ valueInfo.callLine = arg.loc?.start.line;
2639
+ valueInfo.callColumn = arg.loc?.start.column;
2640
+ }
2641
+
2642
+ let mutationId: string | undefined;
2643
+ if (scopeTracker) {
2644
+ const discriminator = scopeTracker.getItemCounter(`OBJECT_MUTATION:Object.assign:${targetName}`);
2645
+ mutationId = computeSemanticId('OBJECT_MUTATION', `Object.assign:${targetName}`, scopeTracker.getContext(), { discriminator });
2646
+ }
2647
+
2648
+ objectMutations.push({
2649
+ id: mutationId,
2650
+ objectName: targetName,
2651
+ propertyName: '<assign>',
2652
+ mutationType: 'assign',
2653
+ file: module.file,
2654
+ line,
2655
+ column,
2656
+ value: valueInfo
2657
+ });
2658
+ }
2659
+ }
1680
2660
  }