Skip to content

Implement Invalid rule provided as rule RUF102 with --fix #17138

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 28 commits into from
Apr 4, 2025
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
203a514
Create rule RUF102 -- Invalid Rule Code -- in preview
maxmynter Apr 2, 2025
437680d
Add tests for RUF102
maxmynter Apr 2, 2025
d0eadbe
Update Schema
maxmynter Apr 2, 2025
223ae54
Integrate RUF102 into ruleset
maxmynter Apr 2, 2025
7a15b89
Apply RUF102 check in noqa handling
maxmynter Apr 2, 2025
1ed6d76
Satisfy Clippy
maxmynter Apr 2, 2025
f7014db
(refactor) Simplify `all_codes_invalid` handling
maxmynter Apr 2, 2025
5cc8305
(tests) Test comment preceeding codes
maxmynter Apr 2, 2025
3f50765
(fixup) Respect external config in invalid rule codes rule
maxmynter Apr 2, 2025
e734d3d
(test) Respect external config in invalid rule codes
maxmynter Apr 2, 2025
c8a17fd
(tests) Add snapshot for External code in ignore invalid codes
maxmynter Apr 2, 2025
2be49a0
(refactor) Use 'split_at' for inline slicing
maxmynter Apr 2, 2025
e3f4e07
(fixup) Use find, not takewhile, to handle multibyte chars
maxmynter Apr 2, 2025
52abe14
Implement two passes and only collect valid codes first pass
maxmynter Apr 2, 2025
d34476f
Move applicable parts RUF102 to general rules test
maxmynter Apr 2, 2025
e044bf9
Remove stale snapshot
maxmynter Apr 2, 2025
22b774b
(fixup) Fix heading level in invalid noqa docs
maxmynter Apr 2, 2025
3050304
Make test name more expressive
maxmynter Apr 3, 2025
2fe49fb
Add external reference to docstring
maxmynter Apr 3, 2025
e9de061
Check common case first in external rule check
maxmynter Apr 3, 2025
979cf98
Use code_str variable
maxmynter Apr 3, 2025
ba56357
Update cago insta snapshot
maxmynter Apr 4, 2025
921f066
Refactor rule fix handling
maxmynter Apr 4, 2025
f9d4cf3
(chore) Wrestling with Clippy
maxmynter Apr 4, 2025
9397c2b
Update / fix test cases
maxmynter Apr 4, 2025
c8a9fe7
(chore) Remove unneccessary ;
maxmynter Apr 4, 2025
e1998a1
Simplifications
MichaReiser Apr 4, 2025
636a364
Delete commented out code
MichaReiser Apr 4, 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
9 changes: 9 additions & 0 deletions crates/ruff_linter/resources/test/fixtures/ruff/RUF102.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Invalid code
import os # noqa: XYZ111
# Valid noqa
import sys # noqa: E402
# Mixed valid and invalid
from typing import List # noqa: F401, INVALID123

# Multiple invalid codes
# noqa: XYZ222, XYZ333
7 changes: 7 additions & 0 deletions crates/ruff_linter/src/checkers/noqa.rs
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,13 @@ pub(crate) fn check_noqa(
);
}

if settings.rules.enabled(Rule::InvalidRuleCode)
&& !per_file_ignores.contains(Rule::InvalidRuleCode)
&& !exemption.enumerates(Rule::InvalidRuleCode)
{
ruff::rules::invalid_noqa_code(diagnostics, &noqa_directives, &locator);
};

ignored_diagnostics.sort_unstable();
ignored_diagnostics
}
1 change: 1 addition & 0 deletions crates/ruff_linter/src/codes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1016,6 +1016,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Ruff, "059") => (RuleGroup::Preview, rules::ruff::rules::UnusedUnpackedVariable),
(Ruff, "100") => (RuleGroup::Stable, rules::ruff::rules::UnusedNOQA),
(Ruff, "101") => (RuleGroup::Stable, rules::ruff::rules::RedirectedNOQA),
(Ruff, "102") => (RuleGroup::Preview, rules::ruff::rules::InvalidRuleCode),

(Ruff, "200") => (RuleGroup::Stable, rules::ruff::rules::InvalidPyprojectToml),
#[cfg(any(feature = "test-rules", test))]
Expand Down
10 changes: 10 additions & 0 deletions crates/ruff_linter/src/rules/ruff/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,16 @@ mod tests {
Ok(())
}

#[test]
fn ruf102() -> Result<()> {
Copy link
Member

@MichaReiser MichaReiser Apr 3, 2025

Choose a reason for hiding this comment

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

NIt:

Suggested change
fn ruf102() -> Result<()> {
fn invalid_rule_code_external_rules() -> Result<()> {

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Adressed in 3050304

let diagnostics = test_path(
Path::new("ruff/RUF102.py"),
&settings::LinterSettings::for_rule(Rule::InvalidRuleCode),
)?;
assert_messages!(diagnostics);
Ok(())
}

#[test]
fn ruff_per_file_ignores() -> Result<()> {
let diagnostics = test_path(
Expand Down
154 changes: 154 additions & 0 deletions crates/ruff_linter/src/rules/ruff/rules/invalid_rule_code.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
use crate::noqa::{Code, Directive};
use crate::registry::Rule;
use crate::Locator;
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
use ruff_macros::{derive_message_formats, ViolationMetadata};
use ruff_text_size::Ranged;

use crate::noqa::{Codes, NoqaDirectiveLine, NoqaDirectives};

/// ### What it does
/// Checks for `noqa` codes that are invalid.
///
/// ## Why is this bad?
/// Invalid rule codes serve no purpose and may indicate outdated code suppressions.
///
/// ## Example
/// ```python
/// import os # noqa: XYZ999
/// ```
///
/// Use instead:
/// ```python
/// import os
/// ```
///
/// Or if there are still valid codes needed:
/// ```python
/// import os # noqa: E402
/// ```

Copy link
Member

Choose a reason for hiding this comment

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

Suggested change

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Adressed in 2fe49fb

#[derive(ViolationMetadata)]
pub(crate) struct InvalidRuleCode {
pub(crate) rule_code: String,
}

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

fn fix_title(&self) -> String {
"Remove the rule code".to_string()
}
}

/// RUF102 for invalid noqa codes
pub(crate) fn invalid_noqa_code(
diagnostics: &mut Vec<Diagnostic>,
noqa_directives: &NoqaDirectives,
locator: &Locator,
) {
for line in noqa_directives.lines() {
let Directive::Codes(directive) = &line.directive else {
continue;
};

let mut invalid_code_refs = vec![];
let mut valid_codes = vec![];

for code in directive.iter() {
let code_str = code.as_str();
if Rule::from_code(code.as_str()).is_err() {
invalid_code_refs.push(code);
} else {
valid_codes.push(code_str);
}
}

if invalid_code_refs.is_empty() {
continue;
} else if valid_codes.is_empty() {
handle_all_codes_invalid(diagnostics, directive, line, locator);
} else {
handle_some_codes_invalid(diagnostics, &invalid_code_refs, &valid_codes, line, locator);
}
}
}

fn handle_all_codes_invalid(
diagnostics: &mut Vec<Diagnostic>,
directive: &Codes<'_>,
line: &NoqaDirectiveLine<'_>,
locator: &Locator,
) {
let invalid_codes = directive
.iter()
.map(|code| code.as_str())
.collect::<Vec<_>>()
.join(", ");

let mut diagnostic = Diagnostic::new(
InvalidRuleCode {
rule_code: invalid_codes,
},
directive.range(),
);

let original_text = locator.slice(line.range());
if let Some(comment_start) = original_text.find('#') {
Copy link
Member

Choose a reason for hiding this comment

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

Does this work with # test # noqa: invalid? Could we use line.directive.range (assuming directive is the Codes variant) here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks for the directive suggestion. It's much cleaner (adressed in f7014db).

Regarding multiple comments on a line it removes the invalid parts

  • # test # noqa: INVALID111# test
  • # test # noqa: INVALID111, VALID111# test # noqa: VALID111

I've added tests for this in 5cc8305.

I guess this is the desired behaviour. Having stale comments isn't nice, but auto removing ones that should stay is worse. What do you think.

Note: I'm writing it with uppercase ASCII plus a number because otherwise it's not parsed as a rule code; in this case the comment remains untouched.

let comment_range = ruff_text_size::TextRange::new(
line.range().start() + ruff_text_size::TextSize::from(comment_start as u32),
line.range().end(),
);
diagnostic.set_fix(Fix::safe_edit(Edit::range_deletion(comment_range)));
} else {
diagnostic.set_fix(Fix::safe_edit(Edit::range_deletion(line.range())));
}
diagnostics.push(diagnostic);
}

fn handle_some_codes_invalid(
diagnostics: &mut Vec<Diagnostic>,
invalid_codes: &[&Code<'_>],
valid_codes: &[&str],
line: &NoqaDirectiveLine<'_>,
locator: &Locator,
) {
let updated_noqa = update_noqa(line, valid_codes, locator);
let fix = Fix::safe_edit(Edit::range_replacement(updated_noqa, line.range()));

for invalid_code in invalid_codes {
let mut diagnostic = Diagnostic::new(
InvalidRuleCode {
rule_code: invalid_code.as_str().to_string(),
},
invalid_code.range(),
);

diagnostic.set_fix(fix.clone());
diagnostics.push(diagnostic);
}
Copy link
Member

Choose a reason for hiding this comment

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

And another case for multispan diagnostics...

I think I'd be surprised that applying the fix for a specific code replaces all invalid codes and not just the one where I positioned my cursor on.

That's why I think we should either:

  • Only create one diagnostic that fixes all invalid codes at once (multispan diagnostics would be great)
  • Create a diagnostic for each invalid code, the fix only removes that one code.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm unsure what you mean by multispan diagnostics or what the exact requirements are. Is that already established in Ruff or something you plan on integrating.

In 921f066 i fixed the handling to create one diagnostic for each invalid code.

Copy link
Member

Choose a reason for hiding this comment

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

I'm unsure what you mean by multispan diagnostics or what the exact requirements are. Is that already established in Ruff or something you plan on integrating.

Sorry, it's something we plan on implementing. This was mainly directed at @ntBre

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah sorry I should have acknowledged and resolved this. I'm keeping a list of places to use multispan diagnostics once they're added to Ruff!

Copy link
Member

Choose a reason for hiding this comment

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

We could create a tracking issue for it which can then be referenced by the reviewers and the references itself becomes a list of known places which can be improved.

Copy link
Contributor

Choose a reason for hiding this comment

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

Oh yes, of course!

}

fn update_noqa(line: &NoqaDirectiveLine<'_>, valid_codes: &[&str], locator: &Locator) -> String {
let noqa_slice = "noqa:";
let formatted_codes = valid_codes.join(", ");
let original_text = locator.slice(line.range());

if let Some(noqa_idx) = original_text.find(noqa_slice) {
let prefix_end = noqa_idx + noqa_slice.len();
let prefix = &original_text[..prefix_end];
let codes_part = &original_text[prefix_end..];
let whitespace_len = codes_part.chars().take_while(|c| c.is_whitespace()).count();
format!(
"{}{}{}",
prefix,
&codes_part[..whitespace_len],
formatted_codes
)
} else {
format!("# noqa: {}", formatted_codes)
}
}
2 changes: 2 additions & 0 deletions crates/ruff_linter/src/rules/ruff/rules/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ pub(crate) use invalid_assert_message_literal_argument::*;
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 map_int_version_parsing::*;
pub(crate) use missing_fstring_syntax::*;
pub(crate) use mutable_class_default::*;
Expand Down Expand Up @@ -78,6 +79,7 @@ mod invalid_assert_message_literal_argument;
mod invalid_formatter_suppression_comment;
mod invalid_index_type;
mod invalid_pyproject_toml;
mod invalid_rule_code;
mod map_int_version_parsing;
mod missing_fstring_syntax;
mod mutable_class_default;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
---
source: crates/ruff_linter/src/rules/ruff/mod.rs
---
RUF102.py:2:12: RUF102 [*] Invalid rule code in `# noqa`: XYZ111
|
1 | # Invalid code
2 | import os # noqa: XYZ111
| ^^^^^^^^^^^^^^ RUF102
3 | # Valid noqa
4 | import sys # noqa: E402
|
= help: Remove the rule code

ℹ Safe fix
1 1 | # Invalid code
2 |-import os # noqa: XYZ111
2 |+import os
3 3 | # Valid noqa
4 4 | import sys # noqa: E402
5 5 | # Mixed valid and invalid

RUF102.py:6:40: RUF102 [*] Invalid rule code in `# noqa`: INVALID123
|
4 | import sys # noqa: E402
5 | # Mixed valid and invalid
6 | from typing import List # noqa: F401, INVALID123
| ^^^^^^^^^^ RUF102
7 |
8 | # Multiple invalid codes
|
= help: Remove the rule code

ℹ Safe fix
3 3 | # Valid noqa
4 4 | import sys # noqa: E402
5 5 | # Mixed valid and invalid
6 |-from typing import List # noqa: F401, INVALID123
6 |+from typing import List # noqa: F401
7 7 |
8 8 | # Multiple invalid codes
9 9 | # noqa: XYZ222, XYZ333

RUF102.py:9:1: RUF102 [*] Invalid rule code in `# noqa`: XYZ222, XYZ333
|
8 | # Multiple invalid codes
9 | # noqa: XYZ222, XYZ333
| ^^^^^^^^^^^^^^^^^^^^^^ RUF102
|
= help: Remove the rule code

ℹ Safe fix
6 6 | from typing import List # noqa: F401, INVALID123
7 7 |
8 8 | # Multiple invalid codes
9 |-# noqa: XYZ222, XYZ333
9 |+
1 change: 1 addition & 0 deletions ruff.schema.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading