Skip to content

Commit f5e168e

Browse files
DanielNoordDetachHeadjacobtylerwalls
authored andcommitted
Fix undefined-loop-variable with NoReturn and Never (#7476)
Co-authored-by: detachhead <[email protected]> Co-authored-by: Jacob Walls <[email protected]>
1 parent fbc9e66 commit f5e168e

File tree

8 files changed

+96
-18
lines changed

8 files changed

+96
-18
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Fix false positive for ``undefined-loop-variable`` in ``for-else`` loops that use a function
2+
having a return type annotation of ``NoReturn`` or ``Never``.
3+
4+
Closes #7311

pylint/checkers/variables.py

+35-8
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,20 @@
1919
from typing import TYPE_CHECKING, Any, NamedTuple
2020

2121
import astroid
22-
from astroid import extract_node, nodes
22+
from astroid import bases, extract_node, nodes
2323
from astroid.typing import InferenceResult
2424

2525
from pylint.checkers import BaseChecker, utils
2626
from pylint.checkers.utils import (
2727
in_type_checking_block,
2828
is_postponed_evaluation_enabled,
2929
)
30-
from pylint.constants import PY39_PLUS, TYPING_TYPE_CHECKS_GUARDS
30+
from pylint.constants import (
31+
PY39_PLUS,
32+
TYPING_NEVER,
33+
TYPING_NORETURN,
34+
TYPING_TYPE_CHECKS_GUARDS,
35+
)
3136
from pylint.interfaces import CONTROL_FLOW, HIGH, INFERENCE, INFERENCE_FAILURE
3237
from pylint.typing import MessageDefinitionTuple
3338

@@ -2245,13 +2250,35 @@ def _loopvar_name(self, node: astroid.Name) -> None:
22452250
if not isinstance(assign, nodes.For):
22462251
self.add_message("undefined-loop-variable", args=node.name, node=node)
22472252
return
2248-
if any(
2249-
isinstance(
2253+
for else_stmt in assign.orelse:
2254+
if isinstance(
22502255
else_stmt, (nodes.Return, nodes.Raise, nodes.Break, nodes.Continue)
2251-
)
2252-
for else_stmt in assign.orelse
2253-
):
2254-
return
2256+
):
2257+
return
2258+
# TODO: 2.16: Consider using RefactoringChecker._is_function_def_never_returning
2259+
if isinstance(else_stmt, nodes.Expr) and isinstance(
2260+
else_stmt.value, nodes.Call
2261+
):
2262+
inferred_func = utils.safe_infer(else_stmt.value.func)
2263+
if (
2264+
isinstance(inferred_func, nodes.FunctionDef)
2265+
and inferred_func.returns
2266+
):
2267+
inferred_return = utils.safe_infer(inferred_func.returns)
2268+
if isinstance(
2269+
inferred_return, nodes.FunctionDef
2270+
) and inferred_return.qname() in {
2271+
*TYPING_NORETURN,
2272+
*TYPING_NEVER,
2273+
"typing._SpecialForm",
2274+
}:
2275+
return
2276+
# typing_extensions.NoReturn returns a _SpecialForm
2277+
if (
2278+
isinstance(inferred_return, bases.Instance)
2279+
and inferred_return.qname() == "typing._SpecialForm"
2280+
):
2281+
return
22552282

22562283
maybe_walrus = utils.get_node_first_ancestor_of_type(node, nodes.NamedExpr)
22572284
if maybe_walrus:

pylint/constants.py

+13
Original file line numberDiff line numberDiff line change
@@ -155,3 +155,16 @@ def _get_pylint_home() -> str:
155155

156156

157157
PYLINT_HOME = _get_pylint_home()
158+
159+
TYPING_NORETURN = frozenset(
160+
(
161+
"typing.NoReturn",
162+
"typing_extensions.NoReturn",
163+
)
164+
)
165+
TYPING_NEVER = frozenset(
166+
(
167+
"typing.Never",
168+
"typing_extensions.Never",
169+
)
170+
)

pylint/extensions/typing.py

+1-6
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
only_required_for_messages,
1818
safe_infer,
1919
)
20+
from pylint.constants import TYPING_NORETURN
2021
from pylint.interfaces import INFERENCE
2122

2223
if TYPE_CHECKING:
@@ -75,12 +76,6 @@ class TypingAlias(NamedTuple):
7576

7677
ALIAS_NAMES = frozenset(key.split(".")[1] for key in DEPRECATED_TYPING_ALIASES)
7778
UNION_NAMES = ("Optional", "Union")
78-
TYPING_NORETURN = frozenset(
79-
(
80-
"typing.NoReturn",
81-
"typing_extensions.NoReturn",
82-
)
83-
)
8479

8580

8681
class DeprecatedTypingAliasMsg(NamedTuple):

tests/functional/u/undefined/undefined_loop_variable.py

+20
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
# pylint: disable=missing-docstring,redefined-builtin, consider-using-f-string, unnecessary-direct-lambda-call
22

3+
import sys
4+
5+
if sys.version_info >= (3, 8):
6+
from typing import NoReturn
7+
else:
8+
from typing_extensions import NoReturn
9+
10+
311
def do_stuff(some_random_list):
412
for var in some_random_list:
513
pass
@@ -125,6 +133,18 @@ def for_else_continue(iterable):
125133
print(thing)
126134

127135

136+
def for_else_no_return(iterable):
137+
def fail() -> NoReturn:
138+
...
139+
140+
while True:
141+
for thing in iterable:
142+
break
143+
else:
144+
fail()
145+
print(thing)
146+
147+
128148
lst = []
129149
lst2 = [1, 2, 3]
130150

Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
undefined-loop-variable:6:11:6:14:do_stuff:Using possibly undefined loop variable 'var':UNDEFINED
2-
undefined-loop-variable:25:7:25:11::Using possibly undefined loop variable 'var1':UNDEFINED
3-
undefined-loop-variable:75:11:75:14:do_stuff_with_redefined_range:Using possibly undefined loop variable 'var':UNDEFINED
4-
undefined-loop-variable:181:11:181:20:find_even_number:Using possibly undefined loop variable 'something':UNDEFINED
1+
undefined-loop-variable:14:11:14:14:do_stuff:Using possibly undefined loop variable 'var':UNDEFINED
2+
undefined-loop-variable:33:7:33:11::Using possibly undefined loop variable 'var1':UNDEFINED
3+
undefined-loop-variable:83:11:83:14:do_stuff_with_redefined_range:Using possibly undefined loop variable 'var':UNDEFINED
4+
undefined-loop-variable:201:11:201:20:find_even_number:Using possibly undefined loop variable 'something':UNDEFINED
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
"""Tests for undefined-loop-variable using Python 3.11 syntax."""
2+
3+
from typing import Never
4+
5+
6+
def for_else_never(iterable):
7+
"""Test for-else with Never type."""
8+
9+
def idontreturn() -> Never:
10+
"""This function never returns."""
11+
12+
while True:
13+
for thing in iterable:
14+
break
15+
else:
16+
idontreturn()
17+
print(thing)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[testoptions]
2+
min_pyver=3.11

0 commit comments

Comments
 (0)