use crate::detection::{FunctionDetectionMarkers, detect_function_bodies};
use crate::loc::counter::LineKind;
use crate::util::mask_strings;
use super::analyzer::{SmellInstance, SmellKind};
pub fn detect_long_functions(
lines: &[String],
kinds: &[LineKind],
markers: &dyn FunctionDetectionMarkers,
max_lines: usize,
) -> Vec<SmellInstance> {
let code_lines: Vec<(usize, &str)> = lines
.iter()
.enumerate()
.filter(|(i, _)| kinds.get(*i) == Some(&LineKind::Code))
.map(|(i, l)| (i, l.as_str()))
.collect();
let functions = detect_function_bodies(lines, &code_lines, markers);
let mut smells = Vec::new();
for func in &functions {
let total = func.code_lines.len();
let overhead = if markers.brace_scoped() {
2.min(total)
} else {
1.min(total)
};
let body_len = total.saturating_sub(overhead);
if body_len > max_lines {
smells.push(SmellInstance {
kind: SmellKind::LongFunction,
line: func.start_line,
detail: format!(
"function `{}` has {} lines (max {})",
func.name, body_len, max_lines
),
});
}
}
smells
}
pub fn detect_long_params(
lines: &[String],
kinds: &[LineKind],
markers: &dyn FunctionDetectionMarkers,
max_params: usize,
) -> Vec<SmellInstance> {
let code_lines: Vec<(usize, &str)> = lines
.iter()
.enumerate()
.filter(|(i, _)| kinds.get(*i) == Some(&LineKind::Code))
.map(|(i, l)| (i, l.as_str()))
.collect();
let functions = detect_function_bodies(lines, &code_lines, markers);
let mut smells = Vec::new();
for func in &functions {
if func.code_lines.is_empty() {
continue;
}
let signature = collect_signature(func, markers.line_comments());
let Some(signature) = signature else {
continue;
};
let Some(open) = signature.find('(') else {
continue;
};
let after_open = &signature[open + 1..];
let close = find_matching_paren(after_open);
if close >= after_open.len() {
continue;
}
let params_str = after_open[..close].trim().trim_end_matches(',').trim();
if params_str.is_empty() {
continue;
}
let param_count = params_str.matches(',').count() + 1;
if param_count > max_params {
smells.push(SmellInstance {
kind: SmellKind::LongParameterList,
line: func.start_line,
detail: format!(
"function `{}` has {} params (max {})",
func.name, param_count, max_params
),
});
}
}
smells
}
fn collect_signature(
func: &crate::detection::FunctionBody<'_>,
line_comments: &[&str],
) -> Option<String> {
let mut sig = String::new();
for &(_, code_line) in &func.code_lines {
let masked = mask_strings(code_line, line_comments);
if sig.is_empty() && !masked.contains('(') {
continue;
}
sig.push_str(&masked);
sig.push(' ');
if let Some(open) = sig.find('(') {
let after = &sig[open + 1..];
let close = find_matching_paren(after);
if close < after.len() {
return Some(sig);
}
}
}
if sig.is_empty() { None } else { Some(sig) }
}
fn find_matching_paren(s: &str) -> usize {
let mut depth = 0i32;
for (i, ch) in s.char_indices() {
match ch {
'(' => depth += 1,
')' => {
if depth == 0 {
return i;
}
depth -= 1;
}
_ => {}
}
}
s.len()
}
const DEBT_KEYWORDS: &[&str] = &["TODO", "FIXME", "HACK", "XXX", "BUG"];
pub fn detect_todo_debt(lines: &[String], kinds: &[LineKind]) -> Vec<SmellInstance> {
let mut smells = Vec::new();
for (i, line) in lines.iter().enumerate() {
if kinds.get(i) != Some(&LineKind::Comment) {
continue;
}
let upper = line.to_uppercase();
for &kw in DEBT_KEYWORDS {
if upper.contains(kw) {
smells.push(SmellInstance {
kind: SmellKind::TodoDebt,
line: i + 1,
detail: format!("{kw} comment"),
});
break;
}
}
}
smells
}
const DECL_KEYWORDS: &[&str] = &["const", "let", "static", "final", "val", "#define", "enum"];
const TRIVIAL_NUMBERS: &[&str] = &["0", "1", "2", "-1"];
fn is_declaration_line(trimmed: &str) -> bool {
let lower = trimmed.to_lowercase();
let first_token = lower.split_whitespace().next().unwrap_or("");
DECL_KEYWORDS.contains(&first_token)
}
fn is_numeric_char(b: u8) -> bool {
b.is_ascii_digit()
|| b == b'.'
|| b == b'_'
|| b == b'x'
|| b == b'X'
|| b == b'b'
|| b == b'B'
|| b == b'o'
|| b == b'O'
|| (b'a'..=b'f').contains(&b)
|| (b'A'..=b'F').contains(&b)
|| b == b'e'
|| b == b'E'
}
pub fn detect_magic_numbers(
lines: &[String],
kinds: &[LineKind],
line_comments: &[&str],
) -> Vec<SmellInstance> {
let mut smells = Vec::new();
for (i, line) in lines.iter().enumerate() {
if kinds.get(i) != Some(&LineKind::Code) {
continue;
}
let masked = mask_strings(line, line_comments);
let trimmed = masked.trim();
if is_declaration_line(trimmed) {
continue;
}
if has_magic_number(trimmed) {
smells.push(SmellInstance {
kind: SmellKind::MagicNumber,
line: i + 1,
detail: "magic number in code".to_string(),
});
}
}
smells
}
fn is_negative_prefix(bytes: &[u8], pos: usize) -> bool {
bytes[pos] == b'-'
&& pos + 1 < bytes.len()
&& bytes[pos + 1].is_ascii_digit()
&& (pos == 0 || !bytes[pos - 1].is_ascii_alphanumeric())
}
fn is_preceded_by_ident(bytes: &[u8], pos: usize) -> bool {
pos > 0 && (bytes[pos - 1].is_ascii_alphanumeric() || bytes[pos - 1] == b'_')
}
fn scan_numeric_literal(bytes: &[u8], start: usize) -> usize {
let mut j = start;
while j < bytes.len() && is_numeric_char(bytes[j]) {
if (bytes[j] == b'e' || bytes[j] == b'E')
&& j + 1 < bytes.len()
&& (bytes[j + 1] == b'+' || bytes[j + 1] == b'-')
{
j += 2; continue;
}
j += 1;
}
j
}
fn has_magic_number(trimmed: &str) -> bool {
let bytes = trimmed.as_bytes();
let mut j = 0;
while j < bytes.len() {
let is_neg = is_negative_prefix(bytes, j);
let start = j;
if is_neg {
j += 1;
}
if j < bytes.len() && bytes[j].is_ascii_digit() {
if is_preceded_by_ident(bytes, start) {
j = scan_numeric_literal(bytes, j);
continue;
}
j = scan_numeric_literal(bytes, j);
if j < bytes.len() && (bytes[j].is_ascii_alphanumeric() || bytes[j] == b'_') {
j += 1;
continue;
}
let num_str = &trimmed[start..j];
let num_clean = num_str.trim_end_matches('.');
if !TRIVIAL_NUMBERS.contains(&num_clean) {
return true;
}
} else {
j += 1;
}
}
false
}
const STRONG_CODE_PATTERNS: &[&str] = &[";", "{", "}", "return ", "if(", "for(", "while(", "else{"];
const WEAK_CODE_PATTERNS: &[&str] = &[
"if ",
"for ",
"while ",
"else ",
"let ",
"var ",
"const ",
"fn ",
"def ",
"func ",
"function ",
"class ",
"= ",
"=>",
"->",
];
fn flush_code_run(run_start: Option<usize>, run_len: usize, smells: &mut Vec<SmellInstance>) {
if run_len >= 2
&& let Some(start) = run_start
{
smells.push(SmellInstance {
kind: SmellKind::CommentedOutCode,
line: start + 1,
detail: format!("{run_len} lines of commented-out code"),
});
}
}
pub fn detect_commented_out_code(lines: &[String], kinds: &[LineKind]) -> Vec<SmellInstance> {
let mut smells = Vec::new();
let mut run_start: Option<usize> = None;
let mut run_len = 0;
for (i, line) in lines.iter().enumerate() {
let is_comment = kinds.get(i) == Some(&LineKind::Comment);
if is_comment && has_code_patterns(line) {
if run_start.is_none() {
run_start = Some(i);
}
run_len += 1;
} else {
flush_code_run(run_start, run_len, &mut smells);
run_start = None;
run_len = 0;
}
}
flush_code_run(run_start, run_len, &mut smells);
smells
}
fn has_code_patterns(line: &str) -> bool {
let stripped = line
.trim()
.trim_start_matches("///")
.trim_start_matches("//")
.trim_start_matches('#')
.trim_start_matches("--")
.trim_start_matches('%')
.trim_start_matches("/*")
.trim_start_matches('*')
.trim();
let strong = STRONG_CODE_PATTERNS
.iter()
.filter(|p| stripped.contains(**p))
.count();
if strong == 0 {
return false;
}
let weak = WEAK_CODE_PATTERNS
.iter()
.filter(|p| stripped.contains(**p))
.count();
strong + weak >= 2
}
#[cfg(test)]
#[path = "rules_test.rs"]
mod tests;