ferrous_forge/
formatting.rs1use crate::{Error, Result};
7use serde::{Deserialize, Serialize};
8use std::path::Path;
9use std::process::Command;
10
11#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct FormatResult {
14 pub formatted: bool,
16 pub unformatted_files: Vec<String>,
18 pub suggestions: Vec<FormatSuggestion>,
20}
21
22#[derive(Debug, Clone, Serialize, Deserialize)]
24pub struct FormatSuggestion {
25 pub file: String,
27 pub line: usize,
29 pub description: String,
31}
32
33impl FormatResult {
34 pub fn report(&self) -> String {
36 let mut report = String::new();
37
38 if self.formatted {
39 report.push_str("ā
Code formatting check passed - All files properly formatted!\n");
40 } else {
41 report.push_str(&format!(
42 "ā ļø Code formatting issues found in {} files\n\n",
43 self.unformatted_files.len()
44 ));
45
46 report.push_str("Files needing formatting:\n");
47 for file in &self.unformatted_files {
48 report.push_str(&format!(" ⢠{}\n", file));
49 }
50
51 if !self.suggestions.is_empty() {
52 report.push_str("\nFormatting suggestions:\n");
53 for suggestion in &self.suggestions.iter().take(10).collect::<Vec<_>>() {
54 report.push_str(&format!(
55 " {}:{} - {}\n",
56 suggestion.file, suggestion.line, suggestion.description
57 ));
58 }
59
60 if self.suggestions.len() > 10 {
61 report.push_str(&format!(
62 " ... and {} more suggestions\n",
63 self.suggestions.len() - 10
64 ));
65 }
66 }
67
68 report.push_str("\nš” Run 'ferrous-forge fix --format' to automatically fix these issues\n");
69 }
70
71 report
72 }
73}
74
75pub async fn check_formatting(project_path: &Path) -> Result<FormatResult> {
77 ensure_rustfmt_installed().await?;
79
80 let output = Command::new("cargo")
82 .args(&["fmt", "--", "--check", "--verbose"])
83 .current_dir(project_path)
84 .output()
85 .map_err(|e| Error::process(format!("Failed to run cargo fmt: {}", e)))?;
86
87 parse_format_output(&output.stdout, &output.stderr, output.status.success())
89}
90
91pub async fn auto_format(project_path: &Path) -> Result<()> {
93 ensure_rustfmt_installed().await?;
95
96 println!("š§ Auto-formatting code...");
97
98 let output = Command::new("cargo")
100 .arg("fmt")
101 .current_dir(project_path)
102 .output()
103 .map_err(|e| Error::process(format!("Failed to run cargo fmt: {}", e)))?;
104
105 if output.status.success() {
106 println!("⨠Code formatted successfully!");
107 Ok(())
108 } else {
109 let stderr = String::from_utf8_lossy(&output.stderr);
110 Err(Error::process(format!("Formatting failed: {}", stderr)))
111 }
112}
113
114pub async fn check_file_formatting(file_path: &Path) -> Result<bool> {
116 ensure_rustfmt_installed().await?;
118
119 let output = Command::new("rustfmt")
121 .args(&["--check", file_path.to_str().ok_or_else(|| Error::process("Invalid file path"))?])
122 .output()
123 .map_err(|e| Error::process(format!("Failed to run rustfmt: {}", e)))?;
124
125 Ok(output.status.success())
126}
127
128pub async fn format_file(file_path: &Path) -> Result<()> {
130 ensure_rustfmt_installed().await?;
132
133 let output = Command::new("rustfmt")
135 .arg(file_path.to_str().ok_or_else(|| Error::process("Invalid file path"))?)
136 .output()
137 .map_err(|e| Error::process(format!("Failed to run rustfmt: {}", e)))?;
138
139 if output.status.success() {
140 Ok(())
141 } else {
142 let stderr = String::from_utf8_lossy(&output.stderr);
143 Err(Error::process(format!("Failed to format {}: {}", file_path.display(), stderr)))
144 }
145}
146
147pub async fn get_format_diff(project_path: &Path) -> Result<String> {
149 ensure_rustfmt_installed().await?;
151
152 let output = Command::new("cargo")
154 .args(&["fmt", "--", "--check", "--emit=stdout"])
155 .current_dir(project_path)
156 .output()
157 .map_err(|e| Error::process(format!("Failed to run cargo fmt: {}", e)))?;
158
159 Ok(String::from_utf8_lossy(&output.stdout).to_string())
160}
161
162async fn ensure_rustfmt_installed() -> Result<()> {
164 let check = Command::new("rustfmt")
165 .arg("--version")
166 .output();
167
168 if check.as_ref().map_or(true, |output| !output.status.success()) {
169 println!("š¦ Installing rustfmt...");
170
171 let install = Command::new("rustup")
172 .args(&["component", "add", "rustfmt"])
173 .output()
174 .map_err(|e| Error::process(format!("Failed to install rustfmt: {}", e)))?;
175
176 if !install.status.success() {
177 return Err(Error::process("Failed to install rustfmt"));
178 }
179
180 println!("ā
rustfmt installed successfully");
181 }
182
183 Ok(())
184}
185
186fn parse_format_output(stdout: &[u8], stderr: &[u8], success: bool) -> Result<FormatResult> {
188 if success {
189 return Ok(FormatResult {
190 formatted: true,
191 unformatted_files: vec![],
192 suggestions: vec![],
193 });
194 }
195
196 let stderr_str = String::from_utf8_lossy(stderr);
197 let stdout_str = String::from_utf8_lossy(stdout);
198
199 let mut unformatted_files = Vec::new();
200 let mut suggestions = Vec::new();
201
202 for line in stderr_str.lines() {
204 if line.starts_with("Diff in") {
205 if let Some(file) = line.strip_prefix("Diff in ") {
206 let file = file.trim_end_matches(" at line 1:");
207 let file = file.trim_end_matches(':');
208 unformatted_files.push(file.to_string());
209 }
210 }
211 }
212
213 for line in stdout_str.lines() {
215 if line.starts_with("warning:") || line.contains("formatting") {
216 if let Some(pos) = line.find(".rs:") {
218 let start = line.rfind('/').unwrap_or(0);
219 let file = &line[start..pos + 3];
220
221 let line_num = if let Some(num_start) = line[pos + 3..].find(':') {
223 line[pos + 4..pos + 3 + num_start].parse().unwrap_or(0)
224 } else {
225 0
226 };
227
228 suggestions.push(FormatSuggestion {
229 file: file.to_string(),
230 line: line_num,
231 description: "Formatting required".to_string(),
232 });
233 }
234 }
235 }
236
237 Ok(FormatResult {
238 formatted: false,
239 unformatted_files,
240 suggestions,
241 })
242}
243
244pub async fn apply_rustfmt_config(project_path: &Path) -> Result<()> {
246 let rustfmt_toml = project_path.join("rustfmt.toml");
247
248 if !rustfmt_toml.exists() {
249 let config = r#"# Ferrous Forge rustfmt configuration
251edition = "2021"
252max_width = 100
253hard_tabs = false
254tab_spaces = 4
255newline_style = "Auto"
256use_small_heuristics = "Default"
257reorder_imports = true
258reorder_modules = true
259remove_nested_parens = true
260format_strings = false
261format_macro_matchers = false
262format_macro_bodies = true
263empty_item_single_line = true
264struct_lit_single_line = true
265fn_single_line = false
266where_single_line = false
267imports_indent = "Block"
268imports_layout = "Mixed"
269merge_derives = true
270group_imports = "StdExternalCrate"
271reorder_impl_items = false
272spaces_around_ranges = false
273trailing_semicolon = true
274trailing_comma = "Vertical"
275match_block_trailing_comma = false
276blank_lines_upper_bound = 1
277blank_lines_lower_bound = 0
278"#;
279
280 tokio::fs::write(&rustfmt_toml, config)
281 .await
282 .map_err(|e| Error::process(format!("Failed to create rustfmt.toml: {}", e)))?;
283
284 println!("ā
Created rustfmt.toml with Ferrous Forge standards");
285 }
286
287 Ok(())
288}
289
290#[cfg(test)]
291mod tests {
292 use super::*;
293
294 #[test]
295 fn test_format_result_formatted() {
296 let result = FormatResult {
297 formatted: true,
298 unformatted_files: vec![],
299 suggestions: vec![],
300 };
301
302 assert!(result.formatted);
303 assert!(result.unformatted_files.is_empty());
304 assert!(result.suggestions.is_empty());
305 }
306
307 #[test]
308 fn test_format_result_unformatted() {
309 let result = FormatResult {
310 formatted: false,
311 unformatted_files: vec!["src/main.rs".to_string()],
312 suggestions: vec![FormatSuggestion {
313 file: "src/main.rs".to_string(),
314 line: 10,
315 description: "Formatting required".to_string(),
316 }],
317 };
318
319 assert!(!result.formatted);
320 assert_eq!(result.unformatted_files.len(), 1);
321 assert_eq!(result.suggestions.len(), 1);
322 }
323}