ferrous_forge/validation/
rust_validator.rs

1//! Core Rust validation - modularized structure
2
3pub mod file_checks;
4pub mod patterns;
5
6use crate::validation::{Severity, Violation, ViolationType};
7use crate::{Error, Result};
8use file_checks::{validate_cargo_toml, validate_rust_file};
9use patterns::ValidationPatterns;
10use regex::Regex;
11use std::path::{Path, PathBuf};
12use std::process::Command;
13
14/// Result from running clippy
15#[derive(Debug, Clone)]
16pub struct ClippyResult {
17    /// Whether clippy ran successfully
18    pub success: bool,
19    /// Output from clippy command
20    pub output: String,
21}
22
23/// Core Rust validator
24pub struct RustValidator {
25    /// Root directory of the project to validate
26    project_root: PathBuf,
27    /// Compiled regex patterns for validation
28    patterns: ValidationPatterns,
29}
30
31impl RustValidator {
32    /// Create a new validator for the given project
33    pub fn new(project_root: PathBuf) -> Result<Self> {
34        let patterns = ValidationPatterns::new()?;
35        Ok(Self {
36            project_root,
37            patterns,
38        })
39    }
40
41    /// Get reference to validation patterns
42    pub fn patterns(&self) -> &ValidationPatterns {
43        &self.patterns
44    }
45
46    /// Validate all Rust code in the project
47    pub async fn validate_project(&self) -> Result<Vec<Violation>> {
48        let mut violations = Vec::new();
49
50        // Check Rust version
51        self.check_rust_version(&mut violations).await?;
52
53        // Find and validate all Cargo.toml files
54        let cargo_files = self.find_cargo_files().await?;
55        for cargo_file in cargo_files {
56            validate_cargo_toml(&cargo_file, &mut violations).await?;
57        }
58
59        // Find and validate all Rust source files
60        let rust_files = self.find_rust_files().await?;
61        for rust_file in rust_files {
62            validate_rust_file(&rust_file, &mut violations, &self.patterns).await?;
63        }
64
65        Ok(violations)
66    }
67
68    /// Generate a human-readable report from violations
69    pub fn generate_report(&self, violations: &[Violation]) -> String {
70        if violations.is_empty() {
71            return "✅ All Rust validation checks passed! Code meets Ferrous Forge standards."
72                .to_string();
73        }
74
75        let mut report = format!(
76            "❌ Found {} violations of Ferrous Forge standards:\n\n",
77            violations.len()
78        );
79
80        let grouped_violations = self.group_violations_by_type(violations);
81        self.add_violation_sections(&mut report, grouped_violations);
82
83        report
84    }
85
86    /// Group violations by their type
87    fn group_violations_by_type<'a>(
88        &self,
89        violations: &'a [Violation],
90    ) -> std::collections::HashMap<&'a ViolationType, Vec<&'a Violation>> {
91        let mut by_type = std::collections::HashMap::new();
92        for violation in violations {
93            by_type
94                .entry(&violation.violation_type)
95                .or_insert_with(Vec::new)
96                .push(violation);
97        }
98        by_type
99    }
100
101    /// Add violation sections to the report
102    fn add_violation_sections(
103        &self,
104        report: &mut String,
105        grouped_violations: std::collections::HashMap<&ViolationType, Vec<&Violation>>,
106    ) {
107        for (violation_type, violations) in grouped_violations {
108            let type_name = format!("{:?}", violation_type)
109                .to_uppercase()
110                .replace('_', " ");
111
112            report.push_str(&format!(
113                "🚨 {} ({} violations):\n",
114                type_name,
115                violations.len()
116            ));
117
118            self.add_violation_details(report, &violations);
119            report.push('\n');
120        }
121    }
122
123    /// Add individual violation details to the report
124    fn add_violation_details(&self, report: &mut String, violations: &[&Violation]) {
125        for violation in violations.iter().take(10) {
126            report.push_str(&format!(
127                "  {}:{} - {}\n",
128                violation.file.display(),
129                violation.line + 1,
130                violation.message
131            ));
132        }
133
134        if violations.len() > 10 {
135            report.push_str(&format!("  ... and {} more\n", violations.len() - 10));
136        }
137    }
138
139    /// Run clippy with strict configuration
140    pub async fn run_clippy(&self) -> Result<ClippyResult> {
141        let output = Command::new("cargo")
142            .args(&[
143                "clippy",
144                "--all-features",
145                "--",
146                "-D",
147                "warnings",
148                "-D",
149                "clippy::unwrap_used",
150                "-D",
151                "clippy::expect_used",
152                "-D",
153                "clippy::panic",
154                "-D",
155                "clippy::unimplemented",
156                "-D",
157                "clippy::todo",
158            ])
159            .current_dir(&self.project_root)
160            .output()
161            .map_err(|e| Error::process(format!("Failed to run clippy: {}", e)))?;
162
163        Ok(ClippyResult {
164            success: output.status.success(),
165            output: String::from_utf8_lossy(&output.stdout).to_string()
166                + &String::from_utf8_lossy(&output.stderr),
167        })
168    }
169
170    async fn check_rust_version(&self, violations: &mut Vec<Violation>) -> Result<()> {
171        let output = Command::new("rustc")
172            .arg("--version")
173            .output()
174            .map_err(|_| Error::validation("Rust compiler not found"))?;
175
176        let version_line = String::from_utf8_lossy(&output.stdout);
177
178        // Extract version (e.g., "rustc 1.85.0" -> "1.85.0")
179        let version_regex = Regex::new(r"rustc (\d+)\.(\d+)\.(\d+)")
180            .map_err(|e| Error::validation(format!("Invalid regex: {}", e)))?;
181
182        if let Some(captures) = version_regex.captures(&version_line) {
183            let major: u32 = captures[1].parse().unwrap_or(0);
184            let minor: u32 = captures[2].parse().unwrap_or(0);
185
186            if major < 1 || (major == 1 && minor < 82) {
187                violations.push(Violation {
188                    violation_type: ViolationType::OldRustVersion,
189                    file: PathBuf::from("<system>"),
190                    line: 0,
191                    message: format!(
192                        "Rust version {}.{} is too old. Minimum required: 1.82.0",
193                        major, minor
194                    ),
195                    severity: Severity::Error,
196                });
197            }
198        } else {
199            violations.push(Violation {
200                violation_type: ViolationType::OldRustVersion,
201                file: PathBuf::from("<system>"),
202                line: 0,
203                message: "Could not parse Rust version".to_string(),
204                severity: Severity::Error,
205            });
206        }
207
208        Ok(())
209    }
210
211    async fn find_rust_files(&self) -> Result<Vec<PathBuf>> {
212        let mut rust_files = Vec::new();
213        self.collect_rust_files_recursive(&self.project_root, &mut rust_files)?;
214        Ok(rust_files)
215    }
216
217    fn collect_rust_files_recursive(
218        &self,
219        path: &Path,
220        rust_files: &mut Vec<PathBuf>,
221    ) -> Result<()> {
222        // Skip any path containing target directory
223        if path.to_string_lossy().contains("target/") {
224            return Ok(());
225        }
226
227        if path.is_file() {
228            if let Some(ext) = path.extension() {
229                if ext == "rs" {
230                    rust_files.push(path.to_path_buf());
231                }
232            }
233        } else if path.is_dir() {
234            let entries = std::fs::read_dir(path)?;
235            for entry in entries {
236                let entry = entry?;
237                let entry_path = entry.path();
238                // Skip target directory entries
239                if entry_path.file_name() == Some(std::ffi::OsStr::new("target")) {
240                    continue;
241                }
242                self.collect_rust_files_recursive(&entry_path, rust_files)?;
243            }
244        }
245
246        Ok(())
247    }
248
249    async fn find_cargo_files(&self) -> Result<Vec<PathBuf>> {
250        let mut cargo_files = Vec::new();
251        self.collect_cargo_files_recursive(&self.project_root, &mut cargo_files)?;
252        Ok(cargo_files)
253    }
254
255    fn collect_cargo_files_recursive(
256        &self,
257        path: &Path,
258        cargo_files: &mut Vec<PathBuf>,
259    ) -> Result<()> {
260        // Skip any path containing target directory
261        if path.to_string_lossy().contains("target/") {
262            return Ok(());
263        }
264
265        if path.is_file() {
266            if path.file_name().and_then(|n| n.to_str()) == Some("Cargo.toml") {
267                cargo_files.push(path.to_path_buf());
268            }
269        } else if path.is_dir() {
270            let entries = std::fs::read_dir(path)?;
271            for entry in entries {
272                let entry = entry?;
273                let entry_path = entry.path();
274                // Skip target directory entries
275                if entry_path.file_name() == Some(std::ffi::OsStr::new("target")) {
276                    continue;
277                }
278                self.collect_cargo_files_recursive(&entry_path, cargo_files)?;
279            }
280        }
281
282        Ok(())
283    }
284}
285
286// Re-export for backwards compatibility
287pub use patterns::is_in_string_literal;