rigsql_rules/convention/
cv06.rs1use rigsql_core::SegmentType;
2
3use crate::rule::{CrawlType, Rule, RuleContext, RuleGroup};
4use crate::violation::{LintViolation, SourceEdit};
5
6#[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 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 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 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}