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::{Error, Result};
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!(
137            "Found {} Rust files and {} Cargo.toml files",
138            rust_files.len(),
139            cargo_files.len()
140        );
141
142        // Validate Cargo.toml files
143        for cargo_file in cargo_files {
144            self.validate_cargo_toml(&cargo_file, &mut violations)
145                .await?;
146        }
147
148        // Validate Rust source files
149        for rust_file in rust_files {
150            // Skip target directory
151            if rust_file.to_string_lossy().contains("target/") {
152                continue;
153            }
154
155            self.validate_rust_file(&rust_file, &mut violations).await?;
156        }
157
158        Ok(violations)
159    }
160
161    /// Generate a human-readable report from violations
162    pub fn generate_report(&self, violations: &[Violation]) -> String {
163        if violations.is_empty() {
164            return "✅ All Rust validation checks passed! Code meets Ferrous Forge standards."
165                .to_string();
166        }
167
168        let mut report = format!(
169            "❌ Found {} violations of Ferrous Forge standards:\n\n",
170            violations.len()
171        );
172
173        // Group by violation type
174        let mut by_type = std::collections::HashMap::new();
175        for violation in violations {
176            by_type
177                .entry(&violation.violation_type)
178                .or_insert_with(Vec::new)
179                .push(violation);
180        }
181
182        for (violation_type, violations) in by_type {
183            let type_name = format!("{:?}", violation_type)
184                .to_uppercase()
185                .replace('_', " ");
186
187            report.push_str(&format!(
188                "🚨 {} ({} violations):\n",
189                type_name,
190                violations.len()
191            ));
192
193            for violation in violations.iter().take(10) {
194                report.push_str(&format!(
195                    "  {}:{} - {}\n",
196                    violation.file.display(),
197                    violation.line + 1,
198                    violation.message
199                ));
200            }
201
202            if violations.len() > 10 {
203                report.push_str(&format!("  ... and {} more\n", violations.len() - 10));
204            }
205
206            report.push('\n');
207        }
208
209        report
210    }
211
212    /// Run clippy with strict configuration
213    pub async fn run_clippy(&self) -> Result<ClippyResult> {
214        let output = Command::new("cargo")
215            .args(&[
216                "clippy",
217                "--all-features",
218                "--",
219                "-D",
220                "warnings",
221                "-D",
222                "clippy::unwrap_used",
223                "-D",
224                "clippy::expect_used",
225                "-D",
226                "clippy::panic",
227                "-D",
228                "clippy::unimplemented",
229                "-D",
230                "clippy::todo",
231            ])
232            .current_dir(&self.project_root)
233            .output()
234            .map_err(|e| Error::process(format!("Failed to run clippy: {}", e)))?;
235
236        Ok(ClippyResult {
237            success: output.status.success(),
238            output: String::from_utf8_lossy(&output.stdout).to_string()
239                + &String::from_utf8_lossy(&output.stderr),
240        })
241    }
242
243    async fn check_rust_version(&self, violations: &mut Vec<Violation>) -> Result<()> {
244        let output = Command::new("rustc")
245            .arg("--version")
246            .output()
247            .map_err(|_| Error::validation("Rust compiler not found"))?;
248
249        let version_line = String::from_utf8_lossy(&output.stdout);
250
251        // Extract version (e.g., "rustc 1.85.0" -> "1.85.0")
252        let version_regex = Regex::new(r"rustc (\d+)\.(\d+)\.(\d+)")
253            .map_err(|e| Error::validation(format!("Invalid regex: {}", e)))?;
254
255        if let Some(captures) = version_regex.captures(&version_line) {
256            let major: u32 = captures[1].parse().unwrap_or(0);
257            let minor: u32 = captures[2].parse().unwrap_or(0);
258
259            if major < 1 || (major == 1 && minor < 82) {
260                violations.push(Violation {
261                    violation_type: ViolationType::OldRustVersion,
262                    file: PathBuf::from("<system>"),
263                    line: 0,
264                    message: format!(
265                        "Rust version {}.{} is too old. Minimum required: 1.82.0",
266                        major, minor
267                    ),
268                    severity: Severity::Error,
269                });
270            }
271        } else {
272            violations.push(Violation {
273                violation_type: ViolationType::OldRustVersion,
274                file: PathBuf::from("<system>"),
275                line: 0,
276                message: "Could not parse Rust version".to_string(),
277                severity: Severity::Error,
278            });
279        }
280
281        Ok(())
282    }
283
284    async fn find_rust_files(&self) -> Result<Vec<PathBuf>> {
285        let mut rust_files = Vec::new();
286        self.collect_rust_files_recursive(&self.project_root, &mut rust_files)?;
287        Ok(rust_files)
288    }
289
290    fn collect_rust_files_recursive(
291        &self,
292        path: &Path,
293        rust_files: &mut Vec<PathBuf>,
294    ) -> Result<()> {
295        if path.is_file() {
296            if let Some(ext) = path.extension() {
297                if ext == "rs" {
298                    rust_files.push(path.to_path_buf());
299                }
300            }
301        } else if path.is_dir() {
302            let entries = std::fs::read_dir(path)?;
303            for entry in entries {
304                let entry = entry?;
305                self.collect_rust_files_recursive(&entry.path(), rust_files)?;
306            }
307        }
308
309        Ok(())
310    }
311
312    async fn find_cargo_files(&self) -> Result<Vec<PathBuf>> {
313        let mut cargo_files = Vec::new();
314        self.collect_cargo_files_recursive(&self.project_root, &mut cargo_files)?;
315        Ok(cargo_files)
316    }
317
318    fn collect_cargo_files_recursive(
319        &self,
320        path: &Path,
321        cargo_files: &mut Vec<PathBuf>,
322    ) -> Result<()> {
323        if path.is_file() {
324            if path.file_name().and_then(|n| n.to_str()) == Some("Cargo.toml") {
325                cargo_files.push(path.to_path_buf());
326            }
327        } else if path.is_dir() && !path.to_string_lossy().contains("target/") {
328            let entries = std::fs::read_dir(path)?;
329            for entry in entries {
330                let entry = entry?;
331                self.collect_cargo_files_recursive(&entry.path(), cargo_files)?;
332            }
333        }
334
335        Ok(())
336    }
337
338    /// Validates a Cargo.toml file for standards compliance
339    ///
340    /// Checks that the Cargo.toml uses Edition 2021 or 2024.
341    pub async fn validate_cargo_toml(
342        &self,
343        cargo_file: &Path,
344        violations: &mut Vec<Violation>,
345    ) -> Result<()> {
346        let content = fs::read_to_string(cargo_file).await?;
347        let lines: Vec<&str> = content.lines().collect();
348
349        // Check for Edition 2021 or 2024
350        let mut edition_found = false;
351        for (i, line) in lines.iter().enumerate() {
352            if line.contains("edition") {
353                if !line.contains("2021") && !line.contains("2024") {
354                    violations.push(Violation {
355                        violation_type: ViolationType::WrongEdition,
356                        file: cargo_file.to_path_buf(),
357                        line: i,
358                        message: "Must use Edition 2021 or 2024".to_string(),
359                        severity: Severity::Error,
360                    });
361                }
362                edition_found = true;
363                break;
364            }
365        }
366
367        if !edition_found {
368            violations.push(Violation {
369                violation_type: ViolationType::WrongEdition,
370                file: cargo_file.to_path_buf(),
371                line: 0,
372                message: "Missing edition specification - must be '2021' or '2024'".to_string(),
373                severity: Severity::Error,
374            });
375        }
376
377        Ok(())
378    }
379
380    /// Validates a Rust source file for standards compliance
381    ///
382    /// Checks for file size limits, line length, function size,
383    /// underscore bandaids, and unwrap/expect usage.
384    pub async fn validate_rust_file(
385        &self,
386        rust_file: &Path,
387        violations: &mut Vec<Violation>,
388    ) -> Result<()> {
389        let content = fs::read_to_string(rust_file).await?;
390        let lines: Vec<&str> = content.lines().collect();
391
392        // Check file size limit (300 lines)
393        if lines.len() > 300 {
394            violations.push(Violation {
395                violation_type: ViolationType::FileTooLarge,
396                file: rust_file.to_path_buf(),
397                line: lines.len() - 1,
398                message: format!("File has {} lines, maximum allowed is 300", lines.len()),
399                severity: Severity::Error,
400            });
401        }
402
403        // Check line lengths (100 character limit)
404        for (i, line) in lines.iter().enumerate() {
405            if line.len() > 100 {
406                violations.push(Violation {
407                    violation_type: ViolationType::LineTooLong,
408                    file: rust_file.to_path_buf(),
409                    line: i,
410                    message: format!("Line has {} characters, maximum allowed is 100", line.len()),
411                    severity: Severity::Warning,
412                });
413            }
414        }
415
416        let mut in_test_block = false;
417        let mut current_function_start: Option<usize> = None;
418
419        for (i, line) in lines.iter().enumerate() {
420            let line_stripped = line.trim();
421
422            // Track test blocks
423            if line_stripped.contains("[test]") || line_stripped.contains("[cfg(test)]") {
424                in_test_block = true;
425            }
426
427            // Track function boundaries
428            if self.patterns.function_def.is_match(line) {
429                // Check previous function size
430                if let Some(start) = current_function_start {
431                    let func_lines = i - start;
432                    if func_lines > 50 {
433                        violations.push(Violation {
434                            violation_type: ViolationType::FunctionTooLarge,
435                            file: rust_file.to_path_buf(),
436                            line: start,
437                            message: format!(
438                                "Function has {} lines, maximum allowed is 50",
439                                func_lines
440                            ),
441                            severity: Severity::Error,
442                        });
443                    }
444                }
445                current_function_start = Some(i);
446            }
447
448            // Check for underscore bandaid coding
449            if self.patterns.underscore_param.is_match(line) {
450                violations.push(Violation {
451                    violation_type: ViolationType::UnderscoreBandaid,
452                    file: rust_file.to_path_buf(),
453                    line: i,
454                    message: "BANNED: Underscore parameter (_param) - fix the design instead of hiding warnings".to_string(),
455                    severity: Severity::Error,
456                });
457            }
458
459            if self.patterns.underscore_let.is_match(line) {
460                violations.push(Violation {
461                    violation_type: ViolationType::UnderscoreBandaid,
462                    file: rust_file.to_path_buf(),
463                    line: i,
464                    message: "BANNED: Underscore assignment (let _ =) - handle errors properly"
465                        .to_string(),
466                    severity: Severity::Error,
467                });
468            }
469
470            // Check for .unwrap() in production code (not in tests)
471            if !in_test_block && self.patterns.unwrap_call.is_match(line) {
472                violations.push(Violation {
473                    violation_type: ViolationType::UnwrapInProduction,
474                    file: rust_file.to_path_buf(),
475                    line: i,
476                    message:
477                        "BANNED: .unwrap() in production code - use proper error handling with ?"
478                            .to_string(),
479                    severity: Severity::Error,
480                });
481            }
482
483            // Check for .expect() in production code
484            if !in_test_block && self.patterns.expect_call.is_match(line) {
485                violations.push(Violation {
486                    violation_type: ViolationType::UnwrapInProduction,
487                    file: rust_file.to_path_buf(),
488                    line: i,
489                    message:
490                        "BANNED: .expect() in production code - use proper error handling with ?"
491                            .to_string(),
492                    severity: Severity::Error,
493                });
494            }
495
496            // Reset test block tracking
497            if line_stripped.starts_with('}') && in_test_block {
498                in_test_block = false;
499            }
500        }
501
502        // Check the last function if any
503        if let Some(start) = current_function_start {
504            let func_lines = lines.len() - start;
505            if func_lines > 50 {
506                violations.push(Violation {
507                    violation_type: ViolationType::FunctionTooLarge,
508                    file: rust_file.to_path_buf(),
509                    line: start,
510                    message: format!("Function has {} lines, maximum allowed is 50", func_lines),
511                    severity: Severity::Error,
512                });
513            }
514        }
515
516        Ok(())
517    }
518}
519
520#[cfg(test)]
521#[allow(clippy::expect_used)] // expect() is fine in tests
522#[allow(clippy::unwrap_used)] // unwrap() is fine in tests
523mod tests {
524    use super::*;
525    use tempfile::TempDir;
526    use tokio::fs;
527
528    #[test]
529    fn test_violation_type_variants() {
530        let types = [
531            ViolationType::UnderscoreBandaid,
532            ViolationType::WrongEdition,
533            ViolationType::FileTooLarge,
534            ViolationType::FunctionTooLarge,
535            ViolationType::LineTooLong,
536            ViolationType::UnwrapInProduction,
537            ViolationType::MissingDocs,
538            ViolationType::MissingDependencies,
539            ViolationType::OldRustVersion,
540        ];
541
542        // Test that variants are distinct
543        for (i, type1) in types.iter().enumerate() {
544            for (j, type2) in types.iter().enumerate() {
545                if i != j {
546                    assert_ne!(type1, type2);
547                }
548            }
549        }
550    }
551
552    #[test]
553    fn test_severity_variants() {
554        let error = Severity::Error;
555        let warning = Severity::Warning;
556        let info = Severity::Info;
557
558        // Just test that we can create instances
559        assert!(matches!(error, Severity::Error));
560        assert!(matches!(warning, Severity::Warning));
561        assert!(matches!(info, Severity::Info));
562    }
563
564    #[test]
565    fn test_violation_creation() {
566        let violation = Violation {
567            violation_type: ViolationType::UnderscoreBandaid,
568            file: PathBuf::from("test.rs"),
569            line: 10,
570            message: "Test violation".to_string(),
571            severity: Severity::Error,
572        };
573
574        assert_eq!(violation.violation_type, ViolationType::UnderscoreBandaid);
575        assert_eq!(violation.file, PathBuf::from("test.rs"));
576        assert_eq!(violation.line, 10);
577        assert_eq!(violation.message, "Test violation");
578        matches!(violation.severity, Severity::Error);
579    }
580
581    #[test]
582    fn test_clippy_result() {
583        let result = ClippyResult {
584            success: true,
585            output: "All checks passed".to_string(),
586        };
587
588        assert!(result.success);
589        assert_eq!(result.output, "All checks passed");
590    }
591
592    #[tokio::test]
593    async fn test_rust_validator_creation() {
594        let temp_dir = TempDir::new().expect("Failed to create temp directory");
595        let validator = RustValidator::new(temp_dir.path().to_path_buf());
596
597        assert!(validator.is_ok());
598        let validator = validator.expect("Should create validator");
599        assert_eq!(validator.project_root, temp_dir.path());
600    }
601
602    #[tokio::test]
603    async fn test_generate_report_no_violations() {
604        let temp_dir = TempDir::new().expect("Failed to create temp directory");
605        let validator =
606            RustValidator::new(temp_dir.path().to_path_buf()).expect("Should create validator");
607
608        let violations = vec![];
609        let report = validator.generate_report(&violations);
610
611        assert!(report.contains("✅"));
612        assert!(report.contains("All Rust validation checks passed"));
613    }
614
615    #[tokio::test]
616    async fn test_generate_report_with_violations() {
617        let temp_dir = TempDir::new().expect("Failed to create temp directory");
618        let validator =
619            RustValidator::new(temp_dir.path().to_path_buf()).expect("Should create validator");
620
621        let violations = vec![
622            Violation {
623                violation_type: ViolationType::UnderscoreBandaid,
624                file: PathBuf::from("test.rs"),
625                line: 10,
626                message: "Underscore parameter".to_string(),
627                severity: Severity::Error,
628            },
629            Violation {
630                violation_type: ViolationType::WrongEdition,
631                file: PathBuf::from("Cargo.toml"),
632                line: 5,
633                message: "Wrong edition".to_string(),
634                severity: Severity::Error,
635            },
636        ];
637
638        let report = validator.generate_report(&violations);
639
640        assert!(report.contains("❌"));
641        assert!(report.contains("Found 2 violations"));
642        assert!(report.contains("UNDERSCOREBANDAID"));
643        assert!(report.contains("WRONGEDITION"));
644        assert!(report.contains("test.rs:11"));
645        assert!(report.contains("Cargo.toml:6"));
646    }
647
648    #[tokio::test]
649    async fn test_validate_cargo_toml_correct_edition() {
650        let temp_dir = TempDir::new().expect("Failed to create temp directory");
651        let cargo_toml = temp_dir.path().join("Cargo.toml");
652
653        fs::write(
654            &cargo_toml,
655            r#"
656[package]
657name = "test"
658version = "0.1.0"
659edition = "2024"
660"#,
661        )
662        .await
663        .expect("Failed to write Cargo.toml");
664
665        let validator =
666            RustValidator::new(temp_dir.path().to_path_buf()).expect("Should create validator");
667
668        let mut violations = Vec::new();
669        validator
670            .validate_cargo_toml(&cargo_toml, &mut violations)
671            .await
672            .expect("Should validate");
673
674        assert!(violations.is_empty());
675    }
676
677    #[tokio::test]
678    async fn test_validate_cargo_toml_wrong_edition() {
679        let temp_dir = TempDir::new().expect("Failed to create temp directory");
680        let cargo_toml = temp_dir.path().join("Cargo.toml");
681
682        fs::write(
683            &cargo_toml,
684            r#"
685[package]
686name = "test"
687version = "0.1.0"
688edition = "2021"
689"#,
690        )
691        .await
692        .expect("Failed to write Cargo.toml");
693
694        let validator =
695            RustValidator::new(temp_dir.path().to_path_buf()).expect("Should create validator");
696
697        let mut violations = Vec::new();
698        validator
699            .validate_cargo_toml(&cargo_toml, &mut violations)
700            .await
701            .expect("Should validate");
702
703        assert_eq!(violations.len(), 0); // 2021 is now valid
704    }
705
706    #[tokio::test]
707    async fn test_validate_cargo_toml_missing_edition() {
708        let temp_dir = TempDir::new().expect("Failed to create temp directory");
709        let cargo_toml = temp_dir.path().join("Cargo.toml");
710
711        fs::write(
712            &cargo_toml,
713            r#"
714[package]
715name = "test"
716version = "0.1.0"
717"#,
718        )
719        .await
720        .expect("Failed to write Cargo.toml");
721
722        let validator =
723            RustValidator::new(temp_dir.path().to_path_buf()).expect("Should create validator");
724
725        let mut violations = Vec::new();
726        validator
727            .validate_cargo_toml(&cargo_toml, &mut violations)
728            .await
729            .expect("Should validate");
730
731        assert_eq!(violations.len(), 1);
732        assert_eq!(violations[0].violation_type, ViolationType::WrongEdition);
733        assert!(violations[0].message.contains("Missing edition"));
734    }
735
736    #[tokio::test]
737    async fn test_validate_rust_file_size_limit() {
738        let temp_dir = TempDir::new().expect("Failed to create temp directory");
739        let rust_file = temp_dir.path().join("test.rs");
740
741        // Create a file with over 300 lines
742        let content = (0..350)
743            .map(|i| format!("// Line {}", i))
744            .collect::<Vec<_>>()
745            .join("\n");
746        fs::write(&rust_file, content)
747            .await
748            .expect("Failed to write Rust file");
749
750        let validator =
751            RustValidator::new(temp_dir.path().to_path_buf()).expect("Should create validator");
752
753        let mut violations = Vec::new();
754        validator
755            .validate_rust_file(&rust_file, &mut violations)
756            .await
757            .expect("Should validate");
758
759        let file_size_violations: Vec<_> = violations
760            .iter()
761            .filter(|v| v.violation_type == ViolationType::FileTooLarge)
762            .collect();
763
764        assert_eq!(file_size_violations.len(), 1);
765        assert!(file_size_violations[0].message.contains("350 lines"));
766    }
767
768    #[tokio::test]
769    async fn test_validate_rust_file_line_length() {
770        let temp_dir = TempDir::new().expect("Failed to create temp directory");
771        let rust_file = temp_dir.path().join("test.rs");
772
773        let long_line = "// ".to_string() + &"x".repeat(150);
774        fs::write(&rust_file, long_line)
775            .await
776            .expect("Failed to write Rust file");
777
778        let validator =
779            RustValidator::new(temp_dir.path().to_path_buf()).expect("Should create validator");
780
781        let mut violations = Vec::new();
782        validator
783            .validate_rust_file(&rust_file, &mut violations)
784            .await
785            .expect("Should validate");
786
787        let line_length_violations: Vec<_> = violations
788            .iter()
789            .filter(|v| v.violation_type == ViolationType::LineTooLong)
790            .collect();
791
792        assert_eq!(line_length_violations.len(), 1);
793        assert!(line_length_violations[0].message.contains("153 characters"));
794    }
795
796    #[tokio::test]
797    async fn test_validate_rust_file_underscore_bandaid() {
798        let temp_dir = TempDir::new().expect("Failed to create temp directory");
799        let rust_file = temp_dir.path().join("test.rs");
800
801        let content = r"
802fn test_function(_param: String) {
803    let _ = some_operation();
804}
805";
806        fs::write(&rust_file, content)
807            .await
808            .expect("Failed to write Rust file");
809
810        let validator =
811            RustValidator::new(temp_dir.path().to_path_buf()).expect("Should create validator");
812
813        let mut violations = Vec::new();
814        validator
815            .validate_rust_file(&rust_file, &mut violations)
816            .await
817            .expect("Should validate");
818
819        let bandaid_violations: Vec<_> = violations
820            .iter()
821            .filter(|v| v.violation_type == ViolationType::UnderscoreBandaid)
822            .collect();
823
824        assert_eq!(bandaid_violations.len(), 2); // One for param, one for let
825        assert!(bandaid_violations
826            .iter()
827            .any(|v| v.message.contains("parameter")));
828        assert!(bandaid_violations
829            .iter()
830            .any(|v| v.message.contains("assignment")));
831    }
832
833    #[tokio::test]
834    async fn test_validate_rust_file_unwrap_in_production() {
835        let temp_dir = TempDir::new().expect("Failed to create temp directory");
836        let rust_file = temp_dir.path().join("test.rs");
837
838        let content = r#"
839fn production_code() {
840    let value = some_result.unwrap();
841    let other = another_result.expect("message");
842}
843
844#[test]
845fn test_code() {
846    let value = some_result.unwrap(); // This should be allowed
847}
848"#;
849        fs::write(&rust_file, content)
850            .await
851            .expect("Failed to write Rust file");
852
853        let validator =
854            RustValidator::new(temp_dir.path().to_path_buf()).expect("Should create validator");
855
856        let mut violations = Vec::new();
857        validator
858            .validate_rust_file(&rust_file, &mut violations)
859            .await
860            .expect("Should validate");
861
862        let unwrap_violations: Vec<_> = violations
863            .iter()
864            .filter(|v| v.violation_type == ViolationType::UnwrapInProduction)
865            .collect();
866
867        // Should find 2 violations in production code, but none in test code
868        assert_eq!(unwrap_violations.len(), 2);
869        assert!(unwrap_violations
870            .iter()
871            .any(|v| v.message.contains("unwrap")));
872        assert!(unwrap_violations
873            .iter()
874            .any(|v| v.message.contains("expect")));
875    }
876
877    #[tokio::test]
878    async fn test_find_rust_files() {
879        let temp_dir = TempDir::new().expect("Failed to create temp directory");
880
881        // Create some Rust files
882        let src_dir = temp_dir.path().join("src");
883        fs::create_dir(&src_dir)
884            .await
885            .expect("Failed to create src dir");
886
887        fs::write(src_dir.join("main.rs"), "fn main() {}")
888            .await
889            .expect("Failed to write main.rs");
890        fs::write(src_dir.join("lib.rs"), "// lib")
891            .await
892            .expect("Failed to write lib.rs");
893        fs::write(temp_dir.path().join("build.rs"), "// build")
894            .await
895            .expect("Failed to write build.rs");
896
897        // Create non-Rust file
898        fs::write(temp_dir.path().join("README.md"), " Test")
899            .await
900            .expect("Failed to write README");
901
902        let validator =
903            RustValidator::new(temp_dir.path().to_path_buf()).expect("Should create validator");
904
905        let rust_files = validator
906            .find_rust_files()
907            .await
908            .expect("Should find files");
909
910        assert_eq!(rust_files.len(), 3);
911        assert!(rust_files
912            .iter()
913            .any(|f| f.file_name().expect("file name") == "main.rs"));
914        assert!(rust_files
915            .iter()
916            .any(|f| f.file_name().expect("file name") == "lib.rs"));
917        assert!(rust_files
918            .iter()
919            .any(|f| f.file_name().expect("file name") == "build.rs"));
920    }
921
922    #[tokio::test]
923    async fn test_find_cargo_files() {
924        let temp_dir = TempDir::new().expect("Failed to create temp directory");
925
926        // Create Cargo.toml files
927        fs::write(temp_dir.path().join("Cargo.toml"), "[package]")
928            .await
929            .expect("Failed to write Cargo.toml");
930
931        let sub_dir = temp_dir.path().join("sub_project");
932        fs::create_dir(&sub_dir)
933            .await
934            .expect("Failed to create sub dir");
935        fs::write(sub_dir.join("Cargo.toml"), "[package]")
936            .await
937            .expect("Failed to write sub Cargo.toml");
938
939        let validator =
940            RustValidator::new(temp_dir.path().to_path_buf()).expect("Should create validator");
941
942        let cargo_files = validator
943            .find_cargo_files()
944            .await
945            .expect("Should find files");
946
947        assert_eq!(cargo_files.len(), 2);
948        assert!(cargo_files
949            .iter()
950            .all(|f| f.file_name().expect("file name") == "Cargo.toml"));
951    }
952
953    #[tokio::test]
954    async fn test_validate_project_integration() {
955        let temp_dir = TempDir::new().expect("Failed to create temp directory");
956
957        // Create a basic Rust project structure
958        let src_dir = temp_dir.path().join("src");
959        fs::create_dir(&src_dir)
960            .await
961            .expect("Failed to create src dir");
962
963        // Cargo.toml with correct edition
964        fs::write(
965            temp_dir.path().join("Cargo.toml"),
966            r#"
967[package]
968name = "test"
969version = "0.1.0"
970edition = "2024"
971"#,
972        )
973        .await
974        .expect("Failed to write Cargo.toml");
975
976        // Good Rust file
977        fs::write(
978            src_dir.join("lib.rs"),
979            r"
980//! Test library
981
982pub fn add(a: i32, b: i32) -> i32 {
983    a + b
984}
985
986#[cfg(test)]
987mod tests {
988    use super::*;
989
990    #[test]
991    fn test_add() {
992        assert_eq!(add(2, 3), 5);
993    }
994}
995",
996        )
997        .await
998        .expect("Failed to write lib.rs");
999
1000        let validator =
1001            RustValidator::new(temp_dir.path().to_path_buf()).expect("Should create validator");
1002
1003        let violations = validator.validate_project().await.expect("Should validate");
1004
1005        // Should pass validation (only rust version check might fail depending on system)
1006        let non_rust_version_violations: Vec<_> = violations
1007            .iter()
1008            .filter(|v| v.violation_type != ViolationType::OldRustVersion)
1009            .collect();
1010
1011        assert!(non_rust_version_violations.is_empty());
1012    }
1013
1014    #[test]
1015    fn test_serialization() {
1016        let violation = Violation {
1017            violation_type: ViolationType::UnderscoreBandaid,
1018            file: PathBuf::from("test.rs"),
1019            line: 10,
1020            message: "Test violation".to_string(),
1021            severity: Severity::Error,
1022        };
1023
1024        let serialized = serde_json::to_string(&violation).expect("Should serialize");
1025        let deserialized: Violation =
1026            serde_json::from_str(&serialized).expect("Should deserialize");
1027
1028        assert_eq!(violation.violation_type, deserialized.violation_type);
1029        assert_eq!(violation.file, deserialized.file);
1030        assert_eq!(violation.line, deserialized.line);
1031        assert_eq!(violation.message, deserialized.message);
1032    }
1033
1034    #[tokio::test]
1035    async fn test_clippy_run() {
1036        let temp_dir = TempDir::new().expect("Failed to create temp directory");
1037
1038        // Create a minimal Cargo.toml
1039        fs::write(
1040            temp_dir.path().join("Cargo.toml"),
1041            r#"
1042[package]
1043name = "test"
1044version = "0.1.0"
1045edition = "2024"
1046"#,
1047        )
1048        .await
1049        .expect("Failed to write Cargo.toml");
1050
1051        // Create src directory with basic lib.rs
1052        let src_dir = temp_dir.path().join("src");
1053        fs::create_dir(&src_dir)
1054            .await
1055            .expect("Failed to create src dir");
1056        fs::write(src_dir.join("lib.rs"), "// Empty lib")
1057            .await
1058            .expect("Failed to write lib.rs");
1059
1060        let validator =
1061            RustValidator::new(temp_dir.path().to_path_buf()).expect("Should create validator");
1062
1063        let result = validator.run_clippy().await;
1064
1065        // The result might succeed or fail depending on the environment
1066        // but we should get a ClippyResult
1067        match result {
1068            Ok(clippy_result) => {
1069                assert!(!clippy_result.output.is_empty());
1070            }
1071            Err(_) => {
1072                // This is acceptable as clippy might not be available in test environment
1073            }
1074        }
1075    }
1076
1077    // Edge case tests
1078    #[tokio::test]
1079    async fn test_empty_rust_file() {
1080        let temp_dir = TempDir::new().expect("Failed to create temp directory");
1081        let rust_file = temp_dir.path().join("empty.rs");
1082
1083        fs::write(&rust_file, "")
1084            .await
1085            .expect("Failed to write empty file");
1086
1087        let validator =
1088            RustValidator::new(temp_dir.path().to_path_buf()).expect("Should create validator");
1089
1090        let mut violations = Vec::new();
1091        validator
1092            .validate_rust_file(&rust_file, &mut violations)
1093            .await
1094            .expect("Should validate");
1095
1096        // Empty file should not generate violations
1097        assert!(violations.is_empty());
1098    }
1099
1100    #[tokio::test]
1101    async fn test_function_size_limit() {
1102        let temp_dir = TempDir::new().expect("Failed to create temp directory");
1103        let rust_file = temp_dir.path().join("test.rs");
1104
1105        // Create a function with more than 50 lines
1106        let mut content = String::from("fn large_function() {\n");
1107        for i in 0..60 {
1108            content.push_str(&format!("    let x{} = {};\n", i, i));
1109        }
1110        content.push_str("}\n\nfn small_function() {\n    let x = 1;\n}\n");
1111
1112        fs::write(&rust_file, content)
1113            .await
1114            .expect("Failed to write Rust file");
1115
1116        let validator =
1117            RustValidator::new(temp_dir.path().to_path_buf()).expect("Should create validator");
1118
1119        let mut violations = Vec::new();
1120        validator
1121            .validate_rust_file(&rust_file, &mut violations)
1122            .await
1123            .expect("Should validate");
1124
1125        let function_size_violations: Vec<_> = violations
1126            .iter()
1127            .filter(|v| v.violation_type == ViolationType::FunctionTooLarge)
1128            .collect();
1129
1130        assert_eq!(function_size_violations.len(), 1);
1131        assert!(function_size_violations[0].message.contains("63 lines"));
1132    }
1133}