sqruff_lib/rules/aliasing/
al05.rs1use 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}