sqruff_lib/rules/aliasing/
al05.rs

1use std::cell::RefCell;
2
3use ahash::{AHashMap, AHashSet};
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::get_select_statement_info;
14
15use crate::core::config::Value;
16use crate::core::rules::context::RuleContext;
17use crate::core::rules::crawlers::{Crawler, SegmentSeekerCrawler};
18use crate::core::rules::{Erased, ErasedRule, LintResult, Rule, RuleGroups};
19
20#[derive(Default, Clone)]
21struct AL05QueryData {
22    aliases: Vec<AliasInfo>,
23    tbl_refs: Vec<SmolStr>,
24}
25
26type QueryKey<'a> = *const RefCell<QueryInner<'a>>;
27type AL05State<'a> = AHashMap<QueryKey<'a>, AL05QueryData>;
28
29#[derive(Debug, Default, Clone)]
30pub struct RuleAL05;
31
32impl Rule for RuleAL05 {
33    fn load_from_config(&self, _config: &AHashMap<String, Value>) -> Result<ErasedRule, String> {
34        Ok(RuleAL05.erased())
35    }
36
37    fn name(&self) -> &'static str {
38        "aliasing.unused"
39    }
40
41    fn description(&self) -> &'static str {
42        "Tables should not be aliased if that alias is not used."
43    }
44
45    fn long_description(&self) -> &'static str {
46        r#"
47**Anti-pattern**
48
49In this example, alias `zoo` is not used.
50
51```sql
52SELECT
53    a
54FROM foo AS zoo
55```
56
57**Best practice**
58
59Use the alias or remove it. An unused alias makes code harder to read without changing any functionality.
60
61```sql
62SELECT
63    zoo.a
64FROM foo AS zoo
65
66-- Alternatively...
67
68SELECT
69    a
70FROM foo
71```
72"#
73    }
74
75    fn groups(&self) -> &'static [RuleGroups] {
76        &[RuleGroups::All, RuleGroups::Core, RuleGroups::Aliasing]
77    }
78
79    fn eval(&self, context: &RuleContext) -> Vec<LintResult> {
80        let mut violations = Vec::new();
81        let select_info = get_select_statement_info(&context.segment, context.dialect.into(), true);
82
83        let Some(select_info) = select_info else {
84            return Vec::new();
85        };
86
87        if select_info.table_aliases.is_empty() {
88            return Vec::new();
89        }
90
91        let query = Query::from_segment(&context.segment, context.dialect, None);
92        let mut payloads = AL05State::default();
93        self.analyze_table_aliases(query.clone(), context.dialect, &mut payloads);
94
95        let payload = payloads.get(&query.id()).cloned().unwrap_or_default();
96
97        if context.dialect.name == DialectKind::Redshift {
98            let mut references = AHashSet::default();
99            let mut aliases = AHashSet::default();
100
101            for alias in &payload.aliases {
102                aliases.insert(alias.ref_str.clone());
103                if let Some(object_reference) = &alias.object_reference {
104                    for seg in object_reference.segments() {
105                        if const {
106                            SyntaxSet::new(&[
107                                SyntaxKind::Identifier,
108                                SyntaxKind::NakedIdentifier,
109                                SyntaxKind::QuotedIdentifier,
110                                SyntaxKind::ObjectReference,
111                            ])
112                        }
113                        .contains(seg.get_type())
114                        {
115                            references.insert(seg.raw().to_smolstr());
116                        }
117                    }
118                }
119            }
120
121            if aliases.intersection(&references).next().is_some() {
122                return Vec::new();
123            }
124        }
125
126        for alias in &payload.aliases {
127            if Self::is_alias_required(&alias.from_expression_element, context.dialect.name) {
128                continue;
129            }
130
131            if alias.aliased && !payload.tbl_refs.contains(&alias.ref_str) {
132                let violation = self.report_unused_alias(alias);
133                violations.push(violation);
134            }
135        }
136
137        violations
138    }
139
140    fn is_fix_compatible(&self) -> bool {
141        true
142    }
143
144    fn crawl_behaviour(&self) -> Crawler {
145        SegmentSeekerCrawler::new(const { SyntaxSet::new(&[SyntaxKind::SelectStatement]) }).into()
146    }
147}
148
149impl RuleAL05 {
150    #[allow(clippy::only_used_in_recursion)]
151    fn analyze_table_aliases<'a>(
152        &self,
153        query: Query<'a>,
154        dialect: &Dialect,
155        payloads: &mut AL05State<'a>,
156    ) {
157        payloads.entry(query.id()).or_default();
158        let selectables = std::mem::take(&mut RefCell::borrow_mut(&query.inner).selectables);
159
160        for selectable in &selectables {
161            if let Some(select_info) = selectable.select_info() {
162                let table_aliases = select_info.table_aliases;
163                let reference_buffer = select_info.reference_buffer;
164
165                payloads
166                    .entry(query.id())
167                    .or_default()
168                    .aliases
169                    .extend(table_aliases);
170
171                for r in reference_buffer {
172                    for tr in
173                        r.extract_possible_references(ObjectReferenceLevel::Table, dialect.name)
174                    {
175                        Self::resolve_and_mark_reference(query.clone(), tr.part, payloads);
176                    }
177                }
178            }
179        }
180
181        RefCell::borrow_mut(&query.inner).selectables = selectables;
182
183        for child in query.children() {
184            self.analyze_table_aliases(child, dialect, payloads);
185        }
186    }
187
188    fn resolve_and_mark_reference<'a>(
189        query: Query<'a>,
190        r#ref: String,
191        payloads: &mut AL05State<'a>,
192    ) {
193        if let Some(payload) = payloads.get_mut(&query.id())
194            && payload.aliases.iter().any(|it| it.ref_str == r#ref)
195        {
196            payload.tbl_refs.push(r#ref.into());
197            return;
198        }
199
200        if let Some(parent) = RefCell::borrow(&query.inner).parent.clone() {
201            Self::resolve_and_mark_reference(parent, r#ref, payloads);
202        }
203    }
204
205    fn is_alias_required(
206        from_expression_element: &ErasedSegment,
207        dialect_name: DialectKind,
208    ) -> bool {
209        for segment in from_expression_element
210            .iter_segments(const { &SyntaxSet::new(&[SyntaxKind::Bracketed]) }, false)
211        {
212            if segment.is_type(SyntaxKind::TableExpression) {
213                return if segment
214                    .child(const { &SyntaxSet::new(&[SyntaxKind::ValuesClause]) })
215                    .is_some()
216                {
217                    matches!(dialect_name, DialectKind::Snowflake)
218                } else {
219                    segment
220                        .iter_segments(const { &SyntaxSet::new(&[SyntaxKind::Bracketed]) }, false)
221                        .iter()
222                        .any(|seg| {
223                            const {
224                                SyntaxSet::new(&[
225                                    SyntaxKind::SelectStatement,
226                                    SyntaxKind::SetExpression,
227                                    SyntaxKind::WithCompoundStatement,
228                                ])
229                            }
230                            .contains(seg.get_type())
231                        })
232                };
233            }
234        }
235        false
236    }
237
238    fn report_unused_alias(&self, alias: &AliasInfo) -> LintResult {
239        let mut fixes = vec![LintFix::delete(alias.alias_expression.clone().unwrap())];
240
241        // Delete contiguous whitespace/meta immediately preceding the alias expression
242        // without allocating intermediate Segments collections.
243        if let Some(alias_idx) = alias
244            .from_expression_element
245            .segments()
246            .iter()
247            .position(|s| s == alias.alias_expression.as_ref().unwrap())
248        {
249            for seg in alias.from_expression_element.segments()[..alias_idx]
250                .iter()
251                .rev()
252                .take_while(|s| s.is_whitespace() || s.is_meta())
253            {
254                fixes.push(LintFix::delete(seg.clone()));
255            }
256        }
257
258        LintResult::new(
259            alias.segment.clone(),
260            fixes,
261            format!(
262                "Alias '{}' is never used in SELECT statement.",
263                alias.ref_str
264            )
265            .into(),
266            None,
267        )
268    }
269}