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_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
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(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
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() && 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
121async 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
156fn is_cargo_clippy_installed() -> bool {
162 which(CARGO_CLIPPY).is_ok()
163}
164
165async 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
210async 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
264pub 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
324pub 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}