Skip to main content

sqruff_lib/rules/layout/
lt08.rs

1use hashbrown::HashMap;
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};
12use crate::utils::reflow::rebreak::LinePosition;
13
14#[derive(Debug, Default, Clone)]
15pub struct RuleLT08;
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18enum CteCommaStyle {
19    Final,
20    Oneline,
21    Trailing,
22    Leading,
23    Floating,
24}
25
26impl Rule for RuleLT08 {
27    fn load_from_config(&self, _config: &HashMap<String, Value>) -> Result<ErasedRule, String> {
28        Ok(RuleLT08.erased())
29    }
30    fn name(&self) -> &'static str {
31        "layout.cte_newline"
32    }
33
34    fn description(&self) -> &'static str {
35        "Blank line expected but not found after CTE closing bracket."
36    }
37
38    fn long_description(&self) -> &'static str {
39        r#"
40**Anti-pattern**
41
42There is no blank line after the CTE closing bracket. In queries with many CTEs, this hinders readability.
43
44```sql
45WITH plop AS (
46    SELECT * FROM foo
47)
48SELECT a FROM plop
49```
50
51**Best practice**
52
53Add a blank line.
54
55```sql
56WITH plop AS (
57    SELECT * FROM foo
58)
59
60SELECT a FROM plop
61```
62"#
63    }
64
65    fn groups(&self) -> &'static [RuleGroups] {
66        &[RuleGroups::All, RuleGroups::Core, RuleGroups::Layout]
67    }
68    fn eval(&self, context: &RuleContext) -> Vec<LintResult> {
69        let mut error_buffer = Vec::new();
70        let global_comma_style = context
71            .config
72            .reflow()
73            .line_position_for(SyntaxKind::Comma)
74            .unwrap()
75            .position();
76        let expanded_segments = context.segment.iter_segments(
77            const { &SyntaxSet::new(&[SyntaxKind::CommonTableExpression]) },
78            false,
79        );
80
81        let bracket_indices = expanded_segments
82            .iter()
83            .enumerate()
84            .filter_map(|(idx, seg)| seg.is_type(SyntaxKind::Bracketed).then_some(idx));
85
86        for bracket_idx in bracket_indices {
87            let forward_slice = &expanded_segments[bracket_idx..];
88            let mut seg_idx = 1;
89            let mut line_idx: usize = 0;
90            let mut comma_seg_idx = 0;
91            let mut blank_lines = 0;
92            let mut comma_line_idx = None;
93            let mut line_blank = false;
94            let mut line_starts = IndexMap::default();
95            let mut comment_lines = Vec::new();
96
97            while forward_slice[seg_idx].is_type(SyntaxKind::Comma)
98                || !forward_slice[seg_idx].is_code()
99            {
100                if forward_slice[seg_idx].is_type(SyntaxKind::Newline) {
101                    if line_blank {
102                        // It's a blank line!
103                        blank_lines += 1;
104                    }
105                    line_blank = true;
106                    line_idx += 1;
107                    line_starts.insert(line_idx, seg_idx + 1);
108                } else if forward_slice[seg_idx].is_type(SyntaxKind::Comment)
109                    || forward_slice[seg_idx].is_type(SyntaxKind::InlineComment)
110                    || forward_slice[seg_idx].is_type(SyntaxKind::BlockComment)
111                {
112                    // Lines with comments aren't blank
113                    line_blank = false;
114                    comment_lines.push(line_idx);
115                } else if forward_slice[seg_idx].is_type(SyntaxKind::Comma) {
116                    // Keep track of where the comma is.
117                    // We'll evaluate it later.
118                    comma_line_idx = line_idx.into();
119                    comma_seg_idx = seg_idx;
120                }
121
122                seg_idx += 1;
123            }
124
125            let comma_style = if comma_line_idx.is_none() {
126                CteCommaStyle::Final
127            } else if line_idx == 0 {
128                CteCommaStyle::Oneline
129            } else if let Some(0) = comma_line_idx {
130                CteCommaStyle::Trailing
131            } else if let Some(idx) = comma_line_idx {
132                if idx == line_idx {
133                    CteCommaStyle::Leading
134                } else {
135                    CteCommaStyle::Floating
136                }
137            } else {
138                CteCommaStyle::Floating
139            };
140
141            if blank_lines >= 1 {
142                continue;
143            }
144
145            let mut is_replace = false;
146            let mut fix_point = None;
147
148            let num_newlines = if comma_style == CteCommaStyle::Oneline {
149                if global_comma_style == LinePosition::Trailing {
150                    fix_point = forward_slice[comma_seg_idx + 1].clone().into();
151                    if forward_slice[comma_seg_idx + 1].is_type(SyntaxKind::Whitespace) {
152                        is_replace = true;
153                    }
154                } else if global_comma_style == LinePosition::Leading {
155                    fix_point = forward_slice[comma_seg_idx].clone().into();
156                } else {
157                    unimplemented!("Unexpected global comma style {global_comma_style:?}");
158                }
159
160                2
161            } else {
162                if comma_style == CteCommaStyle::Leading {
163                    if comma_seg_idx < forward_slice.len() {
164                        fix_point = forward_slice[comma_seg_idx].clone().into();
165                    }
166                } else if comment_lines.is_empty() || !comment_lines.contains(&(line_idx - 1)) {
167                    if matches!(
168                        comma_style,
169                        CteCommaStyle::Trailing | CteCommaStyle::Final | CteCommaStyle::Floating
170                    ) {
171                        if forward_slice[seg_idx - 1].is_type(SyntaxKind::Whitespace) {
172                            fix_point = forward_slice[seg_idx - 1].clone().into();
173                            is_replace = true;
174                        } else {
175                            fix_point = forward_slice[seg_idx].clone().into();
176                        }
177                    }
178                } else {
179                    let mut offset = 1;
180
181                    while line_idx
182                        .checked_sub(offset)
183                        .is_some_and(|idx| comment_lines.contains(&idx))
184                    {
185                        offset += 1;
186                    }
187
188                    let mut effective_line_idx = line_idx - (offset - 1);
189                    if effective_line_idx == 0 {
190                        effective_line_idx = line_idx;
191                    }
192
193                    let line_start_idx = if effective_line_idx < line_starts.len() {
194                        *line_starts.get(&effective_line_idx).unwrap()
195                    } else {
196                        let (_, line_start) = line_starts.last().unwrap_or((&0, &0));
197                        *line_start
198                    };
199
200                    fix_point = forward_slice[line_start_idx].clone().into();
201                }
202
203                1
204            };
205
206            // Only create fixes if we have a valid fix point
207            let fixes = if let Some(anchor) = fix_point {
208                let newlines = std::iter::repeat_n(
209                    SegmentBuilder::newline(context.tables.next_id(), "\n"),
210                    num_newlines,
211                )
212                .collect_vec();
213
214                if is_replace {
215                    vec![LintFix::replace(anchor, newlines, None)]
216                } else {
217                    vec![LintFix::create_before(anchor, newlines)]
218                }
219            } else {
220                // Skip generating a fix if we don't have a valid anchor point
221                Vec::new()
222            };
223
224            error_buffer.push(LintResult::new(
225                forward_slice[seg_idx].clone().into(),
226                fixes,
227                None,
228                None,
229            ));
230        }
231
232        error_buffer
233    }
234
235    fn is_fix_compatible(&self) -> bool {
236        true
237    }
238
239    fn crawl_behaviour(&self) -> Crawler {
240        SegmentSeekerCrawler::new(const { SyntaxSet::new(&[SyntaxKind::WithCompoundStatement]) })
241            .into()
242    }
243}