Skip to main content

sqruff_lib/rules/ambiguous/
am08.rs

1use hashbrown::HashMap;
2use sqruff_lib_core::dialects::syntax::{SyntaxKind, SyntaxSet};
3use sqruff_lib_core::lint_fix::LintFix;
4use sqruff_lib_core::parser::segments::SegmentBuilder;
5
6use crate::core::config::Value;
7use crate::core::rules::context::RuleContext;
8use crate::core::rules::crawlers::{Crawler, SegmentSeekerCrawler};
9use crate::core::rules::{Erased, ErasedRule, LintResult, Rule, RuleGroups};
10
11#[derive(Debug, Clone)]
12pub struct RuleAM08;
13
14impl Rule for RuleAM08 {
15    fn load_from_config(&self, _config: &HashMap<String, Value>) -> Result<ErasedRule, String> {
16        Ok(RuleAM08.erased())
17    }
18
19    fn name(&self) -> &'static str {
20        "ambiguous.join_condition"
21    }
22
23    fn description(&self) -> &'static str {
24        "Implicit cross join detected."
25    }
26
27    fn long_description(&self) -> &'static str {
28        r#"
29**Anti-pattern**
30
31Cross joins are valid, but rare in the wild - and more often created by mistake than on purpose. This rule catches situations where a cross join has been specified, but not explicitly and so the risk of a mistaken cross join is highly likely.
32
33```sql
34SELECT
35    foo
36FROM bar
37JOIN baz;
38```
39
40**Best practice**
41
42Use `CROSS JOIN`.
43
44```sql
45SELECT
46    foo
47FROM bar
48CROSS JOIN baz;
49```
50"#
51    }
52
53    fn groups(&self) -> &'static [RuleGroups] {
54        &[RuleGroups::All, RuleGroups::Ambiguous]
55    }
56
57    fn eval(&self, context: &RuleContext) -> Vec<LintResult> {
58        assert!(context.segment.is_type(SyntaxKind::JoinClause));
59
60        let children = context.segment.segments();
61
62        // Check if this join has an ON condition or USING clause.
63        // In the ANSI dialect, USING is parsed as a keyword within the join
64        // clause rather than a separate UsingClause node.
65        let has_condition = children.iter().any(|s| {
66            s.is_type(SyntaxKind::JoinOnCondition)
67                || s.is_type(SyntaxKind::UsingClause)
68                || s.is_keyword("USING")
69        });
70
71        if has_condition {
72            return Vec::new();
73        }
74
75        // Collect the keywords in the join clause.
76        let keywords: Vec<_> = children
77            .iter()
78            .filter(|s| s.is_type(SyntaxKind::Keyword))
79            .collect();
80
81        // If it's already an explicit CROSS JOIN or a NATURAL JOIN, that's fine.
82        if keywords.iter().any(|k| {
83            k.raw().eq_ignore_ascii_case("CROSS") || k.raw().eq_ignore_ascii_case("NATURAL")
84        }) {
85            return Vec::new();
86        }
87
88        // Find the JOIN keyword to anchor the fix.
89        let join_keyword = keywords
90            .iter()
91            .find(|k| k.raw().eq_ignore_ascii_case("JOIN"));
92
93        let Some(join_kw) = join_keyword else {
94            return Vec::new();
95        };
96
97        // Determine case to match existing style.
98        let cross_keyword = if join_kw.raw() == "JOIN" || join_kw.raw() == "Join" {
99            "CROSS"
100        } else {
101            "cross"
102        };
103
104        vec![LintResult::new(
105            Some((*join_kw).clone()),
106            vec![LintFix::create_before(
107                (*join_kw).clone(),
108                vec![
109                    SegmentBuilder::keyword(context.tables.next_id(), cross_keyword),
110                    SegmentBuilder::whitespace(context.tables.next_id(), " "),
111                ],
112            )],
113            None,
114            None,
115        )]
116    }
117
118    fn is_fix_compatible(&self) -> bool {
119        true
120    }
121
122    fn crawl_behaviour(&self) -> Crawler {
123        SegmentSeekerCrawler::new(const { SyntaxSet::new(&[SyntaxKind::JoinClause]) }).into()
124    }
125}