diff --git a/doc/whatsnew/fragments/8524.bugfix b/doc/whatsnew/fragments/8524.bugfix new file mode 100644 index 0000000000..76bae6e88c --- /dev/null +++ b/doc/whatsnew/fragments/8524.bugfix @@ -0,0 +1,3 @@ +Fixes suggestion for ``nested-min-max`` for expressions with additive operators, list and dict comprehensions. + +Closes #8524 diff --git a/pylint/checkers/nested_min_max.py b/pylint/checkers/nested_min_max.py index 219382ff52..a935d62f53 100644 --- a/pylint/checkers/nested_min_max.py +++ b/pylint/checkers/nested_min_max.py @@ -14,6 +14,7 @@ from pylint.checkers import BaseChecker from pylint.checkers.utils import only_required_for_messages, safe_infer +from pylint.constants import PY39_PLUS from pylint.interfaces import INFERENCE if TYPE_CHECKING: @@ -93,13 +94,10 @@ def visit_call(self, node: nodes.Call) -> None: for idx, arg in enumerate(fixed_node.args): if not isinstance(arg, nodes.Const): - inferred = safe_infer(arg) - if isinstance( - inferred, (nodes.List, nodes.Tuple, nodes.Set, *DICT_TYPES) - ): + if self._is_splattable_expression(arg): splat_node = nodes.Starred( ctx=Context.Load, - lineno=inferred.lineno, + lineno=arg.lineno, col_offset=0, parent=nodes.NodeNG( lineno=None, @@ -125,6 +123,39 @@ def visit_call(self, node: nodes.Call) -> None: confidence=INFERENCE, ) + def _is_splattable_expression(self, arg: nodes.NodeNG) -> bool: + """Returns true if expression under min/max could be converted to splat + expression. + """ + # Support sequence addition (operator __add__) + if isinstance(arg, nodes.BinOp) and arg.op == "+": + return self._is_splattable_expression( + arg.left + ) and self._is_splattable_expression(arg.right) + # Support dict merge (operator __or__ in Python 3.9) + if isinstance(arg, nodes.BinOp) and arg.op == "|" and PY39_PLUS: + return self._is_splattable_expression( + arg.left + ) and self._is_splattable_expression(arg.right) + + inferred = safe_infer(arg) + if inferred and inferred.pytype() in {"builtins.list", "builtins.tuple"}: + return True + if isinstance( + inferred or arg, + ( + nodes.List, + nodes.Tuple, + nodes.Set, + nodes.ListComp, + nodes.DictComp, + *DICT_TYPES, + ), + ): + return True + + return False + def register(linter: PyLinter) -> None: linter.register_checker(NestedMinMaxChecker(linter)) diff --git a/tests/functional/n/nested_min_max.py b/tests/functional/n/nested_min_max.py index 151e035dd1..7bb11264e9 100644 --- a/tests/functional/n/nested_min_max.py +++ b/tests/functional/n/nested_min_max.py @@ -42,3 +42,15 @@ lst2 = [3, 7, 10] max(3, max(nums), max(lst2)) # [nested-min-max] + +max(3, max([5] + [6, 7])) # [nested-min-max] +max(3, *[5] + [6, 7]) + +max(3, max([5] + [i for i in range(4) if i])) # [nested-min-max] +max(3, *[5] + [i for i in range(4) if i]) + +max(3, max([5] + list(range(4)))) # [nested-min-max] +max(3, *[5] + list(range(4))) + +max(3, max(list(range(4)))) # [nested-min-max] +max(3, *list(range(4))) diff --git a/tests/functional/n/nested_min_max.txt b/tests/functional/n/nested_min_max.txt index c03f1b500c..87b31daf65 100644 --- a/tests/functional/n/nested_min_max.txt +++ b/tests/functional/n/nested_min_max.txt @@ -12,3 +12,7 @@ nested-min-max:33:0:33:17::Do not use nested call of 'max'; it's possible to do nested-min-max:37:0:37:17::Do not use nested call of 'max'; it's possible to do 'max(3, *nums)' instead:INFERENCE nested-min-max:40:0:40:26::Do not use nested call of 'max'; it's possible to do 'max(3, *nums.values())' instead:INFERENCE nested-min-max:44:0:44:28::Do not use nested call of 'max'; it's possible to do 'max(3, *nums, *lst2)' instead:INFERENCE +nested-min-max:46:0:46:25::Do not use nested call of 'max'; it's possible to do 'max(3, *[5] + [6, 7])' instead:INFERENCE +nested-min-max:49:0:49:45::Do not use nested call of 'max'; it's possible to do 'max(3, *[5] + [i for i in range(4) if i])' instead:INFERENCE +nested-min-max:52:0:52:33::Do not use nested call of 'max'; it's possible to do 'max(3, *[5] + list(range(4)))' instead:INFERENCE +nested-min-max:55:0:55:27::Do not use nested call of 'max'; it's possible to do 'max(3, *list(range(4)))' instead:INFERENCE diff --git a/tests/functional/n/nested_min_max_py39.py b/tests/functional/n/nested_min_max_py39.py new file mode 100644 index 0000000000..e60146ca1b --- /dev/null +++ b/tests/functional/n/nested_min_max_py39.py @@ -0,0 +1,6 @@ +"""Test detection of redundant nested calls to min/max functions""" + +# pylint: disable=redefined-builtin,unnecessary-lambda-assignment + +max(3, max({1: 2} | {i: i for i in range(4) if i})) # [nested-min-max] +max(3, *{1: 2} | {i: i for i in range(4) if i}) diff --git a/tests/functional/n/nested_min_max_py39.rc b/tests/functional/n/nested_min_max_py39.rc new file mode 100644 index 0000000000..16b75eea75 --- /dev/null +++ b/tests/functional/n/nested_min_max_py39.rc @@ -0,0 +1,2 @@ +[testoptions] +min_pyver=3.9 diff --git a/tests/functional/n/nested_min_max_py39.txt b/tests/functional/n/nested_min_max_py39.txt new file mode 100644 index 0000000000..49541ccc2c --- /dev/null +++ b/tests/functional/n/nested_min_max_py39.txt @@ -0,0 +1 @@ +nested-min-max:5:0:5:51::"Do not use nested call of 'max'; it's possible to do 'max(3, *{1: 2} | {i: i for i in range(4) if i})' instead":INFERENCE