sqrust_rules/lint/
call_statement.rs1use sqrust_core::{Diagnostic, FileContext, Rule};
2use std::collections::HashSet;
3
4pub struct CallStatement;
5
6impl Rule for CallStatement {
7 fn name(&self) -> &'static str {
8 "Lint/CallStatement"
9 }
10
11 fn check(&self, ctx: &FileContext) -> Vec<Diagnostic> {
12 let source = &ctx.source;
13 let skip = build_skip_set(source);
14 let mut diags = Vec::new();
15
16 for (_offset, line, col) in find_keyword_with_offset(source, "call", &skip) {
17 diags.push(Diagnostic {
18 rule: self.name(),
19 message: "CALL statement invokes a stored procedure; consider using a SELECT or \
20 DML statement for portable SQL"
21 .to_string(),
22 line,
23 col,
24 });
25 }
26
27 diags.sort_by_key(|d| (d.line, d.col));
28 diags
29 }
30}
31
32fn build_skip_set(source: &str) -> HashSet<usize> {
33 let mut skip = HashSet::new();
34 let bytes = source.as_bytes();
35 let len = bytes.len();
36 let mut i = 0;
37 while i < len {
38 if bytes[i] == b'\'' {
39 i += 1;
40 while i < len {
41 if bytes[i] == b'\'' {
42 if i + 1 < len && bytes[i + 1] == b'\'' {
43 skip.insert(i);
44 i += 2;
45 } else {
46 i += 1;
47 break;
48 }
49 } else {
50 skip.insert(i);
51 i += 1;
52 }
53 }
54 } else if i + 1 < len && bytes[i] == b'-' && bytes[i + 1] == b'-' {
55 while i < len && bytes[i] != b'\n' {
56 skip.insert(i);
57 i += 1;
58 }
59 } else {
60 i += 1;
61 }
62 }
63 skip
64}
65
66fn find_keyword_with_offset(
67 source: &str,
68 keyword: &str,
69 skip: &HashSet<usize>,
70) -> Vec<(usize, usize, usize)> {
71 let lower = source.to_lowercase();
72 let kw_len = keyword.len();
73 let bytes = lower.as_bytes();
74 let len = bytes.len();
75 let mut results = Vec::new();
76 let mut i = 0;
77 while i + kw_len <= len {
78 if !skip.contains(&i) && lower[i..].starts_with(keyword) {
79 let before_ok = i == 0
80 || {
81 let b = bytes[i - 1];
82 !b.is_ascii_alphanumeric() && b != b'_'
83 };
84 let after_pos = i + kw_len;
85 let after_ok = after_pos >= len
86 || {
87 let b = bytes[after_pos];
88 !b.is_ascii_alphanumeric() && b != b'_'
89 };
90 if before_ok && after_ok {
91 let (line, col) = offset_to_line_col(source, i);
92 results.push((i, line, col));
93 }
94 }
95 i += 1;
96 }
97 results
98}
99
100fn offset_to_line_col(source: &str, offset: usize) -> (usize, usize) {
101 let before = &source[..offset];
102 let line = before.chars().filter(|&c| c == '\n').count() + 1;
103 let col = before.rfind('\n').map(|p| offset - p - 1).unwrap_or(offset) + 1;
104 (line, col)
105}