rigsql_rules/ambiguous/
am05.rs1use rigsql_core::{Segment, SegmentType};
2
3use crate::rule::{CrawlType, Rule, RuleContext, RuleGroup};
4use crate::violation::LintViolation;
5
6#[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 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 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}