sqrust_rules/lint/
empty_in_list.rs1use sqrust_core::{Diagnostic, FileContext, Rule};
2
3pub struct EmptyInList;
4
5impl Rule for EmptyInList {
6 fn name(&self) -> &'static str {
7 "Lint/EmptyInList"
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 let skip = build_skip(bytes);
15
16 let upper: Vec<u8> = bytes.iter().map(|b| b.to_ascii_uppercase()).collect();
18
19 let mut diags = Vec::new();
20 let mut i = 0usize;
21
22 while i < len {
23 if skip[i] {
25 i += 1;
26 continue;
27 }
28
29 if let Some(after_in) = match_keyword_at(&upper, &skip, i, len, b"IN") {
32 let mut j = after_in;
34 while j < len && bytes[j].is_ascii_whitespace() && !skip[j] {
35 j += 1;
36 }
37
38 if j < len && bytes[j] == b'(' && !skip[j] {
40 let open_paren = j;
41 j += 1;
42
43 while j < len && bytes[j].is_ascii_whitespace() && !skip[j] {
45 j += 1;
46 }
47
48 if j < len && bytes[j] == b')' && !skip[j] {
50 let _ = open_paren; let (line, col) = offset_to_line_col(source, i);
53 diags.push(Diagnostic {
54 rule: self.name(),
55 message: "Empty IN list always evaluates to FALSE".to_string(),
56 line,
57 col,
58 });
59 i = j + 1;
61 continue;
62 }
63 }
64
65 i = after_in;
67 continue;
68 }
69
70 i += 1;
71 }
72
73 diags
74 }
75}
76
77fn match_keyword_at(
82 upper: &[u8],
83 skip: &[bool],
84 pos: usize,
85 len: usize,
86 kw: &[u8],
87) -> Option<usize> {
88 let kw_len = kw.len();
89 if pos + kw_len > len {
90 return None;
91 }
92 if skip[pos] {
94 return None;
95 }
96 if &upper[pos..pos + kw_len] != kw {
98 return None;
99 }
100 let before_ok = pos == 0 || {
102 let b = upper[pos - 1];
103 !b.is_ascii_alphanumeric() && b != b'_'
104 };
105 let after_pos = pos + kw_len;
107 let after_ok = after_pos >= len || {
108 let b = upper[after_pos];
109 !b.is_ascii_alphanumeric() && b != b'_'
110 };
111 if before_ok && after_ok {
112 Some(after_pos)
113 } else {
114 None
115 }
116}
117
118fn offset_to_line_col(source: &str, offset: usize) -> (usize, usize) {
120 let before = &source[..offset];
121 let line = before.chars().filter(|&c| c == '\n').count() + 1;
122 let col = before.rfind('\n').map(|p| offset - p - 1).unwrap_or(offset) + 1;
123 (line, col)
124}
125
126fn build_skip(bytes: &[u8]) -> Vec<bool> {
129 let len = bytes.len();
130 let mut skip = vec![false; len];
131 let mut i = 0usize;
132
133 while i < len {
134 if i + 1 < len && bytes[i] == b'-' && bytes[i + 1] == b'-' {
136 let start = i;
137 while i < len && bytes[i] != b'\n' {
138 i += 1;
139 }
140 for s in &mut skip[start..i] {
141 *s = true;
142 }
143 continue;
145 }
146
147 if i + 1 < len && bytes[i] == b'/' && bytes[i + 1] == b'*' {
149 let start = i;
150 i += 2;
151 while i + 1 < len && !(bytes[i] == b'*' && bytes[i + 1] == b'/') {
152 i += 1;
153 }
154 let end = if i + 1 < len { i + 2 } else { i + 1 };
155 for s in &mut skip[start..end.min(len)] {
156 *s = true;
157 }
158 i = end;
159 continue;
160 }
161
162 if bytes[i] == b'\'' {
164 let start = i;
165 i += 1;
166 while i < len {
167 if bytes[i] == b'\'' {
168 if i + 1 < len && bytes[i + 1] == b'\'' {
169 i += 2; } else {
171 i += 1; break;
173 }
174 } else {
175 i += 1;
176 }
177 }
178 for s in &mut skip[start..i.min(len)] {
179 *s = true;
180 }
181 continue;
182 }
183
184 if bytes[i] == b'"' {
186 let start = i;
187 i += 1;
188 while i < len && bytes[i] != b'"' {
189 i += 1;
190 }
191 let end = if i < len { i + 1 } else { i };
192 for s in &mut skip[start..end.min(len)] {
193 *s = true;
194 }
195 i = end;
196 continue;
197 }
198
199 if bytes[i] == b'`' {
201 let start = i;
202 i += 1;
203 while i < len && bytes[i] != b'`' {
204 i += 1;
205 }
206 let end = if i < len { i + 1 } else { i };
207 for s in &mut skip[start..end.min(len)] {
208 *s = true;
209 }
210 i = end;
211 continue;
212 }
213
214 i += 1;
215 }
216
217 skip
218}