use super::{
MaintainabilityImpact, OrganizationAntiPattern, OrganizationDetector, Parameter,
ParameterGroup, ParameterRefactoring,
};
use crate::common::{capitalize_first, SourceLocation};
use std::collections::HashMap;
use syn::{self, visit::Visit};
pub struct ParameterAnalyzer {
max_parameters: usize,
}
impl Default for ParameterAnalyzer {
fn default() -> Self {
Self { max_parameters: 5 }
}
}
impl ParameterAnalyzer {
pub fn new() -> Self {
Self::default()
}
fn suggest_parameter_refactoring(
&self,
function: &FunctionInfo,
data_clumps: &[ParameterGroup],
) -> ParameterRefactoring {
if !data_clumps.is_empty() {
ParameterRefactoring::ExtractStruct
} else if function.parameters.len() > 8 {
ParameterRefactoring::UseBuilder
} else if self.has_many_boolean_parameters(function) {
ParameterRefactoring::UseConfiguration
} else {
ParameterRefactoring::SplitFunction
}
}
fn has_many_boolean_parameters(&self, function: &FunctionInfo) -> bool {
let bool_count = function
.parameters
.iter()
.filter(|p| p.type_name == "bool")
.count();
bool_count > 2
}
fn find_data_clumps(&self, parameters: &[Parameter]) -> Vec<ParameterGroup> {
let mut clumps = Vec::new();
let groups = self.group_parameters_by_semantics(parameters);
for (semantic_group, group_params) in groups {
if group_params.len() >= 3 {
clumps.push(ParameterGroup {
parameters: group_params,
group_name: semantic_group.clone(),
semantic_relationship: semantic_group,
});
}
}
clumps
}
fn group_parameters_by_semantics(
&self,
parameters: &[Parameter],
) -> HashMap<String, Vec<Parameter>> {
let mut groups: HashMap<String, Vec<Parameter>> = HashMap::new();
for param in parameters {
let semantic_group = self.identify_semantic_group(param);
groups
.entry(semantic_group)
.or_default()
.push(param.clone());
}
groups
}
fn identify_semantic_group(&self, parameter: &Parameter) -> String {
let name = parameter.name.to_lowercase();
const SEMANTIC_PATTERNS: &[(&str, &[&str])] = &[
("coordinate", &["x", "y", "z", "width", "height", "depth"]),
("time", &["start", "end", "duration", "timeout", "delay"]),
("user", &["user", "username", "userid", "email", "name"]),
("config", &["config", "setting", "option", "preference"]),
("network", &["host", "port", "url", "endpoint", "address"]),
("file", &["path", "filename", "directory", "extension"]),
(
"authentication",
&["token", "key", "secret", "auth", "credential"],
),
("pagination", &["page", "limit", "offset", "size", "count"]),
];
for (group_name, keywords) in SEMANTIC_PATTERNS {
if keywords.iter().any(|keyword| name.contains(keyword)) {
return group_name.to_string();
}
}
format!("type_{}", parameter.type_name)
}
fn count_clump_occurrences(&self, clump: &ParameterGroup, functions: &[FunctionInfo]) -> usize {
functions
.iter()
.filter(|f| self.function_has_clump(f, clump))
.count()
}
fn function_has_clump(&self, function: &FunctionInfo, clump: &ParameterGroup) -> bool {
let clump_names: Vec<_> = clump.parameters.iter().map(|p| &p.name).collect();
let function_names: Vec<_> = function.parameters.iter().map(|p| &p.name).collect();
clump_names.iter().all(|name| function_names.contains(name))
}
fn suggest_struct_name(&self, clump: &ParameterGroup) -> String {
if !clump.group_name.is_empty() {
format!("{}Config", capitalize_first(&clump.group_name))
} else {
"ConfigParameters".to_string()
}
}
}
impl OrganizationDetector for ParameterAnalyzer {
fn detect_anti_patterns(&self, file: &syn::File) -> Vec<OrganizationAntiPattern> {
let mut patterns = Vec::new();
let mut visitor = FunctionVisitor::new();
visitor.visit_file(file);
let functions = visitor.functions;
for function in &functions {
if function.parameters.len() > self.max_parameters {
let data_clumps = self.find_data_clumps(&function.parameters);
let refactoring = self.suggest_parameter_refactoring(function, &data_clumps);
patterns.push(OrganizationAntiPattern::LongParameterList {
function_name: function.name.clone(),
parameter_count: function.parameters.len(),
data_clumps,
suggested_refactoring: refactoring,
location: SourceLocation::default(), });
}
let data_clumps = self.find_data_clumps(&function.parameters);
for clump in data_clumps {
if clump.parameters.len() >= 3 {
patterns.push(OrganizationAntiPattern::DataClump {
parameter_group: clump.clone(),
occurrence_count: self.count_clump_occurrences(&clump, &functions),
suggested_struct_name: self.suggest_struct_name(&clump),
locations: vec![SourceLocation::default()], });
}
}
}
patterns
}
fn detector_name(&self) -> &'static str {
"ParameterAnalyzer"
}
fn estimate_maintainability_impact(
&self,
pattern: &OrganizationAntiPattern,
) -> MaintainabilityImpact {
match pattern {
OrganizationAntiPattern::LongParameterList {
parameter_count, ..
} => ParameterAnalyzer::classify_parameter_list_impact(*parameter_count),
OrganizationAntiPattern::DataClump {
occurrence_count, ..
} => ParameterAnalyzer::classify_data_clump_impact(*occurrence_count),
_ => MaintainabilityImpact::Low,
}
}
}
impl ParameterAnalyzer {
fn classify_parameter_list_impact(parameter_count: usize) -> MaintainabilityImpact {
match parameter_count {
count if count > 10 => MaintainabilityImpact::High,
count if count > 7 => MaintainabilityImpact::Medium,
_ => MaintainabilityImpact::Low,
}
}
fn classify_data_clump_impact(occurrence_count: usize) -> MaintainabilityImpact {
match occurrence_count {
count if count > 5 => MaintainabilityImpact::Medium,
_ => MaintainabilityImpact::Low,
}
}
}
struct FunctionInfo {
name: String,
parameters: Vec<Parameter>,
}
struct FunctionVisitor {
functions: Vec<FunctionInfo>,
}
impl FunctionVisitor {
fn new() -> Self {
Self {
functions: Vec::new(),
}
}
fn extract_parameters(
&self,
inputs: &syn::punctuated::Punctuated<syn::FnArg, syn::token::Comma>,
) -> Vec<Parameter> {
let mut parameters = Vec::new();
let mut position = 0;
for input in inputs {
if let syn::FnArg::Typed(pat_type) = input {
if let syn::Pat::Ident(pat_ident) = &*pat_type.pat {
let name = pat_ident.ident.to_string();
let type_name = self.extract_type_name(&pat_type.ty);
parameters.push(Parameter {
name,
type_name,
position,
});
position += 1;
}
}
}
parameters
}
#[allow(clippy::only_used_in_recursion)]
fn extract_type_name(&self, ty: &syn::Type) -> String {
match ty {
syn::Type::Path(type_path) => type_path
.path
.segments
.last()
.map(|seg| seg.ident.to_string())
.unwrap_or_else(|| "Unknown".to_string()),
syn::Type::Reference(type_ref) => self.extract_type_name(&type_ref.elem),
_ => "Unknown".to_string(),
}
}
}
impl<'ast> Visit<'ast> for FunctionVisitor {
fn visit_item_fn(&mut self, node: &'ast syn::ItemFn) {
let function = FunctionInfo {
name: node.sig.ident.to_string(),
parameters: self.extract_parameters(&node.sig.inputs),
};
self.functions.push(function);
syn::visit::visit_item_fn(self, node);
}
fn visit_impl_item_fn(&mut self, node: &'ast syn::ImplItemFn) {
let function = FunctionInfo {
name: node.sig.ident.to_string(),
parameters: self.extract_parameters(&node.sig.inputs),
};
self.functions.push(function);
syn::visit::visit_impl_item_fn(self, node);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::organization::{
OrganizationAntiPattern, ParameterGroup, ParameterRefactoring, ValueContext,
};
#[test]
fn test_classify_parameter_list_impact_high() {
assert_eq!(
ParameterAnalyzer::classify_parameter_list_impact(11),
MaintainabilityImpact::High
);
assert_eq!(
ParameterAnalyzer::classify_parameter_list_impact(15),
MaintainabilityImpact::High
);
assert_eq!(
ParameterAnalyzer::classify_parameter_list_impact(100),
MaintainabilityImpact::High
);
}
#[test]
fn test_classify_parameter_list_impact_medium() {
assert_eq!(
ParameterAnalyzer::classify_parameter_list_impact(8),
MaintainabilityImpact::Medium
);
assert_eq!(
ParameterAnalyzer::classify_parameter_list_impact(9),
MaintainabilityImpact::Medium
);
assert_eq!(
ParameterAnalyzer::classify_parameter_list_impact(10),
MaintainabilityImpact::Medium
);
}
#[test]
fn test_classify_parameter_list_impact_low() {
assert_eq!(
ParameterAnalyzer::classify_parameter_list_impact(0),
MaintainabilityImpact::Low
);
assert_eq!(
ParameterAnalyzer::classify_parameter_list_impact(5),
MaintainabilityImpact::Low
);
assert_eq!(
ParameterAnalyzer::classify_parameter_list_impact(7),
MaintainabilityImpact::Low
);
}
#[test]
fn test_classify_data_clump_impact_medium() {
assert_eq!(
ParameterAnalyzer::classify_data_clump_impact(6),
MaintainabilityImpact::Medium
);
assert_eq!(
ParameterAnalyzer::classify_data_clump_impact(10),
MaintainabilityImpact::Medium
);
assert_eq!(
ParameterAnalyzer::classify_data_clump_impact(100),
MaintainabilityImpact::Medium
);
}
#[test]
fn test_classify_data_clump_impact_low() {
assert_eq!(
ParameterAnalyzer::classify_data_clump_impact(0),
MaintainabilityImpact::Low
);
assert_eq!(
ParameterAnalyzer::classify_data_clump_impact(3),
MaintainabilityImpact::Low
);
assert_eq!(
ParameterAnalyzer::classify_data_clump_impact(5),
MaintainabilityImpact::Low
);
}
#[test]
fn test_estimate_maintainability_impact_long_parameter_list() {
let analyzer = ParameterAnalyzer::new();
let pattern = OrganizationAntiPattern::LongParameterList {
function_name: "test_function".to_string(),
parameter_count: 12,
data_clumps: vec![],
suggested_refactoring: ParameterRefactoring::ExtractStruct,
location: SourceLocation::default(),
};
assert_eq!(
analyzer.estimate_maintainability_impact(&pattern),
MaintainabilityImpact::High
);
let pattern = OrganizationAntiPattern::LongParameterList {
function_name: "test_function".to_string(),
parameter_count: 8,
data_clumps: vec![],
suggested_refactoring: ParameterRefactoring::ExtractStruct,
location: SourceLocation::default(),
};
assert_eq!(
analyzer.estimate_maintainability_impact(&pattern),
MaintainabilityImpact::Medium
);
}
#[test]
fn test_estimate_maintainability_impact_data_clump() {
let analyzer = ParameterAnalyzer::new();
let pattern = OrganizationAntiPattern::DataClump {
parameter_group: ParameterGroup {
parameters: vec![],
group_name: "test_group".to_string(),
semantic_relationship: "related".to_string(),
},
occurrence_count: 7,
suggested_struct_name: "TestStruct".to_string(),
locations: vec![],
};
assert_eq!(
analyzer.estimate_maintainability_impact(&pattern),
MaintainabilityImpact::Medium
);
let pattern = OrganizationAntiPattern::DataClump {
parameter_group: ParameterGroup {
parameters: vec![],
group_name: "test_group".to_string(),
semantic_relationship: "related".to_string(),
},
occurrence_count: 3,
suggested_struct_name: "TestStruct".to_string(),
locations: vec![],
};
assert_eq!(
analyzer.estimate_maintainability_impact(&pattern),
MaintainabilityImpact::Low
);
}
#[test]
fn test_estimate_maintainability_impact_other_patterns() {
let analyzer = ParameterAnalyzer::new();
let pattern = OrganizationAntiPattern::MagicValue {
value_type: crate::organization::MagicValueType::NumericLiteral,
value: "42".to_string(),
occurrence_count: 5,
suggested_constant_name: "ANSWER".to_string(),
context: ValueContext::BusinessLogic,
locations: vec![],
};
assert_eq!(
analyzer.estimate_maintainability_impact(&pattern),
MaintainabilityImpact::Low
);
}
#[test]
fn test_parameter_list_boundary_values() {
assert_eq!(
ParameterAnalyzer::classify_parameter_list_impact(7),
MaintainabilityImpact::Low
);
assert_eq!(
ParameterAnalyzer::classify_parameter_list_impact(8),
MaintainabilityImpact::Medium
);
assert_eq!(
ParameterAnalyzer::classify_parameter_list_impact(10),
MaintainabilityImpact::Medium
);
assert_eq!(
ParameterAnalyzer::classify_parameter_list_impact(11),
MaintainabilityImpact::High
);
}
}