sqrust_rules/lint/
lock_table_statement.rs1use 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 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 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}