sqrust_rules/layout/
comparison_operator_spacing.rs1use sqrust_core::{Diagnostic, FileContext, Rule};
2
3pub struct ComparisonOperatorSpacing;
4
5impl Rule for ComparisonOperatorSpacing {
6 fn name(&self) -> &'static str {
7 "Layout/ComparisonOperatorSpacing"
8 }
9
10 fn check(&self, ctx: &FileContext) -> Vec<Diagnostic> {
11 find_violations(&ctx.source, self.name())
12 }
13}
14
15fn find_violations(source: &str, rule_name: &'static str) -> Vec<Diagnostic> {
16 let bytes = source.as_bytes();
17 let len = bytes.len();
18 let mut diags = Vec::new();
19
20 let mut i = 0;
21 let mut in_string = false;
22 let mut in_line_comment = false;
23 let mut block_depth: usize = 0;
24
25 while i < len {
26 if bytes[i] == b'\n' {
28 in_line_comment = false;
29 i += 1;
30 continue;
31 }
32
33 if in_line_comment {
35 i += 1;
36 continue;
37 }
38
39 if !in_string && block_depth == 0 && bytes[i] == b'\'' {
41 in_string = true;
42 i += 1;
43 continue;
44 }
45 if in_string {
46 if bytes[i] == b'\'' {
47 if i + 1 < len && bytes[i + 1] == b'\'' {
49 i += 2;
50 continue;
51 }
52 in_string = false;
53 }
54 i += 1;
55 continue;
56 }
57
58 if block_depth == 0 && i + 1 < len && bytes[i] == b'/' && bytes[i + 1] == b'*' {
60 block_depth += 1;
61 i += 2;
62 continue;
63 }
64 if block_depth > 0 {
66 if i + 1 < len && bytes[i] == b'*' && bytes[i + 1] == b'/' {
67 block_depth -= 1;
68 i += 2;
69 } else {
70 i += 1;
71 }
72 continue;
73 }
74
75 if i + 1 < len && bytes[i] == b'-' && bytes[i + 1] == b'-' {
77 in_line_comment = true;
78 i += 2;
79 continue;
80 }
81
82 if bytes[i] == b'!' && i + 1 < len && bytes[i + 1] == b'=' {
89 let op = "!=";
90 if !well_spaced(bytes, i, 2) {
91 let (line, col) = byte_offset_to_line_col(source, i);
92 diags.push(make_diag(rule_name, op, line, col));
93 }
94 i += 2;
95 continue;
96 }
97
98 if bytes[i] == b'<' {
100 if i + 1 < len && bytes[i + 1] == b'<' {
102 i += 2;
103 continue;
104 }
105 if i + 1 < len && (bytes[i + 1] == b'>' || bytes[i + 1] == b'=') {
108 let op = if bytes[i + 1] == b'>' { "<>" } else { "<=" };
109 if !well_spaced(bytes, i, 2) {
110 let (line, col) = byte_offset_to_line_col(source, i);
111 diags.push(make_diag(rule_name, op, line, col));
112 }
113 i += 2;
114 continue;
115 }
116 if i + 1 < len && bytes[i + 1] == b'!' {
118 i += 1;
119 continue;
120 }
121 if i + 1 < len && bytes[i + 1] == b'/' {
123 i += 1;
124 continue;
125 }
126 if !well_spaced(bytes, i, 1) {
128 let (line, col) = byte_offset_to_line_col(source, i);
129 diags.push(make_diag(rule_name, "<", line, col));
130 }
131 i += 1;
132 continue;
133 }
134
135 if bytes[i] == b'>' {
136 if i + 1 < len && bytes[i + 1] == b'>' {
138 i += 2;
139 continue;
140 }
141 if i + 1 < len && bytes[i + 1] == b'=' {
143 let op = ">=";
144 if !well_spaced(bytes, i, 2) {
145 let (line, col) = byte_offset_to_line_col(source, i);
146 diags.push(make_diag(rule_name, op, line, col));
147 }
148 i += 2;
149 continue;
150 }
151 let prev = if i > 0 { bytes[i - 1] } else { b' ' };
156 if prev == b'-' || prev == b'=' {
157 i += 1;
159 continue;
160 }
161 if !well_spaced(bytes, i, 1) {
162 let (line, col) = byte_offset_to_line_col(source, i);
163 diags.push(make_diag(rule_name, ">", line, col));
164 }
165 i += 1;
166 continue;
167 }
168
169 i += 1;
170 }
171
172 diags
173}
174
175fn well_spaced(bytes: &[u8], pos: usize, op_len: usize) -> bool {
178 let before_ok = if pos == 0 {
179 true
180 } else {
181 let b = bytes[pos - 1];
182 b == b' ' || b == b'\t'
183 };
184
185 let after_pos = pos + op_len;
186 let after_ok = if after_pos >= bytes.len() {
187 true
188 } else {
189 let b = bytes[after_pos];
190 b == b' ' || b == b'\t' || b == b'\n' || b == b'\r'
191 };
192
193 before_ok && after_ok
194}
195
196fn make_diag(rule_name: &'static str, op: &str, line: usize, col: usize) -> Diagnostic {
197 Diagnostic {
198 rule: rule_name,
199 message: format!("Missing spaces around comparison operator '{op}'"),
200 line,
201 col,
202 }
203}
204
205fn byte_offset_to_line_col(source: &str, offset: usize) -> (usize, usize) {
207 let mut line = 1usize;
208 let mut line_start = 0usize;
209 for (i, ch) in source.char_indices() {
210 if i == offset {
211 break;
212 }
213 if ch == '\n' {
214 line += 1;
215 line_start = i + 1;
216 }
217 }
218 let col = offset - line_start + 1;
219 (line, col)
220}