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(
57                        self.code(),
58                        "Redundant ELSE NULL in CASE expression.",
59                        child.span(),
60                        vec![SourceEdit::delete(child.span())],
61                    )];
62                }
63            }
64        }
65
66        vec![]
67    }
68}
69
70#[cfg(test)]
71mod tests {
72    use super::*;
73    use crate::test_utils::lint_sql;
74
75    #[test]
76    fn test_st01_flags_else_null() {
77        let violations = lint_sql("SELECT CASE WHEN x = 1 THEN 'a' ELSE NULL END;", RuleST01);
78        assert_eq!(violations.len(), 1);
79        assert!(violations[0].message.contains("Redundant ELSE NULL"));
80    }
81
82    #[test]
83    fn test_st01_accepts_else_value() {
84        let violations = lint_sql("SELECT CASE WHEN x = 1 THEN 'a' ELSE 'b' END;", RuleST01);
85        assert_eq!(violations.len(), 0);
86    }
87
88    #[test]
89    fn test_st01_accepts_no_else() {
90        let violations = lint_sql("SELECT CASE WHEN x = 1 THEN 'a' END;", RuleST01);
91        assert_eq!(violations.len(), 0);
92    }
93}