Skip to main content

sqrust_rules/ambiguous/
implicit_boolean_comparison.rs

1use 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            // Try to match a comparison operator: =, !=, <>
39            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            // Skip whitespace after operator
60            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            // Try to match TRUE or FALSE (case-insensitive), with word boundary after
78            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            // else: continue from current i (no match, already advanced past operator)
89        }
90
91        diags
92    }
93}
94
95/// Match "TRUE" or "FALSE" at position i, case-insensitive, with word-boundary check.
96/// Returns offset just past the match, or None.
97fn 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            // Word boundary: next char must not be alphanumeric or underscore
115            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
130/// Build a boolean skip-set: `skip[i] == true` means byte `i` is inside a
131/// single-quoted string, double-quoted identifier, block comment, or line comment.
132fn 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        // Single-quoted string: '...' with '' escape.
138        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        // Double-quoted identifier: "..." with "" escape.
159        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        // Block comment: /* ... */
180        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        // Line comment: -- to end of line.
197        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}