Skip to main content

rigsql_rules/aliasing/
al04.rs

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