1use crate::{Error, Result};
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!(
137 "Found {} Rust files and {} Cargo.toml files",
138 rust_files.len(),
139 cargo_files.len()
140 );
141
142 for cargo_file in cargo_files {
144 self.validate_cargo_toml(&cargo_file, &mut violations)
145 .await?;
146 }
147
148 for rust_file in rust_files {
150 if rust_file.to_string_lossy().contains("target/") {
152 continue;
153 }
154
155 self.validate_rust_file(&rust_file, &mut violations).await?;
156 }
157
158 Ok(violations)
159 }
160
161 pub fn generate_report(&self, violations: &[Violation]) -> String {
163 if violations.is_empty() {
164 return "✅ All Rust validation checks passed! Code meets Ferrous Forge standards."
165 .to_string();
166 }
167
168 let mut report = format!(
169 "❌ Found {} violations of Ferrous Forge standards:\n\n",
170 violations.len()
171 );
172
173 let mut by_type = std::collections::HashMap::new();
175 for violation in violations {
176 by_type
177 .entry(&violation.violation_type)
178 .or_insert_with(Vec::new)
179 .push(violation);
180 }
181
182 for (violation_type, violations) in by_type {
183 let type_name = format!("{:?}", violation_type)
184 .to_uppercase()
185 .replace('_', " ");
186
187 report.push_str(&format!(
188 "🚨 {} ({} violations):\n",
189 type_name,
190 violations.len()
191 ));
192
193 for violation in violations.iter().take(10) {
194 report.push_str(&format!(
195 " {}:{} - {}\n",
196 violation.file.display(),
197 violation.line + 1,
198 violation.message
199 ));
200 }
201
202 if violations.len() > 10 {
203 report.push_str(&format!(" ... and {} more\n", violations.len() - 10));
204 }
205
206 report.push('\n');
207 }
208
209 report
210 }
211
212 pub async fn run_clippy(&self) -> Result<ClippyResult> {
214 let output = Command::new("cargo")
215 .args(&[
216 "clippy",
217 "--all-features",
218 "--",
219 "-D",
220 "warnings",
221 "-D",
222 "clippy::unwrap_used",
223 "-D",
224 "clippy::expect_used",
225 "-D",
226 "clippy::panic",
227 "-D",
228 "clippy::unimplemented",
229 "-D",
230 "clippy::todo",
231 ])
232 .current_dir(&self.project_root)
233 .output()
234 .map_err(|e| Error::process(format!("Failed to run clippy: {}", e)))?;
235
236 Ok(ClippyResult {
237 success: output.status.success(),
238 output: String::from_utf8_lossy(&output.stdout).to_string()
239 + &String::from_utf8_lossy(&output.stderr),
240 })
241 }
242
243 async fn check_rust_version(&self, violations: &mut Vec<Violation>) -> Result<()> {
244 let output = Command::new("rustc")
245 .arg("--version")
246 .output()
247 .map_err(|_| Error::validation("Rust compiler not found"))?;
248
249 let version_line = String::from_utf8_lossy(&output.stdout);
250
251 let version_regex = Regex::new(r"rustc (\d+)\.(\d+)\.(\d+)")
253 .map_err(|e| Error::validation(format!("Invalid regex: {}", e)))?;
254
255 if let Some(captures) = version_regex.captures(&version_line) {
256 let major: u32 = captures[1].parse().unwrap_or(0);
257 let minor: u32 = captures[2].parse().unwrap_or(0);
258
259 if major < 1 || (major == 1 && minor < 82) {
260 violations.push(Violation {
261 violation_type: ViolationType::OldRustVersion,
262 file: PathBuf::from("<system>"),
263 line: 0,
264 message: format!(
265 "Rust version {}.{} is too old. Minimum required: 1.82.0",
266 major, minor
267 ),
268 severity: Severity::Error,
269 });
270 }
271 } else {
272 violations.push(Violation {
273 violation_type: ViolationType::OldRustVersion,
274 file: PathBuf::from("<system>"),
275 line: 0,
276 message: "Could not parse Rust version".to_string(),
277 severity: Severity::Error,
278 });
279 }
280
281 Ok(())
282 }
283
284 async fn find_rust_files(&self) -> Result<Vec<PathBuf>> {
285 let mut rust_files = Vec::new();
286 self.collect_rust_files_recursive(&self.project_root, &mut rust_files)?;
287 Ok(rust_files)
288 }
289
290 fn collect_rust_files_recursive(
291 &self,
292 path: &Path,
293 rust_files: &mut Vec<PathBuf>,
294 ) -> Result<()> {
295 if path.is_file() {
296 if let Some(ext) = path.extension() {
297 if ext == "rs" {
298 rust_files.push(path.to_path_buf());
299 }
300 }
301 } else if path.is_dir() {
302 let entries = std::fs::read_dir(path)?;
303 for entry in entries {
304 let entry = entry?;
305 self.collect_rust_files_recursive(&entry.path(), rust_files)?;
306 }
307 }
308
309 Ok(())
310 }
311
312 async fn find_cargo_files(&self) -> Result<Vec<PathBuf>> {
313 let mut cargo_files = Vec::new();
314 self.collect_cargo_files_recursive(&self.project_root, &mut cargo_files)?;
315 Ok(cargo_files)
316 }
317
318 fn collect_cargo_files_recursive(
319 &self,
320 path: &Path,
321 cargo_files: &mut Vec<PathBuf>,
322 ) -> Result<()> {
323 if path.is_file() {
324 if path.file_name().and_then(|n| n.to_str()) == Some("Cargo.toml") {
325 cargo_files.push(path.to_path_buf());
326 }
327 } else if path.is_dir() && !path.to_string_lossy().contains("target/") {
328 let entries = std::fs::read_dir(path)?;
329 for entry in entries {
330 let entry = entry?;
331 self.collect_cargo_files_recursive(&entry.path(), cargo_files)?;
332 }
333 }
334
335 Ok(())
336 }
337
338 pub async fn validate_cargo_toml(
342 &self,
343 cargo_file: &Path,
344 violations: &mut Vec<Violation>,
345 ) -> Result<()> {
346 let content = fs::read_to_string(cargo_file).await?;
347 let lines: Vec<&str> = content.lines().collect();
348
349 let mut edition_found = false;
351 for (i, line) in lines.iter().enumerate() {
352 if line.contains("edition") {
353 if !line.contains("2021") && !line.contains("2024") {
354 violations.push(Violation {
355 violation_type: ViolationType::WrongEdition,
356 file: cargo_file.to_path_buf(),
357 line: i,
358 message: "Must use Edition 2021 or 2024".to_string(),
359 severity: Severity::Error,
360 });
361 }
362 edition_found = true;
363 break;
364 }
365 }
366
367 if !edition_found {
368 violations.push(Violation {
369 violation_type: ViolationType::WrongEdition,
370 file: cargo_file.to_path_buf(),
371 line: 0,
372 message: "Missing edition specification - must be '2021' or '2024'".to_string(),
373 severity: Severity::Error,
374 });
375 }
376
377 Ok(())
378 }
379
380 pub async fn validate_rust_file(
385 &self,
386 rust_file: &Path,
387 violations: &mut Vec<Violation>,
388 ) -> Result<()> {
389 let content = fs::read_to_string(rust_file).await?;
390 let lines: Vec<&str> = content.lines().collect();
391
392 if lines.len() > 300 {
394 violations.push(Violation {
395 violation_type: ViolationType::FileTooLarge,
396 file: rust_file.to_path_buf(),
397 line: lines.len() - 1,
398 message: format!("File has {} lines, maximum allowed is 300", lines.len()),
399 severity: Severity::Error,
400 });
401 }
402
403 for (i, line) in lines.iter().enumerate() {
405 if line.len() > 100 {
406 violations.push(Violation {
407 violation_type: ViolationType::LineTooLong,
408 file: rust_file.to_path_buf(),
409 line: i,
410 message: format!("Line has {} characters, maximum allowed is 100", line.len()),
411 severity: Severity::Warning,
412 });
413 }
414 }
415
416 let mut in_test_block = false;
417 let mut current_function_start: Option<usize> = None;
418
419 for (i, line) in lines.iter().enumerate() {
420 let line_stripped = line.trim();
421
422 if line_stripped.contains("[test]") || line_stripped.contains("[cfg(test)]") {
424 in_test_block = true;
425 }
426
427 if self.patterns.function_def.is_match(line) {
429 if let Some(start) = current_function_start {
431 let func_lines = i - start;
432 if func_lines > 50 {
433 violations.push(Violation {
434 violation_type: ViolationType::FunctionTooLarge,
435 file: rust_file.to_path_buf(),
436 line: start,
437 message: format!(
438 "Function has {} lines, maximum allowed is 50",
439 func_lines
440 ),
441 severity: Severity::Error,
442 });
443 }
444 }
445 current_function_start = Some(i);
446 }
447
448 if self.patterns.underscore_param.is_match(line) {
450 violations.push(Violation {
451 violation_type: ViolationType::UnderscoreBandaid,
452 file: rust_file.to_path_buf(),
453 line: i,
454 message: "BANNED: Underscore parameter (_param) - fix the design instead of hiding warnings".to_string(),
455 severity: Severity::Error,
456 });
457 }
458
459 if self.patterns.underscore_let.is_match(line) {
460 violations.push(Violation {
461 violation_type: ViolationType::UnderscoreBandaid,
462 file: rust_file.to_path_buf(),
463 line: i,
464 message: "BANNED: Underscore assignment (let _ =) - handle errors properly"
465 .to_string(),
466 severity: Severity::Error,
467 });
468 }
469
470 if !in_test_block && self.patterns.unwrap_call.is_match(line) {
472 violations.push(Violation {
473 violation_type: ViolationType::UnwrapInProduction,
474 file: rust_file.to_path_buf(),
475 line: i,
476 message:
477 "BANNED: .unwrap() in production code - use proper error handling with ?"
478 .to_string(),
479 severity: Severity::Error,
480 });
481 }
482
483 if !in_test_block && self.patterns.expect_call.is_match(line) {
485 violations.push(Violation {
486 violation_type: ViolationType::UnwrapInProduction,
487 file: rust_file.to_path_buf(),
488 line: i,
489 message:
490 "BANNED: .expect() in production code - use proper error handling with ?"
491 .to_string(),
492 severity: Severity::Error,
493 });
494 }
495
496 if line_stripped.starts_with('}') && in_test_block {
498 in_test_block = false;
499 }
500 }
501
502 if let Some(start) = current_function_start {
504 let func_lines = lines.len() - start;
505 if func_lines > 50 {
506 violations.push(Violation {
507 violation_type: ViolationType::FunctionTooLarge,
508 file: rust_file.to_path_buf(),
509 line: start,
510 message: format!("Function has {} lines, maximum allowed is 50", func_lines),
511 severity: Severity::Error,
512 });
513 }
514 }
515
516 Ok(())
517 }
518}
519
520#[cfg(test)]
521#[allow(clippy::expect_used)] #[allow(clippy::unwrap_used)] mod tests {
524 use super::*;
525 use tempfile::TempDir;
526 use tokio::fs;
527
528 #[test]
529 fn test_violation_type_variants() {
530 let types = [
531 ViolationType::UnderscoreBandaid,
532 ViolationType::WrongEdition,
533 ViolationType::FileTooLarge,
534 ViolationType::FunctionTooLarge,
535 ViolationType::LineTooLong,
536 ViolationType::UnwrapInProduction,
537 ViolationType::MissingDocs,
538 ViolationType::MissingDependencies,
539 ViolationType::OldRustVersion,
540 ];
541
542 for (i, type1) in types.iter().enumerate() {
544 for (j, type2) in types.iter().enumerate() {
545 if i != j {
546 assert_ne!(type1, type2);
547 }
548 }
549 }
550 }
551
552 #[test]
553 fn test_severity_variants() {
554 let error = Severity::Error;
555 let warning = Severity::Warning;
556 let info = Severity::Info;
557
558 assert!(matches!(error, Severity::Error));
560 assert!(matches!(warning, Severity::Warning));
561 assert!(matches!(info, Severity::Info));
562 }
563
564 #[test]
565 fn test_violation_creation() {
566 let violation = Violation {
567 violation_type: ViolationType::UnderscoreBandaid,
568 file: PathBuf::from("test.rs"),
569 line: 10,
570 message: "Test violation".to_string(),
571 severity: Severity::Error,
572 };
573
574 assert_eq!(violation.violation_type, ViolationType::UnderscoreBandaid);
575 assert_eq!(violation.file, PathBuf::from("test.rs"));
576 assert_eq!(violation.line, 10);
577 assert_eq!(violation.message, "Test violation");
578 matches!(violation.severity, Severity::Error);
579 }
580
581 #[test]
582 fn test_clippy_result() {
583 let result = ClippyResult {
584 success: true,
585 output: "All checks passed".to_string(),
586 };
587
588 assert!(result.success);
589 assert_eq!(result.output, "All checks passed");
590 }
591
592 #[tokio::test]
593 async fn test_rust_validator_creation() {
594 let temp_dir = TempDir::new().expect("Failed to create temp directory");
595 let validator = RustValidator::new(temp_dir.path().to_path_buf());
596
597 assert!(validator.is_ok());
598 let validator = validator.expect("Should create validator");
599 assert_eq!(validator.project_root, temp_dir.path());
600 }
601
602 #[tokio::test]
603 async fn test_generate_report_no_violations() {
604 let temp_dir = TempDir::new().expect("Failed to create temp directory");
605 let validator =
606 RustValidator::new(temp_dir.path().to_path_buf()).expect("Should create validator");
607
608 let violations = vec![];
609 let report = validator.generate_report(&violations);
610
611 assert!(report.contains("✅"));
612 assert!(report.contains("All Rust validation checks passed"));
613 }
614
615 #[tokio::test]
616 async fn test_generate_report_with_violations() {
617 let temp_dir = TempDir::new().expect("Failed to create temp directory");
618 let validator =
619 RustValidator::new(temp_dir.path().to_path_buf()).expect("Should create validator");
620
621 let violations = vec![
622 Violation {
623 violation_type: ViolationType::UnderscoreBandaid,
624 file: PathBuf::from("test.rs"),
625 line: 10,
626 message: "Underscore parameter".to_string(),
627 severity: Severity::Error,
628 },
629 Violation {
630 violation_type: ViolationType::WrongEdition,
631 file: PathBuf::from("Cargo.toml"),
632 line: 5,
633 message: "Wrong edition".to_string(),
634 severity: Severity::Error,
635 },
636 ];
637
638 let report = validator.generate_report(&violations);
639
640 assert!(report.contains("❌"));
641 assert!(report.contains("Found 2 violations"));
642 assert!(report.contains("UNDERSCOREBANDAID"));
643 assert!(report.contains("WRONGEDITION"));
644 assert!(report.contains("test.rs:11"));
645 assert!(report.contains("Cargo.toml:6"));
646 }
647
648 #[tokio::test]
649 async fn test_validate_cargo_toml_correct_edition() {
650 let temp_dir = TempDir::new().expect("Failed to create temp directory");
651 let cargo_toml = temp_dir.path().join("Cargo.toml");
652
653 fs::write(
654 &cargo_toml,
655 r#"
656[package]
657name = "test"
658version = "0.1.0"
659edition = "2024"
660"#,
661 )
662 .await
663 .expect("Failed to write Cargo.toml");
664
665 let validator =
666 RustValidator::new(temp_dir.path().to_path_buf()).expect("Should create validator");
667
668 let mut violations = Vec::new();
669 validator
670 .validate_cargo_toml(&cargo_toml, &mut violations)
671 .await
672 .expect("Should validate");
673
674 assert!(violations.is_empty());
675 }
676
677 #[tokio::test]
678 async fn test_validate_cargo_toml_wrong_edition() {
679 let temp_dir = TempDir::new().expect("Failed to create temp directory");
680 let cargo_toml = temp_dir.path().join("Cargo.toml");
681
682 fs::write(
683 &cargo_toml,
684 r#"
685[package]
686name = "test"
687version = "0.1.0"
688edition = "2021"
689"#,
690 )
691 .await
692 .expect("Failed to write Cargo.toml");
693
694 let validator =
695 RustValidator::new(temp_dir.path().to_path_buf()).expect("Should create validator");
696
697 let mut violations = Vec::new();
698 validator
699 .validate_cargo_toml(&cargo_toml, &mut violations)
700 .await
701 .expect("Should validate");
702
703 assert_eq!(violations.len(), 0); }
705
706 #[tokio::test]
707 async fn test_validate_cargo_toml_missing_edition() {
708 let temp_dir = TempDir::new().expect("Failed to create temp directory");
709 let cargo_toml = temp_dir.path().join("Cargo.toml");
710
711 fs::write(
712 &cargo_toml,
713 r#"
714[package]
715name = "test"
716version = "0.1.0"
717"#,
718 )
719 .await
720 .expect("Failed to write Cargo.toml");
721
722 let validator =
723 RustValidator::new(temp_dir.path().to_path_buf()).expect("Should create validator");
724
725 let mut violations = Vec::new();
726 validator
727 .validate_cargo_toml(&cargo_toml, &mut violations)
728 .await
729 .expect("Should validate");
730
731 assert_eq!(violations.len(), 1);
732 assert_eq!(violations[0].violation_type, ViolationType::WrongEdition);
733 assert!(violations[0].message.contains("Missing edition"));
734 }
735
736 #[tokio::test]
737 async fn test_validate_rust_file_size_limit() {
738 let temp_dir = TempDir::new().expect("Failed to create temp directory");
739 let rust_file = temp_dir.path().join("test.rs");
740
741 let content = (0..350)
743 .map(|i| format!("// Line {}", i))
744 .collect::<Vec<_>>()
745 .join("\n");
746 fs::write(&rust_file, content)
747 .await
748 .expect("Failed to write Rust file");
749
750 let validator =
751 RustValidator::new(temp_dir.path().to_path_buf()).expect("Should create validator");
752
753 let mut violations = Vec::new();
754 validator
755 .validate_rust_file(&rust_file, &mut violations)
756 .await
757 .expect("Should validate");
758
759 let file_size_violations: Vec<_> = violations
760 .iter()
761 .filter(|v| v.violation_type == ViolationType::FileTooLarge)
762 .collect();
763
764 assert_eq!(file_size_violations.len(), 1);
765 assert!(file_size_violations[0].message.contains("350 lines"));
766 }
767
768 #[tokio::test]
769 async fn test_validate_rust_file_line_length() {
770 let temp_dir = TempDir::new().expect("Failed to create temp directory");
771 let rust_file = temp_dir.path().join("test.rs");
772
773 let long_line = "// ".to_string() + &"x".repeat(150);
774 fs::write(&rust_file, long_line)
775 .await
776 .expect("Failed to write Rust file");
777
778 let validator =
779 RustValidator::new(temp_dir.path().to_path_buf()).expect("Should create validator");
780
781 let mut violations = Vec::new();
782 validator
783 .validate_rust_file(&rust_file, &mut violations)
784 .await
785 .expect("Should validate");
786
787 let line_length_violations: Vec<_> = violations
788 .iter()
789 .filter(|v| v.violation_type == ViolationType::LineTooLong)
790 .collect();
791
792 assert_eq!(line_length_violations.len(), 1);
793 assert!(line_length_violations[0].message.contains("153 characters"));
794 }
795
796 #[tokio::test]
797 async fn test_validate_rust_file_underscore_bandaid() {
798 let temp_dir = TempDir::new().expect("Failed to create temp directory");
799 let rust_file = temp_dir.path().join("test.rs");
800
801 let content = r"
802fn test_function(_param: String) {
803 let _ = some_operation();
804}
805";
806 fs::write(&rust_file, content)
807 .await
808 .expect("Failed to write Rust file");
809
810 let validator =
811 RustValidator::new(temp_dir.path().to_path_buf()).expect("Should create validator");
812
813 let mut violations = Vec::new();
814 validator
815 .validate_rust_file(&rust_file, &mut violations)
816 .await
817 .expect("Should validate");
818
819 let bandaid_violations: Vec<_> = violations
820 .iter()
821 .filter(|v| v.violation_type == ViolationType::UnderscoreBandaid)
822 .collect();
823
824 assert_eq!(bandaid_violations.len(), 2); assert!(bandaid_violations
826 .iter()
827 .any(|v| v.message.contains("parameter")));
828 assert!(bandaid_violations
829 .iter()
830 .any(|v| v.message.contains("assignment")));
831 }
832
833 #[tokio::test]
834 async fn test_validate_rust_file_unwrap_in_production() {
835 let temp_dir = TempDir::new().expect("Failed to create temp directory");
836 let rust_file = temp_dir.path().join("test.rs");
837
838 let content = r#"
839fn production_code() {
840 let value = some_result.unwrap();
841 let other = another_result.expect("message");
842}
843
844#[test]
845fn test_code() {
846 let value = some_result.unwrap(); // This should be allowed
847}
848"#;
849 fs::write(&rust_file, content)
850 .await
851 .expect("Failed to write Rust file");
852
853 let validator =
854 RustValidator::new(temp_dir.path().to_path_buf()).expect("Should create validator");
855
856 let mut violations = Vec::new();
857 validator
858 .validate_rust_file(&rust_file, &mut violations)
859 .await
860 .expect("Should validate");
861
862 let unwrap_violations: Vec<_> = violations
863 .iter()
864 .filter(|v| v.violation_type == ViolationType::UnwrapInProduction)
865 .collect();
866
867 assert_eq!(unwrap_violations.len(), 2);
869 assert!(unwrap_violations
870 .iter()
871 .any(|v| v.message.contains("unwrap")));
872 assert!(unwrap_violations
873 .iter()
874 .any(|v| v.message.contains("expect")));
875 }
876
877 #[tokio::test]
878 async fn test_find_rust_files() {
879 let temp_dir = TempDir::new().expect("Failed to create temp directory");
880
881 let src_dir = temp_dir.path().join("src");
883 fs::create_dir(&src_dir)
884 .await
885 .expect("Failed to create src dir");
886
887 fs::write(src_dir.join("main.rs"), "fn main() {}")
888 .await
889 .expect("Failed to write main.rs");
890 fs::write(src_dir.join("lib.rs"), "// lib")
891 .await
892 .expect("Failed to write lib.rs");
893 fs::write(temp_dir.path().join("build.rs"), "// build")
894 .await
895 .expect("Failed to write build.rs");
896
897 fs::write(temp_dir.path().join("README.md"), " Test")
899 .await
900 .expect("Failed to write README");
901
902 let validator =
903 RustValidator::new(temp_dir.path().to_path_buf()).expect("Should create validator");
904
905 let rust_files = validator
906 .find_rust_files()
907 .await
908 .expect("Should find files");
909
910 assert_eq!(rust_files.len(), 3);
911 assert!(rust_files
912 .iter()
913 .any(|f| f.file_name().expect("file name") == "main.rs"));
914 assert!(rust_files
915 .iter()
916 .any(|f| f.file_name().expect("file name") == "lib.rs"));
917 assert!(rust_files
918 .iter()
919 .any(|f| f.file_name().expect("file name") == "build.rs"));
920 }
921
922 #[tokio::test]
923 async fn test_find_cargo_files() {
924 let temp_dir = TempDir::new().expect("Failed to create temp directory");
925
926 fs::write(temp_dir.path().join("Cargo.toml"), "[package]")
928 .await
929 .expect("Failed to write Cargo.toml");
930
931 let sub_dir = temp_dir.path().join("sub_project");
932 fs::create_dir(&sub_dir)
933 .await
934 .expect("Failed to create sub dir");
935 fs::write(sub_dir.join("Cargo.toml"), "[package]")
936 .await
937 .expect("Failed to write sub Cargo.toml");
938
939 let validator =
940 RustValidator::new(temp_dir.path().to_path_buf()).expect("Should create validator");
941
942 let cargo_files = validator
943 .find_cargo_files()
944 .await
945 .expect("Should find files");
946
947 assert_eq!(cargo_files.len(), 2);
948 assert!(cargo_files
949 .iter()
950 .all(|f| f.file_name().expect("file name") == "Cargo.toml"));
951 }
952
953 #[tokio::test]
954 async fn test_validate_project_integration() {
955 let temp_dir = TempDir::new().expect("Failed to create temp directory");
956
957 let src_dir = temp_dir.path().join("src");
959 fs::create_dir(&src_dir)
960 .await
961 .expect("Failed to create src dir");
962
963 fs::write(
965 temp_dir.path().join("Cargo.toml"),
966 r#"
967[package]
968name = "test"
969version = "0.1.0"
970edition = "2024"
971"#,
972 )
973 .await
974 .expect("Failed to write Cargo.toml");
975
976 fs::write(
978 src_dir.join("lib.rs"),
979 r"
980//! Test library
981
982pub fn add(a: i32, b: i32) -> i32 {
983 a + b
984}
985
986#[cfg(test)]
987mod tests {
988 use super::*;
989
990 #[test]
991 fn test_add() {
992 assert_eq!(add(2, 3), 5);
993 }
994}
995",
996 )
997 .await
998 .expect("Failed to write lib.rs");
999
1000 let validator =
1001 RustValidator::new(temp_dir.path().to_path_buf()).expect("Should create validator");
1002
1003 let violations = validator.validate_project().await.expect("Should validate");
1004
1005 let non_rust_version_violations: Vec<_> = violations
1007 .iter()
1008 .filter(|v| v.violation_type != ViolationType::OldRustVersion)
1009 .collect();
1010
1011 assert!(non_rust_version_violations.is_empty());
1012 }
1013
1014 #[test]
1015 fn test_serialization() {
1016 let violation = Violation {
1017 violation_type: ViolationType::UnderscoreBandaid,
1018 file: PathBuf::from("test.rs"),
1019 line: 10,
1020 message: "Test violation".to_string(),
1021 severity: Severity::Error,
1022 };
1023
1024 let serialized = serde_json::to_string(&violation).expect("Should serialize");
1025 let deserialized: Violation =
1026 serde_json::from_str(&serialized).expect("Should deserialize");
1027
1028 assert_eq!(violation.violation_type, deserialized.violation_type);
1029 assert_eq!(violation.file, deserialized.file);
1030 assert_eq!(violation.line, deserialized.line);
1031 assert_eq!(violation.message, deserialized.message);
1032 }
1033
1034 #[tokio::test]
1035 async fn test_clippy_run() {
1036 let temp_dir = TempDir::new().expect("Failed to create temp directory");
1037
1038 fs::write(
1040 temp_dir.path().join("Cargo.toml"),
1041 r#"
1042[package]
1043name = "test"
1044version = "0.1.0"
1045edition = "2024"
1046"#,
1047 )
1048 .await
1049 .expect("Failed to write Cargo.toml");
1050
1051 let src_dir = temp_dir.path().join("src");
1053 fs::create_dir(&src_dir)
1054 .await
1055 .expect("Failed to create src dir");
1056 fs::write(src_dir.join("lib.rs"), "// Empty lib")
1057 .await
1058 .expect("Failed to write lib.rs");
1059
1060 let validator =
1061 RustValidator::new(temp_dir.path().to_path_buf()).expect("Should create validator");
1062
1063 let result = validator.run_clippy().await;
1064
1065 match result {
1068 Ok(clippy_result) => {
1069 assert!(!clippy_result.output.is_empty());
1070 }
1071 Err(_) => {
1072 }
1074 }
1075 }
1076
1077 #[tokio::test]
1079 async fn test_empty_rust_file() {
1080 let temp_dir = TempDir::new().expect("Failed to create temp directory");
1081 let rust_file = temp_dir.path().join("empty.rs");
1082
1083 fs::write(&rust_file, "")
1084 .await
1085 .expect("Failed to write empty file");
1086
1087 let validator =
1088 RustValidator::new(temp_dir.path().to_path_buf()).expect("Should create validator");
1089
1090 let mut violations = Vec::new();
1091 validator
1092 .validate_rust_file(&rust_file, &mut violations)
1093 .await
1094 .expect("Should validate");
1095
1096 assert!(violations.is_empty());
1098 }
1099
1100 #[tokio::test]
1101 async fn test_function_size_limit() {
1102 let temp_dir = TempDir::new().expect("Failed to create temp directory");
1103 let rust_file = temp_dir.path().join("test.rs");
1104
1105 let mut content = String::from("fn large_function() {\n");
1107 for i in 0..60 {
1108 content.push_str(&format!(" let x{} = {};\n", i, i));
1109 }
1110 content.push_str("}\n\nfn small_function() {\n let x = 1;\n}\n");
1111
1112 fs::write(&rust_file, content)
1113 .await
1114 .expect("Failed to write Rust file");
1115
1116 let validator =
1117 RustValidator::new(temp_dir.path().to_path_buf()).expect("Should create validator");
1118
1119 let mut violations = Vec::new();
1120 validator
1121 .validate_rust_file(&rust_file, &mut violations)
1122 .await
1123 .expect("Should validate");
1124
1125 let function_size_violations: Vec<_> = violations
1126 .iter()
1127 .filter(|v| v.violation_type == ViolationType::FunctionTooLarge)
1128 .collect();
1129
1130 assert_eq!(function_size_violations.len(), 1);
1131 assert!(function_size_violations[0].message.contains("63 lines"));
1132 }
1133}