use std::collections::HashMap;
use super::super::categorization::infer_cluster_category;
use super::super::types::MethodCluster;
use super::cohesion::calculate_cluster_cohesion;
const MAX_METHODS_FOR_CLUSTERING: usize = 200;
const MAX_ITERATIONS: usize = 10;
const MIN_CLUSTER_SIZE: usize = 3;
const MIN_COHESION_SCORE: f64 = 0.2;
pub fn apply_community_detection(
methods: &[String],
adjacency: &HashMap<(String, String), usize>,
) -> Vec<MethodCluster> {
if should_skip_clustering(methods, adjacency) {
return Vec::new();
}
let (clusters, method_to_cluster) = initialize_clusters(methods);
let refined = iteratively_improve_clusters(clusters, method_to_cluster, adjacency, methods);
convert_to_method_clusters(refined, adjacency)
}
fn should_skip_clustering(
methods: &[String],
adjacency: &HashMap<(String, String), usize>,
) -> bool {
adjacency.is_empty() || methods.len() > MAX_METHODS_FOR_CLUSTERING
}
fn initialize_clusters(
methods: &[String],
) -> (HashMap<usize, Vec<String>>, HashMap<String, usize>) {
let clusters = methods
.iter()
.enumerate()
.map(|(i, m)| (i, vec![m.clone()]))
.collect();
let method_to_cluster = methods
.iter()
.enumerate()
.map(|(i, m)| (m.clone(), i))
.collect();
(clusters, method_to_cluster)
}
fn iteratively_improve_clusters(
mut clusters: HashMap<usize, Vec<String>>,
mut method_to_cluster: HashMap<String, usize>,
adjacency: &HashMap<(String, String), usize>,
methods: &[String],
) -> (HashMap<usize, Vec<String>>, HashMap<String, usize>) {
let mut improved = true;
let mut iterations = 0;
while improved && iterations < MAX_ITERATIONS {
improved = false;
iterations += 1;
for method in methods {
if let Some(improvement) = try_improve_method_placement(
method,
&clusters,
&method_to_cluster,
adjacency,
methods,
) {
apply_method_move(method, improvement, &mut clusters, &mut method_to_cluster);
improved = true;
}
}
clusters.retain(|_, methods| !methods.is_empty());
}
(clusters, method_to_cluster)
}
fn try_improve_method_placement(
method: &str,
clusters: &HashMap<usize, Vec<String>>,
method_to_cluster: &HashMap<String, usize>,
adjacency: &HashMap<(String, String), usize>,
all_methods: &[String],
) -> Option<usize> {
let current_cluster = *method_to_cluster.get(method)?;
let current_modularity =
calculate_method_modularity(method, &clusters[¤t_cluster], adjacency, all_methods);
let (best_cluster, _) = find_best_cluster(
method,
current_cluster,
current_modularity,
clusters,
adjacency,
all_methods,
);
if best_cluster != current_cluster {
Some(best_cluster)
} else {
None
}
}
fn find_best_cluster(
method: &str,
current_cluster: usize,
current_modularity: f64,
clusters: &HashMap<usize, Vec<String>>,
adjacency: &HashMap<(String, String), usize>,
all_methods: &[String],
) -> (usize, f64) {
let mut best_cluster = current_cluster;
let mut best_modularity = current_modularity;
for (cluster_id, cluster_methods) in clusters {
if *cluster_id == current_cluster {
continue;
}
let modularity =
calculate_method_modularity(method, cluster_methods, adjacency, all_methods);
if modularity > best_modularity {
best_modularity = modularity;
best_cluster = *cluster_id;
}
}
(best_cluster, best_modularity)
}
fn apply_method_move(
method: &str,
target_cluster: usize,
clusters: &mut HashMap<usize, Vec<String>>,
method_to_cluster: &mut HashMap<String, usize>,
) {
let current_cluster = method_to_cluster[method];
if let Some(cluster) = clusters.get_mut(¤t_cluster) {
cluster.retain(|m| m != method);
}
clusters
.entry(target_cluster)
.or_default()
.push(method.to_string());
method_to_cluster.insert(method.to_string(), target_cluster);
}
fn convert_to_method_clusters(
(clusters, method_to_cluster): (HashMap<usize, Vec<String>>, HashMap<String, usize>),
adjacency: &HashMap<(String, String), usize>,
) -> Vec<MethodCluster> {
clusters
.into_values()
.filter(|methods| methods.len() >= MIN_CLUSTER_SIZE)
.filter_map(|methods| build_method_cluster(methods, &method_to_cluster, adjacency))
.filter(|cluster| cluster.cohesion_score > MIN_COHESION_SCORE)
.collect()
}
fn build_method_cluster(
methods: Vec<String>,
method_to_cluster: &HashMap<String, usize>,
adjacency: &HashMap<(String, String), usize>,
) -> Option<MethodCluster> {
let (internal_calls, external_calls) =
calculate_cluster_cohesion(&methods, adjacency, method_to_cluster);
let mut cluster = MethodCluster {
category: infer_cluster_category(&methods),
methods,
fields_accessed: vec![],
internal_calls,
external_calls,
cohesion_score: 0.0,
};
cluster.calculate_cohesion();
Some(cluster)
}
pub fn calculate_method_modularity(
method: &str,
cluster: &[String],
adjacency: &HashMap<(String, String), usize>,
all_methods: &[String],
) -> f64 {
if cluster.is_empty() {
return 0.0;
}
let internal = count_internal_connections(method, cluster, adjacency);
let external = count_external_connections(method, cluster, adjacency, all_methods);
let total = internal + external;
if total == 0 {
0.0
} else {
internal as f64 / total as f64
}
}
fn count_internal_connections(
method: &str,
cluster: &[String],
adjacency: &HashMap<(String, String), usize>,
) -> usize {
cluster
.iter()
.filter(|m| *m != method)
.map(|cluster_method| get_bidirectional_count(method, cluster_method, adjacency))
.sum()
}
fn count_external_connections(
method: &str,
cluster: &[String],
adjacency: &HashMap<(String, String), usize>,
all_methods: &[String],
) -> usize {
all_methods
.iter()
.filter(|m| !cluster.contains(m) && *m != method)
.map(|other_method| get_bidirectional_count(method, other_method, adjacency))
.sum()
}
fn get_bidirectional_count(
method_a: &str,
method_b: &str,
adjacency: &HashMap<(String, String), usize>,
) -> usize {
let forward = adjacency
.get(&(method_a.to_string(), method_b.to_string()))
.unwrap_or(&0);
let backward = adjacency
.get(&(method_b.to_string(), method_a.to_string()))
.unwrap_or(&0);
forward + backward
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_should_skip_empty_adjacency() {
assert!(should_skip_clustering(&["a".into()], &HashMap::new()));
}
#[test]
fn test_should_skip_too_many_methods() {
let methods: Vec<String> = (0..250).map(|i| format!("m{}", i)).collect();
let adjacency = HashMap::from([((String::from("m0"), String::from("m1")), 1)]);
assert!(should_skip_clustering(&methods, &adjacency));
}
#[test]
fn test_initialize_clusters_creates_one_per_method() {
let methods = vec!["a".into(), "b".into(), "c".into()];
let (clusters, method_map) = initialize_clusters(&methods);
assert_eq!(clusters.len(), 3);
assert_eq!(method_map.len(), 3);
}
#[test]
fn test_modularity_with_empty_cluster() {
let modularity = calculate_method_modularity("a", &[], &HashMap::new(), &[]);
assert_eq!(modularity, 0.0);
}
#[test]
fn test_bidirectional_count() {
let adjacency =
HashMap::from([(("a".into(), "b".into()), 2), (("b".into(), "a".into()), 3)]);
assert_eq!(get_bidirectional_count("a", "b", &adjacency), 5);
}
}