sqrust_rules/lint/
execute_statement.rs1use sqrust_core::{Diagnostic, FileContext, Rule};
2use std::collections::HashSet;
3
4pub struct ExecuteStatement;
5
6impl Rule for ExecuteStatement {
7 fn name(&self) -> &'static str {
8 "Lint/ExecuteStatement"
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, "execute", &skip) {
18 let _ = offset; diags.push(Diagnostic {
21 rule: self.name(),
22 message: "EXECUTE statement is dialect-specific (SQL Server/PostgreSQL); \
23 dynamic SQL execution should be avoided in portable queries"
24 .to_string(),
25 line,
26 col,
27 });
28 }
29
30 let execute_offsets: HashSet<usize> = find_keyword_with_offset(source, "execute", &skip)
32 .into_iter()
33 .map(|(off, _, _)| off)
34 .collect();
35
36 for (offset, line, col) in find_keyword_with_offset(source, "exec", &skip) {
37 if execute_offsets.contains(&offset) {
39 continue;
40 }
41 diags.push(Diagnostic {
42 rule: self.name(),
43 message: "EXEC is a SQL Server shorthand for EXECUTE; use standard SQL \
44 instead of dynamic execution"
45 .to_string(),
46 line,
47 col,
48 });
49 }
50
51 diags.sort_by_key(|d| (d.line, d.col));
52 diags
53 }
54}
55
56fn build_skip_set(source: &str) -> HashSet<usize> {
57 let mut skip = HashSet::new();
58 let bytes = source.as_bytes();
59 let len = bytes.len();
60 let mut i = 0;
61 while i < len {
62 if bytes[i] == b'\'' {
63 i += 1;
64 while i < len {
65 if bytes[i] == b'\'' {
66 if i + 1 < len && bytes[i + 1] == b'\'' {
67 skip.insert(i);
68 i += 2;
69 } else {
70 i += 1;
71 break;
72 }
73 } else {
74 skip.insert(i);
75 i += 1;
76 }
77 }
78 } else if i + 1 < len && bytes[i] == b'-' && bytes[i + 1] == b'-' {
79 while i < len && bytes[i] != b'\n' {
80 skip.insert(i);
81 i += 1;
82 }
83 } else {
84 i += 1;
85 }
86 }
87 skip
88}
89
90fn find_keyword_with_offset(
91 source: &str,
92 keyword: &str,
93 skip: &HashSet<usize>,
94) -> Vec<(usize, usize, usize)> {
95 let lower = source.to_lowercase();
96 let kw_len = keyword.len();
97 let bytes = lower.as_bytes();
98 let len = bytes.len();
99 let mut results = Vec::new();
100 let mut i = 0;
101 while i + kw_len <= len {
102 if !skip.contains(&i) && lower[i..].starts_with(keyword) {
103 let before_ok = i == 0
104 || {
105 let b = bytes[i - 1];
106 !b.is_ascii_alphanumeric() && b != b'_'
107 };
108 let after_pos = i + kw_len;
109 let after_ok = after_pos >= len
110 || {
111 let b = bytes[after_pos];
112 !b.is_ascii_alphanumeric() && b != b'_'
113 };
114 if before_ok && after_ok {
115 let (line, col) = offset_to_line_col(source, i);
116 results.push((i, line, col));
117 }
118 }
119 i += 1;
120 }
121 results
122}
123
124fn offset_to_line_col(source: &str, offset: usize) -> (usize, usize) {
125 let before = &source[..offset];
126 let line = before.chars().filter(|&c| c == '\n').count() + 1;
127 let col = before.rfind('\n').map(|p| offset - p - 1).unwrap_or(offset) + 1;
128 (line, col)
129}