ricecoder_generation/
language_validators.rs

1//! Language-specific validation implementations
2//!
3//! Provides language-specific validators for:
4//! - Rust: cargo check, clippy
5//! - TypeScript: tsc, eslint
6//! - Python: mypy, pylint
7//! - Go: go vet, golangci-lint
8//! - Java: javac, checkstyle
9
10use crate::models::{ValidationError, ValidationWarning};
11use tracing::debug;
12
13/// Trait for language-specific validators
14pub trait LanguageValidator: Send + Sync {
15    /// Validates code for this language
16    fn validate(
17        &self,
18        content: &str,
19        file_path: &str,
20    ) -> Result<(Vec<ValidationError>, Vec<ValidationWarning>), String>;
21}
22
23/// Rust validator using cargo check and clippy
24#[derive(Debug, Clone)]
25pub struct RustValidator;
26
27impl RustValidator {
28    /// Creates a new Rust validator
29    pub fn new() -> Self {
30        Self
31    }
32
33    /// Checks for common Rust issues
34    pub fn check_common_issues(&self, content: &str, file_path: &str) -> Vec<ValidationError> {
35        let mut errors = Vec::new();
36
37        // Check for unsafe without documentation
38        for (line_num, line) in content.lines().enumerate() {
39            if line.contains("unsafe") && !line.trim().starts_with("//") {
40                // Check if there's a comment above
41                let has_comment = if line_num > 0 {
42                    content
43                        .lines()
44                        .nth(line_num - 1)
45                        .map(|l| l.trim().starts_with("//"))
46                        .unwrap_or(false)
47                } else {
48                    false
49                };
50
51                if !has_comment {
52                    errors.push(ValidationError {
53                        file: file_path.to_string(),
54                        line: line_num + 1,
55                        column: 1,
56                        message: "unsafe block without documentation comment".to_string(),
57                        code: Some("W0001".to_string()),
58                    });
59                }
60            }
61        }
62
63        errors
64    }
65
66    /// Checks for unwrap() calls
67    pub fn check_unwrap_calls(&self, content: &str, file_path: &str) -> Vec<ValidationWarning> {
68        let mut warnings = Vec::new();
69
70        for (line_num, line) in content.lines().enumerate() {
71            if line.contains(".unwrap()") {
72                warnings.push(ValidationWarning {
73                    file: file_path.to_string(),
74                    line: line_num + 1,
75                    column: line.find(".unwrap()").unwrap_or(0) + 1,
76                    message: "unwrap() call may panic".to_string(),
77                    code: Some("W0002".to_string()),
78                });
79            }
80        }
81
82        warnings
83    }
84
85    /// Checks for panic! calls
86    pub fn check_panic_calls(&self, content: &str, file_path: &str) -> Vec<ValidationWarning> {
87        let mut warnings = Vec::new();
88
89        for (line_num, line) in content.lines().enumerate() {
90            if line.contains("panic!") {
91                warnings.push(ValidationWarning {
92                    file: file_path.to_string(),
93                    line: line_num + 1,
94                    column: line.find("panic!").unwrap_or(0) + 1,
95                    message: "panic! call may crash the application".to_string(),
96                    code: Some("W0003".to_string()),
97                });
98            }
99        }
100
101        warnings
102    }
103}
104
105impl Default for RustValidator {
106    fn default() -> Self {
107        Self::new()
108    }
109}
110
111impl LanguageValidator for RustValidator {
112    fn validate(
113        &self,
114        content: &str,
115        file_path: &str,
116    ) -> Result<(Vec<ValidationError>, Vec<ValidationWarning>), String> {
117        debug!("Validating Rust code: {}", file_path);
118
119        let errors = self.check_common_issues(content, file_path);
120        let warnings = self.check_unwrap_calls(content, file_path);
121        let mut all_warnings = warnings;
122        all_warnings.extend(self.check_panic_calls(content, file_path));
123
124        Ok((errors, all_warnings))
125    }
126}
127
128/// TypeScript validator using tsc and eslint
129#[derive(Debug, Clone)]
130pub struct TypeScriptValidator;
131
132impl TypeScriptValidator {
133    /// Creates a new TypeScript validator
134    pub fn new() -> Self {
135        Self
136    }
137
138    /// Checks for common TypeScript issues
139    pub fn check_common_issues(&self, content: &str, file_path: &str) -> Vec<ValidationError> {
140        let mut errors = Vec::new();
141
142        // Check for any type usage
143        for (line_num, line) in content.lines().enumerate() {
144            if line.contains(": any") || line.contains(": any,") || line.contains(": any)") {
145                errors.push(ValidationError {
146                    file: file_path.to_string(),
147                    line: line_num + 1,
148                    column: line.find(": any").unwrap_or(0) + 1,
149                    message: "Use of 'any' type is not allowed".to_string(),
150                    code: Some("TS7006".to_string()),
151                });
152            }
153        }
154
155        errors
156    }
157
158    /// Checks for missing error handling
159    pub fn check_error_handling(&self, content: &str, file_path: &str) -> Vec<ValidationWarning> {
160        let mut warnings = Vec::new();
161
162        for (line_num, line) in content.lines().enumerate() {
163            if line.contains("throw ") && !line.contains("Error") {
164                warnings.push(ValidationWarning {
165                    file: file_path.to_string(),
166                    line: line_num + 1,
167                    column: line.find("throw").unwrap_or(0) + 1,
168                    message: "throw without Error type".to_string(),
169                    code: Some("W0001".to_string()),
170                });
171            }
172        }
173
174        warnings
175    }
176
177    /// Checks for console usage
178    pub fn check_console_usage(&self, content: &str, file_path: &str) -> Vec<ValidationWarning> {
179        let mut warnings = Vec::new();
180
181        for (line_num, line) in content.lines().enumerate() {
182            if line.contains("console.") && !line.trim().starts_with("//") {
183                warnings.push(ValidationWarning {
184                    file: file_path.to_string(),
185                    line: line_num + 1,
186                    column: line.find("console.").unwrap_or(0) + 1,
187                    message: "console usage in production code".to_string(),
188                    code: Some("W0002".to_string()),
189                });
190            }
191        }
192
193        warnings
194    }
195}
196
197impl Default for TypeScriptValidator {
198    fn default() -> Self {
199        Self::new()
200    }
201}
202
203impl LanguageValidator for TypeScriptValidator {
204    fn validate(
205        &self,
206        content: &str,
207        file_path: &str,
208    ) -> Result<(Vec<ValidationError>, Vec<ValidationWarning>), String> {
209        debug!("Validating TypeScript code: {}", file_path);
210
211        let errors = self.check_common_issues(content, file_path);
212        let mut warnings = self.check_error_handling(content, file_path);
213        warnings.extend(self.check_console_usage(content, file_path));
214
215        Ok((errors, warnings))
216    }
217}
218
219/// Python validator using mypy and pylint
220#[derive(Debug, Clone)]
221pub struct PythonValidator;
222
223impl PythonValidator {
224    /// Creates a new Python validator
225    pub fn new() -> Self {
226        Self
227    }
228
229    /// Checks for common Python issues
230    pub fn check_common_issues(&self, content: &str, file_path: &str) -> Vec<ValidationError> {
231        let mut errors = Vec::new();
232
233        // Check for bare except
234        for (line_num, line) in content.lines().enumerate() {
235            if line.trim() == "except:" {
236                errors.push(ValidationError {
237                    file: file_path.to_string(),
238                    line: line_num + 1,
239                    column: 1,
240                    message: "Bare except clause is not allowed".to_string(),
241                    code: Some("E0001".to_string()),
242                });
243            }
244        }
245
246        errors
247    }
248
249    /// Checks for missing type hints
250    pub fn check_type_hints(&self, content: &str, file_path: &str) -> Vec<ValidationWarning> {
251        let mut warnings = Vec::new();
252
253        for (line_num, line) in content.lines().enumerate() {
254            if (line.trim().starts_with("def ") || line.trim().starts_with("class "))
255                && !line.contains("->")
256                && !line.trim().starts_with("def _")
257            {
258                warnings.push(ValidationWarning {
259                    file: file_path.to_string(),
260                    line: line_num + 1,
261                    column: 1,
262                    message: "Missing type hints".to_string(),
263                    code: Some("W0001".to_string()),
264                });
265            }
266        }
267
268        warnings
269    }
270
271    /// Checks for print usage
272    pub fn check_print_usage(&self, content: &str, file_path: &str) -> Vec<ValidationWarning> {
273        let mut warnings = Vec::new();
274
275        for (line_num, line) in content.lines().enumerate() {
276            if line.contains("print(") && !line.trim().starts_with("#") {
277                warnings.push(ValidationWarning {
278                    file: file_path.to_string(),
279                    line: line_num + 1,
280                    column: line.find("print(").unwrap_or(0) + 1,
281                    message: "print() usage in production code".to_string(),
282                    code: Some("W0002".to_string()),
283                });
284            }
285        }
286
287        warnings
288    }
289}
290
291impl Default for PythonValidator {
292    fn default() -> Self {
293        Self::new()
294    }
295}
296
297impl LanguageValidator for PythonValidator {
298    fn validate(
299        &self,
300        content: &str,
301        file_path: &str,
302    ) -> Result<(Vec<ValidationError>, Vec<ValidationWarning>), String> {
303        debug!("Validating Python code: {}", file_path);
304
305        let errors = self.check_common_issues(content, file_path);
306        let mut warnings = self.check_type_hints(content, file_path);
307        warnings.extend(self.check_print_usage(content, file_path));
308
309        Ok((errors, warnings))
310    }
311}
312
313/// Go validator using go vet and golangci-lint
314#[derive(Debug, Clone)]
315pub struct GoValidator;
316
317impl GoValidator {
318    /// Creates a new Go validator
319    pub fn new() -> Self {
320        Self
321    }
322
323    /// Checks for common Go issues
324    pub fn check_common_issues(&self, content: &str, file_path: &str) -> Vec<ValidationError> {
325        let mut errors = Vec::new();
326
327        // Check for missing error handling
328        for (line_num, line) in content.lines().enumerate() {
329            if line.contains("_ = ") && line.contains("err") {
330                errors.push(ValidationError {
331                    file: file_path.to_string(),
332                    line: line_num + 1,
333                    column: 1,
334                    message: "Error ignored with blank identifier".to_string(),
335                    code: Some("E0001".to_string()),
336                });
337            }
338        }
339
340        errors
341    }
342
343    /// Checks for panic usage
344    pub fn check_panic_usage(&self, content: &str, file_path: &str) -> Vec<ValidationWarning> {
345        let mut warnings = Vec::new();
346
347        for (line_num, line) in content.lines().enumerate() {
348            if line.contains("panic(") {
349                warnings.push(ValidationWarning {
350                    file: file_path.to_string(),
351                    line: line_num + 1,
352                    column: line.find("panic(").unwrap_or(0) + 1,
353                    message: "panic() call may crash the application".to_string(),
354                    code: Some("W0001".to_string()),
355                });
356            }
357        }
358
359        warnings
360    }
361}
362
363impl Default for GoValidator {
364    fn default() -> Self {
365        Self::new()
366    }
367}
368
369impl LanguageValidator for GoValidator {
370    fn validate(
371        &self,
372        content: &str,
373        file_path: &str,
374    ) -> Result<(Vec<ValidationError>, Vec<ValidationWarning>), String> {
375        debug!("Validating Go code: {}", file_path);
376
377        let errors = self.check_common_issues(content, file_path);
378        let warnings = self.check_panic_usage(content, file_path);
379
380        Ok((errors, warnings))
381    }
382}
383
384/// Java validator using javac and checkstyle
385#[derive(Debug, Clone)]
386pub struct JavaValidator;
387
388impl JavaValidator {
389    /// Creates a new Java validator
390    pub fn new() -> Self {
391        Self
392    }
393
394    /// Checks for common Java issues
395    pub fn check_common_issues(&self, content: &str, file_path: &str) -> Vec<ValidationError> {
396        let mut errors = Vec::new();
397
398        // Check for missing class declaration
399        if !content.contains("class ") && !content.contains("interface ") {
400            errors.push(ValidationError {
401                file: file_path.to_string(),
402                line: 1,
403                column: 1,
404                message: "Missing class or interface declaration".to_string(),
405                code: Some("E0001".to_string()),
406            });
407        }
408
409        errors
410    }
411
412    /// Checks for raw type usage
413    pub fn check_raw_types(&self, content: &str, file_path: &str) -> Vec<ValidationWarning> {
414        let mut warnings = Vec::new();
415
416        for (line_num, line) in content.lines().enumerate() {
417            if (line.contains("List ") || line.contains("Map ") || line.contains("Set "))
418                && !line.contains("<")
419            {
420                warnings.push(ValidationWarning {
421                    file: file_path.to_string(),
422                    line: line_num + 1,
423                    column: 1,
424                    message: "Raw type usage without generics".to_string(),
425                    code: Some("W0001".to_string()),
426                });
427            }
428        }
429
430        warnings
431    }
432}
433
434impl Default for JavaValidator {
435    fn default() -> Self {
436        Self::new()
437    }
438}
439
440impl LanguageValidator for JavaValidator {
441    fn validate(
442        &self,
443        content: &str,
444        file_path: &str,
445    ) -> Result<(Vec<ValidationError>, Vec<ValidationWarning>), String> {
446        debug!("Validating Java code: {}", file_path);
447
448        let errors = self.check_common_issues(content, file_path);
449        let warnings = self.check_raw_types(content, file_path);
450
451        Ok((errors, warnings))
452    }
453}
454
455/// Gets the appropriate validator for a language
456pub fn get_validator(language: &str) -> Option<Box<dyn LanguageValidator>> {
457    match language {
458        "rust" | "rs" => Some(Box::new(RustValidator::new())),
459        "typescript" | "ts" => Some(Box::new(TypeScriptValidator::new())),
460        "python" | "py" => Some(Box::new(PythonValidator::new())),
461        "go" => Some(Box::new(GoValidator::new())),
462        "java" => Some(Box::new(JavaValidator::new())),
463        _ => None,
464    }
465}
466
467#[cfg(test)]
468mod tests {
469    use super::*;
470
471    #[test]
472    fn test_rust_validator_unwrap() {
473        let validator = RustValidator::new();
474        let content = "let x = result.unwrap();";
475        let warnings = validator.check_unwrap_calls(content, "main.rs");
476        assert!(!warnings.is_empty());
477        assert!(warnings[0].message.contains("unwrap"));
478    }
479
480    #[test]
481    fn test_rust_validator_panic() {
482        let validator = RustValidator::new();
483        let content = "panic!(\"error\");";
484        let warnings = validator.check_panic_calls(content, "main.rs");
485        assert!(!warnings.is_empty());
486        assert!(warnings[0].message.contains("panic"));
487    }
488
489    #[test]
490    fn test_typescript_validator_any_type() {
491        let validator = TypeScriptValidator::new();
492        let content = "let x: any = 5;";
493        let errors = validator.check_common_issues(content, "main.ts");
494        assert!(!errors.is_empty());
495        assert!(errors[0].message.contains("any"));
496    }
497
498    #[test]
499    fn test_typescript_validator_console() {
500        let validator = TypeScriptValidator::new();
501        let content = "console.log(\"test\");";
502        let warnings = validator.check_console_usage(content, "main.ts");
503        assert!(!warnings.is_empty());
504        assert!(warnings[0].message.contains("console"));
505    }
506
507    #[test]
508    fn test_python_validator_bare_except() {
509        let validator = PythonValidator::new();
510        let content = "try:\n    pass\nexcept:";
511        let errors = validator.check_common_issues(content, "main.py");
512        assert!(!errors.is_empty());
513        assert!(errors[0].message.contains("Bare except"));
514    }
515
516    #[test]
517    fn test_python_validator_print() {
518        let validator = PythonValidator::new();
519        let content = "print(\"test\")";
520        let warnings = validator.check_print_usage(content, "main.py");
521        assert!(!warnings.is_empty());
522        assert!(warnings[0].message.contains("print"));
523    }
524
525    #[test]
526    fn test_go_validator_panic() {
527        let validator = GoValidator::new();
528        let content = "panic(\"error\")";
529        let warnings = validator.check_panic_usage(content, "main.go");
530        assert!(!warnings.is_empty());
531        assert!(warnings[0].message.contains("panic"));
532    }
533
534    #[test]
535    fn test_java_validator_raw_type() {
536        let validator = JavaValidator::new();
537        let content = "List items = new ArrayList();";
538        let warnings = validator.check_raw_types(content, "Main.java");
539        assert!(!warnings.is_empty());
540        assert!(warnings[0].message.contains("Raw type"));
541    }
542
543    #[test]
544    fn test_get_validator_rust() {
545        let validator = get_validator("rust");
546        assert!(validator.is_some());
547    }
548
549    #[test]
550    fn test_get_validator_typescript() {
551        let validator = get_validator("typescript");
552        assert!(validator.is_some());
553    }
554
555    #[test]
556    fn test_get_validator_python() {
557        let validator = get_validator("python");
558        assert!(validator.is_some());
559    }
560
561    #[test]
562    fn test_get_validator_unknown() {
563        let validator = get_validator("unknown");
564        assert!(validator.is_none());
565    }
566
567    #[test]
568    fn test_rust_validator_trait() {
569        let validator = RustValidator::new();
570        let content = "fn main() {}";
571        let result = validator.validate(content, "main.rs");
572        assert!(result.is_ok());
573    }
574
575    #[test]
576    fn test_typescript_validator_trait() {
577        let validator = TypeScriptValidator::new();
578        let content = "function main() {}";
579        let result = validator.validate(content, "main.ts");
580        assert!(result.is_ok());
581    }
582}