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, require-timeout-settings
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,require-timeout-settings
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_debug_snapshot!(errors, @r#"
262 [
263 Violation {
264 code: RequireTimeoutSettings,
265 message: "Missing `set lock_timeout` before potentially slow operations",
266 text_range: 0..31,
267 help: Some(
268 "Configure a `lock_timeout` before this statement.",
269 ),
270 fix: Some(
271 Fix {
272 title: "Add lock timeout",
273 edits: [
274 Edit {
275 text_range: 0..0,
276 text: Some(
277 "set lock_timeout = '1s';\n",
278 ),
279 },
280 ],
281 },
282 ),
283 },
284 Violation {
285 code: RequireTimeoutSettings,
286 message: "Missing `set statement_timeout` before potentially slow operations",
287 text_range: 0..31,
288 help: Some(
289 "Configure a `statement_timeout` before this statement",
290 ),
291 fix: Some(
292 Fix {
293 title: "Add statement timeout",
294 edits: [
295 Edit {
296 text_range: 0..0,
297 text: Some(
298 "set statement_timeout = '5s';\n",
299 ),
300 },
301 ],
302 },
303 ),
304 },
305 Violation {
306 code: BanCharField,
307 message: "Using `character` is likely a mistake and should almost always be replaced by `text` or `varchar`.",
308 text_range: 27..31,
309 help: None,
310 fix: Some(
311 Fix {
312 title: "Replace with `text`",
313 edits: [
314 Edit {
315 text_range: 27..31,
316 text: Some(
317 "text",
318 ),
319 },
320 ],
321 },
322 ),
323 },
324 ]
325 "#);
326 }
327
328 #[test]
329 fn regression_unknown_name() {
330 let mut linter = Linter::with_all_rules();
331 let sql = r#"
332-- squawk-ignore prefer-robust-stmts, require-timeout-settings
333create table test_table (
334 -- squawk-ignore prefer-timestamp-tz
335 created_at timestamp default current_timestamp,
336 other_field text
337);
338 "#;
339
340 let parse = squawk_syntax::SourceFile::parse(sql);
341 let errors = linter.lint(&parse, sql);
342 assert_debug_snapshot!(errors, @"[]");
343 assert_eq!(errors.len(), 0);
344 }
345
346 #[test]
347 fn file_single_rule() {
348 let sql = r#"
349-- squawk-ignore-file ban-drop-column
350alter table t drop column c cascade;
351 "#;
352 let parse = squawk_syntax::SourceFile::parse(sql);
353
354 let mut linter = Linter::from([]);
355 find_ignores(&mut linter, &parse.syntax_node());
356
357 assert_eq!(linter.ignores.len(), 1);
358 let ignore = &linter.ignores[0];
359 assert!(ignore.violation_names.contains(&Rule::BanDropColumn));
360 assert!(matches!(ignore.kind, IgnoreKind::File));
361 }
362
363 #[test]
364 fn file_ignore_with_all_rules() {
365 let sql = r#"
366-- squawk-ignore-file
367alter table t drop column c cascade;
368 "#;
369 let parse = squawk_syntax::SourceFile::parse(sql);
370
371 let mut linter = Linter::from([]);
372 find_ignores(&mut linter, &parse.syntax_node());
373
374 assert_eq!(linter.ignores.len(), 1);
375 let ignore = &linter.ignores[0];
376 assert!(matches!(ignore.kind, IgnoreKind::File));
377 assert!(ignore.violation_names.is_empty());
378
379 let errors: Vec<_> = linter
380 .lint(&parse, sql)
381 .into_iter()
382 .map(|x| x.code)
383 .collect();
384 assert!(errors.is_empty());
385 }
386
387 #[test]
388 fn file_ignore_with_multiple_rules() {
389 let sql = r#"
390-- squawk-ignore-file ban-drop-column, renaming-column
391alter table t drop column c cascade;
392 "#;
393 let parse = squawk_syntax::SourceFile::parse(sql);
394
395 let mut linter = Linter::from([]);
396 find_ignores(&mut linter, &parse.syntax_node());
397
398 assert_eq!(linter.ignores.len(), 1);
399 let ignore = &linter.ignores[0];
400 assert!(ignore.violation_names.contains(&Rule::BanDropColumn));
401 assert!(ignore.violation_names.contains(&Rule::RenamingColumn));
402 assert!(matches!(ignore.kind, IgnoreKind::File));
403 }
404
405 #[test]
406 fn file_ignore_anywhere_works() {
407 let sql = r#"
408alter table t add column x int;
409-- squawk-ignore-file ban-drop-column
410alter table t drop column c cascade;
411 "#;
412 let parse = squawk_syntax::SourceFile::parse(sql);
413
414 let mut linter = Linter::from([]);
415 find_ignores(&mut linter, &parse.syntax_node());
416
417 assert_eq!(linter.ignores.len(), 1);
418 let ignore = &linter.ignores[0];
419 assert!(ignore.violation_names.contains(&Rule::BanDropColumn));
420 assert!(matches!(ignore.kind, IgnoreKind::File));
421 }
422
423 #[test]
424 fn file_ignore_c_style_comment() {
425 let sql = r#"
426/* squawk-ignore-file ban-drop-column */
427alter table t drop column c cascade;
428 "#;
429 let parse = squawk_syntax::SourceFile::parse(sql);
430
431 let mut linter = Linter::from([]);
432 find_ignores(&mut linter, &parse.syntax_node());
433
434 assert_eq!(linter.ignores.len(), 1);
435 let ignore = &linter.ignores[0];
436 assert!(ignore.violation_names.contains(&Rule::BanDropColumn));
437 assert!(matches!(ignore.kind, IgnoreKind::File));
438 }
439
440 #[test]
441 fn file_level_only_ignores_specific_rules() {
442 let mut linter = Linter::with_all_rules();
443 let sql = r#"
444-- squawk-ignore-file ban-drop-column
445alter table t drop column c cascade;
446alter table t2 drop column c2 cascade;
447 "#;
448
449 let parse = squawk_syntax::SourceFile::parse(sql);
450 let errors: Vec<_> = linter
451 .lint(&parse, sql)
452 .into_iter()
453 .map(|x| x.code)
454 .collect();
455
456 assert_debug_snapshot!(errors, @r"
457 [
458 RequireTimeoutSettings,
459 RequireTimeoutSettings,
460 PreferRobustStmts,
461 PreferRobustStmts,
462 ]
463 ");
464 }
465
466 #[test]
467 fn file_ignore_at_end_of_file_is_fine() {
468 let mut linter = Linter::with_all_rules();
469 let sql = r#"
470alter table t drop column c cascade;
471alter table t2 drop column c2 cascade;
472-- squawk-ignore-file ban-drop-column
473 "#;
474
475 let parse = squawk_syntax::SourceFile::parse(sql);
476 let errors: Vec<_> = linter
477 .lint(&parse, sql)
478 .into_iter()
479 .map(|x| x.code)
480 .collect();
481
482 assert_debug_snapshot!(errors, @r"
483 [
484 RequireTimeoutSettings,
485 RequireTimeoutSettings,
486 PreferRobustStmts,
487 PreferRobustStmts,
488 ]
489 ");
490 }
491}