From f5e168e867799013fb380aa9fe8a0c1516a651c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20van=20Noord?= <13665637+DanielNoord@users.noreply.github.com> Date: Mon, 19 Sep 2022 14:12:45 +0200 Subject: [PATCH] Fix `undefined-loop-variable` with `NoReturn` and `Never` (#7476) Co-authored-by: detachhead Co-authored-by: Jacob Walls --- doc/whatsnew/fragments/7311.false_positive | 4 ++ pylint/checkers/variables.py | 43 +++++++++++++++---- pylint/constants.py | 13 ++++++ pylint/extensions/typing.py | 7 +-- .../u/undefined/undefined_loop_variable.py | 20 +++++++++ .../u/undefined/undefined_loop_variable.txt | 8 ++-- .../undefined_loop_variable_py311.py | 17 ++++++++ .../undefined_loop_variable_py311.rc | 2 + 8 files changed, 96 insertions(+), 18 deletions(-) create mode 100644 doc/whatsnew/fragments/7311.false_positive create mode 100644 tests/functional/u/undefined/undefined_loop_variable_py311.py create mode 100644 tests/functional/u/undefined/undefined_loop_variable_py311.rc diff --git a/doc/whatsnew/fragments/7311.false_positive b/doc/whatsnew/fragments/7311.false_positive new file mode 100644 index 0000000000..84d57502b4 --- /dev/null +++ b/doc/whatsnew/fragments/7311.false_positive @@ -0,0 +1,4 @@ +Fix false positive for ``undefined-loop-variable`` in ``for-else`` loops that use a function +having a return type annotation of ``NoReturn`` or ``Never``. + +Closes #7311 diff --git a/pylint/checkers/variables.py b/pylint/checkers/variables.py index 7d2488374a..632f75447a 100644 --- a/pylint/checkers/variables.py +++ b/pylint/checkers/variables.py @@ -19,7 +19,7 @@ from typing import TYPE_CHECKING, Any, NamedTuple import astroid -from astroid import extract_node, nodes +from astroid import bases, extract_node, nodes from astroid.typing import InferenceResult from pylint.checkers import BaseChecker, utils @@ -27,7 +27,12 @@ in_type_checking_block, is_postponed_evaluation_enabled, ) -from pylint.constants import PY39_PLUS, TYPING_TYPE_CHECKS_GUARDS +from pylint.constants import ( + PY39_PLUS, + TYPING_NEVER, + TYPING_NORETURN, + TYPING_TYPE_CHECKS_GUARDS, +) from pylint.interfaces import CONTROL_FLOW, HIGH, INFERENCE, INFERENCE_FAILURE from pylint.typing import MessageDefinitionTuple @@ -2245,13 +2250,35 @@ def _loopvar_name(self, node: astroid.Name) -> None: if not isinstance(assign, nodes.For): self.add_message("undefined-loop-variable", args=node.name, node=node) return - if any( - isinstance( + for else_stmt in assign.orelse: + if isinstance( else_stmt, (nodes.Return, nodes.Raise, nodes.Break, nodes.Continue) - ) - for else_stmt in assign.orelse - ): - return + ): + return + # TODO: 2.16: Consider using RefactoringChecker._is_function_def_never_returning + if isinstance(else_stmt, nodes.Expr) and isinstance( + else_stmt.value, nodes.Call + ): + inferred_func = utils.safe_infer(else_stmt.value.func) + if ( + isinstance(inferred_func, nodes.FunctionDef) + and inferred_func.returns + ): + inferred_return = utils.safe_infer(inferred_func.returns) + if isinstance( + inferred_return, nodes.FunctionDef + ) and inferred_return.qname() in { + *TYPING_NORETURN, + *TYPING_NEVER, + "typing._SpecialForm", + }: + return + # typing_extensions.NoReturn returns a _SpecialForm + if ( + isinstance(inferred_return, bases.Instance) + and inferred_return.qname() == "typing._SpecialForm" + ): + return maybe_walrus = utils.get_node_first_ancestor_of_type(node, nodes.NamedExpr) if maybe_walrus: diff --git a/pylint/constants.py b/pylint/constants.py index a609f9cd6f..6ad5b82d35 100644 --- a/pylint/constants.py +++ b/pylint/constants.py @@ -155,3 +155,16 @@ def _get_pylint_home() -> str: PYLINT_HOME = _get_pylint_home() + +TYPING_NORETURN = frozenset( + ( + "typing.NoReturn", + "typing_extensions.NoReturn", + ) +) +TYPING_NEVER = frozenset( + ( + "typing.Never", + "typing_extensions.Never", + ) +) diff --git a/pylint/extensions/typing.py b/pylint/extensions/typing.py index 615f40a48e..5f89cd5613 100644 --- a/pylint/extensions/typing.py +++ b/pylint/extensions/typing.py @@ -17,6 +17,7 @@ only_required_for_messages, safe_infer, ) +from pylint.constants import TYPING_NORETURN from pylint.interfaces import INFERENCE if TYPE_CHECKING: @@ -75,12 +76,6 @@ class TypingAlias(NamedTuple): ALIAS_NAMES = frozenset(key.split(".")[1] for key in DEPRECATED_TYPING_ALIASES) UNION_NAMES = ("Optional", "Union") -TYPING_NORETURN = frozenset( - ( - "typing.NoReturn", - "typing_extensions.NoReturn", - ) -) class DeprecatedTypingAliasMsg(NamedTuple): diff --git a/tests/functional/u/undefined/undefined_loop_variable.py b/tests/functional/u/undefined/undefined_loop_variable.py index 3e096472d9..9ab08d5953 100644 --- a/tests/functional/u/undefined/undefined_loop_variable.py +++ b/tests/functional/u/undefined/undefined_loop_variable.py @@ -1,5 +1,13 @@ # pylint: disable=missing-docstring,redefined-builtin, consider-using-f-string, unnecessary-direct-lambda-call +import sys + +if sys.version_info >= (3, 8): + from typing import NoReturn +else: + from typing_extensions import NoReturn + + def do_stuff(some_random_list): for var in some_random_list: pass @@ -125,6 +133,18 @@ def for_else_continue(iterable): print(thing) +def for_else_no_return(iterable): + def fail() -> NoReturn: + ... + + while True: + for thing in iterable: + break + else: + fail() + print(thing) + + lst = [] lst2 = [1, 2, 3] diff --git a/tests/functional/u/undefined/undefined_loop_variable.txt b/tests/functional/u/undefined/undefined_loop_variable.txt index dce1300ce3..e10c9e0021 100644 --- a/tests/functional/u/undefined/undefined_loop_variable.txt +++ b/tests/functional/u/undefined/undefined_loop_variable.txt @@ -1,4 +1,4 @@ -undefined-loop-variable:6:11:6:14:do_stuff:Using possibly undefined loop variable 'var':UNDEFINED -undefined-loop-variable:25:7:25:11::Using possibly undefined loop variable 'var1':UNDEFINED -undefined-loop-variable:75:11:75:14:do_stuff_with_redefined_range:Using possibly undefined loop variable 'var':UNDEFINED -undefined-loop-variable:181:11:181:20:find_even_number:Using possibly undefined loop variable 'something':UNDEFINED +undefined-loop-variable:14:11:14:14:do_stuff:Using possibly undefined loop variable 'var':UNDEFINED +undefined-loop-variable:33:7:33:11::Using possibly undefined loop variable 'var1':UNDEFINED +undefined-loop-variable:83:11:83:14:do_stuff_with_redefined_range:Using possibly undefined loop variable 'var':UNDEFINED +undefined-loop-variable:201:11:201:20:find_even_number:Using possibly undefined loop variable 'something':UNDEFINED diff --git a/tests/functional/u/undefined/undefined_loop_variable_py311.py b/tests/functional/u/undefined/undefined_loop_variable_py311.py new file mode 100644 index 0000000000..93b43a5468 --- /dev/null +++ b/tests/functional/u/undefined/undefined_loop_variable_py311.py @@ -0,0 +1,17 @@ +"""Tests for undefined-loop-variable using Python 3.11 syntax.""" + +from typing import Never + + +def for_else_never(iterable): + """Test for-else with Never type.""" + + def idontreturn() -> Never: + """This function never returns.""" + + while True: + for thing in iterable: + break + else: + idontreturn() + print(thing) diff --git a/tests/functional/u/undefined/undefined_loop_variable_py311.rc b/tests/functional/u/undefined/undefined_loop_variable_py311.rc new file mode 100644 index 0000000000..56e6770585 --- /dev/null +++ b/tests/functional/u/undefined/undefined_loop_variable_py311.rc @@ -0,0 +1,2 @@ +[testoptions] +min_pyver=3.11