Skip to main content

sqrust_rules/layout/
leading_operator.rs

1use sqrust_core::{Diagnostic, FileContext, Rule};
2
3pub struct LeadingOperator;
4
5impl Rule for LeadingOperator {
6    fn name(&self) -> &'static str {
7        "Layout/LeadingOperator"
8    }
9
10    fn check(&self, ctx: &FileContext) -> Vec<Diagnostic> {
11        let mut diags = Vec::new();
12
13        // Text-based scan: works even when ctx.parse_errors is non-empty.
14        // Split on '\n' so we get accurate line indices.
15        let lines: Vec<&str> = ctx.source.split('\n').collect();
16
17        for (idx, line) in lines.iter().enumerate() {
18            let trimmed = line.trim_start();
19            let upper = trimmed.to_uppercase();
20
21            // 1-indexed column of the operator keyword in the original line
22            let leading_spaces = line.len() - trimmed.len();
23            let col = leading_spaces + 1;
24
25            // Check for AND at line start.
26            // Word boundary: AND must be followed by whitespace or be the whole token
27            // (i.e. end of line). No SQL keyword starts with "AND" that would cause
28            // a false positive, so no additional guard needed.
29            let is_and = upper == "AND"
30                || upper.starts_with("AND ")
31                || upper.starts_with("AND\t")
32                || upper.starts_with("AND\r");
33
34            if is_and {
35                diags.push(Diagnostic {
36                    rule: self.name(),
37                    message:
38                        "AND at start of line; place operators at the end of the previous line"
39                            .to_string(),
40                    line: idx + 1,
41                    col,
42                });
43                // A line can't be both AND and OR at the same time, so skip OR check.
44                continue;
45            }
46
47            // Check for OR at line start (word boundary), but NOT ORDER BY.
48            // "ORDER" starts with "OR" so we guard against it.
49            let is_or_token = upper == "OR"
50                || upper.starts_with("OR ")
51                || upper.starts_with("OR\t")
52                || upper.starts_with("OR\r");
53
54            if is_or_token && !upper.starts_with("ORDER") {
55                diags.push(Diagnostic {
56                    rule: self.name(),
57                    message:
58                        "OR at start of line; place operators at the end of the previous line"
59                            .to_string(),
60                    line: idx + 1,
61                    col,
62                });
63            }
64        }
65
66        diags
67    }
68}