* Find all circular dependencies in the graph using DFS with recursion stack. * Returns unique cycles (avoids duplicates like A->B->A and B->A->B). * * @param scope Optional path prefix to limit analysis (e.g., 'src/features')
(scope?: string)
| 742 | * @param scope Optional path prefix to limit analysis (e.g., 'src/features') |
| 743 | */ |
| 744 | findCycles(scope?: string): CyclePath[] { |
| 745 | const visited = new Set<string>(); |
| 746 | const recursionStack = new Set<string>(); |
| 747 | const cycles: CyclePath[] = []; |
| 748 | const cycleSignatures = new Set<string>(); // To avoid duplicates |
| 749 | |
| 750 | // Get all files to check |
| 751 | let filesToCheck = Array.from(this.imports.keys()); |
| 752 | if (scope) { |
| 753 | filesToCheck = filesToCheck.filter((f) => f.startsWith(scope)); |
| 754 | } |
| 755 | |
| 756 | const dfs = (node: string, path: string[]): void => { |
| 757 | visited.add(node); |
| 758 | recursionStack.add(node); |
| 759 | path.push(node); |
| 760 | |
| 761 | const neighbors = this.imports.get(node) || new Set<string>(); |
| 762 | for (const neighbor of neighbors) { |
| 763 | // Apply scope filter to neighbors too |
| 764 | if (scope && !neighbor.startsWith(scope)) continue; |
| 765 | |
| 766 | if (recursionStack.has(neighbor)) { |
| 767 | // Found a cycle! Extract just the cycle portion |
| 768 | const cycleStart = path.indexOf(neighbor); |
| 769 | if (cycleStart !== -1) { |
| 770 | const cyclePath = [...path.slice(cycleStart), neighbor]; |
| 771 | |
| 772 | // Create a normalized signature to avoid duplicate cycles |
| 773 | // Sort by the minimum element to create a canonical form |
| 774 | const normalized = [...cyclePath.slice(0, -1)].sort(); |
| 775 | const signature = normalized.join('|'); |
| 776 | |
| 777 | if (!cycleSignatures.has(signature)) { |
| 778 | cycleSignatures.add(signature); |
| 779 | cycles.push({ |
| 780 | files: cyclePath, |
| 781 | length: cyclePath.length - 1 // -1 because last element = first element |
| 782 | }); |
| 783 | } |
| 784 | } |
| 785 | } else if (!visited.has(neighbor)) { |
| 786 | dfs(neighbor, path); |
| 787 | } |
| 788 | } |
| 789 | |
| 790 | path.pop(); |
| 791 | recursionStack.delete(node); |
| 792 | }; |
| 793 | |
| 794 | for (const file of filesToCheck) { |
| 795 | if (!visited.has(file)) { |
| 796 | dfs(file, []); |
| 797 | } |
| 798 | } |
| 799 | |
| 800 | // Sort by cycle length (shorter cycles are often more problematic) |
| 801 | return cycles.sort((a, b) => a.length - b.length); |
no test coverage detected