Skip to main content

sqrust_rules/layout/
arithmetic_operator_padding.rs

1use sqrust_core::{Diagnostic, FileContext, Rule};
2use crate::capitalisation::SkipMap;
3
4pub struct ArithmeticOperatorPadding;
5
6impl Rule for ArithmeticOperatorPadding {
7    fn name(&self) -> &'static str {
8        "Layout/ArithmeticOperatorPadding"
9    }
10
11    fn check(&self, ctx: &FileContext) -> Vec<Diagnostic> {
12        let source = &ctx.source;
13        let bytes = source.as_bytes();
14        let len = bytes.len();
15        let skip = SkipMap::build(source);
16
17        let mut diags = Vec::new();
18        let mut i = 0;
19
20        while i < len {
21            if !skip.is_code(i) {
22                i += 1;
23                continue;
24            }
25
26            let op = bytes[i];
27            if op == b'+' || op == b'-' || op == b'*' || op == b'/' || op == b'%' {
28                // SkipMap already marks comment bytes as non-code; the outer guard
29                // `if !skip.is_code(i)` ensures we never reach this point for `--`
30                // or `/* */` comment bytes. No manual comment skip needed here.
31
32                // * inside parentheses: SELECT *, COUNT(*) etc.
33                if op == b'*' {
34                    let prev_nws = prev_non_whitespace(bytes, i);
35                    let next_nws = next_non_whitespace(bytes, i, len);
36                    if prev_nws == Some(b'(') || next_nws == Some(b')') {
37                        i += 1;
38                        continue;
39                    }
40                }
41                // Unary +/- after (, =, >, <, !, ,
42                if op == b'+' || op == b'-' {
43                    let prev = prev_non_whitespace(bytes, i);
44                    match prev {
45                        None | Some(b'(') | Some(b'=') | Some(b'>') | Some(b'<') | Some(b'!') | Some(b',') => {
46                            i += 1;
47                            continue;
48                        }
49                        _ => {}
50                    }
51                }
52
53                // Check padding: need space before AND after
54                let space_before = i == 0 || is_space(bytes[i - 1]);
55                let space_after = i + 1 >= len || is_space(bytes[i + 1]);
56
57                if !space_before || !space_after {
58                    let (line, col) = offset_to_line_col(source, i);
59                    diags.push(Diagnostic {
60                        rule: self.name(),
61                        message: format!(
62                            "Arithmetic operator '{}' must be padded with spaces on both sides",
63                            bytes[i] as char
64                        ),
65                        line,
66                        col,
67                    });
68                }
69            }
70
71            i += 1;
72        }
73
74        diags
75    }
76}
77
78fn is_space(b: u8) -> bool {
79    b == b' ' || b == b'\t' || b == b'\n' || b == b'\r'
80}
81
82fn prev_non_whitespace(bytes: &[u8], pos: usize) -> Option<u8> {
83    if pos == 0 { return None; }
84    let mut j = pos - 1;
85    loop {
86        if !is_space(bytes[j]) {
87            return Some(bytes[j]);
88        }
89        if j == 0 { return None; }
90        j -= 1;
91    }
92}
93
94fn next_non_whitespace(bytes: &[u8], pos: usize, len: usize) -> Option<u8> {
95    if pos + 1 >= len { return None; }
96    let mut j = pos + 1;
97    while j < len {
98        if !is_space(bytes[j]) {
99            return Some(bytes[j]);
100        }
101        j += 1;
102    }
103    None
104}
105
106fn offset_to_line_col(source: &str, offset: usize) -> (usize, usize) {
107    let before = &source[..offset.min(source.len())];
108    let line = before.chars().filter(|&c| c == '\n').count() + 1;
109    let col = before.rfind('\n').map(|p| offset - p - 1).unwrap_or(offset) + 1;
110    (line, col)
111}