UNPKG

sicua

Version:

A tool for analyzing project structure and dependencies

303 lines (302 loc) 11.5 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.detectZombieComponentClusters = detectZombieComponentClusters; const analysisUtils_1 = require("../../../utils/common/analysisUtils"); const graphUtils_1 = require("../utils/graphUtils"); /** * Detects zombie component clusters using optimized lookups * @param components The list of components to analyze * @param lookupService Pre-initialized lookup service for O(1) component resolution * @returns Complete zombie cluster analysis result */ function detectZombieComponentClusters(components, lookupService) { const context = { graph: {}, allNodes: new Set(), functionToComponent: {}, nodes: [], edges: [], processedNodes: new Set(), visited: new Set(), }; // Build the graph and collect nodes using optimized operations buildGraphAndCollectNodes(components, context, lookupService); // Find entry points - nodes with no incoming edges const entryPoints = findEntryPoints(context); // Mark reachable nodes via DFS from entry points markReachableNodes(entryPoints, context); // Find and process zombie clusters const unvisited = Array.from(context.allNodes).filter((c) => !context.visited.has(c)); const clusters = []; let currentY = 0; const clusterSpacing = 400; processZombieClusters(unvisited, context, lookupService, clusters, currentY, clusterSpacing); // Create the complete zombie cluster graph const zombieClusterGraph = { nodes: context.nodes, edges: context.edges, version: "1.1.0", }; // Calculate statistics const totalZombieComponents = clusters.reduce((sum, cluster) => sum + cluster.components.length, 0); const largestClusterSize = clusters.length > 0 ? Math.max(...clusters.map((c) => c.components.length)) : 0; return { zombieClusterGraph, clusters, stats: { totalClusters: clusters.length, totalZombieComponents, largestCluster: largestClusterSize, entryPointsCount: entryPoints.size, avgComponentsPerCluster: clusters.length > 0 ? totalZombieComponents / clusters.length : 0, }, }; } /** * Builds the component and function graph using optimized lookups */ function buildGraphAndCollectNodes(components, context, lookupService) { for (const component of components) { const componentId = (0, analysisUtils_1.generateComponentId)(component); context.allNodes.add(componentId); // Resolve imports to component IDs using O(1) lookups context.graph[componentId] = component.imports .flatMap((imp) => lookupService.resolveImportToComponentIds(imp)) .filter((targetId) => targetId !== componentId); // Exclude self-references // Process functions if available if (Array.isArray(component.functions) && component.functionCalls) { for (const func of component.functions) { const fullName = `${componentId}.${func}`; context.allNodes.add(fullName); context.functionToComponent[fullName] = componentId; context.graph[fullName] = (component.functionCalls[func] || []).map((callee) => `${componentId}.${callee}`); } } } } /** * Finds entry points - nodes with no incoming edges */ function findEntryPoints(context) { const entryPoints = new Set(context.allNodes); // Remove nodes that have incoming edges for (const targets of Object.values(context.graph)) { for (const target of targets) { entryPoints.delete(target); } } return entryPoints; } /** * Marks reachable nodes from entry points via optimized DFS */ function markReachableNodes(entryPoints, context) { const dfs = (node) => { if (context.visited.has(node)) return; context.visited.add(node); const neighbors = context.graph[node] || []; for (const neighbor of neighbors) { dfs(neighbor); } }; for (const entryPoint of entryPoints) { dfs(entryPoint); } } /** * Processes zombie clusters using optimized lookups */ function processZombieClusters(unvisited, context, lookupService, clusters, currentY, clusterSpacing) { let clusterIndex = 0; while (unvisited.length > 0) { const clusterContext = { clusterIndex, unvisited, clusterId: `cluster-${clusterIndex}`, }; const cluster = processCluster(clusterContext, context, lookupService, currentY + clusterIndex * clusterSpacing); // Convert component IDs back to readable names using O(1) lookups const readableComponentNames = cluster.components .filter((nodeId) => !nodeId.includes(".")) // Only component nodes, not functions .map((componentId) => { const component = lookupService.getComponentById(componentId); return component?.name || componentId; }); // Create ZombieClusterInfo object const clusterInfo = { id: clusterContext.clusterId, components: readableComponentNames, entryPoints: [], functions: cluster.functions, size: readableComponentNames.length, risk: readableComponentNames.length > 5 ? "high" : readableComponentNames.length > 2 ? "medium" : "low", suggestion: getSuggestionForCluster(readableComponentNames.length), }; clusters.push(clusterInfo); clusterIndex++; } } /** * Processes a single zombie cluster using optimized lookups */ function processCluster(clusterContext, context, lookupService, yPosition) { const cluster = []; const queue = [clusterContext.unvisited[0]]; const functions = {}; const processedComponents = new Set(); // Create cluster parent node const clusterNodeData = { label: `Zombie Cluster ${clusterContext.clusterIndex + 1}`, fullPath: clusterContext.clusterId, directory: "", }; const clusterNode = { id: clusterContext.clusterId, position: { x: 0, y: yPosition }, data: clusterNodeData, type: "cluster", }; context.nodes.push(clusterNode); // BFS to find all connected nodes in cluster while (queue.length > 0) { const node = queue.shift(); if (!cluster.includes(node)) { cluster.push(node); if (!context.processedNodes.has(node)) { processNode(node, clusterContext.clusterId, context, lookupService, functions, yPosition, processedComponents); context.processedNodes.add(node); } // Add neighbors to queue const neighbors = context.graph[node] || []; for (const neighbor of neighbors) { if (!cluster.includes(neighbor)) { queue.push(neighbor); } } } } // Remove processed nodes from unvisited for (const node of cluster) { const index = clusterContext.unvisited.indexOf(node); if (index > -1) { clusterContext.unvisited.splice(index, 1); } } return { components: cluster.filter((node) => !node.includes(".")), functions, }; } /** * Processes a single node in a zombie cluster using O(1) lookups */ function processNode(node, clusterId, context, lookupService, functionsMap, clusterY, processedComponents) { const isFunction = node.includes("."); if (isFunction) { const [componentId, funcName] = node.split("."); // Get component data using O(1) lookup const componentData = lookupService.getComponentById(componentId); const directory = componentData?.directory || ""; // Ensure parent component node exists first if (!processedComponents.has(componentId)) { const parentNodeData = { label: componentData?.name || componentId, fullPath: componentData?.fullPath || componentId, directory: componentData?.directory || "", isComponent: true, }; const parentComponentNode = { id: componentId, position: { x: 150, y: clusterY + 50 }, data: parentNodeData, parentNode: clusterId, extent: "parent", type: "zombie", }; context.nodes.push(parentComponentNode); // Add edge from cluster to component const clusterToComponentEdge = { id: (0, graphUtils_1.generateEdgeId)(clusterId, componentId), source: clusterId, target: componentId, }; context.edges.push(clusterToComponentEdge); processedComponents.add(componentId); } // Add function node as child of component const functionNodeData = { label: funcName, fullPath: node, directory, }; const functionNode = { id: node, position: { x: 200, y: clusterY + 100 }, data: functionNodeData, parentNode: componentId, extent: "parent", type: "function", }; context.nodes.push(functionNode); // Add edge from component to function const edge = { id: (0, graphUtils_1.generateEdgeId)(componentId, node), source: componentId, target: node, data: { type: "export" }, }; context.edges.push(edge); // Add function to the functions map using readable component name const componentName = componentData?.name || componentId; if (!functionsMap[componentName]) { functionsMap[componentName] = []; } functionsMap[componentName].push(funcName); } else { // Handle component node using O(1) lookup const componentData = lookupService.getComponentById(node); const nodeData = { label: componentData?.name || node, fullPath: componentData?.fullPath || node, directory: componentData?.directory || "", isComponent: true, }; const componentNode = { id: node, position: { x: 150, y: clusterY + 50 }, data: nodeData, parentNode: clusterId, extent: "parent", type: "zombie", }; context.nodes.push(componentNode); // Add edge from cluster to component const edge = { id: (0, graphUtils_1.generateEdgeId)(clusterId, node), source: clusterId, target: node, }; context.edges.push(edge); processedComponents.add(node); } } /** * Get a suggestion based on the cluster size */ function getSuggestionForCluster(size) { if (size > 5) { return "Consider refactoring this large zombie cluster into smaller, reusable modules with clear entry points."; } else if (size > 2) { return "These components should either be connected to the main application or removed if unused."; } return "This small zombie cluster may be unused code that can be safely removed."; }