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: {join_type}"
47 )
48 })?;
49 Ok(RuleAM05 {
50 fully_qualify_join_types: join_type,
51 }
52 .erased())
53 }
54 }
55 }
56
57 fn name(&self) -> &'static str {
58 "ambiguous.join"
59 }
60
61 fn description(&self) -> &'static str {
62 "Join clauses should be fully qualified."
63 }
64
65 fn long_description(&self) -> &'static str {
66 r#"
67By 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.
68
69* `fully_qualify_join_types`: Which types of JOIN clauses should be fully qualified? Must be one of `['inner', 'outer', 'both']`.
70
71**Anti-pattern**
72
73A join is used without specifying the kind of join.
74
75```sql
76SELECT
77 foo
78FROM bar
79JOIN baz;
80```
81
82**Best practice**
83
84Use `INNER JOIN` rather than `JOIN`.
85
86```sql
87SELECT
88 foo
89FROM bar
90INNER JOIN baz;
91```
92"#
93 }
94
95 fn groups(&self) -> &'static [RuleGroups] {
96 &[RuleGroups::All, RuleGroups::Ambiguous]
97 }
98
99 fn eval(&self, context: &RuleContext) -> Vec<LintResult> {
100 assert!(context.segment.is_type(SyntaxKind::JoinClause));
101
102 let join_clause_keywords = context
103 .segment
104 .segments()
105 .iter()
106 .filter(|segment| segment.is_type(SyntaxKind::Keyword))
107 .collect::<Vec<_>>();
108
109 if (self.fully_qualify_join_types == JoinType::Outer
111 || self.fully_qualify_join_types == JoinType::Both)
112 && ["RIGHT", "LEFT", "FULL"].contains(
113 &join_clause_keywords[0]
114 .raw()
115 .to_uppercase_smolstr()
116 .as_str(),
117 )
118 && join_clause_keywords[1].raw().eq_ignore_ascii_case("JOIN")
119 {
120 let outer_keyword = if join_clause_keywords[1].raw() == "JOIN" {
121 "OUTER"
122 } else {
123 "outer"
124 };
125 return vec![LintResult::new(
126 context.segment.segments()[0].clone().into(),
127 vec![LintFix::create_after(
128 context.segment.segments()[0].clone(),
129 vec![
130 SegmentBuilder::whitespace(context.tables.next_id(), " "),
131 SegmentBuilder::keyword(context.tables.next_id(), outer_keyword),
132 ],
133 None,
134 )],
135 None,
136 None,
137 )];
138 };
139
140 if (self.fully_qualify_join_types == JoinType::Inner
142 || self.fully_qualify_join_types == JoinType::Both)
143 && join_clause_keywords[0].raw().eq_ignore_ascii_case("JOIN")
144 {
145 let inner_keyword = if join_clause_keywords[0].raw() == "JOIN" {
146 "INNER"
147 } else {
148 "inner"
149 };
150 return vec![LintResult::new(
151 context.segment.segments()[0].clone().into(),
152 vec![LintFix::create_before(
153 context.segment.segments()[0].clone(),
154 vec![
155 SegmentBuilder::keyword(context.tables.next_id(), inner_keyword),
156 SegmentBuilder::whitespace(context.tables.next_id(), " "),
157 ],
158 )],
159 None,
160 None,
161 )];
162 }
163 vec![]
164 }
165
166 fn is_fix_compatible(&self) -> bool {
167 true
168 }
169
170 fn crawl_behaviour(&self) -> Crawler {
171 SegmentSeekerCrawler::new(const { SyntaxSet::new(&[SyntaxKind::JoinClause]) }).into()
172 }
173}