1use 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 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
164fn 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 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}