sqrust_rules/lint/
duplicate_alias.rs1use sqrust_core::{Diagnostic, FileContext, Rule};
2use sqlparser::ast::{Query, SelectItem, SetExpr, Statement, TableFactor};
3use std::collections::HashMap;
4
5pub struct DuplicateAlias;
6
7impl Rule for DuplicateAlias {
8 fn name(&self) -> &'static str {
9 "Lint/DuplicateAlias"
10 }
11
12 fn check(&self, ctx: &FileContext) -> Vec<Diagnostic> {
13 if !ctx.parse_errors.is_empty() {
14 return Vec::new();
15 }
16
17 let mut diags = Vec::new();
18 for stmt in &ctx.statements {
19 if let Statement::Query(query) = stmt {
20 check_query(query, &ctx.source, &mut diags);
21 }
22 }
23 diags
24 }
25}
26
27fn check_query(query: &Query, source: &str, diags: &mut Vec<Diagnostic>) {
28 if let Some(with) = &query.with {
30 for cte in &with.cte_tables {
31 check_query(&cte.query, source, diags);
32 }
33 }
34 check_set_expr(&query.body, source, diags);
35}
36
37fn check_set_expr(expr: &SetExpr, source: &str, diags: &mut Vec<Diagnostic>) {
38 match expr {
39 SetExpr::Select(sel) => {
40 let mut seen: HashMap<String, usize> = HashMap::new();
42 let mut dupes: Vec<String> = Vec::new();
44
45 for item in &sel.projection {
46 if let SelectItem::ExprWithAlias { alias, .. } = item {
47 let name = alias.value.to_lowercase();
48 let count = seen.entry(name.clone()).or_insert(0);
49 *count += 1;
50 if *count == 2 {
51 dupes.push(name);
53 }
54 }
55 }
56
57 for dupe in &dupes {
58 let (line, col) = find_alias_position(source, dupe);
60 diags.push(Diagnostic {
61 rule: "Lint/DuplicateAlias",
62 message: format!(
63 "Column alias '{}' is used more than once in this SELECT",
64 dupe
65 ),
66 line,
67 col,
68 });
69 }
70
71 for table in &sel.from {
73 check_table_factor(&table.relation, source, diags);
74 for join in &table.joins {
75 check_table_factor(&join.relation, source, diags);
76 }
77 }
78 }
79 SetExpr::SetOperation { left, right, .. } => {
80 check_set_expr(left, source, diags);
81 check_set_expr(right, source, diags);
82 }
83 SetExpr::Query(inner) => {
85 check_query(inner, source, diags);
86 }
87 _ => {}
88 }
89}
90
91fn check_table_factor(tf: &TableFactor, source: &str, diags: &mut Vec<Diagnostic>) {
92 if let TableFactor::Derived { subquery, .. } = tf {
93 check_query(subquery, source, diags);
94 }
95}
96
97fn find_alias_position(source: &str, alias: &str) -> (usize, usize) {
100 let source_lower = source.to_lowercase();
101 let pattern = format!("as {}", alias);
102
103 let mut search_from = 0usize;
104 while search_from < source_lower.len() {
105 let Some(rel) = source_lower[search_from..].find(&pattern) else {
106 break;
107 };
108 let abs = search_from + rel;
109 let bytes = source_lower.as_bytes();
110
111 let before_ok = abs == 0
113 || {
114 let b = bytes[abs - 1];
115 !b.is_ascii_alphanumeric() && b != b'_'
116 };
117
118 let after_pos = abs + pattern.len();
119 let after_ok = after_pos >= source_lower.len()
121 || {
122 let b = bytes[after_pos];
123 !b.is_ascii_alphanumeric() && b != b'_'
124 };
125
126 if before_ok && after_ok {
127 return offset_to_line_col(source, abs);
128 }
129 search_from = abs + 1;
130 }
131
132 (1, 1)
133}
134
135fn offset_to_line_col(source: &str, offset: usize) -> (usize, usize) {
137 let mut line = 1usize;
138 let mut col = 1usize;
139 for (i, ch) in source.char_indices() {
140 if i == offset {
141 break;
142 }
143 if ch == '\n' {
144 line += 1;
145 col = 1;
146 } else {
147 col += 1;
148 }
149 }
150 (line, col)
151}