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_and_msg_key(
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 "rules.AL09.msg",
65 vec![("name".to_string(), info.source_name.clone())],
66 )]
67 }
68}
69
70struct SelfAliasInfo {
71 source_name: String,
72 alias_name: String,
73 remove_span: Span,
74}
75
76fn extract_self_alias_info(children: &[Segment]) -> Option<SelfAliasInfo> {
78 let mut source_name: Option<String> = None;
79 let mut alias_name: Option<String> = None;
80 let mut as_region_start: Option<u32> = None;
81 let mut found_as = false;
82 let mut prev_trivia_start: Option<u32> = None;
83
84 for child in children {
85 let st = child.segment_type();
86
87 if !found_as {
88 if st == SegmentType::Keyword {
90 if let Segment::Token(t) = child {
91 if t.token.text.as_str().eq_ignore_ascii_case("AS") {
92 found_as = true;
93 as_region_start = Some(prev_trivia_start.unwrap_or(child.span().start));
95 continue;
96 }
97 }
98 }
99 if st.is_trivia() {
100 if prev_trivia_start.is_none() || source_name.is_some() {
101 prev_trivia_start = Some(child.span().start);
102 }
103 } else {
104 prev_trivia_start = None;
105 if st == SegmentType::ColumnRef || st == SegmentType::QualifiedIdentifier {
107 source_name = find_last_identifier_in(child);
108 } else if st == SegmentType::Identifier || st == SegmentType::QuotedIdentifier {
109 if let Segment::Token(t) = child {
110 source_name = Some(t.token.text.to_string());
111 }
112 }
113 }
114 } else {
115 if (st == SegmentType::Identifier || st == SegmentType::QuotedIdentifier)
117 && alias_name.is_none()
118 {
119 if let Segment::Token(t) = child {
120 alias_name = Some(t.token.text.to_string());
121 }
122 }
123 }
124 }
125
126 let end = children.last()?.span().end;
127 Some(SelfAliasInfo {
128 source_name: source_name?,
129 alias_name: alias_name?,
130 remove_span: Span::new(as_region_start?, end),
131 })
132}
133
134fn find_last_identifier_in(segment: &Segment) -> Option<String> {
136 let mut result = None;
137 for child in segment.children() {
138 let st = child.segment_type();
139 if st == SegmentType::Identifier || st == SegmentType::QuotedIdentifier {
140 if let Segment::Token(t) = child {
141 result = Some(t.token.text.to_string());
142 }
143 }
144 }
145 result
146}
147
148#[cfg(test)]
149mod tests {
150 use super::*;
151 use crate::test_utils::lint_sql;
152
153 #[test]
154 fn test_al09_flags_self_alias() {
155 let violations = lint_sql("SELECT col AS col FROM t", RuleAL09);
156 assert_eq!(violations.len(), 1);
157 }
158
159 #[test]
160 fn test_al09_accepts_different_alias() {
161 let violations = lint_sql("SELECT col AS c FROM t", RuleAL09);
162 assert_eq!(violations.len(), 0);
163 }
164}