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(
69 "\nš” Run 'ferrous-forge fix --format' to automatically fix these issues\n",
70 );
71 }
72
73 report
74 }
75}
76
77pub async fn check_formatting(project_path: &Path) -> Result<FormatResult> {
79 ensure_rustfmt_installed().await?;
81
82 let output = Command::new("cargo")
84 .args(&["fmt", "--", "--check", "--verbose"])
85 .current_dir(project_path)
86 .output()
87 .map_err(|e| Error::process(format!("Failed to run cargo fmt: {}", e)))?;
88
89 parse_format_output(&output.stdout, &output.stderr, output.status.success())
91}
92
93pub async fn auto_format(project_path: &Path) -> Result<()> {
95 ensure_rustfmt_installed().await?;
97
98 println!("š§ Auto-formatting code...");
99
100 let output = Command::new("cargo")
102 .arg("fmt")
103 .current_dir(project_path)
104 .output()
105 .map_err(|e| Error::process(format!("Failed to run cargo fmt: {}", e)))?;
106
107 if output.status.success() {
108 println!("⨠Code formatted successfully!");
109 Ok(())
110 } else {
111 let stderr = String::from_utf8_lossy(&output.stderr);
112 Err(Error::process(format!("Formatting failed: {}", stderr)))
113 }
114}
115
116pub async fn check_file_formatting(file_path: &Path) -> Result<bool> {
118 ensure_rustfmt_installed().await?;
120
121 let output = Command::new("rustfmt")
123 .args(&[
124 "--check",
125 file_path
126 .to_str()
127 .ok_or_else(|| Error::process("Invalid file path"))?,
128 ])
129 .output()
130 .map_err(|e| Error::process(format!("Failed to run rustfmt: {}", e)))?;
131
132 Ok(output.status.success())
133}
134
135pub async fn format_file(file_path: &Path) -> Result<()> {
137 ensure_rustfmt_installed().await?;
139
140 let output = Command::new("rustfmt")
142 .arg(
143 file_path
144 .to_str()
145 .ok_or_else(|| Error::process("Invalid file path"))?,
146 )
147 .output()
148 .map_err(|e| Error::process(format!("Failed to run rustfmt: {}", e)))?;
149
150 if output.status.success() {
151 Ok(())
152 } else {
153 let stderr = String::from_utf8_lossy(&output.stderr);
154 Err(Error::process(format!(
155 "Failed to format {}: {}",
156 file_path.display(),
157 stderr
158 )))
159 }
160}
161
162pub async fn get_format_diff(project_path: &Path) -> Result<String> {
164 ensure_rustfmt_installed().await?;
166
167 let output = Command::new("cargo")
169 .args(&["fmt", "--", "--check", "--emit=stdout"])
170 .current_dir(project_path)
171 .output()
172 .map_err(|e| Error::process(format!("Failed to run cargo fmt: {}", e)))?;
173
174 Ok(String::from_utf8_lossy(&output.stdout).to_string())
175}
176
177async fn ensure_rustfmt_installed() -> Result<()> {
179 let check = Command::new("rustfmt").arg("--version").output();
180
181 if check
182 .as_ref()
183 .map_or(true, |output| !output.status.success())
184 {
185 println!("š¦ Installing rustfmt...");
186
187 let install = Command::new("rustup")
188 .args(&["component", "add", "rustfmt"])
189 .output()
190 .map_err(|e| Error::process(format!("Failed to install rustfmt: {}", e)))?;
191
192 if !install.status.success() {
193 return Err(Error::process("Failed to install rustfmt"));
194 }
195
196 println!("ā
rustfmt installed successfully");
197 }
198
199 Ok(())
200}
201
202fn parse_format_output(stdout: &[u8], stderr: &[u8], success: bool) -> Result<FormatResult> {
204 if success {
205 return Ok(FormatResult {
206 formatted: true,
207 unformatted_files: vec![],
208 suggestions: vec![],
209 });
210 }
211
212 let stderr_str = String::from_utf8_lossy(stderr);
213 let stdout_str = String::from_utf8_lossy(stdout);
214
215 let mut unformatted_files = Vec::new();
216 let mut suggestions = Vec::new();
217
218 for line in stderr_str.lines() {
220 if line.starts_with("Diff in") {
221 if let Some(file) = line.strip_prefix("Diff in ") {
222 let file = file.trim_end_matches(" at line 1:");
223 let file = file.trim_end_matches(':');
224 unformatted_files.push(file.to_string());
225 }
226 }
227 }
228
229 for line in stdout_str.lines() {
231 if line.starts_with("warning:") || line.contains("formatting") {
232 if let Some(pos) = line.find(".rs:") {
234 let start = line.rfind('/').unwrap_or(0);
235 let file = &line[start..pos + 3];
236
237 let line_num = if let Some(num_start) = line[pos + 3..].find(':') {
239 line[pos + 4..pos + 3 + num_start].parse().unwrap_or(0)
240 } else {
241 0
242 };
243
244 suggestions.push(FormatSuggestion {
245 file: file.to_string(),
246 line: line_num,
247 description: "Formatting required".to_string(),
248 });
249 }
250 }
251 }
252
253 Ok(FormatResult {
254 formatted: false,
255 unformatted_files,
256 suggestions,
257 })
258}
259
260pub async fn apply_rustfmt_config(project_path: &Path) -> Result<()> {
262 let rustfmt_toml = project_path.join("rustfmt.toml");
263
264 if !rustfmt_toml.exists() {
265 let config = r#"# Ferrous Forge rustfmt configuration
267edition = "2021"
268max_width = 100
269hard_tabs = false
270tab_spaces = 4
271newline_style = "Auto"
272use_small_heuristics = "Default"
273reorder_imports = true
274reorder_modules = true
275remove_nested_parens = true
276format_strings = false
277format_macro_matchers = false
278format_macro_bodies = true
279empty_item_single_line = true
280struct_lit_single_line = true
281fn_single_line = false
282where_single_line = false
283imports_indent = "Block"
284imports_layout = "Mixed"
285merge_derives = true
286group_imports = "StdExternalCrate"
287reorder_impl_items = false
288spaces_around_ranges = false
289trailing_semicolon = true
290trailing_comma = "Vertical"
291match_block_trailing_comma = false
292blank_lines_upper_bound = 1
293blank_lines_lower_bound = 0
294"#;
295
296 tokio::fs::write(&rustfmt_toml, config)
297 .await
298 .map_err(|e| Error::process(format!("Failed to create rustfmt.toml: {}", e)))?;
299
300 println!("ā
Created rustfmt.toml with Ferrous Forge standards");
301 }
302
303 Ok(())
304}
305
306#[cfg(test)]
307#[allow(clippy::expect_used, clippy::unwrap_used)]
308mod tests {
309 use super::*;
310
311 #[test]
312 fn test_format_result_formatted() {
313 let result = FormatResult {
314 formatted: true,
315 unformatted_files: vec![],
316 suggestions: vec![],
317 };
318
319 assert!(result.formatted);
320 assert!(result.unformatted_files.is_empty());
321 assert!(result.suggestions.is_empty());
322 }
323
324 #[test]
325 fn test_format_result_unformatted() {
326 let result = FormatResult {
327 formatted: false,
328 unformatted_files: vec!["src/main.rs".to_string()],
329 suggestions: vec![FormatSuggestion {
330 file: "src/main.rs".to_string(),
331 line: 10,
332 description: "Formatting required".to_string(),
333 }],
334 };
335
336 assert!(!result.formatted);
337 assert_eq!(result.unformatted_files.len(), 1);
338 assert_eq!(result.suggestions.len(), 1);
339 }
340}