tidy/
alphabetical.rs

1//! Checks that a list of items is in alphabetical order
2//!
3//! Use the following marker in the code:
4//! ```rust
5//! // tidy-alphabetical-start
6//! fn aaa() {}
7//! fn eee() {}
8//! fn z() {}
9//! // tidy-alphabetical-end
10//! ```
11//!
12//! The following lines are ignored:
13//! - Empty lines
14//! - Lines that are indented with more or less spaces than the first line
15//! - Lines starting with `//`, `#` (except those starting with `#!`), `)`, `]`, `}` if the comment
16//!   has the same indentation as the first line
17//! - Lines starting with a closing delimiter (`)`, `[`, `}`) are ignored.
18//!
19//! If a line ends with an opening delimiter, we effectively join the following line to it before
20//! checking it. E.g. `foo(\nbar)` is treated like `foo(bar)`.
21
22use std::fmt::Display;
23use std::path::Path;
24
25use crate::walk::{filter_dirs, walk};
26
27#[cfg(test)]
28mod tests;
29
30fn indentation(line: &str) -> usize {
31    line.find(|c| c != ' ').unwrap_or(0)
32}
33
34fn is_close_bracket(c: char) -> bool {
35    matches!(c, ')' | ']' | '}')
36}
37
38const START_MARKER: &str = "tidy-alphabetical-start";
39const END_MARKER: &str = "tidy-alphabetical-end";
40
41fn check_section<'a>(
42    file: impl Display,
43    lines: impl Iterator<Item = (usize, &'a str)>,
44    err: &mut dyn FnMut(&str) -> std::io::Result<()>,
45    bad: &mut bool,
46) {
47    let mut prev_line = String::new();
48    let mut first_indent = None;
49    let mut in_split_line = None;
50
51    for (idx, line) in lines {
52        if line.is_empty() {
53            continue;
54        }
55
56        if line.contains(START_MARKER) {
57            tidy_error_ext!(
58                err,
59                bad,
60                "{file}:{} found `{START_MARKER}` expecting `{END_MARKER}`",
61                idx + 1
62            );
63            return;
64        }
65
66        if line.contains(END_MARKER) {
67            return;
68        }
69
70        let indent = first_indent.unwrap_or_else(|| {
71            let indent = indentation(line);
72            first_indent = Some(indent);
73            indent
74        });
75
76        let line = if let Some(prev_split_line) = in_split_line {
77            // Join the split lines.
78            in_split_line = None;
79            format!("{prev_split_line}{}", line.trim_start())
80        } else {
81            line.to_string()
82        };
83
84        if indentation(&line) != indent {
85            continue;
86        }
87
88        let trimmed_line = line.trim_start_matches(' ');
89
90        if trimmed_line.starts_with("//")
91            || (trimmed_line.starts_with('#') && !trimmed_line.starts_with("#!"))
92            || trimmed_line.starts_with(is_close_bracket)
93        {
94            continue;
95        }
96
97        if line.trim_end().ends_with('(') {
98            in_split_line = Some(line);
99            continue;
100        }
101
102        let prev_line_trimmed_lowercase = prev_line.trim_start_matches(' ').to_lowercase();
103
104        if trimmed_line.to_lowercase() < prev_line_trimmed_lowercase {
105            tidy_error_ext!(err, bad, "{file}:{}: line not in alphabetical order", idx + 1);
106        }
107
108        prev_line = line;
109    }
110
111    tidy_error_ext!(err, bad, "{file}: reached end of file expecting `{END_MARKER}`")
112}
113
114fn check_lines<'a>(
115    file: &impl Display,
116    mut lines: impl Iterator<Item = (usize, &'a str)>,
117    err: &mut dyn FnMut(&str) -> std::io::Result<()>,
118    bad: &mut bool,
119) {
120    while let Some((idx, line)) = lines.next() {
121        if line.contains(END_MARKER) {
122            tidy_error_ext!(
123                err,
124                bad,
125                "{file}:{} found `{END_MARKER}` expecting `{START_MARKER}`",
126                idx + 1
127            )
128        }
129
130        if line.contains(START_MARKER) {
131            check_section(file, &mut lines, err, bad);
132        }
133    }
134}
135
136pub fn check(path: &Path, bad: &mut bool) {
137    let skip =
138        |path: &_, _is_dir| filter_dirs(path) || path.ends_with("tidy/src/alphabetical/tests.rs");
139
140    walk(path, skip, &mut |entry, contents| {
141        let file = &entry.path().display();
142        let lines = contents.lines().enumerate();
143        check_lines(file, lines, &mut crate::tidy_error, bad)
144    });
145}