Skip to main content

dissolve_python/
config.rs

1// Copyright (C) 2024 Jelmer Vernooij <jelmer@samba.org>
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//    http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Configuration management for dissolve
16
17use crate::domain_types::{ModuleName, Version};
18use crate::error::{DissolveError, Result};
19use crate::types::TypeIntrospectionMethod;
20use serde::{Deserialize, Serialize};
21use std::collections::HashMap;
22use std::path::{Path, PathBuf};
23
24/// Main configuration structure
25#[derive(Debug, Clone, Serialize, Deserialize)]
26pub struct Config {
27    /// Type introspection method to use
28    pub type_introspection: TypeIntrospectionMethod,
29
30    /// Paths to scan for deprecated functions
31    pub scan_paths: Vec<PathBuf>,
32
33    /// Modules to exclude from scanning
34    pub excluded_modules: Vec<ModuleName>,
35
36    /// Whether to write changes back to files
37    pub write_changes: bool,
38
39    /// Whether to create backup files
40    pub create_backups: bool,
41
42    /// Current version for version-based removal
43    pub current_version: Option<Version>,
44
45    /// Timeout settings
46    pub timeout: TimeoutConfig,
47
48    /// Performance settings
49    pub performance: PerformanceConfig,
50
51    /// Output settings
52    pub output: OutputConfig,
53}
54
55/// Timeout configuration
56#[derive(Debug, Clone, Serialize, Deserialize)]
57pub struct TimeoutConfig {
58    /// Timeout for LSP operations in seconds
59    pub lsp_timeout: u64,
60
61    /// Timeout for file operations in seconds
62    pub file_timeout: u64,
63
64    /// Timeout for type introspection queries in seconds
65    pub type_query_timeout: u64,
66}
67
68/// Performance configuration
69#[derive(Debug, Clone, Serialize, Deserialize)]
70pub struct PerformanceConfig {
71    /// Number of parallel workers for file processing
72    pub parallel_workers: usize,
73
74    /// Whether to cache parsed ASTs
75    pub cache_asts: bool,
76
77    /// Whether to use string interning
78    pub string_interning: bool,
79
80    /// Maximum files to process in a single batch
81    pub batch_size: usize,
82}
83
84/// Output configuration
85#[derive(Debug, Clone, Serialize, Deserialize)]
86pub struct OutputConfig {
87    /// Whether to show progress bars
88    pub show_progress: bool,
89
90    /// Verbosity level (0 = quiet, 1 = normal, 2 = verbose, 3 = debug)
91    pub verbosity: u8,
92
93    /// Whether to colorize output
94    pub colorize: bool,
95
96    /// Whether to show statistics at the end
97    pub show_stats: bool,
98}
99
100impl Default for Config {
101    fn default() -> Self {
102        Self {
103            type_introspection: TypeIntrospectionMethod::PyrightLsp,
104            scan_paths: vec![PathBuf::from(".")],
105            excluded_modules: vec![],
106            write_changes: false,
107            create_backups: true,
108            current_version: None,
109            timeout: TimeoutConfig::default(),
110            performance: PerformanceConfig::default(),
111            output: OutputConfig::default(),
112        }
113    }
114}
115
116impl Default for TimeoutConfig {
117    fn default() -> Self {
118        Self {
119            lsp_timeout: 30,
120            file_timeout: 10,
121            type_query_timeout: 5,
122        }
123    }
124}
125
126impl Default for PerformanceConfig {
127    fn default() -> Self {
128        Self {
129            parallel_workers: num_cpus::get(),
130            cache_asts: true,
131            string_interning: true,
132            batch_size: 100,
133        }
134    }
135}
136
137impl Default for OutputConfig {
138    fn default() -> Self {
139        Self {
140            show_progress: true,
141            verbosity: 1,
142            colorize: atty::is(atty::Stream::Stdout),
143            show_stats: true,
144        }
145    }
146}
147
148impl Config {
149    /// Create a new configuration with defaults
150    pub fn new() -> Self {
151        Self::default()
152    }
153
154    /// Load configuration from a file
155    pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
156        let content = std::fs::read_to_string(&path).map_err(|e| {
157            DissolveError::config_error(format!(
158                "Failed to read config file {}: {}",
159                path.as_ref().display(),
160                e
161            ))
162        })?;
163
164        let config: Config = if path.as_ref().extension().and_then(|s| s.to_str()) == Some("toml") {
165            toml::from_str(&content).map_err(|e| {
166                DissolveError::config_error(format!("Failed to parse TOML config: {}", e))
167            })?
168        } else {
169            serde_json::from_str(&content).map_err(|e| {
170                DissolveError::config_error(format!("Failed to parse JSON config: {}", e))
171            })?
172        };
173
174        Ok(config)
175    }
176
177    /// Save configuration to a file
178    pub fn save_to_file<P: AsRef<Path>>(&self, path: P) -> Result<()> {
179        let content = if path.as_ref().extension().and_then(|s| s.to_str()) == Some("toml") {
180            toml::to_string_pretty(self).map_err(|e| {
181                DissolveError::config_error(format!("Failed to serialize to TOML: {}", e))
182            })?
183        } else {
184            serde_json::to_string_pretty(self).map_err(|e| {
185                DissolveError::config_error(format!("Failed to serialize to JSON: {}", e))
186            })?
187        };
188
189        std::fs::write(&path, content).map_err(|e| {
190            DissolveError::config_error(format!(
191                "Failed to write config file {}: {}",
192                path.as_ref().display(),
193                e
194            ))
195        })?;
196
197        Ok(())
198    }
199
200    /// Load configuration from multiple sources with priority
201    /// 1. Command line arguments (highest priority)
202    /// 2. Environment variables
203    /// 3. Configuration file
204    /// 4. Defaults (lowest priority)
205    pub fn load_merged(
206        config_file: Option<&Path>,
207        env_overrides: &HashMap<String, String>,
208        cli_overrides: &CliOverrides,
209    ) -> Result<Self> {
210        let mut config = if let Some(config_path) = config_file {
211            if config_path.exists() {
212                Self::from_file(config_path)?
213            } else {
214                Self::default()
215            }
216        } else {
217            Self::default()
218        };
219
220        // Apply environment variable overrides
221        config.apply_env_overrides(env_overrides)?;
222
223        // Apply CLI overrides (highest priority)
224        config.apply_cli_overrides(cli_overrides);
225
226        Ok(config)
227    }
228
229    /// Apply environment variable overrides
230    fn apply_env_overrides(&mut self, env_vars: &HashMap<String, String>) -> Result<()> {
231        if let Some(timeout) = env_vars.get("DISSOLVE_LSP_TIMEOUT") {
232            self.timeout.lsp_timeout = timeout
233                .parse()
234                .map_err(|_| DissolveError::config_error("Invalid LSP timeout value"))?;
235        }
236
237        if let Some(workers) = env_vars.get("DISSOLVE_PARALLEL_WORKERS") {
238            self.performance.parallel_workers = workers
239                .parse()
240                .map_err(|_| DissolveError::config_error("Invalid parallel workers value"))?;
241        }
242
243        if let Some(verbosity) = env_vars.get("DISSOLVE_VERBOSITY") {
244            self.output.verbosity = verbosity
245                .parse()
246                .map_err(|_| DissolveError::config_error("Invalid verbosity value"))?;
247        }
248
249        Ok(())
250    }
251
252    /// Apply CLI overrides
253    fn apply_cli_overrides(&mut self, overrides: &CliOverrides) {
254        if let Some(type_method) = overrides.type_introspection {
255            self.type_introspection = type_method;
256        }
257
258        if let Some(write) = overrides.write_changes {
259            self.write_changes = write;
260        }
261
262        if let Some(verbosity) = overrides.verbosity {
263            self.output.verbosity = verbosity;
264        }
265
266        if let Some(no_color) = overrides.no_color {
267            self.output.colorize = !no_color;
268        }
269    }
270
271    /// Validate the configuration
272    pub fn validate(&self) -> Result<()> {
273        if self.timeout.lsp_timeout == 0 {
274            return Err(DissolveError::config_error(
275                "LSP timeout must be greater than 0",
276            ));
277        }
278
279        if self.performance.parallel_workers == 0 {
280            return Err(DissolveError::config_error(
281                "Parallel workers must be greater than 0",
282            ));
283        }
284
285        if self.output.verbosity > 3 {
286            return Err(DissolveError::config_error("Verbosity level must be 0-3"));
287        }
288
289        Ok(())
290    }
291}
292
293/// CLI overrides for configuration
294#[derive(Debug, Default)]
295pub struct CliOverrides {
296    pub type_introspection: Option<TypeIntrospectionMethod>,
297    pub write_changes: Option<bool>,
298    pub verbosity: Option<u8>,
299    pub no_color: Option<bool>,
300}
301
302/// Configuration builder for programmatic configuration
303pub struct ConfigBuilder {
304    config: Config,
305}
306
307impl ConfigBuilder {
308    pub fn new() -> Self {
309        Self {
310            config: Config::default(),
311        }
312    }
313
314    pub fn type_introspection(mut self, method: TypeIntrospectionMethod) -> Self {
315        self.config.type_introspection = method;
316        self
317    }
318
319    pub fn scan_paths(mut self, paths: Vec<PathBuf>) -> Self {
320        self.config.scan_paths = paths;
321        self
322    }
323
324    pub fn write_changes(mut self, write: bool) -> Self {
325        self.config.write_changes = write;
326        self
327    }
328
329    pub fn current_version(mut self, version: Version) -> Self {
330        self.config.current_version = Some(version);
331        self
332    }
333
334    pub fn parallel_workers(mut self, workers: usize) -> Self {
335        self.config.performance.parallel_workers = workers;
336        self
337    }
338
339    pub fn verbosity(mut self, level: u8) -> Self {
340        self.config.output.verbosity = level;
341        self
342    }
343
344    pub fn build(self) -> Result<Config> {
345        self.config.validate()?;
346        Ok(self.config)
347    }
348}
349
350impl Default for ConfigBuilder {
351    fn default() -> Self {
352        Self::new()
353    }
354}
355
356#[cfg(test)]
357mod tests {
358    use super::*;
359    use tempfile::TempDir;
360
361    #[test]
362    fn test_config_default() {
363        let config = Config::default();
364        assert_eq!(
365            config.type_introspection,
366            TypeIntrospectionMethod::PyrightLsp
367        );
368        assert!(!config.write_changes);
369        assert!(config.create_backups);
370    }
371
372    #[test]
373    fn test_config_builder() {
374        let config = ConfigBuilder::new()
375            .type_introspection(TypeIntrospectionMethod::MypyDaemon)
376            .write_changes(true)
377            .verbosity(2)
378            .build()
379            .unwrap();
380
381        assert_eq!(
382            config.type_introspection,
383            TypeIntrospectionMethod::MypyDaemon
384        );
385        assert!(config.write_changes);
386        assert_eq!(config.output.verbosity, 2);
387    }
388
389    #[test]
390    fn test_config_file_operations() {
391        let temp_dir = TempDir::new().unwrap();
392        let config_path = temp_dir.path().join("test_config.json");
393
394        let config = ConfigBuilder::new()
395            .verbosity(3)
396            .write_changes(true)
397            .build()
398            .unwrap();
399
400        config.save_to_file(&config_path).unwrap();
401
402        let loaded_config = Config::from_file(&config_path).unwrap();
403        assert_eq!(loaded_config.output.verbosity, 3);
404        assert!(loaded_config.write_changes);
405    }
406
407    #[test]
408    fn test_config_validation() {
409        let mut config = Config::default();
410        config.timeout.lsp_timeout = 0;
411
412        assert!(config.validate().is_err());
413    }
414}