ferrous_forge/validation/
rust_validator.rs1pub 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#[derive(Debug, Clone)]
17pub struct ClippyResult {
18 pub success: bool,
20 pub output: String,
22}
23
24pub struct RustValidator {
26 project_root: PathBuf,
28 patterns: ValidationPatterns,
30 config: Config,
32}
33
34impl RustValidator {
35 pub fn new(project_root: PathBuf) -> Result<Self> {
37 Self::with_config(project_root, Config::default())
38 }
39
40 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 pub fn patterns(&self) -> &ValidationPatterns {
52 &self.patterns
53 }
54
55 pub async fn validate_project(&self) -> Result<Vec<Violation>> {
57 let mut violations = Vec::new();
58
59 self.check_rust_version(&mut violations).await?;
61
62 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 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 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 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 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 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 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 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 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 }
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
316pub use patterns::is_in_string_literal;