Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix undefined-loop-variable with NoReturn and Never #7476

Merged
merged 11 commits into from Sep 19, 2022
31 changes: 24 additions & 7 deletions pylint/checkers/variables.py
Expand Up @@ -28,7 +28,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

Expand Down Expand Up @@ -2254,13 +2259,25 @@ 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
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function doesn't actually infer but uses the attrname, which we also match not_typing.we_do_return.NoReturn. That isn't really safe..

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}:
return

maybe_walrus = utils.get_node_first_ancestor_of_type(node, nodes.NamedExpr)
if maybe_walrus:
Expand Down
13 changes: 13 additions & 0 deletions pylint/constants.py
Expand Up @@ -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",
)
)
7 changes: 1 addition & 6 deletions pylint/extensions/typing.py
Expand Up @@ -17,6 +17,7 @@
only_required_for_messages,
safe_infer,
)
from pylint.constants import TYPING_NORETURN
from pylint.interfaces import INFERENCE

if TYPE_CHECKING:
Expand Down Expand Up @@ -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):
Expand Down
47 changes: 30 additions & 17 deletions tests/functional/u/undefined/undefined_loop_variable.py
@@ -1,9 +1,12 @@
# pylint: disable=missing-docstring,redefined-builtin, consider-using-f-string, unnecessary-direct-lambda-call

from typing import NoReturn


def do_stuff(some_random_list):
for var in some_random_list:
pass
return var # [undefined-loop-variable]
return var # [undefined-loop-variable]
jacobtylerwalls marked this conversation as resolved.
Show resolved Hide resolved


def do_else(some_random_list):
Expand All @@ -14,15 +17,16 @@ def do_else(some_random_list):
var = 84
return var

__revision__ = 'yo'

__revision__ = "yo"

TEST_LC = [C for C in __revision__ if C.isalpha()]
B = [B for B in __revision__ if B.isalpha()]
VAR2 = B # nor this one
B = [B for B in __revision__ if B.isalpha()]
VAR2 = B # nor this one

for var1, var2 in TEST_LC:
var1 = var2 + 4
VAR3 = var1 # [undefined-loop-variable]
VAR3 = var1 # [undefined-loop-variable]

for note in __revision__:
note.something()
Expand Down Expand Up @@ -70,9 +74,10 @@ def do_stuff_with_a_range():
def do_stuff_with_redefined_range():
def range(key):
yield from [1, key]

for var in range(3):
pass
return var # [undefined-loop-variable]
return var # [undefined-loop-variable]


def test(content):
Expand All @@ -81,13 +86,13 @@ def handle_line(layne):
if "X" in layne:
layne = layne.replace("X", "Y")
elif "Y" in layne: # line 5
layne = '{}'.format(layne)
layne = "{}".format(layne)
elif "Z" in layne: # line 7
layne = f'{layne}'
layne = f"{layne}"
else:
layne = '%s' % layne # line 10
layne = "%s" % layne # line 10

for layne in content.split('\n'):
for layne in content.split("\n"):
handle_line(layne)


Expand Down Expand Up @@ -125,25 +130,33 @@ 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]

for item in lst:
pass

bigger = [
[
x for x in lst2 if x > item
]
for item in lst
]
bigger = [[x for x in lst2 if x > item] for item in lst]
jacobtylerwalls marked this conversation as resolved.
Show resolved Hide resolved


def lambda_in_first_of_two_loops():
"""https://github.com/PyCQA/pylint/issues/6419"""
my_list = []
for thing in my_list:
print_it = lambda: print(thing) # pylint: disable=cell-var-from-loop, unnecessary-lambda-assignment
# pylint: disable-next=cell-var-from-loop, unnecessary-lambda-assignment
print_it = lambda: print(thing)
print_it()

for thing in my_list:
Expand Down
8 changes: 4 additions & 4 deletions 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:9:11:9:14:do_stuff:Using possibly undefined loop variable 'var':UNDEFINED
undefined-loop-variable:29:7:29:11::Using possibly undefined loop variable 'var1':UNDEFINED
undefined-loop-variable:80:11:80:14:do_stuff_with_redefined_range:Using possibly undefined loop variable 'var':UNDEFINED
undefined-loop-variable:194:11:194:20:find_even_number:Using possibly undefined loop variable 'something':UNDEFINED
17 changes: 17 additions & 0 deletions 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)
2 changes: 2 additions & 0 deletions tests/functional/u/undefined/undefined_loop_variable_py311.rc
@@ -0,0 +1,2 @@
[testoptions]
min_pyver=3.11