sqrust_rules/layout/
single_space_after_comma.rs1use 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 let next = if i + 1 < len { Some(source[i + 1]) } else { None };
31
32 let bad = match next {
33 None => false,
35 Some(b'\n') | Some(b'\r') => false,
37 Some(b' ') => {
39 matches!(source.get(i + 2), Some(b' '))
41 }
42 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
61fn 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}