sqruff_lib/rules/layout/
lt08.rs1use std::iter::repeat;
2
3use ahash::AHashMap;
4use itertools::Itertools;
5use sqruff_lib_core::dialects::syntax::{SyntaxKind, SyntaxSet};
6use sqruff_lib_core::edit_type::EditType;
7use sqruff_lib_core::helpers::IndexMap;
8use sqruff_lib_core::lint_fix::LintFix;
9use sqruff_lib_core::parser::segments::base::SegmentBuilder;
10
11use crate::core::config::Value;
12use crate::core::rules::base::{Erased, ErasedRule, LintResult, Rule, RuleGroups};
13use crate::core::rules::context::RuleContext;
14use crate::core::rules::crawlers::{Crawler, SegmentSeekerCrawler};
15
16#[derive(Debug, Default, Clone)]
17pub struct RuleLT08;
18
19impl Rule for RuleLT08 {
20 fn load_from_config(&self, _config: &AHashMap<String, Value>) -> Result<ErasedRule, String> {
21 Ok(RuleLT08.erased())
22 }
23 fn name(&self) -> &'static str {
24 "layout.cte_newline"
25 }
26
27 fn description(&self) -> &'static str {
28 "Blank line expected but not found after CTE closing bracket."
29 }
30
31 fn long_description(&self) -> &'static str {
32 r#"
33**Anti-pattern**
34
35There is no blank line after the CTE closing bracket. In queries with many CTEs, this hinders readability.
36
37```sql
38WITH plop AS (
39 SELECT * FROM foo
40)
41SELECT a FROM plop
42```
43
44**Best practice**
45
46Add a blank line.
47
48```sql
49WITH plop AS (
50 SELECT * FROM foo
51)
52
53SELECT a FROM plop
54```
55"#
56 }
57
58 fn groups(&self) -> &'static [RuleGroups] {
59 &[RuleGroups::All, RuleGroups::Core, RuleGroups::Layout]
60 }
61 fn eval(&self, context: &RuleContext) -> Vec<LintResult> {
62 let mut error_buffer = Vec::new();
63 let global_comma_style = context.config.raw["layout"]["type"]["comma"]["line_position"]
64 .as_string()
65 .unwrap();
66 let expanded_segments = context.segment.iter_segments(
67 const { &SyntaxSet::new(&[SyntaxKind::CommonTableExpression]) },
68 false,
69 );
70
71 let bracket_indices = expanded_segments
72 .iter()
73 .enumerate()
74 .filter_map(|(idx, seg)| seg.is_type(SyntaxKind::Bracketed).then_some(idx));
75
76 for bracket_idx in bracket_indices {
77 let forward_slice = &expanded_segments[bracket_idx..];
78 let mut seg_idx = 1;
79 let mut line_idx: usize = 0;
80 let mut comma_seg_idx = 0;
81 let mut blank_lines = 0;
82 let mut comma_line_idx = None;
83 let mut line_blank = false;
84 let mut line_starts = IndexMap::default();
85 let mut comment_lines = Vec::new();
86
87 while forward_slice[seg_idx].is_type(SyntaxKind::Comma)
88 || !forward_slice[seg_idx].is_code()
89 {
90 if forward_slice[seg_idx].is_type(SyntaxKind::Newline) {
91 if line_blank {
92 blank_lines += 1;
94 }
95 line_blank = true;
96 line_idx += 1;
97 line_starts.insert(line_idx, seg_idx + 1);
98 } else if forward_slice[seg_idx].is_type(SyntaxKind::Comment)
99 || forward_slice[seg_idx].is_type(SyntaxKind::InlineComment)
100 || forward_slice[seg_idx].is_type(SyntaxKind::BlockComment)
101 {
102 line_blank = false;
104 comment_lines.push(line_idx);
105 } else if forward_slice[seg_idx].is_type(SyntaxKind::Comma) {
106 comma_line_idx = line_idx.into();
109 comma_seg_idx = seg_idx;
110 }
111
112 seg_idx += 1;
113 }
114
115 let comma_style = if comma_line_idx.is_none() {
116 "final"
117 } else if line_idx == 0 {
118 "oneline"
119 } else if let Some(0) = comma_line_idx {
120 "trailing"
121 } else if let Some(idx) = comma_line_idx {
122 if idx == line_idx {
123 "leading"
124 } else {
125 "floating"
126 }
127 } else {
128 "floating"
129 };
130
131 if blank_lines >= 1 {
132 continue;
133 }
134
135 let mut fix_type = EditType::CreateBefore;
136 let mut fix_point = None;
137
138 let num_newlines = if comma_style == "oneline" {
139 if global_comma_style == "trailing" {
140 fix_point = forward_slice[comma_seg_idx + 1].clone().into();
141 if forward_slice[comma_seg_idx + 1].is_type(SyntaxKind::Whitespace) {
142 fix_type = EditType::Replace;
143 }
144 } else if global_comma_style == "leading" {
145 fix_point = forward_slice[comma_seg_idx].clone().into();
146 } else {
147 unimplemented!("Unexpected global comma style {global_comma_style:?}");
148 }
149
150 2
151 } else {
152 if comment_lines.is_empty() || !comment_lines.contains(&(line_idx - 1)) {
153 if matches!(comma_style, "trailing" | "final" | "floating") {
154 if forward_slice[seg_idx - 1].is_type(SyntaxKind::Whitespace) {
155 fix_point = forward_slice[seg_idx - 1].clone().into();
156 fix_type = EditType::Replace;
157 } else {
158 fix_point = forward_slice[seg_idx].clone().into();
159 }
160 }
161 } else if comma_style == "leading" {
162 fix_point = forward_slice[comma_seg_idx].clone().into();
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 = vec![LintFix {
192 edit_type: fix_type,
193 anchor: fix_point.unwrap(),
194 edit: repeat(SegmentBuilder::newline(context.tables.next_id(), "\n"))
195 .take(num_newlines)
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}