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::base::SegmentBuilder;
8
9use crate::core::config::Value;
10use crate::core::rules::base::{Erased, ErasedRule, LintResult, Rule, RuleGroups};
11use crate::core::rules::context::RuleContext;
12use crate::core::rules::crawlers::{Crawler, SegmentSeekerCrawler};
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 comment_lines.is_empty() || !comment_lines.contains(&(line_idx - 1)) {
151 if matches!(comma_style, "trailing" | "final" | "floating") {
152 if forward_slice[seg_idx - 1].is_type(SyntaxKind::Whitespace) {
153 fix_point = forward_slice[seg_idx - 1].clone().into();
154 fix_type = EditType::Replace;
155 } else {
156 fix_point = forward_slice[seg_idx].clone().into();
157 }
158 }
159 } else if comma_style == "leading" {
160 fix_point = forward_slice[comma_seg_idx].clone().into();
161 } else {
162 let mut offset = 1;
163
164 while line_idx
165 .checked_sub(offset)
166 .is_some_and(|idx| comment_lines.contains(&idx))
167 {
168 offset += 1;
169 }
170
171 let mut effective_line_idx = line_idx - (offset - 1);
172 if effective_line_idx == 0 {
173 effective_line_idx = line_idx;
174 }
175
176 let line_start_idx = if effective_line_idx < line_starts.len() {
177 *line_starts.get(&effective_line_idx).unwrap()
178 } else {
179 let (_, line_start) = line_starts.last().unwrap_or((&0, &0));
180 *line_start
181 };
182
183 fix_point = forward_slice[line_start_idx].clone().into();
184 }
185
186 1
187 };
188
189 let fixes = vec![LintFix {
190 edit_type: fix_type,
191 anchor: fix_point.unwrap(),
192 edit: std::iter::repeat_n(
193 SegmentBuilder::newline(context.tables.next_id(), "\n"),
194 num_newlines,
195 )
196 .collect_vec(),
197 source: Vec::new(),
198 }];
199
200 error_buffer.push(LintResult::new(
201 forward_slice[seg_idx].clone().into(),
202 fixes,
203 None,
204 None,
205 ));
206 }
207
208 error_buffer
209 }
210
211 fn is_fix_compatible(&self) -> bool {
212 true
213 }
214
215 fn crawl_behaviour(&self) -> Crawler {
216 SegmentSeekerCrawler::new(const { SyntaxSet::new(&[SyntaxKind::WithCompoundStatement]) })
217 .into()
218 }
219}