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::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 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}