diff --git a/crates/ruff/tests/snapshots/show_settings__display_default_settings.snap b/crates/ruff/tests/snapshots/show_settings__display_default_settings.snap index cbe6a7bc4fd11..a15c475e9bb27 100644 --- a/crates/ruff/tests/snapshots/show_settings__display_default_settings.snap +++ b/crates/ruff/tests/snapshots/show_settings__display_default_settings.snap @@ -370,6 +370,7 @@ linter.pylint.max_statements = 50 linter.pylint.max_public_methods = 20 linter.pylint.max_locals = 15 linter.pyupgrade.keep_runtime_typing = false +linter.ruff.parenthesize_tuple_in_subscript = false # Formatter Settings formatter.exclude = [] diff --git a/crates/ruff_linter/resources/test/fixtures/ruff/RUF031.py b/crates/ruff_linter/resources/test/fixtures/ruff/RUF031.py new file mode 100644 index 0000000000000..72e7975ca0d84 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/ruff/RUF031.py @@ -0,0 +1,28 @@ +d = {(1,2):"a",(3,4):"b",(5,6,7):"c",(8,):"d"} +d[(1,2)] +d[( + 1, + 2 +)] +d[ + 1, + 2 +] +d[(2,4)] +d[(5,6,7)] +d[(8,)] +d[tuple(1,2)] +d[tuple(8)] +d[1,2] +d[3,4] +d[5,6,7] +e = {((1,2),(3,4)):"a"} +e[((1,2),(3,4))] +e[(1,2),(3,4)] + +token_features[ + (window_position, feature_name) +] = self._extract_raw_features_from_token + +d[1,] +d[(1,)] diff --git a/crates/ruff_linter/resources/test/fixtures/ruff/RUF031_prefer_parens.py b/crates/ruff_linter/resources/test/fixtures/ruff/RUF031_prefer_parens.py new file mode 100644 index 0000000000000..aaa18644fc9bd --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/ruff/RUF031_prefer_parens.py @@ -0,0 +1,27 @@ +d = {(1,2):"a",(3,4):"b",(5,6,7):"c",(8,):"d"} +d[(1,2)] +d[( + 1, + 2 +)] +d[ + 1, + 2 +] +d[(2,4)] +d[(5,6,7)] +d[(8,)] +d[tuple(1,2)] +d[tuple(8)] +d[1,2] +d[3,4] +d[5,6,7] +e = {((1,2),(3,4)):"a"} +e[((1,2),(3,4))] +e[(1,2),(3,4)] + +token_features[ + (window_position, feature_name) +] = self._extract_raw_features_from_token +d[1,] +d[(1,)] diff --git a/crates/ruff_linter/src/checkers/ast/analyze/expression.rs b/crates/ruff_linter/src/checkers/ast/analyze/expression.rs index 2f81db7417348..ff80afe5385cc 100644 --- a/crates/ruff_linter/src/checkers/ast/analyze/expression.rs +++ b/crates/ruff_linter/src/checkers/ast/analyze/expression.rs @@ -146,6 +146,10 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) { refurb::rules::fstring_number_format(checker, subscript); } + if checker.enabled(Rule::IncorrectlyParenthesizedTupleInSubscript) { + ruff::rules::subscript_with_parenthesized_tuple(checker, subscript); + } + pandas_vet::rules::subscript(checker, value, expr); } Expr::Tuple(ast::ExprTuple { diff --git a/crates/ruff_linter/src/codes.rs b/crates/ruff_linter/src/codes.rs index 969c5dc4b7066..9faf8c8c373b2 100644 --- a/crates/ruff_linter/src/codes.rs +++ b/crates/ruff_linter/src/codes.rs @@ -957,6 +957,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { (Ruff, "028") => (RuleGroup::Preview, rules::ruff::rules::InvalidFormatterSuppressionComment), (Ruff, "029") => (RuleGroup::Preview, rules::ruff::rules::UnusedAsync), (Ruff, "030") => (RuleGroup::Preview, rules::ruff::rules::AssertWithPrintMessage), + (Ruff, "031") => (RuleGroup::Preview, rules::ruff::rules::IncorrectlyParenthesizedTupleInSubscript), (Ruff, "100") => (RuleGroup::Stable, rules::ruff::rules::UnusedNOQA), (Ruff, "101") => (RuleGroup::Preview, rules::ruff::rules::RedirectedNOQA), diff --git a/crates/ruff_linter/src/rules/ruff/mod.rs b/crates/ruff_linter/src/rules/ruff/mod.rs index b719611759bf2..975122c9f68d2 100644 --- a/crates/ruff_linter/src/rules/ruff/mod.rs +++ b/crates/ruff_linter/src/rules/ruff/mod.rs @@ -1,6 +1,7 @@ //! Ruff-specific rules. pub(crate) mod rules; +pub mod settings; pub(crate) mod typing; #[cfg(test)] @@ -19,6 +20,7 @@ mod tests { use crate::settings::types::{ CompiledPerFileIgnoreList, PerFileIgnore, PreviewMode, PythonVersion, }; + use crate::settings::LinterSettings; use crate::test::{test_path, test_resource_path}; use crate::{assert_messages, settings}; @@ -55,6 +57,7 @@ mod tests { #[test_case(Rule::InvalidFormatterSuppressionComment, Path::new("RUF028.py"))] #[test_case(Rule::UnusedAsync, Path::new("RUF029.py"))] #[test_case(Rule::AssertWithPrintMessage, Path::new("RUF030.py"))] + #[test_case(Rule::IncorrectlyParenthesizedTupleInSubscript, Path::new("RUF031.py"))] #[test_case(Rule::RedirectedNOQA, Path::new("RUF101.py"))] fn rules(rule_code: Rule, path: &Path) -> Result<()> { let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy()); @@ -66,6 +69,21 @@ mod tests { Ok(()) } + #[test] + fn prefer_parentheses_getitem_tuple() -> Result<()> { + let diagnostics = test_path( + Path::new("ruff/RUF031_prefer_parens.py"), + &LinterSettings { + ruff: super::settings::Settings { + parenthesize_tuple_in_subscript: true, + }, + ..LinterSettings::for_rule(Rule::IncorrectlyParenthesizedTupleInSubscript) + }, + )?; + assert_messages!(diagnostics); + Ok(()) + } + #[test_case(Path::new("RUF013_0.py"))] #[test_case(Path::new("RUF013_1.py"))] fn implicit_optional_py39(path: &Path) -> Result<()> { diff --git a/crates/ruff_linter/src/rules/ruff/rules/incorrectly_parenthesized_tuple_in_subscript.rs b/crates/ruff_linter/src/rules/ruff/rules/incorrectly_parenthesized_tuple_in_subscript.rs new file mode 100644 index 0000000000000..15055759abea7 --- /dev/null +++ b/crates/ruff_linter/src/rules/ruff/rules/incorrectly_parenthesized_tuple_in_subscript.rs @@ -0,0 +1,82 @@ +use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix}; +use ruff_macros::{derive_message_formats, violation}; +use ruff_python_ast::ExprSubscript; +use ruff_text_size::Ranged; + +use crate::checkers::ast::Checker; + +/// ## What it does +/// Checks for consistent style regarding whether tuples in subscripts +/// are parenthesized. +/// +/// The exact nature of this violation depends on the setting +/// [`lint.ruff.parenthesize-tuple-in-subscript`]. By default, the use of +/// parentheses is considered a violation. +/// +/// ## Why is this bad? +/// It is good to be consistent and, depending on the codebase, one or the other +/// convention may be preferred. +/// +/// ## Example +/// +/// ```python +/// directions = {(0, 1): "North", (-1, 0): "East", (0, -1): "South", (1, 0): "West"} +/// directions[(0, 1)] +/// ``` +/// +/// Use instead (with default setting): +/// +/// ```python +/// directions = {(0, 1): "North", (-1, 0): "East", (0, -1): "South", (1, 0): "West"} +/// directions[0, 1] +/// ``` + +#[violation] +pub struct IncorrectlyParenthesizedTupleInSubscript { + prefer_parentheses: bool, +} + +impl AlwaysFixableViolation for IncorrectlyParenthesizedTupleInSubscript { + #[derive_message_formats] + fn message(&self) -> String { + if self.prefer_parentheses { + format!("Use parentheses for tuples in subscripts.") + } else { + format!("Avoid parentheses for tuples in subscripts.") + } + } + + fn fix_title(&self) -> String { + if self.prefer_parentheses { + "Parenthesize the tuple.".to_string() + } else { + "Remove the parentheses.".to_string() + } + } +} + +/// RUF031 +pub(crate) fn subscript_with_parenthesized_tuple(checker: &mut Checker, subscript: &ExprSubscript) { + let prefer_parentheses = checker.settings.ruff.parenthesize_tuple_in_subscript; + let Some(tuple_subscript) = subscript.slice.as_tuple_expr() else { + return; + }; + if tuple_subscript.parenthesized == prefer_parentheses { + return; + } + let locator = checker.locator(); + let source_range = subscript.slice.range(); + let new_source = if prefer_parentheses { + format!("({})", locator.slice(source_range)) + } else { + locator.slice(source_range)[1..source_range.len().to_usize() - 1].to_string() + }; + let edit = Edit::range_replacement(new_source, source_range); + checker.diagnostics.push( + Diagnostic::new( + IncorrectlyParenthesizedTupleInSubscript { prefer_parentheses }, + source_range, + ) + .with_fix(Fix::safe_edit(edit)), + ); +} diff --git a/crates/ruff_linter/src/rules/ruff/rules/mod.rs b/crates/ruff_linter/src/rules/ruff/rules/mod.rs index ee615a48d0dff..0f23812df8c93 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/mod.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/mod.rs @@ -7,6 +7,7 @@ pub(crate) use default_factory_kwarg::*; pub(crate) use explicit_f_string_type_conversion::*; pub(crate) use function_call_in_dataclass_default::*; pub(crate) use implicit_optional::*; +pub(crate) use incorrectly_parenthesized_tuple_in_subscript::*; pub(crate) use invalid_formatter_suppression_comment::*; pub(crate) use invalid_index_type::*; pub(crate) use invalid_pyproject_toml::*; @@ -41,6 +42,7 @@ mod explicit_f_string_type_conversion; mod function_call_in_dataclass_default; mod helpers; mod implicit_optional; +mod incorrectly_parenthesized_tuple_in_subscript; mod invalid_formatter_suppression_comment; mod invalid_index_type; mod invalid_pyproject_toml; diff --git a/crates/ruff_linter/src/rules/ruff/settings.rs b/crates/ruff_linter/src/rules/ruff/settings.rs new file mode 100644 index 0000000000000..a1c36b64951e5 --- /dev/null +++ b/crates/ruff_linter/src/rules/ruff/settings.rs @@ -0,0 +1,23 @@ +//! Settings for the `ruff` plugin. + +use crate::display_settings; +use ruff_macros::CacheKey; +use std::fmt; + +#[derive(Debug, Clone, CacheKey, Default)] +pub struct Settings { + pub parenthesize_tuple_in_subscript: bool, +} + +impl fmt::Display for Settings { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + display_settings! { + formatter = f, + namespace = "linter.ruff", + fields = [ + self.parenthesize_tuple_in_subscript + ] + } + Ok(()) + } +} diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF031_RUF031.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF031_RUF031.py.snap new file mode 100644 index 0000000000000..a8e7497800cc2 --- /dev/null +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF031_RUF031.py.snap @@ -0,0 +1,166 @@ +--- +source: crates/ruff_linter/src/rules/ruff/mod.rs +--- +RUF031.py:2:3: RUF031 [*] Avoid parentheses for tuples in subscripts. + | +1 | d = {(1,2):"a",(3,4):"b",(5,6,7):"c",(8,):"d"} +2 | d[(1,2)] + | ^^^^^ RUF031 +3 | d[( +4 | 1, + | + = help: Remove the parentheses. + +ℹ Safe fix +1 1 | d = {(1,2):"a",(3,4):"b",(5,6,7):"c",(8,):"d"} +2 |-d[(1,2)] + 2 |+d[1,2] +3 3 | d[( +4 4 | 1, +5 5 | 2 + +RUF031.py:3:3: RUF031 [*] Avoid parentheses for tuples in subscripts. + | +1 | d = {(1,2):"a",(3,4):"b",(5,6,7):"c",(8,):"d"} +2 | d[(1,2)] +3 | d[( + | ___^ +4 | | 1, +5 | | 2 +6 | | )] + | |_^ RUF031 +7 | d[ +8 | 1, + | + = help: Remove the parentheses. + +ℹ Safe fix +1 1 | d = {(1,2):"a",(3,4):"b",(5,6,7):"c",(8,):"d"} +2 2 | d[(1,2)] +3 |-d[( + 3 |+d[ +4 4 | 1, +5 5 | 2 +6 |-)] + 6 |+] +7 7 | d[ +8 8 | 1, +9 9 | 2 + +RUF031.py:11:3: RUF031 [*] Avoid parentheses for tuples in subscripts. + | + 9 | 2 +10 | ] +11 | d[(2,4)] + | ^^^^^ RUF031 +12 | d[(5,6,7)] +13 | d[(8,)] + | + = help: Remove the parentheses. + +ℹ Safe fix +8 8 | 1, +9 9 | 2 +10 10 | ] +11 |-d[(2,4)] + 11 |+d[2,4] +12 12 | d[(5,6,7)] +13 13 | d[(8,)] +14 14 | d[tuple(1,2)] + +RUF031.py:12:3: RUF031 [*] Avoid parentheses for tuples in subscripts. + | +10 | ] +11 | d[(2,4)] +12 | d[(5,6,7)] + | ^^^^^^^ RUF031 +13 | d[(8,)] +14 | d[tuple(1,2)] + | + = help: Remove the parentheses. + +ℹ Safe fix +9 9 | 2 +10 10 | ] +11 11 | d[(2,4)] +12 |-d[(5,6,7)] + 12 |+d[5,6,7] +13 13 | d[(8,)] +14 14 | d[tuple(1,2)] +15 15 | d[tuple(8)] + +RUF031.py:13:3: RUF031 [*] Avoid parentheses for tuples in subscripts. + | +11 | d[(2,4)] +12 | d[(5,6,7)] +13 | d[(8,)] + | ^^^^ RUF031 +14 | d[tuple(1,2)] +15 | d[tuple(8)] + | + = help: Remove the parentheses. + +ℹ Safe fix +10 10 | ] +11 11 | d[(2,4)] +12 12 | d[(5,6,7)] +13 |-d[(8,)] + 13 |+d[8,] +14 14 | d[tuple(1,2)] +15 15 | d[tuple(8)] +16 16 | d[1,2] + +RUF031.py:20:3: RUF031 [*] Avoid parentheses for tuples in subscripts. + | +18 | d[5,6,7] +19 | e = {((1,2),(3,4)):"a"} +20 | e[((1,2),(3,4))] + | ^^^^^^^^^^^^^ RUF031 +21 | e[(1,2),(3,4)] + | + = help: Remove the parentheses. + +ℹ Safe fix +17 17 | d[3,4] +18 18 | d[5,6,7] +19 19 | e = {((1,2),(3,4)):"a"} +20 |-e[((1,2),(3,4))] +21 20 | e[(1,2),(3,4)] + 21 |+e[(1,2),(3,4)] +22 22 | +23 23 | token_features[ +24 24 | (window_position, feature_name) + +RUF031.py:24:5: RUF031 [*] Avoid parentheses for tuples in subscripts. + | +23 | token_features[ +24 | (window_position, feature_name) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF031 +25 | ] = self._extract_raw_features_from_token + | + = help: Remove the parentheses. + +ℹ Safe fix +21 21 | e[(1,2),(3,4)] +22 22 | +23 23 | token_features[ +24 |- (window_position, feature_name) + 24 |+ window_position, feature_name +25 25 | ] = self._extract_raw_features_from_token +26 26 | +27 27 | d[1,] + +RUF031.py:28:3: RUF031 [*] Avoid parentheses for tuples in subscripts. + | +27 | d[1,] +28 | d[(1,)] + | ^^^^ RUF031 + | + = help: Remove the parentheses. + +ℹ Safe fix +25 25 | ] = self._extract_raw_features_from_token +26 26 | +27 27 | d[1,] +28 |-d[(1,)] + 28 |+d[1,] diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__prefer_parentheses_getitem_tuple.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__prefer_parentheses_getitem_tuple.snap new file mode 100644 index 0000000000000..5b089a85f601d --- /dev/null +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__prefer_parentheses_getitem_tuple.snap @@ -0,0 +1,129 @@ +--- +source: crates/ruff_linter/src/rules/ruff/mod.rs +--- +RUF031_prefer_parens.py:8:5: RUF031 [*] Use parentheses for tuples in subscripts. + | + 6 | )] + 7 | d[ + 8 | 1, + | _____^ + 9 | | 2 + | |_____^ RUF031 +10 | ] +11 | d[(2,4)] + | + = help: Parenthesize the tuple. + +ℹ Safe fix +5 5 | 2 +6 6 | )] +7 7 | d[ +8 |- 1, +9 |- 2 + 8 |+ (1, + 9 |+ 2) +10 10 | ] +11 11 | d[(2,4)] +12 12 | d[(5,6,7)] + +RUF031_prefer_parens.py:16:3: RUF031 [*] Use parentheses for tuples in subscripts. + | +14 | d[tuple(1,2)] +15 | d[tuple(8)] +16 | d[1,2] + | ^^^ RUF031 +17 | d[3,4] +18 | d[5,6,7] + | + = help: Parenthesize the tuple. + +ℹ Safe fix +13 13 | d[(8,)] +14 14 | d[tuple(1,2)] +15 15 | d[tuple(8)] +16 |-d[1,2] + 16 |+d[(1,2)] +17 17 | d[3,4] +18 18 | d[5,6,7] +19 19 | e = {((1,2),(3,4)):"a"} + +RUF031_prefer_parens.py:17:3: RUF031 [*] Use parentheses for tuples in subscripts. + | +15 | d[tuple(8)] +16 | d[1,2] +17 | d[3,4] + | ^^^ RUF031 +18 | d[5,6,7] +19 | e = {((1,2),(3,4)):"a"} + | + = help: Parenthesize the tuple. + +ℹ Safe fix +14 14 | d[tuple(1,2)] +15 15 | d[tuple(8)] +16 16 | d[1,2] +17 |-d[3,4] + 17 |+d[(3,4)] +18 18 | d[5,6,7] +19 19 | e = {((1,2),(3,4)):"a"} +20 20 | e[((1,2),(3,4))] + +RUF031_prefer_parens.py:18:3: RUF031 [*] Use parentheses for tuples in subscripts. + | +16 | d[1,2] +17 | d[3,4] +18 | d[5,6,7] + | ^^^^^ RUF031 +19 | e = {((1,2),(3,4)):"a"} +20 | e[((1,2),(3,4))] + | + = help: Parenthesize the tuple. + +ℹ Safe fix +15 15 | d[tuple(8)] +16 16 | d[1,2] +17 17 | d[3,4] +18 |-d[5,6,7] + 18 |+d[(5,6,7)] +19 19 | e = {((1,2),(3,4)):"a"} +20 20 | e[((1,2),(3,4))] +21 21 | e[(1,2),(3,4)] + +RUF031_prefer_parens.py:21:3: RUF031 [*] Use parentheses for tuples in subscripts. + | +19 | e = {((1,2),(3,4)):"a"} +20 | e[((1,2),(3,4))] +21 | e[(1,2),(3,4)] + | ^^^^^^^^^^^ RUF031 +22 | +23 | token_features[ + | + = help: Parenthesize the tuple. + +ℹ Safe fix +18 18 | d[5,6,7] +19 19 | e = {((1,2),(3,4)):"a"} +20 20 | e[((1,2),(3,4))] +21 |-e[(1,2),(3,4)] + 21 |+e[((1,2),(3,4))] +22 22 | +23 23 | token_features[ +24 24 | (window_position, feature_name) + +RUF031_prefer_parens.py:26:3: RUF031 [*] Use parentheses for tuples in subscripts. + | +24 | (window_position, feature_name) +25 | ] = self._extract_raw_features_from_token +26 | d[1,] + | ^^ RUF031 +27 | d[(1,)] + | + = help: Parenthesize the tuple. + +ℹ Safe fix +23 23 | token_features[ +24 24 | (window_position, feature_name) +25 25 | ] = self._extract_raw_features_from_token +26 |-d[1,] +27 26 | d[(1,)] + 27 |+d[(1,)] diff --git a/crates/ruff_linter/src/settings/mod.rs b/crates/ruff_linter/src/settings/mod.rs index 05f3edbfce540..3099e47d33cf9 100644 --- a/crates/ruff_linter/src/settings/mod.rs +++ b/crates/ruff_linter/src/settings/mod.rs @@ -20,7 +20,7 @@ use crate::rules::{ flake8_comprehensions, flake8_copyright, flake8_errmsg, flake8_gettext, flake8_implicit_str_concat, flake8_import_conventions, flake8_pytest_style, flake8_quotes, flake8_self, flake8_tidy_imports, flake8_type_checking, flake8_unused_arguments, isort, mccabe, - pep8_naming, pycodestyle, pydocstyle, pyflakes, pylint, pyupgrade, + pep8_naming, pycodestyle, pydocstyle, pyflakes, pylint, pyupgrade, ruff, }; use crate::settings::types::{ CompiledPerFileIgnoreList, ExtensionMapping, FilePatternSet, PythonVersion, @@ -265,6 +265,7 @@ pub struct LinterSettings { pub pyflakes: pyflakes::settings::Settings, pub pylint: pylint::settings::Settings, pub pyupgrade: pyupgrade::settings::Settings, + pub ruff: ruff::settings::Settings, } impl Display for LinterSettings { @@ -328,6 +329,7 @@ impl Display for LinterSettings { self.pyflakes | nested, self.pylint | nested, self.pyupgrade | nested, + self.ruff | nested, ] } Ok(()) @@ -428,6 +430,7 @@ impl LinterSettings { pyflakes: pyflakes::settings::Settings::default(), pylint: pylint::settings::Settings::default(), pyupgrade: pyupgrade::settings::Settings::default(), + ruff: ruff::settings::Settings::default(), preview: PreviewMode::default(), explicit_preview_rules: false, extension: ExtensionMapping::default(), diff --git a/crates/ruff_workspace/src/configuration.rs b/crates/ruff_workspace/src/configuration.rs index 5f08d054cdbbb..6e94612b03083 100644 --- a/crates/ruff_workspace/src/configuration.rs +++ b/crates/ruff_workspace/src/configuration.rs @@ -47,7 +47,7 @@ use crate::options::{ Flake8SelfOptions, Flake8TidyImportsOptions, Flake8TypeCheckingOptions, Flake8UnusedArgumentsOptions, FormatOptions, IsortOptions, LintCommonOptions, LintOptions, McCabeOptions, Options, Pep8NamingOptions, PyUpgradeOptions, PycodestyleOptions, - PydocstyleOptions, PyflakesOptions, PylintOptions, + PydocstyleOptions, PyflakesOptions, PylintOptions, RuffOptions, }; use crate::settings::{ FileResolverSettings, FormatterSettings, LineEnding, Settings, EXCLUDE, INCLUDE, @@ -402,6 +402,10 @@ impl Configuration { .pyupgrade .map(PyUpgradeOptions::into_settings) .unwrap_or_default(), + ruff: lint + .ruff + .map(RuffOptions::into_settings) + .unwrap_or_default(), }, formatter, @@ -631,6 +635,7 @@ pub struct LintConfiguration { pub pyflakes: Option, pub pylint: Option, pub pyupgrade: Option, + pub ruff: Option, } impl LintConfiguration { @@ -741,6 +746,7 @@ impl LintConfiguration { pyflakes: options.common.pyflakes, pylint: options.common.pylint, pyupgrade: options.common.pyupgrade, + ruff: options.ruff, }) } @@ -1118,6 +1124,7 @@ impl LintConfiguration { pyflakes: self.pyflakes.combine(config.pyflakes), pylint: self.pylint.combine(config.pylint), pyupgrade: self.pyupgrade.combine(config.pyupgrade), + ruff: self.ruff.combine(config.ruff), } } } diff --git a/crates/ruff_workspace/src/options.rs b/crates/ruff_workspace/src/options.rs index b0dbdd99f325c..db52812c01327 100644 --- a/crates/ruff_workspace/src/options.rs +++ b/crates/ruff_workspace/src/options.rs @@ -21,7 +21,7 @@ use ruff_linter::rules::{ flake8_copyright, flake8_errmsg, flake8_gettext, flake8_implicit_str_concat, flake8_import_conventions, flake8_pytest_style, flake8_quotes, flake8_self, flake8_tidy_imports, flake8_type_checking, flake8_unused_arguments, isort, mccabe, pep8_naming, - pycodestyle, pydocstyle, pyflakes, pylint, pyupgrade, + pycodestyle, pydocstyle, pyflakes, pylint, pyupgrade, ruff, }; use ruff_linter::settings::types::{ IdentifierPattern, OutputFormat, PreviewMode, PythonVersion, RequiredVersion, @@ -455,6 +455,10 @@ pub struct LintOptions { )] pub exclude: Option>, + /// Options for the `ruff` plugin + #[option_group] + pub ruff: Option, + /// Whether to enable preview mode. When preview mode is enabled, Ruff will /// use unstable rules and fixes. #[option( @@ -2969,6 +2973,35 @@ impl PyUpgradeOptions { } } +#[derive( + Clone, Debug, PartialEq, Eq, Default, Serialize, Deserialize, OptionsMetadata, CombineOptions, +)] +#[serde(deny_unknown_fields, rename_all = "kebab-case")] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +pub struct RuffOptions { + /// Whether to prefer accessing items keyed by tuples with + /// parentheses around the tuple (see `RUF031`). + #[option( + default = r#"false"#, + value_type = "bool", + example = r#" + # Make it a violation to use a tuple in a subscript without parentheses. + parenthesize-tuple-in-subscript = true + "# + )] + pub parenthesize_tuple_in_subscript: Option, +} + +impl RuffOptions { + pub fn into_settings(self) -> ruff::settings::Settings { + ruff::settings::Settings { + parenthesize_tuple_in_subscript: self + .parenthesize_tuple_in_subscript + .unwrap_or_default(), + } + } +} + /// Configures the way Ruff formats your code. #[derive( Clone, Debug, PartialEq, Eq, Default, Deserialize, Serialize, OptionsMetadata, CombineOptions, diff --git a/ruff.schema.json b/ruff.schema.json index 3de1fe4db5376..83b27a24f71cf 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -2245,6 +2245,17 @@ } ] }, + "ruff": { + "description": "Options for the `ruff` plugin", + "anyOf": [ + { + "$ref": "#/definitions/RuffOptions" + }, + { + "type": "null" + } + ] + }, "select": { "description": "A list of rule codes or prefixes to enable. Prefixes can specify exact rules (like `F841`), entire categories (like `F`), or anything in between.\n\nWhen breaking ties between enabled and disabled rules (via `select` and `ignore`, respectively), more specific prefixes override less specific prefixes.", "type": [ @@ -2670,6 +2681,19 @@ "RequiredVersion": { "type": "string" }, + "RuffOptions": { + "type": "object", + "properties": { + "parenthesize-tuple-in-subscript": { + "description": "Whether to prefer accessing items keyed by tuples with parentheses around the tuple (see `RUF031`).", + "type": [ + "boolean", + "null" + ] + } + }, + "additionalProperties": false + }, "RuleSelector": { "type": "string", "enum": [ @@ -3711,6 +3735,7 @@ "RUF029", "RUF03", "RUF030", + "RUF031", "RUF1", "RUF10", "RUF100",