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};
15use std::process::Command;
16
17#[derive(Debug, Clone)]
19pub struct ClippyResult {
20 pub success: bool,
22 pub output: String,
24}
25
26pub struct RustValidator {
28 project_root: PathBuf,
30 patterns: ValidationPatterns,
32 config: Config,
34}
35
36impl RustValidator {
37 pub fn new(project_root: PathBuf) -> Result<Self> {
43 Self::with_config(project_root, Config::default())
44 }
45
46 pub fn with_config(project_root: PathBuf, config: Config) -> Result<Self> {
52 let patterns = ValidationPatterns::new()?;
53 Ok(Self {
54 project_root,
55 patterns,
56 config,
57 })
58 }
59
60 pub fn patterns(&self) -> &ValidationPatterns {
62 &self.patterns
63 }
64
65 pub async fn validate_project(&self) -> Result<Vec<Violation>> {
72 let mut violations = Vec::new();
73
74 self.check_rust_version(&mut violations).await?;
76
77 let cargo_files = self.find_cargo_files().await?;
79 for cargo_file in cargo_files {
80 validate_cargo_toml_full(
81 &cargo_file,
82 &mut violations,
83 &self.config.required_edition,
84 &self.config.required_rust_version,
85 )
86 .await?;
87 }
88
89 let rust_files = self.find_rust_files().await?;
91 for rust_file in rust_files {
92 validate_rust_file(
93 &rust_file,
94 &mut violations,
95 &self.patterns,
96 self.config.max_file_lines,
97 self.config.max_function_lines,
98 )
99 .await?;
100 }
101
102 if self
104 .config
105 .validation
106 .check_version_consistency
107 .unwrap_or(true)
108 {
109 let version_validator = crate::validation::VersionConsistencyValidator::new(
110 self.project_root.clone(),
111 self.config.clone(),
112 )?;
113 let version_result = version_validator.validate().await?;
114 violations.extend(version_result.violations);
115 }
116
117 Ok(violations)
118 }
119
120 pub fn generate_report(&self, violations: &[Violation]) -> String {
122 if violations.is_empty() {
123 return "✅ All Rust validation checks passed! Code meets Ferrous Forge standards."
124 .to_string();
125 }
126
127 let mut report = format!(
128 "❌ Found {} violations of Ferrous Forge standards:\n\n",
129 violations.len()
130 );
131
132 let grouped_violations = self.group_violations_by_type(violations);
133 self.add_violation_sections(&mut report, grouped_violations);
134
135 report
136 }
137
138 fn group_violations_by_type<'a>(
140 &self,
141 violations: &'a [Violation],
142 ) -> std::collections::HashMap<&'a ViolationType, Vec<&'a Violation>> {
143 let mut by_type = std::collections::HashMap::new();
144 for violation in violations {
145 by_type
146 .entry(&violation.violation_type)
147 .or_insert_with(Vec::new)
148 .push(violation);
149 }
150 by_type
151 }
152
153 fn add_violation_sections(
155 &self,
156 report: &mut String,
157 grouped_violations: std::collections::HashMap<&ViolationType, Vec<&Violation>>,
158 ) {
159 for (violation_type, violations) in grouped_violations {
160 let type_name = format!("{:?}", violation_type)
161 .to_uppercase()
162 .replace('_', " ");
163
164 report.push_str(&format!(
165 "🚨 {} ({} violations):\n",
166 type_name,
167 violations.len()
168 ));
169
170 self.add_violation_details(report, &violations);
171 report.push('\n');
172 }
173 }
174
175 fn add_violation_details(&self, report: &mut String, violations: &[&Violation]) {
177 for violation in violations.iter().take(10) {
178 report.push_str(&format!(
179 " {}:{} - {}\n",
180 violation.file.display(),
181 violation.line + 1,
182 violation.message
183 ));
184 }
185
186 if violations.len() > 10 {
187 report.push_str(&format!(" ... and {} more\n", violations.len() - 10));
188 }
189 }
190
191 pub async fn run_clippy(&self) -> Result<ClippyResult> {
197 let output = Command::new("cargo")
198 .args(&[
199 "clippy",
200 "--all-features",
201 "--",
202 "-D",
203 "warnings",
204 "-D",
205 "clippy::unwrap_used",
206 "-D",
207 "clippy::expect_used",
208 "-D",
209 "clippy::panic",
210 "-D",
211 "clippy::unimplemented",
212 "-D",
213 "clippy::todo",
214 ])
215 .current_dir(&self.project_root)
216 .output()
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 = Command::new("rustc")
228 .arg("--version")
229 .output()
230 .map_err(|_| Error::validation("Rust compiler not found"))?;
231
232 let version_line = String::from_utf8_lossy(&output.stdout);
233
234 let version_regex = Regex::new(r"rustc (\d+)\.(\d+)\.(\d+)")
235 .map_err(|e| Error::validation(format!("Invalid regex: {}", e)))?;
236
237 if let Some(captures) = version_regex.captures(&version_line) {
238 let major: u32 = captures[1].parse().unwrap_or(0);
239 let minor: u32 = captures[2].parse().unwrap_or(0);
240
241 let min_minor = self.parse_required_minor();
243
244 if major < 1 || (major == 1 && minor < min_minor) {
245 violations.push(Violation {
246 violation_type: ViolationType::OldRustVersion,
247 file: PathBuf::from("<system>"),
248 line: 0,
249 message: format!(
250 "Rust version {}.{} is too old. Minimum required: {}",
251 major, minor, self.config.required_rust_version
252 ),
253 severity: Severity::Error,
254 });
255 }
256 } else {
257 violations.push(Violation {
258 violation_type: ViolationType::OldRustVersion,
259 file: PathBuf::from("<system>"),
260 line: 0,
261 message: "Could not parse Rust version".to_string(),
262 severity: Severity::Error,
263 });
264 }
265
266 Ok(())
267 }
268
269 fn parse_required_minor(&self) -> u32 {
271 let parts: Vec<&str> = self.config.required_rust_version.split('.').collect();
272 if parts.len() >= 2 {
273 parts[1].parse().unwrap_or(82)
274 } else {
275 82 }
277 }
278
279 async fn find_rust_files(&self) -> Result<Vec<PathBuf>> {
280 let mut rust_files = Vec::new();
281 self.collect_rust_files_recursive(&self.project_root, &mut rust_files)?;
282 Ok(rust_files)
283 }
284
285 fn collect_rust_files_recursive(
286 &self,
287 path: &Path,
288 rust_files: &mut Vec<PathBuf>,
289 ) -> Result<()> {
290 if path.to_string_lossy().contains("target/") {
291 return Ok(());
292 }
293
294 if path.is_file() {
295 if let Some(ext) = path.extension()
296 && ext == "rs"
297 {
298 rust_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_rust_files_recursive(&entry_path, rust_files)?;
309 }
310 }
311
312 Ok(())
313 }
314
315 async fn find_cargo_files(&self) -> Result<Vec<PathBuf>> {
316 let mut cargo_files = Vec::new();
317 self.collect_cargo_files_recursive(&self.project_root, &mut cargo_files)?;
318 Ok(cargo_files)
319 }
320
321 fn collect_cargo_files_recursive(
322 &self,
323 path: &Path,
324 cargo_files: &mut Vec<PathBuf>,
325 ) -> Result<()> {
326 if path.to_string_lossy().contains("target/") {
327 return Ok(());
328 }
329
330 if path.is_file() {
331 if path.file_name().and_then(|n| n.to_str()) == Some("Cargo.toml") {
332 cargo_files.push(path.to_path_buf());
333 }
334 } else if path.is_dir() {
335 let entries = std::fs::read_dir(path)?;
336 for entry in entries {
337 let entry = entry?;
338 let entry_path = entry.path();
339 if entry_path.file_name() == Some(std::ffi::OsStr::new("target")) {
340 continue;
341 }
342 self.collect_cargo_files_recursive(&entry_path, cargo_files)?;
343 }
344 }
345
346 Ok(())
347 }
348}
349
350pub use patterns::is_in_string_literal;