sort_all_enums/
main.rs

1use {
2    clap::Parser,
3    codesort::*,
4    lazy_regex::*,
5    std::{
6        fs,
7        io,
8        path::PathBuf,
9    },
10    termimad::crossterm::style::Stylize,
11};
12
13/// Directories we don't want to touch
14static EXCLUDED_DIRS: &[&str] = &[".git", "target", "build"];
15
16/// Keywords which, if found in the annotations before an enum, will
17/// prevent the enum variants from being sorted
18static EXCLUDING_KEYWORDS: &[&str] = &["repr", "serde", "PartialOrd", "Ord"];
19
20/// Sort all enums of all rust files found in the given directory
21///
22/// Are excluded
23/// - files in .git, target and build directories
24/// - files which don't appear correct enough
25#[derive(Debug, Parser)]
26#[command(about, version)]
27pub struct Args {
28    /// directories normally excluded to include
29    #[clap(long, default_value = "")]
30    pub include: Vec<String>,
31
32    /// directory names to exclude
33    #[clap(long, default_value = "")]
34    pub exclude: Vec<String>,
35
36    /// Path to the file(s)
37    pub path: PathBuf,
38}
39
40pub fn get_all_rust_files(
41    root: PathBuf,
42    include: &[String],
43    exclude: &[String],
44) -> io::Result<Vec<PathBuf>> {
45    let mut files = Vec::new();
46    // if we're given a single file, it's probably because the user
47    // wants to sort it, so we don't check the extension
48    if !root.is_dir() {
49        files.push(root);
50        return Ok(files);
51    }
52    let mut dirs = vec![root];
53    while let Some(dir) = dirs.pop() {
54        for entry in fs::read_dir(dir)? {
55            let path = entry?.path();
56            let Some(file_name) = path.file_name().and_then(|s| s.to_str()) else {
57                continue;
58            };
59            if path.is_dir() {
60                if file_name.starts_with('.') {
61                    continue;
62                }
63                if exclude.iter().any(|ex| ex == file_name) {
64                    eprintln!("{} {:?}", "Excluded".yellow(), path);
65                    continue;
66                }
67                if EXCLUDED_DIRS.contains(&file_name) {
68                    if !include.iter().any(|inc| inc == file_name) {
69                        eprintln!("{} {:?}", "Excluded".yellow(), path);
70                        continue;
71                    }
72                }
73                dirs.push(path.to_path_buf());
74                continue;
75            }
76            if let Some(ext) = path.extension() {
77                if ext.to_str() == Some("rs") {
78                    files.push(path.to_path_buf());
79                }
80            }
81        }
82    }
83    Ok(files)
84}
85
86fn main() -> CsResult<()> {
87    let start = std::time::Instant::now();
88    let args = Args::parse();
89    let files = get_all_rust_files(args.path, &args.include, &args.exclude)?;
90    eprintln!("Found {} rust files", files.len());
91    let mut sorted_enum_count = 0;
92    let mut ok_files_count = 0;
93    let mut invalid_files_count = 0;
94    let mut incomplete_files_count = 0;
95    let mut excluded_enums_count = 0;
96    let mut modified_files_count = 0;
97    let mut empty_files_count = 0;
98    for file in &files {
99        let loc_list = LocList::read_file(file, Language::Rust);
100        let mut loc_list = match loc_list {
101            Ok(loc_list) => loc_list,
102            Err(e) => {
103                eprintln!("{} in {}: {:?}", "ERROR".red(), file.display(), e);
104                invalid_files_count += 1;
105                continue;
106            }
107        };
108        if !loc_list.has_content() {
109            empty_files_count += 1;
110            continue;
111        }
112        if !loc_list.is_complete() {
113            eprintln!(
114                "skipping {} ({})",
115                file.display(),
116                "not consistent enough".yellow()
117            );
118            incomplete_files_count += 1;
119            continue;
120        }
121        let mut modified = false;
122        let mut line_idx = 0;
123        ok_files_count += 1;
124        while line_idx + 2 < loc_list.len() {
125            let loc = &loc_list.locs[line_idx];
126            if !loc.starts_normal {
127                line_idx += 1;
128                continue;
129            }
130            let content = &loc.content;
131            let Some((_, name)) =
132                regex_captures!(r"^[\s\w()]*\benum\s+([^({]+)\s+\{\s**$", content)
133            else {
134                line_idx += 1;
135                continue;
136            };
137
138            // We look whether the annotations before the enum contain any one
139            // of the excluding keywords
140            let whole_enum_range = loc_list
141                .block_range_of_line_number(LineNumber::from_index(line_idx))
142                .unwrap();
143            let whole_enum_range = loc_list.trimmed_range(whole_enum_range);
144            let excluding_keyword = EXCLUDING_KEYWORDS.iter().find(|&keyword| {
145                loc_list.locs[whole_enum_range.start.to_index()..line_idx]
146                    .iter()
147                    .any(|loc| loc.sort_key.contains(keyword))
148            });
149            if let Some(excluding_keyword) = excluding_keyword {
150                eprintln!("skipping enum {} ({})", name, excluding_keyword.yellow());
151                excluded_enums_count += 1;
152                line_idx = whole_enum_range.end.to_index() + 1;
153                continue;
154            }
155            loc_list.print_range_debug(
156                &format!(" sorting enum {} ", name.blue()),
157                whole_enum_range,
158            );
159            let range = loc_list.range_around_line_index(line_idx + 1).unwrap();
160            loc_list.sort_range(range).unwrap();
161            line_idx = range.end.to_index() + 2;
162            sorted_enum_count += 1;
163            modified = true;
164        }
165        if modified {
166            loc_list.write_file(file)?;
167            eprintln!("wrote {}", file.display());
168            modified_files_count += 1;
169        }
170    }
171    eprintln!("\nDone in {:.3}s\n", start.elapsed().as_secs_f32());
172    eprintln!("I analyzed {} files", files.len());
173    let mut problems = Vec::new();
174    if empty_files_count > 0 {
175        problems.push(format!("{} empty files", empty_files_count));
176    }
177    if incomplete_files_count > 0 {
178        problems.push(format!("{} incomplete files", incomplete_files_count));
179    }
180    if invalid_files_count > 0 {
181        problems.push(format!("{} invalid files", invalid_files_count));
182    }
183    if problems.is_empty() {
184        eprintln!("All {} files were ok", ok_files_count);
185    } else {
186        eprintln!(
187            "{} files were OK but I encountered {}",
188            ok_files_count,
189            problems.join(", ")
190        );
191    }
192    if excluded_enums_count > 0 {
193        eprintln!(
194            "I excluded {} enums whose annotation contained excluding keywords",
195            excluded_enums_count
196        );
197    }
198    eprintln!(
199        "I sorted {} enums in {} files",
200        sorted_enum_count, modified_files_count
201    );
202    Ok(())
203}