ferrous_forge/
validation.rs

1//! Rust code validation engine
2//!
3//! This module contains the core validation logic ported from the original Python implementation.
4//! It enforces Ferrous Forge standards including:
5//! - Zero underscore bandaid coding
6//! - Edition 2024 enforcement
7//! - File and function size limits
8//! - Documentation requirements
9//! - Security best practices
10
11use crate::{Result, Error};
12use regex::Regex;
13use serde::{Deserialize, Serialize};
14use std::path::{Path, PathBuf};
15use std::process::Command;
16use tokio::fs;
17
18/// Types of violations that can be detected
19#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
20pub enum ViolationType {
21    /// Underscore parameter or let assignment bandaid
22    UnderscoreBandaid,
23    /// Wrong Rust edition (not 2024)
24    WrongEdition,
25    /// File exceeds size limit
26    FileTooLarge,
27    /// Function exceeds size limit
28    FunctionTooLarge,
29    /// Line exceeds length limit
30    LineTooLong,
31    /// Use of .unwrap() or .expect() in production code
32    UnwrapInProduction,
33    /// Missing documentation
34    MissingDocs,
35    /// Missing required dependencies
36    MissingDependencies,
37    /// Rust version too old
38    OldRustVersion,
39}
40
41/// A single standards violation
42#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct Violation {
44    /// Type of violation
45    pub violation_type: ViolationType,
46    /// File where violation occurred
47    pub file: PathBuf,
48    /// Line number (0-based)
49    pub line: usize,
50    /// Human-readable message
51    pub message: String,
52    /// Severity level
53    pub severity: Severity,
54}
55
56/// Severity levels for violations
57#[derive(Debug, Clone, Serialize, Deserialize)]
58pub enum Severity {
59    /// Error - must be fixed
60    Error,
61    /// Warning - should be fixed
62    Warning,
63    /// Info - good to know
64    Info,
65}
66
67/// Result of running clippy
68#[derive(Debug)]
69pub struct ClippyResult {
70    /// Whether clippy passed without errors
71    pub success: bool,
72    /// Combined stdout and stderr output
73    pub output: String,
74}
75
76/// Rust project validator
77pub struct RustValidator {
78    /// Root directory of the project being validated
79    project_root: PathBuf,
80    /// Compiled regex patterns for efficient matching
81    patterns: ValidationPatterns,
82    /// Required crates for Context7 integration
83    _required_crates: Vec<String>,
84}
85
86/// Compiled regex patterns for validation
87struct ValidationPatterns {
88    underscore_param: Regex,
89    underscore_let: Regex,
90    unwrap_call: Regex,
91    expect_call: Regex,
92    function_def: Regex,
93}
94
95impl RustValidator {
96    /// Create a new validator for the given project
97    pub fn new(project_root: PathBuf) -> Result<Self> {
98        let patterns = ValidationPatterns {
99            underscore_param: Regex::new(r"fn\s+\w+\([^)]*_\w+\s*:[^)]*\)")
100                .map_err(|e| Error::validation(format!("Invalid regex: {}", e)))?,
101            underscore_let: Regex::new(r"let\s+_\s*=")
102                .map_err(|e| Error::validation(format!("Invalid regex: {}", e)))?,
103            unwrap_call: Regex::new(r"\.unwrap\(\)")
104                .map_err(|e| Error::validation(format!("Invalid regex: {}", e)))?,
105            expect_call: Regex::new(r"\.expect\(")
106                .map_err(|e| Error::validation(format!("Invalid regex: {}", e)))?,
107            function_def: Regex::new(r"^(\s*)(?:pub\s+)?(?:async\s+)?fn\s+\w+")
108                .map_err(|e| Error::validation(format!("Invalid regex: {}", e)))?,
109        };
110
111        let required_crates = vec![
112            "tokio".to_string(),
113            "thiserror".to_string(), 
114            "anyhow".to_string(),
115            "tracing".to_string(),
116        ];
117
118        Ok(Self {
119            project_root,
120            patterns,
121            _required_crates: required_crates,
122        })
123    }
124
125    /// Validate the entire project
126    pub async fn validate_project(&self) -> Result<Vec<Violation>> {
127        let mut violations = Vec::new();
128
129        // Check Rust version first
130        self.check_rust_version(&mut violations).await?;
131
132        // Find all relevant files
133        let rust_files = self.find_rust_files().await?;
134        let cargo_files = self.find_cargo_files().await?;
135
136        tracing::info!("Found {} Rust files and {} Cargo.toml files", 
137            rust_files.len(), cargo_files.len());
138
139        // Validate Cargo.toml files
140        for cargo_file in cargo_files {
141            self.validate_cargo_toml(&cargo_file, &mut violations).await?;
142        }
143
144        // Validate Rust source files
145        for rust_file in rust_files {
146            // Skip target directory
147            if rust_file.to_string_lossy().contains("target/") {
148                continue;
149            }
150            
151            self.validate_rust_file(&rust_file, &mut violations).await?;
152        }
153
154        Ok(violations)
155    }
156
157    /// Generate a human-readable report from violations
158    pub fn generate_report(&self, violations: &[Violation]) -> String {
159        if violations.is_empty() {
160            return "✅ All Rust validation checks passed! Code meets Ferrous Forge standards.".to_string();
161        }
162
163        let mut report = format!("❌ Found {} violations of Ferrous Forge standards:\n\n", violations.len());
164
165        // Group by violation type
166        let mut by_type = std::collections::HashMap::new();
167        for violation in violations {
168            by_type.entry(&violation.violation_type)
169                .or_insert_with(Vec::new)
170                .push(violation);
171        }
172
173        for (violation_type, violations) in by_type {
174            let type_name = format!("{:?}", violation_type)
175                .to_uppercase()
176                .replace('_', " ");
177                
178            report.push_str(&format!("🚨 {} ({} violations):\n", type_name, violations.len()));
179            
180            for violation in violations.iter().take(10) {
181                report.push_str(&format!(
182                    "  {}:{} - {}\n", 
183                    violation.file.display(), 
184                    violation.line + 1, 
185                    violation.message
186                ));
187            }
188            
189            if violations.len() > 10 {
190                report.push_str(&format!("  ... and {} more\n", violations.len() - 10));
191            }
192            
193            report.push('\n');
194        }
195
196        report
197    }
198
199    /// Run clippy with strict configuration
200    pub async fn run_clippy(&self) -> Result<ClippyResult> {
201        let output = Command::new("cargo")
202            .args(&[
203                "clippy",
204                "--all-features",
205                "--",
206                "-D", "warnings",
207                "-D", "clippy::unwrap_used",
208                "-D", "clippy::expect_used",
209                "-D", "clippy::panic",
210                "-D", "clippy::unimplemented",
211                "-D", "clippy::todo",
212            ])
213            .current_dir(&self.project_root)
214            .output()
215            .map_err(|e| Error::process(format!("Failed to run clippy: {}", e)))?;
216
217        Ok(ClippyResult {
218            success: output.status.success(),
219            output: String::from_utf8_lossy(&output.stdout).to_string() 
220                + &String::from_utf8_lossy(&output.stderr),
221        })
222    }
223
224    async fn check_rust_version(&self, violations: &mut Vec<Violation>) -> Result<()> {
225        let output = Command::new("rustc")
226            .arg("--version")
227            .output()
228            .map_err(|_| Error::validation("Rust compiler not found"))?;
229
230        let version_line = String::from_utf8_lossy(&output.stdout);
231        
232        // Extract version (e.g., "rustc 1.85.0" -> "1.85.0")
233        if let Some(captures) = Regex::new(r"rustc (\d+)\.(\d+)\.(\d+)")
234            .unwrap()
235            .captures(&version_line)
236        {
237            let major: u32 = captures[1].parse().unwrap_or(0);
238            let minor: u32 = captures[2].parse().unwrap_or(0);
239            
240            if major < 1 || (major == 1 && minor < 85) {
241                violations.push(Violation {
242                    violation_type: ViolationType::OldRustVersion,
243                    file: PathBuf::from("<system>"),
244                    line: 0,
245                    message: format!("Rust version {}.{} is too old. Minimum required: 1.85.0", major, minor),
246                    severity: Severity::Error,
247                });
248            }
249        } else {
250            violations.push(Violation {
251                violation_type: ViolationType::OldRustVersion,
252                file: PathBuf::from("<system>"),
253                line: 0,
254                message: "Could not parse Rust version".to_string(),
255                severity: Severity::Error,
256            });
257        }
258
259        Ok(())
260    }
261
262    async fn find_rust_files(&self) -> Result<Vec<PathBuf>> {
263        let mut rust_files = Vec::new();
264        self.collect_rust_files_recursive(&self.project_root, &mut rust_files)?;
265        Ok(rust_files)
266    }
267
268    fn collect_rust_files_recursive(&self, path: &Path, rust_files: &mut Vec<PathBuf>) -> Result<()> {
269        if path.is_file() {
270            if let Some(ext) = path.extension() {
271                if ext == "rs" {
272                    rust_files.push(path.to_path_buf());
273                }
274            }
275        } else if path.is_dir() {
276            let entries = std::fs::read_dir(path)?;
277            for entry in entries {
278                let entry = entry?;
279                self.collect_rust_files_recursive(&entry.path(), rust_files)?;
280            }
281        }
282        
283        Ok(())
284    }
285
286    async fn find_cargo_files(&self) -> Result<Vec<PathBuf>> {
287        let mut cargo_files = Vec::new();
288        self.collect_cargo_files_recursive(&self.project_root, &mut cargo_files)?;
289        Ok(cargo_files)
290    }
291
292    fn collect_cargo_files_recursive(&self, path: &Path, cargo_files: &mut Vec<PathBuf>) -> Result<()> {
293        if path.is_file() {
294            if path.file_name().and_then(|n| n.to_str()) == Some("Cargo.toml") {
295                cargo_files.push(path.to_path_buf());
296            }
297        } else if path.is_dir() && !path.to_string_lossy().contains("target/") {
298            let entries = std::fs::read_dir(path)?;
299            for entry in entries {
300                let entry = entry?;
301                self.collect_cargo_files_recursive(&entry.path(), cargo_files)?;
302            }
303        }
304        
305        Ok(())
306    }
307
308    async fn validate_cargo_toml(&self, cargo_file: &Path, violations: &mut Vec<Violation>) -> Result<()> {
309        let content = fs::read_to_string(cargo_file).await?;
310        let lines: Vec<&str> = content.lines().collect();
311
312        // Check for Edition 2024
313        let mut edition_found = false;
314        for (i, line) in lines.iter().enumerate() {
315            if line.contains("edition") {
316                if !line.contains("2024") {
317                    violations.push(Violation {
318                        violation_type: ViolationType::WrongEdition,
319                        file: cargo_file.to_path_buf(),
320                        line: i,
321                        message: "Must use Edition 2024, not 2021 or older".to_string(),
322                        severity: Severity::Error,
323                    });
324                }
325                edition_found = true;
326                break;
327            }
328        }
329
330        if !edition_found {
331            violations.push(Violation {
332                violation_type: ViolationType::WrongEdition,
333                file: cargo_file.to_path_buf(),
334                line: 0,
335                message: "Missing edition specification - must be '2024'".to_string(),
336                severity: Severity::Error,
337            });
338        }
339
340        Ok(())
341    }
342
343    async fn validate_rust_file(&self, rust_file: &Path, violations: &mut Vec<Violation>) -> Result<()> {
344        let content = fs::read_to_string(rust_file).await?;
345        let lines: Vec<&str> = content.lines().collect();
346
347        // Check file size limit (300 lines)
348        if lines.len() > 300 {
349            violations.push(Violation {
350                violation_type: ViolationType::FileTooLarge,
351                file: rust_file.to_path_buf(),
352                line: lines.len() - 1,
353                message: format!("File has {} lines, maximum allowed is 300", lines.len()),
354                severity: Severity::Error,
355            });
356        }
357        
358        // Check line lengths (100 character limit)
359        for (i, line) in lines.iter().enumerate() {
360            if line.len() > 100 {
361                violations.push(Violation {
362                    violation_type: ViolationType::LineTooLong,
363                    file: rust_file.to_path_buf(),
364                    line: i,
365                    message: format!("Line has {} characters, maximum allowed is 100", line.len()),
366                    severity: Severity::Warning,
367                });
368            }
369        }
370
371        let mut in_test_block = false;
372        let mut current_function_start: Option<usize> = None;
373
374        for (i, line) in lines.iter().enumerate() {
375            let line_stripped = line.trim();
376
377            // Track test blocks
378            if line_stripped.contains("#[test]") || line_stripped.contains("#[cfg(test)]") {
379                in_test_block = true;
380            }
381
382            // Track function boundaries
383            if self.patterns.function_def.is_match(line) {
384                // Check previous function size
385                if let Some(start) = current_function_start {
386                    let func_lines = i - start;
387                    if func_lines > 50 {
388                        violations.push(Violation {
389                            violation_type: ViolationType::FunctionTooLarge,
390                            file: rust_file.to_path_buf(),
391                            line: start,
392                            message: format!("Function has {} lines, maximum allowed is 50", func_lines),
393                            severity: Severity::Error,
394                        });
395                    }
396                }
397                current_function_start = Some(i);
398            }
399
400            // Check for underscore bandaid coding
401            if self.patterns.underscore_param.is_match(line) {
402                violations.push(Violation {
403                    violation_type: ViolationType::UnderscoreBandaid,
404                    file: rust_file.to_path_buf(),
405                    line: i,
406                    message: "BANNED: Underscore parameter (_param) - fix the design instead of hiding warnings".to_string(),
407                    severity: Severity::Error,
408                });
409            }
410
411            if self.patterns.underscore_let.is_match(line) {
412                violations.push(Violation {
413                    violation_type: ViolationType::UnderscoreBandaid,
414                    file: rust_file.to_path_buf(),
415                    line: i,
416                    message: "BANNED: Underscore assignment (let _ =) - handle errors properly".to_string(),
417                    severity: Severity::Error,
418                });
419            }
420
421            // Check for .unwrap() in production code (not in tests)
422            if !in_test_block && self.patterns.unwrap_call.is_match(line) {
423                violations.push(Violation {
424                    violation_type: ViolationType::UnwrapInProduction,
425                    file: rust_file.to_path_buf(),
426                    line: i,
427                    message: "BANNED: .unwrap() in production code - use proper error handling with ?".to_string(),
428                    severity: Severity::Error,
429                });
430            }
431
432            // Check for .expect() in production code
433            if !in_test_block && self.patterns.expect_call.is_match(line) {
434                violations.push(Violation {
435                    violation_type: ViolationType::UnwrapInProduction,
436                    file: rust_file.to_path_buf(),
437                    line: i,
438                    message: "BANNED: .expect() in production code - use proper error handling with ?".to_string(),
439                    severity: Severity::Error,
440                });
441            }
442
443            // Reset test block tracking
444            if line_stripped.starts_with('}') && in_test_block {
445                in_test_block = false;
446            }
447        }
448
449        // Check the last function if any
450        if let Some(start) = current_function_start {
451            let func_lines = lines.len() - start;
452            if func_lines > 50 {
453                violations.push(Violation {
454                    violation_type: ViolationType::FunctionTooLarge,
455                    file: rust_file.to_path_buf(),
456                    line: start,
457                    message: format!("Function has {} lines, maximum allowed is 50", func_lines),
458                    severity: Severity::Error,
459                });
460            }
461        }
462
463        Ok(())
464    }
465}
466
467#[cfg(test)]
468mod tests {
469    use super::*;
470    use tempfile::TempDir;
471    use tokio::fs;
472
473    #[test]
474    fn test_violation_type_variants() {
475        let types = vec![
476            ViolationType::UnderscoreBandaid,
477            ViolationType::WrongEdition,
478            ViolationType::FileTooLarge,
479            ViolationType::FunctionTooLarge,
480            ViolationType::LineTooLong,
481            ViolationType::UnwrapInProduction,
482            ViolationType::MissingDocs,
483            ViolationType::MissingDependencies,
484            ViolationType::OldRustVersion,
485        ];
486        
487        // Test that variants are distinct
488        for (i, type1) in types.iter().enumerate() {
489            for (j, type2) in types.iter().enumerate() {
490                if i != j {
491                    assert_ne!(type1, type2);
492                }
493            }
494        }
495    }
496
497    #[test]
498    fn test_severity_variants() {
499        let error = Severity::Error;
500        let warning = Severity::Warning;
501        let info = Severity::Info;
502        
503        // Just test that we can create instances
504        match error {
505            Severity::Error => {},
506            _ => panic!("Should be error"),
507        }
508        match warning {
509            Severity::Warning => {},
510            _ => panic!("Should be warning"),
511        }
512        match info {
513            Severity::Info => {},
514            _ => panic!("Should be info"),
515        }
516    }
517
518    #[test]
519    fn test_violation_creation() {
520        let violation = Violation {
521            violation_type: ViolationType::UnderscoreBandaid,
522            file: PathBuf::from("test.rs"),
523            line: 10,
524            message: "Test violation".to_string(),
525            severity: Severity::Error,
526        };
527        
528        assert_eq!(violation.violation_type, ViolationType::UnderscoreBandaid);
529        assert_eq!(violation.file, PathBuf::from("test.rs"));
530        assert_eq!(violation.line, 10);
531        assert_eq!(violation.message, "Test violation");
532        matches!(violation.severity, Severity::Error);
533    }
534
535    #[test]
536    fn test_clippy_result() {
537        let result = ClippyResult {
538            success: true,
539            output: "All checks passed".to_string(),
540        };
541        
542        assert!(result.success);
543        assert_eq!(result.output, "All checks passed");
544    }
545
546    #[tokio::test]
547    async fn test_rust_validator_creation() {
548        let temp_dir = TempDir::new().expect("Failed to create temp directory");
549        let validator = RustValidator::new(temp_dir.path().to_path_buf());
550        
551        assert!(validator.is_ok());
552        let validator = validator.expect("Should create validator");
553        assert_eq!(validator.project_root, temp_dir.path());
554    }
555
556    #[tokio::test]
557    async fn test_generate_report_no_violations() {
558        let temp_dir = TempDir::new().expect("Failed to create temp directory");
559        let validator = RustValidator::new(temp_dir.path().to_path_buf())
560            .expect("Should create validator");
561        
562        let violations = vec![];
563        let report = validator.generate_report(&violations);
564        
565        assert!(report.contains("✅"));
566        assert!(report.contains("All Rust validation checks passed"));
567    }
568
569    #[tokio::test]
570    async fn test_generate_report_with_violations() {
571        let temp_dir = TempDir::new().expect("Failed to create temp directory");
572        let validator = RustValidator::new(temp_dir.path().to_path_buf())
573            .expect("Should create validator");
574        
575        let violations = vec![
576            Violation {
577                violation_type: ViolationType::UnderscoreBandaid,
578                file: PathBuf::from("test.rs"),
579                line: 10,
580                message: "Underscore parameter".to_string(),
581                severity: Severity::Error,
582            },
583            Violation {
584                violation_type: ViolationType::WrongEdition,
585                file: PathBuf::from("Cargo.toml"),
586                line: 5,
587                message: "Wrong edition".to_string(),
588                severity: Severity::Error,
589            },
590        ];
591        
592        let report = validator.generate_report(&violations);
593        
594        assert!(report.contains("❌"));
595        assert!(report.contains("Found 2 violations"));
596        assert!(report.contains("UNDERSCOREBANDAID"));
597        assert!(report.contains("WRONGEDITION"));
598        assert!(report.contains("test.rs:11"));
599        assert!(report.contains("Cargo.toml:6"));
600    }
601
602    #[tokio::test]
603    async fn test_validate_cargo_toml_correct_edition() {
604        let temp_dir = TempDir::new().expect("Failed to create temp directory");
605        let cargo_toml = temp_dir.path().join("Cargo.toml");
606        
607        fs::write(&cargo_toml, r#"
608[package]
609name = "test"
610version = "0.1.0"
611edition = "2024"
612"#).await.expect("Failed to write Cargo.toml");
613        
614        let validator = RustValidator::new(temp_dir.path().to_path_buf())
615            .expect("Should create validator");
616        
617        let mut violations = Vec::new();
618        validator.validate_cargo_toml(&cargo_toml, &mut violations)
619            .await
620            .expect("Should validate");
621        
622        assert!(violations.is_empty());
623    }
624
625    #[tokio::test]
626    async fn test_validate_cargo_toml_wrong_edition() {
627        let temp_dir = TempDir::new().expect("Failed to create temp directory");
628        let cargo_toml = temp_dir.path().join("Cargo.toml");
629        
630        fs::write(&cargo_toml, r#"
631[package]
632name = "test"
633version = "0.1.0"
634edition = "2021"
635"#).await.expect("Failed to write Cargo.toml");
636        
637        let validator = RustValidator::new(temp_dir.path().to_path_buf())
638            .expect("Should create validator");
639        
640        let mut violations = Vec::new();
641        validator.validate_cargo_toml(&cargo_toml, &mut violations)
642            .await
643            .expect("Should validate");
644        
645        assert_eq!(violations.len(), 1);
646        assert_eq!(violations[0].violation_type, ViolationType::WrongEdition);
647        assert!(violations[0].message.contains("Edition 2024"));
648    }
649
650    #[tokio::test]
651    async fn test_validate_cargo_toml_missing_edition() {
652        let temp_dir = TempDir::new().expect("Failed to create temp directory");
653        let cargo_toml = temp_dir.path().join("Cargo.toml");
654        
655        fs::write(&cargo_toml, r#"
656[package]
657name = "test"
658version = "0.1.0"
659"#).await.expect("Failed to write Cargo.toml");
660        
661        let validator = RustValidator::new(temp_dir.path().to_path_buf())
662            .expect("Should create validator");
663        
664        let mut violations = Vec::new();
665        validator.validate_cargo_toml(&cargo_toml, &mut violations)
666            .await
667            .expect("Should validate");
668        
669        assert_eq!(violations.len(), 1);
670        assert_eq!(violations[0].violation_type, ViolationType::WrongEdition);
671        assert!(violations[0].message.contains("Missing edition"));
672    }
673
674    #[tokio::test]
675    async fn test_validate_rust_file_size_limit() {
676        let temp_dir = TempDir::new().expect("Failed to create temp directory");
677        let rust_file = temp_dir.path().join("test.rs");
678        
679        // Create a file with over 300 lines
680        let content = (0..350).map(|i| format!("// Line {}", i)).collect::<Vec<_>>().join("\n");
681        fs::write(&rust_file, content).await.expect("Failed to write Rust file");
682        
683        let validator = RustValidator::new(temp_dir.path().to_path_buf())
684            .expect("Should create validator");
685        
686        let mut violations = Vec::new();
687        validator.validate_rust_file(&rust_file, &mut violations)
688            .await
689            .expect("Should validate");
690        
691        let file_size_violations: Vec<_> = violations.iter()
692            .filter(|v| v.violation_type == ViolationType::FileTooLarge)
693            .collect();
694        
695        assert_eq!(file_size_violations.len(), 1);
696        assert!(file_size_violations[0].message.contains("350 lines"));
697    }
698
699    #[tokio::test]
700    async fn test_validate_rust_file_line_length() {
701        let temp_dir = TempDir::new().expect("Failed to create temp directory");
702        let rust_file = temp_dir.path().join("test.rs");
703        
704        let long_line = "// ".to_string() + &"x".repeat(150);
705        fs::write(&rust_file, long_line).await.expect("Failed to write Rust file");
706        
707        let validator = RustValidator::new(temp_dir.path().to_path_buf())
708            .expect("Should create validator");
709        
710        let mut violations = Vec::new();
711        validator.validate_rust_file(&rust_file, &mut violations)
712            .await
713            .expect("Should validate");
714        
715        let line_length_violations: Vec<_> = violations.iter()
716            .filter(|v| v.violation_type == ViolationType::LineTooLong)
717            .collect();
718        
719        assert_eq!(line_length_violations.len(), 1);
720        assert!(line_length_violations[0].message.contains("153 characters"));
721    }
722
723    #[tokio::test]
724    async fn test_validate_rust_file_underscore_bandaid() {
725        let temp_dir = TempDir::new().expect("Failed to create temp directory");
726        let rust_file = temp_dir.path().join("test.rs");
727        
728        let content = r#"
729fn test_function(_param: String) {
730    let _ = some_operation();
731}
732"#;
733        fs::write(&rust_file, content).await.expect("Failed to write Rust file");
734        
735        let validator = RustValidator::new(temp_dir.path().to_path_buf())
736            .expect("Should create validator");
737        
738        let mut violations = Vec::new();
739        validator.validate_rust_file(&rust_file, &mut violations)
740            .await
741            .expect("Should validate");
742        
743        let bandaid_violations: Vec<_> = violations.iter()
744            .filter(|v| v.violation_type == ViolationType::UnderscoreBandaid)
745            .collect();
746        
747        assert_eq!(bandaid_violations.len(), 2); // One for param, one for let
748        assert!(bandaid_violations.iter().any(|v| v.message.contains("parameter")));
749        assert!(bandaid_violations.iter().any(|v| v.message.contains("assignment")));
750    }
751
752    #[tokio::test]
753    async fn test_validate_rust_file_unwrap_in_production() {
754        let temp_dir = TempDir::new().expect("Failed to create temp directory");
755        let rust_file = temp_dir.path().join("test.rs");
756        
757        let content = r#"
758fn production_code() {
759    let value = some_result.unwrap();
760    let other = another_result.expect("message");
761}
762
763#[test]
764fn test_code() {
765    let value = some_result.unwrap(); // This should be allowed
766}
767"#;
768        fs::write(&rust_file, content).await.expect("Failed to write Rust file");
769        
770        let validator = RustValidator::new(temp_dir.path().to_path_buf())
771            .expect("Should create validator");
772        
773        let mut violations = Vec::new();
774        validator.validate_rust_file(&rust_file, &mut violations)
775            .await
776            .expect("Should validate");
777        
778        let unwrap_violations: Vec<_> = violations.iter()
779            .filter(|v| v.violation_type == ViolationType::UnwrapInProduction)
780            .collect();
781        
782        // Should find 2 violations in production code, but none in test code
783        assert_eq!(unwrap_violations.len(), 2);
784        assert!(unwrap_violations.iter().any(|v| v.message.contains("unwrap")));
785        assert!(unwrap_violations.iter().any(|v| v.message.contains("expect")));
786    }
787
788    #[tokio::test]
789    async fn test_find_rust_files() {
790        let temp_dir = TempDir::new().expect("Failed to create temp directory");
791        
792        // Create some Rust files
793        let src_dir = temp_dir.path().join("src");
794        fs::create_dir(&src_dir).await.expect("Failed to create src dir");
795        
796        fs::write(src_dir.join("main.rs"), "fn main() {}").await.expect("Failed to write main.rs");
797        fs::write(src_dir.join("lib.rs"), "// lib").await.expect("Failed to write lib.rs");
798        fs::write(temp_dir.path().join("build.rs"), "// build").await.expect("Failed to write build.rs");
799        
800        // Create non-Rust file
801        fs::write(temp_dir.path().join("README.md"), "# Test").await.expect("Failed to write README");
802        
803        let validator = RustValidator::new(temp_dir.path().to_path_buf())
804            .expect("Should create validator");
805        
806        let rust_files = validator.find_rust_files().await.expect("Should find files");
807        
808        assert_eq!(rust_files.len(), 3);
809        assert!(rust_files.iter().any(|f| f.file_name().unwrap() == "main.rs"));
810        assert!(rust_files.iter().any(|f| f.file_name().unwrap() == "lib.rs"));
811        assert!(rust_files.iter().any(|f| f.file_name().unwrap() == "build.rs"));
812    }
813
814    #[tokio::test]
815    async fn test_find_cargo_files() {
816        let temp_dir = TempDir::new().expect("Failed to create temp directory");
817        
818        // Create Cargo.toml files
819        fs::write(temp_dir.path().join("Cargo.toml"), "[package]").await.expect("Failed to write Cargo.toml");
820        
821        let sub_dir = temp_dir.path().join("sub_project");
822        fs::create_dir(&sub_dir).await.expect("Failed to create sub dir");
823        fs::write(sub_dir.join("Cargo.toml"), "[package]").await.expect("Failed to write sub Cargo.toml");
824        
825        let validator = RustValidator::new(temp_dir.path().to_path_buf())
826            .expect("Should create validator");
827        
828        let cargo_files = validator.find_cargo_files().await.expect("Should find files");
829        
830        assert_eq!(cargo_files.len(), 2);
831        assert!(cargo_files.iter().all(|f| f.file_name().unwrap() == "Cargo.toml"));
832    }
833
834    #[tokio::test]
835    async fn test_validate_project_integration() {
836        let temp_dir = TempDir::new().expect("Failed to create temp directory");
837        
838        // Create a basic Rust project structure
839        let src_dir = temp_dir.path().join("src");
840        fs::create_dir(&src_dir).await.expect("Failed to create src dir");
841        
842        // Cargo.toml with correct edition
843        fs::write(temp_dir.path().join("Cargo.toml"), r#"
844[package]
845name = "test"
846version = "0.1.0"
847edition = "2024"
848"#).await.expect("Failed to write Cargo.toml");
849        
850        // Good Rust file
851        fs::write(src_dir.join("lib.rs"), r#"
852//! Test library
853
854pub fn add(a: i32, b: i32) -> i32 {
855    a + b
856}
857
858#[cfg(test)]
859mod tests {
860    use super::*;
861
862    #[test]
863    fn test_add() {
864        assert_eq!(add(2, 3), 5);
865    }
866}
867"#).await.expect("Failed to write lib.rs");
868        
869        let validator = RustValidator::new(temp_dir.path().to_path_buf())
870            .expect("Should create validator");
871        
872        let violations = validator.validate_project().await.expect("Should validate");
873        
874        // Should pass validation (only rust version check might fail depending on system)
875        let non_rust_version_violations: Vec<_> = violations.iter()
876            .filter(|v| v.violation_type != ViolationType::OldRustVersion)
877            .collect();
878        
879        assert!(non_rust_version_violations.is_empty());
880    }
881
882    #[test]
883    fn test_serialization() {
884        let violation = Violation {
885            violation_type: ViolationType::UnderscoreBandaid,
886            file: PathBuf::from("test.rs"),
887            line: 10,
888            message: "Test violation".to_string(),
889            severity: Severity::Error,
890        };
891        
892        let serialized = serde_json::to_string(&violation).expect("Should serialize");
893        let deserialized: Violation = serde_json::from_str(&serialized).expect("Should deserialize");
894        
895        assert_eq!(violation.violation_type, deserialized.violation_type);
896        assert_eq!(violation.file, deserialized.file);
897        assert_eq!(violation.line, deserialized.line);
898        assert_eq!(violation.message, deserialized.message);
899    }
900
901    #[tokio::test]
902    async fn test_clippy_run() {
903        let temp_dir = TempDir::new().expect("Failed to create temp directory");
904        
905        // Create a minimal Cargo.toml
906        fs::write(temp_dir.path().join("Cargo.toml"), r#"
907[package]
908name = "test"
909version = "0.1.0"
910edition = "2024"
911"#).await.expect("Failed to write Cargo.toml");
912        
913        // Create src directory with basic lib.rs
914        let src_dir = temp_dir.path().join("src");
915        fs::create_dir(&src_dir).await.expect("Failed to create src dir");
916        fs::write(src_dir.join("lib.rs"), "// Empty lib").await.expect("Failed to write lib.rs");
917        
918        let validator = RustValidator::new(temp_dir.path().to_path_buf())
919            .expect("Should create validator");
920        
921        let result = validator.run_clippy().await;
922        
923        // The result might succeed or fail depending on the environment
924        // but we should get a ClippyResult
925        match result {
926            Ok(clippy_result) => {
927                assert!(!clippy_result.output.is_empty());
928            }
929            Err(_) => {
930                // This is acceptable as clippy might not be available in test environment
931            }
932        }
933    }
934
935    // Edge case tests
936    #[tokio::test]
937    async fn test_empty_rust_file() {
938        let temp_dir = TempDir::new().expect("Failed to create temp directory");
939        let rust_file = temp_dir.path().join("empty.rs");
940        
941        fs::write(&rust_file, "").await.expect("Failed to write empty file");
942        
943        let validator = RustValidator::new(temp_dir.path().to_path_buf())
944            .expect("Should create validator");
945        
946        let mut violations = Vec::new();
947        validator.validate_rust_file(&rust_file, &mut violations)
948            .await
949            .expect("Should validate");
950        
951        // Empty file should not generate violations
952        assert!(violations.is_empty());
953    }
954
955    #[tokio::test]
956    async fn test_function_size_limit() {
957        let temp_dir = TempDir::new().expect("Failed to create temp directory");
958        let rust_file = temp_dir.path().join("test.rs");
959        
960        // Create a function with more than 50 lines
961        let mut content = String::from("fn large_function() {\n");
962        for i in 0..60 {
963            content.push_str(&format!("    let x{} = {};\n", i, i));
964        }
965        content.push_str("}\n\nfn small_function() {\n    let x = 1;\n}\n");
966        
967        fs::write(&rust_file, content).await.expect("Failed to write Rust file");
968        
969        let validator = RustValidator::new(temp_dir.path().to_path_buf())
970            .expect("Should create validator");
971        
972        let mut violations = Vec::new();
973        validator.validate_rust_file(&rust_file, &mut violations)
974            .await
975            .expect("Should validate");
976        
977        let function_size_violations: Vec<_> = violations.iter()
978            .filter(|v| v.violation_type == ViolationType::FunctionTooLarge)
979            .collect();
980        
981        assert_eq!(function_size_violations.len(), 1);
982        assert!(function_size_violations[0].message.contains("63 lines"));
983    }
984}