use crate::domain::metrics::{CodeMetrics, ItemType, SmellDetection};
fn build_detection(
id: &str,
name: &str,
confidence: f64,
location: &str,
fn_name: &str,
metrics: &CodeMetrics,
reasons: Vec<String>,
) -> SmellDetection {
SmellDetection {
smell_id: id.into(),
smell_name: name.into(),
confidence,
location: location.into(),
function_name: fn_name.into(),
metrics: metrics.clone(),
reasons,
}
}
struct TieredAccum {
confidence: f64,
reasons: Vec<String>,
}
impl TieredAccum {
fn new() -> Self {
Self {
confidence: 0.0,
reasons: Vec::new(),
}
}
#[allow(clippy::too_many_arguments)]
fn tier(
&mut self,
value: usize,
high: usize,
high_w: f64,
high_msg: String,
low: usize,
low_w: f64,
low_msg: String,
) {
if value > high {
self.reasons.push(high_msg);
self.confidence += high_w;
} else if value > low {
self.reasons.push(low_msg);
self.confidence += low_w;
}
}
fn add(&mut self, weight: f64, reason: String) {
self.reasons.push(reason);
self.confidence += weight;
}
fn into_detection(
self,
id: &str,
name: &str,
location: &str,
fn_name: &str,
metrics: &CodeMetrics,
threshold: f64,
) -> Option<SmellDetection> {
if self.confidence >= threshold {
Some(build_detection(
id,
name,
self.confidence.min(1.0),
location,
fn_name,
metrics,
self.reasons,
))
} else {
None
}
}
}
pub fn detect_long_method(
metrics: &CodeMetrics,
location: &str,
name: &str,
) -> Option<SmellDetection> {
let mut a = TieredAccum::new();
a.tier(
metrics.loc,
50,
0.3,
format!("LOC={} exceeds 50", metrics.loc),
30,
0.15,
format!("LOC={} exceeds 30", metrics.loc),
);
a.tier(
metrics.cyclomatic_complexity,
15,
0.4,
format!("CC={} exceeds 15", metrics.cyclomatic_complexity),
10,
0.25,
format!("CC={} exceeds 10", metrics.cyclomatic_complexity),
);
a.tier(
metrics.nesting_depth,
4,
0.2,
format!("Nesting depth={} exceeds 4", metrics.nesting_depth),
3,
0.1,
format!("Nesting depth={} exceeds 3", metrics.nesting_depth),
);
a.into_detection("SMELL-01", "Long Method", location, name, metrics, 0.5)
}
pub fn detect_long_parameter_list(
metrics: &CodeMetrics,
location: &str,
name: &str,
) -> Option<SmellDetection> {
if metrics.parameter_count <= 5 {
return None;
}
let (confidence, reason) = if metrics.parameter_count > 8 {
(
0.95,
format!("Parameter count={} exceeds 8", metrics.parameter_count),
)
} else if metrics.parameter_count > 6 {
(
0.85,
format!("Parameter count={} exceeds 6", metrics.parameter_count),
)
} else {
(
0.70,
format!("Parameter count={} exceeds 5", metrics.parameter_count),
)
};
Some(build_detection(
"SMELL-02",
"Long Parameter List",
confidence,
location,
name,
metrics,
vec![reason],
))
}
pub fn detect_primitive_obsession(
metrics: &CodeMetrics,
location: &str,
name: &str,
) -> Option<SmellDetection> {
if metrics.primitive_params < 5 {
return None;
}
let ratio = metrics.primitive_params as f64 / metrics.parameter_count.max(1) as f64;
if metrics.primitive_params >= 5 && ratio >= 0.80 {
let reasons = vec![
format!("{} primitive parameters (>=5)", metrics.primitive_params),
format!("{:.0}% of parameters are primitives", ratio * 100.0),
];
Some(build_detection(
"SMELL-03",
"Primitive Obsession",
0.85,
location,
name,
metrics,
reasons,
))
} else {
None
}
}
pub fn detect_large_class(
metrics: &CodeMetrics,
location: &str,
name: &str,
) -> Option<SmellDetection> {
let mut a = TieredAccum::new();
a.tier(
metrics.method_count,
20,
0.4,
format!("Method count={} exceeds 20", metrics.method_count),
15,
0.2,
format!("Method count={} exceeds 15", metrics.method_count),
);
a.tier(
metrics.field_count,
15,
0.3,
format!("Field count={} exceeds 15", metrics.field_count),
10,
0.15,
format!("Field count={} exceeds 10", metrics.field_count),
);
a.tier(
metrics.loc,
300,
0.3,
format!("LOC={} exceeds 300", metrics.loc),
200,
0.15,
format!("LOC={} exceeds 200", metrics.loc),
);
a.into_detection("SMELL-04", "Large Class", location, name, metrics, 0.5)
}
pub fn detect_data_clumps(
metrics: &CodeMetrics,
location: &str,
name: &str,
) -> Option<SmellDetection> {
if metrics.parameter_count < 6 || metrics.primitive_params < 4 {
return None;
}
let (confidence, reasons) = if metrics.parameter_count >= 7 && metrics.primitive_params >= 5 {
(
0.80,
vec![
format!(
"{} parameters with {} primitives suggest data clumps",
metrics.parameter_count, metrics.primitive_params
),
"Consider extracting related parameters into a parameter object".into(),
],
)
} else {
(
0.65,
vec![
format!(
"High parameter count ({}) with many primitives ({})",
metrics.parameter_count, metrics.primitive_params
),
"Some parameters likely belong together".into(),
],
)
};
Some(build_detection(
"SMELL-05",
"Data Clumps",
confidence,
location,
name,
metrics,
reasons,
))
}
pub fn detect_switch_statements(
metrics: &CodeMetrics,
location: &str,
name: &str,
) -> Option<SmellDetection> {
if metrics.branch_count <= 5 {
return None;
}
let (mut confidence, mut reasons) = if metrics.branch_count > 10 {
(
0.90,
vec![format!(
"Excessive branching with {} branches (>10)",
metrics.branch_count
)],
)
} else if metrics.branch_count > 7 {
(
0.75,
vec![format!(
"High branching with {} branches (>7)",
metrics.branch_count
)],
)
} else {
(
0.60,
vec![format!(
"Many branches ({}) suggest need for polymorphism",
metrics.branch_count
)],
)
};
if metrics.cyclomatic_complexity > 15 {
reasons.push(format!(
"Combined with high CC={}",
metrics.cyclomatic_complexity
));
confidence = (confidence + 0.15_f64).min(1.0_f64);
}
Some(build_detection(
"SMELL-06",
"Switch Statements",
confidence,
location,
name,
metrics,
reasons,
))
}
pub fn detect_data_class(
metrics: &CodeMetrics,
location: &str,
name: &str,
) -> Option<SmellDetection> {
if metrics.field_count < 5 {
return None;
}
let ratio = metrics.field_count as f64 / metrics.method_count.max(1) as f64;
if metrics.method_count == 0 || ratio >= 2.0 {
let confidence = if metrics.method_count == 0 {
0.60
} else {
0.75
};
Some(build_detection(
"SMELL-07",
"Data Class",
confidence,
location,
name,
metrics,
vec![
if metrics.method_count == 0 {
format!("{} fields with no behavior methods", metrics.field_count)
} else {
format!("High field-to-method ratio ({ratio:.1})")
},
format!("Field count={}, few behavior methods", metrics.field_count),
],
))
} else {
None
}
}
pub fn detect_shotgun_surgery(
metrics: &CodeMetrics,
location: &str,
name: &str,
dependency_count: usize,
) -> Option<SmellDetection> {
if dependency_count == 0 {
return None;
}
if dependency_count >= 10 {
Some(build_detection(
"SMELL-09",
"Shotgun Surgery",
0.80,
location,
name,
metrics,
vec![
format!("Used by {dependency_count} different files"),
"Changes here will require widespread modifications".into(),
],
))
} else if dependency_count >= 7 {
Some(build_detection(
"SMELL-09",
"Shotgun Surgery",
0.65,
location,
name,
metrics,
vec![
format!("Used by {dependency_count} files"),
"Moderate coupling suggests refactoring risk".into(),
],
))
} else {
None
}
}
pub fn detect_divergent_change(
metrics: &CodeMetrics,
location: &str,
name: &str,
) -> Option<SmellDetection> {
if metrics.cyclomatic_complexity <= 15 || metrics.method_count <= 8 {
return None;
}
let (mut confidence, mut reasons) =
if metrics.cyclomatic_complexity > 25 && metrics.method_count > 15 {
(
0.80,
vec![
format!(
"High CC={} with {} methods",
metrics.cyclomatic_complexity, metrics.method_count
),
"Multiple responsibilities suggest multiple change reasons".into(),
],
)
} else if metrics.cyclomatic_complexity > 20 && metrics.method_count > 12 {
(
0.65,
vec![
format!(
"CC={} with {} methods",
metrics.cyclomatic_complexity, metrics.method_count
),
"Likely has multiple change reasons".into(),
],
)
} else if metrics.cyclomatic_complexity > 15 && metrics.method_count > 8 {
(
0.55,
vec![format!(
"Moderate CC={} and method count",
metrics.cyclomatic_complexity
)],
)
} else {
return None;
};
if confidence >= 0.55 && metrics.field_count > 10 {
reasons.push(format!(
"Many fields ({}) reinforce multiple concerns",
metrics.field_count
));
confidence = (confidence + 0.1_f64).min(1.0_f64);
}
if confidence >= 0.55 {
Some(build_detection(
"SMELL-10",
"Divergent Change",
confidence,
location,
name,
metrics,
reasons,
))
} else {
None
}
}
pub fn detect_lazy_class(
metrics: &CodeMetrics,
location: &str,
name: &str,
) -> Option<SmellDetection> {
if matches!(metrics.item_type, ItemType::Class)
&& metrics.method_count == 0
&& metrics.field_count >= 2
{
return None;
}
if metrics.loc < 8 && metrics.method_count == 0 && metrics.field_count == 0 {
return Some(build_detection(
"SMELL-11",
"Lazy Class",
0.80,
location,
name,
metrics,
vec![
format!("LOC={} is very small", metrics.loc),
"No methods or fields, minimal functionality".into(),
],
));
}
if metrics.loc < 10
&& metrics.method_count <= 1
&& metrics.field_count < 3
&& !(metrics.field_count >= 1 && metrics.method_count == 1)
{
return Some(build_detection(
"SMELL-11",
"Lazy Class",
0.75,
location,
name,
metrics,
vec![
format!("LOC={} is very small", metrics.loc),
format!(
"Method count={}, minimal functionality",
metrics.method_count
),
],
));
}
None
}
pub fn detect_speculative_generality(
metrics: &CodeMetrics,
location: &str,
name: &str,
subclass_count: usize,
usage_count: usize,
) -> Option<SmellDetection> {
let mut reasons: Vec<String> = Vec::new();
let mut confidence: f64 = 0.0;
if subclass_count == 1 {
reasons.push("Abstract class/interface with only one implementation".into());
reasons.push("Abstraction may be premature/unnecessary".into());
confidence = 0.75;
}
if usage_count == 0 && metrics.method_count > 0 {
reasons.push("Class is defined but never used".into());
confidence = confidence.max(0.85);
} else if usage_count == 1 && metrics.method_count > 3 {
reasons.push("Complex class with only one usage point".into());
confidence = confidence.max(0.60);
}
if subclass_count == 1 && usage_count <= 1 {
confidence = 0.90;
}
if confidence >= 0.6 {
if reasons.is_empty() {
reasons.push("Unused or over-engineered abstraction".into());
}
Some(build_detection(
"SMELL-12",
"Speculative Generality",
confidence,
location,
name,
metrics,
reasons,
))
} else {
None
}
}
pub fn detect_duplicate_code(
metrics: &CodeMetrics,
location: &str,
name: &str,
all_hashes: Option<&std::collections::HashMap<String, Vec<String>>>,
) -> Option<SmellDetection> {
let hashes = all_hashes?;
if metrics.ast_hash.is_empty() {
return None;
}
let dup_locs = hashes.get(&metrics.ast_hash)?;
if dup_locs.len() > 1 {
let others: Vec<&str> = dup_locs
.iter()
.filter(|l| l.as_str() != location)
.take(3)
.map(|s| s.as_str())
.collect();
if !others.is_empty() {
let confidence = (0.7 + (dup_locs.len() - 1) as f64 * 0.1).min(0.95);
return Some(build_detection(
"SMELL-13",
"Duplicate Code",
confidence,
location,
name,
metrics,
vec![
format!("Code duplicated in {} locations", dup_locs.len()),
format!("Also found at: {}", others.join(", ")),
],
));
}
}
None
}
pub fn detect_middle_man(
metrics: &CodeMetrics,
location: &str,
name: &str,
) -> Option<SmellDetection> {
if metrics.method_count > 0 && metrics.delegation_methods > 0 {
let ratio = metrics.delegation_methods as f64 / metrics.method_count as f64;
if ratio > 0.7 && metrics.method_count >= 3 {
let confidence = if ratio > 0.85 { 0.85 } else { 0.70 };
return Some(build_detection(
"SMELL-14",
"Middle Man",
confidence,
location,
name,
metrics,
vec![
format!(
"{}/{} methods are simple delegations",
metrics.delegation_methods, metrics.method_count
),
format!("Delegation ratio: {:.0}%", ratio * 100.0),
"Class adds little value, consider removing".into(),
],
));
}
}
if metrics.method_count >= 3
&& metrics.external_calls >= metrics.method_count
&& metrics.loc < 50
{
return Some(build_detection(
"SMELL-14",
"Middle Man",
0.70,
location,
name,
metrics,
vec![
format!(
"{} external calls with {} methods suggest delegation",
metrics.external_calls, metrics.method_count
),
"Low LOC with high forwarding activity".into(),
],
));
}
None
}
pub fn detect_feature_envy(
metrics: &CodeMetrics,
location: &str,
name: &str,
) -> Option<SmellDetection> {
let mut a = TieredAccum::new();
if metrics.external_calls > 5 {
a.add(
0.7,
format!("External calls={} exceeds 5", metrics.external_calls),
);
}
if metrics.return_statements > 5 {
a.add(
0.3,
format!("Return statements={} exceeds 5", metrics.return_statements),
);
}
if metrics.cyclomatic_complexity > 8 && metrics.loc < 40 {
a.add(
0.2,
"High CC with moderate LOC suggests complex branching".into(),
);
}
a.into_detection("SMELL-18", "Feature Envy", location, name, metrics, 0.5)
}
pub fn detect_message_chains(
metrics: &CodeMetrics,
location: &str,
name: &str,
) -> Option<SmellDetection> {
if metrics.method_call_chains <= 3 {
return None;
}
let (confidence, reasons) = if metrics.method_call_chains > 4 {
(
0.90,
vec![
format!(
"Very long call chains (depth={})",
metrics.method_call_chains
),
"Violates Law of Demeter, creates tight coupling".into(),
],
)
} else {
(
0.70,
vec![format!(
"Call chain depth={} suggests coupling",
metrics.method_call_chains
)],
)
};
Some(build_detection(
"SMELL-20",
"Message Chains",
confidence,
location,
name,
metrics,
reasons,
))
}
pub fn detect_god_object(
metrics: &CodeMetrics,
location: &str,
name: &str,
) -> Option<SmellDetection> {
let mut a = TieredAccum::new();
a.tier(
metrics.method_count,
30,
0.35,
format!("Excessive method count={} (>30)", metrics.method_count),
25,
0.2,
format!("Very high method count={} (>25)", metrics.method_count),
);
a.tier(
metrics.field_count,
20,
0.35,
format!("Excessive field count={} (>20)", metrics.field_count),
15,
0.2,
format!("Very high field count={} (>15)", metrics.field_count),
);
a.tier(
metrics.loc,
500,
0.3,
format!("Excessive LOC={} (>500)", metrics.loc),
400,
0.15,
format!("Very high LOC={} (>400)", metrics.loc),
);
if metrics.cyclomatic_complexity > 50 {
a.add(
0.2,
format!("Extreme complexity CC={}", metrics.cyclomatic_complexity),
);
}
a.into_detection("SMELL-21", "God Object", location, name, metrics, 0.6)
}
pub fn detect_parallel_inheritance(
_metrics: &CodeMetrics,
_location: &str,
_name: &str,
) -> Option<SmellDetection> {
None
}
pub fn detect_comments(
metrics: &CodeMetrics,
location: &str,
name: &str,
) -> Option<SmellDetection> {
let inline_count = metrics
.comment_count
.saturating_sub(metrics.doc_comment_count);
if inline_count == 0 || metrics.loc == 0 {
return None;
}
let comment_ratio = inline_count as f64 / metrics.loc as f64;
let mut a = TieredAccum::new();
a.tier(
inline_count,
(metrics.loc as f64 * 0.75) as usize, 0.70,
format!(
"Comment density {:.0}% is very high ({} inline comment lines / {} LOC)",
comment_ratio * 100.0,
inline_count,
metrics.loc
),
(metrics.loc as f64 * 0.35) as usize,
0.40,
format!(
"Comment density {:.0}% suggests code is not self-documenting",
comment_ratio * 100.0
),
);
if metrics.loc > 50 && comment_ratio >= 0.35 {
a.add(
0.25,
"Long method with many comments -- consider extracting named methods".into(),
);
}
if metrics.cyclomatic_complexity > 10 && comment_ratio >= 0.35 {
a.add(
0.20,
format!(
"High CC={} with many comments suggests complex control flow",
metrics.cyclomatic_complexity
),
);
}
a.into_detection("SMELL-16", "Comments", location, name, metrics, 0.65)
}
pub fn detect_dead_code(
_metrics: &CodeMetrics,
_location: &str,
_name: &str,
) -> Option<SmellDetection> {
None
}
pub fn detect_inappropriate_intimacy(
_metrics: &CodeMetrics,
_location: &str,
_name: &str,
) -> Option<SmellDetection> {
None
}
pub fn detect_refused_bequest(
metrics: &CodeMetrics,
location: &str,
name: &str,
) -> Option<SmellDetection> {
if metrics.override_count >= 3 && metrics.method_count <= 5 && metrics.method_count > 0 {
let ratio = metrics.override_count as f64 / metrics.method_count as f64;
if ratio >= 0.5 {
return Some(build_detection(
"SMELL-22",
"Refused Bequest",
0.75,
location,
name,
metrics,
vec![
format!(
"{} out of {} methods are trivial overrides",
metrics.override_count, metrics.method_count
),
"Subclass rejects parent behavior -- consider composition over inheritance"
.into(),
],
));
}
}
if metrics.override_count >= 2 && metrics.method_count <= 4 && metrics.method_count > 0 {
return Some(build_detection(
"SMELL-22",
"Refused Bequest",
0.70,
location,
name,
metrics,
vec![
format!(
"{} trivial overrides suggest rejected parent contract",
metrics.override_count
),
"Consider whether inheritance is appropriate".into(),
],
));
}
if metrics.field_count >= 8
&& metrics.method_count <= 2
&& metrics.method_count > 0
&& metrics.override_count > 0
{
return Some(build_detection(
"SMELL-22",
"Refused Bequest",
0.55,
location,
name,
metrics,
vec![
format!(
"{} fields, {} methods, {} trivial overrides -- inherits without adding value",
metrics.field_count, metrics.method_count, metrics.override_count
),
"Subclass overrides parent behavior but adds little -- consider composition".into(),
],
));
}
if metrics.override_count == 0
&& metrics.method_count <= 3
&& metrics.method_count > 0
&& metrics.field_count <= 2
&& metrics.loc < 30
{
return Some(build_detection(
"SMELL-22",
"Refused Bequest",
0.55,
location,
name,
metrics,
vec![
format!(
"Small class (LOC={}, {} methods, {} fields) likely inherits without adding value",
metrics.loc, metrics.method_count, metrics.field_count
),
"Consider whether inheritance is appropriate or composition would be better".into(),
],
));
}
None
}
pub fn detect_alternative_classes(
_metrics: &CodeMetrics,
_location: &str,
_name: &str,
) -> Option<SmellDetection> {
None
}
pub fn detect_function_smells(
metrics: &CodeMetrics,
location: &str,
name: &str,
) -> Vec<SmellDetection> {
[
detect_long_method(metrics, location, name),
detect_long_parameter_list(metrics, location, name),
detect_primitive_obsession(metrics, location, name),
detect_switch_statements(metrics, location, name),
detect_feature_envy(metrics, location, name),
detect_message_chains(metrics, location, name),
]
.into_iter()
.flatten()
.collect()
}
pub fn detect_class_smells(
metrics: &CodeMetrics,
location: &str,
name: &str,
) -> Vec<SmellDetection> {
[
detect_large_class(metrics, location, name),
detect_data_class(metrics, location, name),
detect_lazy_class(metrics, location, name),
detect_divergent_change(metrics, location, name),
detect_middle_man(metrics, location, name),
detect_god_object(metrics, location, name),
detect_refused_bequest(metrics, location, name),
]
.into_iter()
.flatten()
.collect()
}
pub fn detect_all(metrics: &CodeMetrics, location: &str, name: &str) -> Vec<SmellDetection> {
let mut r = match metrics.item_type {
ItemType::Function => detect_function_smells(metrics, location, name),
ItemType::Class => detect_class_smells(metrics, location, name),
};
r.extend(
[
detect_data_clumps(metrics, location, name),
detect_comments(metrics, location, name),
]
.into_iter()
.flatten(),
);
r
}
#[cfg(test)]
mod tests {
use super::*;
fn make_fn_metrics(loc: usize, cc: usize, nesting: usize, params: usize) -> CodeMetrics {
CodeMetrics {
loc,
cyclomatic_complexity: cc,
nesting_depth: nesting,
parameter_count: params,
..Default::default()
}
}
fn make_class_metrics(loc: usize, methods: usize, fields: usize) -> CodeMetrics {
CodeMetrics {
loc,
cyclomatic_complexity: 1,
method_count: methods,
field_count: fields,
..Default::default()
}
}
#[test]
fn long_method_high_loc_and_cc() {
let d = detect_long_method(&make_fn_metrics(80, 20, 5, 2), "test.py:1", "big_fn").unwrap();
assert_eq!(d.smell_id, "SMELL-01");
assert!((d.confidence - 0.9).abs() < f64::EPSILON);
}
#[test]
fn long_method_below_threshold() {
assert!(
detect_long_method(&make_fn_metrics(10, 2, 1, 0), "test.py:1", "small_fn").is_none()
);
}
#[test]
fn long_method_moderate() {
assert!(
detect_long_method(&make_fn_metrics(40, 12, 2, 1), "test.py:1", "mid_fn").is_none()
);
}
#[test]
fn long_params_6() {
let d = detect_long_parameter_list(&make_fn_metrics(10, 1, 0, 6), "t.py:1", "f").unwrap();
assert_eq!(d.smell_id, "SMELL-02");
assert!((d.confidence - 0.70).abs() < f64::EPSILON);
}
#[test]
fn long_params_7() {
let d = detect_long_parameter_list(&make_fn_metrics(10, 1, 0, 7), "t.py:1", "f").unwrap();
assert!((d.confidence - 0.85).abs() < f64::EPSILON);
}
#[test]
fn long_params_9() {
let d = detect_long_parameter_list(&make_fn_metrics(10, 1, 0, 9), "t.py:1", "f").unwrap();
assert!((d.confidence - 0.95).abs() < f64::EPSILON);
}
#[test]
fn long_params_ok() {
assert!(detect_long_parameter_list(&make_fn_metrics(10, 1, 0, 3), "t.py:1", "f").is_none());
}
#[test]
fn long_params_5_not_flagged() {
assert!(detect_long_parameter_list(&make_fn_metrics(10, 1, 0, 5), "t.py:1", "f").is_none());
}
#[test]
fn primitive_obsession_high() {
let m = CodeMetrics {
primitive_params: 5,
parameter_count: 6,
..Default::default()
};
let d = detect_primitive_obsession(&m, "t.py:1", "f").unwrap();
assert_eq!(d.smell_id, "SMELL-03");
assert!((d.confidence - 0.85).abs() < f64::EPSILON);
}
#[test]
fn primitive_obsession_low_ratio() {
let m = CodeMetrics {
primitive_params: 3,
parameter_count: 10,
..Default::default()
};
assert!(detect_primitive_obsession(&m, "t.py:1", "f").is_none());
}
#[test]
fn primitive_obsession_below_4_not_flagged() {
let m = CodeMetrics {
primitive_params: 3,
parameter_count: 3,
..Default::default()
};
assert!(detect_primitive_obsession(&m, "t.py:1", "f").is_none());
}
#[test]
fn large_class_high() {
let d = detect_large_class(&make_class_metrics(350, 25, 18), "t.py:1", "BigCls").unwrap();
assert_eq!(d.smell_id, "SMELL-04");
assert!(d.confidence >= 0.5);
}
#[test]
fn large_class_none() {
assert!(detect_large_class(&make_class_metrics(50, 5, 3), "t.py:1", "SmallCls").is_none());
}
#[test]
fn data_class_detected() {
let m = CodeMetrics {
method_count: 3,
field_count: 10,
..Default::default()
};
let d = detect_data_class(&m, "t.py:1", "Dto").unwrap();
assert_eq!(d.smell_id, "SMELL-07");
assert!((d.confidence - 0.75).abs() < f64::EPSILON);
}
#[test]
fn data_class_not_enough_fields() {
let m = CodeMetrics {
method_count: 3,
field_count: 3,
..Default::default()
};
assert!(detect_data_class(&m, "t.py:1", "Dto").is_none());
}
#[test]
fn data_class_zero_methods_lower_confidence() {
let m = CodeMetrics {
method_count: 0,
field_count: 8,
..Default::default()
};
let d = detect_data_class(&m, "t.go:1", "Config").unwrap();
assert_eq!(d.smell_id, "SMELL-07");
assert!((d.confidence - 0.60).abs() < f64::EPSILON);
}
#[test]
fn lazy_class_detected() {
let m = CodeMetrics {
loc: 5,
method_count: 0,
field_count: 0,
..Default::default()
};
let d = detect_lazy_class(&m, "t.py:1", "Useless").unwrap();
assert_eq!(d.smell_id, "SMELL-11");
assert!((d.confidence - 0.80).abs() < f64::EPSILON);
}
#[test]
fn lazy_class_with_one_method_no_fields() {
let m = CodeMetrics {
loc: 9,
method_count: 1,
field_count: 0,
..Default::default()
};
let d = detect_lazy_class(&m, "t.py:1", "Tiny").unwrap();
assert_eq!(d.smell_id, "SMELL-11");
assert!((d.confidence - 0.75).abs() < f64::EPSILON);
}
#[test]
fn lazy_class_one_method_with_fields_not_flagged() {
let m = CodeMetrics {
loc: 9,
method_count: 1,
field_count: 2,
..Default::default()
};
assert!(detect_lazy_class(&m, "t.py:1", "Tiny").is_none());
}
#[test]
fn lazy_class_data_struct_not_flagged() {
let m = CodeMetrics {
loc: 15,
method_count: 0,
field_count: 10,
..Default::default()
};
assert!(detect_lazy_class(&m, "t.rs:1", "BuildStats").is_none());
}
#[test]
fn lazy_class_enough_methods_not_flagged() {
let m = CodeMetrics {
loc: 30,
method_count: 3,
field_count: 2,
..Default::default()
};
assert!(detect_lazy_class(&m, "t.py:1", "Active").is_none());
}
#[test]
fn switch_detected() {
let m = CodeMetrics {
branch_count: 8,
..Default::default()
};
let d = detect_switch_statements(&m, "t.py:1", "f").unwrap();
assert_eq!(d.smell_id, "SMELL-06");
assert!((d.confidence - 0.75).abs() < f64::EPSILON);
}
#[test]
fn switch_with_high_cc() {
let m = CodeMetrics {
branch_count: 6,
cyclomatic_complexity: 20,
..Default::default()
};
let d = detect_switch_statements(&m, "t.py:1", "f").unwrap();
assert!((d.confidence - 0.75).abs() < f64::EPSILON); }
#[test]
fn god_object_detected() {
let m = CodeMetrics {
loc: 600,
method_count: 35,
field_count: 25,
..Default::default()
};
let d = detect_god_object(&m, "t.py:1", "God").unwrap();
assert_eq!(d.smell_id, "SMELL-21");
assert!((d.confidence - 1.0).abs() < f64::EPSILON);
}
#[test]
fn message_chains_not_detected_at_3() {
let m = CodeMetrics {
method_call_chains: 3,
..Default::default()
};
assert!(detect_message_chains(&m, "t.py:1", "f").is_none());
}
#[test]
fn message_chains_detected() {
let m = CodeMetrics {
method_call_chains: 4,
..Default::default()
};
let d = detect_message_chains(&m, "t.py:1", "f").unwrap();
assert_eq!(d.smell_id, "SMELL-20");
assert!((d.confidence - 0.70).abs() < f64::EPSILON);
}
#[test]
fn message_chains_long() {
let m = CodeMetrics {
method_call_chains: 6,
..Default::default()
};
let d = detect_message_chains(&m, "t.py:1", "f").unwrap();
assert!((d.confidence - 0.90).abs() < f64::EPSILON);
}
#[test]
fn feature_envy_detected() {
let m = CodeMetrics {
external_calls: 8,
return_statements: 7,
..Default::default()
};
let d = detect_feature_envy(&m, "t.py:1", "f").unwrap();
assert_eq!(d.smell_id, "SMELL-18");
assert!(d.confidence >= 0.5);
}
#[test]
fn detect_all_combines() {
let m = CodeMetrics {
loc: 80,
cyclomatic_complexity: 20,
nesting_depth: 5,
parameter_count: 8,
branch_count: 12,
..Default::default()
};
let results = detect_all(&m, "t.py:1", "mega");
assert!(!results.is_empty());
let ids: Vec<&str> = results.iter().map(|d| d.smell_id.as_str()).collect();
assert!(ids.contains(&"SMELL-01"), "should detect Long Method");
assert!(
ids.contains(&"SMELL-02"),
"should detect Long Parameter List"
);
assert!(ids.contains(&"SMELL-06"), "should detect Switch Statements");
}
#[test]
fn middle_man_detected() {
let m = CodeMetrics {
method_count: 5,
delegation_methods: 4,
..Default::default()
};
let d = detect_middle_man(&m, "t.py:1", "Proxy").unwrap();
assert_eq!(d.smell_id, "SMELL-14");
assert!((d.confidence - 0.70).abs() < f64::EPSILON);
}
#[test]
fn divergent_change_detected() {
let m = CodeMetrics {
cyclomatic_complexity: 30,
method_count: 20,
..Default::default()
};
let d = detect_divergent_change(&m, "t.py:1", "SwissArmy").unwrap();
assert_eq!(d.smell_id, "SMELL-10");
assert!((d.confidence - 0.80).abs() < f64::EPSILON);
}
#[test]
fn data_clumps_below_threshold() {
assert!(detect_data_clumps(&CodeMetrics::default(), "t.py:1", "f").is_none());
}
#[test]
fn data_clumps_high_params() {
let m = CodeMetrics {
parameter_count: 8,
primitive_params: 6,
..Default::default()
};
let d = detect_data_clumps(&m, "t.py:1", "f").unwrap();
assert_eq!(d.smell_id, "SMELL-05");
assert!((d.confidence - 0.80).abs() < f64::EPSILON);
}
#[test]
fn data_clumps_moderate() {
let m = CodeMetrics {
parameter_count: 6,
primitive_params: 4,
..Default::default()
};
let d = detect_data_clumps(&m, "t.py:1", "f").unwrap();
assert_eq!(d.smell_id, "SMELL-05");
assert!((d.confidence - 0.65).abs() < f64::EPSILON);
}
#[test]
fn data_clumps_below_new_threshold() {
let m = CodeMetrics {
parameter_count: 5,
primitive_params: 3,
..Default::default()
};
assert!(detect_data_clumps(&m, "t.py:1", "f").is_none());
}
#[test]
fn shotgun_surgery_zero_deps() {
assert!(detect_shotgun_surgery(&CodeMetrics::default(), "t.py:1", "f", 0).is_none());
}
#[test]
fn speculative_generality_zero() {
assert!(
detect_speculative_generality(&CodeMetrics::default(), "t.py:1", "f", 0, 0).is_none()
);
}
#[test]
fn duplicate_code_no_hashes() {
let m = CodeMetrics {
ast_hash: "abc".into(),
..Default::default()
};
assert!(detect_duplicate_code(&m, "t.py:1", "f", None).is_none());
}
#[test]
fn comments_no_comments() {
let m = CodeMetrics {
loc: 20,
comment_count: 0,
..Default::default()
};
assert!(detect_comments(&m, "t.py:1", "f").is_none());
}
#[test]
fn comments_below_threshold() {
let m = CodeMetrics {
loc: 100,
comment_count: 5, ..Default::default()
};
assert!(detect_comments(&m, "t.py:1", "f").is_none());
}
#[test]
fn comments_doc_comment_not_flagged() {
let m = CodeMetrics {
loc: 100,
comment_count: 30, ..Default::default()
};
assert!(detect_comments(&m, "t.py:1", "f").is_none());
}
#[test]
fn comments_35_percent_boundary() {
let m = CodeMetrics {
loc: 100,
comment_count: 35, ..Default::default()
};
assert!(detect_comments(&m, "t.py:1", "f").is_none());
}
#[test]
fn comments_high_density() {
let m = CodeMetrics {
loc: 100,
comment_count: 60, ..Default::default()
};
let d = detect_comments(&m, "t.py:1", "f").unwrap();
assert_eq!(d.smell_id, "SMELL-16");
assert!(d.confidence >= 0.4);
}
#[test]
fn comments_with_long_method() {
let m = CodeMetrics {
loc: 80,
comment_count: 45, ..Default::default()
};
let d = detect_comments(&m, "t.py:1", "f").unwrap();
assert_eq!(d.smell_id, "SMELL-16");
assert!(d.confidence > 0.4);
}
#[test]
fn comments_with_high_cc() {
let m = CodeMetrics {
loc: 60,
comment_count: 25, cyclomatic_complexity: 15, ..Default::default()
};
let d = detect_comments(&m, "t.py:1", "f").unwrap();
assert_eq!(d.smell_id, "SMELL-16");
assert!(d.confidence >= 0.5);
}
#[test]
fn refused_bequest_many_overrides() {
let m = CodeMetrics {
method_count: 4,
override_count: 3,
..Default::default()
};
let d = detect_refused_bequest(&m, "t.py:1", "BadSub").unwrap();
assert_eq!(d.smell_id, "SMELL-22");
assert!((d.confidence - 0.75).abs() < f64::EPSILON);
}
#[test]
fn refused_bequest_moderate_overrides() {
let m = CodeMetrics {
method_count: 3,
override_count: 2,
..Default::default()
};
let d = detect_refused_bequest(&m, "t.py:1", "Sub").unwrap();
assert_eq!(d.smell_id, "SMELL-22");
assert!((d.confidence - 0.70).abs() < f64::EPSILON);
}
#[test]
fn refused_bequest_lazy_subclass() {
let m = CodeMetrics {
field_count: 10,
method_count: 1,
override_count: 1, ..Default::default()
};
let d = detect_refused_bequest(&m, "t.py:1", "LazySub").unwrap();
assert_eq!(d.smell_id, "SMELL-22");
assert!((d.confidence - 0.55).abs() < f64::EPSILON);
}
#[test]
fn refused_bequest_dto_not_flagged() {
let m = CodeMetrics {
field_count: 12,
method_count: 2,
override_count: 0, ..Default::default()
};
assert!(detect_refused_bequest(&m, "t.py:1", "UserDTO").is_none());
}
#[test]
fn refused_bequest_none() {
let m = CodeMetrics {
method_count: 10,
override_count: 1,
field_count: 3,
..Default::default()
};
assert!(detect_refused_bequest(&m, "t.py:1", "GoodSub").is_none());
}
#[test]
fn refused_bequest_zero_methods() {
let m = CodeMetrics {
method_count: 0,
override_count: 5,
..Default::default()
};
assert!(detect_refused_bequest(&m, "t.py:1", "Empty").is_none());
}
#[test]
fn parallel_inheritance_placeholder() {
assert!(detect_parallel_inheritance(&CodeMetrics::default(), "t.py:1", "f").is_none());
}
#[test]
fn dead_code_placeholder() {
assert!(detect_dead_code(&CodeMetrics::default(), "t.py:1", "f").is_none());
}
#[test]
fn inappropriate_intimacy_placeholder() {
assert!(detect_inappropriate_intimacy(&CodeMetrics::default(), "t.py:1", "f").is_none());
}
#[test]
fn alternative_classes_placeholder() {
assert!(detect_alternative_classes(&CodeMetrics::default(), "t.py:1", "f").is_none());
}
}