Skip to main content

sqrust_rules/convention/
len_function.rs

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
9/// Returns the lowercase last-ident of a function's name, or empty string.
10fn 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
18/// Converts a byte offset in `source` to a 1-indexed (line, col) pair.
19fn 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
26/// Find the Nth occurrence (0-indexed) of `name` as a function call (case-insensitive)
27/// in `source`. Returns byte offset or 0 if not found.
28fn 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        // Word boundary before.
38        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                // Word boundary after (must be followed by '(' to be a function call).
52                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
70/// Walk an expression, pushing diagnostics for any LEN() function call.
71fn 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            // Recurse into function arguments.
96            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    // Projection.
153    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    // WHERE clause.
162    if let Some(selection) = &sel.selection {
163        walk_expr(selection, source, occurrence_counter, rule, diags);
164    }
165    // HAVING clause.
166    if let Some(having) = &sel.having {
167        walk_expr(having, source, occurrence_counter, rule, diags);
168    }
169    // Recurse into subqueries in FROM.
170    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        // AST-based — return empty if the file did not parse.
230        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}