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| 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, std::io::Error> {
36 let content: String = read_to_string(file_path)?;
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)?;
59 }
60 Ok(modified)
61}
62
63async fn find_rust_files(manifest_path: &Path) -> Result<Vec<PathBuf>, std::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)?;
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(
109 dir: &Path,
110 files: &mut Vec<PathBuf>,
111) -> Result<(), std::io::Error> {
112 let mut entries = tokio::fs::read_dir(dir).await?;
113 while let Some(entry) = entries.next_entry().await? {
114 let path: PathBuf = entry.path();
115 if path.is_file()
116 && path
117 .extension()
118 .is_some_and(|ext: &std::ffi::OsStr| ext == "rs")
119 {
120 files.push(path);
121 } else if path.is_dir() {
122 Box::pin(find_rust_files_in_dir(&path, files)).await?;
123 }
124 }
125 Ok(())
126}
127
128async fn format_derive_attributes(manifest_path: &str) -> Result<(), std::io::Error> {
138 let path: &Path = Path::new(manifest_path);
139 let files: Vec<PathBuf> = find_rust_files(path).await?;
140 let modified_count: Arc<Mutex<usize>> = Arc::new(Mutex::new(0));
141 let mut handles: Vec<tokio::task::JoinHandle<Result<(), std::io::Error>>> = Vec::new();
142 for file in files {
143 let counter: Arc<Mutex<usize>> = Arc::clone(&modified_count);
144 let handle: tokio::task::JoinHandle<Result<(), std::io::Error>> =
145 tokio::spawn(async move {
146 if format_derive_in_file(&file).await? {
147 let mut count: tokio::sync::MutexGuard<'_, usize> = counter.lock().await;
148 *count += 1;
149 }
150 Ok(())
151 });
152 handles.push(handle);
153 }
154 for handle in handles {
155 handle.await??;
156 }
157 let count: usize = *modified_count.lock().await;
158 if count > 0 {
159 println!("Sorted derive attributes in {count} files");
160 }
161 Ok(())
162}
163
164async fn is_cargo_clippy_installed() -> bool {
170 Command::new("cargo")
171 .arg("clippy")
172 .arg("--version")
173 .stdout(Stdio::null())
174 .stderr(Stdio::null())
175 .status()
176 .await
177 .is_ok_and(|status: ExitStatus| status.success())
178}
179
180async fn install_cargo_clippy() -> Result<(), std::io::Error> {
186 println!("cargo-clippy not found, installing...");
187 let mut cmd: Command = Command::new("rustup");
188 cmd.arg("component").arg("add").arg("clippy");
189 cmd.stdout(Stdio::inherit()).stderr(Stdio::inherit());
190 let status: ExitStatus = cmd.status().await?;
191 if !status.success() {
192 return Err(std::io::Error::other("failed to install cargo-clippy"));
193 }
194 Ok(())
195}
196
197async fn execute_clippy_fix(args: &Args) -> Result<(), std::io::Error> {
207 if !is_cargo_clippy_installed().await {
208 install_cargo_clippy().await?;
209 }
210 let mut cmd: Command = Command::new("cargo");
211 cmd.arg("clippy")
212 .arg("--fix")
213 .arg("--workspace")
214 .arg("--all-targets")
215 .arg("--allow-dirty");
216 if let Some(ref manifest_path) = args.manifest_path {
217 cmd.arg("--manifest-path").arg(manifest_path);
218 }
219 cmd.stdout(Stdio::inherit()).stderr(Stdio::inherit());
220 let status: ExitStatus = cmd.status().await?;
221 if !status.success() {
222 return Err(std::io::Error::other("cargo clippy --fix failed"));
223 }
224 Ok(())
225}
226
227pub async fn execute_fmt(args: &Args) -> Result<(), std::io::Error> {
237 let manifest_path: String = args
238 .manifest_path
239 .clone()
240 .unwrap_or_else(|| "Cargo.toml".to_string());
241 if !args.check {
242 format_derive_attributes(&manifest_path).await?;
243 }
244 let mut cmd: Command = Command::new("cargo");
245 cmd.arg("fmt");
246 if args.check {
247 cmd.arg("--check");
248 }
249 if let Some(ref manifest_path) = args.manifest_path {
250 cmd.arg("--manifest-path").arg(manifest_path);
251 }
252 cmd.stdout(Stdio::inherit()).stderr(Stdio::inherit());
253 let status: ExitStatus = cmd.status().await?;
254 if !status.success() {
255 return Err(std::io::Error::other("cargo fmt failed"));
256 }
257 if !args.check {
258 execute_clippy_fix(args).await?;
259 }
260 Ok(())
261}
262
263pub async fn format_path(path: &std::path::Path) -> Result<(), std::io::Error> {
273 let mut cmd: Command = Command::new("cargo");
274 cmd.arg("fmt").arg("--").arg(path);
275 cmd.stdout(Stdio::null()).stderr(Stdio::null());
276 cmd.status().await?;
277 Ok(())
278}