Cycle detection over the runtime import graph (LAY-ADR SS7 rule 2).
Only module-level runtime imports create import-time cycles, so lazy
(function-scoped) and if TYPE_CHECKING: imports are excluded from the
graph. Detection uses an iterative Tarjan SCC to stay stack-safe on large
trees.
strongly_connected_components(graph: Mapping[str, Iterable[str]]) -> list[list[str]]
Iterative Tarjan; returns every SCC (singletons included).
Source code in src/symfonic/devtools/archcheck/cycles.py
| def strongly_connected_components(graph: Mapping[str, Iterable[str]]) -> list[list[str]]:
"""Iterative Tarjan; returns every SCC (singletons included)."""
index: dict[str, int] = {}
lowlink: dict[str, int] = {}
on_stack: set[str] = set()
stack: list[str] = []
counter = 0
sccs: list[list[str]] = []
for root in graph:
if root in index:
continue
work: list[tuple[str, Iterator[str]]] = [(root, iter(graph.get(root, ())))]
index[root] = lowlink[root] = counter
counter += 1
stack.append(root)
on_stack.add(root)
while work:
node, edge_iter = work[-1]
advanced = False
for successor in edge_iter:
if successor not in graph:
continue
if successor not in index:
index[successor] = lowlink[successor] = counter
counter += 1
stack.append(successor)
on_stack.add(successor)
work.append((successor, iter(graph.get(successor, ()))))
advanced = True
break
if successor in on_stack:
lowlink[node] = min(lowlink[node], index[successor])
if advanced:
continue
work.pop()
if work:
parent = work[-1][0]
lowlink[parent] = min(lowlink[parent], lowlink[node])
if lowlink[node] == index[node]:
component: list[str] = []
while True:
member = stack.pop()
on_stack.discard(member)
component.append(member)
if member == node:
break
sccs.append(component)
return sccs
|