Skip to main content

rigsql_rules/aliasing/
al09.rs

1use rigsql_core::{Segment, SegmentType, Span};
2
3use crate::rule::{CrawlType, Rule, RuleContext, RuleGroup};
4use crate::violation::{LintViolation, SourceEdit};
5
6/// AL09: Self-aliasing of columns.
7///
8/// Aliasing a column to itself (e.g., `col AS col`) is redundant and
9/// should be removed to improve readability.
10#[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        // Only check column aliases (within SelectClause)
41        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        // Single-pass extraction: source name, alias name, and AS-to-end span
51        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
74/// Single-pass extraction of source name, alias name, and the span to remove.
75fn 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            // Before AS: track source column name
87            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                        // Include preceding whitespace in removal span
92                        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                // Extract source identifier
104                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            // After AS: find alias identifier
114            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
132/// Find the last identifier token within a node (e.g., `table.col` → `col`).
133fn 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}