Skip to main content

sqruff_lib/rules/aliasing/
al05.rs

1use std::cell::RefCell;
2
3use hashbrown::{HashMap, HashSet};
4use smol_str::{SmolStr, ToSmolStr};
5use sqruff_lib_core::dialects::Dialect;
6use sqruff_lib_core::dialects::common::AliasInfo;
7use sqruff_lib_core::dialects::init::DialectKind;
8use sqruff_lib_core::dialects::syntax::{SyntaxKind, SyntaxSet};
9use sqruff_lib_core::lint_fix::LintFix;
10use sqruff_lib_core::parser::segments::ErasedSegment;
11use sqruff_lib_core::parser::segments::object_reference::ObjectReferenceLevel;
12use sqruff_lib_core::utils::analysis::query::{Query, QueryInner};
13use sqruff_lib_core::utils::analysis::select::{
14    SelectStatementColumnsAndTables, get_select_statement_info,
15};
16
17use crate::core::config::Value;
18use crate::core::rules::context::RuleContext;
19use crate::core::rules::crawlers::{Crawler, SegmentSeekerCrawler};
20use crate::core::rules::{Erased, ErasedRule, LintResult, Rule, RuleGroups};
21
22#[derive(Default, Clone)]
23struct AL05QueryData {
24    aliases: Vec<AliasInfo>,
25    tbl_refs: Vec<SmolStr>,
26}
27
28type QueryKey<'a> = *const RefCell<QueryInner<'a>>;
29type AL05State<'a> = HashMap<QueryKey<'a>, AL05QueryData>;
30
31#[derive(Debug, Default, Clone)]
32pub struct RuleAL05;
33
34impl Rule for RuleAL05 {
35    fn load_from_config(&self, _config: &HashMap<String, Value>) -> Result<ErasedRule, String> {
36        Ok(RuleAL05.erased())
37    }
38
39    fn name(&self) -> &'static str {
40        "aliasing.unused"
41    }
42
43    fn description(&self) -> &'static str {
44        "Tables should not be aliased if that alias is not used."
45    }
46
47    fn long_description(&self) -> &'static str {
48        r#"
49**Anti-pattern**
50
51In this example, alias `zoo` is not used.
52
53```sql
54SELECT
55    a
56FROM foo AS zoo
57```
58
59**Best practice**
60
61Use the alias or remove it. An unused alias makes code harder to read without changing any functionality.
62
63```sql
64SELECT
65    zoo.a
66FROM foo AS zoo
67
68-- Alternatively...
69
70SELECT
71    a
72FROM foo
73```
74"#
75    }
76
77    fn groups(&self) -> &'static [RuleGroups] {
78        &[RuleGroups::All, RuleGroups::Core, RuleGroups::Aliasing]
79    }
80
81    fn eval(&self, context: &RuleContext) -> Vec<LintResult> {
82        let mut violations = Vec::new();
83        let select_info = get_select_statement_info(&context.segment, context.dialect.into(), true);
84
85        let Some(select_info) = select_info else {
86            return Vec::new();
87        };
88
89        if select_info.table_aliases.is_empty() {
90            return Vec::new();
91        }
92
93        let query = Query::from_segment(&context.segment, context.dialect, None);
94        let mut payloads = AL05State::default();
95        self.analyze_table_aliases(query.clone(), context.dialect, &mut payloads);
96
97        let payload = payloads.get(&query.id()).cloned().unwrap_or_default();
98
99        if context.dialect.name == DialectKind::Redshift {
100            let mut references: HashSet<SmolStr> = HashSet::new();
101            let mut aliases: HashSet<SmolStr> = HashSet::new();
102
103            for alias in &payload.aliases {
104                aliases.insert(alias.ref_str.clone());
105                if let Some(object_reference) = &alias.object_reference {
106                    for seg in object_reference.segments() {
107                        if const {
108                            SyntaxSet::new(&[
109                                SyntaxKind::Identifier,
110                                SyntaxKind::NakedIdentifier,
111                                SyntaxKind::QuotedIdentifier,
112                                SyntaxKind::ObjectReference,
113                            ])
114                        }
115                        .contains(seg.get_type())
116                        {
117                            references.insert(seg.raw().to_smolstr());
118                        }
119                    }
120                }
121            }
122
123            if aliases.intersection(&references).next().is_some() {
124                return Vec::new();
125            }
126        }
127
128        for alias in &payload.aliases {
129            if Self::is_alias_required(&alias.from_expression_element, context.dialect.name) {
130                continue;
131            }
132
133            if alias.aliased && !payload.tbl_refs.contains(&alias.ref_str) {
134                if Self::has_used_column_aliases(alias, &select_info, context.dialect.name) {
135                    continue;
136                }
137                let violation = self.report_unused_alias(alias);
138                violations.push(violation);
139            }
140        }
141
142        violations
143    }
144
145    fn is_fix_compatible(&self) -> bool {
146        true
147    }
148
149    fn crawl_behaviour(&self) -> Crawler {
150        SegmentSeekerCrawler::new(const { SyntaxSet::new(&[SyntaxKind::SelectStatement]) }).into()
151    }
152}
153
154impl RuleAL05 {
155    #[allow(clippy::only_used_in_recursion)]
156    fn analyze_table_aliases<'a>(
157        &self,
158        query: Query<'a>,
159        dialect: &Dialect,
160        payloads: &mut AL05State<'a>,
161    ) {
162        payloads.entry(query.id()).or_default();
163        let selectables = std::mem::take(&mut RefCell::borrow_mut(&query.inner).selectables);
164
165        for selectable in &selectables {
166            if let Some(select_info) = selectable.select_info() {
167                let table_aliases = select_info.table_aliases;
168                let reference_buffer = select_info.reference_buffer;
169
170                payloads
171                    .entry(query.id())
172                    .or_default()
173                    .aliases
174                    .extend(table_aliases);
175
176                for r in reference_buffer {
177                    for tr in
178                        r.extract_possible_references(ObjectReferenceLevel::Table, dialect.name)
179                    {
180                        Self::resolve_and_mark_reference(query.clone(), tr.part, payloads);
181                    }
182                }
183            }
184        }
185
186        RefCell::borrow_mut(&query.inner).selectables = selectables;
187
188        for child in query.children() {
189            self.analyze_table_aliases(child, dialect, payloads);
190        }
191    }
192
193    fn resolve_and_mark_reference<'a>(
194        query: Query<'a>,
195        r#ref: String,
196        payloads: &mut AL05State<'a>,
197    ) {
198        if let Some(payload) = payloads.get_mut(&query.id())
199            && payload.aliases.iter().any(|it| it.ref_str == r#ref)
200        {
201            payload.tbl_refs.push(r#ref.into());
202            return;
203        }
204
205        if let Some(parent) = RefCell::borrow(&query.inner).parent.clone() {
206            Self::resolve_and_mark_reference(parent, r#ref, payloads);
207        }
208    }
209
210    fn is_alias_required(
211        from_expression_element: &ErasedSegment,
212        dialect_name: DialectKind,
213    ) -> bool {
214        for segment in from_expression_element
215            .iter_segments(const { &SyntaxSet::new(&[SyntaxKind::Bracketed]) }, false)
216        {
217            if segment.is_type(SyntaxKind::TableExpression) {
218                return if segment
219                    .child(const { &SyntaxSet::new(&[SyntaxKind::ValuesClause]) })
220                    .is_some()
221                {
222                    matches!(dialect_name, DialectKind::Snowflake)
223                } else {
224                    segment
225                        .iter_segments(const { &SyntaxSet::new(&[SyntaxKind::Bracketed]) }, false)
226                        .iter()
227                        .any(|seg| {
228                            const {
229                                SyntaxSet::new(&[
230                                    SyntaxKind::SelectStatement,
231                                    SyntaxKind::SetExpression,
232                                    SyntaxKind::WithCompoundStatement,
233                                ])
234                            }
235                            .contains(seg.get_type())
236                        })
237                };
238            }
239        }
240        false
241    }
242
243    fn has_used_column_aliases(
244        alias: &AliasInfo,
245        select_info: &SelectStatementColumnsAndTables,
246        dialect_name: DialectKind,
247    ) -> bool {
248        let Some(alias_expression) = &alias.alias_expression else {
249            return false;
250        };
251
252        // Look for a Bracketed child in the alias expression (the column alias
253        // list, e.g. `(value)` or `(c1, c2)`).
254        let Some(bracketed) =
255            alias_expression.child(const { &SyntaxSet::single(SyntaxKind::Bracketed) })
256        else {
257            return false;
258        };
259
260        // Collect all identifier names from the bracketed column alias list.
261        let col_alias_names: Vec<SmolStr> = bracketed
262            .recursive_crawl(
263                const {
264                    &SyntaxSet::new(&[
265                        SyntaxKind::NakedIdentifier,
266                        SyntaxKind::Identifier,
267                        SyntaxKind::QuotedIdentifier,
268                    ])
269                },
270                true,
271                &SyntaxSet::EMPTY,
272                true,
273            )
274            .into_iter()
275            .map(|seg| seg.raw().to_uppercase().into())
276            .collect();
277
278        if col_alias_names.is_empty() {
279            return false;
280        }
281
282        // Check if any *unqualified* reference (or one qualified with this
283        // alias) has an Object-level part matching a column alias name.
284        // Qualified references like `o.value` belong to table `o`, not to our
285        // alias, so they must not count as usage of the column alias list.
286        for reference in &select_info.reference_buffer {
287            let table_refs =
288                reference.extract_possible_references(ObjectReferenceLevel::Table, dialect_name);
289            if let Some(tbl) = table_refs.first() {
290                // Qualified reference — only count it if the qualifier is our
291                // own table alias.
292                if tbl.part.to_uppercase() != alias.ref_str.to_uppercase() {
293                    continue;
294                }
295            }
296            // Unqualified reference (no table part) or qualified with our alias.
297            for obj_ref in
298                reference.extract_possible_references(ObjectReferenceLevel::Object, dialect_name)
299            {
300                if col_alias_names.contains(&SmolStr::from(obj_ref.part.to_uppercase())) {
301                    return true;
302                }
303            }
304        }
305
306        false
307    }
308
309    fn report_unused_alias(&self, alias: &AliasInfo) -> LintResult {
310        let mut fixes = vec![LintFix::delete(alias.alias_expression.clone().unwrap())];
311
312        // Delete contiguous whitespace/meta immediately preceding the alias expression
313        // without allocating intermediate Segments collections.
314        if let Some(alias_idx) = alias
315            .from_expression_element
316            .segments()
317            .iter()
318            .position(|s| s == alias.alias_expression.as_ref().unwrap())
319        {
320            for seg in alias.from_expression_element.segments()[..alias_idx]
321                .iter()
322                .rev()
323                .take_while(|s| s.is_whitespace() || s.is_meta())
324            {
325                fixes.push(LintFix::delete(seg.clone()));
326            }
327        }
328
329        LintResult::new(
330            alias.segment.clone(),
331            fixes,
332            format!(
333                "Alias '{}' is never used in SELECT statement.",
334                alias.ref_str
335            )
336            .into(),
337            None,
338        )
339    }
340}