sqruff_lib/rules/layout/
lt07.rs

1use ahash::{AHashMap, AHashSet};
2use sqruff_lib_core::dialects::syntax::{SyntaxKind, SyntaxSet};
3use sqruff_lib_core::lint_fix::LintFix;
4use sqruff_lib_core::parser::segments::base::{ErasedSegment, SegmentBuilder};
5
6use crate::core::config::Value;
7use crate::core::rules::base::{Erased, ErasedRule, LintResult, Rule, RuleGroups};
8use crate::core::rules::context::RuleContext;
9use crate::core::rules::crawlers::{Crawler, SegmentSeekerCrawler};
10use crate::utils::functional::context::FunctionalContext;
11
12#[derive(Debug, Default, Clone)]
13pub struct RuleLT07;
14
15impl Rule for RuleLT07 {
16    fn load_from_config(&self, _config: &AHashMap<String, Value>) -> Result<ErasedRule, String> {
17        Ok(RuleLT07.erased())
18    }
19    fn name(&self) -> &'static str {
20        "layout.cte_bracket"
21    }
22
23    fn description(&self) -> &'static str {
24        "'WITH' clause closing bracket should be on a new line."
25    }
26
27    fn long_description(&self) -> &'static str {
28        r#"
29**Anti-pattern**
30
31In this example, the closing bracket is on the same line as CTE.
32
33```sql
34 WITH zoo AS (
35     SELECT a FROM foo)
36
37 SELECT * FROM zoo
38```
39
40**Best practice**
41
42Move the closing bracket on a new line.
43
44```sql
45WITH zoo AS (
46    SELECT a FROM foo
47)
48
49SELECT * FROM zoo
50```
51"#
52    }
53
54    fn groups(&self) -> &'static [RuleGroups] {
55        &[RuleGroups::All, RuleGroups::Core, RuleGroups::Layout]
56    }
57    fn eval(&self, context: &RuleContext) -> Vec<LintResult> {
58        let segments = FunctionalContext::new(context)
59            .segment()
60            .children(Some(|seg| seg.is_type(SyntaxKind::CommonTableExpression)));
61
62        let mut cte_end_brackets = AHashSet::new();
63        for cte in segments.iterate_segments() {
64            let cte_start_bracket = cte
65                .children(None)
66                .find_last(Some(|seg| seg.is_type(SyntaxKind::Bracketed)))
67                .children(None)
68                .find_first(Some(|seg: &ErasedSegment| {
69                    seg.is_type(SyntaxKind::StartBracket)
70                }));
71
72            let cte_end_bracket = cte
73                .children(None)
74                .find_last(Some(|seg| seg.is_type(SyntaxKind::Bracketed)))
75                .children(None)
76                .find_first(Some(|seg: &ErasedSegment| {
77                    seg.is_type(SyntaxKind::EndBracket)
78                }));
79
80            if !cte_start_bracket.is_empty() && !cte_end_bracket.is_empty() {
81                if cte_start_bracket[0]
82                    .get_position_marker()
83                    .unwrap()
84                    .line_no()
85                    == cte_end_bracket[0].get_position_marker().unwrap().line_no()
86                {
87                    continue;
88                }
89                cte_end_brackets.insert(cte_end_bracket[0].clone());
90            }
91        }
92
93        for seg in cte_end_brackets {
94            let mut contains_non_whitespace = false;
95            let idx = context
96                .segment
97                .get_raw_segments()
98                .iter()
99                .position(|it| it == &seg)
100                .unwrap();
101            if idx > 0 {
102                for elem in context.segment.get_raw_segments()[..idx].iter().rev() {
103                    if elem.is_type(SyntaxKind::Newline) {
104                        break;
105                    } else if !(matches!(
106                        elem.get_type(),
107                        SyntaxKind::Indent | SyntaxKind::Implicit
108                    ) || elem.is_type(SyntaxKind::Dedent)
109                        || elem.is_type(SyntaxKind::Whitespace))
110                    {
111                        contains_non_whitespace = true;
112                        break;
113                    }
114                }
115            }
116
117            if contains_non_whitespace {
118                return vec![LintResult::new(
119                    seg.clone().into(),
120                    vec![LintFix::create_before(
121                        seg,
122                        vec![SegmentBuilder::newline(context.tables.next_id(), "\n")],
123                    )],
124                    None,
125                    None,
126                )];
127            }
128        }
129
130        Vec::new()
131    }
132
133    fn is_fix_compatible(&self) -> bool {
134        false
135    }
136
137    fn crawl_behaviour(&self) -> Crawler {
138        SegmentSeekerCrawler::new(const { SyntaxSet::new(&[SyntaxKind::WithCompoundStatement]) })
139            .into()
140    }
141}