mirror of https://github.com/astral-sh/ruff
[`pylint`] Implement `nested-min-max` (`W3301`) (#4200)
This commit is contained in:
parent
5ac2c7d293
commit
3beff29026
|
|
@ -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),
|
||||
)
|
||||
|
|
@ -3000,6 +3000,9 @@ where
|
|||
if self.settings.rules.enabled(Rule::InvalidEnvvarValue) {
|
||||
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
|
||||
if self.settings.rules.enabled(Rule::PytestPatchWithLambda) {
|
||||
|
|
|
|||
|
|
@ -212,6 +212,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<Rule> {
|
|||
(Pylint, "W1508") => Rule::InvalidEnvvarDefault,
|
||||
(Pylint, "W2901") => Rule::RedefinedLoopName,
|
||||
(Pylint, "E0302") => Rule::UnexpectedSpecialMethodSignature,
|
||||
(Pylint, "W3301") => Rule::NestedMinMax,
|
||||
|
||||
// flake8-builtins
|
||||
(Flake8Builtins, "001") => Rule::BuiltinVariableShadowing,
|
||||
|
|
|
|||
|
|
@ -189,6 +189,7 @@ ruff_macros::register_rules!(
|
|||
rules::pylint::rules::LoggingTooFewArgs,
|
||||
rules::pylint::rules::LoggingTooManyArgs,
|
||||
rules::pylint::rules::UnexpectedSpecialMethodSignature,
|
||||
rules::pylint::rules::NestedMinMax,
|
||||
// flake8-builtins
|
||||
rules::flake8_builtins::rules::BuiltinVariableShadowing,
|
||||
rules::flake8_builtins::rules::BuiltinArgumentShadowing,
|
||||
|
|
|
|||
|
|
@ -71,6 +71,7 @@ mod tests {
|
|||
#[test_case(Rule::UselessImportAlias, Path::new("import_aliasing.py"); "PLC0414")]
|
||||
#[test_case(Rule::UselessReturn, Path::new("useless_return.py"); "PLR1711")]
|
||||
#[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<()> {
|
||||
let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy());
|
||||
let diagnostics = test_path(
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ pub use load_before_global_declaration::{
|
|||
pub use logging::{logging_call, LoggingTooFewArgs, LoggingTooManyArgs};
|
||||
pub use magic_value_comparison::{magic_value_comparison, MagicValueComparison};
|
||||
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 property_with_parameters::{property_with_parameters, PropertyWithParameters};
|
||||
pub use redefined_loop_name::{redefined_loop_name, RedefinedLoopName};
|
||||
|
|
@ -68,6 +69,7 @@ mod load_before_global_declaration;
|
|||
mod logging;
|
||||
mod magic_value_comparison;
|
||||
mod manual_import_from;
|
||||
mod nested_min_max;
|
||||
mod nonlocal_without_binding;
|
||||
mod property_with_parameters;
|
||||
mod redefined_loop_name;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
||||
|
||||
|
||||
|
|
@ -2051,6 +2051,10 @@
|
|||
"PLW29",
|
||||
"PLW290",
|
||||
"PLW2901",
|
||||
"PLW3",
|
||||
"PLW33",
|
||||
"PLW330",
|
||||
"PLW3301",
|
||||
"PT",
|
||||
"PT0",
|
||||
"PT00",
|
||||
|
|
|
|||
Loading…
Reference in New Issue