This commit is contained in:
141
src/analyzers/circularDetector.ts
Normal file
141
src/analyzers/circularDetector.ts
Normal file
@@ -0,0 +1,141 @@
|
||||
import { DependencyGraph, GraphEdge, CircularDependency } from '../types';
|
||||
|
||||
export class CircularDependencyDetector {
|
||||
private graph: DependencyGraph;
|
||||
private visited: Set<string>;
|
||||
private recursionStack: Set<string>;
|
||||
private cycles: CircularDependency[];
|
||||
|
||||
constructor(graph: DependencyGraph) {
|
||||
this.graph = graph;
|
||||
this.visited = new Set<string>();
|
||||
this.recursionStack = new Set<string>();
|
||||
this.cycles = [];
|
||||
}
|
||||
|
||||
detect(): CircularDependency[] {
|
||||
this.visited.clear();
|
||||
this.recursionStack.clear();
|
||||
this.cycles = [];
|
||||
|
||||
const nodes = Array.from(this.graph.nodes.keys());
|
||||
|
||||
for (const nodeId of nodes) {
|
||||
if (!this.visited.has(nodeId)) {
|
||||
this.detectCycle(nodeId, []);
|
||||
}
|
||||
}
|
||||
|
||||
return this.cycles;
|
||||
}
|
||||
|
||||
private detectCycle(nodeId: string, path: string[]): void {
|
||||
this.visited.add(nodeId);
|
||||
this.recursionStack.add(nodeId);
|
||||
path.push(nodeId);
|
||||
|
||||
const outgoingEdges = this.getOutgoingEdges(nodeId);
|
||||
|
||||
for (const edge of outgoingEdges) {
|
||||
if (!this.visited.has(edge.target)) {
|
||||
this.detectCycle(edge.target, [...path]);
|
||||
} else if (this.recursionStack.has(edge.target)) {
|
||||
const cycleStartIndex = path.indexOf(edge.target);
|
||||
const cycle = path.slice(cycleStartIndex);
|
||||
|
||||
const existingCycle = this.findEquivalentCycle(cycle);
|
||||
if (!existingCycle) {
|
||||
this.cycles.push({
|
||||
cycle: [...cycle, edge.target],
|
||||
nodes: cycle,
|
||||
edges: this.getEdgesForCycle(cycle),
|
||||
length: cycle.length
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.recursionStack.delete(nodeId);
|
||||
}
|
||||
|
||||
private getOutgoingEdges(nodeId: string): GraphEdge[] {
|
||||
return this.graph.edges.filter((edge) => edge.source === nodeId);
|
||||
}
|
||||
|
||||
private findEquivalentCycle(cycle: string[]): CircularDependency | undefined {
|
||||
const normalizedCycle = this.normalizeCycle(cycle);
|
||||
|
||||
return this.cycles.find((existing) => {
|
||||
const normalizedExisting = this.normalizeCycle(existing.cycle);
|
||||
return this.cyclesEqual(normalizedCycle, normalizedExisting);
|
||||
});
|
||||
}
|
||||
|
||||
private normalizeCycle(cycle: string[]): string[] {
|
||||
if (cycle.length <= 1) return cycle;
|
||||
const minIndex = cycle.findIndex((node) => {
|
||||
const nodeNum = parseInt(node.split('#').pop() || '0', 10);
|
||||
const minVal = Math.min(...cycle.map((n) => parseInt(n.split('#').pop() || '0', 10)));
|
||||
return nodeNum === minVal;
|
||||
});
|
||||
const normalized = cycle.slice(minIndex).concat(cycle.slice(0, minIndex));
|
||||
return normalized.length > 1 && normalized[0] > normalized[normalized.length - 1]
|
||||
? normalized.reverse()
|
||||
: normalized;
|
||||
}
|
||||
|
||||
private cyclesEqual(cycle1: string[], cycle2: string[]): boolean {
|
||||
if (cycle1.length !== cycle2.length) {
|
||||
return false;
|
||||
}
|
||||
return cycle1.every((node, index) => node === cycle2[index]);
|
||||
}
|
||||
|
||||
private getEdgesForCycle(cycle: string[]): GraphEdge[] {
|
||||
const edges: GraphEdge[] = [];
|
||||
for (let i = 0; i < cycle.length - 1; i++) {
|
||||
const edge = this.graph.edges.find(
|
||||
(e) => e.source === cycle[i] && e.target === cycle[i + 1]
|
||||
);
|
||||
if (edge) {
|
||||
edges.push(edge);
|
||||
}
|
||||
}
|
||||
return edges;
|
||||
}
|
||||
|
||||
getShortestCycles(): CircularDependency[] {
|
||||
return this.cycles.sort((a, b) => a.length - b.length);
|
||||
}
|
||||
|
||||
getLongestCycles(): CircularDependency[] {
|
||||
return this.cycles.sort((a, b) => b.length - a.length);
|
||||
}
|
||||
|
||||
getCyclesByNode(nodeId: string): CircularDependency[] {
|
||||
return this.cycles.filter((cycle) => cycle.nodes.includes(nodeId));
|
||||
}
|
||||
|
||||
getCycleCount(): number {
|
||||
return this.cycles.length;
|
||||
}
|
||||
|
||||
getAffectedNodeCount(): number {
|
||||
const affectedNodes = new Set<string>();
|
||||
for (const cycle of this.cycles) {
|
||||
for (const node of cycle.nodes) {
|
||||
affectedNodes.add(node);
|
||||
}
|
||||
}
|
||||
return affectedNodes.size;
|
||||
}
|
||||
|
||||
hasCircularDependencies(): boolean {
|
||||
return this.cycles.length > 0;
|
||||
}
|
||||
}
|
||||
|
||||
export function detectCircularDependencies(graph: DependencyGraph): CircularDependency[] {
|
||||
const detector = new CircularDependencyDetector(graph);
|
||||
return detector.detect();
|
||||
}
|
||||
Reference in New Issue
Block a user