code complexity & repetition analysis tool
at main 185 lines 5.3 kB view raw
1use crate::error::{MccabreError, Result}; 2use serde::{Deserialize, Serialize}; 3use std::fs; 4use std::path::Path; 5 6/// Configuration for mccabre analysis 7#[derive(Debug, Clone, Serialize, Deserialize, Default)] 8pub struct Config { 9 /// Cyclomatic complexity thresholds 10 #[serde(default)] 11 pub complexity: ComplexityConfig, 12 13 /// Clone detection settings 14 #[serde(default)] 15 pub clones: CloneConfig, 16 17 /// File filtering settings 18 #[serde(default)] 19 pub files: FileConfig, 20} 21 22#[derive(Debug, Clone, Serialize, Deserialize)] 23pub struct ComplexityConfig { 24 /// Threshold for warning level (default: 10) 25 #[serde(default = "default_warning_threshold")] 26 pub warning_threshold: usize, 27 28 /// Threshold for error level (default: 20) 29 #[serde(default = "default_error_threshold")] 30 pub error_threshold: usize, 31} 32 33#[derive(Debug, Clone, Serialize, Deserialize)] 34pub struct CloneConfig { 35 /// Minimum number of tokens for clone detection (default: 30) 36 #[serde(default = "default_min_tokens")] 37 pub min_tokens: usize, 38 39 /// Whether to enable clone detection (default: true) 40 #[serde(default = "default_true")] 41 pub enabled: bool, 42} 43 44#[derive(Debug, Clone, Serialize, Deserialize)] 45pub struct FileConfig { 46 /// Whether to respect .gitignore (default: true) 47 #[serde(default = "default_true")] 48 pub respect_gitignore: bool, 49} 50 51impl Default for ComplexityConfig { 52 fn default() -> Self { 53 Self { warning_threshold: default_warning_threshold(), error_threshold: default_error_threshold() } 54 } 55} 56 57impl Default for CloneConfig { 58 fn default() -> Self { 59 Self { min_tokens: default_min_tokens(), enabled: default_true() } 60 } 61} 62 63impl Default for FileConfig { 64 fn default() -> Self { 65 Self { respect_gitignore: default_true() } 66 } 67} 68 69fn default_warning_threshold() -> usize { 70 10 71} 72 73fn default_error_threshold() -> usize { 74 20 75} 76 77fn default_min_tokens() -> usize { 78 30 79} 80 81fn default_true() -> bool { 82 true 83} 84 85impl Config { 86 /// Load configuration from a TOML file 87 pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> { 88 let content = fs::read_to_string(path.as_ref()) 89 .map_err(|e| MccabreError::FileRead { path: path.as_ref().to_path_buf(), source: e })?; 90 91 toml::from_str(&content).map_err(|e| MccabreError::InvalidConfig(e.to_string())) 92 } 93 94 /// Try to load configuration from default locations 95 /// Looks for: mccabre.toml, .mccabre.toml, .mccabre/config.toml 96 pub fn load_default() -> Result<Self> { 97 let candidates = vec!["mccabre.toml", ".mccabre.toml", ".mccabre/config.toml"]; 98 99 for path in candidates { 100 if Path::new(path).exists() { 101 return Self::from_file(path); 102 } 103 } 104 105 Ok(Self::default()) 106 } 107 108 /// Save configuration to a TOML file 109 pub fn save<P: AsRef<Path>>(&self, path: P) -> Result<()> { 110 let content = toml::to_string_pretty(self).map_err(|e| MccabreError::InvalidConfig(e.to_string()))?; 111 112 fs::write(path.as_ref(), content) 113 .map_err(|e| MccabreError::FileRead { path: path.as_ref().to_path_buf(), source: e })?; 114 115 Ok(()) 116 } 117 118 /// Merge with CLI overrides 119 pub fn merge_with_cli( 120 mut self, complexity_threshold: Option<usize>, min_tokens: Option<usize>, respect_gitignore: Option<bool>, 121 ) -> Self { 122 if let Some(threshold) = complexity_threshold { 123 self.complexity.warning_threshold = threshold; 124 } 125 126 if let Some(min) = min_tokens { 127 self.clones.min_tokens = min; 128 } 129 130 if let Some(respect) = respect_gitignore { 131 self.files.respect_gitignore = respect; 132 } 133 134 self 135 } 136} 137 138#[cfg(test)] 139mod tests { 140 use super::*; 141 use tempfile::TempDir; 142 143 #[test] 144 fn test_default_config() { 145 let config = Config::default(); 146 assert_eq!(config.complexity.warning_threshold, 10); 147 assert_eq!(config.complexity.error_threshold, 20); 148 assert_eq!(config.clones.min_tokens, 30); 149 assert!(config.clones.enabled); 150 assert!(config.files.respect_gitignore); 151 } 152 153 #[test] 154 fn test_save_and_load() { 155 let temp_dir = TempDir::new().unwrap(); 156 let config_path = temp_dir.path().join("test_config.toml"); 157 158 let config = Config::default(); 159 config.save(&config_path).unwrap(); 160 161 let loaded = Config::from_file(&config_path).unwrap(); 162 assert_eq!(loaded.complexity.warning_threshold, config.complexity.warning_threshold); 163 assert_eq!(loaded.clones.min_tokens, config.clones.min_tokens); 164 } 165 166 #[test] 167 fn test_merge_with_cli() { 168 let mut config = Config::default(); 169 config = config.merge_with_cli(Some(15), Some(40), Some(false)); 170 171 assert_eq!(config.complexity.warning_threshold, 15); 172 assert_eq!(config.clones.min_tokens, 40); 173 assert!(!config.files.respect_gitignore); 174 } 175 176 #[test] 177 fn test_partial_cli_override() { 178 let mut config = Config::default(); 179 config = config.merge_with_cli(Some(25), None, None); 180 181 assert_eq!(config.complexity.warning_threshold, 25); 182 assert_eq!(config.clones.min_tokens, 30); 183 assert!(config.files.respect_gitignore); 184 } 185}