use crate::core::{DebtItem, DebtType, FunctionMetrics, Priority};
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, PartialEq)]
pub enum SmellType {
LongParameterList,
LargeClass,
LongMethod,
FeatureEnvy,
DataClump,
DeepNesting,
DuplicateCode,
}
#[derive(Debug, Clone)]
pub struct CodeSmell {
pub smell_type: SmellType,
pub location: PathBuf,
pub line: usize,
pub message: String,
pub severity: Priority,
}
impl CodeSmell {
pub fn to_debt_item(&self) -> DebtItem {
DebtItem {
id: format!(
"smell-{:?}-{}-{}",
self.smell_type,
self.location.display(),
self.line
),
debt_type: DebtType::CodeSmell {
smell_type: Some(format!("{:?}", self.smell_type)),
},
priority: self.severity,
file: self.location.clone(),
line: self.line,
column: None,
message: self.message.clone(),
context: None,
}
}
}
pub fn detect_long_parameter_list(func: &FunctionMetrics, param_count: usize) -> Option<CodeSmell> {
const THRESHOLD: usize = 5;
if param_count > THRESHOLD {
Some(CodeSmell {
smell_type: SmellType::LongParameterList,
location: func.file.clone(),
line: func.line,
message: format!(
"Function '{}' has {} parameters (threshold: {})",
func.name, param_count, THRESHOLD
),
severity: if param_count > THRESHOLD * 2 {
Priority::High
} else {
Priority::Medium
},
})
} else {
None
}
}
pub fn detect_large_module(path: &Path, line_count: usize) -> Option<CodeSmell> {
const THRESHOLD: usize = 300;
if line_count > THRESHOLD {
Some(CodeSmell {
smell_type: SmellType::LargeClass,
location: path.to_path_buf(),
line: 1,
message: format!("Module has {line_count} lines (threshold: {THRESHOLD})"),
severity: if line_count > THRESHOLD * 2 {
Priority::High
} else {
Priority::Medium
},
})
} else {
None
}
}
pub fn detect_long_method(func: &FunctionMetrics) -> Option<CodeSmell> {
const THRESHOLD: usize = 50;
if func.length > THRESHOLD {
Some(CodeSmell {
smell_type: SmellType::LongMethod,
location: func.file.clone(),
line: func.line,
message: format!(
"Function '{}' has {} lines (threshold: {})",
func.name, func.length, THRESHOLD
),
severity: if func.length > THRESHOLD * 2 {
Priority::High
} else {
Priority::Medium
},
})
} else {
None
}
}
pub fn detect_deep_nesting(func: &FunctionMetrics) -> Option<CodeSmell> {
const THRESHOLD: u32 = 4;
if func.nesting > THRESHOLD {
Some(CodeSmell {
smell_type: SmellType::DeepNesting,
location: func.file.clone(),
line: func.line,
message: format!(
"Function '{}' has nesting depth of {} (threshold: {})",
func.name, func.nesting, THRESHOLD
),
severity: if func.nesting > THRESHOLD * 2 {
Priority::High
} else {
Priority::Medium
},
})
} else {
None
}
}
pub fn analyze_function_smells(func: &FunctionMetrics, param_count: usize) -> Vec<CodeSmell> {
let mut smells = Vec::new();
if let Some(smell) = detect_long_parameter_list(func, param_count) {
smells.push(smell);
}
if let Some(smell) = detect_long_method(func) {
smells.push(smell);
}
if let Some(smell) = detect_deep_nesting(func) {
smells.push(smell);
}
smells
}
pub fn analyze_module_smells(path: &Path, line_count: usize) -> Vec<CodeSmell> {
let mut smells = Vec::new();
if let Some(smell) = detect_large_module(path, line_count) {
smells.push(smell);
}
smells
}
pub fn detect_data_clumps(functions: &[FunctionMetrics]) -> Vec<CodeSmell> {
let mut smells = Vec::new();
for i in 0..functions.len() {
for j in i + 1..functions.len() {
if functions[i].file == functions[j].file {
if functions[i].length > 30 && functions[j].length > 30 {
smells.push(CodeSmell {
smell_type: SmellType::DataClump,
location: functions[i].file.clone(),
line: functions[i].line,
message: format!(
"Functions '{}' and '{}' may share data clumps",
functions[i].name, functions[j].name
),
severity: Priority::Low,
});
break; }
}
}
}
smells
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::FunctionMetrics;
use std::path::PathBuf;
#[test]
fn test_detect_data_clumps_empty_functions() {
let functions = vec![];
let smells = detect_data_clumps(&functions);
assert_eq!(
smells.len(),
0,
"No smells should be detected for empty input"
);
}
#[test]
fn test_detect_data_clumps_single_function() {
let functions = vec![FunctionMetrics {
name: "large_function".to_string(),
file: PathBuf::from("src/lib.rs"),
line: 10,
cyclomatic: 5,
cognitive: 10,
nesting: 2,
length: 35,
is_test: false,
visibility: None,
is_trait_method: false,
in_test_module: false,
entropy_score: None,
is_pure: None,
purity_confidence: None,
purity_reason: None,
call_dependencies: None,
detected_patterns: None,
upstream_callers: None,
downstream_callees: None,
mapping_pattern_result: None,
adjusted_complexity: None,
composition_metrics: None,
language_specific: None,
purity_level: None,
error_swallowing_count: None,
error_swallowing_patterns: None,
entropy_analysis: None,
}];
let smells = detect_data_clumps(&functions);
assert_eq!(
smells.len(),
0,
"Single function cannot have data clumps with itself"
);
}
#[test]
fn test_detect_data_clumps_different_files() {
let functions = vec![
FunctionMetrics {
name: "function_a".to_string(),
file: PathBuf::from("src/module_a.rs"),
line: 10,
cyclomatic: 5,
cognitive: 10,
nesting: 2,
length: 35,
is_test: false,
visibility: None,
is_trait_method: false,
in_test_module: false,
entropy_score: None,
is_pure: None,
purity_confidence: None,
purity_reason: None,
call_dependencies: None,
detected_patterns: None,
upstream_callers: None,
downstream_callees: None,
mapping_pattern_result: None,
adjusted_complexity: None,
composition_metrics: None,
language_specific: None,
purity_level: None,
error_swallowing_count: None,
error_swallowing_patterns: None,
entropy_analysis: None,
},
FunctionMetrics {
name: "function_b".to_string(),
file: PathBuf::from("src/module_b.rs"),
line: 20,
cyclomatic: 5,
cognitive: 10,
nesting: 2,
length: 35,
is_test: false,
visibility: None,
is_trait_method: false,
in_test_module: false,
entropy_score: None,
is_pure: None,
purity_confidence: None,
purity_reason: None,
call_dependencies: None,
detected_patterns: None,
upstream_callers: None,
downstream_callees: None,
mapping_pattern_result: None,
adjusted_complexity: None,
composition_metrics: None,
language_specific: None,
purity_level: None,
error_swallowing_count: None,
error_swallowing_patterns: None,
entropy_analysis: None,
},
];
let smells = detect_data_clumps(&functions);
assert_eq!(
smells.len(),
0,
"Functions in different files should not be reported as data clumps"
);
}
#[test]
fn test_detect_data_clumps_same_file_large_functions() {
let functions = vec![
FunctionMetrics {
name: "process_user_data".to_string(),
file: PathBuf::from("src/user_handler.rs"),
line: 10,
cyclomatic: 8,
cognitive: 15,
nesting: 3,
length: 40,
is_test: false,
visibility: None,
is_trait_method: false,
in_test_module: false,
entropy_score: None,
is_pure: None,
purity_confidence: None,
purity_reason: None,
call_dependencies: None,
detected_patterns: None,
upstream_callers: None,
downstream_callees: None,
mapping_pattern_result: None,
adjusted_complexity: None,
composition_metrics: None,
language_specific: None,
purity_level: None,
error_swallowing_count: None,
error_swallowing_patterns: None,
entropy_analysis: None,
},
FunctionMetrics {
name: "validate_user_data".to_string(),
file: PathBuf::from("src/user_handler.rs"),
line: 60,
cyclomatic: 6,
cognitive: 12,
nesting: 2,
length: 35,
is_test: false,
visibility: None,
is_trait_method: false,
in_test_module: false,
entropy_score: None,
is_pure: None,
purity_confidence: None,
purity_reason: None,
call_dependencies: None,
detected_patterns: None,
upstream_callers: None,
downstream_callees: None,
mapping_pattern_result: None,
adjusted_complexity: None,
composition_metrics: None,
language_specific: None,
purity_level: None,
error_swallowing_count: None,
error_swallowing_patterns: None,
entropy_analysis: None,
},
];
let smells = detect_data_clumps(&functions);
assert_eq!(
smells.len(),
1,
"Should detect data clump for large functions in same file"
);
let smell = &smells[0];
assert_eq!(smell.smell_type, SmellType::DataClump);
assert_eq!(smell.location, PathBuf::from("src/user_handler.rs"));
assert_eq!(smell.line, 10);
assert!(smell.message.contains("process_user_data"));
assert!(smell.message.contains("validate_user_data"));
assert_eq!(smell.severity, Priority::Low);
}
#[test]
fn test_detect_data_clumps_multiple_clumps() {
let functions = vec![
FunctionMetrics {
name: "func_a".to_string(),
file: PathBuf::from("src/module.rs"),
line: 10,
cyclomatic: 5,
cognitive: 10,
nesting: 2,
length: 35,
is_test: false,
visibility: None,
is_trait_method: false,
in_test_module: false,
entropy_score: None,
is_pure: None,
purity_confidence: None,
purity_reason: None,
call_dependencies: None,
detected_patterns: None,
upstream_callers: None,
downstream_callees: None,
mapping_pattern_result: None,
adjusted_complexity: None,
composition_metrics: None,
language_specific: None,
purity_level: None,
error_swallowing_count: None,
error_swallowing_patterns: None,
entropy_analysis: None,
},
FunctionMetrics {
name: "func_b".to_string(),
file: PathBuf::from("src/module.rs"),
line: 50,
cyclomatic: 5,
cognitive: 10,
nesting: 2,
length: 32,
is_test: false,
visibility: None,
is_trait_method: false,
in_test_module: false,
entropy_score: None,
is_pure: None,
purity_confidence: None,
purity_reason: None,
call_dependencies: None,
detected_patterns: None,
upstream_callers: None,
downstream_callees: None,
mapping_pattern_result: None,
adjusted_complexity: None,
composition_metrics: None,
language_specific: None,
purity_level: None,
error_swallowing_count: None,
error_swallowing_patterns: None,
entropy_analysis: None,
},
FunctionMetrics {
name: "func_c".to_string(),
file: PathBuf::from("src/module.rs"),
line: 90,
cyclomatic: 5,
cognitive: 10,
nesting: 2,
length: 31,
is_test: false,
visibility: None,
is_trait_method: false,
in_test_module: false,
entropy_score: None,
is_pure: None,
purity_confidence: None,
purity_reason: None,
call_dependencies: None,
detected_patterns: None,
upstream_callers: None,
downstream_callees: None,
mapping_pattern_result: None,
adjusted_complexity: None,
composition_metrics: None,
language_specific: None,
purity_level: None,
error_swallowing_count: None,
error_swallowing_patterns: None,
entropy_analysis: None,
},
FunctionMetrics {
name: "small_func".to_string(),
file: PathBuf::from("src/module.rs"),
line: 130,
cyclomatic: 2,
cognitive: 3,
nesting: 1,
length: 10,
is_test: false,
visibility: None,
is_trait_method: false,
in_test_module: false,
entropy_score: None,
is_pure: None,
purity_confidence: None,
purity_reason: None,
call_dependencies: None,
detected_patterns: None,
upstream_callers: None,
downstream_callees: None,
mapping_pattern_result: None,
adjusted_complexity: None,
composition_metrics: None,
language_specific: None,
purity_level: None,
error_swallowing_count: None,
error_swallowing_patterns: None,
entropy_analysis: None,
},
];
let smells = detect_data_clumps(&functions);
assert_eq!(smells.len(), 2, "Should detect multiple data clumps");
assert_eq!(smells[0].line, 10);
assert!(smells[0].message.contains("func_a"));
assert!(smells[0].message.contains("func_b"));
assert_eq!(smells[1].line, 50);
assert!(smells[1].message.contains("func_b"));
assert!(smells[1].message.contains("func_c"));
}
#[test]
fn test_detect_long_parameter_list() {
let func = FunctionMetrics {
name: "test_func".to_string(),
file: PathBuf::from("src/test.rs"),
line: 10,
cyclomatic: 5,
cognitive: 10,
nesting: 2,
length: 20,
is_test: false,
visibility: None,
is_trait_method: false,
in_test_module: false,
entropy_score: None,
is_pure: None,
purity_confidence: None,
purity_reason: None,
call_dependencies: None,
detected_patterns: None,
upstream_callers: None,
downstream_callees: None,
mapping_pattern_result: None,
adjusted_complexity: None,
composition_metrics: None,
language_specific: None,
purity_level: None,
error_swallowing_count: None,
error_swallowing_patterns: None,
entropy_analysis: None,
};
let smell = detect_long_parameter_list(&func, 3);
assert!(smell.is_none(), "Should not detect smell for 3 parameters");
let smell = detect_long_parameter_list(&func, 5);
assert!(smell.is_none(), "Should not detect smell at threshold");
let smell = detect_long_parameter_list(&func, 6);
assert!(smell.is_some(), "Should detect smell for 6 parameters");
let smell = smell.unwrap();
assert_eq!(smell.smell_type, SmellType::LongParameterList);
assert_eq!(smell.severity, Priority::Medium);
assert!(smell.message.contains("6 parameters"));
let smell = detect_long_parameter_list(&func, 12);
assert!(smell.is_some(), "Should detect smell for 12 parameters");
let smell = smell.unwrap();
assert_eq!(smell.severity, Priority::High);
}
#[test]
fn test_detect_large_module() {
let path = PathBuf::from("src/large_module.rs");
let smell = detect_large_module(&path, 250);
assert!(smell.is_none(), "Should not detect smell for 250 lines");
let smell = detect_large_module(&path, 300);
assert!(smell.is_none(), "Should not detect smell at threshold");
let smell = detect_large_module(&path, 350);
assert!(smell.is_some(), "Should detect smell for 350 lines");
let smell = smell.unwrap();
assert_eq!(smell.smell_type, SmellType::LargeClass);
assert_eq!(smell.severity, Priority::Medium);
assert!(smell.message.contains("350 lines"));
let smell = detect_large_module(&path, 700);
assert!(smell.is_some(), "Should detect smell for 700 lines");
let smell = smell.unwrap();
assert_eq!(smell.severity, Priority::High);
}
#[test]
fn test_detect_long_method() {
let func = FunctionMetrics {
name: "long_func".to_string(),
file: PathBuf::from("src/test.rs"),
line: 10,
cyclomatic: 5,
cognitive: 10,
nesting: 2,
length: 40,
is_test: false,
visibility: None,
is_trait_method: false,
in_test_module: false,
entropy_score: None,
is_pure: None,
purity_confidence: None,
purity_reason: None,
call_dependencies: None,
detected_patterns: None,
upstream_callers: None,
downstream_callees: None,
mapping_pattern_result: None,
adjusted_complexity: None,
composition_metrics: None,
language_specific: None,
purity_level: None,
error_swallowing_count: None,
error_swallowing_patterns: None,
entropy_analysis: None,
};
let smell = detect_long_method(&func);
assert!(smell.is_none(), "Should not detect smell for 40 lines");
let mut long_func = func.clone();
long_func.length = 60;
let smell = detect_long_method(&long_func);
assert!(smell.is_some(), "Should detect smell for 60 lines");
let smell = smell.unwrap();
assert_eq!(smell.smell_type, SmellType::LongMethod);
assert_eq!(smell.severity, Priority::Medium);
assert!(smell.message.contains("60 lines"));
long_func.length = 120;
let smell = detect_long_method(&long_func);
assert!(smell.is_some(), "Should detect smell for 120 lines");
let smell = smell.unwrap();
assert_eq!(smell.severity, Priority::High);
}
#[test]
fn test_detect_deep_nesting() {
let func = FunctionMetrics {
name: "nested_func".to_string(),
file: PathBuf::from("src/test.rs"),
line: 10,
cyclomatic: 5,
cognitive: 10,
nesting: 3,
length: 30,
is_test: false,
visibility: None,
is_trait_method: false,
in_test_module: false,
entropy_score: None,
is_pure: None,
purity_confidence: None,
purity_reason: None,
call_dependencies: None,
detected_patterns: None,
upstream_callers: None,
downstream_callees: None,
mapping_pattern_result: None,
adjusted_complexity: None,
composition_metrics: None,
language_specific: None,
purity_level: None,
error_swallowing_count: None,
error_swallowing_patterns: None,
entropy_analysis: None,
};
let smell = detect_deep_nesting(&func);
assert!(
smell.is_none(),
"Should not detect smell for nesting depth 3"
);
let mut nested_func = func.clone();
nested_func.nesting = 4;
let smell = detect_deep_nesting(&nested_func);
assert!(smell.is_none(), "Should not detect smell at threshold");
nested_func.nesting = 5;
let smell = detect_deep_nesting(&nested_func);
assert!(smell.is_some(), "Should detect smell for nesting depth 5");
let smell = smell.unwrap();
assert_eq!(smell.smell_type, SmellType::DeepNesting);
assert_eq!(smell.severity, Priority::Medium);
assert!(smell.message.contains("nesting depth of 5"));
nested_func.nesting = 10;
let smell = detect_deep_nesting(&nested_func);
assert!(smell.is_some(), "Should detect smell for nesting depth 10");
let smell = smell.unwrap();
assert_eq!(smell.severity, Priority::High);
}
#[test]
fn test_analyze_function_smells() {
let func = FunctionMetrics {
name: "complex_func".to_string(),
file: PathBuf::from("src/test.rs"),
line: 10,
cyclomatic: 5,
cognitive: 10,
nesting: 5,
length: 60,
is_test: false,
visibility: None,
is_trait_method: false,
in_test_module: false,
entropy_score: None,
is_pure: None,
purity_confidence: None,
purity_reason: None,
call_dependencies: None,
detected_patterns: None,
upstream_callers: None,
downstream_callees: None,
mapping_pattern_result: None,
adjusted_complexity: None,
composition_metrics: None,
language_specific: None,
purity_level: None,
error_swallowing_count: None,
error_swallowing_patterns: None,
entropy_analysis: None,
};
let smells = analyze_function_smells(&func, 7);
assert_eq!(smells.len(), 3, "Should detect 3 smells");
let smell_types: Vec<SmellType> = smells.iter().map(|s| s.smell_type.clone()).collect();
assert!(smell_types.contains(&SmellType::LongParameterList));
assert!(smell_types.contains(&SmellType::LongMethod));
assert!(smell_types.contains(&SmellType::DeepNesting));
let clean_func = FunctionMetrics {
name: "clean_func".to_string(),
file: PathBuf::from("src/test.rs"),
line: 10,
cyclomatic: 3,
cognitive: 5,
nesting: 2,
length: 20,
is_test: false,
visibility: None,
is_trait_method: false,
in_test_module: false,
entropy_score: None,
is_pure: None,
purity_confidence: None,
purity_reason: None,
call_dependencies: None,
detected_patterns: None,
upstream_callers: None,
downstream_callees: None,
mapping_pattern_result: None,
adjusted_complexity: None,
composition_metrics: None,
language_specific: None,
purity_level: None,
error_swallowing_count: None,
error_swallowing_patterns: None,
entropy_analysis: None,
};
let smells = analyze_function_smells(&clean_func, 3);
assert_eq!(smells.len(), 0, "Clean function should have no smells");
}
#[test]
fn test_analyze_module_smells() {
let path = PathBuf::from("src/module.rs");
let smells = analyze_module_smells(&path, 200);
assert_eq!(smells.len(), 0, "Small module should have no smells");
let smells = analyze_module_smells(&path, 400);
assert_eq!(smells.len(), 1, "Large module should have 1 smell");
assert_eq!(smells[0].smell_type, SmellType::LargeClass);
let smells = analyze_module_smells(&path, 300);
assert_eq!(smells.len(), 0, "Module at threshold should have no smells");
}
#[test]
fn test_code_smell_to_debt_item() {
let smell = CodeSmell {
smell_type: SmellType::LongMethod,
location: PathBuf::from("src/test.rs"),
line: 42,
message: "Test message".to_string(),
severity: Priority::High,
};
let debt_item = smell.to_debt_item();
assert!(matches!(debt_item.debt_type, DebtType::CodeSmell { .. }));
assert_eq!(debt_item.file, PathBuf::from("src/test.rs"));
assert_eq!(debt_item.line, 42);
assert_eq!(debt_item.message, "Test message");
assert_eq!(debt_item.priority, Priority::High);
}
}