Skip to main content

rigsql_rules/structure/
st12.rs

1use rigsql_core::SegmentType;
2
3use crate::rule::{CrawlType, Rule, RuleContext, RuleGroup};
4use crate::violation::LintViolation;
5
6/// ST12: Consecutive semicolons indicate empty statements.
7///
8/// Scans the entire file for semicolons separated only by whitespace/newlines.
9#[derive(Debug, Default)]
10pub struct RuleST12;
11
12impl Rule for RuleST12 {
13    fn code(&self) -> &'static str {
14        "ST12"
15    }
16    fn name(&self) -> &'static str {
17        "structure.consecutive_semicolons"
18    }
19    fn description(&self) -> &'static str {
20        "Consecutive semicolons indicate empty statements."
21    }
22    fn explanation(&self) -> &'static str {
23        "Multiple consecutive semicolons with only whitespace between them indicate \
24         empty statements, which are likely unintentional. Remove the extra semicolons."
25    }
26    fn groups(&self) -> &[RuleGroup] {
27        &[RuleGroup::Structure]
28    }
29    fn is_fixable(&self) -> bool {
30        false
31    }
32
33    fn crawl_type(&self) -> CrawlType {
34        CrawlType::RootOnly
35    }
36
37    fn eval(&self, ctx: &RuleContext) -> Vec<LintViolation> {
38        let mut violations = Vec::new();
39        let mut last_semicolon_span: Option<rigsql_core::Span> = None;
40        let mut only_trivia_since_last = true;
41
42        ctx.segment.walk(&mut |seg| {
43            let st = seg.segment_type();
44            if st == SegmentType::Semicolon {
45                if only_trivia_since_last && last_semicolon_span.is_some() {
46                    violations.push(LintViolation::new(
47                        self.code(),
48                        "Consecutive semicolons found (empty statement).",
49                        seg.span(),
50                    ));
51                }
52                last_semicolon_span = Some(seg.span());
53                only_trivia_since_last = true;
54            } else if !st.is_trivia()
55                && st != SegmentType::File
56                && st != SegmentType::Statement
57                && st != SegmentType::Unparsable
58            {
59                only_trivia_since_last = false;
60            }
61        });
62
63        violations
64    }
65}
66
67#[cfg(test)]
68mod tests {
69    use super::*;
70    use crate::test_utils::lint_sql;
71
72    #[test]
73    fn test_st12_flags_consecutive_semicolons() {
74        let violations = lint_sql("SELECT 1;;", RuleST12);
75        assert_eq!(violations.len(), 1);
76        assert!(violations[0].message.contains("Consecutive"));
77    }
78
79    #[test]
80    fn test_st12_accepts_single_semicolon() {
81        let violations = lint_sql("SELECT 1;", RuleST12);
82        assert_eq!(violations.len(), 0);
83    }
84
85    #[test]
86    fn test_st12_accepts_separate_statements() {
87        let violations = lint_sql("SELECT 1; SELECT 2;", RuleST12);
88        assert_eq!(violations.len(), 0);
89    }
90}