use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct ModuleDependency {
pub from_module: String,
pub to_module: String,
}
impl ModuleDependency {
pub fn new(from: impl Into<String>, to: impl Into<String>) -> Self {
Self {
from_module: from.into(),
to_module: to.into(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LayeringAnalysis {
pub score: f64,
pub backward_dep_count: usize,
pub total_dep_count: usize,
pub problematic_modules: Vec<(String, usize)>,
}
impl Default for LayeringAnalysis {
fn default() -> Self {
Self {
score: 1.0,
backward_dep_count: 0,
total_dep_count: 0,
problematic_modules: vec![],
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct LayeringImpact {
pub backward_deps_caused: usize,
pub affected_modules: Vec<String>,
pub estimated_improvement: f64,
pub current_score: f64,
}
impl Default for LayeringImpact {
fn default() -> Self {
Self {
backward_deps_caused: 0,
affected_modules: vec![],
estimated_improvement: 0.0,
current_score: 1.0,
}
}
}
pub fn compute_layering_score(dependencies: &[ModuleDependency]) -> LayeringAnalysis {
if dependencies.is_empty() {
return LayeringAnalysis::default();
}
let mut modules: HashSet<&str> = HashSet::new();
for dep in dependencies {
modules.insert(&dep.from_module);
modules.insert(&dep.to_module);
}
let mut module_list: Vec<&str> = modules.into_iter().collect();
module_list.sort();
let module_index: HashMap<&str, usize> = module_list
.iter()
.enumerate()
.map(|(i, m)| (*m, i))
.collect();
let ordering = compute_topological_order(dependencies, &module_list, &module_index);
let position: HashMap<&str, usize> = ordering
.iter()
.enumerate()
.map(|(pos, &module)| (module, pos))
.collect();
let mut forward_deps = 0;
let mut backward_deps = 0;
let mut backward_by_module: HashMap<&str, usize> = HashMap::new();
for dep in dependencies {
if dep.from_module == dep.to_module {
continue; }
let from_pos = position.get(dep.from_module.as_str()).copied().unwrap_or(0);
let to_pos = position.get(dep.to_module.as_str()).copied().unwrap_or(0);
if from_pos > to_pos {
forward_deps += 1;
} else {
backward_deps += 1;
*backward_by_module.entry(&dep.from_module).or_insert(0) += 1;
}
}
let total_deps = forward_deps + backward_deps;
let score = if total_deps > 0 {
forward_deps as f64 / total_deps as f64
} else {
1.0
};
let mut problematic_modules: Vec<(String, usize)> = backward_by_module
.into_iter()
.map(|(m, c)| (m.to_string(), c))
.collect();
problematic_modules.sort_by(|a, b| b.1.cmp(&a.1));
LayeringAnalysis {
score,
backward_dep_count: backward_deps,
total_dep_count: total_deps,
problematic_modules,
}
}
fn compute_topological_order<'a>(
dependencies: &[ModuleDependency],
modules: &[&'a str],
module_index: &HashMap<&str, usize>,
) -> Vec<&'a str> {
let n = modules.len();
let mut in_degree = vec![0usize; n];
let mut adj: Vec<Vec<usize>> = vec![Vec::new(); n];
for dep in dependencies {
if dep.from_module == dep.to_module {
continue;
}
if let (Some(&from_idx), Some(&to_idx)) = (
module_index.get(dep.from_module.as_str()),
module_index.get(dep.to_module.as_str()),
) {
adj[to_idx].push(from_idx);
in_degree[from_idx] += 1;
}
}
let mut queue: Vec<usize> = (0..n).filter(|&i| in_degree[i] == 0).collect();
queue.sort();
let mut order = Vec::with_capacity(n);
while let Some(node) = queue.pop() {
order.push(modules[node]);
for &neighbor in &adj[node] {
in_degree[neighbor] -= 1;
if in_degree[neighbor] == 0 {
let pos = queue.partition_point(|&x| x < neighbor);
queue.insert(pos, neighbor);
}
}
}
if order.len() < n {
for (i, &module) in modules.iter().enumerate() {
if !order.contains(&module) {
order.push(modules[i]);
}
}
}
order
}
pub fn path_to_module(path: &str) -> String {
let path = std::path::Path::new(path);
path.parent()
.and_then(|p| p.to_str())
.map(|s| {
let s = s.strip_prefix("./").unwrap_or(s);
let s = s.strip_prefix("src/").unwrap_or(s);
if s.is_empty() || s == "src" {
"root".to_string()
} else {
s.to_string()
}
})
.unwrap_or_else(|| "root".to_string())
}
pub fn compute_layering_impact(module: &str, dependencies: &[ModuleDependency]) -> LayeringImpact {
let module_deps: Vec<_> = dependencies
.iter()
.filter(|d| d.from_module == module || d.to_module == module)
.cloned()
.collect();
if module_deps.is_empty() {
return LayeringImpact::default();
}
let current_analysis = compute_layering_score(dependencies);
let deps_without_module: Vec<_> = dependencies
.iter()
.filter(|d| d.from_module != module && d.to_module != module)
.cloned()
.collect();
let analysis_without = compute_layering_score(&deps_without_module);
let backward_caused: Vec<_> = current_analysis
.problematic_modules
.iter()
.filter(|(m, _)| m == module)
.cloned()
.collect();
let backward_count = backward_caused.first().map(|(_, c)| *c).unwrap_or(0);
let affected: Vec<String> = module_deps
.iter()
.flat_map(|d| {
if d.from_module == module {
Some(d.to_module.clone())
} else {
Some(d.from_module.clone())
}
})
.collect::<HashSet<_>>()
.into_iter()
.collect();
LayeringImpact {
backward_deps_caused: backward_count,
affected_modules: affected,
estimated_improvement: analysis_without.score - current_analysis.score,
current_score: current_analysis.score,
}
}
pub fn calculate_layering_penalty(backward_deps: usize) -> f64 {
match backward_deps {
0..=2 => 0.0,
3..=4 => 0.15,
_ => 0.20,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_perfect_layering_returns_1() {
let deps = vec![
ModuleDependency::new("b", "a"),
ModuleDependency::new("c", "a"),
ModuleDependency::new("c", "b"),
];
let result = compute_layering_score(&deps);
assert_eq!(result.score, 1.0);
assert_eq!(result.backward_dep_count, 0);
}
#[test]
fn test_cyclic_deps_returns_low_score() {
let deps = vec![
ModuleDependency::new("a", "b"),
ModuleDependency::new("b", "c"),
ModuleDependency::new("c", "a"), ];
let result = compute_layering_score(&deps);
assert!(result.score < 1.0, "Cyclic graph should have backward deps");
assert!(result.backward_dep_count > 0);
}
#[test]
fn test_mixed_layering() {
let deps = vec![
ModuleDependency::new("b", "a"), ModuleDependency::new("c", "b"), ModuleDependency::new("a", "c"), ];
let result = compute_layering_score(&deps);
assert!(result.score > 0.0);
assert!(result.score < 1.0);
assert!(result.backward_dep_count > 0);
}
#[test]
fn test_empty_deps_returns_perfect() {
let deps: Vec<ModuleDependency> = vec![];
let result = compute_layering_score(&deps);
assert_eq!(result.score, 1.0);
assert_eq!(result.backward_dep_count, 0);
}
#[test]
fn test_self_deps_ignored() {
let deps = vec![
ModuleDependency::new("a", "a"),
ModuleDependency::new("b", "a"),
];
let result = compute_layering_score(&deps);
assert_eq!(result.total_dep_count, 1);
}
#[test]
fn test_path_to_module_extracts_correctly() {
assert_eq!(path_to_module("src/analysis/dsm.rs"), "analysis");
assert_eq!(path_to_module("src/cli/args.rs"), "cli");
assert_eq!(path_to_module("src/main.rs"), "root");
assert_eq!(path_to_module("./src/lib.rs"), "root");
assert_eq!(path_to_module("src/io/writers/dsm.rs"), "io/writers");
}
#[test]
fn test_problematic_modules_sorted() {
let deps = vec![
ModuleDependency::new("a", "b"),
ModuleDependency::new("a", "c"),
ModuleDependency::new("a", "d"),
ModuleDependency::new("b", "d"),
];
let result = compute_layering_score(&deps);
if result.problematic_modules.len() > 1 {
let counts: Vec<usize> = result.problematic_modules.iter().map(|(_, c)| *c).collect();
for i in 1..counts.len() {
assert!(counts[i - 1] >= counts[i]);
}
}
}
#[test]
fn test_layering_penalty_thresholds() {
assert_eq!(calculate_layering_penalty(0), 0.0);
assert_eq!(calculate_layering_penalty(1), 0.0);
assert_eq!(calculate_layering_penalty(2), 0.0);
assert_eq!(calculate_layering_penalty(3), 0.15);
assert_eq!(calculate_layering_penalty(4), 0.15);
assert_eq!(calculate_layering_penalty(5), 0.20);
assert_eq!(calculate_layering_penalty(10), 0.20);
}
#[test]
fn test_layering_impact_empty() {
let impact = compute_layering_impact("nonexistent", &[]);
assert_eq!(impact.backward_deps_caused, 0);
assert!(impact.affected_modules.is_empty());
}
#[test]
fn test_layering_impact_with_deps() {
let deps = vec![
ModuleDependency::new("god_object", "utils"),
ModuleDependency::new("god_object", "config"),
ModuleDependency::new("utils", "config"),
];
let impact = compute_layering_impact("god_object", &deps);
assert!(!impact.affected_modules.is_empty());
}
#[test]
fn test_deterministic_results() {
let deps = vec![
ModuleDependency::new("a", "b"),
ModuleDependency::new("b", "c"),
ModuleDependency::new("c", "a"),
];
let result1 = compute_layering_score(&deps);
let result2 = compute_layering_score(&deps);
assert_eq!(result1.score, result2.score);
assert_eq!(result1.backward_dep_count, result2.backward_dep_count);
}
}