Skip to main content

rigsql_rules/convention/
cv12.rs

1use rigsql_core::SegmentType;
2
3use crate::rule::{CrawlType, Rule, RuleContext, RuleGroup};
4use crate::violation::LintViolation;
5
6/// CV12: Use JOIN … ON … instead of WHERE … for join conditions.
7///
8/// When FROM has comma-separated tables and WHERE contains join conditions,
9/// prefer explicit JOIN syntax.
10#[derive(Debug, Default)]
11pub struct RuleCV12;
12
13impl Rule for RuleCV12 {
14    fn code(&self) -> &'static str {
15        "CV12"
16    }
17    fn name(&self) -> &'static str {
18        "convention.join_condition"
19    }
20    fn description(&self) -> &'static str {
21        "Use JOIN … ON … instead of implicit join in WHERE."
22    }
23    fn explanation(&self) -> &'static str {
24        "Using comma-separated tables in FROM with join conditions in WHERE (implicit join) \
25         mixes join logic with filtering. Use explicit JOIN … ON … syntax to separate join \
26         conditions from filter conditions, improving readability and maintainability."
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::Segment(vec![SegmentType::SelectStatement])
37    }
38
39    fn eval(&self, ctx: &RuleContext) -> Vec<LintViolation> {
40        let children = ctx.segment.children();
41
42        // Find FROM clause
43        let from_clause = children
44            .iter()
45            .find(|c| c.segment_type() == SegmentType::FromClause);
46        let where_clause = children
47            .iter()
48            .find(|c| c.segment_type() == SegmentType::WhereClause);
49
50        let (Some(from), Some(where_seg)) = (from_clause, where_clause) else {
51            return vec![];
52        };
53
54        // Check if FROM has comma-separated tables (contains Comma)
55        let has_comma = from
56            .children()
57            .iter()
58            .any(|c| c.segment_type() == SegmentType::Comma);
59
60        if !has_comma {
61            return vec![];
62        }
63
64        // FROM has comma-separated tables + WHERE exists → implicit join
65        vec![LintViolation::with_msg_key(
66            self.code(),
67            "Use explicit JOIN … ON … instead of comma-separated tables with WHERE.",
68            where_seg.span(),
69            "rules.CV12.msg",
70            vec![],
71        )]
72    }
73}
74
75#[cfg(test)]
76mod tests {
77    use super::*;
78    use crate::test_utils::lint_sql;
79
80    #[test]
81    fn test_cv12_flags_implicit_join() {
82        let violations = lint_sql("SELECT * FROM a, b WHERE a.id = b.id", RuleCV12);
83        assert_eq!(violations.len(), 1);
84    }
85
86    #[test]
87    fn test_cv12_accepts_explicit_join() {
88        let violations = lint_sql("SELECT * FROM a JOIN b ON a.id = b.id", RuleCV12);
89        assert_eq!(violations.len(), 0);
90    }
91
92    #[test]
93    fn test_cv12_accepts_single_table_where() {
94        let violations = lint_sql("SELECT * FROM t WHERE x = 1", RuleCV12);
95        assert_eq!(violations.len(), 0);
96    }
97}