sqruff_lib/rules/layout/
lt08.rs1use hashbrown::HashMap;
2use itertools::Itertools;
3use sqruff_lib_core::dialects::syntax::{SyntaxKind, SyntaxSet};
4use sqruff_lib_core::helpers::IndexMap;
5use sqruff_lib_core::lint_fix::LintFix;
6use sqruff_lib_core::parser::segments::SegmentBuilder;
7
8use crate::core::config::Value;
9use crate::core::rules::context::RuleContext;
10use crate::core::rules::crawlers::{Crawler, SegmentSeekerCrawler};
11use crate::core::rules::{Erased, ErasedRule, LintResult, Rule, RuleGroups};
12use crate::utils::reflow::rebreak::LinePosition;
13
14#[derive(Debug, Default, Clone)]
15pub struct RuleLT08;
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18enum CteCommaStyle {
19 Final,
20 Oneline,
21 Trailing,
22 Leading,
23 Floating,
24}
25
26impl Rule for RuleLT08 {
27 fn load_from_config(&self, _config: &HashMap<String, Value>) -> Result<ErasedRule, String> {
28 Ok(RuleLT08.erased())
29 }
30 fn name(&self) -> &'static str {
31 "layout.cte_newline"
32 }
33
34 fn description(&self) -> &'static str {
35 "Blank line expected but not found after CTE closing bracket."
36 }
37
38 fn long_description(&self) -> &'static str {
39 r#"
40**Anti-pattern**
41
42There is no blank line after the CTE closing bracket. In queries with many CTEs, this hinders readability.
43
44```sql
45WITH plop AS (
46 SELECT * FROM foo
47)
48SELECT a FROM plop
49```
50
51**Best practice**
52
53Add a blank line.
54
55```sql
56WITH plop AS (
57 SELECT * FROM foo
58)
59
60SELECT a FROM plop
61```
62"#
63 }
64
65 fn groups(&self) -> &'static [RuleGroups] {
66 &[RuleGroups::All, RuleGroups::Core, RuleGroups::Layout]
67 }
68 fn eval(&self, context: &RuleContext) -> Vec<LintResult> {
69 let mut error_buffer = Vec::new();
70 let global_comma_style = context
71 .config
72 .reflow()
73 .line_position_for(SyntaxKind::Comma)
74 .unwrap()
75 .position();
76 let expanded_segments = context.segment.iter_segments(
77 const { &SyntaxSet::new(&[SyntaxKind::CommonTableExpression]) },
78 false,
79 );
80
81 let bracket_indices = expanded_segments
82 .iter()
83 .enumerate()
84 .filter_map(|(idx, seg)| seg.is_type(SyntaxKind::Bracketed).then_some(idx));
85
86 for bracket_idx in bracket_indices {
87 let forward_slice = &expanded_segments[bracket_idx..];
88 let mut seg_idx = 1;
89 let mut line_idx: usize = 0;
90 let mut comma_seg_idx = 0;
91 let mut blank_lines = 0;
92 let mut comma_line_idx = None;
93 let mut line_blank = false;
94 let mut line_starts = IndexMap::default();
95 let mut comment_lines = Vec::new();
96
97 while forward_slice[seg_idx].is_type(SyntaxKind::Comma)
98 || !forward_slice[seg_idx].is_code()
99 {
100 if forward_slice[seg_idx].is_type(SyntaxKind::Newline) {
101 if line_blank {
102 blank_lines += 1;
104 }
105 line_blank = true;
106 line_idx += 1;
107 line_starts.insert(line_idx, seg_idx + 1);
108 } else if forward_slice[seg_idx].is_type(SyntaxKind::Comment)
109 || forward_slice[seg_idx].is_type(SyntaxKind::InlineComment)
110 || forward_slice[seg_idx].is_type(SyntaxKind::BlockComment)
111 {
112 line_blank = false;
114 comment_lines.push(line_idx);
115 } else if forward_slice[seg_idx].is_type(SyntaxKind::Comma) {
116 comma_line_idx = line_idx.into();
119 comma_seg_idx = seg_idx;
120 }
121
122 seg_idx += 1;
123 }
124
125 let comma_style = if comma_line_idx.is_none() {
126 CteCommaStyle::Final
127 } else if line_idx == 0 {
128 CteCommaStyle::Oneline
129 } else if let Some(0) = comma_line_idx {
130 CteCommaStyle::Trailing
131 } else if let Some(idx) = comma_line_idx {
132 if idx == line_idx {
133 CteCommaStyle::Leading
134 } else {
135 CteCommaStyle::Floating
136 }
137 } else {
138 CteCommaStyle::Floating
139 };
140
141 if blank_lines >= 1 {
142 continue;
143 }
144
145 let mut is_replace = false;
146 let mut fix_point = None;
147
148 let num_newlines = if comma_style == CteCommaStyle::Oneline {
149 if global_comma_style == LinePosition::Trailing {
150 fix_point = forward_slice[comma_seg_idx + 1].clone().into();
151 if forward_slice[comma_seg_idx + 1].is_type(SyntaxKind::Whitespace) {
152 is_replace = true;
153 }
154 } else if global_comma_style == LinePosition::Leading {
155 fix_point = forward_slice[comma_seg_idx].clone().into();
156 } else {
157 unimplemented!("Unexpected global comma style {global_comma_style:?}");
158 }
159
160 2
161 } else {
162 if comma_style == CteCommaStyle::Leading {
163 if comma_seg_idx < forward_slice.len() {
164 fix_point = forward_slice[comma_seg_idx].clone().into();
165 }
166 } else if comment_lines.is_empty() || !comment_lines.contains(&(line_idx - 1)) {
167 if matches!(
168 comma_style,
169 CteCommaStyle::Trailing | CteCommaStyle::Final | CteCommaStyle::Floating
170 ) {
171 if forward_slice[seg_idx - 1].is_type(SyntaxKind::Whitespace) {
172 fix_point = forward_slice[seg_idx - 1].clone().into();
173 is_replace = true;
174 } else {
175 fix_point = forward_slice[seg_idx].clone().into();
176 }
177 }
178 } else {
179 let mut offset = 1;
180
181 while line_idx
182 .checked_sub(offset)
183 .is_some_and(|idx| comment_lines.contains(&idx))
184 {
185 offset += 1;
186 }
187
188 let mut effective_line_idx = line_idx - (offset - 1);
189 if effective_line_idx == 0 {
190 effective_line_idx = line_idx;
191 }
192
193 let line_start_idx = if effective_line_idx < line_starts.len() {
194 *line_starts.get(&effective_line_idx).unwrap()
195 } else {
196 let (_, line_start) = line_starts.last().unwrap_or((&0, &0));
197 *line_start
198 };
199
200 fix_point = forward_slice[line_start_idx].clone().into();
201 }
202
203 1
204 };
205
206 let fixes = if let Some(anchor) = fix_point {
208 let newlines = std::iter::repeat_n(
209 SegmentBuilder::newline(context.tables.next_id(), "\n"),
210 num_newlines,
211 )
212 .collect_vec();
213
214 if is_replace {
215 vec![LintFix::replace(anchor, newlines, None)]
216 } else {
217 vec![LintFix::create_before(anchor, newlines)]
218 }
219 } else {
220 Vec::new()
222 };
223
224 error_buffer.push(LintResult::new(
225 forward_slice[seg_idx].clone().into(),
226 fixes,
227 None,
228 None,
229 ));
230 }
231
232 error_buffer
233 }
234
235 fn is_fix_compatible(&self) -> bool {
236 true
237 }
238
239 fn crawl_behaviour(&self) -> Crawler {
240 SegmentSeekerCrawler::new(const { SyntaxSet::new(&[SyntaxKind::WithCompoundStatement]) })
241 .into()
242 }
243}