Skip to main content

rigsql_rules/structure/
st08.rs

1use rigsql_core::{Segment, SegmentType};
2
3use crate::rule::{CrawlType, Rule, RuleContext, RuleGroup};
4use crate::violation::LintViolation;
5
6/// ST08: DISTINCT used with parentheses.
7///
8/// DISTINCT applies to the entire row, not a single column. Using
9/// parentheses after DISTINCT makes it look like a function call.
10#[derive(Debug, Default)]
11pub struct RuleST08;
12
13impl Rule for RuleST08 {
14    fn code(&self) -> &'static str {
15        "ST08"
16    }
17    fn name(&self) -> &'static str {
18        "structure.distinct"
19    }
20    fn description(&self) -> &'static str {
21        "DISTINCT used with parentheses is misleading."
22    }
23    fn explanation(&self) -> &'static str {
24        "DISTINCT is a keyword that applies to the entire SELECT result set, not a function. \
25         Writing SELECT DISTINCT(col) suggests DISTINCT operates on a single column like a \
26         function, which is misleading. Use SELECT DISTINCT col instead."
27    }
28    fn groups(&self) -> &[RuleGroup] {
29        &[RuleGroup::Structure]
30    }
31    fn is_fixable(&self) -> bool {
32        false
33    }
34
35    fn crawl_type(&self) -> CrawlType {
36        CrawlType::Segment(vec![SegmentType::SelectClause])
37    }
38
39    fn eval(&self, ctx: &RuleContext) -> Vec<LintViolation> {
40        let children = ctx.segment.children();
41
42        // Look for DISTINCT keyword followed by a ParenExpression
43        let mut found_distinct = false;
44        for child in children {
45            if child.segment_type().is_trivia() {
46                continue;
47            }
48
49            if found_distinct {
50                // The next non-trivia after DISTINCT
51                if child.segment_type() == SegmentType::ParenExpression {
52                    return vec![LintViolation::with_msg_key(
53                        self.code(),
54                        "DISTINCT used with parentheses is misleading. DISTINCT is not a function.",
55                        child.span(),
56                        "rules.ST08.msg",
57                        vec![],
58                    )];
59                }
60                // If it's not a paren expression, stop looking
61                found_distinct = false;
62            }
63
64            if let Segment::Token(t) = child {
65                if t.segment_type == SegmentType::Keyword
66                    && t.token.text.eq_ignore_ascii_case("DISTINCT")
67                {
68                    found_distinct = true;
69                }
70            }
71        }
72
73        vec![]
74    }
75}
76
77#[cfg(test)]
78mod tests {
79    use super::*;
80    use crate::test_utils::lint_sql;
81
82    #[test]
83    fn test_st08_accepts_distinct_without_parens() {
84        let violations = lint_sql("SELECT DISTINCT a, b FROM t;", RuleST08);
85        assert_eq!(violations.len(), 0);
86    }
87
88    #[test]
89    fn test_st08_accepts_no_distinct() {
90        let violations = lint_sql("SELECT a FROM t;", RuleST08);
91        assert_eq!(violations.len(), 0);
92    }
93}