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