Skip to main content

crate_cli/fmt/
fn.rs

1use super::*;
2
3/// Sort derive traits in a single line
4///
5/// # Arguments
6///
7/// - `&str` - The line containing derive attribute
8///
9/// # Returns
10///
11/// - `Option<String>` - Sorted line if derive found, None otherwise
12fn sort_derive_in_line(line: &str) -> Option<String> {
13    let captures: Captures<'_> = DERIVE_REGEX.captures(line)?;
14    let derive_content: &str = captures.get(1)?.as_str();
15    let mut traits: Vec<String> = derive_content
16        .split(',')
17        .map(|s: &str| s.trim().to_string())
18        .filter(|s: &String| !s.is_empty())
19        .collect();
20    traits.sort_by_key(|a: &String| a.to_lowercase());
21    let sorted_traits: String = traits.join(", ");
22    let result: String = line.replace(derive_content, &sorted_traits);
23    Some(result)
24}
25
26/// Format derive attributes in a file
27///
28/// # Arguments
29///
30/// - `&Path` - Path to the Rust file
31///
32/// # Returns
33///
34/// - `Result<bool, io::Error>` - True if file was modified, false otherwise
35async fn format_derive_in_file(file_path: &Path) -> Result<bool, io::Error> {
36    let content: String = read_to_string(file_path).await?;
37    let lines: std::str::Lines<'_> = content.lines();
38    let mut modified: bool = false;
39    let mut new_content: String = String::new();
40    for line in lines {
41        let trimmed: &str = line.trim();
42        let new_line: String = if trimmed.starts_with(DERIVE_PREFIX) {
43            if let Some(sorted) = sort_derive_in_line(line) {
44                if sorted != line {
45                    modified = true;
46                }
47                sorted
48            } else {
49                line.to_string()
50            }
51        } else {
52            line.to_string()
53        };
54        new_content.push_str(&new_line);
55        new_content.push('\n');
56    }
57    if modified {
58        write(file_path, new_content).await?;
59    }
60    Ok(modified)
61}
62
63/// Find all Rust files in workspace
64///
65/// # Arguments
66///
67/// - `&Path` - Path to Cargo.toml
68///
69/// # Returns
70///
71/// - `Result<Vec<PathBuf>, io::Error>` - List of Rust file paths
72async fn find_rust_files(manifest_path: &Path) -> Result<Vec<PathBuf>, io::Error> {
73    let mut files: Vec<PathBuf> = Vec::new();
74    let workspace_root: &Path = manifest_path.parent().unwrap_or(Path::new("."));
75    let src_dir: PathBuf = workspace_root.join("src");
76    if src_dir.exists() {
77        find_rust_files_in_dir(&src_dir, &mut files).await?;
78    }
79    let content: String = read_to_string(manifest_path).await?;
80    if let Ok(doc) = toml::from_str::<Value>(&content)
81        && let Some(workspace) = doc.get(TOML_WORKSPACE)
82        && let Some(members) = workspace
83            .get(TOML_MEMBERS)
84            .and_then(|m: &Value| m.as_array())
85    {
86        for member in members {
87            if let Some(pattern) = member.as_str() {
88                let member_src: PathBuf = workspace_root.join(pattern).join("src");
89                if member_src.exists() {
90                    find_rust_files_in_dir(&member_src, &mut files).await?;
91                }
92            }
93        }
94    }
95    Ok(files)
96}
97
98/// Recursively find Rust files in directory
99///
100/// # Arguments
101///
102/// - `&Path` - Directory to search
103/// - `&mut Vec<PathBuf>` - Vector to collect file paths
104///
105/// # Returns
106///
107/// - `Result<(), io::Error>` - Success or error
108async fn find_rust_files_in_dir(dir: &Path, files: &mut Vec<PathBuf>) -> Result<(), io::Error> {
109    let mut entries: ReadDir = read_dir(dir).await?;
110    while let Some(entry) = entries.next_entry().await? {
111        let path: PathBuf = entry.path();
112        if path.is_file() && path.extension().is_some_and(|ext: &OsStr| ext == "rs") {
113            files.push(path);
114        } else if path.is_dir() {
115            Box::pin(find_rust_files_in_dir(&path, files)).await?;
116        }
117    }
118    Ok(())
119}
120
121/// Format derive attributes in all workspace files
122///
123/// # Arguments
124///
125/// - `&str` - Path to Cargo.toml
126///
127/// # Returns
128///
129/// - `Result<(), io::Error>` - Success or error
130async fn format_derive_attributes(manifest_path: &str) -> Result<(), io::Error> {
131    let path: &Path = Path::new(manifest_path);
132    let files: Vec<PathBuf> = find_rust_files(path).await?;
133    let modified_count: Arc<Mutex<usize>> = Arc::new(Mutex::new(0));
134    let mut handles: Vec<JoinHandle<Result<(), io::Error>>> = Vec::new();
135    for file in files {
136        let counter: Arc<Mutex<usize>> = Arc::clone(&modified_count);
137        let handle: JoinHandle<Result<(), io::Error>> = spawn(async move {
138            if format_derive_in_file(&file).await? {
139                let mut count: MutexGuard<'_, usize> = counter.lock().await;
140                *count += 1;
141            }
142            Ok(())
143        });
144        handles.push(handle);
145    }
146    for handle in handles {
147        handle.await??;
148    }
149    let count: usize = *modified_count.lock().await;
150    if count > 0 {
151        log::info!("Sorted derive attributes in {count} files");
152    }
153    Ok(())
154}
155
156/// Check if cargo-clippy is installed
157///
158/// # Returns
159///
160/// - `bool` - True if cargo-clippy is available
161fn is_cargo_clippy_installed() -> bool {
162    which(CARGO_CLIPPY).is_ok()
163}
164
165/// Install cargo-clippy using rustup
166///
167/// # Returns
168///
169/// - `Result<(), io::Error>` - Success or error
170async fn install_cargo_clippy() -> Result<(), io::Error> {
171    log::warn!("cargo-clippy not found, installing...");
172    let output: std::process::Output = Command::new(RUSTUP)
173        .arg(RUSTUP_COMPONENT)
174        .arg("add")
175        .arg(CLIPPY)
176        .stdout(Stdio::piped())
177        .stderr(Stdio::piped())
178        .output()
179        .await?;
180    let stdout: String = String::from_utf8_lossy(&output.stdout).trim().to_string();
181    let stderr: String = String::from_utf8_lossy(&output.stderr).trim().to_string();
182    if !stdout.is_empty() {
183        for line in stdout.lines() {
184            log::info!("{line}");
185        }
186    }
187    if !stderr.is_empty() {
188        if output.status.success() {
189            for line in stderr.lines() {
190                if line.is_empty() {
191                    continue;
192                }
193                log::info!("{line}");
194            }
195        } else {
196            for line in stderr.lines() {
197                if line.is_empty() {
198                    continue;
199                }
200                log::error!("{line}");
201            }
202        }
203    }
204    if !output.status.success() {
205        return Err(io::Error::other(ERROR_CLIPPY_INSTALL_FAILED));
206    }
207    Ok(())
208}
209
210/// Execute clippy fix command
211///
212/// # Arguments
213///
214/// - `&Args` - The parsed arguments
215///
216/// # Returns
217///
218/// - `Result<(), io::Error>` - Success or error
219async fn execute_clippy_fix(args: &Args) -> Result<(), io::Error> {
220    if !is_cargo_clippy_installed() {
221        install_cargo_clippy().await?;
222    }
223    let mut cmd: Command = Command::new(CARGO);
224    cmd.arg(CLIPPY)
225        .arg(CLI_FLAG_FIX)
226        .arg(CLI_FLAG_WORKSPACE)
227        .arg(CLI_FLAG_ALL_TARGETS)
228        .arg(CLI_FLAG_ALLOW_DIRTY);
229    if let Some(ref manifest_path) = args.manifest_path {
230        cmd.arg(CLI_FLAG_MANIFEST_PATH).arg(manifest_path);
231    }
232    cmd.stdout(Stdio::piped()).stderr(Stdio::piped());
233    let output: std::process::Output = cmd.output().await?;
234    let stdout: String = String::from_utf8_lossy(&output.stdout).trim().to_string();
235    let stderr: String = String::from_utf8_lossy(&output.stderr).trim().to_string();
236    if !stdout.is_empty() {
237        for line in stdout.lines() {
238            log::info!("{line}");
239        }
240    }
241    if !stderr.is_empty() {
242        if output.status.success() {
243            for line in stderr.lines() {
244                if line.is_empty() {
245                    continue;
246                }
247                log::info!("{line}");
248            }
249        } else {
250            for line in stderr.lines() {
251                if line.is_empty() {
252                    continue;
253                }
254                log::error!("{line}");
255            }
256        }
257    }
258    if !output.status.success() {
259        return Err(io::Error::other(ERROR_CLIPPY_FIX_FAILED));
260    }
261    Ok(())
262}
263
264/// Execute fmt command
265///
266/// # Arguments
267///
268/// - `&Args` - The parsed arguments
269///
270/// # Returns
271///
272/// - `Result<(), io::Error>` - Success or error
273pub async fn execute_fmt(args: &Args) -> Result<(), io::Error> {
274    let manifest_path: String = args
275        .manifest_path
276        .clone()
277        .unwrap_or_else(|| CARGO_TOML.to_string());
278    if !args.check {
279        format_derive_attributes(&manifest_path).await?;
280    }
281    let mut cmd: Command = Command::new(CARGO);
282    cmd.arg(CARGO_FMT);
283    if args.check {
284        cmd.arg(CLI_FLAG_CHECK);
285    }
286    if let Some(ref manifest_path) = args.manifest_path {
287        cmd.arg(CLI_FLAG_MANIFEST_PATH).arg(manifest_path);
288    }
289    cmd.stdout(Stdio::piped()).stderr(Stdio::piped());
290    let output: std::process::Output = cmd.output().await?;
291    let stdout: String = String::from_utf8_lossy(&output.stdout).trim().to_string();
292    let stderr: String = String::from_utf8_lossy(&output.stderr).trim().to_string();
293    if !stdout.is_empty() {
294        for line in stdout.lines() {
295            log::info!("{line}");
296        }
297    }
298    if !stderr.is_empty() {
299        if output.status.success() {
300            for line in stderr.lines() {
301                if line.is_empty() {
302                    continue;
303                }
304                log::info!("{line}");
305            }
306        } else {
307            for line in stderr.lines() {
308                if line.is_empty() {
309                    continue;
310                }
311                log::error!("{line}");
312            }
313        }
314    }
315    if !output.status.success() {
316        return Err(io::Error::other(ERROR_FMT_FAILED));
317    }
318    if !args.check {
319        execute_clippy_fix(args).await?;
320    }
321    Ok(())
322}
323
324/// Format code at specific path
325///
326/// # Arguments
327///
328/// - `&Path` - Path to format
329///
330/// # Returns
331///
332/// - `Result<(), io::Error>` - Success or error
333pub async fn format_path(path: &Path) -> Result<(), io::Error> {
334    let mut cmd: Command = Command::new(CARGO);
335    cmd.arg(CARGO_FMT).arg("--").arg(path);
336    cmd.stdout(Stdio::null()).stderr(Stdio::null());
337    cmd.status().await?;
338    Ok(())
339}