Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 7 additions & 10 deletions src/archunitpython/common/projection/cycles/cycle_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,13 @@ def get_outgoing_neighbours(
def transform_edge_data(edges: list[NumberEdge]) -> list[NumberNode]:
"""Convert a list of edges into a list of nodes with in/out edges."""
unique_ids = CycleUtils.find_unique_nodes(edges)
nodes = []
for node_id in unique_ids:
nodes.append(
NumberNode(
node=node_id,
incoming=[e for e in edges if e.to_node == node_id],
outgoing=[e for e in edges if e.from_node == node_id],
)
)
return nodes
node_id_map = {node_id: NumberNode(node = node_id) for node_id in unique_ids}

for edge in edges:
node_id_map[edge.to_node].incoming.append(edge)
node_id_map[edge.from_node].outgoing.append(edge)

return list(node_id_map.values())

@staticmethod
def find_unique_nodes(edges: list[NumberEdge]) -> list[int]:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,11 @@ def _explore_neighbours(self, current_node: NumberNode) -> None:

def _unblock(self, node: NumberNode) -> None:
self._blocked = [n for n in self._blocked if n is not node]
to_remove: list[_BlockedBy] = []
to_remove: set[_BlockedBy] = set()
for blocker in self._blocked_map:
if blocker.by is node:
self._unblock(blocker.blocked)
to_remove.append(blocker)
to_remove.add(blocker)
self._blocked_map = [b for b in self._blocked_map if b not in to_remove]

def _is_part_of_current_start_cycle(self, current_node: NumberNode) -> bool:
Expand Down
32 changes: 20 additions & 12 deletions src/archunitpython/common/projection/cycles/tarjan_scc.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

from __future__ import annotations

from collections import defaultdict
from typing import Dict, Set

from archunitpython.common.projection.cycles.model import NumberEdge


Expand All @@ -12,7 +15,7 @@ def __init__(self, node_id: int) -> None:
self.id = node_id
self.index = -1
self.lowlink = -1
self.neighbours: list[int] = []
self.neighbours: Set[int] = set()


class TarjanSCC:
Expand All @@ -27,8 +30,11 @@ def find_strongly_connected_components(self, edges: list[NumberEdge]) -> list[li
self._graph: dict[int, _Vertex] = {}
self._index = 0
self._stack: list[_Vertex] = []
self._on_stack: Set[int] = set()
self._sccs: list[list[NumberEdge]] = []
self._edges = edges
self._outgoing_edge_map: Dict[int,list[NumberEdge]] = defaultdict(list)
for edge in edges:
self._outgoing_edge_map[edge.from_node].append(edge)

self._init(edges)

Expand All @@ -47,36 +53,38 @@ def _init(self, edges: list[NumberEdge]) -> None:
self._graph[edge.to_node] = _Vertex(edge.to_node)

v = self._graph[edge.from_node]
if edge.to_node not in v.neighbours:
v.neighbours.append(edge.to_node)
v.neighbours.add(edge.to_node)

def _visit(self, vertex: _Vertex) -> None:
"""DFS visit for Tarjan's algorithm."""
vertex.index = self._index
vertex.lowlink = self._index
self._index += 1
self._stack.append(vertex)
self._on_stack.add(vertex.id)

for neighbour_id in vertex.neighbours:
w = self._graph[neighbour_id]
if w.index < 0:
self._visit(w)
vertex.lowlink = min(vertex.lowlink, w.lowlink)
elif w in self._stack:
elif w.id in self._on_stack:
vertex.lowlink = min(vertex.lowlink, w.index)

if vertex.lowlink == vertex.index:
scc_vertices: list[_Vertex] = []
scc_ids: Set[int] = set()
while True:
w = self._stack.pop()
scc_vertices.append(w)
self._on_stack.remove(w.id)
scc_ids.add(w.id)
if w.id == vertex.id:
break

if scc_vertices:
scc_ids = {v.id for v in scc_vertices}
scc_edges = [
e for e in self._edges if e.from_node in scc_ids and e.to_node in scc_ids
]
if scc_ids:
scc_edges: list[NumberEdge] = []
for id in scc_ids:
for edge in self._outgoing_edge_map[id]:
if edge.to_node in scc_ids:
scc_edges.append(edge)
if scc_edges:
self._sccs.append(scc_edges)
18 changes: 8 additions & 10 deletions src/archunitpython/common/projection/project_nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,23 +27,21 @@ def project_to_nodes(
all_labels: set[str] = set()

for edge in graph:
if edge.external and not include_externals:
# Still record the source (internal file)
all_labels.add(edge.source)
outgoing[edge.source].append(edge)
continue

all_labels.add(edge.source)
all_labels.add(edge.target)
outgoing[edge.source].append(edge)
if edge.source != edge.target: # Don't count self-edges as incoming
incoming[edge.target].append(edge)
outgoing[edge.source].append(edge)

if (edge.external and include_externals) or not edge.external:
# Record the target
all_labels.add(edge.target)
if edge.source != edge.target: # Don't count self-edges as incoming
incoming[edge.target].append(edge)

return [
ProjectedNode(
label=label,
incoming=incoming.get(label, []),
outgoing=[e for e in outgoing.get(label, []) if e.source != e.target],
outgoing=outgoing.get(label, []),
)
for label in sorted(all_labels)
]