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::ErasedSegment;
6
7use crate::core::rules::{ErasedRule, LintResult};
8
9pub trait HasViolation {
10 fn source_position(&self) -> Option<(usize, usize)>;
11}
12
13impl HasViolation for SQLBaseError {
14 fn source_position(&self) -> Option<(usize, usize)> {
15 Some((self.line_no, self.line_pos))
16 }
17}
18
19impl HasViolation for LintResult {
20 fn source_position(&self) -> Option<(usize, usize)> {
21 self.anchor
22 .as_ref()?
23 .get_position_marker()
24 .map(|m| m.source_position())
25 }
26}
27
28#[derive(Eq, PartialEq, Debug, Clone)]
63enum NoQADirective {
64 LineIgnoreAll(LineIgnoreAll),
65 LineIgnoreRules(LineIgnoreRules),
66 RangeIgnoreAll(RangeIgnoreAll),
67 RangeIgnoreRules(RangeIgnoreRules),
68}
69
70impl NoQADirective {
71 #[allow(dead_code)]
74 fn validate_against_rules(&self, available_rules: &HashSet<&str>) -> Result<(), SQLBaseError> {
75 fn check_rules(
76 rules: &HashSet<String>,
77 available_rules: &HashSet<&str>,
78 ) -> Result<(), SQLBaseError> {
79 for rule in rules {
80 if !available_rules.contains(rule.as_str()) {
81 return Err(SQLBaseError {
82 fixable: false,
83 line_no: 0,
84 line_pos: 0,
85 description: format!("Rule {rule} not found in rule set"),
86 rule: None,
87 source_slice: Default::default(),
88 });
89 }
90 }
91 Ok(())
92 }
93
94 match self {
95 NoQADirective::LineIgnoreAll(_) => Ok(()),
96 NoQADirective::LineIgnoreRules(LineIgnoreRules { rules, .. }) => {
97 check_rules(rules, available_rules)
98 }
99 NoQADirective::RangeIgnoreAll(_) => Ok(()),
100 NoQADirective::RangeIgnoreRules(RangeIgnoreRules { rules, .. }) => {
101 check_rules(rules, available_rules)
102 }
103 }
104 }
105
106 fn parse_from_comment(
109 original_comment: &str,
110 line_no: usize,
112 line_pos: usize,
113 ) -> Result<Option<Self>, SQLBaseError> {
114 let comment = original_comment.split("--").last();
118 if let Some(comment) = comment {
119 let comment = comment.trim();
120 if let Some(comment) = comment.strip_prefix(NOQA_PREFIX) {
121 let comment = comment.trim();
122 if comment.is_empty() {
123 Ok(Some(NoQADirective::LineIgnoreAll(LineIgnoreAll {
124 line_no,
125 line_pos,
126 raw_string: original_comment.to_string(),
127 })))
128 } else if let Some(comment) = comment.strip_prefix(":") {
129 let comment = comment.trim();
130 if let Some(comment) = comment.strip_prefix("disable=") {
131 let comment = comment.trim();
132 if comment == "all" {
133 Ok(Some(NoQADirective::RangeIgnoreAll(RangeIgnoreAll {
134 line_no,
135 line_pos,
136 raw_string: original_comment.to_string(),
137 action: IgnoreAction::Disable,
138 })))
139 } else {
140 let rules: HashSet<_> = comment
141 .split(",")
142 .map(|rule| rule.trim().to_string())
143 .filter(|rule| !rule.is_empty())
144 .collect();
145 if rules.is_empty() {
146 Err(SQLBaseError {
147 fixable: false,
148 line_no,
149 line_pos,
150 description: "Malformed 'noqa' section. Expected 'noqa: <rule>[,...] | all'"
151 .into(),
152 rule: None,
153 source_slice: Default::default(),
154 })
155 } else {
156 Ok(Some(NoQADirective::RangeIgnoreRules(RangeIgnoreRules {
157 line_no,
158 line_pos,
159 raw_string: original_comment.into(),
160 action: IgnoreAction::Disable,
161 rules,
162 })))
163 }
164 }
165 } else if let Some(comment) = comment.strip_prefix("enable=") {
166 let comment = comment.trim();
167 if comment == "all" {
168 Ok(Some(NoQADirective::RangeIgnoreAll(RangeIgnoreAll {
169 line_no,
170 line_pos,
171 action: IgnoreAction::Enable,
172 raw_string: original_comment.to_string(),
173 })))
174 } else {
175 let rules: HashSet<_> = comment
176 .split(",")
177 .map(|rule| rule.trim().to_string())
178 .filter(|rule| !rule.is_empty())
179 .collect();
180 if rules.is_empty() {
181 Err(SQLBaseError {
182 fixable: false,
183 line_no,
184 line_pos,
185 description:
186 "Malformed 'noqa' section. Expected 'noqa: <rule>[,...]'"
187 .to_string(),
188 rule: None,
189 source_slice: Default::default(),
190 })
191 } else {
192 Ok(Some(NoQADirective::RangeIgnoreRules(RangeIgnoreRules {
193 line_no,
194 line_pos,
195 raw_string: original_comment.to_string(),
196 action: IgnoreAction::Enable,
197 rules,
198 })))
199 }
200 }
201 } else if !comment.is_empty() {
202 let rules = comment.split(",").map_into().collect::<HashSet<String>>();
203 if rules.is_empty() {
204 Err(SQLBaseError {
205 fixable: false,
206 line_no,
207 line_pos,
208 description:
209 "Malformed 'noqa' section. Expected 'noqa: <rule>[,...] | all'"
210 .into(),
211 rule: None,
212 source_slice: Default::default(),
213 })
214 } else {
215 Ok(Some(NoQADirective::LineIgnoreRules(LineIgnoreRules {
216 line_no,
217 line_pos: 0,
218 raw_string: original_comment.into(),
219 rules,
220 })))
221 }
222 } else {
223 Err(SQLBaseError {
224 fixable: false,
225 line_no,
226 line_pos,
227 description:
228 "Malformed 'noqa' section. Expected 'noqa: <rule>[,...] | all'"
229 .into(),
230 rule: None,
231 source_slice: Default::default(),
232 })
233 }
234 } else {
235 Err(SQLBaseError {
236 fixable: false,
237 line_no,
238 line_pos,
239 description:
240 "Malformed 'noqa' section. Expected 'noqa' or 'noqa: <rule>[,...]'"
241 .to_string(),
242 rule: None,
243 source_slice: Default::default(),
244 })
245 }
246 } else {
247 Ok(None)
248 }
249 } else {
250 Ok(None)
251 }
252 }
253}
254
255#[derive(Eq, PartialEq, Debug, Clone, strum_macros::EnumString)]
256#[strum(serialize_all = "lowercase")]
257enum IgnoreAction {
258 Enable,
259 Disable,
260}
261
262#[derive(Eq, PartialEq, Debug, Clone)]
263struct RangeIgnoreAll {
264 line_no: usize,
265 line_pos: usize,
266 raw_string: String,
267 action: IgnoreAction,
268}
269
270#[derive(Eq, PartialEq, Debug, Clone)]
271struct RangeIgnoreRules {
272 line_no: usize,
273 line_pos: usize,
274 raw_string: String,
275 action: IgnoreAction,
276 rules: HashSet<String>,
277}
278
279#[derive(Eq, PartialEq, Debug, Clone)]
280struct LineIgnoreAll {
281 line_no: usize,
282 line_pos: usize,
283 raw_string: String,
284}
285
286#[derive(Eq, PartialEq, Debug, Clone)]
287struct LineIgnoreRules {
288 line_no: usize,
289 line_pos: usize,
290 raw_string: String,
291 rules: HashSet<String>,
292}
293
294#[derive(Debug, Clone, Default)]
295pub struct IgnoreMask {
296 ignore_list: Vec<NoQADirective>,
297}
298
299const NOQA_PREFIX: &str = "noqa";
300
301impl IgnoreMask {
302 fn extract_ignore_from_comment(
304 comment: ErasedSegment,
305 ) -> Result<Option<NoQADirective>, SQLBaseError> {
306 let mut comment_content = comment.raw().trim();
308 if comment_content.ends_with("*/") {
313 comment_content = comment_content[..comment_content.len() - 2].trim_end();
314 }
315 if comment_content.starts_with("/*") {
316 comment_content = comment_content[2..].trim_start();
317 }
318 let (line_no, line_pos) = comment
319 .get_position_marker()
320 .ok_or(SQLBaseError {
321 fixable: false,
322 line_no: 0,
323 line_pos: 0,
324 description: "Could not get position marker".to_string(),
325 rule: None,
326 source_slice: Default::default(),
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: &impl HasViolation, rule: Option<&ErasedRule>) -> bool {
363 let Some((vline_no, vline_pos)) = violation.source_position() else {
364 return true;
365 };
366
367 let is_masked_by_line_rules = || {
368 for ignore in &self.ignore_list {
369 match ignore {
370 NoQADirective::LineIgnoreAll(LineIgnoreAll { line_no, .. }) => {
371 if vline_no == *line_no {
372 return true;
373 }
374 }
375 NoQADirective::LineIgnoreRules(LineIgnoreRules { line_no, rules, .. }) => {
376 if vline_no == *line_no
377 && let Some(rule) = rule
378 && rules.contains(rule.code())
379 {
380 return true;
381 }
382 }
383 _ => {}
384 }
385 }
386 false
387 };
388
389 let is_masked_by_range_rules = || {
392 let mut directives = Vec::new();
394
395 for ignore in &self.ignore_list {
396 match ignore {
397 NoQADirective::RangeIgnoreAll(RangeIgnoreAll {
398 line_no, line_pos, ..
399 }) => {
400 directives.push((line_no, line_pos, ignore));
401 }
402 NoQADirective::RangeIgnoreRules(RangeIgnoreRules {
403 line_no, line_pos, ..
404 }) => {
405 directives.push((line_no, line_pos, ignore));
406 }
407 _ => {}
408 }
409 }
410
411 directives.sort_by(|(line_no1, line_pos1, _), (line_no2, line_pos2, _)| {
413 line_no1.cmp(line_no2).then(line_pos1.cmp(line_pos2))
414 });
415
416 let mut all_rules_disabled = false;
418 let mut disabled_rules = <HashSet<String>>::default();
419
420 for (line_no, line_pos, ignore) in directives {
422 if *line_no > vline_no {
424 break;
425 }
426 if *line_no == vline_no && *line_pos > vline_pos {
427 break;
428 }
429
430 match ignore {
432 NoQADirective::RangeIgnoreAll(RangeIgnoreAll { action, .. }) => match action {
433 IgnoreAction::Disable => {
434 all_rules_disabled = true;
435 }
436 IgnoreAction::Enable => {
437 all_rules_disabled = false;
438 }
439 },
440 NoQADirective::RangeIgnoreRules(RangeIgnoreRules { action, rules, .. }) => {
441 match action {
442 IgnoreAction::Disable => {
443 for rule in rules {
444 disabled_rules.insert(rule.clone());
445 }
446 }
447 IgnoreAction::Enable => {
448 for rule in rules {
449 disabled_rules.remove(rule);
450 }
451 }
452 }
453 }
454 _ => {}
455 }
456 }
457
458 if all_rules_disabled {
460 return true;
461 } else if let Some(rule) = rule
462 && disabled_rules.contains(rule.code())
463 {
464 return true;
465 }
466
467 false
468 };
469
470 is_masked_by_line_rules() || is_masked_by_range_rules()
471 }
472}
473
474#[cfg(test)]
475mod tests {
476 use super::*;
477 use crate::core::config::FluffConfig;
478 use crate::core::linter::core::Linter;
479 use crate::core::rules::Erased;
480 use crate::core::rules::noqa::NoQADirective;
481 use itertools::Itertools;
482
483 #[test]
484 fn test_is_masked_single_line() {
485 let error = SQLBaseError {
486 fixable: true,
487 line_no: 2,
488 line_pos: 11,
489 description: "Implicit/explicit aliasing of columns.".to_string(),
490 rule: None,
491 source_slice: Default::default(),
492 };
493 let mask = IgnoreMask {
494 ignore_list: vec![NoQADirective::LineIgnoreRules(LineIgnoreRules {
495 line_no: 2,
496 line_pos: 13,
497 raw_string: "--noqa: AL02".to_string(),
498 rules: ["AL02".to_string()].into_iter().collect(),
499 })],
500 };
501 let not_mask_wrong_line = IgnoreMask {
502 ignore_list: vec![NoQADirective::LineIgnoreRules(LineIgnoreRules {
503 line_no: 3,
504 line_pos: 13,
505 raw_string: "--noqa: AL02".to_string(),
506 rules: ["AL02".to_string()].into_iter().collect(),
507 })],
508 };
509 let not_mask_wrong_rule = IgnoreMask {
510 ignore_list: vec![NoQADirective::LineIgnoreRules(LineIgnoreRules {
511 line_no: 3,
512 line_pos: 13,
513 raw_string: "--noqa: AL03".to_string(),
514 rules: ["AL03".to_string()].into_iter().collect(),
515 })],
516 };
517
518 assert!(!not_mask_wrong_line.is_masked(&error, None));
519 assert!(!not_mask_wrong_rule.is_masked(&error, None));
520 assert!(mask.is_masked(
521 &error,
522 Some(&crate::rules::aliasing::al02::RuleAL02::default().erased())
523 ));
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 }
617 }
618 }
619 }
620
621 #[test]
622 fn test_linter_single_noqa() {
624 let linter = Linter::new(
625 FluffConfig::from_source(
626 r#"
627[sqruff]
628dialect = bigquery
629rules = AL02
630 "#,
631 None,
632 ),
633 None,
634 None,
635 false,
636 );
637
638 let sql = r#"SELECT
639 col_a a,
640 col_b b --noqa: AL02
641FROM foo
642"#;
643
644 let result = linter.lint_string(sql, None, false);
645 let violations = result.violations();
646
647 assert_eq!(violations.len(), 1);
648 assert_eq!(
649 violations.iter().map(|v| v.line_no).collect::<Vec<_>>(),
650 [2].to_vec()
651 );
652 }
653
654 #[test]
655 fn test_linter_noqa_but_disabled() {
657 let linter_without_disabled = Linter::new(
658 FluffConfig::from_source(
659 r#"
660[sqruff]
661dialect = bigquery
662rules = AL02
663 "#,
664 None,
665 ),
666 None,
667 None,
668 false,
669 );
670 let linter_with_disabled = Linter::new(
671 FluffConfig::from_source(
672 r#"
673[sqruff]
674dialect = bigquery
675rules = AL02
676disable_noqa = True
677 "#,
678 None,
679 ),
680 None,
681 None,
682 false,
683 );
684
685 let sql = r#"SELECT
686 col_a a,
687 col_b b --noqa
688FROM foo
689 "#;
690 let result_with_disabled = linter_with_disabled.lint_string(sql, None, false);
691 let result_without_disabled = linter_without_disabled.lint_string(sql, None, false);
692
693 assert_eq!(result_without_disabled.violations().len(), 1);
694 assert_eq!(result_with_disabled.violations().len(), 2);
695 }
696
697 #[test]
698 fn test_range_code() {
699 let linter_without_disabled = Linter::new(
700 FluffConfig::from_source(
701 r#"
702[sqruff]
703dialect = bigquery
704rules = AL02
705 "#,
706 None,
707 ),
708 None,
709 None,
710 false,
711 );
712 let sql_disable_rule = r#"SELECT
713 col_a a,
714 col_c c, --noqa: disable=AL02
715 col_d d,
716 col_e e, --noqa: enable=AL02
717 col_f f
718FROM foo
719"#;
720
721 let sql_disable_all = r#"SELECT
722 col_a a,
723 col_c c, --noqa: disable=all
724 col_d d,
725 col_e e, --noqa: enable=all
726 col_f f
727FROM foo
728"#;
729 let result_rule = linter_without_disabled.lint_string(sql_disable_rule, None, false);
730 let result_all = linter_without_disabled.lint_string(sql_disable_all, None, false);
731
732 assert_eq!(result_rule.violations().len(), 3);
733 assert_eq!(result_all.violations().len(), 3);
734 }
735}