Skip to main content

ferrous_forge/validation/
rust_validator.rs

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