Skip to main content

rigsql_rules/ambiguous/
am05.rs

1use rigsql_core::{Segment, SegmentType};
2
3use crate::rule::{CrawlType, Rule, RuleContext, RuleGroup};
4use crate::violation::LintViolation;
5
6/// AM05: JOIN without qualifier (INNER/LEFT/RIGHT/FULL/CROSS).
7///
8/// A bare JOIN is implicitly an INNER JOIN, but this should be explicit.
9#[derive(Debug, Default)]
10pub struct RuleAM05;
11
12impl Rule for RuleAM05 {
13    fn code(&self) -> &'static str {
14        "AM05"
15    }
16    fn name(&self) -> &'static str {
17        "ambiguous.join"
18    }
19    fn description(&self) -> &'static str {
20        "JOIN without qualifier."
21    }
22    fn explanation(&self) -> &'static str {
23        "A bare JOIN keyword without a qualifier (INNER, LEFT, RIGHT, FULL, CROSS, NATURAL) \
24         implicitly means INNER JOIN. Making the join type explicit improves readability \
25         and makes the query's intent clear."
26    }
27    fn groups(&self) -> &[RuleGroup] {
28        &[RuleGroup::Ambiguous]
29    }
30    fn is_fixable(&self) -> bool {
31        false
32    }
33
34    fn crawl_type(&self) -> CrawlType {
35        CrawlType::Segment(vec![SegmentType::JoinClause])
36    }
37
38    fn eval(&self, ctx: &RuleContext) -> Vec<LintViolation> {
39        let children = ctx.segment.children();
40
41        // Look for the JOIN keyword
42        let join_keyword = children.iter().find(|c| {
43            if let Segment::Token(t) = c {
44                t.segment_type == SegmentType::Keyword && t.token.text.eq_ignore_ascii_case("JOIN")
45            } else {
46                false
47            }
48        });
49
50        let join_kw = match join_keyword {
51            Some(kw) => kw,
52            None => return vec![],
53        };
54
55        // Check if there's a qualifier keyword BEFORE the JOIN keyword
56        let qualifiers = [
57            "INNER", "LEFT", "RIGHT", "FULL", "CROSS", "NATURAL", "OUTER",
58        ];
59
60        let has_qualifier = children
61            .iter()
62            .take_while(|c| !std::ptr::eq(*c, join_kw))
63            .any(|c| {
64                if let Segment::Token(t) = c {
65                    t.segment_type == SegmentType::Keyword
66                        && qualifiers
67                            .iter()
68                            .any(|q| t.token.text.eq_ignore_ascii_case(q))
69                } else {
70                    false
71                }
72            });
73
74        if !has_qualifier {
75            return vec![LintViolation::new(
76                self.code(),
77                "JOIN without qualifier. Use INNER JOIN, LEFT JOIN, etc.",
78                join_kw.span(),
79            )];
80        }
81
82        vec![]
83    }
84}
85
86#[cfg(test)]
87mod tests {
88    use super::*;
89    use crate::test_utils::lint_sql;
90
91    #[test]
92    fn test_am05_flags_bare_join() {
93        let violations = lint_sql("SELECT a FROM t JOIN u ON t.id = u.id", RuleAM05);
94        assert_eq!(violations.len(), 1);
95    }
96
97    #[test]
98    fn test_am05_accepts_inner_join() {
99        let violations = lint_sql("SELECT a FROM t INNER JOIN u ON t.id = u.id", RuleAM05);
100        assert_eq!(violations.len(), 0);
101    }
102
103    #[test]
104    fn test_am05_accepts_left_join() {
105        let violations = lint_sql("SELECT a FROM t LEFT JOIN u ON t.id = u.id", RuleAM05);
106        assert_eq!(violations.len(), 0);
107    }
108}