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::{CheckType, report::CheckResult};
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 let Err(error_msg) = check_cargo_availability() {
35 result.add_error(&error_msg);
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 = execute_format_check(project_path)?;
43 result.set_duration(start.elapsed());
44
45 if output.status.success() {
46 result.add_context("All code is properly formatted");
47 } else {
48 process_format_violations(&mut result, &output);
49 }
50
51 Ok(result)
52}
53
54fn check_cargo_availability() -> std::result::Result<(), String> {
56 which::which("cargo")
57 .map_err(|_| "cargo not found in PATH".to_string())
58 .map(|_| ())
59}
60
61fn execute_format_check(project_path: &Path) -> Result<std::process::Output> {
63 Command::new("cargo")
64 .current_dir(project_path)
65 .args(&["fmt", "--check"])
66 .output()
67 .map_err(Into::into)
68}
69
70fn process_format_violations(result: &mut CheckResult, output: &std::process::Output) {
72 result.add_error("Code formatting violations found");
73 result.add_suggestion("Run 'cargo fmt' to fix formatting automatically");
74
75 parse_format_output(result, output);
76 add_format_context_if_needed(result);
77}
78
79fn parse_format_output(result: &mut CheckResult, output: &std::process::Output) {
81 let stdout = String::from_utf8_lossy(&output.stdout);
82 let stderr = String::from_utf8_lossy(&output.stderr);
83
84 for line in stdout.lines().chain(stderr.lines()) {
85 if line.starts_with("Diff in") {
86 result.add_error(format!("Formatting issue: {}", line));
87 } else if line.contains("rustfmt") && line.contains("failed") {
88 result.add_error(line.to_string());
89 }
90 }
91}
92
93fn add_format_context_if_needed(result: &mut CheckResult) {
95 if result.errors.len() == 1 {
96 result.add_context("Run 'cargo fmt' to see detailed formatting issues");
97 }
98}
99
100#[cfg(test)]
101#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
102mod tests {
103 use super::*;
104 use tempfile::TempDir;
105 use tokio::fs;
106
107 #[tokio::test]
108 async fn test_format_check_on_empty_project() {
109 let temp_dir = TempDir::new().unwrap();
110
111 let cargo_toml = r#"[package]
113name = "test"
114version = "0.1.0"
115edition = "2021"
116"#;
117 fs::write(temp_dir.path().join("Cargo.toml"), cargo_toml)
118 .await
119 .unwrap();
120
121 fs::create_dir_all(temp_dir.path().join("src"))
123 .await
124 .unwrap();
125
126 let main_rs = r#"fn main() {
128 println!("Hello, world!");
129}
130"#;
131 fs::write(temp_dir.path().join("src/main.rs"), main_rs)
132 .await
133 .unwrap();
134
135 let result = run(temp_dir.path()).await.unwrap();
136
137 if !result.passed {
140 println!(
143 "Format check had issues (likely beta Rust differences): {:?}",
144 result.errors
145 );
146 }
147 assert!(result.check_type == CheckType::Format);
149 }
150
151 #[test]
152 fn test_format_check_struct() {
153 assert_eq!(FormatCheck::name(), "format");
154 assert!(!FormatCheck::description().is_empty());
155 }
156}