1use std::fs;
7use std::path::{Path, PathBuf};
8use std::process::Command;
9
10use anyhow::{Context, Result};
11
12use crate::CodegenError;
13
14#[derive(Debug, Clone)]
16pub struct QualityConfig {
17 pub run_format: bool,
19 pub run_compile_check: bool,
21 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, }
32 }
33}
34
35pub 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
55pub fn run_format_check(crate_path: &Path) -> Result<()> {
57 println!("🎨 Formatting generated crate...");
58
59 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
134pub 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
165pub 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
201pub 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}