sqruff_lib/rules/structure/
st04.rs

1use ahash::AHashMap;
2use itertools::Itertools;
3use smol_str::ToSmolStr;
4use sqruff_lib_core::dialects::syntax::{SyntaxKind, SyntaxSet};
5use sqruff_lib_core::lint_fix::LintFix;
6use sqruff_lib_core::parser::segments::base::{ErasedSegment, SegmentBuilder, Tables};
7use sqruff_lib_core::utils::functional::segments::Segments;
8
9use crate::core::config::Value;
10use crate::core::rules::base::{CloneRule, ErasedRule, LintResult, Rule, RuleGroups};
11use crate::core::rules::context::RuleContext;
12use crate::core::rules::crawlers::{Crawler, SegmentSeekerCrawler};
13use crate::utils::functional::context::FunctionalContext;
14use crate::utils::reflow::reindent::{IndentUnit, construct_single_indent};
15
16#[derive(Clone, Debug, Default)]
17pub struct RuleST04;
18
19impl Rule for RuleST04 {
20    fn load_from_config(&self, _config: &AHashMap<String, Value>) -> Result<ErasedRule, String> {
21        Ok(RuleST04.erased())
22    }
23
24    fn name(&self) -> &'static str {
25        "structure.nested_case"
26    }
27
28    fn description(&self) -> &'static str {
29        "Nested ``CASE`` statement in ``ELSE`` clause could be flattened."
30    }
31
32    fn long_description(&self) -> &'static str {
33        r"
34## Anti-pattern
35
36In this example, the outer `CASE`'s `ELSE` is an unnecessary, nested `CASE`.
37
38```sql
39SELECT
40  CASE
41    WHEN species = 'Cat' THEN 'Meow'
42    ELSE
43    CASE
44       WHEN species = 'Dog' THEN 'Woof'
45    END
46  END as sound
47FROM mytable
48```
49
50## Best practice
51
52Move the body of the inner `CASE` to the end of the outer one.
53
54```sql
55SELECT
56  CASE
57    WHEN species = 'Cat' THEN 'Meow'
58    WHEN species = 'Dog' THEN 'Woof'
59  END AS sound
60FROM mytable
61```
62"
63    }
64
65    fn groups(&self) -> &'static [RuleGroups] {
66        &[RuleGroups::All, RuleGroups::Structure]
67    }
68
69    fn eval(&self, context: &RuleContext) -> Vec<LintResult> {
70        let segment = FunctionalContext::new(context).segment();
71        let case1_children = segment.children(None);
72        let case1_keywords =
73            case1_children.find_first(Some(|it: &ErasedSegment| it.is_keyword("CASE")));
74        let case1_first_case = case1_keywords.first().unwrap();
75        let case1_when_list = case1_children.find_first(Some(|it: &ErasedSegment| {
76            matches!(
77                it.get_type(),
78                SyntaxKind::WhenClause | SyntaxKind::ElseClause
79            )
80        }));
81        let case1_first_when = case1_when_list.first().unwrap();
82        let when_clause_list =
83            case1_children.find_last(Some(|it| it.is_type(SyntaxKind::WhenClause)));
84        let case1_last_when = when_clause_list.first();
85        let case1_else_clause =
86            case1_children.find_last(Some(|it| it.is_type(SyntaxKind::ElseClause)));
87        let case1_else_expressions =
88            case1_else_clause.children(Some(|it| it.is_type(SyntaxKind::Expression)));
89        let expression_children = case1_else_expressions.children(None);
90        let case2 =
91            expression_children.select::<fn(&ErasedSegment) -> bool>(None, None, None, None);
92        let case2_children = case2.children(None);
93        let case2_case_list =
94            case2_children.find_first(Some(|it: &ErasedSegment| it.is_keyword("CASE")));
95        let case2_first_case = case2_case_list.first();
96        let case2_when_list = case2_children.find_first(Some(|it: &ErasedSegment| {
97            matches!(
98                it.get_type(),
99                SyntaxKind::WhenClause | SyntaxKind::ElseClause
100            )
101        }));
102        let case2_first_when = case2_when_list.first();
103
104        let Some(case1_last_when) = case1_last_when else {
105            return Vec::new();
106        };
107        if case1_else_expressions.len() > 1 || expression_children.len() > 1 || case2.is_empty() {
108            return Vec::new();
109        }
110
111        let x1 = segment
112            .children(Some(|it| it.is_code()))
113            .select::<fn(&ErasedSegment) -> bool>(
114                None,
115                None,
116                case1_first_case.into(),
117                case1_first_when.into(),
118            )
119            .into_iter()
120            .map(|it| it.raw().to_smolstr());
121
122        let x2 = case2
123            .children(Some(|it| it.is_code()))
124            .select::<fn(&ErasedSegment) -> bool>(None, None, case2_first_case, case2_first_when)
125            .into_iter()
126            .map(|it| it.raw().to_smolstr());
127
128        if x1.ne(x2) {
129            return Vec::new();
130        }
131
132        let case1_else_clause_seg = case1_else_clause.first().unwrap();
133
134        let case1_to_delete = case1_children.select::<fn(&ErasedSegment) -> bool>(
135            None,
136            None,
137            case1_last_when.into(),
138            case1_else_clause_seg.into(),
139        );
140
141        let comments = case1_to_delete.find_last(Some(|it: &ErasedSegment| it.is_comment()));
142        let after_last_comment_index = comments
143            .first()
144            .and_then(|comment| case1_to_delete.iter().position(|it| it == comment))
145            .map_or(0, |n| n + 1);
146
147        let case1_comments_to_restore = case1_to_delete.select::<fn(&ErasedSegment) -> bool>(
148            None,
149            None,
150            None,
151            case1_to_delete.base.get(after_last_comment_index),
152        );
153        let after_else_comment = case1_else_clause.children(None).select(
154            Some(|it: &ErasedSegment| {
155                matches!(
156                    it.get_type(),
157                    SyntaxKind::Newline
158                        | SyntaxKind::InlineComment
159                        | SyntaxKind::BlockComment
160                        | SyntaxKind::Comment
161                        | SyntaxKind::Whitespace
162                )
163            }),
164            None,
165            None,
166            case1_else_expressions.first(),
167        );
168
169        let mut fixes = case1_to_delete
170            .into_iter()
171            .map(LintFix::delete)
172            .collect_vec();
173
174        let tab_space_size = context.config.raw["indentation"]["tab_space_size"]
175            .as_int()
176            .unwrap() as usize;
177        let indent_unit = context.config.raw["indentation"]["indent_unit"]
178            .as_string()
179            .unwrap();
180        let indent_unit = IndentUnit::from_type_and_size(indent_unit, tab_space_size);
181
182        let when_indent_str = indentation(&case1_children, case1_last_when, indent_unit);
183        let end_indent_str = indentation(&case1_children, case1_first_case, indent_unit);
184
185        let nested_clauses = case2.children(Some(|it: &ErasedSegment| {
186            matches!(
187                it.get_type(),
188                SyntaxKind::WhenClause
189                    | SyntaxKind::ElseClause
190                    | SyntaxKind::Newline
191                    | SyntaxKind::InlineComment
192                    | SyntaxKind::BlockComment
193                    | SyntaxKind::Comment
194                    | SyntaxKind::Whitespace
195            )
196        }));
197
198        let mut segments = case1_comments_to_restore.base;
199        segments.append(&mut rebuild_spacing(
200            context.tables,
201            &when_indent_str,
202            after_else_comment,
203        ));
204        segments.append(&mut rebuild_spacing(
205            context.tables,
206            &when_indent_str,
207            nested_clauses,
208        ));
209
210        fixes.push(LintFix::create_after(
211            case1_last_when.clone(),
212            segments,
213            None,
214        ));
215        fixes.push(LintFix::delete(case1_else_clause_seg.clone()));
216        fixes.append(&mut nested_end_trailing_comment(
217            context.tables,
218            case1_children,
219            case1_else_clause_seg,
220            &end_indent_str,
221        ));
222
223        vec![LintResult::new(case2.first().cloned(), fixes, None, None)]
224    }
225
226    fn is_fix_compatible(&self) -> bool {
227        true
228    }
229
230    fn crawl_behaviour(&self) -> Crawler {
231        SegmentSeekerCrawler::new(const { SyntaxSet::new(&[SyntaxKind::CaseExpression]) }).into()
232    }
233}
234
235fn indentation(
236    parent_segments: &Segments,
237    segment: &ErasedSegment,
238    indent_unit: IndentUnit,
239) -> String {
240    let leading_whitespace = parent_segments
241        .select::<fn(&ErasedSegment) -> bool>(None, None, None, segment.into())
242        .reversed()
243        .find_first(Some(|it: &ErasedSegment| {
244            it.is_type(SyntaxKind::Whitespace)
245        }));
246    let seg_indent = parent_segments
247        .select::<fn(&ErasedSegment) -> bool>(None, None, None, segment.into())
248        .find_last(Some(|it| it.is_type(SyntaxKind::Indent)));
249    let mut indent_level = 1;
250    if let Some(segment_indent) = seg_indent
251        .last()
252        .filter(|segment_indent| segment_indent.is_indent())
253    {
254        indent_level = segment_indent.indent_val() as usize + 1;
255    }
256
257    let indent_str = if let Some(whitespace_seg) = leading_whitespace.first() {
258        if !leading_whitespace.is_empty() && whitespace_seg.raw().len() > 1 {
259            leading_whitespace
260                .iter()
261                .map(|seg| seg.raw().to_string())
262                .collect::<String>()
263        } else {
264            construct_single_indent(indent_unit).repeat(indent_level)
265        }
266    } else {
267        construct_single_indent(indent_unit).repeat(indent_level)
268    };
269    indent_str
270}
271
272fn rebuild_spacing(
273    tables: &Tables,
274    indent_str: &str,
275    nested_clauses: Segments,
276) -> Vec<ErasedSegment> {
277    let mut buff = Vec::new();
278
279    let mut prior_newline = nested_clauses
280        .find_last(Some(|it: &ErasedSegment| !it.is_whitespace()))
281        .any(Some(|it: &ErasedSegment| it.is_comment()));
282    let mut prior_whitespace = String::new();
283
284    for seg in nested_clauses {
285        if matches!(
286            seg.get_type(),
287            SyntaxKind::WhenClause | SyntaxKind::ElseClause
288        ) || (prior_newline && seg.is_comment())
289        {
290            buff.push(SegmentBuilder::newline(tables.next_id(), "\n"));
291            buff.push(SegmentBuilder::whitespace(tables.next_id(), indent_str));
292            buff.push(seg.clone());
293            prior_newline = false;
294            prior_whitespace.clear();
295        } else if seg.is_type(SyntaxKind::Newline) {
296            prior_newline = true;
297            prior_whitespace.clear();
298        } else if !prior_newline && seg.is_comment() {
299            buff.push(SegmentBuilder::whitespace(
300                tables.next_id(),
301                &prior_whitespace,
302            ));
303            buff.push(seg.clone());
304            prior_newline = false;
305            prior_whitespace.clear();
306        } else if seg.is_whitespace() {
307            prior_whitespace = seg.raw().to_string();
308        }
309    }
310
311    buff
312}
313
314fn nested_end_trailing_comment(
315    tables: &Tables,
316    case1_children: Segments,
317    case1_else_clause_seg: &ErasedSegment,
318    end_indent_str: &str,
319) -> Vec<LintFix> {
320    // Prepend newline spacing to comments on the final nested `END` line.
321    let trailing_end = case1_children.select::<fn(&ErasedSegment) -> bool>(
322        None,
323        Some(|seg: &ErasedSegment| !seg.is_type(SyntaxKind::Newline)),
324        Some(case1_else_clause_seg),
325        None,
326    );
327
328    let mut fixes = trailing_end
329        .select(
330            Some(|seg: &ErasedSegment| seg.is_whitespace()),
331            Some(|seg: &ErasedSegment| !seg.is_comment()),
332            None,
333            None,
334        )
335        .into_iter()
336        .map(LintFix::delete)
337        .collect_vec();
338
339    if let Some(first_comment) = trailing_end
340        .find_first(Some(|seg: &ErasedSegment| seg.is_comment()))
341        .first()
342    {
343        let segments = vec![
344            SegmentBuilder::newline(tables.next_id(), "\n"),
345            SegmentBuilder::whitespace(tables.next_id(), end_indent_str),
346        ];
347        fixes.push(LintFix::create_before(first_comment.clone(), segments));
348    }
349
350    fixes
351}