From ee58f87228376a653d9b72dec87c006a9d03c518 Mon Sep 17 00:00:00 2001 From: Amethyst Reese Date: Mon, 24 Nov 2025 17:55:37 -0800 Subject: [PATCH 01/11] Prototype filtering diagnostics by range suppressions --- crates/ruff_linter/src/linter.rs | 9 ++++++++ crates/ruff_linter/src/suppression.rs | 31 +++++++++++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/crates/ruff_linter/src/linter.rs b/crates/ruff_linter/src/linter.rs index 3ec070dd26385..aaf9f325cb3c9 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; @@ -325,6 +326,14 @@ pub fn check_path( } } + // Apply range suppressions before noqa, so that #noqa on a line already covered by a range + // will be reported as unused noqa rather than unused range. + // TODO: check if enabled? + { + let suppressions = Suppressions::from_tokens(locator.contents(), tokens); + suppressions.filter_diagnostics(&mut context); + } + // Enforce `noqa` directives. if noqa.is_enabled() || context diff --git a/crates/ruff_linter/src/suppression.rs b/crates/ruff_linter/src/suppression.rs index 66ad98d25e304..901ecde9c63e8 100644 --- a/crates/ruff_linter/src/suppression.rs +++ b/crates/ruff_linter/src/suppression.rs @@ -9,6 +9,8 @@ use ruff_python_trivia::Cursor; use ruff_text_size::{Ranged, TextLen, TextRange, TextSize, TextSlice}; use smallvec::{SmallVec, smallvec}; +use crate::checkers::ast::LintContext; + #[allow(unused)] #[derive(Clone, Debug, Eq, PartialEq)] enum SuppressionAction { @@ -116,6 +118,35 @@ impl Suppressions { let builder = SuppressionsBuilder::new(source); builder.load_from_tokens(tokens) } + + /// Check reported diagnostics against the set of valid range suppressions, and remove any + /// diagnostics from the context that should be suppressed by those ranges. + pub(crate) fn filter_diagnostics(&self, context: &mut LintContext) { + let mut ignored: Vec = vec![]; + + 'outer: for (index, diagnostic) in context.iter().enumerate() { + let Some(code) = diagnostic.secondary_code() else { + continue; + }; + let Some(span) = diagnostic.primary_span() else { + continue; + }; + let Some(range) = span.range() else { + continue; + }; + + for suppression in &self.valid { + if *code == suppression.code.as_str() && suppression.range.contains_range(range) { + ignored.push(index); + continue 'outer; + } + } + } + + for index in ignored.iter().rev() { + context.as_mut_vec().swap_remove(*index); + } + } } #[derive(Default)] From dd5d45c6a7d8a16c81635f7745c823198fea0ea3 Mon Sep 17 00:00:00 2001 From: Amethyst Reese Date: Tue, 25 Nov 2025 17:33:53 -0800 Subject: [PATCH 02/11] Refactor suppressions checking into check_noqa --- crates/ruff_linter/src/checkers/noqa.rs | 11 +++++++ crates/ruff_linter/src/linter.rs | 10 ++---- crates/ruff_linter/src/suppression.rs | 44 ++++++++++--------------- 3 files changed, 30 insertions(+), 35 deletions(-) diff --git a/crates/ruff_linter/src/checkers/noqa.rs b/crates/ruff_linter/src/checkers/noqa.rs index 7cf58a5def343..8afacba53c1fd 100644 --- a/crates/ruff_linter/src/checkers/noqa.rs +++ b/crates/ruff_linter/src/checkers/noqa.rs @@ -18,11 +18,13 @@ 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 +33,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 = @@ -60,11 +63,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 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 aaf9f325cb3c9..f00ca66c61da7 100644 --- a/crates/ruff_linter/src/linter.rs +++ b/crates/ruff_linter/src/linter.rs @@ -326,20 +326,13 @@ pub fn check_path( } } - // Apply range suppressions before noqa, so that #noqa on a line already covered by a range - // will be reported as unused noqa rather than unused range. - // TODO: check if enabled? - { - let suppressions = Suppressions::from_tokens(locator.contents(), tokens); - suppressions.filter_diagnostics(&mut context); - } - // Enforce `noqa` directives. if noqa.is_enabled() || context .iter_enabled_rules() .any(|rule_code| rule_code.lint_source().is_noqa()) { + let suppressions = Suppressions::from_tokens(locator.contents(), tokens); let ignored = check_noqa( &mut context, path, @@ -348,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() { diff --git a/crates/ruff_linter/src/suppression.rs b/crates/ruff_linter/src/suppression.rs index 901ecde9c63e8..39eb178bd9839 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,8 +10,6 @@ use ruff_python_trivia::Cursor; use ruff_text_size::{Ranged, TextLen, TextRange, TextSize, TextSlice}; use smallvec::{SmallVec, smallvec}; -use crate::checkers::ast::LintContext; - #[allow(unused)] #[derive(Clone, Debug, Eq, PartialEq)] enum SuppressionAction { @@ -119,33 +118,24 @@ impl Suppressions { builder.load_from_tokens(tokens) } - /// Check reported diagnostics against the set of valid range suppressions, and remove any - /// diagnostics from the context that should be suppressed by those ranges. - pub(crate) fn filter_diagnostics(&self, context: &mut LintContext) { - let mut ignored: Vec = vec![]; - - 'outer: for (index, diagnostic) in context.iter().enumerate() { - let Some(code) = diagnostic.secondary_code() else { - continue; - }; - let Some(span) = diagnostic.primary_span() else { - continue; - }; - let Some(range) = span.range() else { - continue; - }; - - for suppression in &self.valid { - if *code == suppression.code.as_str() && suppression.range.contains_range(range) { - ignored.push(index); - continue 'outer; - } + /// Check if a diagnostic is suppressed by any known range suppressions + pub(crate) fn check_diagnostic(&self, diagnostic: &Diagnostic) -> bool { + 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; } } - - for index in ignored.iter().rev() { - context.as_mut_vec().swap_remove(*index); - } + false } } From df36084563260cd8dc22c6d17fcfbd7539101c0a Mon Sep 17 00:00:00 2001 From: Amethyst Reese Date: Tue, 25 Nov 2025 17:35:05 -0800 Subject: [PATCH 03/11] Short circuit if no suppressions loaded --- crates/ruff_linter/src/suppression.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/ruff_linter/src/suppression.rs b/crates/ruff_linter/src/suppression.rs index 39eb178bd9839..83deca3adc260 100644 --- a/crates/ruff_linter/src/suppression.rs +++ b/crates/ruff_linter/src/suppression.rs @@ -120,6 +120,10 @@ impl Suppressions { /// 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; }; From 67be3d9d408218171c3d2da38a681b86f4382869 Mon Sep 17 00:00:00 2001 From: Amethyst Reese Date: Mon, 1 Dec 2025 17:13:31 -0800 Subject: [PATCH 04/11] Add test covering expected diagnostics from range suppressions --- .../test/fixtures/ruff/suppressions.py | 45 ++++++++++++ crates/ruff_linter/src/rules/ruff/mod.rs | 14 ++++ ...ules__ruff__tests__range_suppressions.snap | 69 +++++++++++++++++++ 3 files changed, 128 insertions(+) create mode 100644 crates/ruff_linter/resources/test/fixtures/ruff/suppressions.py create mode 100644 crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__range_suppressions.snap 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 0000000000000..b5b5158eccd27 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/ruff/suppressions.py @@ -0,0 +1,45 @@ +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 range suppression. + # ruff:disable[E741,F841] + I = 1 + # ruff:enable[E741,F841] + + +def f(): + # These should both be ignored by the range suppression. + # ruff: disable[E741] + # ruff: disable[F841] + I = 1 + # ruff: enable[E741] + # ruff: enable[F841] + + +def f(): + # One should both be ignored by the range suppression, and + # the other logged to the user. + # ruff: disable[E741] + I = 1 + # ruff: enable[E741] + + +def f(): + # Neither of these are ignored and warning is + # 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/rules/ruff/mod.rs b/crates/ruff_linter/src/rules/ruff/mod.rs index 5f06ffdb9f7b4..4f67ed664c6c0 100644 --- a/crates/ruff_linter/src/rules/ruff/mod.rs +++ b/crates/ruff_linter/src/rules/ruff/mod.rs @@ -305,6 +305,20 @@ mod tests { Ok(()) } + #[test] + fn range_suppressions() -> Result<()> { + let diagnostics = test_path( + Path::new("ruff/suppressions.py"), + &settings::LinterSettings::for_rules(vec![ + Rule::UnusedVariable, + Rule::AmbiguousVariableName, + Rule::UnusedNOQA, + ]), + )?; + assert_diagnostics!(diagnostics); + 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 0000000000000..c067b349ff076 --- /dev/null +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__range_suppressions.snap @@ -0,0 +1,69 @@ +--- +source: crates/ruff_linter/src/rules/ruff/mod.rs +--- +F841 [*] Local variable `I` is assigned to but never used + --> suppressions.py:28:5 + | +26 | # the other logged to the user. +27 | # ruff: disable[E741] +28 | I = 1 + | ^ +29 | # ruff: enable[E741] + | +help: Remove assignment to unused variable `I` +25 | # One should both be ignored by the range suppression, and +26 | # the other logged to the user. +27 | # ruff: disable[E741] + - I = 1 +28 + pass +29 | # ruff: enable[E741] +30 | +31 | +note: This is an unsafe fix and may change runtime behavior + +E741 Ambiguous variable name: `I` + --> suppressions.py:36:5 + | +34 | # logged to user +35 | # ruff: disable[E501] +36 | I = 1 + | ^ +37 | # ruff: enable[E501] + | + +F841 [*] Local variable `I` is assigned to but never used + --> suppressions.py:36:5 + | +34 | # logged to user +35 | # ruff: disable[E501] +36 | I = 1 + | ^ +37 | # ruff: enable[E501] + | +help: Remove assignment to unused variable `I` +33 | # Neither of these are ignored and warning is +34 | # logged to user +35 | # ruff: disable[E501] + - I = 1 +36 + pass +37 | # ruff: enable[E501] +38 | +39 | +note: This is an unsafe fix and may change runtime behavior + +RUF100 [*] Unused `noqa` directive (unused: `E741`, `F841`) + --> suppressions.py:44:12 + | +42 | # and an unusued noqa diagnostic should be logged. +43 | # ruff:disable[E741,F841] +44 | I = 1 # noqa: E741,F841 + | ^^^^^^^^^^^^^^^^^ +45 | # ruff:enable[E741,F841] + | +help: Remove unused `noqa` directive +41 | # These should both be ignored by the range suppression, +42 | # and an unusued noqa diagnostic should be logged. +43 | # ruff:disable[E741,F841] + - I = 1 # noqa: E741,F841 +44 + I = 1 +45 | # ruff:enable[E741,F841] From 36fbd223be344eb57a585b96bad35bbf90e4de28 Mon Sep 17 00:00:00 2001 From: Amethyst Reese Date: Tue, 2 Dec 2025 15:06:00 -0800 Subject: [PATCH 05/11] Gate suppression parsing/checking on `--preview` --- crates/ruff_linter/src/checkers/noqa.rs | 3 +- crates/ruff_linter/src/linter.rs | 7 +- crates/ruff_linter/src/preview.rs | 5 + crates/ruff_linter/src/rules/ruff/mod.rs | 17 +- ...f__tests__range_suppressions_preview.snap} | 0 ...uff__tests__range_suppressions_stable.snap | 154 ++++++++++++++++++ crates/ruff_linter/src/settings/mod.rs | 6 + crates/ruff_linter/src/suppression.rs | 2 +- 8 files changed, 190 insertions(+), 4 deletions(-) rename crates/ruff_linter/src/rules/ruff/snapshots/{ruff_linter__rules__ruff__tests__range_suppressions.snap => ruff_linter__rules__ruff__tests__range_suppressions_preview.snap} (100%) create mode 100644 crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__range_suppressions_stable.snap diff --git a/crates/ruff_linter/src/checkers/noqa.rs b/crates/ruff_linter/src/checkers/noqa.rs index 8afacba53c1fd..b9e15e91d5394 100644 --- a/crates/ruff_linter/src/checkers/noqa.rs +++ b/crates/ruff_linter/src/checkers/noqa.rs @@ -12,6 +12,7 @@ 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; @@ -70,7 +71,7 @@ pub(crate) fn check_noqa( } // Apply ranged suppressions next - if suppressions.check_diagnostic(diagnostic) { + if is_range_suppressions_enabled(settings) && suppressions.check_diagnostic(diagnostic) { ignored_diagnostics.push(index); continue; } diff --git a/crates/ruff_linter/src/linter.rs b/crates/ruff_linter/src/linter.rs index f00ca66c61da7..3c9515fc339a7 100644 --- a/crates/ruff_linter/src/linter.rs +++ b/crates/ruff_linter/src/linter.rs @@ -26,6 +26,7 @@ use crate::doc_lines::{doc_lines_from_ast, doc_lines_from_tokens}; use crate::fix::{FixResult, fix_file}; use crate::noqa::add_noqa; use crate::package::PackageRoot; +use crate::preview::is_range_suppressions_enabled; use crate::registry::Rule; #[cfg(any(feature = "test-rules", test))] use crate::rules::ruff::rules::test_rules::{self, TEST_RULES, TestRule}; @@ -332,7 +333,11 @@ pub fn check_path( .iter_enabled_rules() .any(|rule_code| rule_code.lint_source().is_noqa()) { - let suppressions = Suppressions::from_tokens(locator.contents(), tokens); + let suppressions = if is_range_suppressions_enabled(settings) { + Suppressions::from_tokens(locator.contents(), tokens) + } else { + Suppressions::default() + }; let ignored = check_noqa( &mut context, path, diff --git a/crates/ruff_linter/src/preview.rs b/crates/ruff_linter/src/preview.rs index 52be7305454f8..93a49e63a049a 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/ruff/mod.rs b/crates/ruff_linter/src/rules/ruff/mod.rs index 4f67ed664c6c0..8e1877048efae 100644 --- a/crates/ruff_linter/src/rules/ruff/mod.rs +++ b/crates/ruff_linter/src/rules/ruff/mod.rs @@ -306,7 +306,7 @@ mod tests { } #[test] - fn range_suppressions() -> Result<()> { + fn range_suppressions_stable() -> Result<()> { let diagnostics = test_path( Path::new("ruff/suppressions.py"), &settings::LinterSettings::for_rules(vec![ @@ -319,6 +319,21 @@ mod tests { Ok(()) } + #[test] + fn range_suppressions_preview() -> Result<()> { + let diagnostics = test_path( + Path::new("ruff/suppressions.py"), + &settings::LinterSettings::for_rules(vec![ + Rule::UnusedVariable, + Rule::AmbiguousVariableName, + Rule::UnusedNOQA, + ]) + .with_preview_mode(), + )?; + assert_diagnostics!(diagnostics); + 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_preview.snap similarity index 100% rename from crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__range_suppressions.snap rename to crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__range_suppressions_preview.snap diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__range_suppressions_stable.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__range_suppressions_stable.snap new file mode 100644 index 0000000000000..9e8784faa5f6a --- /dev/null +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__range_suppressions_stable.snap @@ -0,0 +1,154 @@ +--- +source: crates/ruff_linter/src/rules/ruff/mod.rs +--- +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:11:5 + | + 9 | # These should both be ignored by the range suppression. +10 | # ruff:disable[E741,F841] +11 | I = 1 + | ^ +12 | # ruff:enable[E741,F841] + | + +F841 [*] Local variable `I` is assigned to but never used + --> suppressions.py:11:5 + | + 9 | # These should both be ignored by the range suppression. +10 | # ruff:disable[E741,F841] +11 | I = 1 + | ^ +12 | # ruff:enable[E741,F841] + | +help: Remove assignment to unused variable `I` +8 | def f(): +9 | # These should both be ignored by the range suppression. +10 | # ruff:disable[E741,F841] + - I = 1 +11 + pass +12 | # ruff:enable[E741,F841] +13 | +14 | +note: This is an unsafe fix and may change runtime behavior + +E741 Ambiguous variable name: `I` + --> suppressions.py:19:5 + | +17 | # ruff: disable[E741] +18 | # ruff: disable[F841] +19 | I = 1 + | ^ +20 | # ruff: enable[E741] +21 | # ruff: enable[F841] + | + +F841 [*] Local variable `I` is assigned to but never used + --> suppressions.py:19:5 + | +17 | # ruff: disable[E741] +18 | # ruff: disable[F841] +19 | I = 1 + | ^ +20 | # ruff: enable[E741] +21 | # ruff: enable[F841] + | +help: Remove assignment to unused variable `I` +16 | # These should both be ignored by the range suppression. +17 | # ruff: disable[E741] +18 | # ruff: disable[F841] + - I = 1 +19 + pass +20 | # ruff: enable[E741] +21 | # ruff: enable[F841] +22 | +note: This is an unsafe fix and may change runtime behavior + +E741 Ambiguous variable name: `I` + --> suppressions.py:28:5 + | +26 | # the other logged to the user. +27 | # ruff: disable[E741] +28 | I = 1 + | ^ +29 | # ruff: enable[E741] + | + +F841 [*] Local variable `I` is assigned to but never used + --> suppressions.py:28:5 + | +26 | # the other logged to the user. +27 | # ruff: disable[E741] +28 | I = 1 + | ^ +29 | # ruff: enable[E741] + | +help: Remove assignment to unused variable `I` +25 | # One should both be ignored by the range suppression, and +26 | # the other logged to the user. +27 | # ruff: disable[E741] + - I = 1 +28 + pass +29 | # ruff: enable[E741] +30 | +31 | +note: This is an unsafe fix and may change runtime behavior + +E741 Ambiguous variable name: `I` + --> suppressions.py:36:5 + | +34 | # logged to user +35 | # ruff: disable[E501] +36 | I = 1 + | ^ +37 | # ruff: enable[E501] + | + +F841 [*] Local variable `I` is assigned to but never used + --> suppressions.py:36:5 + | +34 | # logged to user +35 | # ruff: disable[E501] +36 | I = 1 + | ^ +37 | # ruff: enable[E501] + | +help: Remove assignment to unused variable `I` +33 | # Neither of these are ignored and warning is +34 | # logged to user +35 | # ruff: disable[E501] + - I = 1 +36 + pass +37 | # ruff: enable[E501] +38 | +39 | +note: This is an unsafe fix and may change runtime behavior diff --git a/crates/ruff_linter/src/settings/mod.rs b/crates/ruff_linter/src/settings/mod.rs index b94e4edafbc29..5d5e35aa8d0a0 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 83deca3adc260..b43eae64df267 100644 --- a/crates/ruff_linter/src/suppression.rs +++ b/crates/ruff_linter/src/suppression.rs @@ -99,7 +99,7 @@ pub(crate) struct InvalidSuppression { } #[allow(unused)] -#[derive(Debug)] +#[derive(Debug, Default)] pub(crate) struct Suppressions { /// Valid suppression ranges with associated comments valid: Vec, From 7723b63627d4faff027954d38899de47b4d7d8a8 Mon Sep 17 00:00:00 2001 From: Amethyst Reese Date: Tue, 2 Dec 2025 15:08:11 -0800 Subject: [PATCH 06/11] english --- .../ruff_linter/resources/test/fixtures/ruff/suppressions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/ruff_linter/resources/test/fixtures/ruff/suppressions.py b/crates/ruff_linter/resources/test/fixtures/ruff/suppressions.py index b5b5158eccd27..d8de23de51432 100644 --- a/crates/ruff_linter/resources/test/fixtures/ruff/suppressions.py +++ b/crates/ruff_linter/resources/test/fixtures/ruff/suppressions.py @@ -22,7 +22,7 @@ def f(): def f(): - # One should both be ignored by the range suppression, and + # One should be ignored by the range suppression, and # the other logged to the user. # ruff: disable[E741] I = 1 @@ -30,7 +30,7 @@ def f(): def f(): - # Neither of these are ignored and warning is + # Neither of these are ignored and warnings are # logged to user # ruff: disable[E501] I = 1 From 2f57e205a78a44f6360075eb967a3339e0308877 Mon Sep 17 00:00:00 2001 From: Amethyst Reese Date: Tue, 2 Dec 2025 15:37:20 -0800 Subject: [PATCH 07/11] More integration test cases --- .../test/fixtures/ruff/suppressions.py | 25 +- ...ff__tests__range_suppressions_preview.snap | 138 ++++++++--- ...uff__tests__range_suppressions_stable.snap | 223 ++++++++++++------ 3 files changed, 272 insertions(+), 114 deletions(-) diff --git a/crates/ruff_linter/resources/test/fixtures/ruff/suppressions.py b/crates/ruff_linter/resources/test/fixtures/ruff/suppressions.py index d8de23de51432..7a70c4d548b0c 100644 --- a/crates/ruff_linter/resources/test/fixtures/ruff/suppressions.py +++ b/crates/ruff_linter/resources/test/fixtures/ruff/suppressions.py @@ -6,19 +6,17 @@ def f(): def f(): - # These should both be ignored by the range suppression. + # These should both be ignored by the implicit range suppression. + # Should also generate an "unmatched suppression" warning. # ruff:disable[E741,F841] I = 1 - # ruff:enable[E741,F841] def f(): - # These should both be ignored by the range suppression. - # ruff: disable[E741] - # ruff: disable[F841] + # Neither warning is ignored, and an "unmatched suppression" + # should be generated. I = 1 - # ruff: enable[E741] - # ruff: enable[F841] + # ruff: enable[E741, F841] def f(): @@ -29,6 +27,19 @@ def f(): # 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 diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__range_suppressions_preview.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__range_suppressions_preview.snap index c067b349ff076..7206a81b18c45 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__range_suppressions_preview.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__range_suppressions_preview.snap @@ -1,69 +1,129 @@ --- source: crates/ruff_linter/src/rules/ruff/mod.rs --- +E741 Ambiguous variable name: `I` + --> suppressions.py:18:5 + | +16 | # Neither warning is ignored, and an "unmatched suppression" +17 | # should be generated. +18 | I = 1 + | ^ +19 | # ruff: enable[E741, F841] + | + F841 [*] Local variable `I` is assigned to but never used - --> suppressions.py:28:5 + --> suppressions.py:18:5 | -26 | # the other logged to the user. -27 | # ruff: disable[E741] -28 | I = 1 +16 | # Neither warning is ignored, and an "unmatched suppression" +17 | # should be generated. +18 | I = 1 | ^ -29 | # ruff: enable[E741] +19 | # ruff: enable[E741, F841] | help: Remove assignment to unused variable `I` -25 | # One should both be ignored by the range suppression, and -26 | # the other logged to the user. -27 | # ruff: disable[E741] +15 | def f(): +16 | # Neither warning is ignored, and an "unmatched suppression" +17 | # should be generated. - I = 1 -28 + pass -29 | # ruff: enable[E741] -30 | -31 | +18 + pass +19 | # ruff: enable[E741, F841] +20 | +21 | +note: This is an unsafe fix and may change runtime behavior + +F841 [*] Local variable `I` is assigned to but never used + --> suppressions.py:26:5 + | +24 | # the other logged to the user. +25 | # ruff: disable[E741] +26 | I = 1 + | ^ +27 | # ruff: enable[E741] + | +help: Remove assignment to unused variable `I` +23 | # One should be ignored by the range suppression, and +24 | # the other logged to the user. +25 | # ruff: disable[E741] + - I = 1 +26 + pass +27 | # ruff: enable[E741] +28 | +29 | +note: This is an unsafe fix and may change runtime behavior + +F841 [*] Local variable `l` is assigned to but never used + --> 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 + | +help: Remove assignment to unused variable `l` +32 | # lines should each log a different warning, while the +33 | # middle line should be completely silenced. +34 | # ruff: disable[E741] + - l = 0 +35 | # ruff: disable[F841] +36 | O = 1 +37 | # ruff: enable[E741] note: This is an unsafe fix and may change runtime behavior E741 Ambiguous variable name: `I` - --> suppressions.py:36:5 + --> suppressions.py:39:5 | -34 | # logged to user -35 | # ruff: disable[E501] -36 | I = 1 +37 | O = 1 +38 | # ruff: enable[E741] +39 | I = 2 | ^ -37 | # ruff: enable[E501] +40 | # ruff: enable[F841] | F841 [*] Local variable `I` is assigned to but never used - --> suppressions.py:36:5 + --> suppressions.py:47:5 | -34 | # logged to user -35 | # ruff: disable[E501] -36 | I = 1 +45 | # logged to user +46 | # ruff: disable[E501] +47 | I = 1 | ^ -37 | # ruff: enable[E501] +48 | # ruff: enable[E501] | help: Remove assignment to unused variable `I` -33 | # Neither of these are ignored and warning is -34 | # logged to user -35 | # ruff: disable[E501] +44 | # Neither of these are ignored and warnings are +45 | # logged to user +46 | # ruff: disable[E501] - I = 1 -36 + pass -37 | # ruff: enable[E501] -38 | -39 | +47 + pass +48 | # ruff: enable[E501] +49 | +50 | note: This is an unsafe fix and may change runtime behavior +E741 Ambiguous variable name: `I` + --> suppressions.py:47:5 + | +45 | # logged to user +46 | # ruff: disable[E501] +47 | I = 1 + | ^ +48 | # ruff: enable[E501] + | + RUF100 [*] Unused `noqa` directive (unused: `E741`, `F841`) - --> suppressions.py:44:12 + --> suppressions.py:55:12 | -42 | # and an unusued noqa diagnostic should be logged. -43 | # ruff:disable[E741,F841] -44 | I = 1 # noqa: E741,F841 +53 | # and an unusued noqa diagnostic should be logged. +54 | # ruff:disable[E741,F841] +55 | I = 1 # noqa: E741,F841 | ^^^^^^^^^^^^^^^^^ -45 | # ruff:enable[E741,F841] +56 | # ruff:enable[E741,F841] | help: Remove unused `noqa` directive -41 | # These should both be ignored by the range suppression, -42 | # and an unusued noqa diagnostic should be logged. -43 | # ruff:disable[E741,F841] +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 -44 + I = 1 -45 | # ruff:enable[E741,F841] +55 + I = 1 +56 | # ruff:enable[E741,F841] diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__range_suppressions_stable.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__range_suppressions_stable.snap index 9e8784faa5f6a..be08e909ee986 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__range_suppressions_stable.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__range_suppressions_stable.snap @@ -32,123 +32,210 @@ help: Remove assignment to unused variable `I` note: This is an unsafe fix and may change runtime behavior E741 Ambiguous variable name: `I` - --> suppressions.py:11:5 + --> suppressions.py:12:5 | - 9 | # These should both be ignored by the range suppression. -10 | # ruff:disable[E741,F841] -11 | I = 1 +10 | # Should also generate an "unmatched suppression" warning. +11 | # ruff:disable[E741,F841] +12 | I = 1 | ^ -12 | # ruff:enable[E741,F841] | F841 [*] Local variable `I` is assigned to but never used - --> suppressions.py:11:5 + --> suppressions.py:12:5 | - 9 | # These should both be ignored by the range suppression. -10 | # ruff:disable[E741,F841] -11 | I = 1 +10 | # Should also generate an "unmatched suppression" warning. +11 | # ruff:disable[E741,F841] +12 | I = 1 | ^ -12 | # ruff:enable[E741,F841] | help: Remove assignment to unused variable `I` -8 | def f(): -9 | # These should both be ignored by the range suppression. -10 | # ruff:disable[E741,F841] +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 -11 + pass -12 | # ruff:enable[E741,F841] +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:19:5 + --> suppressions.py:18:5 | -17 | # ruff: disable[E741] -18 | # ruff: disable[F841] -19 | I = 1 +16 | # Neither warning is ignored, and an "unmatched suppression" +17 | # should be generated. +18 | I = 1 | ^ -20 | # ruff: enable[E741] -21 | # ruff: enable[F841] +19 | # ruff: enable[E741, F841] | F841 [*] Local variable `I` is assigned to but never used - --> suppressions.py:19:5 + --> suppressions.py:18:5 | -17 | # ruff: disable[E741] -18 | # ruff: disable[F841] -19 | I = 1 +16 | # Neither warning is ignored, and an "unmatched suppression" +17 | # should be generated. +18 | I = 1 | ^ -20 | # ruff: enable[E741] -21 | # ruff: enable[F841] +19 | # ruff: enable[E741, F841] | help: Remove assignment to unused variable `I` -16 | # These should both be ignored by the range suppression. -17 | # ruff: disable[E741] -18 | # ruff: disable[F841] +15 | def f(): +16 | # Neither warning is ignored, and an "unmatched suppression" +17 | # should be generated. - I = 1 -19 + pass -20 | # ruff: enable[E741] -21 | # ruff: enable[F841] -22 | +18 + pass +19 | # ruff: enable[E741, F841] +20 | +21 | note: This is an unsafe fix and may change runtime behavior E741 Ambiguous variable name: `I` - --> suppressions.py:28:5 + --> suppressions.py:26:5 | -26 | # the other logged to the user. -27 | # ruff: disable[E741] -28 | I = 1 +24 | # the other logged to the user. +25 | # ruff: disable[E741] +26 | I = 1 | ^ -29 | # ruff: enable[E741] +27 | # ruff: enable[E741] | F841 [*] Local variable `I` is assigned to but never used - --> suppressions.py:28:5 + --> suppressions.py:26:5 | -26 | # the other logged to the user. -27 | # ruff: disable[E741] -28 | I = 1 +24 | # the other logged to the user. +25 | # ruff: disable[E741] +26 | I = 1 | ^ -29 | # ruff: enable[E741] +27 | # ruff: enable[E741] | help: Remove assignment to unused variable `I` -25 | # One should both be ignored by the range suppression, and -26 | # the other logged to the user. -27 | # ruff: disable[E741] +23 | # One should be ignored by the range suppression, and +24 | # the other logged to the user. +25 | # ruff: disable[E741] - I = 1 -28 + pass -29 | # ruff: enable[E741] -30 | -31 | +26 + pass +27 | # ruff: enable[E741] +28 | +29 | +note: This is an unsafe fix and may change runtime behavior + +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 + | + +F841 [*] Local variable `l` is assigned to but never used + --> 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 + | +help: Remove assignment to unused variable `l` +32 | # lines should each log a different warning, while the +33 | # middle line should be completely silenced. +34 | # ruff: disable[E741] + - l = 0 +35 | # ruff: disable[F841] +36 | O = 1 +37 | # ruff: enable[E741] +note: This is an unsafe fix and may change runtime behavior + +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 + +E741 Ambiguous variable name: `I` + --> suppressions.py:39:5 + | +37 | O = 1 +38 | # ruff: enable[E741] +39 | I = 2 + | ^ +40 | # ruff: enable[F841] + | + +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 E741 Ambiguous variable name: `I` - --> suppressions.py:36:5 + --> suppressions.py:47:5 | -34 | # logged to user -35 | # ruff: disable[E501] -36 | I = 1 +45 | # logged to user +46 | # ruff: disable[E501] +47 | I = 1 | ^ -37 | # ruff: enable[E501] +48 | # ruff: enable[E501] | F841 [*] Local variable `I` is assigned to but never used - --> suppressions.py:36:5 + --> suppressions.py:47:5 | -34 | # logged to user -35 | # ruff: disable[E501] -36 | I = 1 +45 | # logged to user +46 | # ruff: disable[E501] +47 | I = 1 | ^ -37 | # ruff: enable[E501] +48 | # ruff: enable[E501] | help: Remove assignment to unused variable `I` -33 | # Neither of these are ignored and warning is -34 | # logged to user -35 | # ruff: disable[E501] +44 | # Neither of these are ignored and warnings are +45 | # logged to user +46 | # ruff: disable[E501] - I = 1 -36 + pass -37 | # ruff: enable[E501] -38 | -39 | +47 + pass +48 | # ruff: enable[E501] +49 | +50 | note: This is an unsafe fix and may change runtime behavior From 5ebadd404041aa10bf2bbb6d81abf06fe32c4e22 Mon Sep 17 00:00:00 2001 From: Amethyst Reese Date: Tue, 2 Dec 2025 18:03:17 -0800 Subject: [PATCH 08/11] Added cli tests (currently failing) --- crates/ruff/tests/cli/lint.rs | 119 ++++++++++++++++++++++++++++++++++ 1 file changed, 119 insertions(+) diff --git a/crates/ruff/tests/cli/lint.rs b/crates/ruff/tests/cli/lint.rs index 25500ed346c4f..7fea8dcd50a75 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,53 @@ def unused(x): # noqa: ANN001, ARG001, D103 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()?; From fe830371c56c15b2603aba80741cccdf6a52b852 Mon Sep 17 00:00:00 2001 From: Amethyst Reese Date: Wed, 3 Dec 2025 11:44:26 -0800 Subject: [PATCH 09/11] Load and pass suppressions through when adding noqa comments --- crates/ruff/tests/cli/lint.rs | 47 ++++++++++++++++++++++++++++++++ crates/ruff_linter/src/linter.rs | 7 +++++ crates/ruff_linter/src/noqa.rs | 35 ++++++++++++++++++++++-- 3 files changed, 87 insertions(+), 2 deletions(-) diff --git a/crates/ruff/tests/cli/lint.rs b/crates/ruff/tests/cli/lint.rs index 7fea8dcd50a75..a86d8e81be889 100644 --- a/crates/ruff/tests/cli/lint.rs +++ b/crates/ruff/tests/cli/lint.rs @@ -1704,6 +1704,53 @@ 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()?; diff --git a/crates/ruff_linter/src/linter.rs b/crates/ruff_linter/src/linter.rs index 3c9515fc339a7..0669e110c5748 100644 --- a/crates/ruff_linter/src/linter.rs +++ b/crates/ruff_linter/src/linter.rs @@ -424,6 +424,12 @@ pub fn add_noqa_to_path( target_version, ); + let suppressions = if is_range_suppressions_enabled(settings) { + Suppressions::from_tokens(locator.contents(), parsed.tokens()) + } else { + Suppressions::default() + }; + // Add any missing `# noqa` pragmas. // TODO(dhruvmanila): Add support for Jupyter Notebooks add_noqa( @@ -435,6 +441,7 @@ pub fn add_noqa_to_path( &directives.noqa_line_for, stylist.line_ending(), reason, + &suppressions, ) } diff --git a/crates/ruff_linter/src/noqa.rs b/crates/ruff_linter/src/noqa.rs index da9535817ed07..1ef37cca5d009 100644 --- a/crates/ruff_linter/src/noqa.rs +++ b/crates/ruff_linter/src/noqa.rs @@ -20,6 +20,7 @@ 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. @@ -38,7 +39,15 @@ pub fn generate_noqa_edits( 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 suppressions = Suppressions::default(); // TODO: pipe in tokens or parse from comment ranges + 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 +734,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 +745,7 @@ pub(crate) fn add_noqa( noqa_line_for, line_ending, reason, + suppressions, ); fs::write(path, output)?; @@ -751,6 +762,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 +772,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 +878,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 +895,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 +1279,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 +2875,7 @@ mod tests { &noqa_line_for, LineEnding::Lf, None, + &Suppressions::default(), ); assert_eq!(count, 0); assert_eq!(output, format!("{contents}")); @@ -2872,6 +2900,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 +2932,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 +2964,7 @@ mod tests { &noqa_line_for, LineEnding::Lf, None, + &Suppressions::default(), ); assert_eq!(count, 0); assert_eq!(output, "x = 1 # noqa"); From 210029853ab753301f277227ee6b3d83f0834324 Mon Sep 17 00:00:00 2001 From: Amethyst Reese Date: Wed, 3 Dec 2025 11:51:22 -0800 Subject: [PATCH 10/11] Update comment --- crates/ruff_linter/src/noqa.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/ruff_linter/src/noqa.rs b/crates/ruff_linter/src/noqa.rs index 1ef37cca5d009..87f838149b6f8 100644 --- a/crates/ruff_linter/src/noqa.rs +++ b/crates/ruff_linter/src/noqa.rs @@ -39,7 +39,8 @@ pub fn generate_noqa_edits( 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 suppressions = Suppressions::default(); // TODO: pipe in tokens or parse from comment ranges + // This is called by ruff_server, which already filtered diagnostics via check_path + let suppressions = Suppressions::default(); let comments = find_noqa_comments( diagnostics, locator, From 8f104faa5fb9ef87e06aef822b31f342a177a9d8 Mon Sep 17 00:00:00 2001 From: Amethyst Reese Date: Wed, 3 Dec 2025 14:22:28 -0800 Subject: [PATCH 11/11] Add is_empty to Suppressions, don't abort early in check_noqa --- crates/ruff_linter/src/checkers/noqa.rs | 2 +- crates/ruff_linter/src/suppression.rs | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/crates/ruff_linter/src/checkers/noqa.rs b/crates/ruff_linter/src/checkers/noqa.rs index b9e15e91d5394..2602adeeeef6a 100644 --- a/crates/ruff_linter/src/checkers/noqa.rs +++ b/crates/ruff_linter/src/checkers/noqa.rs @@ -44,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(); } diff --git a/crates/ruff_linter/src/suppression.rs b/crates/ruff_linter/src/suppression.rs index b43eae64df267..dcbd983827cd5 100644 --- a/crates/ruff_linter/src/suppression.rs +++ b/crates/ruff_linter/src/suppression.rs @@ -118,6 +118,10 @@ impl Suppressions { builder.load_from_tokens(tokens) } + 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() {