ferrous_forge/safety/checks/
format.rs

1//! Format checking with cargo fmt
2
3use 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
11/// Format check implementation
12pub 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
28/// Run cargo fmt --check
29pub async fn run(project_path: &Path) -> Result<CheckResult> {
30    let start = Instant::now();
31    let mut result = CheckResult::new(CheckType::Format);
32
33    // Check if cargo is available
34    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    // Execute format check and process results
42    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
54/// Check if cargo is available in PATH
55fn 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
61/// Execute cargo fmt --check command
62fn 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
70/// Process format check violations and parse output
71fn 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
79/// Parse format check output and extract violation details
80fn 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
93/// Add additional context for format violations if no specific errors were found
94fn 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        // Create a basic Cargo.toml
112        let cargo_toml = r#"
113[package]
114name = "test"
115version = "0.1.0"
116edition = "2021"
117"#;
118        fs::write(temp_dir.path().join("Cargo.toml"), cargo_toml)
119            .await
120            .unwrap();
121
122        // Create src directory
123        fs::create_dir_all(temp_dir.path().join("src"))
124            .await
125            .unwrap();
126
127        // Create a properly formatted main.rs
128        let main_rs = r#"fn main() {
129    println!("Hello, world!");
130}
131"#;
132        fs::write(temp_dir.path().join("src/main.rs"), main_rs)
133            .await
134            .unwrap();
135
136        let result = run(temp_dir.path()).await.unwrap();
137
138        // Should pass for properly formatted code
139        assert!(result.passed);
140        assert!(result.errors.is_empty());
141    }
142
143    #[test]
144    fn test_format_check_struct() {
145        assert_eq!(FormatCheck::name(), "format");
146        assert!(!FormatCheck::description().is_empty());
147    }
148}