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