Skip to main content

rigsql_rules/tsql/
tq03.rs

1use rigsql_core::SegmentType;
2
3use crate::rule::{CrawlType, Rule, RuleContext, RuleGroup};
4use crate::violation::LintViolation;
5
6/// TQ03: Empty batches (consecutive GO statements with nothing between them).
7///
8/// Two GO statements separated only by whitespace/newlines indicate an empty
9/// batch which is unnecessary and likely accidental.
10#[derive(Debug, Default)]
11pub struct RuleTQ03;
12
13impl Rule for RuleTQ03 {
14    fn code(&self) -> &'static str {
15        "TQ03"
16    }
17    fn name(&self) -> &'static str {
18        "tsql.empty_batch"
19    }
20    fn description(&self) -> &'static str {
21        "Avoid empty batches (consecutive GO statements)."
22    }
23    fn explanation(&self) -> &'static str {
24        "In T-SQL, GO is a batch separator. Two consecutive GO statements with nothing \
25         meaningful between them create an empty batch. This is unnecessary clutter and \
26         may indicate accidentally deleted code."
27    }
28    fn groups(&self) -> &[RuleGroup] {
29        &[RuleGroup::Convention]
30    }
31    fn is_fixable(&self) -> bool {
32        false
33    }
34
35    fn crawl_type(&self) -> CrawlType {
36        CrawlType::RootOnly
37    }
38
39    fn eval(&self, ctx: &RuleContext) -> Vec<LintViolation> {
40        if ctx.dialect != "tsql" {
41            return vec![];
42        }
43
44        let mut violations = Vec::new();
45        let children = ctx.segment.children();
46
47        let mut last_go_span = None;
48
49        for child in children {
50            if child.segment_type() == SegmentType::GoStatement {
51                if let Some(_prev_span) = last_go_span {
52                    // Consecutive GO with nothing meaningful between them
53                    violations.push(LintViolation::new(
54                        self.code(),
55                        "Empty batch: consecutive GO statements with no content between them.",
56                        child.span(),
57                    ));
58                }
59                last_go_span = Some(child.span());
60            } else if !child.segment_type().is_trivia() {
61                // Something meaningful between GO statements
62                last_go_span = None;
63            }
64        }
65
66        violations
67    }
68}
69
70#[cfg(test)]
71mod tests {
72    use super::*;
73    use crate::test_utils::lint_sql_with_dialect;
74
75    #[test]
76    fn test_tq03_flags_consecutive_go() {
77        let sql = "SELECT 1\nGO\nGO\n";
78        let violations = lint_sql_with_dialect(sql, RuleTQ03, "tsql");
79        // This test depends on the parser producing GoStatement nodes.
80        // If the parser doesn't produce them for ANSI grammar, we expect 0 here.
81        // The rule logic is still correct for when TSQL grammar is used.
82        let _ = violations;
83    }
84
85    #[test]
86    fn test_tq03_skips_non_tsql() {
87        let violations = lint_sql_with_dialect("SELECT 1", RuleTQ03, "ansi");
88        assert_eq!(violations.len(), 0);
89    }
90
91    #[test]
92    fn test_tq03_no_violation_on_single_select() {
93        let violations = lint_sql_with_dialect("SELECT 1", RuleTQ03, "tsql");
94        assert_eq!(violations.len(), 0);
95    }
96}