use std::collections::HashMap;
use super::classification_types::{
ClassificationResult, MethodBodyAnalysis, MethodComplexityClass, ReturnExprType, SignalType,
};
use super::thresholds::GodObjectThresholds;
use super::types::GodObjectConfidence;
use crate::organization::confidence::MINIMUM_CONFIDENCE;
pub fn classify_method_complexity(
method_name: &str,
body_analysis: &MethodBodyAnalysis,
) -> MethodComplexityClass {
let name_lower = method_name.to_lowercase();
if matches!(
name_lower.as_str(),
"new" | "default" | "clone" | "from" | "into" | "try_from" | "try_into"
) {
return MethodComplexityClass::Boilerplate;
}
if matches!(
name_lower.as_str(),
"fmt" | "eq" | "ne" | "hash" | "cmp" | "partial_cmp" | "drop"
) {
return MethodComplexityClass::Boilerplate;
}
let is_accessor_name = name_lower.starts_with("get_")
|| name_lower.starts_with("set_")
|| name_lower.ends_with("_mut")
|| name_lower.starts_with("is_")
|| name_lower.starts_with("has_")
|| name_lower.starts_with("with_");
if is_accessor_name && body_analysis.line_count <= 1 && !body_analysis.has_control_flow {
if let Some(ReturnExprType::FieldAccess) = body_analysis.return_expr_type {
return MethodComplexityClass::TrivialAccessor;
}
if let Some(ReturnExprType::Literal) = body_analysis.return_expr_type {
return MethodComplexityClass::TrivialAccessor;
}
}
if is_accessor_name && body_analysis.line_count <= 3 && !body_analysis.has_control_flow {
return MethodComplexityClass::SimpleAccessor;
}
if body_analysis.line_count <= 2
&& body_analysis.call_count == 1
&& !body_analysis.has_control_flow
{
return MethodComplexityClass::Delegating;
}
MethodComplexityClass::Substantive
}
pub fn calculate_weighted_method_count<'a>(
classes: impl Iterator<Item = &'a MethodComplexityClass>,
) -> f64 {
classes.map(|c| c.weight()).sum()
}
pub fn classify_method_by_name(method_name: &str) -> MethodComplexityClass {
let name_lower = method_name.to_lowercase();
if matches!(
name_lower.as_str(),
"new"
| "default"
| "clone"
| "from"
| "into"
| "try_from"
| "try_into"
| "fmt"
| "eq"
| "ne"
| "hash"
| "cmp"
| "partial_cmp"
| "drop"
| "deref"
| "deref_mut"
| "borrow"
| "borrow_mut"
| "as_ref"
| "as_mut"
| "index"
| "index_mut"
) {
return MethodComplexityClass::Boilerplate;
}
if name_lower.starts_with("get_")
|| name_lower.starts_with("is_")
|| name_lower.starts_with("has_")
|| name_lower == "len"
|| name_lower == "is_empty"
{
return MethodComplexityClass::TrivialAccessor;
}
if name_lower.starts_with("set_")
|| name_lower.ends_with("_mut")
|| name_lower.starts_with("with_")
{
return MethodComplexityClass::SimpleAccessor;
}
MethodComplexityClass::Substantive
}
pub fn calculate_weighted_count_from_names<'a>(method_names: impl Iterator<Item = &'a str>) -> f64 {
method_names
.map(|name| classify_method_by_name(name).weight())
.sum()
}
use super::classification_types::MethodSelfUsage;
use syn::visit::Visit;
pub fn classify_self_usage(method: &syn::ImplItemFn) -> MethodSelfUsage {
let has_self_param = method
.sig
.inputs
.iter()
.any(|arg| matches!(arg, syn::FnArg::Receiver(_)));
if !has_self_param {
return MethodSelfUsage::PureAssociated;
}
let self_used = SelfUsageVisitor::check_body(&method.block);
if self_used {
MethodSelfUsage::InstanceMethod
} else {
MethodSelfUsage::UnusedSelf
}
}
pub fn classify_self_usage_standalone(_func: &syn::ItemFn) -> MethodSelfUsage {
MethodSelfUsage::PureAssociated
}
struct SelfUsageVisitor {
uses_self: bool,
nested_depth: usize,
}
impl SelfUsageVisitor {
fn new() -> Self {
Self {
uses_self: false,
nested_depth: 0,
}
}
pub fn check_body(block: &syn::Block) -> bool {
let mut visitor = Self::new();
visitor.visit_block(block);
visitor.uses_self
}
fn is_self_expr(expr: &syn::Expr) -> bool {
if let syn::Expr::Path(path) = expr {
path.path.is_ident("self")
} else {
false
}
}
}
impl<'ast> Visit<'ast> for SelfUsageVisitor {
fn visit_expr_field(&mut self, field: &'ast syn::ExprField) {
if self.nested_depth == 0 && Self::is_self_expr(&field.base) {
self.uses_self = true;
}
syn::visit::visit_expr_field(self, field);
}
fn visit_expr_method_call(&mut self, call: &'ast syn::ExprMethodCall) {
if self.nested_depth == 0 && Self::is_self_expr(&call.receiver) {
self.uses_self = true;
}
syn::visit::visit_expr_method_call(self, call);
}
fn visit_expr_reference(&mut self, reference: &'ast syn::ExprReference) {
if self.nested_depth == 0 && Self::is_self_expr(&reference.expr) {
self.uses_self = true;
}
syn::visit::visit_expr_reference(self, reference);
}
fn visit_expr_path(&mut self, path: &'ast syn::ExprPath) {
if self.nested_depth == 0 && path.path.is_ident("self") {
self.uses_self = true;
}
syn::visit::visit_expr_path(self, path);
}
fn visit_expr_closure(&mut self, closure: &'ast syn::ExprClosure) {
self.nested_depth += 1;
syn::visit::visit_expr_closure(self, closure);
self.nested_depth -= 1;
}
fn visit_item_fn(&mut self, _: &'ast syn::ItemFn) {
}
}
pub fn calculate_combined_method_weight(
accessor_class: MethodComplexityClass,
self_usage: MethodSelfUsage,
) -> f64 {
accessor_class.weight().min(self_usage.weight())
}
pub fn calculate_combined_weighted_count<'a>(
classifications: impl Iterator<Item = (&'a MethodComplexityClass, &'a MethodSelfUsage)>,
) -> f64 {
classifications
.map(|(accessor, self_usage)| calculate_combined_method_weight(*accessor, *self_usage))
.sum()
}
pub fn determine_confidence(
method_count: usize,
field_count: usize,
responsibility_count: usize,
lines_of_code: usize,
complexity_sum: u32,
thresholds: &GodObjectThresholds,
) -> GodObjectConfidence {
let mut violations = 0;
if method_count > thresholds.max_methods {
violations += 1;
}
if field_count > thresholds.max_fields {
violations += 1;
}
if responsibility_count > thresholds.max_traits {
violations += 1;
}
if lines_of_code > thresholds.max_lines {
violations += 1;
}
if complexity_sum > thresholds.max_complexity {
violations += 1;
}
match violations {
5 => GodObjectConfidence::Definite,
3..=4 => GodObjectConfidence::Probable,
1..=2 => GodObjectConfidence::Possible,
_ => GodObjectConfidence::NotGodObject,
}
}
pub fn infer_responsibility_with_confidence(
method_name: &str,
_method_body: Option<&str>,
) -> ClassificationResult {
use crate::organization::BehavioralCategorizer;
let category = BehavioralCategorizer::categorize_method(method_name);
let category_name = category.display_name();
let confidence = match category {
crate::organization::BehaviorCategory::Domain(_) => 0.45, crate::organization::BehaviorCategory::Utilities => 0.75, _ => 0.85, };
if confidence < MINIMUM_CONFIDENCE {
log::debug!(
"Low confidence classification for method '{}': confidence {:.2} below minimum {:.2}",
method_name,
confidence,
MINIMUM_CONFIDENCE
);
return ClassificationResult {
category: None,
confidence,
signals_used: vec![SignalType::NameHeuristic],
};
}
ClassificationResult {
category: Some(category_name),
confidence,
signals_used: vec![SignalType::NameHeuristic],
}
}
pub fn group_methods_by_responsibility(methods: &[String]) -> HashMap<String, Vec<String>> {
let mut groups: HashMap<String, Vec<String>> = HashMap::new();
for method in methods {
let result = infer_responsibility_with_confidence(method, None);
let responsibility = result
.category
.unwrap_or_else(|| "unclassified".to_string());
groups
.entry(responsibility)
.or_default()
.push(method.clone());
}
groups
}
pub fn analyze_function_responsibility(function_name: &str) -> Option<String> {
let result = infer_responsibility_with_confidence(function_name, None);
if result.confidence >= 0.7 {
result.category
} else {
None
}
}
pub fn classify_struct_domain(struct_name: &str) -> String {
let lower = struct_name.to_lowercase();
let domain_mappings = [
(
["weight", "multiplier", "factor", "scoring"].as_slice(),
"scoring",
),
(["threshold", "limit", "bound"].as_slice(), "thresholds"),
(["detection", "detector", "checker"].as_slice(), "detection"),
(["config", "settings", "options"].as_slice(), "core_config"),
(["data", "info", "metrics"].as_slice(), "data"),
];
domain_mappings
.into_iter()
.find(|(keywords, _)| keywords.iter().any(|k| lower.contains(k)))
.map(|(_, domain)| domain.to_string())
.unwrap_or_else(|| extract_domain_from_name(struct_name))
}
pub fn extract_domain_from_name(name: &str) -> String {
if name.contains('_') {
return name
.split('_')
.next()
.filter(|s| !s.is_empty())
.map(|s| s.to_string())
.unwrap_or_else(|| "Core".to_string());
}
let mut domain = String::new();
for (i, c) in name.chars().enumerate() {
if i > 0 && c.is_uppercase() {
break;
}
domain.push(c);
}
if !domain.is_empty() {
domain
} else {
"Core".to_string()
}
}
pub fn count_distinct_domains(structs: &[super::types::StructMetrics]) -> usize {
use std::collections::HashSet;
let domains: HashSet<String> = structs
.iter()
.map(|s| classify_struct_domain(&s.name))
.collect();
domains.len()
}
pub fn calculate_struct_ratio(struct_count: usize, total_functions: usize) -> f64 {
if total_functions == 0 {
return 0.0;
}
(struct_count as f64) / (total_functions as f64)
}
#[derive(Debug, Clone)]
pub struct DomainContext {
pub primary_keywords: Vec<String>,
pub secondary_keywords: Vec<String>,
pub domain_suffix: Option<String>,
}
impl DomainContext {
pub fn empty() -> Self {
Self {
primary_keywords: Vec::new(),
secondary_keywords: Vec::new(),
domain_suffix: None,
}
}
pub fn has_domain(&self) -> bool {
!self.primary_keywords.is_empty()
}
pub fn primary_domain_name(&self) -> String {
if self.primary_keywords.is_empty() {
"Primary".to_string()
} else {
self.primary_keywords
.iter()
.map(|k| {
let mut chars = k.chars();
match chars.next() {
None => String::new(),
Some(c) => c.to_uppercase().collect::<String>() + chars.as_str(),
}
})
.collect::<Vec<_>>()
.join("")
}
}
}
pub fn extract_domain_context(
struct_name: &str,
field_names: &[String],
field_types: &[String],
) -> DomainContext {
let primary_keywords = extract_domain_keywords(struct_name);
let common_types: std::collections::HashSet<&str> = [
"vec", "hashmap", "btreemap", "hashset", "btreeset", "option", "result", "string", "str",
"bool", "usize", "isize", "u8", "u16", "u32", "u64", "i8", "i16", "i32", "i64", "f32",
"f64", "box", "arc", "rc", "refcell", "cell", "mutex", "rwlock", "pathbuf", "path",
]
.into_iter()
.collect();
let secondary_keywords: Vec<String> = field_names
.iter()
.chain(field_types.iter())
.flat_map(|name| extract_domain_keywords(name))
.filter(|kw| !common_types.contains(kw.as_str()))
.collect();
let domain_suffix = detect_domain_suffix(struct_name);
DomainContext {
primary_keywords,
secondary_keywords,
domain_suffix,
}
}
pub fn detect_domain_suffix(struct_name: &str) -> Option<String> {
let cohesive_suffixes = [
"tracker",
"analyzer",
"builder",
"visitor",
"handler",
"processor",
"calculator",
"resolver",
"extractor",
"detector",
"validator",
"formatter",
"parser",
"renderer",
"serializer",
"deserializer",
"iterator",
"generator",
"factory",
"provider",
"repository",
"service",
"client",
"server",
"cache",
"pool",
"queue",
"stack",
"manager",
"controller",
"adapter",
"wrapper",
"proxy",
"decorator",
"observer",
"listener",
"emitter",
"dispatcher",
"scheduler",
"executor",
"runner",
"loader",
"writer",
"reader",
"mapper",
"converter",
"transformer",
];
let name_lower = struct_name.to_lowercase();
for suffix in &cohesive_suffixes {
if name_lower.ends_with(suffix) {
return Some(suffix.to_string());
}
}
None
}
pub fn group_methods_by_domain(
methods: &[String],
context: &DomainContext,
) -> HashMap<String, Vec<String>> {
if !context.has_domain() {
return group_methods_by_responsibility(methods);
}
let mut groups: HashMap<String, Vec<String>> = HashMap::new();
for method in methods {
let domain = infer_method_domain(method, context);
groups.entry(domain).or_default().push(method.clone());
}
groups
}
fn infer_method_domain(method: &str, context: &DomainContext) -> String {
let method_lower = method.to_lowercase();
let method_keywords = extract_domain_keywords(method);
let utility_methods: std::collections::HashSet<&str> = [
"new",
"default",
"clone",
"fmt",
"drop",
"from",
"into",
"as_ref",
"as_mut",
"len",
"is_empty",
"iter",
"iter_mut",
"clear",
"with_capacity",
]
.into_iter()
.collect();
if utility_methods.contains(method_lower.as_str()) {
return context.primary_domain_name();
}
let matches_primary = context.primary_keywords.iter().any(|pk| {
method_lower.contains(pk.as_str())
|| method_keywords.iter().any(|mk| mk == pk || mk.contains(pk))
});
if matches_primary {
return context.primary_domain_name();
}
let matching_secondary: Vec<&String> = context
.secondary_keywords
.iter()
.filter(|sk| {
method_lower.contains(sk.as_str())
|| method_keywords
.iter()
.any(|mk| mk == *sk || mk.contains(*sk))
})
.collect();
if !matching_secondary.is_empty() {
return context.primary_domain_name();
}
if let Some(ref suffix) = context.domain_suffix {
let suffix_root = suffix.trim_end_matches("er").trim_end_matches("or");
if method_lower.contains(suffix_root) {
return context.primary_domain_name();
}
}
let result = infer_responsibility_with_confidence(method, None);
result
.category
.unwrap_or_else(|| "unclassified".to_string())
}
pub fn extract_domain_keywords(name: &str) -> Vec<String> {
let mut keywords = Vec::new();
if name.contains('_') {
for part in name.split('_') {
if !part.is_empty() && part.len() > 1 {
keywords.push(part.to_lowercase());
}
}
} else {
let mut current_word = String::new();
for c in name.chars() {
if c.is_uppercase() && !current_word.is_empty() {
if current_word.len() > 1 {
keywords.push(current_word.to_lowercase());
}
current_word = c.to_string();
} else {
current_word.push(c);
}
}
if current_word.len() > 1 {
keywords.push(current_word.to_lowercase());
}
}
let non_domain_words: std::collections::HashSet<&str> = [
"new", "get", "set", "is", "has", "the", "a", "an", "impl", "default",
]
.into_iter()
.collect();
keywords
.into_iter()
.filter(|w| !non_domain_words.contains(w.as_str()))
.collect()
}
pub fn calculate_domain_cohesion(struct_name: &str, methods: &[String]) -> f64 {
if methods.is_empty() {
return 1.0; }
let domain_keywords = extract_domain_keywords(struct_name);
if domain_keywords.is_empty() {
return 0.5; }
let utility_methods: std::collections::HashSet<&str> = [
"new", "default", "clone", "fmt", "drop", "from", "into", "as_ref", "as_mut",
]
.into_iter()
.collect();
let mut domain_aligned = 0;
let mut non_utility_count = 0;
for method in methods {
let method_lower = method.to_lowercase();
if utility_methods.contains(method_lower.as_str()) {
continue;
}
non_utility_count += 1;
let method_keywords = extract_domain_keywords(method);
let has_domain_keyword = domain_keywords
.iter()
.any(|dk| method_lower.contains(dk) || method_keywords.contains(dk));
if has_domain_keyword {
domain_aligned += 1;
}
}
if non_utility_count == 0 {
return 1.0; }
domain_aligned as f64 / non_utility_count as f64
}
pub fn is_cohesive_struct(struct_name: &str, methods: &[String]) -> bool {
const COHESION_THRESHOLD: f64 = 0.5;
let cohesion = calculate_domain_cohesion(struct_name, methods);
let name_lower = struct_name.to_lowercase();
let cohesive_suffixes = [
"tracker",
"analyzer",
"builder",
"visitor",
"handler",
"processor",
"calculator",
"resolver",
"extractor",
"detector",
"validator",
"formatter",
"parser",
"renderer",
"serializer",
"deserializer",
"iterator",
"generator",
"factory",
"provider",
"repository",
"service",
"client",
"server",
"cache",
"pool",
"queue",
"stack",
];
let has_cohesive_suffix = cohesive_suffixes
.iter()
.any(|suffix| name_lower.ends_with(suffix));
cohesion > COHESION_THRESHOLD || (has_cohesive_suffix && cohesion > 0.3)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_confidence_mapping_definite() {
let thresholds = GodObjectThresholds::default();
let confidence = determine_confidence(30, 20, 8, 1500, 300, &thresholds);
assert_eq!(confidence, GodObjectConfidence::Definite);
}
#[test]
fn test_confidence_mapping_probable() {
let thresholds = GodObjectThresholds::default();
let confidence = determine_confidence(30, 20, 8, 500, 100, &thresholds);
assert_eq!(confidence, GodObjectConfidence::Probable);
}
#[test]
fn test_confidence_mapping_possible() {
let thresholds = GodObjectThresholds::default();
let confidence = determine_confidence(30, 20, 3, 500, 100, &thresholds);
assert_eq!(confidence, GodObjectConfidence::Possible);
}
#[test]
fn test_confidence_mapping_not_god_object() {
let thresholds = GodObjectThresholds::default();
let confidence = determine_confidence(10, 8, 3, 500, 100, &thresholds);
assert_eq!(confidence, GodObjectConfidence::NotGodObject);
}
#[test]
fn test_group_methods_by_responsibility_basic() {
let methods = vec![
"parse_json".to_string(),
"format_output".to_string(),
"validate_input".to_string(),
];
let groups = group_methods_by_responsibility(&methods);
assert!(groups.contains_key("Parsing"));
assert!(groups.contains_key("Rendering"));
assert!(groups.contains_key("Validation"));
}
#[test]
fn test_group_methods_by_responsibility_unclassified() {
let methods = vec!["foo".to_string()]; let groups = group_methods_by_responsibility(&methods);
assert!(groups.contains_key("unclassified"));
}
#[test]
fn test_classify_struct_domain_scoring() {
assert_eq!(classify_struct_domain("ScoringWeight"), "scoring");
assert_eq!(classify_struct_domain("MultiplicandFactor"), "scoring");
}
#[test]
fn test_classify_struct_domain_thresholds() {
assert_eq!(classify_struct_domain("ThresholdConfig"), "thresholds");
assert_eq!(classify_struct_domain("LimitSettings"), "thresholds");
}
#[test]
fn test_classify_struct_domain_detection() {
assert_eq!(classify_struct_domain("DetectorModule"), "detection");
assert_eq!(classify_struct_domain("CheckerSystem"), "detection");
}
#[test]
fn test_classify_struct_domain_config() {
assert_eq!(classify_struct_domain("ConfigOptions"), "core_config");
assert_eq!(classify_struct_domain("SystemSettings"), "core_config");
}
#[test]
fn test_classify_struct_domain_data() {
assert_eq!(classify_struct_domain("DataStructure"), "data");
assert_eq!(classify_struct_domain("MetricsInfo"), "data");
}
#[test]
fn test_classify_struct_domain_fallback() {
assert_eq!(classify_struct_domain("UserProfile"), "User");
assert_eq!(classify_struct_domain("OrderProcessor"), "Order");
}
#[test]
fn test_extract_domain_from_name_camel_case() {
assert_eq!(extract_domain_from_name("UserProfile"), "User");
assert_eq!(extract_domain_from_name("OrderManager"), "Order");
}
#[test]
fn test_extract_domain_from_name_snake_case() {
assert_eq!(extract_domain_from_name("user_profile"), "user");
assert_eq!(extract_domain_from_name("order_data"), "order");
}
#[test]
fn test_extract_domain_from_name_empty() {
assert_eq!(extract_domain_from_name(""), "Core");
}
#[test]
fn test_count_distinct_domains() {
use super::super::types::StructMetrics;
let structs = vec![
StructMetrics {
name: "ThresholdConfig".to_string(),
line_span: (0, 10),
method_count: 2,
field_count: 5,
responsibilities: vec!["configuration".to_string()],
},
StructMetrics {
name: "ThresholdValidator".to_string(),
line_span: (11, 20),
method_count: 3,
field_count: 4,
responsibilities: vec!["validation".to_string()],
},
StructMetrics {
name: "ScoringWeight".to_string(),
line_span: (21, 30),
method_count: 4,
field_count: 3,
responsibilities: vec!["calculation".to_string()],
},
];
assert_eq!(count_distinct_domains(&structs), 2);
}
#[test]
fn test_calculate_struct_ratio_normal() {
assert_eq!(calculate_struct_ratio(10, 20), 0.5);
assert_eq!(calculate_struct_ratio(15, 10), 1.5);
}
#[test]
fn test_calculate_struct_ratio_zero_functions() {
assert_eq!(calculate_struct_ratio(10, 0), 0.0);
}
#[test]
fn test_calculate_struct_ratio_zero_structs() {
assert_eq!(calculate_struct_ratio(0, 10), 0.0);
}
use proptest::prelude::*;
proptest! {
#[test]
fn prop_classification_idempotent(method_name in "[a-z_]{1,20}") {
let r1 = infer_responsibility_with_confidence(&method_name, None);
let r2 = infer_responsibility_with_confidence(&method_name, None);
prop_assert_eq!(r1.category, r2.category);
prop_assert_eq!(r1.confidence, r2.confidence);
prop_assert_eq!(r1.signals_used, r2.signals_used);
}
#[test]
fn prop_struct_domain_classification_idempotent(struct_name in "[A-Z][a-zA-Z0-9_]{1,30}") {
let d1 = classify_struct_domain(&struct_name);
let d2 = classify_struct_domain(&struct_name);
prop_assert_eq!(d1, d2);
}
#[test]
fn prop_confidence_calculation_idempotent(
method_count in 0usize..100,
field_count in 0usize..50,
responsibility_count in 0usize..20,
lines_of_code in 0usize..5000,
complexity_sum in 0u32..1000
) {
let thresholds = GodObjectThresholds::default();
let c1 = determine_confidence(method_count, field_count, responsibility_count, lines_of_code, complexity_sum, &thresholds);
let c2 = determine_confidence(method_count, field_count, responsibility_count, lines_of_code, complexity_sum, &thresholds);
prop_assert_eq!(c1, c2);
}
#[test]
fn prop_confidence_violations_mapping(
method_count in 0usize..100,
field_count in 0usize..50,
responsibility_count in 0usize..20,
lines_of_code in 0usize..5000,
complexity_sum in 0u32..1000
) {
let thresholds = GodObjectThresholds::default();
let confidence = determine_confidence(method_count, field_count, responsibility_count, lines_of_code, complexity_sum, &thresholds);
let mut violations = 0;
if method_count > thresholds.max_methods { violations += 1; }
if field_count > thresholds.max_fields { violations += 1; }
if responsibility_count > thresholds.max_traits { violations += 1; }
if lines_of_code > thresholds.max_lines { violations += 1; }
if complexity_sum > thresholds.max_complexity { violations += 1; }
match violations {
5 => prop_assert_eq!(confidence, GodObjectConfidence::Definite),
3..=4 => prop_assert_eq!(confidence, GodObjectConfidence::Probable),
1..=2 => prop_assert_eq!(confidence, GodObjectConfidence::Possible),
_ => prop_assert_eq!(confidence, GodObjectConfidence::NotGodObject),
}
}
#[test]
fn prop_struct_ratio_non_negative(struct_count in 0usize..100, total_functions in 0usize..200) {
let ratio = calculate_struct_ratio(struct_count, total_functions);
prop_assert!(ratio >= 0.0);
}
#[test]
fn prop_struct_ratio_zero_functions(struct_count in 0usize..100) {
let ratio = calculate_struct_ratio(struct_count, 0);
prop_assert_eq!(ratio, 0.0);
}
}
#[test]
fn test_analyze_function_responsibility_validation() {
assert_eq!(
analyze_function_responsibility("validate_email"),
Some("Validation".to_string())
);
assert_eq!(
analyze_function_responsibility("check_bounds"),
Some("Validation".to_string())
);
assert_eq!(
analyze_function_responsibility("verify_signature"),
Some("Validation".to_string())
);
assert_eq!(
analyze_function_responsibility("is_valid"),
Some("Validation".to_string())
);
}
#[test]
fn test_analyze_function_responsibility_parsing() {
assert_eq!(
analyze_function_responsibility("parse_json"),
Some("Parsing".to_string())
);
assert_eq!(
analyze_function_responsibility("read_config"),
Some("Parsing".to_string())
);
assert_eq!(
analyze_function_responsibility("extract_data"),
Some("Parsing".to_string())
);
assert_eq!(
analyze_function_responsibility("decode_message"),
Some("Parsing".to_string())
);
}
#[test]
fn test_analyze_function_responsibility_data_access() {
assert_eq!(
analyze_function_responsibility("get_user"),
Some("Data Access".to_string())
);
assert_eq!(
analyze_function_responsibility("set_property"),
Some("Data Access".to_string())
);
assert_eq!(
analyze_function_responsibility("fetch_record"),
Some("Data Access".to_string())
);
assert_eq!(
analyze_function_responsibility("retrieve_data"),
Some("Data Access".to_string())
);
}
#[test]
fn test_analyze_function_responsibility_rendering() {
assert_eq!(
analyze_function_responsibility("render_view"),
Some("Rendering".to_string())
);
assert_eq!(
analyze_function_responsibility("draw_chart"),
Some("Rendering".to_string())
);
assert_eq!(
analyze_function_responsibility("paint_canvas"),
Some("Rendering".to_string())
);
assert_eq!(
analyze_function_responsibility("format_output"),
Some("Rendering".to_string())
);
}
#[test]
fn test_analyze_function_responsibility_construction() {
assert_eq!(
analyze_function_responsibility("create_instance"),
Some("Construction".to_string())
);
assert_eq!(
analyze_function_responsibility("build_object"),
Some("Construction".to_string())
);
assert_eq!(
analyze_function_responsibility("make_widget"),
Some("Construction".to_string())
);
}
#[test]
fn test_analyze_function_responsibility_filtering() {
assert_eq!(
analyze_function_responsibility("filter_results"),
Some("Filtering".to_string())
);
assert_eq!(
analyze_function_responsibility("select_items"),
Some("Filtering".to_string())
);
assert_eq!(
analyze_function_responsibility("find_matches"),
Some("Filtering".to_string())
);
assert_eq!(
analyze_function_responsibility("search_database"),
Some("Filtering".to_string())
);
}
#[test]
fn test_analyze_function_responsibility_transformation() {
assert_eq!(
analyze_function_responsibility("transform_data"),
Some("Transformation".to_string())
);
assert_eq!(
analyze_function_responsibility("convert_to_json"),
Some("Transformation".to_string())
);
assert_eq!(
analyze_function_responsibility("map_values"),
Some("Transformation".to_string())
);
}
#[test]
fn test_analyze_function_responsibility_communication() {
assert_eq!(
analyze_function_responsibility("send_message"),
Some("Communication".to_string())
);
assert_eq!(
analyze_function_responsibility("receive_data"),
Some("Communication".to_string())
);
assert_eq!(
analyze_function_responsibility("transmit_packet"),
Some("Communication".to_string())
);
assert_eq!(
analyze_function_responsibility("notify_observers"),
Some("Communication".to_string())
);
}
#[test]
fn test_analyze_function_responsibility_persistence() {
assert_eq!(
analyze_function_responsibility("save_state"),
Some("Persistence".to_string())
);
assert_eq!(
analyze_function_responsibility("load_config"),
Some("Persistence".to_string())
);
}
#[test]
fn test_analyze_function_responsibility_event_handling() {
assert_eq!(
analyze_function_responsibility("handle_keypress"),
Some("Event Handling".to_string())
);
assert_eq!(
analyze_function_responsibility("on_mouse_down"),
Some("Event Handling".to_string())
);
assert_eq!(
analyze_function_responsibility("dispatch_event"),
Some("Event Handling".to_string())
);
}
#[test]
fn test_analyze_function_responsibility_processing() {
assert_eq!(
analyze_function_responsibility("process_request"),
Some("Processing".to_string())
);
assert_eq!(
analyze_function_responsibility("execute_task"),
Some("Processing".to_string())
);
assert_eq!(
analyze_function_responsibility("run_pipeline"),
Some("Processing".to_string())
);
}
#[test]
fn test_analyze_function_responsibility_low_confidence() {
assert_eq!(analyze_function_responsibility("do_something"), None);
assert_eq!(analyze_function_responsibility("helper"), None);
assert_eq!(analyze_function_responsibility("utils"), None);
assert_eq!(analyze_function_responsibility("foo"), None);
assert_eq!(analyze_function_responsibility("bar"), None);
}
#[test]
fn test_analyze_function_responsibility_purity() {
let result1 = analyze_function_responsibility("validate_input");
let result2 = analyze_function_responsibility("validate_input");
assert_eq!(result1, result2);
assert_eq!(result1, Some("Validation".to_string()));
for _ in 0..10 {
assert_eq!(
analyze_function_responsibility("parse_json"),
Some("Parsing".to_string())
);
}
}
#[test]
fn test_analyze_function_responsibility_empty_string() {
assert_eq!(analyze_function_responsibility(""), None);
}
#[test]
fn test_analyze_function_responsibility_lifecycle() {
assert_eq!(
analyze_function_responsibility("initialize_system"),
Some("Lifecycle".to_string())
);
assert_eq!(
analyze_function_responsibility("cleanup"),
Some("Lifecycle".to_string())
);
}
#[test]
fn test_analyze_function_responsibility_state_management() {
assert_eq!(
analyze_function_responsibility("update_state"),
Some("State Management".to_string())
);
}
#[test]
fn test_extract_domain_keywords_camel_case() {
let keywords = extract_domain_keywords("CrossModuleTracker");
assert!(keywords.contains(&"cross".to_string()));
assert!(keywords.contains(&"module".to_string()));
assert!(keywords.contains(&"tracker".to_string()));
}
#[test]
fn test_extract_domain_keywords_snake_case() {
let keywords = extract_domain_keywords("call_graph_builder");
assert!(keywords.contains(&"call".to_string()));
assert!(keywords.contains(&"graph".to_string()));
assert!(keywords.contains(&"builder".to_string()));
}
#[test]
fn test_calculate_domain_cohesion_high() {
let struct_name = "ModuleTracker";
let methods = vec![
"get_module".to_string(),
"track_module".to_string(),
"resolve_module_call".to_string(),
"is_module_valid".to_string(),
"new".to_string(),
];
let cohesion = calculate_domain_cohesion(struct_name, &methods);
assert!(cohesion > 0.6, "Expected high cohesion, got {}", cohesion);
}
#[test]
fn test_calculate_domain_cohesion_low() {
let struct_name = "GodObject";
let methods = vec![
"parse_json".to_string(),
"render_html".to_string(),
"validate_email".to_string(),
"send_notification".to_string(),
"save_to_database".to_string(),
];
let cohesion = calculate_domain_cohesion(struct_name, &methods);
assert!(cohesion < 0.3, "Expected low cohesion, got {}", cohesion);
}
#[test]
fn test_is_cohesive_struct_tracker() {
let struct_name = "CrossModuleTracker";
let methods = vec![
"new".to_string(),
"analyze_workspace".to_string(),
"get_cross_module_calls".to_string(),
"get_public_apis".to_string(),
"is_public_api".to_string(),
"resolve_module_call".to_string(),
"get_statistics".to_string(),
"infer_module_path".to_string(),
];
assert!(
is_cohesive_struct(struct_name, &methods),
"CrossModuleTracker should be detected as cohesive"
);
}
#[test]
fn test_is_cohesive_struct_god_object() {
let struct_name = "ApplicationManager";
let methods = vec![
"parse_config".to_string(),
"render_ui".to_string(),
"validate_input".to_string(),
"send_email".to_string(),
"save_data".to_string(),
"load_plugin".to_string(),
"handle_request".to_string(),
];
assert!(
!is_cohesive_struct(struct_name, &methods),
"ApplicationManager with unrelated methods should NOT be cohesive"
);
}
#[test]
fn test_domain_context_extraction() {
let context = extract_domain_context(
"ModuleTracker",
&["modules".into(), "boundaries".into()],
&["HashMap".into(), "Vec".into()],
);
assert!(context.primary_keywords.contains(&"module".to_string()));
assert!(context.primary_keywords.contains(&"tracker".to_string()));
assert_eq!(context.domain_suffix, Some("tracker".to_string()));
assert!(context.secondary_keywords.contains(&"modules".to_string()));
assert!(context
.secondary_keywords
.contains(&"boundaries".to_string()));
}
#[test]
fn test_domain_context_filters_common_types() {
let context = extract_domain_context(
"DataStore",
&["items".into()],
&["HashMap".into(), "String".into(), "Vec".into()],
);
assert!(!context.secondary_keywords.contains(&"hashmap".to_string()));
assert!(!context.secondary_keywords.contains(&"string".to_string()));
assert!(!context.secondary_keywords.contains(&"vec".to_string()));
}
#[test]
fn test_detect_domain_suffix() {
assert_eq!(
detect_domain_suffix("ModuleTracker"),
Some("tracker".to_string())
);
assert_eq!(
detect_domain_suffix("RequestHandler"),
Some("handler".to_string())
);
assert_eq!(
detect_domain_suffix("JsonParser"),
Some("parser".to_string())
);
assert_eq!(
detect_domain_suffix("UserRepository"),
Some("repository".to_string())
);
assert_eq!(detect_domain_suffix("SomeStruct"), None);
assert_eq!(detect_domain_suffix("Config"), None);
}
#[test]
fn test_domain_context_primary_domain_name() {
let context = DomainContext {
primary_keywords: vec!["module".into(), "tracker".into()],
secondary_keywords: vec![],
domain_suffix: Some("tracker".into()),
};
assert_eq!(context.primary_domain_name(), "ModuleTracker");
let empty_context = DomainContext::empty();
assert_eq!(empty_context.primary_domain_name(), "Primary");
}
#[test]
fn test_group_methods_by_domain_cohesive_struct() {
let context = DomainContext {
primary_keywords: vec!["module".into()],
secondary_keywords: vec!["boundary".into()],
domain_suffix: Some("tracker".into()),
};
let methods = vec![
"get_modules".into(),
"track_module".into(),
"resolve_boundary".into(),
"new".into(),
];
let groups = group_methods_by_domain(&methods, &context);
assert!(
groups.len() <= 2,
"Cohesive methods should group together, got {} groups: {:?}",
groups.len(),
groups.keys().collect::<Vec<_>>()
);
}
#[test]
fn test_group_methods_by_domain_god_object() {
let context = DomainContext {
primary_keywords: vec!["application".into()],
secondary_keywords: vec![],
domain_suffix: Some("manager".into()),
};
let methods = vec![
"parse_json".into(),
"render_html".into(),
"validate_email".into(),
"send_notification".into(),
];
let groups = group_methods_by_domain(&methods, &context);
assert!(
groups.len() >= 4,
"Unrelated methods should stay separate, got {} groups: {:?}",
groups.len(),
groups.keys().collect::<Vec<_>>()
);
}
#[test]
fn test_group_methods_by_domain_utility_methods() {
let context = DomainContext {
primary_keywords: vec!["module".into()],
secondary_keywords: vec![],
domain_suffix: Some("tracker".into()),
};
let methods = vec!["new".into(), "default".into(), "clone".into(), "len".into()];
let groups = group_methods_by_domain(&methods, &context);
assert_eq!(
groups.len(),
1,
"Utility methods should all group together under primary domain"
);
}
#[test]
fn test_group_methods_by_domain_suffix_alignment() {
let context = DomainContext {
primary_keywords: vec!["module".into()],
secondary_keywords: vec![],
domain_suffix: Some("tracker".into()),
};
let methods = vec!["track_item".into(), "get_tracked".into(), "untrack".into()];
let groups = group_methods_by_domain(&methods, &context);
assert!(
groups.len() <= 2,
"Methods aligning with suffix should group together"
);
}
#[test]
fn test_group_methods_by_domain_empty_context() {
let context = DomainContext::empty();
let methods = vec![
"parse_json".into(),
"validate_input".into(),
"get_data".into(),
];
let groups = group_methods_by_domain(&methods, &context);
assert!(
!groups.is_empty(),
"Should produce groups even with empty context"
);
}
#[test]
fn test_cross_module_tracker_domain_grouping() {
let context = extract_domain_context(
"CrossModuleTracker",
&["modules".into(), "calls".into(), "boundaries".into()],
&["HashMap".into(), "Vec".into()],
);
let methods = vec![
"get_module_calls".into(),
"is_public_api".into(),
"resolve_module_call".into(),
"infer_module_path".into(),
"new".into(),
];
let groups = group_methods_by_domain(&methods, &context);
assert!(
groups.len() <= 2,
"CrossModuleTracker methods should have ≤2 domain groups, got {}: {:?}",
groups.len(),
groups.keys().collect::<Vec<_>>()
);
}
#[test]
fn test_domain_context_has_domain() {
let context = DomainContext {
primary_keywords: vec!["module".into()],
secondary_keywords: vec![],
domain_suffix: None,
};
assert!(context.has_domain());
let empty = DomainContext::empty();
assert!(!empty.has_domain());
}
proptest! {
#[test]
fn prop_domain_context_extraction_idempotent(
struct_name in "[A-Z][a-zA-Z0-9]{1,20}",
field_name in "[a-z_][a-z_0-9]{0,15}"
) {
let fields = vec![field_name.clone()];
let types = vec!["String".to_string()];
let c1 = extract_domain_context(&struct_name, &fields, &types);
let c2 = extract_domain_context(&struct_name, &fields, &types);
prop_assert_eq!(c1.primary_keywords, c2.primary_keywords);
prop_assert_eq!(c1.secondary_keywords, c2.secondary_keywords);
prop_assert_eq!(c1.domain_suffix, c2.domain_suffix);
}
#[test]
fn prop_group_methods_by_domain_idempotent(
struct_name in "[A-Z][a-zA-Z0-9]{1,15}Tracker",
method_name in "[a-z_][a-z_0-9]{0,15}"
) {
let context = extract_domain_context(&struct_name, &[], &[]);
let methods = vec![method_name.clone()];
let g1 = group_methods_by_domain(&methods, &context);
let g2 = group_methods_by_domain(&methods, &context);
prop_assert_eq!(g1.len(), g2.len());
for key in g1.keys() {
prop_assert!(g2.contains_key(key));
}
}
}
#[test]
fn test_classify_trivial_accessor_field_access() {
let analysis = MethodBodyAnalysis {
line_count: 1,
has_control_flow: false,
call_count: 0,
return_expr_type: Some(ReturnExprType::FieldAccess),
has_self_param: true,
is_mutating: false,
};
assert_eq!(
classify_method_complexity("get_name", &analysis),
MethodComplexityClass::TrivialAccessor
);
}
#[test]
fn test_classify_trivial_accessor_literal() {
let analysis = MethodBodyAnalysis {
line_count: 1,
has_control_flow: false,
call_count: 0,
return_expr_type: Some(ReturnExprType::Literal),
has_self_param: true,
is_mutating: false,
};
assert_eq!(
classify_method_complexity("is_enabled", &analysis),
MethodComplexityClass::TrivialAccessor
);
}
#[test]
fn test_classify_trivial_accessor_mut() {
let analysis = MethodBodyAnalysis {
line_count: 1,
has_control_flow: false,
call_count: 0,
return_expr_type: Some(ReturnExprType::FieldAccess),
has_self_param: true,
is_mutating: true,
};
assert_eq!(
classify_method_complexity("data_mut", &analysis),
MethodComplexityClass::TrivialAccessor
);
}
#[test]
fn test_classify_simple_accessor() {
let analysis = MethodBodyAnalysis {
line_count: 2,
has_control_flow: false,
call_count: 1, return_expr_type: Some(ReturnExprType::MethodCall),
has_self_param: true,
is_mutating: false,
};
assert_eq!(
classify_method_complexity("get_name", &analysis),
MethodComplexityClass::SimpleAccessor
);
}
#[test]
fn test_classify_boilerplate_new() {
let analysis = MethodBodyAnalysis {
line_count: 5,
has_control_flow: false,
call_count: 1,
return_expr_type: None,
has_self_param: false,
is_mutating: false,
};
assert_eq!(
classify_method_complexity("new", &analysis),
MethodComplexityClass::Boilerplate
);
}
#[test]
fn test_classify_boilerplate_default() {
let analysis = MethodBodyAnalysis {
line_count: 10,
has_control_flow: false,
call_count: 0,
return_expr_type: None,
has_self_param: false,
is_mutating: false,
};
assert_eq!(
classify_method_complexity("default", &analysis),
MethodComplexityClass::Boilerplate
);
}
#[test]
fn test_classify_boilerplate_clone() {
let analysis = MethodBodyAnalysis {
line_count: 1,
has_control_flow: false,
call_count: 1,
return_expr_type: None,
has_self_param: true,
is_mutating: false,
};
assert_eq!(
classify_method_complexity("clone", &analysis),
MethodComplexityClass::Boilerplate
);
}
#[test]
fn test_classify_boilerplate_from() {
let analysis = MethodBodyAnalysis {
line_count: 3,
has_control_flow: false,
call_count: 2,
return_expr_type: None,
has_self_param: false,
is_mutating: false,
};
assert_eq!(
classify_method_complexity("from", &analysis),
MethodComplexityClass::Boilerplate
);
}
#[test]
fn test_classify_boilerplate_into() {
let analysis = MethodBodyAnalysis::default();
assert_eq!(
classify_method_complexity("into", &analysis),
MethodComplexityClass::Boilerplate
);
}
#[test]
fn test_classify_boilerplate_trait_impls() {
let analysis = MethodBodyAnalysis::default();
assert_eq!(
classify_method_complexity("fmt", &analysis),
MethodComplexityClass::Boilerplate
);
assert_eq!(
classify_method_complexity("eq", &analysis),
MethodComplexityClass::Boilerplate
);
assert_eq!(
classify_method_complexity("hash", &analysis),
MethodComplexityClass::Boilerplate
);
assert_eq!(
classify_method_complexity("drop", &analysis),
MethodComplexityClass::Boilerplate
);
}
#[test]
fn test_classify_delegating() {
let analysis = MethodBodyAnalysis {
line_count: 1,
has_control_flow: false,
call_count: 1,
return_expr_type: Some(ReturnExprType::MethodCall),
has_self_param: true,
is_mutating: false,
};
assert_eq!(
classify_method_complexity("process", &analysis),
MethodComplexityClass::Delegating
);
}
#[test]
fn test_classify_substantive_complex_logic() {
let analysis = MethodBodyAnalysis {
line_count: 25,
has_control_flow: true,
call_count: 5,
return_expr_type: Some(ReturnExprType::Complex),
has_self_param: true,
is_mutating: true,
};
assert_eq!(
classify_method_complexity("analyze_workspace", &analysis),
MethodComplexityClass::Substantive
);
}
#[test]
fn test_classify_substantive_with_control_flow() {
let analysis = MethodBodyAnalysis {
line_count: 5,
has_control_flow: true, call_count: 2,
return_expr_type: Some(ReturnExprType::Complex),
has_self_param: true,
is_mutating: false,
};
assert_eq!(
classify_method_complexity("get_name", &analysis), MethodComplexityClass::Substantive
);
}
#[test]
fn test_classify_has_prefix() {
let analysis = MethodBodyAnalysis {
line_count: 1,
has_control_flow: false,
call_count: 0,
return_expr_type: Some(ReturnExprType::FieldAccess),
has_self_param: true,
is_mutating: false,
};
assert_eq!(
classify_method_complexity("has_children", &analysis),
MethodComplexityClass::TrivialAccessor
);
}
#[test]
fn test_classify_with_prefix_builder() {
let analysis = MethodBodyAnalysis {
line_count: 2,
has_control_flow: false,
call_count: 0,
return_expr_type: Some(ReturnExprType::Complex),
has_self_param: true,
is_mutating: true,
};
assert_eq!(
classify_method_complexity("with_timeout", &analysis),
MethodComplexityClass::SimpleAccessor
);
}
#[test]
fn test_calculate_weighted_method_count() {
let classes = [
MethodComplexityClass::TrivialAccessor, MethodComplexityClass::TrivialAccessor, MethodComplexityClass::Boilerplate, MethodComplexityClass::Substantive, ];
let weighted = calculate_weighted_method_count(classes.iter());
assert!(
(weighted - 1.2).abs() < 0.01,
"Expected 1.2, got {}",
weighted
);
}
#[test]
fn test_weighted_count_all_accessors() {
let classes = [MethodComplexityClass::TrivialAccessor; 10];
let weighted = calculate_weighted_method_count(classes.iter());
assert!(
(weighted - 1.0).abs() < 0.01,
"Expected 1.0, got {}",
weighted
);
}
#[test]
fn test_weighted_count_mixed() {
let mut classes = vec![];
for _ in 0..10 {
classes.push(MethodComplexityClass::TrivialAccessor);
}
classes.push(MethodComplexityClass::Boilerplate);
for _ in 0..5 {
classes.push(MethodComplexityClass::Substantive);
}
let weighted = calculate_weighted_method_count(classes.iter());
assert!(
(weighted - 6.0).abs() < 0.01,
"Expected 6.0, got {}",
weighted
);
}
#[test]
fn test_method_complexity_class_weights() {
assert!((MethodComplexityClass::TrivialAccessor.weight() - 0.1).abs() < f64::EPSILON);
assert!((MethodComplexityClass::SimpleAccessor.weight() - 0.3).abs() < f64::EPSILON);
assert!((MethodComplexityClass::Boilerplate.weight() - 0.0).abs() < f64::EPSILON);
assert!((MethodComplexityClass::Delegating.weight() - 0.5).abs() < f64::EPSILON);
assert!((MethodComplexityClass::Substantive.weight() - 1.0).abs() < f64::EPSILON);
}
#[test]
fn test_method_complexity_class_default() {
assert_eq!(
MethodComplexityClass::default(),
MethodComplexityClass::Substantive
);
}
proptest! {
#[test]
fn prop_classify_method_idempotent(
method_name in "[a-z_]{1,20}",
line_count in 0usize..100,
has_control_flow in proptest::bool::ANY,
call_count in 0usize..20
) {
let analysis = MethodBodyAnalysis {
line_count,
has_control_flow,
call_count,
return_expr_type: Some(ReturnExprType::Complex),
has_self_param: true,
is_mutating: false,
};
let c1 = classify_method_complexity(&method_name, &analysis);
let c2 = classify_method_complexity(&method_name, &analysis);
prop_assert_eq!(c1, c2);
}
#[test]
fn prop_weighted_count_non_negative(
num_trivial in 0usize..50,
num_simple in 0usize..50,
num_boilerplate in 0usize..50,
num_delegating in 0usize..50,
num_substantive in 0usize..50
) {
let mut classes = vec![];
classes.extend(std::iter::repeat_n(MethodComplexityClass::TrivialAccessor, num_trivial));
classes.extend(std::iter::repeat_n(MethodComplexityClass::SimpleAccessor, num_simple));
classes.extend(std::iter::repeat_n(MethodComplexityClass::Boilerplate, num_boilerplate));
classes.extend(std::iter::repeat_n(MethodComplexityClass::Delegating, num_delegating));
classes.extend(std::iter::repeat_n(MethodComplexityClass::Substantive, num_substantive));
let weighted = calculate_weighted_method_count(classes.iter());
prop_assert!(weighted >= 0.0);
}
}
#[test]
fn test_classify_self_usage_pure_associated() {
let code = r#"
impl Foo {
fn helper(x: &str) -> bool { x.is_empty() }
}
"#;
let file: syn::File = syn::parse_str(code).unwrap();
if let syn::Item::Impl(impl_block) = &file.items[0] {
if let syn::ImplItem::Fn(method) = &impl_block.items[0] {
assert_eq!(
classify_self_usage(method),
MethodSelfUsage::PureAssociated,
"Method without self should be PureAssociated"
);
}
}
}
#[test]
fn test_classify_self_usage_instance_method() {
let code = r#"
impl Foo {
fn get_data(&self) -> &Data { &self.data }
}
"#;
let file: syn::File = syn::parse_str(code).unwrap();
if let syn::Item::Impl(impl_block) = &file.items[0] {
if let syn::ImplItem::Fn(method) = &impl_block.items[0] {
assert_eq!(
classify_self_usage(method),
MethodSelfUsage::InstanceMethod,
"Method using self.field should be InstanceMethod"
);
}
}
}
#[test]
fn test_classify_self_usage_instance_method_call() {
let code = r#"
impl Foo {
fn process(&self) { self.do_work(); }
}
"#;
let file: syn::File = syn::parse_str(code).unwrap();
if let syn::Item::Impl(impl_block) = &file.items[0] {
if let syn::ImplItem::Fn(method) = &impl_block.items[0] {
assert_eq!(
classify_self_usage(method),
MethodSelfUsage::InstanceMethod,
"Method calling self.method() should be InstanceMethod"
);
}
}
}
#[test]
fn test_classify_self_usage_unused_self() {
let code = r#"
impl Foo {
fn debug(&self) { println!("debug"); }
}
"#;
let file: syn::File = syn::parse_str(code).unwrap();
if let syn::Item::Impl(impl_block) = &file.items[0] {
if let syn::ImplItem::Fn(method) = &impl_block.items[0] {
assert_eq!(
classify_self_usage(method),
MethodSelfUsage::UnusedSelf,
"Method with unused self should be UnusedSelf"
);
}
}
}
#[test]
fn test_classify_self_usage_self_type_call() {
let code = r#"
impl Foo {
fn helper(&self) { Self::other_helper(); }
}
"#;
let file: syn::File = syn::parse_str(code).unwrap();
if let syn::Item::Impl(impl_block) = &file.items[0] {
if let syn::ImplItem::Fn(method) = &impl_block.items[0] {
assert_eq!(
classify_self_usage(method),
MethodSelfUsage::UnusedSelf,
"Method calling Self::method() (not self) should be UnusedSelf"
);
}
}
}
#[test]
fn test_classify_self_usage_closure_capture() {
let code = r#"
impl Foo {
fn with_closure(&self) {
let f = || { println!("no self usage"); };
f();
}
}
"#;
let file: syn::File = syn::parse_str(code).unwrap();
if let syn::Item::Impl(impl_block) = &file.items[0] {
if let syn::ImplItem::Fn(method) = &impl_block.items[0] {
assert_eq!(
classify_self_usage(method),
MethodSelfUsage::UnusedSelf,
"Self in closure should not count as self usage"
);
}
}
}
#[test]
fn test_classify_self_usage_mut_self() {
let code = r#"
impl Foo {
fn add_item(&mut self, item: Item) { self.items.push(item); }
}
"#;
let file: syn::File = syn::parse_str(code).unwrap();
if let syn::Item::Impl(impl_block) = &file.items[0] {
if let syn::ImplItem::Fn(method) = &impl_block.items[0] {
assert_eq!(
classify_self_usage(method),
MethodSelfUsage::InstanceMethod,
"Method with &mut self that mutates should be InstanceMethod"
);
}
}
}
#[test]
fn test_calculate_combined_method_weight_pure_accessor() {
let weight = calculate_combined_method_weight(
MethodComplexityClass::TrivialAccessor,
MethodSelfUsage::PureAssociated,
);
assert!(
(weight - 0.1).abs() < f64::EPSILON,
"Pure accessor should have weight 0.1, got {}",
weight
);
}
#[test]
fn test_calculate_combined_method_weight_instance_substantive() {
let weight = calculate_combined_method_weight(
MethodComplexityClass::Substantive,
MethodSelfUsage::InstanceMethod,
);
assert!(
(weight - 1.0).abs() < f64::EPSILON,
"Instance substantive should have weight 1.0, got {}",
weight
);
}
#[test]
fn test_calculate_combined_method_weight_pure_substantive() {
let weight = calculate_combined_method_weight(
MethodComplexityClass::Substantive,
MethodSelfUsage::PureAssociated,
);
assert!(
(weight - 0.2).abs() < f64::EPSILON,
"Pure substantive should have weight 0.2, got {}",
weight
);
}
#[test]
fn test_calculate_combined_method_weight_boilerplate_instance() {
let weight = calculate_combined_method_weight(
MethodComplexityClass::Boilerplate,
MethodSelfUsage::InstanceMethod,
);
assert!(
weight.abs() < f64::EPSILON,
"Boilerplate should have weight 0.0, got {}",
weight
);
}
#[test]
fn test_calculate_combined_weighted_count() {
let accessor_classes = vec![
MethodComplexityClass::Substantive,
MethodComplexityClass::Substantive,
MethodComplexityClass::Substantive,
]
.into_iter()
.chain(std::iter::repeat_n(MethodComplexityClass::Substantive, 21))
.collect::<Vec<_>>();
let self_usages = vec![
MethodSelfUsage::InstanceMethod,
MethodSelfUsage::InstanceMethod,
MethodSelfUsage::InstanceMethod,
]
.into_iter()
.chain(std::iter::repeat_n(MethodSelfUsage::PureAssociated, 21))
.collect::<Vec<_>>();
let weighted =
calculate_combined_weighted_count(accessor_classes.iter().zip(self_usages.iter()));
assert!(
(weighted - 7.2).abs() < 0.01,
"Expected ~7.2, got {}",
weighted
);
}
#[test]
fn test_method_self_usage_weights() {
assert!(
(MethodSelfUsage::PureAssociated.weight() - 0.2).abs() < f64::EPSILON,
"PureAssociated weight"
);
assert!(
(MethodSelfUsage::UnusedSelf.weight() - 0.3).abs() < f64::EPSILON,
"UnusedSelf weight"
);
assert!(
(MethodSelfUsage::InstanceMethod.weight() - 1.0).abs() < f64::EPSILON,
"InstanceMethod weight"
);
}
#[test]
fn test_method_self_usage_default() {
assert_eq!(
MethodSelfUsage::default(),
MethodSelfUsage::InstanceMethod,
"Default should be conservative (InstanceMethod)"
);
}
#[test]
fn test_method_self_usage_is_pure() {
assert!(
MethodSelfUsage::PureAssociated.is_pure(),
"PureAssociated should be pure"
);
assert!(
MethodSelfUsage::UnusedSelf.is_pure(),
"UnusedSelf should be pure"
);
assert!(
!MethodSelfUsage::InstanceMethod.is_pure(),
"InstanceMethod should NOT be pure"
);
}
proptest! {
#[test]
fn prop_classify_self_usage_deterministic(
has_self in proptest::bool::ANY,
uses_field in proptest::bool::ANY
) {
let code = if !has_self {
r#"impl Foo { fn helper() {} }"#.to_string()
} else if uses_field {
r#"impl Foo { fn get(&self) { self.x } }"#.to_string()
} else {
r#"impl Foo { fn debug(&self) { println!("x"); } }"#.to_string()
};
if let Ok(file) = syn::parse_str::<syn::File>(&code) {
if let syn::Item::Impl(impl_block) = &file.items[0] {
if let syn::ImplItem::Fn(method) = &impl_block.items[0] {
let c1 = classify_self_usage(method);
let c2 = classify_self_usage(method);
prop_assert_eq!(c1, c2);
}
}
}
}
#[test]
fn prop_combined_weight_bounded(
accessor in prop::sample::select(vec![
MethodComplexityClass::TrivialAccessor,
MethodComplexityClass::SimpleAccessor,
MethodComplexityClass::Boilerplate,
MethodComplexityClass::Delegating,
MethodComplexityClass::Substantive,
]),
self_usage in prop::sample::select(vec![
MethodSelfUsage::PureAssociated,
MethodSelfUsage::UnusedSelf,
MethodSelfUsage::InstanceMethod,
])
) {
let weight = calculate_combined_method_weight(accessor, self_usage);
prop_assert!((0.0..=1.0).contains(&weight),
"Combined weight {} out of bounds", weight);
}
#[test]
fn prop_pure_always_lower_weight_than_instance(
num_methods in 1..50usize
) {
let pure_weight: f64 = (0..num_methods)
.map(|_| MethodSelfUsage::PureAssociated.weight())
.sum();
let instance_weight: f64 = (0..num_methods)
.map(|_| MethodSelfUsage::InstanceMethod.weight())
.sum();
prop_assert!(pure_weight < instance_weight);
}
}
}