diff --git a/crates/ruff/tests/cli/lint.rs b/crates/ruff/tests/cli/lint.rs index 25500ed346..a86d8e81be 100644 --- a/crates/ruff/tests/cli/lint.rs +++ b/crates/ruff/tests/cli/lint.rs @@ -1440,6 +1440,78 @@ def function(): Ok(()) } +#[test] +fn ignore_noqa() -> Result<()> { + let fixture = CliTest::new()?; + fixture.write_file( + "ruff.toml", + r#" +[lint] +select = ["F401"] +"#, + )?; + + fixture.write_file( + "noqa.py", + r#" +import os # noqa: F401 + +# ruff: disable[F401] +import sys +"#, + )?; + + // without --ignore-noqa + assert_cmd_snapshot!(fixture + .check_command() + .args(["--config", "ruff.toml"]) + .arg("noqa.py"), + @r" + success: false + exit_code: 1 + ----- stdout ----- + noqa.py:5:8: F401 [*] `sys` imported but unused + Found 1 error. + [*] 1 fixable with the `--fix` option. + + ----- stderr ----- + "); + + assert_cmd_snapshot!(fixture + .check_command() + .args(["--config", "ruff.toml"]) + .arg("noqa.py") + .args(["--preview"]), + @r" + success: true + exit_code: 0 + ----- stdout ----- + All checks passed! + + ----- stderr ----- + "); + + // with --ignore-noqa --preview + assert_cmd_snapshot!(fixture + .check_command() + .args(["--config", "ruff.toml"]) + .arg("noqa.py") + .args(["--ignore-noqa", "--preview"]), + @r" + success: false + exit_code: 1 + ----- stdout ----- + noqa.py:2:8: F401 [*] `os` imported but unused + noqa.py:5:8: F401 [*] `sys` imported but unused + Found 2 errors. + [*] 2 fixable with the `--fix` option. + + ----- stderr ----- + "); + + Ok(()) +} + #[test] fn add_noqa() -> Result<()> { let fixture = CliTest::new()?; @@ -1632,6 +1704,100 @@ def unused(x): # noqa: ANN001, ARG001, D103 Ok(()) } +#[test] +fn add_noqa_existing_file_level_noqa() -> Result<()> { + let fixture = CliTest::new()?; + fixture.write_file( + "ruff.toml", + r#" +[lint] +select = ["F401"] +"#, + )?; + + fixture.write_file( + "noqa.py", + r#" +# ruff: noqa F401 +import os +"#, + )?; + + assert_cmd_snapshot!(fixture + .check_command() + .args(["--config", "ruff.toml"]) + .arg("noqa.py") + .arg("--preview") + .args(["--add-noqa"]) + .arg("-") + .pass_stdin(r#" + +"#), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + "); + + let test_code = + fs::read_to_string(fixture.root().join("noqa.py")).expect("should read test file"); + + insta::assert_snapshot!(test_code, @r" + # ruff: noqa F401 + import os + "); + + Ok(()) +} + +#[test] +fn add_noqa_existing_range_suppression() -> Result<()> { + let fixture = CliTest::new()?; + fixture.write_file( + "ruff.toml", + r#" +[lint] +select = ["F401"] +"#, + )?; + + fixture.write_file( + "noqa.py", + r#" +# ruff: disable[F401] +import os +"#, + )?; + + assert_cmd_snapshot!(fixture + .check_command() + .args(["--config", "ruff.toml"]) + .arg("noqa.py") + .arg("--preview") + .args(["--add-noqa"]) + .arg("-") + .pass_stdin(r#" + +"#), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + "); + + let test_code = + fs::read_to_string(fixture.root().join("noqa.py")).expect("should read test file"); + + insta::assert_snapshot!(test_code, @r" + # ruff: disable[F401] + import os + "); + + Ok(()) +} + #[test] fn add_noqa_multiline_comment() -> Result<()> { let fixture = CliTest::new()?; diff --git a/crates/ruff_linter/resources/test/fixtures/ruff/suppressions.py b/crates/ruff_linter/resources/test/fixtures/ruff/suppressions.py new file mode 100644 index 0000000000..7a70c4d548 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/ruff/suppressions.py @@ -0,0 +1,56 @@ +def f(): + # These should both be ignored by the range suppression. + # ruff: disable[E741, F841] + I = 1 + # ruff: enable[E741, F841] + + +def f(): + # These should both be ignored by the implicit range suppression. + # Should also generate an "unmatched suppression" warning. + # ruff:disable[E741,F841] + I = 1 + + +def f(): + # Neither warning is ignored, and an "unmatched suppression" + # should be generated. + I = 1 + # ruff: enable[E741, F841] + + +def f(): + # One should be ignored by the range suppression, and + # the other logged to the user. + # ruff: disable[E741] + I = 1 + # ruff: enable[E741] + + +def f(): + # Test interleaved range suppressions. The first and last + # lines should each log a different warning, while the + # middle line should be completely silenced. + # ruff: disable[E741] + l = 0 + # ruff: disable[F841] + O = 1 + # ruff: enable[E741] + I = 2 + # ruff: enable[F841] + + +def f(): + # Neither of these are ignored and warnings are + # logged to user + # ruff: disable[E501] + I = 1 + # ruff: enable[E501] + + +def f(): + # These should both be ignored by the range suppression, + # and an unusued noqa diagnostic should be logged. + # ruff:disable[E741,F841] + I = 1 # noqa: E741,F841 + # ruff:enable[E741,F841] diff --git a/crates/ruff_linter/src/checkers/noqa.rs b/crates/ruff_linter/src/checkers/noqa.rs index 7cf58a5def..2602adeeee 100644 --- a/crates/ruff_linter/src/checkers/noqa.rs +++ b/crates/ruff_linter/src/checkers/noqa.rs @@ -12,17 +12,20 @@ use crate::fix::edits::delete_comment; use crate::noqa::{ Code, Directive, FileExemption, FileNoqaDirectives, NoqaDirectives, NoqaMapping, }; +use crate::preview::is_range_suppressions_enabled; use crate::registry::Rule; use crate::rule_redirects::get_redirect_target; use crate::rules::pygrep_hooks; use crate::rules::ruff; use crate::rules::ruff::rules::{UnusedCodes, UnusedNOQA}; use crate::settings::LinterSettings; +use crate::suppression::Suppressions; use crate::{Edit, Fix, Locator}; use super::ast::LintContext; /// RUF100 +#[expect(clippy::too_many_arguments)] pub(crate) fn check_noqa( context: &mut LintContext, path: &Path, @@ -31,6 +34,7 @@ pub(crate) fn check_noqa( noqa_line_for: &NoqaMapping, analyze_directives: bool, settings: &LinterSettings, + suppressions: &Suppressions, ) -> Vec { // Identify any codes that are globally exempted (within the current file). let file_noqa_directives = @@ -40,7 +44,7 @@ pub(crate) fn check_noqa( let mut noqa_directives = NoqaDirectives::from_commented_ranges(comment_ranges, &settings.external, path, locator); - if file_noqa_directives.is_empty() && noqa_directives.is_empty() { + if file_noqa_directives.is_empty() && noqa_directives.is_empty() && suppressions.is_empty() { return Vec::new(); } @@ -60,11 +64,19 @@ pub(crate) fn check_noqa( continue; } + // Apply file-level suppressions first if exemption.contains_secondary_code(code) { ignored_diagnostics.push(index); continue; } + // Apply ranged suppressions next + if is_range_suppressions_enabled(settings) && suppressions.check_diagnostic(diagnostic) { + ignored_diagnostics.push(index); + continue; + } + + // Apply end-of-line noqa suppressions last let noqa_offsets = diagnostic .parent() .into_iter() diff --git a/crates/ruff_linter/src/linter.rs b/crates/ruff_linter/src/linter.rs index 719d5ac9c5..08c0417020 100644 --- a/crates/ruff_linter/src/linter.rs +++ b/crates/ruff_linter/src/linter.rs @@ -32,6 +32,7 @@ use crate::rules::ruff::rules::test_rules::{self, TEST_RULES, TestRule}; use crate::settings::types::UnsafeFixes; use crate::settings::{LinterSettings, TargetVersion, flags}; use crate::source_kind::SourceKind; +use crate::suppression::Suppressions; use crate::{Locator, directives, fs}; pub(crate) mod float; @@ -128,6 +129,7 @@ pub fn check_path( source_type: PySourceType, parsed: &Parsed, target_version: TargetVersion, + suppressions: &Suppressions, ) -> Vec { // Aggregate all diagnostics. let mut context = LintContext::new(path, locator.contents(), settings); @@ -339,6 +341,7 @@ pub fn check_path( &directives.noqa_line_for, parsed.has_valid_syntax(), settings, + suppressions, ); if noqa.is_enabled() { for index in ignored.iter().rev() { @@ -400,6 +403,9 @@ pub fn add_noqa_to_path( &indexer, ); + // Parse range suppression comments + let suppressions = Suppressions::from_tokens(settings, locator.contents(), parsed.tokens()); + // Generate diagnostics, ignoring any existing `noqa` directives. let diagnostics = check_path( path, @@ -414,6 +420,7 @@ pub fn add_noqa_to_path( source_type, &parsed, target_version, + &suppressions, ); // Add any missing `# noqa` pragmas. @@ -427,6 +434,7 @@ pub fn add_noqa_to_path( &directives.noqa_line_for, stylist.line_ending(), reason, + &suppressions, ) } @@ -461,6 +469,9 @@ pub fn lint_only( &indexer, ); + // Parse range suppression comments + let suppressions = Suppressions::from_tokens(settings, locator.contents(), parsed.tokens()); + // Generate diagnostics. let diagnostics = check_path( path, @@ -475,6 +486,7 @@ pub fn lint_only( source_type, &parsed, target_version, + &suppressions, ); LinterResult { @@ -566,6 +578,9 @@ pub fn lint_fix<'a>( &indexer, ); + // Parse range suppression comments + let suppressions = Suppressions::from_tokens(settings, locator.contents(), parsed.tokens()); + // Generate diagnostics. let diagnostics = check_path( path, @@ -580,6 +595,7 @@ pub fn lint_fix<'a>( source_type, &parsed, target_version, + &suppressions, ); if iterations == 0 { @@ -769,6 +785,7 @@ mod tests { use crate::registry::Rule; use crate::settings::LinterSettings; use crate::source_kind::SourceKind; + use crate::suppression::Suppressions; use crate::test::{TestedNotebook, assert_notebook_path, test_contents, test_snippet}; use crate::{Locator, assert_diagnostics, directives, settings}; @@ -944,6 +961,7 @@ mod tests { &locator, &indexer, ); + let suppressions = Suppressions::from_tokens(settings, locator.contents(), parsed.tokens()); let mut diagnostics = check_path( path, None, @@ -957,6 +975,7 @@ mod tests { source_type, &parsed, target_version, + &suppressions, ); diagnostics.sort_by(Diagnostic::ruff_start_ordering); diagnostics diff --git a/crates/ruff_linter/src/noqa.rs b/crates/ruff_linter/src/noqa.rs index da9535817e..e8c3ada650 100644 --- a/crates/ruff_linter/src/noqa.rs +++ b/crates/ruff_linter/src/noqa.rs @@ -20,12 +20,14 @@ use crate::Locator; use crate::fs::relativize_path; use crate::registry::Rule; use crate::rule_redirects::get_redirect_target; +use crate::suppression::Suppressions; /// Generates an array of edits that matches the length of `messages`. /// Each potential edit in the array is paired, in order, with the associated diagnostic. /// Each edit will add a `noqa` comment to the appropriate line in the source to hide /// the diagnostic. These edits may conflict with each other and should not be applied /// simultaneously. +#[expect(clippy::too_many_arguments)] pub fn generate_noqa_edits( path: &Path, diagnostics: &[Diagnostic], @@ -34,11 +36,19 @@ pub fn generate_noqa_edits( external: &[String], noqa_line_for: &NoqaMapping, line_ending: LineEnding, + suppressions: &Suppressions, ) -> Vec> { let file_directives = FileNoqaDirectives::extract(locator, comment_ranges, external, path); let exemption = FileExemption::from(&file_directives); let directives = NoqaDirectives::from_commented_ranges(comment_ranges, external, path, locator); - let comments = find_noqa_comments(diagnostics, locator, &exemption, &directives, noqa_line_for); + let comments = find_noqa_comments( + diagnostics, + locator, + &exemption, + &directives, + noqa_line_for, + suppressions, + ); build_noqa_edits_by_diagnostic(comments, locator, line_ending, None) } @@ -725,6 +735,7 @@ pub(crate) fn add_noqa( noqa_line_for: &NoqaMapping, line_ending: LineEnding, reason: Option<&str>, + suppressions: &Suppressions, ) -> Result { let (count, output) = add_noqa_inner( path, @@ -735,6 +746,7 @@ pub(crate) fn add_noqa( noqa_line_for, line_ending, reason, + suppressions, ); fs::write(path, output)?; @@ -751,6 +763,7 @@ fn add_noqa_inner( noqa_line_for: &NoqaMapping, line_ending: LineEnding, reason: Option<&str>, + suppressions: &Suppressions, ) -> (usize, String) { let mut count = 0; @@ -760,7 +773,14 @@ fn add_noqa_inner( let directives = NoqaDirectives::from_commented_ranges(comment_ranges, external, path, locator); - let comments = find_noqa_comments(diagnostics, locator, &exemption, &directives, noqa_line_for); + let comments = find_noqa_comments( + diagnostics, + locator, + &exemption, + &directives, + noqa_line_for, + suppressions, + ); let edits = build_noqa_edits_by_line(comments, locator, line_ending, reason); @@ -859,6 +879,7 @@ fn find_noqa_comments<'a>( exemption: &'a FileExemption, directives: &'a NoqaDirectives, noqa_line_for: &NoqaMapping, + suppressions: &Suppressions, ) -> Vec>> { // List of noqa comments, ordered to match up with `messages` let mut comments_by_line: Vec>> = vec![]; @@ -875,6 +896,12 @@ fn find_noqa_comments<'a>( continue; } + // Apply ranged suppressions next + if suppressions.check_diagnostic(message) { + comments_by_line.push(None); + continue; + } + // Is the violation ignored by a `noqa` directive on the parent line? if let Some(parent) = message.parent() { if let Some(directive_line) = @@ -1253,6 +1280,7 @@ mod tests { use crate::rules::pycodestyle::rules::{AmbiguousVariableName, UselessSemicolon}; use crate::rules::pyflakes::rules::UnusedVariable; use crate::rules::pyupgrade::rules::PrintfStringFormatting; + use crate::suppression::Suppressions; use crate::{Edit, Violation}; use crate::{Locator, generate_noqa_edits}; @@ -2848,6 +2876,7 @@ mod tests { &noqa_line_for, LineEnding::Lf, None, + &Suppressions::default(), ); assert_eq!(count, 0); assert_eq!(output, format!("{contents}")); @@ -2872,6 +2901,7 @@ mod tests { &noqa_line_for, LineEnding::Lf, None, + &Suppressions::default(), ); assert_eq!(count, 1); assert_eq!(output, "x = 1 # noqa: F841\n"); @@ -2903,6 +2933,7 @@ mod tests { &noqa_line_for, LineEnding::Lf, None, + &Suppressions::default(), ); assert_eq!(count, 1); assert_eq!(output, "x = 1 # noqa: E741, F841\n"); @@ -2934,6 +2965,7 @@ mod tests { &noqa_line_for, LineEnding::Lf, None, + &Suppressions::default(), ); assert_eq!(count, 0); assert_eq!(output, "x = 1 # noqa"); @@ -2956,6 +2988,7 @@ print( let messages = [PrintfStringFormatting .into_diagnostic(TextRange::new(12.into(), 79.into()), &source_file)]; let comment_ranges = CommentRanges::default(); + let suppressions = Suppressions::default(); let edits = generate_noqa_edits( path, &messages, @@ -2964,6 +2997,7 @@ print( &[], &noqa_line_for, LineEnding::Lf, + &suppressions, ); assert_eq!( edits, @@ -2987,6 +3021,7 @@ bar = [UselessSemicolon.into_diagnostic(TextRange::new(4.into(), 5.into()), &source_file)]; let noqa_line_for = NoqaMapping::default(); let comment_ranges = CommentRanges::default(); + let suppressions = Suppressions::default(); let edits = generate_noqa_edits( path, &messages, @@ -2995,6 +3030,7 @@ bar = &[], &noqa_line_for, LineEnding::Lf, + &suppressions, ); assert_eq!( edits, diff --git a/crates/ruff_linter/src/preview.rs b/crates/ruff_linter/src/preview.rs index 52be730545..93a49e63a0 100644 --- a/crates/ruff_linter/src/preview.rs +++ b/crates/ruff_linter/src/preview.rs @@ -286,3 +286,8 @@ pub(crate) const fn is_s310_resolve_string_literal_bindings_enabled( ) -> bool { settings.preview.is_enabled() } + +// https://github.com/astral-sh/ruff/pull/21623 +pub(crate) const fn is_range_suppressions_enabled(settings: &LinterSettings) -> bool { + settings.preview.is_enabled() +} diff --git a/crates/ruff_linter/src/rules/pyflakes/mod.rs b/crates/ruff_linter/src/rules/pyflakes/mod.rs index d290ed38c5..02cd5158a8 100644 --- a/crates/ruff_linter/src/rules/pyflakes/mod.rs +++ b/crates/ruff_linter/src/rules/pyflakes/mod.rs @@ -28,6 +28,7 @@ mod tests { use crate::settings::types::PreviewMode; use crate::settings::{LinterSettings, flags}; use crate::source_kind::SourceKind; + use crate::suppression::Suppressions; use crate::test::{test_contents, test_path, test_snippet}; use crate::{Locator, assert_diagnostics, assert_diagnostics_diff, directives}; @@ -955,6 +956,8 @@ mod tests { &locator, &indexer, ); + let suppressions = + Suppressions::from_tokens(&settings, locator.contents(), parsed.tokens()); let mut messages = check_path( Path::new(""), None, @@ -968,6 +971,7 @@ mod tests { source_type, &parsed, target_version, + &suppressions, ); messages.sort_by(Diagnostic::ruff_start_ordering); let actual = messages diff --git a/crates/ruff_linter/src/rules/ruff/mod.rs b/crates/ruff_linter/src/rules/ruff/mod.rs index 5f06ffdb9f..c2d03fb1ae 100644 --- a/crates/ruff_linter/src/rules/ruff/mod.rs +++ b/crates/ruff_linter/src/rules/ruff/mod.rs @@ -305,6 +305,25 @@ mod tests { Ok(()) } + #[test] + fn range_suppressions() -> Result<()> { + assert_diagnostics_diff!( + Path::new("ruff/suppressions.py"), + &settings::LinterSettings::for_rules(vec![ + Rule::UnusedVariable, + Rule::AmbiguousVariableName, + Rule::UnusedNOQA, + ]), + &settings::LinterSettings::for_rules(vec![ + Rule::UnusedVariable, + Rule::AmbiguousVariableName, + Rule::UnusedNOQA, + ]) + .with_preview_mode(), + ); + Ok(()) + } + #[test] fn ruf100_0() -> Result<()> { let diagnostics = test_path( diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__range_suppressions.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__range_suppressions.snap new file mode 100644 index 0000000000..4e09507482 --- /dev/null +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__range_suppressions.snap @@ -0,0 +1,168 @@ +--- +source: crates/ruff_linter/src/rules/ruff/mod.rs +--- +--- Linter settings --- +-linter.preview = disabled ++linter.preview = enabled + +--- Summary --- +Removed: 9 +Added: 1 + +--- Removed --- +E741 Ambiguous variable name: `I` + --> suppressions.py:4:5 + | +2 | # These should both be ignored by the range suppression. +3 | # ruff: disable[E741, F841] +4 | I = 1 + | ^ +5 | # ruff: enable[E741, F841] + | + + +F841 [*] Local variable `I` is assigned to but never used + --> suppressions.py:4:5 + | +2 | # These should both be ignored by the range suppression. +3 | # ruff: disable[E741, F841] +4 | I = 1 + | ^ +5 | # ruff: enable[E741, F841] + | +help: Remove assignment to unused variable `I` +1 | def f(): +2 | # These should both be ignored by the range suppression. +3 | # ruff: disable[E741, F841] + - I = 1 +4 + pass +5 | # ruff: enable[E741, F841] +6 | +7 | +note: This is an unsafe fix and may change runtime behavior + + +E741 Ambiguous variable name: `I` + --> suppressions.py:12:5 + | +10 | # Should also generate an "unmatched suppression" warning. +11 | # ruff:disable[E741,F841] +12 | I = 1 + | ^ + | + + +F841 [*] Local variable `I` is assigned to but never used + --> suppressions.py:12:5 + | +10 | # Should also generate an "unmatched suppression" warning. +11 | # ruff:disable[E741,F841] +12 | I = 1 + | ^ + | +help: Remove assignment to unused variable `I` +9 | # These should both be ignored by the implicit range suppression. +10 | # Should also generate an "unmatched suppression" warning. +11 | # ruff:disable[E741,F841] + - I = 1 +12 + pass +13 | +14 | +15 | def f(): +note: This is an unsafe fix and may change runtime behavior + + +E741 Ambiguous variable name: `I` + --> suppressions.py:26:5 + | +24 | # the other logged to the user. +25 | # ruff: disable[E741] +26 | I = 1 + | ^ +27 | # ruff: enable[E741] + | + + +E741 Ambiguous variable name: `l` + --> suppressions.py:35:5 + | +33 | # middle line should be completely silenced. +34 | # ruff: disable[E741] +35 | l = 0 + | ^ +36 | # ruff: disable[F841] +37 | O = 1 + | + + +E741 Ambiguous variable name: `O` + --> suppressions.py:37:5 + | +35 | l = 0 +36 | # ruff: disable[F841] +37 | O = 1 + | ^ +38 | # ruff: enable[E741] +39 | I = 2 + | + + +F841 [*] Local variable `O` is assigned to but never used + --> suppressions.py:37:5 + | +35 | l = 0 +36 | # ruff: disable[F841] +37 | O = 1 + | ^ +38 | # ruff: enable[E741] +39 | I = 2 + | +help: Remove assignment to unused variable `O` +34 | # ruff: disable[E741] +35 | l = 0 +36 | # ruff: disable[F841] + - O = 1 +37 | # ruff: enable[E741] +38 | I = 2 +39 | # ruff: enable[F841] +note: This is an unsafe fix and may change runtime behavior + + +F841 [*] Local variable `I` is assigned to but never used + --> suppressions.py:39:5 + | +37 | O = 1 +38 | # ruff: enable[E741] +39 | I = 2 + | ^ +40 | # ruff: enable[F841] + | +help: Remove assignment to unused variable `I` +36 | # ruff: disable[F841] +37 | O = 1 +38 | # ruff: enable[E741] + - I = 2 +39 | # ruff: enable[F841] +40 | +41 | +note: This is an unsafe fix and may change runtime behavior + + + +--- Added --- +RUF100 [*] Unused `noqa` directive (unused: `E741`, `F841`) + --> suppressions.py:55:12 + | +53 | # and an unusued noqa diagnostic should be logged. +54 | # ruff:disable[E741,F841] +55 | I = 1 # noqa: E741,F841 + | ^^^^^^^^^^^^^^^^^ +56 | # ruff:enable[E741,F841] + | +help: Remove unused `noqa` directive +52 | # These should both be ignored by the range suppression, +53 | # and an unusued noqa diagnostic should be logged. +54 | # ruff:disable[E741,F841] + - I = 1 # noqa: E741,F841 +55 + I = 1 +56 | # ruff:enable[E741,F841] diff --git a/crates/ruff_linter/src/settings/mod.rs b/crates/ruff_linter/src/settings/mod.rs index b94e4edafb..5d5e35aa8d 100644 --- a/crates/ruff_linter/src/settings/mod.rs +++ b/crates/ruff_linter/src/settings/mod.rs @@ -465,6 +465,12 @@ impl LinterSettings { self } + #[must_use] + pub fn with_preview_mode(mut self) -> Self { + self.preview = PreviewMode::Enabled; + self + } + /// Resolve the [`TargetVersion`] to use for linting. /// /// This method respects the per-file version overrides in diff --git a/crates/ruff_linter/src/suppression.rs b/crates/ruff_linter/src/suppression.rs index 66ad98d25e..3c1a2f57ab 100644 --- a/crates/ruff_linter/src/suppression.rs +++ b/crates/ruff_linter/src/suppression.rs @@ -1,5 +1,6 @@ use compact_str::CompactString; use core::fmt; +use ruff_db::diagnostic::Diagnostic; use ruff_python_ast::token::{TokenKind, Tokens}; use ruff_python_ast::whitespace::indentation; use std::{error::Error, fmt::Formatter}; @@ -9,6 +10,9 @@ use ruff_python_trivia::Cursor; use ruff_text_size::{Ranged, TextLen, TextRange, TextSize, TextSlice}; use smallvec::{SmallVec, smallvec}; +use crate::preview::is_range_suppressions_enabled; +use crate::settings::LinterSettings; + #[allow(unused)] #[derive(Clone, Debug, Eq, PartialEq)] enum SuppressionAction { @@ -98,8 +102,8 @@ pub(crate) struct InvalidSuppression { } #[allow(unused)] -#[derive(Debug)] -pub(crate) struct Suppressions { +#[derive(Debug, Default)] +pub struct Suppressions { /// Valid suppression ranges with associated comments valid: Vec, @@ -112,9 +116,41 @@ pub(crate) struct Suppressions { #[allow(unused)] impl Suppressions { - pub(crate) fn from_tokens(source: &str, tokens: &Tokens) -> Suppressions { - let builder = SuppressionsBuilder::new(source); - builder.load_from_tokens(tokens) + pub fn from_tokens(settings: &LinterSettings, source: &str, tokens: &Tokens) -> Suppressions { + if is_range_suppressions_enabled(settings) { + let builder = SuppressionsBuilder::new(source); + builder.load_from_tokens(tokens) + } else { + Suppressions::default() + } + } + + pub(crate) fn is_empty(&self) -> bool { + self.valid.is_empty() + } + + /// Check if a diagnostic is suppressed by any known range suppressions + pub(crate) fn check_diagnostic(&self, diagnostic: &Diagnostic) -> bool { + if self.valid.is_empty() { + return false; + } + + let Some(code) = diagnostic.secondary_code() else { + return false; + }; + let Some(span) = diagnostic.primary_span() else { + return false; + }; + let Some(range) = span.range() else { + return false; + }; + + for suppression in &self.valid { + if *code == suppression.code.as_str() && suppression.range.contains_range(range) { + return true; + } + } + false } } @@ -457,9 +493,12 @@ mod tests { use ruff_text_size::{TextRange, TextSize}; use similar::DiffableStr; - use crate::suppression::{ - InvalidSuppression, ParseError, Suppression, SuppressionAction, SuppressionComment, - SuppressionParser, Suppressions, + use crate::{ + settings::LinterSettings, + suppression::{ + InvalidSuppression, ParseError, Suppression, SuppressionAction, SuppressionComment, + SuppressionParser, Suppressions, + }, }; #[test] @@ -1376,7 +1415,11 @@ def bar(): /// Parse all suppressions and errors in a module for testing fn debug(source: &'_ str) -> DebugSuppressions<'_> { let parsed = parse(source, ParseOptions::from(Mode::Module)).unwrap(); - let suppressions = Suppressions::from_tokens(source, parsed.tokens()); + let suppressions = Suppressions::from_tokens( + &LinterSettings::default().with_preview_mode(), + source, + parsed.tokens(), + ); DebugSuppressions { source, suppressions, diff --git a/crates/ruff_linter/src/test.rs b/crates/ruff_linter/src/test.rs index 67a6728404..344c921890 100644 --- a/crates/ruff_linter/src/test.rs +++ b/crates/ruff_linter/src/test.rs @@ -32,6 +32,7 @@ use crate::packaging::detect_package_root; use crate::settings::types::UnsafeFixes; use crate::settings::{LinterSettings, flags}; use crate::source_kind::SourceKind; +use crate::suppression::Suppressions; use crate::{Applicability, FixAvailability}; use crate::{Locator, directives}; @@ -234,6 +235,7 @@ pub(crate) fn test_contents<'a>( &locator, &indexer, ); + let suppressions = Suppressions::from_tokens(settings, locator.contents(), parsed.tokens()); let messages = check_path( path, path.parent() @@ -249,6 +251,7 @@ pub(crate) fn test_contents<'a>( source_type, &parsed, target_version, + &suppressions, ); let source_has_errors = parsed.has_invalid_syntax(); @@ -299,6 +302,8 @@ pub(crate) fn test_contents<'a>( &indexer, ); + let suppressions = + Suppressions::from_tokens(settings, locator.contents(), parsed.tokens()); let fixed_messages = check_path( path, None, @@ -312,6 +317,7 @@ pub(crate) fn test_contents<'a>( source_type, &parsed, target_version, + &suppressions, ); if parsed.has_invalid_syntax() && !source_has_errors { diff --git a/crates/ruff_server/src/lint.rs b/crates/ruff_server/src/lint.rs index c9d0d76bec..db3f9ce4d8 100644 --- a/crates/ruff_server/src/lint.rs +++ b/crates/ruff_server/src/lint.rs @@ -20,6 +20,7 @@ use ruff_linter::{ packaging::detect_package_root, settings::flags, source_kind::SourceKind, + suppression::Suppressions, }; use ruff_notebook::Notebook; use ruff_python_codegen::Stylist; @@ -118,6 +119,10 @@ pub(crate) fn check( // Extract the `# noqa` and `# isort: skip` directives from the source. let directives = extract_directives(parsed.tokens(), Flags::all(), &locator, &indexer); + // Parse range suppression comments + let suppressions = + Suppressions::from_tokens(&settings.linter, locator.contents(), parsed.tokens()); + // Generate checks. let diagnostics = check_path( &document_path, @@ -132,6 +137,7 @@ pub(crate) fn check( source_type, &parsed, target_version, + &suppressions, ); let noqa_edits = generate_noqa_edits( @@ -142,6 +148,7 @@ pub(crate) fn check( &settings.linter.external, &directives.noqa_line_for, stylist.line_ending(), + &suppressions, ); let mut diagnostics_map = DiagnosticsMap::default(); diff --git a/crates/ruff_wasm/src/lib.rs b/crates/ruff_wasm/src/lib.rs index 8c18111d16..6dd49a15bf 100644 --- a/crates/ruff_wasm/src/lib.rs +++ b/crates/ruff_wasm/src/lib.rs @@ -2,6 +2,7 @@ use std::path::Path; use js_sys::Error; use ruff_linter::settings::types::PythonVersion; +use ruff_linter::suppression::Suppressions; use serde::{Deserialize, Serialize}; use wasm_bindgen::prelude::*; @@ -212,6 +213,9 @@ impl Workspace { &indexer, ); + let suppressions = + Suppressions::from_tokens(&self.settings.linter, locator.contents(), parsed.tokens()); + // Generate checks. let diagnostics = check_path( Path::new(""), @@ -226,6 +230,7 @@ impl Workspace { source_type, &parsed, target_version, + &suppressions, ); let source_code = locator.to_source_code();