1use 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#[derive(Debug, Clone)]
19pub struct CodeValidator {
20 config: ValidationConfig,
22}
23
24impl CodeValidator {
25 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 pub fn with_config(config: ValidationConfig) -> Self {
39 Self { config }
40 }
41
42 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 pub fn validate_file(&self, file: &GeneratedFile) -> Result<ValidationResult, GenerationError> {
76 let mut errors = Vec::new();
77 let mut warnings = Vec::new();
78
79 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 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 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 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 fn check_rust_syntax(
145 &self,
146 content: &str,
147 file_path: &str,
148 ) -> Result<Vec<ValidationError>, GenerationError> {
149 let mut errors = Vec::new();
151
152 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 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 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 fn check_typescript_syntax(
205 &self,
206 content: &str,
207 file_path: &str,
208 ) -> Result<Vec<ValidationError>, GenerationError> {
209 let mut errors = Vec::new();
211
212 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 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 fn check_python_syntax(
249 &self,
250 content: &str,
251 file_path: &str,
252 ) -> Result<Vec<ValidationError>, GenerationError> {
253 let mut errors = Vec::new();
255
256 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 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 fn check_go_syntax(
293 &self,
294 content: &str,
295 file_path: &str,
296 ) -> Result<Vec<ValidationError>, GenerationError> {
297 let mut errors = Vec::new();
299
300 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 fn check_java_syntax(
321 &self,
322 content: &str,
323 file_path: &str,
324 ) -> Result<Vec<ValidationError>, GenerationError> {
325 let mut errors = Vec::new();
327
328 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 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 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 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 fn run_rust_type_checking(
402 &self,
403 _content: &str,
404 _file_path: &str,
405 ) -> Result<Vec<ValidationError>, GenerationError> {
406 debug!("Rust type checking would be performed by cargo check");
413 Ok(Vec::new())
414 }
415
416 fn run_typescript_type_checking(
418 &self,
419 _content: &str,
420 _file_path: &str,
421 ) -> Result<Vec<ValidationError>, GenerationError> {
422 debug!("TypeScript type checking would be performed by tsc");
429 Ok(Vec::new())
430 }
431
432 fn run_python_type_checking(
434 &self,
435 _content: &str,
436 _file_path: &str,
437 ) -> Result<Vec<ValidationError>, GenerationError> {
438 debug!("Python type checking would be performed by mypy");
445 Ok(Vec::new())
446 }
447
448 fn run_go_type_checking(
450 &self,
451 _content: &str,
452 _file_path: &str,
453 ) -> Result<Vec<ValidationError>, GenerationError> {
454 debug!("Go type checking would be performed by go vet");
461 Ok(Vec::new())
462 }
463
464 fn run_java_type_checking(
466 &self,
467 _content: &str,
468 _file_path: &str,
469 ) -> Result<Vec<ValidationError>, GenerationError> {
470 debug!("Java type checking would be performed by javac");
477 Ok(Vec::new())
478 }
479
480 pub fn is_valid(&self, result: &ValidationResult) -> bool {
482 result.valid
483 }
484
485 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 let result = ValidationResult {
674 valid: false, 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}