rigsql_rules/aliasing/
al04.rs1use 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#[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
73fn collect_table_aliases(segment: &Segment, aliases: &mut Vec<(String, rigsql_core::Span)>) {
75 let st = segment.segment_type();
76
77 if st == SegmentType::FromClause || st == SegmentType::JoinClause {
79 find_alias_names(segment, aliases);
80 return;
81 }
82
83 if st == SegmentType::SelectStatement || st == SegmentType::Subquery {
85 if st == SegmentType::Subquery {
88 return;
89 }
90 }
91
92 for child in segment.children() {
93 collect_table_aliases(child, aliases);
94 }
95}
96
97fn 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 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}