use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MappingPatternConfig {
#[serde(default = "default_enabled")]
pub enabled: bool,
#[serde(default = "default_min_mapping_ratio")]
pub min_mapping_ratio: f64,
#[serde(default = "default_adjustment_factor")]
pub adjustment_factor: f64,
#[serde(default = "default_max_arm_complexity")]
pub max_arm_complexity: u32,
}
impl Default for MappingPatternConfig {
fn default() -> Self {
Self {
enabled: default_enabled(),
min_mapping_ratio: default_min_mapping_ratio(),
adjustment_factor: default_adjustment_factor(),
max_arm_complexity: default_max_arm_complexity(),
}
}
}
fn default_enabled() -> bool {
true
}
fn default_min_mapping_ratio() -> f64 {
0.8
}
fn default_adjustment_factor() -> f64 {
0.4
}
fn default_max_arm_complexity() -> u32 {
2
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct MappingPatternResult {
pub is_pure_mapping: bool,
pub confidence: f64,
pub mapping_ratio: f64,
pub complexity_adjustment_factor: f64,
pub pattern_description: String,
}
impl Default for MappingPatternResult {
fn default() -> Self {
Self {
is_pure_mapping: false,
confidence: 0.0,
mapping_ratio: 0.0,
complexity_adjustment_factor: 1.0,
pattern_description: String::new(),
}
}
}
pub struct MappingPatternDetector {
config: MappingPatternConfig,
}
impl MappingPatternDetector {
pub fn new(config: MappingPatternConfig) -> Self {
Self { config }
}
pub fn analyze_function(
&self,
function_body: &str,
cyclomatic_complexity: u32,
) -> MappingPatternResult {
if !self.config.enabled || cyclomatic_complexity < 10 {
return MappingPatternResult::default();
}
if let Some(result) = self.detect_rust_match_pattern(function_body) {
return result;
}
if let Some(result) = self.detect_switch_pattern(function_body) {
return result;
}
if let Some(result) = self.detect_ifelse_chain_pattern(function_body) {
return result;
}
MappingPatternResult::default()
}
fn detect_rust_match_pattern(&self, body: &str) -> Option<MappingPatternResult> {
if !body.contains("match ") {
return None;
}
let lines: Vec<&str> = body.lines().collect();
let non_empty_lines: Vec<&str> = lines
.iter()
.filter(|l| !l.trim().is_empty())
.copied()
.collect();
let total_lines = non_empty_lines.len();
let arm_count = lines.iter().filter(|line| line.contains("=>")).count();
if arm_count < 3 {
return None; }
let match_start = non_empty_lines
.iter()
.position(|line| line.contains("match "))?;
let match_end = non_empty_lines
.iter()
.rposition(|line| line.trim_end().ends_with('}'))?;
let match_lines = match_end.saturating_sub(match_start) + 1;
let mapping_ratio = match_lines as f64 / total_lines as f64;
if mapping_ratio < self.config.min_mapping_ratio {
return None;
}
let complex_arm_count = lines
.iter()
.filter(|line| {
let line_lower = line.to_lowercase();
line.contains("=>")
&& (line_lower.contains("if ")
|| line_lower.contains("match ")
|| line_lower.contains("loop ")
|| line_lower.contains("for ")
|| line_lower.contains("while "))
})
.count();
if complex_arm_count > 0 {
return None; }
let match_count = body.matches("match ").count();
let is_nested = match_count == 2;
Some(MappingPatternResult {
is_pure_mapping: true,
confidence: 0.9,
mapping_ratio,
complexity_adjustment_factor: self.config.adjustment_factor,
pattern_description: if is_nested {
format!("nested exhaustive match with {} arms", arm_count)
} else {
format!("exhaustive match with {} arms", arm_count)
},
})
}
fn detect_switch_pattern(&self, body: &str) -> Option<MappingPatternResult> {
if !body.contains("switch") {
return None;
}
let lines: Vec<&str> = body.lines().collect();
let non_empty_lines: Vec<&str> = lines
.iter()
.filter(|l| !l.trim().is_empty())
.copied()
.collect();
let total_lines = non_empty_lines.len();
let case_count = lines
.iter()
.filter(|line| line.trim().starts_with("case "))
.count();
if case_count < 3 {
return None;
}
let return_count = lines.iter().filter(|line| line.contains("return ")).count();
if return_count < case_count {
return None; }
let switch_start = non_empty_lines
.iter()
.position(|line| line.contains("switch"))?;
let switch_end = non_empty_lines
.iter()
.rposition(|line| line.trim() == "}")?;
let switch_lines = switch_end.saturating_sub(switch_start) + 1;
let mapping_ratio = switch_lines as f64 / total_lines as f64;
if mapping_ratio < self.config.min_mapping_ratio {
return None;
}
Some(MappingPatternResult {
is_pure_mapping: true,
confidence: 0.85,
mapping_ratio,
complexity_adjustment_factor: self.config.adjustment_factor,
pattern_description: format!("switch statement with {} cases", case_count),
})
}
fn detect_ifelse_chain_pattern(&self, body: &str) -> Option<MappingPatternResult> {
let lines: Vec<&str> = body.lines().collect();
let _total_lines = lines.len();
let elif_count = lines
.iter()
.filter(|line| line.trim().starts_with("elif "))
.count();
if elif_count < 2 {
return None;
}
let return_count = lines
.iter()
.filter(|line| line.trim().starts_with("return "))
.count();
let branch_count = elif_count + 1;
if return_count < branch_count {
return None;
}
let mapping_ratio = 0.9;
if mapping_ratio < self.config.min_mapping_ratio {
return None;
}
Some(MappingPatternResult {
is_pure_mapping: true,
confidence: 0.8,
mapping_ratio,
complexity_adjustment_factor: self.config.adjustment_factor,
pattern_description: format!("if-elif chain with {} branches", branch_count),
})
}
}
pub fn calculate_adjusted_complexity(
cyclomatic: u32,
cognitive: u32,
mapping_result: &MappingPatternResult,
) -> f64 {
if !mapping_result.is_pure_mapping {
return cyclomatic as f64;
}
let adjusted_cyclomatic = cyclomatic as f64 * mapping_result.complexity_adjustment_factor;
let cognitive_weight = 0.7;
adjusted_cyclomatic * (1.0 - cognitive_weight) + cognitive as f64 * cognitive_weight
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn detects_rust_enum_match_mapping() {
let code = r#"
fn format(val: MyEnum) -> &'static str {
match val {
MyEnum::A => "a",
MyEnum::B => "b",
MyEnum::C => "c",
MyEnum::D => "d",
MyEnum::E => "e",
}
}
"#;
let detector = MappingPatternDetector::new(MappingPatternConfig::default());
let result = detector.analyze_function(code, 15);
assert!(result.is_pure_mapping);
assert!(result.mapping_ratio > 0.8);
}
#[test]
fn rejects_complex_match_arms() {
let code = r#"
fn process(val: MyEnum) -> Result<String> {
match val {
MyEnum::A => {
if condition {
Ok("a".to_string())
} else {
Err("error")
}
},
MyEnum::B => Ok("b".to_string()),
}
}
"#;
let detector = MappingPatternDetector::new(MappingPatternConfig::default());
let result = detector.analyze_function(code, 8);
assert!(!result.is_pure_mapping);
}
#[test]
fn detects_nested_match_pattern() {
let code = r#"
fn format(outer: Outer, inner: Inner) -> String {
let label = match outer {
Outer::A => "A",
Outer::B => "B",
};
match inner {
Inner::X => label.green(),
Inner::Y => label.blue(),
}
}
"#;
let detector = MappingPatternDetector::new(MappingPatternConfig::default());
let result = detector.analyze_function(code, 15);
assert!(result.is_pure_mapping);
assert!(result.pattern_description.contains("nested"));
}
#[test]
fn applies_adjustment_factor_correctly() {
let mapping_result = MappingPatternResult {
is_pure_mapping: true,
confidence: 0.9,
mapping_ratio: 0.95,
complexity_adjustment_factor: 0.4,
pattern_description: "test".to_string(),
};
let adjusted = calculate_adjusted_complexity(15, 3, &mapping_result);
assert!((adjusted - 3.9).abs() < 0.1);
}
#[test]
fn skips_detection_for_low_complexity() {
let code = r#"
fn simple(val: MyEnum) -> &'static str {
match val {
MyEnum::A => "a",
MyEnum::B => "b",
}
}
"#;
let detector = MappingPatternDetector::new(MappingPatternConfig::default());
let result = detector.analyze_function(code, 3);
assert!(!result.is_pure_mapping);
}
#[test]
fn detects_switch_statement() {
let code = r#"
function format(val) {
switch (val) {
case 'A': return 'apple';
case 'B': return 'banana';
case 'C': return 'cherry';
case 'D': return 'date';
case 'E': return 'elderberry';
}
}
"#;
let detector = MappingPatternDetector::new(MappingPatternConfig::default());
let result = detector.analyze_function(code, 12);
assert!(result.is_pure_mapping);
assert!(result.pattern_description.contains("switch"));
}
#[test]
fn detects_python_ifelse_chain() {
let code = r#"
def format(val):
if val == 'A':
return 'apple'
elif val == 'B':
return 'banana'
elif val == 'C':
return 'cherry'
elif val == 'D':
return 'date'
else:
return 'unknown'
"#;
let detector = MappingPatternDetector::new(MappingPatternConfig::default());
let result = detector.analyze_function(code, 10);
assert!(result.is_pure_mapping);
assert!(result.pattern_description.contains("if-elif"));
}
}