sqruff_lib/rules/ambiguous/
am05.rs1use std::str::FromStr;
2
3use ahash::AHashMap;
4use smol_str::StrExt;
5use sqruff_lib_core::dialects::syntax::{SyntaxKind, SyntaxSet};
6use sqruff_lib_core::lint_fix::LintFix;
7use sqruff_lib_core::parser::segments::SegmentBuilder;
8use strum_macros::{AsRefStr, EnumString};
9
10use crate::core::config::Value;
11use crate::core::rules::context::RuleContext;
12use crate::core::rules::crawlers::{Crawler, SegmentSeekerCrawler};
13use crate::core::rules::{Erased, ErasedRule, LintResult, Rule, RuleGroups};
14
15#[derive(Clone, Debug)]
16pub struct RuleAM05 {
17 fully_qualify_join_types: JoinType,
18}
19
20#[derive(Clone, Debug, Copy, PartialEq, Eq, Hash, AsRefStr, EnumString)]
21#[strum(serialize_all = "lowercase")]
22enum JoinType {
23 Inner,
24 Outer,
25 Both,
26}
27
28impl Default for RuleAM05 {
29 fn default() -> Self {
30 Self {
31 fully_qualify_join_types: JoinType::Inner,
32 }
33 }
34}
35
36impl Rule for RuleAM05 {
37 fn load_from_config(&self, config: &AHashMap<String, Value>) -> Result<ErasedRule, String> {
38 let fully_qualify_join_types = config["fully_qualify_join_types"].as_string();
39 match fully_qualify_join_types {
41 None => Err("Rule AM05 expects a `fully_qualify_join_types` array".to_string()),
42 Some(join_type) => {
43 let join_type = JoinType::from_str(join_type).map_err(|_| {
44 format!(
45 "Rule AM05 expects a `fully_qualify_join_types` array of valid join \
46 types. Got: {}",
47 join_type
48 )
49 })?;
50 Ok(RuleAM05 {
51 fully_qualify_join_types: join_type,
52 }
53 .erased())
54 }
55 }
56 }
57
58 fn name(&self) -> &'static str {
59 "ambiguous.join"
60 }
61
62 fn description(&self) -> &'static str {
63 "Join clauses should be fully qualified."
64 }
65
66 fn long_description(&self) -> &'static str {
67 r#"
68By default this rule is configured to enforce fully qualified `INNER JOIN` clauses, but not `[LEFT/RIGHT/FULL] OUTER JOIN`. If you prefer a stricter lint then this is configurable.
69
70* `fully_qualify_join_types`: Which types of JOIN clauses should be fully qualified? Must be one of `['inner', 'outer', 'both']`.
71
72**Anti-pattern**
73
74A join is used without specifying the kind of join.
75
76```sql
77SELECT
78 foo
79FROM bar
80JOIN baz;
81```
82
83**Best practice**
84
85Use `INNER JOIN` rather than `JOIN`.
86
87```sql
88SELECT
89 foo
90FROM bar
91INNER JOIN baz;
92```
93"#
94 }
95
96 fn groups(&self) -> &'static [RuleGroups] {
97 &[RuleGroups::All, RuleGroups::Ambiguous]
98 }
99
100 fn eval(&self, context: &RuleContext) -> Vec<LintResult> {
101 assert!(context.segment.is_type(SyntaxKind::JoinClause));
102
103 let join_clause_keywords = context
104 .segment
105 .segments()
106 .iter()
107 .filter(|segment| segment.is_type(SyntaxKind::Keyword))
108 .collect::<Vec<_>>();
109
110 if (self.fully_qualify_join_types == JoinType::Outer
112 || self.fully_qualify_join_types == JoinType::Both)
113 && ["RIGHT", "LEFT", "FULL"].contains(
114 &join_clause_keywords[0]
115 .raw()
116 .to_uppercase_smolstr()
117 .as_str(),
118 )
119 && join_clause_keywords[1].raw().eq_ignore_ascii_case("JOIN")
120 {
121 let outer_keyword = if join_clause_keywords[1].raw() == "JOIN" {
122 "OUTER"
123 } else {
124 "outer"
125 };
126 return vec![LintResult::new(
127 context.segment.segments()[0].clone().into(),
128 vec![LintFix::create_after(
129 context.segment.segments()[0].clone(),
130 vec![
131 SegmentBuilder::whitespace(context.tables.next_id(), " "),
132 SegmentBuilder::keyword(context.tables.next_id(), outer_keyword),
133 ],
134 None,
135 )],
136 None,
137 None,
138 )];
139 };
140
141 if (self.fully_qualify_join_types == JoinType::Inner
143 || self.fully_qualify_join_types == JoinType::Both)
144 && join_clause_keywords[0].raw().eq_ignore_ascii_case("JOIN")
145 {
146 let inner_keyword = if join_clause_keywords[0].raw() == "JOIN" {
147 "INNER"
148 } else {
149 "inner"
150 };
151 return vec![LintResult::new(
152 context.segment.segments()[0].clone().into(),
153 vec![LintFix::create_before(
154 context.segment.segments()[0].clone(),
155 vec![
156 SegmentBuilder::keyword(context.tables.next_id(), inner_keyword),
157 SegmentBuilder::whitespace(context.tables.next_id(), " "),
158 ],
159 )],
160 None,
161 None,
162 )];
163 }
164 vec![]
165 }
166
167 fn is_fix_compatible(&self) -> bool {
168 true
169 }
170
171 fn crawl_behaviour(&self) -> Crawler {
172 SegmentSeekerCrawler::new(const { SyntaxSet::new(&[SyntaxKind::JoinClause]) }).into()
173 }
174}