ferrous_forge/validation/
rust_validator.rs1pub mod file_checks;
5pub mod patterns;
7
8use crate::config::Config;
9use crate::validation::{Severity, Violation, ViolationType};
10use crate::{Error, Result};
11use file_checks::{validate_cargo_toml_full, validate_rust_file};
12use patterns::ValidationPatterns;
13use regex::Regex;
14use std::path::{Path, PathBuf};
15
16#[derive(Debug, Clone)]
18pub struct ClippyResult {
19 pub success: bool,
21 pub output: String,
23}
24
25pub struct RustValidator {
27 project_root: PathBuf,
29 patterns: ValidationPatterns,
31 config: Config,
33}
34
35impl RustValidator {
36 pub fn new(project_root: PathBuf) -> Result<Self> {
42 Self::with_config(project_root, Config::default())
43 }
44
45 pub fn with_config(project_root: PathBuf, config: Config) -> Result<Self> {
51 let patterns = ValidationPatterns::new()?;
52 Ok(Self {
53 project_root,
54 patterns,
55 config,
56 })
57 }
58
59 pub fn patterns(&self) -> &ValidationPatterns {
61 &self.patterns
62 }
63
64 pub async fn validate_project(&self) -> Result<Vec<Violation>> {
71 let mut violations = Vec::new();
72
73 self.check_rust_version(&mut violations).await?;
75
76 let cargo_files = self.find_cargo_files().await?;
78 for cargo_file in cargo_files {
79 validate_cargo_toml_full(
80 &cargo_file,
81 &mut violations,
82 &self.config.required_edition,
83 &self.config.required_rust_version,
84 )
85 .await?;
86 }
87
88 let rust_files = self.find_rust_files().await?;
90 for rust_file in rust_files {
91 validate_rust_file(
92 &rust_file,
93 &mut violations,
94 &self.patterns,
95 self.config.max_file_lines,
96 self.config.max_function_lines,
97 )
98 .await?;
99 }
100
101 if self
103 .config
104 .validation
105 .check_version_consistency
106 .unwrap_or(true)
107 {
108 let version_validator = crate::validation::VersionConsistencyValidator::new(
109 self.project_root.clone(),
110 self.config.clone(),
111 )?;
112 let version_result = version_validator.validate().await?;
113 violations.extend(version_result.violations);
114 }
115
116 Ok(violations)
117 }
118
119 pub fn generate_report(&self, violations: &[Violation]) -> String {
121 if violations.is_empty() {
122 return "✅ All Rust validation checks passed! Code meets Ferrous Forge standards."
123 .to_string();
124 }
125
126 let mut report = format!(
127 "❌ Found {} violations of Ferrous Forge standards:\n\n",
128 violations.len()
129 );
130
131 let grouped_violations = self.group_violations_by_type(violations);
132 self.add_violation_sections(&mut report, grouped_violations);
133
134 report
135 }
136
137 fn group_violations_by_type<'a>(
139 &self,
140 violations: &'a [Violation],
141 ) -> std::collections::HashMap<&'a ViolationType, Vec<&'a Violation>> {
142 let mut by_type = std::collections::HashMap::new();
143 for violation in violations {
144 by_type
145 .entry(&violation.violation_type)
146 .or_insert_with(Vec::new)
147 .push(violation);
148 }
149 by_type
150 }
151
152 fn add_violation_sections(
154 &self,
155 report: &mut String,
156 grouped_violations: std::collections::HashMap<&ViolationType, Vec<&Violation>>,
157 ) {
158 for (violation_type, violations) in grouped_violations {
159 let type_name = format!("{:?}", violation_type)
160 .to_uppercase()
161 .replace('_', " ");
162
163 report.push_str(&format!(
164 "🚨 {} ({} violations):\n",
165 type_name,
166 violations.len()
167 ));
168
169 self.add_violation_details(report, &violations);
170 report.push('\n');
171 }
172 }
173
174 fn add_violation_details(&self, report: &mut String, violations: &[&Violation]) {
176 for violation in violations.iter().take(10) {
177 report.push_str(&format!(
178 " {}:{} - {}\n",
179 violation.file.display(),
180 violation.line + 1,
181 violation.message
182 ));
183 }
184
185 if violations.len() > 10 {
186 report.push_str(&format!(" ... and {} more\n", violations.len() - 10));
187 }
188 }
189
190 pub async fn run_clippy(&self) -> Result<ClippyResult> {
196 let output = tokio::process::Command::new("cargo")
197 .args(&[
198 "clippy",
199 "--all-features",
200 "--",
201 "-D",
202 "warnings",
203 "-D",
204 "clippy::unwrap_used",
205 "-D",
206 "clippy::expect_used",
207 "-D",
208 "clippy::panic",
209 "-D",
210 "clippy::unimplemented",
211 "-D",
212 "clippy::todo",
213 ])
214 .current_dir(&self.project_root)
215 .output()
216 .await
217 .map_err(|e| Error::process(format!("Failed to run clippy: {}", e)))?;
218
219 Ok(ClippyResult {
220 success: output.status.success(),
221 output: String::from_utf8_lossy(&output.stdout).to_string()
222 + &String::from_utf8_lossy(&output.stderr),
223 })
224 }
225
226 async fn check_rust_version(&self, violations: &mut Vec<Violation>) -> Result<()> {
227 let output = tokio::process::Command::new("rustc")
228 .arg("--version")
229 .output()
230 .await
231 .map_err(|_| Error::validation("Rust compiler not found"))?;
232
233 let version_line = String::from_utf8_lossy(&output.stdout);
234
235 let version_regex = Regex::new(r"rustc (\d+)\.(\d+)\.(\d+)")
236 .map_err(|e| Error::validation(format!("Invalid regex: {}", e)))?;
237
238 if let Some(captures) = version_regex.captures(&version_line) {
239 let major: u32 = captures[1].parse().unwrap_or(0);
240 let minor: u32 = captures[2].parse().unwrap_or(0);
241
242 let min_minor = self.parse_required_minor();
244
245 if major < 1 || (major == 1 && minor < min_minor) {
246 violations.push(Violation {
247 violation_type: ViolationType::OldRustVersion,
248 file: PathBuf::from("<system>"),
249 line: 0,
250 message: format!(
251 "Rust version {}.{} is too old. Minimum required: {}",
252 major, minor, self.config.required_rust_version
253 ),
254 severity: Severity::Error,
255 });
256 }
257 } else {
258 violations.push(Violation {
259 violation_type: ViolationType::OldRustVersion,
260 file: PathBuf::from("<system>"),
261 line: 0,
262 message: "Could not parse Rust version".to_string(),
263 severity: Severity::Error,
264 });
265 }
266
267 Ok(())
268 }
269
270 fn parse_required_minor(&self) -> u32 {
272 let parts: Vec<&str> = self.config.required_rust_version.split('.').collect();
273 if parts.len() >= 2 {
274 parts[1].parse().unwrap_or(82)
275 } else {
276 82 }
278 }
279
280 async fn find_rust_files(&self) -> Result<Vec<PathBuf>> {
281 let root = self.project_root.clone();
282 tokio::task::spawn_blocking(move || {
283 let mut rust_files = Vec::new();
284 collect_rust_files_recursive(&root, &mut rust_files)?;
285 Ok(rust_files)
286 })
287 .await
288 .map_err(|e| Error::process(format!("Task join error: {}", e)))?
289 }
290
291 async fn find_cargo_files(&self) -> Result<Vec<PathBuf>> {
292 let root = self.project_root.clone();
293 tokio::task::spawn_blocking(move || {
294 let mut cargo_files = Vec::new();
295 collect_cargo_files_recursive(&root, &mut cargo_files)?;
296 Ok(cargo_files)
297 })
298 .await
299 .map_err(|e| Error::process(format!("Task join error: {}", e)))?
300 }
301}
302
303const SKIP_DIRS: &[&str] = &[
305 "target",
306 "node_modules",
307 ".git",
308 ".claude",
309 ".next",
310 "dist",
311 "build",
312 ".turbo",
313 ".pnpm",
314 ".yarn",
315 "__pycache__",
316 ".venv",
317 "vendor",
318];
319
320fn should_skip_dir(entry_path: &Path) -> bool {
322 entry_path
323 .file_name()
324 .and_then(|n| n.to_str())
325 .is_some_and(|name| SKIP_DIRS.contains(&name))
326}
327
328fn collect_rust_files_recursive(path: &Path, rust_files: &mut Vec<PathBuf>) -> Result<()> {
330 if should_skip_dir(path) {
331 return Ok(());
332 }
333
334 if path.is_file() {
335 if let Some(ext) = path.extension()
336 && ext == "rs"
337 {
338 rust_files.push(path.to_path_buf());
339 }
340 } else if path.is_dir() {
341 let entries = std::fs::read_dir(path)?;
342 for entry in entries {
343 let entry = entry?;
344 let entry_path = entry.path();
345 if should_skip_dir(&entry_path) {
346 continue;
347 }
348 collect_rust_files_recursive(&entry_path, rust_files)?;
349 }
350 }
351
352 Ok(())
353}
354
355fn collect_cargo_files_recursive(path: &Path, cargo_files: &mut Vec<PathBuf>) -> Result<()> {
357 if should_skip_dir(path) {
358 return Ok(());
359 }
360
361 if path.is_file() {
362 if path.file_name().and_then(|n| n.to_str()) == Some("Cargo.toml") {
363 cargo_files.push(path.to_path_buf());
364 }
365 } else if path.is_dir() {
366 let entries = std::fs::read_dir(path)?;
367 for entry in entries {
368 let entry = entry?;
369 let entry_path = entry.path();
370 if should_skip_dir(&entry_path) {
371 continue;
372 }
373 collect_cargo_files_recursive(&entry_path, cargo_files)?;
374 }
375 }
376
377 Ok(())
378}
379
380pub use patterns::is_in_string_literal;