Closes #17042 ## Summary This PR fixes the issue outlined in #17042 where RUF100 (unused-noqa) fails to detect unused file-level noqa directives (`# ruff: noqa` or `# ruff: noqa: {code}`). The issue stems from two underlying causes: 1. For blanket file-level directives (`# ruff: noqa`), there's a circular dependency: the directive exempts all rules including RUF100 itself, which prevents checking for usage. This isn't changed by this PR. I would argue it is intendend behavior - a blanket `# ruff: noqa` directive should exempt all rules including RUF100 itself. 2. For code-specific file-level directives (e.g. `# ruff: noqa: F841`), the handling was missing in the `check_noqa` function. This is added in this PR. ## Notes - For file-level directives, the `matches` array is pre-populated with the specified codes during parsing, unlike line-level directives which only populate their `matches` array when actually suppressing diagnostics. This difference requires the somewhat clunky handling of both cases. I would appreciate guidance on a cleaner design :) - A more fundamental solution would be to change how file-level directives initialize the `matches` array in `FileNoqaDirectives::extract()`, but that requires more substantial changes as it breaks existing functionality. I suspect discussions in #16483 are relevant for this. ## Test Plan - Local verification - Added a test case and fixture
This commit is contained in:
1
crates/ruff_linter/resources/test/fixtures/ruff/RUF100_6.py
vendored
Normal file
1
crates/ruff_linter/resources/test/fixtures/ruff/RUF100_6.py
vendored
Normal file
@@ -0,0 +1 @@
|
||||
# ruff: noqa: F841 -- intentional unused file directive; will be removed
|
||||
8
crates/ruff_linter/resources/test/fixtures/ruff/RUF100_7.py
vendored
Normal file
8
crates/ruff_linter/resources/test/fixtures/ruff/RUF100_7.py
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
# flake8: noqa: F841, E501 -- used followed by unused code
|
||||
# ruff: noqa: E701, F541 -- unused followed by used code
|
||||
|
||||
|
||||
def a():
|
||||
# Violating noqa'd F841 (unused) and F541 (no-placeholder f-string)
|
||||
x = f"Hello, world"
|
||||
return 6
|
||||
@@ -110,10 +110,29 @@ pub(crate) fn check_noqa(
|
||||
&& !exemption.includes(Rule::UnusedNOQA)
|
||||
&& !per_file_ignores.contains(Rule::UnusedNOQA)
|
||||
{
|
||||
for line in noqa_directives.lines() {
|
||||
match &line.directive {
|
||||
let directives: Vec<_> = if settings.preview.is_enabled() {
|
||||
noqa_directives
|
||||
.lines()
|
||||
.iter()
|
||||
.map(|line| (&line.directive, &line.matches, false))
|
||||
.chain(
|
||||
file_noqa_directives
|
||||
.lines()
|
||||
.iter()
|
||||
.map(|line| (&line.parsed_file_exemption, &line.matches, true)),
|
||||
)
|
||||
.collect()
|
||||
} else {
|
||||
noqa_directives
|
||||
.lines()
|
||||
.iter()
|
||||
.map(|line| (&line.directive, &line.matches, false))
|
||||
.collect()
|
||||
};
|
||||
for (directive, matches, is_file_level) in directives {
|
||||
match directive {
|
||||
Directive::All(directive) => {
|
||||
if line.matches.is_empty() {
|
||||
if matches.is_empty() {
|
||||
let edit = delete_comment(directive.range(), locator);
|
||||
let mut diagnostic =
|
||||
Diagnostic::new(UnusedNOQA { codes: None }, directive.range());
|
||||
@@ -137,17 +156,21 @@ pub(crate) fn check_noqa(
|
||||
break;
|
||||
}
|
||||
|
||||
if !seen_codes.insert(original_code) {
|
||||
duplicated_codes.push(original_code);
|
||||
} else if line.matches.iter().any(|match_| *match_ == code)
|
||||
|| settings
|
||||
if seen_codes.insert(original_code) {
|
||||
let is_code_used = if is_file_level {
|
||||
diagnostics
|
||||
.iter()
|
||||
.any(|diag| diag.kind.rule().noqa_code() == code)
|
||||
} else {
|
||||
matches.iter().any(|match_| *match_ == code)
|
||||
} || settings
|
||||
.external
|
||||
.iter()
|
||||
.any(|external| code.starts_with(external))
|
||||
{
|
||||
valid_codes.push(original_code);
|
||||
} else {
|
||||
if let Ok(rule) = Rule::from_code(code) {
|
||||
.any(|external| code.starts_with(external));
|
||||
|
||||
if is_code_used {
|
||||
valid_codes.push(original_code);
|
||||
} else if let Ok(rule) = Rule::from_code(code) {
|
||||
if settings.rules.enabled(rule) {
|
||||
unmatched_codes.push(original_code);
|
||||
} else {
|
||||
@@ -156,6 +179,8 @@ pub(crate) fn check_noqa(
|
||||
} else {
|
||||
unknown_codes.push(original_code);
|
||||
}
|
||||
} else {
|
||||
duplicated_codes.push(original_code);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -171,8 +196,18 @@ pub(crate) fn check_noqa(
|
||||
let edit = if valid_codes.is_empty() {
|
||||
delete_comment(directive.range(), locator)
|
||||
} else {
|
||||
let original_text = locator.slice(directive.range());
|
||||
let prefix = if is_file_level {
|
||||
if original_text.contains("flake8") {
|
||||
"# flake8: noqa: "
|
||||
} else {
|
||||
"# ruff: noqa: "
|
||||
}
|
||||
} else {
|
||||
"# noqa: "
|
||||
};
|
||||
Edit::range_replacement(
|
||||
format!("# noqa: {}", valid_codes.join(", ")),
|
||||
format!("{}{}", prefix, valid_codes.join(", ")),
|
||||
directive.range(),
|
||||
)
|
||||
};
|
||||
|
||||
@@ -315,6 +315,37 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ruff_noqa_filedirective_unused() -> Result<()> {
|
||||
let diagnostics = test_path(
|
||||
Path::new("ruff/RUF100_6.py"),
|
||||
&settings::LinterSettings {
|
||||
preview: PreviewMode::Enabled,
|
||||
..settings::LinterSettings::for_rules(vec![Rule::UnusedNOQA])
|
||||
},
|
||||
)?;
|
||||
assert_messages!(diagnostics);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ruff_noqa_filedirective_unused_last_of_many() -> Result<()> {
|
||||
let diagnostics = test_path(
|
||||
Path::new("ruff/RUF100_7.py"),
|
||||
&settings::LinterSettings {
|
||||
preview: PreviewMode::Enabled,
|
||||
..settings::LinterSettings::for_rules(vec![
|
||||
Rule::UnusedNOQA,
|
||||
Rule::FStringMissingPlaceholders,
|
||||
Rule::LineTooLong,
|
||||
Rule::UnusedVariable,
|
||||
])
|
||||
},
|
||||
)?;
|
||||
assert_messages!(diagnostics);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_rule_code_external_rules() -> Result<()> {
|
||||
let diagnostics = test_path(
|
||||
@@ -324,7 +355,6 @@ mod tests {
|
||||
..settings::LinterSettings::for_rule(Rule::InvalidRuleCode)
|
||||
},
|
||||
)?;
|
||||
|
||||
assert_messages!(diagnostics);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/ruff/mod.rs
|
||||
---
|
||||
RUF100_6.py:1:1: RUF100 [*] Unused `noqa` directive (non-enabled: `F841`)
|
||||
|
|
||||
1 | # ruff: noqa: F841 -- intentional unused file directive; will be removed
|
||||
| ^^^^^^^^^^^^^^^^^^ RUF100
|
||||
|
|
||||
= help: Remove unused `noqa` directive
|
||||
|
||||
ℹ Safe fix
|
||||
1 |-# ruff: noqa: F841 -- intentional unused file directive; will be removed
|
||||
1 |+
|
||||
@@ -0,0 +1,33 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/ruff/mod.rs
|
||||
---
|
||||
RUF100_7.py:1:1: RUF100 [*] Unused `noqa` directive (unused: `E501`)
|
||||
|
|
||||
1 | # flake8: noqa: F841, E501 -- used followed by unused code
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF100
|
||||
2 | # ruff: noqa: E701, F541 -- unused followed by used code
|
||||
|
|
||||
= help: Remove unused `noqa` directive
|
||||
|
||||
ℹ Safe fix
|
||||
1 |-# flake8: noqa: F841, E501 -- used followed by unused code
|
||||
1 |+# flake8: noqa: F841 -- used followed by unused code
|
||||
2 2 | # ruff: noqa: E701, F541 -- unused followed by used code
|
||||
3 3 |
|
||||
4 4 |
|
||||
|
||||
RUF100_7.py:2:1: RUF100 [*] Unused `noqa` directive (non-enabled: `E701`)
|
||||
|
|
||||
1 | # flake8: noqa: F841, E501 -- used followed by unused code
|
||||
2 | # ruff: noqa: E701, F541 -- unused followed by used code
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^ RUF100
|
||||
|
|
||||
= help: Remove unused `noqa` directive
|
||||
|
||||
ℹ Safe fix
|
||||
1 1 | # flake8: noqa: F841, E501 -- used followed by unused code
|
||||
2 |-# ruff: noqa: E701, F541 -- unused followed by used code
|
||||
2 |+# ruff: noqa: F541 -- unused followed by used code
|
||||
3 3 |
|
||||
4 4 |
|
||||
5 5 | def a():
|
||||
Reference in New Issue
Block a user