rez_lsp_server/validation/
validation_engine.rs1use super::{PythonValidator, RezValidator, ValidationIssue, ValidationResult, Validator};
4use crate::core::Result;
5use std::sync::Arc;
6use std::time::Instant;
7
8#[derive(Debug, Clone)]
10pub struct ValidationConfig {
11 pub enable_python_validation: bool,
13 pub enable_rez_validation: bool,
15 pub max_issues_per_file: usize,
17 pub include_style_warnings: bool,
19 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
35pub struct ValidationEngine {
37 config: ValidationConfig,
39 python_validator: Option<Arc<PythonValidator>>,
41 rez_validator: Option<Arc<RezValidator>>,
43}
44
45impl ValidationEngine {
46 pub fn new() -> Result<Self> {
48 Self::with_config(ValidationConfig::default())
49 }
50
51 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 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 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 eprintln!("Python validation failed: {}", e);
86 }
87 }
88 }
89
90 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 eprintln!("Rez validation failed: {}", e);
99 }
100 }
101 }
102
103 all_issues = self.filter_issues(all_issues);
105
106 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 if all_issues.len() > self.config.max_issues_per_file {
116 all_issues.truncate(self.config.max_issues_per_file);
117
118 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 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 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 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 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 pub fn update_config(&mut self, config: ValidationConfig) -> Result<()> {
223 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 pub fn config(&self) -> &ValidationConfig {
242 &self.config
243 }
244}
245
246#[derive(Debug, Clone)]
248pub struct ValidationSummary {
249 pub total_files: usize,
251 pub files_with_errors: usize,
253 pub files_with_warnings: usize,
255 pub total_issues: usize,
257 pub total_critical: usize,
259 pub total_errors: usize,
261 pub total_warnings: usize,
263 pub total_info: usize,
265 pub total_validation_time_ms: u64,
267}
268
269impl ValidationSummary {
270 pub fn has_errors(&self) -> bool {
272 self.total_critical > 0 || self.total_errors > 0
273 }
274
275 pub fn has_issues(&self) -> bool {
277 self.total_issues > 0
278 }
279
280 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}