1use crate::{Result, Error};
12use regex::Regex;
13use serde::{Deserialize, Serialize};
14use std::path::{Path, PathBuf};
15use std::process::Command;
16use tokio::fs;
17
18#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
20pub enum ViolationType {
21 UnderscoreBandaid,
23 WrongEdition,
25 FileTooLarge,
27 FunctionTooLarge,
29 LineTooLong,
31 UnwrapInProduction,
33 MissingDocs,
35 MissingDependencies,
37 OldRustVersion,
39}
40
41#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct Violation {
44 pub violation_type: ViolationType,
46 pub file: PathBuf,
48 pub line: usize,
50 pub message: String,
52 pub severity: Severity,
54}
55
56#[derive(Debug, Clone, Serialize, Deserialize)]
58pub enum Severity {
59 Error,
61 Warning,
63 Info,
65}
66
67#[derive(Debug)]
69pub struct ClippyResult {
70 pub success: bool,
72 pub output: String,
74}
75
76pub struct RustValidator {
78 project_root: PathBuf,
80 patterns: ValidationPatterns,
82 _required_crates: Vec<String>,
84}
85
86struct ValidationPatterns {
88 underscore_param: Regex,
89 underscore_let: Regex,
90 unwrap_call: Regex,
91 expect_call: Regex,
92 function_def: Regex,
93}
94
95impl RustValidator {
96 pub fn new(project_root: PathBuf) -> Result<Self> {
98 let patterns = ValidationPatterns {
99 underscore_param: Regex::new(r"fn\s+\w+\([^)]*_\w+\s*:[^)]*\)")
100 .map_err(|e| Error::validation(format!("Invalid regex: {}", e)))?,
101 underscore_let: Regex::new(r"let\s+_\s*=")
102 .map_err(|e| Error::validation(format!("Invalid regex: {}", e)))?,
103 unwrap_call: Regex::new(r"\.unwrap\(\)")
104 .map_err(|e| Error::validation(format!("Invalid regex: {}", e)))?,
105 expect_call: Regex::new(r"\.expect\(")
106 .map_err(|e| Error::validation(format!("Invalid regex: {}", e)))?,
107 function_def: Regex::new(r"^(\s*)(?:pub\s+)?(?:async\s+)?fn\s+\w+")
108 .map_err(|e| Error::validation(format!("Invalid regex: {}", e)))?,
109 };
110
111 let required_crates = vec![
112 "tokio".to_string(),
113 "thiserror".to_string(),
114 "anyhow".to_string(),
115 "tracing".to_string(),
116 ];
117
118 Ok(Self {
119 project_root,
120 patterns,
121 _required_crates: required_crates,
122 })
123 }
124
125 pub async fn validate_project(&self) -> Result<Vec<Violation>> {
127 let mut violations = Vec::new();
128
129 self.check_rust_version(&mut violations).await?;
131
132 let rust_files = self.find_rust_files().await?;
134 let cargo_files = self.find_cargo_files().await?;
135
136 tracing::info!("Found {} Rust files and {} Cargo.toml files",
137 rust_files.len(), cargo_files.len());
138
139 for cargo_file in cargo_files {
141 self.validate_cargo_toml(&cargo_file, &mut violations).await?;
142 }
143
144 for rust_file in rust_files {
146 if rust_file.to_string_lossy().contains("target/") {
148 continue;
149 }
150
151 self.validate_rust_file(&rust_file, &mut violations).await?;
152 }
153
154 Ok(violations)
155 }
156
157 pub fn generate_report(&self, violations: &[Violation]) -> String {
159 if violations.is_empty() {
160 return "✅ All Rust validation checks passed! Code meets Ferrous Forge standards.".to_string();
161 }
162
163 let mut report = format!("❌ Found {} violations of Ferrous Forge standards:\n\n", violations.len());
164
165 let mut by_type = std::collections::HashMap::new();
167 for violation in violations {
168 by_type.entry(&violation.violation_type)
169 .or_insert_with(Vec::new)
170 .push(violation);
171 }
172
173 for (violation_type, violations) in by_type {
174 let type_name = format!("{:?}", violation_type)
175 .to_uppercase()
176 .replace('_', " ");
177
178 report.push_str(&format!("🚨 {} ({} violations):\n", type_name, violations.len()));
179
180 for violation in violations.iter().take(10) {
181 report.push_str(&format!(
182 " {}:{} - {}\n",
183 violation.file.display(),
184 violation.line + 1,
185 violation.message
186 ));
187 }
188
189 if violations.len() > 10 {
190 report.push_str(&format!(" ... and {} more\n", violations.len() - 10));
191 }
192
193 report.push('\n');
194 }
195
196 report
197 }
198
199 pub async fn run_clippy(&self) -> Result<ClippyResult> {
201 let output = Command::new("cargo")
202 .args(&[
203 "clippy",
204 "--all-features",
205 "--",
206 "-D", "warnings",
207 "-D", "clippy::unwrap_used",
208 "-D", "clippy::expect_used",
209 "-D", "clippy::panic",
210 "-D", "clippy::unimplemented",
211 "-D", "clippy::todo",
212 ])
213 .current_dir(&self.project_root)
214 .output()
215 .map_err(|e| Error::process(format!("Failed to run clippy: {}", e)))?;
216
217 Ok(ClippyResult {
218 success: output.status.success(),
219 output: String::from_utf8_lossy(&output.stdout).to_string()
220 + &String::from_utf8_lossy(&output.stderr),
221 })
222 }
223
224 async fn check_rust_version(&self, violations: &mut Vec<Violation>) -> Result<()> {
225 let output = Command::new("rustc")
226 .arg("--version")
227 .output()
228 .map_err(|_| Error::validation("Rust compiler not found"))?;
229
230 let version_line = String::from_utf8_lossy(&output.stdout);
231
232 if let Some(captures) = Regex::new(r"rustc (\d+)\.(\d+)\.(\d+)")
234 .unwrap()
235 .captures(&version_line)
236 {
237 let major: u32 = captures[1].parse().unwrap_or(0);
238 let minor: u32 = captures[2].parse().unwrap_or(0);
239
240 if major < 1 || (major == 1 && minor < 85) {
241 violations.push(Violation {
242 violation_type: ViolationType::OldRustVersion,
243 file: PathBuf::from("<system>"),
244 line: 0,
245 message: format!("Rust version {}.{} is too old. Minimum required: 1.85.0", major, minor),
246 severity: Severity::Error,
247 });
248 }
249 } else {
250 violations.push(Violation {
251 violation_type: ViolationType::OldRustVersion,
252 file: PathBuf::from("<system>"),
253 line: 0,
254 message: "Could not parse Rust version".to_string(),
255 severity: Severity::Error,
256 });
257 }
258
259 Ok(())
260 }
261
262 async fn find_rust_files(&self) -> Result<Vec<PathBuf>> {
263 let mut rust_files = Vec::new();
264 self.collect_rust_files_recursive(&self.project_root, &mut rust_files)?;
265 Ok(rust_files)
266 }
267
268 fn collect_rust_files_recursive(&self, path: &Path, rust_files: &mut Vec<PathBuf>) -> Result<()> {
269 if path.is_file() {
270 if let Some(ext) = path.extension() {
271 if ext == "rs" {
272 rust_files.push(path.to_path_buf());
273 }
274 }
275 } else if path.is_dir() {
276 let entries = std::fs::read_dir(path)?;
277 for entry in entries {
278 let entry = entry?;
279 self.collect_rust_files_recursive(&entry.path(), rust_files)?;
280 }
281 }
282
283 Ok(())
284 }
285
286 async fn find_cargo_files(&self) -> Result<Vec<PathBuf>> {
287 let mut cargo_files = Vec::new();
288 self.collect_cargo_files_recursive(&self.project_root, &mut cargo_files)?;
289 Ok(cargo_files)
290 }
291
292 fn collect_cargo_files_recursive(&self, path: &Path, cargo_files: &mut Vec<PathBuf>) -> Result<()> {
293 if path.is_file() {
294 if path.file_name().and_then(|n| n.to_str()) == Some("Cargo.toml") {
295 cargo_files.push(path.to_path_buf());
296 }
297 } else if path.is_dir() && !path.to_string_lossy().contains("target/") {
298 let entries = std::fs::read_dir(path)?;
299 for entry in entries {
300 let entry = entry?;
301 self.collect_cargo_files_recursive(&entry.path(), cargo_files)?;
302 }
303 }
304
305 Ok(())
306 }
307
308 async fn validate_cargo_toml(&self, cargo_file: &Path, violations: &mut Vec<Violation>) -> Result<()> {
309 let content = fs::read_to_string(cargo_file).await?;
310 let lines: Vec<&str> = content.lines().collect();
311
312 let mut edition_found = false;
314 for (i, line) in lines.iter().enumerate() {
315 if line.contains("edition") {
316 if !line.contains("2024") {
317 violations.push(Violation {
318 violation_type: ViolationType::WrongEdition,
319 file: cargo_file.to_path_buf(),
320 line: i,
321 message: "Must use Edition 2024, not 2021 or older".to_string(),
322 severity: Severity::Error,
323 });
324 }
325 edition_found = true;
326 break;
327 }
328 }
329
330 if !edition_found {
331 violations.push(Violation {
332 violation_type: ViolationType::WrongEdition,
333 file: cargo_file.to_path_buf(),
334 line: 0,
335 message: "Missing edition specification - must be '2024'".to_string(),
336 severity: Severity::Error,
337 });
338 }
339
340 Ok(())
341 }
342
343 async fn validate_rust_file(&self, rust_file: &Path, violations: &mut Vec<Violation>) -> Result<()> {
344 let content = fs::read_to_string(rust_file).await?;
345 let lines: Vec<&str> = content.lines().collect();
346
347 if lines.len() > 300 {
349 violations.push(Violation {
350 violation_type: ViolationType::FileTooLarge,
351 file: rust_file.to_path_buf(),
352 line: lines.len() - 1,
353 message: format!("File has {} lines, maximum allowed is 300", lines.len()),
354 severity: Severity::Error,
355 });
356 }
357
358 for (i, line) in lines.iter().enumerate() {
360 if line.len() > 100 {
361 violations.push(Violation {
362 violation_type: ViolationType::LineTooLong,
363 file: rust_file.to_path_buf(),
364 line: i,
365 message: format!("Line has {} characters, maximum allowed is 100", line.len()),
366 severity: Severity::Warning,
367 });
368 }
369 }
370
371 let mut in_test_block = false;
372 let mut current_function_start: Option<usize> = None;
373
374 for (i, line) in lines.iter().enumerate() {
375 let line_stripped = line.trim();
376
377 if line_stripped.contains("#[test]") || line_stripped.contains("#[cfg(test)]") {
379 in_test_block = true;
380 }
381
382 if self.patterns.function_def.is_match(line) {
384 if let Some(start) = current_function_start {
386 let func_lines = i - start;
387 if func_lines > 50 {
388 violations.push(Violation {
389 violation_type: ViolationType::FunctionTooLarge,
390 file: rust_file.to_path_buf(),
391 line: start,
392 message: format!("Function has {} lines, maximum allowed is 50", func_lines),
393 severity: Severity::Error,
394 });
395 }
396 }
397 current_function_start = Some(i);
398 }
399
400 if self.patterns.underscore_param.is_match(line) {
402 violations.push(Violation {
403 violation_type: ViolationType::UnderscoreBandaid,
404 file: rust_file.to_path_buf(),
405 line: i,
406 message: "BANNED: Underscore parameter (_param) - fix the design instead of hiding warnings".to_string(),
407 severity: Severity::Error,
408 });
409 }
410
411 if self.patterns.underscore_let.is_match(line) {
412 violations.push(Violation {
413 violation_type: ViolationType::UnderscoreBandaid,
414 file: rust_file.to_path_buf(),
415 line: i,
416 message: "BANNED: Underscore assignment (let _ =) - handle errors properly".to_string(),
417 severity: Severity::Error,
418 });
419 }
420
421 if !in_test_block && self.patterns.unwrap_call.is_match(line) {
423 violations.push(Violation {
424 violation_type: ViolationType::UnwrapInProduction,
425 file: rust_file.to_path_buf(),
426 line: i,
427 message: "BANNED: .unwrap() in production code - use proper error handling with ?".to_string(),
428 severity: Severity::Error,
429 });
430 }
431
432 if !in_test_block && self.patterns.expect_call.is_match(line) {
434 violations.push(Violation {
435 violation_type: ViolationType::UnwrapInProduction,
436 file: rust_file.to_path_buf(),
437 line: i,
438 message: "BANNED: .expect() in production code - use proper error handling with ?".to_string(),
439 severity: Severity::Error,
440 });
441 }
442
443 if line_stripped.starts_with('}') && in_test_block {
445 in_test_block = false;
446 }
447 }
448
449 if let Some(start) = current_function_start {
451 let func_lines = lines.len() - start;
452 if func_lines > 50 {
453 violations.push(Violation {
454 violation_type: ViolationType::FunctionTooLarge,
455 file: rust_file.to_path_buf(),
456 line: start,
457 message: format!("Function has {} lines, maximum allowed is 50", func_lines),
458 severity: Severity::Error,
459 });
460 }
461 }
462
463 Ok(())
464 }
465}
466
467#[cfg(test)]
468mod tests {
469 use super::*;
470 use tempfile::TempDir;
471 use tokio::fs;
472
473 #[test]
474 fn test_violation_type_variants() {
475 let types = vec![
476 ViolationType::UnderscoreBandaid,
477 ViolationType::WrongEdition,
478 ViolationType::FileTooLarge,
479 ViolationType::FunctionTooLarge,
480 ViolationType::LineTooLong,
481 ViolationType::UnwrapInProduction,
482 ViolationType::MissingDocs,
483 ViolationType::MissingDependencies,
484 ViolationType::OldRustVersion,
485 ];
486
487 for (i, type1) in types.iter().enumerate() {
489 for (j, type2) in types.iter().enumerate() {
490 if i != j {
491 assert_ne!(type1, type2);
492 }
493 }
494 }
495 }
496
497 #[test]
498 fn test_severity_variants() {
499 let error = Severity::Error;
500 let warning = Severity::Warning;
501 let info = Severity::Info;
502
503 match error {
505 Severity::Error => {},
506 _ => panic!("Should be error"),
507 }
508 match warning {
509 Severity::Warning => {},
510 _ => panic!("Should be warning"),
511 }
512 match info {
513 Severity::Info => {},
514 _ => panic!("Should be info"),
515 }
516 }
517
518 #[test]
519 fn test_violation_creation() {
520 let violation = Violation {
521 violation_type: ViolationType::UnderscoreBandaid,
522 file: PathBuf::from("test.rs"),
523 line: 10,
524 message: "Test violation".to_string(),
525 severity: Severity::Error,
526 };
527
528 assert_eq!(violation.violation_type, ViolationType::UnderscoreBandaid);
529 assert_eq!(violation.file, PathBuf::from("test.rs"));
530 assert_eq!(violation.line, 10);
531 assert_eq!(violation.message, "Test violation");
532 matches!(violation.severity, Severity::Error);
533 }
534
535 #[test]
536 fn test_clippy_result() {
537 let result = ClippyResult {
538 success: true,
539 output: "All checks passed".to_string(),
540 };
541
542 assert!(result.success);
543 assert_eq!(result.output, "All checks passed");
544 }
545
546 #[tokio::test]
547 async fn test_rust_validator_creation() {
548 let temp_dir = TempDir::new().expect("Failed to create temp directory");
549 let validator = RustValidator::new(temp_dir.path().to_path_buf());
550
551 assert!(validator.is_ok());
552 let validator = validator.expect("Should create validator");
553 assert_eq!(validator.project_root, temp_dir.path());
554 }
555
556 #[tokio::test]
557 async fn test_generate_report_no_violations() {
558 let temp_dir = TempDir::new().expect("Failed to create temp directory");
559 let validator = RustValidator::new(temp_dir.path().to_path_buf())
560 .expect("Should create validator");
561
562 let violations = vec![];
563 let report = validator.generate_report(&violations);
564
565 assert!(report.contains("✅"));
566 assert!(report.contains("All Rust validation checks passed"));
567 }
568
569 #[tokio::test]
570 async fn test_generate_report_with_violations() {
571 let temp_dir = TempDir::new().expect("Failed to create temp directory");
572 let validator = RustValidator::new(temp_dir.path().to_path_buf())
573 .expect("Should create validator");
574
575 let violations = vec![
576 Violation {
577 violation_type: ViolationType::UnderscoreBandaid,
578 file: PathBuf::from("test.rs"),
579 line: 10,
580 message: "Underscore parameter".to_string(),
581 severity: Severity::Error,
582 },
583 Violation {
584 violation_type: ViolationType::WrongEdition,
585 file: PathBuf::from("Cargo.toml"),
586 line: 5,
587 message: "Wrong edition".to_string(),
588 severity: Severity::Error,
589 },
590 ];
591
592 let report = validator.generate_report(&violations);
593
594 assert!(report.contains("❌"));
595 assert!(report.contains("Found 2 violations"));
596 assert!(report.contains("UNDERSCOREBANDAID"));
597 assert!(report.contains("WRONGEDITION"));
598 assert!(report.contains("test.rs:11"));
599 assert!(report.contains("Cargo.toml:6"));
600 }
601
602 #[tokio::test]
603 async fn test_validate_cargo_toml_correct_edition() {
604 let temp_dir = TempDir::new().expect("Failed to create temp directory");
605 let cargo_toml = temp_dir.path().join("Cargo.toml");
606
607 fs::write(&cargo_toml, r#"
608[package]
609name = "test"
610version = "0.1.0"
611edition = "2024"
612"#).await.expect("Failed to write Cargo.toml");
613
614 let validator = RustValidator::new(temp_dir.path().to_path_buf())
615 .expect("Should create validator");
616
617 let mut violations = Vec::new();
618 validator.validate_cargo_toml(&cargo_toml, &mut violations)
619 .await
620 .expect("Should validate");
621
622 assert!(violations.is_empty());
623 }
624
625 #[tokio::test]
626 async fn test_validate_cargo_toml_wrong_edition() {
627 let temp_dir = TempDir::new().expect("Failed to create temp directory");
628 let cargo_toml = temp_dir.path().join("Cargo.toml");
629
630 fs::write(&cargo_toml, r#"
631[package]
632name = "test"
633version = "0.1.0"
634edition = "2021"
635"#).await.expect("Failed to write Cargo.toml");
636
637 let validator = RustValidator::new(temp_dir.path().to_path_buf())
638 .expect("Should create validator");
639
640 let mut violations = Vec::new();
641 validator.validate_cargo_toml(&cargo_toml, &mut violations)
642 .await
643 .expect("Should validate");
644
645 assert_eq!(violations.len(), 1);
646 assert_eq!(violations[0].violation_type, ViolationType::WrongEdition);
647 assert!(violations[0].message.contains("Edition 2024"));
648 }
649
650 #[tokio::test]
651 async fn test_validate_cargo_toml_missing_edition() {
652 let temp_dir = TempDir::new().expect("Failed to create temp directory");
653 let cargo_toml = temp_dir.path().join("Cargo.toml");
654
655 fs::write(&cargo_toml, r#"
656[package]
657name = "test"
658version = "0.1.0"
659"#).await.expect("Failed to write Cargo.toml");
660
661 let validator = RustValidator::new(temp_dir.path().to_path_buf())
662 .expect("Should create validator");
663
664 let mut violations = Vec::new();
665 validator.validate_cargo_toml(&cargo_toml, &mut violations)
666 .await
667 .expect("Should validate");
668
669 assert_eq!(violations.len(), 1);
670 assert_eq!(violations[0].violation_type, ViolationType::WrongEdition);
671 assert!(violations[0].message.contains("Missing edition"));
672 }
673
674 #[tokio::test]
675 async fn test_validate_rust_file_size_limit() {
676 let temp_dir = TempDir::new().expect("Failed to create temp directory");
677 let rust_file = temp_dir.path().join("test.rs");
678
679 let content = (0..350).map(|i| format!("// Line {}", i)).collect::<Vec<_>>().join("\n");
681 fs::write(&rust_file, content).await.expect("Failed to write Rust file");
682
683 let validator = RustValidator::new(temp_dir.path().to_path_buf())
684 .expect("Should create validator");
685
686 let mut violations = Vec::new();
687 validator.validate_rust_file(&rust_file, &mut violations)
688 .await
689 .expect("Should validate");
690
691 let file_size_violations: Vec<_> = violations.iter()
692 .filter(|v| v.violation_type == ViolationType::FileTooLarge)
693 .collect();
694
695 assert_eq!(file_size_violations.len(), 1);
696 assert!(file_size_violations[0].message.contains("350 lines"));
697 }
698
699 #[tokio::test]
700 async fn test_validate_rust_file_line_length() {
701 let temp_dir = TempDir::new().expect("Failed to create temp directory");
702 let rust_file = temp_dir.path().join("test.rs");
703
704 let long_line = "// ".to_string() + &"x".repeat(150);
705 fs::write(&rust_file, long_line).await.expect("Failed to write Rust file");
706
707 let validator = RustValidator::new(temp_dir.path().to_path_buf())
708 .expect("Should create validator");
709
710 let mut violations = Vec::new();
711 validator.validate_rust_file(&rust_file, &mut violations)
712 .await
713 .expect("Should validate");
714
715 let line_length_violations: Vec<_> = violations.iter()
716 .filter(|v| v.violation_type == ViolationType::LineTooLong)
717 .collect();
718
719 assert_eq!(line_length_violations.len(), 1);
720 assert!(line_length_violations[0].message.contains("153 characters"));
721 }
722
723 #[tokio::test]
724 async fn test_validate_rust_file_underscore_bandaid() {
725 let temp_dir = TempDir::new().expect("Failed to create temp directory");
726 let rust_file = temp_dir.path().join("test.rs");
727
728 let content = r#"
729fn test_function(_param: String) {
730 let _ = some_operation();
731}
732"#;
733 fs::write(&rust_file, content).await.expect("Failed to write Rust file");
734
735 let validator = RustValidator::new(temp_dir.path().to_path_buf())
736 .expect("Should create validator");
737
738 let mut violations = Vec::new();
739 validator.validate_rust_file(&rust_file, &mut violations)
740 .await
741 .expect("Should validate");
742
743 let bandaid_violations: Vec<_> = violations.iter()
744 .filter(|v| v.violation_type == ViolationType::UnderscoreBandaid)
745 .collect();
746
747 assert_eq!(bandaid_violations.len(), 2); assert!(bandaid_violations.iter().any(|v| v.message.contains("parameter")));
749 assert!(bandaid_violations.iter().any(|v| v.message.contains("assignment")));
750 }
751
752 #[tokio::test]
753 async fn test_validate_rust_file_unwrap_in_production() {
754 let temp_dir = TempDir::new().expect("Failed to create temp directory");
755 let rust_file = temp_dir.path().join("test.rs");
756
757 let content = r#"
758fn production_code() {
759 let value = some_result.unwrap();
760 let other = another_result.expect("message");
761}
762
763#[test]
764fn test_code() {
765 let value = some_result.unwrap(); // This should be allowed
766}
767"#;
768 fs::write(&rust_file, content).await.expect("Failed to write Rust file");
769
770 let validator = RustValidator::new(temp_dir.path().to_path_buf())
771 .expect("Should create validator");
772
773 let mut violations = Vec::new();
774 validator.validate_rust_file(&rust_file, &mut violations)
775 .await
776 .expect("Should validate");
777
778 let unwrap_violations: Vec<_> = violations.iter()
779 .filter(|v| v.violation_type == ViolationType::UnwrapInProduction)
780 .collect();
781
782 assert_eq!(unwrap_violations.len(), 2);
784 assert!(unwrap_violations.iter().any(|v| v.message.contains("unwrap")));
785 assert!(unwrap_violations.iter().any(|v| v.message.contains("expect")));
786 }
787
788 #[tokio::test]
789 async fn test_find_rust_files() {
790 let temp_dir = TempDir::new().expect("Failed to create temp directory");
791
792 let src_dir = temp_dir.path().join("src");
794 fs::create_dir(&src_dir).await.expect("Failed to create src dir");
795
796 fs::write(src_dir.join("main.rs"), "fn main() {}").await.expect("Failed to write main.rs");
797 fs::write(src_dir.join("lib.rs"), "// lib").await.expect("Failed to write lib.rs");
798 fs::write(temp_dir.path().join("build.rs"), "// build").await.expect("Failed to write build.rs");
799
800 fs::write(temp_dir.path().join("README.md"), "# Test").await.expect("Failed to write README");
802
803 let validator = RustValidator::new(temp_dir.path().to_path_buf())
804 .expect("Should create validator");
805
806 let rust_files = validator.find_rust_files().await.expect("Should find files");
807
808 assert_eq!(rust_files.len(), 3);
809 assert!(rust_files.iter().any(|f| f.file_name().unwrap() == "main.rs"));
810 assert!(rust_files.iter().any(|f| f.file_name().unwrap() == "lib.rs"));
811 assert!(rust_files.iter().any(|f| f.file_name().unwrap() == "build.rs"));
812 }
813
814 #[tokio::test]
815 async fn test_find_cargo_files() {
816 let temp_dir = TempDir::new().expect("Failed to create temp directory");
817
818 fs::write(temp_dir.path().join("Cargo.toml"), "[package]").await.expect("Failed to write Cargo.toml");
820
821 let sub_dir = temp_dir.path().join("sub_project");
822 fs::create_dir(&sub_dir).await.expect("Failed to create sub dir");
823 fs::write(sub_dir.join("Cargo.toml"), "[package]").await.expect("Failed to write sub Cargo.toml");
824
825 let validator = RustValidator::new(temp_dir.path().to_path_buf())
826 .expect("Should create validator");
827
828 let cargo_files = validator.find_cargo_files().await.expect("Should find files");
829
830 assert_eq!(cargo_files.len(), 2);
831 assert!(cargo_files.iter().all(|f| f.file_name().unwrap() == "Cargo.toml"));
832 }
833
834 #[tokio::test]
835 async fn test_validate_project_integration() {
836 let temp_dir = TempDir::new().expect("Failed to create temp directory");
837
838 let src_dir = temp_dir.path().join("src");
840 fs::create_dir(&src_dir).await.expect("Failed to create src dir");
841
842 fs::write(temp_dir.path().join("Cargo.toml"), r#"
844[package]
845name = "test"
846version = "0.1.0"
847edition = "2024"
848"#).await.expect("Failed to write Cargo.toml");
849
850 fs::write(src_dir.join("lib.rs"), r#"
852//! Test library
853
854pub fn add(a: i32, b: i32) -> i32 {
855 a + b
856}
857
858#[cfg(test)]
859mod tests {
860 use super::*;
861
862 #[test]
863 fn test_add() {
864 assert_eq!(add(2, 3), 5);
865 }
866}
867"#).await.expect("Failed to write lib.rs");
868
869 let validator = RustValidator::new(temp_dir.path().to_path_buf())
870 .expect("Should create validator");
871
872 let violations = validator.validate_project().await.expect("Should validate");
873
874 let non_rust_version_violations: Vec<_> = violations.iter()
876 .filter(|v| v.violation_type != ViolationType::OldRustVersion)
877 .collect();
878
879 assert!(non_rust_version_violations.is_empty());
880 }
881
882 #[test]
883 fn test_serialization() {
884 let violation = Violation {
885 violation_type: ViolationType::UnderscoreBandaid,
886 file: PathBuf::from("test.rs"),
887 line: 10,
888 message: "Test violation".to_string(),
889 severity: Severity::Error,
890 };
891
892 let serialized = serde_json::to_string(&violation).expect("Should serialize");
893 let deserialized: Violation = serde_json::from_str(&serialized).expect("Should deserialize");
894
895 assert_eq!(violation.violation_type, deserialized.violation_type);
896 assert_eq!(violation.file, deserialized.file);
897 assert_eq!(violation.line, deserialized.line);
898 assert_eq!(violation.message, deserialized.message);
899 }
900
901 #[tokio::test]
902 async fn test_clippy_run() {
903 let temp_dir = TempDir::new().expect("Failed to create temp directory");
904
905 fs::write(temp_dir.path().join("Cargo.toml"), r#"
907[package]
908name = "test"
909version = "0.1.0"
910edition = "2024"
911"#).await.expect("Failed to write Cargo.toml");
912
913 let src_dir = temp_dir.path().join("src");
915 fs::create_dir(&src_dir).await.expect("Failed to create src dir");
916 fs::write(src_dir.join("lib.rs"), "// Empty lib").await.expect("Failed to write lib.rs");
917
918 let validator = RustValidator::new(temp_dir.path().to_path_buf())
919 .expect("Should create validator");
920
921 let result = validator.run_clippy().await;
922
923 match result {
926 Ok(clippy_result) => {
927 assert!(!clippy_result.output.is_empty());
928 }
929 Err(_) => {
930 }
932 }
933 }
934
935 #[tokio::test]
937 async fn test_empty_rust_file() {
938 let temp_dir = TempDir::new().expect("Failed to create temp directory");
939 let rust_file = temp_dir.path().join("empty.rs");
940
941 fs::write(&rust_file, "").await.expect("Failed to write empty file");
942
943 let validator = RustValidator::new(temp_dir.path().to_path_buf())
944 .expect("Should create validator");
945
946 let mut violations = Vec::new();
947 validator.validate_rust_file(&rust_file, &mut violations)
948 .await
949 .expect("Should validate");
950
951 assert!(violations.is_empty());
953 }
954
955 #[tokio::test]
956 async fn test_function_size_limit() {
957 let temp_dir = TempDir::new().expect("Failed to create temp directory");
958 let rust_file = temp_dir.path().join("test.rs");
959
960 let mut content = String::from("fn large_function() {\n");
962 for i in 0..60 {
963 content.push_str(&format!(" let x{} = {};\n", i, i));
964 }
965 content.push_str("}\n\nfn small_function() {\n let x = 1;\n}\n");
966
967 fs::write(&rust_file, content).await.expect("Failed to write Rust file");
968
969 let validator = RustValidator::new(temp_dir.path().to_path_buf())
970 .expect("Should create validator");
971
972 let mut violations = Vec::new();
973 validator.validate_rust_file(&rust_file, &mut violations)
974 .await
975 .expect("Should validate");
976
977 let function_size_violations: Vec<_> = violations.iter()
978 .filter(|v| v.violation_type == ViolationType::FunctionTooLarge)
979 .collect();
980
981 assert_eq!(function_size_violations.len(), 1);
982 assert!(function_size_violations[0].message.contains("63 lines"));
983 }
984}