use std::collections::{HashMap, HashSet};
use syn::{self, visit::Visit};
pub struct TraitPatternAnalyzer;
impl TraitPatternAnalyzer {
pub fn analyze_file(ast: &syn::File) -> TraitPatternMetrics {
let mut visitor = ImplVisitor::new();
visitor.visit_file(ast);
let impl_block_count = visitor.impl_blocks.len();
let unique_traits: HashSet<String> = visitor
.impl_blocks
.iter()
.filter_map(|impl_block| impl_block.trait_name.clone())
.collect();
let mut trait_counts: HashMap<String, usize> = HashMap::new();
for impl_block in &visitor.impl_blocks {
if let Some(trait_name) = &impl_block.trait_name {
*trait_counts.entry(trait_name.clone()).or_insert(0) += 1;
}
}
let most_common_trait = trait_counts.into_iter().max_by_key(|(_, count)| *count);
let method_uniformity = Self::calculate_method_uniformity(&visitor.impl_blocks);
let shared_methods = Self::detect_shared_methods(&visitor.impl_blocks);
let (avg_method_complexity, complexity_variance) =
Self::calculate_complexity_metrics(&visitor.impl_blocks);
let avg_method_lines = Self::calculate_avg_method_lines(&visitor.impl_blocks);
TraitPatternMetrics {
impl_block_count,
unique_traits,
most_common_trait,
method_uniformity,
shared_methods,
avg_method_complexity,
complexity_variance,
avg_method_lines,
}
}
pub fn calculate_method_uniformity(impl_blocks: &[ImplBlockInfo]) -> f64 {
if impl_blocks.is_empty() {
return 0.0;
}
let mut method_counts: HashMap<String, usize> = HashMap::new();
for impl_block in impl_blocks {
for method_name in &impl_block.method_names {
*method_counts.entry(method_name.clone()).or_insert(0) += 1;
}
}
if method_counts.is_empty() {
return 0.0;
}
let max_count = method_counts.values().max().copied().unwrap_or(0);
max_count as f64 / impl_blocks.len() as f64
}
pub fn detect_shared_methods(impl_blocks: &[ImplBlockInfo]) -> Vec<(String, f64)> {
if impl_blocks.is_empty() {
return vec![];
}
let mut method_counts: HashMap<String, usize> = HashMap::new();
for impl_block in impl_blocks {
for method_name in &impl_block.method_names {
*method_counts.entry(method_name.clone()).or_insert(0) += 1;
}
}
let total_impls = impl_blocks.len();
let mut shared: Vec<(String, f64)> = method_counts
.into_iter()
.map(|(name, count)| (name, count as f64 / total_impls as f64))
.filter(|(_, freq)| *freq >= 0.5) .collect();
shared.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap());
shared
}
fn calculate_complexity_metrics(impl_blocks: &[ImplBlockInfo]) -> (f64, f64) {
let mut complexities = Vec::new();
for impl_block in impl_blocks {
for complexity in &impl_block.method_complexities {
complexities.push(*complexity as f64);
}
}
if complexities.is_empty() {
return (0.0, 0.0);
}
let avg = complexities.iter().sum::<f64>() / complexities.len() as f64;
let variance = if complexities.len() > 1 {
let sum_sq_diff: f64 = complexities.iter().map(|x| (x - avg).powi(2)).sum();
sum_sq_diff / complexities.len() as f64
} else {
0.0
};
(avg, variance)
}
fn calculate_avg_method_lines(impl_blocks: &[ImplBlockInfo]) -> f64 {
let total_lines: usize = impl_blocks.iter().map(|b| b.total_lines).sum();
let total_methods: usize = impl_blocks.iter().map(|b| b.method_names.len()).sum();
if total_methods == 0 {
0.0
} else {
total_lines as f64 / total_methods as f64
}
}
}
#[derive(Debug, Clone)]
pub struct TraitPatternMetrics {
pub impl_block_count: usize,
pub unique_traits: HashSet<String>,
pub most_common_trait: Option<(String, usize)>,
pub method_uniformity: f64,
pub shared_methods: Vec<(String, f64)>,
pub avg_method_complexity: f64,
pub complexity_variance: f64,
pub avg_method_lines: f64,
}
#[derive(Debug, Clone)]
#[allow(dead_code)]
pub struct ImplBlockInfo {
trait_name: Option<String>,
type_name: String,
method_names: Vec<String>,
method_complexities: Vec<u32>,
total_lines: usize,
}
struct ImplVisitor {
impl_blocks: Vec<ImplBlockInfo>,
}
impl ImplVisitor {
fn new() -> Self {
Self {
impl_blocks: Vec::new(),
}
}
fn extract_type_name(self_ty: &syn::Type) -> String {
match self_ty {
syn::Type::Path(type_path) => type_path
.path
.get_ident()
.map(|id| id.to_string())
.unwrap_or_else(|| "Unknown".to_string()),
_ => "Unknown".to_string(),
}
}
fn extract_trait_name(
trait_ref: &Option<(Option<syn::token::Not>, syn::Path, syn::token::For)>,
) -> Option<String> {
trait_ref
.as_ref()
.and_then(|(_, path, _)| path.segments.last().map(|seg| seg.ident.to_string()))
}
fn estimate_complexity(block: &syn::Block) -> u32 {
crate::complexity::cyclomatic::calculate_cyclomatic(block)
}
}
impl<'ast> Visit<'ast> for ImplVisitor {
fn visit_item_impl(&mut self, node: &'ast syn::ItemImpl) {
let type_name = Self::extract_type_name(&node.self_ty);
let trait_name = Self::extract_trait_name(&node.trait_);
let mut method_names = Vec::new();
let mut method_complexities = Vec::new();
let mut total_lines = 0;
for item in &node.items {
if let syn::ImplItem::Fn(method) = item {
method_names.push(method.sig.ident.to_string());
let complexity = Self::estimate_complexity(&method.block);
method_complexities.push(complexity);
total_lines += 5; }
}
self.impl_blocks.push(ImplBlockInfo {
trait_name,
type_name,
method_names,
method_complexities,
total_lines,
});
syn::visit::visit_item_impl(self, node);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_calculate_method_uniformity_perfect() {
let impl_blocks = vec![
ImplBlockInfo {
trait_name: Some("Flag".to_string()),
type_name: "Flag1".to_string(),
method_names: vec!["name_long".to_string(), "is_switch".to_string()],
method_complexities: vec![1, 1],
total_lines: 10,
},
ImplBlockInfo {
trait_name: Some("Flag".to_string()),
type_name: "Flag2".to_string(),
method_names: vec!["name_long".to_string(), "is_switch".to_string()],
method_complexities: vec![1, 1],
total_lines: 10,
},
ImplBlockInfo {
trait_name: Some("Flag".to_string()),
type_name: "Flag3".to_string(),
method_names: vec!["name_long".to_string(), "is_switch".to_string()],
method_complexities: vec![1, 1],
total_lines: 10,
},
];
let uniformity = TraitPatternAnalyzer::calculate_method_uniformity(&impl_blocks);
assert_eq!(uniformity, 1.0); }
#[test]
fn test_calculate_method_uniformity_partial() {
let impl_blocks = vec![
ImplBlockInfo {
trait_name: Some("Flag".to_string()),
type_name: "Flag1".to_string(),
method_names: vec!["name_long".to_string(), "is_switch".to_string()],
method_complexities: vec![1, 1],
total_lines: 10,
},
ImplBlockInfo {
trait_name: Some("Flag".to_string()),
type_name: "Flag2".to_string(),
method_names: vec!["name_long".to_string(), "is_switch".to_string()],
method_complexities: vec![1, 1],
total_lines: 10,
},
ImplBlockInfo {
trait_name: Some("Flag".to_string()),
type_name: "Flag3".to_string(),
method_names: vec!["name_long".to_string()], method_complexities: vec![1],
total_lines: 5,
},
];
let uniformity = TraitPatternAnalyzer::calculate_method_uniformity(&impl_blocks);
assert_eq!(uniformity, 1.0);
}
#[test]
fn test_detect_shared_methods() {
let impl_blocks = vec![
ImplBlockInfo {
trait_name: Some("Flag".to_string()),
type_name: "Flag1".to_string(),
method_names: vec![
"name_long".to_string(),
"is_switch".to_string(),
"unique_method".to_string(),
],
method_complexities: vec![1, 1, 2],
total_lines: 15,
},
ImplBlockInfo {
trait_name: Some("Flag".to_string()),
type_name: "Flag2".to_string(),
method_names: vec!["name_long".to_string(), "is_switch".to_string()],
method_complexities: vec![1, 1],
total_lines: 10,
},
];
let shared = TraitPatternAnalyzer::detect_shared_methods(&impl_blocks);
assert_eq!(shared.len(), 3);
assert!(shared
.iter()
.any(|(name, freq)| name == "name_long" && *freq == 1.0));
assert!(shared
.iter()
.any(|(name, freq)| name == "is_switch" && *freq == 1.0));
assert!(shared
.iter()
.any(|(name, freq)| name == "unique_method" && *freq == 0.5));
}
#[test]
fn test_calculate_complexity_metrics() {
let impl_blocks = vec![
ImplBlockInfo {
trait_name: Some("Flag".to_string()),
type_name: "Flag1".to_string(),
method_names: vec!["method1".to_string(), "method2".to_string()],
method_complexities: vec![1, 1],
total_lines: 10,
},
ImplBlockInfo {
trait_name: Some("Flag".to_string()),
type_name: "Flag2".to_string(),
method_names: vec!["method1".to_string(), "method2".to_string()],
method_complexities: vec![1, 2],
total_lines: 10,
},
];
let (avg, variance) = TraitPatternAnalyzer::calculate_complexity_metrics(&impl_blocks);
assert_eq!(avg, 1.25); assert!(variance > 0.0); }
}