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::path::Path;
7use std::process::Command;
8
9use anyhow::{Context, Result};
10
11use crate::CodegenError;
12
13/// Configuration for quality checks
14#[derive(Debug, Clone)]
15pub struct QualityConfig {
16    /// Whether to run cargo fmt on the generated crate
17    pub run_format: bool,
18    /// Whether to run cargo check on the generated crate
19    pub run_compile_check: bool,
20    /// Whether to run cargo clippy on the generated crate
21    pub run_clippy: bool,
22}
23
24impl Default for QualityConfig {
25    fn default() -> Self {
26        Self {
27            run_format: true,
28            run_compile_check: true,
29            run_clippy: false, // Disabled by default as it might be too strict for generated code
30        }
31    }
32}
33
34/// Run all configured quality checks on a generated crate
35pub fn run_quality_checks(crate_path: &Path, config: &QualityConfig) -> Result<()> {
36    println!("🔍 Running quality checks on generated crate...");
37
38    if config.run_format {
39        run_format_check(crate_path)?;
40    }
41
42    if config.run_compile_check {
43        run_compile_check(crate_path)?;
44    }
45
46    if config.run_clippy {
47        run_clippy_check(crate_path)?;
48    }
49
50    println!("✅ All quality checks passed");
51    Ok(())
52}
53
54/// Format the generated crate using rustfmt
55pub fn run_format_check(crate_path: &Path) -> Result<()> {
56    println!("🎨 Formatting generated crate...");
57
58    // Try cargo fmt first, fallback to rustfmt directly if it fails
59    let output = Command::new("cargo")
60        .arg("fmt")
61        .arg("--all")
62        .current_dir(crate_path)
63        .output()
64        .with_context(|| format!("Failed to execute cargo fmt in {}", crate_path.display()))?;
65
66    if !output.status.success() {
67        let stderr = String::from_utf8_lossy(&output.stderr);
68
69        // If cargo fmt fails due to no targets, try formatting the lib.rs directly
70        if stderr.contains("Failed to find targets") || stderr.contains("no targets") {
71            println!("⚠️  cargo fmt found no targets, trying direct rustfmt...");
72
73            let lib_rs = crate_path.join("src").join("lib.rs");
74            if lib_rs.exists() {
75                let rustfmt_output = Command::new("rustfmt")
76                    .arg("--edition")
77                    .arg("2021")
78                    .arg(&lib_rs)
79                    .output()
80                    .with_context(|| "Failed to execute rustfmt directly")?;
81
82                if !rustfmt_output.status.success() {
83                    let rustfmt_stderr = String::from_utf8_lossy(&rustfmt_output.stderr);
84                    return Err(anyhow::anyhow!("rustfmt failed: {rustfmt_stderr}"));
85                }
86
87                println!("✅ Formatting completed successfully (using rustfmt directly)");
88                return Ok(());
89            }
90        }
91
92        return Err(anyhow::anyhow!("cargo fmt failed: {stderr}"));
93    }
94
95    println!("✅ Formatting completed successfully");
96    Ok(())
97}
98
99/// Run cargo check on the generated crate as a compilation quality gate
100pub fn run_compile_check(crate_path: &Path) -> Result<()> {
101    println!("🔧 Running cargo check on generated crate...");
102
103    let output = Command::new("cargo")
104        .arg("check")
105        .current_dir(crate_path)
106        .output()
107        .with_context(|| format!("Failed to execute cargo check in {}", crate_path.display()))?;
108
109    if !output.status.success() {
110        let stderr = String::from_utf8_lossy(&output.stderr);
111        let stdout = String::from_utf8_lossy(&output.stdout);
112
113        eprintln!("❌ Cargo check failed for generated crate:");
114        if !stdout.is_empty() {
115            eprintln!("stdout: {stdout}");
116        }
117        if !stderr.is_empty() {
118            eprintln!("stderr: {stderr}");
119        }
120
121        return Err(anyhow::anyhow!(
122            "Generated crate failed cargo check. See output above for details."
123        ));
124    }
125
126    println!("✅ Cargo check passed for generated crate");
127    Ok(())
128}
129
130/// Run cargo clippy on the generated crate for additional linting
131pub fn run_clippy_check(crate_path: &Path) -> Result<()> {
132    println!("📎 Running cargo clippy on generated crate...");
133
134    let output = Command::new("cargo")
135        .arg("clippy")
136        .arg("--all-targets")
137        .arg("--all-features")
138        .arg("--")
139        .arg("-D")
140        .arg("warnings")
141        .current_dir(crate_path)
142        .output()
143        .with_context(|| format!("Failed to execute cargo clippy in {}", crate_path.display()))?;
144
145    if !output.status.success() {
146        let stderr = String::from_utf8_lossy(&output.stderr);
147        let stdout = String::from_utf8_lossy(&output.stdout);
148
149        eprintln!("❌ Cargo clippy failed for generated crate:");
150        if !stdout.is_empty() {
151            eprintln!("stdout: {stdout}");
152        }
153        if !stderr.is_empty() {
154            eprintln!("stderr: {stderr}");
155        }
156
157        return Err(anyhow::anyhow!(
158            "Generated crate failed cargo clippy. See output above for details."
159        ));
160    }
161
162    println!("✅ Cargo clippy passed for generated crate");
163    Ok(())
164}
165
166/// Legacy wrapper for backward compatibility
167pub fn format_generated_crate<P: AsRef<Path>>(crate_path: P) -> Result<(), CodegenError> {
168    run_format_check(crate_path.as_ref()).map_err(|e| CodegenError::Generation {
169        message: e.to_string(),
170    })
171}