sqrust_rules/ambiguous/
implicit_boolean_comparison.rs1use sqrust_core::{Diagnostic, FileContext, Rule};
2
3pub struct ImplicitBooleanComparison;
4
5impl Rule for ImplicitBooleanComparison {
6 fn name(&self) -> &'static str {
7 "Ambiguous/ImplicitBooleanComparison"
8 }
9
10 fn check(&self, ctx: &FileContext) -> Vec<Diagnostic> {
11 let source = &ctx.source;
12 let bytes = source.as_bytes();
13 let len = bytes.len();
14
15 if len == 0 {
16 return Vec::new();
17 }
18
19 let skip = build_skip_set(bytes, len);
20 let mut diags = Vec::new();
21 let mut line: usize = 1;
22 let mut line_start: usize = 0;
23 let mut i = 0;
24
25 while i < len {
26 if bytes[i] == b'\n' {
27 line += 1;
28 line_start = i + 1;
29 i += 1;
30 continue;
31 }
32
33 if skip[i] {
34 i += 1;
35 continue;
36 }
37
38 let op_len = if bytes[i] == b'='
40 && (i == 0
41 || (bytes[i - 1] != b'!'
42 && bytes[i - 1] != b'<'
43 && bytes[i - 1] != b'>'))
44 {
45 1
46 } else if i + 1 < len && bytes[i] == b'!' && bytes[i + 1] == b'=' {
47 2
48 } else if i + 1 < len && bytes[i] == b'<' && bytes[i + 1] == b'>' {
49 2
50 } else {
51 i += 1;
52 continue;
53 };
54
55 let op_start = i;
56 let op_col = op_start - line_start + 1;
57 i += op_len;
58
59 while i < len
61 && (bytes[i] == b' '
62 || bytes[i] == b'\t'
63 || bytes[i] == b'\n'
64 || bytes[i] == b'\r')
65 {
66 if bytes[i] == b'\n' {
67 line += 1;
68 line_start = i + 1;
69 }
70 i += 1;
71 }
72
73 if i >= len || skip[i] {
74 continue;
75 }
76
77 if let Some(end) = match_bool_keyword(bytes, &skip, i) {
79 let col = op_col;
80 diags.push(Diagnostic {
81 rule: self.name(),
82 message: "Comparing to TRUE/FALSE is redundant and dialect-specific; use the expression directly or add IS TRUE / IS FALSE".to_string(),
83 line,
84 col,
85 });
86 i = end;
87 }
88 }
90
91 diags
92 }
93}
94
95fn match_bool_keyword(bytes: &[u8], skip: &[bool], i: usize) -> Option<usize> {
98 let len = bytes.len();
99
100 if i >= len || skip[i] {
101 return None;
102 }
103
104 for keyword in [b"TRUE".as_slice(), b"FALSE".as_slice()] {
105 let klen = keyword.len();
106 if i + klen > len {
107 continue;
108 }
109 let matches = (0..klen).all(|k| {
110 !skip[i + k] && bytes[i + k].eq_ignore_ascii_case(&keyword[k])
111 });
112 if matches {
113 let end = i + klen;
114 if end < len && is_word_char(bytes[end]) {
116 continue;
117 }
118 return Some(end);
119 }
120 }
121
122 None
123}
124
125#[inline]
126fn is_word_char(ch: u8) -> bool {
127 ch.is_ascii_alphanumeric() || ch == b'_'
128}
129
130fn build_skip_set(bytes: &[u8], len: usize) -> Vec<bool> {
133 let mut skip = vec![false; len];
134 let mut i = 0;
135
136 while i < len {
137 if bytes[i] == b'\'' {
139 skip[i] = true;
140 i += 1;
141 while i < len {
142 skip[i] = true;
143 if bytes[i] == b'\'' {
144 if i + 1 < len && bytes[i + 1] == b'\'' {
145 i += 1;
146 skip[i] = true;
147 i += 1;
148 continue;
149 }
150 i += 1;
151 break;
152 }
153 i += 1;
154 }
155 continue;
156 }
157
158 if bytes[i] == b'"' {
160 skip[i] = true;
161 i += 1;
162 while i < len {
163 skip[i] = true;
164 if bytes[i] == b'"' {
165 if i + 1 < len && bytes[i + 1] == b'"' {
166 i += 1;
167 skip[i] = true;
168 i += 1;
169 continue;
170 }
171 i += 1;
172 break;
173 }
174 i += 1;
175 }
176 continue;
177 }
178
179 if i + 1 < len && bytes[i] == b'/' && bytes[i + 1] == b'*' {
181 skip[i] = true;
182 skip[i + 1] = true;
183 i += 2;
184 while i < len {
185 skip[i] = true;
186 if i + 1 < len && bytes[i] == b'*' && bytes[i + 1] == b'/' {
187 skip[i + 1] = true;
188 i += 2;
189 break;
190 }
191 i += 1;
192 }
193 continue;
194 }
195
196 if i + 1 < len && bytes[i] == b'-' && bytes[i + 1] == b'-' {
198 skip[i] = true;
199 skip[i + 1] = true;
200 i += 2;
201 while i < len && bytes[i] != b'\n' {
202 skip[i] = true;
203 i += 1;
204 }
205 continue;
206 }
207
208 i += 1;
209 }
210
211 skip
212}