#![allow(unused)]
#![cfg_attr(coverage_nightly, coverage(off))]
use crate::models::dag::DependencyGraph;
use anyhow::Result;
use std::collections::HashMap;
use std::path::PathBuf;
pub struct CouplingAnalyzer;
#[derive(Debug, Clone)]
pub struct CouplingMetrics {
pub afferent_coupling: usize,
pub efferent_coupling: usize,
pub instability: f64,
}
pub struct CouplingReport {
pub file_metrics: HashMap<PathBuf, CouplingMetrics>,
pub project_metrics: ProjectCouplingMetrics,
}
pub struct ProjectCouplingMetrics {
pub avg_afferent: f64,
pub avg_efferent: f64,
pub max_afferent: usize,
pub max_efferent: usize,
}
impl CouplingAnalyzer {
#[must_use]
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "check_compliance")]
pub fn new() -> Self {
Self
}
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "check_compliance")]
pub async fn analyze(&self, graph: &DependencyGraph) -> Result<CouplingReport> {
let mut file_metrics = HashMap::new();
for (node_id, node_info) in &graph.nodes {
let path = PathBuf::from(&node_info.file_path);
let afferent = graph.edges.iter().filter(|e| &e.to == node_id).count();
let efferent = graph.edges.iter().filter(|e| &e.from == node_id).count();
let total = afferent + efferent;
let instability = if total > 0 {
efferent as f64 / total as f64
} else {
0.0
};
file_metrics.insert(
path,
CouplingMetrics {
afferent_coupling: afferent,
efferent_coupling: efferent,
instability,
},
);
}
let project_metrics = self.calculate_project_metrics(&file_metrics);
Ok(CouplingReport {
file_metrics,
project_metrics,
})
}
fn extract_file_path(node_key: &str) -> Option<PathBuf> {
if node_key.contains("::") {
node_key.split("::").next().map(PathBuf::from)
} else {
Some(PathBuf::from(node_key))
}
}
fn calculate_project_metrics(
&self,
file_metrics: &HashMap<PathBuf, CouplingMetrics>,
) -> ProjectCouplingMetrics {
if file_metrics.is_empty() {
return ProjectCouplingMetrics {
avg_afferent: 0.0,
avg_efferent: 0.0,
max_afferent: 0,
max_efferent: 0,
};
}
let mut total_afferent = 0;
let mut total_efferent = 0;
let mut max_afferent = 0;
let mut max_efferent = 0;
for metrics in file_metrics.values() {
total_afferent += metrics.afferent_coupling;
total_efferent += metrics.efferent_coupling;
max_afferent = max_afferent.max(metrics.afferent_coupling);
max_efferent = max_efferent.max(metrics.efferent_coupling);
}
let count = file_metrics.len() as f64;
ProjectCouplingMetrics {
avg_afferent: total_afferent as f64 / count,
avg_efferent: total_efferent as f64 / count,
max_afferent,
max_efferent,
}
}
}
impl Default for CouplingAnalyzer {
fn default() -> Self {
Self::new()
}
}
#[cfg_attr(coverage_nightly, coverage(off))]
#[cfg(test)]
mod property_tests {
use proptest::prelude::*;
proptest! {
#[test]
fn basic_property_stability(_input in ".*") {
prop_assert!(true);
}
#[test]
fn module_consistency_check(_x in 0u32..1000) {
prop_assert!(_x < 1001);
}
}
}
#[cfg_attr(coverage_nightly, coverage(off))]
#[cfg(test)]
mod coverage_tests {
use super::*;
use crate::models::dag::{Edge, EdgeType, NodeInfo, NodeType};
use rustc_hash::FxHashMap;
fn create_node(id: &str, file_path: &str) -> NodeInfo {
NodeInfo {
id: id.to_string(),
label: id.to_string(),
node_type: NodeType::Function,
file_path: file_path.to_string(),
line_number: 1,
complexity: 1,
metadata: FxHashMap::default(),
}
}
fn create_edge(from: &str, to: &str) -> Edge {
Edge {
from: from.to_string(),
to: to.to_string(),
edge_type: EdgeType::Calls,
weight: 1,
}
}
#[test]
fn test_coupling_analyzer_new() {
let analyzer = CouplingAnalyzer::new();
let _ = analyzer;
}
#[test]
fn test_coupling_analyzer_default() {
let analyzer = CouplingAnalyzer;
let _ = analyzer;
}
#[tokio::test]
async fn test_analyze_empty_graph() {
let analyzer = CouplingAnalyzer::new();
let graph = DependencyGraph::new();
let report = analyzer.analyze(&graph).await.unwrap();
assert!(report.file_metrics.is_empty());
assert_eq!(report.project_metrics.avg_afferent, 0.0);
assert_eq!(report.project_metrics.avg_efferent, 0.0);
assert_eq!(report.project_metrics.max_afferent, 0);
assert_eq!(report.project_metrics.max_efferent, 0);
}
#[tokio::test]
async fn test_analyze_single_node_no_edges() {
let analyzer = CouplingAnalyzer::new();
let mut graph = DependencyGraph::new();
graph.add_node(create_node("node_a", "src/a.rs"));
let report = analyzer.analyze(&graph).await.unwrap();
assert_eq!(report.file_metrics.len(), 1);
let metrics = report.file_metrics.get(&PathBuf::from("src/a.rs")).unwrap();
assert_eq!(metrics.afferent_coupling, 0);
assert_eq!(metrics.efferent_coupling, 0);
assert_eq!(metrics.instability, 0.0);
}
#[tokio::test]
async fn test_analyze_two_nodes_one_edge() {
let analyzer = CouplingAnalyzer::new();
let mut graph = DependencyGraph::new();
graph.add_node(create_node("node_a", "src/a.rs"));
graph.add_node(create_node("node_b", "src/b.rs"));
graph.add_edge(create_edge("node_a", "node_b"));
let report = analyzer.analyze(&graph).await.unwrap();
assert_eq!(report.file_metrics.len(), 2);
let metrics_a = report.file_metrics.get(&PathBuf::from("src/a.rs")).unwrap();
assert_eq!(metrics_a.afferent_coupling, 0);
assert_eq!(metrics_a.efferent_coupling, 1);
assert_eq!(metrics_a.instability, 1.0);
let metrics_b = report.file_metrics.get(&PathBuf::from("src/b.rs")).unwrap();
assert_eq!(metrics_b.afferent_coupling, 1);
assert_eq!(metrics_b.efferent_coupling, 0);
assert_eq!(metrics_b.instability, 0.0); }
#[tokio::test]
async fn test_analyze_complex_graph() {
let analyzer = CouplingAnalyzer::new();
let mut graph = DependencyGraph::new();
graph.add_node(create_node("hub", "src/hub.rs"));
graph.add_node(create_node("spoke1", "src/spoke1.rs"));
graph.add_node(create_node("spoke2", "src/spoke2.rs"));
graph.add_node(create_node("spoke3", "src/spoke3.rs"));
graph.add_edge(create_edge("spoke1", "hub"));
graph.add_edge(create_edge("spoke2", "hub"));
graph.add_edge(create_edge("spoke3", "hub"));
let report = analyzer.analyze(&graph).await.unwrap();
let hub_metrics = report
.file_metrics
.get(&PathBuf::from("src/hub.rs"))
.unwrap();
assert_eq!(hub_metrics.afferent_coupling, 3);
assert_eq!(hub_metrics.efferent_coupling, 0);
assert_eq!(hub_metrics.instability, 0.0);
let spoke1_metrics = report
.file_metrics
.get(&PathBuf::from("src/spoke1.rs"))
.unwrap();
assert_eq!(spoke1_metrics.efferent_coupling, 1);
assert_eq!(spoke1_metrics.instability, 1.0); }
#[tokio::test]
async fn test_analyze_bidirectional_coupling() {
let analyzer = CouplingAnalyzer::new();
let mut graph = DependencyGraph::new();
graph.add_node(create_node("node_a", "src/a.rs"));
graph.add_node(create_node("node_b", "src/b.rs"));
graph.add_edge(create_edge("node_a", "node_b"));
graph.add_edge(create_edge("node_b", "node_a"));
let report = analyzer.analyze(&graph).await.unwrap();
let metrics_a = report.file_metrics.get(&PathBuf::from("src/a.rs")).unwrap();
assert_eq!(metrics_a.afferent_coupling, 1);
assert_eq!(metrics_a.efferent_coupling, 1);
assert_eq!(metrics_a.instability, 0.5);
let metrics_b = report.file_metrics.get(&PathBuf::from("src/b.rs")).unwrap();
assert_eq!(metrics_b.afferent_coupling, 1);
assert_eq!(metrics_b.efferent_coupling, 1);
assert_eq!(metrics_b.instability, 0.5);
}
#[test]
fn test_extract_file_path_with_module_separator() {
let result = CouplingAnalyzer::extract_file_path("src/main.rs::my_module");
assert_eq!(result, Some(PathBuf::from("src/main.rs")));
}
#[test]
fn test_extract_file_path_without_module_separator() {
let result = CouplingAnalyzer::extract_file_path("src/main.rs");
assert_eq!(result, Some(PathBuf::from("src/main.rs")));
}
#[test]
fn test_extract_file_path_complex_path() {
let result =
CouplingAnalyzer::extract_file_path("src/services/analyzer.rs::SubModule::method");
assert_eq!(result, Some(PathBuf::from("src/services/analyzer.rs")));
}
#[tokio::test]
async fn test_project_metrics_calculation() {
let analyzer = CouplingAnalyzer::new();
let mut graph = DependencyGraph::new();
graph.add_node(create_node("a", "a.rs"));
graph.add_node(create_node("b", "b.rs"));
graph.add_node(create_node("c", "c.rs"));
graph.add_edge(create_edge("a", "b"));
graph.add_edge(create_edge("a", "c"));
let report = analyzer.analyze(&graph).await.unwrap();
assert!((report.project_metrics.avg_afferent - 0.667).abs() < 0.01);
assert!((report.project_metrics.avg_efferent - 0.667).abs() < 0.01);
assert_eq!(report.project_metrics.max_afferent, 1);
assert_eq!(report.project_metrics.max_efferent, 2);
}
#[test]
fn test_coupling_metrics_clone() {
let metrics = CouplingMetrics {
afferent_coupling: 5,
efferent_coupling: 3,
instability: 0.375,
};
let cloned = metrics.clone();
assert_eq!(cloned.afferent_coupling, 5);
assert_eq!(cloned.efferent_coupling, 3);
assert_eq!(cloned.instability, 0.375);
}
#[test]
fn test_coupling_metrics_debug() {
let metrics = CouplingMetrics {
afferent_coupling: 5,
efferent_coupling: 3,
instability: 0.375,
};
let debug_str = format!("{:?}", metrics);
assert!(debug_str.contains("afferent_coupling"));
assert!(debug_str.contains("5"));
}
}