1use ahash::HashSet;
2use itertools::Itertools;
3use sqruff_lib_core::dialects::syntax::{SyntaxKind, SyntaxSet};
4use sqruff_lib_core::errors::SQLBaseError;
5use sqruff_lib_core::parser::segments::base::ErasedSegment;
6
7#[derive(Eq, PartialEq, Debug, Clone)]
42enum NoQADirective {
43 LineIgnoreAll(LineIgnoreAll),
44 LineIgnoreRules(LineIgnoreRules),
45 RangeIgnoreAll(RangeIgnoreAll),
46 RangeIgnoreRules(RangeIgnoreRules),
47}
48
49impl NoQADirective {
50 #[allow(dead_code)]
53 fn validate_against_rules(&self, available_rules: &HashSet<&str>) -> Result<(), SQLBaseError> {
54 fn check_rules(
55 rules: &HashSet<String>,
56 available_rules: &HashSet<&str>,
57 ) -> Result<(), SQLBaseError> {
58 for rule in rules {
59 if !available_rules.contains(rule.as_str()) {
60 return Err(SQLBaseError {
61 fatal: true,
62 ignore: false,
63 warning: false,
64 line_no: 0,
65 line_pos: 0,
66 description: format!("Rule {} not found in rule set", rule),
67 rule: None,
68 source_slice: Default::default(),
69 fixable: false,
70 });
71 }
72 }
73 Ok(())
74 }
75
76 match self {
77 NoQADirective::LineIgnoreAll(_) => Ok(()),
78 NoQADirective::LineIgnoreRules(LineIgnoreRules { rules, .. }) => {
79 check_rules(rules, available_rules)
80 }
81 NoQADirective::RangeIgnoreAll(_) => Ok(()),
82 NoQADirective::RangeIgnoreRules(RangeIgnoreRules { rules, .. }) => {
83 check_rules(rules, available_rules)
84 }
85 }
86 }
87
88 fn parse_from_comment(
91 original_comment: &str,
92 line_no: usize,
94 line_pos: usize,
95 ) -> Result<Option<Self>, SQLBaseError> {
96 let comment = original_comment.split("--").last();
100 if let Some(comment) = comment {
101 let comment = comment.trim();
102 if let Some(comment) = comment.strip_prefix(NOQA_PREFIX) {
103 let comment = comment.trim();
104 if comment.is_empty() {
105 Ok(Some(NoQADirective::LineIgnoreAll(LineIgnoreAll {
106 line_no,
107 line_pos,
108 raw_string: original_comment.to_string(),
109 })))
110 } else if let Some(comment) = comment.strip_prefix(":") {
111 let comment = comment.trim();
112 if let Some(comment) = comment.strip_prefix("disable=") {
113 let comment = comment.trim();
114 if comment == "all" {
115 Ok(Some(NoQADirective::RangeIgnoreAll(RangeIgnoreAll {
116 line_no,
117 line_pos,
118 raw_string: original_comment.to_string(),
119 action: IgnoreAction::Disable,
120 })))
121 } else {
122 let rules: HashSet<_> = comment
123 .split(",")
124 .map(|rule| rule.trim().to_string())
125 .filter(|rule| !rule.is_empty())
126 .collect();
127 if rules.is_empty() {
128 Err(SQLBaseError {
129 fatal: true,
130 ignore: false,
131 warning: false,
132 line_no,
133 line_pos,
134 description: "Malformed 'noqa' section. Expected 'noqa: <rule>[,...] | all'"
135 .into(),
136 rule: None,
137 source_slice: Default::default(),
138 fixable: false,
139 })
140 } else {
141 Ok(Some(NoQADirective::RangeIgnoreRules(RangeIgnoreRules {
142 line_no,
143 line_pos,
144 raw_string: original_comment.into(),
145 action: IgnoreAction::Disable,
146 rules,
147 })))
148 }
149 }
150 } else if let Some(comment) = comment.strip_prefix("enable=") {
151 let comment = comment.trim();
152 if comment == "all" {
153 Ok(Some(NoQADirective::RangeIgnoreAll(RangeIgnoreAll {
154 line_no,
155 line_pos,
156 action: IgnoreAction::Enable,
157 raw_string: original_comment.to_string(),
158 })))
159 } else {
160 let rules: HashSet<_> = comment
161 .split(",")
162 .map(|rule| rule.trim().to_string())
163 .filter(|rule| !rule.is_empty())
164 .collect();
165 if rules.is_empty() {
166 Err(SQLBaseError {
167 fatal: true,
168 ignore: false,
169 warning: false,
170 line_no,
171 line_pos,
172 description:
173 "Malformed 'noqa' section. Expected 'noqa: <rule>[,...]'"
174 .to_string(),
175 rule: None,
176 source_slice: Default::default(),
177 fixable: false,
178 })
179 } else {
180 Ok(Some(NoQADirective::RangeIgnoreRules(RangeIgnoreRules {
181 line_no,
182 line_pos,
183 raw_string: original_comment.to_string(),
184 action: IgnoreAction::Enable,
185 rules,
186 })))
187 }
188 }
189 } else if !comment.is_empty() {
190 let rules = comment.split(",").map_into().collect::<HashSet<String>>();
191 if rules.is_empty() {
192 Err(SQLBaseError {
193 fatal: true,
194 ignore: false,
195 warning: false,
196 line_no,
197 line_pos,
198 description:
199 "Malformed 'noqa' section. Expected 'noqa: <rule>[,...] | all'"
200 .into(),
201 rule: None,
202 source_slice: Default::default(),
203 fixable: false,
204 })
205 } else {
206 return Ok(Some(NoQADirective::LineIgnoreRules(LineIgnoreRules {
207 line_no,
208 line_pos: 0,
209 raw_string: original_comment.into(),
210 rules,
211 })));
212 }
213 } else {
214 Err(SQLBaseError {
215 fatal: true,
216 ignore: false,
217 warning: false,
218 line_no,
219 line_pos,
220 description:
221 "Malformed 'noqa' section. Expected 'noqa: <rule>[,...] | all'"
222 .into(),
223 rule: None,
224 source_slice: Default::default(),
225 fixable: false,
226 })
227 }
228 } else {
229 Err(SQLBaseError {
230 fatal: true,
231 ignore: false,
232 warning: false,
233 line_no,
234 line_pos,
235 description:
236 "Malformed 'noqa' section. Expected 'noqa' or 'noqa: <rule>[,...]'"
237 .to_string(),
238 rule: None,
239 source_slice: Default::default(),
240 fixable: false,
241 })
242 }
243 } else {
244 Ok(None)
245 }
246 } else {
247 Ok(None)
248 }
249 }
250}
251
252#[derive(Eq, PartialEq, Debug, Clone, strum_macros::EnumString)]
253#[strum(serialize_all = "lowercase")]
254enum IgnoreAction {
255 Enable,
256 Disable,
257}
258
259#[derive(Eq, PartialEq, Debug, Clone)]
260struct RangeIgnoreAll {
261 line_no: usize,
262 line_pos: usize,
263 raw_string: String,
264 action: IgnoreAction,
265}
266
267#[derive(Eq, PartialEq, Debug, Clone)]
268struct RangeIgnoreRules {
269 line_no: usize,
270 line_pos: usize,
271 raw_string: String,
272 action: IgnoreAction,
273 rules: HashSet<String>,
274}
275
276#[derive(Eq, PartialEq, Debug, Clone)]
277struct LineIgnoreAll {
278 line_no: usize,
279 line_pos: usize,
280 raw_string: String,
281}
282
283#[derive(Eq, PartialEq, Debug, Clone)]
284struct LineIgnoreRules {
285 line_no: usize,
286 line_pos: usize,
287 raw_string: String,
288 rules: HashSet<String>,
289}
290
291#[derive(Debug, Clone, Default)]
292pub struct IgnoreMask {
293 ignore_list: Vec<NoQADirective>,
294}
295
296const NOQA_PREFIX: &str = "noqa";
297
298impl IgnoreMask {
299 fn extract_ignore_from_comment(
301 comment: ErasedSegment,
302 ) -> Result<Option<NoQADirective>, SQLBaseError> {
303 let mut comment_content = comment.raw().trim();
305 if comment_content.ends_with("*/") {
310 comment_content = comment_content[..comment_content.len() - 2].trim_end();
311 }
312 if comment_content.starts_with("/*") {
313 comment_content = comment_content[2..].trim_start();
314 }
315 let (line_no, line_pos) = comment
316 .get_position_marker()
317 .ok_or(SQLBaseError {
318 fatal: true,
319 ignore: false,
320 warning: false,
321 line_no: 0,
322 line_pos: 0,
323 description: "Could not get position marker".to_string(),
324 rule: None,
325 source_slice: Default::default(),
326 fixable: false,
327 })?
328 .source_position();
329 NoQADirective::parse_from_comment(comment_content, line_no, line_pos)
330 }
331
332 pub fn from_tree(tree: &ErasedSegment) -> (IgnoreMask, Vec<SQLBaseError>) {
336 let mut ignore_list: Vec<NoQADirective> = vec![];
337 let mut violations: Vec<SQLBaseError> = vec![];
338 for comment in tree.recursive_crawl(
339 const {
340 &SyntaxSet::new(&[
341 SyntaxKind::Comment,
342 SyntaxKind::InlineComment,
343 SyntaxKind::BlockComment,
344 ])
345 },
346 false,
347 &SyntaxSet::new(&[]),
348 false,
349 ) {
350 let ignore_entry = IgnoreMask::extract_ignore_from_comment(comment);
351 if let Err(err) = ignore_entry {
352 violations.push(err);
353 } else if let Ok(Some(ignore_entry)) = ignore_entry {
354 ignore_list.push(ignore_entry);
355 }
356 }
357 (IgnoreMask { ignore_list }, violations)
358 }
359
360 pub fn is_masked(&self, violation: &SQLBaseError) -> bool {
363 fn is_masked_by_line_rules(ignore_mask: &IgnoreMask, violation: &SQLBaseError) -> bool {
364 for ignore in &ignore_mask.ignore_list {
365 match ignore {
366 NoQADirective::LineIgnoreAll(LineIgnoreAll { line_no, .. }) => {
367 if violation.line_no == *line_no {
368 return true;
369 }
370 }
371 NoQADirective::LineIgnoreRules(LineIgnoreRules { line_no, rules, .. }) => {
372 if violation.line_no == *line_no {
373 if let Some(rule) = &violation.rule {
374 if rules.contains(rule.code) {
375 return true;
376 }
377 }
378 }
379 }
380 _ => {}
381 }
382 }
383 false
384 }
385
386 fn is_masked_by_range_rules(ignore_mask: &IgnoreMask, violation: &SQLBaseError) -> bool {
389 let mut directives = Vec::new();
391
392 for ignore in &ignore_mask.ignore_list {
393 match ignore {
394 NoQADirective::RangeIgnoreAll(RangeIgnoreAll {
395 line_no, line_pos, ..
396 }) => {
397 directives.push((line_no, line_pos, ignore));
398 }
399 NoQADirective::RangeIgnoreRules(RangeIgnoreRules {
400 line_no, line_pos, ..
401 }) => {
402 directives.push((line_no, line_pos, ignore));
403 }
404 _ => {}
405 }
406 }
407
408 directives.sort_by(|(line_no1, line_pos1, _), (line_no2, line_pos2, _)| {
410 line_no1.cmp(line_no2).then(line_pos1.cmp(line_pos2))
411 });
412
413 let mut all_rules_disabled = false;
415 let mut disabled_rules = <HashSet<String>>::default();
416
417 for (line_no, line_pos, ignore) in directives {
419 if *line_no > violation.line_no {
421 break;
422 }
423 if *line_no == violation.line_no && *line_pos > violation.line_pos {
424 break;
425 }
426
427 match ignore {
429 NoQADirective::RangeIgnoreAll(RangeIgnoreAll { action, .. }) => match action {
430 IgnoreAction::Disable => {
431 all_rules_disabled = true;
432 }
433 IgnoreAction::Enable => {
434 all_rules_disabled = false;
435 }
436 },
437 NoQADirective::RangeIgnoreRules(RangeIgnoreRules { action, rules, .. }) => {
438 match action {
439 IgnoreAction::Disable => {
440 for rule in rules {
441 disabled_rules.insert(rule.clone());
442 }
443 }
444 IgnoreAction::Enable => {
445 for rule in rules {
446 disabled_rules.remove(rule);
447 }
448 }
449 }
450 }
451 _ => {}
452 }
453 }
454
455 if all_rules_disabled {
457 return true;
458 } else if let Some(rule) = &violation.rule {
459 if disabled_rules.contains(rule.code) {
460 return true;
461 }
462 }
463
464 false
465 }
466
467 is_masked_by_line_rules(self, violation) || is_masked_by_range_rules(self, violation)
468 }
469}
470
471#[cfg(test)]
472mod tests {
473 use super::*;
474 use crate::core::config::FluffConfig;
475 use crate::core::linter::core::Linter;
476 use crate::core::rules::noqa::NoQADirective;
477 use itertools::Itertools;
478 use sqruff_lib_core::errors::ErrorStructRule;
479
480 #[test]
481 fn test_is_masked_single_line() {
482 let error = SQLBaseError {
483 fatal: false,
484 ignore: false,
485 warning: false,
486 line_no: 2,
487 line_pos: 11,
488 description: "Implicit/explicit aliasing of columns.".to_string(),
489 rule: Some(ErrorStructRule {
490 name: "aliasing.column",
491 code: "AL02",
492 }),
493 source_slice: Default::default(),
494 fixable: true,
495 };
496 let mask = IgnoreMask {
497 ignore_list: vec![NoQADirective::LineIgnoreRules(LineIgnoreRules {
498 line_no: 2,
499 line_pos: 13,
500 raw_string: "--noqa: AL02".to_string(),
501 rules: ["AL02".to_string()].into_iter().collect(),
502 })],
503 };
504 let not_mask_wrong_line = IgnoreMask {
505 ignore_list: vec![NoQADirective::LineIgnoreRules(LineIgnoreRules {
506 line_no: 3,
507 line_pos: 13,
508 raw_string: "--noqa: AL02".to_string(),
509 rules: ["AL02".to_string()].into_iter().collect(),
510 })],
511 };
512 let not_mask_wrong_rule = IgnoreMask {
513 ignore_list: vec![NoQADirective::LineIgnoreRules(LineIgnoreRules {
514 line_no: 3,
515 line_pos: 13,
516 raw_string: "--noqa: AL03".to_string(),
517 rules: ["AL03".to_string()].into_iter().collect(),
518 })],
519 };
520
521 assert!(!not_mask_wrong_line.is_masked(&error));
522 assert!(!not_mask_wrong_rule.is_masked(&error));
523 assert!(mask.is_masked(&error));
524 }
525
526 #[test]
527 fn test_parse_noqa() {
528 let test_cases = vec![
529 ("", Ok::<Option<NoQADirective>, &'static str>(None)),
530 (
531 "noqa",
532 Ok(Some(NoQADirective::LineIgnoreAll(LineIgnoreAll {
533 line_no: 0,
534 line_pos: 0,
535 raw_string: "noqa".to_string(),
536 }))),
537 ),
538 (
539 "noqa?",
540 Err("Malformed 'noqa' section. Expected 'noqa' or 'noqa: <rule>[,...]'"),
541 ),
542 (
543 "noqa:",
544 Err("Malformed 'noqa' section. Expected 'noqa: <rule>[,...] | all'"),
545 ),
546 (
547 "noqa: ",
548 Err("Malformed 'noqa' section. Expected 'noqa: <rule>[,...] | all'"),
549 ),
550 (
551 "noqa: LT01,LT02",
552 Ok(Some(NoQADirective::LineIgnoreRules(LineIgnoreRules {
553 line_no: 0,
554 line_pos: 0,
555 raw_string: "noqa: LT01,LT02".into(),
556 rules: ["LT01", "LT02"]
557 .into_iter()
558 .map_into()
559 .collect::<HashSet<String>>(),
560 }))),
561 ),
562 (
563 "noqa: enable=LT01",
564 Ok(Some(NoQADirective::RangeIgnoreRules(RangeIgnoreRules {
565 line_no: 0,
566 line_pos: 0,
567 raw_string: "noqa: enable=LT01".to_string(),
568 action: IgnoreAction::Enable,
569 rules: ["LT01"].into_iter().map_into().collect::<HashSet<String>>(),
570 }))),
571 ),
572 (
573 "noqa: disable=CP01",
574 Ok(Some(NoQADirective::RangeIgnoreRules(RangeIgnoreRules {
575 line_no: 0,
576 line_pos: 0,
577 raw_string: "noqa: disable=CP01".to_string(),
578 action: IgnoreAction::Disable,
579 rules: ["CP01"].into_iter().map_into().collect::<HashSet<String>>(),
580 }))),
581 ),
582 (
583 "noqa: disable=all",
584 Ok(Some(NoQADirective::RangeIgnoreAll(RangeIgnoreAll {
585 line_no: 0,
586 line_pos: 0,
587 raw_string: "noqa: disable=all".to_string(),
588 action: IgnoreAction::Disable,
589 }))),
590 ),
591 (
594 "Inline comment before inline ignore -- noqa: disable=LT01,LT02",
595 Ok(Some(NoQADirective::RangeIgnoreRules(RangeIgnoreRules {
596 line_no: 0,
597 line_pos: 0,
598 raw_string: "Inline comment before inline ignore -- noqa: disable=LT01,LT02"
599 .to_string(),
600 action: IgnoreAction::Disable,
601 rules: ["LT01".to_string(), "LT02".to_string()]
602 .into_iter()
603 .collect(),
604 }))),
605 ),
606 ];
607
608 for (input, expected) in test_cases {
609 let result = NoQADirective::parse_from_comment(input, 0, 0);
610 match expected {
611 Ok(_) => assert_eq!(result.unwrap(), expected.unwrap()),
612 Err(err) => {
613 assert!(result.is_err());
614 let result_err = result.err().unwrap();
615 assert_eq!(result_err.description, err);
616 assert!(result_err.fatal);
617 }
618 }
619 }
620 }
621
622 #[test]
623 fn test_linter_single_noqa() {
625 let linter = Linter::new(
626 FluffConfig::from_source(
627 r#"
628[sqruff]
629dialect = bigquery
630rules = AL02
631 "#,
632 None,
633 ),
634 None,
635 None,
636 false,
637 );
638
639 let sql = r#"SELECT
640 col_a a,
641 col_b b --noqa: AL02
642FROM foo
643"#;
644
645 let result = linter.lint_string(sql, None, false);
646 let violations = result.get_violations(None);
647
648 assert_eq!(violations.len(), 1);
649 assert_eq!(
650 violations.iter().map(|v| v.line_no).collect::<Vec<_>>(),
651 [2].to_vec()
652 );
653 }
654
655 #[test]
656 fn test_linter_noqa_but_disabled() {
658 let linter_without_disabled = Linter::new(
659 FluffConfig::from_source(
660 r#"
661[sqruff]
662dialect = bigquery
663rules = AL02
664 "#,
665 None,
666 ),
667 None,
668 None,
669 false,
670 );
671 let linter_with_disabled = Linter::new(
672 FluffConfig::from_source(
673 r#"
674[sqruff]
675dialect = bigquery
676rules = AL02
677disable_noqa = True
678 "#,
679 None,
680 ),
681 None,
682 None,
683 false,
684 );
685
686 let sql = r#"SELECT
687 col_a a,
688 col_b b --noqa
689FROM foo
690 "#;
691 let result_with_disabled = linter_with_disabled.lint_string(sql, None, false);
692 let result_without_disabled = linter_without_disabled.lint_string(sql, None, false);
693
694 assert_eq!(result_without_disabled.get_violations(None).len(), 1);
695 assert_eq!(result_with_disabled.get_violations(None).len(), 2);
696 }
697
698 #[test]
699 fn test_range_code() {
700 let linter_without_disabled = Linter::new(
701 FluffConfig::from_source(
702 r#"
703[sqruff]
704dialect = bigquery
705rules = AL02
706 "#,
707 None,
708 ),
709 None,
710 None,
711 false,
712 );
713 let sql_disable_rule = r#"SELECT
714 col_a a,
715 col_c c, --noqa: disable=AL02
716 col_d d,
717 col_e e, --noqa: enable=AL02
718 col_f f
719FROM foo
720"#;
721
722 let sql_disable_all = r#"SELECT
723 col_a a,
724 col_c c, --noqa: disable=all
725 col_d d,
726 col_e e, --noqa: enable=all
727 col_f f
728FROM foo
729"#;
730 let result_rule = linter_without_disabled.lint_string(sql_disable_rule, None, false);
731 let result_all = linter_without_disabled.lint_string(sql_disable_all, None, false);
732
733 assert_eq!(result_rule.get_violations(None).len(), 3);
734 assert_eq!(result_all.get_violations(None).len(), 3);
735 }
736}