use crate::model::Module;
use petgraph::Direction;
use petgraph::algo::toposort;
use petgraph::graph::{DiGraph, NodeIndex};
use std::collections::{HashMap, HashSet, VecDeque};
use std::path::PathBuf;
pub struct DependencyGraph {
graph: DiGraph<PathBuf, ()>,
node_indices: HashMap<PathBuf, NodeIndex>,
}
impl DependencyGraph {
pub fn build(modules: &[Module]) -> Self {
let mut graph = DiGraph::new();
let mut node_indices = HashMap::new();
for module in modules {
let idx = graph.add_node(module.path.clone());
node_indices.insert(module.path.clone(), idx);
}
for module in modules {
let from_idx = match node_indices.get(&module.path) {
Some(idx) => *idx,
None => continue,
};
for import in &module.imports {
if let Some(target_path) = resolve_import(import, modules) {
if let Some(to_idx) = node_indices.get(&target_path) {
graph.add_edge(from_idx, *to_idx, ());
}
}
}
}
Self {
graph,
node_indices,
}
}
pub fn graph(&self) -> &DiGraph<PathBuf, ()> {
&self.graph
}
pub fn node_indices(&self) -> &HashMap<PathBuf, NodeIndex> {
&self.node_indices
}
pub fn into_inner(self) -> DiGraph<PathBuf, ()> {
self.graph
}
pub fn fan_in(&self, path: &PathBuf) -> usize {
if let Some(idx) = self.node_indices.get(path) {
self.graph
.neighbors_directed(*idx, Direction::Incoming)
.count()
} else {
0
}
}
pub fn fan_out(&self, path: &PathBuf) -> usize {
if let Some(idx) = self.node_indices.get(path) {
self.graph
.neighbors_directed(*idx, Direction::Outgoing)
.count()
} else {
0
}
}
pub fn topological_order(&self) -> Option<Vec<PathBuf>> {
toposort(&self.graph, None).ok().map(|indices| {
indices
.into_iter()
.map(|idx| self.graph[idx].clone())
.collect()
})
}
pub fn topological_order_with_cycles(&self) -> Vec<PathBuf> {
match self.topological_order() {
Some(order) => order,
None => self.kahn_with_cycle_handling(),
}
}
fn kahn_with_cycle_handling(&self) -> Vec<PathBuf> {
let mut in_degree: HashMap<NodeIndex, usize> = HashMap::new();
let mut result = Vec::new();
let mut queue = VecDeque::new();
let mut visited = HashSet::new();
for idx in self.graph.node_indices() {
let degree = self
.graph
.neighbors_directed(idx, Direction::Incoming)
.count();
in_degree.insert(idx, degree);
if degree == 0 {
queue.push_back(idx);
}
}
while let Some(idx) = queue.pop_front() {
if visited.contains(&idx) {
continue;
}
visited.insert(idx);
result.push(self.graph[idx].clone());
for neighbor in self.graph.neighbors_directed(idx, Direction::Outgoing) {
if let Some(degree) = in_degree.get_mut(&neighbor) {
*degree = degree.saturating_sub(1);
if *degree == 0 && !visited.contains(&neighbor) {
queue.push_back(neighbor);
}
}
}
}
for idx in self.graph.node_indices() {
if !visited.contains(&idx) {
result.push(self.graph[idx].clone());
}
}
result
}
pub fn direct_dependents(&self, path: &PathBuf) -> Vec<PathBuf> {
if let Some(idx) = self.node_indices.get(path) {
self.graph
.neighbors_directed(*idx, Direction::Incoming)
.filter_map(|idx| self.graph.node_weight(idx).cloned())
.collect()
} else {
Vec::new()
}
}
pub fn contains(&self, path: &PathBuf) -> bool {
self.node_indices.contains_key(path)
}
pub fn importance_score(&self, path: &PathBuf, modules: &[Module]) -> f64 {
let fan_in = self.fan_in(path) as f64;
let fan_out = self.fan_out(path) as f64;
let module = modules.iter().find(|m| &m.path == path);
let model_bonus = if let Some(m) = module {
if m.name.contains("model")
|| m.name.contains("types")
|| m.name.contains("schema")
|| m.name.contains("entity")
{
10.0
} else {
0.0
}
} else {
0.0
};
let data_structure_bonus = if let Some(m) = module {
m.definitions
.iter()
.filter(|d| {
matches!(
d.kind,
crate::model::DefinitionKind::Struct | crate::model::DefinitionKind::Enum
)
})
.count() as f64
* 1.5
} else {
0.0
};
fan_in * 2.0 + fan_out + model_bonus + data_structure_bonus
}
}
fn resolve_import(import: &str, modules: &[Module]) -> Option<PathBuf> {
let segments: Vec<&str> = import.split("::").collect();
if segments.is_empty() {
return None;
}
let module_segments = if segments[0] == "crate" && segments.len() > 1 {
&segments[1..]
} else if segments[0] == "super" || segments[0] == "self" {
return None;
} else {
return None;
};
if module_segments.is_empty() {
return None;
}
let first_segment = module_segments[0].to_lowercase();
modules
.iter()
.find(|m| {
let path_str = m.path.to_string_lossy().to_lowercase();
let is_mod_file = path_str.ends_with(&format!("/{}/mod.rs", first_segment))
|| path_str.ends_with(&format!("\\{}\\mod.rs", first_segment));
let is_direct_file = path_str.ends_with(&format!("/{}.rs", first_segment))
|| path_str.ends_with(&format!("\\{}.rs", first_segment));
let is_submodule = if module_segments.len() > 1 {
let second_segment = module_segments[1].to_lowercase();
path_str.ends_with(&format!("/{}/{}.rs", first_segment, second_segment))
|| path_str.ends_with(&format!("\\{}\\{}.rs", first_segment, second_segment))
} else {
false
};
is_mod_file || is_direct_file || is_submodule
})
.map(|m| m.path.clone())
}