1use sqrust_core::{Diagnostic, FileContext, Rule};
2use sqlparser::ast::{Query, Select, SelectItem, SetExpr, Statement, TableFactor};
3
4pub struct ExplicitColumnAlias;
5
6fn line_col(source: &str, offset: usize) -> (usize, usize) {
8 let before = &source[..offset];
9 let line = before.chars().filter(|&c| c == '\n').count() + 1;
10 let col = before.rfind('\n').map(|p| offset - p - 1).unwrap_or(offset) + 1;
11 (line, col)
12}
13
14fn is_word(b: u8) -> bool {
16 b.is_ascii_alphanumeric() || b == b'_'
17}
18
19fn find_alias_occurrence(source: &str, alias: &str, occurrence: usize) -> Option<usize> {
22 let bytes = source.as_bytes();
23 let alias_bytes = alias.as_bytes();
24 let alias_len = alias_bytes.len();
25 let src_len = bytes.len();
26 let mut count = 0usize;
27 let mut i = 0;
28
29 while i + alias_len <= src_len {
30 let before_ok = i == 0 || !is_word(bytes[i - 1]);
32 if before_ok {
33 let matched = bytes[i..i + alias_len]
34 .iter()
35 .zip(alias_bytes.iter())
36 .all(|(&a, b)| a.eq_ignore_ascii_case(b));
37 if matched {
38 let after_ok = i + alias_len >= src_len || !is_word(bytes[i + alias_len]);
40 if after_ok {
41 if count == occurrence {
42 return Some(i);
43 }
44 count += 1;
45 }
46 }
47 }
48 i += 1;
49 }
50 None
51}
52
53fn has_as_before(source: &str, pos: usize) -> bool {
57 let bytes = source.as_bytes();
58
59 let mut j = pos;
61 if j == 0 {
62 return false;
63 }
64 if j > 0 && (bytes[j - 1] == b'"' || bytes[j - 1] == b'`' || bytes[j - 1] == b'\'') {
66 j -= 1;
68 }
69
70 while j > 0 && (bytes[j - 1] == b' ' || bytes[j - 1] == b'\t' || bytes[j - 1] == b'\n' || bytes[j - 1] == b'\r') {
72 j -= 1;
73 }
74
75 if j < 2 {
76 return false;
77 }
78
79 let candidate = &bytes[j - 2..j];
82 if !candidate.eq_ignore_ascii_case(b"AS") {
83 return false;
84 }
85 let before_as = j - 2;
87 if before_as > 0 && is_word(bytes[before_as - 1]) {
88 return false;
89 }
90 true
91}
92
93fn check_projection(
96 projection: &[SelectItem],
97 source: &str,
98 rule: &'static str,
99 diags: &mut Vec<Diagnostic>,
100 alias_counts: &mut std::collections::HashMap<String, usize>,
101) {
102 for item in projection {
103 if let SelectItem::ExprWithAlias { alias, .. } = item {
104 let alias_str = alias.value.as_str();
105 let key = alias_str.to_lowercase();
106 let occ = *alias_counts.get(&key).unwrap_or(&0);
107 *alias_counts.entry(key).or_insert(0) += 1;
108
109 if let Some(pos) = find_alias_occurrence(source, alias_str, occ) {
110 if !has_as_before(source, pos) {
111 let (line, col) = line_col(source, pos);
112 diags.push(Diagnostic {
113 rule,
114 message: format!(
115 "Column alias '{}' omits the AS keyword — use 'expression AS alias' for clarity",
116 alias_str
117 ),
118 line,
119 col,
120 });
121 }
122 }
123 }
124 }
125}
126
127fn check_select(
128 sel: &Select,
129 source: &str,
130 rule: &'static str,
131 diags: &mut Vec<Diagnostic>,
132 alias_counts: &mut std::collections::HashMap<String, usize>,
133) {
134 check_projection(&sel.projection, source, rule, diags, alias_counts);
135
136 for twj in &sel.from {
138 recurse_table_factor(&twj.relation, source, rule, diags, alias_counts);
139 for join in &twj.joins {
140 recurse_table_factor(&join.relation, source, rule, diags, alias_counts);
141 }
142 }
143}
144
145fn recurse_table_factor(
146 tf: &TableFactor,
147 source: &str,
148 rule: &'static str,
149 diags: &mut Vec<Diagnostic>,
150 alias_counts: &mut std::collections::HashMap<String, usize>,
151) {
152 if let TableFactor::Derived { subquery, .. } = tf {
153 check_query(subquery, source, rule, diags, alias_counts);
154 }
155}
156
157fn check_set_expr(
158 expr: &SetExpr,
159 source: &str,
160 rule: &'static str,
161 diags: &mut Vec<Diagnostic>,
162 alias_counts: &mut std::collections::HashMap<String, usize>,
163) {
164 match expr {
165 SetExpr::Select(sel) => check_select(sel, source, rule, diags, alias_counts),
166 SetExpr::Query(inner) => check_query(inner, source, rule, diags, alias_counts),
167 SetExpr::SetOperation { left, right, .. } => {
168 check_set_expr(left, source, rule, diags, alias_counts);
169 check_set_expr(right, source, rule, diags, alias_counts);
170 }
171 _ => {}
172 }
173}
174
175fn check_query(
176 query: &Query,
177 source: &str,
178 rule: &'static str,
179 diags: &mut Vec<Diagnostic>,
180 alias_counts: &mut std::collections::HashMap<String, usize>,
181) {
182 if let Some(with) = &query.with {
183 for cte in &with.cte_tables {
184 check_query(&cte.query, source, rule, diags, alias_counts);
185 }
186 }
187 check_set_expr(&query.body, source, rule, diags, alias_counts);
188}
189
190impl Rule for ExplicitColumnAlias {
191 fn name(&self) -> &'static str {
192 "Convention/ExplicitColumnAlias"
193 }
194
195 fn check(&self, ctx: &FileContext) -> Vec<Diagnostic> {
196 if !ctx.parse_errors.is_empty() {
197 return Vec::new();
198 }
199
200 let mut diags = Vec::new();
201 let mut alias_counts: std::collections::HashMap<String, usize> =
204 std::collections::HashMap::new();
205
206 for stmt in &ctx.statements {
207 if let Statement::Query(query) = stmt {
208 check_query(
209 query,
210 &ctx.source,
211 self.name(),
212 &mut diags,
213 &mut alias_counts,
214 );
215 }
216 }
217
218 diags
219 }
220}