rigsql_rules/aliasing/
al09.rs1use rigsql_core::{Segment, SegmentType, Span};
2
3use crate::rule::{CrawlType, Rule, RuleContext, RuleGroup};
4use crate::violation::{LintViolation, SourceEdit};
5
6#[derive(Debug, Default)]
11pub struct RuleAL09;
12
13impl Rule for RuleAL09 {
14 fn code(&self) -> &'static str {
15 "AL09"
16 }
17 fn name(&self) -> &'static str {
18 "aliasing.self_alias.column"
19 }
20 fn description(&self) -> &'static str {
21 "Self-aliasing of columns is redundant."
22 }
23 fn explanation(&self) -> &'static str {
24 "Writing `col AS col` or `table.col AS col` aliases a column to its own name. \
25 This is redundant and adds unnecessary noise. Remove the AS clause to simplify \
26 the query."
27 }
28 fn groups(&self) -> &[RuleGroup] {
29 &[RuleGroup::Aliasing]
30 }
31 fn is_fixable(&self) -> bool {
32 true
33 }
34
35 fn crawl_type(&self) -> CrawlType {
36 CrawlType::Segment(vec![SegmentType::AliasExpression])
37 }
38
39 fn eval(&self, ctx: &RuleContext) -> Vec<LintViolation> {
40 let in_select = ctx
42 .parent
43 .is_some_and(|p| p.segment_type() == SegmentType::SelectClause);
44 if !in_select {
45 return vec![];
46 }
47
48 let children = ctx.segment.children();
49
50 let Some(info) = extract_self_alias_info(children) else {
52 return vec![];
53 };
54
55 if !info.alias_name.eq_ignore_ascii_case(&info.source_name) {
56 return vec![];
57 }
58
59 vec![LintViolation::with_fix(
60 self.code(),
61 format!("Column '{}' is aliased to itself.", info.source_name),
62 ctx.segment.span(),
63 vec![SourceEdit::delete(info.remove_span)],
64 )]
65 }
66}
67
68struct SelfAliasInfo {
69 source_name: String,
70 alias_name: String,
71 remove_span: Span,
72}
73
74fn extract_self_alias_info(children: &[Segment]) -> Option<SelfAliasInfo> {
76 let mut source_name: Option<String> = None;
77 let mut alias_name: Option<String> = None;
78 let mut as_region_start: Option<u32> = None;
79 let mut found_as = false;
80 let mut prev_trivia_start: Option<u32> = None;
81
82 for child in children {
83 let st = child.segment_type();
84
85 if !found_as {
86 if st == SegmentType::Keyword {
88 if let Segment::Token(t) = child {
89 if t.token.text.as_str().eq_ignore_ascii_case("AS") {
90 found_as = true;
91 as_region_start = Some(prev_trivia_start.unwrap_or(child.span().start));
93 continue;
94 }
95 }
96 }
97 if st.is_trivia() {
98 if prev_trivia_start.is_none() || source_name.is_some() {
99 prev_trivia_start = Some(child.span().start);
100 }
101 } else {
102 prev_trivia_start = None;
103 if st == SegmentType::ColumnRef || st == SegmentType::QualifiedIdentifier {
105 source_name = find_last_identifier_in(child);
106 } else if st == SegmentType::Identifier || st == SegmentType::QuotedIdentifier {
107 if let Segment::Token(t) = child {
108 source_name = Some(t.token.text.to_string());
109 }
110 }
111 }
112 } else {
113 if (st == SegmentType::Identifier || st == SegmentType::QuotedIdentifier)
115 && alias_name.is_none()
116 {
117 if let Segment::Token(t) = child {
118 alias_name = Some(t.token.text.to_string());
119 }
120 }
121 }
122 }
123
124 let end = children.last()?.span().end;
125 Some(SelfAliasInfo {
126 source_name: source_name?,
127 alias_name: alias_name?,
128 remove_span: Span::new(as_region_start?, end),
129 })
130}
131
132fn find_last_identifier_in(segment: &Segment) -> Option<String> {
134 let mut result = None;
135 for child in segment.children() {
136 let st = child.segment_type();
137 if st == SegmentType::Identifier || st == SegmentType::QuotedIdentifier {
138 if let Segment::Token(t) = child {
139 result = Some(t.token.text.to_string());
140 }
141 }
142 }
143 result
144}
145
146#[cfg(test)]
147mod tests {
148 use super::*;
149 use crate::test_utils::lint_sql;
150
151 #[test]
152 fn test_al09_flags_self_alias() {
153 let violations = lint_sql("SELECT col AS col FROM t", RuleAL09);
154 assert_eq!(violations.len(), 1);
155 }
156
157 #[test]
158 fn test_al09_accepts_different_alias() {
159 let violations = lint_sql("SELECT col AS c FROM t", RuleAL09);
160 assert_eq!(violations.len(), 0);
161 }
162}