use crate::analyzers::type_registry::MethodSignature;
use crate::organization::god_object::types::ModuleSplit;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
pub struct AntiPatternDetector {
config: AntiPatternConfig,
}
#[derive(Clone, Debug)]
pub struct AntiPatternConfig {
pub max_parameters: usize,
pub max_mixed_types: usize,
pub critical_penalty: f64,
pub high_penalty: f64,
pub medium_penalty: f64,
pub low_penalty: f64,
}
impl Default for AntiPatternConfig {
fn default() -> Self {
Self {
max_parameters: 4,
max_mixed_types: 3,
critical_penalty: 20.0,
high_penalty: 10.0,
medium_penalty: 5.0,
low_penalty: 2.0,
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct AntiPattern {
pub pattern_type: AntiPatternType,
pub severity: AntiPatternSeverity,
pub location: String,
pub description: String,
pub correction: String,
pub affected_methods: Vec<String>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum AntiPatternType {
UtilitiesModule,
TechnicalGrouping,
ParameterPassing,
MixedDataTypes,
LackOfTypeOwnership,
}
#[derive(Clone, Debug, PartialEq, Ord, PartialOrd, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum AntiPatternSeverity {
Critical,
High,
Medium,
Low,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct SplitQualityReport {
pub quality_score: f64,
pub anti_patterns: Vec<AntiPattern>,
pub total_splits: usize,
pub idiomatic_splits: usize,
}
impl AntiPatternDetector {
pub fn new() -> Self {
Self {
config: AntiPatternConfig::default(),
}
}
pub fn with_config(config: AntiPatternConfig) -> Self {
Self { config }
}
pub fn detect_utilities_module(&self, split: &ModuleSplit) -> Option<AntiPattern> {
let module_name = split
.suggested_name
.trim_end_matches(".rs")
.rsplit('/')
.next()
.unwrap_or(&split.suggested_name);
let is_utilities = module_name == "utilities"
|| module_name == "utils"
|| module_name == "helpers"
|| module_name == "common";
if !is_utilities {
return None;
}
Some(AntiPattern {
pattern_type: AntiPatternType::UtilitiesModule,
severity: AntiPatternSeverity::Critical,
location: split.suggested_name.clone(),
description: format!(
"Utilities module '{}' is a catch-all with {} mixed responsibilities. \
This violates Single Responsibility Principle and creates unclear ownership.",
split.suggested_name,
split.methods_to_move.len()
),
correction: self.suggest_utilities_correction(split),
affected_methods: split.methods_to_move.clone(),
})
}
fn suggest_utilities_correction(&self, _split: &ModuleSplit) -> String {
"Split utilities into domain-specific modules:\n\
1. Group methods by the primary type they operate on\n\
2. Move formatting methods to the type they format (e.g., PriorityItem::display)\n\
3. Extract parameter clumps into new types with methods\n\
4. Consider trait implementations (Display, From, TryFrom) instead of utility functions"
.to_string()
}
pub fn detect_technical_grouping(&self, split: &ModuleSplit) -> Option<AntiPattern> {
let module_name = split
.suggested_name
.rsplit('/')
.next()
.unwrap_or(&split.suggested_name)
.trim_end_matches(".rs");
let is_verb_name = self.is_likely_verb(module_name);
let method_verbs: Vec<_> = split
.methods_to_move
.iter()
.filter_map(|m| self.extract_leading_verb(m))
.collect();
let shared_verb = if method_verbs.len() >= split.methods_to_move.len() / 2 {
let most_common = self.most_common_element(&method_verbs);
Some(most_common)
} else {
None
};
let is_domain_term = self.is_domain_term(module_name);
if (is_verb_name || shared_verb.is_some()) && !is_domain_term {
Some(AntiPattern {
pattern_type: AntiPatternType::TechnicalGrouping,
severity: AntiPatternSeverity::High,
location: split.suggested_name.clone(),
description: format!(
"Module '{}' is grouped by technical operation (verb) instead of data domain. \
This scatters type-related behavior across multiple modules.",
module_name
),
correction: self.suggest_type_based_grouping(split),
affected_methods: split.methods_to_move.clone(),
})
} else {
None
}
}
fn is_likely_verb(&self, word: &str) -> bool {
word.ends_with("ing")
|| word.ends_with("tion")
|| word.ends_with("ment")
|| word.ends_with("sion")
|| word.ends_with("ance")
|| word.ends_with("ence")
|| matches!(
word,
"calculate" | "compute" | "process" | "handle" | "manage" |
"render" | "format" | "display" | "show" | "print" |
"validate" | "check" | "verify" | "ensure" |
"parse" | "transform" | "convert" | "serialize" | "deserialize" |
"get" | "set" | "update" | "modify" | "create" | "delete" |
"authenticate" | "authorize" | "encrypt" | "decrypt"
)
}
fn extract_leading_verb(&self, method_name: &str) -> Option<String> {
method_name.split('_').next().map(|s| s.to_string())
}
fn most_common_element(&self, items: &[String]) -> String {
let mut counts: HashMap<&str, usize> = HashMap::new();
for item in items {
*counts.entry(item.as_str()).or_insert(0) += 1;
}
counts
.into_iter()
.max_by_key(|(_, count)| *count)
.map(|(item, _)| item.to_string())
.unwrap_or_default()
}
fn is_domain_term(&self, word: &str) -> bool {
word.ends_with("metrics")
|| word.ends_with("data")
|| word.ends_with("config")
|| word.ends_with("settings")
|| word.ends_with("context")
|| word.ends_with("item")
|| word.ends_with("result")
|| word.ends_with("info")
|| word.ends_with("details")
|| (word.ends_with('s') && !word.ends_with("ss"))
|| matches!(
word,
"priority" | "god_object" | "debt" | "complexity" |
"coverage" | "analysis" | "report" | "summary"
)
}
fn suggest_type_based_grouping(&self, _split: &ModuleSplit) -> String {
"Reorganize by data types:\n\
1. Identify the primary types these methods operate on\n\
2. Create modules named after those types (e.g., priority_item.rs, god_object_section.rs)\n\
3. Move all methods operating on a type to its module\n\
4. Use impl blocks to associate methods with their types\n\
\n\
Example:\n\
Instead of: calculate/calculate_score.rs\n\
Use: god_object_metrics.rs with impl GodObjectMetrics { fn score() }"
.to_string()
}
pub fn detect_parameter_passing(
&self,
signatures: &[MethodSignature],
split: &ModuleSplit,
) -> Vec<AntiPattern> {
let mut anti_patterns = Vec::new();
let split_methods: Vec<_> = signatures
.iter()
.filter(|sig| split.methods_to_move.contains(&sig.name))
.collect();
for signature in split_methods {
if signature.param_types.len() >= self.config.max_parameters {
anti_patterns.push(AntiPattern {
pattern_type: AntiPatternType::ParameterPassing,
severity: AntiPatternSeverity::Medium,
location: format!("{}::{}", split.suggested_name, signature.name),
description: format!(
"Method '{}' has {} parameters. Functions with 4+ parameters are hard to call and maintain.",
signature.name,
signature.param_types.len()
),
correction: format!(
"Encapsulate related parameters into a struct:\n\
1. Identify parameter clumps (params that are always passed together)\n\
2. Create a new struct to hold these parameters\n\
3. Update method signature to take the struct\n\
\n\
Example:\n\
Instead of: fn {}({}) {{\n\
Use: struct {}Params {{ ... }}\n\
fn {}(params: {}Params) {{",
signature.name,
signature.param_types.join(", "),
to_pascal_case(&signature.name),
signature.name,
to_pascal_case(&signature.name)
),
affected_methods: vec![signature.name.clone()],
});
}
}
anti_patterns
}
pub fn detect_mixed_data_types(
&self,
signatures: &[MethodSignature],
split: &ModuleSplit,
) -> Option<AntiPattern> {
let split_methods: Vec<_> = signatures
.iter()
.filter(|sig| split.methods_to_move.contains(&sig.name))
.collect();
let mut types = std::collections::HashSet::new();
for signature in &split_methods {
for param_type in &signature.param_types {
if !is_primitive(param_type) {
types.insert(param_type.clone());
}
}
if let Some(return_type) = &signature.return_type {
if !is_primitive(return_type) {
types.insert(return_type.clone());
}
}
}
if types.len() > self.config.max_mixed_types - 1 {
let type_list: Vec<_> = types.iter().cloned().collect();
Some(AntiPattern {
pattern_type: AntiPatternType::MixedDataTypes,
severity: AntiPatternSeverity::High,
location: split.suggested_name.clone(),
description: format!(
"Module '{}' operates on {} distinct non-primitive types: {}. \
This indicates mixed responsibilities and unclear domain boundaries.",
split.suggested_name,
types.len(),
type_list.join(", ")
),
correction: format!(
"Split module by primary data type:\n\
1. Group methods by the main type they operate on\n\
2. Create separate modules for each type (e.g., {}.rs, {}.rs)\n\
3. Move cross-cutting concerns to trait implementations\n\
\n\
Detected types: {}",
type_list.first().unwrap_or(&"type1".to_string()),
type_list.get(1).unwrap_or(&"type2".to_string()),
type_list.join(", ")
),
affected_methods: split.methods_to_move.clone(),
})
} else {
None
}
}
pub fn analyze_split(
&self,
split: &ModuleSplit,
signatures: &[MethodSignature],
) -> Vec<AntiPattern> {
let mut anti_patterns = Vec::new();
if let Some(pattern) = self.detect_utilities_module(split) {
anti_patterns.push(pattern);
}
if let Some(pattern) = self.detect_technical_grouping(split) {
anti_patterns.push(pattern);
}
anti_patterns.extend(self.detect_parameter_passing(signatures, split));
if let Some(pattern) = self.detect_mixed_data_types(signatures, split) {
anti_patterns.push(pattern);
}
anti_patterns.sort_by(|a, b| b.severity.cmp(&a.severity));
anti_patterns
}
pub fn calculate_split_quality(
&self,
splits: &[ModuleSplit],
signatures: &[MethodSignature],
) -> SplitQualityReport {
let mut all_anti_patterns = Vec::new();
for split in splits {
let patterns = self.analyze_split(split, signatures);
all_anti_patterns.extend(patterns);
}
let critical_count = all_anti_patterns
.iter()
.filter(|p| p.severity == AntiPatternSeverity::Critical)
.count();
let high_count = all_anti_patterns
.iter()
.filter(|p| p.severity == AntiPatternSeverity::High)
.count();
let medium_count = all_anti_patterns
.iter()
.filter(|p| p.severity == AntiPatternSeverity::Medium)
.count();
let low_count = all_anti_patterns
.iter()
.filter(|p| p.severity == AntiPatternSeverity::Low)
.count();
let quality_score = 100.0
- (critical_count as f64 * self.config.critical_penalty)
- (high_count as f64 * self.config.high_penalty)
- (medium_count as f64 * self.config.medium_penalty)
- (low_count as f64 * self.config.low_penalty);
SplitQualityReport {
quality_score: quality_score.max(0.0),
anti_patterns: all_anti_patterns,
total_splits: splits.len(),
idiomatic_splits: splits
.len()
.saturating_sub(critical_count)
.saturating_sub(high_count),
}
}
}
impl Default for AntiPatternDetector {
fn default() -> Self {
Self::new()
}
}
pub fn to_pascal_case(s: &str) -> String {
s.split('_')
.map(|word| {
let mut chars = word.chars();
match chars.next() {
None => String::new(),
Some(first) => first.to_uppercase().chain(chars).collect(),
}
})
.collect()
}
pub fn is_primitive(type_name: &str) -> bool {
matches!(
type_name,
"String"
| "str"
| "usize"
| "isize"
| "u32"
| "i32"
| "u64"
| "i64"
| "u8"
| "i8"
| "u16"
| "i16"
| "u128"
| "i128"
| "f32"
| "f64"
| "bool"
| "char"
| "()"
| "Vec"
| "Option"
| "Result"
| "Box"
| "Rc"
| "Arc"
| "HashMap"
| "HashSet"
| "BTreeMap"
| "BTreeSet"
| "VecDeque"
| "LinkedList"
| "BinaryHeap"
| "Path"
| "PathBuf"
| "OsString"
| "OsStr"
| "File"
| "BufReader"
| "BufWriter"
| "Cow"
| "RefCell"
| "Cell"
| "Mutex"
| "RwLock"
| "Error"
) || type_name.starts_with('&')
|| type_name.starts_with("&mut")
}
use std::fmt;
const BOX_WIDTH: usize = 62;
const BOX_TOP: &str = "╔══════════════════════════════════════════════════════════════╗";
const BOX_BOTTOM: &str = "╚══════════════════════════════════════════════════════════════╝";
const BOX_DIVIDER: &str = "╠══════════════════════════════════════════════════════════════╣";
fn format_box_row(content: &str, width: usize) -> String {
format!("║ {:<width$} ║", content, width = width)
}
fn build_summary_lines(report: &SplitQualityReport) -> Vec<String> {
vec![
BOX_TOP.to_string(),
format_box_row("Split Quality Analysis", BOX_WIDTH),
BOX_DIVIDER.to_string(),
format_box_row(
&format!("Quality Score: {:.1}/100.0", report.quality_score),
BOX_WIDTH,
),
format_box_row(&format!("Total Splits: {}", report.total_splits), BOX_WIDTH),
format_box_row(
&format!("Idiomatic Splits: {}", report.idiomatic_splits),
BOX_WIDTH,
),
BOX_BOTTOM.to_string(),
]
}
fn build_anti_patterns_header(count: usize) -> Vec<String> {
vec![
String::new(),
BOX_TOP.to_string(),
format_box_row(&format!("Anti-Patterns Found ({count:<2})"), BOX_WIDTH),
BOX_BOTTOM.to_string(),
]
}
impl fmt::Display for SplitQualityReport {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for line in build_summary_lines(self) {
writeln!(f, "{line}")?;
}
if !self.anti_patterns.is_empty() {
for line in build_anti_patterns_header(self.anti_patterns.len()) {
writeln!(f, "{line}")?;
}
for (i, pattern) in self.anti_patterns.iter().enumerate() {
if i > 0 {
writeln!(f)?;
}
write!(f, "{pattern}")?;
}
}
Ok(())
}
}
const DETAIL_TOP: &str = "┌──────────────────────────────────────────────────────────────┐";
const DETAIL_BOTTOM: &str = "└──────────────────────────────────────────────────────────────┘";
const DETAIL_DIVIDER: &str = "├──────────────────────────────────────────────────────────────┤";
const DETAIL_WIDTH: usize = 58;
const DETAIL_INNER_WIDTH: usize = 56;
fn severity_display(severity: &AntiPatternSeverity) -> &'static str {
match severity {
AntiPatternSeverity::Critical => "🔴 CRITICAL",
AntiPatternSeverity::High => "🟠 HIGH",
AntiPatternSeverity::Medium => "🟡 MEDIUM",
AntiPatternSeverity::Low => "🟢 LOW",
}
}
fn pattern_type_display(pattern_type: &AntiPatternType) -> &'static str {
match pattern_type {
AntiPatternType::UtilitiesModule => "Utilities Module",
AntiPatternType::TechnicalGrouping => "Technical Grouping",
AntiPatternType::ParameterPassing => "Parameter Passing",
AntiPatternType::MixedDataTypes => "Mixed Data Types",
AntiPatternType::LackOfTypeOwnership => "Lack of Type Ownership",
}
}
fn format_detail_row(content: &str, width: usize) -> String {
format!("│ {:<width$} │", content, width = width)
}
fn format_indented_row(content: &str, width: usize) -> String {
format!("│ {:<width$} │", content, width = width)
}
fn build_wrapped_section(header: &str, text: &str) -> Vec<String> {
let mut lines = vec![format_detail_row(header, BOX_WIDTH)];
for line in wrap_text(text, DETAIL_WIDTH) {
lines.push(format_indented_row(&line, DETAIL_WIDTH));
}
lines
}
fn build_affected_methods_section(methods: &[String]) -> Vec<String> {
if methods.is_empty() {
return Vec::new();
}
let mut lines = vec![
DETAIL_DIVIDER.to_string(),
format_detail_row(
&format!("Affected Methods ({:<2}):", methods.len()),
BOX_WIDTH,
),
];
for method in methods.iter().take(5) {
lines.push(format_indented_row(
&format!("• {method}"),
DETAIL_INNER_WIDTH,
));
}
if methods.len() > 5 {
lines.push(format_indented_row(
&format!("... and {} more", methods.len() - 5),
DETAIL_INNER_WIDTH,
));
}
lines
}
impl fmt::Display for AntiPattern {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let severity = severity_display(&self.severity);
let pattern_name = pattern_type_display(&self.pattern_type);
let mut lines = vec![
DETAIL_TOP.to_string(),
format!("│ {severity} - {pattern_name:<40} │"),
DETAIL_DIVIDER.to_string(),
format_detail_row(&format!("Location: {}", self.location), BOX_WIDTH),
DETAIL_DIVIDER.to_string(),
];
lines.extend(build_wrapped_section("Description:", &self.description));
lines.push(DETAIL_DIVIDER.to_string());
lines.extend(build_wrapped_section("Correction:", &self.correction));
lines.extend(build_affected_methods_section(&self.affected_methods));
lines.push(DETAIL_BOTTOM.to_string());
for line in lines {
writeln!(f, "{line}")?;
}
Ok(())
}
}
fn wrap_text(text: &str, width: usize) -> Vec<String> {
let mut lines = Vec::new();
let mut current_line = String::new();
for word in text.split_whitespace() {
if current_line.is_empty() {
current_line = word.to_string();
} else if current_line.len() + word.len() < width {
current_line.push(' ');
current_line.push_str(word);
} else {
lines.push(current_line);
current_line = word.to_string();
}
}
if !current_line.is_empty() {
lines.push(current_line);
}
lines
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_utilities_module_detection() {
let split = ModuleSplit {
suggested_name: "god_object/utilities.rs".to_string(),
methods_to_move: vec!["foo".to_string(), "bar".to_string()],
responsibility: "utilities".to_string(),
estimated_lines: 100,
method_count: 2,
..Default::default()
};
let detector = AntiPatternDetector::new();
let pattern = detector.detect_utilities_module(&split);
assert!(pattern.is_some());
let pattern = pattern.unwrap();
assert_eq!(pattern.pattern_type, AntiPatternType::UtilitiesModule);
assert_eq!(pattern.severity, AntiPatternSeverity::Critical);
}
#[test]
fn test_technical_grouping_detection() {
let split = ModuleSplit {
suggested_name: "god_object/calculate.rs".to_string(),
methods_to_move: vec!["calculate_score".to_string()],
responsibility: "calculation".to_string(),
estimated_lines: 50,
method_count: 1,
..Default::default()
};
let detector = AntiPatternDetector::new();
let pattern = detector.detect_technical_grouping(&split);
assert!(pattern.is_some());
let pattern = pattern.unwrap();
assert_eq!(pattern.pattern_type, AntiPatternType::TechnicalGrouping);
assert_eq!(pattern.severity, AntiPatternSeverity::High);
}
#[test]
fn test_quality_score_calculation() {
let splits = vec![
ModuleSplit {
suggested_name: "good_module.rs".to_string(),
methods_to_move: vec!["foo".to_string()],
responsibility: "domain".to_string(),
estimated_lines: 50,
method_count: 1,
..Default::default()
},
ModuleSplit {
suggested_name: "utilities.rs".to_string(),
methods_to_move: vec!["bar".to_string()],
responsibility: "utilities".to_string(),
estimated_lines: 50,
method_count: 1,
..Default::default()
},
];
let detector = AntiPatternDetector::new();
let report = detector.calculate_split_quality(&splits, &[]);
assert!(report.quality_score < 100.0);
assert!(!report.anti_patterns.is_empty());
}
#[test]
fn test_is_likely_verb() {
let detector = AntiPatternDetector::new();
assert!(detector.is_likely_verb("rendering"));
assert!(detector.is_likely_verb("calculation"));
assert!(detector.is_likely_verb("management"));
assert!(detector.is_likely_verb("format"));
assert!(detector.is_likely_verb("calculate"));
assert!(!detector.is_likely_verb("metrics"));
assert!(!detector.is_likely_verb("config"));
assert!(!detector.is_likely_verb("data"));
}
#[test]
fn test_is_domain_term() {
let detector = AntiPatternDetector::new();
assert!(detector.is_domain_term("metrics"));
assert!(detector.is_domain_term("config"));
assert!(detector.is_domain_term("priority"));
assert!(detector.is_domain_term("results"));
assert!(!detector.is_domain_term("rendering"));
assert!(!detector.is_domain_term("calculate"));
}
#[test]
fn test_to_pascal_case() {
assert_eq!(to_pascal_case("hello_world"), "HelloWorld");
assert_eq!(to_pascal_case("format_header"), "FormatHeader");
assert_eq!(to_pascal_case("simple"), "Simple");
}
#[test]
fn test_is_primitive() {
assert!(is_primitive("String"));
assert!(is_primitive("usize"));
assert!(is_primitive("Vec"));
assert!(is_primitive("Option"));
assert!(is_primitive("&str"));
assert!(!is_primitive("CustomType"));
assert!(!is_primitive("MyStruct"));
}
}