Skip to main content

sqrust_rules/lint/
execute_statement.rs

1use 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        // Check for EXECUTE (longer keyword must be checked before EXEC to avoid double-matching)
17        for (offset, line, col) in find_keyword_with_offset(source, "execute", &skip) {
18            // Ensure we don't also match this as EXEC (EXECUTE already caught it)
19            let _ = offset; // offset used for dedup below
20            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        // Collect EXECUTE offsets to avoid flagging EXEC at the same position
31        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            // Skip if this position is covered by an EXECUTE match
38            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}