sqrust_rules/layout/
arithmetic_operator_padding.rs1use 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 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 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 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}