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_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
76/// Single-pass extraction of source name, alias name, and the span to remove.
77fn 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            // Before AS: track source column name
89            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                        // Include preceding whitespace in removal span
94                        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                // Extract source identifier
106                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            // After AS: find alias identifier
116            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
134/// Find the last identifier token within a node (e.g., `table.col` → `col`).
135fn 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}