1use sqrust_core::{Diagnostic, FileContext, Rule};
2use sqlparser::ast::{
3 Expr, FunctionArg, FunctionArgExpr, FunctionArguments, Query, Select, SelectItem, SetExpr,
4 Statement, TableFactor,
5};
6
7pub struct LenFunction;
8
9fn func_name_lower(func: &sqlparser::ast::Function) -> String {
11 func.name
12 .0
13 .last()
14 .map(|ident| ident.value.to_lowercase())
15 .unwrap_or_default()
16}
17
18fn line_col(source: &str, offset: usize) -> (usize, usize) {
20 let before = &source[..offset];
21 let line = before.chars().filter(|&c| c == '\n').count() + 1;
22 let col = before.rfind('\n').map(|p| offset - p - 1).unwrap_or(offset) + 1;
23 (line, col)
24}
25
26fn find_occurrence(source: &str, name: &str, occurrence: usize) -> usize {
29 let bytes = source.as_bytes();
30 let name_upper: Vec<u8> = name.bytes().map(|b| b.to_ascii_uppercase()).collect();
31 let name_len = name_upper.len();
32 let len = bytes.len();
33 let mut count = 0usize;
34 let mut i = 0;
35
36 while i + name_len <= len {
37 let before_ok = i == 0
39 || {
40 let b = bytes[i - 1];
41 !b.is_ascii_alphanumeric() && b != b'_'
42 };
43
44 if before_ok {
45 let matches = bytes[i..i + name_len]
46 .iter()
47 .zip(name_upper.iter())
48 .all(|(&a, &b)| a.eq_ignore_ascii_case(&b));
49
50 if matches {
51 let after = i + name_len;
53 let after_ok = after < len && bytes[after] == b'(';
54
55 if after_ok {
56 if count == occurrence {
57 return i;
58 }
59 count += 1;
60 }
61 }
62 }
63
64 i += 1;
65 }
66
67 0
68}
69
70fn walk_expr(
72 expr: &Expr,
73 source: &str,
74 occurrence_counter: &mut usize,
75 rule: &'static str,
76 diags: &mut Vec<Diagnostic>,
77) {
78 match expr {
79 Expr::Function(func) => {
80 let lower = func_name_lower(func);
81 if lower == "len" {
82 let occ = *occurrence_counter;
83 *occurrence_counter += 1;
84
85 let offset = find_occurrence(source, "LEN", occ);
86 let (line, col) = line_col(source, offset);
87 diags.push(Diagnostic {
88 rule,
89 message: "LEN() is SQL Server-specific — use LENGTH() for portable string length".to_string(),
90 line,
91 col,
92 });
93 }
94
95 if let FunctionArguments::List(list) = &func.args {
97 for arg in &list.args {
98 let inner_expr = match arg {
99 FunctionArg::Named { arg, .. }
100 | FunctionArg::Unnamed(arg)
101 | FunctionArg::ExprNamed { arg, .. } => match arg {
102 FunctionArgExpr::Expr(e) => Some(e),
103 _ => None,
104 },
105 };
106 if let Some(e) = inner_expr {
107 walk_expr(e, source, occurrence_counter, rule, diags);
108 }
109 }
110 }
111 }
112 Expr::BinaryOp { left, right, .. } => {
113 walk_expr(left, source, occurrence_counter, rule, diags);
114 walk_expr(right, source, occurrence_counter, rule, diags);
115 }
116 Expr::UnaryOp { expr: inner, .. } => {
117 walk_expr(inner, source, occurrence_counter, rule, diags);
118 }
119 Expr::Nested(inner) => {
120 walk_expr(inner, source, occurrence_counter, rule, diags);
121 }
122 Expr::Case {
123 operand,
124 conditions,
125 results,
126 else_result,
127 } => {
128 if let Some(op) = operand {
129 walk_expr(op, source, occurrence_counter, rule, diags);
130 }
131 for c in conditions {
132 walk_expr(c, source, occurrence_counter, rule, diags);
133 }
134 for r in results {
135 walk_expr(r, source, occurrence_counter, rule, diags);
136 }
137 if let Some(e) = else_result {
138 walk_expr(e, source, occurrence_counter, rule, diags);
139 }
140 }
141 _ => {}
142 }
143}
144
145fn check_select(
146 sel: &Select,
147 source: &str,
148 occurrence_counter: &mut usize,
149 rule: &'static str,
150 diags: &mut Vec<Diagnostic>,
151) {
152 for item in &sel.projection {
154 match item {
155 SelectItem::UnnamedExpr(e) | SelectItem::ExprWithAlias { expr: e, .. } => {
156 walk_expr(e, source, occurrence_counter, rule, diags);
157 }
158 _ => {}
159 }
160 }
161 if let Some(selection) = &sel.selection {
163 walk_expr(selection, source, occurrence_counter, rule, diags);
164 }
165 if let Some(having) = &sel.having {
167 walk_expr(having, source, occurrence_counter, rule, diags);
168 }
169 for twj in &sel.from {
171 recurse_table_factor(&twj.relation, source, occurrence_counter, rule, diags);
172 for join in &twj.joins {
173 recurse_table_factor(&join.relation, source, occurrence_counter, rule, diags);
174 }
175 }
176}
177
178fn recurse_table_factor(
179 tf: &TableFactor,
180 source: &str,
181 occurrence_counter: &mut usize,
182 rule: &'static str,
183 diags: &mut Vec<Diagnostic>,
184) {
185 if let TableFactor::Derived { subquery, .. } = tf {
186 check_query(subquery, source, occurrence_counter, rule, diags);
187 }
188}
189
190fn check_set_expr(
191 expr: &SetExpr,
192 source: &str,
193 occurrence_counter: &mut usize,
194 rule: &'static str,
195 diags: &mut Vec<Diagnostic>,
196) {
197 match expr {
198 SetExpr::Select(sel) => check_select(sel, source, occurrence_counter, rule, diags),
199 SetExpr::Query(inner) => check_query(inner, source, occurrence_counter, rule, diags),
200 SetExpr::SetOperation { left, right, .. } => {
201 check_set_expr(left, source, occurrence_counter, rule, diags);
202 check_set_expr(right, source, occurrence_counter, rule, diags);
203 }
204 _ => {}
205 }
206}
207
208fn check_query(
209 query: &Query,
210 source: &str,
211 occurrence_counter: &mut usize,
212 rule: &'static str,
213 diags: &mut Vec<Diagnostic>,
214) {
215 if let Some(with) = &query.with {
216 for cte in &with.cte_tables {
217 check_query(&cte.query, source, occurrence_counter, rule, diags);
218 }
219 }
220 check_set_expr(&query.body, source, occurrence_counter, rule, diags);
221}
222
223impl Rule for LenFunction {
224 fn name(&self) -> &'static str {
225 "Convention/LenFunction"
226 }
227
228 fn check(&self, ctx: &FileContext) -> Vec<Diagnostic> {
229 if !ctx.parse_errors.is_empty() {
231 return Vec::new();
232 }
233
234 let mut diags = Vec::new();
235 let mut occurrence_counter = 0usize;
236
237 for stmt in &ctx.statements {
238 if let Statement::Query(query) = stmt {
239 check_query(
240 query,
241 &ctx.source,
242 &mut occurrence_counter,
243 self.name(),
244 &mut diags,
245 );
246 }
247 }
248
249 diags
250 }
251}