sqruff_lib/rules/ambiguous/
am08.rs1use 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 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 let keywords: Vec<_> = children
77 .iter()
78 .filter(|s| s.is_type(SyntaxKind::Keyword))
79 .collect();
80
81 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 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 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}