sqruff_lib/rules/layout/
lt08.rs

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