1use super::*;
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::<Value>(&content)
81 && let Some(workspace) = doc.get("workspace")
82 && let Some(members) = workspace.get("members").and_then(|m: &Value| m.as_array())
83 {
84 for member in members {
85 if let Some(pattern) = member.as_str() {
86 let member_src: PathBuf = workspace_root.join(pattern).join("src");
87 if member_src.exists() {
88 find_rust_files_in_dir(&member_src, &mut files).await?;
89 }
90 }
91 }
92 }
93 Ok(files)
94}
95
96async fn find_rust_files_in_dir(dir: &Path, files: &mut Vec<PathBuf>) -> Result<(), io::Error> {
107 let mut entries: ReadDir = read_dir(dir).await?;
108 while let Some(entry) = entries.next_entry().await? {
109 let path: PathBuf = entry.path();
110 if path.is_file() && path.extension().is_some_and(|ext: &OsStr| ext == "rs") {
111 files.push(path);
112 } else if path.is_dir() {
113 Box::pin(find_rust_files_in_dir(&path, files)).await?;
114 }
115 }
116 Ok(())
117}
118
119async fn format_derive_attributes(manifest_path: &str) -> Result<(), io::Error> {
129 let path: &Path = Path::new(manifest_path);
130 let files: Vec<PathBuf> = find_rust_files(path).await?;
131 let modified_count: Arc<Mutex<usize>> = Arc::new(Mutex::new(0));
132 let mut handles: Vec<JoinHandle<Result<(), io::Error>>> = Vec::new();
133 for file in files {
134 let counter: Arc<Mutex<usize>> = Arc::clone(&modified_count);
135 let handle: JoinHandle<Result<(), io::Error>> = spawn(async move {
136 if format_derive_in_file(&file).await? {
137 let mut count: MutexGuard<'_, usize> = counter.lock().await;
138 *count += 1;
139 }
140 Ok(())
141 });
142 handles.push(handle);
143 }
144 for handle in handles {
145 handle.await??;
146 }
147 let count: usize = *modified_count.lock().await;
148 if count > 0 {
149 log::info!("Sorted derive attributes in {count} files");
150 }
151 Ok(())
152}
153
154fn is_cargo_clippy_installed() -> bool {
160 which("cargo-clippy").is_ok()
161}
162
163async fn install_cargo_clippy() -> Result<(), io::Error> {
169 log::warn!("cargo-clippy not found, installing...");
170 let output: std::process::Output = Command::new("rustup")
171 .arg("component")
172 .arg("add")
173 .arg("clippy")
174 .stdout(Stdio::piped())
175 .stderr(Stdio::piped())
176 .output()
177 .await?;
178 let stdout: String = String::from_utf8_lossy(&output.stdout).trim().to_string();
179 let stderr: String = String::from_utf8_lossy(&output.stderr).trim().to_string();
180 if !stdout.is_empty() {
181 for line in stdout.lines() {
182 log::info!("{line}");
183 }
184 }
185 if !stderr.is_empty() {
186 if output.status.success() {
187 for line in stderr.lines() {
188 if line.is_empty() {
189 continue;
190 }
191 log::info!("{line}");
192 }
193 } else {
194 for line in stderr.lines() {
195 if line.is_empty() {
196 continue;
197 }
198 log::error!("{line}");
199 }
200 }
201 }
202 if !output.status.success() {
203 return Err(io::Error::other("failed to install cargo-clippy"));
204 }
205 Ok(())
206}
207
208async fn execute_clippy_fix(args: &Args) -> Result<(), io::Error> {
218 if !is_cargo_clippy_installed() {
219 install_cargo_clippy().await?;
220 }
221 let mut cmd: Command = Command::new("cargo");
222 cmd.arg("clippy")
223 .arg("--fix")
224 .arg("--workspace")
225 .arg("--all-targets")
226 .arg("--allow-dirty");
227 if let Some(ref manifest_path) = args.manifest_path {
228 cmd.arg("--manifest-path").arg(manifest_path);
229 }
230 cmd.stdout(Stdio::piped()).stderr(Stdio::piped());
231 let output: std::process::Output = cmd.output().await?;
232 let stdout: String = String::from_utf8_lossy(&output.stdout).trim().to_string();
233 let stderr: String = String::from_utf8_lossy(&output.stderr).trim().to_string();
234 if !stdout.is_empty() {
235 for line in stdout.lines() {
236 log::info!("{line}");
237 }
238 }
239 if !stderr.is_empty() {
240 if output.status.success() {
241 for line in stderr.lines() {
242 if line.is_empty() {
243 continue;
244 }
245 log::info!("{line}");
246 }
247 } else {
248 for line in stderr.lines() {
249 if line.is_empty() {
250 continue;
251 }
252 log::error!("{line}");
253 }
254 }
255 }
256 if !output.status.success() {
257 return Err(io::Error::other("cargo clippy --fix failed"));
258 }
259 Ok(())
260}
261
262pub async fn execute_fmt(args: &Args) -> Result<(), io::Error> {
272 let manifest_path: String = args
273 .manifest_path
274 .clone()
275 .unwrap_or_else(|| "Cargo.toml".to_string());
276 if !args.check {
277 format_derive_attributes(&manifest_path).await?;
278 }
279 let mut cmd: Command = Command::new("cargo");
280 cmd.arg("fmt");
281 if args.check {
282 cmd.arg("--check");
283 }
284 if let Some(ref manifest_path) = args.manifest_path {
285 cmd.arg("--manifest-path").arg(manifest_path);
286 }
287 cmd.stdout(Stdio::piped()).stderr(Stdio::piped());
288 let output: std::process::Output = cmd.output().await?;
289 let stdout: String = String::from_utf8_lossy(&output.stdout).trim().to_string();
290 let stderr: String = String::from_utf8_lossy(&output.stderr).trim().to_string();
291 if !stdout.is_empty() {
292 for line in stdout.lines() {
293 log::info!("{line}");
294 }
295 }
296 if !stderr.is_empty() {
297 if output.status.success() {
298 for line in stderr.lines() {
299 if line.is_empty() {
300 continue;
301 }
302 log::info!("{line}");
303 }
304 } else {
305 for line in stderr.lines() {
306 if line.is_empty() {
307 continue;
308 }
309 log::error!("{line}");
310 }
311 }
312 }
313 if !output.status.success() {
314 return Err(io::Error::other("cargo fmt failed"));
315 }
316 if !args.check {
317 execute_clippy_fix(args).await?;
318 }
319 Ok(())
320}
321
322pub async fn format_path(path: &Path) -> Result<(), io::Error> {
332 let mut cmd: Command = Command::new("cargo");
333 cmd.arg("fmt").arg("--").arg(path);
334 cmd.stdout(Stdio::null()).stderr(Stdio::null());
335 cmd.status().await?;
336 Ok(())
337}