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