Skip to main content

rigsql_rules/convention/
cv06.rs

1use rigsql_core::SegmentType;
2
3use crate::rule::{CrawlType, Rule, RuleContext, RuleGroup};
4use crate::violation::LintViolation;
5
6/// CV06: Statements must end with a semicolon.
7#[derive(Debug, Default)]
8pub struct RuleCV06;
9
10impl Rule for RuleCV06 {
11    fn code(&self) -> &'static str {
12        "CV06"
13    }
14    fn name(&self) -> &'static str {
15        "convention.terminator"
16    }
17    fn description(&self) -> &'static str {
18        "Statements must end with a semicolon."
19    }
20    fn explanation(&self) -> &'static str {
21        "All SQL statements should be terminated with a semicolon. While some databases \
22         accept statements without terminators, including them is good practice for \
23         portability and clarity."
24    }
25    fn groups(&self) -> &[RuleGroup] {
26        &[RuleGroup::Convention]
27    }
28    fn is_fixable(&self) -> bool {
29        true
30    }
31
32    fn crawl_type(&self) -> CrawlType {
33        CrawlType::Segment(vec![SegmentType::Statement])
34    }
35
36    fn eval(&self, ctx: &RuleContext) -> Vec<LintViolation> {
37        // In TSQL, semicolons are optional in most contexts
38        if ctx.dialect == "tsql" {
39            return vec![];
40        }
41
42        let children = ctx.segment.children();
43        if children.is_empty() {
44            return vec![];
45        }
46
47        // Check if the last non-trivia child is a semicolon
48        let has_semicolon = children
49            .iter()
50            .rev()
51            .find(|s| !s.segment_type().is_trivia())
52            .is_some_and(|s| s.segment_type() == SegmentType::Semicolon);
53
54        if !has_semicolon {
55            let span = ctx.segment.span();
56            return vec![LintViolation::new(
57                self.code(),
58                "Statement is not terminated with a semicolon.",
59                rigsql_core::Span::new(span.end, span.end),
60            )];
61        }
62
63        vec![]
64    }
65}
66
67#[cfg(test)]
68mod tests {
69    use super::*;
70    use crate::test_utils::{lint_sql, lint_sql_with_dialect};
71
72    #[test]
73    fn test_cv06_flags_missing_semicolon() {
74        let violations = lint_sql("SELECT 1", RuleCV06);
75        assert_eq!(violations.len(), 1);
76    }
77
78    #[test]
79    fn test_cv06_accepts_semicolon() {
80        let violations = lint_sql("SELECT 1;", RuleCV06);
81        assert_eq!(violations.len(), 0);
82    }
83
84    #[test]
85    fn test_cv06_skips_tsql() {
86        let violations = lint_sql_with_dialect("SELECT 1", RuleCV06, "tsql");
87        assert_eq!(violations.len(), 0);
88    }
89}