1use std::cmp::Ordering;
23use std::fmt::Display;
24use std::iter::Peekable;
25use std::path::Path;
26
27use crate::diagnostics::{CheckId, DiagCtx, RunningCheck};
28use crate::walk::{filter_dirs, walk};
29
30#[cfg(test)]
31mod tests;
32
33fn indentation(line: &str) -> usize {
34 line.find(|c| c != ' ').unwrap_or(0)
35}
36
37fn is_close_bracket(c: char) -> bool {
38 matches!(c, ')' | ']' | '}')
39}
40
41const START_MARKER: &str = "tidy-alphabetical-start";
42const END_MARKER: &str = "tidy-alphabetical-end";
43
44fn check_section<'a>(
45 file: impl Display,
46 lines: impl Iterator<Item = (usize, &'a str)>,
47 check: &mut RunningCheck,
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 check.error(format!(
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 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(' ');
103
104 if version_sort(trimmed_line, prev_line_trimmed_lowercase).is_lt() {
105 check.error(format!("{file}:{}: line not in alphabetical order", idx + 1));
106 }
107
108 prev_line = line;
109 }
110
111 check.error(format!("{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 check: &mut RunningCheck,
118) {
119 while let Some((idx, line)) = lines.next() {
120 if line.contains(END_MARKER) {
121 check.error(format!(
122 "{file}:{} found `{END_MARKER}` expecting `{START_MARKER}`",
123 idx + 1
124 ));
125 }
126
127 if line.contains(START_MARKER) {
128 check_section(file, &mut lines, check);
129 }
130 }
131}
132
133pub fn check(path: &Path, diag_ctx: DiagCtx) {
134 let mut check = diag_ctx.start_check(CheckId::new("alphabetical").path(path));
135
136 let skip =
137 |path: &_, _is_dir| filter_dirs(path) || path.ends_with("tidy/src/alphabetical/tests.rs");
138
139 walk(path, skip, &mut |entry, contents| {
140 let file = &entry.path().display();
141 let lines = contents.lines().enumerate();
142 check_lines(file, lines, &mut check)
143 });
144}
145
146fn consume_numeric_prefix<I: Iterator<Item = char>>(it: &mut Peekable<I>) -> String {
147 let mut result = String::new();
148
149 while let Some(&c) = it.peek() {
150 if !c.is_numeric() {
151 break;
152 }
153
154 result.push(c);
155 it.next();
156 }
157
158 result
159}
160
161fn version_sort(a: &str, b: &str) -> Ordering {
164 let mut it1 = a.chars().peekable();
165 let mut it2 = b.chars().peekable();
166
167 while let (Some(x), Some(y)) = (it1.peek(), it2.peek()) {
168 match (x.is_numeric(), y.is_numeric()) {
169 (true, true) => {
170 let num1: String = consume_numeric_prefix(it1.by_ref());
171 let num2: String = consume_numeric_prefix(it2.by_ref());
172
173 let int1: u64 = num1.parse().unwrap();
174 let int2: u64 = num2.parse().unwrap();
175
176 match int1.cmp(&int2).then_with(|| num1.cmp(&num2)) {
178 Ordering::Equal => continue,
179 different => return different,
180 }
181 }
182 (false, false) => match x.cmp(y) {
183 Ordering::Equal => {
184 it1.next();
185 it2.next();
186 continue;
187 }
188 different => return different,
189 },
190 (false, true) | (true, false) => {
191 return x.cmp(y);
192 }
193 }
194 }
195
196 it1.next().cmp(&it2.next())
197}