ricecoder_generation/
code_validator.rs

1//! Code validation for generated code
2//!
3//! Validates generated code before writing by:
4//! - Checking syntax for target language
5//! - Running language-specific linters (clippy for Rust, eslint for TypeScript, etc.)
6//! - Running type checking (cargo check, tsc, mypy)
7//! - Reporting all errors with file paths and line numbers
8//! - Preventing writing if validation fails
9
10use crate::error::GenerationError;
11use crate::language_validators::get_validator;
12use crate::models::{
13    GeneratedFile, ValidationConfig, ValidationError, ValidationResult, ValidationWarning,
14};
15use tracing::{debug, warn};
16
17/// Validates generated code before writing
18#[derive(Debug, Clone)]
19pub struct CodeValidator {
20    /// Configuration for validation
21    config: ValidationConfig,
22}
23
24impl CodeValidator {
25    /// Creates a new CodeValidator with default configuration
26    pub fn new() -> Self {
27        Self {
28            config: ValidationConfig {
29                check_syntax: true,
30                run_linters: true,
31                run_type_checking: true,
32                warnings_as_errors: false,
33            },
34        }
35    }
36
37    /// Creates a new CodeValidator with custom configuration
38    pub fn with_config(config: ValidationConfig) -> Self {
39        Self { config }
40    }
41
42    /// Validates a collection of generated files
43    ///
44    /// # Arguments
45    /// * `files` - The generated files to validate
46    ///
47    /// # Returns
48    /// A ValidationResult containing all errors and warnings found
49    ///
50    /// # Errors
51    /// Returns `GenerationError` if validation process fails
52    pub fn validate(&self, files: &[GeneratedFile]) -> Result<ValidationResult, GenerationError> {
53        let mut all_errors = Vec::new();
54        let mut all_warnings = Vec::new();
55
56        for file in files {
57            debug!("Validating file: {}", file.path);
58
59            let result = self.validate_file(file)?;
60            all_errors.extend(result.errors);
61            all_warnings.extend(result.warnings);
62        }
63
64        let valid =
65            (all_warnings.is_empty() || !self.config.warnings_as_errors) && all_errors.is_empty();
66
67        Ok(ValidationResult {
68            valid,
69            errors: all_errors,
70            warnings: all_warnings,
71        })
72    }
73
74    /// Validates a single generated file
75    pub fn validate_file(&self, file: &GeneratedFile) -> Result<ValidationResult, GenerationError> {
76        let mut errors = Vec::new();
77        let mut warnings = Vec::new();
78
79        // Check syntax
80        if self.config.check_syntax {
81            match self.check_syntax(&file.content, &file.language, &file.path) {
82                Ok(syntax_errors) => errors.extend(syntax_errors),
83                Err(e) => {
84                    warn!("Syntax check failed for {}: {}", file.path, e);
85                }
86            }
87        }
88
89        // Run linters
90        if self.config.run_linters {
91            match self.run_linters(&file.content, &file.language, &file.path) {
92                Ok((lint_errors, lint_warnings)) => {
93                    errors.extend(lint_errors);
94                    warnings.extend(lint_warnings);
95                }
96                Err(e) => {
97                    warn!("Linting failed for {}: {}", file.path, e);
98                }
99            }
100        }
101
102        // Run type checking
103        if self.config.run_type_checking {
104            match self.run_type_checking(&file.content, &file.language, &file.path) {
105                Ok(type_errors) => errors.extend(type_errors),
106                Err(e) => {
107                    warn!("Type checking failed for {}: {}", file.path, e);
108                }
109            }
110        }
111
112        let valid = (warnings.is_empty() || !self.config.warnings_as_errors) && errors.is_empty();
113
114        Ok(ValidationResult {
115            valid,
116            errors,
117            warnings,
118        })
119    }
120
121    /// Checks syntax for the target language
122    fn check_syntax(
123        &self,
124        content: &str,
125        language: &str,
126        file_path: &str,
127    ) -> Result<Vec<ValidationError>, GenerationError> {
128        debug!("Checking syntax for {} file: {}", language, file_path);
129
130        match language {
131            "rust" => self.check_rust_syntax(content, file_path),
132            "typescript" | "ts" => self.check_typescript_syntax(content, file_path),
133            "python" | "py" => self.check_python_syntax(content, file_path),
134            "go" => self.check_go_syntax(content, file_path),
135            "java" => self.check_java_syntax(content, file_path),
136            _ => {
137                debug!("No syntax checker available for language: {}", language);
138                Ok(Vec::new())
139            }
140        }
141    }
142
143    /// Checks Rust syntax
144    fn check_rust_syntax(
145        &self,
146        content: &str,
147        file_path: &str,
148    ) -> Result<Vec<ValidationError>, GenerationError> {
149        // Basic syntax validation for Rust
150        let mut errors = Vec::new();
151
152        // Check for unmatched braces
153        let open_braces = content.matches('{').count();
154        let close_braces = content.matches('}').count();
155        if open_braces != close_braces {
156            errors.push(ValidationError {
157                file: file_path.to_string(),
158                line: 1,
159                column: 1,
160                message: format!(
161                    "Unmatched braces: {} open, {} close",
162                    open_braces, close_braces
163                ),
164                code: Some("E0001".to_string()),
165            });
166        }
167
168        // Check for unmatched parentheses
169        let open_parens = content.matches('(').count();
170        let close_parens = content.matches(')').count();
171        if open_parens != close_parens {
172            errors.push(ValidationError {
173                file: file_path.to_string(),
174                line: 1,
175                column: 1,
176                message: format!(
177                    "Unmatched parentheses: {} open, {} close",
178                    open_parens, close_parens
179                ),
180                code: Some("E0002".to_string()),
181            });
182        }
183
184        // Check for unmatched brackets
185        let open_brackets = content.matches('[').count();
186        let close_brackets = content.matches(']').count();
187        if open_brackets != close_brackets {
188            errors.push(ValidationError {
189                file: file_path.to_string(),
190                line: 1,
191                column: 1,
192                message: format!(
193                    "Unmatched brackets: {} open, {} close",
194                    open_brackets, close_brackets
195                ),
196                code: Some("E0003".to_string()),
197            });
198        }
199
200        Ok(errors)
201    }
202
203    /// Checks TypeScript syntax
204    fn check_typescript_syntax(
205        &self,
206        content: &str,
207        file_path: &str,
208    ) -> Result<Vec<ValidationError>, GenerationError> {
209        // Basic syntax validation for TypeScript
210        let mut errors = Vec::new();
211
212        // Check for unmatched braces
213        let open_braces = content.matches('{').count();
214        let close_braces = content.matches('}').count();
215        if open_braces != close_braces {
216            errors.push(ValidationError {
217                file: file_path.to_string(),
218                line: 1,
219                column: 1,
220                message: format!(
221                    "Unmatched braces: {} open, {} close",
222                    open_braces, close_braces
223                ),
224                code: Some("TS1005".to_string()),
225            });
226        }
227
228        // Check for unmatched parentheses
229        let open_parens = content.matches('(').count();
230        let close_parens = content.matches(')').count();
231        if open_parens != close_parens {
232            errors.push(ValidationError {
233                file: file_path.to_string(),
234                line: 1,
235                column: 1,
236                message: format!(
237                    "Unmatched parentheses: {} open, {} close",
238                    open_parens, close_parens
239                ),
240                code: Some("TS1005".to_string()),
241            });
242        }
243
244        Ok(errors)
245    }
246
247    /// Checks Python syntax
248    fn check_python_syntax(
249        &self,
250        content: &str,
251        file_path: &str,
252    ) -> Result<Vec<ValidationError>, GenerationError> {
253        // Basic syntax validation for Python
254        let mut errors = Vec::new();
255
256        // Check for unmatched braces
257        let open_braces = content.matches('{').count();
258        let close_braces = content.matches('}').count();
259        if open_braces != close_braces {
260            errors.push(ValidationError {
261                file: file_path.to_string(),
262                line: 1,
263                column: 1,
264                message: format!(
265                    "Unmatched braces: {} open, {} close",
266                    open_braces, close_braces
267                ),
268                code: Some("E0001".to_string()),
269            });
270        }
271
272        // Check for unmatched parentheses
273        let open_parens = content.matches('(').count();
274        let close_parens = content.matches(')').count();
275        if open_parens != close_parens {
276            errors.push(ValidationError {
277                file: file_path.to_string(),
278                line: 1,
279                column: 1,
280                message: format!(
281                    "Unmatched parentheses: {} open, {} close",
282                    open_parens, close_parens
283                ),
284                code: Some("E0001".to_string()),
285            });
286        }
287
288        Ok(errors)
289    }
290
291    /// Checks Go syntax
292    fn check_go_syntax(
293        &self,
294        content: &str,
295        file_path: &str,
296    ) -> Result<Vec<ValidationError>, GenerationError> {
297        // Basic syntax validation for Go
298        let mut errors = Vec::new();
299
300        // Check for unmatched braces
301        let open_braces = content.matches('{').count();
302        let close_braces = content.matches('}').count();
303        if open_braces != close_braces {
304            errors.push(ValidationError {
305                file: file_path.to_string(),
306                line: 1,
307                column: 1,
308                message: format!(
309                    "Unmatched braces: {} open, {} close",
310                    open_braces, close_braces
311                ),
312                code: Some("E0001".to_string()),
313            });
314        }
315
316        Ok(errors)
317    }
318
319    /// Checks Java syntax
320    fn check_java_syntax(
321        &self,
322        content: &str,
323        file_path: &str,
324    ) -> Result<Vec<ValidationError>, GenerationError> {
325        // Basic syntax validation for Java
326        let mut errors = Vec::new();
327
328        // Check for unmatched braces
329        let open_braces = content.matches('{').count();
330        let close_braces = content.matches('}').count();
331        if open_braces != close_braces {
332            errors.push(ValidationError {
333                file: file_path.to_string(),
334                line: 1,
335                column: 1,
336                message: format!(
337                    "Unmatched braces: {} open, {} close",
338                    open_braces, close_braces
339                ),
340                code: Some("E0001".to_string()),
341            });
342        }
343
344        Ok(errors)
345    }
346
347    /// Runs language-specific linters
348    fn run_linters(
349        &self,
350        content: &str,
351        language: &str,
352        file_path: &str,
353    ) -> Result<(Vec<ValidationError>, Vec<ValidationWarning>), GenerationError> {
354        debug!("Running linters for {} file: {}", language, file_path);
355
356        // Try to get a language-specific validator
357        if let Some(validator) = get_validator(language) {
358            match validator.validate(content, file_path) {
359                Ok((errors, warnings)) => {
360                    debug!(
361                        "Language-specific validation found {} errors and {} warnings",
362                        errors.len(),
363                        warnings.len()
364                    );
365                    Ok((errors, warnings))
366                }
367                Err(e) => {
368                    warn!("Language-specific validation failed: {}", e);
369                    Ok((Vec::new(), Vec::new()))
370                }
371            }
372        } else {
373            debug!("No language-specific validator available for: {}", language);
374            Ok((Vec::new(), Vec::new()))
375        }
376    }
377
378    /// Runs type checking for the target language
379    fn run_type_checking(
380        &self,
381        content: &str,
382        language: &str,
383        file_path: &str,
384    ) -> Result<Vec<ValidationError>, GenerationError> {
385        debug!("Running type checking for {} file: {}", language, file_path);
386
387        match language {
388            "rust" => self.run_rust_type_checking(content, file_path),
389            "typescript" | "ts" => self.run_typescript_type_checking(content, file_path),
390            "python" | "py" => self.run_python_type_checking(content, file_path),
391            "go" => self.run_go_type_checking(content, file_path),
392            "java" => self.run_java_type_checking(content, file_path),
393            _ => {
394                debug!("No type checker available for language: {}", language);
395                Ok(Vec::new())
396            }
397        }
398    }
399
400    /// Runs Rust type checking (cargo check)
401    fn run_rust_type_checking(
402        &self,
403        _content: &str,
404        _file_path: &str,
405    ) -> Result<Vec<ValidationError>, GenerationError> {
406        // In a real implementation, we would:
407        // 1. Write content to a temporary file
408        // 2. Run `cargo check` on it
409        // 3. Parse the output
410        // 4. Return errors
411
412        debug!("Rust type checking would be performed by cargo check");
413        Ok(Vec::new())
414    }
415
416    /// Runs TypeScript type checking (tsc)
417    fn run_typescript_type_checking(
418        &self,
419        _content: &str,
420        _file_path: &str,
421    ) -> Result<Vec<ValidationError>, GenerationError> {
422        // In a real implementation, we would:
423        // 1. Write content to a temporary file
424        // 2. Run `tsc` on it
425        // 3. Parse the output
426        // 4. Return errors
427
428        debug!("TypeScript type checking would be performed by tsc");
429        Ok(Vec::new())
430    }
431
432    /// Runs Python type checking (mypy)
433    fn run_python_type_checking(
434        &self,
435        _content: &str,
436        _file_path: &str,
437    ) -> Result<Vec<ValidationError>, GenerationError> {
438        // In a real implementation, we would:
439        // 1. Write content to a temporary file
440        // 2. Run `mypy` on it
441        // 3. Parse the output
442        // 4. Return errors
443
444        debug!("Python type checking would be performed by mypy");
445        Ok(Vec::new())
446    }
447
448    /// Runs Go type checking (go vet)
449    fn run_go_type_checking(
450        &self,
451        _content: &str,
452        _file_path: &str,
453    ) -> Result<Vec<ValidationError>, GenerationError> {
454        // In a real implementation, we would:
455        // 1. Write content to a temporary file
456        // 2. Run `go vet` on it
457        // 3. Parse the output
458        // 4. Return errors
459
460        debug!("Go type checking would be performed by go vet");
461        Ok(Vec::new())
462    }
463
464    /// Runs Java type checking (javac)
465    fn run_java_type_checking(
466        &self,
467        _content: &str,
468        _file_path: &str,
469    ) -> Result<Vec<ValidationError>, GenerationError> {
470        // In a real implementation, we would:
471        // 1. Write content to a temporary file
472        // 2. Run `javac` on it
473        // 3. Parse the output
474        // 4. Return errors
475
476        debug!("Java type checking would be performed by javac");
477        Ok(Vec::new())
478    }
479
480    /// Checks if validation passed
481    pub fn is_valid(&self, result: &ValidationResult) -> bool {
482        result.valid
483    }
484
485    /// Gets all validation issues (errors and warnings)
486    pub fn get_all_issues(&self, result: &ValidationResult) -> Vec<String> {
487        let mut issues = Vec::new();
488
489        for error in &result.errors {
490            issues.push(format!(
491                "ERROR: {}:{}:{} - {} ({})",
492                error.file,
493                error.line,
494                error.column,
495                error.message,
496                error.code.as_deref().unwrap_or("unknown")
497            ));
498        }
499
500        for warning in &result.warnings {
501            issues.push(format!(
502                "WARNING: {}:{}:{} - {} ({})",
503                warning.file,
504                warning.line,
505                warning.column,
506                warning.message,
507                warning.code.as_deref().unwrap_or("unknown")
508            ));
509        }
510
511        issues
512    }
513}
514
515impl Default for CodeValidator {
516    fn default() -> Self {
517        Self::new()
518    }
519}
520
521#[cfg(test)]
522mod tests {
523    use super::*;
524
525    #[test]
526    fn test_check_rust_syntax_valid() {
527        let validator = CodeValidator::new();
528        let content = "fn main() { println!(\"Hello\"); }";
529        let errors = validator.check_rust_syntax(content, "main.rs").unwrap();
530        assert!(errors.is_empty());
531    }
532
533    #[test]
534    fn test_check_rust_syntax_unmatched_braces() {
535        let validator = CodeValidator::new();
536        let content = "fn main() { println!(\"Hello\"); ";
537        let errors = validator.check_rust_syntax(content, "main.rs").unwrap();
538        assert!(!errors.is_empty());
539        assert!(errors[0].message.contains("Unmatched braces"));
540    }
541
542    #[test]
543    fn test_check_typescript_syntax_valid() {
544        let validator = CodeValidator::new();
545        let content = "function hello() { console.log(\"Hello\"); }";
546        let errors = validator
547            .check_typescript_syntax(content, "main.ts")
548            .unwrap();
549        assert!(errors.is_empty());
550    }
551
552    #[test]
553    fn test_check_typescript_syntax_unmatched_parens() {
554        let validator = CodeValidator::new();
555        let content = "function hello( { console.log(\"Hello\"); }";
556        let errors = validator
557            .check_typescript_syntax(content, "main.ts")
558            .unwrap();
559        assert!(!errors.is_empty());
560        assert!(errors[0].message.contains("Unmatched parentheses"));
561    }
562
563    #[test]
564    fn test_check_python_syntax_valid() {
565        let validator = CodeValidator::new();
566        let content = "def hello():\n    print(\"Hello\")";
567        let errors = validator.check_python_syntax(content, "main.py").unwrap();
568        assert!(errors.is_empty());
569    }
570
571    #[test]
572    fn test_validate_file_rust() {
573        let validator = CodeValidator::new();
574        let file = GeneratedFile {
575            path: "src/main.rs".to_string(),
576            content: "fn main() { println!(\"Hello\"); }".to_string(),
577            language: "rust".to_string(),
578        };
579
580        let result = validator.validate_file(&file).unwrap();
581        assert!(result.valid);
582        assert!(result.errors.is_empty());
583    }
584
585    #[test]
586    fn test_validate_file_with_errors() {
587        let validator = CodeValidator::new();
588        let file = GeneratedFile {
589            path: "src/main.rs".to_string(),
590            content: "fn main() { println!(\"Hello\"); ".to_string(),
591            language: "rust".to_string(),
592        };
593
594        let result = validator.validate_file(&file).unwrap();
595        assert!(!result.valid);
596        assert!(!result.errors.is_empty());
597    }
598
599    #[test]
600    fn test_validate_multiple_files() {
601        let validator = CodeValidator::new();
602        let files = vec![
603            GeneratedFile {
604                path: "src/main.rs".to_string(),
605                content: "fn main() { println!(\"Hello\"); }".to_string(),
606                language: "rust".to_string(),
607            },
608            GeneratedFile {
609                path: "src/lib.rs".to_string(),
610                content: "pub fn hello() { println!(\"Hello\"); }".to_string(),
611                language: "rust".to_string(),
612            },
613        ];
614
615        let result = validator.validate(&files).unwrap();
616        assert!(result.valid);
617        assert!(result.errors.is_empty());
618    }
619
620    #[test]
621    fn test_get_all_issues() {
622        let validator = CodeValidator::new();
623        let result = ValidationResult {
624            valid: false,
625            errors: vec![ValidationError {
626                file: "main.rs".to_string(),
627                line: 1,
628                column: 1,
629                message: "Unmatched braces".to_string(),
630                code: Some("E0001".to_string()),
631            }],
632            warnings: vec![ValidationWarning {
633                file: "main.rs".to_string(),
634                line: 2,
635                column: 1,
636                message: "Unused variable".to_string(),
637                code: Some("W0001".to_string()),
638            }],
639        };
640
641        let issues = validator.get_all_issues(&result);
642        assert_eq!(issues.len(), 2);
643        assert!(issues[0].contains("ERROR"));
644        assert!(issues[1].contains("WARNING"));
645    }
646
647    #[test]
648    fn test_validation_config_default() {
649        let config = ValidationConfig {
650            check_syntax: true,
651            run_linters: true,
652            run_type_checking: true,
653            warnings_as_errors: false,
654        };
655
656        assert!(config.check_syntax);
657        assert!(config.run_linters);
658        assert!(config.run_type_checking);
659        assert!(!config.warnings_as_errors);
660    }
661
662    #[test]
663    fn test_warnings_as_errors() {
664        let config = ValidationConfig {
665            check_syntax: true,
666            run_linters: false,
667            run_type_checking: false,
668            warnings_as_errors: true,
669        };
670
671        let validator = CodeValidator::with_config(config);
672        // When warnings_as_errors is true, a result with warnings should be invalid
673        let result = ValidationResult {
674            valid: false, // This should be false when warnings_as_errors is true and there are warnings
675            errors: Vec::new(),
676            warnings: vec![ValidationWarning {
677                file: "main.rs".to_string(),
678                line: 1,
679                column: 1,
680                message: "Warning".to_string(),
681                code: None,
682            }],
683        };
684
685        assert!(!validator.is_valid(&result));
686    }
687}