Skip to content
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
90f82c1
Define rule codes RUF103/104 for invalid/unmatched suppression comments
amyreese Dec 10, 2025
976a52c
Report RUF104 unmatched suppression diagnostics
amyreese Dec 10, 2025
05f63c9
Report invalid suppression diagnostics
amyreese Dec 11, 2025
aa91353
Todos
amyreese Dec 11, 2025
ebf0121
Fix test cases causing panics
amyreese Dec 11, 2025
2b6dc03
Move rule-enabled checks to local logic, extract some bits
amyreese Dec 12, 2025
bcd9374
Generate invalid rule code diagnostics
amyreese Dec 12, 2025
58f5734
Target the diagnostic at the code even if only one code in comment
amyreese Dec 12, 2025
4793f52
clippy
amyreese Dec 12, 2025
c50026e
Document that implicit ranges will produce RUF104
amyreese Dec 12, 2025
a6e9ca8
Add test case covering external rules
amyreese Dec 16, 2025
6cee82d
Correct pub usage
amyreese Dec 16, 2025
5b7d82b
Improve documentation for unmatched diagnostic
amyreese Dec 17, 2025
b659b5b
Mark invalid comment diagnostics as unsafe fixes
amyreese Dec 17, 2025
83706a9
Update test snapshots
amyreese Dec 17, 2025
5c112af
update wording in invalid rule code diagnostics
amyreese Dec 17, 2025
0f3c74f
Revert "Target the diagnostic at the code even if only one code in co…
amyreese Dec 17, 2025
f5532ff
Avoid unnecessary assignments
amyreese Dec 17, 2025
ea17270
Use itertools unique
amyreese Dec 17, 2025
b90f418
Handle external codes and redirects
amyreese Dec 17, 2025
5bdf049
Reorganize check_suppressions to iterate over valid/errors only once
amyreese Dec 18, 2025
0548374
clippy
amyreese Dec 18, 2025
bafc629
Fix docs
amyreese Dec 18, 2025
a6c73ae
Only target the invalid rule code
amyreese Dec 18, 2025
0e8b8aa
Treat missing codes as invalid suppression comment, update error message
amyreese Dec 18, 2025
5f5bd05
Better implementation of highlighting only code for invalid rule codes
amyreese Dec 18, 2025
b14a6ac
More test cases, improve error/fix wording
amyreese Dec 18, 2025
78e8594
Update comment to clarify unused external codes
amyreese Dec 18, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions crates/ruff_linter/resources/test/fixtures/ruff/suppressions.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,3 +86,19 @@ def f():
# Multiple codes but none are used
# ruff: disable[E741, F401, F841]
print("hello")


def f():
# Unknown rule codes
# ruff: disable[YF829]
# ruff: disable[F841, RQW320]
value = 0
# ruff: enable[F841, RQW320]
# ruff: enable[YF829]


def f():
# External rule codes should be ignored
# ruff: disable[TK421]
print("hello")
# ruff: enable[TK421]
2 changes: 2 additions & 0 deletions crates/ruff_linter/src/codes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1063,6 +1063,8 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Ruff, "100") => rules::ruff::rules::UnusedNOQA,
(Ruff, "101") => rules::ruff::rules::RedirectedNOQA,
(Ruff, "102") => rules::ruff::rules::InvalidRuleCode,
(Ruff, "103") => rules::ruff::rules::InvalidSuppressionComment,
(Ruff, "104") => rules::ruff::rules::UnmatchedSuppressionComment,

(Ruff, "200") => rules::ruff::rules::InvalidPyprojectToml,
#[cfg(any(feature = "test-rules", test))]
Expand Down
10 changes: 9 additions & 1 deletion crates/ruff_linter/src/rules/ruff/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -313,12 +313,20 @@ mod tests {
Rule::UnusedVariable,
Rule::AmbiguousVariableName,
Rule::UnusedNOQA,
]),
Rule::InvalidRuleCode,
Rule::InvalidSuppressionComment,
Rule::UnmatchedSuppressionComment,
])
.with_external_rules(&["TK421"]),
&settings::LinterSettings::for_rules(vec![
Rule::UnusedVariable,
Rule::AmbiguousVariableName,
Rule::UnusedNOQA,
Rule::InvalidRuleCode,
Rule::InvalidSuppressionComment,
Rule::UnmatchedSuppressionComment,
])
.with_external_rules(&["TK421"])
.with_preview_mode(),
);
Ok(())
Expand Down
37 changes: 30 additions & 7 deletions crates/ruff_linter/src/rules/ruff/rules/invalid_rule_code.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,21 @@ use crate::registry::Rule;
use crate::rule_redirects::get_redirect_target;
use crate::{AlwaysFixableViolation, Edit, Fix};

#[derive(Debug, PartialEq, Eq)]
pub(crate) enum InvalidRuleCodeKind {
Noqa,
Suppression,
}

impl InvalidRuleCodeKind {
fn as_str(&self) -> &str {
match self {
InvalidRuleCodeKind::Noqa => "`# noqa`",
InvalidRuleCodeKind::Suppression => "suppression",
}
}
}

/// ## What it does
/// Checks for `noqa` codes that are invalid.
///
Expand Down Expand Up @@ -36,12 +51,17 @@ use crate::{AlwaysFixableViolation, Edit, Fix};
#[violation_metadata(preview_since = "0.11.4")]
pub(crate) struct InvalidRuleCode {
pub(crate) rule_code: String,
pub(crate) kind: InvalidRuleCodeKind,
}

impl AlwaysFixableViolation for InvalidRuleCode {
#[derive_message_formats]
fn message(&self) -> String {
format!("Invalid rule code in `# noqa`: {}", self.rule_code)
format!(
"Invalid rule code in {}: {}",
self.kind.as_str(),
self.rule_code
)
}

fn fix_title(&self) -> String {
Expand All @@ -61,15 +81,17 @@ pub(crate) fn invalid_noqa_code(
continue;
};

let all_valid = directive.iter().all(|code| code_is_valid(code, external));
let all_valid = directive
.iter()
.all(|code| code_is_valid(code.as_str(), external));

if all_valid {
continue;
}

let (valid_codes, invalid_codes): (Vec<_>, Vec<_>) = directive
.iter()
.partition(|&code| code_is_valid(code, external));
.partition(|&code| code_is_valid(code.as_str(), external));

if valid_codes.is_empty() {
all_codes_invalid_diagnostic(directive, invalid_codes, context);
Expand All @@ -81,10 +103,9 @@ pub(crate) fn invalid_noqa_code(
}
}

fn code_is_valid(code: &Code, external: &[String]) -> bool {
let code_str = code.as_str();
Rule::from_code(get_redirect_target(code_str).unwrap_or(code_str)).is_ok()
|| external.iter().any(|ext| code_str.starts_with(ext))
pub(crate) fn code_is_valid(code: &str, external: &[String]) -> bool {
Rule::from_code(get_redirect_target(code).unwrap_or(code)).is_ok()
|| external.iter().any(|ext| code.starts_with(ext))
}

fn all_codes_invalid_diagnostic(
Expand All @@ -100,6 +121,7 @@ fn all_codes_invalid_diagnostic(
.map(Code::as_str)
.collect::<Vec<_>>()
.join(", "),
kind: InvalidRuleCodeKind::Noqa,
},
directive.range(),
)
Expand All @@ -116,6 +138,7 @@ fn some_codes_are_invalid_diagnostic(
.report_diagnostic(
InvalidRuleCode {
rule_code: invalid_code.to_string(),
kind: InvalidRuleCodeKind::Noqa,
},
invalid_code.range(),
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
use ruff_macros::{ViolationMetadata, derive_message_formats};

use crate::AlwaysFixableViolation;
use crate::suppression::{InvalidSuppressionKind, ParseErrorKind};

/// ## What it does
/// Checks for invalid suppression comments
///
/// ## Why is this bad?
/// Invalid suppression comments are ignored by Ruff, and should either
/// be fixed or removed to avoid confusion.
///
/// ## Example
/// ```python
/// ruff: disable # missing codes
/// ```
///
/// Use instead:
/// ```python
/// # ruff: disable[E501]
/// ```
///
/// Or delete the invalid suppression comment.
///
/// ## References
/// - [Ruff error suppression](https://docs.astral.sh/ruff/linter/#error-suppression)
#[derive(ViolationMetadata)]
#[violation_metadata(preview_since = "0.14.9")]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'll have to bump these after the release this week.

pub(crate) struct InvalidSuppressionComment {
pub(crate) kind: InvalidSuppressionCommentKind,
}

impl AlwaysFixableViolation for InvalidSuppressionComment {
#[derive_message_formats]
fn message(&self) -> String {
let msg = match self.kind {
InvalidSuppressionCommentKind::Invalid(InvalidSuppressionKind::Indentation) => {
"unexpected indentation".to_string()
}
InvalidSuppressionCommentKind::Invalid(InvalidSuppressionKind::Trailing) => {
"trailing comments are not supported".to_string()
}
InvalidSuppressionCommentKind::Invalid(InvalidSuppressionKind::Unmatched) => {
"no matching 'disable' comment".to_string()
}
InvalidSuppressionCommentKind::Error(error) => format!("{error}"),
};
format!("Invalid suppression comment: {msg}")
}

fn fix_title(&self) -> String {
"Remove invalid suppression comment".to_string()
}
}

pub(crate) enum InvalidSuppressionCommentKind {
Invalid(InvalidSuppressionKind),
Error(ParseErrorKind),
}
4 changes: 4 additions & 0 deletions crates/ruff_linter/src/rules/ruff/rules/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ pub(crate) use invalid_formatter_suppression_comment::*;
pub(crate) use invalid_index_type::*;
pub(crate) use invalid_pyproject_toml::*;
pub(crate) use invalid_rule_code::*;
pub(crate) use invalid_suppression_comment::*;
pub(crate) use legacy_form_pytest_raises::*;
pub(crate) use logging_eager_conversion::*;
pub(crate) use map_int_version_parsing::*;
Expand All @@ -46,6 +47,7 @@ pub(crate) use starmap_zip::*;
pub(crate) use static_key_dict_comprehension::*;
#[cfg(any(feature = "test-rules", test))]
pub(crate) use test_rules::*;
pub(crate) use unmatched_suppression_comment::*;
pub(crate) use unnecessary_cast_to_int::*;
pub(crate) use unnecessary_iterable_allocation_for_first_element::*;
pub(crate) use unnecessary_key_check::*;
Expand Down Expand Up @@ -87,6 +89,7 @@ mod invalid_formatter_suppression_comment;
mod invalid_index_type;
mod invalid_pyproject_toml;
mod invalid_rule_code;
mod invalid_suppression_comment;
mod legacy_form_pytest_raises;
mod logging_eager_conversion;
mod map_int_version_parsing;
Expand All @@ -113,6 +116,7 @@ mod static_key_dict_comprehension;
mod suppression_comment_visitor;
#[cfg(any(feature = "test-rules", test))]
pub(crate) mod test_rules;
mod unmatched_suppression_comment;
mod unnecessary_cast_to_int;
mod unnecessary_iterable_allocation_for_first_element;
mod unnecessary_key_check;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
use ruff_macros::{ViolationMetadata, derive_message_formats};

use crate::Violation;

/// ## What it does
/// Checks for unmatched range suppression comments
///
/// ## Why is this bad?
/// Unmatched range suppression comments can inadvertently suppress violations
/// over larger sections of code than intended, particularly at module scope.
///
/// ## Example
/// ```python
/// def foo():
/// # ruff: disable[E501] # unmatched
/// REALLY_LONG_VALUES = [...]
///
/// print(REALLY_LONG_VALUES)
/// ```
///
/// Use instead:
/// ```python
/// def foo():
/// # ruff: disable[E501]
/// REALLY_LONG_VALUES = [...]
/// # ruff: enable[E501]
///
/// print(REALLY_LONG_VALUES)
/// ```
///
/// ## References
/// - [Ruff error suppression](https://docs.astral.sh/ruff/linter/#error-suppression)
#[derive(ViolationMetadata)]
#[violation_metadata(preview_since = "0.14.9")]
pub(crate) struct UnmatchedSuppressionComment;

impl Violation for UnmatchedSuppressionComment {
#[derive_message_formats]
fn message(&self) -> String {
"Suppression comment without matching `#ruff:enable` comment".to_string()
}
}
Loading
Loading