sqruff_lib/rules/convention/
cv07.rs

1use ahash::AHashMap;
2use sqruff_lib_core::dialects::syntax::SyntaxKind;
3use sqruff_lib_core::lint_fix::LintFix;
4use sqruff_lib_core::parser::segments::base::ErasedSegment;
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, RootOnlyCrawler};
10
11#[derive(Default, Clone, Debug)]
12pub struct RuleCV07;
13
14impl Rule for RuleCV07 {
15    fn load_from_config(&self, _config: &AHashMap<String, Value>) -> Result<ErasedRule, String> {
16        Ok(RuleCV07.erased())
17    }
18
19    fn name(&self) -> &'static str {
20        "convention.statement_brackets"
21    }
22
23    fn description(&self) -> &'static str {
24        "Top-level statements should not be wrapped in brackets."
25    }
26
27    fn long_description(&self) -> &'static str {
28        r#"
29**Anti-pattern**
30
31A top-level statement is wrapped in brackets.
32
33```sql
34 (SELECT
35     foo
36 FROM bar)
37
38 -- This also applies to statements containing a sub-query.
39
40 (SELECT
41     foo
42 FROM (SELECT * FROM bar))
43```
44
45**Best practice**
46
47Don’t wrap top-level statements in brackets.
48
49```sql
50 SELECT
51     foo
52 FROM bar
53
54 -- Likewise for statements containing a sub-query.
55
56 SELECT
57     foo
58 FROM (SELECT * FROM bar)
59```
60"#
61    }
62
63    fn groups(&self) -> &'static [RuleGroups] {
64        &[RuleGroups::All, RuleGroups::Convention]
65    }
66
67    fn eval(&self, context: &RuleContext) -> Vec<LintResult> {
68        let mut results = vec![];
69
70        for (parent, bracketed_segment) in Self::iter_bracketed_statements(context.segment.clone())
71        {
72            let bracket_set = [SyntaxKind::StartBracket, SyntaxKind::EndBracket];
73
74            let mut filtered_children: Vec<ErasedSegment> = bracketed_segment
75                .segments()
76                .iter()
77                .filter(|&segment| !bracket_set.contains(&segment.get_type()) && !segment.is_meta())
78                .cloned()
79                .collect();
80
81            // Lift leading/trailing whitespace and inline comments to the
82            // segment above. This avoids introducing a parse error (ANSI and other
83            // dialects generally don't allow this at lower levels of the parse
84            // tree).
85            let to_lift_predicate = |segment: &ErasedSegment| {
86                segment.is_type(SyntaxKind::Whitespace)
87                    || segment.is_type(SyntaxKind::InlineComment)
88            };
89            let leading = filtered_children
90                .clone()
91                .into_iter()
92                .take_while(to_lift_predicate)
93                .collect::<Vec<_>>();
94            let trailing = filtered_children
95                .clone()
96                .into_iter()
97                .rev()
98                .take_while(to_lift_predicate)
99                .collect::<Vec<_>>();
100
101            let lift_nodes = leading
102                .iter()
103                .chain(trailing.iter())
104                .cloned()
105                .collect::<Vec<_>>();
106            let mut fixes = vec![];
107            if !lift_nodes.is_empty() {
108                fixes.push(LintFix::create_before(parent.clone(), leading.clone()));
109                fixes.push(LintFix::create_after(parent, trailing.clone(), None));
110                fixes.extend(lift_nodes.into_iter().map(LintFix::delete));
111                filtered_children = filtered_children
112                    [leading.len()..filtered_children.len() - trailing.len()]
113                    .into();
114            }
115
116            fixes.push(LintFix::replace(
117                bracketed_segment.clone(),
118                filtered_children,
119                None,
120            ));
121
122            results.push(LintResult::new(Some(bracketed_segment), fixes, None, None))
123        }
124        results
125    }
126
127    fn is_fix_compatible(&self) -> bool {
128        true
129    }
130
131    fn crawl_behaviour(&self) -> Crawler {
132        RootOnlyCrawler.into()
133    }
134}
135
136impl RuleCV07 {
137    fn iter_statements(file_segment: ErasedSegment) -> Vec<ErasedSegment> {
138        file_segment
139            .segments()
140            .iter()
141            .filter_map(|seg| {
142                if seg.is_type(SyntaxKind::Batch) {
143                    Some(
144                        seg.segments()
145                            .iter()
146                            .filter(|seg| seg.is_type(SyntaxKind::Statement))
147                            .cloned()
148                            .collect::<Vec<_>>(),
149                    )
150                } else if seg.is_type(SyntaxKind::Statement) {
151                    Some(vec![seg.clone()])
152                } else {
153                    None
154                }
155            })
156            .flatten()
157            .collect()
158    }
159
160    fn iter_bracketed_statements(
161        file_segment: ErasedSegment,
162    ) -> Vec<(ErasedSegment, ErasedSegment)> {
163        Self::iter_statements(file_segment)
164            .into_iter()
165            .flat_map(|stmt| {
166                stmt.segments()
167                    .iter()
168                    .filter_map(|seg| {
169                        if seg.is_type(SyntaxKind::Bracketed) {
170                            Some((stmt.clone(), seg.clone()))
171                        } else {
172                            None
173                        }
174                    })
175                    .collect::<Vec<_>>()
176            })
177            .collect()
178    }
179}