sqruff_lib/rules/structure/
st12.rs1use 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}