use std::collections::HashMap;
use super::signals::{NamingCase, PatternSignals};
use crate::types::{NamingConvention, NamingPattern, NamingViolation};
pub fn signals_to_pattern(signals: &PatternSignals) -> Option<NamingPattern> {
let naming = &signals.naming;
if !naming.has_signals() {
return None;
}
let functions = detect_majority_convention(&naming.function_names);
let classes = detect_majority_convention(&naming.class_names);
let constants = detect_majority_convention(&naming.constant_names);
let function_consistency = calculate_consistency(&naming.function_names, &functions);
let class_consistency = calculate_consistency(&naming.class_names, &classes);
let constant_consistency = calculate_consistency(&naming.constant_names, &constants);
let total_items =
naming.function_names.len() + naming.class_names.len() + naming.constant_names.len();
let consistency_score = if total_items > 0 {
let fn_weight = naming.function_names.len() as f64 / total_items as f64;
let cls_weight = naming.class_names.len() as f64 / total_items as f64;
let const_weight = naming.constant_names.len() as f64 / total_items as f64;
function_consistency * fn_weight
+ class_consistency * cls_weight
+ constant_consistency * const_weight
} else {
0.0
};
let mut violations = Vec::new();
violations.extend(find_violations(&naming.function_names, &functions));
violations.extend(find_violations(&naming.class_names, &classes));
violations.extend(find_violations(&naming.constant_names, &constants));
let private_prefix = naming
.private_prefixes
.iter()
.max_by_key(|(_, count)| *count)
.map(|(prefix, _)| prefix.clone());
Some(NamingPattern {
functions: naming_case_to_convention(functions),
classes: naming_case_to_convention(classes),
constants: naming_case_to_convention(constants),
private_prefix,
consistency_score,
violations,
})
}
fn detect_majority_convention(names: &[(String, NamingCase, String)]) -> NamingCase {
if names.is_empty() {
return NamingCase::Unknown;
}
let mut counts: HashMap<NamingCase, usize> = HashMap::new();
for (_, case, _) in names {
if *case != NamingCase::Unknown {
*counts.entry(*case).or_insert(0) += 1;
}
}
counts
.into_iter()
.max_by_key(|(_, count)| *count)
.map(|(case, _)| case)
.unwrap_or(NamingCase::Unknown)
}
fn calculate_consistency(names: &[(String, NamingCase, String)], expected: &NamingCase) -> f64 {
if names.is_empty() || *expected == NamingCase::Unknown {
return 0.0;
}
let matching = names.iter().filter(|(_, case, _)| case == expected).count();
matching as f64 / names.len() as f64
}
fn find_violations(
names: &[(String, NamingCase, String)],
expected: &NamingCase,
) -> Vec<NamingViolation> {
if *expected == NamingCase::Unknown {
return Vec::new();
}
names
.iter()
.filter(|(_, case, _)| *case != NamingCase::Unknown && case != expected)
.map(|(name, case, file)| NamingViolation {
name: name.clone(),
expected: naming_case_to_convention(*expected),
actual: naming_case_to_convention(*case),
file: file.clone(),
line: 0, })
.collect()
}
fn naming_case_to_convention(case: NamingCase) -> NamingConvention {
match case {
NamingCase::SnakeCase => NamingConvention::SnakeCase,
NamingCase::CamelCase => NamingConvention::CamelCase,
NamingCase::PascalCase => NamingConvention::PascalCase,
NamingCase::UpperSnakeCase => NamingConvention::UpperSnakeCase,
NamingCase::Unknown => NamingConvention::Mixed,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_no_signals_returns_none() {
let signals = PatternSignals::default();
assert!(signals_to_pattern(&signals).is_none());
}
#[test]
fn test_snake_case_functions_detected() {
let mut signals = PatternSignals::default();
signals.naming.function_names.push((
"find_user_by_id".to_string(),
NamingCase::SnakeCase,
"service.py".to_string(),
));
signals.naming.function_names.push((
"get_all_users".to_string(),
NamingCase::SnakeCase,
"service.py".to_string(),
));
signals.naming.function_names.push((
"create_user".to_string(),
NamingCase::SnakeCase,
"service.py".to_string(),
));
let pattern = signals_to_pattern(&signals).unwrap();
assert_eq!(pattern.functions, NamingConvention::SnakeCase);
assert!(pattern.consistency_score >= 0.9);
}
#[test]
fn test_pascal_case_classes_detected() {
let mut signals = PatternSignals::default();
signals.naming.class_names.push((
"UserService".to_string(),
NamingCase::PascalCase,
"service.py".to_string(),
));
signals.naming.class_names.push((
"OrderRepository".to_string(),
NamingCase::PascalCase,
"repo.py".to_string(),
));
let pattern = signals_to_pattern(&signals).unwrap();
assert_eq!(pattern.classes, NamingConvention::PascalCase);
}
#[test]
fn test_upper_snake_case_constants_detected() {
let mut signals = PatternSignals::default();
signals.naming.constant_names.push((
"MAX_RETRY_COUNT".to_string(),
NamingCase::UpperSnakeCase,
"config.py".to_string(),
));
signals.naming.constant_names.push((
"DEFAULT_TIMEOUT".to_string(),
NamingCase::UpperSnakeCase,
"config.py".to_string(),
));
let pattern = signals_to_pattern(&signals).unwrap();
assert_eq!(pattern.constants, NamingConvention::UpperSnakeCase);
}
#[test]
fn test_violation_detected() {
let mut signals = PatternSignals::default();
signals.naming.function_names.push((
"find_user".to_string(),
NamingCase::SnakeCase,
"service.py".to_string(),
));
signals.naming.function_names.push((
"getUser".to_string(), NamingCase::CamelCase,
"service.py".to_string(),
));
signals.naming.function_names.push((
"create_user".to_string(),
NamingCase::SnakeCase,
"service.py".to_string(),
));
let pattern = signals_to_pattern(&signals).unwrap();
assert!(!pattern.violations.is_empty());
assert_eq!(pattern.violations[0].name, "getUser");
assert_eq!(pattern.violations[0].expected, NamingConvention::SnakeCase);
assert_eq!(pattern.violations[0].actual, NamingConvention::CamelCase);
}
}