Skip to main content

rh_codegen/
quality.rs

1//! Quality assurance module for generated FHIR crates
2//!
3//! This module provides functionality to run quality checks on generated Rust crates,
4//! including formatting with rustfmt and compilation checks with cargo check.
5
6use std::fs;
7use std::path::{Path, PathBuf};
8use std::process::Command;
9
10use anyhow::{Context, Result};
11
12use crate::CodegenError;
13
14/// Configuration for quality checks
15#[derive(Debug, Clone)]
16pub struct QualityConfig {
17    /// Whether to run cargo fmt on the generated crate
18    pub run_format: bool,
19    /// Whether to run cargo check on the generated crate
20    pub run_compile_check: bool,
21    /// Whether to run cargo clippy on the generated crate
22    pub run_clippy: bool,
23}
24
25impl Default for QualityConfig {
26    fn default() -> Self {
27        Self {
28            run_format: true,
29            run_compile_check: true,
30            run_clippy: false, // Disabled by default as it might be too strict for generated code
31        }
32    }
33}
34
35/// Run all configured quality checks on a generated crate
36pub fn run_quality_checks(crate_path: &Path, config: &QualityConfig) -> Result<()> {
37    println!("🔍 Running quality checks on generated crate...");
38
39    if config.run_format {
40        run_format_check(crate_path)?;
41    }
42
43    if config.run_compile_check {
44        run_compile_check(crate_path)?;
45    }
46
47    if config.run_clippy {
48        run_clippy_check(crate_path)?;
49    }
50
51    println!("✅ All quality checks passed");
52    Ok(())
53}
54
55/// Format the generated crate using rustfmt
56pub fn run_format_check(crate_path: &Path) -> Result<()> {
57    println!("🎨 Formatting generated crate...");
58
59    // Try cargo fmt first, fallback to rustfmt directly if it fails
60    let output = Command::new("cargo")
61        .arg("fmt")
62        .arg("--all")
63        .current_dir(crate_path)
64        .output()
65        .with_context(|| format!("Failed to execute cargo fmt in {}", crate_path.display()))?;
66
67    if !output.status.success() {
68        let stderr = String::from_utf8_lossy(&output.stderr);
69
70        if should_fallback_to_rustfmt(&stderr) {
71            println!(
72                "⚠️  cargo fmt could not format the generated crate, trying direct rustfmt..."
73            );
74            run_rustfmt_direct(crate_path)?;
75            println!("✅ Formatting completed successfully (using rustfmt directly)");
76            return Ok(());
77        }
78
79        return Err(anyhow::anyhow!("cargo fmt failed: {stderr}"));
80    }
81
82    println!("✅ Formatting completed successfully");
83    Ok(())
84}
85
86fn should_fallback_to_rustfmt(stderr: &str) -> bool {
87    stderr.contains("Failed to find targets")
88        || stderr.contains("no targets")
89        || stderr.contains("failed to find a workspace root")
90}
91
92fn run_rustfmt_direct(crate_path: &Path) -> Result<()> {
93    let src_dir = crate_path.join("src");
94    let mut rust_files = Vec::new();
95    collect_rust_files(&src_dir, &mut rust_files)?;
96
97    if rust_files.is_empty() {
98        return Err(anyhow::anyhow!(
99            "rustfmt fallback found no Rust files under {}",
100            src_dir.display()
101        ));
102    }
103
104    let rustfmt_output = Command::new("rustfmt")
105        .arg("--edition")
106        .arg("2021")
107        .args(&rust_files)
108        .output()
109        .with_context(|| "Failed to execute rustfmt directly")?;
110
111    if !rustfmt_output.status.success() {
112        let rustfmt_stderr = String::from_utf8_lossy(&rustfmt_output.stderr);
113        return Err(anyhow::anyhow!("rustfmt failed: {rustfmt_stderr}"));
114    }
115
116    Ok(())
117}
118
119fn collect_rust_files(dir: &Path, rust_files: &mut Vec<PathBuf>) -> Result<()> {
120    for entry in fs::read_dir(dir).with_context(|| format!("Failed to read {}", dir.display()))? {
121        let entry = entry.with_context(|| format!("Failed to read entry in {}", dir.display()))?;
122        let path = entry.path();
123
124        if path.is_dir() {
125            collect_rust_files(&path, rust_files)?;
126        } else if path.extension().is_some_and(|extension| extension == "rs") {
127            rust_files.push(path);
128        }
129    }
130
131    Ok(())
132}
133
134/// Run cargo check on the generated crate as a compilation quality gate
135pub fn run_compile_check(crate_path: &Path) -> Result<()> {
136    println!("🔧 Running cargo check on generated crate...");
137
138    let output = Command::new("cargo")
139        .arg("check")
140        .current_dir(crate_path)
141        .output()
142        .with_context(|| format!("Failed to execute cargo check in {}", crate_path.display()))?;
143
144    if !output.status.success() {
145        let stderr = String::from_utf8_lossy(&output.stderr);
146        let stdout = String::from_utf8_lossy(&output.stdout);
147
148        eprintln!("❌ Cargo check failed for generated crate:");
149        if !stdout.is_empty() {
150            eprintln!("stdout: {stdout}");
151        }
152        if !stderr.is_empty() {
153            eprintln!("stderr: {stderr}");
154        }
155
156        return Err(anyhow::anyhow!(
157            "Generated crate failed cargo check. See output above for details."
158        ));
159    }
160
161    println!("✅ Cargo check passed for generated crate");
162    Ok(())
163}
164
165/// Run cargo clippy on the generated crate for additional linting
166pub fn run_clippy_check(crate_path: &Path) -> Result<()> {
167    println!("📎 Running cargo clippy on generated crate...");
168
169    let output = Command::new("cargo")
170        .arg("clippy")
171        .arg("--all-targets")
172        .arg("--all-features")
173        .arg("--")
174        .arg("-D")
175        .arg("warnings")
176        .current_dir(crate_path)
177        .output()
178        .with_context(|| format!("Failed to execute cargo clippy in {}", crate_path.display()))?;
179
180    if !output.status.success() {
181        let stderr = String::from_utf8_lossy(&output.stderr);
182        let stdout = String::from_utf8_lossy(&output.stdout);
183
184        eprintln!("❌ Cargo clippy failed for generated crate:");
185        if !stdout.is_empty() {
186            eprintln!("stdout: {stdout}");
187        }
188        if !stderr.is_empty() {
189            eprintln!("stderr: {stderr}");
190        }
191
192        return Err(anyhow::anyhow!(
193            "Generated crate failed cargo clippy. See output above for details."
194        ));
195    }
196
197    println!("✅ Cargo clippy passed for generated crate");
198    Ok(())
199}
200
201/// Legacy wrapper for backward compatibility
202pub fn format_generated_crate<P: AsRef<Path>>(crate_path: P) -> Result<(), CodegenError> {
203    run_format_check(crate_path.as_ref()).map_err(|e| CodegenError::Generation {
204        message: e.to_string(),
205    })
206}