Skip to main content

hyperlane_cli/fmt/
fn.rs

1use crate::*;
2
3/// Sort derive traits in a single line
4///
5/// # Arguments
6///
7/// - `&str`: The line containing derive attribute
8///
9/// # Returns
10///
11/// - `Option<String>`: Sorted line if derive found, None otherwise
12fn 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
26/// Format derive attributes in a file
27///
28/// # Arguments
29///
30/// - `&Path`: Path to the Rust file
31///
32/// # Returns
33///
34/// - `Result<bool, io::Error>`: True if file was modified, false otherwise
35async 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
63/// Find all Rust files in workspace
64///
65/// # Arguments
66///
67/// - `&Path`: Path to Cargo.toml
68///
69/// # Returns
70///
71/// - `Result<Vec<PathBuf>, io::Error>`: List of Rust file paths
72async 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
98/// Recursively find Rust files in directory
99///
100/// # Arguments
101///
102/// - `&Path`: Directory to search
103/// - `&mut Vec<PathBuf>`: Vector to collect file paths
104///
105/// # Returns
106///
107/// - `Result<(), io::Error>`: Success or error
108async 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
125/// Format derive attributes in all workspace files
126///
127/// # Arguments
128///
129/// - `&str`: Path to Cargo.toml
130///
131/// # Returns
132///
133/// - `Result<(), io::Error>`: Success or error
134async 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
160/// Check if cargo-clippy is installed
161///
162/// # Returns
163///
164/// - `bool`: True if cargo-clippy is available
165fn is_cargo_clippy_installed() -> bool {
166    which("cargo-clippy").is_ok()
167}
168
169/// Install cargo-clippy using rustup
170///
171/// # Returns
172///
173/// - `Result<(), io::Error>`: Success or error
174async 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
214/// Execute clippy fix command
215///
216/// # Arguments
217///
218/// - `&Args`: The parsed arguments
219///
220/// # Returns
221///
222/// - `Result<(), io::Error>`: Success or error
223async 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
268/// Execute fmt command
269///
270/// # Arguments
271///
272/// - `&Args`: The parsed arguments
273///
274/// # Returns
275///
276/// - `Result<(), io::Error>`: Success or error
277pub 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
328/// Format code at specific path
329///
330/// # Arguments
331///
332/// - `&Path`: Path to format
333///
334/// # Returns
335///
336/// - `Result<(), io::Error>`: Success or error
337pub 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}