[ruff] Implement incorrectly-parenthesized-tuple-in-subscript (RUF031) (#12480)

Implements the new fixable lint rule `RUF031` which checks for the use or omission of parentheses around tuples in subscripts, depending on the setting `lint.ruff.parenthesize-tuple-in-getitem`. By default, the use of parentheses is considered a violation.
This commit is contained in:
Dylan 2024-08-07 08:11:29 -05:00 committed by GitHub
parent d380b37a09
commit 7997da47f5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 552 additions and 3 deletions

View file

@ -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 = []

View file

@ -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,)]

View file

@ -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,)]

View file

@ -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 {

View file

@ -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),

View file

@ -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<()> {

View file

@ -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)),
);
}

View file

@ -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;

View file

@ -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(())
}
}

View file

@ -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,]

View file

@ -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,)]

View file

@ -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(),

View file

@ -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<PyflakesOptions>,
pub pylint: Option<PylintOptions>,
pub pyupgrade: Option<PyUpgradeOptions>,
pub ruff: Option<RuffOptions>,
}
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),
}
}
}

View file

@ -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<Vec<String>>,
/// Options for the `ruff` plugin
#[option_group]
pub ruff: Option<RuffOptions>,
/// 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<bool>,
}
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,

25
ruff.schema.json generated
View file

@ -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",