Skip to main content

rigsql_rules/convention/
cv05.rs

1use rigsql_core::{Segment, SegmentType, TokenKind};
2
3use crate::rule::{CrawlType, Rule, RuleContext, RuleGroup};
4use crate::violation::LintViolation;
5
6/// CV05: Comparisons with NULL should use IS or IS NOT, not = or !=.
7///
8/// `WHERE col = NULL` is always false in SQL; use `WHERE col IS NULL` instead.
9#[derive(Debug, Default)]
10pub struct RuleCV05;
11
12impl Rule for RuleCV05 {
13    fn code(&self) -> &'static str {
14        "CV05"
15    }
16    fn name(&self) -> &'static str {
17        "convention.is_null"
18    }
19    fn description(&self) -> &'static str {
20        "Comparisons with NULL should use IS or IS NOT."
21    }
22    fn explanation(&self) -> &'static str {
23        "In SQL, NULL is not a value but the absence of one. Comparison operators \
24         (=, !=, <>) with NULL always return NULL (which is falsy). Use 'IS NULL' or \
25         'IS NOT NULL' instead of '= NULL' or '!= NULL'."
26    }
27    fn groups(&self) -> &[RuleGroup] {
28        &[RuleGroup::Convention]
29    }
30    fn is_fixable(&self) -> bool {
31        true
32    }
33
34    fn crawl_type(&self) -> CrawlType {
35        CrawlType::Segment(vec![SegmentType::BinaryExpression])
36    }
37
38    fn eval(&self, ctx: &RuleContext) -> Vec<LintViolation> {
39        let children = ctx.segment.children();
40
41        // Look for pattern: <expr> <comparison_op> NULL
42        // or: NULL <comparison_op> <expr>
43        let non_trivia: Vec<_> = children
44            .iter()
45            .filter(|c| !c.segment_type().is_trivia())
46            .collect();
47
48        if non_trivia.len() < 3 {
49            return vec![];
50        }
51
52        // Check if operator is comparison (=, !=, <>)
53        let op = non_trivia[1];
54        let is_comparison = matches!(op.segment_type(), SegmentType::ComparisonOperator);
55        if !is_comparison {
56            return vec![];
57        }
58
59        // Check if either side is NULL literal
60        let lhs_is_null = is_null_literal(non_trivia[0]);
61        let rhs_is_null = non_trivia.get(2).is_some_and(|s| is_null_literal(s));
62
63        if lhs_is_null || rhs_is_null {
64            return vec![LintViolation::new(
65                self.code(),
66                "Use IS NULL or IS NOT NULL instead of comparison operator with NULL.",
67                ctx.segment.span(),
68            )];
69        }
70
71        vec![]
72    }
73}
74
75fn is_null_literal(seg: &Segment) -> bool {
76    match seg {
77        Segment::Token(t) => {
78            t.segment_type == SegmentType::NullLiteral
79                || (t.token.kind == TokenKind::Word && t.token.text.eq_ignore_ascii_case("NULL"))
80        }
81        Segment::Node(n) => n.segment_type == SegmentType::NullLiteral,
82    }
83}