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