Add analyzer modules
Some checks failed
CI / test (push) Has been cancelled

This commit is contained in:
2026-01-30 00:58:57 +00:00
parent eaebae1833
commit 832d62f25c

View 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();
}