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).to_string();
185 let stderr: String = String::from_utf8_lossy(&output.stderr).to_string();
186 if !stdout.is_empty() {
187 log::info!("{stdout}");
188 }
189 if !stderr.is_empty() {
190 log::error!("{stderr}");
191 }
192 if !output.status.success() {
193 return Err(io::Error::other("failed to install cargo-clippy"));
194 }
195 Ok(())
196}
197
198async fn execute_clippy_fix(args: &Args) -> Result<(), io::Error> {
208 if !is_cargo_clippy_installed() {
209 install_cargo_clippy().await?;
210 }
211 let mut cmd: Command = Command::new("cargo");
212 cmd.arg("clippy")
213 .arg("--fix")
214 .arg("--workspace")
215 .arg("--all-targets")
216 .arg("--allow-dirty");
217 if let Some(ref manifest_path) = args.manifest_path {
218 cmd.arg("--manifest-path").arg(manifest_path);
219 }
220 cmd.stdout(Stdio::piped()).stderr(Stdio::piped());
221 let output: std::process::Output = cmd.output().await?;
222 let stdout: String = String::from_utf8_lossy(&output.stdout).to_string();
223 let stderr: String = String::from_utf8_lossy(&output.stderr).to_string();
224 if !stdout.is_empty() {
225 log::info!("{stdout}");
226 }
227 if !stderr.is_empty() {
228 log::error!("{stderr}");
229 }
230 if !output.status.success() {
231 return Err(io::Error::other("cargo clippy --fix failed"));
232 }
233 Ok(())
234}
235
236pub async fn execute_fmt(args: &Args) -> Result<(), io::Error> {
246 let manifest_path: String = args
247 .manifest_path
248 .clone()
249 .unwrap_or_else(|| "Cargo.toml".to_string());
250 if !args.check {
251 format_derive_attributes(&manifest_path).await?;
252 }
253 let mut cmd: Command = Command::new("cargo");
254 cmd.arg("fmt");
255 if args.check {
256 cmd.arg("--check");
257 }
258 if let Some(ref manifest_path) = args.manifest_path {
259 cmd.arg("--manifest-path").arg(manifest_path);
260 }
261 cmd.stdout(Stdio::piped()).stderr(Stdio::piped());
262 let output: std::process::Output = cmd.output().await?;
263 let stdout: String = String::from_utf8_lossy(&output.stdout).to_string();
264 let stderr: String = String::from_utf8_lossy(&output.stderr).to_string();
265 if !stdout.is_empty() {
266 log::info!("{stdout}");
267 }
268 if !stderr.is_empty() {
269 log::error!("{stderr}");
270 }
271 if !output.status.success() {
272 return Err(io::Error::other("cargo fmt failed"));
273 }
274 if !args.check {
275 execute_clippy_fix(args).await?;
276 }
277 Ok(())
278}
279
280pub async fn format_path(path: &Path) -> Result<(), io::Error> {
290 let mut cmd: Command = Command::new("cargo");
291 cmd.arg("fmt").arg("--").arg(path);
292 cmd.stdout(Stdio::null()).stderr(Stdio::null());
293 cmd.status().await?;
294 Ok(())
295}