use std::collections::{HashMap, HashSet};
use crate::config::sections::SrpConfig;
use super::union_find::UnionFind;
use super::{MethodFieldData, ResponsibilityCluster, SrpWarning, StructInfo};
pub fn build_struct_warnings(
structs: &[StructInfo],
methods: &[MethodFieldData],
config: &SrpConfig,
) -> Vec<SrpWarning> {
let mut methods_by_type: HashMap<&str, Vec<&MethodFieldData>> = HashMap::new();
for m in methods {
methods_by_type.entry(&m.parent_type).or_default().push(m);
}
structs
.iter()
.filter_map(|s| {
let type_methods = methods_by_type.get(s.name.as_str());
let method_list: Vec<&MethodFieldData> =
type_methods.map(|v| v.to_vec()).unwrap_or_default();
if method_list.len() < 2 {
return None;
}
let field_idx = build_field_method_index(&method_list, &s.fields);
let (lcom4, clusters) = compute_lcom4(&method_list, &s.fields, &field_idx);
let fan_out = compute_fan_out(&method_list);
let composite =
compute_composite_score(lcom4, s.fields.len(), method_list.len(), fan_out, config);
if composite >= config.smell_threshold {
Some(SrpWarning {
struct_name: s.name.clone(),
file: s.file.clone(),
line: s.line,
lcom4,
field_count: s.fields.len(),
method_count: method_list.len(),
fan_out,
composite_score: composite,
clusters,
suppressed: false,
})
} else {
None
}
})
.collect()
}
pub(crate) fn build_field_method_index<'a>(
methods: &[&'a MethodFieldData],
struct_fields: &'a [String],
) -> HashMap<&'a str, Vec<usize>> {
let direct_fields: HashMap<&str, &HashSet<String>> = methods
.iter()
.map(|m| (m.method_name.as_str(), &m.field_accesses))
.collect();
let struct_field_set: HashSet<&str> = struct_fields.iter().map(String::as_str).collect();
let mut field_to_methods: HashMap<&str, Vec<usize>> = HashMap::new();
methods.iter().enumerate().for_each(|(i, m)| {
let mut fields_to_add: HashSet<&str> = if m.is_constructor {
struct_fields.iter().map(String::as_str).collect()
} else {
m.field_accesses
.iter()
.map(String::as_str)
.filter(|f| struct_field_set.contains(f))
.collect()
};
m.self_method_calls.iter().for_each(|callee| {
if let Some(callee_fields) = direct_fields.get(callee.as_str()) {
callee_fields
.iter()
.map(String::as_str)
.filter(|f| struct_field_set.contains(f))
.for_each(|f| {
fields_to_add.insert(f);
});
}
});
fields_to_add.iter().for_each(|&field| {
field_to_methods.entry(field).or_default().push(i);
});
});
field_to_methods
}
pub(crate) fn compute_lcom4(
methods: &[&MethodFieldData],
struct_fields: &[String],
field_to_methods: &HashMap<&str, Vec<usize>>,
) -> (usize, Vec<ResponsibilityCluster>) {
let n = methods.len();
if n == 0 {
return (0, vec![]);
}
let _ = struct_fields; let make_uf = |size| UnionFind::new(size);
let mut uf = make_uf(n);
let unite = |uf: &mut UnionFind, a, b| uf.union(a, b);
let components = |uf: &mut UnionFind| uf.component_members();
field_to_methods.values().for_each(|indices| {
indices.windows(2).for_each(|w| unite(&mut uf, w[0], w[1]));
});
let method_to_fields = invert_field_to_methods(field_to_methods, n);
let component_members = components(&mut uf);
let mut clusters: Vec<ResponsibilityCluster> = component_members
.values()
.map(|member_indices| build_cluster(member_indices, methods, &method_to_fields))
.collect();
clusters.sort_by(|a, b| a.methods.cmp(&b.methods).then(a.fields.cmp(&b.fields)));
(component_members.len(), clusters)
}
fn invert_field_to_methods<'a>(
field_to_methods: &HashMap<&'a str, Vec<usize>>,
method_count: usize,
) -> Vec<Vec<&'a str>> {
let mut out: Vec<Vec<&'a str>> = vec![Vec::new(); method_count];
field_to_methods.iter().for_each(|(field, indices)| {
indices.iter().for_each(|&i| out[i].push(field));
});
out
}
fn build_cluster(
member_indices: &[usize],
methods: &[&MethodFieldData],
method_to_fields: &[Vec<&str>],
) -> ResponsibilityCluster {
let mut cluster_methods: Vec<String> = member_indices
.iter()
.map(|&i| methods[i].method_name.clone())
.collect();
cluster_methods.sort();
let cluster_fields_set: HashSet<String> = member_indices
.iter()
.flat_map(|&i| method_to_fields[i].iter().map(|f| (*f).to_string()))
.collect();
let mut cluster_fields: Vec<String> = cluster_fields_set.into_iter().collect();
cluster_fields.sort();
ResponsibilityCluster {
methods: cluster_methods,
fields: cluster_fields,
}
}
pub(crate) fn compute_fan_out(methods: &[&MethodFieldData]) -> usize {
let all_targets: HashSet<&str> = methods
.iter()
.flat_map(|m| m.call_targets.iter().map(|s| s.as_str()))
.collect();
all_targets.len()
}
pub(crate) fn compute_composite_score(
lcom4: usize,
field_count: usize,
method_count: usize,
fan_out: usize,
config: &SrpConfig,
) -> f64 {
let lcom4_norm = if lcom4 <= 1 {
0.0
} else {
let excess = (lcom4 - 1) as f64;
let threshold_range = (config.lcom4_threshold.max(1) - 1) as f64;
if threshold_range > 0.0 {
(excess / threshold_range).min(1.0)
} else {
1.0
}
};
let field_norm = normalised_ratio(field_count, config.max_fields);
let method_norm = normalised_ratio(method_count, config.max_methods);
let fan_out_norm = normalised_ratio(fan_out, config.max_fan_out);
let [w_lcom4, w_fields, w_methods, w_fan_out] = config.weights;
w_lcom4 * lcom4_norm
+ w_fields * field_norm
+ w_methods * method_norm
+ w_fan_out * fan_out_norm
}
fn normalised_ratio(value: usize, max: usize) -> f64 {
if max == 0 {
return if value == 0 { 0.0 } else { 1.0 };
}
(value as f64 / max as f64).min(1.0)
}