use std::collections::{BTreeSet, HashMap};
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct MethodInfo {
name: String,
accessed_fields: BTreeSet<String>,
called_methods: BTreeSet<String>,
}
impl MethodInfo {
#[must_use]
pub fn new(
name: impl Into<String>,
accessed_fields: BTreeSet<String>,
called_methods: BTreeSet<String>,
) -> Self {
Self {
name: name.into(),
accessed_fields,
called_methods,
}
}
#[must_use]
pub fn name(&self) -> &str {
&self.name
}
#[must_use]
pub fn accessed_fields(&self) -> &BTreeSet<String> {
&self.accessed_fields
}
#[must_use]
pub fn called_methods(&self) -> &BTreeSet<String> {
&self.called_methods
}
}
struct UnionFind {
parent: Vec<usize>,
rank: Vec<usize>,
}
impl UnionFind {
fn new(n: usize) -> Self {
Self {
parent: (0..n).collect(),
rank: vec![0; n],
}
}
fn find(&mut self, x: usize) -> usize {
if self.parent[x] != x {
self.parent[x] = self.find(self.parent[x]);
}
self.parent[x]
}
fn lower_rank(&self, root_a: usize, root_b: usize) -> bool {
self.rank[root_a] < self.rank[root_b]
}
fn union(&mut self, x: usize, y: usize) {
let root_x = self.find(x);
let root_y = self.find(y);
if root_x == root_y {
return;
}
if self.lower_rank(root_x, root_y) {
self.parent[root_x] = root_y;
} else if self.lower_rank(root_y, root_x) {
self.parent[root_y] = root_x;
} else {
self.parent[root_y] = root_x;
self.rank[root_x] += 1;
}
}
fn component_count(&mut self) -> usize {
let n = self.parent.len();
for i in 0..n {
self.find(i);
}
let mut roots: Vec<usize> = self.parent[..n].to_vec();
roots.sort_unstable();
roots.dedup();
roots.len()
}
}
fn build_field_index<'a>(methods: &'a [MethodInfo]) -> HashMap<&'a str, Vec<usize>> {
let mut index: HashMap<&'a str, Vec<usize>> = HashMap::new();
for (idx, method) in methods.iter().enumerate() {
for field in method.accessed_fields() {
index.entry(field.as_str()).or_default().push(idx);
}
}
index
}
fn union_methods_by_index(indices: &[usize], uf: &mut UnionFind) {
if let Some((&first, rest)) = indices.split_first() {
for &other in rest {
uf.union(first, other);
}
}
}
fn union_by_shared_fields(methods: &[MethodInfo], uf: &mut UnionFind) {
let field_index = build_field_index(methods);
for indices in field_index.values() {
union_methods_by_index(indices, uf);
}
}
fn build_method_index<'a>(methods: &'a [MethodInfo]) -> HashMap<&'a str, Vec<usize>> {
let mut index: HashMap<&'a str, Vec<usize>> = HashMap::new();
for (idx, method) in methods.iter().enumerate() {
index.entry(method.name()).or_default().push(idx);
}
index
}
fn union_by_method_calls(methods: &[MethodInfo], uf: &mut UnionFind) {
let method_index = build_method_index(methods);
for (caller_idx, method) in methods.iter().enumerate() {
let callee_indices: Vec<usize> = method
.called_methods()
.iter()
.filter_map(|name| method_index.get(name.as_str()))
.flatten()
.copied()
.collect();
for callee_idx in callee_indices {
uf.union(caller_idx, callee_idx);
}
}
}
#[must_use]
pub fn cohesion_components(methods: &[MethodInfo]) -> usize {
if methods.is_empty() {
return 0;
}
let mut uf = UnionFind::new(methods.len());
union_by_shared_fields(methods, &mut uf);
union_by_method_calls(methods, &mut uf);
uf.component_count()
}
pub mod extract;
pub use extract::{MethodInfoBuilder, collect_method_infos};
#[cfg(test)]
mod tests;