mirror of https://github.com/astral-sh/ruff
apply range suppressions to filter diagnostics (#21623)
Builds on range suppressions from https://github.com/astral-sh/ruff/pull/21441 Filters diagnostics based on parsed valid range suppressions. Issue: #3711
This commit is contained in:
parent
8ea18966cf
commit
4e67a219bb
|
|
@ -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()?;
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
@ -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<usize> {
|
||||
// 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()
|
||||
|
|
|
|||
|
|
@ -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<ModModule>,
|
||||
target_version: TargetVersion,
|
||||
suppressions: &Suppressions,
|
||||
) -> Vec<Diagnostic> {
|
||||
// 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
|
||||
|
|
|
|||
|
|
@ -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<Option<Edit>> {
|
||||
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<usize> {
|
||||
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<Option<NoqaComment<'a>>> {
|
||||
// List of noqa comments, ordered to match up with `messages`
|
||||
let mut comments_by_line: Vec<Option<NoqaComment<'a>>> = 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,
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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("<filename>"),
|
||||
None,
|
||||
|
|
@ -968,6 +971,7 @@ mod tests {
|
|||
source_type,
|
||||
&parsed,
|
||||
target_version,
|
||||
&suppressions,
|
||||
);
|
||||
messages.sort_by(Diagnostic::ruff_start_ordering);
|
||||
let actual = messages
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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<Suppression>,
|
||||
|
||||
|
|
@ -112,9 +116,41 @@ pub(crate) struct Suppressions {
|
|||
|
||||
#[allow(unused)]
|
||||
impl Suppressions {
|
||||
pub(crate) fn from_tokens(source: &str, tokens: &Tokens) -> Suppressions {
|
||||
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::{
|
||||
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,
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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("<filename>"),
|
||||
|
|
@ -226,6 +230,7 @@ impl Workspace {
|
|||
source_type,
|
||||
&parsed,
|
||||
target_version,
|
||||
&suppressions,
|
||||
);
|
||||
|
||||
let source_code = locator.to_source_code();
|
||||
|
|
|
|||
Loading…
Reference in New Issue