Skip to main content

hyperlane_cli/fmt/
fn.rs

1use super::*;
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::<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
96/// Recursively find Rust files in directory
97///
98/// # Arguments
99///
100/// - `&Path`: Directory to search
101/// - `&mut Vec<PathBuf>`: Vector to collect file paths
102///
103/// # Returns
104///
105/// - `Result<(), io::Error>`: Success or error
106async 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
119/// Format derive attributes in all workspace files
120///
121/// # Arguments
122///
123/// - `&str`: Path to Cargo.toml
124///
125/// # Returns
126///
127/// - `Result<(), io::Error>`: Success or error
128async 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
154/// Check if cargo-clippy is installed
155///
156/// # Returns
157///
158/// - `bool`: True if cargo-clippy is available
159fn is_cargo_clippy_installed() -> bool {
160    which("cargo-clippy").is_ok()
161}
162
163/// Install cargo-clippy using rustup
164///
165/// # Returns
166///
167/// - `Result<(), io::Error>`: Success or error
168async 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
208/// Execute clippy fix command
209///
210/// # Arguments
211///
212/// - `&Args`: The parsed arguments
213///
214/// # Returns
215///
216/// - `Result<(), io::Error>`: Success or error
217async 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
262/// Execute fmt command
263///
264/// # Arguments
265///
266/// - `&Args`: The parsed arguments
267///
268/// # Returns
269///
270/// - `Result<(), io::Error>`: Success or error
271pub 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
322/// Format code at specific path
323///
324/// # Arguments
325///
326/// - `&Path`: Path to format
327///
328/// # Returns
329///
330/// - `Result<(), io::Error>`: Success or error
331pub 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}