Skip to main content

sqruff_lib/rules/structure/
st12.rs

1use hashbrown::HashMap;
2use sqruff_lib_core::dialects::syntax::SyntaxKind;
3use sqruff_lib_core::lint_fix::LintFix;
4
5use crate::core::config::Value;
6use crate::core::rules::context::RuleContext;
7use crate::core::rules::crawlers::{Crawler, RootOnlyCrawler};
8use crate::core::rules::{Erased, ErasedRule, LintResult, Rule, RuleGroups};
9
10#[derive(Debug, Default, Clone)]
11pub struct RuleST12;
12
13fn is_semicolon(kind: SyntaxKind) -> bool {
14    matches!(
15        kind,
16        SyntaxKind::StatementTerminator | SyntaxKind::Semicolon
17    )
18}
19
20impl Rule for RuleST12 {
21    fn load_from_config(&self, _config: &HashMap<String, Value>) -> Result<ErasedRule, String> {
22        Ok(RuleST12.erased())
23    }
24
25    fn name(&self) -> &'static str {
26        "structure.consecutive_semicolons"
27    }
28
29    fn description(&self) -> &'static str {
30        "Remove consecutive semicolons."
31    }
32
33    fn long_description(&self) -> &'static str {
34        r#"
35**Anti-pattern**
36
37Multiple semicolons in a row, with only whitespace between them.
38
39```sql
40SELECT 1;;
41```
42
43**Best practice**
44
45Use only a single semicolon.
46
47```sql
48SELECT 1;
49```
50"#
51    }
52
53    fn groups(&self) -> &'static [RuleGroups] {
54        &[RuleGroups::All, RuleGroups::Structure]
55    }
56
57    fn eval(&self, context: &RuleContext) -> Vec<LintResult> {
58        let all_segments: Vec<_> = context
59            .segment
60            .recursive_crawl_all(false)
61            .into_iter()
62            .filter(|seg| seg.segments().is_empty())
63            .collect();
64
65        let mut results = Vec::new();
66        let mut i = 0;
67
68        while i < all_segments.len() {
69            if !is_semicolon(all_segments[i].get_type()) {
70                i += 1;
71                continue;
72            }
73
74            let first_term = i;
75            i += 1;
76
77            let mut fixes = Vec::new();
78            loop {
79                let ws_start = i;
80                while i < all_segments.len()
81                    && matches!(
82                        all_segments[i].get_type(),
83                        SyntaxKind::Whitespace
84                            | SyntaxKind::Newline
85                            | SyntaxKind::Indent
86                            | SyntaxKind::Dedent
87                    )
88                {
89                    i += 1;
90                }
91
92                if i < all_segments.len() && is_semicolon(all_segments[i].get_type()) {
93                    for seg in &all_segments[ws_start..i] {
94                        if !seg.is_meta() {
95                            fixes.push(LintFix::delete(seg.clone()));
96                        }
97                    }
98                    fixes.push(LintFix::delete(all_segments[i].clone()));
99                    i += 1;
100                } else {
101                    break;
102                }
103            }
104
105            if !fixes.is_empty() {
106                results.push(LintResult::new(
107                    all_segments[first_term].clone().into(),
108                    fixes,
109                    None,
110                    None,
111                ));
112            }
113        }
114
115        results
116    }
117
118    fn is_fix_compatible(&self) -> bool {
119        true
120    }
121
122    fn crawl_behaviour(&self) -> Crawler {
123        RootOnlyCrawler.into()
124    }
125}