rigsql_rules/aliasing/
al04.rs1use rigsql_core::{Segment, SegmentType};
2
3use crate::rule::{CrawlType, Rule, RuleContext, RuleGroup};
4use crate::violation::LintViolation;
5
6#[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
65fn collect_table_aliases(segment: &Segment, aliases: &mut Vec<(String, rigsql_core::Span)>) {
67 let st = segment.segment_type();
68
69 if st == SegmentType::FromClause || st == SegmentType::JoinClause {
71 find_alias_names(segment, aliases);
72 return;
73 }
74
75 if st == SegmentType::SelectStatement || st == SegmentType::Subquery {
77 if st == SegmentType::Subquery {
80 return;
81 }
82 }
83
84 for child in segment.children() {
85 collect_table_aliases(child, aliases);
86 }
87}
88
89fn 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 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
108fn extract_alias_name(alias_expr: &Segment) -> Option<String> {
111 let children = alias_expr.children();
112 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 if st.is_trivia() {
122 continue;
123 }
124 if st != SegmentType::Keyword {
126 break;
127 }
128 }
129 None
130}