use crate::Result;
use std::path::Path;
use std::process::Command;
use std::time::Instant;
use super::SafetyCheck;
use crate::safety::{CheckType, report::CheckResult};
pub struct FormatCheck;
impl SafetyCheck for FormatCheck {
async fn run(project_path: &Path) -> Result<CheckResult> {
run(project_path).await
}
fn name() -> &'static str {
"format"
}
fn description() -> &'static str {
"Validates code formatting with rustfmt"
}
}
pub async fn run(project_path: &Path) -> Result<CheckResult> {
let start = Instant::now();
let mut result = CheckResult::new(CheckType::Format);
if let Err(error_msg) = check_cargo_availability() {
result.add_error(&error_msg);
result.add_suggestion("Install Rust and cargo from https://rustup.rs");
result.set_duration(start.elapsed());
return Ok(result);
}
let output = execute_format_check(project_path)?;
result.set_duration(start.elapsed());
if output.status.success() {
result.add_context("All code is properly formatted");
} else {
process_format_violations(&mut result, &output);
}
Ok(result)
}
fn check_cargo_availability() -> std::result::Result<(), String> {
which::which("cargo")
.map_err(|_| "cargo not found in PATH".to_string())
.map(|_| ())
}
fn execute_format_check(project_path: &Path) -> Result<std::process::Output> {
Command::new("cargo")
.current_dir(project_path)
.args(&["fmt", "--check"])
.output()
.map_err(Into::into)
}
fn process_format_violations(result: &mut CheckResult, output: &std::process::Output) {
result.add_error("Code formatting violations found");
result.add_suggestion("Run 'cargo fmt' to fix formatting automatically");
parse_format_output(result, output);
add_format_context_if_needed(result);
}
fn parse_format_output(result: &mut CheckResult, output: &std::process::Output) {
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
for line in stdout.lines().chain(stderr.lines()) {
if line.starts_with("Diff in") {
result.add_error(format!("Formatting issue: {}", line));
} else if line.contains("rustfmt") && line.contains("failed") {
result.add_error(line.to_string());
}
}
}
fn add_format_context_if_needed(result: &mut CheckResult) {
if result.errors.len() == 1 {
result.add_context("Run 'cargo fmt' to see detailed formatting issues");
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
mod tests {
use super::*;
use tempfile::TempDir;
use tokio::fs;
#[tokio::test]
async fn test_format_check_on_empty_project() {
let temp_dir = TempDir::new().unwrap();
let cargo_toml = r#"[package]
name = "test"
version = "0.1.0"
edition = "2021"
"#;
fs::write(temp_dir.path().join("Cargo.toml"), cargo_toml)
.await
.unwrap();
fs::create_dir_all(temp_dir.path().join("src"))
.await
.unwrap();
let main_rs = r#"fn main() {
println!("Hello, world!");
}
"#;
fs::write(temp_dir.path().join("src/main.rs"), main_rs)
.await
.unwrap();
let result = run(temp_dir.path()).await.unwrap();
if !result.passed {
println!(
"Format check had issues (likely beta Rust differences): {:?}",
result.errors
);
}
assert!(result.check_type == CheckType::Format);
}
#[test]
fn test_format_check_struct() {
assert_eq!(FormatCheck::name(), "format");
assert!(!FormatCheck::description().is_empty());
}
}