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, SourceEdit};
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            // Find the end of the last non-trivia child for insertion point
57            let insert_pos = children
58                .iter()
59                .rev()
60                .find(|s| !s.segment_type().is_trivia())
61                .map(|s| s.span().end)
62                .unwrap_or(span.end);
63            return vec![LintViolation::with_fix_and_msg_key(
64                self.code(),
65                "Statement is not terminated with a semicolon.",
66                rigsql_core::Span::new(span.end, span.end),
67                vec![SourceEdit::insert(insert_pos, ";")],
68                "rules.CV06.msg",
69                vec![],
70            )];
71        }
72
73        vec![]
74    }
75}
76
77#[cfg(test)]
78mod tests {
79    use super::*;
80    use crate::test_utils::{lint_sql, lint_sql_with_dialect};
81
82    #[test]
83    fn test_cv06_flags_missing_semicolon() {
84        let violations = lint_sql("SELECT 1", RuleCV06);
85        assert_eq!(violations.len(), 1);
86        assert_eq!(violations[0].fixes.len(), 1);
87        assert_eq!(violations[0].fixes[0].new_text, ";");
88    }
89
90    #[test]
91    fn test_cv06_accepts_semicolon() {
92        let violations = lint_sql("SELECT 1;", RuleCV06);
93        assert_eq!(violations.len(), 0);
94    }
95
96    #[test]
97    fn test_cv06_skips_tsql() {
98        let violations = lint_sql_with_dialect("SELECT 1", RuleCV06, "tsql");
99        assert_eq!(violations.len(), 0);
100    }
101}