[`pylint`] Implement `nested-min-max` (`W3301`) (#4200)

This commit is contained in:
Trevor McCulloch 2023-05-06 20:14:14 -07:00 committed by GitHub
parent 5ac2c7d293
commit 3beff29026
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 360 additions and 0 deletions

View File

@ -0,0 +1,21 @@
min(1, 2, 3)
min(1, min(2, 3))
min(1, min(2, min(3, 4)))
min(1, foo("a", "b"), min(3, 4))
min(1, max(2, 3))
max(1, 2, 3)
max(1, max(2, 3))
max(1, max(2, max(3, 4)))
max(1, foo("a", "b"), max(3, 4))
# These should not trigger; we do not flag cases with keyword args.
min(1, min(2, 3), key=test)
min(1, min(2, 3, key=test))
# This will still trigger, to merge the calls without keyword args.
min(1, min(2, 3, key=test), min(4, 5))
# Don't provide a fix if there are comments within the call.
min(
1, # This is a comment.
min(2, 3),
)

View File

@ -3000,6 +3000,9 @@ where
if self.settings.rules.enabled(Rule::InvalidEnvvarValue) { if self.settings.rules.enabled(Rule::InvalidEnvvarValue) {
pylint::rules::invalid_envvar_value(self, func, args, keywords); pylint::rules::invalid_envvar_value(self, func, args, keywords);
} }
if self.settings.rules.enabled(Rule::NestedMinMax) {
pylint::rules::nested_min_max(self, expr, func, args, keywords);
}
// flake8-pytest-style // flake8-pytest-style
if self.settings.rules.enabled(Rule::PytestPatchWithLambda) { if self.settings.rules.enabled(Rule::PytestPatchWithLambda) {

View File

@ -212,6 +212,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<Rule> {
(Pylint, "W1508") => Rule::InvalidEnvvarDefault, (Pylint, "W1508") => Rule::InvalidEnvvarDefault,
(Pylint, "W2901") => Rule::RedefinedLoopName, (Pylint, "W2901") => Rule::RedefinedLoopName,
(Pylint, "E0302") => Rule::UnexpectedSpecialMethodSignature, (Pylint, "E0302") => Rule::UnexpectedSpecialMethodSignature,
(Pylint, "W3301") => Rule::NestedMinMax,
// flake8-builtins // flake8-builtins
(Flake8Builtins, "001") => Rule::BuiltinVariableShadowing, (Flake8Builtins, "001") => Rule::BuiltinVariableShadowing,

View File

@ -189,6 +189,7 @@ ruff_macros::register_rules!(
rules::pylint::rules::LoggingTooFewArgs, rules::pylint::rules::LoggingTooFewArgs,
rules::pylint::rules::LoggingTooManyArgs, rules::pylint::rules::LoggingTooManyArgs,
rules::pylint::rules::UnexpectedSpecialMethodSignature, rules::pylint::rules::UnexpectedSpecialMethodSignature,
rules::pylint::rules::NestedMinMax,
// flake8-builtins // flake8-builtins
rules::flake8_builtins::rules::BuiltinVariableShadowing, rules::flake8_builtins::rules::BuiltinVariableShadowing,
rules::flake8_builtins::rules::BuiltinArgumentShadowing, rules::flake8_builtins::rules::BuiltinArgumentShadowing,

View File

@ -71,6 +71,7 @@ mod tests {
#[test_case(Rule::UselessImportAlias, Path::new("import_aliasing.py"); "PLC0414")] #[test_case(Rule::UselessImportAlias, Path::new("import_aliasing.py"); "PLC0414")]
#[test_case(Rule::UselessReturn, Path::new("useless_return.py"); "PLR1711")] #[test_case(Rule::UselessReturn, Path::new("useless_return.py"); "PLR1711")]
#[test_case(Rule::YieldInInit, Path::new("yield_in_init.py"); "PLE0100")] #[test_case(Rule::YieldInInit, Path::new("yield_in_init.py"); "PLE0100")]
#[test_case(Rule::NestedMinMax, Path::new("nested_min_max.py"); "PLW3301")]
fn rules(rule_code: Rule, path: &Path) -> Result<()> { fn rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy()); let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy());
let diagnostics = test_path( let diagnostics = test_path(

View File

@ -25,6 +25,7 @@ pub use load_before_global_declaration::{
pub use logging::{logging_call, LoggingTooFewArgs, LoggingTooManyArgs}; pub use logging::{logging_call, LoggingTooFewArgs, LoggingTooManyArgs};
pub use magic_value_comparison::{magic_value_comparison, MagicValueComparison}; pub use magic_value_comparison::{magic_value_comparison, MagicValueComparison};
pub use manual_import_from::{manual_from_import, ManualFromImport}; pub use manual_import_from::{manual_from_import, ManualFromImport};
pub use nested_min_max::{nested_min_max, NestedMinMax};
pub use nonlocal_without_binding::NonlocalWithoutBinding; pub use nonlocal_without_binding::NonlocalWithoutBinding;
pub use property_with_parameters::{property_with_parameters, PropertyWithParameters}; pub use property_with_parameters::{property_with_parameters, PropertyWithParameters};
pub use redefined_loop_name::{redefined_loop_name, RedefinedLoopName}; pub use redefined_loop_name::{redefined_loop_name, RedefinedLoopName};
@ -68,6 +69,7 @@ mod load_before_global_declaration;
mod logging; mod logging;
mod magic_value_comparison; mod magic_value_comparison;
mod manual_import_from; mod manual_import_from;
mod nested_min_max;
mod nonlocal_without_binding; mod nonlocal_without_binding;
mod property_with_parameters; mod property_with_parameters;
mod redefined_loop_name; mod redefined_loop_name;

View File

@ -0,0 +1,131 @@
use ruff_text_size::TextSize;
use rustpython_parser::ast::{Expr, ExprKind, Keyword};
use ruff_diagnostics::{Diagnostic, Edit, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::{has_comments, unparse_expr};
use ruff_python_semantic::context::Context;
use crate::{checkers::ast::Checker, registry::AsRule};
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum MinMax {
Min,
Max,
}
#[violation]
pub struct NestedMinMax {
func: MinMax,
fixable: bool,
}
impl Violation for NestedMinMax {
#[derive_message_formats]
fn message(&self) -> String {
format!("Nested `{}` calls can be flattened", self.func)
}
fn autofix_title_formatter(&self) -> Option<fn(&Self) -> String> {
self.fixable
.then_some(|NestedMinMax { func, .. }| format!("Flatten nested `{func}` calls"))
}
}
impl MinMax {
/// Converts a function call [`Expr`] into a [`MinMax`] if it is a call to `min` or `max`.
fn try_from_call(func: &Expr, keywords: &[Keyword], context: &Context) -> Option<MinMax> {
if !keywords.is_empty() {
return None;
}
let ExprKind::Name { id, .. } = func.node() else {
return None;
};
if id == "min" && context.is_builtin("min") {
Some(MinMax::Min)
} else if id == "max" && context.is_builtin("max") {
Some(MinMax::Max)
} else {
None
}
}
}
impl std::fmt::Display for MinMax {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
MinMax::Min => write!(f, "min"),
MinMax::Max => write!(f, "max"),
}
}
}
/// Collect a new set of arguments to by either accepting existing args as-is or
/// collecting child arguments, if it's a call to the same function.
fn collect_nested_args(context: &Context, min_max: MinMax, args: &[Expr]) -> Vec<Expr> {
fn inner(context: &Context, min_max: MinMax, args: &[Expr], new_args: &mut Vec<Expr>) {
for arg in args {
if let ExprKind::Call {
func,
args,
keywords,
} = arg.node()
{
if MinMax::try_from_call(func, keywords, context) == Some(min_max) {
inner(context, min_max, args, new_args);
continue;
}
}
new_args.push(arg.clone());
}
}
let mut new_args = Vec::with_capacity(args.len());
inner(context, min_max, args, &mut new_args);
new_args
}
/// W3301
pub fn nested_min_max(
checker: &mut Checker,
expr: &Expr,
func: &Expr,
args: &[Expr],
keywords: &[Keyword],
) {
let Some(min_max) = MinMax::try_from_call(func, keywords, &checker.ctx) else {
return;
};
if args.iter().any(|arg| {
let ExprKind::Call { func, keywords, ..} = arg.node() else {
return false;
};
MinMax::try_from_call(func, keywords, &checker.ctx) == Some(min_max)
}) {
let fixable = !has_comments(expr, checker.locator);
let mut diagnostic = Diagnostic::new(
NestedMinMax {
func: min_max,
fixable,
},
expr.range(),
);
if fixable && checker.patch(diagnostic.kind.rule()) {
let flattened_expr = Expr::new(
TextSize::default(),
TextSize::default(),
ExprKind::Call {
func: Box::new(func.clone()),
args: collect_nested_args(&checker.ctx, min_max, args),
keywords: keywords.to_owned(),
},
);
diagnostic.set_fix(Edit::range_replacement(
unparse_expr(&flattened_expr, checker.stylist),
expr.range(),
));
}
checker.diagnostics.push(diagnostic);
}
}

View File

@ -0,0 +1,196 @@
---
source: crates/ruff/src/rules/pylint/mod.rs
---
nested_min_max.py:2:1: PLW3301 [*] Nested `min` calls can be flattened
|
2 | min(1, 2, 3)
3 | min(1, min(2, 3))
| ^^^^^^^^^^^^^^^^^ PLW3301
4 | min(1, min(2, min(3, 4)))
5 | min(1, foo("a", "b"), min(3, 4))
|
= help: Flatten nested `min` calls
Suggested fix
1 1 | min(1, 2, 3)
2 |-min(1, min(2, 3))
2 |+min(1, 2, 3)
3 3 | min(1, min(2, min(3, 4)))
4 4 | min(1, foo("a", "b"), min(3, 4))
5 5 | min(1, max(2, 3))
nested_min_max.py:3:1: PLW3301 [*] Nested `min` calls can be flattened
|
3 | min(1, 2, 3)
4 | min(1, min(2, 3))
5 | min(1, min(2, min(3, 4)))
| ^^^^^^^^^^^^^^^^^^^^^^^^^ PLW3301
6 | min(1, foo("a", "b"), min(3, 4))
7 | min(1, max(2, 3))
|
= help: Flatten nested `min` calls
Suggested fix
1 1 | min(1, 2, 3)
2 2 | min(1, min(2, 3))
3 |-min(1, min(2, min(3, 4)))
3 |+min(1, 2, 3, 4)
4 4 | min(1, foo("a", "b"), min(3, 4))
5 5 | min(1, max(2, 3))
6 6 | max(1, 2, 3)
nested_min_max.py:3:8: PLW3301 [*] Nested `min` calls can be flattened
|
3 | min(1, 2, 3)
4 | min(1, min(2, 3))
5 | min(1, min(2, min(3, 4)))
| ^^^^^^^^^^^^^^^^^ PLW3301
6 | min(1, foo("a", "b"), min(3, 4))
7 | min(1, max(2, 3))
|
= help: Flatten nested `min` calls
Suggested fix
1 1 | min(1, 2, 3)
2 2 | min(1, min(2, 3))
3 |-min(1, min(2, min(3, 4)))
3 |+min(1, min(2, 3, 4))
4 4 | min(1, foo("a", "b"), min(3, 4))
5 5 | min(1, max(2, 3))
6 6 | max(1, 2, 3)
nested_min_max.py:4:1: PLW3301 [*] Nested `min` calls can be flattened
|
4 | min(1, min(2, 3))
5 | min(1, min(2, min(3, 4)))
6 | min(1, foo("a", "b"), min(3, 4))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PLW3301
7 | min(1, max(2, 3))
8 | max(1, 2, 3)
|
= help: Flatten nested `min` calls
Suggested fix
1 1 | min(1, 2, 3)
2 2 | min(1, min(2, 3))
3 3 | min(1, min(2, min(3, 4)))
4 |-min(1, foo("a", "b"), min(3, 4))
4 |+min(1, foo("a", "b"), 3, 4)
5 5 | min(1, max(2, 3))
6 6 | max(1, 2, 3)
7 7 | max(1, max(2, 3))
nested_min_max.py:7:1: PLW3301 [*] Nested `max` calls can be flattened
|
7 | min(1, max(2, 3))
8 | max(1, 2, 3)
9 | max(1, max(2, 3))
| ^^^^^^^^^^^^^^^^^ PLW3301
10 | max(1, max(2, max(3, 4)))
11 | max(1, foo("a", "b"), max(3, 4))
|
= help: Flatten nested `max` calls
Suggested fix
4 4 | min(1, foo("a", "b"), min(3, 4))
5 5 | min(1, max(2, 3))
6 6 | max(1, 2, 3)
7 |-max(1, max(2, 3))
7 |+max(1, 2, 3)
8 8 | max(1, max(2, max(3, 4)))
9 9 | max(1, foo("a", "b"), max(3, 4))
10 10 |
nested_min_max.py:8:1: PLW3301 [*] Nested `max` calls can be flattened
|
8 | max(1, 2, 3)
9 | max(1, max(2, 3))
10 | max(1, max(2, max(3, 4)))
| ^^^^^^^^^^^^^^^^^^^^^^^^^ PLW3301
11 | max(1, foo("a", "b"), max(3, 4))
|
= help: Flatten nested `max` calls
Suggested fix
5 5 | min(1, max(2, 3))
6 6 | max(1, 2, 3)
7 7 | max(1, max(2, 3))
8 |-max(1, max(2, max(3, 4)))
8 |+max(1, 2, 3, 4)
9 9 | max(1, foo("a", "b"), max(3, 4))
10 10 |
11 11 | # These should not trigger; we do not flag cases with keyword args.
nested_min_max.py:8:8: PLW3301 [*] Nested `max` calls can be flattened
|
8 | max(1, 2, 3)
9 | max(1, max(2, 3))
10 | max(1, max(2, max(3, 4)))
| ^^^^^^^^^^^^^^^^^ PLW3301
11 | max(1, foo("a", "b"), max(3, 4))
|
= help: Flatten nested `max` calls
Suggested fix
5 5 | min(1, max(2, 3))
6 6 | max(1, 2, 3)
7 7 | max(1, max(2, 3))
8 |-max(1, max(2, max(3, 4)))
8 |+max(1, max(2, 3, 4))
9 9 | max(1, foo("a", "b"), max(3, 4))
10 10 |
11 11 | # These should not trigger; we do not flag cases with keyword args.
nested_min_max.py:9:1: PLW3301 [*] Nested `max` calls can be flattened
|
9 | max(1, max(2, 3))
10 | max(1, max(2, max(3, 4)))
11 | max(1, foo("a", "b"), max(3, 4))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PLW3301
12 |
13 | # These should not trigger; we do not flag cases with keyword args.
|
= help: Flatten nested `max` calls
Suggested fix
6 6 | max(1, 2, 3)
7 7 | max(1, max(2, 3))
8 8 | max(1, max(2, max(3, 4)))
9 |-max(1, foo("a", "b"), max(3, 4))
9 |+max(1, foo("a", "b"), 3, 4)
10 10 |
11 11 | # These should not trigger; we do not flag cases with keyword args.
12 12 | min(1, min(2, 3), key=test)
nested_min_max.py:15:1: PLW3301 [*] Nested `min` calls can be flattened
|
15 | min(1, min(2, 3, key=test))
16 | # This will still trigger, to merge the calls without keyword args.
17 | min(1, min(2, 3, key=test), min(4, 5))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PLW3301
18 |
19 | # Don't provide a fix if there are comments within the call.
|
= help: Flatten nested `min` calls
Suggested fix
12 12 | min(1, min(2, 3), key=test)
13 13 | min(1, min(2, 3, key=test))
14 14 | # This will still trigger, to merge the calls without keyword args.
15 |-min(1, min(2, 3, key=test), min(4, 5))
15 |+min(1, min(2, 3, key=test), 4, 5)
16 16 |
17 17 | # Don't provide a fix if there are comments within the call.
18 18 | min(
nested_min_max.py:18:1: PLW3301 Nested `min` calls can be flattened
|
18 | # Don't provide a fix if there are comments within the call.
19 | / min(
20 | | 1, # This is a comment.
21 | | min(2, 3),
22 | | )
| |_^ PLW3301
|

4
ruff.schema.json generated
View File

@ -2051,6 +2051,10 @@
"PLW29", "PLW29",
"PLW290", "PLW290",
"PLW2901", "PLW2901",
"PLW3",
"PLW33",
"PLW330",
"PLW3301",
"PT", "PT",
"PT0", "PT0",
"PT00", "PT00",