@grafema/core 0.1.1-alpha → 0.2.0-beta

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (319) hide show
  1. package/dist/Orchestrator.d.ts +7 -0
  2. package/dist/Orchestrator.d.ts.map +1 -1
  3. package/dist/Orchestrator.js +25 -3
  4. package/dist/config/ConfigLoader.d.ts +18 -0
  5. package/dist/config/ConfigLoader.d.ts.map +1 -1
  6. package/dist/config/ConfigLoader.js +65 -3
  7. package/dist/core/FileExplainer.d.ts +101 -0
  8. package/dist/core/FileExplainer.d.ts.map +1 -0
  9. package/dist/core/FileExplainer.js +139 -0
  10. package/dist/core/NodeFactory.d.ts +44 -5
  11. package/dist/core/NodeFactory.d.ts.map +1 -1
  12. package/dist/core/NodeFactory.js +52 -7
  13. package/dist/core/nodes/ArrayLiteralNode.d.ts.map +1 -1
  14. package/dist/core/nodes/ArrayLiteralNode.js +4 -2
  15. package/dist/core/nodes/BranchNode.d.ts +41 -0
  16. package/dist/core/nodes/BranchNode.d.ts.map +1 -0
  17. package/dist/core/nodes/BranchNode.js +82 -0
  18. package/dist/core/nodes/CallSiteNode.d.ts +2 -2
  19. package/dist/core/nodes/CallSiteNode.d.ts.map +1 -1
  20. package/dist/core/nodes/CallSiteNode.js +9 -5
  21. package/dist/core/nodes/CaseNode.d.ts +43 -0
  22. package/dist/core/nodes/CaseNode.d.ts.map +1 -0
  23. package/dist/core/nodes/CaseNode.js +81 -0
  24. package/dist/core/nodes/ClassNode.d.ts +2 -2
  25. package/dist/core/nodes/ClassNode.d.ts.map +1 -1
  26. package/dist/core/nodes/ClassNode.js +8 -4
  27. package/dist/core/nodes/ConstantNode.d.ts +2 -2
  28. package/dist/core/nodes/ConstantNode.d.ts.map +1 -1
  29. package/dist/core/nodes/ConstantNode.js +6 -4
  30. package/dist/core/nodes/ConstructorCallNode.d.ts +51 -0
  31. package/dist/core/nodes/ConstructorCallNode.d.ts.map +1 -0
  32. package/dist/core/nodes/ConstructorCallNode.js +171 -0
  33. package/dist/core/nodes/DatabaseQueryNode.d.ts +3 -2
  34. package/dist/core/nodes/DatabaseQueryNode.d.ts.map +1 -1
  35. package/dist/core/nodes/DatabaseQueryNode.js +5 -2
  36. package/dist/core/nodes/DecoratorNode.d.ts +2 -2
  37. package/dist/core/nodes/DecoratorNode.d.ts.map +1 -1
  38. package/dist/core/nodes/DecoratorNode.js +5 -3
  39. package/dist/core/nodes/EnumNode.d.ts +2 -2
  40. package/dist/core/nodes/EnumNode.d.ts.map +1 -1
  41. package/dist/core/nodes/EnumNode.js +5 -3
  42. package/dist/core/nodes/EventListenerNode.d.ts +4 -4
  43. package/dist/core/nodes/EventListenerNode.d.ts.map +1 -1
  44. package/dist/core/nodes/EventListenerNode.js +7 -4
  45. package/dist/core/nodes/ExportNode.d.ts +2 -2
  46. package/dist/core/nodes/ExportNode.d.ts.map +1 -1
  47. package/dist/core/nodes/ExportNode.js +8 -4
  48. package/dist/core/nodes/ExpressionNode.d.ts +2 -2
  49. package/dist/core/nodes/ExpressionNode.d.ts.map +1 -1
  50. package/dist/core/nodes/ExpressionNode.js +6 -4
  51. package/dist/core/nodes/ExternalModuleNode.d.ts +4 -0
  52. package/dist/core/nodes/ExternalModuleNode.d.ts.map +1 -1
  53. package/dist/core/nodes/ExternalModuleNode.js +10 -2
  54. package/dist/core/nodes/HttpRequestNode.d.ts +4 -4
  55. package/dist/core/nodes/HttpRequestNode.d.ts.map +1 -1
  56. package/dist/core/nodes/HttpRequestNode.js +7 -4
  57. package/dist/core/nodes/ImportNode.d.ts +10 -2
  58. package/dist/core/nodes/ImportNode.d.ts.map +1 -1
  59. package/dist/core/nodes/ImportNode.js +21 -4
  60. package/dist/core/nodes/InterfaceNode.d.ts +2 -2
  61. package/dist/core/nodes/InterfaceNode.d.ts.map +1 -1
  62. package/dist/core/nodes/InterfaceNode.js +5 -3
  63. package/dist/core/nodes/LiteralNode.d.ts +2 -2
  64. package/dist/core/nodes/LiteralNode.d.ts.map +1 -1
  65. package/dist/core/nodes/LiteralNode.js +6 -4
  66. package/dist/core/nodes/MethodCallNode.d.ts +2 -2
  67. package/dist/core/nodes/MethodCallNode.d.ts.map +1 -1
  68. package/dist/core/nodes/MethodCallNode.js +9 -5
  69. package/dist/core/nodes/MethodNode.d.ts +2 -2
  70. package/dist/core/nodes/MethodNode.d.ts.map +1 -1
  71. package/dist/core/nodes/MethodNode.js +8 -4
  72. package/dist/core/nodes/ObjectLiteralNode.d.ts.map +1 -1
  73. package/dist/core/nodes/ObjectLiteralNode.js +4 -2
  74. package/dist/core/nodes/ParameterNode.d.ts +2 -2
  75. package/dist/core/nodes/ParameterNode.d.ts.map +1 -1
  76. package/dist/core/nodes/ParameterNode.js +5 -3
  77. package/dist/core/nodes/TypeNode.d.ts +2 -2
  78. package/dist/core/nodes/TypeNode.d.ts.map +1 -1
  79. package/dist/core/nodes/TypeNode.js +5 -3
  80. package/dist/core/nodes/VariableDeclarationNode.d.ts +2 -2
  81. package/dist/core/nodes/VariableDeclarationNode.d.ts.map +1 -1
  82. package/dist/core/nodes/VariableDeclarationNode.js +9 -5
  83. package/dist/core/nodes/index.d.ts +3 -0
  84. package/dist/core/nodes/index.d.ts.map +1 -1
  85. package/dist/core/nodes/index.js +3 -0
  86. package/dist/data/builtins/BuiltinRegistry.d.ts +78 -0
  87. package/dist/data/builtins/BuiltinRegistry.d.ts.map +1 -0
  88. package/dist/data/builtins/BuiltinRegistry.js +110 -0
  89. package/dist/data/builtins/definitions.d.ts +28 -0
  90. package/dist/data/builtins/definitions.d.ts.map +1 -0
  91. package/dist/data/builtins/definitions.js +250 -0
  92. package/dist/data/builtins/index.d.ts +10 -0
  93. package/dist/data/builtins/index.d.ts.map +1 -0
  94. package/dist/data/builtins/index.js +8 -0
  95. package/dist/data/builtins/jsGlobals.d.ts +18 -0
  96. package/dist/data/builtins/jsGlobals.d.ts.map +1 -0
  97. package/dist/data/builtins/jsGlobals.js +26 -0
  98. package/dist/data/builtins/types.d.ts +34 -0
  99. package/dist/data/builtins/types.d.ts.map +1 -0
  100. package/dist/data/builtins/types.js +7 -0
  101. package/dist/data/globals/definitions.d.ts +27 -0
  102. package/dist/data/globals/definitions.d.ts.map +1 -0
  103. package/dist/data/globals/definitions.js +117 -0
  104. package/dist/data/globals/index.d.ts +36 -0
  105. package/dist/data/globals/index.d.ts.map +1 -0
  106. package/dist/data/globals/index.js +52 -0
  107. package/dist/diagnostics/DiagnosticReporter.d.ts +23 -0
  108. package/dist/diagnostics/DiagnosticReporter.d.ts.map +1 -1
  109. package/dist/diagnostics/DiagnosticReporter.js +88 -0
  110. package/dist/diagnostics/index.d.ts +1 -1
  111. package/dist/diagnostics/index.d.ts.map +1 -1
  112. package/dist/errors/GrafemaError.d.ts +43 -0
  113. package/dist/errors/GrafemaError.d.ts.map +1 -1
  114. package/dist/errors/GrafemaError.js +50 -0
  115. package/dist/index.d.ts +17 -1
  116. package/dist/index.d.ts.map +1 -1
  117. package/dist/index.js +17 -1
  118. package/dist/plugins/analysis/DatabaseAnalyzer.d.ts.map +1 -1
  119. package/dist/plugins/analysis/DatabaseAnalyzer.js +3 -2
  120. package/dist/plugins/analysis/ExpressAnalyzer.d.ts.map +1 -1
  121. package/dist/plugins/analysis/ExpressAnalyzer.js +3 -1
  122. package/dist/plugins/analysis/ExpressResponseAnalyzer.d.ts +148 -0
  123. package/dist/plugins/analysis/ExpressResponseAnalyzer.d.ts.map +1 -0
  124. package/dist/plugins/analysis/ExpressResponseAnalyzer.js +495 -0
  125. package/dist/plugins/analysis/ExpressRouteAnalyzer.d.ts.map +1 -1
  126. package/dist/plugins/analysis/ExpressRouteAnalyzer.js +53 -18
  127. package/dist/plugins/analysis/FetchAnalyzer.d.ts +40 -0
  128. package/dist/plugins/analysis/FetchAnalyzer.d.ts.map +1 -1
  129. package/dist/plugins/analysis/FetchAnalyzer.js +163 -15
  130. package/dist/plugins/analysis/JSASTAnalyzer.d.ts +157 -26
  131. package/dist/plugins/analysis/JSASTAnalyzer.d.ts.map +1 -1
  132. package/dist/plugins/analysis/JSASTAnalyzer.js +2418 -191
  133. package/dist/plugins/analysis/RustAnalyzer.js +4 -4
  134. package/dist/plugins/analysis/SQLiteAnalyzer.d.ts.map +1 -1
  135. package/dist/plugins/analysis/SQLiteAnalyzer.js +4 -2
  136. package/dist/plugins/analysis/SocketIOAnalyzer.d.ts +9 -0
  137. package/dist/plugins/analysis/SocketIOAnalyzer.d.ts.map +1 -1
  138. package/dist/plugins/analysis/SocketIOAnalyzer.js +91 -7
  139. package/dist/plugins/analysis/ast/GraphBuilder.d.ts +173 -0
  140. package/dist/plugins/analysis/ast/GraphBuilder.d.ts.map +1 -1
  141. package/dist/plugins/analysis/ast/GraphBuilder.js +1256 -65
  142. package/dist/plugins/analysis/ast/types.d.ts +294 -0
  143. package/dist/plugins/analysis/ast/types.d.ts.map +1 -1
  144. package/dist/plugins/analysis/ast/visitors/ASTVisitor.d.ts +5 -1
  145. package/dist/plugins/analysis/ast/visitors/ASTVisitor.d.ts.map +1 -1
  146. package/dist/plugins/analysis/ast/visitors/CallExpressionVisitor.d.ts +1 -0
  147. package/dist/plugins/analysis/ast/visitors/CallExpressionVisitor.d.ts.map +1 -1
  148. package/dist/plugins/analysis/ast/visitors/CallExpressionVisitor.js +12 -1
  149. package/dist/plugins/analysis/ast/visitors/FunctionVisitor.d.ts +10 -0
  150. package/dist/plugins/analysis/ast/visitors/FunctionVisitor.d.ts.map +1 -1
  151. package/dist/plugins/analysis/ast/visitors/FunctionVisitor.js +62 -0
  152. package/dist/plugins/analysis/ast/visitors/ImportExportVisitor.d.ts +4 -0
  153. package/dist/plugins/analysis/ast/visitors/ImportExportVisitor.d.ts.map +1 -1
  154. package/dist/plugins/analysis/ast/visitors/ImportExportVisitor.js +101 -0
  155. package/dist/plugins/analysis/ast/visitors/VariableVisitor.d.ts +16 -1
  156. package/dist/plugins/analysis/ast/visitors/VariableVisitor.d.ts.map +1 -1
  157. package/dist/plugins/analysis/ast/visitors/VariableVisitor.js +233 -39
  158. package/dist/plugins/discovery/WorkspaceDiscovery.d.ts.map +1 -1
  159. package/dist/plugins/discovery/WorkspaceDiscovery.js +9 -4
  160. package/dist/plugins/enrichment/AliasTracker.d.ts.map +1 -1
  161. package/dist/plugins/enrichment/AliasTracker.js +16 -1
  162. package/dist/plugins/enrichment/ArgumentParameterLinker.d.ts +32 -0
  163. package/dist/plugins/enrichment/ArgumentParameterLinker.d.ts.map +1 -0
  164. package/dist/plugins/enrichment/ArgumentParameterLinker.js +175 -0
  165. package/dist/plugins/enrichment/ClosureCaptureEnricher.d.ts +51 -0
  166. package/dist/plugins/enrichment/ClosureCaptureEnricher.d.ts.map +1 -0
  167. package/dist/plugins/enrichment/ClosureCaptureEnricher.js +205 -0
  168. package/dist/plugins/enrichment/ExternalCallResolver.d.ts +42 -0
  169. package/dist/plugins/enrichment/ExternalCallResolver.d.ts.map +1 -0
  170. package/dist/plugins/enrichment/ExternalCallResolver.js +213 -0
  171. package/dist/plugins/enrichment/FunctionCallResolver.d.ts +58 -0
  172. package/dist/plugins/enrichment/FunctionCallResolver.d.ts.map +1 -0
  173. package/dist/plugins/enrichment/FunctionCallResolver.js +340 -0
  174. package/dist/plugins/enrichment/HTTPConnectionEnricher.d.ts +16 -3
  175. package/dist/plugins/enrichment/HTTPConnectionEnricher.d.ts.map +1 -1
  176. package/dist/plugins/enrichment/HTTPConnectionEnricher.js +64 -20
  177. package/dist/plugins/enrichment/MethodCallResolver.d.ts.map +1 -1
  178. package/dist/plugins/enrichment/MethodCallResolver.js +15 -1
  179. package/dist/plugins/enrichment/MountPointResolver.d.ts +14 -12
  180. package/dist/plugins/enrichment/MountPointResolver.d.ts.map +1 -1
  181. package/dist/plugins/enrichment/MountPointResolver.js +172 -151
  182. package/dist/plugins/enrichment/NodejsBuiltinsResolver.d.ts +44 -0
  183. package/dist/plugins/enrichment/NodejsBuiltinsResolver.d.ts.map +1 -0
  184. package/dist/plugins/enrichment/NodejsBuiltinsResolver.js +271 -0
  185. package/dist/plugins/enrichment/ValueDomainAnalyzer.d.ts +5 -27
  186. package/dist/plugins/enrichment/ValueDomainAnalyzer.d.ts.map +1 -1
  187. package/dist/plugins/enrichment/ValueDomainAnalyzer.js +62 -139
  188. package/dist/plugins/indexing/JSModuleIndexer.d.ts +15 -0
  189. package/dist/plugins/indexing/JSModuleIndexer.d.ts.map +1 -1
  190. package/dist/plugins/indexing/JSModuleIndexer.js +58 -0
  191. package/dist/plugins/indexing/RustModuleIndexer.d.ts +1 -1
  192. package/dist/plugins/indexing/RustModuleIndexer.js +4 -4
  193. package/dist/plugins/validation/BrokenImportValidator.d.ts +31 -0
  194. package/dist/plugins/validation/BrokenImportValidator.d.ts.map +1 -0
  195. package/dist/plugins/validation/BrokenImportValidator.js +249 -0
  196. package/dist/plugins/validation/CallResolverValidator.d.ts +21 -10
  197. package/dist/plugins/validation/CallResolverValidator.d.ts.map +1 -1
  198. package/dist/plugins/validation/CallResolverValidator.js +101 -76
  199. package/dist/plugins/validation/DataFlowValidator.d.ts.map +1 -1
  200. package/dist/plugins/validation/DataFlowValidator.js +49 -41
  201. package/dist/plugins/validation/GraphConnectivityValidator.d.ts.map +1 -1
  202. package/dist/plugins/validation/GraphConnectivityValidator.js +25 -1
  203. package/dist/plugins/validation/SQLInjectionValidator.d.ts.map +1 -1
  204. package/dist/plugins/validation/SQLInjectionValidator.js +2 -3
  205. package/dist/queries/findCallsInFunction.d.ts +52 -0
  206. package/dist/queries/findCallsInFunction.d.ts.map +1 -0
  207. package/dist/queries/findCallsInFunction.js +135 -0
  208. package/dist/queries/findContainingFunction.d.ts +45 -0
  209. package/dist/queries/findContainingFunction.d.ts.map +1 -0
  210. package/dist/queries/findContainingFunction.js +54 -0
  211. package/dist/queries/index.d.ts +14 -0
  212. package/dist/queries/index.d.ts.map +1 -0
  213. package/dist/queries/index.js +11 -0
  214. package/dist/queries/traceValues.d.ts +70 -0
  215. package/dist/queries/traceValues.d.ts.map +1 -0
  216. package/dist/queries/traceValues.js +299 -0
  217. package/dist/queries/types.d.ts +163 -0
  218. package/dist/queries/types.d.ts.map +1 -0
  219. package/dist/queries/types.js +9 -0
  220. package/dist/schema/GraphSchemaExtractor.d.ts +53 -0
  221. package/dist/schema/GraphSchemaExtractor.d.ts.map +1 -0
  222. package/dist/schema/GraphSchemaExtractor.js +124 -0
  223. package/dist/schema/InterfaceSchemaExtractor.d.ts +73 -0
  224. package/dist/schema/InterfaceSchemaExtractor.d.ts.map +1 -0
  225. package/dist/schema/InterfaceSchemaExtractor.js +112 -0
  226. package/dist/schema/index.d.ts +5 -0
  227. package/dist/schema/index.d.ts.map +1 -0
  228. package/dist/schema/index.js +2 -0
  229. package/dist/storage/backends/RFDBServerBackend.d.ts +12 -18
  230. package/dist/storage/backends/RFDBServerBackend.d.ts.map +1 -1
  231. package/dist/storage/backends/RFDBServerBackend.js +41 -52
  232. package/dist/storage/backends/typeValidation.d.ts.map +1 -1
  233. package/dist/storage/backends/typeValidation.js +1 -0
  234. package/package.json +3 -3
  235. package/src/Orchestrator.ts +35 -3
  236. package/src/config/ConfigLoader.ts +94 -3
  237. package/src/core/FileExplainer.ts +179 -0
  238. package/src/core/NodeFactory.ts +72 -8
  239. package/src/core/nodes/ArrayLiteralNode.ts +3 -2
  240. package/src/core/nodes/BranchNode.ts +113 -0
  241. package/src/core/nodes/CallSiteNode.ts +7 -5
  242. package/src/core/nodes/CaseNode.ts +123 -0
  243. package/src/core/nodes/ClassNode.ts +6 -4
  244. package/src/core/nodes/ConstantNode.ts +5 -4
  245. package/src/core/nodes/ConstructorCallNode.ts +217 -0
  246. package/src/core/nodes/DatabaseQueryNode.ts +5 -1
  247. package/src/core/nodes/DecoratorNode.ts +4 -3
  248. package/src/core/nodes/EnumNode.ts +4 -3
  249. package/src/core/nodes/EventListenerNode.ts +7 -4
  250. package/src/core/nodes/ExportNode.ts +6 -4
  251. package/src/core/nodes/ExpressionNode.ts +5 -4
  252. package/src/core/nodes/ExternalModuleNode.ts +11 -2
  253. package/src/core/nodes/HttpRequestNode.ts +7 -4
  254. package/src/core/nodes/ImportNode.ts +31 -4
  255. package/src/core/nodes/InterfaceNode.ts +4 -3
  256. package/src/core/nodes/LiteralNode.ts +5 -4
  257. package/src/core/nodes/MethodCallNode.ts +7 -5
  258. package/src/core/nodes/MethodNode.ts +6 -4
  259. package/src/core/nodes/ObjectLiteralNode.ts +3 -2
  260. package/src/core/nodes/ParameterNode.ts +4 -3
  261. package/src/core/nodes/TypeNode.ts +4 -3
  262. package/src/core/nodes/VariableDeclarationNode.ts +7 -5
  263. package/src/core/nodes/index.ts +3 -0
  264. package/src/data/builtins/BuiltinRegistry.ts +124 -0
  265. package/src/data/builtins/definitions.ts +267 -0
  266. package/src/data/builtins/index.ts +10 -0
  267. package/src/data/builtins/jsGlobals.ts +28 -0
  268. package/src/data/builtins/types.ts +36 -0
  269. package/src/data/globals/definitions.ts +156 -0
  270. package/src/data/globals/index.ts +66 -0
  271. package/src/diagnostics/DiagnosticReporter.ts +120 -0
  272. package/src/diagnostics/index.ts +1 -1
  273. package/src/errors/GrafemaError.ts +65 -0
  274. package/src/index.ts +45 -0
  275. package/src/plugins/analysis/DatabaseAnalyzer.ts +4 -2
  276. package/src/plugins/analysis/ExpressAnalyzer.ts +5 -1
  277. package/src/plugins/analysis/ExpressResponseAnalyzer.ts +636 -0
  278. package/src/plugins/analysis/ExpressRouteAnalyzer.ts +57 -18
  279. package/src/plugins/analysis/FetchAnalyzer.ts +204 -16
  280. package/src/plugins/analysis/JSASTAnalyzer.ts +2958 -260
  281. package/src/plugins/analysis/RustAnalyzer.ts +4 -4
  282. package/src/plugins/analysis/SQLiteAnalyzer.ts +5 -2
  283. package/src/plugins/analysis/SocketIOAnalyzer.ts +121 -7
  284. package/src/plugins/analysis/ast/GraphBuilder.ts +1578 -70
  285. package/src/plugins/analysis/ast/types.ts +387 -0
  286. package/src/plugins/analysis/ast/visitors/ASTVisitor.ts +8 -0
  287. package/src/plugins/analysis/ast/visitors/CallExpressionVisitor.ts +16 -1
  288. package/src/plugins/analysis/ast/visitors/FunctionVisitor.ts +77 -2
  289. package/src/plugins/analysis/ast/visitors/ImportExportVisitor.ts +112 -1
  290. package/src/plugins/analysis/ast/visitors/VariableVisitor.ts +272 -47
  291. package/src/plugins/discovery/WorkspaceDiscovery.ts +11 -4
  292. package/src/plugins/enrichment/AliasTracker.ts +22 -1
  293. package/src/plugins/enrichment/ArgumentParameterLinker.ts +240 -0
  294. package/src/plugins/enrichment/ClosureCaptureEnricher.ts +267 -0
  295. package/src/plugins/enrichment/ExternalCallResolver.ts +262 -0
  296. package/src/plugins/enrichment/FunctionCallResolver.ts +456 -0
  297. package/src/plugins/enrichment/HTTPConnectionEnricher.ts +70 -20
  298. package/src/plugins/enrichment/MethodCallResolver.ts +21 -1
  299. package/src/plugins/enrichment/MountPointResolver.ts +206 -198
  300. package/src/plugins/enrichment/NodejsBuiltinsResolver.ts +365 -0
  301. package/src/plugins/enrichment/ValueDomainAnalyzer.ts +67 -184
  302. package/src/plugins/indexing/JSModuleIndexer.ts +66 -0
  303. package/src/plugins/indexing/RustModuleIndexer.ts +4 -4
  304. package/src/plugins/validation/BrokenImportValidator.ts +325 -0
  305. package/src/plugins/validation/CallResolverValidator.ts +129 -109
  306. package/src/plugins/validation/DataFlowValidator.ts +75 -58
  307. package/src/plugins/validation/GraphConnectivityValidator.ts +39 -1
  308. package/src/plugins/validation/SQLInjectionValidator.ts +2 -5
  309. package/src/queries/README.md +46 -0
  310. package/src/queries/findCallsInFunction.ts +206 -0
  311. package/src/queries/findContainingFunction.ts +83 -0
  312. package/src/queries/index.ts +23 -0
  313. package/src/queries/traceValues.ts +398 -0
  314. package/src/queries/types.ts +187 -0
  315. package/src/schema/GraphSchemaExtractor.ts +177 -0
  316. package/src/schema/InterfaceSchemaExtractor.ts +173 -0
  317. package/src/schema/index.ts +5 -0
  318. package/src/storage/backends/RFDBServerBackend.ts +58 -70
  319. package/src/storage/backends/typeValidation.ts +1 -0
@@ -33,6 +33,9 @@ import { Profiler } from '../../core/Profiler.js';
33
33
  import { ScopeTracker } from '../../core/ScopeTracker.js';
34
34
  import { computeSemanticId } from '../../core/SemanticId.js';
35
35
  import { ExpressionNode } from '../../core/nodes/ExpressionNode.js';
36
+ import { ConstructorCallNode } from '../../core/nodes/ConstructorCallNode.js';
37
+ import { ObjectLiteralNode } from '../../core/nodes/ObjectLiteralNode.js';
38
+ import { NodeFactory } from '../../core/NodeFactory.js';
36
39
  export class JSASTAnalyzer extends Plugin {
37
40
  graphBuilder;
38
41
  analyzedModules;
@@ -61,7 +64,9 @@ export class JSASTAnalyzer extends Plugin {
61
64
  'WRITES_TO', 'IMPORTS', 'INSTANCE_OF', 'HANDLED_BY', 'HAS_CALLBACK',
62
65
  'PASSES_ARGUMENT', 'MAKES_REQUEST', 'IMPORTS_FROM', 'EXPORTS_TO', 'ASSIGNED_FROM',
63
66
  // TypeScript-specific edges
64
- 'IMPLEMENTS', 'EXTENDS', 'DECORATED_BY'
67
+ 'IMPLEMENTS', 'EXTENDS', 'DECORATED_BY',
68
+ // Promise data flow
69
+ 'RESOLVES_TO'
65
70
  ]
66
71
  },
67
72
  dependencies: ['JSModuleIndexer']
@@ -355,14 +360,30 @@ export class JSASTAnalyzer extends Plugin {
355
360
  /**
356
361
  * Отслеживает присваивание переменной для data flow анализа
357
362
  */
358
- trackVariableAssignment(initNode, variableId, variableName, module, line, literals, variableAssignments, literalCounterRef) {
363
+ trackVariableAssignment(initNode, variableId, variableName, module, line, literals, variableAssignments, literalCounterRef, objectLiterals, objectProperties, objectLiteralCounterRef) {
359
364
  if (!initNode)
360
365
  return;
361
366
  // initNode is already typed as t.Expression
362
367
  const initExpression = initNode;
363
368
  // 0. AwaitExpression
364
369
  if (initExpression.type === 'AwaitExpression') {
365
- return this.trackVariableAssignment(initExpression.argument, variableId, variableName, module, line, literals, variableAssignments, literalCounterRef);
370
+ return this.trackVariableAssignment(initExpression.argument, variableId, variableName, module, line, literals, variableAssignments, literalCounterRef, objectLiterals, objectProperties, objectLiteralCounterRef);
371
+ }
372
+ // 0.5. ObjectExpression (REG-328) - must be before literal check
373
+ if (initExpression.type === 'ObjectExpression') {
374
+ const column = initExpression.loc?.start.column ?? 0;
375
+ const objectNode = ObjectLiteralNode.create(module.file, line, column, { counter: objectLiteralCounterRef.value++ });
376
+ // Add to objectLiterals collection for GraphBuilder to create the node
377
+ objectLiterals.push(objectNode);
378
+ // Extract properties from the object literal
379
+ this.extractObjectProperties(initExpression, objectNode.id, module, objectProperties, objectLiterals, objectLiteralCounterRef, literals, literalCounterRef);
380
+ // Create ASSIGNED_FROM edge: VARIABLE -> OBJECT_LITERAL
381
+ variableAssignments.push({
382
+ variableId,
383
+ sourceId: objectNode.id,
384
+ sourceType: 'OBJECT_LITERAL'
385
+ });
386
+ return;
366
387
  }
367
388
  // 1. Literal
368
389
  const literalValue = ExpressionEvaluator.extractLiteralValue(initExpression);
@@ -457,17 +478,31 @@ export class JSASTAnalyzer extends Plugin {
457
478
  });
458
479
  return;
459
480
  }
460
- // 5. NewExpression
481
+ // 5. NewExpression -> CONSTRUCTOR_CALL
461
482
  if (initExpression.type === 'NewExpression') {
462
483
  const callee = initExpression.callee;
484
+ let className;
463
485
  if (callee.type === 'Identifier') {
464
- variableAssignments.push({
465
- variableId,
466
- sourceType: 'CLASS',
467
- className: callee.name,
468
- line: line
469
- });
486
+ className = callee.name;
487
+ }
488
+ else if (callee.type === 'MemberExpression' && callee.property.type === 'Identifier') {
489
+ // Handle: new module.ClassName()
490
+ className = callee.property.name;
491
+ }
492
+ else {
493
+ // Unknown callee type, skip
494
+ return;
470
495
  }
496
+ const callLine = initExpression.loc?.start.line ?? line;
497
+ const callColumn = initExpression.loc?.start.column ?? 0;
498
+ variableAssignments.push({
499
+ variableId,
500
+ sourceType: 'CONSTRUCTOR_CALL',
501
+ className,
502
+ file: module.file,
503
+ line: callLine,
504
+ column: callColumn
505
+ });
471
506
  return;
472
507
  }
473
508
  // 6. ArrowFunctionExpression or FunctionExpression
@@ -542,8 +577,8 @@ export class JSASTAnalyzer extends Plugin {
542
577
  line: line,
543
578
  column: column
544
579
  });
545
- this.trackVariableAssignment(initExpression.consequent, variableId, variableName, module, line, literals, variableAssignments, literalCounterRef);
546
- this.trackVariableAssignment(initExpression.alternate, variableId, variableName, module, line, literals, variableAssignments, literalCounterRef);
580
+ this.trackVariableAssignment(initExpression.consequent, variableId, variableName, module, line, literals, variableAssignments, literalCounterRef, objectLiterals, objectProperties, objectLiteralCounterRef);
581
+ this.trackVariableAssignment(initExpression.alternate, variableId, variableName, module, line, literals, variableAssignments, literalCounterRef, objectLiterals, objectProperties, objectLiteralCounterRef);
547
582
  return;
548
583
  }
549
584
  // 10. LogicalExpression
@@ -562,8 +597,8 @@ export class JSASTAnalyzer extends Plugin {
562
597
  line: line,
563
598
  column: column
564
599
  });
565
- this.trackVariableAssignment(initExpression.left, variableId, variableName, module, line, literals, variableAssignments, literalCounterRef);
566
- this.trackVariableAssignment(initExpression.right, variableId, variableName, module, line, literals, variableAssignments, literalCounterRef);
600
+ this.trackVariableAssignment(initExpression.left, variableId, variableName, module, line, literals, variableAssignments, literalCounterRef, objectLiterals, objectProperties, objectLiteralCounterRef);
601
+ this.trackVariableAssignment(initExpression.right, variableId, variableName, module, line, literals, variableAssignments, literalCounterRef, objectLiterals, objectProperties, objectLiteralCounterRef);
567
602
  return;
568
603
  }
569
604
  // 11. TemplateLiteral
@@ -586,12 +621,360 @@ export class JSASTAnalyzer extends Plugin {
586
621
  for (const expr of initExpression.expressions) {
587
622
  // Filter out TSType nodes (only in TypeScript code)
588
623
  if (t.isExpression(expr)) {
589
- this.trackVariableAssignment(expr, variableId, variableName, module, line, literals, variableAssignments, literalCounterRef);
624
+ this.trackVariableAssignment(expr, variableId, variableName, module, line, literals, variableAssignments, literalCounterRef, objectLiterals, objectProperties, objectLiteralCounterRef);
590
625
  }
591
626
  }
592
627
  return;
593
628
  }
594
629
  }
630
+ /**
631
+ * Extract object properties and create ObjectPropertyInfo records.
632
+ * Handles nested object/array literals recursively. (REG-328)
633
+ */
634
+ extractObjectProperties(objectExpr, objectId, module, objectProperties, objectLiterals, objectLiteralCounterRef, literals, literalCounterRef) {
635
+ for (const prop of objectExpr.properties) {
636
+ const propLine = prop.loc?.start.line || 0;
637
+ const propColumn = prop.loc?.start.column || 0;
638
+ // Handle spread properties: { ...other }
639
+ if (prop.type === 'SpreadElement') {
640
+ const spreadArg = prop.argument;
641
+ const propertyInfo = {
642
+ objectId,
643
+ propertyName: '<spread>',
644
+ valueType: 'SPREAD',
645
+ file: module.file,
646
+ line: propLine,
647
+ column: propColumn
648
+ };
649
+ if (spreadArg.type === 'Identifier') {
650
+ propertyInfo.valueName = spreadArg.name;
651
+ propertyInfo.valueType = 'VARIABLE';
652
+ }
653
+ objectProperties.push(propertyInfo);
654
+ continue;
655
+ }
656
+ // Handle regular properties
657
+ if (prop.type === 'ObjectProperty') {
658
+ let propertyName;
659
+ // Get property name
660
+ if (prop.key.type === 'Identifier') {
661
+ propertyName = prop.key.name;
662
+ }
663
+ else if (prop.key.type === 'StringLiteral') {
664
+ propertyName = prop.key.value;
665
+ }
666
+ else if (prop.key.type === 'NumericLiteral') {
667
+ propertyName = String(prop.key.value);
668
+ }
669
+ else {
670
+ propertyName = '<computed>';
671
+ }
672
+ const propertyInfo = {
673
+ objectId,
674
+ propertyName,
675
+ file: module.file,
676
+ line: propLine,
677
+ column: propColumn,
678
+ valueType: 'EXPRESSION'
679
+ };
680
+ const value = prop.value;
681
+ // Nested object literal - check BEFORE extractLiteralValue
682
+ if (value.type === 'ObjectExpression') {
683
+ const nestedObjectNode = ObjectLiteralNode.create(module.file, value.loc?.start.line || 0, value.loc?.start.column || 0, { counter: objectLiteralCounterRef.value++ });
684
+ objectLiterals.push(nestedObjectNode);
685
+ const nestedObjectId = nestedObjectNode.id;
686
+ // Recursively extract nested properties
687
+ this.extractObjectProperties(value, nestedObjectId, module, objectProperties, objectLiterals, objectLiteralCounterRef, literals, literalCounterRef);
688
+ propertyInfo.valueType = 'OBJECT_LITERAL';
689
+ propertyInfo.nestedObjectId = nestedObjectId;
690
+ propertyInfo.valueNodeId = nestedObjectId;
691
+ }
692
+ // Literal value (primitives only - objects/arrays handled above)
693
+ else {
694
+ const literalValue = ExpressionEvaluator.extractLiteralValue(value);
695
+ // Handle both non-null literals AND explicit null literals (NullLiteral)
696
+ if (literalValue !== null || value.type === 'NullLiteral') {
697
+ const literalId = `LITERAL#${propertyName}#${module.file}#${propLine}:${propColumn}:${literalCounterRef.value++}`;
698
+ literals.push({
699
+ id: literalId,
700
+ type: 'LITERAL',
701
+ value: literalValue,
702
+ valueType: typeof literalValue,
703
+ file: module.file,
704
+ line: propLine,
705
+ column: propColumn,
706
+ parentCallId: objectId,
707
+ argIndex: 0
708
+ });
709
+ propertyInfo.valueType = 'LITERAL';
710
+ propertyInfo.valueNodeId = literalId;
711
+ propertyInfo.literalValue = literalValue;
712
+ }
713
+ // Variable reference
714
+ else if (value.type === 'Identifier') {
715
+ propertyInfo.valueType = 'VARIABLE';
716
+ propertyInfo.valueName = value.name;
717
+ }
718
+ // Call expression
719
+ else if (value.type === 'CallExpression') {
720
+ propertyInfo.valueType = 'CALL';
721
+ propertyInfo.callLine = value.loc?.start.line;
722
+ propertyInfo.callColumn = value.loc?.start.column;
723
+ }
724
+ // Other expressions
725
+ else {
726
+ propertyInfo.valueType = 'EXPRESSION';
727
+ }
728
+ }
729
+ objectProperties.push(propertyInfo);
730
+ }
731
+ // Handle object methods: { foo() {} }
732
+ else if (prop.type === 'ObjectMethod') {
733
+ const propertyName = prop.key.type === 'Identifier' ? prop.key.name : '<computed>';
734
+ objectProperties.push({
735
+ objectId,
736
+ propertyName,
737
+ valueType: 'EXPRESSION',
738
+ file: module.file,
739
+ line: propLine,
740
+ column: propColumn
741
+ });
742
+ }
743
+ }
744
+ }
745
+ /**
746
+ * Recursively unwrap AwaitExpression to get the underlying expression.
747
+ * await await fetch() -> fetch()
748
+ */
749
+ unwrapAwaitExpression(node) {
750
+ if (node.type === 'AwaitExpression' && node.argument) {
751
+ return this.unwrapAwaitExpression(node.argument);
752
+ }
753
+ return node;
754
+ }
755
+ /**
756
+ * Extract call site information from CallExpression.
757
+ * Returns null if not a valid CallExpression.
758
+ */
759
+ extractCallInfo(node) {
760
+ if (node.type !== 'CallExpression') {
761
+ return null;
762
+ }
763
+ const callee = node.callee;
764
+ let name;
765
+ let isMethodCall = false;
766
+ // Direct call: fetchUser()
767
+ if (t.isIdentifier(callee)) {
768
+ name = callee.name;
769
+ }
770
+ // Method call: obj.fetchUser() or arr.map()
771
+ else if (t.isMemberExpression(callee)) {
772
+ isMethodCall = true;
773
+ const objectName = t.isIdentifier(callee.object)
774
+ ? callee.object.name
775
+ : (t.isThisExpression(callee.object) ? 'this' : 'unknown');
776
+ const methodName = t.isIdentifier(callee.property)
777
+ ? callee.property.name
778
+ : 'unknown';
779
+ name = `${objectName}.${methodName}`;
780
+ }
781
+ else {
782
+ return null;
783
+ }
784
+ return {
785
+ line: node.loc?.start.line ?? 0,
786
+ column: node.loc?.start.column ?? 0,
787
+ name,
788
+ isMethodCall
789
+ };
790
+ }
791
+ /**
792
+ * Check if expression is CallExpression or AwaitExpression wrapping a call.
793
+ */
794
+ isCallOrAwaitExpression(node) {
795
+ const unwrapped = this.unwrapAwaitExpression(node);
796
+ return unwrapped.type === 'CallExpression';
797
+ }
798
+ /**
799
+ * Tracks destructuring assignments for data flow analysis.
800
+ *
801
+ * For ObjectPattern: creates EXPRESSION nodes representing source.property
802
+ * For ArrayPattern: creates EXPRESSION nodes representing source[index]
803
+ *
804
+ * Supports:
805
+ * - Phase 1 (REG-201): Identifier init expressions (const { x } = obj)
806
+ * - Phase 2 (REG-223): CallExpression/AwaitExpression init (const { x } = getConfig())
807
+ *
808
+ * @param pattern - The destructuring pattern (ObjectPattern or ArrayPattern)
809
+ * @param initNode - The init expression (right-hand side)
810
+ * @param variables - Extracted variables with propertyPath/arrayIndex metadata and IDs
811
+ * @param module - Module context
812
+ * @param variableAssignments - Collection to push assignment info to
813
+ */
814
+ trackDestructuringAssignment(pattern, initNode, variables, module, variableAssignments) {
815
+ if (!initNode)
816
+ return;
817
+ // Phase 1: Simple Identifier init expressions (REG-201)
818
+ // Examples: const { x } = obj, const [a] = arr
819
+ if (t.isIdentifier(initNode)) {
820
+ const sourceBaseName = initNode.name;
821
+ // Process each extracted variable
822
+ for (const varInfo of variables) {
823
+ const variableId = varInfo.id;
824
+ // Handle rest elements specially - create edge to whole source
825
+ if (varInfo.isRest) {
826
+ variableAssignments.push({
827
+ variableId,
828
+ sourceType: 'VARIABLE',
829
+ sourceName: sourceBaseName,
830
+ line: varInfo.loc.start.line
831
+ });
832
+ continue;
833
+ }
834
+ // ObjectPattern: const { headers } = req → headers ASSIGNED_FROM req.headers
835
+ if (t.isObjectPattern(pattern) && varInfo.propertyPath && varInfo.propertyPath.length > 0) {
836
+ const propertyPath = varInfo.propertyPath;
837
+ const expressionLine = varInfo.loc.start.line;
838
+ const expressionColumn = varInfo.loc.start.column;
839
+ // Build property path string (e.g., "req.headers.contentType" for nested)
840
+ const fullPath = [sourceBaseName, ...propertyPath].join('.');
841
+ const expressionId = ExpressionNode.generateId('MemberExpression', module.file, expressionLine, expressionColumn);
842
+ variableAssignments.push({
843
+ variableId,
844
+ sourceType: 'EXPRESSION',
845
+ sourceId: expressionId,
846
+ expressionType: 'MemberExpression',
847
+ object: sourceBaseName,
848
+ property: propertyPath[propertyPath.length - 1], // Last property for simple display
849
+ computed: false,
850
+ path: fullPath,
851
+ objectSourceName: sourceBaseName, // Use objectSourceName for DERIVES_FROM edge creation
852
+ propertyPath: propertyPath,
853
+ file: module.file,
854
+ line: expressionLine,
855
+ column: expressionColumn
856
+ });
857
+ }
858
+ // ArrayPattern: const [first, second] = arr → first ASSIGNED_FROM arr[0]
859
+ else if (t.isArrayPattern(pattern) && varInfo.arrayIndex !== undefined) {
860
+ const arrayIndex = varInfo.arrayIndex;
861
+ const expressionLine = varInfo.loc.start.line;
862
+ const expressionColumn = varInfo.loc.start.column;
863
+ // Check if we also have propertyPath (mixed destructuring: { items: [first] } = data)
864
+ const hasPropertyPath = varInfo.propertyPath && varInfo.propertyPath.length > 0;
865
+ const expressionId = ExpressionNode.generateId('MemberExpression', module.file, expressionLine, expressionColumn);
866
+ variableAssignments.push({
867
+ variableId,
868
+ sourceType: 'EXPRESSION',
869
+ sourceId: expressionId,
870
+ expressionType: 'MemberExpression',
871
+ object: sourceBaseName,
872
+ property: String(arrayIndex),
873
+ computed: true,
874
+ objectSourceName: sourceBaseName, // Use objectSourceName for DERIVES_FROM edge creation
875
+ arrayIndex: arrayIndex,
876
+ propertyPath: hasPropertyPath ? varInfo.propertyPath : undefined,
877
+ file: module.file,
878
+ line: expressionLine,
879
+ column: expressionColumn
880
+ });
881
+ }
882
+ }
883
+ }
884
+ // Phase 2: CallExpression or AwaitExpression (REG-223)
885
+ else if (this.isCallOrAwaitExpression(initNode)) {
886
+ const unwrapped = this.unwrapAwaitExpression(initNode);
887
+ const callInfo = this.extractCallInfo(unwrapped);
888
+ if (!callInfo) {
889
+ // Unsupported call pattern (computed callee, etc.)
890
+ return;
891
+ }
892
+ const callRepresentation = `${callInfo.name}()`;
893
+ // Process each extracted variable
894
+ for (const varInfo of variables) {
895
+ const variableId = varInfo.id;
896
+ // Handle rest elements - create direct CALL_SITE assignment
897
+ if (varInfo.isRest) {
898
+ variableAssignments.push({
899
+ variableId,
900
+ sourceType: 'CALL_SITE',
901
+ callName: callInfo.name,
902
+ callLine: callInfo.line,
903
+ callColumn: callInfo.column,
904
+ callSourceLine: callInfo.line,
905
+ callSourceColumn: callInfo.column,
906
+ callSourceFile: module.file,
907
+ callSourceName: callInfo.name,
908
+ line: varInfo.loc.start.line
909
+ });
910
+ continue;
911
+ }
912
+ // ObjectPattern: const { data } = fetchUser() → data ASSIGNED_FROM fetchUser().data
913
+ if (t.isObjectPattern(pattern) && varInfo.propertyPath && varInfo.propertyPath.length > 0) {
914
+ const propertyPath = varInfo.propertyPath;
915
+ const expressionLine = varInfo.loc.start.line;
916
+ const expressionColumn = varInfo.loc.start.column;
917
+ // Build property path string: "fetchUser().data" or "fetchUser().user.name"
918
+ const fullPath = [callRepresentation, ...propertyPath].join('.');
919
+ const expressionId = ExpressionNode.generateId('MemberExpression', module.file, expressionLine, expressionColumn);
920
+ variableAssignments.push({
921
+ variableId,
922
+ sourceType: 'EXPRESSION',
923
+ sourceId: expressionId,
924
+ expressionType: 'MemberExpression',
925
+ object: callRepresentation, // "fetchUser()" - display name
926
+ property: propertyPath[propertyPath.length - 1],
927
+ computed: false,
928
+ path: fullPath, // "fetchUser().data"
929
+ propertyPath: propertyPath, // ["data"]
930
+ // Call source for DERIVES_FROM lookup (REG-223)
931
+ callSourceLine: callInfo.line,
932
+ callSourceColumn: callInfo.column,
933
+ callSourceFile: module.file,
934
+ callSourceName: callInfo.name,
935
+ sourceMetadata: {
936
+ sourceType: callInfo.isMethodCall ? 'method-call' : 'call'
937
+ },
938
+ file: module.file,
939
+ line: expressionLine,
940
+ column: expressionColumn
941
+ });
942
+ }
943
+ // ArrayPattern: const [first] = arr.map(fn) → first ASSIGNED_FROM arr.map(fn)[0]
944
+ else if (t.isArrayPattern(pattern) && varInfo.arrayIndex !== undefined) {
945
+ const arrayIndex = varInfo.arrayIndex;
946
+ const expressionLine = varInfo.loc.start.line;
947
+ const expressionColumn = varInfo.loc.start.column;
948
+ const hasPropertyPath = varInfo.propertyPath && varInfo.propertyPath.length > 0;
949
+ const expressionId = ExpressionNode.generateId('MemberExpression', module.file, expressionLine, expressionColumn);
950
+ variableAssignments.push({
951
+ variableId,
952
+ sourceType: 'EXPRESSION',
953
+ sourceId: expressionId,
954
+ expressionType: 'MemberExpression',
955
+ object: callRepresentation,
956
+ property: String(arrayIndex),
957
+ computed: true,
958
+ arrayIndex: arrayIndex,
959
+ propertyPath: hasPropertyPath ? varInfo.propertyPath : undefined,
960
+ // Call source for DERIVES_FROM lookup (REG-223)
961
+ callSourceLine: callInfo.line,
962
+ callSourceColumn: callInfo.column,
963
+ callSourceFile: module.file,
964
+ callSourceName: callInfo.name,
965
+ sourceMetadata: {
966
+ sourceType: callInfo.isMethodCall ? 'method-call' : 'call'
967
+ },
968
+ file: module.file,
969
+ line: expressionLine,
970
+ column: expressionColumn
971
+ });
972
+ }
973
+ }
974
+ }
975
+ // Unsupported init type (MemberExpression without call, etc.)
976
+ // else: do nothing - skip silently
977
+ }
595
978
  /**
596
979
  * Получить все MODULE ноды из графа
597
980
  */
@@ -624,11 +1007,17 @@ export class JSASTAnalyzer extends Plugin {
624
1007
  const functions = [];
625
1008
  const parameters = [];
626
1009
  const scopes = [];
1010
+ // Branching (switch statements)
1011
+ const branches = [];
1012
+ const cases = [];
1013
+ // Control flow (loops)
1014
+ const loops = [];
627
1015
  const variableDeclarations = [];
628
1016
  const callSites = [];
629
1017
  const methodCalls = [];
630
1018
  const eventListeners = [];
631
1019
  const classInstantiations = [];
1020
+ const constructorCalls = [];
632
1021
  const classDeclarations = [];
633
1022
  const methodCallbacks = [];
634
1023
  const callArguments = [];
@@ -651,6 +1040,16 @@ export class JSASTAnalyzer extends Plugin {
651
1040
  const arrayMutations = [];
652
1041
  // Object mutation tracking for FLOWS_INTO edges
653
1042
  const objectMutations = [];
1043
+ // Variable reassignment tracking for FLOWS_INTO edges (REG-290)
1044
+ const variableReassignments = [];
1045
+ // Return statement tracking for RETURNS edges
1046
+ const returnStatements = [];
1047
+ // Update expression tracking for MODIFIES edges (REG-288, REG-312)
1048
+ const updateExpressions = [];
1049
+ // Promise resolution tracking for RESOLVES_TO edges (REG-334)
1050
+ const promiseResolutions = [];
1051
+ // Promise executor contexts (REG-334) - keyed by executor function's start:end position
1052
+ const promiseExecutorContexts = new Map();
654
1053
  const ifScopeCounterRef = { value: 0 };
655
1054
  const scopeCounterRef = { value: 0 };
656
1055
  const varDeclCounterRef = { value: 0 };
@@ -661,6 +1060,8 @@ export class JSASTAnalyzer extends Plugin {
661
1060
  const anonymousFunctionCounterRef = { value: 0 };
662
1061
  const objectLiteralCounterRef = { value: 0 };
663
1062
  const arrayLiteralCounterRef = { value: 0 };
1063
+ const branchCounterRef = { value: 0 };
1064
+ const caseCounterRef = { value: 0 };
664
1065
  const processedNodes = {
665
1066
  functions: new Set(),
666
1067
  classes: new Set(),
@@ -680,13 +1081,18 @@ export class JSASTAnalyzer extends Plugin {
680
1081
  this.profiler.end('traverse_imports');
681
1082
  // Variables
682
1083
  this.profiler.start('traverse_variables');
683
- const variableVisitor = new VariableVisitor(module, { variableDeclarations, classInstantiations, literals, variableAssignments, varDeclCounterRef, literalCounterRef }, this.extractVariableNamesFromPattern.bind(this), this.trackVariableAssignment.bind(this), scopeTracker // Pass ScopeTracker for semantic ID generation
1084
+ const variableVisitor = new VariableVisitor(module, { variableDeclarations, classInstantiations, literals, variableAssignments, varDeclCounterRef, literalCounterRef, scopes, scopeCounterRef, objectLiterals, objectProperties, objectLiteralCounterRef }, this.extractVariableNamesFromPattern.bind(this), this.trackVariableAssignment.bind(this), scopeTracker // Pass ScopeTracker for semantic ID generation
684
1085
  );
685
1086
  traverse(ast, variableVisitor.getHandlers());
686
1087
  this.profiler.end('traverse_variables');
687
1088
  const allCollections = {
688
- functions, parameters, scopes, variableDeclarations, callSites, methodCalls,
689
- eventListeners, methodCallbacks, callArguments, classInstantiations, classDeclarations,
1089
+ functions, parameters, scopes,
1090
+ // Branching (switch statements)
1091
+ branches, cases,
1092
+ // Control flow (loops)
1093
+ loops,
1094
+ variableDeclarations, callSites, methodCalls,
1095
+ eventListeners, methodCallbacks, callArguments, classInstantiations, constructorCalls, classDeclarations,
690
1096
  httpRequests, literals, variableAssignments,
691
1097
  // TypeScript-specific collections
692
1098
  interfaces, typeAliases, enums, decorators,
@@ -696,10 +1102,21 @@ export class JSASTAnalyzer extends Plugin {
696
1102
  arrayMutations,
697
1103
  // Object mutation tracking
698
1104
  objectMutations,
1105
+ // Variable reassignment tracking (REG-290)
1106
+ variableReassignments,
1107
+ // Return statement tracking
1108
+ returnStatements,
1109
+ // Update expression tracking (REG-288, REG-312)
1110
+ updateExpressions,
1111
+ // Promise resolution tracking (REG-334)
1112
+ promiseResolutions,
1113
+ promiseExecutorContexts,
699
1114
  objectLiteralCounterRef, arrayLiteralCounterRef,
700
1115
  ifScopeCounterRef, scopeCounterRef, varDeclCounterRef,
701
1116
  callSiteCounterRef, functionCounterRef, httpRequestCounterRef,
702
- literalCounterRef, anonymousFunctionCounterRef, processedNodes,
1117
+ literalCounterRef, anonymousFunctionCounterRef,
1118
+ branchCounterRef, caseCounterRef,
1119
+ processedNodes,
703
1120
  imports, exports, code,
704
1121
  // VisitorCollections compatibility
705
1122
  classes: classDeclarations,
@@ -769,13 +1186,38 @@ export class JSASTAnalyzer extends Plugin {
769
1186
  this.analyzeFunctionBody(funcPath, funcBodyScopeId, module, allCollections);
770
1187
  scopeTracker.exitScope();
771
1188
  }
1189
+ // === VARIABLE REASSIGNMENT (REG-290) ===
1190
+ // Check if LHS is simple identifier (not obj.prop, not arr[i])
1191
+ // Must be checked at module level too
1192
+ if (assignNode.left.type === 'Identifier') {
1193
+ // Initialize collection if not exists
1194
+ if (!allCollections.variableReassignments) {
1195
+ allCollections.variableReassignments = [];
1196
+ }
1197
+ const variableReassignments = allCollections.variableReassignments;
1198
+ this.detectVariableReassignment(assignNode, module, variableReassignments, scopeTracker);
1199
+ }
1200
+ // === END VARIABLE REASSIGNMENT ===
772
1201
  // Check for indexed array assignment at module level: arr[i] = value
773
- this.detectIndexedArrayAssignment(assignNode, module, arrayMutations);
1202
+ this.detectIndexedArrayAssignment(assignNode, module, arrayMutations, scopeTracker);
774
1203
  // Check for object property assignment at module level: obj.prop = value
775
1204
  this.detectObjectPropertyAssignment(assignNode, module, objectMutations, scopeTracker);
776
1205
  }
777
1206
  });
778
1207
  this.profiler.end('traverse_assignments');
1208
+ // Module-level UpdateExpression (obj.count++, arr[i]++, i++) - REG-288/REG-312
1209
+ this.profiler.start('traverse_updates');
1210
+ traverse(ast, {
1211
+ UpdateExpression: (updatePath) => {
1212
+ // Skip if inside a function - analyzeFunctionBody handles those
1213
+ const functionParent = updatePath.getFunctionParent();
1214
+ if (functionParent)
1215
+ return;
1216
+ // Module-level update expression: no parentScopeId
1217
+ this.collectUpdateExpression(updatePath.node, module, updateExpressions, undefined, scopeTracker);
1218
+ }
1219
+ });
1220
+ this.profiler.end('traverse_updates');
779
1221
  // Classes
780
1222
  this.profiler.start('traverse_classes');
781
1223
  const classVisitor = new ClassVisitor(module, allCollections, this.analyzeFunctionBody.bind(this), scopeTracker // Pass ScopeTracker for semantic ID generation
@@ -837,6 +1279,72 @@ export class JSASTAnalyzer extends Plugin {
837
1279
  const callExpressionVisitor = new CallExpressionVisitor(module, allCollections, scopeTracker);
838
1280
  traverse(ast, callExpressionVisitor.getHandlers());
839
1281
  this.profiler.end('traverse_calls');
1282
+ // Module-level NewExpression (constructor calls)
1283
+ // This handles top-level code like `const x = new Date()` that's not inside a function
1284
+ this.profiler.start('traverse_new');
1285
+ const processedConstructorCalls = new Set();
1286
+ traverse(ast, {
1287
+ NewExpression: (newPath) => {
1288
+ const newNode = newPath.node;
1289
+ const nodeKey = `constructor:new:${newNode.start}:${newNode.end}`;
1290
+ if (processedConstructorCalls.has(nodeKey)) {
1291
+ return;
1292
+ }
1293
+ processedConstructorCalls.add(nodeKey);
1294
+ // Determine className from callee
1295
+ let className = null;
1296
+ if (newNode.callee.type === 'Identifier') {
1297
+ className = newNode.callee.name;
1298
+ }
1299
+ else if (newNode.callee.type === 'MemberExpression' && newNode.callee.property.type === 'Identifier') {
1300
+ className = newNode.callee.property.name;
1301
+ }
1302
+ if (className) {
1303
+ const line = getLine(newNode);
1304
+ const column = getColumn(newNode);
1305
+ const constructorCallId = ConstructorCallNode.generateId(className, module.file, line, column);
1306
+ const isBuiltin = ConstructorCallNode.isBuiltinConstructor(className);
1307
+ constructorCalls.push({
1308
+ id: constructorCallId,
1309
+ type: 'CONSTRUCTOR_CALL',
1310
+ className,
1311
+ isBuiltin,
1312
+ file: module.file,
1313
+ line,
1314
+ column
1315
+ });
1316
+ // REG-334: If this is Promise constructor with executor callback,
1317
+ // register the context for resolve/reject detection
1318
+ if (className === 'Promise' && newNode.arguments.length > 0) {
1319
+ const executorArg = newNode.arguments[0];
1320
+ // Only handle inline function expressions (not variable references)
1321
+ if (t.isArrowFunctionExpression(executorArg) || t.isFunctionExpression(executorArg)) {
1322
+ // Extract resolve/reject parameter names
1323
+ let resolveName;
1324
+ let rejectName;
1325
+ if (executorArg.params.length > 0 && t.isIdentifier(executorArg.params[0])) {
1326
+ resolveName = executorArg.params[0].name;
1327
+ }
1328
+ if (executorArg.params.length > 1 && t.isIdentifier(executorArg.params[1])) {
1329
+ rejectName = executorArg.params[1].name;
1330
+ }
1331
+ if (resolveName) {
1332
+ // Key by function node position to allow nested Promise detection
1333
+ const funcKey = `${executorArg.start}:${executorArg.end}`;
1334
+ promiseExecutorContexts.set(funcKey, {
1335
+ constructorCallId,
1336
+ resolveName,
1337
+ rejectName,
1338
+ file: module.file,
1339
+ line
1340
+ });
1341
+ }
1342
+ }
1343
+ }
1344
+ }
1345
+ }
1346
+ });
1347
+ this.profiler.end('traverse_new');
840
1348
  // Module-level IfStatements
841
1349
  this.profiler.start('traverse_ifs');
842
1350
  traverse(ast, {
@@ -889,14 +1397,25 @@ export class JSASTAnalyzer extends Plugin {
889
1397
  const result = await this.graphBuilder.build(module, graph, projectPath, {
890
1398
  functions,
891
1399
  scopes,
1400
+ // Branching (switch statements) - use allCollections refs as they're populated by analyzeFunctionBody
1401
+ branches: allCollections.branches || branches,
1402
+ cases: allCollections.cases || cases,
1403
+ // Control flow (loops) - use allCollections refs as they're populated by analyzeFunctionBody
1404
+ loops: allCollections.loops || loops,
1405
+ // Control flow (try/catch/finally) - Phase 4
1406
+ tryBlocks: allCollections.tryBlocks,
1407
+ catchBlocks: allCollections.catchBlocks,
1408
+ finallyBlocks: allCollections.finallyBlocks,
892
1409
  variableDeclarations,
893
1410
  callSites,
894
1411
  methodCalls,
895
1412
  eventListeners,
896
1413
  classInstantiations,
1414
+ constructorCalls,
897
1415
  classDeclarations,
898
1416
  methodCallbacks,
899
- callArguments,
1417
+ // REG-334: Use allCollections.callArguments to include function-level resolve/reject arguments
1418
+ callArguments: allCollections.callArguments || callArguments,
900
1419
  imports,
901
1420
  exports,
902
1421
  httpRequests,
@@ -912,8 +1431,17 @@ export class JSASTAnalyzer extends Plugin {
912
1431
  arrayMutations,
913
1432
  // Object mutation tracking
914
1433
  objectMutations,
1434
+ // Variable reassignment tracking (REG-290)
1435
+ variableReassignments,
1436
+ // Return statement tracking
1437
+ returnStatements,
1438
+ // Update expression tracking (REG-288, REG-312)
1439
+ updateExpressions,
1440
+ // Promise resolution tracking (REG-334)
1441
+ promiseResolutions: allCollections.promiseResolutions || promiseResolutions,
915
1442
  // Object/Array literal tracking - use allCollections refs as visitors may have created new arrays
916
1443
  objectLiterals: allCollections.objectLiterals || objectLiterals,
1444
+ objectProperties: allCollections.objectProperties || objectProperties,
917
1445
  arrayLiterals: allCollections.arrayLiterals || arrayLiterals
918
1446
  });
919
1447
  this.profiler.end('graph_build');
@@ -980,23 +1508,34 @@ export class JSASTAnalyzer extends Plugin {
980
1508
  * @param literalCounterRef - Counter for unique literal IDs
981
1509
  * @param scopeTracker - Tracker for semantic ID generation
982
1510
  * @param parentScopeVariables - Set to track variables for closure analysis
1511
+ * @param objectLiterals - Collection for object literal nodes (REG-328)
1512
+ * @param objectProperties - Collection for object property edges (REG-328)
1513
+ * @param objectLiteralCounterRef - Counter for unique object literal IDs (REG-328)
983
1514
  */
984
- handleVariableDeclaration(varPath, parentScopeId, module, variableDeclarations, classInstantiations, literals, variableAssignments, varDeclCounterRef, literalCounterRef, scopeTracker, parentScopeVariables) {
1515
+ handleVariableDeclaration(varPath, parentScopeId, module, variableDeclarations, classInstantiations, literals, variableAssignments, varDeclCounterRef, literalCounterRef, scopeTracker, parentScopeVariables, objectLiterals, objectProperties, objectLiteralCounterRef) {
985
1516
  const varNode = varPath.node;
986
1517
  const isConst = varNode.kind === 'const';
1518
+ // Check if this is a loop variable (for...of or for...in)
1519
+ const parent = varPath.parent;
1520
+ const isLoopVariable = (t.isForOfStatement(parent) || t.isForInStatement(parent)) && parent.left === varNode;
987
1521
  varNode.declarations.forEach(declarator => {
988
1522
  const variables = this.extractVariableNamesFromPattern(declarator.id);
1523
+ const variablesWithIds = [];
989
1524
  variables.forEach(varInfo => {
990
1525
  const literalValue = declarator.init ? ExpressionEvaluator.extractLiteralValue(declarator.init) : null;
991
1526
  const isLiteral = literalValue !== null;
992
1527
  const isNewExpression = declarator.init && declarator.init.type === 'NewExpression';
993
- const shouldBeConstant = isConst && (isLiteral || isNewExpression);
1528
+ // Loop variables with const should be CONSTANT (they can't be reassigned in loop body)
1529
+ // Regular variables with const are CONSTANT only if initialized with literal or new expression
1530
+ const shouldBeConstant = isConst && (isLoopVariable || isLiteral || isNewExpression);
994
1531
  const nodeType = shouldBeConstant ? 'CONSTANT' : 'VARIABLE';
995
1532
  // Generate semantic ID (primary) or legacy ID (fallback)
996
1533
  const legacyId = `${nodeType}#${varInfo.name}#${module.file}#${varInfo.loc.start.line}:${varInfo.loc.start.column}:${varDeclCounterRef.value++}`;
997
1534
  const varId = scopeTracker
998
1535
  ? computeSemanticId(nodeType, varInfo.name, scopeTracker.getContext())
999
1536
  : legacyId;
1537
+ // Collect variable info with ID for destructuring tracking
1538
+ variablesWithIds.push({ ...varInfo, id: varId });
1000
1539
  parentScopeVariables.add({
1001
1540
  name: varInfo.name,
1002
1541
  id: varId,
@@ -1037,16 +1576,199 @@ export class JSASTAnalyzer extends Plugin {
1037
1576
  parentScopeId
1038
1577
  });
1039
1578
  }
1040
- if (declarator.init) {
1041
- this.trackVariableAssignment(declarator.init, varId, varInfo.name, module, varInfo.loc.start.line, literals, variableAssignments, literalCounterRef);
1042
- }
1043
1579
  });
1580
+ // Track assignments after all variables are created
1581
+ if (isLoopVariable) {
1582
+ // For loop variables, track assignment from the source collection (right side of for...of/for...in)
1583
+ const loopParent = parent;
1584
+ const sourceExpression = loopParent.right;
1585
+ if (t.isObjectPattern(declarator.id) || t.isArrayPattern(declarator.id)) {
1586
+ // Destructuring in loop: track each variable separately
1587
+ this.trackDestructuringAssignment(declarator.id, sourceExpression, variablesWithIds, module, variableAssignments);
1588
+ }
1589
+ else {
1590
+ // Simple loop variable: create DERIVES_FROM edges (not ASSIGNED_FROM)
1591
+ // Loop variables derive their values from the collection (semantic difference)
1592
+ variablesWithIds.forEach(varInfo => {
1593
+ if (t.isIdentifier(sourceExpression)) {
1594
+ variableAssignments.push({
1595
+ variableId: varInfo.id,
1596
+ sourceType: 'DERIVES_FROM_VARIABLE',
1597
+ sourceName: sourceExpression.name,
1598
+ file: module.file,
1599
+ line: varInfo.loc.start.line
1600
+ });
1601
+ }
1602
+ else {
1603
+ // Fallback to regular tracking for non-identifier expressions
1604
+ this.trackVariableAssignment(sourceExpression, varInfo.id, varInfo.name, module, varInfo.loc.start.line, literals, variableAssignments, literalCounterRef, objectLiterals, objectProperties, objectLiteralCounterRef);
1605
+ }
1606
+ });
1607
+ }
1608
+ }
1609
+ else if (declarator.init) {
1610
+ // Regular variable declaration with initializer
1611
+ if (t.isObjectPattern(declarator.id) || t.isArrayPattern(declarator.id)) {
1612
+ // Destructuring: use specialized tracking
1613
+ this.trackDestructuringAssignment(declarator.id, declarator.init, variablesWithIds, module, variableAssignments);
1614
+ }
1615
+ else {
1616
+ // Simple assignment: use existing tracking
1617
+ const varInfo = variablesWithIds[0];
1618
+ this.trackVariableAssignment(declarator.init, varInfo.id, varInfo.name, module, varInfo.loc.start.line, literals, variableAssignments, literalCounterRef, objectLiterals, objectProperties, objectLiteralCounterRef);
1619
+ }
1620
+ }
1044
1621
  });
1045
1622
  }
1046
- createLoopScopeHandler(trackerScopeType, scopeType, parentScopeId, module, scopes, scopeCounterRef, scopeTracker) {
1623
+ createLoopScopeHandler(trackerScopeType, scopeType, loopType, parentScopeId, module, scopes, loops, scopeCounterRef, loopCounterRef, scopeTracker, scopeIdStack, controlFlowState) {
1047
1624
  return {
1048
1625
  enter: (path) => {
1049
1626
  const node = path.node;
1627
+ // Phase 6 (REG-267): Increment loop count for cyclomatic complexity
1628
+ if (controlFlowState) {
1629
+ controlFlowState.loopCount++;
1630
+ }
1631
+ // 1. Create LOOP node
1632
+ const loopCounter = loopCounterRef.value++;
1633
+ const legacyLoopId = `${module.file}:LOOP:${loopType}:${getLine(node)}:${loopCounter}`;
1634
+ const loopId = scopeTracker
1635
+ ? computeSemanticId('LOOP', loopType, scopeTracker.getContext(), { discriminator: loopCounter })
1636
+ : legacyLoopId;
1637
+ // 2. Extract iteration target for for-in/for-of
1638
+ let iteratesOverName;
1639
+ let iteratesOverLine;
1640
+ let iteratesOverColumn;
1641
+ if (loopType === 'for-in' || loopType === 'for-of') {
1642
+ const loopNode = node;
1643
+ if (t.isIdentifier(loopNode.right)) {
1644
+ iteratesOverName = loopNode.right.name;
1645
+ iteratesOverLine = getLine(loopNode.right);
1646
+ iteratesOverColumn = getColumn(loopNode.right);
1647
+ }
1648
+ else if (t.isMemberExpression(loopNode.right)) {
1649
+ iteratesOverName = this.memberExpressionToString(loopNode.right);
1650
+ iteratesOverLine = getLine(loopNode.right);
1651
+ iteratesOverColumn = getColumn(loopNode.right);
1652
+ }
1653
+ }
1654
+ // 2b. Extract init/test/update for classic for loops and test for while/do-while (REG-282)
1655
+ let initVariableName;
1656
+ let initLine;
1657
+ let testExpressionId;
1658
+ let testExpressionType;
1659
+ let testLine;
1660
+ let testColumn;
1661
+ let updateExpressionId;
1662
+ let updateExpressionType;
1663
+ let updateLine;
1664
+ let updateColumn;
1665
+ if (loopType === 'for') {
1666
+ const forNode = node;
1667
+ // Extract init: let i = 0
1668
+ if (forNode.init) {
1669
+ initLine = getLine(forNode.init);
1670
+ if (t.isVariableDeclaration(forNode.init)) {
1671
+ // Get name of first declared variable
1672
+ const firstDeclarator = forNode.init.declarations[0];
1673
+ if (t.isIdentifier(firstDeclarator.id)) {
1674
+ initVariableName = firstDeclarator.id.name;
1675
+ }
1676
+ }
1677
+ }
1678
+ // Extract test: i < 10
1679
+ if (forNode.test) {
1680
+ testLine = getLine(forNode.test);
1681
+ testColumn = getColumn(forNode.test);
1682
+ testExpressionType = forNode.test.type;
1683
+ testExpressionId = ExpressionNode.generateId(forNode.test.type, module.file, testLine, testColumn);
1684
+ }
1685
+ // Extract update: i++
1686
+ if (forNode.update) {
1687
+ updateLine = getLine(forNode.update);
1688
+ updateColumn = getColumn(forNode.update);
1689
+ updateExpressionType = forNode.update.type;
1690
+ updateExpressionId = ExpressionNode.generateId(forNode.update.type, module.file, updateLine, updateColumn);
1691
+ }
1692
+ }
1693
+ // Extract test condition for while and do-while loops
1694
+ if (loopType === 'while' || loopType === 'do-while') {
1695
+ const condLoop = node;
1696
+ if (condLoop.test) {
1697
+ testLine = getLine(condLoop.test);
1698
+ testColumn = getColumn(condLoop.test);
1699
+ testExpressionType = condLoop.test.type;
1700
+ testExpressionId = ExpressionNode.generateId(condLoop.test.type, module.file, testLine, testColumn);
1701
+ }
1702
+ }
1703
+ // Extract async flag for for-await-of (REG-284)
1704
+ let isAsync;
1705
+ if (loopType === 'for-of') {
1706
+ const forOfNode = node;
1707
+ isAsync = forOfNode.await === true ? true : undefined;
1708
+ }
1709
+ // 3. Determine actual parent - use stack for nested loops, otherwise original parentScopeId
1710
+ const actualParentScopeId = (scopeIdStack && scopeIdStack.length > 0)
1711
+ ? scopeIdStack[scopeIdStack.length - 1]
1712
+ : parentScopeId;
1713
+ // 3.5. Extract condition expression for while/do-while/for loops (REG-280)
1714
+ // Note: for-in and for-of don't have test expressions (they use ITERATES_OVER instead)
1715
+ let conditionExpressionId;
1716
+ let conditionExpressionType;
1717
+ let conditionLine;
1718
+ let conditionColumn;
1719
+ if (loopType === 'while' || loopType === 'do-while') {
1720
+ const testNode = node.test;
1721
+ if (testNode) {
1722
+ const condResult = this.extractDiscriminantExpression(testNode, module);
1723
+ conditionExpressionId = condResult.id;
1724
+ conditionExpressionType = condResult.expressionType;
1725
+ conditionLine = condResult.line;
1726
+ conditionColumn = condResult.column;
1727
+ }
1728
+ }
1729
+ else if (loopType === 'for') {
1730
+ const forNode = node;
1731
+ // for loop test may be null (infinite loop: for(;;))
1732
+ if (forNode.test) {
1733
+ const condResult = this.extractDiscriminantExpression(forNode.test, module);
1734
+ conditionExpressionId = condResult.id;
1735
+ conditionExpressionType = condResult.expressionType;
1736
+ conditionLine = condResult.line;
1737
+ conditionColumn = condResult.column;
1738
+ }
1739
+ }
1740
+ // 4. Push LOOP info
1741
+ loops.push({
1742
+ id: loopId,
1743
+ semanticId: loopId,
1744
+ type: 'LOOP',
1745
+ loopType,
1746
+ file: module.file,
1747
+ line: getLine(node),
1748
+ column: getColumn(node),
1749
+ parentScopeId: actualParentScopeId,
1750
+ iteratesOverName,
1751
+ iteratesOverLine,
1752
+ iteratesOverColumn,
1753
+ conditionExpressionId,
1754
+ conditionExpressionType,
1755
+ conditionLine,
1756
+ conditionColumn,
1757
+ // REG-282: init/test/update for classic for loops
1758
+ initVariableName,
1759
+ initLine,
1760
+ testExpressionId,
1761
+ testExpressionType,
1762
+ testLine,
1763
+ testColumn,
1764
+ updateExpressionId,
1765
+ updateExpressionType,
1766
+ updateLine,
1767
+ updateColumn,
1768
+ // REG-284: async flag for for-await-of
1769
+ async: isAsync
1770
+ });
1771
+ // 5. Create body SCOPE (backward compatibility)
1050
1772
  const scopeId = `SCOPE#${scopeType}#${module.file}#${getLine(node)}:${scopeCounterRef.value++}`;
1051
1773
  const semanticId = this.generateSemanticId(scopeType, scopeTracker);
1052
1774
  scopes.push({
@@ -1056,14 +1778,23 @@ export class JSASTAnalyzer extends Plugin {
1056
1778
  semanticId,
1057
1779
  file: module.file,
1058
1780
  line: getLine(node),
1059
- parentScopeId
1781
+ parentScopeId: loopId // Parent is LOOP, not original parentScopeId
1060
1782
  });
1783
+ // 6. Push body SCOPE to scopeIdStack (for CONTAINS edges to nested items)
1784
+ // The body scope is the container for nested loops, not the LOOP itself
1785
+ if (scopeIdStack) {
1786
+ scopeIdStack.push(scopeId);
1787
+ }
1061
1788
  // Enter scope for semantic ID generation
1062
1789
  if (scopeTracker) {
1063
1790
  scopeTracker.enterCountedScope(trackerScopeType);
1064
1791
  }
1065
1792
  },
1066
1793
  exit: () => {
1794
+ // Pop loop scope from stack
1795
+ if (scopeIdStack) {
1796
+ scopeIdStack.pop();
1797
+ }
1067
1798
  // Exit scope
1068
1799
  if (scopeTracker) {
1069
1800
  scopeTracker.exitScope();
@@ -1072,174 +1803,596 @@ export class JSASTAnalyzer extends Plugin {
1072
1803
  };
1073
1804
  }
1074
1805
  /**
1075
- * Process VariableDeclarations within a try/catch/finally block.
1076
- * This is a simplified version that doesn't track parentScopeVariables or class instantiations.
1806
+ * Factory method to create TryStatement handler.
1807
+ * Creates TRY_BLOCK, CATCH_BLOCK, FINALLY_BLOCK nodes and body SCOPEs.
1808
+ * Does NOT use skip() - allows normal traversal for CallExpression/NewExpression visitors.
1809
+ *
1810
+ * Phase 4 (REG-267): Creates control flow nodes with HAS_CATCH and HAS_FINALLY edges.
1811
+ *
1812
+ * @param parentScopeId - Parent scope ID for the scope nodes
1813
+ * @param module - Module context
1814
+ * @param scopes - Collection to push scope nodes to
1815
+ * @param tryBlocks - Collection to push TRY_BLOCK nodes to
1816
+ * @param catchBlocks - Collection to push CATCH_BLOCK nodes to
1817
+ * @param finallyBlocks - Collection to push FINALLY_BLOCK nodes to
1818
+ * @param scopeCounterRef - Counter for unique scope IDs
1819
+ * @param tryBlockCounterRef - Counter for unique TRY_BLOCK IDs
1820
+ * @param catchBlockCounterRef - Counter for unique CATCH_BLOCK IDs
1821
+ * @param finallyBlockCounterRef - Counter for unique FINALLY_BLOCK IDs
1822
+ * @param scopeTracker - Tracker for semantic ID generation
1823
+ * @param tryScopeMap - Map to track try/catch/finally scope transitions
1824
+ * @param scopeIdStack - Stack for tracking current scope ID for CONTAINS edges
1825
+ */
1826
+ createTryStatementHandler(parentScopeId, module, scopes, tryBlocks, catchBlocks, finallyBlocks, scopeCounterRef, tryBlockCounterRef, catchBlockCounterRef, finallyBlockCounterRef, scopeTracker, tryScopeMap, scopeIdStack, controlFlowState) {
1827
+ return {
1828
+ enter: (tryPath) => {
1829
+ const tryNode = tryPath.node;
1830
+ // Phase 6 (REG-267): Mark that this function has try/catch
1831
+ if (controlFlowState) {
1832
+ controlFlowState.hasTryCatch = true;
1833
+ }
1834
+ // Determine actual parent - use stack for nested structures, otherwise original parentScopeId
1835
+ const actualParentScopeId = (scopeIdStack && scopeIdStack.length > 0)
1836
+ ? scopeIdStack[scopeIdStack.length - 1]
1837
+ : parentScopeId;
1838
+ // 1. Create TRY_BLOCK node
1839
+ const tryBlockCounter = tryBlockCounterRef.value++;
1840
+ const legacyTryBlockId = `${module.file}:TRY_BLOCK:${getLine(tryNode)}:${tryBlockCounter}`;
1841
+ const tryBlockId = scopeTracker
1842
+ ? computeSemanticId('TRY_BLOCK', 'try', scopeTracker.getContext(), { discriminator: tryBlockCounter })
1843
+ : legacyTryBlockId;
1844
+ tryBlocks.push({
1845
+ id: tryBlockId,
1846
+ semanticId: tryBlockId,
1847
+ type: 'TRY_BLOCK',
1848
+ file: module.file,
1849
+ line: getLine(tryNode),
1850
+ column: getColumn(tryNode),
1851
+ parentScopeId: actualParentScopeId
1852
+ });
1853
+ // 2. Create try-body SCOPE (backward compatibility)
1854
+ // Parent is now TRY_BLOCK, not original parentScopeId
1855
+ const tryScopeId = `SCOPE#try-block#${module.file}#${getLine(tryNode)}:${scopeCounterRef.value++}`;
1856
+ const trySemanticId = this.generateSemanticId('try-block', scopeTracker);
1857
+ scopes.push({
1858
+ id: tryScopeId,
1859
+ type: 'SCOPE',
1860
+ scopeType: 'try-block',
1861
+ semanticId: trySemanticId,
1862
+ file: module.file,
1863
+ line: getLine(tryNode),
1864
+ parentScopeId: tryBlockId // Parent is TRY_BLOCK
1865
+ });
1866
+ // 3. Create CATCH_BLOCK and catch-body SCOPE if handler exists
1867
+ let catchBlockId = null;
1868
+ let catchScopeId = null;
1869
+ if (tryNode.handler) {
1870
+ const catchClause = tryNode.handler;
1871
+ const catchBlockCounter = catchBlockCounterRef.value++;
1872
+ const legacyCatchBlockId = `${module.file}:CATCH_BLOCK:${getLine(catchClause)}:${catchBlockCounter}`;
1873
+ catchBlockId = scopeTracker
1874
+ ? computeSemanticId('CATCH_BLOCK', 'catch', scopeTracker.getContext(), { discriminator: catchBlockCounter })
1875
+ : legacyCatchBlockId;
1876
+ // Extract parameter name if present
1877
+ let parameterName;
1878
+ if (catchClause.param && t.isIdentifier(catchClause.param)) {
1879
+ parameterName = catchClause.param.name;
1880
+ }
1881
+ catchBlocks.push({
1882
+ id: catchBlockId,
1883
+ semanticId: catchBlockId,
1884
+ type: 'CATCH_BLOCK',
1885
+ file: module.file,
1886
+ line: getLine(catchClause),
1887
+ column: getColumn(catchClause),
1888
+ parentScopeId,
1889
+ parentTryBlockId: tryBlockId,
1890
+ parameterName
1891
+ });
1892
+ // Create catch-body SCOPE (backward compatibility)
1893
+ catchScopeId = `SCOPE#catch-block#${module.file}#${getLine(catchClause)}:${scopeCounterRef.value++}`;
1894
+ const catchSemanticId = this.generateSemanticId('catch-block', scopeTracker);
1895
+ scopes.push({
1896
+ id: catchScopeId,
1897
+ type: 'SCOPE',
1898
+ scopeType: 'catch-block',
1899
+ semanticId: catchSemanticId,
1900
+ file: module.file,
1901
+ line: getLine(catchClause),
1902
+ parentScopeId: catchBlockId // Parent is CATCH_BLOCK
1903
+ });
1904
+ }
1905
+ // 4. Create FINALLY_BLOCK and finally-body SCOPE if finalizer exists
1906
+ let finallyBlockId = null;
1907
+ let finallyScopeId = null;
1908
+ if (tryNode.finalizer) {
1909
+ const finallyBlockCounter = finallyBlockCounterRef.value++;
1910
+ const legacyFinallyBlockId = `${module.file}:FINALLY_BLOCK:${getLine(tryNode.finalizer)}:${finallyBlockCounter}`;
1911
+ finallyBlockId = scopeTracker
1912
+ ? computeSemanticId('FINALLY_BLOCK', 'finally', scopeTracker.getContext(), { discriminator: finallyBlockCounter })
1913
+ : legacyFinallyBlockId;
1914
+ finallyBlocks.push({
1915
+ id: finallyBlockId,
1916
+ semanticId: finallyBlockId,
1917
+ type: 'FINALLY_BLOCK',
1918
+ file: module.file,
1919
+ line: getLine(tryNode.finalizer),
1920
+ column: getColumn(tryNode.finalizer),
1921
+ parentScopeId,
1922
+ parentTryBlockId: tryBlockId
1923
+ });
1924
+ // Create finally-body SCOPE (backward compatibility)
1925
+ finallyScopeId = `SCOPE#finally-block#${module.file}#${getLine(tryNode.finalizer)}:${scopeCounterRef.value++}`;
1926
+ const finallySemanticId = this.generateSemanticId('finally-block', scopeTracker);
1927
+ scopes.push({
1928
+ id: finallyScopeId,
1929
+ type: 'SCOPE',
1930
+ scopeType: 'finally-block',
1931
+ semanticId: finallySemanticId,
1932
+ file: module.file,
1933
+ line: getLine(tryNode.finalizer),
1934
+ parentScopeId: finallyBlockId // Parent is FINALLY_BLOCK
1935
+ });
1936
+ }
1937
+ // 5. Push try scope onto stack for CONTAINS edges
1938
+ if (scopeIdStack) {
1939
+ scopeIdStack.push(tryScopeId);
1940
+ }
1941
+ // Enter try scope for semantic ID generation
1942
+ if (scopeTracker) {
1943
+ scopeTracker.enterCountedScope('try');
1944
+ }
1945
+ // 6. Store scope info for catch/finally transitions
1946
+ tryScopeMap.set(tryNode, {
1947
+ tryScopeId,
1948
+ catchScopeId,
1949
+ finallyScopeId,
1950
+ currentBlock: 'try',
1951
+ tryBlockId,
1952
+ catchBlockId,
1953
+ finallyBlockId
1954
+ });
1955
+ },
1956
+ exit: (tryPath) => {
1957
+ const tryNode = tryPath.node;
1958
+ const scopeInfo = tryScopeMap.get(tryNode);
1959
+ // Pop the current scope from stack (could be try, catch, or finally)
1960
+ if (scopeIdStack) {
1961
+ scopeIdStack.pop();
1962
+ }
1963
+ // Exit the current scope
1964
+ if (scopeTracker) {
1965
+ scopeTracker.exitScope();
1966
+ }
1967
+ // Clean up
1968
+ tryScopeMap.delete(tryNode);
1969
+ }
1970
+ };
1971
+ }
1972
+ /**
1973
+ * Factory method to create CatchClause handler.
1974
+ * Handles scope transition from try to catch and processes catch parameter.
1077
1975
  *
1078
- * @param blockPath - The NodePath for the block to process
1079
- * @param blockScopeId - The scope ID for variables in this block
1080
1976
  * @param module - Module context
1081
1977
  * @param variableDeclarations - Collection to push variable declarations to
1082
- * @param literals - Collection for literal tracking
1083
- * @param variableAssignments - Collection for variable assignment tracking
1084
1978
  * @param varDeclCounterRef - Counter for unique variable declaration IDs
1085
- * @param literalCounterRef - Counter for unique literal IDs
1086
1979
  * @param scopeTracker - Tracker for semantic ID generation
1980
+ * @param tryScopeMap - Map to track try/catch/finally scope transitions
1981
+ * @param scopeIdStack - Stack for tracking current scope ID for CONTAINS edges
1087
1982
  */
1088
- processBlockVariables(blockPath, blockScopeId, module, variableDeclarations, literals, variableAssignments, varDeclCounterRef, literalCounterRef, scopeTracker) {
1089
- blockPath.traverse({
1090
- VariableDeclaration: (varPath) => {
1091
- const varNode = varPath.node;
1092
- const isConst = varNode.kind === 'const';
1093
- varNode.declarations.forEach(declarator => {
1094
- const variables = this.extractVariableNamesFromPattern(declarator.id);
1095
- variables.forEach(varInfo => {
1096
- const literalValue = declarator.init ? ExpressionEvaluator.extractLiteralValue(declarator.init) : null;
1097
- const isLiteral = literalValue !== null;
1098
- const isNewExpression = declarator.init && declarator.init.type === 'NewExpression';
1099
- const shouldBeConstant = isConst && (isLiteral || isNewExpression);
1100
- const nodeType = shouldBeConstant ? 'CONSTANT' : 'VARIABLE';
1101
- const legacyId = `${nodeType}#${varInfo.name}#${module.file}#${varInfo.loc.start.line}:${varInfo.loc.start.column}:${varDeclCounterRef.value++}`;
1983
+ createCatchClauseHandler(module, variableDeclarations, varDeclCounterRef, scopeTracker, tryScopeMap, scopeIdStack) {
1984
+ return {
1985
+ enter: (catchPath) => {
1986
+ const catchNode = catchPath.node;
1987
+ const parent = catchPath.parent;
1988
+ if (!t.isTryStatement(parent))
1989
+ return;
1990
+ const scopeInfo = tryScopeMap.get(parent);
1991
+ if (!scopeInfo || !scopeInfo.catchScopeId)
1992
+ return;
1993
+ // Transition from try scope to catch scope
1994
+ if (scopeInfo.currentBlock === 'try') {
1995
+ // Pop try scope, push catch scope
1996
+ if (scopeIdStack) {
1997
+ scopeIdStack.pop();
1998
+ scopeIdStack.push(scopeInfo.catchScopeId);
1999
+ }
2000
+ // Exit try scope, enter catch scope for semantic ID
2001
+ if (scopeTracker) {
2002
+ scopeTracker.exitScope();
2003
+ scopeTracker.enterCountedScope('catch');
2004
+ }
2005
+ scopeInfo.currentBlock = 'catch';
2006
+ }
2007
+ // Handle catch parameter (e.g., catch (e) or catch ({ message }))
2008
+ if (catchNode.param) {
2009
+ const errorVarInfo = this.extractVariableNamesFromPattern(catchNode.param);
2010
+ errorVarInfo.forEach(varInfo => {
2011
+ const legacyId = `VARIABLE#${varInfo.name}#${module.file}#${varInfo.loc.start.line}:${varInfo.loc.start.column}:${varDeclCounterRef.value++}`;
1102
2012
  const varId = scopeTracker
1103
- ? computeSemanticId(nodeType, varInfo.name, scopeTracker.getContext())
2013
+ ? computeSemanticId('VARIABLE', varInfo.name, scopeTracker.getContext())
1104
2014
  : legacyId;
1105
2015
  variableDeclarations.push({
1106
2016
  id: varId,
1107
- type: nodeType,
2017
+ type: 'VARIABLE',
1108
2018
  name: varInfo.name,
1109
2019
  file: module.file,
1110
2020
  line: varInfo.loc.start.line,
1111
- parentScopeId: blockScopeId
2021
+ parentScopeId: scopeInfo.catchScopeId
1112
2022
  });
1113
- if (declarator.init) {
1114
- this.trackVariableAssignment(declarator.init, varId, varInfo.name, module, varInfo.loc.start.line, literals, variableAssignments, literalCounterRef);
1115
- }
1116
2023
  });
1117
- });
2024
+ }
1118
2025
  }
1119
- });
2026
+ };
1120
2027
  }
1121
2028
  /**
1122
- * Handles TryStatement nodes within function bodies.
1123
- * Creates try, catch (with optional error parameter), and finally scopes,
1124
- * and processes variable declarations within each block.
2029
+ * Handles SwitchStatement nodes.
2030
+ * Creates BRANCH node for switch, CASE nodes for each case clause,
2031
+ * and EXPRESSION node for discriminant.
1125
2032
  *
1126
- * @param tryPath - The NodePath for the TryStatement
1127
- * @param parentScopeId - Parent scope ID for the scope nodes
2033
+ * @param switchPath - The NodePath for the SwitchStatement
2034
+ * @param parentScopeId - Parent scope ID
1128
2035
  * @param module - Module context
1129
- * @param scopes - Collection to push scope nodes to
1130
- * @param variableDeclarations - Collection to push variable declarations to
1131
- * @param literals - Collection for literal tracking
1132
- * @param variableAssignments - Collection for variable assignment tracking
1133
- * @param scopeCounterRef - Counter for unique scope IDs
1134
- * @param varDeclCounterRef - Counter for unique variable declaration IDs
1135
- * @param literalCounterRef - Counter for unique literal IDs
2036
+ * @param collections - AST collections
1136
2037
  * @param scopeTracker - Tracker for semantic ID generation
1137
2038
  */
1138
- handleTryStatement(tryPath, parentScopeId, module, scopes, variableDeclarations, literals, variableAssignments, scopeCounterRef, varDeclCounterRef, literalCounterRef, scopeTracker) {
1139
- const tryNode = tryPath.node;
1140
- // Create and process try block
1141
- const tryScopeId = `SCOPE#try-block#${module.file}#${getLine(tryNode)}:${scopeCounterRef.value++}`;
1142
- const trySemanticId = this.generateSemanticId('try-block', scopeTracker);
1143
- scopes.push({
1144
- id: tryScopeId,
1145
- type: 'SCOPE',
1146
- scopeType: 'try-block',
1147
- semanticId: trySemanticId,
2039
+ handleSwitchStatement(switchPath, parentScopeId, module, collections, scopeTracker, controlFlowState) {
2040
+ const switchNode = switchPath.node;
2041
+ // Phase 6 (REG-267): Count branch and non-default cases for cyclomatic complexity
2042
+ if (controlFlowState) {
2043
+ controlFlowState.branchCount++; // switch itself is a branch
2044
+ // Count non-default cases
2045
+ for (const caseNode of switchNode.cases) {
2046
+ if (caseNode.test !== null) { // Not default case
2047
+ controlFlowState.caseCount++;
2048
+ }
2049
+ }
2050
+ }
2051
+ // Initialize collections if not exist
2052
+ if (!collections.branches) {
2053
+ collections.branches = [];
2054
+ }
2055
+ if (!collections.cases) {
2056
+ collections.cases = [];
2057
+ }
2058
+ if (!collections.branchCounterRef) {
2059
+ collections.branchCounterRef = { value: 0 };
2060
+ }
2061
+ if (!collections.caseCounterRef) {
2062
+ collections.caseCounterRef = { value: 0 };
2063
+ }
2064
+ const branches = collections.branches;
2065
+ const cases = collections.cases;
2066
+ const branchCounterRef = collections.branchCounterRef;
2067
+ const caseCounterRef = collections.caseCounterRef;
2068
+ // Create BRANCH node
2069
+ const branchCounter = branchCounterRef.value++;
2070
+ const legacyBranchId = `${module.file}:BRANCH:switch:${getLine(switchNode)}:${branchCounter}`;
2071
+ const branchId = scopeTracker
2072
+ ? computeSemanticId('BRANCH', 'switch', scopeTracker.getContext(), { discriminator: branchCounter })
2073
+ : legacyBranchId;
2074
+ // Handle discriminant expression - store metadata directly (Linus improvement)
2075
+ let discriminantExpressionId;
2076
+ let discriminantExpressionType;
2077
+ let discriminantLine;
2078
+ let discriminantColumn;
2079
+ if (switchNode.discriminant) {
2080
+ const discResult = this.extractDiscriminantExpression(switchNode.discriminant, module);
2081
+ discriminantExpressionId = discResult.id;
2082
+ discriminantExpressionType = discResult.expressionType;
2083
+ discriminantLine = discResult.line;
2084
+ discriminantColumn = discResult.column;
2085
+ }
2086
+ branches.push({
2087
+ id: branchId,
2088
+ semanticId: branchId,
2089
+ type: 'BRANCH',
2090
+ branchType: 'switch',
1148
2091
  file: module.file,
1149
- line: getLine(tryNode),
1150
- parentScopeId
2092
+ line: getLine(switchNode),
2093
+ parentScopeId,
2094
+ discriminantExpressionId,
2095
+ discriminantExpressionType,
2096
+ discriminantLine,
2097
+ discriminantColumn
1151
2098
  });
1152
- if (scopeTracker) {
1153
- scopeTracker.enterCountedScope('try');
1154
- }
1155
- this.processBlockVariables(tryPath.get('block'), tryScopeId, module, variableDeclarations, literals, variableAssignments, varDeclCounterRef, literalCounterRef, scopeTracker);
1156
- if (scopeTracker) {
1157
- scopeTracker.exitScope();
1158
- }
1159
- // Create and process catch block if present
1160
- if (tryNode.handler) {
1161
- const catchBlock = tryNode.handler;
1162
- const catchScopeId = `SCOPE#catch-block#${module.file}#${getLine(catchBlock)}:${scopeCounterRef.value++}`;
1163
- const catchSemanticId = this.generateSemanticId('catch-block', scopeTracker);
1164
- scopes.push({
1165
- id: catchScopeId,
1166
- type: 'SCOPE',
1167
- scopeType: 'catch-block',
1168
- semanticId: catchSemanticId,
2099
+ // Process each case clause
2100
+ for (let i = 0; i < switchNode.cases.length; i++) {
2101
+ const caseNode = switchNode.cases[i];
2102
+ const isDefault = caseNode.test === null;
2103
+ const isEmpty = caseNode.consequent.length === 0;
2104
+ // Detect fall-through: no break/return/throw at end of consequent
2105
+ const fallsThrough = isEmpty || !this.caseTerminates(caseNode);
2106
+ // Extract case value
2107
+ const value = isDefault ? null : this.extractCaseValue(caseNode.test ?? null);
2108
+ const caseCounter = caseCounterRef.value++;
2109
+ const valueName = isDefault ? 'default' : String(value);
2110
+ const legacyCaseId = `${module.file}:CASE:${valueName}:${getLine(caseNode)}:${caseCounter}`;
2111
+ const caseId = scopeTracker
2112
+ ? computeSemanticId('CASE', valueName, scopeTracker.getContext(), { discriminator: caseCounter })
2113
+ : legacyCaseId;
2114
+ cases.push({
2115
+ id: caseId,
2116
+ semanticId: caseId,
2117
+ type: 'CASE',
2118
+ value,
2119
+ isDefault,
2120
+ fallsThrough,
2121
+ isEmpty,
1169
2122
  file: module.file,
1170
- line: getLine(catchBlock),
1171
- parentScopeId
2123
+ line: getLine(caseNode),
2124
+ parentBranchId: branchId
1172
2125
  });
1173
- if (scopeTracker) {
1174
- scopeTracker.enterCountedScope('catch');
1175
- }
1176
- // Handle catch parameter (e.g., catch (e))
1177
- if (catchBlock.param) {
1178
- const errorVarInfo = this.extractVariableNamesFromPattern(catchBlock.param);
1179
- errorVarInfo.forEach(varInfo => {
1180
- const legacyId = `VARIABLE#${varInfo.name}#${module.file}#${varInfo.loc.start.line}:${varInfo.loc.start.column}:${varDeclCounterRef.value++}`;
1181
- const varId = scopeTracker
1182
- ? computeSemanticId('VARIABLE', varInfo.name, scopeTracker.getContext())
1183
- : legacyId;
1184
- variableDeclarations.push({
1185
- id: varId,
1186
- type: 'VARIABLE',
1187
- name: varInfo.name,
1188
- file: module.file,
1189
- line: varInfo.loc.start.line,
1190
- parentScopeId: catchScopeId
1191
- });
1192
- });
2126
+ }
2127
+ }
2128
+ /**
2129
+ * Extract EXPRESSION node ID and metadata for switch discriminant
2130
+ */
2131
+ extractDiscriminantExpression(discriminant, module) {
2132
+ const line = getLine(discriminant);
2133
+ const column = getColumn(discriminant);
2134
+ if (t.isIdentifier(discriminant)) {
2135
+ // Simple identifier: switch(x) - create EXPRESSION node
2136
+ return {
2137
+ id: ExpressionNode.generateId('Identifier', module.file, line, column),
2138
+ expressionType: 'Identifier',
2139
+ line,
2140
+ column
2141
+ };
2142
+ }
2143
+ else if (t.isMemberExpression(discriminant)) {
2144
+ // Member expression: switch(action.type)
2145
+ return {
2146
+ id: ExpressionNode.generateId('MemberExpression', module.file, line, column),
2147
+ expressionType: 'MemberExpression',
2148
+ line,
2149
+ column
2150
+ };
2151
+ }
2152
+ else if (t.isCallExpression(discriminant)) {
2153
+ // Call expression: switch(getType())
2154
+ const callee = t.isIdentifier(discriminant.callee) ? discriminant.callee.name : '<complex>';
2155
+ // Return CALL node ID instead of EXPRESSION (reuse existing call tracking)
2156
+ return {
2157
+ id: `${module.file}:CALL:${callee}:${line}:${column}`,
2158
+ expressionType: 'CallExpression',
2159
+ line,
2160
+ column
2161
+ };
2162
+ }
2163
+ // Default: create generic EXPRESSION
2164
+ return {
2165
+ id: ExpressionNode.generateId(discriminant.type, module.file, line, column),
2166
+ expressionType: discriminant.type,
2167
+ line,
2168
+ column
2169
+ };
2170
+ }
2171
+ /**
2172
+ * Extract case test value as a primitive
2173
+ */
2174
+ extractCaseValue(test) {
2175
+ if (!test)
2176
+ return null;
2177
+ if (t.isStringLiteral(test)) {
2178
+ return test.value;
2179
+ }
2180
+ else if (t.isNumericLiteral(test)) {
2181
+ return test.value;
2182
+ }
2183
+ else if (t.isBooleanLiteral(test)) {
2184
+ return test.value;
2185
+ }
2186
+ else if (t.isNullLiteral(test)) {
2187
+ return null;
2188
+ }
2189
+ else if (t.isIdentifier(test)) {
2190
+ // Constant reference: case CONSTANTS.ADD
2191
+ return test.name;
2192
+ }
2193
+ else if (t.isMemberExpression(test)) {
2194
+ // Member expression: case Action.ADD
2195
+ return this.memberExpressionToString(test);
2196
+ }
2197
+ return '<complex>';
2198
+ }
2199
+ /**
2200
+ * Check if case clause terminates (has break, return, throw)
2201
+ */
2202
+ caseTerminates(caseNode) {
2203
+ const statements = caseNode.consequent;
2204
+ if (statements.length === 0)
2205
+ return false;
2206
+ // Check last statement (or any statement for early returns)
2207
+ for (const stmt of statements) {
2208
+ if (t.isBreakStatement(stmt))
2209
+ return true;
2210
+ if (t.isReturnStatement(stmt))
2211
+ return true;
2212
+ if (t.isThrowStatement(stmt))
2213
+ return true;
2214
+ if (t.isContinueStatement(stmt))
2215
+ return true; // In switch inside loop
2216
+ // Check for nested blocks (if last statement is block, check inside)
2217
+ if (t.isBlockStatement(stmt)) {
2218
+ const lastInBlock = stmt.body[stmt.body.length - 1];
2219
+ if (lastInBlock && (t.isBreakStatement(lastInBlock) ||
2220
+ t.isReturnStatement(lastInBlock) ||
2221
+ t.isThrowStatement(lastInBlock))) {
2222
+ return true;
2223
+ }
2224
+ }
2225
+ // Check for if-else where both branches terminate
2226
+ if (t.isIfStatement(stmt) && stmt.alternate) {
2227
+ const ifTerminates = this.blockTerminates(stmt.consequent);
2228
+ const elseTerminates = this.blockTerminates(stmt.alternate);
2229
+ if (ifTerminates && elseTerminates)
2230
+ return true;
2231
+ }
2232
+ }
2233
+ return false;
2234
+ }
2235
+ /**
2236
+ * Check if a block/statement terminates
2237
+ */
2238
+ blockTerminates(node) {
2239
+ if (t.isBreakStatement(node))
2240
+ return true;
2241
+ if (t.isReturnStatement(node))
2242
+ return true;
2243
+ if (t.isThrowStatement(node))
2244
+ return true;
2245
+ if (t.isBlockStatement(node)) {
2246
+ const last = node.body[node.body.length - 1];
2247
+ return last ? this.blockTerminates(last) : false;
2248
+ }
2249
+ return false;
2250
+ }
2251
+ /**
2252
+ * Count logical operators (&& and ||) in a condition expression.
2253
+ * Used for cyclomatic complexity calculation (Phase 6 REG-267).
2254
+ *
2255
+ * @param node - The condition expression to analyze
2256
+ * @returns Number of logical operators found
2257
+ */
2258
+ countLogicalOperators(node) {
2259
+ let count = 0;
2260
+ const traverse = (expr) => {
2261
+ if (t.isLogicalExpression(expr)) {
2262
+ // Count && and || operators
2263
+ if (expr.operator === '&&' || expr.operator === '||') {
2264
+ count++;
2265
+ }
2266
+ traverse(expr.left);
2267
+ traverse(expr.right);
1193
2268
  }
1194
- this.processBlockVariables(tryPath.get('handler.body'), catchScopeId, module, variableDeclarations, literals, variableAssignments, varDeclCounterRef, literalCounterRef, scopeTracker);
1195
- if (scopeTracker) {
1196
- scopeTracker.exitScope();
1197
- }
1198
- }
1199
- // Create and process finally block if present
1200
- if (tryNode.finalizer) {
1201
- const finallyScopeId = `SCOPE#finally-block#${module.file}#${getLine(tryNode.finalizer)}:${scopeCounterRef.value++}`;
1202
- const finallySemanticId = this.generateSemanticId('finally-block', scopeTracker);
1203
- scopes.push({
1204
- id: finallyScopeId,
1205
- type: 'SCOPE',
1206
- scopeType: 'finally-block',
1207
- semanticId: finallySemanticId,
1208
- file: module.file,
1209
- line: getLine(tryNode.finalizer),
1210
- parentScopeId
1211
- });
1212
- if (scopeTracker) {
1213
- scopeTracker.enterCountedScope('finally');
2269
+ else if (t.isConditionalExpression(expr)) {
2270
+ // Handle ternary conditions: test ? consequent : alternate
2271
+ traverse(expr.test);
2272
+ traverse(expr.consequent);
2273
+ traverse(expr.alternate);
1214
2274
  }
1215
- const finalizerPath = tryPath.get('finalizer');
1216
- if (finalizerPath.node) {
1217
- this.processBlockVariables(finalizerPath, finallyScopeId, module, variableDeclarations, literals, variableAssignments, varDeclCounterRef, literalCounterRef, scopeTracker);
2275
+ else if (t.isUnaryExpression(expr)) {
2276
+ traverse(expr.argument);
1218
2277
  }
1219
- if (scopeTracker) {
1220
- scopeTracker.exitScope();
2278
+ else if (t.isBinaryExpression(expr)) {
2279
+ traverse(expr.left);
2280
+ traverse(expr.right);
2281
+ }
2282
+ else if (t.isSequenceExpression(expr)) {
2283
+ for (const e of expr.expressions) {
2284
+ traverse(e);
2285
+ }
2286
+ }
2287
+ else if (t.isParenthesizedExpression(expr)) {
2288
+ traverse(expr.expression);
2289
+ }
2290
+ };
2291
+ traverse(node);
2292
+ return count;
2293
+ }
2294
+ /**
2295
+ * Convert MemberExpression to string representation
2296
+ */
2297
+ memberExpressionToString(expr) {
2298
+ const parts = [];
2299
+ let current = expr;
2300
+ while (t.isMemberExpression(current)) {
2301
+ if (t.isIdentifier(current.property)) {
2302
+ parts.unshift(current.property.name);
1221
2303
  }
2304
+ else {
2305
+ parts.unshift('<computed>');
2306
+ }
2307
+ current = current.object;
1222
2308
  }
1223
- tryPath.skip();
2309
+ if (t.isIdentifier(current)) {
2310
+ parts.unshift(current.name);
2311
+ }
2312
+ return parts.join('.');
1224
2313
  }
1225
2314
  /**
1226
2315
  * Factory method to create IfStatement handler.
1227
- * Creates if scope with condition parsing and optional else scope.
2316
+ * Creates BRANCH node for if statement and SCOPE nodes for if/else bodies.
1228
2317
  * Tracks if/else scope transitions via ifElseScopeMap.
1229
2318
  *
2319
+ * Phase 3 (REG-267): Creates BRANCH node with branchType='if' and
2320
+ * HAS_CONSEQUENT/HAS_ALTERNATE edges to body SCOPEs.
2321
+ *
1230
2322
  * @param parentScopeId - Parent scope ID for the scope nodes
1231
2323
  * @param module - Module context
1232
2324
  * @param scopes - Collection to push scope nodes to
2325
+ * @param branches - Collection to push BRANCH nodes to
1233
2326
  * @param ifScopeCounterRef - Counter for unique if scope IDs
2327
+ * @param branchCounterRef - Counter for unique BRANCH IDs
1234
2328
  * @param scopeTracker - Tracker for semantic ID generation
1235
2329
  * @param sourceCode - Source code for extracting condition text
1236
2330
  * @param ifElseScopeMap - Map to track if/else scope transitions
2331
+ * @param scopeIdStack - Stack for tracking current scope ID for CONTAINS edges
1237
2332
  */
1238
- createIfStatementHandler(parentScopeId, module, scopes, ifScopeCounterRef, scopeTracker, sourceCode, ifElseScopeMap) {
2333
+ createIfStatementHandler(parentScopeId, module, scopes, branches, ifScopeCounterRef, branchCounterRef, scopeTracker, sourceCode, ifElseScopeMap, scopeIdStack, controlFlowState, countLogicalOperators) {
1239
2334
  return {
1240
2335
  enter: (ifPath) => {
1241
2336
  const ifNode = ifPath.node;
1242
2337
  const condition = sourceCode.substring(ifNode.test.start, ifNode.test.end) || 'condition';
2338
+ // Phase 6 (REG-267): Increment branch count and count logical operators
2339
+ if (controlFlowState) {
2340
+ controlFlowState.branchCount++;
2341
+ if (countLogicalOperators) {
2342
+ controlFlowState.logicalOpCount += countLogicalOperators(ifNode.test);
2343
+ }
2344
+ }
2345
+ // Check if this if-statement is an else-if (alternate of parent IfStatement)
2346
+ const isElseIf = t.isIfStatement(ifPath.parent) && ifPath.parentKey === 'alternate';
2347
+ // Determine actual parent scope
2348
+ let actualParentScopeId;
2349
+ if (isElseIf) {
2350
+ // For else-if, parent should be the outer BRANCH (stored in ifElseScopeMap)
2351
+ const parentIfInfo = ifElseScopeMap.get(ifPath.parent);
2352
+ if (parentIfInfo) {
2353
+ actualParentScopeId = parentIfInfo.branchId;
2354
+ }
2355
+ else {
2356
+ // Fallback to stack
2357
+ actualParentScopeId = (scopeIdStack && scopeIdStack.length > 0)
2358
+ ? scopeIdStack[scopeIdStack.length - 1]
2359
+ : parentScopeId;
2360
+ }
2361
+ }
2362
+ else {
2363
+ // For regular if statements, use stack or original parentScopeId
2364
+ actualParentScopeId = (scopeIdStack && scopeIdStack.length > 0)
2365
+ ? scopeIdStack[scopeIdStack.length - 1]
2366
+ : parentScopeId;
2367
+ }
2368
+ // 1. Create BRANCH node for if statement
2369
+ const branchCounter = branchCounterRef.value++;
2370
+ const legacyBranchId = `${module.file}:BRANCH:if:${getLine(ifNode)}:${branchCounter}`;
2371
+ const branchId = scopeTracker
2372
+ ? computeSemanticId('BRANCH', 'if', scopeTracker.getContext(), { discriminator: branchCounter })
2373
+ : legacyBranchId;
2374
+ // 2. Extract condition expression info for HAS_CONDITION edge
2375
+ const conditionResult = this.extractDiscriminantExpression(ifNode.test, module);
2376
+ // For else-if, get the parent branch ID
2377
+ const isAlternateOfBranchId = isElseIf
2378
+ ? ifElseScopeMap.get(ifPath.parent)?.branchId
2379
+ : undefined;
2380
+ branches.push({
2381
+ id: branchId,
2382
+ semanticId: branchId,
2383
+ type: 'BRANCH',
2384
+ branchType: 'if',
2385
+ file: module.file,
2386
+ line: getLine(ifNode),
2387
+ parentScopeId: actualParentScopeId,
2388
+ discriminantExpressionId: conditionResult.id,
2389
+ discriminantExpressionType: conditionResult.expressionType,
2390
+ discriminantLine: conditionResult.line,
2391
+ discriminantColumn: conditionResult.column,
2392
+ isAlternateOfBranchId
2393
+ });
2394
+ // 3. Create if-body SCOPE (backward compatibility)
2395
+ // Parent is now BRANCH, not original parentScopeId
1243
2396
  const counterId = ifScopeCounterRef.value++;
1244
2397
  const ifScopeId = `SCOPE#if#${module.file}#${getLine(ifNode)}:${getColumn(ifNode)}:${counterId}`;
1245
2398
  // Parse condition to extract constraints
@@ -1256,17 +2409,22 @@ export class JSASTAnalyzer extends Plugin {
1256
2409
  constraints: constraints.length > 0 ? constraints : undefined,
1257
2410
  file: module.file,
1258
2411
  line: getLine(ifNode),
1259
- parentScopeId
2412
+ parentScopeId: branchId // Parent is BRANCH, not original parentScopeId
1260
2413
  });
2414
+ // 4. Push if scope onto stack for CONTAINS edges
2415
+ if (scopeIdStack) {
2416
+ scopeIdStack.push(ifScopeId);
2417
+ }
1261
2418
  // Enter scope for semantic ID generation
1262
2419
  if (scopeTracker) {
1263
2420
  scopeTracker.enterCountedScope('if');
1264
2421
  }
1265
- // Handle else branch if present
2422
+ // 5. Handle else branch if present
2423
+ let elseScopeId = null;
1266
2424
  if (ifNode.alternate && !t.isIfStatement(ifNode.alternate)) {
1267
2425
  // Only create else scope for actual else block, not else-if
1268
2426
  const elseCounterId = ifScopeCounterRef.value++;
1269
- const elseScopeId = `SCOPE#else#${module.file}#${getLine(ifNode.alternate)}:${getColumn(ifNode.alternate)}:${elseCounterId}`;
2427
+ elseScopeId = `SCOPE#else#${module.file}#${getLine(ifNode.alternate)}:${getColumn(ifNode.alternate)}:${elseCounterId}`;
1270
2428
  const negatedConstraints = constraints.length > 0 ? ConditionParser.negate(constraints) : undefined;
1271
2429
  const elseSemanticId = this.generateSemanticId('else_statement', scopeTracker);
1272
2430
  scopes.push({
@@ -1279,17 +2437,21 @@ export class JSASTAnalyzer extends Plugin {
1279
2437
  constraints: negatedConstraints,
1280
2438
  file: module.file,
1281
2439
  line: getLine(ifNode.alternate),
1282
- parentScopeId
2440
+ parentScopeId: branchId // Parent is BRANCH, not original parentScopeId
1283
2441
  });
1284
2442
  // Store info to switch to else scope when we enter alternate
1285
- ifElseScopeMap.set(ifNode, { inElse: false, hasElse: true });
2443
+ ifElseScopeMap.set(ifNode, { inElse: false, hasElse: true, ifScopeId, elseScopeId, branchId });
1286
2444
  }
1287
2445
  else {
1288
- ifElseScopeMap.set(ifNode, { inElse: false, hasElse: false });
2446
+ ifElseScopeMap.set(ifNode, { inElse: false, hasElse: false, ifScopeId, elseScopeId: null, branchId });
1289
2447
  }
1290
2448
  },
1291
2449
  exit: (ifPath) => {
1292
2450
  const ifNode = ifPath.node;
2451
+ // Pop scope from stack (either if or else, depending on what we're exiting)
2452
+ if (scopeIdStack) {
2453
+ scopeIdStack.pop();
2454
+ }
1293
2455
  // Exit the current scope (either if or else)
1294
2456
  if (scopeTracker) {
1295
2457
  scopeTracker.exitScope();
@@ -1301,26 +2463,116 @@ export class JSASTAnalyzer extends Plugin {
1301
2463
  };
1302
2464
  }
1303
2465
  /**
1304
- * Factory method to create BlockStatement handler for tracking if/else transitions.
2466
+ * Factory method to create ConditionalExpression (ternary) handler.
2467
+ * Creates BRANCH nodes with branchType='ternary' and increments branchCount for cyclomatic complexity.
2468
+ *
2469
+ * Key difference from IfStatement: ternary has EXPRESSIONS as branches, not SCOPE blocks.
2470
+ * We store consequentExpressionId and alternateExpressionId in BranchInfo for HAS_CONSEQUENT/HAS_ALTERNATE edges.
2471
+ *
2472
+ * @param parentScopeId - Parent scope ID for the BRANCH node
2473
+ * @param module - Module context
2474
+ * @param branches - Collection to push BRANCH nodes to
2475
+ * @param branchCounterRef - Counter for unique BRANCH IDs
2476
+ * @param scopeTracker - Tracker for semantic ID generation
2477
+ * @param scopeIdStack - Stack for tracking current scope ID for CONTAINS edges
2478
+ * @param controlFlowState - State for tracking control flow metrics (complexity)
2479
+ * @param countLogicalOperators - Function to count logical operators in condition
2480
+ */
2481
+ createConditionalExpressionHandler(parentScopeId, module, branches, branchCounterRef, scopeTracker, scopeIdStack, controlFlowState, countLogicalOperators) {
2482
+ return (condPath) => {
2483
+ const condNode = condPath.node;
2484
+ // Increment branch count for cyclomatic complexity
2485
+ if (controlFlowState) {
2486
+ controlFlowState.branchCount++;
2487
+ // Count logical operators in the test condition (e.g., a && b ? x : y)
2488
+ if (countLogicalOperators) {
2489
+ controlFlowState.logicalOpCount += countLogicalOperators(condNode.test);
2490
+ }
2491
+ }
2492
+ // Determine parent scope from stack or fallback
2493
+ const actualParentScopeId = (scopeIdStack && scopeIdStack.length > 0)
2494
+ ? scopeIdStack[scopeIdStack.length - 1]
2495
+ : parentScopeId;
2496
+ // Create BRANCH node with branchType='ternary'
2497
+ const branchCounter = branchCounterRef.value++;
2498
+ const legacyBranchId = `${module.file}:BRANCH:ternary:${getLine(condNode)}:${branchCounter}`;
2499
+ const branchId = scopeTracker
2500
+ ? computeSemanticId('BRANCH', 'ternary', scopeTracker.getContext(), { discriminator: branchCounter })
2501
+ : legacyBranchId;
2502
+ // Extract condition expression info for HAS_CONDITION edge
2503
+ const conditionResult = this.extractDiscriminantExpression(condNode.test, module);
2504
+ // Generate expression IDs for consequent and alternate
2505
+ const consequentLine = getLine(condNode.consequent);
2506
+ const consequentColumn = getColumn(condNode.consequent);
2507
+ const consequentExpressionId = ExpressionNode.generateId(condNode.consequent.type, module.file, consequentLine, consequentColumn);
2508
+ const alternateLine = getLine(condNode.alternate);
2509
+ const alternateColumn = getColumn(condNode.alternate);
2510
+ const alternateExpressionId = ExpressionNode.generateId(condNode.alternate.type, module.file, alternateLine, alternateColumn);
2511
+ branches.push({
2512
+ id: branchId,
2513
+ semanticId: branchId,
2514
+ type: 'BRANCH',
2515
+ branchType: 'ternary',
2516
+ file: module.file,
2517
+ line: getLine(condNode),
2518
+ parentScopeId: actualParentScopeId,
2519
+ discriminantExpressionId: conditionResult.id,
2520
+ discriminantExpressionType: conditionResult.expressionType,
2521
+ discriminantLine: conditionResult.line,
2522
+ discriminantColumn: conditionResult.column,
2523
+ consequentExpressionId,
2524
+ alternateExpressionId
2525
+ });
2526
+ };
2527
+ }
2528
+ /**
2529
+ * Factory method to create BlockStatement handler for tracking if/else and try/finally transitions.
1305
2530
  * When entering an else block, switches scope from if to else.
2531
+ * When entering a finally block, switches scope from try/catch to finally.
1306
2532
  *
1307
2533
  * @param scopeTracker - Tracker for semantic ID generation
1308
2534
  * @param ifElseScopeMap - Map to track if/else scope transitions
2535
+ * @param tryScopeMap - Map to track try/catch/finally scope transitions
2536
+ * @param scopeIdStack - Stack for tracking current scope ID for CONTAINS edges
1309
2537
  */
1310
- createIfElseBlockStatementHandler(scopeTracker, ifElseScopeMap) {
2538
+ createBlockStatementHandler(scopeTracker, ifElseScopeMap, tryScopeMap, scopeIdStack) {
1311
2539
  return {
1312
2540
  enter: (blockPath) => {
1313
- // Check if this block is the alternate of an IfStatement
1314
2541
  const parent = blockPath.parent;
2542
+ // Check if this block is the alternate of an IfStatement
1315
2543
  if (t.isIfStatement(parent) && parent.alternate === blockPath.node) {
1316
2544
  const scopeInfo = ifElseScopeMap.get(parent);
1317
- if (scopeInfo && scopeInfo.hasElse && !scopeInfo.inElse && scopeTracker) {
1318
- // Exit if scope, enter else scope
1319
- scopeTracker.exitScope();
1320
- scopeTracker.enterCountedScope('else');
2545
+ if (scopeInfo && scopeInfo.hasElse && !scopeInfo.inElse) {
2546
+ // Swap if-scope for else-scope on the stack
2547
+ if (scopeIdStack && scopeInfo.elseScopeId) {
2548
+ scopeIdStack.pop(); // Remove if-scope
2549
+ scopeIdStack.push(scopeInfo.elseScopeId); // Push else-scope
2550
+ }
2551
+ // Exit if scope, enter else scope for semantic ID tracking
2552
+ if (scopeTracker) {
2553
+ scopeTracker.exitScope();
2554
+ scopeTracker.enterCountedScope('else');
2555
+ }
1321
2556
  scopeInfo.inElse = true;
1322
2557
  }
1323
2558
  }
2559
+ // Check if this block is the finalizer of a TryStatement
2560
+ if (t.isTryStatement(parent) && parent.finalizer === blockPath.node) {
2561
+ const scopeInfo = tryScopeMap.get(parent);
2562
+ if (scopeInfo && scopeInfo.finallyScopeId && scopeInfo.currentBlock !== 'finally') {
2563
+ // Pop current scope (try or catch), push finally scope
2564
+ if (scopeIdStack) {
2565
+ scopeIdStack.pop();
2566
+ scopeIdStack.push(scopeInfo.finallyScopeId);
2567
+ }
2568
+ // Exit current scope, enter finally scope for semantic ID tracking
2569
+ if (scopeTracker) {
2570
+ scopeTracker.exitScope();
2571
+ scopeTracker.enterCountedScope('finally');
2572
+ }
2573
+ scopeInfo.currentBlock = 'finally';
2574
+ }
2575
+ }
1324
2576
  }
1325
2577
  };
1326
2578
  }
@@ -1338,6 +2590,7 @@ export class JSASTAnalyzer extends Plugin {
1338
2590
  const eventListeners = (collections.eventListeners ?? []);
1339
2591
  const methodCallbacks = (collections.methodCallbacks ?? []);
1340
2592
  const classInstantiations = (collections.classInstantiations ?? []);
2593
+ const constructorCalls = (collections.constructorCalls ?? []);
1341
2594
  const httpRequests = (collections.httpRequests ?? []);
1342
2595
  const literals = (collections.literals ?? []);
1343
2596
  const variableAssignments = (collections.variableAssignments ?? []);
@@ -1350,6 +2603,32 @@ export class JSASTAnalyzer extends Plugin {
1350
2603
  const literalCounterRef = (collections.literalCounterRef ?? { value: 0 });
1351
2604
  const anonymousFunctionCounterRef = (collections.anonymousFunctionCounterRef ?? { value: 0 });
1352
2605
  const scopeTracker = collections.scopeTracker;
2606
+ // Object literal tracking (REG-328)
2607
+ if (!collections.objectLiterals) {
2608
+ collections.objectLiterals = [];
2609
+ }
2610
+ if (!collections.objectProperties) {
2611
+ collections.objectProperties = [];
2612
+ }
2613
+ if (!collections.objectLiteralCounterRef) {
2614
+ collections.objectLiteralCounterRef = { value: 0 };
2615
+ }
2616
+ const objectLiterals = collections.objectLiterals;
2617
+ const objectProperties = collections.objectProperties;
2618
+ const objectLiteralCounterRef = collections.objectLiteralCounterRef;
2619
+ const returnStatements = (collections.returnStatements ?? []);
2620
+ const parameters = (collections.parameters ?? []);
2621
+ // Control flow collections (Phase 2: LOOP nodes)
2622
+ // Initialize if not exist to ensure nested function calls share same arrays
2623
+ if (!collections.loops) {
2624
+ collections.loops = [];
2625
+ }
2626
+ if (!collections.loopCounterRef) {
2627
+ collections.loopCounterRef = { value: 0 };
2628
+ }
2629
+ const loops = collections.loops;
2630
+ const loopCounterRef = collections.loopCounterRef;
2631
+ const updateExpressions = (collections.updateExpressions ?? []);
1353
2632
  const processedNodes = collections.processedNodes ?? {
1354
2633
  functions: new Set(),
1355
2634
  classes: new Set(),
@@ -1366,22 +2645,241 @@ export class JSASTAnalyzer extends Plugin {
1366
2645
  const processedVarDecls = processedNodes.varDecls;
1367
2646
  const processedMethodCalls = processedNodes.methodCalls;
1368
2647
  const processedEventListeners = processedNodes.eventListeners;
1369
- // Track if/else scope transitions
2648
+ // Track if/else scope transitions (Phase 3: extended with branchId)
1370
2649
  const ifElseScopeMap = new Map();
2650
+ // Ensure branches and branchCounterRef are initialized (used by IfStatement and SwitchStatement)
2651
+ if (!collections.branches) {
2652
+ collections.branches = [];
2653
+ }
2654
+ if (!collections.branchCounterRef) {
2655
+ collections.branchCounterRef = { value: 0 };
2656
+ }
2657
+ const branches = collections.branches;
2658
+ const branchCounterRef = collections.branchCounterRef;
2659
+ // Phase 4: Initialize try/catch/finally collections and counters
2660
+ if (!collections.tryBlocks) {
2661
+ collections.tryBlocks = [];
2662
+ }
2663
+ if (!collections.catchBlocks) {
2664
+ collections.catchBlocks = [];
2665
+ }
2666
+ if (!collections.finallyBlocks) {
2667
+ collections.finallyBlocks = [];
2668
+ }
2669
+ if (!collections.tryBlockCounterRef) {
2670
+ collections.tryBlockCounterRef = { value: 0 };
2671
+ }
2672
+ if (!collections.catchBlockCounterRef) {
2673
+ collections.catchBlockCounterRef = { value: 0 };
2674
+ }
2675
+ if (!collections.finallyBlockCounterRef) {
2676
+ collections.finallyBlockCounterRef = { value: 0 };
2677
+ }
2678
+ const tryBlocks = collections.tryBlocks;
2679
+ const catchBlocks = collections.catchBlocks;
2680
+ const finallyBlocks = collections.finallyBlocks;
2681
+ const tryBlockCounterRef = collections.tryBlockCounterRef;
2682
+ const catchBlockCounterRef = collections.catchBlockCounterRef;
2683
+ const finallyBlockCounterRef = collections.finallyBlockCounterRef;
2684
+ // Track try/catch/finally scope transitions
2685
+ const tryScopeMap = new Map();
2686
+ // REG-334: Use shared Promise executor contexts from collections.
2687
+ // These are populated by module-level NewExpression handler and function-level NewExpression handler.
2688
+ if (!collections.promiseExecutorContexts) {
2689
+ collections.promiseExecutorContexts = new Map();
2690
+ }
2691
+ const promiseExecutorContexts = collections.promiseExecutorContexts;
2692
+ // Initialize promiseResolutions array if not exists
2693
+ if (!collections.promiseResolutions) {
2694
+ collections.promiseResolutions = [];
2695
+ }
2696
+ const promiseResolutions = collections.promiseResolutions;
2697
+ // Dynamic scope ID stack for CONTAINS edges
2698
+ // Starts with the function body scope, gets updated as we enter/exit conditional scopes
2699
+ const scopeIdStack = [parentScopeId];
2700
+ const getCurrentScopeId = () => scopeIdStack[scopeIdStack.length - 1];
2701
+ // Determine the ID of the function we're analyzing for RETURNS edges
2702
+ // Find by matching file/line/column in functions collection (it was just added by the visitor)
2703
+ const funcNode = funcPath.node;
2704
+ const funcLine = getLine(funcNode);
2705
+ const funcColumn = getColumn(funcNode);
2706
+ let currentFunctionId = null;
2707
+ const matchingFunction = functions.find(f => f.file === module.file &&
2708
+ f.line === funcLine &&
2709
+ (f.column === undefined || f.column === funcColumn));
2710
+ if (matchingFunction) {
2711
+ currentFunctionId = matchingFunction.id;
2712
+ }
2713
+ // Phase 6 (REG-267): Control flow tracking state for cyclomatic complexity
2714
+ const controlFlowState = {
2715
+ branchCount: 0, // if/switch statements
2716
+ loopCount: 0, // for/while/do-while/for-in/for-of
2717
+ caseCount: 0, // switch cases (excluding default)
2718
+ logicalOpCount: 0, // && and || in conditions
2719
+ hasTryCatch: false,
2720
+ hasEarlyReturn: false,
2721
+ hasThrow: false,
2722
+ returnCount: 0, // Track total return count for early return detection
2723
+ totalStatements: 0 // Track if there are statements after returns
2724
+ };
2725
+ // Handle implicit return for THIS arrow function if it has an expression body
2726
+ // e.g., `const double = x => x * 2;` - the function we're analyzing IS an arrow with expression body
2727
+ if (t.isArrowFunctionExpression(funcNode) && !t.isBlockStatement(funcNode.body) && currentFunctionId) {
2728
+ const bodyExpr = funcNode.body;
2729
+ const bodyLine = getLine(bodyExpr);
2730
+ const bodyColumn = getColumn(bodyExpr);
2731
+ const returnInfo = {
2732
+ parentFunctionId: currentFunctionId,
2733
+ file: module.file,
2734
+ line: bodyLine,
2735
+ column: bodyColumn,
2736
+ returnValueType: 'NONE',
2737
+ isImplicitReturn: true
2738
+ };
2739
+ // Apply type detection logic for the implicit return
2740
+ if (t.isIdentifier(bodyExpr)) {
2741
+ returnInfo.returnValueType = 'VARIABLE';
2742
+ returnInfo.returnValueName = bodyExpr.name;
2743
+ }
2744
+ // TemplateLiteral must come BEFORE isLiteral (TemplateLiteral extends Literal)
2745
+ else if (t.isTemplateLiteral(bodyExpr)) {
2746
+ returnInfo.returnValueType = 'EXPRESSION';
2747
+ returnInfo.expressionType = 'TemplateLiteral';
2748
+ returnInfo.returnValueLine = getLine(bodyExpr);
2749
+ returnInfo.returnValueColumn = getColumn(bodyExpr);
2750
+ returnInfo.returnValueId = NodeFactory.generateExpressionId('TemplateLiteral', module.file, getLine(bodyExpr), getColumn(bodyExpr));
2751
+ const sourceNames = [];
2752
+ for (const expr of bodyExpr.expressions) {
2753
+ if (t.isIdentifier(expr))
2754
+ sourceNames.push(expr.name);
2755
+ }
2756
+ if (sourceNames.length > 0)
2757
+ returnInfo.expressionSourceNames = sourceNames;
2758
+ }
2759
+ else if (t.isLiteral(bodyExpr)) {
2760
+ returnInfo.returnValueType = 'LITERAL';
2761
+ const literalId = `LITERAL#implicit_return#${module.file}#${funcLine}:${funcColumn}:${literalCounterRef.value++}`;
2762
+ returnInfo.returnValueId = literalId;
2763
+ literals.push({
2764
+ id: literalId,
2765
+ type: 'LITERAL',
2766
+ value: ExpressionEvaluator.extractLiteralValue(bodyExpr),
2767
+ valueType: typeof ExpressionEvaluator.extractLiteralValue(bodyExpr),
2768
+ file: module.file,
2769
+ line: bodyLine,
2770
+ column: bodyColumn
2771
+ });
2772
+ }
2773
+ else if (t.isCallExpression(bodyExpr) && t.isIdentifier(bodyExpr.callee)) {
2774
+ returnInfo.returnValueType = 'CALL_SITE';
2775
+ returnInfo.returnValueLine = getLine(bodyExpr);
2776
+ returnInfo.returnValueColumn = getColumn(bodyExpr);
2777
+ returnInfo.returnValueCallName = bodyExpr.callee.name;
2778
+ }
2779
+ else if (t.isCallExpression(bodyExpr) && t.isMemberExpression(bodyExpr.callee)) {
2780
+ returnInfo.returnValueType = 'METHOD_CALL';
2781
+ returnInfo.returnValueLine = getLine(bodyExpr);
2782
+ returnInfo.returnValueColumn = getColumn(bodyExpr);
2783
+ if (t.isIdentifier(bodyExpr.callee.property)) {
2784
+ returnInfo.returnValueCallName = bodyExpr.callee.property.name;
2785
+ }
2786
+ }
2787
+ // REG-276: Detailed EXPRESSION handling for implicit arrow returns
2788
+ else if (t.isBinaryExpression(bodyExpr)) {
2789
+ returnInfo.returnValueType = 'EXPRESSION';
2790
+ returnInfo.expressionType = 'BinaryExpression';
2791
+ returnInfo.returnValueLine = getLine(bodyExpr);
2792
+ returnInfo.returnValueColumn = getColumn(bodyExpr);
2793
+ returnInfo.operator = bodyExpr.operator;
2794
+ returnInfo.returnValueId = NodeFactory.generateExpressionId('BinaryExpression', module.file, getLine(bodyExpr), getColumn(bodyExpr));
2795
+ if (t.isIdentifier(bodyExpr.left))
2796
+ returnInfo.leftSourceName = bodyExpr.left.name;
2797
+ if (t.isIdentifier(bodyExpr.right))
2798
+ returnInfo.rightSourceName = bodyExpr.right.name;
2799
+ }
2800
+ else if (t.isLogicalExpression(bodyExpr)) {
2801
+ returnInfo.returnValueType = 'EXPRESSION';
2802
+ returnInfo.expressionType = 'LogicalExpression';
2803
+ returnInfo.returnValueLine = getLine(bodyExpr);
2804
+ returnInfo.returnValueColumn = getColumn(bodyExpr);
2805
+ returnInfo.operator = bodyExpr.operator;
2806
+ returnInfo.returnValueId = NodeFactory.generateExpressionId('LogicalExpression', module.file, getLine(bodyExpr), getColumn(bodyExpr));
2807
+ if (t.isIdentifier(bodyExpr.left))
2808
+ returnInfo.leftSourceName = bodyExpr.left.name;
2809
+ if (t.isIdentifier(bodyExpr.right))
2810
+ returnInfo.rightSourceName = bodyExpr.right.name;
2811
+ }
2812
+ else if (t.isConditionalExpression(bodyExpr)) {
2813
+ returnInfo.returnValueType = 'EXPRESSION';
2814
+ returnInfo.expressionType = 'ConditionalExpression';
2815
+ returnInfo.returnValueLine = getLine(bodyExpr);
2816
+ returnInfo.returnValueColumn = getColumn(bodyExpr);
2817
+ returnInfo.returnValueId = NodeFactory.generateExpressionId('ConditionalExpression', module.file, getLine(bodyExpr), getColumn(bodyExpr));
2818
+ if (t.isIdentifier(bodyExpr.consequent))
2819
+ returnInfo.consequentSourceName = bodyExpr.consequent.name;
2820
+ if (t.isIdentifier(bodyExpr.alternate))
2821
+ returnInfo.alternateSourceName = bodyExpr.alternate.name;
2822
+ }
2823
+ else if (t.isUnaryExpression(bodyExpr)) {
2824
+ returnInfo.returnValueType = 'EXPRESSION';
2825
+ returnInfo.expressionType = 'UnaryExpression';
2826
+ returnInfo.returnValueLine = getLine(bodyExpr);
2827
+ returnInfo.returnValueColumn = getColumn(bodyExpr);
2828
+ returnInfo.operator = bodyExpr.operator;
2829
+ returnInfo.returnValueId = NodeFactory.generateExpressionId('UnaryExpression', module.file, getLine(bodyExpr), getColumn(bodyExpr));
2830
+ if (t.isIdentifier(bodyExpr.argument))
2831
+ returnInfo.unaryArgSourceName = bodyExpr.argument.name;
2832
+ }
2833
+ else if (t.isMemberExpression(bodyExpr)) {
2834
+ returnInfo.returnValueType = 'EXPRESSION';
2835
+ returnInfo.expressionType = 'MemberExpression';
2836
+ returnInfo.returnValueLine = getLine(bodyExpr);
2837
+ returnInfo.returnValueColumn = getColumn(bodyExpr);
2838
+ returnInfo.returnValueId = NodeFactory.generateExpressionId('MemberExpression', module.file, getLine(bodyExpr), getColumn(bodyExpr));
2839
+ if (t.isIdentifier(bodyExpr.object)) {
2840
+ returnInfo.object = bodyExpr.object.name;
2841
+ returnInfo.objectSourceName = bodyExpr.object.name;
2842
+ }
2843
+ if (t.isIdentifier(bodyExpr.property))
2844
+ returnInfo.property = bodyExpr.property.name;
2845
+ returnInfo.computed = bodyExpr.computed;
2846
+ }
2847
+ else {
2848
+ // Fallback: any other expression type
2849
+ returnInfo.returnValueType = 'EXPRESSION';
2850
+ returnInfo.expressionType = bodyExpr.type;
2851
+ returnInfo.returnValueLine = getLine(bodyExpr);
2852
+ returnInfo.returnValueColumn = getColumn(bodyExpr);
2853
+ returnInfo.returnValueId = NodeFactory.generateExpressionId(bodyExpr.type, module.file, getLine(bodyExpr), getColumn(bodyExpr));
2854
+ }
2855
+ returnStatements.push(returnInfo);
2856
+ }
1371
2857
  funcPath.traverse({
1372
2858
  VariableDeclaration: (varPath) => {
1373
- this.handleVariableDeclaration(varPath, parentScopeId, module, variableDeclarations, classInstantiations, literals, variableAssignments, varDeclCounterRef, literalCounterRef, scopeTracker, parentScopeVariables);
2859
+ this.handleVariableDeclaration(varPath, getCurrentScopeId(), module, variableDeclarations, classInstantiations, literals, variableAssignments, varDeclCounterRef, literalCounterRef, scopeTracker, parentScopeVariables, objectLiterals, objectProperties, objectLiteralCounterRef);
1374
2860
  },
1375
2861
  // Detect indexed array assignments: arr[i] = value
1376
2862
  AssignmentExpression: (assignPath) => {
1377
2863
  const assignNode = assignPath.node;
2864
+ // === VARIABLE REASSIGNMENT (REG-290) ===
2865
+ // Check if LHS is simple identifier (not obj.prop, not arr[i])
2866
+ // Must be checked FIRST before array/object mutation handlers
2867
+ if (assignNode.left.type === 'Identifier') {
2868
+ // Initialize collection if not exists
2869
+ if (!collections.variableReassignments) {
2870
+ collections.variableReassignments = [];
2871
+ }
2872
+ const variableReassignments = collections.variableReassignments;
2873
+ this.detectVariableReassignment(assignNode, module, variableReassignments, scopeTracker);
2874
+ }
2875
+ // === END VARIABLE REASSIGNMENT ===
1378
2876
  // Initialize collection if not exists
1379
2877
  if (!collections.arrayMutations) {
1380
2878
  collections.arrayMutations = [];
1381
2879
  }
1382
2880
  const arrayMutations = collections.arrayMutations;
1383
2881
  // Check for indexed array assignment: arr[i] = value
1384
- this.detectIndexedArrayAssignment(assignNode, module, arrayMutations);
2882
+ this.detectIndexedArrayAssignment(assignNode, module, arrayMutations, scopeTracker);
1385
2883
  // Initialize object mutations collection if not exists
1386
2884
  if (!collections.objectMutations) {
1387
2885
  collections.objectMutations = [];
@@ -1390,27 +2888,236 @@ export class JSASTAnalyzer extends Plugin {
1390
2888
  // Check for object property assignment: obj.prop = value
1391
2889
  this.detectObjectPropertyAssignment(assignNode, module, objectMutations, scopeTracker);
1392
2890
  },
1393
- ForStatement: this.createLoopScopeHandler('for', 'for-loop', parentScopeId, module, scopes, scopeCounterRef, scopeTracker),
1394
- ForInStatement: this.createLoopScopeHandler('for-in', 'for-in-loop', parentScopeId, module, scopes, scopeCounterRef, scopeTracker),
1395
- ForOfStatement: this.createLoopScopeHandler('for-of', 'for-of-loop', parentScopeId, module, scopes, scopeCounterRef, scopeTracker),
1396
- WhileStatement: this.createLoopScopeHandler('while', 'while-loop', parentScopeId, module, scopes, scopeCounterRef, scopeTracker),
1397
- DoWhileStatement: this.createLoopScopeHandler('do-while', 'do-while-loop', parentScopeId, module, scopes, scopeCounterRef, scopeTracker),
1398
- TryStatement: (tryPath) => {
1399
- this.handleTryStatement(tryPath, parentScopeId, module, scopes, variableDeclarations, literals, variableAssignments, scopeCounterRef, varDeclCounterRef, literalCounterRef, scopeTracker);
2891
+ // Handle return statements for RETURNS edges
2892
+ ReturnStatement: (returnPath) => {
2893
+ // Skip if we couldn't determine the function ID
2894
+ if (!currentFunctionId) {
2895
+ return;
2896
+ }
2897
+ // Skip if this return is inside a nested function (not the function we're analyzing)
2898
+ // Check if there's a function ancestor BETWEEN us and funcNode
2899
+ // Stop checking once we reach funcNode - parents above funcNode are outside scope
2900
+ let parent = returnPath.parentPath;
2901
+ let isInsideConditional = false;
2902
+ while (parent) {
2903
+ // If we've reached funcNode, we're done checking - this return belongs to funcNode
2904
+ if (parent.node === funcNode) {
2905
+ break;
2906
+ }
2907
+ if (t.isFunction(parent.node)) {
2908
+ // Found a function between returnPath and funcNode - this return is inside a nested function
2909
+ return;
2910
+ }
2911
+ // Track if return is inside a conditional block (if/else, switch case, loop, try/catch)
2912
+ if (t.isIfStatement(parent.node) ||
2913
+ t.isSwitchCase(parent.node) ||
2914
+ t.isLoop(parent.node) ||
2915
+ t.isTryStatement(parent.node) ||
2916
+ t.isCatchClause(parent.node)) {
2917
+ isInsideConditional = true;
2918
+ }
2919
+ parent = parent.parentPath;
2920
+ }
2921
+ // Phase 6 (REG-267): Track return count and early return detection
2922
+ controlFlowState.returnCount++;
2923
+ // A return is "early" if it's inside a conditional structure
2924
+ // (More returns after this one indicate the function doesn't always end here)
2925
+ if (isInsideConditional) {
2926
+ controlFlowState.hasEarlyReturn = true;
2927
+ }
2928
+ const returnNode = returnPath.node;
2929
+ const returnLine = getLine(returnNode);
2930
+ const returnColumn = getColumn(returnNode);
2931
+ // Handle bare return; (no value)
2932
+ if (!returnNode.argument) {
2933
+ // Skip - no data flow value
2934
+ return;
2935
+ }
2936
+ const arg = returnNode.argument;
2937
+ // Determine return value type and extract relevant info
2938
+ const returnInfo = {
2939
+ parentFunctionId: currentFunctionId,
2940
+ file: module.file,
2941
+ line: returnLine,
2942
+ column: returnColumn,
2943
+ returnValueType: 'NONE'
2944
+ };
2945
+ // Identifier (variable reference)
2946
+ if (t.isIdentifier(arg)) {
2947
+ returnInfo.returnValueType = 'VARIABLE';
2948
+ returnInfo.returnValueName = arg.name;
2949
+ }
2950
+ // TemplateLiteral must come BEFORE isLiteral (TemplateLiteral extends Literal)
2951
+ else if (t.isTemplateLiteral(arg)) {
2952
+ returnInfo.returnValueType = 'EXPRESSION';
2953
+ returnInfo.expressionType = 'TemplateLiteral';
2954
+ returnInfo.returnValueLine = getLine(arg);
2955
+ returnInfo.returnValueColumn = getColumn(arg);
2956
+ returnInfo.returnValueId = NodeFactory.generateExpressionId('TemplateLiteral', module.file, getLine(arg), getColumn(arg));
2957
+ // Extract all embedded expression identifiers
2958
+ const sourceNames = [];
2959
+ for (const expr of arg.expressions) {
2960
+ if (t.isIdentifier(expr)) {
2961
+ sourceNames.push(expr.name);
2962
+ }
2963
+ }
2964
+ if (sourceNames.length > 0) {
2965
+ returnInfo.expressionSourceNames = sourceNames;
2966
+ }
2967
+ }
2968
+ // Literal values (after TemplateLiteral check)
2969
+ else if (t.isLiteral(arg)) {
2970
+ returnInfo.returnValueType = 'LITERAL';
2971
+ // Create a LITERAL node ID for this return value
2972
+ const literalId = `LITERAL#return#${module.file}#${returnLine}:${returnColumn}:${literalCounterRef.value++}`;
2973
+ returnInfo.returnValueId = literalId;
2974
+ // Also add to literals collection for node creation
2975
+ literals.push({
2976
+ id: literalId,
2977
+ type: 'LITERAL',
2978
+ value: ExpressionEvaluator.extractLiteralValue(arg),
2979
+ valueType: typeof ExpressionEvaluator.extractLiteralValue(arg),
2980
+ file: module.file,
2981
+ line: returnLine,
2982
+ column: returnColumn
2983
+ });
2984
+ }
2985
+ // Direct function call: return foo()
2986
+ else if (t.isCallExpression(arg) && t.isIdentifier(arg.callee)) {
2987
+ returnInfo.returnValueType = 'CALL_SITE';
2988
+ returnInfo.returnValueLine = getLine(arg);
2989
+ returnInfo.returnValueColumn = getColumn(arg);
2990
+ returnInfo.returnValueCallName = arg.callee.name;
2991
+ }
2992
+ // Method call: return obj.method()
2993
+ else if (t.isCallExpression(arg) && t.isMemberExpression(arg.callee)) {
2994
+ returnInfo.returnValueType = 'METHOD_CALL';
2995
+ returnInfo.returnValueLine = getLine(arg);
2996
+ returnInfo.returnValueColumn = getColumn(arg);
2997
+ // Extract method name for lookup
2998
+ if (t.isIdentifier(arg.callee.property)) {
2999
+ returnInfo.returnValueCallName = arg.callee.property.name;
3000
+ }
3001
+ }
3002
+ // BinaryExpression: return a + b
3003
+ else if (t.isBinaryExpression(arg)) {
3004
+ returnInfo.returnValueType = 'EXPRESSION';
3005
+ returnInfo.expressionType = 'BinaryExpression';
3006
+ returnInfo.returnValueLine = getLine(arg);
3007
+ returnInfo.returnValueColumn = getColumn(arg);
3008
+ returnInfo.operator = arg.operator;
3009
+ // Generate stable ID for the EXPRESSION node
3010
+ returnInfo.returnValueId = NodeFactory.generateExpressionId('BinaryExpression', module.file, getLine(arg), getColumn(arg));
3011
+ // Extract left operand source
3012
+ if (t.isIdentifier(arg.left)) {
3013
+ returnInfo.leftSourceName = arg.left.name;
3014
+ }
3015
+ // Extract right operand source
3016
+ if (t.isIdentifier(arg.right)) {
3017
+ returnInfo.rightSourceName = arg.right.name;
3018
+ }
3019
+ }
3020
+ // LogicalExpression: return a && b, return a || b
3021
+ else if (t.isLogicalExpression(arg)) {
3022
+ returnInfo.returnValueType = 'EXPRESSION';
3023
+ returnInfo.expressionType = 'LogicalExpression';
3024
+ returnInfo.returnValueLine = getLine(arg);
3025
+ returnInfo.returnValueColumn = getColumn(arg);
3026
+ returnInfo.operator = arg.operator;
3027
+ returnInfo.returnValueId = NodeFactory.generateExpressionId('LogicalExpression', module.file, getLine(arg), getColumn(arg));
3028
+ if (t.isIdentifier(arg.left)) {
3029
+ returnInfo.leftSourceName = arg.left.name;
3030
+ }
3031
+ if (t.isIdentifier(arg.right)) {
3032
+ returnInfo.rightSourceName = arg.right.name;
3033
+ }
3034
+ }
3035
+ // ConditionalExpression: return condition ? a : b
3036
+ else if (t.isConditionalExpression(arg)) {
3037
+ returnInfo.returnValueType = 'EXPRESSION';
3038
+ returnInfo.expressionType = 'ConditionalExpression';
3039
+ returnInfo.returnValueLine = getLine(arg);
3040
+ returnInfo.returnValueColumn = getColumn(arg);
3041
+ returnInfo.returnValueId = NodeFactory.generateExpressionId('ConditionalExpression', module.file, getLine(arg), getColumn(arg));
3042
+ // Extract consequent (then branch) source
3043
+ if (t.isIdentifier(arg.consequent)) {
3044
+ returnInfo.consequentSourceName = arg.consequent.name;
3045
+ }
3046
+ // Extract alternate (else branch) source
3047
+ if (t.isIdentifier(arg.alternate)) {
3048
+ returnInfo.alternateSourceName = arg.alternate.name;
3049
+ }
3050
+ }
3051
+ // UnaryExpression: return !x, return -x
3052
+ else if (t.isUnaryExpression(arg)) {
3053
+ returnInfo.returnValueType = 'EXPRESSION';
3054
+ returnInfo.expressionType = 'UnaryExpression';
3055
+ returnInfo.returnValueLine = getLine(arg);
3056
+ returnInfo.returnValueColumn = getColumn(arg);
3057
+ returnInfo.operator = arg.operator;
3058
+ returnInfo.returnValueId = NodeFactory.generateExpressionId('UnaryExpression', module.file, getLine(arg), getColumn(arg));
3059
+ if (t.isIdentifier(arg.argument)) {
3060
+ returnInfo.unaryArgSourceName = arg.argument.name;
3061
+ }
3062
+ }
3063
+ // MemberExpression (property access): return obj.prop
3064
+ else if (t.isMemberExpression(arg)) {
3065
+ returnInfo.returnValueType = 'EXPRESSION';
3066
+ returnInfo.expressionType = 'MemberExpression';
3067
+ returnInfo.returnValueLine = getLine(arg);
3068
+ returnInfo.returnValueColumn = getColumn(arg);
3069
+ returnInfo.returnValueId = NodeFactory.generateExpressionId('MemberExpression', module.file, getLine(arg), getColumn(arg));
3070
+ // Extract object.property info
3071
+ if (t.isIdentifier(arg.object)) {
3072
+ returnInfo.object = arg.object.name;
3073
+ returnInfo.objectSourceName = arg.object.name;
3074
+ }
3075
+ if (t.isIdentifier(arg.property)) {
3076
+ returnInfo.property = arg.property.name;
3077
+ }
3078
+ returnInfo.computed = arg.computed;
3079
+ }
3080
+ // NewExpression: return new Foo()
3081
+ else if (t.isNewExpression(arg)) {
3082
+ returnInfo.returnValueType = 'EXPRESSION';
3083
+ returnInfo.expressionType = 'NewExpression';
3084
+ returnInfo.returnValueLine = getLine(arg);
3085
+ returnInfo.returnValueColumn = getColumn(arg);
3086
+ returnInfo.returnValueId = NodeFactory.generateExpressionId('NewExpression', module.file, getLine(arg), getColumn(arg));
3087
+ }
3088
+ // Fallback for other expression types
3089
+ else {
3090
+ returnInfo.returnValueType = 'EXPRESSION';
3091
+ returnInfo.expressionType = arg.type;
3092
+ returnInfo.returnValueLine = getLine(arg);
3093
+ returnInfo.returnValueColumn = getColumn(arg);
3094
+ returnInfo.returnValueId = NodeFactory.generateExpressionId(arg.type, module.file, getLine(arg), getColumn(arg));
3095
+ }
3096
+ returnStatements.push(returnInfo);
3097
+ },
3098
+ // Phase 6 (REG-267): Track throw statements for control flow metadata
3099
+ ThrowStatement: (throwPath) => {
3100
+ // Skip if this throw is inside a nested function (not the function we're analyzing)
3101
+ let parent = throwPath.parentPath;
3102
+ while (parent) {
3103
+ if (t.isFunction(parent.node) && parent.node !== funcNode) {
3104
+ // This throw is inside a nested function - skip it
3105
+ return;
3106
+ }
3107
+ parent = parent.parentPath;
3108
+ }
3109
+ controlFlowState.hasThrow = true;
1400
3110
  },
3111
+ ForStatement: this.createLoopScopeHandler('for', 'for-loop', 'for', parentScopeId, module, scopes, loops, scopeCounterRef, loopCounterRef, scopeTracker, scopeIdStack, controlFlowState),
3112
+ ForInStatement: this.createLoopScopeHandler('for-in', 'for-in-loop', 'for-in', parentScopeId, module, scopes, loops, scopeCounterRef, loopCounterRef, scopeTracker, scopeIdStack, controlFlowState),
3113
+ ForOfStatement: this.createLoopScopeHandler('for-of', 'for-of-loop', 'for-of', parentScopeId, module, scopes, loops, scopeCounterRef, loopCounterRef, scopeTracker, scopeIdStack, controlFlowState),
3114
+ WhileStatement: this.createLoopScopeHandler('while', 'while-loop', 'while', parentScopeId, module, scopes, loops, scopeCounterRef, loopCounterRef, scopeTracker, scopeIdStack, controlFlowState),
3115
+ DoWhileStatement: this.createLoopScopeHandler('do-while', 'do-while-loop', 'do-while', parentScopeId, module, scopes, loops, scopeCounterRef, loopCounterRef, scopeTracker, scopeIdStack, controlFlowState),
3116
+ // Phase 4 (REG-267): Now creates TRY_BLOCK, CATCH_BLOCK, FINALLY_BLOCK nodes
3117
+ TryStatement: this.createTryStatementHandler(parentScopeId, module, scopes, tryBlocks, catchBlocks, finallyBlocks, scopeCounterRef, tryBlockCounterRef, catchBlockCounterRef, finallyBlockCounterRef, scopeTracker, tryScopeMap, scopeIdStack, controlFlowState),
3118
+ CatchClause: this.createCatchClauseHandler(module, variableDeclarations, varDeclCounterRef, scopeTracker, tryScopeMap, scopeIdStack),
1401
3119
  SwitchStatement: (switchPath) => {
1402
- const switchNode = switchPath.node;
1403
- const scopeId = `SCOPE#switch-case#${module.file}#${getLine(switchNode)}:${scopeCounterRef.value++}`;
1404
- const semanticId = this.generateSemanticId('switch-case', scopeTracker);
1405
- scopes.push({
1406
- id: scopeId,
1407
- type: 'SCOPE',
1408
- scopeType: 'switch-case',
1409
- semanticId,
1410
- file: module.file,
1411
- line: getLine(switchNode),
1412
- parentScopeId
1413
- });
3120
+ this.handleSwitchStatement(switchPath, parentScopeId, module, collections, scopeTracker, controlFlowState);
1414
3121
  },
1415
3122
  FunctionExpression: (funcPath) => {
1416
3123
  const node = funcPath.node;
@@ -1509,10 +3216,144 @@ export class JSASTAnalyzer extends Plugin {
1509
3216
  scopeTracker.exitScope();
1510
3217
  }
1511
3218
  }
3219
+ else {
3220
+ // Arrow function with expression body (implicit return)
3221
+ // e.g., x => x * 2, () => 42
3222
+ const bodyExpr = node.body;
3223
+ const bodyLine = getLine(bodyExpr);
3224
+ const bodyColumn = getColumn(bodyExpr);
3225
+ const returnInfo = {
3226
+ parentFunctionId: functionId,
3227
+ file: module.file,
3228
+ line: bodyLine,
3229
+ column: bodyColumn,
3230
+ returnValueType: 'NONE',
3231
+ isImplicitReturn: true
3232
+ };
3233
+ // Apply same type detection logic as ReturnStatement handler
3234
+ if (t.isIdentifier(bodyExpr)) {
3235
+ returnInfo.returnValueType = 'VARIABLE';
3236
+ returnInfo.returnValueName = bodyExpr.name;
3237
+ }
3238
+ // TemplateLiteral must come BEFORE isLiteral (TemplateLiteral extends Literal)
3239
+ else if (t.isTemplateLiteral(bodyExpr)) {
3240
+ returnInfo.returnValueType = 'EXPRESSION';
3241
+ returnInfo.expressionType = 'TemplateLiteral';
3242
+ returnInfo.returnValueLine = getLine(bodyExpr);
3243
+ returnInfo.returnValueColumn = getColumn(bodyExpr);
3244
+ returnInfo.returnValueId = NodeFactory.generateExpressionId('TemplateLiteral', module.file, getLine(bodyExpr), getColumn(bodyExpr));
3245
+ const sourceNames = [];
3246
+ for (const expr of bodyExpr.expressions) {
3247
+ if (t.isIdentifier(expr))
3248
+ sourceNames.push(expr.name);
3249
+ }
3250
+ if (sourceNames.length > 0)
3251
+ returnInfo.expressionSourceNames = sourceNames;
3252
+ }
3253
+ else if (t.isLiteral(bodyExpr)) {
3254
+ returnInfo.returnValueType = 'LITERAL';
3255
+ const literalId = `LITERAL#implicit_return#${module.file}#${line}:${column}:${literalCounterRef.value++}`;
3256
+ returnInfo.returnValueId = literalId;
3257
+ literals.push({
3258
+ id: literalId,
3259
+ type: 'LITERAL',
3260
+ value: ExpressionEvaluator.extractLiteralValue(bodyExpr),
3261
+ valueType: typeof ExpressionEvaluator.extractLiteralValue(bodyExpr),
3262
+ file: module.file,
3263
+ line: bodyLine,
3264
+ column: bodyColumn
3265
+ });
3266
+ }
3267
+ else if (t.isCallExpression(bodyExpr) && t.isIdentifier(bodyExpr.callee)) {
3268
+ returnInfo.returnValueType = 'CALL_SITE';
3269
+ returnInfo.returnValueLine = getLine(bodyExpr);
3270
+ returnInfo.returnValueColumn = getColumn(bodyExpr);
3271
+ returnInfo.returnValueCallName = bodyExpr.callee.name;
3272
+ }
3273
+ else if (t.isCallExpression(bodyExpr) && t.isMemberExpression(bodyExpr.callee)) {
3274
+ returnInfo.returnValueType = 'METHOD_CALL';
3275
+ returnInfo.returnValueLine = getLine(bodyExpr);
3276
+ returnInfo.returnValueColumn = getColumn(bodyExpr);
3277
+ if (t.isIdentifier(bodyExpr.callee.property)) {
3278
+ returnInfo.returnValueCallName = bodyExpr.callee.property.name;
3279
+ }
3280
+ }
3281
+ // REG-276: Detailed EXPRESSION handling for nested implicit arrow returns
3282
+ else if (t.isBinaryExpression(bodyExpr)) {
3283
+ returnInfo.returnValueType = 'EXPRESSION';
3284
+ returnInfo.expressionType = 'BinaryExpression';
3285
+ returnInfo.returnValueLine = getLine(bodyExpr);
3286
+ returnInfo.returnValueColumn = getColumn(bodyExpr);
3287
+ returnInfo.operator = bodyExpr.operator;
3288
+ returnInfo.returnValueId = NodeFactory.generateExpressionId('BinaryExpression', module.file, getLine(bodyExpr), getColumn(bodyExpr));
3289
+ if (t.isIdentifier(bodyExpr.left))
3290
+ returnInfo.leftSourceName = bodyExpr.left.name;
3291
+ if (t.isIdentifier(bodyExpr.right))
3292
+ returnInfo.rightSourceName = bodyExpr.right.name;
3293
+ }
3294
+ else if (t.isLogicalExpression(bodyExpr)) {
3295
+ returnInfo.returnValueType = 'EXPRESSION';
3296
+ returnInfo.expressionType = 'LogicalExpression';
3297
+ returnInfo.returnValueLine = getLine(bodyExpr);
3298
+ returnInfo.returnValueColumn = getColumn(bodyExpr);
3299
+ returnInfo.operator = bodyExpr.operator;
3300
+ returnInfo.returnValueId = NodeFactory.generateExpressionId('LogicalExpression', module.file, getLine(bodyExpr), getColumn(bodyExpr));
3301
+ if (t.isIdentifier(bodyExpr.left))
3302
+ returnInfo.leftSourceName = bodyExpr.left.name;
3303
+ if (t.isIdentifier(bodyExpr.right))
3304
+ returnInfo.rightSourceName = bodyExpr.right.name;
3305
+ }
3306
+ else if (t.isConditionalExpression(bodyExpr)) {
3307
+ returnInfo.returnValueType = 'EXPRESSION';
3308
+ returnInfo.expressionType = 'ConditionalExpression';
3309
+ returnInfo.returnValueLine = getLine(bodyExpr);
3310
+ returnInfo.returnValueColumn = getColumn(bodyExpr);
3311
+ returnInfo.returnValueId = NodeFactory.generateExpressionId('ConditionalExpression', module.file, getLine(bodyExpr), getColumn(bodyExpr));
3312
+ if (t.isIdentifier(bodyExpr.consequent))
3313
+ returnInfo.consequentSourceName = bodyExpr.consequent.name;
3314
+ if (t.isIdentifier(bodyExpr.alternate))
3315
+ returnInfo.alternateSourceName = bodyExpr.alternate.name;
3316
+ }
3317
+ else if (t.isUnaryExpression(bodyExpr)) {
3318
+ returnInfo.returnValueType = 'EXPRESSION';
3319
+ returnInfo.expressionType = 'UnaryExpression';
3320
+ returnInfo.returnValueLine = getLine(bodyExpr);
3321
+ returnInfo.returnValueColumn = getColumn(bodyExpr);
3322
+ returnInfo.operator = bodyExpr.operator;
3323
+ returnInfo.returnValueId = NodeFactory.generateExpressionId('UnaryExpression', module.file, getLine(bodyExpr), getColumn(bodyExpr));
3324
+ if (t.isIdentifier(bodyExpr.argument))
3325
+ returnInfo.unaryArgSourceName = bodyExpr.argument.name;
3326
+ }
3327
+ else if (t.isMemberExpression(bodyExpr)) {
3328
+ returnInfo.returnValueType = 'EXPRESSION';
3329
+ returnInfo.expressionType = 'MemberExpression';
3330
+ returnInfo.returnValueLine = getLine(bodyExpr);
3331
+ returnInfo.returnValueColumn = getColumn(bodyExpr);
3332
+ returnInfo.returnValueId = NodeFactory.generateExpressionId('MemberExpression', module.file, getLine(bodyExpr), getColumn(bodyExpr));
3333
+ if (t.isIdentifier(bodyExpr.object)) {
3334
+ returnInfo.object = bodyExpr.object.name;
3335
+ returnInfo.objectSourceName = bodyExpr.object.name;
3336
+ }
3337
+ if (t.isIdentifier(bodyExpr.property))
3338
+ returnInfo.property = bodyExpr.property.name;
3339
+ returnInfo.computed = bodyExpr.computed;
3340
+ }
3341
+ else {
3342
+ returnInfo.returnValueType = 'EXPRESSION';
3343
+ returnInfo.expressionType = bodyExpr.type;
3344
+ returnInfo.returnValueLine = getLine(bodyExpr);
3345
+ returnInfo.returnValueColumn = getColumn(bodyExpr);
3346
+ returnInfo.returnValueId = NodeFactory.generateExpressionId(bodyExpr.type, module.file, getLine(bodyExpr), getColumn(bodyExpr));
3347
+ }
3348
+ returnStatements.push(returnInfo);
3349
+ }
1512
3350
  arrowPath.skip();
1513
3351
  },
1514
3352
  UpdateExpression: (updatePath) => {
1515
3353
  const updateNode = updatePath.node;
3354
+ // REG-288/REG-312: Collect update expression info for graph building
3355
+ this.collectUpdateExpression(updateNode, module, updateExpressions, getCurrentScopeId(), scopeTracker);
3356
+ // Legacy behavior: update scope.modifies for IDENTIFIER targets
1516
3357
  if (updateNode.argument.type === 'Identifier') {
1517
3358
  const varName = updateNode.argument.name;
1518
3359
  // Find variable by name - could be from parent scope or declarations
@@ -1534,19 +3375,171 @@ export class JSASTAnalyzer extends Plugin {
1534
3375
  }
1535
3376
  },
1536
3377
  // IF statements - создаём условные scope и обходим содержимое для CALL узлов
1537
- IfStatement: this.createIfStatementHandler(parentScopeId, module, scopes, ifScopeCounterRef, scopeTracker, collections.code ?? '', ifElseScopeMap),
3378
+ // Phase 3 (REG-267): Now creates BRANCH nodes with branchType='if'
3379
+ IfStatement: this.createIfStatementHandler(parentScopeId, module, scopes, branches, ifScopeCounterRef, branchCounterRef, scopeTracker, collections.code ?? '', ifElseScopeMap, scopeIdStack, controlFlowState, this.countLogicalOperators.bind(this)),
3380
+ // Ternary expressions (REG-287): Creates BRANCH nodes with branchType='ternary'
3381
+ ConditionalExpression: this.createConditionalExpressionHandler(parentScopeId, module, branches, branchCounterRef, scopeTracker, scopeIdStack, controlFlowState, this.countLogicalOperators.bind(this)),
1538
3382
  // Track when we enter the alternate (else) block of an IfStatement
1539
- BlockStatement: this.createIfElseBlockStatementHandler(scopeTracker, ifElseScopeMap),
3383
+ BlockStatement: this.createBlockStatementHandler(scopeTracker, ifElseScopeMap, tryScopeMap, scopeIdStack),
1540
3384
  // Function call expressions
1541
3385
  CallExpression: (callPath) => {
1542
- this.handleCallExpression(callPath.node, processedCallSites, processedMethodCalls, callSites, methodCalls, module, callSiteCounterRef, scopeTracker, parentScopeId, collections);
3386
+ this.handleCallExpression(callPath.node, processedCallSites, processedMethodCalls, callSites, methodCalls, module, callSiteCounterRef, scopeTracker, getCurrentScopeId(), collections);
3387
+ // REG-334: Check for resolve/reject calls inside Promise executors
3388
+ const callNode = callPath.node;
3389
+ if (t.isIdentifier(callNode.callee)) {
3390
+ const calleeName = callNode.callee.name;
3391
+ // Walk up function parents to find Promise executor context
3392
+ // This handles nested callbacks like: new Promise((resolve) => { db.query((err, data) => { resolve(data); }); });
3393
+ let funcParent = callPath.getFunctionParent();
3394
+ while (funcParent) {
3395
+ const funcNode = funcParent.node;
3396
+ const funcKey = `${funcNode.start}:${funcNode.end}`;
3397
+ const context = promiseExecutorContexts.get(funcKey);
3398
+ if (context) {
3399
+ const isResolve = calleeName === context.resolveName;
3400
+ const isReject = calleeName === context.rejectName;
3401
+ if (isResolve || isReject) {
3402
+ // Find the CALL node ID for this resolve/reject call
3403
+ // It was just added by handleCallExpression
3404
+ const callLine = getLine(callNode);
3405
+ const callColumn = getColumn(callNode);
3406
+ // Find matching call site that was just added
3407
+ const resolveCall = callSites.find(cs => cs.name === calleeName &&
3408
+ cs.file === module.file &&
3409
+ cs.line === callLine &&
3410
+ cs.column === callColumn);
3411
+ if (resolveCall) {
3412
+ promiseResolutions.push({
3413
+ callId: resolveCall.id,
3414
+ constructorCallId: context.constructorCallId,
3415
+ isReject,
3416
+ file: module.file,
3417
+ line: callLine
3418
+ });
3419
+ // REG-334: Collect arguments for resolve/reject calls
3420
+ // This enables traceValues to follow PASSES_ARGUMENT edges
3421
+ if (!collections.callArguments) {
3422
+ collections.callArguments = [];
3423
+ }
3424
+ const callArgumentsArr = collections.callArguments;
3425
+ // Process arguments (typically just one: resolve(value))
3426
+ callNode.arguments.forEach((arg, argIndex) => {
3427
+ const argInfo = {
3428
+ callId: resolveCall.id,
3429
+ argIndex,
3430
+ file: module.file,
3431
+ line: getLine(arg),
3432
+ column: getColumn(arg)
3433
+ };
3434
+ // Handle different argument types
3435
+ if (t.isIdentifier(arg)) {
3436
+ argInfo.targetType = 'VARIABLE';
3437
+ argInfo.targetName = arg.name;
3438
+ }
3439
+ else if (t.isLiteral(arg) && !t.isTemplateLiteral(arg)) {
3440
+ // Create LITERAL node for the argument value
3441
+ const literalValue = ExpressionEvaluator.extractLiteralValue(arg);
3442
+ if (literalValue !== null) {
3443
+ const argLine = getLine(arg);
3444
+ const argColumn = getColumn(arg);
3445
+ const literalId = `LITERAL#arg${argIndex}#${module.file}#${argLine}:${argColumn}:${literalCounterRef.value++}`;
3446
+ literals.push({
3447
+ id: literalId,
3448
+ type: 'LITERAL',
3449
+ value: literalValue,
3450
+ valueType: typeof literalValue,
3451
+ file: module.file,
3452
+ line: argLine,
3453
+ column: argColumn,
3454
+ parentCallId: resolveCall.id,
3455
+ argIndex
3456
+ });
3457
+ argInfo.targetType = 'LITERAL';
3458
+ argInfo.targetId = literalId;
3459
+ argInfo.literalValue = literalValue;
3460
+ }
3461
+ }
3462
+ else if (t.isCallExpression(arg)) {
3463
+ argInfo.targetType = 'CALL';
3464
+ argInfo.nestedCallLine = getLine(arg);
3465
+ argInfo.nestedCallColumn = getColumn(arg);
3466
+ }
3467
+ else {
3468
+ argInfo.targetType = 'EXPRESSION';
3469
+ argInfo.expressionType = arg.type;
3470
+ }
3471
+ callArgumentsArr.push(argInfo);
3472
+ });
3473
+ }
3474
+ break; // Found context, stop searching
3475
+ }
3476
+ }
3477
+ funcParent = funcParent.getFunctionParent();
3478
+ }
3479
+ }
1543
3480
  },
1544
3481
  // NewExpression (constructor calls)
1545
3482
  NewExpression: (newPath) => {
1546
3483
  const newNode = newPath.node;
3484
+ const nodeKey = `new:${newNode.start}:${newNode.end}`;
3485
+ // Determine className from callee
3486
+ let className = null;
3487
+ if (newNode.callee.type === 'Identifier') {
3488
+ className = newNode.callee.name;
3489
+ }
3490
+ else if (newNode.callee.type === 'MemberExpression' && newNode.callee.property.type === 'Identifier') {
3491
+ className = newNode.callee.property.name;
3492
+ }
3493
+ // Create CONSTRUCTOR_CALL node (always, for all NewExpressions)
3494
+ if (className) {
3495
+ const constructorKey = `constructor:${nodeKey}`;
3496
+ if (!processedCallSites.has(constructorKey)) {
3497
+ processedCallSites.add(constructorKey);
3498
+ const line = getLine(newNode);
3499
+ const column = getColumn(newNode);
3500
+ const constructorCallId = ConstructorCallNode.generateId(className, module.file, line, column);
3501
+ const isBuiltin = ConstructorCallNode.isBuiltinConstructor(className);
3502
+ constructorCalls.push({
3503
+ id: constructorCallId,
3504
+ type: 'CONSTRUCTOR_CALL',
3505
+ className,
3506
+ isBuiltin,
3507
+ file: module.file,
3508
+ line,
3509
+ column
3510
+ });
3511
+ // REG-334: If this is Promise constructor with executor callback,
3512
+ // register the context for resolve/reject detection
3513
+ if (className === 'Promise' && newNode.arguments.length > 0) {
3514
+ const executorArg = newNode.arguments[0];
3515
+ // Only handle inline function expressions (not variable references)
3516
+ if (t.isArrowFunctionExpression(executorArg) || t.isFunctionExpression(executorArg)) {
3517
+ // Extract resolve/reject parameter names
3518
+ let resolveName;
3519
+ let rejectName;
3520
+ if (executorArg.params.length > 0 && t.isIdentifier(executorArg.params[0])) {
3521
+ resolveName = executorArg.params[0].name;
3522
+ }
3523
+ if (executorArg.params.length > 1 && t.isIdentifier(executorArg.params[1])) {
3524
+ rejectName = executorArg.params[1].name;
3525
+ }
3526
+ if (resolveName) {
3527
+ // Key by function node position to allow nested Promise detection
3528
+ const funcKey = `${executorArg.start}:${executorArg.end}`;
3529
+ promiseExecutorContexts.set(funcKey, {
3530
+ constructorCallId,
3531
+ resolveName,
3532
+ rejectName,
3533
+ file: module.file,
3534
+ line
3535
+ });
3536
+ }
3537
+ }
3538
+ }
3539
+ }
3540
+ }
1547
3541
  // Handle simple constructor: new Foo()
1548
3542
  if (newNode.callee.type === 'Identifier') {
1549
- const nodeKey = `new:${newNode.start}:${newNode.end}`;
1550
3543
  if (processedCallSites.has(nodeKey)) {
1551
3544
  return;
1552
3545
  }
@@ -1565,7 +3558,7 @@ export class JSASTAnalyzer extends Plugin {
1565
3558
  name: constructorName,
1566
3559
  file: module.file,
1567
3560
  line: getLine(newNode),
1568
- parentScopeId,
3561
+ parentScopeId: getCurrentScopeId(),
1569
3562
  targetFunctionName: constructorName,
1570
3563
  isNew: true
1571
3564
  });
@@ -1576,7 +3569,6 @@ export class JSASTAnalyzer extends Plugin {
1576
3569
  const object = memberCallee.object;
1577
3570
  const property = memberCallee.property;
1578
3571
  if (object.type === 'Identifier' && property.type === 'Identifier') {
1579
- const nodeKey = `new:${newNode.start}:${newNode.end}`;
1580
3572
  if (processedMethodCalls.has(nodeKey)) {
1581
3573
  return;
1582
3574
  }
@@ -1600,13 +3592,29 @@ export class JSASTAnalyzer extends Plugin {
1600
3592
  file: module.file,
1601
3593
  line: getLine(newNode),
1602
3594
  column: getColumn(newNode),
1603
- parentScopeId,
3595
+ parentScopeId: getCurrentScopeId(),
1604
3596
  isNew: true
1605
3597
  });
1606
3598
  }
1607
3599
  }
1608
3600
  }
1609
3601
  });
3602
+ // Phase 6 (REG-267): Attach control flow metadata to the function node
3603
+ if (matchingFunction) {
3604
+ const cyclomaticComplexity = 1 +
3605
+ controlFlowState.branchCount +
3606
+ controlFlowState.loopCount +
3607
+ controlFlowState.caseCount +
3608
+ controlFlowState.logicalOpCount;
3609
+ matchingFunction.controlFlow = {
3610
+ hasBranches: controlFlowState.branchCount > 0,
3611
+ hasLoops: controlFlowState.loopCount > 0,
3612
+ hasTryCatch: controlFlowState.hasTryCatch,
3613
+ hasEarlyReturn: controlFlowState.hasEarlyReturn,
3614
+ hasThrow: controlFlowState.hasThrow,
3615
+ cyclomaticComplexity
3616
+ };
3617
+ }
1610
3618
  }
1611
3619
  /**
1612
3620
  * Handle CallExpression nodes: direct function calls (greet(), main())
@@ -1651,6 +3659,7 @@ export class JSASTAnalyzer extends Plugin {
1651
3659
  name: calleeName,
1652
3660
  file: module.file,
1653
3661
  line: getLine(callNode),
3662
+ column: getColumn(callNode), // REG-223: Add column for coordinate-based lookup
1654
3663
  parentScopeId,
1655
3664
  targetFunctionName: calleeName
1656
3665
  });
@@ -1828,7 +3837,7 @@ export class JSASTAnalyzer extends Plugin {
1828
3837
  * @param module - Current module being analyzed
1829
3838
  * @param arrayMutations - Collection to push mutation info into
1830
3839
  */
1831
- detectIndexedArrayAssignment(assignNode, module, arrayMutations) {
3840
+ detectIndexedArrayAssignment(assignNode, module, arrayMutations, scopeTracker) {
1832
3841
  // Check for indexed array assignment: arr[i] = value
1833
3842
  if (assignNode.left.type === 'MemberExpression' && assignNode.left.computed) {
1834
3843
  const memberExpr = assignNode.left;
@@ -1872,8 +3881,11 @@ export class JSASTAnalyzer extends Plugin {
1872
3881
  // Use defensive loc checks instead of ! assertions
1873
3882
  const line = assignNode.loc?.start.line ?? 0;
1874
3883
  const column = assignNode.loc?.start.column ?? 0;
3884
+ // Capture scope path for scope-aware lookup (REG-309)
3885
+ const scopePath = scopeTracker?.getContext().scopePath ?? [];
1875
3886
  arrayMutations.push({
1876
3887
  arrayName,
3888
+ mutationScopePath: scopePath,
1877
3889
  mutationMethod: 'indexed',
1878
3890
  file: module.file,
1879
3891
  line: line,
@@ -1956,6 +3968,8 @@ export class JSASTAnalyzer extends Plugin {
1956
3968
  // Use defensive loc checks
1957
3969
  const line = assignNode.loc?.start.line ?? 0;
1958
3970
  const column = assignNode.loc?.start.column ?? 0;
3971
+ // Capture scope path for scope-aware lookup (REG-309)
3972
+ const scopePath = scopeTracker?.getContext().scopePath ?? [];
1959
3973
  // Generate semantic ID if scopeTracker available
1960
3974
  let mutationId;
1961
3975
  if (scopeTracker) {
@@ -1965,6 +3979,7 @@ export class JSASTAnalyzer extends Plugin {
1965
3979
  objectMutations.push({
1966
3980
  id: mutationId,
1967
3981
  objectName,
3982
+ mutationScopePath: scopePath,
1968
3983
  enclosingClassName, // REG-152: Class name for 'this' mutations
1969
3984
  propertyName,
1970
3985
  mutationType,
@@ -1975,6 +3990,218 @@ export class JSASTAnalyzer extends Plugin {
1975
3990
  value: valueInfo
1976
3991
  });
1977
3992
  }
3993
+ /**
3994
+ * Collect update expression info for graph building (i++, obj.prop++, arr[i]++).
3995
+ *
3996
+ * REG-288: Simple identifiers (i++, --count)
3997
+ * REG-312: Member expressions (obj.prop++, arr[i]++, this.count++)
3998
+ *
3999
+ * Creates UpdateExpressionInfo entries that GraphBuilder uses to create:
4000
+ * - UPDATE_EXPRESSION nodes
4001
+ * - MODIFIES edges to target variables/objects
4002
+ * - READS_FROM self-loops
4003
+ * - CONTAINS edges for scope hierarchy
4004
+ */
4005
+ collectUpdateExpression(updateNode, module, updateExpressions, parentScopeId, scopeTracker) {
4006
+ const operator = updateNode.operator;
4007
+ const prefix = updateNode.prefix;
4008
+ const line = getLine(updateNode);
4009
+ const column = getColumn(updateNode);
4010
+ // CASE 1: Simple identifier (i++, --count) - REG-288 behavior
4011
+ if (updateNode.argument.type === 'Identifier') {
4012
+ const variableName = updateNode.argument.name;
4013
+ updateExpressions.push({
4014
+ targetType: 'IDENTIFIER',
4015
+ variableName,
4016
+ variableLine: getLine(updateNode.argument),
4017
+ operator,
4018
+ prefix,
4019
+ file: module.file,
4020
+ line,
4021
+ column,
4022
+ parentScopeId
4023
+ });
4024
+ return;
4025
+ }
4026
+ // CASE 2: Member expression (obj.prop++, arr[i]++) - REG-312 new
4027
+ if (updateNode.argument.type === 'MemberExpression') {
4028
+ const memberExpr = updateNode.argument;
4029
+ // Extract object name (reuses detectObjectPropertyAssignment pattern)
4030
+ let objectName;
4031
+ let enclosingClassName;
4032
+ if (memberExpr.object.type === 'Identifier') {
4033
+ objectName = memberExpr.object.name;
4034
+ }
4035
+ else if (memberExpr.object.type === 'ThisExpression') {
4036
+ objectName = 'this';
4037
+ // REG-152: Extract enclosing class name from scope context
4038
+ if (scopeTracker) {
4039
+ enclosingClassName = scopeTracker.getEnclosingScope('CLASS');
4040
+ }
4041
+ }
4042
+ else {
4043
+ // Complex expressions: obj.nested.prop++, (obj || fallback).count++
4044
+ // Skip for now (documented limitation, same as detectObjectPropertyAssignment)
4045
+ return;
4046
+ }
4047
+ // Extract property name (reuses detectObjectPropertyAssignment pattern)
4048
+ let propertyName;
4049
+ let mutationType;
4050
+ let computedPropertyVar;
4051
+ if (!memberExpr.computed) {
4052
+ // obj.prop++
4053
+ if (memberExpr.property.type === 'Identifier') {
4054
+ propertyName = memberExpr.property.name;
4055
+ mutationType = 'property';
4056
+ }
4057
+ else {
4058
+ return; // Unexpected property type
4059
+ }
4060
+ }
4061
+ else {
4062
+ // obj['prop']++ or obj[key]++
4063
+ if (memberExpr.property.type === 'StringLiteral') {
4064
+ // obj['prop']++ - static string
4065
+ propertyName = memberExpr.property.value;
4066
+ mutationType = 'property';
4067
+ }
4068
+ else {
4069
+ // obj[key]++, arr[i]++ - computed property
4070
+ propertyName = '<computed>';
4071
+ mutationType = 'computed';
4072
+ if (memberExpr.property.type === 'Identifier') {
4073
+ computedPropertyVar = memberExpr.property.name;
4074
+ }
4075
+ }
4076
+ }
4077
+ updateExpressions.push({
4078
+ targetType: 'MEMBER_EXPRESSION',
4079
+ objectName,
4080
+ objectLine: getLine(memberExpr.object),
4081
+ enclosingClassName,
4082
+ propertyName,
4083
+ mutationType,
4084
+ computedPropertyVar,
4085
+ operator,
4086
+ prefix,
4087
+ file: module.file,
4088
+ line,
4089
+ column,
4090
+ parentScopeId
4091
+ });
4092
+ }
4093
+ }
4094
+ /**
4095
+ * Detect variable reassignment for FLOWS_INTO edge creation.
4096
+ * Handles all assignment operators: =, +=, -=, *=, /=, etc.
4097
+ *
4098
+ * Captures COMPLETE metadata for:
4099
+ * - LITERAL values (literalValue field)
4100
+ * - EXPRESSION nodes (expressionType, expressionMetadata fields)
4101
+ * - VARIABLE, CALL_SITE, METHOD_CALL references
4102
+ *
4103
+ * REG-290: No deferred functionality - all value types captured.
4104
+ */
4105
+ detectVariableReassignment(assignNode, module, variableReassignments, scopeTracker) {
4106
+ // LHS must be simple identifier (checked by caller)
4107
+ const leftId = assignNode.left;
4108
+ const variableName = leftId.name;
4109
+ const operator = assignNode.operator; // '=', '+=', '-=', etc.
4110
+ // Get RHS value info
4111
+ const rightExpr = assignNode.right;
4112
+ const line = getLine(assignNode);
4113
+ const column = getColumn(assignNode);
4114
+ // Extract value source (similar to VariableVisitor pattern)
4115
+ let valueType;
4116
+ let valueName;
4117
+ let valueId = null;
4118
+ let callLine;
4119
+ let callColumn;
4120
+ // Complete metadata for node creation
4121
+ let literalValue;
4122
+ let expressionType;
4123
+ let expressionMetadata;
4124
+ // 1. Literal value
4125
+ const extractedLiteralValue = ExpressionEvaluator.extractLiteralValue(rightExpr);
4126
+ if (extractedLiteralValue !== null) {
4127
+ valueType = 'LITERAL';
4128
+ valueId = `LITERAL#${line}:${rightExpr.start}#${module.file}`;
4129
+ literalValue = extractedLiteralValue; // Store for GraphBuilder
4130
+ }
4131
+ // 2. Simple identifier (variable reference)
4132
+ else if (rightExpr.type === 'Identifier') {
4133
+ valueType = 'VARIABLE';
4134
+ valueName = rightExpr.name;
4135
+ }
4136
+ // 3. CallExpression (function call)
4137
+ else if (rightExpr.type === 'CallExpression' && rightExpr.callee.type === 'Identifier') {
4138
+ valueType = 'CALL_SITE';
4139
+ valueName = rightExpr.callee.name;
4140
+ callLine = getLine(rightExpr);
4141
+ callColumn = getColumn(rightExpr);
4142
+ }
4143
+ // 4. MemberExpression (method call: obj.method())
4144
+ else if (rightExpr.type === 'CallExpression' && rightExpr.callee.type === 'MemberExpression') {
4145
+ valueType = 'METHOD_CALL';
4146
+ callLine = getLine(rightExpr);
4147
+ callColumn = getColumn(rightExpr);
4148
+ }
4149
+ // 5. Everything else is EXPRESSION
4150
+ else {
4151
+ valueType = 'EXPRESSION';
4152
+ expressionType = rightExpr.type; // Store AST node type
4153
+ // Use correct EXPRESSION ID format: {file}:EXPRESSION:{type}:{line}:{column}
4154
+ valueId = `${module.file}:EXPRESSION:${expressionType}:${line}:${column}`;
4155
+ // Extract type-specific metadata (matches VariableAssignmentInfo pattern)
4156
+ expressionMetadata = {};
4157
+ // MemberExpression: obj.prop or obj[key]
4158
+ if (rightExpr.type === 'MemberExpression') {
4159
+ const objName = rightExpr.object.type === 'Identifier' ? rightExpr.object.name : undefined;
4160
+ const propName = rightExpr.property.type === 'Identifier' ? rightExpr.property.name : undefined;
4161
+ const computed = rightExpr.computed;
4162
+ expressionMetadata.object = objName;
4163
+ expressionMetadata.property = propName;
4164
+ expressionMetadata.computed = computed;
4165
+ // Computed property variable: obj[varName]
4166
+ if (computed && rightExpr.property.type === 'Identifier') {
4167
+ expressionMetadata.computedPropertyVar = rightExpr.property.name;
4168
+ }
4169
+ }
4170
+ // BinaryExpression: a + b, a - b, etc.
4171
+ else if (rightExpr.type === 'BinaryExpression' || rightExpr.type === 'LogicalExpression') {
4172
+ expressionMetadata.operator = rightExpr.operator;
4173
+ expressionMetadata.leftSourceName = rightExpr.left.type === 'Identifier' ? rightExpr.left.name : undefined;
4174
+ expressionMetadata.rightSourceName = rightExpr.right.type === 'Identifier' ? rightExpr.right.name : undefined;
4175
+ }
4176
+ // ConditionalExpression: condition ? a : b
4177
+ else if (rightExpr.type === 'ConditionalExpression') {
4178
+ expressionMetadata.consequentSourceName = rightExpr.consequent.type === 'Identifier' ? rightExpr.consequent.name : undefined;
4179
+ expressionMetadata.alternateSourceName = rightExpr.alternate.type === 'Identifier' ? rightExpr.alternate.name : undefined;
4180
+ }
4181
+ // Add more expression types as needed
4182
+ }
4183
+ // Capture scope path for scope-aware lookup (REG-309)
4184
+ const scopePath = scopeTracker?.getContext().scopePath ?? [];
4185
+ // Push reassignment info to collection
4186
+ variableReassignments.push({
4187
+ variableName,
4188
+ variableLine: getLine(leftId),
4189
+ mutationScopePath: scopePath,
4190
+ valueType,
4191
+ valueName,
4192
+ valueId,
4193
+ callLine,
4194
+ callColumn,
4195
+ operator,
4196
+ // Complete metadata
4197
+ literalValue,
4198
+ expressionType,
4199
+ expressionMetadata,
4200
+ file: module.file,
4201
+ line,
4202
+ column
4203
+ });
4204
+ }
1978
4205
  /**
1979
4206
  * Extract value information from an expression for mutation tracking
1980
4207
  */