Add graph, analyzers, and exporters modules
Some checks failed
CI / test (push) Has been cancelled
CI / build (push) Has been cancelled

This commit is contained in:
2026-02-02 02:40:22 +00:00
parent cd4cffa80d
commit fbecb42d6c

View File

@@ -0,0 +1,144 @@
from dataclasses import dataclass, field
from pathlib import Path
import networkx as nx
from src.graph.builder import GraphBuilder, NodeType
@dataclass
class DependencyReport:
total_files: int = 0
total_functions: int = 0
total_classes: int = 0
circular_dependencies: list[list[str]] = field(default_factory=list)
orphan_files: list[str] = field(default_factory=list)
dependency_cycles: list[list[Path]] = field(default_factory=list)
modules: dict[str, list[str]] = field(default_factory=dict)
warnings: list[str] = field(default_factory=list)
class DependencyAnalyzer:
def __init__(self, graph_builder: GraphBuilder):
self.graph_builder = graph_builder
self.graph = graph_builder.get_graph()
self.report = DependencyReport()
def analyze(self) -> DependencyReport:
self.report = DependencyReport()
self._count_entities()
self._detect_circular_dependencies()
self._find_orphan_files()
self._identify_modules()
return self.report
def _count_entities(self) -> None:
nodes = self.graph_builder.get_nodes()
for node in nodes:
if node.node_type == NodeType.FILE:
self.report.total_files += 1
elif node.node_type == NodeType.FUNCTION:
self.report.total_functions += 1
elif node.node_type == NodeType.CLASS:
self.report.total_classes += 1
def _detect_circular_dependencies(self) -> None:
try:
cycles = list(nx.simple_cycles(self.graph))
for cycle in cycles:
if len(cycle) > 1:
cycle_paths = []
for node_id in cycle:
node = self.graph_builder.get_node_by_id(node_id)
if node and node.file_path:
cycle_paths.append(node.file_path)
if cycle_paths:
self.report.circular_dependencies.append(cycle)
self.report.dependency_cycles.append(cycle_paths)
if cycles:
self.report.warnings.append(f"Found {len(cycles)} circular dependency cycle(s)")
except Exception:
pass
def _find_orphan_files(self) -> None:
nodes = self.graph_builder.get_nodes()
for node in nodes:
if node.node_type == NodeType.FILE:
predecessors = list(self.graph.predecessors(node.node_id))
import_edges = [
e for e in self.graph_builder.edges
if e.source == node.node_id and e.edge_type == "imports"
]
if not import_edges and not predecessors:
if node.file_path:
self.report.orphan_files.append(str(node.file_path))
def _identify_modules(self) -> None:
nodes = self.graph_builder.get_nodes()
for node in nodes:
if node.node_type == NodeType.FILE and node.file_path:
parts = node.file_path.parts
module_name = parts[0] if parts else "root"
if module_name not in self.report.modules:
self.report.modules[module_name] = []
self.report.modules[module_name].append(str(node.file_path))
def get_file_dependencies(self, file_path: Path) -> list[tuple[str, str]]:
dependencies = []
nodes = self.graph_builder.get_nodes()
file_node_id = f"file_{file_path}"
for node in nodes:
if node.node_id == file_node_id:
for edge in self.graph_builder.edges:
if edge.source == file_node_id and edge.edge_type == "imports":
target_node = self.graph_builder.get_node_by_id(edge.target)
if target_node:
dependencies.append((target_node.name, edge.label))
break
return dependencies
def get_file_dependents(self, file_path: Path) -> list[str]:
dependents = []
file_node_id = f"file_{file_path}"
for edge in self.graph_builder.edges:
if edge.target == file_node_id and edge.edge_type == "imports":
source_node = self.graph_builder.get_node_by_id(edge.source)
if source_node:
dependents.append(source_node.name)
return dependents
def get_call_graph(self) -> nx.Graph:
call_graph = nx.DiGraph()
for edge in self.graph_builder.edges:
if edge.edge_type == "calls":
call_graph.add_edge(edge.source, edge.target)
return call_graph
def get_architecture_layers(self) -> dict[str, list[str]]:
layers = {"presentation": [], "business": [], "data": [], "other": []}
layer_keywords = {
"presentation": ["ui", "view", "controller", "handler", "route", "api"],
"business": ["service", "business", "logic", "manager", "facade"],
"data": ["repository", "dao", "model", "entity", "database", "db"],
}
for node in self.graph_builder.get_nodes():
if node.node_type == NodeType.FILE:
path_lower = str(node.file_path).lower() if node.file_path else ""
for layer, keywords in layer_keywords.items():
if any(kw in path_lower for kw in keywords):
layers[layer].append(node.name)
break
else:
layers["other"].append(node.name)
return {k: v for k, v in layers.items() if v}