use crate::organization::god_object::types::ModuleSplit;
use crate::organization::struct_ownership::StructOwnershipAnalyzer;
use crate::priority::call_graph::CallGraph;
use std::collections::HashSet;
use std::path::Path;
#[derive(Debug, Clone)]
pub struct FileCohesionResult {
pub score: f64,
pub internal_calls: usize,
pub external_calls: usize,
pub functions_analyzed: usize,
}
const MIN_FUNCTIONS_FOR_COHESION: usize = 3;
pub fn calculate_file_cohesion(
file_path: &Path,
call_graph: &CallGraph,
) -> Option<FileCohesionResult> {
let file_functions: HashSet<String> = call_graph
.get_all_functions()
.filter(|func| func.file == file_path)
.map(|func| func.name.clone())
.collect();
let functions_count = file_functions.len();
if functions_count < MIN_FUNCTIONS_FOR_COHESION {
return None;
}
let mut internal_calls = 0;
let mut external_calls = 0;
for call in call_graph.get_all_calls() {
if call.caller.file != file_path {
continue;
}
if call.callee.file == file_path {
internal_calls += 1;
} else {
external_calls += 1;
}
}
let total_calls = internal_calls + external_calls;
let score = if total_calls == 0 {
1.0
} else {
internal_calls as f64 / total_calls as f64
};
Some(FileCohesionResult {
score,
internal_calls,
external_calls,
functions_analyzed: functions_count,
})
}
pub fn calculate_cohesion_score(
split: &ModuleSplit,
call_graph: &CallGraph,
_ownership: &StructOwnershipAnalyzer,
) -> f64 {
let structs_in_module: HashSet<&str> =
split.structs_to_move.iter().map(|s| s.as_str()).collect();
let mut internal_calls = 0;
let mut external_calls = 0;
for call in call_graph.get_all_calls() {
let caller_struct = extract_struct_name(&call.caller.name);
let callee_struct = extract_struct_name(&call.callee.name);
if let Some(caller_s) = caller_struct {
if structs_in_module.contains(caller_s.as_str()) {
if let Some(callee_s) = callee_struct {
if structs_in_module.contains(callee_s.as_str()) {
internal_calls += 1;
} else {
external_calls += 1;
}
} else {
external_calls += 1;
}
}
}
}
let total_calls = internal_calls + external_calls;
if total_calls == 0 {
return 1.0;
}
internal_calls as f64 / total_calls as f64
}
fn extract_struct_name(full_name: &str) -> Option<String> {
let parts: Vec<&str> = full_name.split("::").collect();
if parts.len() >= 2 {
Some(parts[parts.len() - 2].to_string())
} else {
None
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::organization::god_object::types::ModuleSplit;
use crate::organization::god_object::types::Priority;
use crate::organization::struct_ownership::StructOwnershipAnalyzer;
use crate::priority::call_graph::{CallGraph, CallType, FunctionCall, FunctionId};
use std::path::PathBuf;
fn create_test_split(structs: Vec<&str>) -> ModuleSplit {
ModuleSplit {
suggested_name: "test_module".to_string(),
methods_to_move: vec![],
structs_to_move: structs.iter().map(|s| s.to_string()).collect(),
responsibility: "test".to_string(),
estimated_lines: 100,
method_count: 5,
warning: None,
priority: Priority::Medium,
cohesion_score: None,
dependencies_in: vec![],
dependencies_out: vec![],
domain: String::new(),
rationale: None,
method: crate::organization::SplitAnalysisMethod::None,
severity: None,
interface_estimate: None,
classification_evidence: None,
representative_methods: vec![],
fields_needed: vec![],
trait_suggestion: None,
behavior_category: None,
..Default::default()
}
}
fn create_test_call_graph(calls: Vec<(&str, &str, &str, &str)>) -> CallGraph {
let mut graph = CallGraph::new();
for (caller_struct, caller_method, callee_struct, callee_method) in calls {
let caller_name = format!("{}::{}", caller_struct, caller_method);
let callee_name = format!("{}::{}", callee_struct, callee_method);
let call = FunctionCall {
caller: FunctionId::new(PathBuf::from("test.rs"), caller_name, 1),
callee: FunctionId::new(PathBuf::from("test.rs"), callee_name, 10),
call_type: CallType::Direct,
};
graph.add_call(call);
}
graph
}
fn create_test_ownership(struct_methods: Vec<(&str, Vec<&str>)>) -> StructOwnershipAnalyzer {
let code = struct_methods
.iter()
.map(|(struct_name, methods)| {
let methods_code = methods
.iter()
.map(|m| format!(" pub fn {}(&self) {{}}", m))
.collect::<Vec<_>>()
.join("\n");
format!(
"struct {} {{}}\nimpl {} {{\n{}\n}}",
struct_name, struct_name, methods_code
)
})
.collect::<Vec<_>>()
.join("\n\n");
let parsed = syn::parse_file(&code).expect("Failed to parse test code");
StructOwnershipAnalyzer::analyze_file(&parsed)
}
#[test]
fn test_perfect_cohesion() {
let split = create_test_split(vec!["StructA", "StructB"]);
let call_graph = create_test_call_graph(vec![
("StructA", "m1", "StructB", "m2"),
("StructB", "m2", "StructA", "m3"),
]);
let ownership =
create_test_ownership(vec![("StructA", vec!["m1", "m3"]), ("StructB", vec!["m2"])]);
let cohesion = calculate_cohesion_score(&split, &call_graph, &ownership);
assert_eq!(
cohesion, 1.0,
"All internal calls should give cohesion of 1.0"
);
}
#[test]
fn test_zero_cohesion() {
let split = create_test_split(vec!["StructA"]);
let call_graph = create_test_call_graph(vec![
("StructA", "m1", "StructB", "m2"),
("StructA", "m2", "StructC", "m1"),
]);
let ownership = create_test_ownership(vec![
("StructA", vec!["m1", "m2"]),
("StructB", vec!["m2"]),
("StructC", vec!["m1"]),
]);
let cohesion = calculate_cohesion_score(&split, &call_graph, &ownership);
assert_eq!(
cohesion, 0.0,
"All external calls should give cohesion of 0.0"
);
}
#[test]
fn test_mixed_cohesion() {
let split = create_test_split(vec!["StructA", "StructB"]);
let call_graph = create_test_call_graph(vec![
("StructA", "m1", "StructB", "m1"), ("StructB", "m1", "StructA", "m2"), ("StructA", "m2", "StructC", "m1"), ("StructA", "m3", "StructD", "m1"), ("StructB", "m2", "StructE", "m1"), ]);
let ownership = create_test_ownership(vec![
("StructA", vec!["m1", "m2", "m3"]),
("StructB", vec!["m1", "m2"]),
("StructC", vec!["m1"]),
("StructD", vec!["m1"]),
("StructE", vec!["m1"]),
]);
let cohesion = calculate_cohesion_score(&split, &call_graph, &ownership);
assert_eq!(cohesion, 0.4, "2 internal / 5 total should give 0.4");
}
#[test]
fn test_no_calls_cohesion() {
let split = create_test_split(vec!["StructA"]);
let call_graph = create_test_call_graph(vec![]);
let ownership = create_test_ownership(vec![("StructA", vec!["m1"])]);
let cohesion = calculate_cohesion_score(&split, &call_graph, &ownership);
assert_eq!(cohesion, 1.0, "No calls should default to perfect cohesion");
}
fn create_test_file_call_graph(calls: Vec<(&str, &str, &str, &str)>) -> CallGraph {
let mut graph = CallGraph::new();
let file_a = PathBuf::from("src/file_a.rs");
let file_b = PathBuf::from("src/file_b.rs");
for (caller_file, caller_func, callee_file, callee_func) in &calls {
let caller_path = if *caller_file == "A" {
&file_a
} else {
&file_b
};
let callee_path = if *callee_file == "A" {
&file_a
} else {
&file_b
};
graph.add_function(
FunctionId::new(caller_path.clone(), (*caller_func).to_string(), 1),
false,
false,
5,
20,
);
graph.add_function(
FunctionId::new(callee_path.clone(), (*callee_func).to_string(), 10),
false,
false,
5,
20,
);
}
for (caller_file, caller_func, callee_file, callee_func) in calls {
let caller_path = if caller_file == "A" { &file_a } else { &file_b };
let callee_path = if callee_file == "A" { &file_a } else { &file_b };
let call = FunctionCall {
caller: FunctionId::new(caller_path.clone(), caller_func.to_string(), 1),
callee: FunctionId::new(callee_path.clone(), callee_func.to_string(), 10),
call_type: CallType::Direct,
};
graph.add_call(call);
}
graph
}
#[test]
fn test_file_cohesion_perfect() {
let calls = vec![
("A", "func1", "A", "func2"),
("A", "func2", "A", "func3"),
("A", "func3", "A", "func1"),
];
let graph = create_test_file_call_graph(calls);
let result = calculate_file_cohesion(Path::new("src/file_a.rs"), &graph);
assert!(result.is_some());
let r = result.unwrap();
assert_eq!(r.score, 1.0);
assert_eq!(r.internal_calls, 3);
assert_eq!(r.external_calls, 0);
assert_eq!(r.functions_analyzed, 3);
}
#[test]
fn test_file_cohesion_low() {
let calls = vec![
("A", "func1", "B", "helper1"),
("A", "func2", "B", "helper2"),
("A", "func3", "B", "helper3"),
];
let graph = create_test_file_call_graph(calls);
let result = calculate_file_cohesion(Path::new("src/file_a.rs"), &graph);
assert!(result.is_some());
let r = result.unwrap();
assert_eq!(r.score, 0.0);
assert_eq!(r.internal_calls, 0);
assert_eq!(r.external_calls, 3);
assert_eq!(r.functions_analyzed, 3);
}
#[test]
fn test_file_cohesion_mixed() {
let calls = vec![
("A", "func1", "A", "func2"), ("A", "func2", "A", "func3"), ("A", "func3", "B", "helper"), ];
let graph = create_test_file_call_graph(calls);
let result = calculate_file_cohesion(Path::new("src/file_a.rs"), &graph);
assert!(result.is_some());
let r = result.unwrap();
assert!((r.score - 0.6666).abs() < 0.01);
assert_eq!(r.internal_calls, 2);
assert_eq!(r.external_calls, 1);
}
#[test]
fn test_file_cohesion_too_few_functions() {
let mut graph = CallGraph::new();
let file = PathBuf::from("src/small.rs");
graph.add_function(
FunctionId::new(file.clone(), "func1".to_string(), 1),
false,
false,
5,
20,
);
graph.add_function(
FunctionId::new(file.clone(), "func2".to_string(), 10),
false,
false,
5,
20,
);
let result = calculate_file_cohesion(Path::new("src/small.rs"), &graph);
assert!(
result.is_none(),
"Files with <3 functions should return None"
);
}
#[test]
fn test_file_cohesion_no_calls() {
let mut graph = CallGraph::new();
let file = PathBuf::from("src/isolated.rs");
graph.add_function(
FunctionId::new(file.clone(), "func1".to_string(), 1),
false,
false,
5,
20,
);
graph.add_function(
FunctionId::new(file.clone(), "func2".to_string(), 10),
false,
false,
5,
20,
);
graph.add_function(
FunctionId::new(file.clone(), "func3".to_string(), 20),
false,
false,
5,
20,
);
let result = calculate_file_cohesion(Path::new("src/isolated.rs"), &graph);
assert!(result.is_some());
let r = result.unwrap();
assert_eq!(r.score, 1.0);
assert_eq!(r.internal_calls, 0);
assert_eq!(r.external_calls, 0);
}
}