Skip to main content

sqrust_rules/lint/
explain_statement.rs

1use sqrust_core::{Diagnostic, FileContext, Rule};
2use std::collections::HashSet;
3
4pub struct ExplainStatement;
5
6impl Rule for ExplainStatement {
7    fn name(&self) -> &'static str {
8        "Lint/ExplainStatement"
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, "explain", &skip) {
17            diags.push(Diagnostic {
18                rule: self.name(),
19                message: "EXPLAIN statement should not appear in production SQL files; \
20                          remove before committing"
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}