sqruff_lib/rules/aliasing/
al07.rs

1use std::iter::once;
2
3use ahash::{AHashMap, AHashSet};
4use itertools::chain;
5use smol_str::ToSmolStr;
6use sqruff_lib_core::dialects::syntax::{SyntaxKind, SyntaxSet};
7use sqruff_lib_core::lint_fix::LintFix;
8use sqruff_lib_core::parser::segments::base::{ErasedSegment, SegmentBuilder, Tables};
9
10use crate::core::config::Value;
11use crate::core::rules::base::{Erased, ErasedRule, LintResult, Rule, RuleGroups};
12use crate::core::rules::context::RuleContext;
13use crate::core::rules::crawlers::{Crawler, SegmentSeekerCrawler};
14use crate::utils::functional::context::FunctionalContext;
15
16#[derive(Debug)]
17struct TableAliasInfo {
18    table_ref: ErasedSegment,
19    whitespace_ref: Option<ErasedSegment>,
20    alias_exp_ref: ErasedSegment,
21    alias_identifier_ref: Option<ErasedSegment>,
22}
23
24#[derive(Debug, Clone, Default)]
25pub struct RuleAL07 {
26    force_enable: bool,
27}
28
29impl RuleAL07 {
30    fn lint_aliases_in_join(
31        &self,
32        tables: &Tables,
33        base_table: Option<ErasedSegment>,
34        from_expression_elements: Vec<ErasedSegment>,
35        column_reference_segments: Vec<ErasedSegment>,
36        segment: ErasedSegment,
37    ) -> Vec<LintResult> {
38        let mut violation_buff = Vec::new();
39        let to_check = self.filter_table_expressions(base_table, from_expression_elements);
40
41        let mut table_counts = AHashMap::new();
42        for ai in &to_check {
43            *table_counts
44                .entry(ai.table_ref.raw().to_smolstr())
45                .or_insert(0) += 1;
46        }
47
48        let mut table_aliases: AHashMap<_, AHashSet<_>> = AHashMap::new();
49        for ai in &to_check {
50            if let (table_ref, Some(alias_identifier_ref)) =
51                (&ai.table_ref, &ai.alias_identifier_ref)
52            {
53                table_aliases
54                    .entry(table_ref.raw().to_smolstr())
55                    .or_default()
56                    .insert(alias_identifier_ref.raw().to_smolstr());
57            }
58        }
59
60        for alias_info in to_check {
61            if let (table_ref, Some(alias_identifier_ref)) =
62                (&alias_info.table_ref, &alias_info.alias_identifier_ref)
63            {
64                // Skip processing if table appears more than once with different aliases
65                let raw_table = table_ref.raw().to_smolstr();
66                if table_counts.get(&raw_table).unwrap_or(&0) > &1
67                    && table_aliases
68                        .get(&raw_table)
69                        .is_some_and(|aliases| aliases.len() > 1)
70                {
71                    continue;
72                }
73
74                let select_clause = segment
75                    .child(const { &SyntaxSet::new(&[SyntaxKind::SelectClause]) })
76                    .unwrap();
77                let mut ids_refs = Vec::new();
78
79                let alias_name = alias_identifier_ref.raw();
80                if !alias_name.is_empty() {
81                    // Find all references to alias in select clause
82                    for alias_with_column in select_clause.recursive_crawl(
83                        const { &SyntaxSet::new(&[SyntaxKind::ObjectReference]) },
84                        true,
85                        &SyntaxSet::EMPTY,
86                        true,
87                    ) {
88                        if let Some(used_alias_ref) = alias_with_column.child(
89                            const {
90                                &SyntaxSet::new(&[
91                                    SyntaxKind::Identifier,
92                                    SyntaxKind::NakedIdentifier,
93                                ])
94                            },
95                        ) {
96                            if used_alias_ref.raw() == alias_name {
97                                ids_refs.push(used_alias_ref);
98                            }
99                        }
100                    }
101
102                    // Find all references to alias in column references
103                    for exp_ref in column_reference_segments.clone() {
104                        if let Some(used_alias_ref) = exp_ref.child(
105                            const {
106                                &SyntaxSet::new(&[
107                                    SyntaxKind::Identifier,
108                                    SyntaxKind::NakedIdentifier,
109                                ])
110                            },
111                        ) {
112                            if used_alias_ref.raw() == alias_name
113                                && exp_ref
114                                    .child(const { &SyntaxSet::new(&[SyntaxKind::Dot]) })
115                                    .is_some()
116                            {
117                                ids_refs.push(used_alias_ref);
118                            }
119                        }
120                    }
121                }
122
123                // Prepare fixes for deleting and editing references to aliased tables
124                let mut fixes = Vec::new();
125
126                fixes.push(LintFix::delete(alias_info.alias_exp_ref));
127
128                if let Some(whitespace_ref) = &alias_info.whitespace_ref {
129                    fixes.push(LintFix::delete(whitespace_ref.clone()));
130                }
131
132                for alias in ids_refs.iter().chain(once(alias_identifier_ref)) {
133                    let tmp = table_ref.raw();
134                    let identifier_parts: Vec<_> = tmp.split('.').collect();
135                    let mut edits = Vec::new();
136                    for (i, part) in identifier_parts.iter().enumerate() {
137                        if i > 0 {
138                            edits.push(SegmentBuilder::symbol(tables.next_id(), "."));
139                        }
140                        edits.push(
141                            SegmentBuilder::token(tables.next_id(), part, SyntaxKind::Identifier)
142                                .finish(),
143                        );
144                    }
145                    fixes.push(LintFix::replace(
146                        alias.clone(),
147                        edits,
148                        Some(vec![table_ref.clone()]),
149                    ));
150                }
151
152                violation_buff.push(LintResult::new(
153                    alias_info.alias_identifier_ref,
154                    fixes,
155                    "Avoid aliases in from clauses and join conditions."
156                        .to_owned()
157                        .into(),
158                    None,
159                ));
160            }
161        }
162
163        violation_buff
164    }
165
166    fn filter_table_expressions(
167        &self,
168        base_table: Option<ErasedSegment>,
169        from_expression_elements: Vec<ErasedSegment>,
170    ) -> Vec<TableAliasInfo> {
171        let mut acc = Vec::new();
172
173        for from_expression in from_expression_elements {
174            let table_expression =
175                from_expression.child(const { &SyntaxSet::new(&[SyntaxKind::TableExpression]) });
176            let Some(table_expression) = table_expression else {
177                continue;
178            };
179
180            let table_ref =
181                table_expression.child(const { &SyntaxSet::new(&[SyntaxKind::ObjectReference, SyntaxKind::TableReference]) });
182            let Some(table_ref) = table_ref else {
183                continue;
184            };
185
186            if let Some(ref base_table) = base_table {
187                if base_table.raw() == table_ref.raw() && base_table != &table_ref {
188                    continue;
189                }
190            }
191
192            let whitespace_ref =
193                from_expression.child(const { &SyntaxSet::new(&[SyntaxKind::Whitespace]) });
194
195            let alias_exp_ref =
196                from_expression.child(const { &SyntaxSet::new(&[SyntaxKind::AliasExpression]) });
197            let Some(alias_exp_ref) = alias_exp_ref else {
198                continue;
199            };
200
201            let alias_identifier_ref = alias_exp_ref.child(
202                const { &SyntaxSet::new(&[SyntaxKind::Identifier, SyntaxKind::NakedIdentifier]) },
203            );
204
205            acc.push(TableAliasInfo {
206                table_ref,
207                whitespace_ref,
208                alias_exp_ref,
209                alias_identifier_ref,
210            });
211        }
212
213        acc
214    }
215}
216
217impl Rule for RuleAL07 {
218    fn load_from_config(&self, _config: &AHashMap<String, Value>) -> Result<ErasedRule, String> {
219        Ok(RuleAL07 {
220            force_enable: _config["force_enable"].as_bool().unwrap(),
221        }
222        .erased())
223    }
224
225    fn name(&self) -> &'static str {
226        "aliasing.forbid"
227    }
228    fn description(&self) -> &'static str {
229        "Avoid table aliases in from clauses and join conditions."
230    }
231
232    fn long_description(&self) -> &'static str {
233        r#"
234**Anti-pattern**
235
236In this example, alias o is used for the orders table, and c is used for customers table.
237
238```sql
239SELECT
240    COUNT(o.customer_id) as order_amount,
241    c.name
242FROM orders as o
243JOIN customers as c on o.id = c.user_id
244```
245
246**Best practice**
247
248Avoid aliases.
249
250```sql
251SELECT
252    COUNT(orders.customer_id) as order_amount,
253    customers.name
254FROM orders
255JOIN customers on orders.id = customers.user_id
256
257-- Self-join will not raise issue
258
259SELECT
260    table1.a,
261    table_alias.b,
262FROM
263    table1
264    LEFT JOIN table1 AS table_alias ON
265        table1.foreign_key = table_alias.foreign_key
266```
267"#
268    }
269
270    fn groups(&self) -> &'static [RuleGroups] {
271        &[RuleGroups::All, RuleGroups::Aliasing]
272    }
273
274    fn eval(&self, context: &RuleContext) -> Vec<LintResult> {
275        if !self.force_enable {
276            return Vec::new();
277        }
278
279        let children = FunctionalContext::new(context).segment().children(None);
280        let from_clause_segment = children
281            .select(
282                Some(|it: &ErasedSegment| it.is_type(SyntaxKind::FromClause)),
283                None,
284                None,
285                None,
286            )
287            .find_first::<fn(&_) -> _>(None);
288
289        let base_table = from_clause_segment
290            .children(Some(|it| it.is_type(SyntaxKind::FromExpression)))
291            .find_first::<fn(&_) -> _>(None)
292            .children(Some(|it| it.is_type(SyntaxKind::FromExpressionElement)))
293            .find_first::<fn(&_) -> _>(None)
294            .children(Some(|it| it.is_type(SyntaxKind::TableExpression)))
295            .find_first::<fn(&_) -> _>(None)
296            .children(Some(|it| {
297                it.is_type(SyntaxKind::ObjectReference) || it.is_type(SyntaxKind::TableReference)
298            }));
299
300        if base_table.is_empty() {
301            return Vec::new();
302        }
303
304        let mut from_expression_elements = Vec::new();
305        let mut column_reference_segments = Vec::new();
306
307        let after_from_clause = children.select::<fn(&ErasedSegment) -> bool>(
308            None,
309            None,
310            Some(&from_clause_segment[0]),
311            None,
312        );
313        for clause in chain(from_clause_segment, after_from_clause) {
314            for from_expression_element in clause.recursive_crawl(
315                const { &SyntaxSet::new(&[SyntaxKind::FromExpressionElement]) },
316                true,
317                &SyntaxSet::EMPTY,
318                true,
319            ) {
320                from_expression_elements.push(from_expression_element);
321            }
322
323            for from_expression_element in clause.recursive_crawl(
324                const { &SyntaxSet::new(&[SyntaxKind::ColumnReference]) },
325                true,
326                &SyntaxSet::EMPTY,
327                true,
328            ) {
329                column_reference_segments.push(from_expression_element);
330            }
331        }
332
333        self.lint_aliases_in_join(
334            context.tables,
335            base_table.first().cloned(),
336            from_expression_elements,
337            column_reference_segments,
338            context.segment.clone(),
339        )
340    }
341
342    fn is_fix_compatible(&self) -> bool {
343        true
344    }
345
346    fn crawl_behaviour(&self) -> Crawler {
347        SegmentSeekerCrawler::new(const { SyntaxSet::new(&[SyntaxKind::SelectStatement]) }).into()
348    }
349}