1use crate::*;
2
3fn 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
26async 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(") {
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
63async 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::<toml::Value>(&content)
81 && let Some(workspace) = doc.get("workspace")
82 && let Some(members) = workspace
83 .get("members")
84 .and_then(|m: &toml::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
98async 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()
113 && path
114 .extension()
115 .is_some_and(|ext: &std::ffi::OsStr| ext == "rs")
116 {
117 files.push(path);
118 } else if path.is_dir() {
119 Box::pin(find_rust_files_in_dir(&path, files)).await?;
120 }
121 }
122 Ok(())
123}
124
125async fn format_derive_attributes(manifest_path: &str) -> Result<(), io::Error> {
135 let path: &Path = Path::new(manifest_path);
136 let files: Vec<PathBuf> = find_rust_files(path).await?;
137 let modified_count: Arc<Mutex<usize>> = Arc::new(Mutex::new(0));
138 let mut handles: Vec<JoinHandle<Result<(), io::Error>>> = Vec::new();
139 for file in files {
140 let counter: Arc<Mutex<usize>> = Arc::clone(&modified_count);
141 let handle: JoinHandle<Result<(), io::Error>> = spawn(async move {
142 if format_derive_in_file(&file).await? {
143 let mut count: MutexGuard<'_, usize> = counter.lock().await;
144 *count += 1;
145 }
146 Ok(())
147 });
148 handles.push(handle);
149 }
150 for handle in handles {
151 handle.await??;
152 }
153 let count: usize = *modified_count.lock().await;
154 if count > 0 {
155 log::info!("Sorted derive attributes in {count} files");
156 }
157 Ok(())
158}
159
160fn is_cargo_clippy_installed() -> bool {
166 which("cargo-clippy").is_ok()
167}
168
169async fn install_cargo_clippy() -> Result<(), io::Error> {
175 log::warn!("cargo-clippy not found, installing...");
176 let output: std::process::Output = Command::new("rustup")
177 .arg("component")
178 .arg("add")
179 .arg("clippy")
180 .stdout(Stdio::piped())
181 .stderr(Stdio::piped())
182 .output()
183 .await?;
184 let stdout: String = String::from_utf8_lossy(&output.stdout).trim().to_string();
185 let stderr: String = String::from_utf8_lossy(&output.stderr).trim().to_string();
186 if !stdout.is_empty() {
187 for line in stdout.lines() {
188 log::info!("{line}");
189 }
190 }
191 if !stderr.is_empty() {
192 if output.status.success() {
193 for line in stderr.lines() {
194 if line.is_empty() {
195 continue;
196 }
197 log::info!("{line}");
198 }
199 } else {
200 for line in stderr.lines() {
201 if line.is_empty() {
202 continue;
203 }
204 log::error!("{line}");
205 }
206 }
207 }
208 if !output.status.success() {
209 return Err(io::Error::other("failed to install cargo-clippy"));
210 }
211 Ok(())
212}
213
214async fn execute_clippy_fix(args: &Args) -> Result<(), io::Error> {
224 if !is_cargo_clippy_installed() {
225 install_cargo_clippy().await?;
226 }
227 let mut cmd: Command = Command::new("cargo");
228 cmd.arg("clippy")
229 .arg("--fix")
230 .arg("--workspace")
231 .arg("--all-targets")
232 .arg("--allow-dirty");
233 if let Some(ref manifest_path) = args.manifest_path {
234 cmd.arg("--manifest-path").arg(manifest_path);
235 }
236 cmd.stdout(Stdio::piped()).stderr(Stdio::piped());
237 let output: std::process::Output = cmd.output().await?;
238 let stdout: String = String::from_utf8_lossy(&output.stdout).trim().to_string();
239 let stderr: String = String::from_utf8_lossy(&output.stderr).trim().to_string();
240 if !stdout.is_empty() {
241 for line in stdout.lines() {
242 log::info!("{line}");
243 }
244 }
245 if !stderr.is_empty() {
246 if output.status.success() {
247 for line in stderr.lines() {
248 if line.is_empty() {
249 continue;
250 }
251 log::info!("{line}");
252 }
253 } else {
254 for line in stderr.lines() {
255 if line.is_empty() {
256 continue;
257 }
258 log::error!("{line}");
259 }
260 }
261 }
262 if !output.status.success() {
263 return Err(io::Error::other("cargo clippy --fix failed"));
264 }
265 Ok(())
266}
267
268pub async fn execute_fmt(args: &Args) -> Result<(), io::Error> {
278 let manifest_path: String = args
279 .manifest_path
280 .clone()
281 .unwrap_or_else(|| "Cargo.toml".to_string());
282 if !args.check {
283 format_derive_attributes(&manifest_path).await?;
284 }
285 let mut cmd: Command = Command::new("cargo");
286 cmd.arg("fmt");
287 if args.check {
288 cmd.arg("--check");
289 }
290 if let Some(ref manifest_path) = args.manifest_path {
291 cmd.arg("--manifest-path").arg(manifest_path);
292 }
293 cmd.stdout(Stdio::piped()).stderr(Stdio::piped());
294 let output: std::process::Output = cmd.output().await?;
295 let stdout: String = String::from_utf8_lossy(&output.stdout).trim().to_string();
296 let stderr: String = String::from_utf8_lossy(&output.stderr).trim().to_string();
297 if !stdout.is_empty() {
298 for line in stdout.lines() {
299 log::info!("{line}");
300 }
301 }
302 if !stderr.is_empty() {
303 if output.status.success() {
304 for line in stderr.lines() {
305 if line.is_empty() {
306 continue;
307 }
308 log::info!("{line}");
309 }
310 } else {
311 for line in stderr.lines() {
312 if line.is_empty() {
313 continue;
314 }
315 log::error!("{line}");
316 }
317 }
318 }
319 if !output.status.success() {
320 return Err(io::Error::other("cargo fmt failed"));
321 }
322 if !args.check {
323 execute_clippy_fix(args).await?;
324 }
325 Ok(())
326}
327
328pub async fn format_path(path: &Path) -> Result<(), io::Error> {
338 let mut cmd: Command = Command::new("cargo");
339 cmd.arg("fmt").arg("--").arg(path);
340 cmd.stdout(Stdio::null()).stderr(Stdio::null());
341 cmd.status().await?;
342 Ok(())
343}