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| 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, std::io::Error>`: True if file was modified, false otherwise
35async 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
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>, std::io::Error>`: List of Rust file paths
72async 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
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<(), std::io::Error>`: Success or error
108async 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
128/// Format derive attributes in all workspace files
129///
130/// # Arguments
131///
132/// - `&str`: Path to Cargo.toml
133///
134/// # Returns
135///
136/// - `Result<(), std::io::Error>`: Success or error
137async 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        log::info!("Sorted derive attributes in {count} files");
160    }
161    Ok(())
162}
163
164/// Check if cargo-clippy is installed
165///
166/// # Returns
167///
168/// - `bool`: True if cargo-clippy is available
169async 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
180/// Install cargo-clippy using rustup
181///
182/// # Returns
183///
184/// - `Result<(), std::io::Error>`: Success or error
185async fn install_cargo_clippy() -> Result<(), std::io::Error> {
186    log::warn!("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
197/// Execute clippy fix command
198///
199/// # Arguments
200///
201/// - `&Args`: The parsed arguments
202///
203/// # Returns
204///
205/// - `Result<(), std::io::Error>`: Success or error
206async 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
227/// Execute fmt command
228///
229/// # Arguments
230///
231/// - `&Args`: The parsed arguments
232///
233/// # Returns
234///
235/// - `Result<(), std::io::Error>`: Success or error
236pub 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
263/// Format code at specific path
264///
265/// # Arguments
266///
267/// - `&Path`: Path to format
268///
269/// # Returns
270///
271/// - `Result<(), std::io::Error>`: Success or error
272pub 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}