From 78b97b1e15391bdf57c83c4c446781b1b6a5e069 Mon Sep 17 00:00:00 2001 From: Pierre Sassoulas Date: Wed, 8 Oct 2025 09:58:38 +0200 Subject: [PATCH 1/3] [fix] Crash in 'nested-min-max' when using ``builtins.min`` instead of ``min`` directly. Closes #10626 --- doc/whatsnew/fragments/10626.bugfix | 4 +++ pylint/checkers/nested_min_max.py | 29 ++++++++++--------- .../r/regression_02/regression_10626.py | 11 +++++++ .../r/regression_02/regression_10626.txt | 6 ++++ 4 files changed, 37 insertions(+), 13 deletions(-) create mode 100644 doc/whatsnew/fragments/10626.bugfix create mode 100644 tests/functional/r/regression_02/regression_10626.py create mode 100644 tests/functional/r/regression_02/regression_10626.txt diff --git a/doc/whatsnew/fragments/10626.bugfix b/doc/whatsnew/fragments/10626.bugfix new file mode 100644 index 0000000000..0f109099da --- /dev/null +++ b/doc/whatsnew/fragments/10626.bugfix @@ -0,0 +1,4 @@ +Fix a crash in :ref:`nested-min-max` when using ``builtins.min`` or ``builtins.max`` +instead of ``min`` or ``max`` directly. + +Closes #10626 diff --git a/pylint/checkers/nested_min_max.py b/pylint/checkers/nested_min_max.py index 9af804c98e..561d4e0d4a 100644 --- a/pylint/checkers/nested_min_max.py +++ b/pylint/checkers/nested_min_max.py @@ -46,24 +46,26 @@ class NestedMinMaxChecker(BaseChecker): } @classmethod - def is_min_max_call(cls, node: nodes.NodeNG) -> bool: - if not isinstance(node, nodes.Call): - return False - + def get_inferred_min_max_call(cls, node: nodes.Call) -> nodes.FunctionDef | None: inferred = safe_infer(node.func) - return ( + if ( isinstance(inferred, nodes.FunctionDef) and inferred.qname() in cls.FUNC_NAMES - ) + ): + return inferred + return None @classmethod - def get_redundant_calls(cls, node: nodes.Call) -> list[nodes.Call]: + def get_redundant_calls( + cls, node: nodes.Call, inferred_call: nodes.FunctionDef + ) -> list[nodes.Call]: return [ arg for arg in node.args if ( - cls.is_min_max_call(arg) - and arg.func.name == node.func.name + isinstance(arg, nodes.Call) + and (inferred := cls.get_inferred_min_max_call(arg)) + and inferred.qname == inferred_call.qname # Nesting is useful for finding the maximum in a matrix. # Allow: max(max([[1, 2, 3], [4, 5, 6]])) # Meaning, redundant call only if parent max call has more than 1 arg. @@ -73,10 +75,11 @@ def get_redundant_calls(cls, node: nodes.Call) -> list[nodes.Call]: @only_required_for_messages("nested-min-max") def visit_call(self, node: nodes.Call) -> None: - if not self.is_min_max_call(node): + inferred = self.get_inferred_min_max_call(node) + if inferred is None: return - redundant_calls = self.get_redundant_calls(node) + redundant_calls = self.get_redundant_calls(node, inferred) if not redundant_calls: return @@ -96,7 +99,7 @@ def visit_call(self, node: nodes.Call) -> None: ) break - redundant_calls = self.get_redundant_calls(fixed_node) + redundant_calls = self.get_redundant_calls(fixed_node, inferred) for idx, arg in enumerate(fixed_node.args): if not isinstance(arg, nodes.Const): @@ -125,7 +128,7 @@ def visit_call(self, node: nodes.Call) -> None: self.add_message( "nested-min-max", node=node, - args=(node.func.name, fixed_node.as_string()), + args=(inferred.qname(), fixed_node.as_string()), confidence=INFERENCE, ) diff --git a/tests/functional/r/regression_02/regression_10626.py b/tests/functional/r/regression_02/regression_10626.py new file mode 100644 index 0000000000..42a9a9ab89 --- /dev/null +++ b/tests/functional/r/regression_02/regression_10626.py @@ -0,0 +1,11 @@ +""" Test case for issue #10626: nested builtins.min / builtins.max calls""" + +import builtins + +builtins.min(1, min(2, 3)) # [nested-min-max] +min(1, builtins.min(2, 3)) # [nested-min-max] +builtins.min(1, builtins.min(2, 3)) # [nested-min-max] + +builtins.max(1, max(2, 3)) # [nested-min-max] +max(1, builtins.max(2, 3)) # [nested-min-max] +builtins.max(1, builtins.max(2, 3)) # [nested-min-max] diff --git a/tests/functional/r/regression_02/regression_10626.txt b/tests/functional/r/regression_02/regression_10626.txt new file mode 100644 index 0000000000..2cbae5e61e --- /dev/null +++ b/tests/functional/r/regression_02/regression_10626.txt @@ -0,0 +1,6 @@ +nested-min-max:5:0:5:26::Do not use nested call of 'builtins.min'; it's possible to do 'builtins.min(1, 2, 3)' instead:INFERENCE +nested-min-max:6:0:6:26::Do not use nested call of 'builtins.min'; it's possible to do 'min(1, 2, 3)' instead:INFERENCE +nested-min-max:7:0:7:35::Do not use nested call of 'builtins.min'; it's possible to do 'builtins.min(1, 2, 3)' instead:INFERENCE +nested-min-max:9:0:9:26::Do not use nested call of 'builtins.max'; it's possible to do 'builtins.max(1, 2, 3)' instead:INFERENCE +nested-min-max:10:0:10:26::Do not use nested call of 'builtins.max'; it's possible to do 'max(1, 2, 3)' instead:INFERENCE +nested-min-max:11:0:11:35::Do not use nested call of 'builtins.max'; it's possible to do 'builtins.max(1, 2, 3)' instead:INFERENCE From 17934bb66b54b5149560dcb49e461068f992e58a Mon Sep 17 00:00:00 2001 From: Pierre Sassoulas Date: Wed, 8 Oct 2025 10:30:05 +0200 Subject: [PATCH 2/3] Use shortened original name even if it's not the qualified name --- pylint/checkers/nested_min_max.py | 8 ++++++-- .../functional/r/regression_02/regression_10626.txt | 12 ++++++------ 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/pylint/checkers/nested_min_max.py b/pylint/checkers/nested_min_max.py index 561d4e0d4a..5283d6975b 100644 --- a/pylint/checkers/nested_min_max.py +++ b/pylint/checkers/nested_min_max.py @@ -124,11 +124,15 @@ def visit_call(self, node: nodes.Call) -> None: splat_node, *fixed_node.args[idx + 1 : idx], ] - + func_name = ( + node.func.attrname + if isinstance(node.func, nodes.Attribute) + else node.func.name + ) self.add_message( "nested-min-max", node=node, - args=(inferred.qname(), fixed_node.as_string()), + args=(func_name, fixed_node.as_string()), confidence=INFERENCE, ) diff --git a/tests/functional/r/regression_02/regression_10626.txt b/tests/functional/r/regression_02/regression_10626.txt index 2cbae5e61e..732f22de05 100644 --- a/tests/functional/r/regression_02/regression_10626.txt +++ b/tests/functional/r/regression_02/regression_10626.txt @@ -1,6 +1,6 @@ -nested-min-max:5:0:5:26::Do not use nested call of 'builtins.min'; it's possible to do 'builtins.min(1, 2, 3)' instead:INFERENCE -nested-min-max:6:0:6:26::Do not use nested call of 'builtins.min'; it's possible to do 'min(1, 2, 3)' instead:INFERENCE -nested-min-max:7:0:7:35::Do not use nested call of 'builtins.min'; it's possible to do 'builtins.min(1, 2, 3)' instead:INFERENCE -nested-min-max:9:0:9:26::Do not use nested call of 'builtins.max'; it's possible to do 'builtins.max(1, 2, 3)' instead:INFERENCE -nested-min-max:10:0:10:26::Do not use nested call of 'builtins.max'; it's possible to do 'max(1, 2, 3)' instead:INFERENCE -nested-min-max:11:0:11:35::Do not use nested call of 'builtins.max'; it's possible to do 'builtins.max(1, 2, 3)' instead:INFERENCE +nested-min-max:5:0:5:26::Do not use nested call of 'min'; it's possible to do 'builtins.min(1, 2, 3)' instead:INFERENCE +nested-min-max:6:0:6:26::Do not use nested call of 'min'; it's possible to do 'min(1, 2, 3)' instead:INFERENCE +nested-min-max:7:0:7:35::Do not use nested call of 'min'; it's possible to do 'builtins.min(1, 2, 3)' instead:INFERENCE +nested-min-max:9:0:9:26::Do not use nested call of 'max'; it's possible to do 'builtins.max(1, 2, 3)' instead:INFERENCE +nested-min-max:10:0:10:26::Do not use nested call of 'max'; it's possible to do 'max(1, 2, 3)' instead:INFERENCE +nested-min-max:11:0:11:35::Do not use nested call of 'max'; it's possible to do 'builtins.max(1, 2, 3)' instead:INFERENCE From dd5549d28481481736b930b683d1d1183ba882d2 Mon Sep 17 00:00:00 2001 From: Pierre Sassoulas Date: Thu, 9 Oct 2025 13:46:07 +0200 Subject: [PATCH 3/3] get_inferred_min_max_call => maybe_get_inferred_min_max_call MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Daniƫl van Noord <13665637+DanielNoord@users.noreply.github.com> --- pylint/checkers/nested_min_max.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/pylint/checkers/nested_min_max.py b/pylint/checkers/nested_min_max.py index 5283d6975b..3a687ea564 100644 --- a/pylint/checkers/nested_min_max.py +++ b/pylint/checkers/nested_min_max.py @@ -46,7 +46,9 @@ class NestedMinMaxChecker(BaseChecker): } @classmethod - def get_inferred_min_max_call(cls, node: nodes.Call) -> nodes.FunctionDef | None: + def maybe_get_inferred_min_max_call( + cls, node: nodes.Call + ) -> nodes.FunctionDef | None: inferred = safe_infer(node.func) if ( isinstance(inferred, nodes.FunctionDef) @@ -64,7 +66,7 @@ def get_redundant_calls( for arg in node.args if ( isinstance(arg, nodes.Call) - and (inferred := cls.get_inferred_min_max_call(arg)) + and (inferred := cls.maybe_get_inferred_min_max_call(arg)) and inferred.qname == inferred_call.qname # Nesting is useful for finding the maximum in a matrix. # Allow: max(max([[1, 2, 3], [4, 5, 6]])) @@ -75,7 +77,7 @@ def get_redundant_calls( @only_required_for_messages("nested-min-max") def visit_call(self, node: nodes.Call) -> None: - inferred = self.get_inferred_min_max_call(node) + inferred = self.maybe_get_inferred_min_max_call(node) if inferred is None: return