Skip to main content

sqrust_rules/layout/
single_space_after_comma.rs

1use sqrust_core::{Diagnostic, FileContext, Rule};
2use crate::capitalisation::SkipMap;
3
4pub struct SingleSpaceAfterComma;
5
6impl Rule for SingleSpaceAfterComma {
7    fn name(&self) -> &'static str {
8        "Layout/SingleSpaceAfterComma"
9    }
10
11    fn check(&self, ctx: &FileContext) -> Vec<Diagnostic> {
12        let source = ctx.source.as_bytes();
13        let len = source.len();
14        if len == 0 {
15            return Vec::new();
16        }
17
18        let skip_map = SkipMap::build(&ctx.source);
19        let mut diags = Vec::new();
20
21        for i in 0..len {
22            if source[i] != b',' {
23                continue;
24            }
25            if !skip_map.is_code(i) {
26                continue;
27            }
28
29            // Determine what follows the comma
30            let next = if i + 1 < len { Some(source[i + 1]) } else { None };
31
32            let bad = match next {
33                // End of file with no following character — not a violation
34                None => false,
35                // Trailing comma at end of line — OK
36                Some(b'\n') | Some(b'\r') => false,
37                // Exactly one space — only bad if the character after that space is also a space
38                Some(b' ') => {
39                    // Check for double space (extra space)
40                    matches!(source.get(i + 2), Some(b' '))
41                }
42                // Any other character (non-space, non-newline) — missing space
43                Some(_) => true,
44            };
45
46            if bad {
47                let (line, col) = byte_offset_to_line_col(&ctx.source, i);
48                diags.push(Diagnostic {
49                    rule: self.name(),
50                    message: "Expected single space after comma".to_string(),
51                    line,
52                    col,
53                });
54            }
55        }
56
57        diags
58    }
59}
60
61/// Converts a byte offset into a 1-indexed (line, col) pair.
62/// Col counts bytes (ASCII SQL is byte == char for identifiers).
63fn byte_offset_to_line_col(source: &str, offset: usize) -> (usize, usize) {
64    let mut line = 1usize;
65    let mut line_start = 0usize;
66    for (i, ch) in source.char_indices() {
67        if i == offset {
68            break;
69        }
70        if ch == '\n' {
71            line += 1;
72            line_start = i + 1;
73        }
74    }
75    let col = offset - line_start + 1;
76    (line, col)
77}