Skip to main content

rigsql_rules/convention/
cv07.rs

1use rigsql_core::{Segment, SegmentType};
2
3use crate::rule::{CrawlType, Rule, RuleContext, RuleGroup};
4use crate::violation::{LintViolation, SourceEdit};
5
6/// CV07: Top-level statements should not be wrapped in brackets.
7///
8/// Parentheses around a top-level statement are unnecessary and reduce
9/// readability.
10#[derive(Debug, Default)]
11pub struct RuleCV07;
12
13impl Rule for RuleCV07 {
14    fn code(&self) -> &'static str {
15        "CV07"
16    }
17    fn name(&self) -> &'static str {
18        "convention.statement_brackets"
19    }
20    fn description(&self) -> &'static str {
21        "Top-level statements should not be wrapped in brackets."
22    }
23    fn explanation(&self) -> &'static str {
24        "Wrapping an entire statement in parentheses is unnecessary and can be \
25         confusing. Remove the outer brackets to improve readability."
26    }
27    fn groups(&self) -> &[RuleGroup] {
28        &[RuleGroup::Convention]
29    }
30    fn is_fixable(&self) -> bool {
31        true
32    }
33
34    fn crawl_type(&self) -> CrawlType {
35        CrawlType::Segment(vec![SegmentType::Statement])
36    }
37
38    fn eval(&self, ctx: &RuleContext) -> Vec<LintViolation> {
39        let children = ctx.segment.children();
40        let non_trivia: Vec<_> = children
41            .iter()
42            .filter(|c| !c.segment_type().is_trivia())
43            .collect();
44
45        if non_trivia.len() < 2 {
46            return vec![];
47        }
48
49        let first = non_trivia.first().unwrap();
50        let last_idx = non_trivia.len() - 1;
51        // Last non-trivia might be Semicolon, check the one before it
52        let (check_last, _has_semi) =
53            if non_trivia[last_idx].segment_type() == SegmentType::Semicolon && last_idx >= 2 {
54                (non_trivia[last_idx - 1], true)
55            } else {
56                (non_trivia[last_idx], false)
57            };
58
59        let is_lparen = first.segment_type() == SegmentType::LParen
60            || matches!(first, Segment::Token(t) if t.token.text.as_str() == "(");
61        let is_rparen = check_last.segment_type() == SegmentType::RParen
62            || matches!(check_last, Segment::Token(t) if t.token.text.as_str() == ")");
63
64        if is_lparen && is_rparen {
65            vec![LintViolation::with_fix_and_msg_key(
66                self.code(),
67                "Unnecessary brackets around statement.",
68                ctx.segment.span(),
69                vec![
70                    SourceEdit::delete(first.span()),
71                    SourceEdit::delete(check_last.span()),
72                ],
73                "rules.CV07.msg",
74                vec![],
75            )]
76        } else {
77            vec![]
78        }
79    }
80}
81
82#[cfg(test)]
83mod tests {
84    use super::*;
85    use crate::test_utils::lint_sql;
86
87    #[test]
88    fn test_cv07_accepts_normal_statement() {
89        let violations = lint_sql("SELECT 1", RuleCV07);
90        assert_eq!(violations.len(), 0);
91    }
92}