ferrous_forge/safety/checks/
format.rs1use crate::Result;
4use std::path::Path;
5use std::process::Command;
6use std::time::Instant;
7
8use super::SafetyCheck;
9use crate::safety::{report::CheckResult, CheckType};
10
11pub struct FormatCheck;
13
14impl SafetyCheck for FormatCheck {
15 async fn run(project_path: &Path) -> Result<CheckResult> {
16 run(project_path).await
17 }
18
19 fn name() -> &'static str {
20 "format"
21 }
22
23 fn description() -> &'static str {
24 "Validates code formatting with rustfmt"
25 }
26}
27
28pub async fn run(project_path: &Path) -> Result<CheckResult> {
30 let start = Instant::now();
31 let mut result = CheckResult::new(CheckType::Format);
32
33 if which::which("cargo").is_err() {
35 result.add_error("cargo not found in PATH");
36 result.add_suggestion("Install Rust and cargo from https://rustup.rs");
37 result.set_duration(start.elapsed());
38 return Ok(result);
39 }
40
41 let output = Command::new("cargo")
43 .current_dir(project_path)
44 .args(&["fmt", "--check"])
45 .output()?;
46
47 result.set_duration(start.elapsed());
48
49 if !output.status.success() {
50 result.add_error("Code formatting violations found");
51 result.add_suggestion("Run 'cargo fmt' to fix formatting automatically");
52
53 let stdout = String::from_utf8_lossy(&output.stdout);
55 let stderr = String::from_utf8_lossy(&output.stderr);
56
57 for line in stdout.lines().chain(stderr.lines()) {
59 if line.starts_with("Diff in") {
60 result.add_error(format!("Formatting issue: {}", line));
61 } else if line.contains("rustfmt") && line.contains("failed") {
62 result.add_error(line.to_string());
63 }
64 }
65
66 if result.errors.len() == 1 {
68 result.add_context("Run 'cargo fmt' to see detailed formatting issues");
69 }
70 } else {
71 result.add_context("All code is properly formatted");
72 }
73
74 Ok(result)
75}
76
77#[cfg(test)]
78#[allow(clippy::unwrap_used, clippy::expect_used)]
79mod tests {
80 use super::*;
81 use tempfile::TempDir;
82 use tokio::fs;
83
84 #[tokio::test]
85 async fn test_format_check_on_empty_project() {
86 let temp_dir = TempDir::new().unwrap();
87
88 let cargo_toml = r#"
90[package]
91name = "test"
92version = "0.1.0"
93edition = "2021"
94"#;
95 fs::write(temp_dir.path().join("Cargo.toml"), cargo_toml)
96 .await
97 .unwrap();
98
99 fs::create_dir_all(temp_dir.path().join("src"))
101 .await
102 .unwrap();
103
104 let main_rs = r#"fn main() {
106 println!("Hello, world!");
107}
108"#;
109 fs::write(temp_dir.path().join("src/main.rs"), main_rs)
110 .await
111 .unwrap();
112
113 let result = run(temp_dir.path()).await.unwrap();
114
115 assert!(result.passed);
117 assert!(result.errors.is_empty());
118 }
119
120 #[test]
121 fn test_format_check_struct() {
122 assert_eq!(FormatCheck::name(), "format");
123 assert!(!FormatCheck::description().is_empty());
124 }
125}