From 2e338eb3d88ffeee941ab9538eec49e9cf16bfd5 Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Sun, 12 May 2024 13:05:48 -0400 Subject: [PATCH 1/2] Log RecursionError out as warning during inference Partner to pylint-dev/astroid#2385 --- doc/whatsnew/fragments/9139.bugfix | 3 ++ pylint/checkers/base/basic_checker.py | 9 +++++ pylint/checkers/base/comparison_checker.py | 2 +- pylint/checkers/classes/class_checker.py | 36 +++++++++++++++-- .../classes/special_methods_checker.py | 7 ++++ pylint/checkers/newstyle.py | 4 ++ .../implicit_booleaness_checker.py | 3 ++ .../refactoring/refactoring_checker.py | 4 +- pylint/checkers/stdlib.py | 7 ++++ pylint/checkers/strings.py | 6 +++ pylint/checkers/typecheck.py | 39 ++++++++++++------- pylint/checkers/utils.py | 36 +++++++++++++++++ pylint/checkers/variables.py | 12 ++++++ pylint/extensions/typing.py | 29 ++++++++------ pylint/pyreverse/utils.py | 8 ++++ 15 files changed, 173 insertions(+), 32 deletions(-) create mode 100644 doc/whatsnew/fragments/9139.bugfix diff --git a/doc/whatsnew/fragments/9139.bugfix b/doc/whatsnew/fragments/9139.bugfix new file mode 100644 index 0000000000..1f1ef0e441 --- /dev/null +++ b/doc/whatsnew/fragments/9139.bugfix @@ -0,0 +1,3 @@ +Log RecursionError out as warning during inference. + +Closes #9139 diff --git a/pylint/checkers/base/basic_checker.py b/pylint/checkers/base/basic_checker.py index bd31905282..d2c8e6601c 100644 --- a/pylint/checkers/base/basic_checker.py +++ b/pylint/checkers/base/basic_checker.py @@ -374,6 +374,9 @@ def _check_using_constant_test( call_inferred = list(inferred.infer_call_result(node)) except astroid.InferenceError: call_inferred = None + except RecursionError: + utils.warn_on_recursion_error() + call_inferred = None if call_inferred: self.add_message( "missing-parentheses-for-call-in-test", @@ -608,6 +611,9 @@ def is_iterable(internal_node: nodes.NodeNG) -> bool: value = next(default.infer()) except astroid.InferenceError: continue + except RecursionError: + utils.warn_on_recursion_error() + continue if ( isinstance(value, astroid.Instance) @@ -839,6 +845,9 @@ def _check_reversed(self, node: nodes.Call) -> None: func = next(node.args[0].func.infer()) except astroid.InferenceError: return + except RecursionError: + utils.warn_on_recursion_error() + return if getattr( func, "name", None ) == "iter" and utils.is_builtin_object(func): diff --git a/pylint/checkers/base/comparison_checker.py b/pylint/checkers/base/comparison_checker.py index 6fb053e2e1..d4cb21b087 100644 --- a/pylint/checkers/base/comparison_checker.py +++ b/pylint/checkers/base/comparison_checker.py @@ -148,7 +148,7 @@ def _is_float_nan(node: nodes.NodeNG) -> bool: if isinstance(node, nodes.Call) and len(node.args) == 1: if ( node.args[0].value.lower() == "nan" - and node.inferred()[0].pytype() == "builtins.float" + and utils.safe_infer(node).pytype() == "builtins.float" ): return True return False diff --git a/pylint/checkers/classes/class_checker.py b/pylint/checkers/classes/class_checker.py index ffe47ab156..531a438489 100644 --- a/pylint/checkers/classes/class_checker.py +++ b/pylint/checkers/classes/class_checker.py @@ -252,9 +252,12 @@ def _has_different_parameters_default_value( if is_same_fn is None: # If the default value comparison is unhandled, assume the value is different return True - if not is_same_fn(original_default, overridden_default): - # Two args with same type but different values - return True + try: + if not is_same_fn(original_default, overridden_default): + # Two args with same type but different values + return True + except RecursionError: + utils.warn_on_recursion_error() return False @@ -409,6 +412,9 @@ def _has_data_descriptor(cls: nodes.ClassDef, attr: str) -> bool: except astroid.InferenceError: # Can't infer, avoid emitting a false positive in this case. return True + except RecursionError: + utils.warn_on_recursion_error() + return True return False @@ -435,6 +441,9 @@ def _called_in_methods( bound = next(call.func.infer()) except (astroid.InferenceError, StopIteration): continue + except RecursionError: + utils.warn_on_recursion_error() + continue if not isinstance(bound, astroid.BoundMethod): continue func_obj = bound._proxied @@ -466,6 +475,9 @@ def _is_attribute_property(name: str, klass: nodes.ClassDef) -> bool: inferred = next(attr.infer()) except astroid.InferenceError: continue + except RecursionError: + utils.warn_on_recursion_error() + continue if isinstance(inferred, nodes.FunctionDef) and decorated_with_property( inferred ): @@ -483,7 +495,11 @@ def _is_attribute_property(name: str, klass: nodes.ClassDef) -> bool: def _has_same_layout_slots( slots: list[nodes.Const | None], assigned_value: nodes.Name ) -> bool: - inferred = next(assigned_value.infer()) + try: + inferred = next(assigned_value.infer()) + except RecursionError: + utils.warn_on_recursion_error() + return False if isinstance(inferred, nodes.ClassDef): other_slots = inferred.slots() if all( @@ -1278,6 +1294,9 @@ def visit_functiondef(self, node: nodes.FunctionDef) -> None: inferred = next(inferred.infer_call_result(inferred)) except astroid.InferenceError: return + except RecursionError: + utils.warn_on_recursion_error() + return try: if ( isinstance(inferred, (astroid.Instance, nodes.ClassDef)) @@ -1513,6 +1532,9 @@ def _check_slots(self, node: nodes.ClassDef) -> None: self._check_slots_elt(elt, node) except astroid.InferenceError: continue + except RecursionError: + utils.warn_on_recursion_error() + continue self._check_redefined_slots(node, slots, values) def _check_redefined_slots( @@ -2196,6 +2218,9 @@ def _check_init(self, node: nodes.FunctionDef, klass_node: nodes.ClassDef) -> No ) except astroid.InferenceError: continue + except RecursionError: + utils.warn_on_recursion_error() + continue for klass, method in not_called_yet.items(): # Check if the init of the class that defines this init has already # been called. @@ -2350,4 +2375,7 @@ def _ancestors_to_call( to_call[base_node] = init_node except astroid.InferenceError: continue + except RecursionError: + utils.warn_on_recursion_error() + continue return to_call diff --git a/pylint/checkers/classes/special_methods_checker.py b/pylint/checkers/classes/special_methods_checker.py index 025f285622..4015815817 100644 --- a/pylint/checkers/classes/special_methods_checker.py +++ b/pylint/checkers/classes/special_methods_checker.py @@ -21,6 +21,7 @@ is_function_body_ellipsis, only_required_for_messages, safe_infer, + warn_on_recursion_error, ) from pylint.lint.pylinter import PyLinter @@ -44,6 +45,9 @@ def _safe_infer_call_result( return None # inference failed except StopIteration: return None # no values inferred + except RecursionError: + warn_on_recursion_error() + return None try: next(inferit) return None # there is ambiguity on the inferred node @@ -51,6 +55,9 @@ def _safe_infer_call_result( return None # there is some kind of ambiguity except StopIteration: return value + except RecursionError: + warn_on_recursion_error() + return value class SpecialMethodsChecker(BaseChecker): diff --git a/pylint/checkers/newstyle.py b/pylint/checkers/newstyle.py index 0c2c559fe8..546444e1b9 100644 --- a/pylint/checkers/newstyle.py +++ b/pylint/checkers/newstyle.py @@ -16,6 +16,7 @@ has_known_bases, node_frame_class, only_required_for_messages, + warn_on_recursion_error, ) from pylint.typing import MessageDefinitionTuple @@ -107,6 +108,9 @@ def visit_functiondef(self, node: nodes.FunctionDef) -> None: supcls = call.args and next(call.args[0].infer(), None) except astroid.InferenceError: continue + except RecursionError: + warn_on_recursion_error() + continue # If the supcls is in the ancestors of klass super can be used to skip # a step in the mro() and get a method from a higher parent diff --git a/pylint/checkers/refactoring/implicit_booleaness_checker.py b/pylint/checkers/refactoring/implicit_booleaness_checker.py index 5818c2f4ac..74d033d987 100644 --- a/pylint/checkers/refactoring/implicit_booleaness_checker.py +++ b/pylint/checkers/refactoring/implicit_booleaness_checker.py @@ -144,6 +144,9 @@ def visit_call(self, node: nodes.Call) -> None: except astroid.InferenceError: # Probably undefined-variable, abort check return + except RecursionError: + utils.warn_on_recursion_error() + return mother_classes = self.base_names_of_instance(instance) affected_by_pep8 = any( t in mother_classes for t in ("str", "tuple", "list", "set") diff --git a/pylint/checkers/refactoring/refactoring_checker.py b/pylint/checkers/refactoring/refactoring_checker.py index 24d13c3a96..e6f9c64448 100644 --- a/pylint/checkers/refactoring/refactoring_checker.py +++ b/pylint/checkers/refactoring/refactoring_checker.py @@ -20,7 +20,7 @@ from pylint import checkers from pylint.checkers import utils from pylint.checkers.base.basic_error_checker import _loop_exits_early -from pylint.checkers.utils import node_frame_class +from pylint.checkers.utils import node_frame_class, safe_infer from pylint.interfaces import HIGH, INFERENCE, Confidence if TYPE_CHECKING: @@ -1997,7 +1997,7 @@ def _is_node_return_ended(self, node: nodes.NodeNG) -> bool: return True if isinstance(node, nodes.Call): try: - funcdef_node = node.func.inferred()[0] + funcdef_node = safe_infer(node.func) if self._is_function_def_never_returning(funcdef_node): return True except astroid.InferenceError: diff --git a/pylint/checkers/stdlib.py b/pylint/checkers/stdlib.py index 10c1d54bfc..71a3147be7 100644 --- a/pylint/checkers/stdlib.py +++ b/pylint/checkers/stdlib.py @@ -592,6 +592,8 @@ def _check_shallow_copy_environ(self, node: nodes.Call) -> None: confidence = INFERENCE try: inferred_args = arg.inferred() + except RecursionError: + utils.warn_on_recursion_error() except astroid.InferenceError: return for inferred in inferred_args: @@ -713,6 +715,8 @@ def _check_lru_cache_decorators(self, node: nodes.FunctionDef) -> None: break except astroid.InferenceError: pass + except RecursionError: + utils.warn_on_recursion_error() for lru_cache_node in lru_cache_nodes: self.add_message( "method-cache-max-size-none", @@ -767,6 +771,9 @@ def _check_datetime(self, node: nodes.NodeNG) -> None: inferred = next(node.infer()) except astroid.InferenceError: return + except RecursionError: + utils.warn_on_recursion_error() + return if isinstance(inferred, astroid.Instance) and inferred.qname() in { "_pydatetime.time", "datetime.time", diff --git a/pylint/checkers/strings.py b/pylint/checkers/strings.py index 90493fa006..ce9d883376 100644 --- a/pylint/checkers/strings.py +++ b/pylint/checkers/strings.py @@ -470,6 +470,9 @@ def _check_new_format(self, node: nodes.Call, func: bases.BoundMethod) -> None: strnode = next(func.bound.infer()) except astroid.InferenceError: return + except RecursionError: + utils.warn_on_recursion_error() + return if not (isinstance(strnode, nodes.Const) and isinstance(strnode.value, str)): return try: @@ -634,6 +637,9 @@ def _check_new_format_specifiers( except astroid.InferenceError: # can't check further if we can't infer it break + except RecursionError: + utils.warn_on_recursion_error() + break class StringConstantChecker(BaseTokenChecker, BaseRawFileChecker): diff --git a/pylint/checkers/typecheck.py b/pylint/checkers/typecheck.py index 56bd729c18..3f4ca8e7fc 100644 --- a/pylint/checkers/typecheck.py +++ b/pylint/checkers/typecheck.py @@ -48,6 +48,7 @@ supports_getitem, supports_membership_test, supports_setitem, + warn_on_recursion_error, ) from pylint.constants import PY310_PLUS from pylint.interfaces import HIGH, INFERENCE @@ -793,6 +794,9 @@ def _infer_from_metaclass_constructor( inferred = next(func.infer_call_result(func, context), None) except astroid.InferenceError: return None + except RecursionError: + utils.warn_on_recursion_error() + return None return inferred or None @@ -1087,6 +1091,9 @@ def visit_attribute( try: inferred = list(node.expr.infer()) + except RecursionError: + warn_on_recursion_error() + return except astroid.InferenceError: return @@ -1359,6 +1366,9 @@ def _check_uninferable_call(self, node: nodes.Call) -> None: call_results = list(attr.infer_call_result(node)) except astroid.InferenceError: continue + except RecursionError: + utils.warn_on_recursion_error() + continue if all( isinstance(return_node, util.UninferableBase) @@ -1673,21 +1683,24 @@ def _keyword_argument_is_in_all_decorator_returns( if not isinstance(inferred, nodes.FunctionDef): return False - for return_value in inferred.infer_call_result(caller=None): - # infer_call_result() returns nodes.Const.None for None return values - # so this also catches non-returning decorators - if not isinstance(return_value, nodes.FunctionDef): - return False - - # If the return value uses a kwarg the keyword will be consumed - if return_value.args.kwarg: - continue + try: + for return_value in inferred.infer_call_result(caller=None): + # infer_call_result() returns nodes.Const.None for None return values + # so this also catches non-returning decorators + if not isinstance(return_value, nodes.FunctionDef): + return False + + # If the return value uses a kwarg the keyword will be consumed + if return_value.args.kwarg: + continue - # Check if the keyword is another type of argument - if return_value.args.is_argument(keyword): - continue + # Check if the keyword is another type of argument + if return_value.args.is_argument(keyword): + continue - return False + return False + except RecursionError: + utils.warn_on_recursion_error() return True diff --git a/pylint/checkers/utils.py b/pylint/checkers/utils.py index a3e6496519..30313f605d 100644 --- a/pylint/checkers/utils.py +++ b/pylint/checkers/utils.py @@ -12,6 +12,7 @@ import numbers import re import string +import warnings from collections.abc import Iterable, Iterator from functools import lru_cache, partial from re import Match @@ -803,6 +804,8 @@ def decorated_with_property(node: nodes.FunctionDef) -> bool: return True except astroid.InferenceError: pass + except RecursionError: + warn_on_recursion_error() return False @@ -879,6 +882,9 @@ def decorated_with( return True except astroid.InferenceError: continue + except RecursionError: + warn_on_recursion_error() + continue return False @@ -1338,6 +1344,15 @@ def _get_python_type_of_node(node: nodes.NodeNG) -> str | None: return None +def warn_on_recursion_error(): + warnings.warn( + "Inference failed due to recursion limit. Retry with " + "--init-hook='import sys; sys.setrecursionlimit(2000)' or higher.", + UserWarning, + stacklevel=3, + ) + + @lru_cache(maxsize=1024) def safe_infer( node: nodes.NodeNG, @@ -1359,6 +1374,8 @@ def safe_infer( value = next(infer_gen) except astroid.InferenceError: return None + except RecursionError: + warn_on_recursion_error() except Exception as e: # pragma: no cover raise AstroidError from e @@ -1388,6 +1405,8 @@ def safe_infer( return None # There is some kind of ambiguity except StopIteration: return value + except RecursionError: + warn_on_recursion_error() except Exception as e: # pragma: no cover raise AstroidError from e return value if len(inferred_types) <= 1 else None @@ -1401,6 +1420,9 @@ def infer_all( return list(node.infer(context=context)) except astroid.InferenceError: return [] + except RecursionError: + warn_on_recursion_error() + return [] except Exception as e: # pragma: no cover raise AstroidError from e @@ -1479,6 +1501,9 @@ def node_type(node: nodes.NodeNG) -> SuccessfulInferenceResult | None: return None except astroid.InferenceError: return None + except RecursionError: + warn_on_recursion_error() + return None return types.pop() if types else None @@ -1510,6 +1535,9 @@ def is_registered_in_singledispatch_function(node: nodes.FunctionDef) -> bool: func_def = next(func.expr.infer()) except astroid.InferenceError: continue + except RecursionError: + warn_on_recursion_error() + continue if isinstance(func_def, nodes.FunctionDef): return decorated_with(func_def, singledispatch_qnames) @@ -1663,6 +1691,9 @@ def is_protocol_class(cls: nodes.NodeNG) -> bool: return True except astroid.InferenceError: continue + except RecursionError: + warn_on_recursion_error() + continue return False @@ -2041,6 +2072,9 @@ def is_hashable(node: nodes.NodeNG) -> bool: return False except astroid.InferenceError: return True + except RecursionError: + warn_on_recursion_error() + return True def subscript_chain_is_equal(left: nodes.Subscript, right: nodes.Subscript) -> bool: @@ -2165,6 +2199,8 @@ def is_terminating_func(node: nodes.Call) -> bool: and inferred.qname() in TERMINATING_FUNCS_QNAMES ): return True + except RecursionError: + warn_on_recursion_error() except (StopIteration, astroid.InferenceError): pass diff --git a/pylint/checkers/variables.py b/pylint/checkers/variables.py index 0a5a60c1b0..334f71e7cd 100644 --- a/pylint/checkers/variables.py +++ b/pylint/checkers/variables.py @@ -2667,6 +2667,9 @@ def _loopvar_name(self, node: astroid.Name) -> None: likely_call = assign.iter.body if isinstance(likely_call, nodes.Call): inferred = next(likely_call.args[0].infer()) + except RecursionError: + utils.warn_on_recursion_error() + self.add_message("undefined-loop-variable", args=node.name, node=node) except astroid.InferenceError: self.add_message("undefined-loop-variable", args=node.name, node=node) else: @@ -3117,6 +3120,9 @@ def _check_module_attrs( "no-name-in-module", args=(name, module.name), node=node ) return None + except RecursionError: + utils.warn_on_recursion_error() + return None except astroid.InferenceError: return None if module_names: @@ -3136,6 +3142,9 @@ def _check_all( assigned = next(node.igetattr("__all__")) except astroid.InferenceError: return + except RecursionError: + utils.warn_on_recursion_error() + return if isinstance(assigned, util.UninferableBase): return if assigned.pytype() not in {"builtins.list", "builtins.tuple"}: @@ -3147,6 +3156,9 @@ def _check_all( elt_name = next(elt.infer()) except astroid.InferenceError: continue + except RecursionError: + utils.warn_on_recursion_error() + continue if isinstance(elt_name, util.UninferableBase): continue if not elt_name.parent: diff --git a/pylint/extensions/typing.py b/pylint/extensions/typing.py index 2956465cf6..da1824c863 100644 --- a/pylint/extensions/typing.py +++ b/pylint/extensions/typing.py @@ -16,6 +16,7 @@ is_postponed_evaluation_enabled, only_required_for_messages, safe_infer, + warn_on_recursion_error, ) from pylint.constants import TYPING_NORETURN from pylint.interfaces import HIGH, INFERENCE @@ -435,18 +436,22 @@ def _check_broken_noreturn(self, node: nodes.Name | nodes.Attribute) -> None: ): return - for inferred in node.infer(): - # To deal with typing_extensions, don't use safe_infer - if ( - isinstance(inferred, (nodes.FunctionDef, nodes.ClassDef)) - and inferred.qname() in TYPING_NORETURN - # In Python 3.7 - 3.8, NoReturn is alias of '_SpecialForm' - or isinstance(inferred, astroid.bases.BaseInstance) - and isinstance(inferred._proxied, nodes.ClassDef) - and inferred._proxied.qname() == "typing._SpecialForm" - ): - self.add_message("broken-noreturn", node=node, confidence=INFERENCE) - break + try: + for inferred in node.infer(): + # To deal with typing_extensions, don't use safe_infer + if ( + isinstance(inferred, (nodes.FunctionDef, nodes.ClassDef)) + and inferred.qname() in TYPING_NORETURN + # In Python 3.7 - 3.8, NoReturn is alias of '_SpecialForm' + or isinstance(inferred, astroid.bases.BaseInstance) + and isinstance(inferred._proxied, nodes.ClassDef) + and inferred._proxied.qname() == "typing._SpecialForm" + ): + self.add_message("broken-noreturn", node=node, confidence=INFERENCE) + break + except RecursionError: + warn_on_recursion_error() + return def _check_broken_callable(self, node: nodes.Name | nodes.Attribute) -> None: """Check for 'collections.abc.Callable' inside Optional and Union.""" diff --git a/pylint/pyreverse/utils.py b/pylint/pyreverse/utils.py index bdd28dc7c3..67dc609468 100644 --- a/pylint/pyreverse/utils.py +++ b/pylint/pyreverse/utils.py @@ -17,6 +17,8 @@ from astroid import nodes from astroid.typing import InferenceResult +from pylint.checkers.utils import warn_on_recursion_error + if TYPE_CHECKING: from pylint.pyreverse.diagrams import ClassDiagram, PackageDiagram @@ -192,6 +194,9 @@ def get_annotation( default, *_ = node.infer() except astroid.InferenceError: default = "" + except RecursionError: + warn_on_recursion_error() + default = "" label = get_annotation_label(ann) @@ -229,6 +234,9 @@ def infer_node(node: nodes.AssignAttr | nodes.AssignName) -> set[InferenceResult return set(node.infer()) except astroid.InferenceError: return {ann} if ann else set() + except RecursionError: + warn_on_recursion_error() + return {ann} if ann else set() def check_graphviz_availability() -> None: From ed4fe10933cbcb24414a2bf73655278a27fc42c9 Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Sun, 12 May 2024 13:38:35 -0400 Subject: [PATCH 2/2] nits --- pylint/checkers/classes/class_checker.py | 2 +- pylint/checkers/typecheck.py | 1 + pylint/checkers/utils.py | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/pylint/checkers/classes/class_checker.py b/pylint/checkers/classes/class_checker.py index 531a438489..2d9e7885df 100644 --- a/pylint/checkers/classes/class_checker.py +++ b/pylint/checkers/classes/class_checker.py @@ -1232,7 +1232,7 @@ def _check_attribute_defined_outside_init(self, cnode: nodes.ClassDef) -> None: "attribute-defined-outside-init", args=attr, node=node ) - # pylint: disable = too-many-branches + # pylint: disable = too-many-branches, too-many-return-statements def visit_functiondef(self, node: nodes.FunctionDef) -> None: """Check method arguments, overriding.""" # ignore actual functions diff --git a/pylint/checkers/typecheck.py b/pylint/checkers/typecheck.py index 3f4ca8e7fc..c694a03fce 100644 --- a/pylint/checkers/typecheck.py +++ b/pylint/checkers/typecheck.py @@ -1661,6 +1661,7 @@ def visit_call(self, node: nodes.Call) -> None: confidence=INFERENCE, ) + # pylint: disable=too-many-try-statements @staticmethod def _keyword_argument_is_in_all_decorator_returns( func: nodes.FunctionDef, keyword: str diff --git a/pylint/checkers/utils.py b/pylint/checkers/utils.py index 30313f605d..18aaab1efa 100644 --- a/pylint/checkers/utils.py +++ b/pylint/checkers/utils.py @@ -1344,7 +1344,7 @@ def _get_python_type_of_node(node: nodes.NodeNG) -> str | None: return None -def warn_on_recursion_error(): +def warn_on_recursion_error() -> None: warnings.warn( "Inference failed due to recursion limit. Retry with " "--init-hook='import sys; sys.setrecursionlimit(2000)' or higher.",