Skip to main content

rigsql_rules/aliasing/
al04.rs

1use rigsql_core::{Segment, SegmentType};
2
3use crate::rule::{CrawlType, Rule, RuleContext, RuleGroup};
4use crate::violation::LintViolation;
5
6/// AL04: Table aliases should be unique within a statement.
7///
8/// Duplicate table aliases create ambiguity in column references.
9#[derive(Debug, Default)]
10pub struct RuleAL04;
11
12impl Rule for RuleAL04 {
13    fn code(&self) -> &'static str {
14        "AL04"
15    }
16    fn name(&self) -> &'static str {
17        "aliasing.unique_table"
18    }
19    fn description(&self) -> &'static str {
20        "Table aliases should be unique within a statement."
21    }
22    fn explanation(&self) -> &'static str {
23        "When the same alias is used for multiple tables in a single statement, \
24         column references become ambiguous. Each table alias must be unique within \
25         its containing statement."
26    }
27    fn groups(&self) -> &[RuleGroup] {
28        &[RuleGroup::Aliasing]
29    }
30    fn is_fixable(&self) -> bool {
31        false
32    }
33
34    fn crawl_type(&self) -> CrawlType {
35        CrawlType::Segment(vec![SegmentType::SelectStatement])
36    }
37
38    fn eval(&self, ctx: &RuleContext) -> Vec<LintViolation> {
39        let mut aliases: Vec<(String, rigsql_core::Span)> = Vec::new();
40        collect_table_aliases(ctx.segment, &mut aliases);
41
42        let mut violations = Vec::new();
43        let mut seen: Vec<(String, rigsql_core::Span)> = Vec::new();
44
45        for (name, span) in &aliases {
46            let lower = name.to_lowercase();
47            if let Some((_, first_span)) = seen.iter().find(|(n, _)| *n == lower) {
48                violations.push(LintViolation::new(
49                    self.code(),
50                    format!(
51                        "Duplicate table alias '{}'. First used at offset {}.",
52                        name, first_span.start,
53                    ),
54                    *span,
55                ));
56            } else {
57                seen.push((lower, *span));
58            }
59        }
60
61        violations
62    }
63}
64
65/// Collect alias names from FROM and JOIN clauses within a statement.
66fn collect_table_aliases(segment: &Segment, aliases: &mut Vec<(String, rigsql_core::Span)>) {
67    let st = segment.segment_type();
68
69    // Only look for aliases within FROM and JOIN clauses
70    if st == SegmentType::FromClause || st == SegmentType::JoinClause {
71        find_alias_names(segment, aliases);
72        return;
73    }
74
75    // Don't recurse into nested SelectStatements (subqueries have their own scope)
76    if st == SegmentType::SelectStatement || st == SegmentType::Subquery {
77        // Only recurse into top-level children for the current statement
78        // Skip nested selects
79        if st == SegmentType::Subquery {
80            return;
81        }
82    }
83
84    for child in segment.children() {
85        collect_table_aliases(child, aliases);
86    }
87}
88
89/// Find AliasExpression nodes and extract the alias name (last identifier).
90fn find_alias_names(segment: &Segment, aliases: &mut Vec<(String, rigsql_core::Span)>) {
91    if segment.segment_type() == SegmentType::AliasExpression {
92        if let Some(name) = extract_alias_name(segment) {
93            aliases.push((name, segment.span()));
94        }
95        return;
96    }
97
98    // Don't recurse into subqueries
99    if segment.segment_type() == SegmentType::Subquery {
100        return;
101    }
102
103    for child in segment.children() {
104        find_alias_names(child, aliases);
105    }
106}
107
108/// Extract the alias name from an AliasExpression.
109/// The alias name is typically the last Identifier after AS keyword.
110fn extract_alias_name(alias_expr: &Segment) -> Option<String> {
111    let children = alias_expr.children();
112    // Walk children in reverse to find the last identifier (the alias name)
113    for child in children.iter().rev() {
114        let st = child.segment_type();
115        if st == SegmentType::Identifier || st == SegmentType::QuotedIdentifier {
116            if let Segment::Token(t) = child {
117                return Some(t.token.text.to_string());
118            }
119        }
120        // Skip trivia
121        if st.is_trivia() {
122            continue;
123        }
124        // If we hit something that's not trivia or identifier, stop
125        if st != SegmentType::Keyword {
126            break;
127        }
128    }
129    None
130}