ferrous_forge/safety/checks/
format.rs1use crate::Result;
4use std::path::Path;
5use std::time::Instant;
6
7use super::SafetyCheck;
8use crate::safety::{CheckType, report::CheckResult};
9
10pub struct FormatCheck;
12
13impl SafetyCheck for FormatCheck {
14 async fn run(project_path: &Path) -> Result<CheckResult> {
15 run(project_path).await
16 }
17
18 fn name() -> &'static str {
19 "format"
20 }
21
22 fn description() -> &'static str {
23 "Validates code formatting with rustfmt"
24 }
25}
26
27pub async fn run(project_path: &Path) -> Result<CheckResult> {
33 let start = Instant::now();
34 let mut result = CheckResult::new(CheckType::Format);
35
36 if let Err(error_msg) = check_cargo_availability() {
38 result.add_error(&error_msg);
39 result.add_suggestion("Install Rust and cargo from https://rustup.rs");
40 result.set_duration(start.elapsed());
41 return Ok(result);
42 }
43
44 let output = execute_format_check(project_path).await?;
46 result.set_duration(start.elapsed());
47
48 if output.status.success() {
49 result.add_context("All code is properly formatted");
50 } else {
51 process_format_violations(&mut result, &output);
52 }
53
54 Ok(result)
55}
56
57fn check_cargo_availability() -> std::result::Result<(), String> {
59 which::which("cargo")
60 .map_err(|_| "cargo not found in PATH".to_string())
61 .map(|_| ())
62}
63
64async fn execute_format_check(project_path: &Path) -> Result<std::process::Output> {
66 tokio::process::Command::new("cargo")
67 .current_dir(project_path)
68 .args(&["fmt", "--check"])
69 .output()
70 .await
71 .map_err(Into::into)
72}
73
74fn process_format_violations(result: &mut CheckResult, output: &std::process::Output) {
76 result.add_error("Code formatting violations found");
77 result.add_suggestion("Run 'cargo fmt' to fix formatting automatically");
78
79 parse_format_output(result, output);
80 add_format_context_if_needed(result);
81}
82
83fn parse_format_output(result: &mut CheckResult, output: &std::process::Output) {
85 let stdout = String::from_utf8_lossy(&output.stdout);
86 let stderr = String::from_utf8_lossy(&output.stderr);
87
88 for line in stdout.lines().chain(stderr.lines()) {
89 if line.starts_with("Diff in") {
90 result.add_error(format!("Formatting issue: {}", line));
91 } else if line.contains("rustfmt") && line.contains("failed") {
92 result.add_error(line.to_string());
93 }
94 }
95}
96
97fn add_format_context_if_needed(result: &mut CheckResult) {
99 if result.errors.len() == 1 {
100 result.add_context("Run 'cargo fmt' to see detailed formatting issues");
101 }
102}
103
104#[cfg(test)]
105#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
106mod tests {
107 use super::*;
108 use tempfile::TempDir;
109 use tokio::fs;
110
111 #[tokio::test]
112 async fn test_format_check_on_empty_project() {
113 let temp_dir = TempDir::new().unwrap();
114
115 let cargo_toml = r#"[package]
117name = "test"
118version = "0.1.0"
119edition = "2021"
120"#;
121 fs::write(temp_dir.path().join("Cargo.toml"), cargo_toml)
122 .await
123 .unwrap();
124
125 fs::create_dir_all(temp_dir.path().join("src"))
127 .await
128 .unwrap();
129
130 let main_rs = r#"fn main() {
132 println!("Hello, world!");
133}
134"#;
135 fs::write(temp_dir.path().join("src/main.rs"), main_rs)
136 .await
137 .unwrap();
138
139 let result = run(temp_dir.path()).await.unwrap();
140
141 if !result.passed {
144 println!(
147 "Format check had issues (likely beta Rust differences): {:?}",
148 result.errors
149 );
150 }
151 assert!(result.check_type == CheckType::Format);
153 }
154
155 #[test]
156 fn test_format_check_struct() {
157 assert_eq!(FormatCheck::name(), "format");
158 assert!(!FormatCheck::description().is_empty());
159 }
160}