ferrous_forge/validation/
rust_validator.rs1pub 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#[derive(Debug, Clone)]
16pub struct ClippyResult {
17 pub success: bool,
19 pub output: String,
21}
22
23pub struct RustValidator {
25 project_root: PathBuf,
27 patterns: ValidationPatterns,
29}
30
31impl RustValidator {
32 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 pub fn patterns(&self) -> &ValidationPatterns {
43 &self.patterns
44 }
45
46 pub async fn validate_project(&self) -> Result<Vec<Violation>> {
48 let mut violations = Vec::new();
49
50 self.check_rust_version(&mut violations).await?;
52
53 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 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 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 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 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 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 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 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 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 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 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 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
286pub use patterns::is_in_string_literal;