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::cmp::Ordering;
23use std::fmt::Display;
24use std::iter::Peekable;
25use std::path::Path;
26
27use crate::walk::{filter_dirs, walk};
28
29#[cfg(test)]
30mod tests;
31
32fn indentation(line: &str) -> usize {
33    line.find(|c| c != ' ').unwrap_or(0)
34}
35
36fn is_close_bracket(c: char) -> bool {
37    matches!(c, ')' | ']' | '}')
38}
39
40const START_MARKER: &str = "tidy-alphabetical-start";
41const END_MARKER: &str = "tidy-alphabetical-end";
42
43fn check_section<'a>(
44    file: impl Display,
45    lines: impl Iterator<Item = (usize, &'a str)>,
46    err: &mut dyn FnMut(&str) -> std::io::Result<()>,
47    bad: &mut bool,
48) {
49    let mut prev_line = String::new();
50    let mut first_indent = None;
51    let mut in_split_line = None;
52
53    for (idx, line) in lines {
54        if line.is_empty() {
55            continue;
56        }
57
58        if line.contains(START_MARKER) {
59            tidy_error_ext!(
60                err,
61                bad,
62                "{file}:{} found `{START_MARKER}` expecting `{END_MARKER}`",
63                idx + 1
64            );
65            return;
66        }
67
68        if line.contains(END_MARKER) {
69            return;
70        }
71
72        let indent = first_indent.unwrap_or_else(|| {
73            let indent = indentation(line);
74            first_indent = Some(indent);
75            indent
76        });
77
78        let line = if let Some(prev_split_line) = in_split_line {
79            // Join the split lines.
80            in_split_line = None;
81            format!("{prev_split_line}{}", line.trim_start())
82        } else {
83            line.to_string()
84        };
85
86        if indentation(&line) != indent {
87            continue;
88        }
89
90        let trimmed_line = line.trim_start_matches(' ');
91
92        if trimmed_line.starts_with("//")
93            || (trimmed_line.starts_with('#') && !trimmed_line.starts_with("#!"))
94            || trimmed_line.starts_with(is_close_bracket)
95        {
96            continue;
97        }
98
99        if line.trim_end().ends_with('(') {
100            in_split_line = Some(line);
101            continue;
102        }
103
104        let prev_line_trimmed_lowercase = prev_line.trim_start_matches(' ');
105
106        if version_sort(&trimmed_line, &prev_line_trimmed_lowercase).is_lt() {
107            tidy_error_ext!(err, bad, "{file}:{}: line not in alphabetical order", idx + 1);
108        }
109
110        prev_line = line;
111    }
112
113    tidy_error_ext!(err, bad, "{file}: reached end of file expecting `{END_MARKER}`")
114}
115
116fn check_lines<'a>(
117    file: &impl Display,
118    mut lines: impl Iterator<Item = (usize, &'a str)>,
119    err: &mut dyn FnMut(&str) -> std::io::Result<()>,
120    bad: &mut bool,
121) {
122    while let Some((idx, line)) = lines.next() {
123        if line.contains(END_MARKER) {
124            tidy_error_ext!(
125                err,
126                bad,
127                "{file}:{} found `{END_MARKER}` expecting `{START_MARKER}`",
128                idx + 1
129            )
130        }
131
132        if line.contains(START_MARKER) {
133            check_section(file, &mut lines, err, bad);
134        }
135    }
136}
137
138pub fn check(path: &Path, bad: &mut bool) {
139    let skip =
140        |path: &_, _is_dir| filter_dirs(path) || path.ends_with("tidy/src/alphabetical/tests.rs");
141
142    walk(path, skip, &mut |entry, contents| {
143        let file = &entry.path().display();
144        let lines = contents.lines().enumerate();
145        check_lines(file, lines, &mut crate::tidy_error, bad)
146    });
147}
148
149fn consume_numeric_prefix<I: Iterator<Item = char>>(it: &mut Peekable<I>) -> String {
150    let mut result = String::new();
151
152    while let Some(&c) = it.peek() {
153        if !c.is_numeric() {
154            break;
155        }
156
157        result.push(c);
158        it.next();
159    }
160
161    result
162}
163
164// A sorting function that is case-sensitive, and sorts sequences of digits by their numeric value,
165// so that `9` sorts before `12`.
166fn version_sort(a: &str, b: &str) -> Ordering {
167    let mut it1 = a.chars().peekable();
168    let mut it2 = b.chars().peekable();
169
170    while let (Some(x), Some(y)) = (it1.peek(), it2.peek()) {
171        match (x.is_numeric(), y.is_numeric()) {
172            (true, true) => {
173                let num1: String = consume_numeric_prefix(it1.by_ref());
174                let num2: String = consume_numeric_prefix(it2.by_ref());
175
176                let int1: u64 = num1.parse().unwrap();
177                let int2: u64 = num2.parse().unwrap();
178
179                // Compare strings when the numeric value is equal to handle "00" versus "0".
180                match int1.cmp(&int2).then_with(|| num1.cmp(&num2)) {
181                    Ordering::Equal => continue,
182                    different => return different,
183                }
184            }
185            (false, false) => match x.cmp(y) {
186                Ordering::Equal => {
187                    it1.next();
188                    it2.next();
189                    continue;
190                }
191                different => return different,
192            },
193            (false, true) | (true, false) => {
194                return x.cmp(y);
195            }
196        }
197    }
198
199    it1.next().cmp(&it2.next())
200}