1use std::collections::HashSet;
2
3use rowan::{NodeOrToken, TextRange, TextSize};
4use squawk_syntax::{SyntaxKind, SyntaxNode, SyntaxToken};
5
6use crate::{Linter, Rule, Violation};
7
8#[derive(Debug)]
9pub enum IgnoreKind {
10 File,
11 Line,
12}
13
14#[derive(Debug)]
15pub struct Ignore {
16 pub range: TextRange,
17 pub violation_names: HashSet<Rule>,
18 pub kind: IgnoreKind,
19}
20
21fn comment_body(token: &SyntaxToken) -> Option<(&str, TextRange)> {
22 let range = token.text_range();
23 if token.kind() == SyntaxKind::COMMENT {
24 let text = token.text();
25 if let Some(trimmed) = text.strip_prefix("--") {
26 if let Some(start) = range.start().checked_add(2.into()) {
27 let end = range.end();
28 let updated_range = TextRange::new(start, end);
29 return Some((trimmed, updated_range));
30 }
31 }
32 if let Some(trimmed) = text.strip_prefix("/*").and_then(|x| x.strip_suffix("*/")) {
33 if let Some(start) = range.start().checked_add(2.into()) {
34 if let Some(end) = range.end().checked_sub(2.into()) {
35 let updated_range = TextRange::new(start, end);
36 return Some((trimmed, updated_range));
37 }
38 }
39 }
40 }
41 None
42}
43
44pub const IGNORE_LINE_TEXT: &str = "squawk-ignore";
46pub const IGNORE_FILE_TEXT: &str = "squawk-ignore-file";
47
48pub fn ignore_rule_info(token: &SyntaxToken) -> Option<(&str, TextRange, IgnoreKind)> {
49 if let Some((comment_body, range)) = comment_body(token) {
50 let without_start = comment_body.trim_start();
51 let trim_start_size = comment_body.len() - without_start.len();
52 let trimmed_comment = without_start.trim_end();
53 let trim_end_size = without_start.len() - trimmed_comment.len();
54
55 for (prefix, kind) in [
56 (IGNORE_FILE_TEXT, IgnoreKind::File),
57 (IGNORE_LINE_TEXT, IgnoreKind::Line),
58 ] {
59 if let Some(without_prefix) = trimmed_comment.strip_prefix(prefix) {
60 let range = TextRange::new(
61 range.start() + TextSize::new((trim_start_size + prefix.len()) as u32),
62 range.end() - TextSize::new(trim_end_size as u32),
63 );
64 return Some((without_prefix, range, kind));
65 }
66 }
67 }
68 None
69}
70
71pub(crate) fn find_ignores(ctx: &mut Linter, file: &SyntaxNode) {
72 for event in file.preorder_with_tokens() {
73 match event {
74 rowan::WalkEvent::Enter(NodeOrToken::Token(token))
75 if token.kind() == SyntaxKind::COMMENT =>
76 {
77 if let Some((rule_names, range, kind)) = ignore_rule_info(&token) {
78 let mut set = HashSet::new();
79 let mut offset = 0usize;
80
81 for x in rule_names.split(",") {
85 if x.is_empty() {
86 continue;
87 }
88 if let Ok(violation_name) = Rule::try_from(x.trim()) {
89 set.insert(violation_name);
90 } else {
91 let without_start = x.trim_start();
92 let trim_start_size = x.len() - without_start.len();
93 let trimmed = without_start.trim_end();
94
95 let range = range.checked_add(TextSize::new(offset as u32)).unwrap();
96
97 let start = range.start() + TextSize::new(trim_start_size as u32);
98 let end = start + TextSize::new(trimmed.len() as u32);
99 let range = TextRange::new(start, end);
100
101 ctx.report(Violation::for_range(
102 Rule::UnusedIgnore,
103 format!("unknown name {trimmed}"),
104 range,
105 ));
106 }
107
108 offset += x.len() + 1;
109 }
110 ctx.ignore(Ignore {
111 range,
112 violation_names: set,
113 kind,
114 });
115 }
116 }
117 _ => (),
118 }
119 }
120}
121
122#[cfg(test)]
123mod test {
124
125 use insta::assert_debug_snapshot;
126
127 use super::IgnoreKind;
128 use crate::{Linter, Rule, find_ignores};
129
130 #[test]
131 fn single_ignore() {
132 let sql = r#"
133-- squawk-ignore ban-drop-column
134alter table t drop column c cascade;
135 "#;
136 let parse = squawk_syntax::SourceFile::parse(sql);
137
138 let mut linter = Linter::from([]);
139 find_ignores(&mut linter, &parse.syntax_node());
140
141 assert_eq!(linter.ignores.len(), 1);
142 let ignore = &linter.ignores[0];
143 assert!(ignore.violation_names.contains(&Rule::BanDropColumn));
144 }
145
146 #[test]
147 fn multiple_sql_comments_with_ignore_is_ok() {
148 let sql = "
149-- fooo bar
150-- buzz
151-- squawk-ignore prefer-robust-stmts
152create table x();
153
154select 1;
155";
156
157 let parse = squawk_syntax::SourceFile::parse(sql);
158 let mut linter = Linter::with_all_rules();
159 find_ignores(&mut linter, &parse.syntax_node());
160
161 assert_eq!(linter.ignores.len(), 1);
162 let ignore = &linter.ignores[0];
163 assert!(
164 ignore.violation_names.contains(&Rule::PreferRobustStmts),
165 "Make sure we picked up the ignore"
166 );
167
168 let errors = linter.lint(&parse, sql);
169
170 assert_eq!(
171 errors,
172 vec![],
173 "We shouldn't have any errors because we have the ignore setup"
174 );
175 }
176
177 #[test]
178 fn single_ignore_c_style_comment() {
179 let sql = r#"
180/* squawk-ignore ban-drop-column */
181alter table t drop column c cascade;
182 "#;
183 let parse = squawk_syntax::SourceFile::parse(sql);
184
185 let mut linter = Linter::from([]);
186
187 find_ignores(&mut linter, &parse.syntax_node());
188
189 assert_eq!(linter.ignores.len(), 1);
190 let ignore = &linter.ignores[0];
191 assert!(ignore.violation_names.contains(&Rule::BanDropColumn));
192 }
193
194 #[test]
195 fn multi_ignore() {
196 let sql = r#"
197-- squawk-ignore ban-drop-column, renaming-column,ban-drop-database
198alter table t drop column c cascade;
199 "#;
200 let parse = squawk_syntax::SourceFile::parse(sql);
201
202 let mut linter = Linter::from([]);
203
204 find_ignores(&mut linter, &parse.syntax_node());
205
206 assert_eq!(linter.ignores.len(), 1);
207 let ignore = &linter.ignores[0];
208 assert!(ignore.violation_names.contains(&Rule::BanDropColumn));
209 assert!(ignore.violation_names.contains(&Rule::RenamingColumn));
210 assert!(ignore.violation_names.contains(&Rule::BanDropDatabase));
211 }
212
213 #[test]
214 fn multi_ignore_c_style_comment() {
215 let sql = r#"
216/* squawk-ignore ban-drop-column, renaming-column,ban-drop-database */
217alter table t drop column c cascade;
218 "#;
219 let parse = squawk_syntax::SourceFile::parse(sql);
220
221 let mut linter = Linter::from([]);
222
223 find_ignores(&mut linter, &parse.syntax_node());
224
225 assert_eq!(linter.ignores.len(), 1);
226 let ignore = &linter.ignores[0];
227 assert!(ignore.violation_names.contains(&Rule::BanDropColumn));
228 assert!(ignore.violation_names.contains(&Rule::RenamingColumn));
229 assert!(ignore.violation_names.contains(&Rule::BanDropDatabase));
230 }
231
232 #[test]
233 fn ignore_multiple_stmts() {
234 let mut linter = Linter::with_all_rules();
235 let sql = r#"
236-- squawk-ignore ban-char-field,prefer-robust-stmts
237alter table t add column c char;
238
239ALTER TABLE foo
240-- squawk-ignore adding-field-with-default,prefer-robust-stmts
241ADD COLUMN bar numeric GENERATED
242 ALWAYS AS (bar + baz) STORED;
243
244-- squawk-ignore prefer-robust-stmts
245create table users (
246);
247"#;
248
249 let parse = squawk_syntax::SourceFile::parse(sql);
250 let errors = linter.lint(&parse, sql);
251 assert_eq!(errors.len(), 0);
252 }
253
254 #[test]
255 fn starting_line_aka_zero() {
256 let mut linter = Linter::with_all_rules();
257 let sql = r#"alter table t add column c char;"#;
258
259 let parse = squawk_syntax::SourceFile::parse(sql);
260 let errors = linter.lint(&parse, sql);
261 assert_eq!(errors.len(), 1);
262 }
263
264 #[test]
265 fn regression_unknown_name() {
266 let mut linter = Linter::with_all_rules();
267 let sql = r#"
268-- squawk-ignore prefer-robust-stmts
269create table test_table (
270 -- squawk-ignore prefer-timestamp-tz
271 created_at timestamp default current_timestamp,
272 other_field text
273);
274 "#;
275
276 let parse = squawk_syntax::SourceFile::parse(sql);
277 let errors = linter.lint(&parse, sql);
278 assert_eq!(errors.len(), 0);
279 }
280
281 #[test]
282 fn file_single_rule() {
283 let sql = r#"
284-- squawk-ignore-file ban-drop-column
285alter table t drop column c cascade;
286 "#;
287 let parse = squawk_syntax::SourceFile::parse(sql);
288
289 let mut linter = Linter::from([]);
290 find_ignores(&mut linter, &parse.syntax_node());
291
292 assert_eq!(linter.ignores.len(), 1);
293 let ignore = &linter.ignores[0];
294 assert!(ignore.violation_names.contains(&Rule::BanDropColumn));
295 assert!(matches!(ignore.kind, IgnoreKind::File));
296 }
297
298 #[test]
299 fn file_ignore_with_all_rules() {
300 let sql = r#"
301-- squawk-ignore-file
302alter table t drop column c cascade;
303 "#;
304 let parse = squawk_syntax::SourceFile::parse(sql);
305
306 let mut linter = Linter::from([]);
307 find_ignores(&mut linter, &parse.syntax_node());
308
309 assert_eq!(linter.ignores.len(), 1);
310 let ignore = &linter.ignores[0];
311 assert!(matches!(ignore.kind, IgnoreKind::File));
312 assert!(ignore.violation_names.is_empty());
313
314 let errors: Vec<_> = linter
315 .lint(&parse, sql)
316 .into_iter()
317 .map(|x| x.code)
318 .collect();
319 assert!(errors.is_empty());
320 }
321
322 #[test]
323 fn file_ignore_with_multiple_rules() {
324 let sql = r#"
325-- squawk-ignore-file ban-drop-column, renaming-column
326alter table t drop column c cascade;
327 "#;
328 let parse = squawk_syntax::SourceFile::parse(sql);
329
330 let mut linter = Linter::from([]);
331 find_ignores(&mut linter, &parse.syntax_node());
332
333 assert_eq!(linter.ignores.len(), 1);
334 let ignore = &linter.ignores[0];
335 assert!(ignore.violation_names.contains(&Rule::BanDropColumn));
336 assert!(ignore.violation_names.contains(&Rule::RenamingColumn));
337 assert!(matches!(ignore.kind, IgnoreKind::File));
338 }
339
340 #[test]
341 fn file_ignore_anywhere_works() {
342 let sql = r#"
343alter table t add column x int;
344-- squawk-ignore-file ban-drop-column
345alter table t drop column c cascade;
346 "#;
347 let parse = squawk_syntax::SourceFile::parse(sql);
348
349 let mut linter = Linter::from([]);
350 find_ignores(&mut linter, &parse.syntax_node());
351
352 assert_eq!(linter.ignores.len(), 1);
353 let ignore = &linter.ignores[0];
354 assert!(ignore.violation_names.contains(&Rule::BanDropColumn));
355 assert!(matches!(ignore.kind, IgnoreKind::File));
356 }
357
358 #[test]
359 fn file_ignore_c_style_comment() {
360 let sql = r#"
361/* squawk-ignore-file ban-drop-column */
362alter table t drop column c cascade;
363 "#;
364 let parse = squawk_syntax::SourceFile::parse(sql);
365
366 let mut linter = Linter::from([]);
367 find_ignores(&mut linter, &parse.syntax_node());
368
369 assert_eq!(linter.ignores.len(), 1);
370 let ignore = &linter.ignores[0];
371 assert!(ignore.violation_names.contains(&Rule::BanDropColumn));
372 assert!(matches!(ignore.kind, IgnoreKind::File));
373 }
374
375 #[test]
376 fn file_level_only_ignores_specific_rules() {
377 let mut linter = Linter::with_all_rules();
378 let sql = r#"
379-- squawk-ignore-file ban-drop-column
380alter table t drop column c cascade;
381alter table t2 drop column c2 cascade;
382 "#;
383
384 let parse = squawk_syntax::SourceFile::parse(sql);
385 let errors: Vec<_> = linter
386 .lint(&parse, sql)
387 .into_iter()
388 .map(|x| x.code)
389 .collect();
390
391 assert_debug_snapshot!(errors, @r"
392 [
393 PreferRobustStmts,
394 PreferRobustStmts,
395 ]
396 ");
397 }
398
399 #[test]
400 fn file_ignore_at_end_of_file_is_fine() {
401 let mut linter = Linter::with_all_rules();
402 let sql = r#"
403alter table t drop column c cascade;
404alter table t2 drop column c2 cascade;
405-- squawk-ignore-file ban-drop-column
406 "#;
407
408 let parse = squawk_syntax::SourceFile::parse(sql);
409 let errors: Vec<_> = linter
410 .lint(&parse, sql)
411 .into_iter()
412 .map(|x| x.code)
413 .collect();
414
415 assert_debug_snapshot!(errors, @r"
416 [
417 PreferRobustStmts,
418 PreferRobustStmts,
419 ]
420 ");
421 }
422}