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