@grafema/core 0.1.0-alpha.1
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/LICENSE +190 -0
- package/README.md +76 -0
- package/dist/Orchestrator.d.ts +142 -0
- package/dist/Orchestrator.d.ts.map +1 -0
- package/dist/Orchestrator.js +481 -0
- package/dist/api/GraphAPI.d.ts +87 -0
- package/dist/api/GraphAPI.d.ts.map +1 -0
- package/dist/api/GraphAPI.js +210 -0
- package/dist/api/GuaranteeAPI.d.ts +147 -0
- package/dist/api/GuaranteeAPI.d.ts.map +1 -0
- package/dist/api/GuaranteeAPI.js +288 -0
- package/dist/core/ASTWorker.d.ts +133 -0
- package/dist/core/ASTWorker.d.ts.map +1 -0
- package/dist/core/ASTWorker.js +352 -0
- package/dist/core/ASTWorkerPool.d.ts +85 -0
- package/dist/core/ASTWorkerPool.d.ts.map +1 -0
- package/dist/core/ASTWorkerPool.js +207 -0
- package/dist/core/AnalysisQueue.d.ts +104 -0
- package/dist/core/AnalysisQueue.d.ts.map +1 -0
- package/dist/core/AnalysisQueue.js +299 -0
- package/dist/core/AnalysisWorker.d.ts +14 -0
- package/dist/core/AnalysisWorker.d.ts.map +1 -0
- package/dist/core/AnalysisWorker.js +307 -0
- package/dist/core/GraphBackend.d.ts +156 -0
- package/dist/core/GraphBackend.d.ts.map +1 -0
- package/dist/core/GraphBackend.js +85 -0
- package/dist/core/GuaranteeManager.d.ts +230 -0
- package/dist/core/GuaranteeManager.d.ts.map +1 -0
- package/dist/core/GuaranteeManager.js +352 -0
- package/dist/core/ManifestStore.d.ts +71 -0
- package/dist/core/ManifestStore.d.ts.map +1 -0
- package/dist/core/ManifestStore.js +146 -0
- package/dist/core/NodeFactory.d.ts +160 -0
- package/dist/core/NodeFactory.d.ts.map +1 -0
- package/dist/core/NodeFactory.js +137 -0
- package/dist/core/NodeId.d.ts +88 -0
- package/dist/core/NodeId.d.ts.map +1 -0
- package/dist/core/NodeId.js +170 -0
- package/dist/core/ParallelAnalyzer.d.ts +120 -0
- package/dist/core/ParallelAnalyzer.d.ts.map +1 -0
- package/dist/core/ParallelAnalyzer.js +331 -0
- package/dist/core/PriorityQueue.d.ts +106 -0
- package/dist/core/PriorityQueue.d.ts.map +1 -0
- package/dist/core/PriorityQueue.js +168 -0
- package/dist/core/Profiler.d.ts +75 -0
- package/dist/core/Profiler.d.ts.map +1 -0
- package/dist/core/Profiler.js +149 -0
- package/dist/core/QueueWorker.d.ts +12 -0
- package/dist/core/QueueWorker.d.ts.map +1 -0
- package/dist/core/QueueWorker.js +567 -0
- package/dist/core/RFDBClient.d.ts +179 -0
- package/dist/core/RFDBClient.d.ts.map +1 -0
- package/dist/core/RFDBClient.js +429 -0
- package/dist/core/Task.d.ts +56 -0
- package/dist/core/Task.d.ts.map +1 -0
- package/dist/core/Task.js +85 -0
- package/dist/core/TaskTypes.d.ts +20 -0
- package/dist/core/TaskTypes.d.ts.map +1 -0
- package/dist/core/TaskTypes.js +10 -0
- package/dist/core/VersionManager.d.ts +166 -0
- package/dist/core/VersionManager.d.ts.map +1 -0
- package/dist/core/VersionManager.js +237 -0
- package/dist/core/WorkerPool.d.ts +82 -0
- package/dist/core/WorkerPool.d.ts.map +1 -0
- package/dist/core/WorkerPool.js +109 -0
- package/dist/core/nodes/CallSiteNode.d.ts +26 -0
- package/dist/core/nodes/CallSiteNode.d.ts.map +1 -0
- package/dist/core/nodes/CallSiteNode.js +44 -0
- package/dist/core/nodes/ClassNode.d.ts +25 -0
- package/dist/core/nodes/ClassNode.d.ts.map +1 -0
- package/dist/core/nodes/ClassNode.js +40 -0
- package/dist/core/nodes/ConstantNode.d.ts +24 -0
- package/dist/core/nodes/ConstantNode.d.ts.map +1 -0
- package/dist/core/nodes/ConstantNode.js +39 -0
- package/dist/core/nodes/DatabaseQueryNode.d.ts +22 -0
- package/dist/core/nodes/DatabaseQueryNode.d.ts.map +1 -0
- package/dist/core/nodes/DatabaseQueryNode.js +37 -0
- package/dist/core/nodes/EntrypointNode.d.ts +102 -0
- package/dist/core/nodes/EntrypointNode.d.ts.map +1 -0
- package/dist/core/nodes/EntrypointNode.js +119 -0
- package/dist/core/nodes/EventListenerNode.d.ts +25 -0
- package/dist/core/nodes/EventListenerNode.d.ts.map +1 -0
- package/dist/core/nodes/EventListenerNode.js +39 -0
- package/dist/core/nodes/ExportNode.d.ts +26 -0
- package/dist/core/nodes/ExportNode.d.ts.map +1 -0
- package/dist/core/nodes/ExportNode.js +40 -0
- package/dist/core/nodes/ExternalStdioNode.d.ts +17 -0
- package/dist/core/nodes/ExternalStdioNode.d.ts.map +1 -0
- package/dist/core/nodes/ExternalStdioNode.js +26 -0
- package/dist/core/nodes/FunctionNode.d.ts +27 -0
- package/dist/core/nodes/FunctionNode.d.ts.map +1 -0
- package/dist/core/nodes/FunctionNode.js +53 -0
- package/dist/core/nodes/GuaranteeNode.d.ts +76 -0
- package/dist/core/nodes/GuaranteeNode.d.ts.map +1 -0
- package/dist/core/nodes/GuaranteeNode.js +117 -0
- package/dist/core/nodes/HttpRequestNode.d.ts +24 -0
- package/dist/core/nodes/HttpRequestNode.d.ts.map +1 -0
- package/dist/core/nodes/HttpRequestNode.js +38 -0
- package/dist/core/nodes/ImportNode.d.ts +27 -0
- package/dist/core/nodes/ImportNode.d.ts.map +1 -0
- package/dist/core/nodes/ImportNode.js +43 -0
- package/dist/core/nodes/LiteralNode.d.ts +26 -0
- package/dist/core/nodes/LiteralNode.d.ts.map +1 -0
- package/dist/core/nodes/LiteralNode.js +40 -0
- package/dist/core/nodes/MethodCallNode.d.ts +29 -0
- package/dist/core/nodes/MethodCallNode.d.ts.map +1 -0
- package/dist/core/nodes/MethodCallNode.js +47 -0
- package/dist/core/nodes/MethodNode.d.ts +29 -0
- package/dist/core/nodes/MethodNode.d.ts.map +1 -0
- package/dist/core/nodes/MethodNode.js +44 -0
- package/dist/core/nodes/ModuleNode.d.ts +29 -0
- package/dist/core/nodes/ModuleNode.d.ts.map +1 -0
- package/dist/core/nodes/ModuleNode.js +49 -0
- package/dist/core/nodes/NodeKind.d.ts +91 -0
- package/dist/core/nodes/NodeKind.d.ts.map +1 -0
- package/dist/core/nodes/NodeKind.js +146 -0
- package/dist/core/nodes/ParameterNode.d.ts +26 -0
- package/dist/core/nodes/ParameterNode.d.ts.map +1 -0
- package/dist/core/nodes/ParameterNode.js +43 -0
- package/dist/core/nodes/ScopeNode.d.ts +32 -0
- package/dist/core/nodes/ScopeNode.d.ts.map +1 -0
- package/dist/core/nodes/ScopeNode.js +47 -0
- package/dist/core/nodes/ServiceNode.d.ts +44 -0
- package/dist/core/nodes/ServiceNode.d.ts.map +1 -0
- package/dist/core/nodes/ServiceNode.js +49 -0
- package/dist/core/nodes/VariableDeclarationNode.d.ts +22 -0
- package/dist/core/nodes/VariableDeclarationNode.d.ts.map +1 -0
- package/dist/core/nodes/VariableDeclarationNode.js +38 -0
- package/dist/core/nodes/index.d.ts +25 -0
- package/dist/core/nodes/index.d.ts.map +1 -0
- package/dist/core/nodes/index.js +30 -0
- package/dist/index.d.ts +57 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +63 -0
- package/dist/plugins/Plugin.d.ts +44 -0
- package/dist/plugins/Plugin.d.ts.map +1 -0
- package/dist/plugins/Plugin.js +46 -0
- package/dist/plugins/analysis/DatabaseAnalyzer.d.ts +23 -0
- package/dist/plugins/analysis/DatabaseAnalyzer.d.ts.map +1 -0
- package/dist/plugins/analysis/DatabaseAnalyzer.js +260 -0
- package/dist/plugins/analysis/ExpressAnalyzer.d.ts +19 -0
- package/dist/plugins/analysis/ExpressAnalyzer.d.ts.map +1 -0
- package/dist/plugins/analysis/ExpressAnalyzer.js +306 -0
- package/dist/plugins/analysis/ExpressRouteAnalyzer.d.ts +17 -0
- package/dist/plugins/analysis/ExpressRouteAnalyzer.d.ts.map +1 -0
- package/dist/plugins/analysis/ExpressRouteAnalyzer.js +308 -0
- package/dist/plugins/analysis/FetchAnalyzer.d.ts +38 -0
- package/dist/plugins/analysis/FetchAnalyzer.d.ts.map +1 -0
- package/dist/plugins/analysis/FetchAnalyzer.js +344 -0
- package/dist/plugins/analysis/IncrementalAnalysisPlugin.d.ts +65 -0
- package/dist/plugins/analysis/IncrementalAnalysisPlugin.d.ts.map +1 -0
- package/dist/plugins/analysis/IncrementalAnalysisPlugin.js +472 -0
- package/dist/plugins/analysis/JSASTAnalyzer.d.ts +84 -0
- package/dist/plugins/analysis/JSASTAnalyzer.d.ts.map +1 -0
- package/dist/plugins/analysis/JSASTAnalyzer.js +1378 -0
- package/dist/plugins/analysis/ReactAnalyzer.d.ts +90 -0
- package/dist/plugins/analysis/ReactAnalyzer.d.ts.map +1 -0
- package/dist/plugins/analysis/ReactAnalyzer.js +1153 -0
- package/dist/plugins/analysis/RustAnalyzer.d.ts +13 -0
- package/dist/plugins/analysis/RustAnalyzer.d.ts.map +1 -0
- package/dist/plugins/analysis/RustAnalyzer.js +259 -0
- package/dist/plugins/analysis/SQLiteAnalyzer.d.ts +21 -0
- package/dist/plugins/analysis/SQLiteAnalyzer.d.ts.map +1 -0
- package/dist/plugins/analysis/SQLiteAnalyzer.js +317 -0
- package/dist/plugins/analysis/ServiceLayerAnalyzer.d.ts +35 -0
- package/dist/plugins/analysis/ServiceLayerAnalyzer.d.ts.map +1 -0
- package/dist/plugins/analysis/ServiceLayerAnalyzer.js +303 -0
- package/dist/plugins/analysis/SocketIOAnalyzer.d.ts +33 -0
- package/dist/plugins/analysis/SocketIOAnalyzer.d.ts.map +1 -0
- package/dist/plugins/analysis/SocketIOAnalyzer.js +283 -0
- package/dist/plugins/analysis/SystemDbAnalyzer.d.ts +27 -0
- package/dist/plugins/analysis/SystemDbAnalyzer.d.ts.map +1 -0
- package/dist/plugins/analysis/SystemDbAnalyzer.js +211 -0
- package/dist/plugins/analysis/ast/ConditionParser.d.ts +85 -0
- package/dist/plugins/analysis/ast/ConditionParser.d.ts.map +1 -0
- package/dist/plugins/analysis/ast/ConditionParser.js +277 -0
- package/dist/plugins/analysis/ast/ExpressionEvaluator.d.ts +15 -0
- package/dist/plugins/analysis/ast/ExpressionEvaluator.d.ts.map +1 -0
- package/dist/plugins/analysis/ast/ExpressionEvaluator.js +91 -0
- package/dist/plugins/analysis/ast/GraphBuilder.d.ts +77 -0
- package/dist/plugins/analysis/ast/GraphBuilder.d.ts.map +1 -0
- package/dist/plugins/analysis/ast/GraphBuilder.js +1077 -0
- package/dist/plugins/analysis/ast/OxcAdapter.d.ts +41 -0
- package/dist/plugins/analysis/ast/OxcAdapter.d.ts.map +1 -0
- package/dist/plugins/analysis/ast/OxcAdapter.js +40 -0
- package/dist/plugins/analysis/ast/types.d.ts +346 -0
- package/dist/plugins/analysis/ast/types.d.ts.map +1 -0
- package/dist/plugins/analysis/ast/types.js +4 -0
- package/dist/plugins/analysis/ast/visitors/ASTVisitor.d.ts +93 -0
- package/dist/plugins/analysis/ast/visitors/ASTVisitor.d.ts.map +1 -0
- package/dist/plugins/analysis/ast/visitors/ASTVisitor.js +24 -0
- package/dist/plugins/analysis/ast/visitors/CallExpressionVisitor.d.ts +77 -0
- package/dist/plugins/analysis/ast/visitors/CallExpressionVisitor.d.ts.map +1 -0
- package/dist/plugins/analysis/ast/visitors/CallExpressionVisitor.js +377 -0
- package/dist/plugins/analysis/ast/visitors/ClassVisitor.d.ts +27 -0
- package/dist/plugins/analysis/ast/visitors/ClassVisitor.d.ts.map +1 -0
- package/dist/plugins/analysis/ast/visitors/ClassVisitor.js +232 -0
- package/dist/plugins/analysis/ast/visitors/FunctionVisitor.d.ts +25 -0
- package/dist/plugins/analysis/ast/visitors/FunctionVisitor.d.ts.map +1 -0
- package/dist/plugins/analysis/ast/visitors/FunctionVisitor.js +172 -0
- package/dist/plugins/analysis/ast/visitors/ImportExportVisitor.d.ts +29 -0
- package/dist/plugins/analysis/ast/visitors/ImportExportVisitor.d.ts.map +1 -0
- package/dist/plugins/analysis/ast/visitors/ImportExportVisitor.js +180 -0
- package/dist/plugins/analysis/ast/visitors/TypeScriptVisitor.d.ts +14 -0
- package/dist/plugins/analysis/ast/visitors/TypeScriptVisitor.d.ts.map +1 -0
- package/dist/plugins/analysis/ast/visitors/TypeScriptVisitor.js +200 -0
- package/dist/plugins/analysis/ast/visitors/VariableVisitor.d.ts +45 -0
- package/dist/plugins/analysis/ast/visitors/VariableVisitor.d.ts.map +1 -0
- package/dist/plugins/analysis/ast/visitors/VariableVisitor.js +150 -0
- package/dist/plugins/analysis/ast/visitors/index.d.ts +17 -0
- package/dist/plugins/analysis/ast/visitors/index.d.ts.map +1 -0
- package/dist/plugins/analysis/ast/visitors/index.js +13 -0
- package/dist/plugins/discovery/DiscoveryPlugin.d.ts +34 -0
- package/dist/plugins/discovery/DiscoveryPlugin.d.ts.map +1 -0
- package/dist/plugins/discovery/DiscoveryPlugin.js +26 -0
- package/dist/plugins/discovery/MonorepoServiceDiscovery.d.ts +26 -0
- package/dist/plugins/discovery/MonorepoServiceDiscovery.d.ts.map +1 -0
- package/dist/plugins/discovery/MonorepoServiceDiscovery.js +79 -0
- package/dist/plugins/discovery/SimpleProjectDiscovery.d.ts +14 -0
- package/dist/plugins/discovery/SimpleProjectDiscovery.d.ts.map +1 -0
- package/dist/plugins/discovery/SimpleProjectDiscovery.js +65 -0
- package/dist/plugins/discovery/ZonServiceDiscovery.d.ts +19 -0
- package/dist/plugins/discovery/ZonServiceDiscovery.d.ts.map +1 -0
- package/dist/plugins/discovery/ZonServiceDiscovery.js +204 -0
- package/dist/plugins/enrichment/AliasTracker.d.ts +40 -0
- package/dist/plugins/enrichment/AliasTracker.d.ts.map +1 -0
- package/dist/plugins/enrichment/AliasTracker.js +290 -0
- package/dist/plugins/enrichment/HTTPConnectionEnricher.d.ts +30 -0
- package/dist/plugins/enrichment/HTTPConnectionEnricher.d.ts.map +1 -0
- package/dist/plugins/enrichment/HTTPConnectionEnricher.js +135 -0
- package/dist/plugins/enrichment/ImportExportLinker.d.ts +30 -0
- package/dist/plugins/enrichment/ImportExportLinker.d.ts.map +1 -0
- package/dist/plugins/enrichment/ImportExportLinker.js +176 -0
- package/dist/plugins/enrichment/InstanceOfResolver.d.ts +21 -0
- package/dist/plugins/enrichment/InstanceOfResolver.d.ts.map +1 -0
- package/dist/plugins/enrichment/InstanceOfResolver.js +117 -0
- package/dist/plugins/enrichment/MethodCallResolver.d.ts +41 -0
- package/dist/plugins/enrichment/MethodCallResolver.d.ts.map +1 -0
- package/dist/plugins/enrichment/MethodCallResolver.js +252 -0
- package/dist/plugins/enrichment/MountPointResolver.d.ts +26 -0
- package/dist/plugins/enrichment/MountPointResolver.d.ts.map +1 -0
- package/dist/plugins/enrichment/MountPointResolver.js +189 -0
- package/dist/plugins/enrichment/PrefixEvaluator.d.ts +89 -0
- package/dist/plugins/enrichment/PrefixEvaluator.d.ts.map +1 -0
- package/dist/plugins/enrichment/PrefixEvaluator.js +415 -0
- package/dist/plugins/enrichment/RustFFIEnricher.d.ts +25 -0
- package/dist/plugins/enrichment/RustFFIEnricher.d.ts.map +1 -0
- package/dist/plugins/enrichment/RustFFIEnricher.js +170 -0
- package/dist/plugins/enrichment/ValueDomainAnalyzer.d.ts +114 -0
- package/dist/plugins/enrichment/ValueDomainAnalyzer.d.ts.map +1 -0
- package/dist/plugins/enrichment/ValueDomainAnalyzer.js +464 -0
- package/dist/plugins/indexing/IncrementalModuleIndexer.d.ts +27 -0
- package/dist/plugins/indexing/IncrementalModuleIndexer.d.ts.map +1 -0
- package/dist/plugins/indexing/IncrementalModuleIndexer.js +238 -0
- package/dist/plugins/indexing/JSModuleIndexer.d.ts +33 -0
- package/dist/plugins/indexing/JSModuleIndexer.d.ts.map +1 -0
- package/dist/plugins/indexing/JSModuleIndexer.js +299 -0
- package/dist/plugins/indexing/RustModuleIndexer.d.ts +28 -0
- package/dist/plugins/indexing/RustModuleIndexer.d.ts.map +1 -0
- package/dist/plugins/indexing/RustModuleIndexer.js +140 -0
- package/dist/plugins/indexing/ServiceDetector.d.ts +46 -0
- package/dist/plugins/indexing/ServiceDetector.d.ts.map +1 -0
- package/dist/plugins/indexing/ServiceDetector.js +164 -0
- package/dist/plugins/validation/CallResolverValidator.d.ts +23 -0
- package/dist/plugins/validation/CallResolverValidator.d.ts.map +1 -0
- package/dist/plugins/validation/CallResolverValidator.js +108 -0
- package/dist/plugins/validation/DataFlowValidator.d.ts +24 -0
- package/dist/plugins/validation/DataFlowValidator.d.ts.map +1 -0
- package/dist/plugins/validation/DataFlowValidator.js +148 -0
- package/dist/plugins/validation/EvalBanValidator.d.ts +25 -0
- package/dist/plugins/validation/EvalBanValidator.d.ts.map +1 -0
- package/dist/plugins/validation/EvalBanValidator.js +123 -0
- package/dist/plugins/validation/GraphConnectivityValidator.d.ts +11 -0
- package/dist/plugins/validation/GraphConnectivityValidator.d.ts.map +1 -0
- package/dist/plugins/validation/GraphConnectivityValidator.js +135 -0
- package/dist/plugins/validation/SQLInjectionValidator.d.ts +43 -0
- package/dist/plugins/validation/SQLInjectionValidator.d.ts.map +1 -0
- package/dist/plugins/validation/SQLInjectionValidator.js +251 -0
- package/dist/plugins/validation/ShadowingDetector.d.ts +26 -0
- package/dist/plugins/validation/ShadowingDetector.d.ts.map +1 -0
- package/dist/plugins/validation/ShadowingDetector.js +119 -0
- package/dist/plugins/validation/TypeScriptDeadCodeValidator.d.ts +21 -0
- package/dist/plugins/validation/TypeScriptDeadCodeValidator.d.ts.map +1 -0
- package/dist/plugins/validation/TypeScriptDeadCodeValidator.js +151 -0
- package/dist/plugins/vcs/GitPlugin.d.ts +84 -0
- package/dist/plugins/vcs/GitPlugin.d.ts.map +1 -0
- package/dist/plugins/vcs/GitPlugin.js +295 -0
- package/dist/plugins/vcs/VCSPlugin.d.ts +133 -0
- package/dist/plugins/vcs/VCSPlugin.d.ts.map +1 -0
- package/dist/plugins/vcs/VCSPlugin.js +82 -0
- package/dist/plugins/vcs/index.d.ts +10 -0
- package/dist/plugins/vcs/index.d.ts.map +1 -0
- package/dist/plugins/vcs/index.js +18 -0
- package/dist/storage/backends/RFDBServerBackend.d.ts +258 -0
- package/dist/storage/backends/RFDBServerBackend.d.ts.map +1 -0
- package/dist/storage/backends/RFDBServerBackend.js +565 -0
- package/dist/storage/backends/typeValidation.d.ts +47 -0
- package/dist/storage/backends/typeValidation.d.ts.map +1 -0
- package/dist/storage/backends/typeValidation.js +137 -0
- package/dist/validation/PathValidator.d.ts +81 -0
- package/dist/validation/PathValidator.d.ts.map +1 -0
- package/dist/validation/PathValidator.js +251 -0
- package/package.json +57 -0
- package/src/.rfguard/current-session.txt +1 -0
- package/src/Orchestrator.ts +673 -0
- package/src/api/GraphAPI.ts +305 -0
- package/src/api/GuaranteeAPI.ts +401 -0
- package/src/core/ASTWorker.ts +567 -0
- package/src/core/ASTWorkerPool.ts +299 -0
- package/src/core/AnalysisQueue.ts +447 -0
- package/src/core/AnalysisWorker.ts +410 -0
- package/src/core/GraphBackend.ts +265 -0
- package/src/core/GuaranteeManager.ts +581 -0
- package/src/core/ManifestStore.ts +196 -0
- package/src/core/NodeFactory.ts +274 -0
- package/src/core/NodeId.ts +257 -0
- package/src/core/ParallelAnalyzer.ts +476 -0
- package/src/core/PriorityQueue.ts +227 -0
- package/src/core/Profiler.ts +188 -0
- package/src/core/QueueWorker.ts +780 -0
- package/src/core/Task.ts +107 -0
- package/src/core/TaskTypes.ts +40 -0
- package/src/core/VersionManager.ts +404 -0
- package/src/core/WorkerPool.ts +180 -0
- package/src/core/nodes/CallSiteNode.ts +72 -0
- package/src/core/nodes/ClassNode.ts +69 -0
- package/src/core/nodes/ConstantNode.ts +63 -0
- package/src/core/nodes/DatabaseQueryNode.ts +60 -0
- package/src/core/nodes/EntrypointNode.ts +164 -0
- package/src/core/nodes/EventListenerNode.ts +64 -0
- package/src/core/nodes/ExportNode.ts +71 -0
- package/src/core/nodes/ExternalStdioNode.ts +36 -0
- package/src/core/nodes/FunctionNode.ts +78 -0
- package/src/core/nodes/GuaranteeNode.ts +162 -0
- package/src/core/nodes/HttpRequestNode.ts +63 -0
- package/src/core/nodes/ImportNode.ts +75 -0
- package/src/core/nodes/LiteralNode.ts +67 -0
- package/src/core/nodes/MethodCallNode.ts +79 -0
- package/src/core/nodes/MethodNode.ts +78 -0
- package/src/core/nodes/ModuleNode.ts +74 -0
- package/src/core/nodes/NodeKind.ts +171 -0
- package/src/core/nodes/ParameterNode.ts +73 -0
- package/src/core/nodes/ScopeNode.ts +80 -0
- package/src/core/nodes/ServiceNode.ts +86 -0
- package/src/core/nodes/VariableDeclarationNode.ts +60 -0
- package/src/core/nodes/index.ts +49 -0
- package/src/index.ts +93 -0
- package/src/plugins/Plugin.ts +74 -0
- package/src/plugins/analysis/DatabaseAnalyzer.ts +322 -0
- package/src/plugins/analysis/ExpressAnalyzer.ts +401 -0
- package/src/plugins/analysis/ExpressRouteAnalyzer.ts +414 -0
- package/src/plugins/analysis/FetchAnalyzer.ts +441 -0
- package/src/plugins/analysis/IncrementalAnalysisPlugin.ts +686 -0
- package/src/plugins/analysis/JSASTAnalyzer.ts +1680 -0
- package/src/plugins/analysis/ReactAnalyzer.ts +1368 -0
- package/src/plugins/analysis/RustAnalyzer.ts +438 -0
- package/src/plugins/analysis/SQLiteAnalyzer.ts +388 -0
- package/src/plugins/analysis/ServiceLayerAnalyzer.ts +429 -0
- package/src/plugins/analysis/SocketIOAnalyzer.ts +395 -0
- package/src/plugins/analysis/SystemDbAnalyzer.ts +284 -0
- package/src/plugins/analysis/ast/ConditionParser.ts +333 -0
- package/src/plugins/analysis/ast/ExpressionEvaluator.ts +117 -0
- package/src/plugins/analysis/ast/GraphBuilder.ts +1371 -0
- package/src/plugins/analysis/ast/OxcAdapter.ts +63 -0
- package/src/plugins/analysis/ast/types.ts +400 -0
- package/src/plugins/analysis/ast/visitors/ASTVisitor.ts +137 -0
- package/src/plugins/analysis/ast/visitors/CallExpressionVisitor.ts +528 -0
- package/src/plugins/analysis/ast/visitors/ClassVisitor.ts +339 -0
- package/src/plugins/analysis/ast/visitors/FunctionVisitor.ts +273 -0
- package/src/plugins/analysis/ast/visitors/ImportExportVisitor.ts +259 -0
- package/src/plugins/analysis/ast/visitors/TypeScriptVisitor.ts +235 -0
- package/src/plugins/analysis/ast/visitors/VariableVisitor.ts +268 -0
- package/src/plugins/analysis/ast/visitors/index.ts +36 -0
- package/src/plugins/discovery/DiscoveryPlugin.ts +50 -0
- package/src/plugins/discovery/MonorepoServiceDiscovery.ts +117 -0
- package/src/plugins/discovery/SimpleProjectDiscovery.ts +102 -0
- package/src/plugins/enrichment/AliasTracker.ts +399 -0
- package/src/plugins/enrichment/HTTPConnectionEnricher.ts +192 -0
- package/src/plugins/enrichment/ImportExportLinker.ts +221 -0
- package/src/plugins/enrichment/InstanceOfResolver.ts +165 -0
- package/src/plugins/enrichment/MethodCallResolver.ts +333 -0
- package/src/plugins/enrichment/MountPointResolver.ts +264 -0
- package/src/plugins/enrichment/PrefixEvaluator.ts +527 -0
- package/src/plugins/enrichment/RustFFIEnricher.ts +218 -0
- package/src/plugins/enrichment/ValueDomainAnalyzer.ts +682 -0
- package/src/plugins/indexing/IncrementalModuleIndexer.ts +287 -0
- package/src/plugins/indexing/JSModuleIndexer.ts +374 -0
- package/src/plugins/indexing/RustModuleIndexer.ts +160 -0
- package/src/plugins/indexing/ServiceDetector.ts +230 -0
- package/src/plugins/validation/CallResolverValidator.ts +170 -0
- package/src/plugins/validation/DataFlowValidator.ts +233 -0
- package/src/plugins/validation/EvalBanValidator.ts +175 -0
- package/src/plugins/validation/GraphConnectivityValidator.ts +201 -0
- package/src/plugins/validation/SQLInjectionValidator.ts +363 -0
- package/src/plugins/validation/ShadowingDetector.ts +173 -0
- package/src/plugins/validation/TypeScriptDeadCodeValidator.ts +203 -0
- package/src/plugins/vcs/GitPlugin.ts +344 -0
- package/src/plugins/vcs/VCSPlugin.ts +190 -0
- package/src/plugins/vcs/index.ts +32 -0
- package/src/storage/backends/RFDBServerBackend.ts +687 -0
- package/src/storage/backends/typeValidation.ts +151 -0
- package/src/validation/PathValidator.ts +342 -0
|
@@ -0,0 +1,1368 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ReactAnalyzer - React/Browser domain-specific analysis
|
|
3
|
+
*
|
|
4
|
+
* Detects React patterns:
|
|
5
|
+
* - Components and rendering tree
|
|
6
|
+
* - Props flow between components
|
|
7
|
+
* - Event handlers (onClick, onSubmit, etc.)
|
|
8
|
+
* - Hooks (useState, useEffect, useCallback, useMemo, useRef, useReducer, useContext)
|
|
9
|
+
* - Browser APIs (localStorage, timers, DOM, observers)
|
|
10
|
+
* - Edge cases (stale closures, missing cleanup, RAF bugs)
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { readFileSync } from 'fs';
|
|
14
|
+
import { parse, ParserPlugin } from '@babel/parser';
|
|
15
|
+
import traverseModule from '@babel/traverse';
|
|
16
|
+
import type { NodePath } from '@babel/traverse';
|
|
17
|
+
import type { Node, CallExpression, JSXElement, JSXAttribute, VariableDeclarator, FunctionDeclaration } from '@babel/types';
|
|
18
|
+
import { Plugin, createSuccessResult, createErrorResult } from '../Plugin.js';
|
|
19
|
+
import type { PluginContext, PluginResult, PluginMetadata } from '../Plugin.js';
|
|
20
|
+
import type { NodeRecord } from '@grafema/types';
|
|
21
|
+
|
|
22
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
23
|
+
const traverse = (traverseModule as any).default || traverseModule;
|
|
24
|
+
|
|
25
|
+
// React event handlers mapping
|
|
26
|
+
const REACT_EVENTS: Record<string, string> = {
|
|
27
|
+
// Mouse events
|
|
28
|
+
onClick: 'click', onDoubleClick: 'dblclick', onContextMenu: 'contextmenu',
|
|
29
|
+
onMouseDown: 'mousedown', onMouseUp: 'mouseup', onMouseEnter: 'mouseenter',
|
|
30
|
+
onMouseLeave: 'mouseleave', onMouseMove: 'mousemove', onMouseOver: 'mouseover',
|
|
31
|
+
onMouseOut: 'mouseout',
|
|
32
|
+
// Keyboard events
|
|
33
|
+
onKeyDown: 'keydown', onKeyUp: 'keyup', onKeyPress: 'keypress',
|
|
34
|
+
// Focus events
|
|
35
|
+
onFocus: 'focus', onBlur: 'blur', onFocusCapture: 'focus:capture',
|
|
36
|
+
// Form events
|
|
37
|
+
onSubmit: 'submit', onReset: 'reset', onChange: 'change', onInput: 'input',
|
|
38
|
+
onInvalid: 'invalid',
|
|
39
|
+
// Touch events
|
|
40
|
+
onTouchStart: 'touchstart', onTouchMove: 'touchmove', onTouchEnd: 'touchend',
|
|
41
|
+
onTouchCancel: 'touchcancel',
|
|
42
|
+
// Drag events
|
|
43
|
+
onDragStart: 'dragstart', onDrag: 'drag', onDragEnd: 'dragend',
|
|
44
|
+
onDragEnter: 'dragenter', onDragOver: 'dragover', onDragLeave: 'dragleave',
|
|
45
|
+
onDrop: 'drop',
|
|
46
|
+
// Scroll/Wheel events
|
|
47
|
+
onScroll: 'scroll', onWheel: 'wheel',
|
|
48
|
+
// Clipboard events
|
|
49
|
+
onCopy: 'copy', onCut: 'cut', onPaste: 'paste',
|
|
50
|
+
// Composition events
|
|
51
|
+
onCompositionStart: 'compositionstart', onCompositionUpdate: 'compositionupdate',
|
|
52
|
+
onCompositionEnd: 'compositionend',
|
|
53
|
+
// Media events
|
|
54
|
+
onPlay: 'play', onPause: 'pause', onEnded: 'ended', onTimeUpdate: 'timeupdate',
|
|
55
|
+
onLoadedData: 'loadeddata', onLoadedMetadata: 'loadedmetadata',
|
|
56
|
+
onCanPlay: 'canplay', onWaiting: 'waiting', onSeeking: 'seeking',
|
|
57
|
+
onSeeked: 'seeked', onError: 'error', onVolumeChange: 'volumechange',
|
|
58
|
+
// Image events
|
|
59
|
+
onLoad: 'load',
|
|
60
|
+
// Animation events
|
|
61
|
+
onAnimationStart: 'animationstart', onAnimationEnd: 'animationend',
|
|
62
|
+
onAnimationIteration: 'animationiteration',
|
|
63
|
+
// Transition events
|
|
64
|
+
onTransitionEnd: 'transitionend',
|
|
65
|
+
// Pointer events
|
|
66
|
+
onPointerDown: 'pointerdown', onPointerUp: 'pointerup', onPointerMove: 'pointermove',
|
|
67
|
+
onPointerEnter: 'pointerenter', onPointerLeave: 'pointerleave',
|
|
68
|
+
onPointerCancel: 'pointercancel', onGotPointerCapture: 'gotpointercapture',
|
|
69
|
+
onLostPointerCapture: 'lostpointercapture'
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
// React hooks that need tracking
|
|
73
|
+
const REACT_HOOKS = [
|
|
74
|
+
'useState', 'useEffect', 'useLayoutEffect', 'useInsertionEffect',
|
|
75
|
+
'useCallback', 'useMemo', 'useRef', 'useReducer', 'useContext',
|
|
76
|
+
'useImperativeHandle', 'useDebugValue', 'useDeferredValue',
|
|
77
|
+
'useTransition', 'useId', 'useSyncExternalStore'
|
|
78
|
+
];
|
|
79
|
+
|
|
80
|
+
// Browser APIs that create side effects
|
|
81
|
+
const BROWSER_APIS = {
|
|
82
|
+
timers: ['setTimeout', 'setInterval', 'requestAnimationFrame', 'requestIdleCallback'],
|
|
83
|
+
cleanup: {
|
|
84
|
+
setTimeout: 'clearTimeout',
|
|
85
|
+
setInterval: 'clearInterval',
|
|
86
|
+
requestAnimationFrame: 'cancelAnimationFrame',
|
|
87
|
+
requestIdleCallback: 'cancelIdleCallback'
|
|
88
|
+
} as Record<string, string>,
|
|
89
|
+
observers: ['IntersectionObserver', 'ResizeObserver', 'MutationObserver', 'PerformanceObserver'],
|
|
90
|
+
storage: ['localStorage', 'sessionStorage'],
|
|
91
|
+
async: ['fetch', 'XMLHttpRequest', 'WebSocket', 'EventSource'],
|
|
92
|
+
dom: ['document', 'getElementById', 'querySelector', 'querySelectorAll'],
|
|
93
|
+
workers: ['Worker', 'SharedWorker', 'ServiceWorker'],
|
|
94
|
+
geolocation: ['grafemagator.geolocation'],
|
|
95
|
+
notifications: ['Notification'],
|
|
96
|
+
fullscreen: ['requestFullscreen', 'exitFullscreen'],
|
|
97
|
+
clipboard: ['grafemagator.clipboard'],
|
|
98
|
+
history: ['history.pushState', 'history.replaceState'],
|
|
99
|
+
blocking: ['alert', 'confirm', 'prompt']
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Component node
|
|
104
|
+
*/
|
|
105
|
+
interface ComponentNode {
|
|
106
|
+
id: string;
|
|
107
|
+
type: 'react:component';
|
|
108
|
+
name: string;
|
|
109
|
+
file: string;
|
|
110
|
+
line: number;
|
|
111
|
+
column: number;
|
|
112
|
+
kind: 'arrow' | 'function' | 'forwardRef';
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Hook node
|
|
117
|
+
*/
|
|
118
|
+
interface HookNode {
|
|
119
|
+
id: string;
|
|
120
|
+
type: string;
|
|
121
|
+
file: string;
|
|
122
|
+
line: number;
|
|
123
|
+
column: number;
|
|
124
|
+
hookName: string;
|
|
125
|
+
[key: string]: unknown;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Event node
|
|
130
|
+
*/
|
|
131
|
+
interface EventNode {
|
|
132
|
+
id: string;
|
|
133
|
+
type: 'dom:event';
|
|
134
|
+
eventType: string;
|
|
135
|
+
reactProp: string;
|
|
136
|
+
handler: string;
|
|
137
|
+
file: string;
|
|
138
|
+
line: number;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Browser API node
|
|
143
|
+
*/
|
|
144
|
+
interface BrowserAPINode {
|
|
145
|
+
id: string;
|
|
146
|
+
type: string;
|
|
147
|
+
file: string;
|
|
148
|
+
line: number;
|
|
149
|
+
[key: string]: unknown;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Issue node
|
|
154
|
+
*/
|
|
155
|
+
interface IssueNode {
|
|
156
|
+
id: string;
|
|
157
|
+
type: string;
|
|
158
|
+
file: string;
|
|
159
|
+
line: number;
|
|
160
|
+
[key: string]: unknown;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Edge info
|
|
165
|
+
*/
|
|
166
|
+
interface EdgeInfo {
|
|
167
|
+
edgeType: string;
|
|
168
|
+
src: string;
|
|
169
|
+
dst: string;
|
|
170
|
+
file: string;
|
|
171
|
+
line: number;
|
|
172
|
+
[key: string]: unknown;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Analysis result
|
|
177
|
+
*/
|
|
178
|
+
interface AnalysisResult {
|
|
179
|
+
components: ComponentNode[];
|
|
180
|
+
hooks: HookNode[];
|
|
181
|
+
events: EventNode[];
|
|
182
|
+
browserAPIs: BrowserAPINode[];
|
|
183
|
+
issues: IssueNode[];
|
|
184
|
+
edges: EdgeInfo[];
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Analysis stats
|
|
189
|
+
*/
|
|
190
|
+
interface AnalysisStats {
|
|
191
|
+
components: number;
|
|
192
|
+
hooks: number;
|
|
193
|
+
events: number;
|
|
194
|
+
browserAPIs: number;
|
|
195
|
+
issues: number;
|
|
196
|
+
edges: number;
|
|
197
|
+
[key: string]: unknown;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export class ReactAnalyzer extends Plugin {
|
|
201
|
+
get metadata(): PluginMetadata {
|
|
202
|
+
return {
|
|
203
|
+
name: 'ReactAnalyzer',
|
|
204
|
+
phase: 'ANALYSIS',
|
|
205
|
+
priority: 70, // After JSASTAnalyzer and ExpressAnalyzer
|
|
206
|
+
creates: {
|
|
207
|
+
nodes: [
|
|
208
|
+
'react:component', 'react:state', 'react:effect', 'react:callback',
|
|
209
|
+
'react:memo', 'react:ref', 'react:reducer', 'react:context',
|
|
210
|
+
'dom:event', 'browser:storage', 'browser:timer', 'browser:observer',
|
|
211
|
+
'browser:async', 'browser:worker', 'browser:api',
|
|
212
|
+
'canvas:context', 'canvas:draw',
|
|
213
|
+
'issue:stale-closure', 'issue:missing-cleanup', 'issue:raf-leak',
|
|
214
|
+
'issue:canvas-leak', 'issue:state-after-unmount'
|
|
215
|
+
],
|
|
216
|
+
edges: [
|
|
217
|
+
'RENDERS', 'PASSES_PROP', 'HANDLES_EVENT', 'UPDATES_STATE',
|
|
218
|
+
'DEPENDS_ON', 'SCHEDULES', 'CLEANS_UP', 'DISPATCHES',
|
|
219
|
+
'PROVIDES', 'CONSUMES', 'FORWARDS_REF', 'OBSERVES'
|
|
220
|
+
]
|
|
221
|
+
},
|
|
222
|
+
dependencies: ['JSASTAnalyzer']
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async execute(context: PluginContext): Promise<PluginResult> {
|
|
227
|
+
try {
|
|
228
|
+
const { graph } = context;
|
|
229
|
+
const modules = await this.getModules(graph);
|
|
230
|
+
|
|
231
|
+
const stats: AnalysisStats = {
|
|
232
|
+
components: 0,
|
|
233
|
+
hooks: 0,
|
|
234
|
+
events: 0,
|
|
235
|
+
browserAPIs: 0,
|
|
236
|
+
issues: 0,
|
|
237
|
+
edges: 0
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
for (const module of modules) {
|
|
241
|
+
// Only analyze .jsx, .tsx, or files that import React
|
|
242
|
+
if (!this.isReactFile(module.file!)) {
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
try {
|
|
247
|
+
const result = await this.analyzeModule(module, graph);
|
|
248
|
+
stats.components += result.components;
|
|
249
|
+
stats.hooks += result.hooks;
|
|
250
|
+
stats.events += result.events;
|
|
251
|
+
stats.browserAPIs += result.browserAPIs;
|
|
252
|
+
stats.issues += result.issues;
|
|
253
|
+
stats.edges += result.edges;
|
|
254
|
+
} catch (err) {
|
|
255
|
+
console.error(`[ReactAnalyzer] Error analyzing ${module.file}:`, (err as Error).message);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
console.log(`[ReactAnalyzer] Found ${stats.components} components, ${stats.hooks} hooks, ${stats.events} events, ${stats.issues} issues`);
|
|
260
|
+
|
|
261
|
+
return createSuccessResult(
|
|
262
|
+
{
|
|
263
|
+
nodes: stats.components + stats.hooks + stats.events + stats.browserAPIs + stats.issues,
|
|
264
|
+
edges: stats.edges
|
|
265
|
+
},
|
|
266
|
+
stats
|
|
267
|
+
);
|
|
268
|
+
} catch (error) {
|
|
269
|
+
console.error(`[ReactAnalyzer] Error:`, error);
|
|
270
|
+
return createErrorResult(error as Error);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
private isReactFile(filePath: string): boolean {
|
|
275
|
+
if (filePath.endsWith('.jsx') || filePath.endsWith('.tsx')) {
|
|
276
|
+
return true;
|
|
277
|
+
}
|
|
278
|
+
// Could also check for React import in .js/.ts files
|
|
279
|
+
return false;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
private async analyzeModule(module: NodeRecord, graph: PluginContext['graph']): Promise<AnalysisStats> {
|
|
283
|
+
const code = readFileSync(module.file!, 'utf-8');
|
|
284
|
+
const ast = parse(code, {
|
|
285
|
+
sourceType: 'module',
|
|
286
|
+
plugins: ['jsx', 'typescript'] as ParserPlugin[]
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
return this.analyzeAST(ast, module.file!, graph, module.id);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Main AST analysis - can be called directly for testing
|
|
294
|
+
*/
|
|
295
|
+
async analyzeAST(
|
|
296
|
+
ast: Node,
|
|
297
|
+
filePath: string,
|
|
298
|
+
graph: PluginContext['graph'],
|
|
299
|
+
moduleId: string | null = null
|
|
300
|
+
): Promise<AnalysisStats> {
|
|
301
|
+
const analysis: AnalysisResult = {
|
|
302
|
+
components: [],
|
|
303
|
+
hooks: [],
|
|
304
|
+
events: [],
|
|
305
|
+
browserAPIs: [],
|
|
306
|
+
issues: [],
|
|
307
|
+
edges: []
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
const importedIdentifiers = new Set<string>();
|
|
311
|
+
|
|
312
|
+
// Collect imported identifiers (stable references)
|
|
313
|
+
traverse(ast, {
|
|
314
|
+
ImportDeclaration: (path: NodePath) => {
|
|
315
|
+
const node = path.node as { specifiers: Array<{ local?: { name: string } }> };
|
|
316
|
+
for (const specifier of node.specifiers) {
|
|
317
|
+
if (specifier.local?.name) {
|
|
318
|
+
importedIdentifiers.add(specifier.local.name);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
// First pass: collect component definitions
|
|
325
|
+
traverse(ast, {
|
|
326
|
+
// Arrow function components: const App = () => ...
|
|
327
|
+
VariableDeclarator: (path: NodePath<VariableDeclarator>) => {
|
|
328
|
+
if (this.isReactComponent(path)) {
|
|
329
|
+
const node = path.node;
|
|
330
|
+
const name = (node.id as { name: string }).name;
|
|
331
|
+
const loc = node.loc!;
|
|
332
|
+
const component: ComponentNode = {
|
|
333
|
+
id: `react:component#${name}#${filePath}:${loc.start.line}`,
|
|
334
|
+
type: 'react:component',
|
|
335
|
+
name,
|
|
336
|
+
file: filePath,
|
|
337
|
+
line: loc.start.line,
|
|
338
|
+
column: loc.start.column,
|
|
339
|
+
kind: 'arrow'
|
|
340
|
+
};
|
|
341
|
+
analysis.components.push(component);
|
|
342
|
+
}
|
|
343
|
+
},
|
|
344
|
+
|
|
345
|
+
// Function declaration components: function App() {...}
|
|
346
|
+
FunctionDeclaration: (path: NodePath<FunctionDeclaration>) => {
|
|
347
|
+
if (this.isReactComponent(path)) {
|
|
348
|
+
const name = path.node.id?.name;
|
|
349
|
+
if (!name) return;
|
|
350
|
+
|
|
351
|
+
const loc = path.node.loc!;
|
|
352
|
+
const component: ComponentNode = {
|
|
353
|
+
id: `react:component#${name}#${filePath}:${loc.start.line}`,
|
|
354
|
+
type: 'react:component',
|
|
355
|
+
name,
|
|
356
|
+
file: filePath,
|
|
357
|
+
line: loc.start.line,
|
|
358
|
+
column: loc.start.column,
|
|
359
|
+
kind: 'function'
|
|
360
|
+
};
|
|
361
|
+
analysis.components.push(component);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
// Second pass: analyze hooks, events, JSX
|
|
367
|
+
traverse(ast, {
|
|
368
|
+
CallExpression: (path: NodePath<CallExpression>) => {
|
|
369
|
+
const callee = path.node.callee;
|
|
370
|
+
|
|
371
|
+
// Detect React hooks
|
|
372
|
+
if (callee.type === 'Identifier' && REACT_HOOKS.includes(callee.name)) {
|
|
373
|
+
const hookData = this.analyzeHook(path, filePath);
|
|
374
|
+
if (hookData) {
|
|
375
|
+
analysis.hooks.push(hookData);
|
|
376
|
+
|
|
377
|
+
// Check for issues in hooks
|
|
378
|
+
if (callee.name === 'useEffect' || callee.name === 'useLayoutEffect') {
|
|
379
|
+
this.checkEffectIssues(path, filePath, analysis, hookData, importedIdentifiers);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Detect forwardRef
|
|
385
|
+
if (callee.type === 'Identifier' && callee.name === 'forwardRef') {
|
|
386
|
+
this.analyzeForwardRef(path, filePath, analysis);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// Detect createContext
|
|
390
|
+
if (callee.type === 'Identifier' && callee.name === 'createContext') {
|
|
391
|
+
this.analyzeCreateContext(path, filePath, analysis);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Detect browser APIs
|
|
395
|
+
this.analyzeBrowserAPI(path, filePath, analysis);
|
|
396
|
+
},
|
|
397
|
+
|
|
398
|
+
// JSX elements
|
|
399
|
+
JSXElement: (path: NodePath<JSXElement>) => {
|
|
400
|
+
this.analyzeJSXElement(path, filePath, analysis);
|
|
401
|
+
},
|
|
402
|
+
|
|
403
|
+
// JSX attributes (for event handlers and props)
|
|
404
|
+
JSXAttribute: (path: NodePath<JSXAttribute>) => {
|
|
405
|
+
this.analyzeJSXAttribute(path, filePath, analysis);
|
|
406
|
+
}
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
// Add all nodes and edges to graph
|
|
410
|
+
await this.addToGraph(analysis, graph, moduleId);
|
|
411
|
+
|
|
412
|
+
return {
|
|
413
|
+
components: analysis.components.length,
|
|
414
|
+
hooks: analysis.hooks.length,
|
|
415
|
+
events: analysis.events.length,
|
|
416
|
+
browserAPIs: analysis.browserAPIs.length,
|
|
417
|
+
issues: analysis.issues.length,
|
|
418
|
+
edges: analysis.edges.length
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Check if a function is a React component (returns JSX)
|
|
424
|
+
*/
|
|
425
|
+
private isReactComponent(path: NodePath): boolean {
|
|
426
|
+
let hasJSXReturn = false;
|
|
427
|
+
|
|
428
|
+
// Check for arrow function or function
|
|
429
|
+
const node = path.node as { init?: Node };
|
|
430
|
+
const func = node.init || path.node;
|
|
431
|
+
if (!func) return false;
|
|
432
|
+
|
|
433
|
+
// Must be a function
|
|
434
|
+
if (func.type !== 'ArrowFunctionExpression' &&
|
|
435
|
+
func.type !== 'FunctionExpression' &&
|
|
436
|
+
func.type !== 'FunctionDeclaration') {
|
|
437
|
+
return false;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// Name must start with uppercase (React component convention)
|
|
441
|
+
const pathNode = path.node as { id?: { name: string } };
|
|
442
|
+
const name = pathNode.id?.name;
|
|
443
|
+
if (!name || !/^[A-Z]/.test(name)) {
|
|
444
|
+
return false;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// Check if body contains JSX
|
|
448
|
+
path.traverse({
|
|
449
|
+
JSXElement: () => { hasJSXReturn = true; },
|
|
450
|
+
JSXFragment: () => { hasJSXReturn = true; }
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
return hasJSXReturn;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* Analyze React hooks
|
|
458
|
+
*/
|
|
459
|
+
private analyzeHook(path: NodePath<CallExpression>, filePath: string): HookNode | null {
|
|
460
|
+
const callee = path.node.callee as { name: string };
|
|
461
|
+
const hookName = callee.name;
|
|
462
|
+
const loc = path.node.loc!;
|
|
463
|
+
const args = path.node.arguments;
|
|
464
|
+
|
|
465
|
+
const hookBase = {
|
|
466
|
+
file: filePath,
|
|
467
|
+
line: loc.start.line,
|
|
468
|
+
column: loc.start.column,
|
|
469
|
+
hookName
|
|
470
|
+
};
|
|
471
|
+
|
|
472
|
+
switch (hookName) {
|
|
473
|
+
case 'useState': {
|
|
474
|
+
// const [state, setState] = useState(initialValue)
|
|
475
|
+
const parent = path.parent as { type: string; id?: { type: string; elements?: Array<{ name?: string }> } };
|
|
476
|
+
if (parent.type === 'VariableDeclarator' &&
|
|
477
|
+
parent.id?.type === 'ArrayPattern' &&
|
|
478
|
+
parent.id.elements?.length === 2) {
|
|
479
|
+
const stateName = parent.id.elements[0]?.name;
|
|
480
|
+
const setterName = parent.id.elements[1]?.name;
|
|
481
|
+
const initialValue = args[0];
|
|
482
|
+
|
|
483
|
+
return {
|
|
484
|
+
id: `react:state#${stateName}#${filePath}:${loc.start.line}`,
|
|
485
|
+
type: 'react:state',
|
|
486
|
+
...hookBase,
|
|
487
|
+
stateName,
|
|
488
|
+
setterName,
|
|
489
|
+
initialValue: this.getExpressionValue(initialValue as Node)
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
break;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
case 'useEffect':
|
|
496
|
+
case 'useLayoutEffect':
|
|
497
|
+
case 'useInsertionEffect': {
|
|
498
|
+
// useEffect(() => {...}, [deps])
|
|
499
|
+
const callback = args[0];
|
|
500
|
+
const depsArg = args[1];
|
|
501
|
+
const deps = this.extractDeps(depsArg as Node);
|
|
502
|
+
const hasCleanup = this.hasCleanupReturn(callback as Node);
|
|
503
|
+
|
|
504
|
+
const effectType = hookName === 'useEffect' ? 'react:effect' :
|
|
505
|
+
hookName === 'useLayoutEffect' ? 'react:layout-effect' :
|
|
506
|
+
'react:insertion-effect';
|
|
507
|
+
|
|
508
|
+
return {
|
|
509
|
+
id: `${effectType}#${filePath}:${loc.start.line}`,
|
|
510
|
+
type: effectType,
|
|
511
|
+
...hookBase,
|
|
512
|
+
deps,
|
|
513
|
+
hasCleanup,
|
|
514
|
+
depsType: !depsArg ? 'none' : (deps?.length === 0 ? 'empty' : 'array')
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
case 'useCallback': {
|
|
519
|
+
// const fn = useCallback(() => {...}, [deps])
|
|
520
|
+
const parent = path.parent as { type: string; id?: { name: string } };
|
|
521
|
+
const callbackName = parent.type === 'VariableDeclarator' ? parent.id?.name : null;
|
|
522
|
+
const depsArg = args[1];
|
|
523
|
+
const deps = this.extractDeps(depsArg as Node);
|
|
524
|
+
|
|
525
|
+
return {
|
|
526
|
+
id: `react:callback#${callbackName || 'anonymous'}#${filePath}:${loc.start.line}`,
|
|
527
|
+
type: 'react:callback',
|
|
528
|
+
...hookBase,
|
|
529
|
+
callbackName,
|
|
530
|
+
deps
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
case 'useMemo': {
|
|
535
|
+
// const value = useMemo(() => computation, [deps])
|
|
536
|
+
const parent = path.parent as { type: string; id?: { name: string } };
|
|
537
|
+
const memoName = parent.type === 'VariableDeclarator' ? parent.id?.name : null;
|
|
538
|
+
const depsArg = args[1];
|
|
539
|
+
const deps = this.extractDeps(depsArg as Node);
|
|
540
|
+
|
|
541
|
+
return {
|
|
542
|
+
id: `react:memo#${memoName || 'anonymous'}#${filePath}:${loc.start.line}`,
|
|
543
|
+
type: 'react:memo',
|
|
544
|
+
...hookBase,
|
|
545
|
+
memoName,
|
|
546
|
+
deps
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
case 'useRef': {
|
|
551
|
+
// const ref = useRef(initialValue)
|
|
552
|
+
const parent = path.parent as { type: string; id?: { name: string } };
|
|
553
|
+
const refName = parent.type === 'VariableDeclarator' ? parent.id?.name : null;
|
|
554
|
+
const initialValue = args[0];
|
|
555
|
+
|
|
556
|
+
return {
|
|
557
|
+
id: `react:ref#${refName || 'anonymous'}#${filePath}:${loc.start.line}`,
|
|
558
|
+
type: 'react:ref',
|
|
559
|
+
...hookBase,
|
|
560
|
+
refName,
|
|
561
|
+
initialValue: this.getExpressionValue(initialValue as Node)
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
case 'useReducer': {
|
|
566
|
+
// const [state, dispatch] = useReducer(reducer, initialState)
|
|
567
|
+
const parent = path.parent as { type: string; id?: { type: string; elements?: Array<{ name?: string }> } };
|
|
568
|
+
if (parent.type === 'VariableDeclarator' &&
|
|
569
|
+
parent.id?.type === 'ArrayPattern' &&
|
|
570
|
+
parent.id.elements && parent.id.elements.length >= 2) {
|
|
571
|
+
const stateName = parent.id.elements[0]?.name;
|
|
572
|
+
const dispatchName = parent.id.elements[1]?.name;
|
|
573
|
+
const reducerArg = args[0] as Node | undefined;
|
|
574
|
+
const reducerName = reducerArg?.type === 'Identifier' ? (reducerArg as { name: string }).name : null;
|
|
575
|
+
|
|
576
|
+
return {
|
|
577
|
+
id: `react:reducer#${stateName}#${filePath}:${loc.start.line}`,
|
|
578
|
+
type: 'react:reducer',
|
|
579
|
+
...hookBase,
|
|
580
|
+
stateName,
|
|
581
|
+
dispatchName,
|
|
582
|
+
reducerName
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
break;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
case 'useContext': {
|
|
589
|
+
// const value = useContext(Context)
|
|
590
|
+
const parent = path.parent as { type: string; id?: { name: string } };
|
|
591
|
+
const valueName = parent.type === 'VariableDeclarator' ? parent.id?.name : null;
|
|
592
|
+
const contextArg = args[0] as Node | undefined;
|
|
593
|
+
const contextName = contextArg?.type === 'Identifier' ? (contextArg as { name: string }).name : null;
|
|
594
|
+
|
|
595
|
+
return {
|
|
596
|
+
id: `react:context-use#${contextName || 'unknown'}#${filePath}:${loc.start.line}`,
|
|
597
|
+
type: 'react:context-use',
|
|
598
|
+
...hookBase,
|
|
599
|
+
valueName,
|
|
600
|
+
contextName
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
case 'useImperativeHandle': {
|
|
605
|
+
// useImperativeHandle(ref, () => ({ method1, method2 }), [deps])
|
|
606
|
+
const refArg = args[0] as Node | undefined;
|
|
607
|
+
const refName = refArg?.type === 'Identifier' ? (refArg as { name: string }).name : null;
|
|
608
|
+
const createHandle = args[1] as Node | undefined;
|
|
609
|
+
|
|
610
|
+
// Extract exposed methods
|
|
611
|
+
const exposedMethods: string[] = [];
|
|
612
|
+
if (createHandle?.type === 'ArrowFunctionExpression' ||
|
|
613
|
+
createHandle?.type === 'FunctionExpression') {
|
|
614
|
+
const body = (createHandle as { body: Node }).body;
|
|
615
|
+
if (body.type === 'ObjectExpression') {
|
|
616
|
+
const objExpr = body as { properties: Array<{ key?: { name: string } }> };
|
|
617
|
+
for (const prop of objExpr.properties) {
|
|
618
|
+
if (prop.key?.name) {
|
|
619
|
+
exposedMethods.push(prop.key.name);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
return {
|
|
626
|
+
id: `react:imperative-handle#${filePath}:${loc.start.line}`,
|
|
627
|
+
type: 'react:imperative-handle',
|
|
628
|
+
...hookBase,
|
|
629
|
+
refName,
|
|
630
|
+
exposedMethods
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
return null;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
/**
|
|
639
|
+
* Extract dependency array from hook
|
|
640
|
+
*/
|
|
641
|
+
private extractDeps(depsArg: Node | undefined): string[] | null {
|
|
642
|
+
if (!depsArg) return null; // No deps argument
|
|
643
|
+
if (depsArg.type !== 'ArrayExpression') return ['<dynamic>'];
|
|
644
|
+
|
|
645
|
+
const arrExpr = depsArg as { elements: Array<Node | null> };
|
|
646
|
+
return arrExpr.elements.map(el => {
|
|
647
|
+
if (!el) return '<empty>';
|
|
648
|
+
if (el.type === 'Identifier') return (el as { name: string }).name;
|
|
649
|
+
if (el.type === 'MemberExpression') {
|
|
650
|
+
return this.getMemberExpressionName(el);
|
|
651
|
+
}
|
|
652
|
+
return '<expression>';
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
private getMemberExpressionName(node: Node): string {
|
|
657
|
+
if (node.type !== 'MemberExpression') {
|
|
658
|
+
return (node as { name?: string }).name || '<unknown>';
|
|
659
|
+
}
|
|
660
|
+
const memExpr = node as { object: Node; property: { name?: string; value?: string } };
|
|
661
|
+
const object = this.getMemberExpressionName(memExpr.object);
|
|
662
|
+
const property = memExpr.property.name || memExpr.property.value || '<computed>';
|
|
663
|
+
return `${object}.${property}`;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
/**
|
|
667
|
+
* Check if effect callback has cleanup return
|
|
668
|
+
*/
|
|
669
|
+
private hasCleanupReturn(callback: Node | undefined): boolean {
|
|
670
|
+
if (!callback) return false;
|
|
671
|
+
if (callback.type !== 'ArrowFunctionExpression' &&
|
|
672
|
+
callback.type !== 'FunctionExpression') {
|
|
673
|
+
return false;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// Simple AST traversal without using babel traverse
|
|
677
|
+
const checkForCleanupReturn = (node: Node | null): boolean => {
|
|
678
|
+
if (!node) return false;
|
|
679
|
+
|
|
680
|
+
if (node.type === 'ReturnStatement') {
|
|
681
|
+
const retStmt = node as { argument?: Node };
|
|
682
|
+
const arg = retStmt.argument;
|
|
683
|
+
if (arg && (arg.type === 'ArrowFunctionExpression' ||
|
|
684
|
+
arg.type === 'FunctionExpression')) {
|
|
685
|
+
return true;
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
// Check body
|
|
690
|
+
const nodeWithBody = node as { body?: Node | Node[] };
|
|
691
|
+
if (nodeWithBody.body) {
|
|
692
|
+
if (Array.isArray(nodeWithBody.body)) {
|
|
693
|
+
return nodeWithBody.body.some(n => checkForCleanupReturn(n));
|
|
694
|
+
} else if (nodeWithBody.body.type === 'BlockStatement') {
|
|
695
|
+
const blockStmt = nodeWithBody.body as { body?: Node[] };
|
|
696
|
+
if (blockStmt.body) {
|
|
697
|
+
return blockStmt.body.some(n => checkForCleanupReturn(n));
|
|
698
|
+
}
|
|
699
|
+
} else {
|
|
700
|
+
return checkForCleanupReturn(nodeWithBody.body);
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
return false;
|
|
705
|
+
};
|
|
706
|
+
|
|
707
|
+
return checkForCleanupReturn(callback);
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
/**
|
|
711
|
+
* Check for issues in useEffect/useLayoutEffect
|
|
712
|
+
*/
|
|
713
|
+
private checkEffectIssues(
|
|
714
|
+
path: NodePath<CallExpression>,
|
|
715
|
+
filePath: string,
|
|
716
|
+
analysis: AnalysisResult,
|
|
717
|
+
hookData: HookNode,
|
|
718
|
+
importedIdentifiers: Set<string> = new Set()
|
|
719
|
+
): void {
|
|
720
|
+
const callback = path.node.arguments[0];
|
|
721
|
+
if (!callback) return;
|
|
722
|
+
|
|
723
|
+
const deps = hookData.deps as string[] | null;
|
|
724
|
+
const usedVars = new Set<string>();
|
|
725
|
+
const setterCalls = new Set<string>();
|
|
726
|
+
const callbackParams = new Set<string>(); // Track parameters of nested callback functions
|
|
727
|
+
|
|
728
|
+
// Simple recursive AST walker to collect identifiers
|
|
729
|
+
const collectIdentifiers = (node: Node | null, parentType: string | null = null, isPropertyKey = false): void => {
|
|
730
|
+
if (!node) return;
|
|
731
|
+
|
|
732
|
+
if (node.type === 'Identifier') {
|
|
733
|
+
const id = node as { name: string };
|
|
734
|
+
// Skip if it's a property access key
|
|
735
|
+
if (!isPropertyKey && !callbackParams.has(id.name)) {
|
|
736
|
+
usedVars.add(id.name);
|
|
737
|
+
}
|
|
738
|
+
return;
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
if (node.type === 'MemberExpression') {
|
|
742
|
+
const memExpr = node as { object: Node; property: { type: string; name?: string } };
|
|
743
|
+
// Check for ref.current pattern (valid for stable refs)
|
|
744
|
+
if (memExpr.property?.type === 'Identifier' && memExpr.property.name === 'current') {
|
|
745
|
+
// This is likely a ref.current access - collect only the ref name, not 'current'
|
|
746
|
+
collectIdentifiers(memExpr.object, 'MemberExpression', false);
|
|
747
|
+
return;
|
|
748
|
+
}
|
|
749
|
+
collectIdentifiers(memExpr.object, 'MemberExpression', false);
|
|
750
|
+
// Skip property name
|
|
751
|
+
return;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
if (node.type === 'CallExpression') {
|
|
755
|
+
const callExpr = node as { callee: Node & { name?: string }; arguments: Node[] };
|
|
756
|
+
const callee = callExpr.callee;
|
|
757
|
+
if (callee.type === 'Identifier' && callee.name?.startsWith('set')) {
|
|
758
|
+
const arg = callExpr.arguments[0];
|
|
759
|
+
if (arg?.type === 'ArrowFunctionExpression' ||
|
|
760
|
+
arg?.type === 'FunctionExpression') {
|
|
761
|
+
setterCalls.add(callee.name);
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
collectIdentifiers(callee, 'CallExpression', false);
|
|
765
|
+
callExpr.arguments?.forEach(arg => collectIdentifiers(arg, 'CallExpression', false));
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
if (node.type === 'ObjectProperty') {
|
|
770
|
+
const prop = node as { value: Node };
|
|
771
|
+
// Skip key, process value
|
|
772
|
+
collectIdentifiers(prop.value, 'ObjectProperty', false);
|
|
773
|
+
return;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
// Handle function parameters - skip them (AC-1: callback parameters are not external deps)
|
|
777
|
+
if (node.type === 'ArrowFunctionExpression' || node.type === 'FunctionExpression') {
|
|
778
|
+
const func = node as { params?: Array<{ type: string; name?: string }>; body: Node };
|
|
779
|
+
// Collect parameter names from nested callbacks
|
|
780
|
+
func.params?.forEach(p => {
|
|
781
|
+
if (p.type === 'Identifier' && p.name) {
|
|
782
|
+
callbackParams.add(p.name); // Add to global tracking
|
|
783
|
+
}
|
|
784
|
+
});
|
|
785
|
+
|
|
786
|
+
const collectInBody = (bodyNode: Node | null): void => {
|
|
787
|
+
if (!bodyNode) return;
|
|
788
|
+
if (bodyNode.type === 'Identifier') {
|
|
789
|
+
const id = bodyNode as { name: string };
|
|
790
|
+
if (!callbackParams.has(id.name)) {
|
|
791
|
+
usedVars.add(id.name);
|
|
792
|
+
}
|
|
793
|
+
} else if (typeof bodyNode === 'object') {
|
|
794
|
+
Object.values(bodyNode).forEach(child => {
|
|
795
|
+
if (child && typeof child === 'object') {
|
|
796
|
+
if (Array.isArray(child)) {
|
|
797
|
+
child.forEach(c => collectInBody(c as Node));
|
|
798
|
+
} else {
|
|
799
|
+
collectInBody(child as Node);
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
});
|
|
803
|
+
}
|
|
804
|
+
};
|
|
805
|
+
collectInBody(func.body);
|
|
806
|
+
return;
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
// Recurse into child nodes
|
|
810
|
+
if (typeof node === 'object') {
|
|
811
|
+
Object.entries(node).forEach(([key, child]) => {
|
|
812
|
+
if (key === 'loc' || key === 'start' || key === 'end' || key === 'type') return;
|
|
813
|
+
if (child && typeof child === 'object') {
|
|
814
|
+
if (Array.isArray(child)) {
|
|
815
|
+
child.forEach(c => collectIdentifiers(c as Node, node.type, false));
|
|
816
|
+
} else {
|
|
817
|
+
collectIdentifiers(child as Node, node.type, false);
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
});
|
|
821
|
+
}
|
|
822
|
+
};
|
|
823
|
+
|
|
824
|
+
collectIdentifiers(callback as Node);
|
|
825
|
+
|
|
826
|
+
// Check for stale closures
|
|
827
|
+
if (deps !== null && deps.length >= 0) { // Has deps array
|
|
828
|
+
const depsSet = new Set(deps);
|
|
829
|
+
|
|
830
|
+
// Hooks that don't cause stale closure issues
|
|
831
|
+
const safeHooks = ['useState', 'useReducer', 'useRef', 'useCallback', 'useMemo',
|
|
832
|
+
'useEffect', 'useLayoutEffect', 'useContext'];
|
|
833
|
+
const setterPrefixes = ['set'];
|
|
834
|
+
|
|
835
|
+
for (const used of usedVars) {
|
|
836
|
+
// Skip safe identifiers
|
|
837
|
+
if (safeHooks.includes(used)) continue;
|
|
838
|
+
if (setterPrefixes.some(p => used.startsWith(p))) continue;
|
|
839
|
+
if (depsSet.has(used)) continue;
|
|
840
|
+
if (used === 'console' || used === 'window' || used === 'document') continue;
|
|
841
|
+
if (used === 'Math' || used === 'JSON' || used === 'Date') continue;
|
|
842
|
+
if (used === 'undefined' || used === 'null' || used === 'true' || used === 'false') continue;
|
|
843
|
+
// AC-2: Skip imported identifiers (stable references)
|
|
844
|
+
if (importedIdentifiers.has(used)) continue;
|
|
845
|
+
|
|
846
|
+
// This variable is used but not in deps - potential stale closure
|
|
847
|
+
const issue: IssueNode = {
|
|
848
|
+
id: `issue:stale-closure#${used}#${filePath}:${hookData.line}`,
|
|
849
|
+
type: 'issue:stale-closure',
|
|
850
|
+
file: filePath,
|
|
851
|
+
line: hookData.line,
|
|
852
|
+
variable: used,
|
|
853
|
+
hookType: hookData.hookName,
|
|
854
|
+
deps: deps,
|
|
855
|
+
message: `Variable '${used}' is used in ${hookData.hookName} but not listed in dependencies`
|
|
856
|
+
};
|
|
857
|
+
analysis.issues.push(issue);
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
// Check for missing cleanup
|
|
862
|
+
this.checkMissingCleanup(callback as Node, filePath, analysis, hookData);
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
/**
|
|
866
|
+
* Check for missing cleanup in effect callback
|
|
867
|
+
*/
|
|
868
|
+
private checkMissingCleanup(
|
|
869
|
+
callback: Node,
|
|
870
|
+
filePath: string,
|
|
871
|
+
analysis: AnalysisResult,
|
|
872
|
+
hookData: HookNode
|
|
873
|
+
): void {
|
|
874
|
+
const hasCleanup = hookData.hasCleanup as boolean;
|
|
875
|
+
|
|
876
|
+
// Simple recursive AST walker
|
|
877
|
+
const checkNode = (node: Node | null, parent: Node | null = null): void => {
|
|
878
|
+
if (!node || typeof node !== 'object') return;
|
|
879
|
+
|
|
880
|
+
if (node.type === 'CallExpression') {
|
|
881
|
+
const callExpr = node as { callee?: { type: string; name?: string }; loc?: { start: { line: number } } };
|
|
882
|
+
const callee = callExpr.callee;
|
|
883
|
+
const loc = callExpr.loc;
|
|
884
|
+
|
|
885
|
+
// Check for timer APIs
|
|
886
|
+
if (callee?.type === 'Identifier') {
|
|
887
|
+
const api = callee.name;
|
|
888
|
+
if (api && BROWSER_APIS.timers.includes(api)) {
|
|
889
|
+
// Timer called without storing reference
|
|
890
|
+
if (!hasCleanup && api === 'requestAnimationFrame') {
|
|
891
|
+
analysis.issues.push({
|
|
892
|
+
id: `issue:raf-leak#${filePath}:${loc?.start?.line || 0}`,
|
|
893
|
+
type: 'issue:raf-leak',
|
|
894
|
+
file: filePath,
|
|
895
|
+
line: loc?.start?.line || 0,
|
|
896
|
+
message: `requestAnimationFrame called without cleanup - will leak on unmount`
|
|
897
|
+
});
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
if (node.type === 'NewExpression') {
|
|
904
|
+
const newExpr = node as { callee?: { type: string; name?: string }; loc?: { start: { line: number } } };
|
|
905
|
+
const callee = newExpr.callee;
|
|
906
|
+
const loc = newExpr.loc;
|
|
907
|
+
|
|
908
|
+
// Check for WebSocket without cleanup
|
|
909
|
+
if (callee?.type === 'Identifier' && callee.name === 'WebSocket') {
|
|
910
|
+
if (!hasCleanup) {
|
|
911
|
+
analysis.issues.push({
|
|
912
|
+
id: `issue:missing-cleanup#websocket#${filePath}:${loc?.start?.line || 0}`,
|
|
913
|
+
type: 'issue:missing-cleanup',
|
|
914
|
+
file: filePath,
|
|
915
|
+
line: loc?.start?.line || 0,
|
|
916
|
+
api: 'WebSocket',
|
|
917
|
+
message: `WebSocket created without cleanup - connection will leak on unmount`
|
|
918
|
+
});
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
// Check for observers without cleanup
|
|
923
|
+
if (callee?.type === 'Identifier' && callee.name && BROWSER_APIS.observers.includes(callee.name)) {
|
|
924
|
+
if (!hasCleanup) {
|
|
925
|
+
analysis.issues.push({
|
|
926
|
+
id: `issue:missing-cleanup#${callee.name}#${filePath}:${loc?.start?.line || 0}`,
|
|
927
|
+
type: 'issue:missing-cleanup',
|
|
928
|
+
file: filePath,
|
|
929
|
+
line: loc?.start?.line || 0,
|
|
930
|
+
api: callee.name,
|
|
931
|
+
message: `${callee.name} created without disconnect in cleanup`
|
|
932
|
+
});
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
// Recurse into child nodes
|
|
938
|
+
Object.entries(node).forEach(([key, child]) => {
|
|
939
|
+
if (key === 'loc' || key === 'start' || key === 'end' || key === 'type') return;
|
|
940
|
+
if (child && typeof child === 'object') {
|
|
941
|
+
if (Array.isArray(child)) {
|
|
942
|
+
child.forEach(c => checkNode(c as Node, node));
|
|
943
|
+
} else {
|
|
944
|
+
checkNode(child as Node, node);
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
});
|
|
948
|
+
};
|
|
949
|
+
|
|
950
|
+
checkNode(callback);
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
/**
|
|
954
|
+
* Analyze JSX element for component rendering
|
|
955
|
+
*/
|
|
956
|
+
private analyzeJSXElement(path: NodePath<JSXElement>, filePath: string, analysis: AnalysisResult): void {
|
|
957
|
+
const openingElement = path.node.openingElement;
|
|
958
|
+
const elementName = this.getJSXElementName(openingElement.name);
|
|
959
|
+
|
|
960
|
+
// Skip native HTML elements (lowercase)
|
|
961
|
+
if (/^[a-z]/.test(elementName)) {
|
|
962
|
+
return;
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
// This is a React component being rendered
|
|
966
|
+
const loc = openingElement.loc!;
|
|
967
|
+
|
|
968
|
+
// Find parent component
|
|
969
|
+
let parentComponent: string | null = null;
|
|
970
|
+
let parentPath: NodePath<Node> | null = path.parentPath;
|
|
971
|
+
while (parentPath) {
|
|
972
|
+
if (parentPath.node.type === 'FunctionDeclaration' ||
|
|
973
|
+
parentPath.node.type === 'ArrowFunctionExpression' ||
|
|
974
|
+
parentPath.node.type === 'FunctionExpression') {
|
|
975
|
+
// Check if this function is a component
|
|
976
|
+
const funcName = this.getFunctionName(parentPath);
|
|
977
|
+
if (funcName && /^[A-Z]/.test(funcName)) {
|
|
978
|
+
parentComponent = funcName;
|
|
979
|
+
break;
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
parentPath = parentPath.parentPath;
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
if (parentComponent) {
|
|
986
|
+
analysis.edges.push({
|
|
987
|
+
edgeType: 'RENDERS',
|
|
988
|
+
src: `react:component#${parentComponent}`,
|
|
989
|
+
dst: `react:component#${elementName}`,
|
|
990
|
+
file: filePath,
|
|
991
|
+
line: loc.start.line
|
|
992
|
+
});
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
private getJSXElementName(nameNode: Node): string {
|
|
997
|
+
if (nameNode.type === 'JSXIdentifier') {
|
|
998
|
+
return (nameNode as { name: string }).name;
|
|
999
|
+
}
|
|
1000
|
+
if (nameNode.type === 'JSXMemberExpression') {
|
|
1001
|
+
const memExpr = nameNode as { object: Node; property: { name: string } };
|
|
1002
|
+
return `${this.getJSXElementName(memExpr.object)}.${memExpr.property.name}`;
|
|
1003
|
+
}
|
|
1004
|
+
return '<unknown>';
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
private getFunctionName(path: NodePath): string | null {
|
|
1008
|
+
// Arrow function assigned to variable
|
|
1009
|
+
const parent = path.parent as { type: string; id?: { name: string } };
|
|
1010
|
+
if (parent?.type === 'VariableDeclarator') {
|
|
1011
|
+
return parent.id?.name || null;
|
|
1012
|
+
}
|
|
1013
|
+
// Function declaration
|
|
1014
|
+
const node = path.node as { id?: { name: string } };
|
|
1015
|
+
if (node.id?.name) {
|
|
1016
|
+
return node.id.name;
|
|
1017
|
+
}
|
|
1018
|
+
return null;
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
/**
|
|
1022
|
+
* Analyze JSX attribute for props and event handlers
|
|
1023
|
+
*/
|
|
1024
|
+
private analyzeJSXAttribute(path: NodePath<JSXAttribute>, filePath: string, analysis: AnalysisResult): void {
|
|
1025
|
+
const attr = path.node;
|
|
1026
|
+
if (!attr.name || attr.name.type !== 'JSXIdentifier') return;
|
|
1027
|
+
|
|
1028
|
+
const attrName = attr.name.name;
|
|
1029
|
+
const loc = attr.loc!;
|
|
1030
|
+
|
|
1031
|
+
// Get parent JSX element info first
|
|
1032
|
+
const jsxOpeningElement = path.parent as { type: string; name?: Node };
|
|
1033
|
+
let componentName: string | null = null;
|
|
1034
|
+
let isReactComponent = false;
|
|
1035
|
+
|
|
1036
|
+
if (jsxOpeningElement?.type === 'JSXOpeningElement' && jsxOpeningElement.name) {
|
|
1037
|
+
componentName = this.getJSXElementName(jsxOpeningElement.name);
|
|
1038
|
+
isReactComponent = /^[A-Z]/.test(componentName);
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
// Check if it's an event handler
|
|
1042
|
+
if (REACT_EVENTS[attrName]) {
|
|
1043
|
+
const eventType = REACT_EVENTS[attrName];
|
|
1044
|
+
const handler = attr.value as { type: string; expression?: Node } | null;
|
|
1045
|
+
|
|
1046
|
+
let handlerName = '<inline>';
|
|
1047
|
+
if (handler?.type === 'JSXExpressionContainer') {
|
|
1048
|
+
const expr = handler.expression;
|
|
1049
|
+
if (expr?.type === 'Identifier') {
|
|
1050
|
+
handlerName = (expr as { name: string }).name;
|
|
1051
|
+
} else if (expr?.type === 'MemberExpression') {
|
|
1052
|
+
handlerName = this.getMemberExpressionName(expr);
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
const event: EventNode = {
|
|
1057
|
+
id: `dom:event#${eventType}#${filePath}:${loc.start.line}`,
|
|
1058
|
+
type: 'dom:event',
|
|
1059
|
+
eventType,
|
|
1060
|
+
reactProp: attrName,
|
|
1061
|
+
handler: handlerName,
|
|
1062
|
+
file: filePath,
|
|
1063
|
+
line: loc.start.line
|
|
1064
|
+
};
|
|
1065
|
+
analysis.events.push(event);
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
// For React components (uppercase), create PASSES_PROP edges for all props
|
|
1069
|
+
if (isReactComponent && componentName && attrName !== 'key' && attrName !== 'ref' && attrName !== 'children') {
|
|
1070
|
+
let propValue = '<expression>';
|
|
1071
|
+
const value = attr.value as { type: string; value?: string; expression?: Node } | null;
|
|
1072
|
+
if (value?.type === 'StringLiteral') {
|
|
1073
|
+
propValue = value.value || '';
|
|
1074
|
+
} else if (value?.type === 'JSXExpressionContainer') {
|
|
1075
|
+
propValue = this.getExpressionValue(value.expression!);
|
|
1076
|
+
} else if (value === null) {
|
|
1077
|
+
propValue = 'true'; // Boolean shorthand
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
// Find parent component
|
|
1081
|
+
let parentComponent: string | null = null;
|
|
1082
|
+
let parentPath: NodePath | null = path.parentPath;
|
|
1083
|
+
while (parentPath) {
|
|
1084
|
+
const node = parentPath.node as { type: string; id?: { name: string } };
|
|
1085
|
+
if (node.type === 'FunctionDeclaration' && node.id?.name) {
|
|
1086
|
+
if (/^[A-Z]/.test(node.id.name)) {
|
|
1087
|
+
parentComponent = node.id.name;
|
|
1088
|
+
break;
|
|
1089
|
+
}
|
|
1090
|
+
} else if (node.type === 'ArrowFunctionExpression' || node.type === 'FunctionExpression') {
|
|
1091
|
+
const funcName = this.getFunctionName(parentPath);
|
|
1092
|
+
if (funcName && /^[A-Z]/.test(funcName)) {
|
|
1093
|
+
parentComponent = funcName;
|
|
1094
|
+
break;
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
parentPath = parentPath.parentPath;
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
if (parentComponent) {
|
|
1101
|
+
analysis.edges.push({
|
|
1102
|
+
edgeType: 'PASSES_PROP',
|
|
1103
|
+
src: `react:component#${parentComponent}`,
|
|
1104
|
+
dst: `react:component#${componentName}`,
|
|
1105
|
+
propName: attrName,
|
|
1106
|
+
propValue,
|
|
1107
|
+
file: filePath,
|
|
1108
|
+
line: loc.start.line
|
|
1109
|
+
});
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
/**
|
|
1115
|
+
* Analyze forwardRef usage
|
|
1116
|
+
*/
|
|
1117
|
+
private analyzeForwardRef(path: NodePath<CallExpression>, filePath: string, analysis: AnalysisResult): void {
|
|
1118
|
+
const loc = path.node.loc!;
|
|
1119
|
+
const parent = path.parent as { type: string; id?: { name: string } };
|
|
1120
|
+
const componentName = parent.type === 'VariableDeclarator' ? parent.id?.name : null;
|
|
1121
|
+
|
|
1122
|
+
if (componentName) {
|
|
1123
|
+
analysis.components.push({
|
|
1124
|
+
id: `react:component#${componentName}#${filePath}:${loc.start.line}`,
|
|
1125
|
+
type: 'react:component',
|
|
1126
|
+
name: componentName,
|
|
1127
|
+
file: filePath,
|
|
1128
|
+
line: loc.start.line,
|
|
1129
|
+
column: loc.start.column,
|
|
1130
|
+
kind: 'forwardRef'
|
|
1131
|
+
});
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
/**
|
|
1136
|
+
* Analyze createContext usage
|
|
1137
|
+
*/
|
|
1138
|
+
private analyzeCreateContext(path: NodePath<CallExpression>, filePath: string, analysis: AnalysisResult): void {
|
|
1139
|
+
const loc = path.node.loc!;
|
|
1140
|
+
const parent = path.parent as { type: string; id?: { name: string } };
|
|
1141
|
+
const contextName = parent.type === 'VariableDeclarator' ? parent.id?.name : null;
|
|
1142
|
+
|
|
1143
|
+
if (contextName) {
|
|
1144
|
+
const defaultValue = path.node.arguments[0];
|
|
1145
|
+
analysis.hooks.push({
|
|
1146
|
+
id: `react:context#${contextName}#${filePath}:${loc.start.line}`,
|
|
1147
|
+
type: 'react:context',
|
|
1148
|
+
contextName,
|
|
1149
|
+
file: filePath,
|
|
1150
|
+
line: loc.start.line,
|
|
1151
|
+
column: loc.start.column,
|
|
1152
|
+
hookName: 'createContext',
|
|
1153
|
+
defaultValue: this.getExpressionValue(defaultValue as Node)
|
|
1154
|
+
});
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
/**
|
|
1159
|
+
* Analyze browser API calls
|
|
1160
|
+
*/
|
|
1161
|
+
private analyzeBrowserAPI(path: NodePath<CallExpression>, filePath: string, analysis: AnalysisResult): void {
|
|
1162
|
+
const callee = path.node.callee;
|
|
1163
|
+
const loc = path.node.loc!;
|
|
1164
|
+
|
|
1165
|
+
// Direct function call: setTimeout, fetch, alert
|
|
1166
|
+
if (callee.type === 'Identifier') {
|
|
1167
|
+
const name = callee.name;
|
|
1168
|
+
|
|
1169
|
+
// Timers
|
|
1170
|
+
if (BROWSER_APIS.timers.includes(name)) {
|
|
1171
|
+
analysis.browserAPIs.push({
|
|
1172
|
+
id: `browser:timer#${name}#${filePath}:${loc.start.line}`,
|
|
1173
|
+
type: 'browser:timer',
|
|
1174
|
+
api: name,
|
|
1175
|
+
file: filePath,
|
|
1176
|
+
line: loc.start.line
|
|
1177
|
+
});
|
|
1178
|
+
return;
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
// Blocking APIs
|
|
1182
|
+
if (BROWSER_APIS.blocking.includes(name)) {
|
|
1183
|
+
analysis.browserAPIs.push({
|
|
1184
|
+
id: `browser:blocking#${name}#${filePath}:${loc.start.line}`,
|
|
1185
|
+
type: 'browser:blocking',
|
|
1186
|
+
api: name,
|
|
1187
|
+
file: filePath,
|
|
1188
|
+
line: loc.start.line
|
|
1189
|
+
});
|
|
1190
|
+
return;
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
// Fetch
|
|
1194
|
+
if (name === 'fetch') {
|
|
1195
|
+
analysis.browserAPIs.push({
|
|
1196
|
+
id: `browser:async#fetch#${filePath}:${loc.start.line}`,
|
|
1197
|
+
type: 'browser:async',
|
|
1198
|
+
api: 'fetch',
|
|
1199
|
+
file: filePath,
|
|
1200
|
+
line: loc.start.line
|
|
1201
|
+
});
|
|
1202
|
+
return;
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
// Member expression: localStorage.setItem, document.querySelector
|
|
1207
|
+
if (callee.type === 'MemberExpression') {
|
|
1208
|
+
const fullName = this.getMemberExpressionName(callee);
|
|
1209
|
+
|
|
1210
|
+
// localStorage/sessionStorage
|
|
1211
|
+
if (fullName.startsWith('localStorage.') || fullName.startsWith('sessionStorage.')) {
|
|
1212
|
+
const [storage, method] = fullName.split('.');
|
|
1213
|
+
const operation = method === 'getItem' ? 'read' :
|
|
1214
|
+
method === 'setItem' ? 'write' :
|
|
1215
|
+
method === 'removeItem' ? 'delete' : method;
|
|
1216
|
+
|
|
1217
|
+
analysis.browserAPIs.push({
|
|
1218
|
+
id: `browser:storage#${storage}:${operation}#${filePath}:${loc.start.line}`,
|
|
1219
|
+
type: 'browser:storage',
|
|
1220
|
+
storage,
|
|
1221
|
+
operation,
|
|
1222
|
+
file: filePath,
|
|
1223
|
+
line: loc.start.line
|
|
1224
|
+
});
|
|
1225
|
+
return;
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
// DOM queries
|
|
1229
|
+
if (fullName.startsWith('document.') &&
|
|
1230
|
+
(fullName.includes('querySelector') || fullName.includes('getElementById'))) {
|
|
1231
|
+
analysis.browserAPIs.push({
|
|
1232
|
+
id: `browser:dom#query#${filePath}:${loc.start.line}`,
|
|
1233
|
+
type: 'browser:dom',
|
|
1234
|
+
operation: 'query',
|
|
1235
|
+
api: fullName,
|
|
1236
|
+
file: filePath,
|
|
1237
|
+
line: loc.start.line
|
|
1238
|
+
});
|
|
1239
|
+
return;
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
// History API
|
|
1243
|
+
if (fullName.startsWith('history.') || fullName.startsWith('window.history.')) {
|
|
1244
|
+
analysis.browserAPIs.push({
|
|
1245
|
+
id: `browser:history#${filePath}:${loc.start.line}`,
|
|
1246
|
+
type: 'browser:history',
|
|
1247
|
+
api: fullName,
|
|
1248
|
+
file: filePath,
|
|
1249
|
+
line: loc.start.line
|
|
1250
|
+
});
|
|
1251
|
+
return;
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
// Clipboard API
|
|
1255
|
+
if (fullName.includes('clipboard')) {
|
|
1256
|
+
analysis.browserAPIs.push({
|
|
1257
|
+
id: `browser:clipboard#${filePath}:${loc.start.line}`,
|
|
1258
|
+
type: 'browser:clipboard',
|
|
1259
|
+
api: fullName,
|
|
1260
|
+
file: filePath,
|
|
1261
|
+
line: loc.start.line
|
|
1262
|
+
});
|
|
1263
|
+
return;
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
// Geolocation
|
|
1267
|
+
if (fullName.includes('geolocation')) {
|
|
1268
|
+
analysis.browserAPIs.push({
|
|
1269
|
+
id: `browser:geolocation#${filePath}:${loc.start.line}`,
|
|
1270
|
+
type: 'browser:geolocation',
|
|
1271
|
+
api: fullName,
|
|
1272
|
+
file: filePath,
|
|
1273
|
+
line: loc.start.line
|
|
1274
|
+
});
|
|
1275
|
+
return;
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
// Canvas context
|
|
1279
|
+
if (fullName.match(/\.(fillRect|strokeRect|fillText|strokeText|beginPath|closePath|moveTo|lineTo|arc|fill|stroke|clearRect|drawImage|save|restore|translate|rotate|scale)$/)) {
|
|
1280
|
+
const method = fullName.split('.').pop();
|
|
1281
|
+
analysis.browserAPIs.push({
|
|
1282
|
+
id: `canvas:draw#${method}#${filePath}:${loc.start.line}`,
|
|
1283
|
+
type: 'canvas:draw',
|
|
1284
|
+
method,
|
|
1285
|
+
file: filePath,
|
|
1286
|
+
line: loc.start.line
|
|
1287
|
+
});
|
|
1288
|
+
return;
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
// matchMedia
|
|
1292
|
+
if (fullName === 'window.matchMedia' || fullName === 'matchMedia') {
|
|
1293
|
+
analysis.browserAPIs.push({
|
|
1294
|
+
id: `browser:media-query#${filePath}:${loc.start.line}`,
|
|
1295
|
+
type: 'browser:media-query',
|
|
1296
|
+
api: 'matchMedia',
|
|
1297
|
+
file: filePath,
|
|
1298
|
+
line: loc.start.line
|
|
1299
|
+
});
|
|
1300
|
+
return;
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
private getExpressionValue(expr: Node | undefined): string {
|
|
1306
|
+
if (!expr) return 'undefined';
|
|
1307
|
+
if (expr.type === 'StringLiteral') return `"${(expr as { value: string }).value}"`;
|
|
1308
|
+
if (expr.type === 'NumericLiteral') return String((expr as { value: number }).value);
|
|
1309
|
+
if (expr.type === 'BooleanLiteral') return String((expr as { value: boolean }).value);
|
|
1310
|
+
if (expr.type === 'NullLiteral') return 'null';
|
|
1311
|
+
if (expr.type === 'Identifier') return (expr as { name: string }).name;
|
|
1312
|
+
if (expr.type === 'ObjectExpression') return '{...}';
|
|
1313
|
+
if (expr.type === 'ArrayExpression') return '[...]';
|
|
1314
|
+
if (expr.type === 'ArrowFunctionExpression') return '() => {...}';
|
|
1315
|
+
if (expr.type === 'FunctionExpression') return 'function() {...}';
|
|
1316
|
+
return '<expression>';
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
/**
|
|
1320
|
+
* Add all analysis results to graph
|
|
1321
|
+
*/
|
|
1322
|
+
private async addToGraph(
|
|
1323
|
+
analysis: AnalysisResult,
|
|
1324
|
+
graph: PluginContext['graph'],
|
|
1325
|
+
moduleId: string | null
|
|
1326
|
+
): Promise<void> {
|
|
1327
|
+
// Add component nodes
|
|
1328
|
+
for (const component of analysis.components) {
|
|
1329
|
+
await graph.addNode(component as unknown as NodeRecord);
|
|
1330
|
+
if (moduleId) {
|
|
1331
|
+
await graph.addEdge({
|
|
1332
|
+
type: 'DEFINES',
|
|
1333
|
+
src: moduleId,
|
|
1334
|
+
dst: component.id
|
|
1335
|
+
});
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
// Add hook nodes
|
|
1340
|
+
for (const hook of analysis.hooks) {
|
|
1341
|
+
await graph.addNode(hook as unknown as NodeRecord);
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
// Add event nodes
|
|
1345
|
+
for (const event of analysis.events) {
|
|
1346
|
+
await graph.addNode(event as unknown as NodeRecord);
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
// Add browser API nodes
|
|
1350
|
+
for (const api of analysis.browserAPIs) {
|
|
1351
|
+
await graph.addNode(api as unknown as NodeRecord);
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
// Add issue nodes
|
|
1355
|
+
for (const issue of analysis.issues) {
|
|
1356
|
+
await graph.addNode(issue as unknown as NodeRecord);
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
// Add edges
|
|
1360
|
+
for (const edge of analysis.edges) {
|
|
1361
|
+
const { edgeType, ...rest } = edge;
|
|
1362
|
+
await graph.addEdge({
|
|
1363
|
+
type: edgeType,
|
|
1364
|
+
...rest
|
|
1365
|
+
});
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
}
|