Skip to content

Commit

Permalink
Correctly track loop depth for nested functions/classes (#15403)
Browse files Browse the repository at this point in the history
Fixes #15378
  • Loading branch information
ilevkivskyi authored and jhance committed Jun 12, 2023
1 parent a7f52db commit 9caf095
Show file tree
Hide file tree
Showing 2 changed files with 27 additions and 9 deletions.
22 changes: 13 additions & 9 deletions mypy/semanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -375,7 +375,7 @@ class SemanticAnalyzer(
missing_names: list[set[str]]
# Callbacks that will be called after semantic analysis to tweak things.
patches: list[tuple[int, Callable[[], None]]]
loop_depth = 0 # Depth of breakable loops
loop_depth: list[int] # Depth of breakable loops
cur_mod_id = "" # Current module id (or None) (phase 2)
_is_stub_file = False # Are we analyzing a stub file?
_is_typeshed_stub_file = False # Are we analyzing a typeshed stub file?
Expand Down Expand Up @@ -428,7 +428,7 @@ def __init__(
self.tvar_scope = TypeVarLikeScope()
self.function_stack = []
self.block_depth = [0]
self.loop_depth = 0
self.loop_depth = [0]
self.errors = errors
self.modules = modules
self.msg = MessageBuilder(errors, modules)
Expand Down Expand Up @@ -1808,12 +1808,14 @@ def enter_class(self, info: TypeInfo) -> None:
self.locals.append(None) # Add class scope
self.is_comprehension_stack.append(False)
self.block_depth.append(-1) # The class body increments this to 0
self.loop_depth.append(0)
self._type = info
self.missing_names.append(set())

def leave_class(self) -> None:
"""Restore analyzer state."""
self.block_depth.pop()
self.loop_depth.pop()
self.locals.pop()
self.is_comprehension_stack.pop()
self._type = self.type_stack.pop()
Expand Down Expand Up @@ -3219,7 +3221,7 @@ def unwrap_final(self, s: AssignmentStmt) -> bool:
if lval.is_new_def:
lval.is_inferred_def = s.type is None

if self.loop_depth > 0:
if self.loop_depth[-1] > 0:
self.fail("Cannot use Final inside a loop", s)
if self.type and self.type.is_protocol:
self.msg.protocol_members_cant_be_final(s)
Expand Down Expand Up @@ -4698,9 +4700,9 @@ def visit_operator_assignment_stmt(self, s: OperatorAssignmentStmt) -> None:
def visit_while_stmt(self, s: WhileStmt) -> None:
self.statement = s
s.expr.accept(self)
self.loop_depth += 1
self.loop_depth[-1] += 1
s.body.accept(self)
self.loop_depth -= 1
self.loop_depth[-1] -= 1
self.visit_block_maybe(s.else_body)

def visit_for_stmt(self, s: ForStmt) -> None:
Expand All @@ -4722,20 +4724,20 @@ def visit_for_stmt(self, s: ForStmt) -> None:
self.store_declared_types(s.index, analyzed)
s.index_type = analyzed

self.loop_depth += 1
self.loop_depth[-1] += 1
self.visit_block(s.body)
self.loop_depth -= 1
self.loop_depth[-1] -= 1

self.visit_block_maybe(s.else_body)

def visit_break_stmt(self, s: BreakStmt) -> None:
self.statement = s
if self.loop_depth == 0:
if self.loop_depth[-1] == 0:
self.fail('"break" outside loop', s, serious=True, blocker=True)

def visit_continue_stmt(self, s: ContinueStmt) -> None:
self.statement = s
if self.loop_depth == 0:
if self.loop_depth[-1] == 0:
self.fail('"continue" outside loop', s, serious=True, blocker=True)

def visit_if_stmt(self, s: IfStmt) -> None:
Expand Down Expand Up @@ -6230,6 +6232,7 @@ def enter(
self.nonlocal_decls.append(set())
# -1 since entering block will increment this to 0.
self.block_depth.append(-1)
self.loop_depth.append(0)
self.missing_names.append(set())
try:
yield
Expand All @@ -6239,6 +6242,7 @@ def enter(
self.global_decls.pop()
self.nonlocal_decls.pop()
self.block_depth.pop()
self.loop_depth.pop()
self.missing_names.pop()

def is_func_scope(self) -> bool:
Expand Down
14 changes: 14 additions & 0 deletions test-data/unit/check-statements.test
Original file line number Diff line number Diff line change
Expand Up @@ -2212,3 +2212,17 @@ main:1: error: Module "typing" has no attribute "_FutureFeatureFixture"
main:1: note: Use `from typing_extensions import _FutureFeatureFixture` instead
main:1: note: See https://mypy.readthedocs.io/en/stable/runtime_troubles.html#using-new-additions-to-the-typing-module
[builtins fixtures/tuple.pyi]

[case testNoCrashOnBreakOutsideLoopFunction]
def foo():
for x in [1, 2]:
def inner():
break # E: "break" outside loop
[builtins fixtures/list.pyi]

[case testNoCrashOnBreakOutsideLoopClass]
class Outer:
for x in [1, 2]:
class Inner:
break # E: "break" outside loop
[builtins fixtures/list.pyi]

0 comments on commit 9caf095

Please sign in to comment.