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).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
198/// Execute clippy fix command
199///
200/// # Arguments
201///
202/// - `&Args`: The parsed arguments
203///
204/// # Returns
205///
206/// - `Result<(), io::Error>`: Success or error
207async 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
236/// Execute fmt command
237///
238/// # Arguments
239///
240/// - `&Args`: The parsed arguments
241///
242/// # Returns
243///
244/// - `Result<(), io::Error>`: Success or error
245pub 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
280/// Format code at specific path
281///
282/// # Arguments
283///
284/// - `&Path`: Path to format
285///
286/// # Returns
287///
288/// - `Result<(), io::Error>`: Success or error
289pub 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}