use crate::detection::{FunctionDetectionMarkers, FunctionSyntax};
use crate::loc::counter::LineKind;
use super::*;
struct TestMarkers;
impl FunctionDetectionMarkers for TestMarkers {
fn function_markers(&self) -> &[&str] {
&["fn "]
}
fn brace_scoped(&self) -> bool {
true
}
fn function_syntax(&self) -> FunctionSyntax {
FunctionSyntax::KeywordWithLifetimes
}
fn line_comments(&self) -> &[&str] {
&["//"]
}
}
fn lines(s: &str) -> Vec<String> {
s.lines().map(String::from).collect()
}
fn all_code(n: usize) -> Vec<LineKind> {
vec![LineKind::Code; n]
}
#[test]
fn long_function_detected() {
let mut src = String::from("fn foo() {\n");
for i in 0..53 {
src.push_str(&format!(" let x{i} = {i};\n"));
}
src.push_str("}\n");
let ls = lines(&src);
let kinds = all_code(ls.len());
let smells = detect_long_functions(&ls, &kinds, &TestMarkers, 50);
assert_eq!(smells.len(), 1);
assert_eq!(smells[0].kind, SmellKind::LongFunction);
assert!(smells[0].detail.contains("foo"));
assert!(smells[0].detail.contains("53 lines"));
}
#[test]
fn long_function_body_excludes_signature_and_closing() {
let mut src = String::from("fn bar() {\n");
for i in 0..50 {
src.push_str(&format!(" let x{i} = {i};\n"));
}
src.push_str("}\n");
let ls = lines(&src);
let kinds = all_code(ls.len());
let smells = detect_long_functions(&ls, &kinds, &TestMarkers, 50);
assert!(
smells.is_empty(),
"50 body lines should not trigger at max_lines=50"
);
}
#[test]
fn short_function_not_detected() {
let src = "fn bar() {\n let x = 1;\n}\n";
let ls = lines(src);
let kinds = all_code(ls.len());
let smells = detect_long_functions(&ls, &kinds, &TestMarkers, 50);
assert!(smells.is_empty());
}
#[test]
fn long_params_detected() {
let src = "fn baz(a: i32, b: i32, c: i32, d: i32, e: i32) {\n}\n";
let ls = lines(src);
let kinds = all_code(ls.len());
let smells = detect_long_params(&ls, &kinds, &TestMarkers, 4);
assert_eq!(smells.len(), 1);
assert_eq!(smells[0].kind, SmellKind::LongParameterList);
assert!(smells[0].detail.contains("5 params"));
}
#[test]
fn long_params_multiline_detected() {
let src =
"fn process(\n a: i32,\n b: i32,\n c: i32,\n d: i32,\n e: i32,\n) {\n}\n";
let ls = lines(src);
let kinds = all_code(ls.len());
let smells = detect_long_params(&ls, &kinds, &TestMarkers, 4);
assert_eq!(smells.len(), 1, "multi-line signature should be detected");
assert!(smells[0].detail.contains("5 params"));
}
#[test]
fn few_params_not_detected() {
let src = "fn qux(a: i32, b: i32) {\n}\n";
let ls = lines(src);
let kinds = all_code(ls.len());
let smells = detect_long_params(&ls, &kinds, &TestMarkers, 4);
assert!(smells.is_empty());
}
#[test]
fn no_params_not_detected() {
let src = "fn empty() {\n}\n";
let ls = lines(src);
let kinds = all_code(ls.len());
let smells = detect_long_params(&ls, &kinds, &TestMarkers, 4);
assert!(smells.is_empty());
}
#[test]
fn todo_detected() {
let ls = lines("// TODO: fix this\nlet x = 1;");
let kinds = vec![LineKind::Comment, LineKind::Code];
let smells = detect_todo_debt(&ls, &kinds);
assert_eq!(smells.len(), 1);
assert_eq!(smells[0].kind, SmellKind::TodoDebt);
}
#[test]
fn fixme_detected() {
let ls = lines("// FIXME: broken");
let kinds = vec![LineKind::Comment];
let smells = detect_todo_debt(&ls, &kinds);
assert_eq!(smells.len(), 1);
assert!(smells[0].detail.contains("FIXME"));
}
#[test]
fn no_debt_in_code() {
let ls = lines("let todo = 5;");
let kinds = vec![LineKind::Code];
let smells = detect_todo_debt(&ls, &kinds);
assert!(smells.is_empty());
}
#[test]
fn debt_keyword_not_matched_as_substring_in_prose() {
let ls = lines("# harmless prose: this is about debugging a bug report from a customer");
let kinds = vec![LineKind::Comment];
let smells = detect_todo_debt(&ls, &kinds);
assert!(
smells.is_empty(),
"prose containing 'bug' as a common English word should not trigger todo_debt: got {:?}",
smells
);
}
#[test]
fn debt_keyword_matched_as_whole_word_with_colon() {
let ls = lines("// BUG: race condition when N > 100");
let kinds = vec![LineKind::Comment];
let smells = detect_todo_debt(&ls, &kinds);
assert_eq!(smells.len(), 1);
assert!(smells[0].detail.contains("BUG"));
}
#[test]
fn debt_keyword_matched_as_whole_word_with_parens() {
let ls = lines("# FIXME(alice) revisit after cache lands");
let kinds = vec![LineKind::Comment];
let smells = detect_todo_debt(&ls, &kinds);
assert_eq!(smells.len(), 1);
assert!(smells[0].detail.contains("FIXME"));
}
#[test]
fn debt_keyword_not_matched_inside_longer_word() {
let ls = lines("# hackathon last week\n# my todolist for the sprint\n# fixed a buggy behavior");
let kinds = vec![LineKind::Comment, LineKind::Comment, LineKind::Comment];
let smells = detect_todo_debt(&ls, &kinds);
assert!(
smells.is_empty(),
"words containing debt keywords as substrings should not trigger: got {:?}",
smells
);
}
#[test]
fn lowercase_bug_in_prose_not_matched() {
let ls = lines("# the bug fix landed yesterday");
let kinds = vec![LineKind::Comment];
let smells = detect_todo_debt(&ls, &kinds);
assert!(smells.is_empty());
}
#[test]
fn magic_number_detected() {
let ls = lines(" timeout(3600);");
let kinds = vec![LineKind::Code];
let smells = detect_magic_numbers(&ls, &kinds, &["//"][..]);
assert_eq!(smells.len(), 1);
assert_eq!(smells[0].kind, SmellKind::MagicNumber);
}
#[test]
fn trivial_numbers_not_detected() {
let ls = lines(" x = 0;\n y = 1;\n z = 2;\n w = -1;");
let kinds = all_code(4);
let smells = detect_magic_numbers(&ls, &kinds, &["//"][..]);
assert!(smells.is_empty());
}
#[test]
fn const_declaration_not_detected() {
let ls = lines("const MAX: i32 = 100;");
let kinds = vec![LineKind::Code];
let smells = detect_magic_numbers(&ls, &kinds, &["//"][..]);
assert!(smells.is_empty());
}
#[test]
fn let_declaration_not_detected() {
let ls = lines("let threshold = 42;");
let kinds = vec![LineKind::Code];
let smells = detect_magic_numbers(&ls, &kinds, &["//"][..]);
assert!(smells.is_empty());
}
#[test]
fn readonly_declaration_not_detected() {
let ls = lines("readonly FINGERPRINT_LEN=16");
let kinds = vec![LineKind::Code];
let smells = detect_magic_numbers(&ls, &kinds, &["#"][..]);
assert!(smells.is_empty());
}
#[test]
fn declare_r_declaration_not_detected() {
let ls = lines("declare -r MAX_RETRIES=42");
let kinds = vec![LineKind::Code];
let smells = detect_magic_numbers(&ls, &kinds, &["#"][..]);
assert!(smells.is_empty());
}
#[test]
fn export_declaration_not_detected() {
let ls = lines("export PORT=8443");
let kinds = vec![LineKind::Code];
let smells = detect_magic_numbers(&ls, &kinds, &["#"][..]);
assert!(smells.is_empty());
}
#[test]
fn local_r_declaration_not_detected() {
let ls = lines(" local -r TIMEOUT=30");
let kinds = vec![LineKind::Code];
let smells = detect_magic_numbers(&ls, &kinds, &["#"][..]);
assert!(smells.is_empty());
}
#[test]
fn decl_keyword_not_matched_as_substring() {
let ls = lines(" is_deleteable(3600);");
let kinds = vec![LineKind::Code];
let smells = detect_magic_numbers(&ls, &kinds, &["//"][..]);
assert_eq!(
smells.len(),
1,
"substring match should not suppress detection"
);
}
#[test]
fn hex_literal_detected() {
let ls = lines(" mask = 0xFF;");
let kinds = vec![LineKind::Code];
let smells = detect_magic_numbers(&ls, &kinds, &["//"][..]);
assert_eq!(smells.len(), 1);
}
#[test]
fn scientific_notation_detected() {
let ls = lines(" threshold = 1.5e10;");
let kinds = vec![LineKind::Code];
let smells = detect_magic_numbers(&ls, &kinds, &["//"][..]);
assert_eq!(smells.len(), 1);
}
#[test]
fn digit_separator_detected_as_magic_number() {
let ls = lines(" timeout(1_000_000);");
let kinds = vec![LineKind::Code];
let smells = detect_magic_numbers(&ls, &kinds, &["//"][..]);
assert_eq!(
smells.len(),
1,
"1_000_000 should be detected as a magic number"
);
}
#[test]
fn hex_with_digit_separator_detected() {
let ls = lines(" mask = 0xFF_FF;");
let kinds = vec![LineKind::Code];
let smells = detect_magic_numbers(&ls, &kinds, &["//"][..]);
assert_eq!(
smells.len(),
1,
"0xFF_FF should be detected as a magic number"
);
}
#[test]
fn commented_out_code_detected() {
let ls = lines("// let x = 1;\n// if (y > 0) {\n// return z;\n// }");
let kinds = vec![
LineKind::Comment,
LineKind::Comment,
LineKind::Comment,
LineKind::Comment,
];
let smells = detect_commented_out_code(&ls, &kinds);
assert_eq!(smells.len(), 1);
assert_eq!(smells[0].kind, SmellKind::CommentedOutCode);
}
#[test]
fn regular_comments_not_detected() {
let ls = lines("// This is a description\n// of the module behavior");
let kinds = vec![LineKind::Comment, LineKind::Comment];
let smells = detect_commented_out_code(&ls, &kinds);
assert!(smells.is_empty());
}
#[test]
fn single_commented_code_line_not_detected() {
let ls = lines("// let x = 1;");
let kinds = vec![LineKind::Comment];
let smells = detect_commented_out_code(&ls, &kinds);
assert!(smells.is_empty());
}
#[test]
fn doc_comment_with_use_not_false_positive() {
let ls = lines("/// use std::io::Read;\n/// use tokio::runtime::Runtime;");
let kinds = vec![LineKind::Comment, LineKind::Comment];
let smells = detect_commented_out_code(&ls, &kinds);
assert!(
smells.is_empty(),
"doc comments with `use` should not trigger"
);
}