Skip to main content

sqrust_rules/lint/
lock_table_statement.rs

1use sqrust_core::{Diagnostic, FileContext, Rule};
2use std::collections::HashSet;
3
4pub struct LockTableStatement;
5
6impl Rule for LockTableStatement {
7    fn name(&self) -> &'static str {
8        "Lint/LockTableStatement"
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        let lower = source.to_lowercase();
17
18        for (offset, line, col) in find_keyword_with_offset(source, "lock", &skip) {
19            // Find the next non-whitespace token after LOCK
20            let after_lock = offset + "lock".len();
21            let rest = &lower[after_lock..];
22            let trimmed = rest.trim_start();
23            if trimmed.starts_with("tables") || trimmed.starts_with("table") {
24                // Make sure "table" or "tables" is followed by a word boundary
25                let token = if trimmed.starts_with("tables") {
26                    "tables"
27                } else {
28                    "table"
29                };
30                let after_token = token.len();
31                let after_ok = after_token >= trimmed.len()
32                    || {
33                        let b = trimmed.as_bytes()[after_token];
34                        !b.is_ascii_alphanumeric() && b != b'_'
35                    };
36                if after_ok {
37                    diags.push(Diagnostic {
38                        rule: self.name(),
39                        message: "LOCK TABLE acquires an exclusive lock and blocks concurrent \
40                                  sessions; use transactions or application-level locking instead"
41                            .to_string(),
42                        line,
43                        col,
44                    });
45                }
46            }
47        }
48
49        diags.sort_by_key(|d| (d.line, d.col));
50        diags
51    }
52}
53
54fn build_skip_set(source: &str) -> HashSet<usize> {
55    let mut skip = HashSet::new();
56    let bytes = source.as_bytes();
57    let len = bytes.len();
58    let mut i = 0;
59    while i < len {
60        if bytes[i] == b'\'' {
61            i += 1;
62            while i < len {
63                if bytes[i] == b'\'' {
64                    if i + 1 < len && bytes[i + 1] == b'\'' {
65                        skip.insert(i);
66                        i += 2;
67                    } else {
68                        i += 1;
69                        break;
70                    }
71                } else {
72                    skip.insert(i);
73                    i += 1;
74                }
75            }
76        } else if i + 1 < len && bytes[i] == b'-' && bytes[i + 1] == b'-' {
77            while i < len && bytes[i] != b'\n' {
78                skip.insert(i);
79                i += 1;
80            }
81        } else {
82            i += 1;
83        }
84    }
85    skip
86}
87
88fn find_keyword_with_offset(
89    source: &str,
90    keyword: &str,
91    skip: &HashSet<usize>,
92) -> Vec<(usize, usize, usize)> {
93    let lower = source.to_lowercase();
94    let kw_len = keyword.len();
95    let bytes = lower.as_bytes();
96    let len = bytes.len();
97    let mut results = Vec::new();
98    let mut i = 0;
99    while i + kw_len <= len {
100        if !skip.contains(&i) && lower[i..].starts_with(keyword) {
101            let before_ok = i == 0
102                || {
103                    let b = bytes[i - 1];
104                    !b.is_ascii_alphanumeric() && b != b'_'
105                };
106            let after_pos = i + kw_len;
107            let after_ok = after_pos >= len
108                || {
109                    let b = bytes[after_pos];
110                    !b.is_ascii_alphanumeric() && b != b'_'
111                };
112            if before_ok && after_ok {
113                let (line, col) = offset_to_line_col(source, i);
114                results.push((i, line, col));
115            }
116        }
117        i += 1;
118    }
119    results
120}
121
122fn offset_to_line_col(source: &str, offset: usize) -> (usize, usize) {
123    let before = &source[..offset];
124    let line = before.chars().filter(|&c| c == '\n').count() + 1;
125    let col = before.rfind('\n').map(|p| offset - p - 1).unwrap_or(offset) + 1;
126    (line, col)
127}