sqruff_lib/rules/structure/
st01.rs

1use ahash::AHashMap;
2use sqruff_lib_core::dialects::syntax::{SyntaxKind, SyntaxSet};
3use sqruff_lib_core::lint_fix::LintFix;
4use sqruff_lib_core::parser::segments::base::ErasedSegment;
5
6use crate::core::config::Value;
7use crate::core::rules::base::{Erased, ErasedRule, LintResult, Rule, RuleGroups};
8use crate::core::rules::context::RuleContext;
9use crate::core::rules::crawlers::{Crawler, SegmentSeekerCrawler};
10use crate::utils::functional::context::FunctionalContext;
11
12#[derive(Default, Debug, Clone)]
13pub struct RuleST01;
14
15impl Rule for RuleST01 {
16    fn load_from_config(&self, _config: &AHashMap<String, Value>) -> Result<ErasedRule, String> {
17        Ok(RuleST01.erased())
18    }
19
20    fn name(&self) -> &'static str {
21        "structure.else_null"
22    }
23
24    fn description(&self) -> &'static str {
25        "Do not specify 'else null' in a case when statement (redundant)."
26    }
27
28    fn long_description(&self) -> &'static str {
29        r#"
30**Anti-pattern**
31
32```sql
33select
34    case
35        when name like '%cat%' then 'meow'
36        when name like '%dog%' then 'woof'
37        else null
38    end
39from x
40```
41
42**Best practice**
43
44Omit `else null`
45
46```sql
47select
48    case
49        when name like '%cat%' then 'meow'
50        when name like '%dog%' then 'woof'
51    end
52from x
53```
54"#
55    }
56
57    fn groups(&self) -> &'static [RuleGroups] {
58        &[RuleGroups::All, RuleGroups::Structure]
59    }
60
61    fn eval(&self, context: &RuleContext) -> Vec<LintResult> {
62        let anchor = context.segment.clone();
63
64        let children = FunctionalContext::new(context).segment().children(None);
65        let else_clause = children.find_first(Some(|it: &ErasedSegment| {
66            it.is_type(SyntaxKind::ElseClause)
67        }));
68
69        if !else_clause
70            .children(Some(|child| child.raw().eq_ignore_ascii_case("NULL")))
71            .is_empty()
72        {
73            let before_else = children.reversed().select::<fn(&ErasedSegment) -> bool>(
74                None,
75                Some(|it| {
76                    matches!(it.get_type(), SyntaxKind::Whitespace | SyntaxKind::Newline)
77                        | it.is_meta()
78                }),
79                else_clause.first().unwrap().into(),
80                None,
81            );
82
83            let mut fixes = Vec::with_capacity(before_else.len() + 1);
84            fixes.push(LintFix::delete(else_clause.first().unwrap().clone()));
85            fixes.extend(before_else.into_iter().map(LintFix::delete));
86
87            vec![LintResult::new(anchor.into(), fixes, None, None)]
88        } else {
89            Vec::new()
90        }
91    }
92
93    fn crawl_behaviour(&self) -> Crawler {
94        SegmentSeekerCrawler::new(const { SyntaxSet::new(&[SyntaxKind::CaseExpression]) }).into()
95    }
96}