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