1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
//! Checks that a list of items is in alphabetical order
//!
//! Use the following marker in the code:
//! ```rust
//! // tidy-alphabetical-start
//! fn aaa() {}
//! fn eee() {}
//! fn z() {}
//! // tidy-alphabetical-end
//! ```
//!
//! The following lines are ignored:
//! - Empty lines
//! - Lines that are indented with more or less spaces than the first line
//! - Lines starting with `//`, `#` (except those starting with `#!`), `)`, `]`, `}` if the comment
//!   has the same indentation as the first line
//! - Lines starting with a closing delimiter (`)`, `[`, `}`) are ignored.
//!
//! If a line ends with an opening delimiter, we effectively join the following line to it before
//! checking it. E.g. `foo(\nbar)` is treated like `foo(bar)`.

use std::fmt::Display;
use std::path::Path;

use crate::walk::{filter_dirs, walk};

#[cfg(test)]
mod tests;

fn indentation(line: &str) -> usize {
    line.find(|c| c != ' ').unwrap_or(0)
}

fn is_close_bracket(c: char) -> bool {
    matches!(c, ')' | ']' | '}')
}

const START_MARKER: &str = "tidy-alphabetical-start";
const END_MARKER: &str = "tidy-alphabetical-end";

fn check_section<'a>(
    file: impl Display,
    lines: impl Iterator<Item = (usize, &'a str)>,
    err: &mut dyn FnMut(&str) -> std::io::Result<()>,
    bad: &mut bool,
) {
    let mut prev_line = String::new();
    let mut first_indent = None;
    let mut in_split_line = None;

    for (idx, line) in lines {
        if line.is_empty() {
            continue;
        }

        if line.contains(START_MARKER) {
            tidy_error_ext!(
                err,
                bad,
                "{file}:{} found `{START_MARKER}` expecting `{END_MARKER}`",
                idx + 1
            );
            return;
        }

        if line.contains(END_MARKER) {
            return;
        }

        let indent = first_indent.unwrap_or_else(|| {
            let indent = indentation(line);
            first_indent = Some(indent);
            indent
        });

        let line = if let Some(prev_split_line) = in_split_line {
            // Join the split lines.
            in_split_line = None;
            format!("{prev_split_line}{}", line.trim_start())
        } else {
            line.to_string()
        };

        if indentation(&line) != indent {
            continue;
        }

        let trimmed_line = line.trim_start_matches(' ');

        if trimmed_line.starts_with("//")
            || (trimmed_line.starts_with('#') && !trimmed_line.starts_with("#!"))
            || trimmed_line.starts_with(is_close_bracket)
        {
            continue;
        }

        if line.trim_end().ends_with('(') {
            in_split_line = Some(line);
            continue;
        }

        let prev_line_trimmed_lowercase = prev_line.trim_start_matches(' ').to_lowercase();

        if trimmed_line.to_lowercase() < prev_line_trimmed_lowercase {
            tidy_error_ext!(err, bad, "{file}:{}: line not in alphabetical order", idx + 1);
        }

        prev_line = line;
    }

    tidy_error_ext!(err, bad, "{file}: reached end of file expecting `{END_MARKER}`")
}

fn check_lines<'a>(
    file: &impl Display,
    mut lines: impl Iterator<Item = (usize, &'a str)>,
    err: &mut dyn FnMut(&str) -> std::io::Result<()>,
    bad: &mut bool,
) {
    while let Some((idx, line)) = lines.next() {
        if line.contains(END_MARKER) {
            tidy_error_ext!(
                err,
                bad,
                "{file}:{} found `{END_MARKER}` expecting `{START_MARKER}`",
                idx + 1
            )
        }

        if line.contains(START_MARKER) {
            check_section(file, &mut lines, err, bad);
        }
    }
}

pub fn check(path: &Path, bad: &mut bool) {
    let skip =
        |path: &_, _is_dir| filter_dirs(path) || path.ends_with("tidy/src/alphabetical/tests.rs");

    walk(path, skip, &mut |entry, contents| {
        let file = &entry.path().display();
        let lines = contents.lines().enumerate();
        check_lines(file, lines, &mut crate::tidy_error, bad)
    });
}