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