Skip to content

Commit d6481c0

Browse files
committed
feat: v0.1.9
1 parent 4c812ba commit d6481c0

5 files changed

Lines changed: 187 additions & 3 deletions

File tree

Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "hyperlane-cli"
3-
version = "0.1.8"
3+
version = "0.1.9"
44
readme = "README.md"
55
edition = "2024"
66
authors = ["root@ltpp.vip"]
@@ -13,6 +13,7 @@ exclude = ["target", "Cargo.lock", "sh", ".github", "tmp"]
1313

1414
[dependencies]
1515
toml = "0.9.8"
16+
regex = "1.11.1"
1617
thiserror = "2.0.18"
1718
tokio = { version = "1.49.0", features = ["full"] }
1819

src/fmt/fn.rs

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,168 @@
11
use crate::*;
22

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
12+
fn 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
35+
async 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
72+
async 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+
if let Some(workspace) = doc.get("workspace") {
82+
if 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+
}
96+
}
97+
Ok(files)
98+
}
99+
100+
/// Recursively find Rust files in directory
101+
///
102+
/// # Arguments
103+
///
104+
/// - `&Path`: Directory to search
105+
/// - `&mut Vec<PathBuf>`: Vector to collect file paths
106+
///
107+
/// # Returns
108+
///
109+
/// - `Result<(), std::io::Error>`: Success or error
110+
async fn find_rust_files_in_dir(
111+
dir: &Path,
112+
files: &mut Vec<PathBuf>,
113+
) -> Result<(), std::io::Error> {
114+
let mut entries = tokio::fs::read_dir(dir).await?;
115+
while let Some(entry) = entries.next_entry().await? {
116+
let path: PathBuf = entry.path();
117+
if path.is_file()
118+
&& path
119+
.extension()
120+
.is_some_and(|ext: &std::ffi::OsStr| ext == "rs")
121+
{
122+
files.push(path);
123+
} else if path.is_dir() {
124+
Box::pin(find_rust_files_in_dir(&path, files)).await?;
125+
}
126+
}
127+
Ok(())
128+
}
129+
130+
/// Format derive attributes in all workspace files
131+
///
132+
/// # Arguments
133+
///
134+
/// - `&str`: Path to Cargo.toml
135+
///
136+
/// # Returns
137+
///
138+
/// - `Result<(), std::io::Error>`: Success or error
139+
async fn format_derive_attributes(manifest_path: &str) -> Result<(), std::io::Error> {
140+
let path: &Path = Path::new(manifest_path);
141+
let files: Vec<PathBuf> = find_rust_files(path).await?;
142+
let modified_count: Arc<Mutex<usize>> = Arc::new(Mutex::new(0));
143+
let mut handles: Vec<tokio::task::JoinHandle<Result<(), std::io::Error>>> = Vec::new();
144+
for file in files {
145+
let counter: Arc<Mutex<usize>> = Arc::clone(&modified_count);
146+
let handle: tokio::task::JoinHandle<Result<(), std::io::Error>> =
147+
tokio::spawn(async move {
148+
if format_derive_in_file(&file).await? {
149+
let mut count: tokio::sync::MutexGuard<'_, usize> = counter.lock().await;
150+
*count += 1;
151+
}
152+
Ok(())
153+
});
154+
handles.push(handle);
155+
}
156+
for handle in handles {
157+
handle.await??;
158+
}
159+
let count: usize = *modified_count.lock().await;
160+
if count > 0 {
161+
println!("Sorted derive attributes in {count} files");
162+
}
163+
Ok(())
164+
}
165+
3166
/// Check if cargo-clippy is installed
4167
///
5168
/// # Returns
@@ -73,6 +236,13 @@ async fn execute_clippy_fix(args: &Args) -> Result<(), std::io::Error> {
73236
///
74237
/// - `Result<(), std::io::Error>`: Success or error
75238
pub(crate) async fn execute_fmt(args: &Args) -> Result<(), std::io::Error> {
239+
let manifest_path: String = args
240+
.manifest_path
241+
.clone()
242+
.unwrap_or_else(|| "Cargo.toml".to_string());
243+
if !args.check {
244+
format_derive_attributes(&manifest_path).await?;
245+
}
76246
let mut cmd: Command = Command::new("cargo");
77247
cmd.arg("fmt");
78248
if args.check {

src/fmt/mod.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
mod r#fn;
2+
mod r#static;
23

34
#[cfg(test)]
45
mod test;
56

6-
pub(crate) use r#fn::*;
7+
pub(crate) use {r#fn::*, r#static::*};

src/fmt/static.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
use crate::*;
2+
3+
/// Regex pattern to match derive attribute
4+
///
5+
/// This pattern matches `#[derive(...)]` attributes in Rust code.
6+
pub(crate) static DERIVE_REGEX: LazyLock<Regex> = LazyLock::new(|| {
7+
regex::Regex::new(r"#\[derive\s*\(([^)]+)\)\]").expect("Invalid regex pattern")
8+
});

src/main.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,13 @@ pub(crate) use std::{
2525
path::{Path, PathBuf},
2626
process::{ExitStatus, Stdio, exit},
2727
str::FromStr,
28+
sync::{Arc, LazyLock},
2829
};
2930

30-
pub(crate) use tokio::process::Command;
31+
pub(crate) use {
32+
regex::{Captures, Regex},
33+
tokio::{process::Command, sync::Mutex},
34+
};
3135

3236
#[tokio::main]
3337
async fn main() {

0 commit comments

Comments
 (0)