From 8fdb1623eedc5a3a534e74ef8bcdd3b9af2f569e Mon Sep 17 00:00:00 2001 From: Amethyst Reese Date: Mon, 23 Feb 2026 18:36:57 -0800 Subject: [PATCH 1/6] Report invalid codes in file-level noqa --- crates/ruff_linter/src/checkers/noqa.rs | 8 ++++- .../src/rules/ruff/rules/invalid_rule_code.rs | 30 ++++++++++++------- 2 files changed, 26 insertions(+), 12 deletions(-) diff --git a/crates/ruff_linter/src/checkers/noqa.rs b/crates/ruff_linter/src/checkers/noqa.rs index 7ba55e2294e76..83f026a462402 100644 --- a/crates/ruff_linter/src/checkers/noqa.rs +++ b/crates/ruff_linter/src/checkers/noqa.rs @@ -271,7 +271,13 @@ pub(crate) fn check_noqa( if context.is_rule_enabled(Rule::InvalidRuleCode) && !exemption.enumerates(Rule::InvalidRuleCode) { - ruff::rules::invalid_noqa_code(context, &noqa_directives, locator, &settings.external); + ruff::rules::invalid_noqa_code( + context, + &file_noqa_directives, + &noqa_directives, + locator, + &settings.external, + ); } ignored_diagnostics.sort_unstable(); diff --git a/crates/ruff_linter/src/rules/ruff/rules/invalid_rule_code.rs b/crates/ruff_linter/src/rules/ruff/rules/invalid_rule_code.rs index 038a2d6a46a32..f62425e1bc892 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/invalid_rule_code.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/invalid_rule_code.rs @@ -4,7 +4,7 @@ use ruff_text_size::{Ranged, TextLen, TextRange, TextSize}; use crate::Locator; use crate::checkers::ast::LintContext; use crate::fix::edits::delete_comment; -use crate::noqa::{Code, Directive}; +use crate::noqa::{Code, Directive, FileNoqaDirectives}; use crate::noqa::{Codes, NoqaDirectives}; use crate::registry::Rule; use crate::rule_redirects::get_redirect_target; @@ -83,34 +83,42 @@ impl AlwaysFixableViolation for InvalidRuleCode { /// RUF102 for invalid noqa codes pub(crate) fn invalid_noqa_code( context: &LintContext, + file_noqa_directives: &FileNoqaDirectives, noqa_directives: &NoqaDirectives, locator: &Locator, external: &[String], ) { - for line in noqa_directives.lines() { - let Directive::Codes(directive) = &line.directive else { - continue; - }; - - let all_valid = directive + let check_codes = |codes: &Codes<'_>| { + let all_valid = codes .iter() .all(|code| code_is_valid(code.as_str(), external)); if all_valid { - continue; + return; } - let (valid_codes, invalid_codes): (Vec<_>, Vec<_>) = directive + let (valid_codes, invalid_codes): (Vec<_>, Vec<_>) = codes .iter() .partition(|&code| code_is_valid(code.as_str(), external)); if valid_codes.is_empty() { - all_codes_invalid_diagnostic(directive, invalid_codes, locator, context); + all_codes_invalid_diagnostic(codes, invalid_codes, locator, context); } else { for invalid_code in invalid_codes { - some_codes_are_invalid_diagnostic(directive, invalid_code, locator, context); + some_codes_are_invalid_diagnostic(codes, invalid_code, locator, context); } } + }; + + for line in file_noqa_directives.lines() { + if let Directive::Codes(codes) = &line.parsed_file_exemption { + check_codes(codes); + } + } + for line in noqa_directives.lines() { + if let Directive::Codes(codes) = &line.directive { + check_codes(codes); + } } } From 6621355757bb15c681cad56cff81a06551edd260 Mon Sep 17 00:00:00 2001 From: Amethyst Reese Date: Mon, 23 Feb 2026 18:45:36 -0800 Subject: [PATCH 2/6] test case for file-level ruf102 --- .../resources/test/fixtures/ruff/RUF102_1.py | 8 ++++ crates/ruff_linter/src/rules/ruff/mod.rs | 14 +++++++ ...ules__ruff__tests__RUF102_RUF102_1.py.snap | 38 +++++++++++++++++++ ...d_rule_code_external_rules_file_level.snap | 19 ++++++++++ 4 files changed, 79 insertions(+) create mode 100644 crates/ruff_linter/resources/test/fixtures/ruff/RUF102_1.py create mode 100644 crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF102_RUF102_1.py.snap create mode 100644 crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__invalid_rule_code_external_rules_file_level.snap diff --git a/crates/ruff_linter/resources/test/fixtures/ruff/RUF102_1.py b/crates/ruff_linter/resources/test/fixtures/ruff/RUF102_1.py new file mode 100644 index 0000000000000..c809118f4fd27 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/ruff/RUF102_1.py @@ -0,0 +1,8 @@ +# Invalid file-level code +# ruff: noqa: INVALID123 + +# External file-level code +# ruff: noqa: V123 + +# Valid file-level code +# ruff: noqa: E402 diff --git a/crates/ruff_linter/src/rules/ruff/mod.rs b/crates/ruff_linter/src/rules/ruff/mod.rs index 552b8b1095ca2..e8657856167d0 100644 --- a/crates/ruff_linter/src/rules/ruff/mod.rs +++ b/crates/ruff_linter/src/rules/ruff/mod.rs @@ -121,6 +121,7 @@ mod tests { #[test_case(Rule::RedirectedNOQA, Path::new("RUF101_0.py"))] #[test_case(Rule::RedirectedNOQA, Path::new("RUF101_1.py"))] #[test_case(Rule::InvalidRuleCode, Path::new("RUF102.py"))] + #[test_case(Rule::InvalidRuleCode, Path::new("RUF102_1.py"))] #[test_case(Rule::NonEmptyInitModule, Path::new("RUF067/modules/__init__.py"))] #[test_case(Rule::NonEmptyInitModule, Path::new("RUF067/modules/okay.py"))] fn rules(rule_code: Rule, path: &Path) -> Result<()> { @@ -510,6 +511,19 @@ mod tests { Ok(()) } + #[test] + fn invalid_rule_code_external_rules_file_level() -> Result<()> { + let diagnostics = test_path( + Path::new("ruff/RUF102_1.py"), + &settings::LinterSettings { + external: vec!["V".to_string()], + ..settings::LinterSettings::for_rule(Rule::InvalidRuleCode) + }, + )?; + assert_diagnostics!(diagnostics); + Ok(()) + } + #[test] fn ruff_per_file_ignores() -> Result<()> { let diagnostics = test_path( diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF102_RUF102_1.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF102_RUF102_1.py.snap new file mode 100644 index 0000000000000..29d124efd6d47 --- /dev/null +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF102_RUF102_1.py.snap @@ -0,0 +1,38 @@ +--- +source: crates/ruff_linter/src/rules/ruff/mod.rs +--- +RUF102 [*] Invalid rule code in `# noqa`: INVALID123 + --> RUF102_1.py:2:1 + | +1 | # Invalid file-level code +2 | # ruff: noqa: INVALID123 + | ^^^^^^^^^^^^^^^^^^^^^^^^ +3 | +4 | # External file-level code + | +help: Add non-Ruff rule codes to the `lint.external` configuration option +help: Remove the `# noqa` comment +1 | # Invalid file-level code + - # ruff: noqa: INVALID123 +2 | +3 | # External file-level code +4 | # ruff: noqa: V123 + +RUF102 [*] Invalid rule code in `# noqa`: V123 + --> RUF102_1.py:5:1 + | +4 | # External file-level code +5 | # ruff: noqa: V123 + | ^^^^^^^^^^^^^^^^^^ +6 | +7 | # Valid file-level code + | +help: Add non-Ruff rule codes to the `lint.external` configuration option +help: Remove the `# noqa` comment +2 | # ruff: noqa: INVALID123 +3 | +4 | # External file-level code + - # ruff: noqa: V123 +5 | +6 | # Valid file-level code +7 | # ruff: noqa: E402 diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__invalid_rule_code_external_rules_file_level.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__invalid_rule_code_external_rules_file_level.snap new file mode 100644 index 0000000000000..12581c2826e07 --- /dev/null +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__invalid_rule_code_external_rules_file_level.snap @@ -0,0 +1,19 @@ +--- +source: crates/ruff_linter/src/rules/ruff/mod.rs +--- +RUF102 [*] Invalid rule code in `# noqa`: INVALID123 + --> RUF102_1.py:2:1 + | +1 | # Invalid file-level code +2 | # ruff: noqa: INVALID123 + | ^^^^^^^^^^^^^^^^^^^^^^^^ +3 | +4 | # External file-level code + | +help: Add non-Ruff rule codes to the `lint.external` configuration option +help: Remove the `# noqa` comment +1 | # Invalid file-level code + - # ruff: noqa: INVALID123 +2 | +3 | # External file-level code +4 | # ruff: noqa: V123 From f302c03ecdb784e0cad636a015351e3710a58906 Mon Sep 17 00:00:00 2001 From: Amethyst Reese Date: Mon, 23 Feb 2026 18:45:51 -0800 Subject: [PATCH 3/6] drop old warning --- crates/ruff/tests/cli/lint.rs | 1 - ...warn_invalid_noqa_with_no_diagnostics.snap | 1 - crates/ruff_linter/src/noqa.rs | 30 ++++++++----------- 3 files changed, 12 insertions(+), 20 deletions(-) diff --git a/crates/ruff/tests/cli/lint.rs b/crates/ruff/tests/cli/lint.rs index ab1778fc4b81f..973a034cfd95e 100644 --- a/crates/ruff/tests/cli/lint.rs +++ b/crates/ruff/tests/cli/lint.rs @@ -1120,7 +1120,6 @@ import os [*] 1 fixable with the `--fix` option. ----- stderr ----- - warning: Invalid rule code provided to `# ruff: noqa` at -:2: BBB102 "); Ok(()) diff --git a/crates/ruff/tests/cli/snapshots/cli__lint__warn_invalid_noqa_with_no_diagnostics.snap b/crates/ruff/tests/cli/snapshots/cli__lint__warn_invalid_noqa_with_no_diagnostics.snap index 1dfaffe5b38fd..f5cb5d9ab4227 100644 --- a/crates/ruff/tests/cli/snapshots/cli__lint__warn_invalid_noqa_with_no_diagnostics.snap +++ b/crates/ruff/tests/cli/snapshots/cli__lint__warn_invalid_noqa_with_no_diagnostics.snap @@ -19,4 +19,3 @@ exit_code: 0 All checks passed! ----- stderr ----- -warning: Invalid rule code provided to `# ruff: noqa` at -:2: AAA101 diff --git a/crates/ruff_linter/src/noqa.rs b/crates/ruff_linter/src/noqa.rs index a324aef00ac04..8441bf943405a 100644 --- a/crates/ruff_linter/src/noqa.rs +++ b/crates/ruff_linter/src/noqa.rs @@ -277,24 +277,18 @@ impl<'a> FileNoqaDirectives<'a> { vec![] } Directive::Codes(codes) => { - codes.iter().filter_map(|code| { - let code = code.as_str(); - // Ignore externally-defined rules. - if external.iter().any(|external| code.starts_with(external)) { - return None; - } - - if let Ok(rule) = Rule::from_code(get_redirect_target(code).unwrap_or(code)) - { - Some(rule) - } else { - #[expect(deprecated)] - let line = locator.compute_line_index(range.start()); - let path_display = relativize_path(path); - warn!("Invalid rule code provided to `# ruff: noqa` at {path_display}:{line}: {code}"); - None - } - }).collect() + codes + .iter() + .filter_map(|code| { + let code = code.as_str(); + // Ignore externally-defined rules. + if external.iter().any(|external| code.starts_with(external)) { + return None; + } + + Rule::from_code(get_redirect_target(code).unwrap_or(code)).ok() + }) + .collect() } }; From 39c6438795d67261b615ade00de0a01cf13d65f7 Mon Sep 17 00:00:00 2001 From: Amethyst Reese Date: Mon, 23 Feb 2026 19:17:39 -0800 Subject: [PATCH 4/6] drop old test --- crates/ruff/tests/cli/lint.rs | 18 ---------------- ...warn_invalid_noqa_with_no_diagnostics.snap | 21 ------------------- 2 files changed, 39 deletions(-) delete mode 100644 crates/ruff/tests/cli/snapshots/cli__lint__warn_invalid_noqa_with_no_diagnostics.snap diff --git a/crates/ruff/tests/cli/lint.rs b/crates/ruff/tests/cli/lint.rs index 973a034cfd95e..c8e8f307f9908 100644 --- a/crates/ruff/tests/cli/lint.rs +++ b/crates/ruff/tests/cli/lint.rs @@ -1075,24 +1075,6 @@ include = ["*.ipy"] Ok(()) } -#[test] -fn warn_invalid_noqa_with_no_diagnostics() { - assert_cmd_snapshot!( - Command::new(get_cargo_bin(BIN_NAME)) - .args(STDIN_BASE_OPTIONS) - .args(["--isolated"]) - .arg("--select") - .arg("F401") - .arg("-") - .pass_stdin( - r#" -# ruff: noqa: AAA101 -print("Hello world!") -"# - ) - ); -} - #[test] fn file_noqa_external() -> Result<()> { let fixture = CliTest::with_file( diff --git a/crates/ruff/tests/cli/snapshots/cli__lint__warn_invalid_noqa_with_no_diagnostics.snap b/crates/ruff/tests/cli/snapshots/cli__lint__warn_invalid_noqa_with_no_diagnostics.snap deleted file mode 100644 index f5cb5d9ab4227..0000000000000 --- a/crates/ruff/tests/cli/snapshots/cli__lint__warn_invalid_noqa_with_no_diagnostics.snap +++ /dev/null @@ -1,21 +0,0 @@ ---- -source: crates/ruff/tests/cli/lint.rs -info: - program: ruff - args: - - check - - "--no-cache" - - "--output-format" - - concise - - "--isolated" - - "--select" - - F401 - - "-" - stdin: "\n# ruff: noqa: AAA101\nprint(\"Hello world!\")\n" ---- -success: true -exit_code: 0 ------ stdout ----- -All checks passed! - ------ stderr ----- From 5f2727f7f6b52965530c2598133f9a426a861bea Mon Sep 17 00:00:00 2001 From: Amethyst Reese Date: Tue, 24 Feb 2026 11:24:19 -0800 Subject: [PATCH 5/6] Preview gate, merge test cases --- crates/ruff_linter/src/preview.rs | 5 +++ crates/ruff_linter/src/rules/ruff/mod.rs | 27 ++++++--------- .../src/rules/ruff/rules/invalid_rule_code.rs | 9 +++-- ...ules__ruff__tests__RUF102_RUF102_1.py.snap | 34 ------------------- ..._code_external_rules_ruff__RUF102.py.snap} | 0 ...ode_external_rules_ruff__RUF102_1.py.snap} | 0 6 files changed, 21 insertions(+), 54 deletions(-) rename crates/ruff_linter/src/rules/ruff/snapshots/{ruff_linter__rules__ruff__tests__invalid_rule_code_external_rules.snap => ruff_linter__rules__ruff__tests__invalid_rule_code_external_rules_ruff__RUF102.py.snap} (100%) rename crates/ruff_linter/src/rules/ruff/snapshots/{ruff_linter__rules__ruff__tests__invalid_rule_code_external_rules_file_level.snap => ruff_linter__rules__ruff__tests__invalid_rule_code_external_rules_ruff__RUF102_1.py.snap} (100%) diff --git a/crates/ruff_linter/src/preview.rs b/crates/ruff_linter/src/preview.rs index 3cdb0e7d0f49f..d7531160c0baf 100644 --- a/crates/ruff_linter/src/preview.rs +++ b/crates/ruff_linter/src/preview.rs @@ -302,3 +302,8 @@ pub(crate) const fn is_baseloader_safe_in_yaml_load_enabled(settings: &LinterSet pub(crate) const fn is_expanded_import_conventions_enabled(preview: PreviewMode) -> bool { preview.is_enabled() } + +// https://github.com/astral-sh/ruff/pull/23535 +pub(crate) const fn is_file_level_invalid_rule_code_enabled(settings: &LinterSettings) -> bool { + settings.preview.is_enabled() +} diff --git a/crates/ruff_linter/src/rules/ruff/mod.rs b/crates/ruff_linter/src/rules/ruff/mod.rs index e8657856167d0..939067fd24d41 100644 --- a/crates/ruff_linter/src/rules/ruff/mod.rs +++ b/crates/ruff_linter/src/rules/ruff/mod.rs @@ -498,29 +498,22 @@ mod tests { Ok(()) } - #[test] - fn invalid_rule_code_external_rules() -> Result<()> { - let diagnostics = test_path( - Path::new("ruff/RUF102.py"), - &settings::LinterSettings { - external: vec!["V".to_string()], - ..settings::LinterSettings::for_rule(Rule::InvalidRuleCode) - }, - )?; - assert_diagnostics!(diagnostics); - Ok(()) - } - - #[test] - fn invalid_rule_code_external_rules_file_level() -> Result<()> { + #[test_case(Path::new("ruff/RUF102.py"))] + #[test_case(Path::new("ruff/RUF102_1.py"))] + fn invalid_rule_code_external_rules(path: &Path) -> Result<()> { + let snapshot = format!( + "invalid_rule_code_external_rules_{}", + path.to_string_lossy(), + ); let diagnostics = test_path( - Path::new("ruff/RUF102_1.py"), + path, &settings::LinterSettings { external: vec!["V".to_string()], + preview: PreviewMode::Enabled, ..settings::LinterSettings::for_rule(Rule::InvalidRuleCode) }, )?; - assert_diagnostics!(diagnostics); + assert_diagnostics!(snapshot, diagnostics); Ok(()) } diff --git a/crates/ruff_linter/src/rules/ruff/rules/invalid_rule_code.rs b/crates/ruff_linter/src/rules/ruff/rules/invalid_rule_code.rs index f62425e1bc892..4988591910f39 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/invalid_rule_code.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/invalid_rule_code.rs @@ -6,6 +6,7 @@ use crate::checkers::ast::LintContext; use crate::fix::edits::delete_comment; use crate::noqa::{Code, Directive, FileNoqaDirectives}; use crate::noqa::{Codes, NoqaDirectives}; +use crate::preview::is_file_level_invalid_rule_code_enabled; use crate::registry::Rule; use crate::rule_redirects::get_redirect_target; use crate::{AlwaysFixableViolation, Edit, Fix}; @@ -110,9 +111,11 @@ pub(crate) fn invalid_noqa_code( } }; - for line in file_noqa_directives.lines() { - if let Directive::Codes(codes) = &line.parsed_file_exemption { - check_codes(codes); + if is_file_level_invalid_rule_code_enabled(context.settings()) { + for line in file_noqa_directives.lines() { + if let Directive::Codes(codes) = &line.parsed_file_exemption { + check_codes(codes); + } } } for line in noqa_directives.lines() { diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF102_RUF102_1.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF102_RUF102_1.py.snap index 29d124efd6d47..7f58cfd7246a3 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF102_RUF102_1.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF102_RUF102_1.py.snap @@ -1,38 +1,4 @@ --- source: crates/ruff_linter/src/rules/ruff/mod.rs --- -RUF102 [*] Invalid rule code in `# noqa`: INVALID123 - --> RUF102_1.py:2:1 - | -1 | # Invalid file-level code -2 | # ruff: noqa: INVALID123 - | ^^^^^^^^^^^^^^^^^^^^^^^^ -3 | -4 | # External file-level code - | -help: Add non-Ruff rule codes to the `lint.external` configuration option -help: Remove the `# noqa` comment -1 | # Invalid file-level code - - # ruff: noqa: INVALID123 -2 | -3 | # External file-level code -4 | # ruff: noqa: V123 -RUF102 [*] Invalid rule code in `# noqa`: V123 - --> RUF102_1.py:5:1 - | -4 | # External file-level code -5 | # ruff: noqa: V123 - | ^^^^^^^^^^^^^^^^^^ -6 | -7 | # Valid file-level code - | -help: Add non-Ruff rule codes to the `lint.external` configuration option -help: Remove the `# noqa` comment -2 | # ruff: noqa: INVALID123 -3 | -4 | # External file-level code - - # ruff: noqa: V123 -5 | -6 | # Valid file-level code -7 | # ruff: noqa: E402 diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__invalid_rule_code_external_rules.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__invalid_rule_code_external_rules_ruff__RUF102.py.snap similarity index 100% rename from crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__invalid_rule_code_external_rules.snap rename to crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__invalid_rule_code_external_rules_ruff__RUF102.py.snap diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__invalid_rule_code_external_rules_file_level.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__invalid_rule_code_external_rules_ruff__RUF102_1.py.snap similarity index 100% rename from crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__invalid_rule_code_external_rules_file_level.snap rename to crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__invalid_rule_code_external_rules_ruff__RUF102_1.py.snap From 127c5958b7eb8416a11ada5c471a094f3214a0d5 Mon Sep 17 00:00:00 2001 From: Amethyst Reese Date: Tue, 24 Feb 2026 13:28:25 -0800 Subject: [PATCH 6/6] Remove file noqa test --- crates/ruff/tests/cli/lint.rs | 32 -------------------------------- 1 file changed, 32 deletions(-) diff --git a/crates/ruff/tests/cli/lint.rs b/crates/ruff/tests/cli/lint.rs index c8e8f307f9908..12b697a5ddbdc 100644 --- a/crates/ruff/tests/cli/lint.rs +++ b/crates/ruff/tests/cli/lint.rs @@ -1075,38 +1075,6 @@ include = ["*.ipy"] Ok(()) } -#[test] -fn file_noqa_external() -> Result<()> { - let fixture = CliTest::with_file( - "ruff.toml", - r#" -[lint] -external = ["AAA"] -"#, - )?; - - assert_cmd_snapshot!(fixture - .check_command() - .arg("--config") - .arg("ruff.toml") - .arg("-") - .pass_stdin(r#" -# flake8: noqa: AAA101, BBB102 -import os -"#), @" - success: false - exit_code: 1 - ----- stdout ----- - -:3:8: F401 [*] `os` imported but unused - Found 1 error. - [*] 1 fixable with the `--fix` option. - - ----- stderr ----- - "); - - Ok(()) -} - #[test] fn required_version_fails_to_parse() -> Result<()> { let fixture = CliTest::with_file(