Skip to main content

rigsql_rules/structure/
st01.rs

1use rigsql_core::SegmentType;
2
3use crate::rule::{CrawlType, Rule, RuleContext, RuleGroup};
4use crate::violation::{LintViolation, SourceEdit};
5
6/// ST01: Do not specify redundant ELSE NULL in a CASE expression.
7///
8/// CASE expressions without an ELSE clause implicitly return NULL,
9/// so `ELSE NULL` is redundant and should be removed.
10#[derive(Debug, Default)]
11pub struct RuleST01;
12
13impl Rule for RuleST01 {
14    fn code(&self) -> &'static str {
15        "ST01"
16    }
17    fn name(&self) -> &'static str {
18        "structure.else_null"
19    }
20    fn description(&self) -> &'static str {
21        "Do not specify redundant ELSE NULL in a CASE expression."
22    }
23    fn explanation(&self) -> &'static str {
24        "A CASE expression without an ELSE clause implicitly returns NULL. \
25         Writing ELSE NULL is therefore redundant and adds unnecessary noise \
26         to the query. Remove the ELSE NULL clause for clarity."
27    }
28    fn groups(&self) -> &[RuleGroup] {
29        &[RuleGroup::Structure]
30    }
31    fn is_fixable(&self) -> bool {
32        true
33    }
34
35    fn crawl_type(&self) -> CrawlType {
36        CrawlType::Segment(vec![SegmentType::CaseExpression])
37    }
38
39    fn eval(&self, ctx: &RuleContext) -> Vec<LintViolation> {
40        let children = ctx.segment.children();
41
42        for child in children {
43            if child.segment_type() == SegmentType::ElseClause {
44                // Check if the ElseClause's non-trivia content after ELSE keyword is just NullLiteral
45                let else_children = child.children();
46                let non_trivia: Vec<_> = else_children
47                    .iter()
48                    .filter(|s| !s.segment_type().is_trivia())
49                    .collect();
50
51                // Should be [Keyword("ELSE"), NullLiteral]
52                if non_trivia.len() == 2
53                    && non_trivia[0].segment_type() == SegmentType::Keyword
54                    && non_trivia[1].segment_type() == SegmentType::NullLiteral
55                {
56                    return vec![LintViolation::with_fix_and_msg_key(
57                        self.code(),
58                        "Redundant ELSE NULL in CASE expression.",
59                        child.span(),
60                        vec![SourceEdit::delete(child.span())],
61                        "rules.ST01.msg",
62                        vec![],
63                    )];
64                }
65            }
66        }
67
68        vec![]
69    }
70}
71
72#[cfg(test)]
73mod tests {
74    use super::*;
75    use crate::test_utils::lint_sql;
76
77    #[test]
78    fn test_st01_flags_else_null() {
79        let violations = lint_sql("SELECT CASE WHEN x = 1 THEN 'a' ELSE NULL END;", RuleST01);
80        assert_eq!(violations.len(), 1);
81        assert!(violations[0].message.contains("Redundant ELSE NULL"));
82    }
83
84    #[test]
85    fn test_st01_accepts_else_value() {
86        let violations = lint_sql("SELECT CASE WHEN x = 1 THEN 'a' ELSE 'b' END;", RuleST01);
87        assert_eq!(violations.len(), 0);
88    }
89
90    #[test]
91    fn test_st01_accepts_no_else() {
92        let violations = lint_sql("SELECT CASE WHEN x = 1 THEN 'a' END;", RuleST01);
93        assert_eq!(violations.len(), 0);
94    }
95}