1use std::cmp::Ordering;
35use std::fs;
36use std::io::{Seek, Write};
37use std::iter::Peekable;
38use std::ops::{Range, RangeBounds};
39use std::path::Path;
40
41use crate::diagnostics::{CheckId, RunningCheck, TidyCtx};
42use crate::walk::{filter_dirs, walk};
43
44#[cfg(test)]
45mod tests;
46
47fn indentation(line: &str) -> usize {
48 line.find(|c| c != ' ').unwrap_or(0)
49}
50
51fn is_close_bracket(c: char) -> bool {
52 matches!(c, ')' | ']' | '}')
53}
54
55fn is_empty_or_comment(line: &&str) -> bool {
56 let trimmed_line = line.trim_start_matches(' ').trim_end_matches('\n');
57
58 trimmed_line.is_empty()
59 || trimmed_line.starts_with("//")
60 || (trimmed_line.starts_with('#') && !trimmed_line.starts_with("#!"))
61}
62
63const START_MARKER: &str = "tidy-alphabetical-start";
64const END_MARKER: &str = "tidy-alphabetical-end";
65
66fn sort_section(section: &str) -> String {
69 struct Item {
71 full: String,
73 trimmed: String,
75 }
76
77 let mut items = Vec::new();
78 let mut lines = section.split_inclusive('\n').peekable();
79
80 let end_comments = loop {
81 let mut full = String::new();
82 let mut trimmed = String::new();
83
84 while let Some(comment) = lines.next_if(is_empty_or_comment) {
85 full.push_str(comment);
86 }
87
88 let Some(line) = lines.next() else {
89 break full;
91 };
92
93 let mut push = |line| {
94 full.push_str(line);
95 trimmed.push_str(line.trim_start_matches(' ').trim_end_matches('\n'))
96 };
97
98 push(line);
99
100 let indent = indentation(line);
101 let mut multiline = false;
102
103 while let Some(more_indented) =
105 lines.next_if(|&line: &&_| indent < indentation(line) || line == "\n")
106 {
107 multiline = true;
108 push(more_indented);
109 }
110
111 if multiline
112 && let Some(indented) =
113 lines.next_if(|l| {
122 indentation(l) == indent
123 && l.trim_start_matches(' ').starts_with(is_close_bracket)
124 })
125 {
126 push(indented);
127 }
128
129 items.push(Item { full, trimmed });
130 };
131
132 items.sort_by(|a, b| version_sort(&a.trimmed, &b.trimmed));
133 items.into_iter().map(|l| l.full).chain([end_comments]).collect()
134}
135
136fn check_lines<'a>(path: &Path, content: &'a str, tidy_ctx: &TidyCtx, check: &mut RunningCheck) {
137 let mut offset = 0;
138
139 loop {
140 let rest = &content[offset..];
141 let start = rest.find(START_MARKER);
142 let end = rest.find(END_MARKER);
143
144 match (start, end) {
145 (Some(start), Some(end)) if end < start => {
149 check.error(format!(
150 "{path}:{line_number} found `{END_MARKER}` expecting `{START_MARKER}`",
151 path = path.display(),
152 line_number = content[..offset + end].lines().count(),
153 ));
154 break;
155 }
156
157 (None, Some(end)) => {
159 check.error(format!(
160 "{path}:{line_number} found `{END_MARKER}` expecting `{START_MARKER}`",
161 path = path.display(),
162 line_number = content[..offset + end].lines().count(),
163 ));
164 break;
165 }
166
167 (Some(start), None) => {
169 check.error(format!(
170 "{path}:{line_number} `{START_MARKER}` without a matching `{END_MARKER}`",
171 path = path.display(),
172 line_number = content[..offset + start].lines().count(),
173 ));
174 break;
175 }
176
177 (Some(start), Some(end))
179 if rest[start + START_MARKER.len()..end].contains(START_MARKER) =>
180 {
181 check.error(format!(
182 "{path}:{line_number} found `{START_MARKER}` expecting `{END_MARKER}`",
183 path = path.display(),
184 line_number = content[..offset
185 + sub_find(rest, start + START_MARKER.len()..end, START_MARKER)
186 .unwrap()
187 .start]
188 .lines()
189 .count()
190 ));
191 break;
192 }
193
194 (Some(start), Some(end)) => {
196 assert!(start <= end);
197
198 let start_nl_end = sub_find(rest, start + START_MARKER.len().., "\n").unwrap().end;
203
204 let end_nl_start = rest[..end].rfind('\n').unwrap();
206
207 let end_nl_end = sub_find(rest, end + END_MARKER.len().., "\n")
209 .map(|r| r.end)
210 .unwrap_or(content.len() - offset);
211
212 if end_nl_start < start_nl_end {
215 offset += end_nl_end;
216 continue;
217 }
218
219 let section = &rest[start_nl_end..=end_nl_start];
220 let sorted = sort_section(section);
221
222 if sorted != section {
224 if !tidy_ctx.is_bless_enabled() {
225 let pre = &content[..offset + start_nl_end];
226 assert_eq!(pre.chars().rev().next(), Some('\n'));
227 let base_line_number = pre.lines().count() + 1;
230 let line_offset = sorted
231 .lines()
232 .zip(section.lines())
233 .enumerate()
234 .find(|(_, (a, b))| a != b)
235 .unwrap()
236 .0;
237 let line_number = base_line_number + line_offset;
238
239 check.error(format!(
240 "{path}:{line_number}: line not in alphabetical order (tip: use --bless to sort this list)",
241 path = path.display(),
242 ));
243 } else {
244 let mut tempfile =
246 tempfile::Builder::new().tempfile_in(path.parent().unwrap()).unwrap();
247
248 fs::copy(path, tempfile.path()).unwrap();
249
250 tempfile
251 .as_file_mut()
252 .seek(std::io::SeekFrom::Start((offset + start_nl_end) as u64))
253 .unwrap();
254 tempfile.as_file_mut().write_all(sorted.as_bytes()).unwrap();
255
256 tempfile.persist(path).unwrap();
257 }
258 }
259
260 offset += end_nl_end;
262 }
263
264 (None, None) => break,
266 }
267 }
268}
269
270pub fn check(path: &Path, tidy_ctx: TidyCtx) {
271 let mut check = tidy_ctx.start_check(CheckId::new("alphabetical").path(path));
272
273 let skip = |path: &_, _is_dir| {
274 filter_dirs(path)
275 || path.ends_with("tidy/src/alphabetical.rs")
276 || path.ends_with("tidy/src/alphabetical/tests.rs")
277 };
278
279 walk(path, skip, &mut |entry, content| {
280 check_lines(entry.path(), content, &tidy_ctx, &mut check)
281 });
282}
283
284fn consume_numeric_prefix<I: Iterator<Item = char>>(it: &mut Peekable<I>) -> String {
285 let mut result = String::new();
286
287 while let Some(&c) = it.peek() {
288 if !c.is_numeric() {
289 break;
290 }
291
292 result.push(c);
293 it.next();
294 }
295
296 result
297}
298
299fn version_sort(a: &str, b: &str) -> Ordering {
302 let mut it1 = a.chars().peekable();
303 let mut it2 = b.chars().peekable();
304
305 while let (Some(x), Some(y)) = (it1.peek(), it2.peek()) {
306 match (x.is_numeric(), y.is_numeric()) {
307 (true, true) => {
308 let num1: String = consume_numeric_prefix(it1.by_ref());
309 let num2: String = consume_numeric_prefix(it2.by_ref());
310
311 let int1: u64 = num1.parse().unwrap();
312 let int2: u64 = num2.parse().unwrap();
313
314 match int1.cmp(&int2).then_with(|| num1.cmp(&num2)) {
316 Ordering::Equal => continue,
317 different => return different,
318 }
319 }
320 (false, false) => match x.cmp(y) {
321 Ordering::Equal => {
322 it1.next();
323 it2.next();
324 continue;
325 }
326 different => return different,
327 },
328 (false, true) | (true, false) => {
329 return x.cmp(y);
330 }
331 }
332 }
333
334 it1.next().cmp(&it2.next())
335}
336
337fn sub_find(s: &str, range: impl RangeBounds<usize>, pat: &str) -> Option<Range<usize>> {
339 s[(range.start_bound().cloned(), range.end_bound().cloned())]
340 .find(pat)
341 .map(|pos| {
342 pos + match range.start_bound().cloned() {
343 std::ops::Bound::Included(x) => x,
344 std::ops::Bound::Excluded(x) => x + 1,
345 std::ops::Bound::Unbounded => 0,
346 }
347 })
348 .map(|pos| pos..pos + pat.len())
349}