Skip to main content

rez_lsp_server/validation/
validation_engine.rs

1//! Validation engine that coordinates multiple validators.
2
3use super::{PythonValidator, RezValidator, ValidationIssue, ValidationResult, Validator};
4use crate::core::Result;
5use std::sync::Arc;
6use std::time::Instant;
7
8/// Configuration for the validation engine.
9#[derive(Debug, Clone)]
10pub struct ValidationConfig {
11    /// Whether to enable Python syntax validation
12    pub enable_python_validation: bool,
13    /// Whether to enable Rez-specific validation
14    pub enable_rez_validation: bool,
15    /// Maximum number of issues to report per file
16    pub max_issues_per_file: usize,
17    /// Whether to include style warnings
18    pub include_style_warnings: bool,
19    /// Whether to include informational messages
20    pub include_info_messages: bool,
21}
22
23impl Default for ValidationConfig {
24    fn default() -> Self {
25        Self {
26            enable_python_validation: true,
27            enable_rez_validation: true,
28            max_issues_per_file: 100,
29            include_style_warnings: true,
30            include_info_messages: false,
31        }
32    }
33}
34
35/// Main validation engine that coordinates multiple validators.
36pub struct ValidationEngine {
37    /// Configuration for validation
38    config: ValidationConfig,
39    /// Python syntax validator
40    python_validator: Option<Arc<PythonValidator>>,
41    /// Rez-specific validator
42    rez_validator: Option<Arc<RezValidator>>,
43}
44
45impl ValidationEngine {
46    /// Create a new validation engine with default configuration.
47    pub fn new() -> Result<Self> {
48        Self::with_config(ValidationConfig::default())
49    }
50
51    /// Create a new validation engine with custom configuration.
52    pub fn with_config(config: ValidationConfig) -> Result<Self> {
53        let python_validator = if config.enable_python_validation {
54            Some(Arc::new(PythonValidator::new()?))
55        } else {
56            None
57        };
58
59        let rez_validator = if config.enable_rez_validation {
60            Some(Arc::new(RezValidator::new()?))
61        } else {
62            None
63        };
64
65        Ok(Self {
66            config,
67            python_validator,
68            rez_validator,
69        })
70    }
71
72    /// Validate a package.py file and return all issues found.
73    pub fn validate_file(&self, content: &str, file_path: &str) -> Result<ValidationResult> {
74        let start_time = Instant::now();
75        let mut all_issues = Vec::new();
76
77        // Run Python validation if enabled
78        if let Some(validator) = &self.python_validator {
79            match validator.validate(content, file_path) {
80                Ok(mut issues) => {
81                    all_issues.append(&mut issues);
82                }
83                Err(e) => {
84                    // Log error but continue with other validators
85                    eprintln!("Python validation failed: {}", e);
86                }
87            }
88        }
89
90        // Run Rez validation if enabled
91        if let Some(validator) = &self.rez_validator {
92            match validator.validate(content, file_path) {
93                Ok(mut issues) => {
94                    all_issues.append(&mut issues);
95                }
96                Err(e) => {
97                    // Log error but continue
98                    eprintln!("Rez validation failed: {}", e);
99                }
100            }
101        }
102
103        // Filter issues based on configuration
104        all_issues = self.filter_issues(all_issues);
105
106        // Sort issues by severity (critical first), then by line number
107        all_issues.sort_by(|a, b| {
108            b.severity
109                .cmp(&a.severity)
110                .then_with(|| a.line.cmp(&b.line))
111                .then_with(|| a.column.cmp(&b.column))
112        });
113
114        // Limit number of issues if configured
115        if all_issues.len() > self.config.max_issues_per_file {
116            all_issues.truncate(self.config.max_issues_per_file);
117
118            // Add a warning about truncation
119            all_issues.push(
120                ValidationIssue::new(
121                    super::Severity::Warning,
122                    1,
123                    1,
124                    1,
125                    format!(
126                        "Too many issues found. Showing first {} issues.",
127                        self.config.max_issues_per_file
128                    ),
129                    "V001",
130                )
131                .with_suggestion("Fix the most critical issues first"),
132            );
133        }
134
135        let validation_time = start_time.elapsed().as_millis() as u64;
136        Ok(ValidationResult::new(
137            file_path,
138            all_issues,
139            validation_time,
140        ))
141    }
142
143    /// Validate multiple files concurrently.
144    pub fn validate_files(&self, files: &[(String, String)]) -> Result<Vec<ValidationResult>> {
145        let mut results = Vec::new();
146
147        for (file_path, content) in files {
148            match self.validate_file(content, file_path) {
149                Ok(result) => results.push(result),
150                Err(e) => {
151                    eprintln!("Failed to validate {}: {}", file_path, e);
152                    // Create an error result
153                    let error_issue = ValidationIssue::new(
154                        super::Severity::Critical,
155                        1,
156                        1,
157                        1,
158                        format!("Validation failed: {}", e),
159                        "V999",
160                    );
161                    results.push(ValidationResult::new(file_path, vec![error_issue], 0));
162                }
163            }
164        }
165
166        Ok(results)
167    }
168
169    /// Filter issues based on configuration.
170    fn filter_issues(&self, issues: Vec<ValidationIssue>) -> Vec<ValidationIssue> {
171        issues
172            .into_iter()
173            .filter(|issue| match issue.severity {
174                super::Severity::Info => self.config.include_info_messages,
175                super::Severity::Warning => self.config.include_style_warnings,
176                super::Severity::Error | super::Severity::Critical => true,
177            })
178            .collect()
179    }
180
181    /// Get validation statistics for a set of results.
182    pub fn get_summary_stats(&self, results: &[ValidationResult]) -> ValidationSummary {
183        let total_files = results.len();
184        let mut files_with_errors = 0;
185        let mut files_with_warnings = 0;
186        let mut total_issues = 0;
187        let mut total_critical = 0;
188        let mut total_errors = 0;
189        let mut total_warnings = 0;
190        let mut total_info = 0;
191        let mut total_validation_time = 0;
192
193        for result in results {
194            total_issues += result.stats.total_issues;
195            total_critical += result.stats.critical_count;
196            total_errors += result.stats.error_count;
197            total_warnings += result.stats.warning_count;
198            total_info += result.stats.info_count;
199            total_validation_time += result.stats.validation_time_ms;
200
201            if result.has_errors() {
202                files_with_errors += 1;
203            } else if result.stats.warning_count > 0 {
204                files_with_warnings += 1;
205            }
206        }
207
208        ValidationSummary {
209            total_files,
210            files_with_errors,
211            files_with_warnings,
212            total_issues,
213            total_critical,
214            total_errors,
215            total_warnings,
216            total_info,
217            total_validation_time_ms: total_validation_time,
218        }
219    }
220
221    /// Update the validation configuration.
222    pub fn update_config(&mut self, config: ValidationConfig) -> Result<()> {
223        // Recreate validators if needed
224        if config.enable_python_validation && self.python_validator.is_none() {
225            self.python_validator = Some(Arc::new(PythonValidator::new()?));
226        } else if !config.enable_python_validation {
227            self.python_validator = None;
228        }
229
230        if config.enable_rez_validation && self.rez_validator.is_none() {
231            self.rez_validator = Some(Arc::new(RezValidator::new()?));
232        } else if !config.enable_rez_validation {
233            self.rez_validator = None;
234        }
235
236        self.config = config;
237        Ok(())
238    }
239
240    /// Get the current configuration.
241    pub fn config(&self) -> &ValidationConfig {
242        &self.config
243    }
244}
245
246/// Summary statistics for validation results.
247#[derive(Debug, Clone)]
248pub struct ValidationSummary {
249    /// Total number of files validated
250    pub total_files: usize,
251    /// Number of files with errors
252    pub files_with_errors: usize,
253    /// Number of files with warnings (but no errors)
254    pub files_with_warnings: usize,
255    /// Total number of issues found
256    pub total_issues: usize,
257    /// Total critical issues
258    pub total_critical: usize,
259    /// Total errors
260    pub total_errors: usize,
261    /// Total warnings
262    pub total_warnings: usize,
263    /// Total info messages
264    pub total_info: usize,
265    /// Total validation time in milliseconds
266    pub total_validation_time_ms: u64,
267}
268
269impl ValidationSummary {
270    /// Check if any critical issues or errors were found.
271    pub fn has_errors(&self) -> bool {
272        self.total_critical > 0 || self.total_errors > 0
273    }
274
275    /// Check if any issues were found.
276    pub fn has_issues(&self) -> bool {
277        self.total_issues > 0
278    }
279
280    /// Get the average validation time per file.
281    pub fn average_validation_time_ms(&self) -> f64 {
282        if self.total_files > 0 {
283            self.total_validation_time_ms as f64 / self.total_files as f64
284        } else {
285            0.0
286        }
287    }
288}
289
290impl Default for ValidationEngine {
291    fn default() -> Self {
292        Self::new().expect("Failed to create ValidationEngine")
293    }
294}
295
296#[cfg(test)]
297mod tests {
298    use super::*;
299
300    #[test]
301    fn test_validation_engine_creation() {
302        let engine = ValidationEngine::new();
303        assert!(engine.is_ok());
304    }
305
306    #[test]
307    fn test_validation_engine_with_config() {
308        let config = ValidationConfig {
309            enable_python_validation: false,
310            enable_rez_validation: true,
311            ..Default::default()
312        };
313
314        let engine = ValidationEngine::with_config(config);
315        assert!(engine.is_ok());
316
317        let engine = engine.unwrap();
318        assert!(engine.python_validator.is_none());
319        assert!(engine.rez_validator.is_some());
320    }
321
322    #[test]
323    fn test_file_validation() {
324        let engine = ValidationEngine::new().unwrap();
325        let content = r#"
326name = "test"
327version = "1.0.0"
328description = "Test package"
329"#;
330
331        let result = engine.validate_file(content, "test.py");
332        assert!(result.is_ok());
333
334        let result = result.unwrap();
335        assert_eq!(result.file_path, "test.py");
336        assert!(result.stats.validation_time_ms > 0);
337    }
338
339    #[test]
340    fn test_multiple_file_validation() {
341        let engine = ValidationEngine::new().unwrap();
342        let files = vec![
343            (
344                "test1.py".to_string(),
345                "name = \"test1\"\nversion = \"1.0.0\"".to_string(),
346            ),
347            (
348                "test2.py".to_string(),
349                "name = \"test2\"\nversion = \"2.0.0\"".to_string(),
350            ),
351        ];
352
353        let results = engine.validate_files(&files);
354        assert!(results.is_ok());
355
356        let results = results.unwrap();
357        assert_eq!(results.len(), 2);
358    }
359
360    #[test]
361    fn test_validation_summary() {
362        let engine = ValidationEngine::new().unwrap();
363        let files = vec![
364            (
365                "test1.py".to_string(),
366                "name = \"test1\"\nversion = \"1.0.0\"".to_string(),
367            ),
368            ("test2.py".to_string(), "invalid syntax here".to_string()),
369        ];
370
371        let results = engine.validate_files(&files).unwrap();
372        let summary = engine.get_summary_stats(&results);
373
374        assert_eq!(summary.total_files, 2);
375        assert!(summary.total_validation_time_ms > 0);
376    }
377}