sicua
Version:
A tool for analyzing project structure and dependencies
156 lines (155 loc) • 5.65 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.detectCircularDependencies = detectCircularDependencies;
const graphUtils_1 = require("../utils/graphUtils");
/**
* Detects circular dependencies in the component graph using optimized lookups
* @param graph The dependency graph with unique component IDs
* @param lookupService Pre-initialized lookup service for O(1) component resolution
* @returns Complete circular dependency analysis
*/
function detectCircularDependencies(graph, lookupService) {
const context = {
visited: {},
recursionStack: {},
nodesInCycles: new Set(),
edges: [],
};
const circularGroups = [];
// Optimized DFS with early cycle detection
const dfs = (nodeId, path) => {
context.visited[nodeId] = true;
context.recursionStack[nodeId] = true;
path.push(nodeId);
const neighbors = graph[nodeId];
if (neighbors) {
for (const neighborId of neighbors) {
if (!context.visited[neighborId]) {
dfs(neighborId, path);
}
else if (context.recursionStack[neighborId]) {
// Found cycle - extract it efficiently
const cycleStart = path.indexOf(neighborId);
const cycleNodes = path.slice(cycleStart);
// Mark all nodes in cycle
for (const node of cycleNodes) {
context.nodesInCycles.add(node);
}
// Store circular group
circularGroups.push([...cycleNodes]);
// Create edges for visualization
createCircularEdges(cycleNodes, context.edges);
}
}
}
context.recursionStack[nodeId] = false;
path.pop();
};
// Run DFS on all unvisited nodes
const allNodeIds = Object.keys(graph);
for (const nodeId of allNodeIds) {
if (!context.visited[nodeId]) {
dfs(nodeId, []);
}
}
// Generate optimized node layout
const nodeIds = Array.from(context.nodesInCycles);
const positions = (0, graphUtils_1.generateCircularLayout)(nodeIds.length, 400, // centerX
300, // centerY
250 // radius
);
// Create nodes using O(1) lookups
const nodes = nodeIds.map((nodeId, index) => {
const component = lookupService.getComponentById(nodeId);
const nodeData = {
label: component?.name || nodeId,
fullPath: component?.fullPath || nodeId,
directory: component?.directory || "",
isComponent: true,
fileType: component?.fullPath
? component.fullPath.split(".").pop()
: undefined,
};
return {
id: nodeId,
position: positions[index],
data: nodeData,
type: "circular",
};
});
// Create detailed circular group information using O(1) lookups
const circularGroupInfos = circularGroups.map((group, index) => {
const componentNames = group.map((componentId) => {
const component = lookupService.getComponentById(componentId);
return component?.name || componentId;
});
return {
id: `circular-${index}`,
components: componentNames,
path: componentNames,
size: group.length,
isCritical: group.length > 3,
breakSuggestions: [
{
component: componentNames[0],
alternativeDesign: "Consider extracting common functionality into a shared utility",
},
],
};
});
// Create the complete graph data
const circularDependencyGraph = {
nodes: nodes,
edges: context.edges,
version: "1.1.0",
};
// Create stats with O(1) lookups
const componentsByCircularGroups = circularGroups.reduce((acc, group, i) => {
const componentNames = group.map((componentId) => {
const component = lookupService.getComponentById(componentId);
return component?.name || componentId;
});
acc[`circular-${i}`] = componentNames;
return acc;
}, {});
return {
circularDependencyGraph,
circularGroups: circularGroupInfos,
stats: {
totalCircularGroups: circularGroups.length,
totalComponentsInCircular: context.nodesInCycles.size,
maxCircularPathLength: Math.max(...circularGroups.map((g) => g.length), 0),
criticalCircularPaths: circularGroups.filter((g) => g.length > 3).length,
componentsByCircularGroups,
},
};
}
/**
* Create circular dependency edges efficiently
*/
function createCircularEdges(cycleNodes, edges) {
for (let i = 0; i < cycleNodes.length; i++) {
const currentNode = cycleNodes[i];
const nextNode = cycleNodes[(i + 1) % cycleNodes.length];
const edgeData = {
type: "import",
label: "circular",
};
const edge = {
id: (0, graphUtils_1.generateEdgeId)(currentNode, nextNode),
source: currentNode,
target: nextNode,
data: edgeData,
animated: true,
style: {
stroke: "#ff6b6b",
strokeWidth: 2,
},
markerEnd: {
type: "arrowclosed",
color: "#ff6b6b",
},
};
edges.push(edge);
}
}