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