diff --git a/crates/ruff_linter/resources/test/fixtures/ruff/RUF100_6.py b/crates/ruff_linter/resources/test/fixtures/ruff/RUF100_6.py new file mode 100644 index 00000000000000..5dbfdb26202483 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/ruff/RUF100_6.py @@ -0,0 +1 @@ +# ruff: noqa: F841 -- intentional unused file directive; will be removed diff --git a/crates/ruff_linter/resources/test/fixtures/ruff/RUF100_7.py b/crates/ruff_linter/resources/test/fixtures/ruff/RUF100_7.py new file mode 100644 index 00000000000000..a8e7b2a0674d36 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/ruff/RUF100_7.py @@ -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 diff --git a/crates/ruff_linter/src/checkers/noqa.rs b/crates/ruff_linter/src/checkers/noqa.rs index 785250f63d80d1..cb01ac686b6872 100644 --- a/crates/ruff_linter/src/checkers/noqa.rs +++ b/crates/ruff_linter/src/checkers/noqa.rs @@ -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(), ) }; diff --git a/crates/ruff_linter/src/rules/ruff/mod.rs b/crates/ruff_linter/src/rules/ruff/mod.rs index 75c59307957f84..c3543aeb90d830 100644 --- a/crates/ruff_linter/src/rules/ruff/mod.rs +++ b/crates/ruff_linter/src/rules/ruff/mod.rs @@ -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(()) } diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__ruff_noqa_filedirective_unused.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__ruff_noqa_filedirective_unused.snap new file mode 100644 index 00000000000000..1a570501c14918 --- /dev/null +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__ruff_noqa_filedirective_unused.snap @@ -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 |+ diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__ruff_noqa_filedirective_unused_last_of_many.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__ruff_noqa_filedirective_unused_last_of_many.snap new file mode 100644 index 00000000000000..49022bf7592127 --- /dev/null +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__ruff_noqa_filedirective_unused_last_of_many.snap @@ -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():