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 let section = &rest[start_nl_end..=end_nl_start];
213 let sorted = sort_section(section);
214
215 if sorted != section {
217 if !tidy_ctx.is_bless_enabled() {
218 let pre = &content[..offset + start_nl_end];
219 assert_eq!(pre.chars().rev().next(), Some('\n'));
220 let base_line_number = pre.lines().count() + 1;
223 let line_offset = sorted
224 .lines()
225 .zip(section.lines())
226 .enumerate()
227 .find(|(_, (a, b))| a != b)
228 .unwrap()
229 .0;
230 let line_number = base_line_number + line_offset;
231
232 check.error(format!(
233 "{path}:{line_number}: line not in alphabetical order (tip: use --bless to sort this list)",
234 path = path.display(),
235 ));
236 } else {
237 let mut tempfile =
239 tempfile::Builder::new().tempfile_in(path.parent().unwrap()).unwrap();
240
241 fs::copy(path, tempfile.path()).unwrap();
242
243 tempfile
244 .as_file_mut()
245 .seek(std::io::SeekFrom::Start((offset + start_nl_end) as u64))
246 .unwrap();
247 tempfile.as_file_mut().write_all(sorted.as_bytes()).unwrap();
248
249 tempfile.persist(path).unwrap();
250 }
251 }
252
253 offset += end_nl_end;
255 }
256
257 (None, None) => break,
259 }
260 }
261}
262
263pub fn check(path: &Path, tidy_ctx: TidyCtx) {
264 let mut check = tidy_ctx.start_check(CheckId::new("alphabetical").path(path));
265
266 let skip = |path: &_, _is_dir| {
267 filter_dirs(path)
268 || path.ends_with("tidy/src/alphabetical.rs")
269 || path.ends_with("tidy/src/alphabetical/tests.rs")
270 };
271
272 walk(path, skip, &mut |entry, content| {
273 check_lines(entry.path(), content, &tidy_ctx, &mut check)
274 });
275}
276
277fn consume_numeric_prefix<I: Iterator<Item = char>>(it: &mut Peekable<I>) -> String {
278 let mut result = String::new();
279
280 while let Some(&c) = it.peek() {
281 if !c.is_numeric() {
282 break;
283 }
284
285 result.push(c);
286 it.next();
287 }
288
289 result
290}
291
292fn version_sort(a: &str, b: &str) -> Ordering {
295 let mut it1 = a.chars().peekable();
296 let mut it2 = b.chars().peekable();
297
298 while let (Some(x), Some(y)) = (it1.peek(), it2.peek()) {
299 match (x.is_numeric(), y.is_numeric()) {
300 (true, true) => {
301 let num1: String = consume_numeric_prefix(it1.by_ref());
302 let num2: String = consume_numeric_prefix(it2.by_ref());
303
304 let int1: u64 = num1.parse().unwrap();
305 let int2: u64 = num2.parse().unwrap();
306
307 match int1.cmp(&int2).then_with(|| num1.cmp(&num2)) {
309 Ordering::Equal => continue,
310 different => return different,
311 }
312 }
313 (false, false) => match x.cmp(y) {
314 Ordering::Equal => {
315 it1.next();
316 it2.next();
317 continue;
318 }
319 different => return different,
320 },
321 (false, true) | (true, false) => {
322 return x.cmp(y);
323 }
324 }
325 }
326
327 it1.next().cmp(&it2.next())
328}
329
330fn sub_find(s: &str, range: impl RangeBounds<usize>, pat: &str) -> Option<Range<usize>> {
332 s[(range.start_bound().cloned(), range.end_bound().cloned())]
333 .find(pat)
334 .map(|pos| {
335 pos + match range.start_bound().cloned() {
336 std::ops::Bound::Included(x) => x,
337 std::ops::Bound::Excluded(x) => x + 1,
338 std::ops::Bound::Unbounded => 0,
339 }
340 })
341 .map(|pos| pos..pos + pat.len())
342}