From 164eb31e91c122678ef3a3080cee71daf3d7cce9 Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Fri, 10 Jul 2026 15:37:54 -0400 Subject: [PATCH 01/17] [`ruff`] Add `rule-codes-in-selectors` (`RUF201`) Summary -- This PR adds a new lint rule for TOML config files that checks for the use of rule codes as selectors and offers a fix to replace them with human-readable names. At a high-level, the implementation just deserializes a TOML config into a stripped down version of our normal configuration types and then checks each of its `Spanned` selector entries. Unless I oversimplified this too much, it didn't end up being as tricky as I thought. The main tricky part was adding basic support for fixes in TOML files, but even this was pretty straightforward. Codex was concerned about calling this a safe fix because applying the fix with `--preview`, or another temporary way of enabling preview, would break the config if preview isn't always enabled. That seemed to go beyond our usual fix safety policy to me, but I'm also happy to mark the fix as unsafe if that's preferable. Test Plan -- New tests covering all of the expected selectors and new CLI tests to make sure that `ruff.toml` files get linted and respect various ways of suppressing fixes. Previously we only linted `pyproject.toml` files for `RUF200` and bypassed some of the checks in `LintContext`. I left the change to use `LintContext` as a separate commit since it also affects `RUF200` and I briefly considered splitting it off into a separate PR. --- crates/ruff/src/commands/check.rs | 3 +- crates/ruff/src/commands/show_files.rs | 3 +- crates/ruff/src/diagnostics.rs | 101 +++- crates/ruff/tests/cli/lint.rs | 40 +- ...ires_python_extend_from_shared_config.snap | 2 + .../cli__lint__requires_python_no_tool.snap | 2 + ...quires_python_no_tool_preview_enabled.snap | 2 + ...ython_no_tool_target_version_override.snap | 2 + ..._requires_python_pyproject_toml_above.snap | 2 + ...python_pyproject_toml_above_with_tool.snap | 2 + ...nt__requires_python_ruff_toml_above-2.snap | 2 + ...lint__requires_python_ruff_toml_above.snap | 2 + ...s_python_ruff_toml_no_target_fallback.snap | 2 + ...ow_settings__display_default_settings.snap | 2 + ...isplay_settings_from_nested_directory.snap | 2 + .../rule_codes_in_selectors/pyproject.toml | 35 ++ .../ruff/rule_codes_in_selectors/ruff.toml | 34 ++ crates/ruff_linter/src/codes.rs | 2 + crates/ruff_linter/src/linter.rs | 8 +- crates/ruff_linter/src/pyproject_toml.rs | 229 ++++++++-- crates/ruff_linter/src/registry.rs | 2 +- crates/ruff_linter/src/rules/ruff/mod.rs | 36 +- .../ruff_linter/src/rules/ruff/rules/mod.rs | 2 + .../ruff/rules/rule_codes_in_selectors.rs | 280 ++++++++++++ ...s__ruff__tests__RUF201_pyproject.toml.snap | 431 ++++++++++++++++++ ..._rules__ruff__tests__RUF201_ruff.toml.snap | 430 +++++++++++++++++ crates/ruff_linter/src/source_kind.rs | 9 + crates/ruff_linter/src/test.rs | 18 + crates/ruff_python_ast/src/lib.rs | 5 + crates/ruff_workspace/src/options.rs | 9 +- crates/ruff_workspace/src/settings.rs | 4 + ruff.schema.json | 4 +- 32 files changed, 1626 insertions(+), 81 deletions(-) create mode 100644 crates/ruff_linter/resources/test/fixtures/ruff/rule_codes_in_selectors/pyproject.toml create mode 100644 crates/ruff_linter/resources/test/fixtures/ruff/rule_codes_in_selectors/ruff.toml create mode 100644 crates/ruff_linter/src/rules/ruff/rules/rule_codes_in_selectors.rs create mode 100644 crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF201_pyproject.toml.snap create mode 100644 crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF201_ruff.toml.snap diff --git a/crates/ruff/src/commands/check.rs b/crates/ruff/src/commands/check.rs index 5a80e4f5ac3b26..d363801ccdb856 100644 --- a/crates/ruff/src/commands/check.rs +++ b/crates/ruff/src/commands/check.rs @@ -50,7 +50,8 @@ pub(crate) fn check( if let Ok(ResolvedFile::Root(path) | ResolvedFile::Nested(path)) = path { matches!( SourceType::from(path), - SourceType::Python(_) | SourceType::Toml(TomlSourceType::Pyproject) + SourceType::Python(_) + | SourceType::Toml(TomlSourceType::Pyproject | TomlSourceType::Ruff) ) } else { true diff --git a/crates/ruff/src/commands/show_files.rs b/crates/ruff/src/commands/show_files.rs index 22826dbd0d5b90..9ac22700d24d77 100644 --- a/crates/ruff/src/commands/show_files.rs +++ b/crates/ruff/src/commands/show_files.rs @@ -25,7 +25,8 @@ pub(crate) fn show_files( if let Ok(ResolvedFile::Root(path) | ResolvedFile::Nested(path)) = path { matches!( SourceType::from(path), - SourceType::Python(_) | SourceType::Toml(TomlSourceType::Pyproject) + SourceType::Python(_) + | SourceType::Toml(TomlSourceType::Pyproject | TomlSourceType::Ruff) ) } else { true diff --git a/crates/ruff/src/diagnostics.rs b/crates/ruff/src/diagnostics.rs index 8a6535a00997a8..146ed40af0e287 100644 --- a/crates/ruff/src/diagnostics.rs +++ b/crates/ruff/src/diagnostics.rs @@ -14,10 +14,10 @@ use ruff_db::diagnostic::Diagnostic; use ruff_linter::codes::Rule; use ruff_linter::linter::{FixTable, FixerResult, LinterResult, ParseSource, lint_fix, lint_only}; use ruff_linter::package::PackageRoot; -use ruff_linter::pyproject_toml::lint_pyproject_toml; +use ruff_linter::pyproject_toml::{TomlFixerResult, lint_fix_toml, lint_toml}; use ruff_linter::settings::types::UnsafeFixes; use ruff_linter::settings::{LinterSettings, flags}; -use ruff_linter::source_kind::{SourceError, SourceKind}; +use ruff_linter::source_kind::{SourceError, SourceKind, SourceKindDiff}; use ruff_linter::{IOError, Violation, fs}; use ruff_notebook::{NotebookError, NotebookIndex}; use ruff_python_ast::{SourceType, TomlSourceType}; @@ -213,8 +213,8 @@ pub(crate) fn lint_path( debug!("Checking: {}", path.display()); let source_type = match settings.extension.get_source_type(path) { - SourceType::Toml(TomlSourceType::Pyproject) => { - let diagnostics = if settings + SourceType::Toml(source_type @ (TomlSourceType::Pyproject | TomlSourceType::Ruff)) => { + let (diagnostics, fixed) = if settings .rules .iter_enabled() .any(|rule_code| rule_code.lint_source().is_pyproject_toml()) @@ -226,13 +226,47 @@ pub(crate) fn lint_path( } }; let source_file = SourceFileBuilder::new(path.to_string_lossy(), contents).finish(); - lint_pyproject_toml(&source_file, settings) + if matches!(fix_mode, flags::FixMode::Apply | flags::FixMode::Diff) { + let TomlFixerResult { + diagnostics, + transformed, + fixed, + } = lint_fix_toml(&source_file, settings, source_type, unsafe_fixes); + + if !fixed.is_empty() { + match fix_mode { + flags::FixMode::Apply => { + File::create(path)?.write_all(transformed.as_bytes())?; + } + flags::FixMode::Diff => { + write!( + &mut io::stdout().lock(), + "{}", + SourceKindDiff::from_text( + source_file.source_text(), + transformed.as_ref(), + Some(path), + ) + )?; + } + flags::FixMode::Generate => {} + } + } + + (diagnostics, fixed) + } else { + ( + lint_toml(&source_file, settings, source_type), + FixTable::default(), + ) + } } else { - vec![] + (vec![], FixTable::default()) }; return Ok(Diagnostics { inner: diagnostics, - ..Diagnostics::default() + fixed: FixMap::from_iter([(fs::relativize_path(path), fixed)]), + notebook_indexes: FxHashMap::default(), }); } SourceType::Toml(_) | SourceType::Markdown => return Ok(Diagnostics::default()), @@ -359,7 +393,7 @@ pub(crate) fn lint_stdin( .map(|path| settings.linter.extension.get_source_type(path)) .unwrap_or_default() { - SourceType::Toml(source_type) if source_type.is_pyproject() => { + SourceType::Toml(source_type @ (TomlSourceType::Pyproject | TomlSourceType::Ruff)) => { if !settings .linter .rules @@ -370,17 +404,52 @@ pub(crate) fn lint_stdin( } let path = path.unwrap(); - let source_file = - SourceFileBuilder::new(path.to_string_lossy(), contents.clone()).finish(); + let source_file = SourceFileBuilder::new(path.to_string_lossy(), contents).finish(); + + let (diagnostics, fixed) = + if matches!(fix_mode, flags::FixMode::Apply | flags::FixMode::Diff) { + let TomlFixerResult { + diagnostics, + transformed, + fixed, + } = lint_fix_toml( + &source_file, + &settings.linter, + source_type, + settings.unsafe_fixes, + ); - match fix_mode { - flags::FixMode::Diff | flags::FixMode::Generate => {} - flags::FixMode::Apply => write!(&mut io::stdout().lock(), "{contents}")?, - } + match fix_mode { + flags::FixMode::Apply => { + write!(&mut io::stdout().lock(), "{transformed}")?; + } + flags::FixMode::Diff => { + if !fixed.is_empty() { + write!( + &mut io::stdout().lock(), + "{}", + SourceKindDiff::from_text( + source_file.source_text(), + transformed.as_ref(), + Some(path), + ) + )?; + } + } + flags::FixMode::Generate => {} + } + + (diagnostics, fixed) + } else { + ( + lint_toml(&source_file, &settings.linter, source_type), + FixTable::default(), + ) + }; return Ok(Diagnostics { - inner: lint_pyproject_toml(&source_file, &settings.linter), - fixed: FixMap::from_iter([(fs::relativize_path(path), FixTable::default())]), + inner: diagnostics, + fixed: FixMap::from_iter([(fs::relativize_path(path), fixed)]), notebook_indexes: FxHashMap::default(), }); } diff --git a/crates/ruff/tests/cli/lint.rs b/crates/ruff/tests/cli/lint.rs index bacd40530c15ce..598df3f39a92ab 100644 --- a/crates/ruff/tests/cli/lint.rs +++ b/crates/ruff/tests/cli/lint.rs @@ -468,7 +468,6 @@ ignore = ["D203", "D212"] All checks passed! ----- stderr ----- - warning: No Python files found under the given path(s) "); Ok(()) @@ -5192,3 +5191,42 @@ fn preview_default_rules() -> Result<()> { ); Ok(()) } + +#[test] +fn ruff_toml_is_linted() -> Result<()> { + let test = CliTest::with_file("ruff.toml", r#"lint.select = ["F401"]"#)?; + + assert_cmd_snapshot!( + test.command().args([ + "check", + "--no-cache", + "--isolated", + "--preview", + "--select", + "RUF201", + ]), + @r#" + success: false + exit_code: 1 + ----- stdout ----- + rule-codes-in-selectors: [*] Rule code used instead of name in lint.select + --> ruff.toml:1:17 + | + 1 | lint.select = ["F401"] + | ^^^^ + | + help: Replace rule code with name + | + - lint.select = ["F401"] + 1 + lint.select = ["unused-import"] + | + + Found 1 error. + [*] 1 fixable with the `--fix` option. + + ----- stderr ----- + "#, + ); + + Ok(()) +} diff --git a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_extend_from_shared_config.snap b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_extend_from_shared_config.snap index f5a44bbbd8779d..b4a34f4c7bed98 100644 --- a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_extend_from_shared_config.snap +++ b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_extend_from_shared_config.snap @@ -59,6 +59,8 @@ file_resolver.include = [ "*.pyi", "*.ipynb", "**/pyproject.toml", + "**/ruff.toml", + "**/.ruff.toml", ] file_resolver.extend_include = [] file_resolver.respect_gitignore = true diff --git a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool.snap b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool.snap index 322b41688ed6d2..fafd0ded8c0923 100644 --- a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool.snap +++ b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool.snap @@ -61,6 +61,8 @@ file_resolver.include = [ "*.pyi", "*.ipynb", "**/pyproject.toml", + "**/ruff.toml", + "**/.ruff.toml", ] file_resolver.extend_include = [] file_resolver.respect_gitignore = true diff --git a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool_preview_enabled.snap b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool_preview_enabled.snap index 3e71de36fc342f..0a24b5e1d80859 100644 --- a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool_preview_enabled.snap +++ b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool_preview_enabled.snap @@ -63,6 +63,8 @@ file_resolver.include = [ "*.pyw", "*.ipynb", "**/pyproject.toml", + "**/ruff.toml", + "**/.ruff.toml", "*.md", ] file_resolver.extend_include = [] diff --git a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool_target_version_override.snap b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool_target_version_override.snap index 0dadf95bea3802..48c3d81fed3e86 100644 --- a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool_target_version_override.snap +++ b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool_target_version_override.snap @@ -63,6 +63,8 @@ file_resolver.include = [ "*.pyi", "*.ipynb", "**/pyproject.toml", + "**/ruff.toml", + "**/.ruff.toml", ] file_resolver.extend_include = [] file_resolver.respect_gitignore = true diff --git a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_pyproject_toml_above.snap b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_pyproject_toml_above.snap index 140513dc935dee..99f233b5093f00 100644 --- a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_pyproject_toml_above.snap +++ b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_pyproject_toml_above.snap @@ -60,6 +60,8 @@ file_resolver.include = [ "*.pyi", "*.ipynb", "**/pyproject.toml", + "**/ruff.toml", + "**/.ruff.toml", ] file_resolver.extend_include = [] file_resolver.respect_gitignore = true diff --git a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_pyproject_toml_above_with_tool.snap b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_pyproject_toml_above_with_tool.snap index 8cc415c4967749..a9727552e04821 100644 --- a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_pyproject_toml_above_with_tool.snap +++ b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_pyproject_toml_above_with_tool.snap @@ -61,6 +61,8 @@ file_resolver.include = [ "*.pyi", "*.ipynb", "**/pyproject.toml", + "**/ruff.toml", + "**/.ruff.toml", ] file_resolver.extend_include = [] file_resolver.respect_gitignore = true diff --git a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_above-2.snap b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_above-2.snap index 73ceea46bb3884..72982b964975a3 100644 --- a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_above-2.snap +++ b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_above-2.snap @@ -59,6 +59,8 @@ file_resolver.include = [ "*.pyi", "*.ipynb", "**/pyproject.toml", + "**/ruff.toml", + "**/.ruff.toml", ] file_resolver.extend_include = [] file_resolver.respect_gitignore = true diff --git a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_above.snap b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_above.snap index f8136b2d971f59..7a50948dfc7252 100644 --- a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_above.snap +++ b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_above.snap @@ -59,6 +59,8 @@ file_resolver.include = [ "*.pyi", "*.ipynb", "**/pyproject.toml", + "**/ruff.toml", + "**/.ruff.toml", ] file_resolver.extend_include = [] file_resolver.respect_gitignore = true diff --git a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_no_target_fallback.snap b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_no_target_fallback.snap index d281c8f869c4a0..60137f66c8575e 100644 --- a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_no_target_fallback.snap +++ b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_no_target_fallback.snap @@ -59,6 +59,8 @@ file_resolver.include = [ "*.pyi", "*.ipynb", "**/pyproject.toml", + "**/ruff.toml", + "**/.ruff.toml", ] file_resolver.extend_include = [] file_resolver.respect_gitignore = true diff --git a/crates/ruff/tests/cli/snapshots/cli__show_settings__display_default_settings.snap b/crates/ruff/tests/cli/snapshots/cli__show_settings__display_default_settings.snap index 792399f068ed64..882ccb119a13ba 100644 --- a/crates/ruff/tests/cli/snapshots/cli__show_settings__display_default_settings.snap +++ b/crates/ruff/tests/cli/snapshots/cli__show_settings__display_default_settings.snap @@ -56,6 +56,8 @@ file_resolver.include = [ "*.pyi", "*.ipynb", "**/pyproject.toml", + "**/ruff.toml", + "**/.ruff.toml", ] file_resolver.extend_include = [] file_resolver.respect_gitignore = true diff --git a/crates/ruff/tests/cli/snapshots/cli__show_settings__display_settings_from_nested_directory.snap b/crates/ruff/tests/cli/snapshots/cli__show_settings__display_settings_from_nested_directory.snap index ff29f68590cbde..f155e549bc0a34 100644 --- a/crates/ruff/tests/cli/snapshots/cli__show_settings__display_settings_from_nested_directory.snap +++ b/crates/ruff/tests/cli/snapshots/cli__show_settings__display_settings_from_nested_directory.snap @@ -56,6 +56,8 @@ file_resolver.include = [ "*.pyi", "*.ipynb", "**/pyproject.toml", + "**/ruff.toml", + "**/.ruff.toml", ] file_resolver.extend_include = [] file_resolver.respect_gitignore = true diff --git a/crates/ruff_linter/resources/test/fixtures/ruff/rule_codes_in_selectors/pyproject.toml b/crates/ruff_linter/resources/test/fixtures/ruff/rule_codes_in_selectors/pyproject.toml new file mode 100644 index 00000000000000..fd3fa4ea336f31 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/ruff/rule_codes_in_selectors/pyproject.toml @@ -0,0 +1,35 @@ +[tool.ruff] +select = [ + "F401", + "unused-import", + "F", +] +extend-select = ['F841'] +fixable = ["""E501"""] +extend-fixable = ['''UP035'''] +ignore = ["F401"] +extend-ignore = ["F841"] +per-file-ignores = { "foo.py" = ["E501"] } +extend-per-file-ignores = { "bar.py" = ["UP035"] } +unfixable = ["F401"] +extend-unfixable = ["F841"] +extend-safe-fixes = ["E501"] +extend-unsafe-fixes = ["UP035"] + +[tool.ruff.lint] +select = [ + "F401", + "unused-import", + "unknown-rule", +] +extend-select = ['F841'] +fixable = ["""E501"""] +extend-fixable = ['''UP035'''] +ignore = ["F401"] +extend-ignore = ["F841"] +per-file-ignores = { "foo.py" = ["E501"] } +extend-per-file-ignores = { "bar.py" = ["UP035"] } +unfixable = ["F401"] +extend-unfixable = ["F841"] +extend-safe-fixes = ["E501"] +extend-unsafe-fixes = ["UP035"] diff --git a/crates/ruff_linter/resources/test/fixtures/ruff/rule_codes_in_selectors/ruff.toml b/crates/ruff_linter/resources/test/fixtures/ruff/rule_codes_in_selectors/ruff.toml new file mode 100644 index 00000000000000..c319305f1bcc42 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/ruff/rule_codes_in_selectors/ruff.toml @@ -0,0 +1,34 @@ +select = [ + "F401", + "unused-import", + "F", +] +extend-select = ['F841'] +fixable = ["""E501"""] +extend-fixable = ['''UP035'''] +ignore = ["F401"] +extend-ignore = ["F841"] +per-file-ignores = { "foo.py" = ["E501"] } +extend-per-file-ignores = { "bar.py" = ["UP035"] } +unfixable = ["F401"] +extend-unfixable = ["F841"] +extend-safe-fixes = ["E501"] +extend-unsafe-fixes = ["UP035"] + +[lint] +select = [ + "F401", + "unused-import", + "unknown-rule", +] +extend-select = ['F841'] +fixable = ["""E501"""] +extend-fixable = ['''UP035'''] +ignore = ["F401"] +extend-ignore = ["F841"] +per-file-ignores = { "foo.py" = ["E501"] } +extend-per-file-ignores = { "bar.py" = ["UP035"] } +unfixable = ["F401"] +extend-unfixable = ["F841"] +extend-safe-fixes = ["E501"] +extend-unsafe-fixes = ["UP035"] diff --git a/crates/ruff_linter/src/codes.rs b/crates/ruff_linter/src/codes.rs index a554f9fee2168f..645b9266a249bd 100644 --- a/crates/ruff_linter/src/codes.rs +++ b/crates/ruff_linter/src/codes.rs @@ -1093,6 +1093,8 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { (Ruff, "106") => rules::ruff::rules::RuleCodesInSuppressionComments, (Ruff, "200") => rules::ruff::rules::InvalidPyprojectToml, + (Ruff, "201") => rules::ruff::rules::RuleCodesInSelectors, + #[cfg(any(feature = "test-rules", test))] (Ruff, "900") => rules::ruff::rules::StableTestRule, #[cfg(any(feature = "test-rules", test))] diff --git a/crates/ruff_linter/src/linter.rs b/crates/ruff_linter/src/linter.rs index b8039417a96483..e83a57416d4414 100644 --- a/crates/ruff_linter/src/linter.rs +++ b/crates/ruff_linter/src/linter.rs @@ -369,7 +369,7 @@ pub fn check_path( ) } -const MAX_ITERATIONS: usize = 100; +pub(crate) const MAX_ITERATIONS: usize = 100; /// Add any missing suppression comments to the source code at the given `Path`. pub fn add_suppressions_to_path( @@ -676,7 +676,11 @@ where } #[expect(clippy::print_stderr)] -fn report_failed_to_converge_error(path: &Path, transformed: &str, diagnostics: &[Diagnostic]) { +pub(crate) fn report_failed_to_converge_error( + path: &Path, + transformed: &str, + diagnostics: &[Diagnostic], +) { let codes = collect_rule_codes(diagnostics.iter().filter_map(Diagnostic::secondary_code)); if cfg!(debug_assertions) { eprintln!( diff --git a/crates/ruff_linter/src/pyproject_toml.rs b/crates/ruff_linter/src/pyproject_toml.rs index 8b319f595cddd6..97cd75414b214a 100644 --- a/crates/ruff_linter/src/pyproject_toml.rs +++ b/crates/ruff_linter/src/pyproject_toml.rs @@ -1,56 +1,57 @@ +use std::borrow::Cow; +use std::ops::Range; +use std::path::Path; + use colored::Colorize; use log::warn; use pyproject_toml::PyProjectToml; +use ruff_python_ast::TomlSourceType; use ruff_text_size::{TextRange, TextSize}; use ruff_db::diagnostic::Diagnostic; -use ruff_source_file::SourceFile; +use ruff_source_file::{SourceFile, SourceFileBuilder}; +use crate::Locator; +use crate::fix::{FixResult, fix_file}; +use crate::linter::{FixTable, MAX_ITERATIONS, report_failed_to_converge_error}; use crate::registry::Rule; -use crate::rules::ruff::rules::InvalidPyprojectToml; +use crate::rules::ruff::rules::{InvalidPyprojectToml, rule_codes_in_selectors}; use crate::settings::LinterSettings; +use crate::settings::types::UnsafeFixes; use crate::{IOError, Violation}; +pub struct TomlFixerResult<'a> { + pub diagnostics: Vec, + pub transformed: Cow<'a, str>, + pub fixed: FixTable, +} + /// RUF200 -pub fn lint_pyproject_toml(source_file: &SourceFile, settings: &LinterSettings) -> Vec { - let Some(err) = toml::from_str::(source_file.source_text()).err() else { - return Vec::default(); - }; +pub fn lint_toml( + source_file: &SourceFile, + settings: &LinterSettings, + source_type: TomlSourceType, +) -> Vec { + let mut messages = rule_codes_in_selectors(source_file, settings, source_type); - let mut messages = Vec::new(); - let range = match err.span() { - // This is bad but sometimes toml and/or serde just don't give us spans - // TODO(konstin,micha): https://github.com/astral-sh/ruff/issues/4571 - None => TextRange::default(), - Some(range) => { - let Ok(end) = TextSize::try_from(range.end) else { - let message = format!( - "{} is larger than 4GB, but ruff assumes all files to be smaller", - source_file.name(), - ); - if settings.rules.enabled(Rule::IOError) { - let diagnostic = - IOError { message }.into_diagnostic(TextRange::default(), source_file); - messages.push(diagnostic); - } else { - warn!( - "{}{}{} {message}", - "Failed to lint ".bold(), - source_file.name().bold(), - ":".bold() - ); - } - return messages; - }; - TextRange::new( - // start <= end, so if end < 4GB follows start < 4GB - TextSize::try_from(range.start).unwrap(), - end, - ) - } - }; + if settings.rules.enabled(Rule::InvalidPyprojectToml) && source_type.is_pyproject() { + let Some(err) = toml::from_str::(source_file.source_text()).err() else { + return messages; + }; + + let range = match err.span() { + // This is bad but sometimes toml and/or serde just don't give us spans + // TODO(konstin,micha): https://github.com/astral-sh/ruff/issues/4571 + None => TextRange::default(), + Some(range) => { + let Some(range) = text_range_from_std(range, source_file, settings, &mut messages) + else { + return messages; + }; + range + } + }; - if settings.rules.enabled(Rule::InvalidPyprojectToml) { let toml_err = err.message().to_string(); let diagnostic = InvalidPyprojectToml { message: toml_err }.into_diagnostic(range, source_file); @@ -59,3 +60,153 @@ pub fn lint_pyproject_toml(source_file: &SourceFile, settings: &LinterSettings) messages } + +/// Generate diagnostics for a TOML configuration file, iteratively applying fixes until the source +/// stabilizes. +pub fn lint_fix_toml<'a>( + source_file: &'a SourceFile, + settings: &LinterSettings, + source_type: TomlSourceType, + unsafe_fixes: UnsafeFixes, +) -> TomlFixerResult<'a> { + let mut diagnostics = lint_toml(source_file, settings, source_type); + let mut transformed = Cow::Borrowed(source_file.source_text()); + let mut fixed = FixTable::default(); + let mut iterations = 0; + + loop { + let locator = Locator::new(transformed.as_ref()); + let Some(FixResult { code, fixes, .. }) = fix_file(&diagnostics, &locator, unsafe_fixes) + else { + return TomlFixerResult { + diagnostics, + transformed, + fixed, + }; + }; + + if iterations >= MAX_ITERATIONS { + report_failed_to_converge_error( + Path::new(source_file.name()), + transformed.as_ref(), + &diagnostics, + ); + return TomlFixerResult { + diagnostics, + transformed, + fixed, + }; + } + + for (rule, name, count) in fixes.iter() { + *fixed.entry(rule).or_default(name) += count; + } + + transformed = Cow::Owned(code); + iterations += 1; + + let transformed_source = + SourceFileBuilder::new(source_file.name(), transformed.as_ref()).finish(); + diagnostics = lint_toml(&transformed_source, settings, source_type); + } +} + +/// Try to convert a `range` into a `TextRange`, emitting an `IOError` diagnostic if the file is too +/// large or a warning if the `IOError` lint rule is disabled. +pub(crate) fn text_range_from_std( + range: Range, + source_file: &SourceFile, + settings: &LinterSettings, + diagnostics: &mut Vec, +) -> Option { + let Ok(end) = TextSize::try_from(range.end) else { + let message = format!( + "{} is larger than 4GB, but ruff assumes all files to be smaller", + source_file.name(), + ); + if settings.rules.enabled(Rule::IOError) { + let diagnostic = IOError { message }.into_diagnostic(TextRange::default(), source_file); + diagnostics.push(diagnostic); + } else { + warn!( + "{}{}{} {message}", + "Failed to lint ".bold(), + source_file.name().bold(), + ":".bold() + ); + } + return None; + }; + Some(TextRange::new( + // start <= end, so if end < 4GB follows start < 4GB + TextSize::try_from(range.start).unwrap(), + end, + )) +} + +#[cfg(test)] +mod tests { + use insta::assert_snapshot; + + use ruff_db::diagnostic::{DisplayDiagnosticConfig, DisplayDiagnostics, DummyFileResolver}; + use ruff_python_ast::TomlSourceType; + use ruff_source_file::SourceFileBuilder; + + use crate::codes::Rule; + use crate::settings::LinterSettings; + use crate::settings::types::UnsafeFixes; + + use super::lint_fix_toml; + + #[test] + fn fixes_toml() { + let source_file = SourceFileBuilder::new("ruff.toml", r#"lint.select = ["F401"]"#).finish(); + let settings = LinterSettings::for_rule(Rule::RuleCodesInSelectors).with_preview_mode(); + + let result = lint_fix_toml( + &source_file, + &settings, + TomlSourceType::Ruff, + UnsafeFixes::Disabled, + ); + + let result = std::fmt::from_fn(|f| { + let count = result.fixed.counts().sum::(); + writeln!( + f, + "Applied {count} fix{es}", + es = if count != 1 { "es" } else { "" } + )?; + writeln!( + f, + "\n## Transformed output\n\n```toml\n{}\n```", + result.transformed + )?; + if !result.diagnostics.is_empty() { + writeln!( + f, + "\n## Remaining diagnostics\n\n{}", + DisplayDiagnostics::new( + &DummyFileResolver, + &DisplayDiagnosticConfig::new("ruff"), + &result.diagnostics + ) + )?; + } + Ok(()) + }); + + assert_snapshot!( + result, + @r#" + Applied 1 fix + + ## Transformed output + + ```toml + lint.select = ["unused-import"] + ``` + "#, + ); + } +} diff --git a/crates/ruff_linter/src/registry.rs b/crates/ruff_linter/src/registry.rs index 990352abfb0dcb..aa1ea2514e15ed 100644 --- a/crates/ruff_linter/src/registry.rs +++ b/crates/ruff_linter/src/registry.rs @@ -249,7 +249,7 @@ impl Rule { /// physical lines). pub const fn lint_source(&self) -> LintSource { match self { - Rule::InvalidPyprojectToml => LintSource::PyprojectToml, + Rule::InvalidPyprojectToml | Rule::RuleCodesInSelectors => LintSource::PyprojectToml, Rule::BlanketNOQA | Rule::NoqaComments | Rule::RedirectedNOQA diff --git a/crates/ruff_linter/src/rules/ruff/mod.rs b/crates/ruff_linter/src/rules/ruff/mod.rs index 68bd7b17265e93..e0bf6f7365aca7 100644 --- a/crates/ruff_linter/src/rules/ruff/mod.rs +++ b/crates/ruff_linter/src/rules/ruff/mod.rs @@ -7,23 +7,20 @@ pub(crate) mod typing; #[cfg(test)] mod tests { - use std::fs; use std::path::Path; use anyhow::Result; use regex::Regex; - use ruff_python_ast::PythonVersion; - use ruff_source_file::SourceFileBuilder; + use ruff_python_ast::{PythonVersion, TomlSourceType}; use rustc_hash::FxHashSet; use test_case::test_case; - use crate::pyproject_toml::lint_pyproject_toml; use crate::registry::Rule; use crate::rules::pydocstyle::settings::Settings as PydocstyleSettings; use crate::settings::LinterSettings; use crate::settings::types::{CompiledPerFileIgnoreList, PerFileIgnore, PreviewMode}; use crate::source_kind::SourceKind; - use crate::test::{test_contents, test_path, test_resource_path, test_snippet}; + use crate::test::{test_contents, test_path, test_resource_path, test_snippet, test_toml_path}; use crate::{UnresolvedRuleSelector, assert_diagnostics, assert_diagnostics_diff, settings}; #[test_case(Rule::CollectionLiteralConcatenation, Path::new("RUF005.py"))] @@ -789,21 +786,30 @@ mod tests { #[test_case(Rule::InvalidPyprojectToml, Path::new("pep639"))] fn invalid_pyproject_toml(rule_code: Rule, path: &Path) -> Result<()> { let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy()); - let path = test_resource_path("fixtures") - .join("ruff") - .join("pyproject_toml") - .join(path) - .join("pyproject.toml"); - let contents = fs::read_to_string(path)?; - let source_file = SourceFileBuilder::new("pyproject.toml", contents).finish(); - let messages = lint_pyproject_toml( - &source_file, + let messages = test_toml_path( + Path::new("ruff/pyproject_toml") + .join(path) + .join("pyproject.toml"), &settings::LinterSettings::for_rule(Rule::InvalidPyprojectToml), - ); + TomlSourceType::Pyproject, + )?; assert_diagnostics!(snapshot, messages); Ok(()) } + #[test_case(Path::new("pyproject.toml"), TomlSourceType::Pyproject)] + #[test_case(Path::new("ruff.toml"), TomlSourceType::Ruff)] + fn rule_codes_in_selectors(path: &Path, source_type: TomlSourceType) -> Result<()> { + let root = Path::new("ruff/rule_codes_in_selectors"); + let diagnostics = test_toml_path( + root.join(path), + &LinterSettings::for_rule(Rule::RuleCodesInSelectors).with_preview_mode(), + source_type, + )?; + assert_diagnostics!(format!("RUF201_{}", path.display()), diagnostics); + Ok(()) + } + #[test_case(Rule::MutableDataclassDefault, Path::new("RUF008.py"))] #[test_case(Rule::MutableDataclassDefault, Path::new("RUF008_attrs.py"))] #[test_case(Rule::UnrawRePattern, Path::new("RUF039.py"))] diff --git a/crates/ruff_linter/src/rules/ruff/rules/mod.rs b/crates/ruff_linter/src/rules/ruff/rules/mod.rs index 71cd885d0f19c0..1b2aeca17b3a92 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/mod.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/mod.rs @@ -50,6 +50,7 @@ pub(crate) use pytest_raises_ambiguous_pattern::*; pub(crate) use quadratic_list_summation::*; pub(crate) use redirected_noqa::*; pub(crate) use redundant_bool_literal::*; +pub(crate) use rule_codes_in_selectors::*; pub(crate) use rule_codes_in_suppression_comments::*; pub(crate) use sort_dunder_all::*; pub(crate) use sort_dunder_slots::*; @@ -130,6 +131,7 @@ mod pytest_raises_ambiguous_pattern; mod quadratic_list_summation; mod redirected_noqa; mod redundant_bool_literal; +mod rule_codes_in_selectors; mod rule_codes_in_suppression_comments; mod sequence_sorting; mod sort_dunder_all; diff --git a/crates/ruff_linter/src/rules/ruff/rules/rule_codes_in_selectors.rs b/crates/ruff_linter/src/rules/ruff/rules/rule_codes_in_selectors.rs new file mode 100644 index 00000000000000..fa165df883dd6a --- /dev/null +++ b/crates/ruff_linter/src/rules/ruff/rules/rule_codes_in_selectors.rs @@ -0,0 +1,280 @@ +use std::iter::repeat; +use std::ops::Range; + +use rustc_hash::FxHashMap; +use serde::Deserialize; +use toml::Spanned; + +use ruff_db::diagnostic::Diagnostic; +use ruff_diagnostics::{Edit, Fix}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; +use ruff_python_ast::TomlSourceType; +use ruff_source_file::SourceFile; + +use crate::{ + FixAvailability, Violation, codes::Rule, preview::is_human_readable_names_enabled, + pyproject_toml::text_range_from_std, rule_redirects::get_redirect_target, + settings::LinterSettings, +}; + +/// ## What it does +/// +/// Checks for any configuration files that use rule codes as selectors. +/// +/// ## Why is this bad? +/// +/// Human-readable rule names are easier to understand than rule codes. Using names also avoids +/// requiring readers to look up the meaning of each code. +/// +/// ## Example +/// +/// ```toml +/// [tool.ruff.lint] +/// select = ["F401"] +/// ``` +/// +/// Use instead: +/// +/// ```toml +/// [tool.ruff.lint] +/// select = ["unused-import"] +/// ``` +#[derive(ViolationMetadata)] +#[violation_metadata(preview_since = "NEXT_RUFF_VERSION")] +pub(crate) struct RuleCodesInSelectors { + selector: &'static str, +} + +impl Violation for RuleCodesInSelectors { + const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes; + + #[derive_message_formats] + fn message(&self) -> String { + let Self { selector } = self; + format!("Rule code used instead of name in {selector}") + } + + fn fix_title(&self) -> Option { + Some("Replace rule code with name".to_string()) + } +} + +/// RUF201 +pub(crate) fn rule_codes_in_selectors( + source_file: &SourceFile, + settings: &LinterSettings, + source_type: TomlSourceType, +) -> Vec { + let mut diagnostics = Vec::new(); + + if !settings.rules.enabled(Rule::RuleCodesInSelectors) + || !is_human_readable_names_enabled(settings.preview) + { + return diagnostics; + } + + let Ok(Some(document)) = Document::from_toml_str(source_file.source_text(), source_type) else { + return diagnostics; + }; + + for (spanned, selector) in document.selectors() { + let code = spanned.get_ref(); + let code = get_redirect_target(code).unwrap_or(code); + let Ok(rule) = Rule::from_code(code) else { + continue; + }; + + let Some(content_range) = + toml_string_content_range(source_file.source_text(), spanned.span()) + else { + debug_assert!(false, "a TOML string span should include its quotes"); + continue; + }; + + // This can only fail if the file is larger than 4GB, so abort analysis of the entire file + // rather than continuing to the next selector. + let Some(range) = + text_range_from_std(content_range, source_file, settings, &mut diagnostics) + else { + return diagnostics; + }; + + let mut diagnostic = RuleCodesInSelectors { selector }.into_diagnostic(range, source_file); + diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement( + rule.name().to_string(), + range, + ))); + + diagnostics.push(diagnostic); + } + + diagnostics +} + +enum Document { + Pyproject(Pyproject), + Ruff(Ruff), +} + +impl Document { + fn from_toml_str( + source: &str, + source_type: TomlSourceType, + ) -> Result, toml::de::Error> { + match source_type { + TomlSourceType::Pyproject => Ok(Some(Self::Pyproject(toml::from_str(source)?))), + TomlSourceType::Ruff => Ok(Some(Self::Ruff(toml::from_str(source)?))), + _ => Ok(None), + } + } + + fn selectors(&self) -> impl Iterator, &'static str)> { + let ruff = match self { + Document::Pyproject(pyproject) => &pyproject.tool.ruff, + Document::Ruff(ruff) => ruff, + }; + ruff.select + .iter() + .zip(repeat("select")) + .chain(ruff.extend_select.iter().zip(repeat("extend-select"))) + .chain(ruff.fixable.iter().zip(repeat("fixable"))) + .chain(ruff.extend_fixable.iter().zip(repeat("extend-fixable"))) + .chain(ruff.ignore.iter().zip(repeat("ignore"))) + .chain(ruff.extend_ignore.iter().zip(repeat("extend-ignore"))) + .chain( + ruff.per_file_ignores + .values() + .flatten() + .zip(repeat("per-file-ignores")), + ) + .chain( + ruff.extend_per_file_ignores + .values() + .flatten() + .zip(repeat("extend-per-file-ignores")), + ) + .chain(ruff.unfixable.iter().zip(repeat("unfixable"))) + .chain(ruff.extend_unfixable.iter().zip(repeat("extend-unfixable"))) + .chain( + ruff.extend_safe_fixes + .iter() + .zip(repeat("extend-safe-fixes")), + ) + .chain( + ruff.extend_unsafe_fixes + .iter() + .zip(repeat("extend-unsafe-fixes")), + ) + .chain(ruff.lint.selectors()) + } +} + +#[derive(Deserialize)] +struct Pyproject { + tool: Tool, +} + +#[derive(Deserialize)] +struct Tool { + ruff: Ruff, +} + +type Selector = Spanned; + +#[derive(Default, Deserialize)] +#[serde(default, rename_all = "kebab-case")] +struct Ruff { + // Keep these deprecated top-level fields in sync with the selector-valued fields in + // `ruff_workspace::options::LintCommonOptions`. + select: Vec, + extend_select: Vec, + fixable: Vec, + extend_fixable: Vec, + ignore: Vec, + extend_ignore: Vec, + per_file_ignores: FxHashMap>, + extend_per_file_ignores: FxHashMap>, + unfixable: Vec, + extend_unfixable: Vec, + extend_safe_fixes: Vec, + extend_unsafe_fixes: Vec, + + // Linter options + lint: Lint, +} + +#[derive(Default, Deserialize)] +#[serde(default, rename_all = "kebab-case")] +struct Lint { + // Keep these fields in sync with the selector-valued fields in + // `ruff_workspace::options::LintCommonOptions`. + select: Vec, + extend_select: Vec, + fixable: Vec, + extend_fixable: Vec, + ignore: Vec, + extend_ignore: Vec, + per_file_ignores: FxHashMap>, + extend_per_file_ignores: FxHashMap>, + unfixable: Vec, + extend_unfixable: Vec, + extend_safe_fixes: Vec, + extend_unsafe_fixes: Vec, +} + +impl Lint { + fn selectors(&self) -> impl Iterator, &'static str)> { + self.select + .iter() + .zip(repeat("lint.select")) + .chain(self.extend_select.iter().zip(repeat("lint.extend-select"))) + .chain(self.fixable.iter().zip(repeat("lint.fixable"))) + .chain( + self.extend_fixable + .iter() + .zip(repeat("lint.extend-fixable")), + ) + .chain(self.ignore.iter().zip(repeat("lint.ignore"))) + .chain(self.extend_ignore.iter().zip(repeat("lint.extend-ignore"))) + .chain( + self.per_file_ignores + .values() + .flatten() + .zip(repeat("lint.per-file-ignores")), + ) + .chain( + self.extend_per_file_ignores + .values() + .flatten() + .zip(repeat("lint.extend-per-file-ignores")), + ) + .chain(self.unfixable.iter().zip(repeat("lint.unfixable"))) + .chain( + self.extend_unfixable + .iter() + .zip(repeat("lint.extend-unfixable")), + ) + .chain( + self.extend_safe_fixes + .iter() + .zip(repeat("lint.extend-safe-fixes")), + ) + .chain( + self.extend_unsafe_fixes + .iter() + .zip(repeat("lint.extend-unsafe-fixes")), + ) + } +} + +fn toml_string_content_range(source: &str, range: Range) -> Option> { + let string = source.get(range.clone())?; + let content = string.trim_matches(['"', '\'']); + if content.is_empty() || content.len() == string.len() { + return None; + } + let leading_quotes = string.len() - string.trim_start_matches(['"', '\'']).len(); + let start = range.start + leading_quotes; + + Some(start..start + content.len()) +} diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF201_pyproject.toml.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF201_pyproject.toml.snap new file mode 100644 index 00000000000000..20278fceb4d71a --- /dev/null +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF201_pyproject.toml.snap @@ -0,0 +1,431 @@ +--- +source: crates/ruff_linter/src/rules/ruff/mod.rs +--- +RUF201 [*] Rule code used instead of name in select + --> pyproject.toml:3:6 + | +1 | [tool.ruff] +2 | select = [ +3 | "F401", + | ^^^^ +4 | "unused-import", +5 | "F", + | +help: Replace rule code with name + | +2 | select = [ + - "F401", +3 | "unused-import", +4 + "unused-import", +5 | "F", + | + +RUF201 [*] Rule code used instead of name in extend-select + --> pyproject.toml:7:19 + | +5 | "F", +6 | ] +7 | extend-select = ['F841'] + | ^^^^ +8 | fixable = ["""E501"""] +9 | extend-fixable = ['''UP035'''] + | +help: Replace rule code with name + | +6 | ] + - extend-select = ['F841'] +7 + extend-select = ['unused-variable'] +8 | fixable = ["""E501"""] + | + +RUF201 [*] Rule code used instead of name in fixable + --> pyproject.toml:8:15 + | + 6 | ] + 7 | extend-select = ['F841'] + 8 | fixable = ["""E501"""] + | ^^^^ + 9 | extend-fixable = ['''UP035'''] +10 | ignore = ["F401"] + | +help: Replace rule code with name + | +7 | extend-select = ['F841'] + - fixable = ["""E501"""] +8 + fixable = ["""line-too-long"""] +9 | extend-fixable = ['''UP035'''] + | + +RUF201 [*] Rule code used instead of name in extend-fixable + --> pyproject.toml:9:22 + | + 7 | extend-select = ['F841'] + 8 | fixable = ["""E501"""] + 9 | extend-fixable = ['''UP035'''] + | ^^^^^ +10 | ignore = ["F401"] +11 | extend-ignore = ["F841"] + | +help: Replace rule code with name + | +8 | fixable = ["""E501"""] + - extend-fixable = ['''UP035'''] +9 + extend-fixable = ['''deprecated-import'''] +10 | ignore = ["F401"] + | + +RUF201 [*] Rule code used instead of name in ignore + --> pyproject.toml:10:12 + | + 8 | fixable = ["""E501"""] + 9 | extend-fixable = ['''UP035'''] +10 | ignore = ["F401"] + | ^^^^ +11 | extend-ignore = ["F841"] +12 | per-file-ignores = { "foo.py" = ["E501"] } + | +help: Replace rule code with name + | +9 | extend-fixable = ['''UP035'''] + - ignore = ["F401"] +10 + ignore = ["unused-import"] +11 | extend-ignore = ["F841"] + | + +RUF201 [*] Rule code used instead of name in extend-ignore + --> pyproject.toml:11:19 + | + 9 | extend-fixable = ['''UP035'''] +10 | ignore = ["F401"] +11 | extend-ignore = ["F841"] + | ^^^^ +12 | per-file-ignores = { "foo.py" = ["E501"] } +13 | extend-per-file-ignores = { "bar.py" = ["UP035"] } + | +help: Replace rule code with name + | +10 | ignore = ["F401"] + - extend-ignore = ["F841"] +11 + extend-ignore = ["unused-variable"] +12 | per-file-ignores = { "foo.py" = ["E501"] } + | + +RUF201 [*] Rule code used instead of name in per-file-ignores + --> pyproject.toml:12:35 + | +10 | ignore = ["F401"] +11 | extend-ignore = ["F841"] +12 | per-file-ignores = { "foo.py" = ["E501"] } + | ^^^^ +13 | extend-per-file-ignores = { "bar.py" = ["UP035"] } +14 | unfixable = ["F401"] + | +help: Replace rule code with name + | +11 | extend-ignore = ["F841"] + - per-file-ignores = { "foo.py" = ["E501"] } +12 + per-file-ignores = { "foo.py" = ["line-too-long"] } +13 | extend-per-file-ignores = { "bar.py" = ["UP035"] } + | + +RUF201 [*] Rule code used instead of name in extend-per-file-ignores + --> pyproject.toml:13:42 + | +11 | extend-ignore = ["F841"] +12 | per-file-ignores = { "foo.py" = ["E501"] } +13 | extend-per-file-ignores = { "bar.py" = ["UP035"] } + | ^^^^^ +14 | unfixable = ["F401"] +15 | extend-unfixable = ["F841"] + | +help: Replace rule code with name + | +12 | per-file-ignores = { "foo.py" = ["E501"] } + - extend-per-file-ignores = { "bar.py" = ["UP035"] } +13 + extend-per-file-ignores = { "bar.py" = ["deprecated-import"] } +14 | unfixable = ["F401"] + | + +RUF201 [*] Rule code used instead of name in unfixable + --> pyproject.toml:14:15 + | +12 | per-file-ignores = { "foo.py" = ["E501"] } +13 | extend-per-file-ignores = { "bar.py" = ["UP035"] } +14 | unfixable = ["F401"] + | ^^^^ +15 | extend-unfixable = ["F841"] +16 | extend-safe-fixes = ["E501"] + | +help: Replace rule code with name + | +13 | extend-per-file-ignores = { "bar.py" = ["UP035"] } + - unfixable = ["F401"] +14 + unfixable = ["unused-import"] +15 | extend-unfixable = ["F841"] + | + +RUF201 [*] Rule code used instead of name in extend-unfixable + --> pyproject.toml:15:22 + | +13 | extend-per-file-ignores = { "bar.py" = ["UP035"] } +14 | unfixable = ["F401"] +15 | extend-unfixable = ["F841"] + | ^^^^ +16 | extend-safe-fixes = ["E501"] +17 | extend-unsafe-fixes = ["UP035"] + | +help: Replace rule code with name + | +14 | unfixable = ["F401"] + - extend-unfixable = ["F841"] +15 + extend-unfixable = ["unused-variable"] +16 | extend-safe-fixes = ["E501"] + | + +RUF201 [*] Rule code used instead of name in extend-safe-fixes + --> pyproject.toml:16:23 + | +14 | unfixable = ["F401"] +15 | extend-unfixable = ["F841"] +16 | extend-safe-fixes = ["E501"] + | ^^^^ +17 | extend-unsafe-fixes = ["UP035"] + | +help: Replace rule code with name + | +15 | extend-unfixable = ["F841"] + - extend-safe-fixes = ["E501"] +16 + extend-safe-fixes = ["line-too-long"] +17 | extend-unsafe-fixes = ["UP035"] + | + +RUF201 [*] Rule code used instead of name in extend-unsafe-fixes + --> pyproject.toml:17:25 + | +15 | extend-unfixable = ["F841"] +16 | extend-safe-fixes = ["E501"] +17 | extend-unsafe-fixes = ["UP035"] + | ^^^^^ +18 | +19 | [tool.ruff.lint] + | +help: Replace rule code with name + | +16 | extend-safe-fixes = ["E501"] + - extend-unsafe-fixes = ["UP035"] +17 + extend-unsafe-fixes = ["deprecated-import"] +18 | + | + +RUF201 [*] Rule code used instead of name in lint.select + --> pyproject.toml:21:6 + | +19 | [tool.ruff.lint] +20 | select = [ +21 | "F401", + | ^^^^ +22 | "unused-import", +23 | "unknown-rule", + | +help: Replace rule code with name + | +20 | select = [ + - "F401", +21 | "unused-import", +22 + "unused-import", +23 | "unknown-rule", + | + +RUF201 [*] Rule code used instead of name in lint.extend-select + --> pyproject.toml:25:19 + | +23 | "unknown-rule", +24 | ] +25 | extend-select = ['F841'] + | ^^^^ +26 | fixable = ["""E501"""] +27 | extend-fixable = ['''UP035'''] + | +help: Replace rule code with name + | +24 | ] + - extend-select = ['F841'] +25 + extend-select = ['unused-variable'] +26 | fixable = ["""E501"""] + | + +RUF201 [*] Rule code used instead of name in lint.fixable + --> pyproject.toml:26:15 + | +24 | ] +25 | extend-select = ['F841'] +26 | fixable = ["""E501"""] + | ^^^^ +27 | extend-fixable = ['''UP035'''] +28 | ignore = ["F401"] + | +help: Replace rule code with name + | +25 | extend-select = ['F841'] + - fixable = ["""E501"""] +26 + fixable = ["""line-too-long"""] +27 | extend-fixable = ['''UP035'''] + | + +RUF201 [*] Rule code used instead of name in lint.extend-fixable + --> pyproject.toml:27:22 + | +25 | extend-select = ['F841'] +26 | fixable = ["""E501"""] +27 | extend-fixable = ['''UP035'''] + | ^^^^^ +28 | ignore = ["F401"] +29 | extend-ignore = ["F841"] + | +help: Replace rule code with name + | +26 | fixable = ["""E501"""] + - extend-fixable = ['''UP035'''] +27 + extend-fixable = ['''deprecated-import'''] +28 | ignore = ["F401"] + | + +RUF201 [*] Rule code used instead of name in lint.ignore + --> pyproject.toml:28:12 + | +26 | fixable = ["""E501"""] +27 | extend-fixable = ['''UP035'''] +28 | ignore = ["F401"] + | ^^^^ +29 | extend-ignore = ["F841"] +30 | per-file-ignores = { "foo.py" = ["E501"] } + | +help: Replace rule code with name + | +27 | extend-fixable = ['''UP035'''] + - ignore = ["F401"] +28 + ignore = ["unused-import"] +29 | extend-ignore = ["F841"] + | + +RUF201 [*] Rule code used instead of name in lint.extend-ignore + --> pyproject.toml:29:19 + | +27 | extend-fixable = ['''UP035'''] +28 | ignore = ["F401"] +29 | extend-ignore = ["F841"] + | ^^^^ +30 | per-file-ignores = { "foo.py" = ["E501"] } +31 | extend-per-file-ignores = { "bar.py" = ["UP035"] } + | +help: Replace rule code with name + | +28 | ignore = ["F401"] + - extend-ignore = ["F841"] +29 + extend-ignore = ["unused-variable"] +30 | per-file-ignores = { "foo.py" = ["E501"] } + | + +RUF201 [*] Rule code used instead of name in lint.per-file-ignores + --> pyproject.toml:30:35 + | +28 | ignore = ["F401"] +29 | extend-ignore = ["F841"] +30 | per-file-ignores = { "foo.py" = ["E501"] } + | ^^^^ +31 | extend-per-file-ignores = { "bar.py" = ["UP035"] } +32 | unfixable = ["F401"] + | +help: Replace rule code with name + | +29 | extend-ignore = ["F841"] + - per-file-ignores = { "foo.py" = ["E501"] } +30 + per-file-ignores = { "foo.py" = ["line-too-long"] } +31 | extend-per-file-ignores = { "bar.py" = ["UP035"] } + | + +RUF201 [*] Rule code used instead of name in lint.extend-per-file-ignores + --> pyproject.toml:31:42 + | +29 | extend-ignore = ["F841"] +30 | per-file-ignores = { "foo.py" = ["E501"] } +31 | extend-per-file-ignores = { "bar.py" = ["UP035"] } + | ^^^^^ +32 | unfixable = ["F401"] +33 | extend-unfixable = ["F841"] + | +help: Replace rule code with name + | +30 | per-file-ignores = { "foo.py" = ["E501"] } + - extend-per-file-ignores = { "bar.py" = ["UP035"] } +31 + extend-per-file-ignores = { "bar.py" = ["deprecated-import"] } +32 | unfixable = ["F401"] + | + +RUF201 [*] Rule code used instead of name in lint.unfixable + --> pyproject.toml:32:15 + | +30 | per-file-ignores = { "foo.py" = ["E501"] } +31 | extend-per-file-ignores = { "bar.py" = ["UP035"] } +32 | unfixable = ["F401"] + | ^^^^ +33 | extend-unfixable = ["F841"] +34 | extend-safe-fixes = ["E501"] + | +help: Replace rule code with name + | +31 | extend-per-file-ignores = { "bar.py" = ["UP035"] } + - unfixable = ["F401"] +32 + unfixable = ["unused-import"] +33 | extend-unfixable = ["F841"] + | + +RUF201 [*] Rule code used instead of name in lint.extend-unfixable + --> pyproject.toml:33:22 + | +31 | extend-per-file-ignores = { "bar.py" = ["UP035"] } +32 | unfixable = ["F401"] +33 | extend-unfixable = ["F841"] + | ^^^^ +34 | extend-safe-fixes = ["E501"] +35 | extend-unsafe-fixes = ["UP035"] + | +help: Replace rule code with name + | +32 | unfixable = ["F401"] + - extend-unfixable = ["F841"] +33 + extend-unfixable = ["unused-variable"] +34 | extend-safe-fixes = ["E501"] + | + +RUF201 [*] Rule code used instead of name in lint.extend-safe-fixes + --> pyproject.toml:34:23 + | +32 | unfixable = ["F401"] +33 | extend-unfixable = ["F841"] +34 | extend-safe-fixes = ["E501"] + | ^^^^ +35 | extend-unsafe-fixes = ["UP035"] + | +help: Replace rule code with name + | +33 | extend-unfixable = ["F841"] + - extend-safe-fixes = ["E501"] +34 + extend-safe-fixes = ["line-too-long"] +35 | extend-unsafe-fixes = ["UP035"] + | + +RUF201 [*] Rule code used instead of name in lint.extend-unsafe-fixes + --> pyproject.toml:35:25 + | +33 | extend-unfixable = ["F841"] +34 | extend-safe-fixes = ["E501"] +35 | extend-unsafe-fixes = ["UP035"] + | ^^^^^ + | +help: Replace rule code with name + | +34 | extend-safe-fixes = ["E501"] + - extend-unsafe-fixes = ["UP035"] +35 + extend-unsafe-fixes = ["deprecated-import"] + | diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF201_ruff.toml.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF201_ruff.toml.snap new file mode 100644 index 00000000000000..b8edbfd7b77e07 --- /dev/null +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF201_ruff.toml.snap @@ -0,0 +1,430 @@ +--- +source: crates/ruff_linter/src/rules/ruff/mod.rs +--- +RUF201 [*] Rule code used instead of name in select + --> ruff.toml:2:6 + | +1 | select = [ +2 | "F401", + | ^^^^ +3 | "unused-import", +4 | "F", + | +help: Replace rule code with name + | +1 | select = [ + - "F401", +2 | "unused-import", +3 + "unused-import", +4 | "F", + | + +RUF201 [*] Rule code used instead of name in extend-select + --> ruff.toml:6:19 + | +4 | "F", +5 | ] +6 | extend-select = ['F841'] + | ^^^^ +7 | fixable = ["""E501"""] +8 | extend-fixable = ['''UP035'''] + | +help: Replace rule code with name + | +5 | ] + - extend-select = ['F841'] +6 + extend-select = ['unused-variable'] +7 | fixable = ["""E501"""] + | + +RUF201 [*] Rule code used instead of name in fixable + --> ruff.toml:7:15 + | +5 | ] +6 | extend-select = ['F841'] +7 | fixable = ["""E501"""] + | ^^^^ +8 | extend-fixable = ['''UP035'''] +9 | ignore = ["F401"] + | +help: Replace rule code with name + | +6 | extend-select = ['F841'] + - fixable = ["""E501"""] +7 + fixable = ["""line-too-long"""] +8 | extend-fixable = ['''UP035'''] + | + +RUF201 [*] Rule code used instead of name in extend-fixable + --> ruff.toml:8:22 + | + 6 | extend-select = ['F841'] + 7 | fixable = ["""E501"""] + 8 | extend-fixable = ['''UP035'''] + | ^^^^^ + 9 | ignore = ["F401"] +10 | extend-ignore = ["F841"] + | +help: Replace rule code with name + | +7 | fixable = ["""E501"""] + - extend-fixable = ['''UP035'''] +8 + extend-fixable = ['''deprecated-import'''] +9 | ignore = ["F401"] + | + +RUF201 [*] Rule code used instead of name in ignore + --> ruff.toml:9:12 + | + 7 | fixable = ["""E501"""] + 8 | extend-fixable = ['''UP035'''] + 9 | ignore = ["F401"] + | ^^^^ +10 | extend-ignore = ["F841"] +11 | per-file-ignores = { "foo.py" = ["E501"] } + | +help: Replace rule code with name + | +8 | extend-fixable = ['''UP035'''] + - ignore = ["F401"] +9 + ignore = ["unused-import"] +10 | extend-ignore = ["F841"] + | + +RUF201 [*] Rule code used instead of name in extend-ignore + --> ruff.toml:10:19 + | + 8 | extend-fixable = ['''UP035'''] + 9 | ignore = ["F401"] +10 | extend-ignore = ["F841"] + | ^^^^ +11 | per-file-ignores = { "foo.py" = ["E501"] } +12 | extend-per-file-ignores = { "bar.py" = ["UP035"] } + | +help: Replace rule code with name + | +9 | ignore = ["F401"] + - extend-ignore = ["F841"] +10 + extend-ignore = ["unused-variable"] +11 | per-file-ignores = { "foo.py" = ["E501"] } + | + +RUF201 [*] Rule code used instead of name in per-file-ignores + --> ruff.toml:11:35 + | + 9 | ignore = ["F401"] +10 | extend-ignore = ["F841"] +11 | per-file-ignores = { "foo.py" = ["E501"] } + | ^^^^ +12 | extend-per-file-ignores = { "bar.py" = ["UP035"] } +13 | unfixable = ["F401"] + | +help: Replace rule code with name + | +10 | extend-ignore = ["F841"] + - per-file-ignores = { "foo.py" = ["E501"] } +11 + per-file-ignores = { "foo.py" = ["line-too-long"] } +12 | extend-per-file-ignores = { "bar.py" = ["UP035"] } + | + +RUF201 [*] Rule code used instead of name in extend-per-file-ignores + --> ruff.toml:12:42 + | +10 | extend-ignore = ["F841"] +11 | per-file-ignores = { "foo.py" = ["E501"] } +12 | extend-per-file-ignores = { "bar.py" = ["UP035"] } + | ^^^^^ +13 | unfixable = ["F401"] +14 | extend-unfixable = ["F841"] + | +help: Replace rule code with name + | +11 | per-file-ignores = { "foo.py" = ["E501"] } + - extend-per-file-ignores = { "bar.py" = ["UP035"] } +12 + extend-per-file-ignores = { "bar.py" = ["deprecated-import"] } +13 | unfixable = ["F401"] + | + +RUF201 [*] Rule code used instead of name in unfixable + --> ruff.toml:13:15 + | +11 | per-file-ignores = { "foo.py" = ["E501"] } +12 | extend-per-file-ignores = { "bar.py" = ["UP035"] } +13 | unfixable = ["F401"] + | ^^^^ +14 | extend-unfixable = ["F841"] +15 | extend-safe-fixes = ["E501"] + | +help: Replace rule code with name + | +12 | extend-per-file-ignores = { "bar.py" = ["UP035"] } + - unfixable = ["F401"] +13 + unfixable = ["unused-import"] +14 | extend-unfixable = ["F841"] + | + +RUF201 [*] Rule code used instead of name in extend-unfixable + --> ruff.toml:14:22 + | +12 | extend-per-file-ignores = { "bar.py" = ["UP035"] } +13 | unfixable = ["F401"] +14 | extend-unfixable = ["F841"] + | ^^^^ +15 | extend-safe-fixes = ["E501"] +16 | extend-unsafe-fixes = ["UP035"] + | +help: Replace rule code with name + | +13 | unfixable = ["F401"] + - extend-unfixable = ["F841"] +14 + extend-unfixable = ["unused-variable"] +15 | extend-safe-fixes = ["E501"] + | + +RUF201 [*] Rule code used instead of name in extend-safe-fixes + --> ruff.toml:15:23 + | +13 | unfixable = ["F401"] +14 | extend-unfixable = ["F841"] +15 | extend-safe-fixes = ["E501"] + | ^^^^ +16 | extend-unsafe-fixes = ["UP035"] + | +help: Replace rule code with name + | +14 | extend-unfixable = ["F841"] + - extend-safe-fixes = ["E501"] +15 + extend-safe-fixes = ["line-too-long"] +16 | extend-unsafe-fixes = ["UP035"] + | + +RUF201 [*] Rule code used instead of name in extend-unsafe-fixes + --> ruff.toml:16:25 + | +14 | extend-unfixable = ["F841"] +15 | extend-safe-fixes = ["E501"] +16 | extend-unsafe-fixes = ["UP035"] + | ^^^^^ +17 | +18 | [lint] + | +help: Replace rule code with name + | +15 | extend-safe-fixes = ["E501"] + - extend-unsafe-fixes = ["UP035"] +16 + extend-unsafe-fixes = ["deprecated-import"] +17 | + | + +RUF201 [*] Rule code used instead of name in lint.select + --> ruff.toml:20:6 + | +18 | [lint] +19 | select = [ +20 | "F401", + | ^^^^ +21 | "unused-import", +22 | "unknown-rule", + | +help: Replace rule code with name + | +19 | select = [ + - "F401", +20 | "unused-import", +21 + "unused-import", +22 | "unknown-rule", + | + +RUF201 [*] Rule code used instead of name in lint.extend-select + --> ruff.toml:24:19 + | +22 | "unknown-rule", +23 | ] +24 | extend-select = ['F841'] + | ^^^^ +25 | fixable = ["""E501"""] +26 | extend-fixable = ['''UP035'''] + | +help: Replace rule code with name + | +23 | ] + - extend-select = ['F841'] +24 + extend-select = ['unused-variable'] +25 | fixable = ["""E501"""] + | + +RUF201 [*] Rule code used instead of name in lint.fixable + --> ruff.toml:25:15 + | +23 | ] +24 | extend-select = ['F841'] +25 | fixable = ["""E501"""] + | ^^^^ +26 | extend-fixable = ['''UP035'''] +27 | ignore = ["F401"] + | +help: Replace rule code with name + | +24 | extend-select = ['F841'] + - fixable = ["""E501"""] +25 + fixable = ["""line-too-long"""] +26 | extend-fixable = ['''UP035'''] + | + +RUF201 [*] Rule code used instead of name in lint.extend-fixable + --> ruff.toml:26:22 + | +24 | extend-select = ['F841'] +25 | fixable = ["""E501"""] +26 | extend-fixable = ['''UP035'''] + | ^^^^^ +27 | ignore = ["F401"] +28 | extend-ignore = ["F841"] + | +help: Replace rule code with name + | +25 | fixable = ["""E501"""] + - extend-fixable = ['''UP035'''] +26 + extend-fixable = ['''deprecated-import'''] +27 | ignore = ["F401"] + | + +RUF201 [*] Rule code used instead of name in lint.ignore + --> ruff.toml:27:12 + | +25 | fixable = ["""E501"""] +26 | extend-fixable = ['''UP035'''] +27 | ignore = ["F401"] + | ^^^^ +28 | extend-ignore = ["F841"] +29 | per-file-ignores = { "foo.py" = ["E501"] } + | +help: Replace rule code with name + | +26 | extend-fixable = ['''UP035'''] + - ignore = ["F401"] +27 + ignore = ["unused-import"] +28 | extend-ignore = ["F841"] + | + +RUF201 [*] Rule code used instead of name in lint.extend-ignore + --> ruff.toml:28:19 + | +26 | extend-fixable = ['''UP035'''] +27 | ignore = ["F401"] +28 | extend-ignore = ["F841"] + | ^^^^ +29 | per-file-ignores = { "foo.py" = ["E501"] } +30 | extend-per-file-ignores = { "bar.py" = ["UP035"] } + | +help: Replace rule code with name + | +27 | ignore = ["F401"] + - extend-ignore = ["F841"] +28 + extend-ignore = ["unused-variable"] +29 | per-file-ignores = { "foo.py" = ["E501"] } + | + +RUF201 [*] Rule code used instead of name in lint.per-file-ignores + --> ruff.toml:29:35 + | +27 | ignore = ["F401"] +28 | extend-ignore = ["F841"] +29 | per-file-ignores = { "foo.py" = ["E501"] } + | ^^^^ +30 | extend-per-file-ignores = { "bar.py" = ["UP035"] } +31 | unfixable = ["F401"] + | +help: Replace rule code with name + | +28 | extend-ignore = ["F841"] + - per-file-ignores = { "foo.py" = ["E501"] } +29 + per-file-ignores = { "foo.py" = ["line-too-long"] } +30 | extend-per-file-ignores = { "bar.py" = ["UP035"] } + | + +RUF201 [*] Rule code used instead of name in lint.extend-per-file-ignores + --> ruff.toml:30:42 + | +28 | extend-ignore = ["F841"] +29 | per-file-ignores = { "foo.py" = ["E501"] } +30 | extend-per-file-ignores = { "bar.py" = ["UP035"] } + | ^^^^^ +31 | unfixable = ["F401"] +32 | extend-unfixable = ["F841"] + | +help: Replace rule code with name + | +29 | per-file-ignores = { "foo.py" = ["E501"] } + - extend-per-file-ignores = { "bar.py" = ["UP035"] } +30 + extend-per-file-ignores = { "bar.py" = ["deprecated-import"] } +31 | unfixable = ["F401"] + | + +RUF201 [*] Rule code used instead of name in lint.unfixable + --> ruff.toml:31:15 + | +29 | per-file-ignores = { "foo.py" = ["E501"] } +30 | extend-per-file-ignores = { "bar.py" = ["UP035"] } +31 | unfixable = ["F401"] + | ^^^^ +32 | extend-unfixable = ["F841"] +33 | extend-safe-fixes = ["E501"] + | +help: Replace rule code with name + | +30 | extend-per-file-ignores = { "bar.py" = ["UP035"] } + - unfixable = ["F401"] +31 + unfixable = ["unused-import"] +32 | extend-unfixable = ["F841"] + | + +RUF201 [*] Rule code used instead of name in lint.extend-unfixable + --> ruff.toml:32:22 + | +30 | extend-per-file-ignores = { "bar.py" = ["UP035"] } +31 | unfixable = ["F401"] +32 | extend-unfixable = ["F841"] + | ^^^^ +33 | extend-safe-fixes = ["E501"] +34 | extend-unsafe-fixes = ["UP035"] + | +help: Replace rule code with name + | +31 | unfixable = ["F401"] + - extend-unfixable = ["F841"] +32 + extend-unfixable = ["unused-variable"] +33 | extend-safe-fixes = ["E501"] + | + +RUF201 [*] Rule code used instead of name in lint.extend-safe-fixes + --> ruff.toml:33:23 + | +31 | unfixable = ["F401"] +32 | extend-unfixable = ["F841"] +33 | extend-safe-fixes = ["E501"] + | ^^^^ +34 | extend-unsafe-fixes = ["UP035"] + | +help: Replace rule code with name + | +32 | extend-unfixable = ["F841"] + - extend-safe-fixes = ["E501"] +33 + extend-safe-fixes = ["line-too-long"] +34 | extend-unsafe-fixes = ["UP035"] + | + +RUF201 [*] Rule code used instead of name in lint.extend-unsafe-fixes + --> ruff.toml:34:25 + | +32 | extend-unfixable = ["F841"] +33 | extend-safe-fixes = ["E501"] +34 | extend-unsafe-fixes = ["UP035"] + | ^^^^^ + | +help: Replace rule code with name + | +33 | extend-safe-fixes = ["E501"] + - extend-unsafe-fixes = ["UP035"] +34 + extend-unsafe-fixes = ["deprecated-import"] + | diff --git a/crates/ruff_linter/src/source_kind.rs b/crates/ruff_linter/src/source_kind.rs index 7b029ce1375462..54b3cabd6b6d6a 100644 --- a/crates/ruff_linter/src/source_kind.rs +++ b/crates/ruff_linter/src/source_kind.rs @@ -205,6 +205,15 @@ pub struct SourceKindDiff<'a> { path: Option<&'a Path>, } +impl<'a> SourceKindDiff<'a> { + pub fn from_text(original: &'a str, modified: &'a str, path: Option<&'a Path>) -> Self { + Self { + kind: DiffKind::Text(original, modified), + path, + } + } +} + impl std::fmt::Display for SourceKindDiff<'_> { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self.kind { diff --git a/crates/ruff_linter/src/test.rs b/crates/ruff_linter/src/test.rs index cba6b200fe39fc..14871456fa35bf 100644 --- a/crates/ruff_linter/src/test.rs +++ b/crates/ruff_linter/src/test.rs @@ -134,6 +134,24 @@ pub(crate) fn test_path( Ok(test_contents(&source_kind, &path, settings).0) } +/// Run the configuration TOML linter on a file in the `resources/test/fixtures` directory. +#[cfg(test)] +pub(crate) fn test_toml_path( + path: impl AsRef, + settings: &LinterSettings, + source_type: ruff_python_ast::TomlSourceType, +) -> Result> { + let path = test_resource_path("fixtures").join(path); + let filename = path.file_name().unwrap_or_else(|| path.as_os_str()); + let contents = std::fs::read_to_string(&path)?; + let source_file = SourceFileBuilder::new(filename.to_string_lossy(), contents).finish(); + Ok(crate::pyproject_toml::lint_toml( + &source_file, + settings, + source_type, + )) +} + /// Test a file with two different settings and return the differences #[cfg(test)] pub(crate) fn test_path_with_settings_diff( diff --git a/crates/ruff_python_ast/src/lib.rs b/crates/ruff_python_ast/src/lib.rs index bd49f93b5eddec..7f2eae3fd69326 100644 --- a/crates/ruff_python_ast/src/lib.rs +++ b/crates/ruff_python_ast/src/lib.rs @@ -69,6 +69,9 @@ impl> From

for SourceType { Some(filename) if filename == "pyproject.toml" => Self::Toml(TomlSourceType::Pyproject), Some(filename) if filename == "Pipfile" => Self::Toml(TomlSourceType::Pipfile), Some(filename) if filename == "poetry.lock" => Self::Toml(TomlSourceType::Poetry), + Some(filename) if filename == "ruff.toml" || filename == ".ruff.toml" => { + Self::Toml(TomlSourceType::Ruff) + } _ => Self::from_extension( path.as_ref() .extension() @@ -83,6 +86,8 @@ impl> From

for SourceType { pub enum TomlSourceType { /// The source is a `pyproject.toml`. Pyproject, + /// The source is a `ruff.toml` or `.ruff.toml`. + Ruff, /// The source is a `Pipfile`. Pipfile, /// The source is a `poetry.lock`. diff --git a/crates/ruff_workspace/src/options.rs b/crates/ruff_workspace/src/options.rs index f6ec9bd3727700..cd19803f520678 100644 --- a/crates/ruff_workspace/src/options.rs +++ b/crates/ruff_workspace/src/options.rs @@ -257,15 +257,16 @@ pub struct Options { /// A list of file patterns to include when linting. /// /// Inclusion are based on globs, and should be single-path patterns, like - /// `*.pyw`, to include any file with the `.pyw` extension. `pyproject.toml` is - /// included here not for configuration but because we lint whether e.g. the - /// `[project]` matches the schema. + /// `*.pyw`, to include any file with the `.pyw` extension. + /// `pyproject.toml`, `ruff.toml`, and `.ruff.toml` are included here not for + /// configuration but because we lint whether e.g. the `[project]` matches + /// the schema in `pyproject.toml` or that rule names are used as selectors. /// /// Notebook files (`.ipynb` extension) are included by default on Ruff 0.6.0+. /// /// For more information on the glob syntax, refer to the [`globset` documentation](https://docs.rs/globset/latest/globset/#syntax). #[option( - default = r#"["*.py", "*.pyi", "*.pyw", "*.ipynb", "*.md", "**/pyproject.toml"]"#, + default = r#"["*.py", "*.pyi", "*.pyw", "*.ipynb", "*.md", "**/pyproject.toml", "**/ruff.toml", "**/.ruff.toml"]"#, value_type = "list[str]", example = r#" include = ["*.py"] diff --git a/crates/ruff_workspace/src/settings.rs b/crates/ruff_workspace/src/settings.rs index 9490d716fec2f6..a1c3dd4af225a9 100644 --- a/crates/ruff_workspace/src/settings.rs +++ b/crates/ruff_workspace/src/settings.rs @@ -143,6 +143,8 @@ pub(crate) static INCLUDE: &[FilePattern] = &[ FilePattern::Builtin("*.pyi"), FilePattern::Builtin("*.ipynb"), FilePattern::Builtin("**/pyproject.toml"), + FilePattern::Builtin("**/ruff.toml"), + FilePattern::Builtin("**/.ruff.toml"), ]; pub(crate) static INCLUDE_PREVIEW: &[FilePattern] = &[ FilePattern::Builtin("*.py"), @@ -150,6 +152,8 @@ pub(crate) static INCLUDE_PREVIEW: &[FilePattern] = &[ FilePattern::Builtin("*.pyw"), FilePattern::Builtin("*.ipynb"), FilePattern::Builtin("**/pyproject.toml"), + FilePattern::Builtin("**/ruff.toml"), + FilePattern::Builtin("**/.ruff.toml"), FilePattern::Builtin("*.md"), ]; diff --git a/ruff.schema.json b/ruff.schema.json index f846497844eb90..212dbdf6aab0c8 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -465,7 +465,7 @@ "deprecated": true }, "include": { - "description": "A list of file patterns to include when linting.\n\nInclusion are based on globs, and should be single-path patterns, like\n`*.pyw`, to include any file with the `.pyw` extension. `pyproject.toml` is\nincluded here not for configuration but because we lint whether e.g. the\n`[project]` matches the schema.\n\nNotebook files (`.ipynb` extension) are included by default on Ruff 0.6.0+.\n\nFor more information on the glob syntax, refer to the [`globset` documentation](https://docs.rs/globset/latest/globset/#syntax).", + "description": "A list of file patterns to include when linting.\n\nInclusion are based on globs, and should be single-path patterns, like\n`*.pyw`, to include any file with the `.pyw` extension.\n`pyproject.toml`, `ruff.toml`, and `.ruff.toml` are included here not for\nconfiguration but because we lint whether e.g. the `[project]` matches\nthe schema in `pyproject.toml` or that rule names are used as selectors.\n\nNotebook files (`.ipynb` extension) are included by default on Ruff 0.6.0+.\n\nFor more information on the glob syntax, refer to the [`globset` documentation](https://docs.rs/globset/latest/globset/#syntax).", "type": [ "array", "null" @@ -4270,6 +4270,7 @@ "RUF2", "RUF20", "RUF200", + "RUF201", "S", "S1", "S10", @@ -5209,6 +5210,7 @@ "return-outside-function", "reuse-of-groupby-generator", "root-logger-call", + "rule-codes-in-selectors", "rule-codes-in-suppression-comments", "run-process-in-async-function", "runtime-cast-value", From 6a2d6a5a326819ca9288fc256fcf239d9773acc1 Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Mon, 13 Jul 2026 13:25:34 -0400 Subject: [PATCH 02/17] use LintContext to respect other rule settings --- crates/ruff/src/diagnostics.rs | 15 ++- crates/ruff/tests/cli/lint.rs | 103 ++++++++++++++++++ crates/ruff_linter/src/pyproject_toml.rs | 65 +++++------ .../ruff/rules/rule_codes_in_selectors.rs | 37 ++----- crates/ruff_linter/src/test.rs | 4 +- 5 files changed, 152 insertions(+), 72 deletions(-) diff --git a/crates/ruff/src/diagnostics.rs b/crates/ruff/src/diagnostics.rs index 146ed40af0e287..f1b8bc14d4148c 100644 --- a/crates/ruff/src/diagnostics.rs +++ b/crates/ruff/src/diagnostics.rs @@ -225,13 +225,12 @@ pub(crate) fn lint_path( return Ok(Diagnostics::from_source_error(&err, Some(path), settings)); } }; - let source_file = SourceFileBuilder::new(path.to_string_lossy(), contents).finish(); if matches!(fix_mode, flags::FixMode::Apply | flags::FixMode::Diff) { let TomlFixerResult { diagnostics, transformed, fixed, - } = lint_fix_toml(&source_file, settings, source_type, unsafe_fixes); + } = lint_fix_toml(path, &contents, settings, source_type, unsafe_fixes); if !fixed.is_empty() { match fix_mode { @@ -243,7 +242,7 @@ pub(crate) fn lint_path( &mut io::stdout().lock(), "{}", SourceKindDiff::from_text( - source_file.source_text(), + &contents, transformed.as_ref(), Some(path), ) @@ -256,7 +255,7 @@ pub(crate) fn lint_path( (diagnostics, fixed) } else { ( - lint_toml(&source_file, settings, source_type), + lint_toml(path, &contents, settings, source_type), FixTable::default(), ) } @@ -404,7 +403,6 @@ pub(crate) fn lint_stdin( } let path = path.unwrap(); - let source_file = SourceFileBuilder::new(path.to_string_lossy(), contents).finish(); let (diagnostics, fixed) = if matches!(fix_mode, flags::FixMode::Apply | flags::FixMode::Diff) { @@ -413,7 +411,8 @@ pub(crate) fn lint_stdin( transformed, fixed, } = lint_fix_toml( - &source_file, + path, + &contents, &settings.linter, source_type, settings.unsafe_fixes, @@ -429,7 +428,7 @@ pub(crate) fn lint_stdin( &mut io::stdout().lock(), "{}", SourceKindDiff::from_text( - source_file.source_text(), + &contents, transformed.as_ref(), Some(path), ) @@ -442,7 +441,7 @@ pub(crate) fn lint_stdin( (diagnostics, fixed) } else { ( - lint_toml(&source_file, &settings.linter, source_type), + lint_toml(path, &contents, &settings.linter, source_type), FixTable::default(), ) }; diff --git a/crates/ruff/tests/cli/lint.rs b/crates/ruff/tests/cli/lint.rs index 598df3f39a92ab..454b83a61a5f05 100644 --- a/crates/ruff/tests/cli/lint.rs +++ b/crates/ruff/tests/cli/lint.rs @@ -5230,3 +5230,106 @@ fn ruff_toml_is_linted() -> Result<()> { Ok(()) } + +#[test] +fn ruff_toml_respects_unfixable() -> Result<()> { + let test = CliTest::with_file("ruff.toml", r#"lint.select = ["F401"]"#)?; + + assert_cmd_snapshot!( + test.command().args([ + "check", + "--no-cache", + "--isolated", + "--preview", + "--select", + "RUF201", + "--unfixable", + "RUF201", + "--fix", + ]), + @r#" + success: false + exit_code: 1 + ----- stdout ----- + rule-codes-in-selectors: Rule code used instead of name in lint.select + --> ruff.toml:1:17 + | + 1 | lint.select = ["F401"] + | ^^^^ + | + help: Replace rule code with name + + Found 1 error. + + ----- stderr ----- + "# + ); + + Ok(()) +} + +#[test] +fn ruff_toml_respects_fix_safety_overrides() -> Result<()> { + let test = CliTest::with_file("ruff.toml", r#"lint.select = ["F401"]"#)?; + + assert_cmd_snapshot!( + test.command().args([ + "check", + "--no-cache", + "--isolated", + "--preview", + "--select", + "RUF201", + "--config", + r#"lint.extend-unsafe-fixes=["RUF201"]"#, + "--fix", + ]), + @r#" + success: false + exit_code: 1 + ----- stdout ----- + rule-codes-in-selectors: Rule code used instead of name in lint.select + --> ruff.toml:1:17 + | + 1 | lint.select = ["F401"] + | ^^^^ + | + help: Replace rule code with name + + Found 1 error. + No fixes available (1 hidden fix can be enabled with the `--unsafe-fixes` option). + + ----- stderr ----- + "# + ); + + Ok(()) +} + +#[test] +fn ruff_toml_respects_per_file_ignores() -> Result<()> { + let test = CliTest::with_file("ruff.toml", r#"lint.select = ["F401"]"#)?; + + assert_cmd_snapshot!( + test.command().args([ + "check", + "--no-cache", + "--isolated", + "--preview", + "--select", + "RUF201", + "--per-file-ignores", + "ruff.toml:RUF201", + ]), + @" + success: true + exit_code: 0 + ----- stdout ----- + All checks passed! + + ----- stderr ----- + " + ); + + Ok(()) +} diff --git a/crates/ruff_linter/src/pyproject_toml.rs b/crates/ruff_linter/src/pyproject_toml.rs index 97cd75414b214a..e60521797a573e 100644 --- a/crates/ruff_linter/src/pyproject_toml.rs +++ b/crates/ruff_linter/src/pyproject_toml.rs @@ -9,16 +9,16 @@ use ruff_python_ast::TomlSourceType; use ruff_text_size::{TextRange, TextSize}; use ruff_db::diagnostic::Diagnostic; -use ruff_source_file::{SourceFile, SourceFileBuilder}; +use crate::IOError; use crate::Locator; +use crate::checkers::ast::LintContext; use crate::fix::{FixResult, fix_file}; use crate::linter::{FixTable, MAX_ITERATIONS, report_failed_to_converge_error}; use crate::registry::Rule; use crate::rules::ruff::rules::{InvalidPyprojectToml, rule_codes_in_selectors}; use crate::settings::LinterSettings; use crate::settings::types::UnsafeFixes; -use crate::{IOError, Violation}; pub struct TomlFixerResult<'a> { pub diagnostics: Vec, @@ -28,15 +28,17 @@ pub struct TomlFixerResult<'a> { /// RUF200 pub fn lint_toml( - source_file: &SourceFile, + path: &Path, + source: &str, settings: &LinterSettings, source_type: TomlSourceType, ) -> Vec { - let mut messages = rule_codes_in_selectors(source_file, settings, source_type); + let context = LintContext::new(path, source, settings); + rule_codes_in_selectors(&context, source_type); - if settings.rules.enabled(Rule::InvalidPyprojectToml) && source_type.is_pyproject() { - let Some(err) = toml::from_str::(source_file.source_text()).err() else { - return messages; + if context.is_rule_enabled(Rule::InvalidPyprojectToml) && source_type.is_pyproject() { + let Some(err) = toml::from_str::(source).err() else { + return context.into_parts().0; }; let range = match err.span() { @@ -44,33 +46,31 @@ pub fn lint_toml( // TODO(konstin,micha): https://github.com/astral-sh/ruff/issues/4571 None => TextRange::default(), Some(range) => { - let Some(range) = text_range_from_std(range, source_file, settings, &mut messages) - else { - return messages; + let Some(range) = text_range_from_std(range, &context) else { + return context.into_parts().0; }; range } }; let toml_err = err.message().to_string(); - let diagnostic = - InvalidPyprojectToml { message: toml_err }.into_diagnostic(range, source_file); - messages.push(diagnostic); + context.report_diagnostic(InvalidPyprojectToml { message: toml_err }, range); } - messages + context.into_parts().0 } /// Generate diagnostics for a TOML configuration file, iteratively applying fixes until the source /// stabilizes. pub fn lint_fix_toml<'a>( - source_file: &'a SourceFile, + path: &Path, + source: &'a str, settings: &LinterSettings, source_type: TomlSourceType, unsafe_fixes: UnsafeFixes, ) -> TomlFixerResult<'a> { - let mut diagnostics = lint_toml(source_file, settings, source_type); - let mut transformed = Cow::Borrowed(source_file.source_text()); + let mut diagnostics = lint_toml(path, source, settings, source_type); + let mut transformed = Cow::Borrowed(source); let mut fixed = FixTable::default(); let mut iterations = 0; @@ -86,11 +86,7 @@ pub fn lint_fix_toml<'a>( }; if iterations >= MAX_ITERATIONS { - report_failed_to_converge_error( - Path::new(source_file.name()), - transformed.as_ref(), - &diagnostics, - ); + report_failed_to_converge_error(path, transformed.as_ref(), &diagnostics); return TomlFixerResult { diagnostics, transformed, @@ -105,28 +101,21 @@ pub fn lint_fix_toml<'a>( transformed = Cow::Owned(code); iterations += 1; - let transformed_source = - SourceFileBuilder::new(source_file.name(), transformed.as_ref()).finish(); - diagnostics = lint_toml(&transformed_source, settings, source_type); + diagnostics = lint_toml(path, transformed.as_ref(), settings, source_type); } } /// Try to convert a `range` into a `TextRange`, emitting an `IOError` diagnostic if the file is too /// large or a warning if the `IOError` lint rule is disabled. -pub(crate) fn text_range_from_std( - range: Range, - source_file: &SourceFile, - settings: &LinterSettings, - diagnostics: &mut Vec, -) -> Option { +pub(crate) fn text_range_from_std(range: Range, context: &LintContext) -> Option { let Ok(end) = TextSize::try_from(range.end) else { + let source_file = context.source_file(); let message = format!( "{} is larger than 4GB, but ruff assumes all files to be smaller", source_file.name(), ); - if settings.rules.enabled(Rule::IOError) { - let diagnostic = IOError { message }.into_diagnostic(TextRange::default(), source_file); - diagnostics.push(diagnostic); + if context.is_rule_enabled(Rule::IOError) { + context.report_diagnostic(IOError { message }, TextRange::default()); } else { warn!( "{}{}{} {message}", @@ -146,11 +135,12 @@ pub(crate) fn text_range_from_std( #[cfg(test)] mod tests { + use std::path::Path; + use insta::assert_snapshot; use ruff_db::diagnostic::{DisplayDiagnosticConfig, DisplayDiagnostics, DummyFileResolver}; use ruff_python_ast::TomlSourceType; - use ruff_source_file::SourceFileBuilder; use crate::codes::Rule; use crate::settings::LinterSettings; @@ -160,11 +150,12 @@ mod tests { #[test] fn fixes_toml() { - let source_file = SourceFileBuilder::new("ruff.toml", r#"lint.select = ["F401"]"#).finish(); + let source = r#"lint.select = ["F401"]"#; let settings = LinterSettings::for_rule(Rule::RuleCodesInSelectors).with_preview_mode(); let result = lint_fix_toml( - &source_file, + Path::new("ruff.toml"), + source, &settings, TomlSourceType::Ruff, UnsafeFixes::Disabled, diff --git a/crates/ruff_linter/src/rules/ruff/rules/rule_codes_in_selectors.rs b/crates/ruff_linter/src/rules/ruff/rules/rule_codes_in_selectors.rs index fa165df883dd6a..ba810913a45585 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/rule_codes_in_selectors.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/rule_codes_in_selectors.rs @@ -5,16 +5,14 @@ use rustc_hash::FxHashMap; use serde::Deserialize; use toml::Spanned; -use ruff_db::diagnostic::Diagnostic; use ruff_diagnostics::{Edit, Fix}; use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::TomlSourceType; -use ruff_source_file::SourceFile; use crate::{ - FixAvailability, Violation, codes::Rule, preview::is_human_readable_names_enabled, - pyproject_toml::text_range_from_std, rule_redirects::get_redirect_target, - settings::LinterSettings, + FixAvailability, Violation, checkers::ast::LintContext, codes::Rule, + preview::is_human_readable_names_enabled, pyproject_toml::text_range_from_std, + rule_redirects::get_redirect_target, }; /// ## What it does @@ -60,21 +58,16 @@ impl Violation for RuleCodesInSelectors { } /// RUF201 -pub(crate) fn rule_codes_in_selectors( - source_file: &SourceFile, - settings: &LinterSettings, - source_type: TomlSourceType, -) -> Vec { - let mut diagnostics = Vec::new(); - - if !settings.rules.enabled(Rule::RuleCodesInSelectors) - || !is_human_readable_names_enabled(settings.preview) +pub(crate) fn rule_codes_in_selectors(context: &LintContext, source_type: TomlSourceType) { + if !context.is_rule_enabled(Rule::RuleCodesInSelectors) + || !is_human_readable_names_enabled(context.settings().preview) { - return diagnostics; + return; } + let source_file = context.source_file(); let Ok(Some(document)) = Document::from_toml_str(source_file.source_text(), source_type) else { - return diagnostics; + return; }; for (spanned, selector) in document.selectors() { @@ -93,22 +86,16 @@ pub(crate) fn rule_codes_in_selectors( // This can only fail if the file is larger than 4GB, so abort analysis of the entire file // rather than continuing to the next selector. - let Some(range) = - text_range_from_std(content_range, source_file, settings, &mut diagnostics) - else { - return diagnostics; + let Some(range) = text_range_from_std(content_range, context) else { + return; }; - let mut diagnostic = RuleCodesInSelectors { selector }.into_diagnostic(range, source_file); + let mut diagnostic = context.report_diagnostic(RuleCodesInSelectors { selector }, range); diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement( rule.name().to_string(), range, ))); - - diagnostics.push(diagnostic); } - - diagnostics } enum Document { diff --git a/crates/ruff_linter/src/test.rs b/crates/ruff_linter/src/test.rs index 14871456fa35bf..8908392eb53271 100644 --- a/crates/ruff_linter/src/test.rs +++ b/crates/ruff_linter/src/test.rs @@ -144,9 +144,9 @@ pub(crate) fn test_toml_path( let path = test_resource_path("fixtures").join(path); let filename = path.file_name().unwrap_or_else(|| path.as_os_str()); let contents = std::fs::read_to_string(&path)?; - let source_file = SourceFileBuilder::new(filename.to_string_lossy(), contents).finish(); Ok(crate::pyproject_toml::lint_toml( - &source_file, + Path::new(filename), + &contents, settings, source_type, )) From ac8b93c11bf2dc343bd216641612364ee6b3316e Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Mon, 13 Jul 2026 14:24:24 -0400 Subject: [PATCH 03/17] skip additional paths for formatter stability check --- crates/ruff_dev/src/format_dev.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/ruff_dev/src/format_dev.rs b/crates/ruff_dev/src/format_dev.rs index 25f81fbb802767..ac081bff1b1c25 100644 --- a/crates/ruff_dev/src/format_dev.rs +++ b/crates/ruff_dev/src/format_dev.rs @@ -554,7 +554,10 @@ fn format_dir_entry( ) -> anyhow::Result<(Result, PathBuf), Error> { let resolved_file = resolved_file.context("Iterating the files in the repository failed")?; // For some reason it does not filter in the beginning - if resolved_file.file_name() == "pyproject.toml" { + if ["pyproject.toml", "ruff.toml", ".ruff.toml"] + .iter() + .any(|&path| resolved_file.file_name() == path) + { return Ok((Ok(Statistics::default()), resolved_file.into_path())); } From 86dbdfb276a2c8d9f3e87e9e407b3d28c0fe2b2d Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Mon, 13 Jul 2026 14:27:16 -0400 Subject: [PATCH 04/17] add backticks around selector in diagnostic --- crates/ruff/tests/cli/lint.rs | 6 +-- .../ruff/rules/rule_codes_in_selectors.rs | 2 +- ...s__ruff__tests__RUF201_pyproject.toml.snap | 48 +++++++++---------- ..._rules__ruff__tests__RUF201_ruff.toml.snap | 48 +++++++++---------- 4 files changed, 52 insertions(+), 52 deletions(-) diff --git a/crates/ruff/tests/cli/lint.rs b/crates/ruff/tests/cli/lint.rs index 454b83a61a5f05..d10a4849ebbbd3 100644 --- a/crates/ruff/tests/cli/lint.rs +++ b/crates/ruff/tests/cli/lint.rs @@ -5209,7 +5209,7 @@ fn ruff_toml_is_linted() -> Result<()> { success: false exit_code: 1 ----- stdout ----- - rule-codes-in-selectors: [*] Rule code used instead of name in lint.select + rule-codes-in-selectors: [*] Rule code used instead of name in `lint.select` --> ruff.toml:1:17 | 1 | lint.select = ["F401"] @@ -5251,7 +5251,7 @@ fn ruff_toml_respects_unfixable() -> Result<()> { success: false exit_code: 1 ----- stdout ----- - rule-codes-in-selectors: Rule code used instead of name in lint.select + rule-codes-in-selectors: Rule code used instead of name in `lint.select` --> ruff.toml:1:17 | 1 | lint.select = ["F401"] @@ -5288,7 +5288,7 @@ fn ruff_toml_respects_fix_safety_overrides() -> Result<()> { success: false exit_code: 1 ----- stdout ----- - rule-codes-in-selectors: Rule code used instead of name in lint.select + rule-codes-in-selectors: Rule code used instead of name in `lint.select` --> ruff.toml:1:17 | 1 | lint.select = ["F401"] diff --git a/crates/ruff_linter/src/rules/ruff/rules/rule_codes_in_selectors.rs b/crates/ruff_linter/src/rules/ruff/rules/rule_codes_in_selectors.rs index ba810913a45585..c117a977bb485d 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/rule_codes_in_selectors.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/rule_codes_in_selectors.rs @@ -49,7 +49,7 @@ impl Violation for RuleCodesInSelectors { #[derive_message_formats] fn message(&self) -> String { let Self { selector } = self; - format!("Rule code used instead of name in {selector}") + format!("Rule code used instead of name in `{selector}`") } fn fix_title(&self) -> Option { diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF201_pyproject.toml.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF201_pyproject.toml.snap index 20278fceb4d71a..67702fa5208626 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF201_pyproject.toml.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF201_pyproject.toml.snap @@ -1,7 +1,7 @@ --- source: crates/ruff_linter/src/rules/ruff/mod.rs --- -RUF201 [*] Rule code used instead of name in select +RUF201 [*] Rule code used instead of name in `select` --> pyproject.toml:3:6 | 1 | [tool.ruff] @@ -20,7 +20,7 @@ help: Replace rule code with name 5 | "F", | -RUF201 [*] Rule code used instead of name in extend-select +RUF201 [*] Rule code used instead of name in `extend-select` --> pyproject.toml:7:19 | 5 | "F", @@ -38,7 +38,7 @@ help: Replace rule code with name 8 | fixable = ["""E501"""] | -RUF201 [*] Rule code used instead of name in fixable +RUF201 [*] Rule code used instead of name in `fixable` --> pyproject.toml:8:15 | 6 | ] @@ -56,7 +56,7 @@ help: Replace rule code with name 9 | extend-fixable = ['''UP035'''] | -RUF201 [*] Rule code used instead of name in extend-fixable +RUF201 [*] Rule code used instead of name in `extend-fixable` --> pyproject.toml:9:22 | 7 | extend-select = ['F841'] @@ -74,7 +74,7 @@ help: Replace rule code with name 10 | ignore = ["F401"] | -RUF201 [*] Rule code used instead of name in ignore +RUF201 [*] Rule code used instead of name in `ignore` --> pyproject.toml:10:12 | 8 | fixable = ["""E501"""] @@ -92,7 +92,7 @@ help: Replace rule code with name 11 | extend-ignore = ["F841"] | -RUF201 [*] Rule code used instead of name in extend-ignore +RUF201 [*] Rule code used instead of name in `extend-ignore` --> pyproject.toml:11:19 | 9 | extend-fixable = ['''UP035'''] @@ -110,7 +110,7 @@ help: Replace rule code with name 12 | per-file-ignores = { "foo.py" = ["E501"] } | -RUF201 [*] Rule code used instead of name in per-file-ignores +RUF201 [*] Rule code used instead of name in `per-file-ignores` --> pyproject.toml:12:35 | 10 | ignore = ["F401"] @@ -128,7 +128,7 @@ help: Replace rule code with name 13 | extend-per-file-ignores = { "bar.py" = ["UP035"] } | -RUF201 [*] Rule code used instead of name in extend-per-file-ignores +RUF201 [*] Rule code used instead of name in `extend-per-file-ignores` --> pyproject.toml:13:42 | 11 | extend-ignore = ["F841"] @@ -146,7 +146,7 @@ help: Replace rule code with name 14 | unfixable = ["F401"] | -RUF201 [*] Rule code used instead of name in unfixable +RUF201 [*] Rule code used instead of name in `unfixable` --> pyproject.toml:14:15 | 12 | per-file-ignores = { "foo.py" = ["E501"] } @@ -164,7 +164,7 @@ help: Replace rule code with name 15 | extend-unfixable = ["F841"] | -RUF201 [*] Rule code used instead of name in extend-unfixable +RUF201 [*] Rule code used instead of name in `extend-unfixable` --> pyproject.toml:15:22 | 13 | extend-per-file-ignores = { "bar.py" = ["UP035"] } @@ -182,7 +182,7 @@ help: Replace rule code with name 16 | extend-safe-fixes = ["E501"] | -RUF201 [*] Rule code used instead of name in extend-safe-fixes +RUF201 [*] Rule code used instead of name in `extend-safe-fixes` --> pyproject.toml:16:23 | 14 | unfixable = ["F401"] @@ -199,7 +199,7 @@ help: Replace rule code with name 17 | extend-unsafe-fixes = ["UP035"] | -RUF201 [*] Rule code used instead of name in extend-unsafe-fixes +RUF201 [*] Rule code used instead of name in `extend-unsafe-fixes` --> pyproject.toml:17:25 | 15 | extend-unfixable = ["F841"] @@ -217,7 +217,7 @@ help: Replace rule code with name 18 | | -RUF201 [*] Rule code used instead of name in lint.select +RUF201 [*] Rule code used instead of name in `lint.select` --> pyproject.toml:21:6 | 19 | [tool.ruff.lint] @@ -236,7 +236,7 @@ help: Replace rule code with name 23 | "unknown-rule", | -RUF201 [*] Rule code used instead of name in lint.extend-select +RUF201 [*] Rule code used instead of name in `lint.extend-select` --> pyproject.toml:25:19 | 23 | "unknown-rule", @@ -254,7 +254,7 @@ help: Replace rule code with name 26 | fixable = ["""E501"""] | -RUF201 [*] Rule code used instead of name in lint.fixable +RUF201 [*] Rule code used instead of name in `lint.fixable` --> pyproject.toml:26:15 | 24 | ] @@ -272,7 +272,7 @@ help: Replace rule code with name 27 | extend-fixable = ['''UP035'''] | -RUF201 [*] Rule code used instead of name in lint.extend-fixable +RUF201 [*] Rule code used instead of name in `lint.extend-fixable` --> pyproject.toml:27:22 | 25 | extend-select = ['F841'] @@ -290,7 +290,7 @@ help: Replace rule code with name 28 | ignore = ["F401"] | -RUF201 [*] Rule code used instead of name in lint.ignore +RUF201 [*] Rule code used instead of name in `lint.ignore` --> pyproject.toml:28:12 | 26 | fixable = ["""E501"""] @@ -308,7 +308,7 @@ help: Replace rule code with name 29 | extend-ignore = ["F841"] | -RUF201 [*] Rule code used instead of name in lint.extend-ignore +RUF201 [*] Rule code used instead of name in `lint.extend-ignore` --> pyproject.toml:29:19 | 27 | extend-fixable = ['''UP035'''] @@ -326,7 +326,7 @@ help: Replace rule code with name 30 | per-file-ignores = { "foo.py" = ["E501"] } | -RUF201 [*] Rule code used instead of name in lint.per-file-ignores +RUF201 [*] Rule code used instead of name in `lint.per-file-ignores` --> pyproject.toml:30:35 | 28 | ignore = ["F401"] @@ -344,7 +344,7 @@ help: Replace rule code with name 31 | extend-per-file-ignores = { "bar.py" = ["UP035"] } | -RUF201 [*] Rule code used instead of name in lint.extend-per-file-ignores +RUF201 [*] Rule code used instead of name in `lint.extend-per-file-ignores` --> pyproject.toml:31:42 | 29 | extend-ignore = ["F841"] @@ -362,7 +362,7 @@ help: Replace rule code with name 32 | unfixable = ["F401"] | -RUF201 [*] Rule code used instead of name in lint.unfixable +RUF201 [*] Rule code used instead of name in `lint.unfixable` --> pyproject.toml:32:15 | 30 | per-file-ignores = { "foo.py" = ["E501"] } @@ -380,7 +380,7 @@ help: Replace rule code with name 33 | extend-unfixable = ["F841"] | -RUF201 [*] Rule code used instead of name in lint.extend-unfixable +RUF201 [*] Rule code used instead of name in `lint.extend-unfixable` --> pyproject.toml:33:22 | 31 | extend-per-file-ignores = { "bar.py" = ["UP035"] } @@ -398,7 +398,7 @@ help: Replace rule code with name 34 | extend-safe-fixes = ["E501"] | -RUF201 [*] Rule code used instead of name in lint.extend-safe-fixes +RUF201 [*] Rule code used instead of name in `lint.extend-safe-fixes` --> pyproject.toml:34:23 | 32 | unfixable = ["F401"] @@ -415,7 +415,7 @@ help: Replace rule code with name 35 | extend-unsafe-fixes = ["UP035"] | -RUF201 [*] Rule code used instead of name in lint.extend-unsafe-fixes +RUF201 [*] Rule code used instead of name in `lint.extend-unsafe-fixes` --> pyproject.toml:35:25 | 33 | extend-unfixable = ["F841"] diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF201_ruff.toml.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF201_ruff.toml.snap index b8edbfd7b77e07..76a494b7cf9a35 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF201_ruff.toml.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF201_ruff.toml.snap @@ -1,7 +1,7 @@ --- source: crates/ruff_linter/src/rules/ruff/mod.rs --- -RUF201 [*] Rule code used instead of name in select +RUF201 [*] Rule code used instead of name in `select` --> ruff.toml:2:6 | 1 | select = [ @@ -19,7 +19,7 @@ help: Replace rule code with name 4 | "F", | -RUF201 [*] Rule code used instead of name in extend-select +RUF201 [*] Rule code used instead of name in `extend-select` --> ruff.toml:6:19 | 4 | "F", @@ -37,7 +37,7 @@ help: Replace rule code with name 7 | fixable = ["""E501"""] | -RUF201 [*] Rule code used instead of name in fixable +RUF201 [*] Rule code used instead of name in `fixable` --> ruff.toml:7:15 | 5 | ] @@ -55,7 +55,7 @@ help: Replace rule code with name 8 | extend-fixable = ['''UP035'''] | -RUF201 [*] Rule code used instead of name in extend-fixable +RUF201 [*] Rule code used instead of name in `extend-fixable` --> ruff.toml:8:22 | 6 | extend-select = ['F841'] @@ -73,7 +73,7 @@ help: Replace rule code with name 9 | ignore = ["F401"] | -RUF201 [*] Rule code used instead of name in ignore +RUF201 [*] Rule code used instead of name in `ignore` --> ruff.toml:9:12 | 7 | fixable = ["""E501"""] @@ -91,7 +91,7 @@ help: Replace rule code with name 10 | extend-ignore = ["F841"] | -RUF201 [*] Rule code used instead of name in extend-ignore +RUF201 [*] Rule code used instead of name in `extend-ignore` --> ruff.toml:10:19 | 8 | extend-fixable = ['''UP035'''] @@ -109,7 +109,7 @@ help: Replace rule code with name 11 | per-file-ignores = { "foo.py" = ["E501"] } | -RUF201 [*] Rule code used instead of name in per-file-ignores +RUF201 [*] Rule code used instead of name in `per-file-ignores` --> ruff.toml:11:35 | 9 | ignore = ["F401"] @@ -127,7 +127,7 @@ help: Replace rule code with name 12 | extend-per-file-ignores = { "bar.py" = ["UP035"] } | -RUF201 [*] Rule code used instead of name in extend-per-file-ignores +RUF201 [*] Rule code used instead of name in `extend-per-file-ignores` --> ruff.toml:12:42 | 10 | extend-ignore = ["F841"] @@ -145,7 +145,7 @@ help: Replace rule code with name 13 | unfixable = ["F401"] | -RUF201 [*] Rule code used instead of name in unfixable +RUF201 [*] Rule code used instead of name in `unfixable` --> ruff.toml:13:15 | 11 | per-file-ignores = { "foo.py" = ["E501"] } @@ -163,7 +163,7 @@ help: Replace rule code with name 14 | extend-unfixable = ["F841"] | -RUF201 [*] Rule code used instead of name in extend-unfixable +RUF201 [*] Rule code used instead of name in `extend-unfixable` --> ruff.toml:14:22 | 12 | extend-per-file-ignores = { "bar.py" = ["UP035"] } @@ -181,7 +181,7 @@ help: Replace rule code with name 15 | extend-safe-fixes = ["E501"] | -RUF201 [*] Rule code used instead of name in extend-safe-fixes +RUF201 [*] Rule code used instead of name in `extend-safe-fixes` --> ruff.toml:15:23 | 13 | unfixable = ["F401"] @@ -198,7 +198,7 @@ help: Replace rule code with name 16 | extend-unsafe-fixes = ["UP035"] | -RUF201 [*] Rule code used instead of name in extend-unsafe-fixes +RUF201 [*] Rule code used instead of name in `extend-unsafe-fixes` --> ruff.toml:16:25 | 14 | extend-unfixable = ["F841"] @@ -216,7 +216,7 @@ help: Replace rule code with name 17 | | -RUF201 [*] Rule code used instead of name in lint.select +RUF201 [*] Rule code used instead of name in `lint.select` --> ruff.toml:20:6 | 18 | [lint] @@ -235,7 +235,7 @@ help: Replace rule code with name 22 | "unknown-rule", | -RUF201 [*] Rule code used instead of name in lint.extend-select +RUF201 [*] Rule code used instead of name in `lint.extend-select` --> ruff.toml:24:19 | 22 | "unknown-rule", @@ -253,7 +253,7 @@ help: Replace rule code with name 25 | fixable = ["""E501"""] | -RUF201 [*] Rule code used instead of name in lint.fixable +RUF201 [*] Rule code used instead of name in `lint.fixable` --> ruff.toml:25:15 | 23 | ] @@ -271,7 +271,7 @@ help: Replace rule code with name 26 | extend-fixable = ['''UP035'''] | -RUF201 [*] Rule code used instead of name in lint.extend-fixable +RUF201 [*] Rule code used instead of name in `lint.extend-fixable` --> ruff.toml:26:22 | 24 | extend-select = ['F841'] @@ -289,7 +289,7 @@ help: Replace rule code with name 27 | ignore = ["F401"] | -RUF201 [*] Rule code used instead of name in lint.ignore +RUF201 [*] Rule code used instead of name in `lint.ignore` --> ruff.toml:27:12 | 25 | fixable = ["""E501"""] @@ -307,7 +307,7 @@ help: Replace rule code with name 28 | extend-ignore = ["F841"] | -RUF201 [*] Rule code used instead of name in lint.extend-ignore +RUF201 [*] Rule code used instead of name in `lint.extend-ignore` --> ruff.toml:28:19 | 26 | extend-fixable = ['''UP035'''] @@ -325,7 +325,7 @@ help: Replace rule code with name 29 | per-file-ignores = { "foo.py" = ["E501"] } | -RUF201 [*] Rule code used instead of name in lint.per-file-ignores +RUF201 [*] Rule code used instead of name in `lint.per-file-ignores` --> ruff.toml:29:35 | 27 | ignore = ["F401"] @@ -343,7 +343,7 @@ help: Replace rule code with name 30 | extend-per-file-ignores = { "bar.py" = ["UP035"] } | -RUF201 [*] Rule code used instead of name in lint.extend-per-file-ignores +RUF201 [*] Rule code used instead of name in `lint.extend-per-file-ignores` --> ruff.toml:30:42 | 28 | extend-ignore = ["F841"] @@ -361,7 +361,7 @@ help: Replace rule code with name 31 | unfixable = ["F401"] | -RUF201 [*] Rule code used instead of name in lint.unfixable +RUF201 [*] Rule code used instead of name in `lint.unfixable` --> ruff.toml:31:15 | 29 | per-file-ignores = { "foo.py" = ["E501"] } @@ -379,7 +379,7 @@ help: Replace rule code with name 32 | extend-unfixable = ["F841"] | -RUF201 [*] Rule code used instead of name in lint.extend-unfixable +RUF201 [*] Rule code used instead of name in `lint.extend-unfixable` --> ruff.toml:32:22 | 30 | extend-per-file-ignores = { "bar.py" = ["UP035"] } @@ -397,7 +397,7 @@ help: Replace rule code with name 33 | extend-safe-fixes = ["E501"] | -RUF201 [*] Rule code used instead of name in lint.extend-safe-fixes +RUF201 [*] Rule code used instead of name in `lint.extend-safe-fixes` --> ruff.toml:33:23 | 31 | unfixable = ["F401"] @@ -414,7 +414,7 @@ help: Replace rule code with name 34 | extend-unsafe-fixes = ["UP035"] | -RUF201 [*] Rule code used instead of name in lint.extend-unsafe-fixes +RUF201 [*] Rule code used instead of name in `lint.extend-unsafe-fixes` --> ruff.toml:34:25 | 32 | extend-unfixable = ["F841"] From 2dc6636ab6a5355170adb49156e17925d825a0fe Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Mon, 13 Jul 2026 16:19:46 -0400 Subject: [PATCH 05/17] tidy up --- crates/ruff_linter/src/checkers/ast/mod.rs | 5 +++ .../src/checkers/physical_lines.rs | 2 +- crates/ruff_linter/src/pyproject_toml.rs | 20 +++++----- .../ruff/rules/rule_codes_in_selectors.rs | 38 +++++++++---------- 4 files changed, 35 insertions(+), 30 deletions(-) diff --git a/crates/ruff_linter/src/checkers/ast/mod.rs b/crates/ruff_linter/src/checkers/ast/mod.rs index cdfa6c1bc4a781..afdf134f1e0f12 100644 --- a/crates/ruff_linter/src/checkers/ast/mod.rs +++ b/crates/ruff_linter/src/checkers/ast/mod.rs @@ -3548,6 +3548,11 @@ impl<'a> LintContext<'a> { (self.diagnostics.into_inner(), self.source_file) } + #[inline] + pub(crate) fn into_diagnostics(self) -> Vec { + self.diagnostics.into_inner() + } + #[inline] pub(crate) fn as_mut_vec(&mut self) -> &mut Vec { self.diagnostics.get_mut() diff --git a/crates/ruff_linter/src/checkers/physical_lines.rs b/crates/ruff_linter/src/checkers/physical_lines.rs index 5cc8f7a9089c82..dbfe48a30811da 100644 --- a/crates/ruff_linter/src/checkers/physical_lines.rs +++ b/crates/ruff_linter/src/checkers/physical_lines.rs @@ -115,7 +115,7 @@ mod tests { }; let diagnostics = LintContext::new(Path::new(""), line, &settings); check_physical_lines(&locator, &stylist, &indexer, &[], &settings, &diagnostics); - diagnostics.into_parts().0 + diagnostics.into_diagnostics() }; let line_length = LineLength::try_from(8).unwrap(); assert_eq!(check_with_max_line_length(line_length), vec![]); diff --git a/crates/ruff_linter/src/pyproject_toml.rs b/crates/ruff_linter/src/pyproject_toml.rs index e60521797a573e..732c7e1ec65f11 100644 --- a/crates/ruff_linter/src/pyproject_toml.rs +++ b/crates/ruff_linter/src/pyproject_toml.rs @@ -5,10 +5,10 @@ use std::path::Path; use colored::Colorize; use log::warn; use pyproject_toml::PyProjectToml; -use ruff_python_ast::TomlSourceType; -use ruff_text_size::{TextRange, TextSize}; use ruff_db::diagnostic::Diagnostic; +use ruff_python_ast::TomlSourceType; +use ruff_text_size::{TextRange, TextSize}; use crate::IOError; use crate::Locator; @@ -26,7 +26,6 @@ pub struct TomlFixerResult<'a> { pub fixed: FixTable, } -/// RUF200 pub fn lint_toml( path: &Path, source: &str, @@ -34,11 +33,15 @@ pub fn lint_toml( source_type: TomlSourceType, ) -> Vec { let context = LintContext::new(path, source, settings); - rule_codes_in_selectors(&context, source_type); + if context.is_rule_enabled(Rule::RuleCodesInSelectors) { + rule_codes_in_selectors(&context, source_type); + } + + // RUF200 if context.is_rule_enabled(Rule::InvalidPyprojectToml) && source_type.is_pyproject() { let Some(err) = toml::from_str::(source).err() else { - return context.into_parts().0; + return context.into_diagnostics(); }; let range = match err.span() { @@ -47,7 +50,7 @@ pub fn lint_toml( None => TextRange::default(), Some(range) => { let Some(range) = text_range_from_std(range, &context) else { - return context.into_parts().0; + return context.into_diagnostics(); }; range } @@ -57,11 +60,10 @@ pub fn lint_toml( context.report_diagnostic(InvalidPyprojectToml { message: toml_err }, range); } - context.into_parts().0 + context.into_diagnostics() } -/// Generate diagnostics for a TOML configuration file, iteratively applying fixes until the source -/// stabilizes. +/// Generate [`Diagnostic`]s for a TOML configuration file, iteratively fixing until stable. pub fn lint_fix_toml<'a>( path: &Path, source: &'a str, diff --git a/crates/ruff_linter/src/rules/ruff/rules/rule_codes_in_selectors.rs b/crates/ruff_linter/src/rules/ruff/rules/rule_codes_in_selectors.rs index c117a977bb485d..322c66df1ef079 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/rule_codes_in_selectors.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/rule_codes_in_selectors.rs @@ -10,7 +10,7 @@ use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::TomlSourceType; use crate::{ - FixAvailability, Violation, checkers::ast::LintContext, codes::Rule, + AlwaysFixableViolation, checkers::ast::LintContext, codes::Rule, preview::is_human_readable_names_enabled, pyproject_toml::text_range_from_std, rule_redirects::get_redirect_target, }; @@ -43,34 +43,31 @@ pub(crate) struct RuleCodesInSelectors { selector: &'static str, } -impl Violation for RuleCodesInSelectors { - const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes; - +impl AlwaysFixableViolation for RuleCodesInSelectors { #[derive_message_formats] fn message(&self) -> String { let Self { selector } = self; format!("Rule code used instead of name in `{selector}`") } - fn fix_title(&self) -> Option { - Some("Replace rule code with name".to_string()) + fn fix_title(&self) -> String { + "Replace rule code with name".to_string() } } /// RUF201 pub(crate) fn rule_codes_in_selectors(context: &LintContext, source_type: TomlSourceType) { - if !context.is_rule_enabled(Rule::RuleCodesInSelectors) - || !is_human_readable_names_enabled(context.settings().preview) - { + if !is_human_readable_names_enabled(context.settings().preview) { return; } let source_file = context.source_file(); - let Ok(Some(document)) = Document::from_toml_str(source_file.source_text(), source_type) else { + let Ok(Some(config_file)) = ConfigFile::from_toml_str(source_file.source_text(), source_type) + else { return; }; - for (spanned, selector) in document.selectors() { + for (spanned, selector) in config_file.selectors() { let code = spanned.get_ref(); let code = get_redirect_target(code).unwrap_or(code); let Ok(rule) = Rule::from_code(code) else { @@ -90,20 +87,21 @@ pub(crate) fn rule_codes_in_selectors(context: &LintContext, source_type: TomlSo return; }; - let mut diagnostic = context.report_diagnostic(RuleCodesInSelectors { selector }, range); - diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement( - rule.name().to_string(), - range, - ))); + context + .report_diagnostic(RuleCodesInSelectors { selector }, range) + .set_fix(Fix::safe_edit(Edit::range_replacement( + rule.name().to_string(), + range, + ))); } } -enum Document { +enum ConfigFile { Pyproject(Pyproject), Ruff(Ruff), } -impl Document { +impl ConfigFile { fn from_toml_str( source: &str, source_type: TomlSourceType, @@ -117,8 +115,8 @@ impl Document { fn selectors(&self) -> impl Iterator, &'static str)> { let ruff = match self { - Document::Pyproject(pyproject) => &pyproject.tool.ruff, - Document::Ruff(ruff) => ruff, + ConfigFile::Pyproject(pyproject) => &pyproject.tool.ruff, + ConfigFile::Ruff(ruff) => ruff, }; ruff.select .iter() From 046b94ae3ce55bf36f2f1fe287e011a0a22d0ed5 Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Wed, 15 Jul 2026 09:56:15 -0400 Subject: [PATCH 06/17] pyproject_toml -> toml https://github.com/astral-sh/ruff/pull/26772#discussion_r3576626267 --- crates/ruff/src/diagnostics.rs | 2 +- crates/ruff_linter/src/lib.rs | 2 +- crates/ruff_linter/src/test.rs | 2 +- crates/ruff_linter/src/{pyproject_toml.rs => toml.rs} | 0 crates/ruff_mdtest/src/lib.rs | 2 +- 5 files changed, 4 insertions(+), 4 deletions(-) rename crates/ruff_linter/src/{pyproject_toml.rs => toml.rs} (100%) diff --git a/crates/ruff/src/diagnostics.rs b/crates/ruff/src/diagnostics.rs index f1b8bc14d4148c..1cd66a4bdf0e0a 100644 --- a/crates/ruff/src/diagnostics.rs +++ b/crates/ruff/src/diagnostics.rs @@ -14,10 +14,10 @@ use ruff_db::diagnostic::Diagnostic; use ruff_linter::codes::Rule; use ruff_linter::linter::{FixTable, FixerResult, LinterResult, ParseSource, lint_fix, lint_only}; use ruff_linter::package::PackageRoot; -use ruff_linter::pyproject_toml::{TomlFixerResult, lint_fix_toml, lint_toml}; use ruff_linter::settings::types::UnsafeFixes; use ruff_linter::settings::{LinterSettings, flags}; use ruff_linter::source_kind::{SourceError, SourceKind, SourceKindDiff}; +use ruff_linter::toml::{TomlFixerResult, lint_fix_toml, lint_toml}; use ruff_linter::{IOError, Violation, fs}; use ruff_notebook::{NotebookError, NotebookIndex}; use ruff_python_ast::{SourceType, TomlSourceType}; diff --git a/crates/ruff_linter/src/lib.rs b/crates/ruff_linter/src/lib.rs index 4a59b41a926051..f971c43b4d69da 100644 --- a/crates/ruff_linter/src/lib.rs +++ b/crates/ruff_linter/src/lib.rs @@ -38,7 +38,6 @@ mod noqa; pub mod package; pub mod packaging; pub mod preview; -pub mod pyproject_toml; pub mod registry; mod renamer; mod rule_redirects; @@ -48,6 +47,7 @@ pub mod settings; pub mod source_kind; pub mod suppression; mod text_helpers; +pub mod toml; pub mod upstream_categories; mod violation; diff --git a/crates/ruff_linter/src/test.rs b/crates/ruff_linter/src/test.rs index 8908392eb53271..d48155c72827b4 100644 --- a/crates/ruff_linter/src/test.rs +++ b/crates/ruff_linter/src/test.rs @@ -144,7 +144,7 @@ pub(crate) fn test_toml_path( let path = test_resource_path("fixtures").join(path); let filename = path.file_name().unwrap_or_else(|| path.as_os_str()); let contents = std::fs::read_to_string(&path)?; - Ok(crate::pyproject_toml::lint_toml( + Ok(crate::toml::lint_toml( Path::new(filename), &contents, settings, diff --git a/crates/ruff_linter/src/pyproject_toml.rs b/crates/ruff_linter/src/toml.rs similarity index 100% rename from crates/ruff_linter/src/pyproject_toml.rs rename to crates/ruff_linter/src/toml.rs diff --git a/crates/ruff_mdtest/src/lib.rs b/crates/ruff_mdtest/src/lib.rs index 4884272ef9c0dd..c8c9fa67a40848 100644 --- a/crates/ruff_mdtest/src/lib.rs +++ b/crates/ruff_mdtest/src/lib.rs @@ -10,9 +10,9 @@ use ruff_db::diagnostic::{Annotation, Diagnostic, Span}; use ruff_db::files::{File, system_path_to_file}; use ruff_db::source::source_text; use ruff_db::system::{DbWithWritableSystem as _, SystemPathBuf}; -use ruff_linter::pyproject_toml::lint_toml; use ruff_linter::source_kind::SourceKind; use ruff_linter::test::test_contents; +use ruff_linter::toml::lint_toml; use ruff_python_ast::SourceType; use ruff_ranged_value::{ValueSource, ValueSourceGuard}; use ruff_workspace::configuration::Configuration; From 0388bfce89368ac65d290877073967ff3e025e5a Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Wed, 15 Jul 2026 10:13:28 -0400 Subject: [PATCH 07/17] drop ruff.toml and .ruff.toml from stable includes https://github.com/astral-sh/ruff/pull/26772#discussion_r3576684942 update stable include snapshots --- crates/ruff/tests/cli/lint.rs | 1 + .../cli__lint__requires_python_extend_from_shared_config.snap | 2 -- .../tests/cli/snapshots/cli__lint__requires_python_no_tool.snap | 2 -- ...__lint__requires_python_no_tool_target_version_override.snap | 2 -- .../cli__lint__requires_python_pyproject_toml_above.snap | 2 -- ...i__lint__requires_python_pyproject_toml_above_with_tool.snap | 2 -- .../snapshots/cli__lint__requires_python_ruff_toml_above-2.snap | 2 -- .../snapshots/cli__lint__requires_python_ruff_toml_above.snap | 2 -- ...cli__lint__requires_python_ruff_toml_no_target_fallback.snap | 2 -- .../snapshots/cli__show_settings__display_default_settings.snap | 2 -- ...__show_settings__display_settings_from_nested_directory.snap | 2 -- crates/ruff_workspace/src/settings.rs | 2 -- 12 files changed, 1 insertion(+), 22 deletions(-) diff --git a/crates/ruff/tests/cli/lint.rs b/crates/ruff/tests/cli/lint.rs index d10a4849ebbbd3..35784e86201e3e 100644 --- a/crates/ruff/tests/cli/lint.rs +++ b/crates/ruff/tests/cli/lint.rs @@ -468,6 +468,7 @@ ignore = ["D203", "D212"] All checks passed! ----- stderr ----- + warning: No Python files found under the given path(s) "); Ok(()) diff --git a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_extend_from_shared_config.snap b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_extend_from_shared_config.snap index b4a34f4c7bed98..f5a44bbbd8779d 100644 --- a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_extend_from_shared_config.snap +++ b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_extend_from_shared_config.snap @@ -59,8 +59,6 @@ file_resolver.include = [ "*.pyi", "*.ipynb", "**/pyproject.toml", - "**/ruff.toml", - "**/.ruff.toml", ] file_resolver.extend_include = [] file_resolver.respect_gitignore = true diff --git a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool.snap b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool.snap index fafd0ded8c0923..322b41688ed6d2 100644 --- a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool.snap +++ b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool.snap @@ -61,8 +61,6 @@ file_resolver.include = [ "*.pyi", "*.ipynb", "**/pyproject.toml", - "**/ruff.toml", - "**/.ruff.toml", ] file_resolver.extend_include = [] file_resolver.respect_gitignore = true diff --git a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool_target_version_override.snap b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool_target_version_override.snap index 48c3d81fed3e86..0dadf95bea3802 100644 --- a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool_target_version_override.snap +++ b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool_target_version_override.snap @@ -63,8 +63,6 @@ file_resolver.include = [ "*.pyi", "*.ipynb", "**/pyproject.toml", - "**/ruff.toml", - "**/.ruff.toml", ] file_resolver.extend_include = [] file_resolver.respect_gitignore = true diff --git a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_pyproject_toml_above.snap b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_pyproject_toml_above.snap index 99f233b5093f00..140513dc935dee 100644 --- a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_pyproject_toml_above.snap +++ b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_pyproject_toml_above.snap @@ -60,8 +60,6 @@ file_resolver.include = [ "*.pyi", "*.ipynb", "**/pyproject.toml", - "**/ruff.toml", - "**/.ruff.toml", ] file_resolver.extend_include = [] file_resolver.respect_gitignore = true diff --git a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_pyproject_toml_above_with_tool.snap b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_pyproject_toml_above_with_tool.snap index a9727552e04821..8cc415c4967749 100644 --- a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_pyproject_toml_above_with_tool.snap +++ b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_pyproject_toml_above_with_tool.snap @@ -61,8 +61,6 @@ file_resolver.include = [ "*.pyi", "*.ipynb", "**/pyproject.toml", - "**/ruff.toml", - "**/.ruff.toml", ] file_resolver.extend_include = [] file_resolver.respect_gitignore = true diff --git a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_above-2.snap b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_above-2.snap index 72982b964975a3..73ceea46bb3884 100644 --- a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_above-2.snap +++ b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_above-2.snap @@ -59,8 +59,6 @@ file_resolver.include = [ "*.pyi", "*.ipynb", "**/pyproject.toml", - "**/ruff.toml", - "**/.ruff.toml", ] file_resolver.extend_include = [] file_resolver.respect_gitignore = true diff --git a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_above.snap b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_above.snap index 7a50948dfc7252..f8136b2d971f59 100644 --- a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_above.snap +++ b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_above.snap @@ -59,8 +59,6 @@ file_resolver.include = [ "*.pyi", "*.ipynb", "**/pyproject.toml", - "**/ruff.toml", - "**/.ruff.toml", ] file_resolver.extend_include = [] file_resolver.respect_gitignore = true diff --git a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_no_target_fallback.snap b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_no_target_fallback.snap index 60137f66c8575e..d281c8f869c4a0 100644 --- a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_no_target_fallback.snap +++ b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_no_target_fallback.snap @@ -59,8 +59,6 @@ file_resolver.include = [ "*.pyi", "*.ipynb", "**/pyproject.toml", - "**/ruff.toml", - "**/.ruff.toml", ] file_resolver.extend_include = [] file_resolver.respect_gitignore = true diff --git a/crates/ruff/tests/cli/snapshots/cli__show_settings__display_default_settings.snap b/crates/ruff/tests/cli/snapshots/cli__show_settings__display_default_settings.snap index 882ccb119a13ba..792399f068ed64 100644 --- a/crates/ruff/tests/cli/snapshots/cli__show_settings__display_default_settings.snap +++ b/crates/ruff/tests/cli/snapshots/cli__show_settings__display_default_settings.snap @@ -56,8 +56,6 @@ file_resolver.include = [ "*.pyi", "*.ipynb", "**/pyproject.toml", - "**/ruff.toml", - "**/.ruff.toml", ] file_resolver.extend_include = [] file_resolver.respect_gitignore = true diff --git a/crates/ruff/tests/cli/snapshots/cli__show_settings__display_settings_from_nested_directory.snap b/crates/ruff/tests/cli/snapshots/cli__show_settings__display_settings_from_nested_directory.snap index f155e549bc0a34..ff29f68590cbde 100644 --- a/crates/ruff/tests/cli/snapshots/cli__show_settings__display_settings_from_nested_directory.snap +++ b/crates/ruff/tests/cli/snapshots/cli__show_settings__display_settings_from_nested_directory.snap @@ -56,8 +56,6 @@ file_resolver.include = [ "*.pyi", "*.ipynb", "**/pyproject.toml", - "**/ruff.toml", - "**/.ruff.toml", ] file_resolver.extend_include = [] file_resolver.respect_gitignore = true diff --git a/crates/ruff_workspace/src/settings.rs b/crates/ruff_workspace/src/settings.rs index a1c3dd4af225a9..4e8c113465dc45 100644 --- a/crates/ruff_workspace/src/settings.rs +++ b/crates/ruff_workspace/src/settings.rs @@ -143,8 +143,6 @@ pub(crate) static INCLUDE: &[FilePattern] = &[ FilePattern::Builtin("*.pyi"), FilePattern::Builtin("*.ipynb"), FilePattern::Builtin("**/pyproject.toml"), - FilePattern::Builtin("**/ruff.toml"), - FilePattern::Builtin("**/.ruff.toml"), ]; pub(crate) static INCLUDE_PREVIEW: &[FilePattern] = &[ FilePattern::Builtin("*.py"), From c5dea35e2870f895a98faa2f3ecbacb1fe846026 Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Wed, 15 Jul 2026 10:38:46 -0400 Subject: [PATCH 08/17] move to mdtests --- crates/ruff/tests/cli/lint.rs | 103 ----- .../mdtest/ruff/rule-codes-in-selectors.md | 183 ++++++++ .../rule_codes_in_selectors/pyproject.toml | 35 -- .../ruff/rule_codes_in_selectors/ruff.toml | 34 -- crates/ruff_linter/src/rules/ruff/mod.rs | 13 - ...s__ruff__tests__RUF201_pyproject.toml.snap | 431 ------------------ ..._rules__ruff__tests__RUF201_ruff.toml.snap | 430 ----------------- crates/ruff_linter/src/toml.rs | 69 --- 8 files changed, 183 insertions(+), 1115 deletions(-) create mode 100644 crates/ruff_linter/resources/mdtest/ruff/rule-codes-in-selectors.md delete mode 100644 crates/ruff_linter/resources/test/fixtures/ruff/rule_codes_in_selectors/pyproject.toml delete mode 100644 crates/ruff_linter/resources/test/fixtures/ruff/rule_codes_in_selectors/ruff.toml delete mode 100644 crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF201_pyproject.toml.snap delete mode 100644 crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF201_ruff.toml.snap diff --git a/crates/ruff/tests/cli/lint.rs b/crates/ruff/tests/cli/lint.rs index 35784e86201e3e..8506f85f21d457 100644 --- a/crates/ruff/tests/cli/lint.rs +++ b/crates/ruff/tests/cli/lint.rs @@ -5231,106 +5231,3 @@ fn ruff_toml_is_linted() -> Result<()> { Ok(()) } - -#[test] -fn ruff_toml_respects_unfixable() -> Result<()> { - let test = CliTest::with_file("ruff.toml", r#"lint.select = ["F401"]"#)?; - - assert_cmd_snapshot!( - test.command().args([ - "check", - "--no-cache", - "--isolated", - "--preview", - "--select", - "RUF201", - "--unfixable", - "RUF201", - "--fix", - ]), - @r#" - success: false - exit_code: 1 - ----- stdout ----- - rule-codes-in-selectors: Rule code used instead of name in `lint.select` - --> ruff.toml:1:17 - | - 1 | lint.select = ["F401"] - | ^^^^ - | - help: Replace rule code with name - - Found 1 error. - - ----- stderr ----- - "# - ); - - Ok(()) -} - -#[test] -fn ruff_toml_respects_fix_safety_overrides() -> Result<()> { - let test = CliTest::with_file("ruff.toml", r#"lint.select = ["F401"]"#)?; - - assert_cmd_snapshot!( - test.command().args([ - "check", - "--no-cache", - "--isolated", - "--preview", - "--select", - "RUF201", - "--config", - r#"lint.extend-unsafe-fixes=["RUF201"]"#, - "--fix", - ]), - @r#" - success: false - exit_code: 1 - ----- stdout ----- - rule-codes-in-selectors: Rule code used instead of name in `lint.select` - --> ruff.toml:1:17 - | - 1 | lint.select = ["F401"] - | ^^^^ - | - help: Replace rule code with name - - Found 1 error. - No fixes available (1 hidden fix can be enabled with the `--unsafe-fixes` option). - - ----- stderr ----- - "# - ); - - Ok(()) -} - -#[test] -fn ruff_toml_respects_per_file_ignores() -> Result<()> { - let test = CliTest::with_file("ruff.toml", r#"lint.select = ["F401"]"#)?; - - assert_cmd_snapshot!( - test.command().args([ - "check", - "--no-cache", - "--isolated", - "--preview", - "--select", - "RUF201", - "--per-file-ignores", - "ruff.toml:RUF201", - ]), - @" - success: true - exit_code: 0 - ----- stdout ----- - All checks passed! - - ----- stderr ----- - " - ); - - Ok(()) -} diff --git a/crates/ruff_linter/resources/mdtest/ruff/rule-codes-in-selectors.md b/crates/ruff_linter/resources/mdtest/ruff/rule-codes-in-selectors.md new file mode 100644 index 00000000000000..13d116f7b72dc0 --- /dev/null +++ b/crates/ruff_linter/resources/mdtest/ruff/rule-codes-in-selectors.md @@ -0,0 +1,183 @@ +# `rule-codes-in-selectors` (`RUF201`) + +```toml +[lint] +preview = true +select = ["rule-codes-in-selectors"] +``` + +## Various quotes + +`ruff.toml`: + +```toml +[lint] +select = [ + "F401", # snapshot: rule-codes-in-selectors + 'F402', # snapshot: rule-codes-in-selectors + """F403""", # snapshot: rule-codes-in-selectors + '''F404''', # snapshot: rule-codes-in-selectors +] +``` + +```snapshot +error[RUF201]: Rule code used instead of name in `lint.select` + --> src/ruff.toml:3:6 + | +3 | "F401", # snapshot: rule-codes-in-selectors + | ^^^^ + | +help: Replace rule code with name + | +2 | select = [ + - "F401", # snapshot: rule-codes-in-selectors +3 + "unused-import", # snapshot: rule-codes-in-selectors +4 | 'F402', # snapshot: rule-codes-in-selectors + | + + +error[RUF201]: Rule code used instead of name in `lint.select` + --> src/ruff.toml:4:6 + | +4 | 'F402', # snapshot: rule-codes-in-selectors + | ^^^^ + | +help: Replace rule code with name + | +3 | "F401", # snapshot: rule-codes-in-selectors + - 'F402', # snapshot: rule-codes-in-selectors +4 + 'import-shadowed-by-loop-var', # snapshot: rule-codes-in-selectors +5 | """F403""", # snapshot: rule-codes-in-selectors + | + + +error[RUF201]: Rule code used instead of name in `lint.select` + --> src/ruff.toml:5:8 + | +5 | """F403""", # snapshot: rule-codes-in-selectors + | ^^^^ + | +help: Replace rule code with name + | +4 | 'F402', # snapshot: rule-codes-in-selectors + - """F403""", # snapshot: rule-codes-in-selectors +5 + """undefined-local-with-import-star""", # snapshot: rule-codes-in-selectors +6 | '''F404''', # snapshot: rule-codes-in-selectors + | + + +error[RUF201]: Rule code used instead of name in `lint.select` + --> src/ruff.toml:6:8 + | +6 | '''F404''', # snapshot: rule-codes-in-selectors + | ^^^^ + | +help: Replace rule code with name + | +5 | """F403""", # snapshot: rule-codes-in-selectors + - '''F404''', # snapshot: rule-codes-in-selectors +6 + '''late-future-import''', # snapshot: rule-codes-in-selectors +7 | ] + | +``` + +## Invalid rule codes + +Invalid rule codes are not flagged, including nested quoting issues like `"'F401'"`: + +`ruff.toml`: + +```toml +[lint] +select = ["'F401'"] +``` + +## Prefixes and names + +Prefixes and rule names are also left alone: + +`ruff.toml`: + +```toml +[lint] +select = ["F", "unused-import"] +``` + +## All selectors + +Test that we flag all selectors both in the `lint` table and in the deprecated top-level settings: + +`ruff.toml`: + +```toml +select = ["F401"] # error: [rule-codes-in-selectors] +extend-select = ["F841"] # error: [rule-codes-in-selectors] +fixable = ["E501"] # error: [rule-codes-in-selectors] +extend-fixable = ["UP035"] # error: [rule-codes-in-selectors] +ignore = ["F401"] # error: [rule-codes-in-selectors] +extend-ignore = ["F841"] # error: [rule-codes-in-selectors] +per-file-ignores = { "foo.py" = ["E501"] } # error: [rule-codes-in-selectors] +extend-per-file-ignores = { "bar.py" = ["UP035"] } # error: [rule-codes-in-selectors] +unfixable = ["F401"] # error: [rule-codes-in-selectors] +extend-unfixable = ["F841"] # error: [rule-codes-in-selectors] +extend-safe-fixes = ["E501"] # error: [rule-codes-in-selectors] +extend-unsafe-fixes = ["UP035"] # error: [rule-codes-in-selectors] + +[lint] +select = ["F401"] # error: [rule-codes-in-selectors] +extend-select = ["F841"] # error: [rule-codes-in-selectors] +fixable = ["E501"] # error: [rule-codes-in-selectors] +extend-fixable = ["UP035"] # error: [rule-codes-in-selectors] +ignore = ["F401"] # error: [rule-codes-in-selectors] +extend-ignore = ["F841"] # error: [rule-codes-in-selectors] +per-file-ignores = { "foo.py" = ["E501"] } # error: [rule-codes-in-selectors] +extend-per-file-ignores = { "bar.py" = ["UP035"] } # error: [rule-codes-in-selectors] +unfixable = ["F401"] # error: [rule-codes-in-selectors] +extend-unfixable = ["F841"] # error: [rule-codes-in-selectors] +extend-safe-fixes = ["E501"] # error: [rule-codes-in-selectors] +extend-unsafe-fixes = ["UP035"] # error: [rule-codes-in-selectors] +``` + +## `pyproject.toml` + +`pyproject.toml`: + +```toml +[tool.ruff] +ignore = ["F401"] # error: [rule-codes-in-selectors] + +[tool.ruff.lint] +select = ["F402"] # error: [rule-codes-in-selectors] +``` + +## `unfixable` + +Test that `rule-codes-in-selectors` and other TOML-specific lints respect the user's `unfixable` +settings: + +```toml +[lint] +preview = true +select = ["rule-codes-in-selectors"] +unfixable = ["rule-codes-in-selectors"] +``` + +`ruff.toml`: + +```toml +# snapshot: rule-codes-in-selectors +lint.select = ["F401"] +``` + +```snapshot +error[RUF201]: Rule code used instead of name in `lint.select` + --> src/ruff.toml:2:17 + | +2 | lint.select = ["F401"] + | ^^^^ + | +help: Replace rule code with name +``` + +This should also cover settings like `extend-unsafe-fixes` and `per-file-ignores`, all of which are +handled through the `LintContext`. diff --git a/crates/ruff_linter/resources/test/fixtures/ruff/rule_codes_in_selectors/pyproject.toml b/crates/ruff_linter/resources/test/fixtures/ruff/rule_codes_in_selectors/pyproject.toml deleted file mode 100644 index fd3fa4ea336f31..00000000000000 --- a/crates/ruff_linter/resources/test/fixtures/ruff/rule_codes_in_selectors/pyproject.toml +++ /dev/null @@ -1,35 +0,0 @@ -[tool.ruff] -select = [ - "F401", - "unused-import", - "F", -] -extend-select = ['F841'] -fixable = ["""E501"""] -extend-fixable = ['''UP035'''] -ignore = ["F401"] -extend-ignore = ["F841"] -per-file-ignores = { "foo.py" = ["E501"] } -extend-per-file-ignores = { "bar.py" = ["UP035"] } -unfixable = ["F401"] -extend-unfixable = ["F841"] -extend-safe-fixes = ["E501"] -extend-unsafe-fixes = ["UP035"] - -[tool.ruff.lint] -select = [ - "F401", - "unused-import", - "unknown-rule", -] -extend-select = ['F841'] -fixable = ["""E501"""] -extend-fixable = ['''UP035'''] -ignore = ["F401"] -extend-ignore = ["F841"] -per-file-ignores = { "foo.py" = ["E501"] } -extend-per-file-ignores = { "bar.py" = ["UP035"] } -unfixable = ["F401"] -extend-unfixable = ["F841"] -extend-safe-fixes = ["E501"] -extend-unsafe-fixes = ["UP035"] diff --git a/crates/ruff_linter/resources/test/fixtures/ruff/rule_codes_in_selectors/ruff.toml b/crates/ruff_linter/resources/test/fixtures/ruff/rule_codes_in_selectors/ruff.toml deleted file mode 100644 index c319305f1bcc42..00000000000000 --- a/crates/ruff_linter/resources/test/fixtures/ruff/rule_codes_in_selectors/ruff.toml +++ /dev/null @@ -1,34 +0,0 @@ -select = [ - "F401", - "unused-import", - "F", -] -extend-select = ['F841'] -fixable = ["""E501"""] -extend-fixable = ['''UP035'''] -ignore = ["F401"] -extend-ignore = ["F841"] -per-file-ignores = { "foo.py" = ["E501"] } -extend-per-file-ignores = { "bar.py" = ["UP035"] } -unfixable = ["F401"] -extend-unfixable = ["F841"] -extend-safe-fixes = ["E501"] -extend-unsafe-fixes = ["UP035"] - -[lint] -select = [ - "F401", - "unused-import", - "unknown-rule", -] -extend-select = ['F841'] -fixable = ["""E501"""] -extend-fixable = ['''UP035'''] -ignore = ["F401"] -extend-ignore = ["F841"] -per-file-ignores = { "foo.py" = ["E501"] } -extend-per-file-ignores = { "bar.py" = ["UP035"] } -unfixable = ["F401"] -extend-unfixable = ["F841"] -extend-safe-fixes = ["E501"] -extend-unsafe-fixes = ["UP035"] diff --git a/crates/ruff_linter/src/rules/ruff/mod.rs b/crates/ruff_linter/src/rules/ruff/mod.rs index e0bf6f7365aca7..4252591fd153e8 100644 --- a/crates/ruff_linter/src/rules/ruff/mod.rs +++ b/crates/ruff_linter/src/rules/ruff/mod.rs @@ -797,19 +797,6 @@ mod tests { Ok(()) } - #[test_case(Path::new("pyproject.toml"), TomlSourceType::Pyproject)] - #[test_case(Path::new("ruff.toml"), TomlSourceType::Ruff)] - fn rule_codes_in_selectors(path: &Path, source_type: TomlSourceType) -> Result<()> { - let root = Path::new("ruff/rule_codes_in_selectors"); - let diagnostics = test_toml_path( - root.join(path), - &LinterSettings::for_rule(Rule::RuleCodesInSelectors).with_preview_mode(), - source_type, - )?; - assert_diagnostics!(format!("RUF201_{}", path.display()), diagnostics); - Ok(()) - } - #[test_case(Rule::MutableDataclassDefault, Path::new("RUF008.py"))] #[test_case(Rule::MutableDataclassDefault, Path::new("RUF008_attrs.py"))] #[test_case(Rule::UnrawRePattern, Path::new("RUF039.py"))] diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF201_pyproject.toml.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF201_pyproject.toml.snap deleted file mode 100644 index 67702fa5208626..00000000000000 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF201_pyproject.toml.snap +++ /dev/null @@ -1,431 +0,0 @@ ---- -source: crates/ruff_linter/src/rules/ruff/mod.rs ---- -RUF201 [*] Rule code used instead of name in `select` - --> pyproject.toml:3:6 - | -1 | [tool.ruff] -2 | select = [ -3 | "F401", - | ^^^^ -4 | "unused-import", -5 | "F", - | -help: Replace rule code with name - | -2 | select = [ - - "F401", -3 | "unused-import", -4 + "unused-import", -5 | "F", - | - -RUF201 [*] Rule code used instead of name in `extend-select` - --> pyproject.toml:7:19 - | -5 | "F", -6 | ] -7 | extend-select = ['F841'] - | ^^^^ -8 | fixable = ["""E501"""] -9 | extend-fixable = ['''UP035'''] - | -help: Replace rule code with name - | -6 | ] - - extend-select = ['F841'] -7 + extend-select = ['unused-variable'] -8 | fixable = ["""E501"""] - | - -RUF201 [*] Rule code used instead of name in `fixable` - --> pyproject.toml:8:15 - | - 6 | ] - 7 | extend-select = ['F841'] - 8 | fixable = ["""E501"""] - | ^^^^ - 9 | extend-fixable = ['''UP035'''] -10 | ignore = ["F401"] - | -help: Replace rule code with name - | -7 | extend-select = ['F841'] - - fixable = ["""E501"""] -8 + fixable = ["""line-too-long"""] -9 | extend-fixable = ['''UP035'''] - | - -RUF201 [*] Rule code used instead of name in `extend-fixable` - --> pyproject.toml:9:22 - | - 7 | extend-select = ['F841'] - 8 | fixable = ["""E501"""] - 9 | extend-fixable = ['''UP035'''] - | ^^^^^ -10 | ignore = ["F401"] -11 | extend-ignore = ["F841"] - | -help: Replace rule code with name - | -8 | fixable = ["""E501"""] - - extend-fixable = ['''UP035'''] -9 + extend-fixable = ['''deprecated-import'''] -10 | ignore = ["F401"] - | - -RUF201 [*] Rule code used instead of name in `ignore` - --> pyproject.toml:10:12 - | - 8 | fixable = ["""E501"""] - 9 | extend-fixable = ['''UP035'''] -10 | ignore = ["F401"] - | ^^^^ -11 | extend-ignore = ["F841"] -12 | per-file-ignores = { "foo.py" = ["E501"] } - | -help: Replace rule code with name - | -9 | extend-fixable = ['''UP035'''] - - ignore = ["F401"] -10 + ignore = ["unused-import"] -11 | extend-ignore = ["F841"] - | - -RUF201 [*] Rule code used instead of name in `extend-ignore` - --> pyproject.toml:11:19 - | - 9 | extend-fixable = ['''UP035'''] -10 | ignore = ["F401"] -11 | extend-ignore = ["F841"] - | ^^^^ -12 | per-file-ignores = { "foo.py" = ["E501"] } -13 | extend-per-file-ignores = { "bar.py" = ["UP035"] } - | -help: Replace rule code with name - | -10 | ignore = ["F401"] - - extend-ignore = ["F841"] -11 + extend-ignore = ["unused-variable"] -12 | per-file-ignores = { "foo.py" = ["E501"] } - | - -RUF201 [*] Rule code used instead of name in `per-file-ignores` - --> pyproject.toml:12:35 - | -10 | ignore = ["F401"] -11 | extend-ignore = ["F841"] -12 | per-file-ignores = { "foo.py" = ["E501"] } - | ^^^^ -13 | extend-per-file-ignores = { "bar.py" = ["UP035"] } -14 | unfixable = ["F401"] - | -help: Replace rule code with name - | -11 | extend-ignore = ["F841"] - - per-file-ignores = { "foo.py" = ["E501"] } -12 + per-file-ignores = { "foo.py" = ["line-too-long"] } -13 | extend-per-file-ignores = { "bar.py" = ["UP035"] } - | - -RUF201 [*] Rule code used instead of name in `extend-per-file-ignores` - --> pyproject.toml:13:42 - | -11 | extend-ignore = ["F841"] -12 | per-file-ignores = { "foo.py" = ["E501"] } -13 | extend-per-file-ignores = { "bar.py" = ["UP035"] } - | ^^^^^ -14 | unfixable = ["F401"] -15 | extend-unfixable = ["F841"] - | -help: Replace rule code with name - | -12 | per-file-ignores = { "foo.py" = ["E501"] } - - extend-per-file-ignores = { "bar.py" = ["UP035"] } -13 + extend-per-file-ignores = { "bar.py" = ["deprecated-import"] } -14 | unfixable = ["F401"] - | - -RUF201 [*] Rule code used instead of name in `unfixable` - --> pyproject.toml:14:15 - | -12 | per-file-ignores = { "foo.py" = ["E501"] } -13 | extend-per-file-ignores = { "bar.py" = ["UP035"] } -14 | unfixable = ["F401"] - | ^^^^ -15 | extend-unfixable = ["F841"] -16 | extend-safe-fixes = ["E501"] - | -help: Replace rule code with name - | -13 | extend-per-file-ignores = { "bar.py" = ["UP035"] } - - unfixable = ["F401"] -14 + unfixable = ["unused-import"] -15 | extend-unfixable = ["F841"] - | - -RUF201 [*] Rule code used instead of name in `extend-unfixable` - --> pyproject.toml:15:22 - | -13 | extend-per-file-ignores = { "bar.py" = ["UP035"] } -14 | unfixable = ["F401"] -15 | extend-unfixable = ["F841"] - | ^^^^ -16 | extend-safe-fixes = ["E501"] -17 | extend-unsafe-fixes = ["UP035"] - | -help: Replace rule code with name - | -14 | unfixable = ["F401"] - - extend-unfixable = ["F841"] -15 + extend-unfixable = ["unused-variable"] -16 | extend-safe-fixes = ["E501"] - | - -RUF201 [*] Rule code used instead of name in `extend-safe-fixes` - --> pyproject.toml:16:23 - | -14 | unfixable = ["F401"] -15 | extend-unfixable = ["F841"] -16 | extend-safe-fixes = ["E501"] - | ^^^^ -17 | extend-unsafe-fixes = ["UP035"] - | -help: Replace rule code with name - | -15 | extend-unfixable = ["F841"] - - extend-safe-fixes = ["E501"] -16 + extend-safe-fixes = ["line-too-long"] -17 | extend-unsafe-fixes = ["UP035"] - | - -RUF201 [*] Rule code used instead of name in `extend-unsafe-fixes` - --> pyproject.toml:17:25 - | -15 | extend-unfixable = ["F841"] -16 | extend-safe-fixes = ["E501"] -17 | extend-unsafe-fixes = ["UP035"] - | ^^^^^ -18 | -19 | [tool.ruff.lint] - | -help: Replace rule code with name - | -16 | extend-safe-fixes = ["E501"] - - extend-unsafe-fixes = ["UP035"] -17 + extend-unsafe-fixes = ["deprecated-import"] -18 | - | - -RUF201 [*] Rule code used instead of name in `lint.select` - --> pyproject.toml:21:6 - | -19 | [tool.ruff.lint] -20 | select = [ -21 | "F401", - | ^^^^ -22 | "unused-import", -23 | "unknown-rule", - | -help: Replace rule code with name - | -20 | select = [ - - "F401", -21 | "unused-import", -22 + "unused-import", -23 | "unknown-rule", - | - -RUF201 [*] Rule code used instead of name in `lint.extend-select` - --> pyproject.toml:25:19 - | -23 | "unknown-rule", -24 | ] -25 | extend-select = ['F841'] - | ^^^^ -26 | fixable = ["""E501"""] -27 | extend-fixable = ['''UP035'''] - | -help: Replace rule code with name - | -24 | ] - - extend-select = ['F841'] -25 + extend-select = ['unused-variable'] -26 | fixable = ["""E501"""] - | - -RUF201 [*] Rule code used instead of name in `lint.fixable` - --> pyproject.toml:26:15 - | -24 | ] -25 | extend-select = ['F841'] -26 | fixable = ["""E501"""] - | ^^^^ -27 | extend-fixable = ['''UP035'''] -28 | ignore = ["F401"] - | -help: Replace rule code with name - | -25 | extend-select = ['F841'] - - fixable = ["""E501"""] -26 + fixable = ["""line-too-long"""] -27 | extend-fixable = ['''UP035'''] - | - -RUF201 [*] Rule code used instead of name in `lint.extend-fixable` - --> pyproject.toml:27:22 - | -25 | extend-select = ['F841'] -26 | fixable = ["""E501"""] -27 | extend-fixable = ['''UP035'''] - | ^^^^^ -28 | ignore = ["F401"] -29 | extend-ignore = ["F841"] - | -help: Replace rule code with name - | -26 | fixable = ["""E501"""] - - extend-fixable = ['''UP035'''] -27 + extend-fixable = ['''deprecated-import'''] -28 | ignore = ["F401"] - | - -RUF201 [*] Rule code used instead of name in `lint.ignore` - --> pyproject.toml:28:12 - | -26 | fixable = ["""E501"""] -27 | extend-fixable = ['''UP035'''] -28 | ignore = ["F401"] - | ^^^^ -29 | extend-ignore = ["F841"] -30 | per-file-ignores = { "foo.py" = ["E501"] } - | -help: Replace rule code with name - | -27 | extend-fixable = ['''UP035'''] - - ignore = ["F401"] -28 + ignore = ["unused-import"] -29 | extend-ignore = ["F841"] - | - -RUF201 [*] Rule code used instead of name in `lint.extend-ignore` - --> pyproject.toml:29:19 - | -27 | extend-fixable = ['''UP035'''] -28 | ignore = ["F401"] -29 | extend-ignore = ["F841"] - | ^^^^ -30 | per-file-ignores = { "foo.py" = ["E501"] } -31 | extend-per-file-ignores = { "bar.py" = ["UP035"] } - | -help: Replace rule code with name - | -28 | ignore = ["F401"] - - extend-ignore = ["F841"] -29 + extend-ignore = ["unused-variable"] -30 | per-file-ignores = { "foo.py" = ["E501"] } - | - -RUF201 [*] Rule code used instead of name in `lint.per-file-ignores` - --> pyproject.toml:30:35 - | -28 | ignore = ["F401"] -29 | extend-ignore = ["F841"] -30 | per-file-ignores = { "foo.py" = ["E501"] } - | ^^^^ -31 | extend-per-file-ignores = { "bar.py" = ["UP035"] } -32 | unfixable = ["F401"] - | -help: Replace rule code with name - | -29 | extend-ignore = ["F841"] - - per-file-ignores = { "foo.py" = ["E501"] } -30 + per-file-ignores = { "foo.py" = ["line-too-long"] } -31 | extend-per-file-ignores = { "bar.py" = ["UP035"] } - | - -RUF201 [*] Rule code used instead of name in `lint.extend-per-file-ignores` - --> pyproject.toml:31:42 - | -29 | extend-ignore = ["F841"] -30 | per-file-ignores = { "foo.py" = ["E501"] } -31 | extend-per-file-ignores = { "bar.py" = ["UP035"] } - | ^^^^^ -32 | unfixable = ["F401"] -33 | extend-unfixable = ["F841"] - | -help: Replace rule code with name - | -30 | per-file-ignores = { "foo.py" = ["E501"] } - - extend-per-file-ignores = { "bar.py" = ["UP035"] } -31 + extend-per-file-ignores = { "bar.py" = ["deprecated-import"] } -32 | unfixable = ["F401"] - | - -RUF201 [*] Rule code used instead of name in `lint.unfixable` - --> pyproject.toml:32:15 - | -30 | per-file-ignores = { "foo.py" = ["E501"] } -31 | extend-per-file-ignores = { "bar.py" = ["UP035"] } -32 | unfixable = ["F401"] - | ^^^^ -33 | extend-unfixable = ["F841"] -34 | extend-safe-fixes = ["E501"] - | -help: Replace rule code with name - | -31 | extend-per-file-ignores = { "bar.py" = ["UP035"] } - - unfixable = ["F401"] -32 + unfixable = ["unused-import"] -33 | extend-unfixable = ["F841"] - | - -RUF201 [*] Rule code used instead of name in `lint.extend-unfixable` - --> pyproject.toml:33:22 - | -31 | extend-per-file-ignores = { "bar.py" = ["UP035"] } -32 | unfixable = ["F401"] -33 | extend-unfixable = ["F841"] - | ^^^^ -34 | extend-safe-fixes = ["E501"] -35 | extend-unsafe-fixes = ["UP035"] - | -help: Replace rule code with name - | -32 | unfixable = ["F401"] - - extend-unfixable = ["F841"] -33 + extend-unfixable = ["unused-variable"] -34 | extend-safe-fixes = ["E501"] - | - -RUF201 [*] Rule code used instead of name in `lint.extend-safe-fixes` - --> pyproject.toml:34:23 - | -32 | unfixable = ["F401"] -33 | extend-unfixable = ["F841"] -34 | extend-safe-fixes = ["E501"] - | ^^^^ -35 | extend-unsafe-fixes = ["UP035"] - | -help: Replace rule code with name - | -33 | extend-unfixable = ["F841"] - - extend-safe-fixes = ["E501"] -34 + extend-safe-fixes = ["line-too-long"] -35 | extend-unsafe-fixes = ["UP035"] - | - -RUF201 [*] Rule code used instead of name in `lint.extend-unsafe-fixes` - --> pyproject.toml:35:25 - | -33 | extend-unfixable = ["F841"] -34 | extend-safe-fixes = ["E501"] -35 | extend-unsafe-fixes = ["UP035"] - | ^^^^^ - | -help: Replace rule code with name - | -34 | extend-safe-fixes = ["E501"] - - extend-unsafe-fixes = ["UP035"] -35 + extend-unsafe-fixes = ["deprecated-import"] - | diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF201_ruff.toml.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF201_ruff.toml.snap deleted file mode 100644 index 76a494b7cf9a35..00000000000000 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF201_ruff.toml.snap +++ /dev/null @@ -1,430 +0,0 @@ ---- -source: crates/ruff_linter/src/rules/ruff/mod.rs ---- -RUF201 [*] Rule code used instead of name in `select` - --> ruff.toml:2:6 - | -1 | select = [ -2 | "F401", - | ^^^^ -3 | "unused-import", -4 | "F", - | -help: Replace rule code with name - | -1 | select = [ - - "F401", -2 | "unused-import", -3 + "unused-import", -4 | "F", - | - -RUF201 [*] Rule code used instead of name in `extend-select` - --> ruff.toml:6:19 - | -4 | "F", -5 | ] -6 | extend-select = ['F841'] - | ^^^^ -7 | fixable = ["""E501"""] -8 | extend-fixable = ['''UP035'''] - | -help: Replace rule code with name - | -5 | ] - - extend-select = ['F841'] -6 + extend-select = ['unused-variable'] -7 | fixable = ["""E501"""] - | - -RUF201 [*] Rule code used instead of name in `fixable` - --> ruff.toml:7:15 - | -5 | ] -6 | extend-select = ['F841'] -7 | fixable = ["""E501"""] - | ^^^^ -8 | extend-fixable = ['''UP035'''] -9 | ignore = ["F401"] - | -help: Replace rule code with name - | -6 | extend-select = ['F841'] - - fixable = ["""E501"""] -7 + fixable = ["""line-too-long"""] -8 | extend-fixable = ['''UP035'''] - | - -RUF201 [*] Rule code used instead of name in `extend-fixable` - --> ruff.toml:8:22 - | - 6 | extend-select = ['F841'] - 7 | fixable = ["""E501"""] - 8 | extend-fixable = ['''UP035'''] - | ^^^^^ - 9 | ignore = ["F401"] -10 | extend-ignore = ["F841"] - | -help: Replace rule code with name - | -7 | fixable = ["""E501"""] - - extend-fixable = ['''UP035'''] -8 + extend-fixable = ['''deprecated-import'''] -9 | ignore = ["F401"] - | - -RUF201 [*] Rule code used instead of name in `ignore` - --> ruff.toml:9:12 - | - 7 | fixable = ["""E501"""] - 8 | extend-fixable = ['''UP035'''] - 9 | ignore = ["F401"] - | ^^^^ -10 | extend-ignore = ["F841"] -11 | per-file-ignores = { "foo.py" = ["E501"] } - | -help: Replace rule code with name - | -8 | extend-fixable = ['''UP035'''] - - ignore = ["F401"] -9 + ignore = ["unused-import"] -10 | extend-ignore = ["F841"] - | - -RUF201 [*] Rule code used instead of name in `extend-ignore` - --> ruff.toml:10:19 - | - 8 | extend-fixable = ['''UP035'''] - 9 | ignore = ["F401"] -10 | extend-ignore = ["F841"] - | ^^^^ -11 | per-file-ignores = { "foo.py" = ["E501"] } -12 | extend-per-file-ignores = { "bar.py" = ["UP035"] } - | -help: Replace rule code with name - | -9 | ignore = ["F401"] - - extend-ignore = ["F841"] -10 + extend-ignore = ["unused-variable"] -11 | per-file-ignores = { "foo.py" = ["E501"] } - | - -RUF201 [*] Rule code used instead of name in `per-file-ignores` - --> ruff.toml:11:35 - | - 9 | ignore = ["F401"] -10 | extend-ignore = ["F841"] -11 | per-file-ignores = { "foo.py" = ["E501"] } - | ^^^^ -12 | extend-per-file-ignores = { "bar.py" = ["UP035"] } -13 | unfixable = ["F401"] - | -help: Replace rule code with name - | -10 | extend-ignore = ["F841"] - - per-file-ignores = { "foo.py" = ["E501"] } -11 + per-file-ignores = { "foo.py" = ["line-too-long"] } -12 | extend-per-file-ignores = { "bar.py" = ["UP035"] } - | - -RUF201 [*] Rule code used instead of name in `extend-per-file-ignores` - --> ruff.toml:12:42 - | -10 | extend-ignore = ["F841"] -11 | per-file-ignores = { "foo.py" = ["E501"] } -12 | extend-per-file-ignores = { "bar.py" = ["UP035"] } - | ^^^^^ -13 | unfixable = ["F401"] -14 | extend-unfixable = ["F841"] - | -help: Replace rule code with name - | -11 | per-file-ignores = { "foo.py" = ["E501"] } - - extend-per-file-ignores = { "bar.py" = ["UP035"] } -12 + extend-per-file-ignores = { "bar.py" = ["deprecated-import"] } -13 | unfixable = ["F401"] - | - -RUF201 [*] Rule code used instead of name in `unfixable` - --> ruff.toml:13:15 - | -11 | per-file-ignores = { "foo.py" = ["E501"] } -12 | extend-per-file-ignores = { "bar.py" = ["UP035"] } -13 | unfixable = ["F401"] - | ^^^^ -14 | extend-unfixable = ["F841"] -15 | extend-safe-fixes = ["E501"] - | -help: Replace rule code with name - | -12 | extend-per-file-ignores = { "bar.py" = ["UP035"] } - - unfixable = ["F401"] -13 + unfixable = ["unused-import"] -14 | extend-unfixable = ["F841"] - | - -RUF201 [*] Rule code used instead of name in `extend-unfixable` - --> ruff.toml:14:22 - | -12 | extend-per-file-ignores = { "bar.py" = ["UP035"] } -13 | unfixable = ["F401"] -14 | extend-unfixable = ["F841"] - | ^^^^ -15 | extend-safe-fixes = ["E501"] -16 | extend-unsafe-fixes = ["UP035"] - | -help: Replace rule code with name - | -13 | unfixable = ["F401"] - - extend-unfixable = ["F841"] -14 + extend-unfixable = ["unused-variable"] -15 | extend-safe-fixes = ["E501"] - | - -RUF201 [*] Rule code used instead of name in `extend-safe-fixes` - --> ruff.toml:15:23 - | -13 | unfixable = ["F401"] -14 | extend-unfixable = ["F841"] -15 | extend-safe-fixes = ["E501"] - | ^^^^ -16 | extend-unsafe-fixes = ["UP035"] - | -help: Replace rule code with name - | -14 | extend-unfixable = ["F841"] - - extend-safe-fixes = ["E501"] -15 + extend-safe-fixes = ["line-too-long"] -16 | extend-unsafe-fixes = ["UP035"] - | - -RUF201 [*] Rule code used instead of name in `extend-unsafe-fixes` - --> ruff.toml:16:25 - | -14 | extend-unfixable = ["F841"] -15 | extend-safe-fixes = ["E501"] -16 | extend-unsafe-fixes = ["UP035"] - | ^^^^^ -17 | -18 | [lint] - | -help: Replace rule code with name - | -15 | extend-safe-fixes = ["E501"] - - extend-unsafe-fixes = ["UP035"] -16 + extend-unsafe-fixes = ["deprecated-import"] -17 | - | - -RUF201 [*] Rule code used instead of name in `lint.select` - --> ruff.toml:20:6 - | -18 | [lint] -19 | select = [ -20 | "F401", - | ^^^^ -21 | "unused-import", -22 | "unknown-rule", - | -help: Replace rule code with name - | -19 | select = [ - - "F401", -20 | "unused-import", -21 + "unused-import", -22 | "unknown-rule", - | - -RUF201 [*] Rule code used instead of name in `lint.extend-select` - --> ruff.toml:24:19 - | -22 | "unknown-rule", -23 | ] -24 | extend-select = ['F841'] - | ^^^^ -25 | fixable = ["""E501"""] -26 | extend-fixable = ['''UP035'''] - | -help: Replace rule code with name - | -23 | ] - - extend-select = ['F841'] -24 + extend-select = ['unused-variable'] -25 | fixable = ["""E501"""] - | - -RUF201 [*] Rule code used instead of name in `lint.fixable` - --> ruff.toml:25:15 - | -23 | ] -24 | extend-select = ['F841'] -25 | fixable = ["""E501"""] - | ^^^^ -26 | extend-fixable = ['''UP035'''] -27 | ignore = ["F401"] - | -help: Replace rule code with name - | -24 | extend-select = ['F841'] - - fixable = ["""E501"""] -25 + fixable = ["""line-too-long"""] -26 | extend-fixable = ['''UP035'''] - | - -RUF201 [*] Rule code used instead of name in `lint.extend-fixable` - --> ruff.toml:26:22 - | -24 | extend-select = ['F841'] -25 | fixable = ["""E501"""] -26 | extend-fixable = ['''UP035'''] - | ^^^^^ -27 | ignore = ["F401"] -28 | extend-ignore = ["F841"] - | -help: Replace rule code with name - | -25 | fixable = ["""E501"""] - - extend-fixable = ['''UP035'''] -26 + extend-fixable = ['''deprecated-import'''] -27 | ignore = ["F401"] - | - -RUF201 [*] Rule code used instead of name in `lint.ignore` - --> ruff.toml:27:12 - | -25 | fixable = ["""E501"""] -26 | extend-fixable = ['''UP035'''] -27 | ignore = ["F401"] - | ^^^^ -28 | extend-ignore = ["F841"] -29 | per-file-ignores = { "foo.py" = ["E501"] } - | -help: Replace rule code with name - | -26 | extend-fixable = ['''UP035'''] - - ignore = ["F401"] -27 + ignore = ["unused-import"] -28 | extend-ignore = ["F841"] - | - -RUF201 [*] Rule code used instead of name in `lint.extend-ignore` - --> ruff.toml:28:19 - | -26 | extend-fixable = ['''UP035'''] -27 | ignore = ["F401"] -28 | extend-ignore = ["F841"] - | ^^^^ -29 | per-file-ignores = { "foo.py" = ["E501"] } -30 | extend-per-file-ignores = { "bar.py" = ["UP035"] } - | -help: Replace rule code with name - | -27 | ignore = ["F401"] - - extend-ignore = ["F841"] -28 + extend-ignore = ["unused-variable"] -29 | per-file-ignores = { "foo.py" = ["E501"] } - | - -RUF201 [*] Rule code used instead of name in `lint.per-file-ignores` - --> ruff.toml:29:35 - | -27 | ignore = ["F401"] -28 | extend-ignore = ["F841"] -29 | per-file-ignores = { "foo.py" = ["E501"] } - | ^^^^ -30 | extend-per-file-ignores = { "bar.py" = ["UP035"] } -31 | unfixable = ["F401"] - | -help: Replace rule code with name - | -28 | extend-ignore = ["F841"] - - per-file-ignores = { "foo.py" = ["E501"] } -29 + per-file-ignores = { "foo.py" = ["line-too-long"] } -30 | extend-per-file-ignores = { "bar.py" = ["UP035"] } - | - -RUF201 [*] Rule code used instead of name in `lint.extend-per-file-ignores` - --> ruff.toml:30:42 - | -28 | extend-ignore = ["F841"] -29 | per-file-ignores = { "foo.py" = ["E501"] } -30 | extend-per-file-ignores = { "bar.py" = ["UP035"] } - | ^^^^^ -31 | unfixable = ["F401"] -32 | extend-unfixable = ["F841"] - | -help: Replace rule code with name - | -29 | per-file-ignores = { "foo.py" = ["E501"] } - - extend-per-file-ignores = { "bar.py" = ["UP035"] } -30 + extend-per-file-ignores = { "bar.py" = ["deprecated-import"] } -31 | unfixable = ["F401"] - | - -RUF201 [*] Rule code used instead of name in `lint.unfixable` - --> ruff.toml:31:15 - | -29 | per-file-ignores = { "foo.py" = ["E501"] } -30 | extend-per-file-ignores = { "bar.py" = ["UP035"] } -31 | unfixable = ["F401"] - | ^^^^ -32 | extend-unfixable = ["F841"] -33 | extend-safe-fixes = ["E501"] - | -help: Replace rule code with name - | -30 | extend-per-file-ignores = { "bar.py" = ["UP035"] } - - unfixable = ["F401"] -31 + unfixable = ["unused-import"] -32 | extend-unfixable = ["F841"] - | - -RUF201 [*] Rule code used instead of name in `lint.extend-unfixable` - --> ruff.toml:32:22 - | -30 | extend-per-file-ignores = { "bar.py" = ["UP035"] } -31 | unfixable = ["F401"] -32 | extend-unfixable = ["F841"] - | ^^^^ -33 | extend-safe-fixes = ["E501"] -34 | extend-unsafe-fixes = ["UP035"] - | -help: Replace rule code with name - | -31 | unfixable = ["F401"] - - extend-unfixable = ["F841"] -32 + extend-unfixable = ["unused-variable"] -33 | extend-safe-fixes = ["E501"] - | - -RUF201 [*] Rule code used instead of name in `lint.extend-safe-fixes` - --> ruff.toml:33:23 - | -31 | unfixable = ["F401"] -32 | extend-unfixable = ["F841"] -33 | extend-safe-fixes = ["E501"] - | ^^^^ -34 | extend-unsafe-fixes = ["UP035"] - | -help: Replace rule code with name - | -32 | extend-unfixable = ["F841"] - - extend-safe-fixes = ["E501"] -33 + extend-safe-fixes = ["line-too-long"] -34 | extend-unsafe-fixes = ["UP035"] - | - -RUF201 [*] Rule code used instead of name in `lint.extend-unsafe-fixes` - --> ruff.toml:34:25 - | -32 | extend-unfixable = ["F841"] -33 | extend-safe-fixes = ["E501"] -34 | extend-unsafe-fixes = ["UP035"] - | ^^^^^ - | -help: Replace rule code with name - | -33 | extend-safe-fixes = ["E501"] - - extend-unsafe-fixes = ["UP035"] -34 + extend-unsafe-fixes = ["deprecated-import"] - | diff --git a/crates/ruff_linter/src/toml.rs b/crates/ruff_linter/src/toml.rs index 9eb4e78f3ae7e5..e20e6a32fce196 100644 --- a/crates/ruff_linter/src/toml.rs +++ b/crates/ruff_linter/src/toml.rs @@ -85,72 +85,3 @@ pub fn lint_fix_toml<'a>( diagnostics = lint_toml(path, transformed.as_ref(), settings, source_type); } } - -#[cfg(test)] -mod tests { - use std::path::Path; - - use insta::assert_snapshot; - - use ruff_db::diagnostic::{DisplayDiagnosticConfig, DisplayDiagnostics, DummyFileResolver}; - use ruff_python_ast::TomlSourceType; - - use crate::codes::Rule; - use crate::settings::LinterSettings; - use crate::settings::types::UnsafeFixes; - - use super::lint_fix_toml; - - #[test] - fn fixes_toml() { - let source = r#"lint.select = ["F401"]"#; - let settings = LinterSettings::for_rule(Rule::RuleCodesInSelectors).with_preview_mode(); - - let result = lint_fix_toml( - Path::new("ruff.toml"), - source, - &settings, - TomlSourceType::Ruff, - UnsafeFixes::Disabled, - ); - - let result = std::fmt::from_fn(|f| { - let count = result.fixed.counts().sum::(); - writeln!( - f, - "Applied {count} fix{es}", - es = if count != 1 { "es" } else { "" } - )?; - writeln!( - f, - "\n## Transformed output\n\n```toml\n{}\n```", - result.transformed - )?; - if !result.diagnostics.is_empty() { - writeln!( - f, - "\n## Remaining diagnostics\n\n{}", - DisplayDiagnostics::new( - &DummyFileResolver, - &DisplayDiagnosticConfig::new("ruff"), - &result.diagnostics - ) - )?; - } - Ok(()) - }); - - assert_snapshot!( - result, - @r#" - Applied 1 fix - - ## Transformed output - - ```toml - lint.select = ["unused-import"] - ``` - "#, - ); - } -} From efa9ab61bc6e5c768f53292e9395fc9b079b209b Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Wed, 15 Jul 2026 11:13:42 -0400 Subject: [PATCH 09/17] add RuleCode and from_spanned --- .../ruff/rules/rule_codes_in_selectors.rs | 79 ++++++++++++------- 1 file changed, 49 insertions(+), 30 deletions(-) diff --git a/crates/ruff_linter/src/rules/ruff/rules/rule_codes_in_selectors.rs b/crates/ruff_linter/src/rules/ruff/rules/rule_codes_in_selectors.rs index cacddb824eb647..26c8e3b8e4d022 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/rule_codes_in_selectors.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/rule_codes_in_selectors.rs @@ -1,7 +1,7 @@ use std::iter::repeat; -use std::ops::Range; -use ruff_text_size::{TextRange, TextSize}; +use ruff_db::diagnostic::LintName; +use ruff_text_size::{TextLen, TextRange, TextSize}; use rustc_hash::FxHashMap; use serde::Deserialize; use toml::Spanned; @@ -61,35 +61,20 @@ pub(crate) fn rule_codes_in_selectors(context: &LintContext, source_type: TomlSo return; } - let source_file = context.source_file(); - let Ok(Some(config_file)) = ConfigFile::from_toml_str(source_file.source_text(), source_type) - else { + let source = context.source_file().source_text(); + let Ok(Some(config_file)) = ConfigFile::from_toml_str(source, source_type) else { return; }; for (spanned, selector) in config_file.selectors() { - let code = spanned.get_ref(); - let code = get_redirect_target(code).unwrap_or(code); - let Ok(rule) = Rule::from_code(code) else { - continue; - }; - - let Some(content_range) = - toml_string_content_range(source_file.source_text(), spanned.span()) - else { - debug_assert!(false, "a TOML string span should include its quotes"); + let Some(RuleCode { name, range }) = RuleCode::from_spanned(spanned, source) else { continue; }; - let range = TextRange::new( - TextSize::try_from(content_range.start).unwrap(), - TextSize::try_from(content_range.end).unwrap(), - ); - context .report_diagnostic(RuleCodesInSelectors { selector }, range) .set_fix(Fix::safe_edit(Edit::range_replacement( - rule.name().to_string(), + name.to_string(), range, ))); } @@ -251,14 +236,48 @@ impl Lint { } } -fn toml_string_content_range(source: &str, range: Range) -> Option> { - let string = source.get(range.clone())?; - let content = string.trim_matches(['"', '\'']); - if content.is_empty() || content.len() == string.len() { - return None; - } - let leading_quotes = string.len() - string.trim_start_matches(['"', '\'']).len(); - let start = range.start + leading_quotes; +struct RuleCode { + name: LintName, + range: TextRange, +} - Some(start..start + content.len()) +impl RuleCode { + /// Extract a rule code and its range from a spanned TOML string. + /// + /// The range corresponds to the code itself rather than the surrounding string: + /// + /// ```toml + /// [lint] + /// select = ["F401"] + /// ^^^^ + /// ``` + fn from_spanned(spanned: &Spanned, source: &str) -> Option { + let code = spanned.get_ref(); + let code = get_redirect_target(code).unwrap_or(code); + let rule = Rule::from_code(code).ok()?; + + let span = spanned.span(); + let range = TextRange::new( + TextSize::try_from(span.start).unwrap(), + TextSize::try_from(span.end).unwrap(), + ); + + // Note that this should be infallible because the `Spanned` guarantees that the + // source is surrounded by valid TOML quotes, and `Rule::from_code` above guarantees that + // the string content is a valid rule code. This means that we don't have to worry about + // stripping nested quotes like `"'F401'"` or similar. + let range = { + let string = &source[range]; + let content = string.trim_start_matches(['"', '\'']); + let quote_len = string.text_len() - content.text_len(); + let start = range.start() + quote_len; + let end = range.end() - quote_len; + TextRange::new(start, end) + }; + + Some(Self { + name: rule.name(), + range, + }) + } } From 56cb707022d13ba0f894b348aa55b0345c6bc5e1 Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Wed, 15 Jul 2026 12:26:41 -0400 Subject: [PATCH 10/17] switch to DeTable --- .../ruff/rules/rule_codes_in_selectors.rs | 263 +++++++----------- 1 file changed, 93 insertions(+), 170 deletions(-) diff --git a/crates/ruff_linter/src/rules/ruff/rules/rule_codes_in_selectors.rs b/crates/ruff_linter/src/rules/ruff/rules/rule_codes_in_selectors.rs index 26c8e3b8e4d022..3b9436d46ba744 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/rule_codes_in_selectors.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/rule_codes_in_selectors.rs @@ -1,14 +1,11 @@ -use std::iter::repeat; - -use ruff_db::diagnostic::LintName; -use ruff_text_size::{TextLen, TextRange, TextSize}; -use rustc_hash::FxHashMap; -use serde::Deserialize; use toml::Spanned; +use toml::de::{DeArray, DeTable, DeValue}; +use ruff_db::diagnostic::LintName; use ruff_diagnostics::{Edit, Fix}; use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::TomlSourceType; +use ruff_text_size::{TextLen, TextRange, TextSize}; use crate::{ AlwaysFixableViolation, checkers::ast::LintContext, codes::Rule, @@ -41,13 +38,21 @@ use crate::{ #[violation_metadata(preview_since = "NEXT_RUFF_VERSION")] pub(crate) struct RuleCodesInSelectors { selector: &'static str, + in_lint_table: bool, } impl AlwaysFixableViolation for RuleCodesInSelectors { #[derive_message_formats] fn message(&self) -> String { - let Self { selector } = self; - format!("Rule code used instead of name in `{selector}`") + let Self { + selector, + in_lint_table, + } = self; + if *in_lint_table { + format!("Rule code used instead of name in `lint.{selector}`") + } else { + format!("Rule code used instead of name in `{selector}`") + } } fn fix_title(&self) -> String { @@ -62,177 +67,96 @@ pub(crate) fn rule_codes_in_selectors(context: &LintContext, source_type: TomlSo } let source = context.source_file().source_text(); - let Ok(Some(config_file)) = ConfigFile::from_toml_str(source, source_type) else { + let Ok(document) = DeTable::parse(source) else { return; }; - for (spanned, selector) in config_file.selectors() { - let Some(RuleCode { name, range }) = RuleCode::from_spanned(spanned, source) else { - continue; - }; + let root = document.get_ref(); + let ruff = match source_type { + TomlSourceType::Pyproject => root + .get("tool") + .and_then(|tool| tool.get_ref().get("ruff")) + .and_then(|ruff| ruff.get_ref().as_table()), + TomlSourceType::Ruff => Some(root), + _ => None, + }; - context - .report_diagnostic(RuleCodesInSelectors { selector }, range) - .set_fix(Fix::safe_edit(Edit::range_replacement( - name.to_string(), - range, - ))); - } -} + let Some(ruff) = ruff else { + return; + }; -enum ConfigFile { - Pyproject(Pyproject), - Ruff(Ruff), -} + check_selectors(context, ruff, false); -impl ConfigFile { - fn from_toml_str( - source: &str, - source_type: TomlSourceType, - ) -> Result, toml::de::Error> { - match source_type { - TomlSourceType::Pyproject => Ok(Some(Self::Pyproject(toml::from_str(source)?))), - TomlSourceType::Ruff => Ok(Some(Self::Ruff(toml::from_str(source)?))), - _ => Ok(None), - } - } - - fn selectors(&self) -> impl Iterator, &'static str)> { - let ruff = match self { - ConfigFile::Pyproject(pyproject) => &pyproject.tool.ruff, - ConfigFile::Ruff(ruff) => ruff, - }; - ruff.select - .iter() - .zip(repeat("select")) - .chain(ruff.extend_select.iter().zip(repeat("extend-select"))) - .chain(ruff.fixable.iter().zip(repeat("fixable"))) - .chain(ruff.extend_fixable.iter().zip(repeat("extend-fixable"))) - .chain(ruff.ignore.iter().zip(repeat("ignore"))) - .chain(ruff.extend_ignore.iter().zip(repeat("extend-ignore"))) - .chain( - ruff.per_file_ignores - .values() - .flatten() - .zip(repeat("per-file-ignores")), - ) - .chain( - ruff.extend_per_file_ignores - .values() - .flatten() - .zip(repeat("extend-per-file-ignores")), - ) - .chain(ruff.unfixable.iter().zip(repeat("unfixable"))) - .chain(ruff.extend_unfixable.iter().zip(repeat("extend-unfixable"))) - .chain( - ruff.extend_safe_fixes - .iter() - .zip(repeat("extend-safe-fixes")), - ) - .chain( - ruff.extend_unsafe_fixes - .iter() - .zip(repeat("extend-unsafe-fixes")), - ) - .chain(ruff.lint.selectors()) + if let Some(lint) = ruff.get("lint").and_then(|lint| lint.get_ref().as_table()) { + check_selectors(context, lint, true); } } -#[derive(Deserialize)] -struct Pyproject { - tool: Tool, -} +const SELECTORS: &[&str] = &[ + "select", + "extend-select", + "fixable", + "extend-fixable", + "ignore", + "extend-ignore", + "per-file-ignores", + "extend-per-file-ignores", + "unfixable", + "extend-unfixable", + "extend-safe-fixes", + "extend-unsafe-fixes", +]; + +fn check_selectors(context: &LintContext, table: &DeTable<'_>, in_lint_table: bool) { + for &selector in SELECTORS { + let Some(value) = table.get(selector) else { + continue; + }; -#[derive(Deserialize)] -struct Tool { - ruff: Ruff, + match value.get_ref() { + // select = [values...] + DeValue::Array(values) => { + check_selector_array(context, values, selector, in_lint_table); + } + // per-file-ignores = { file = [values...] } + DeValue::Table(per_file) => { + for value in per_file.values() { + let Some(values) = value.get_ref().as_array() else { + continue; + }; + check_selector_array(context, values, selector, in_lint_table); + } + } + _ => {} + } + } } -type Selector = Spanned; - -#[derive(Default, Deserialize)] -#[serde(default, rename_all = "kebab-case")] -struct Ruff { - // Keep these deprecated top-level fields in sync with the selector-valued fields in - // `ruff_workspace::options::LintCommonOptions`. - select: Vec, - extend_select: Vec, - fixable: Vec, - extend_fixable: Vec, - ignore: Vec, - extend_ignore: Vec, - per_file_ignores: FxHashMap>, - extend_per_file_ignores: FxHashMap>, - unfixable: Vec, - extend_unfixable: Vec, - extend_safe_fixes: Vec, - extend_unsafe_fixes: Vec, - - // Linter options - lint: Lint, -} +fn check_selector_array( + context: &LintContext, + values: &DeArray<'_>, + selector: &'static str, + in_lint_table: bool, +) { + let source = context.source_file().source_text(); -#[derive(Default, Deserialize)] -#[serde(default, rename_all = "kebab-case")] -struct Lint { - // Keep these fields in sync with the selector-valued fields in - // `ruff_workspace::options::LintCommonOptions`. - select: Vec, - extend_select: Vec, - fixable: Vec, - extend_fixable: Vec, - ignore: Vec, - extend_ignore: Vec, - per_file_ignores: FxHashMap>, - extend_per_file_ignores: FxHashMap>, - unfixable: Vec, - extend_unfixable: Vec, - extend_safe_fixes: Vec, - extend_unsafe_fixes: Vec, -} + for value in values { + let Some(RuleCode { name, range }) = RuleCode::from_spanned(value, source) else { + continue; + }; -impl Lint { - fn selectors(&self) -> impl Iterator, &'static str)> { - self.select - .iter() - .zip(repeat("lint.select")) - .chain(self.extend_select.iter().zip(repeat("lint.extend-select"))) - .chain(self.fixable.iter().zip(repeat("lint.fixable"))) - .chain( - self.extend_fixable - .iter() - .zip(repeat("lint.extend-fixable")), - ) - .chain(self.ignore.iter().zip(repeat("lint.ignore"))) - .chain(self.extend_ignore.iter().zip(repeat("lint.extend-ignore"))) - .chain( - self.per_file_ignores - .values() - .flatten() - .zip(repeat("lint.per-file-ignores")), - ) - .chain( - self.extend_per_file_ignores - .values() - .flatten() - .zip(repeat("lint.extend-per-file-ignores")), - ) - .chain(self.unfixable.iter().zip(repeat("lint.unfixable"))) - .chain( - self.extend_unfixable - .iter() - .zip(repeat("lint.extend-unfixable")), - ) - .chain( - self.extend_safe_fixes - .iter() - .zip(repeat("lint.extend-safe-fixes")), - ) - .chain( - self.extend_unsafe_fixes - .iter() - .zip(repeat("lint.extend-unsafe-fixes")), + context + .report_diagnostic( + RuleCodesInSelectors { + selector, + in_lint_table, + }, + range, ) + .set_fix(Fix::safe_edit(Edit::range_replacement( + name.to_string(), + range, + ))); } } @@ -251,8 +175,8 @@ impl RuleCode { /// select = ["F401"] /// ^^^^ /// ``` - fn from_spanned(spanned: &Spanned, source: &str) -> Option { - let code = spanned.get_ref(); + fn from_spanned(spanned: &Spanned>, source: &str) -> Option { + let code = spanned.get_ref().as_str()?; let code = get_redirect_target(code).unwrap_or(code); let rule = Rule::from_code(code).ok()?; @@ -262,10 +186,9 @@ impl RuleCode { TextSize::try_from(span.end).unwrap(), ); - // Note that this should be infallible because the `Spanned` guarantees that the - // source is surrounded by valid TOML quotes, and `Rule::from_code` above guarantees that - // the string content is a valid rule code. This means that we don't have to worry about - // stripping nested quotes like `"'F401'"` or similar. + // Note that this should be infallible because the parsed TOML string is surrounded by valid + // quotes, and `Rule::from_code` above guarantees that its content is a valid rule code. This + // means that we don't have to worry about stripping nested quotes like `"'F401'"` or similar. let range = { let string = &source[range]; let content = string.trim_start_matches(['"', '\'']); From 4c9f3ca7a81d9a6741552a2d67b8cd148d09f66f Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Wed, 15 Jul 2026 12:36:50 -0400 Subject: [PATCH 11/17] share initial DeTable parse with RUF200 --- .../ruff/rules/invalid_pyproject_toml.rs | 22 ++++++++++++++++++- .../ruff/rules/rule_codes_in_selectors.rs | 16 ++++++-------- crates/ruff_linter/src/toml.rs | 16 ++++++++------ 3 files changed, 37 insertions(+), 17 deletions(-) diff --git a/crates/ruff_linter/src/rules/ruff/rules/invalid_pyproject_toml.rs b/crates/ruff_linter/src/rules/ruff/rules/invalid_pyproject_toml.rs index be78db98cf6e35..9e9675041cf2ed 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/invalid_pyproject_toml.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/invalid_pyproject_toml.rs @@ -1,3 +1,8 @@ +use pyproject_toml::PyProjectToml; +use serde::Deserialize; +use toml::Spanned; +use toml::de::DeTable; + use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_text_size::{TextRange, TextSize}; @@ -48,7 +53,22 @@ impl Violation for InvalidPyprojectToml { } /// RUF200 -pub(crate) fn invalid_pyproject_toml(context: &LintContext, err: &toml::de::Error) { +pub(crate) fn invalid_pyproject_toml( + context: &LintContext, + document: Result>, toml::de::Error>, +) { + let err = match document { + Ok(document) => { + let deserializer = toml::de::Deserializer::from(document); + let Err(mut err) = PyProjectToml::deserialize(deserializer) else { + return; + }; + err.set_input(Some(context.source_file().source_text())); + err + } + Err(err) => err, + }; + let range = match err.span() { // This is bad but sometimes toml and/or serde just don't give us spans // TODO(konstin,micha): https://github.com/astral-sh/ruff/issues/4571 diff --git a/crates/ruff_linter/src/rules/ruff/rules/rule_codes_in_selectors.rs b/crates/ruff_linter/src/rules/ruff/rules/rule_codes_in_selectors.rs index 3b9436d46ba744..0444deb366bcf2 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/rule_codes_in_selectors.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/rule_codes_in_selectors.rs @@ -61,23 +61,21 @@ impl AlwaysFixableViolation for RuleCodesInSelectors { } /// RUF201 -pub(crate) fn rule_codes_in_selectors(context: &LintContext, source_type: TomlSourceType) { +pub(crate) fn rule_codes_in_selectors( + context: &LintContext, + document: &DeTable<'_>, + source_type: TomlSourceType, +) { if !is_human_readable_names_enabled(context.settings().preview) { return; } - let source = context.source_file().source_text(); - let Ok(document) = DeTable::parse(source) else { - return; - }; - - let root = document.get_ref(); let ruff = match source_type { - TomlSourceType::Pyproject => root + TomlSourceType::Pyproject => document .get("tool") .and_then(|tool| tool.get_ref().get("ruff")) .and_then(|ruff| ruff.get_ref().as_table()), - TomlSourceType::Ruff => Some(root), + TomlSourceType::Ruff => Some(document), _ => None, }; diff --git a/crates/ruff_linter/src/toml.rs b/crates/ruff_linter/src/toml.rs index e20e6a32fce196..4a0fa817eebdb3 100644 --- a/crates/ruff_linter/src/toml.rs +++ b/crates/ruff_linter/src/toml.rs @@ -1,7 +1,7 @@ use std::borrow::Cow; use std::path::Path; -use pyproject_toml::PyProjectToml; +use toml::de::DeTable; use ruff_db::diagnostic::Diagnostic; use ruff_python_ast::TomlSourceType; @@ -29,14 +29,16 @@ pub fn lint_toml( ) -> Vec { let context = LintContext::new(path, contents, settings); - if let Err(err) = toml::from_str::(contents) { - if source_type.is_pyproject() && context.is_rule_enabled(Rule::InvalidPyprojectToml) { - invalid_pyproject_toml(&context, &err); - } + let document = DeTable::parse(contents); + + if context.is_rule_enabled(Rule::RuleCodesInSelectors) + && let Ok(document) = &document + { + rule_codes_in_selectors(&context, document.get_ref(), source_type); } - if context.is_rule_enabled(Rule::RuleCodesInSelectors) { - rule_codes_in_selectors(&context, source_type); + if source_type.is_pyproject() && context.is_rule_enabled(Rule::InvalidPyprojectToml) { + invalid_pyproject_toml(&context, document); } context.into_diagnostics() From 0392becbdfe6cc4eba097ba2c510d6df2e421dd2 Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Wed, 15 Jul 2026 12:46:44 -0400 Subject: [PATCH 12/17] LintSource::PyprojectToml -> Toml --- crates/ruff/src/diagnostics.rs | 4 ++-- crates/ruff_linter/src/registry.rs | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/crates/ruff/src/diagnostics.rs b/crates/ruff/src/diagnostics.rs index 1cd66a4bdf0e0a..634ad80b1c7a46 100644 --- a/crates/ruff/src/diagnostics.rs +++ b/crates/ruff/src/diagnostics.rs @@ -217,7 +217,7 @@ pub(crate) fn lint_path( let (diagnostics, fixed) = if settings .rules .iter_enabled() - .any(|rule_code| rule_code.lint_source().is_pyproject_toml()) + .any(|rule_code| rule_code.lint_source().is_toml()) { let contents = match std::fs::read_to_string(path).map_err(SourceError::from) { Ok(contents) => contents, @@ -397,7 +397,7 @@ pub(crate) fn lint_stdin( .linter .rules .iter_enabled() - .any(|rule_code| rule_code.lint_source().is_pyproject_toml()) + .any(|rule_code| rule_code.lint_source().is_toml()) { return Ok(Diagnostics::default()); } diff --git a/crates/ruff_linter/src/registry.rs b/crates/ruff_linter/src/registry.rs index aa1ea2514e15ed..21892be74d79f2 100644 --- a/crates/ruff_linter/src/registry.rs +++ b/crates/ruff_linter/src/registry.rs @@ -241,7 +241,8 @@ pub enum LintSource { Imports, Noqa, Filesystem, - PyprojectToml, + /// A TOML config file, either `pyproject.toml`, `ruff.toml`, or `.ruff.toml`. + Toml, } impl Rule { @@ -249,7 +250,7 @@ impl Rule { /// physical lines). pub const fn lint_source(&self) -> LintSource { match self { - Rule::InvalidPyprojectToml | Rule::RuleCodesInSelectors => LintSource::PyprojectToml, + Rule::InvalidPyprojectToml | Rule::RuleCodesInSelectors => LintSource::Toml, Rule::BlanketNOQA | Rule::NoqaComments | Rule::RedirectedNOQA From bd94dd9a94ca9a49f8784285db74c610f8497d23 Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Wed, 15 Jul 2026 13:19:08 -0400 Subject: [PATCH 13/17] Revert "drop ruff.toml and .ruff.toml from stable includes" This reverts commit 0388bfce89368ac65d290877073967ff3e025e5a. --- crates/ruff/tests/cli/lint.rs | 1 - .../cli__lint__requires_python_extend_from_shared_config.snap | 2 ++ .../tests/cli/snapshots/cli__lint__requires_python_no_tool.snap | 2 ++ ...__lint__requires_python_no_tool_target_version_override.snap | 2 ++ .../cli__lint__requires_python_pyproject_toml_above.snap | 2 ++ ...i__lint__requires_python_pyproject_toml_above_with_tool.snap | 2 ++ .../snapshots/cli__lint__requires_python_ruff_toml_above-2.snap | 2 ++ .../snapshots/cli__lint__requires_python_ruff_toml_above.snap | 2 ++ ...cli__lint__requires_python_ruff_toml_no_target_fallback.snap | 2 ++ .../snapshots/cli__show_settings__display_default_settings.snap | 2 ++ ...__show_settings__display_settings_from_nested_directory.snap | 2 ++ crates/ruff_workspace/src/settings.rs | 2 ++ 12 files changed, 22 insertions(+), 1 deletion(-) diff --git a/crates/ruff/tests/cli/lint.rs b/crates/ruff/tests/cli/lint.rs index 8506f85f21d457..9ecd4573e32494 100644 --- a/crates/ruff/tests/cli/lint.rs +++ b/crates/ruff/tests/cli/lint.rs @@ -468,7 +468,6 @@ ignore = ["D203", "D212"] All checks passed! ----- stderr ----- - warning: No Python files found under the given path(s) "); Ok(()) diff --git a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_extend_from_shared_config.snap b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_extend_from_shared_config.snap index f5a44bbbd8779d..b4a34f4c7bed98 100644 --- a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_extend_from_shared_config.snap +++ b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_extend_from_shared_config.snap @@ -59,6 +59,8 @@ file_resolver.include = [ "*.pyi", "*.ipynb", "**/pyproject.toml", + "**/ruff.toml", + "**/.ruff.toml", ] file_resolver.extend_include = [] file_resolver.respect_gitignore = true diff --git a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool.snap b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool.snap index 322b41688ed6d2..fafd0ded8c0923 100644 --- a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool.snap +++ b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool.snap @@ -61,6 +61,8 @@ file_resolver.include = [ "*.pyi", "*.ipynb", "**/pyproject.toml", + "**/ruff.toml", + "**/.ruff.toml", ] file_resolver.extend_include = [] file_resolver.respect_gitignore = true diff --git a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool_target_version_override.snap b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool_target_version_override.snap index 0dadf95bea3802..48c3d81fed3e86 100644 --- a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool_target_version_override.snap +++ b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool_target_version_override.snap @@ -63,6 +63,8 @@ file_resolver.include = [ "*.pyi", "*.ipynb", "**/pyproject.toml", + "**/ruff.toml", + "**/.ruff.toml", ] file_resolver.extend_include = [] file_resolver.respect_gitignore = true diff --git a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_pyproject_toml_above.snap b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_pyproject_toml_above.snap index 140513dc935dee..99f233b5093f00 100644 --- a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_pyproject_toml_above.snap +++ b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_pyproject_toml_above.snap @@ -60,6 +60,8 @@ file_resolver.include = [ "*.pyi", "*.ipynb", "**/pyproject.toml", + "**/ruff.toml", + "**/.ruff.toml", ] file_resolver.extend_include = [] file_resolver.respect_gitignore = true diff --git a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_pyproject_toml_above_with_tool.snap b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_pyproject_toml_above_with_tool.snap index 8cc415c4967749..a9727552e04821 100644 --- a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_pyproject_toml_above_with_tool.snap +++ b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_pyproject_toml_above_with_tool.snap @@ -61,6 +61,8 @@ file_resolver.include = [ "*.pyi", "*.ipynb", "**/pyproject.toml", + "**/ruff.toml", + "**/.ruff.toml", ] file_resolver.extend_include = [] file_resolver.respect_gitignore = true diff --git a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_above-2.snap b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_above-2.snap index 73ceea46bb3884..72982b964975a3 100644 --- a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_above-2.snap +++ b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_above-2.snap @@ -59,6 +59,8 @@ file_resolver.include = [ "*.pyi", "*.ipynb", "**/pyproject.toml", + "**/ruff.toml", + "**/.ruff.toml", ] file_resolver.extend_include = [] file_resolver.respect_gitignore = true diff --git a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_above.snap b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_above.snap index f8136b2d971f59..7a50948dfc7252 100644 --- a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_above.snap +++ b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_above.snap @@ -59,6 +59,8 @@ file_resolver.include = [ "*.pyi", "*.ipynb", "**/pyproject.toml", + "**/ruff.toml", + "**/.ruff.toml", ] file_resolver.extend_include = [] file_resolver.respect_gitignore = true diff --git a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_no_target_fallback.snap b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_no_target_fallback.snap index d281c8f869c4a0..60137f66c8575e 100644 --- a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_no_target_fallback.snap +++ b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_no_target_fallback.snap @@ -59,6 +59,8 @@ file_resolver.include = [ "*.pyi", "*.ipynb", "**/pyproject.toml", + "**/ruff.toml", + "**/.ruff.toml", ] file_resolver.extend_include = [] file_resolver.respect_gitignore = true diff --git a/crates/ruff/tests/cli/snapshots/cli__show_settings__display_default_settings.snap b/crates/ruff/tests/cli/snapshots/cli__show_settings__display_default_settings.snap index 792399f068ed64..882ccb119a13ba 100644 --- a/crates/ruff/tests/cli/snapshots/cli__show_settings__display_default_settings.snap +++ b/crates/ruff/tests/cli/snapshots/cli__show_settings__display_default_settings.snap @@ -56,6 +56,8 @@ file_resolver.include = [ "*.pyi", "*.ipynb", "**/pyproject.toml", + "**/ruff.toml", + "**/.ruff.toml", ] file_resolver.extend_include = [] file_resolver.respect_gitignore = true diff --git a/crates/ruff/tests/cli/snapshots/cli__show_settings__display_settings_from_nested_directory.snap b/crates/ruff/tests/cli/snapshots/cli__show_settings__display_settings_from_nested_directory.snap index ff29f68590cbde..f155e549bc0a34 100644 --- a/crates/ruff/tests/cli/snapshots/cli__show_settings__display_settings_from_nested_directory.snap +++ b/crates/ruff/tests/cli/snapshots/cli__show_settings__display_settings_from_nested_directory.snap @@ -56,6 +56,8 @@ file_resolver.include = [ "*.pyi", "*.ipynb", "**/pyproject.toml", + "**/ruff.toml", + "**/.ruff.toml", ] file_resolver.extend_include = [] file_resolver.respect_gitignore = true diff --git a/crates/ruff_workspace/src/settings.rs b/crates/ruff_workspace/src/settings.rs index 4e8c113465dc45..a1c3dd4af225a9 100644 --- a/crates/ruff_workspace/src/settings.rs +++ b/crates/ruff_workspace/src/settings.rs @@ -143,6 +143,8 @@ pub(crate) static INCLUDE: &[FilePattern] = &[ FilePattern::Builtin("*.pyi"), FilePattern::Builtin("*.ipynb"), FilePattern::Builtin("**/pyproject.toml"), + FilePattern::Builtin("**/ruff.toml"), + FilePattern::Builtin("**/.ruff.toml"), ]; pub(crate) static INCLUDE_PREVIEW: &[FilePattern] = &[ FilePattern::Builtin("*.py"), From 26640e574394ddbe8a7561ebb5f84704c7b5f4f5 Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Thu, 16 Jul 2026 09:05:01 -0400 Subject: [PATCH 14/17] test valid code with invalid code --- .../mdtest/ruff/rule-codes-in-selectors.md | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/crates/ruff_linter/resources/mdtest/ruff/rule-codes-in-selectors.md b/crates/ruff_linter/resources/mdtest/ruff/rule-codes-in-selectors.md index 13d116f7b72dc0..819444faafc0e5 100644 --- a/crates/ruff_linter/resources/mdtest/ruff/rule-codes-in-selectors.md +++ b/crates/ruff_linter/resources/mdtest/ruff/rule-codes-in-selectors.md @@ -83,13 +83,30 @@ help: Replace rule code with name ## Invalid rule codes -Invalid rule codes are not flagged, including nested quoting issues like `"'F401'"`: +Invalid rule codes are not flagged, including nested quoting issues like `"'F401'"`, but valid codes +in the same selector are still analyzed: `ruff.toml`: ```toml [lint] -select = ["'F401'"] +# snapshot: rule-codes-in-selectors +select = ["'F401'", "F402"] +``` + +```snapshot +error[RUF201]: Rule code used instead of name in `lint.select` + --> src/ruff.toml:3:22 + | +3 | select = ["'F401'", "F402"] + | ^^^^ + | +help: Replace rule code with name + | +2 | # snapshot: rule-codes-in-selectors + - select = ["'F401'", "F402"] +3 + select = ["'F401'", "import-shadowed-by-loop-var"] + | ``` ## Prefixes and names From f1155ee1690e897ca268c90c070fa0b860ed4050 Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Thu, 16 Jul 2026 09:08:38 -0400 Subject: [PATCH 15/17] include rule name in fix title --- crates/ruff/tests/cli/lint.rs | 2 +- .../resources/mdtest/ruff/rule-codes-in-selectors.md | 12 ++++++------ .../src/rules/ruff/rules/rule_codes_in_selectors.rs | 5 ++++- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/crates/ruff/tests/cli/lint.rs b/crates/ruff/tests/cli/lint.rs index 9ecd4573e32494..a1edec6a7a7175 100644 --- a/crates/ruff/tests/cli/lint.rs +++ b/crates/ruff/tests/cli/lint.rs @@ -5215,7 +5215,7 @@ fn ruff_toml_is_linted() -> Result<()> { 1 | lint.select = ["F401"] | ^^^^ | - help: Replace rule code with name + help: Replace rule code with `unused-import` | - lint.select = ["F401"] 1 + lint.select = ["unused-import"] diff --git a/crates/ruff_linter/resources/mdtest/ruff/rule-codes-in-selectors.md b/crates/ruff_linter/resources/mdtest/ruff/rule-codes-in-selectors.md index 819444faafc0e5..4ba7f874eca43c 100644 --- a/crates/ruff_linter/resources/mdtest/ruff/rule-codes-in-selectors.md +++ b/crates/ruff_linter/resources/mdtest/ruff/rule-codes-in-selectors.md @@ -27,7 +27,7 @@ error[RUF201]: Rule code used instead of name in `lint.select` 3 | "F401", # snapshot: rule-codes-in-selectors | ^^^^ | -help: Replace rule code with name +help: Replace rule code with `unused-import` | 2 | select = [ - "F401", # snapshot: rule-codes-in-selectors @@ -42,7 +42,7 @@ error[RUF201]: Rule code used instead of name in `lint.select` 4 | 'F402', # snapshot: rule-codes-in-selectors | ^^^^ | -help: Replace rule code with name +help: Replace rule code with `import-shadowed-by-loop-var` | 3 | "F401", # snapshot: rule-codes-in-selectors - 'F402', # snapshot: rule-codes-in-selectors @@ -57,7 +57,7 @@ error[RUF201]: Rule code used instead of name in `lint.select` 5 | """F403""", # snapshot: rule-codes-in-selectors | ^^^^ | -help: Replace rule code with name +help: Replace rule code with `undefined-local-with-import-star` | 4 | 'F402', # snapshot: rule-codes-in-selectors - """F403""", # snapshot: rule-codes-in-selectors @@ -72,7 +72,7 @@ error[RUF201]: Rule code used instead of name in `lint.select` 6 | '''F404''', # snapshot: rule-codes-in-selectors | ^^^^ | -help: Replace rule code with name +help: Replace rule code with `late-future-import` | 5 | """F403""", # snapshot: rule-codes-in-selectors - '''F404''', # snapshot: rule-codes-in-selectors @@ -101,7 +101,7 @@ error[RUF201]: Rule code used instead of name in `lint.select` 3 | select = ["'F401'", "F402"] | ^^^^ | -help: Replace rule code with name +help: Replace rule code with `import-shadowed-by-loop-var` | 2 | # snapshot: rule-codes-in-selectors - select = ["'F401'", "F402"] @@ -193,7 +193,7 @@ error[RUF201]: Rule code used instead of name in `lint.select` 2 | lint.select = ["F401"] | ^^^^ | -help: Replace rule code with name +help: Replace rule code with `unused-import` ``` This should also cover settings like `extend-unsafe-fixes` and `per-file-ignores`, all of which are diff --git a/crates/ruff_linter/src/rules/ruff/rules/rule_codes_in_selectors.rs b/crates/ruff_linter/src/rules/ruff/rules/rule_codes_in_selectors.rs index 0444deb366bcf2..7349a724088028 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/rule_codes_in_selectors.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/rule_codes_in_selectors.rs @@ -38,6 +38,7 @@ use crate::{ #[violation_metadata(preview_since = "NEXT_RUFF_VERSION")] pub(crate) struct RuleCodesInSelectors { selector: &'static str, + name: &'static str, in_lint_table: bool, } @@ -47,6 +48,7 @@ impl AlwaysFixableViolation for RuleCodesInSelectors { let Self { selector, in_lint_table, + name: _, } = self; if *in_lint_table { format!("Rule code used instead of name in `lint.{selector}`") @@ -56,7 +58,7 @@ impl AlwaysFixableViolation for RuleCodesInSelectors { } fn fix_title(&self) -> String { - "Replace rule code with name".to_string() + format!("Replace rule code with `{name}`", name = self.name) } } @@ -148,6 +150,7 @@ fn check_selector_array( RuleCodesInSelectors { selector, in_lint_table, + name: name.as_str(), }, range, ) From 820c663b905502011f16e0b2a6a3d808d6f7f8c7 Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Thu, 16 Jul 2026 09:12:07 -0400 Subject: [PATCH 16/17] add a note to UnresolvedRuleSelector --- crates/ruff_linter/src/rule_selector.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/ruff_linter/src/rule_selector.rs b/crates/ruff_linter/src/rule_selector.rs index 7a477aeae923de..70b6303c8d796c 100644 --- a/crates/ruff_linter/src/rule_selector.rs +++ b/crates/ruff_linter/src/rule_selector.rs @@ -15,6 +15,10 @@ use crate::rule_redirects::get_redirect; use crate::settings::types::PreviewMode; use crate::warn_user_once_by_message; +/// A potential rule selector that has not yet been validated and tracks its source. +/// +/// If you add a new field that uses this type, be sure to update `rule-codes-in-selectors` +/// (`RUF201`) to validate the additional selector field. #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)] #[serde(transparent)] pub struct UnresolvedRuleSelector(RangedValue); From 815ce38417f5708449e615edae170667d8ebb955 Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Thu, 16 Jul 2026 09:16:46 -0400 Subject: [PATCH 17/17] split selectors lists --- .../mdtest/ruff/rule-codes-in-selectors.md | 13 +++++ .../ruff/rules/rule_codes_in_selectors.rs | 50 ++++++++++++------- 2 files changed, 46 insertions(+), 17 deletions(-) diff --git a/crates/ruff_linter/resources/mdtest/ruff/rule-codes-in-selectors.md b/crates/ruff_linter/resources/mdtest/ruff/rule-codes-in-selectors.md index 4ba7f874eca43c..cd9bada1049057 100644 --- a/crates/ruff_linter/resources/mdtest/ruff/rule-codes-in-selectors.md +++ b/crates/ruff_linter/resources/mdtest/ruff/rule-codes-in-selectors.md @@ -109,6 +109,19 @@ help: Replace rule code with `import-shadowed-by-loop-var` | ``` +## Invalid selector shapes + +Just in case these ever make it past our actual config deserialization, the rule skips over +malformed selectors (e.g. table for `select`, non-table for `per-file-ignores`): + +`ruff.toml`: + +```toml +[lint] +select = { nested = ["F401"] } +per-file-ignores = ["F401"] +``` + ## Prefixes and names Prefixes and rule names are also left alone: diff --git a/crates/ruff_linter/src/rules/ruff/rules/rule_codes_in_selectors.rs b/crates/ruff_linter/src/rules/ruff/rules/rule_codes_in_selectors.rs index 7349a724088028..9b99508579056a 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/rule_codes_in_selectors.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/rule_codes_in_selectors.rs @@ -92,42 +92,58 @@ pub(crate) fn rule_codes_in_selectors( } } -const SELECTORS: &[&str] = &[ +/// Selectors that are themselves arrays. +/// +/// For example: +/// +/// ```toml +/// select = ["F401"] +/// ``` +const ARRAY_SELECTORS: &[&str] = &[ "select", "extend-select", "fixable", "extend-fixable", "ignore", "extend-ignore", - "per-file-ignores", - "extend-per-file-ignores", "unfixable", "extend-unfixable", "extend-safe-fixes", "extend-unsafe-fixes", ]; +/// Selectors that are tables containing arrays. +/// +/// For example: +/// +/// ```toml +/// per-file-ignores = { "*.py" = ["F401"] } +/// ``` +const TABLE_SELECTORS: &[&str] = &["per-file-ignores", "extend-per-file-ignores"]; + fn check_selectors(context: &LintContext, table: &DeTable<'_>, in_lint_table: bool) { - for &selector in SELECTORS { + for &selector in ARRAY_SELECTORS { let Some(value) = table.get(selector) else { continue; }; - match value.get_ref() { - // select = [values...] - DeValue::Array(values) => { + if let DeValue::Array(values) = value.get_ref() { + check_selector_array(context, values, selector, in_lint_table); + } + } + + for &selector in TABLE_SELECTORS { + let Some(value) = table.get(selector) else { + continue; + }; + + if let DeValue::Table(per_file) = value.get_ref() { + for value in per_file.values() { + let Some(values) = value.get_ref().as_array() else { + continue; + }; check_selector_array(context, values, selector, in_lint_table); } - // per-file-ignores = { file = [values...] } - DeValue::Table(per_file) => { - for value in per_file.values() { - let Some(values) = value.get_ref().as_array() else { - continue; - }; - check_selector_array(context, values, selector, in_lint_table); - } - } - _ => {} } } }