use crate::detectors::base::{Detector, DetectorConfig};
use crate::graph::GraphStore;
use crate::models::{Finding, Severity};
use anyhow::Result;
use std::collections::HashSet;
use std::path::PathBuf;
use tracing::{debug, info};
use uuid::Uuid;
#[derive(Debug, Clone)]
pub struct LongParameterThresholds {
pub max_params: usize,
pub high_params: usize,
pub critical_params: usize,
}
impl Default for LongParameterThresholds {
fn default() -> Self {
Self {
max_params: 5,
high_params: 7,
critical_params: 10,
}
}
}
static SKIP_PARAMS: &[&str] = &["self", "cls"];
pub struct LongParameterListDetector {
config: DetectorConfig,
thresholds: LongParameterThresholds,
skip_params: HashSet<String>,
}
impl LongParameterListDetector {
pub fn new() -> Self {
Self::with_thresholds(LongParameterThresholds::default())
}
pub fn with_thresholds(thresholds: LongParameterThresholds) -> Self {
let skip_params: HashSet<String> = SKIP_PARAMS.iter().map(|s| s.to_string()).collect();
Self {
config: DetectorConfig::new(),
thresholds,
skip_params,
}
}
pub fn with_config(config: DetectorConfig) -> Self {
let thresholds = LongParameterThresholds {
max_params: config.get_option_or("max_params", 5),
high_params: config.get_option_or("high_params", 7),
critical_params: config.get_option_or("critical_params", 10),
};
let skip_params: HashSet<String> = SKIP_PARAMS.iter().map(|s| s.to_string()).collect();
Self {
config,
thresholds,
skip_params,
}
}
fn get_meaningful_params(&self, params: &[serde_json::Value]) -> Vec<String> {
params
.iter()
.filter_map(|p| {
let name = if p.is_string() {
p.as_str().map(|s| s.to_string())
} else if let Some(obj) = p.as_object() {
obj.get("name").and_then(|n| n.as_str()).map(|s| s.to_string())
} else {
None
};
name.filter(|n| !self.skip_params.contains(n))
})
.collect()
}
fn calculate_severity(&self, param_count: usize) -> Severity {
if param_count >= self.thresholds.critical_params {
Severity::Critical
} else if param_count >= self.thresholds.high_params {
Severity::High
} else if param_count > self.thresholds.max_params {
Severity::Medium
} else {
Severity::Low
}
}
fn suggest_config_name(&self, func_name: &str, params: &[String]) -> String {
if let Some(base) = func_name.strip_prefix("create_") {
return format!("{}Config", to_pascal_case(base));
}
if let Some(base) = func_name.strip_prefix("init_") {
return format!("{}Options", to_pascal_case(base));
}
if let Some(base) = func_name.strip_prefix("initialize_") {
return format!("{}Options", to_pascal_case(base));
}
if let Some(base) = func_name.strip_prefix("process_") {
return format!("{}Params", to_pascal_case(base));
}
if let Some(base) = func_name.strip_prefix("configure_") {
return format!("{}Config", to_pascal_case(base));
}
let param_set: HashSet<&str> = params.iter().map(|s| s.as_str()).collect();
if param_set.contains("host") && param_set.contains("port") {
return "ConnectionConfig".to_string();
}
if param_set.contains("url") && param_set.contains("timeout") {
return "ConnectionConfig".to_string();
}
if param_set.contains("username") && param_set.contains("password") {
return "Credentials".to_string();
}
if param_set.contains("width") && param_set.contains("height") {
return "Dimensions".to_string();
}
if param_set.contains("x") && param_set.contains("y") {
return "Position".to_string();
}
if param_set.contains("start") && param_set.contains("end") {
return "Range".to_string();
}
format!("{}Config", to_pascal_case(func_name))
}
fn generate_suggestion(&self, func_name: &str, params: &[String]) -> String {
let config_name = self.suggest_config_name(func_name, params);
let mut lines = vec![
"**Refactoring Options:**\n".to_string(),
"**1. Introduce Parameter Object:**".to_string(),
"```python".to_string(),
"from dataclasses import dataclass".to_string(),
String::new(),
"@dataclass".to_string(),
format!("class {}:", config_name),
];
for p in params.iter().take(6) {
lines.push(format!(" {}: Any # TODO: add type", p));
}
if params.len() > 6 {
lines.push(format!(" # ... and {} more fields", params.len() - 6));
}
lines.push(String::new());
lines.push(format!("def {}(config: {}):", func_name, config_name));
lines.push(" ...".to_string());
lines.push("```".to_string());
lines.push(String::new());
if params.len() >= 8 {
let builder_name = format!("{}Builder", to_pascal_case(func_name));
lines.push("**2. Use Builder Pattern:**".to_string());
lines.push("```python".to_string());
lines.push(format!("class {}:", builder_name));
if let Some(p) = params.first() {
lines.push(format!(" def with_{}(self, value): ...", p));
}
if let Some(p) = params.get(1) {
lines.push(format!(" def with_{}(self, value): ...", p));
}
lines.push(" # ... more setters".to_string());
lines.push(format!(" def build(self): return {}(...)", func_name));
lines.push("```".to_string());
lines.push(String::new());
}
let option_num = if params.len() >= 8 { "3" } else { "2" };
lines.push(format!("**{}. Split Into Smaller Functions:**", option_num));
lines.push(format!(
"- Break `{}` into functions with focused responsibilities",
func_name
));
lines.push("- Each function handles a subset of the original task".to_string());
lines.join("\n")
}
fn estimate_effort(&self, param_count: usize) -> String {
if param_count >= 12 {
"Large (1-2 days)".to_string()
} else if param_count >= 8 {
"Medium (4-8 hours)".to_string()
} else if param_count >= 6 {
"Small (2-4 hours)".to_string()
} else {
"Small (1 hour)".to_string()
}
}
fn create_finding(
&self,
_qualified_name: String,
func_name: String,
file_path: String,
line_start: Option<u32>,
params: Vec<String>,
) -> Finding {
let param_count = params.len();
let severity = self.calculate_severity(param_count);
let mut params_display = params.iter().take(8).cloned().collect::<Vec<_>>().join(", ");
if params.len() > 8 {
params_display.push_str(&format!(" ... ({} total)", params.len()));
}
let description = if param_count >= self.thresholds.critical_params {
format!(
"Function `{}` has {} parameters: `{}`\n\n\
**Threshold**: >{} parameters\n\n\
This is a critical issue. Such long parameter lists:\n\
- Are nearly impossible to use correctly\n\
- Indicate the function is doing way too much\n\
- Should be split into multiple smaller functions",
func_name, param_count, params_display, self.thresholds.max_params
)
} else if param_count >= self.thresholds.high_params {
format!(
"Function `{}` has {} parameters: `{}`\n\n\
**Threshold**: >{} parameters\n\n\
Consider refactoring to:\n\
- Group related parameters into a data class\n\
- Split the function into smaller functions\n\
- Use the Builder pattern for complex construction",
func_name, param_count, params_display, self.thresholds.max_params
)
} else {
format!(
"Function `{}` has {} parameters: `{}`\n\n\
**Threshold**: >{} parameters\n\n\
Consider whether some parameters can be grouped \
into a single configuration object or data class.",
func_name, param_count, params_display, self.thresholds.max_params
)
};
Finding {
id: Uuid::new_v4().to_string(),
detector: "LongParameterListDetector".to_string(),
severity,
title: format!("Long parameter list: {} ({} params)", func_name, param_count),
description,
affected_files: vec![PathBuf::from(&file_path)],
line_start,
line_end: None,
suggested_fix: Some(self.generate_suggestion(&func_name, ¶ms)),
estimated_effort: Some(self.estimate_effort(param_count)),
category: Some("code_smell".to_string()),
cwe_id: None,
why_it_matters: Some(
"Long parameter lists make functions difficult to use correctly. \
Callers must remember the order and meaning of each parameter, \
leading to errors. They also indicate that a function may be \
doing too much and should be split."
.to_string(),
),
..Default::default()
}
}
}
impl Default for LongParameterListDetector {
fn default() -> Self {
Self::new()
}
}
impl Detector for LongParameterListDetector {
fn name(&self) -> &'static str {
"LongParameterListDetector"
}
fn description(&self) -> &'static str {
"Detects functions with too many parameters"
}
fn category(&self) -> &'static str {
"code_smell"
}
fn config(&self) -> Option<&DetectorConfig> {
Some(&self.config)
}
fn detect(&self, graph: &GraphStore) -> Result<Vec<Finding>> {
let mut findings = Vec::new();
for func in graph.get_functions() {
let param_count = func.param_count().unwrap_or(0) as usize;
if param_count > self.thresholds.max_params {
let severity = self.calculate_severity(param_count);
findings.push(Finding {
id: Uuid::new_v4().to_string(),
detector: "LongParameterListDetector".to_string(),
severity,
title: format!("Long parameter list: {}", func.name),
description: format!(
"Function '{}' has {} parameters (threshold: {}). Consider using a config object.",
func.name, param_count, self.thresholds.max_params
),
affected_files: vec![func.file_path.clone().into()],
line_start: Some(func.line_start),
line_end: Some(func.line_end),
suggested_fix: Some("Group related parameters into a configuration object or class".to_string()),
estimated_effort: Some(self.estimate_effort(param_count)),
category: Some("quality".to_string()),
cwe_id: None,
why_it_matters: Some("Long parameter lists make functions hard to call and understand".to_string()),
..Default::default()
});
}
}
Ok(findings)
}
}
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()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_thresholds() {
let detector = LongParameterListDetector::new();
assert_eq!(detector.thresholds.max_params, 5);
assert_eq!(detector.thresholds.high_params, 7);
assert_eq!(detector.thresholds.critical_params, 10);
}
#[test]
fn test_severity_calculation() {
let detector = LongParameterListDetector::new();
assert_eq!(detector.calculate_severity(5), Severity::Low);
assert_eq!(detector.calculate_severity(6), Severity::Medium);
assert_eq!(detector.calculate_severity(7), Severity::High);
assert_eq!(detector.calculate_severity(10), Severity::Critical);
}
#[test]
fn test_meaningful_params() {
let detector = LongParameterListDetector::new();
let params = vec![
serde_json::json!("self"),
serde_json::json!("x"),
serde_json::json!("y"),
serde_json::json!({"name": "cls"}),
serde_json::json!({"name": "config"}),
];
let meaningful = detector.get_meaningful_params(¶ms);
assert_eq!(meaningful, vec!["x", "y", "config"]);
}
#[test]
fn test_to_pascal_case() {
assert_eq!(to_pascal_case("hello_world"), "HelloWorld");
assert_eq!(to_pascal_case("create_user"), "CreateUser");
assert_eq!(to_pascal_case("x"), "X");
}
#[test]
fn test_suggest_config_name() {
let detector = LongParameterListDetector::new();
assert_eq!(
detector.suggest_config_name("create_user", &[]),
"UserConfig"
);
assert_eq!(
detector.suggest_config_name(
"connect",
&["host".to_string(), "port".to_string()]
),
"ConnectionConfig"
);
assert_eq!(
detector.suggest_config_name(
"login",
&["username".to_string(), "password".to_string()]
),
"Credentials"
);
}
}