sqruff_lib/rules/structure/
st04.rs1use 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::{ErasedSegment, SegmentBuilder, Tables};
7use sqruff_lib_core::utils::functional::segments::Segments;
8
9use crate::core::config::Value;
10use crate::core::rules::context::RuleContext;
11use crate::core::rules::crawlers::{Crawler, SegmentSeekerCrawler};
12use crate::core::rules::{Erased as _, ErasedRule, LintResult, Rule, RuleGroups};
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 Some(case2_first_case) = case2_first_case else {
114 return Vec::new();
115 };
116
117 if !case2.any(Some(|seg: &ErasedSegment| {
119 seg.is_type(SyntaxKind::CaseExpression)
120 })) {
121 return Vec::new();
122 }
123
124 let x1 = segment
125 .children(Some(|it| it.is_code()))
126 .select::<fn(&ErasedSegment) -> bool>(
127 None,
128 None,
129 case1_first_case.into(),
130 case1_first_when.into(),
131 )
132 .into_iter()
133 .map(|it| it.raw().to_smolstr());
134
135 let x2 = case2
136 .children(Some(|it| it.is_code()))
137 .select::<fn(&ErasedSegment) -> bool>(
138 None,
139 None,
140 case2_first_case.into(),
141 case2_first_when,
142 )
143 .into_iter()
144 .map(|it| it.raw().to_smolstr());
145
146 if x1.ne(x2) {
147 return Vec::new();
148 }
149
150 let case1_else_clause_seg = case1_else_clause.first().unwrap();
151
152 let case1_to_delete = case1_children.select::<fn(&ErasedSegment) -> bool>(
153 None,
154 None,
155 case1_last_when.into(),
156 case1_else_clause_seg.into(),
157 );
158
159 let comments = case1_to_delete.find_last(Some(|it: &ErasedSegment| it.is_comment()));
160 let after_last_comment_index = comments
161 .first()
162 .and_then(|comment| case1_to_delete.iter().position(|it| it == comment))
163 .map_or(0, |n| n + 1);
164
165 let case1_comments_to_restore = case1_to_delete.select::<fn(&ErasedSegment) -> bool>(
166 None,
167 None,
168 None,
169 case1_to_delete.base.get(after_last_comment_index),
170 );
171 let after_else_comment = case1_else_clause.children(None).select(
172 Some(|it: &ErasedSegment| {
173 matches!(
174 it.get_type(),
175 SyntaxKind::Newline
176 | SyntaxKind::InlineComment
177 | SyntaxKind::BlockComment
178 | SyntaxKind::Comment
179 | SyntaxKind::Whitespace
180 )
181 }),
182 None,
183 None,
184 case1_else_expressions.first(),
185 );
186
187 let mut fixes = case1_to_delete
188 .into_iter()
189 .map(LintFix::delete)
190 .collect_vec();
191
192 let tab_space_size = context.config.raw["indentation"]["tab_space_size"]
193 .as_int()
194 .unwrap() as usize;
195 let indent_unit = context.config.raw["indentation"]["indent_unit"]
196 .as_string()
197 .unwrap();
198 let indent_unit = IndentUnit::from_type_and_size(indent_unit, tab_space_size);
199
200 let when_indent_str = indentation(&case1_children, case1_last_when, indent_unit);
201 let end_indent_str = indentation(&case1_children, case1_first_case, indent_unit);
202
203 let nested_clauses = case2.children(Some(|it: &ErasedSegment| {
204 matches!(
205 it.get_type(),
206 SyntaxKind::WhenClause
207 | SyntaxKind::ElseClause
208 | SyntaxKind::Newline
209 | SyntaxKind::InlineComment
210 | SyntaxKind::BlockComment
211 | SyntaxKind::Comment
212 | SyntaxKind::Whitespace
213 )
214 }));
215
216 let mut segments = case1_comments_to_restore.base;
217 segments.append(&mut rebuild_spacing(
218 context.tables,
219 &when_indent_str,
220 after_else_comment,
221 ));
222 segments.append(&mut rebuild_spacing(
223 context.tables,
224 &when_indent_str,
225 nested_clauses,
226 ));
227
228 fixes.push(LintFix::create_after(
229 case1_last_when.clone(),
230 segments,
231 None,
232 ));
233 fixes.push(LintFix::delete(case1_else_clause_seg.clone()));
234 fixes.append(&mut nested_end_trailing_comment(
235 context.tables,
236 case1_children,
237 case1_else_clause_seg,
238 &end_indent_str,
239 ));
240
241 vec![LintResult::new(case2.first().cloned(), fixes, None, None)]
242 }
243
244 fn is_fix_compatible(&self) -> bool {
245 true
246 }
247
248 fn crawl_behaviour(&self) -> Crawler {
249 SegmentSeekerCrawler::new(const { SyntaxSet::new(&[SyntaxKind::CaseExpression]) }).into()
250 }
251}
252
253fn indentation(
254 parent_segments: &Segments,
255 segment: &ErasedSegment,
256 indent_unit: IndentUnit,
257) -> String {
258 let leading_whitespace = parent_segments
259 .select::<fn(&ErasedSegment) -> bool>(None, None, None, segment.into())
260 .reversed()
261 .find_first(Some(|it: &ErasedSegment| {
262 it.is_type(SyntaxKind::Whitespace)
263 }));
264 let seg_indent = parent_segments
265 .select::<fn(&ErasedSegment) -> bool>(None, None, None, segment.into())
266 .find_last(Some(|it| it.is_type(SyntaxKind::Indent)));
267 let mut indent_level = 1;
268 if let Some(segment_indent) = seg_indent
269 .last()
270 .filter(|segment_indent| segment_indent.is_indent())
271 {
272 indent_level = segment_indent.indent_val() as usize + 1;
273 }
274
275 if let Some(whitespace_seg) = leading_whitespace.first() {
276 if !leading_whitespace.is_empty() && whitespace_seg.raw().len() > 1 {
277 leading_whitespace
278 .iter()
279 .map(|seg| seg.raw().to_string())
280 .collect::<String>()
281 } else {
282 construct_single_indent(indent_unit).repeat(indent_level)
283 }
284 } else {
285 construct_single_indent(indent_unit).repeat(indent_level)
286 }
287}
288
289fn rebuild_spacing(
290 tables: &Tables,
291 indent_str: &str,
292 nested_clauses: Segments,
293) -> Vec<ErasedSegment> {
294 let mut buff = Vec::new();
295
296 let mut prior_newline = nested_clauses
297 .find_last(Some(|it: &ErasedSegment| !it.is_whitespace()))
298 .any(Some(|it: &ErasedSegment| it.is_comment()));
299 let mut prior_whitespace = String::new();
300
301 for seg in nested_clauses {
302 if matches!(
303 seg.get_type(),
304 SyntaxKind::WhenClause | SyntaxKind::ElseClause
305 ) || (prior_newline && seg.is_comment())
306 {
307 buff.push(SegmentBuilder::newline(tables.next_id(), "\n"));
308 buff.push(SegmentBuilder::whitespace(tables.next_id(), indent_str));
309 buff.push(seg.clone());
310 prior_newline = false;
311 prior_whitespace.clear();
312 } else if seg.is_type(SyntaxKind::Newline) {
313 prior_newline = true;
314 prior_whitespace.clear();
315 } else if !prior_newline && seg.is_comment() {
316 buff.push(SegmentBuilder::whitespace(
317 tables.next_id(),
318 &prior_whitespace,
319 ));
320 buff.push(seg.clone());
321 prior_newline = false;
322 prior_whitespace.clear();
323 } else if seg.is_whitespace() {
324 prior_whitespace = seg.raw().to_string();
325 }
326 }
327
328 buff
329}
330
331fn nested_end_trailing_comment(
332 tables: &Tables,
333 case1_children: Segments,
334 case1_else_clause_seg: &ErasedSegment,
335 end_indent_str: &str,
336) -> Vec<LintFix> {
337 let trailing_end = case1_children.select::<fn(&ErasedSegment) -> bool>(
339 None,
340 Some(|seg: &ErasedSegment| !seg.is_type(SyntaxKind::Newline)),
341 Some(case1_else_clause_seg),
342 None,
343 );
344
345 let mut fixes = trailing_end
346 .select(
347 Some(|seg: &ErasedSegment| seg.is_whitespace()),
348 Some(|seg: &ErasedSegment| !seg.is_comment()),
349 None,
350 None,
351 )
352 .into_iter()
353 .map(LintFix::delete)
354 .collect_vec();
355
356 if let Some(first_comment) = trailing_end
357 .find_first(Some(|seg: &ErasedSegment| seg.is_comment()))
358 .first()
359 {
360 let segments = vec![
361 SegmentBuilder::newline(tables.next_id(), "\n"),
362 SegmentBuilder::whitespace(tables.next_id(), end_indent_str),
363 ];
364 fixes.push(LintFix::create_before(first_comment.clone(), segments));
365 }
366
367 fixes
368}