sqrust_rules/convention/
no_null_default.rs1use sqrust_core::{Diagnostic, FileContext, Rule};
2
3pub struct NoNullDefault;
4
5fn line_col(source: &str, offset: usize) -> (usize, usize) {
7 let before = &source[..offset];
8 let line = before.chars().filter(|&c| c == '\n').count() + 1;
9 let col = before.rfind('\n').map(|p| offset - p - 1).unwrap_or(offset) + 1;
10 (line, col)
11}
12
13#[inline]
15fn is_word_char(ch: u8) -> bool {
16 ch.is_ascii_alphanumeric() || ch == b'_'
17}
18
19#[inline]
20fn is_whitespace(ch: u8) -> bool {
21 ch == b' ' || ch == b'\t' || ch == b'\n' || ch == b'\r'
22}
23
24fn build_skip(bytes: &[u8]) -> Vec<bool> {
26 let len = bytes.len();
27 let mut skip = vec![false; len];
28 let mut i = 0;
29
30 while i < len {
31 if i + 1 < len && bytes[i] == b'-' && bytes[i + 1] == b'-' {
33 skip[i] = true;
34 skip[i + 1] = true;
35 i += 2;
36 while i < len && bytes[i] != b'\n' {
37 skip[i] = true;
38 i += 1;
39 }
40 continue;
41 }
42
43 if i + 1 < len && bytes[i] == b'/' && bytes[i + 1] == b'*' {
45 skip[i] = true;
46 skip[i + 1] = true;
47 i += 2;
48 while i < len {
49 if i + 1 < len && bytes[i] == b'*' && bytes[i + 1] == b'/' {
50 skip[i] = true;
51 skip[i + 1] = true;
52 i += 2;
53 break;
54 }
55 skip[i] = true;
56 i += 1;
57 }
58 continue;
59 }
60
61 if bytes[i] == b'\'' {
63 skip[i] = true;
64 i += 1;
65 while i < len {
66 if bytes[i] == b'\'' {
67 skip[i] = true;
68 i += 1;
69 if i < len && bytes[i] == b'\'' {
70 skip[i] = true;
71 i += 1;
72 continue;
73 }
74 break;
75 }
76 skip[i] = true;
77 i += 1;
78 }
79 continue;
80 }
81
82 if bytes[i] == b'"' {
84 skip[i] = true;
85 i += 1;
86 while i < len && bytes[i] != b'"' {
87 skip[i] = true;
88 i += 1;
89 }
90 if i < len {
91 skip[i] = true;
92 i += 1;
93 }
94 continue;
95 }
96
97 if bytes[i] == b'`' {
99 skip[i] = true;
100 i += 1;
101 while i < len && bytes[i] != b'`' {
102 skip[i] = true;
103 i += 1;
104 }
105 if i < len {
106 skip[i] = true;
107 i += 1;
108 }
109 continue;
110 }
111
112 i += 1;
113 }
114
115 skip
116}
117
118fn matches_keyword_at(bytes: &[u8], len: usize, skip: &[bool], pos: usize, keyword: &[u8]) -> bool {
121 let kw_len = keyword.len();
122 if pos + kw_len > len {
123 return false;
124 }
125 (0..kw_len).all(|k| !skip[pos + k] && bytes[pos + k].eq_ignore_ascii_case(&keyword[k]))
126}
127
128fn find_default_null_offsets(source: &str, skip: &[bool]) -> Vec<usize> {
131 let bytes = source.as_bytes();
132 let len = bytes.len();
133 let mut results = Vec::new();
134 let mut i = 0;
135
136 while i < len {
137 if skip[i] {
138 i += 1;
139 continue;
140 }
141
142 if !matches_keyword_at(bytes, len, skip, i, b"DEFAULT") {
144 i += 1;
145 continue;
146 }
147
148 if i > 0 && is_word_char(bytes[i - 1]) {
150 i += 1;
151 continue;
152 }
153
154 let default_start = i;
155 let default_end = i + 7; if default_end < len && is_word_char(bytes[default_end]) {
159 i += 1;
160 continue;
161 }
162
163 let mut j = default_end;
165 while j < len && !skip[j] && is_whitespace(bytes[j]) {
166 j += 1;
167 }
168
169 if j == default_end {
171 i += 1;
172 continue;
173 }
174
175 if !matches_keyword_at(bytes, len, skip, j, b"NULL") {
177 i += 1;
178 continue;
179 }
180
181 if j > 0 && is_word_char(bytes[j - 1]) {
183 i += 1;
184 continue;
185 }
186
187 let null_end = j + 4; if null_end < len && is_word_char(bytes[null_end]) {
191 i += 1;
192 continue;
193 }
194
195 results.push(default_start);
196 i = null_end;
197 }
198
199 results
200}
201
202impl Rule for NoNullDefault {
203 fn name(&self) -> &'static str {
204 "Convention/NoNullDefault"
205 }
206
207 fn check(&self, ctx: &FileContext) -> Vec<Diagnostic> {
208 let source = &ctx.source;
209 let bytes = source.as_bytes();
210 let skip = build_skip(bytes);
211 let offsets = find_default_null_offsets(source, &skip);
212
213 offsets
214 .into_iter()
215 .map(|offset| {
216 let (line, col) = line_col(source, offset);
217 Diagnostic {
218 rule: self.name(),
219 message: "DEFAULT NULL is redundant; omit it to use the implicit default"
220 .to_string(),
221 line,
222 col,
223 }
224 })
225 .collect()
226 }
227}