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 Ok(violations)
103 }
104
105 pub fn generate_report(&self, violations: &[Violation]) -> String {
107 if violations.is_empty() {
108 return "✅ All Rust validation checks passed! Code meets Ferrous Forge standards."
109 .to_string();
110 }
111
112 let mut report = format!(
113 "❌ Found {} violations of Ferrous Forge standards:\n\n",
114 violations.len()
115 );
116
117 let grouped_violations = self.group_violations_by_type(violations);
118 self.add_violation_sections(&mut report, grouped_violations);
119
120 report
121 }
122
123 fn group_violations_by_type<'a>(
125 &self,
126 violations: &'a [Violation],
127 ) -> std::collections::HashMap<&'a ViolationType, Vec<&'a Violation>> {
128 let mut by_type = std::collections::HashMap::new();
129 for violation in violations {
130 by_type
131 .entry(&violation.violation_type)
132 .or_insert_with(Vec::new)
133 .push(violation);
134 }
135 by_type
136 }
137
138 fn add_violation_sections(
140 &self,
141 report: &mut String,
142 grouped_violations: std::collections::HashMap<&ViolationType, Vec<&Violation>>,
143 ) {
144 for (violation_type, violations) in grouped_violations {
145 let type_name = format!("{:?}", violation_type)
146 .to_uppercase()
147 .replace('_', " ");
148
149 report.push_str(&format!(
150 "🚨 {} ({} violations):\n",
151 type_name,
152 violations.len()
153 ));
154
155 self.add_violation_details(report, &violations);
156 report.push('\n');
157 }
158 }
159
160 fn add_violation_details(&self, report: &mut String, violations: &[&Violation]) {
162 for violation in violations.iter().take(10) {
163 report.push_str(&format!(
164 " {}:{} - {}\n",
165 violation.file.display(),
166 violation.line + 1,
167 violation.message
168 ));
169 }
170
171 if violations.len() > 10 {
172 report.push_str(&format!(" ... and {} more\n", violations.len() - 10));
173 }
174 }
175
176 pub async fn run_clippy(&self) -> Result<ClippyResult> {
182 let output = Command::new("cargo")
183 .args(&[
184 "clippy",
185 "--all-features",
186 "--",
187 "-D",
188 "warnings",
189 "-D",
190 "clippy::unwrap_used",
191 "-D",
192 "clippy::expect_used",
193 "-D",
194 "clippy::panic",
195 "-D",
196 "clippy::unimplemented",
197 "-D",
198 "clippy::todo",
199 ])
200 .current_dir(&self.project_root)
201 .output()
202 .map_err(|e| Error::process(format!("Failed to run clippy: {}", e)))?;
203
204 Ok(ClippyResult {
205 success: output.status.success(),
206 output: String::from_utf8_lossy(&output.stdout).to_string()
207 + &String::from_utf8_lossy(&output.stderr),
208 })
209 }
210
211 async fn check_rust_version(&self, violations: &mut Vec<Violation>) -> Result<()> {
212 let output = Command::new("rustc")
213 .arg("--version")
214 .output()
215 .map_err(|_| Error::validation("Rust compiler not found"))?;
216
217 let version_line = String::from_utf8_lossy(&output.stdout);
218
219 let version_regex = Regex::new(r"rustc (\d+)\.(\d+)\.(\d+)")
220 .map_err(|e| Error::validation(format!("Invalid regex: {}", e)))?;
221
222 if let Some(captures) = version_regex.captures(&version_line) {
223 let major: u32 = captures[1].parse().unwrap_or(0);
224 let minor: u32 = captures[2].parse().unwrap_or(0);
225
226 let min_minor = self.parse_required_minor();
228
229 if major < 1 || (major == 1 && minor < min_minor) {
230 violations.push(Violation {
231 violation_type: ViolationType::OldRustVersion,
232 file: PathBuf::from("<system>"),
233 line: 0,
234 message: format!(
235 "Rust version {}.{} is too old. Minimum required: {}",
236 major, minor, self.config.required_rust_version
237 ),
238 severity: Severity::Error,
239 });
240 }
241 } else {
242 violations.push(Violation {
243 violation_type: ViolationType::OldRustVersion,
244 file: PathBuf::from("<system>"),
245 line: 0,
246 message: "Could not parse Rust version".to_string(),
247 severity: Severity::Error,
248 });
249 }
250
251 Ok(())
252 }
253
254 fn parse_required_minor(&self) -> u32 {
256 let parts: Vec<&str> = self.config.required_rust_version.split('.').collect();
257 if parts.len() >= 2 {
258 parts[1].parse().unwrap_or(82)
259 } else {
260 82 }
262 }
263
264 async fn find_rust_files(&self) -> Result<Vec<PathBuf>> {
265 let mut rust_files = Vec::new();
266 self.collect_rust_files_recursive(&self.project_root, &mut rust_files)?;
267 Ok(rust_files)
268 }
269
270 fn collect_rust_files_recursive(
271 &self,
272 path: &Path,
273 rust_files: &mut Vec<PathBuf>,
274 ) -> Result<()> {
275 if path.to_string_lossy().contains("target/") {
276 return Ok(());
277 }
278
279 if path.is_file() {
280 if let Some(ext) = path.extension()
281 && ext == "rs"
282 {
283 rust_files.push(path.to_path_buf());
284 }
285 } else if path.is_dir() {
286 let entries = std::fs::read_dir(path)?;
287 for entry in entries {
288 let entry = entry?;
289 let entry_path = entry.path();
290 if entry_path.file_name() == Some(std::ffi::OsStr::new("target")) {
291 continue;
292 }
293 self.collect_rust_files_recursive(&entry_path, rust_files)?;
294 }
295 }
296
297 Ok(())
298 }
299
300 async fn find_cargo_files(&self) -> Result<Vec<PathBuf>> {
301 let mut cargo_files = Vec::new();
302 self.collect_cargo_files_recursive(&self.project_root, &mut cargo_files)?;
303 Ok(cargo_files)
304 }
305
306 fn collect_cargo_files_recursive(
307 &self,
308 path: &Path,
309 cargo_files: &mut Vec<PathBuf>,
310 ) -> Result<()> {
311 if path.to_string_lossy().contains("target/") {
312 return Ok(());
313 }
314
315 if path.is_file() {
316 if path.file_name().and_then(|n| n.to_str()) == Some("Cargo.toml") {
317 cargo_files.push(path.to_path_buf());
318 }
319 } else if path.is_dir() {
320 let entries = std::fs::read_dir(path)?;
321 for entry in entries {
322 let entry = entry?;
323 let entry_path = entry.path();
324 if entry_path.file_name() == Some(std::ffi::OsStr::new("target")) {
325 continue;
326 }
327 self.collect_cargo_files_recursive(&entry_path, cargo_files)?;
328 }
329 }
330
331 Ok(())
332 }
333}
334
335pub use patterns::is_in_string_literal;