sqruff_lib/rules/layout/
lt08.rs

1use 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                        // It's a blank line!
93                        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                    // Lines with comments aren't blank
103                    line_blank = false;
104                    comment_lines.push(line_idx);
105                } else if forward_slice[seg_idx].is_type(SyntaxKind::Comma) {
106                    // Keep track of where the comma is.
107                    // We'll evaluate it later.
108                    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}