code complexity & repetition analysis tool
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}