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 rustfmt is available
34    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    // Run cargo fmt --check
42    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        // Parse formatting violations from output
54        let stdout = String::from_utf8_lossy(&output.stdout);
55        let stderr = String::from_utf8_lossy(&output.stderr);
56
57        // Look for diff output
58        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 no specific errors found, add general message
67        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        // Create a basic Cargo.toml
89        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        // Create src directory
100        fs::create_dir_all(temp_dir.path().join("src"))
101            .await
102            .unwrap();
103
104        // Create a properly formatted main.rs
105        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        // Should pass for properly formatted code
116        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}