sqruff_lib/rules/aliasing/
al05.rs1use 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 let Some(bracketed) =
255 alias_expression.child(const { &SyntaxSet::single(SyntaxKind::Bracketed) })
256 else {
257 return false;
258 };
259
260 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 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 if tbl.part.to_uppercase() != alias.ref_str.to_uppercase() {
293 continue;
294 }
295 }
296 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 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}