sqruff_lib/rules/layout/
lt08.rs

1use 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                        // It's a blank line!
91                        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                    // Lines with comments aren't blank
101                    line_blank = false;
102                    comment_lines.push(line_idx);
103                } else if forward_slice[seg_idx].is_type(SyntaxKind::Comma) {
104                    // Keep track of where the comma is.
105                    // We'll evaluate it later.
106                    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            // Only create fixes if we have a valid fix point
192            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                // Skip generating a fix if we don't have a valid anchor point
205                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}