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}