Skip to content

Commit 499adae

Browse files
Added checks for invalid usage of continue/break/return in except* block (#18132)
Fixes #18123 This PR addresses an issue where mypy incorrectly allows break/continue/return statements in the except* block. (see https://peps.python.org/pep-0654/#forbidden-combinations)
1 parent 08340c2 commit 499adae

File tree

2 files changed

+125
-5
lines changed

2 files changed

+125
-5
lines changed

Diff for: mypy/semanal.py

+39-5
Original file line numberDiff line numberDiff line change
@@ -484,6 +484,12 @@ def __init__(
484484
# Used to pass information about current overload index to visit_func_def().
485485
self.current_overload_item: int | None = None
486486

487+
# Used to track whether currently inside an except* block. This helps
488+
# to invoke errors when continue/break/return is used inside except* block.
489+
self.inside_except_star_block: bool = False
490+
# Used to track edge case when return is still inside except* if it enters a loop
491+
self.return_stmt_inside_except_star_block: bool = False
492+
487493
# mypyc doesn't properly handle implementing an abstractproperty
488494
# with a regular attribute so we make them properties
489495
@property
@@ -511,6 +517,25 @@ def allow_unbound_tvars_set(self) -> Iterator[None]:
511517
finally:
512518
self.allow_unbound_tvars = old
513519

520+
@contextmanager
521+
def inside_except_star_block_set(
522+
self, value: bool, entering_loop: bool = False
523+
) -> Iterator[None]:
524+
old = self.inside_except_star_block
525+
self.inside_except_star_block = value
526+
527+
# Return statement would still be in except* scope if entering loops
528+
if not entering_loop:
529+
old_return_stmt_flag = self.return_stmt_inside_except_star_block
530+
self.return_stmt_inside_except_star_block = value
531+
532+
try:
533+
yield
534+
finally:
535+
self.inside_except_star_block = old
536+
if not entering_loop:
537+
self.return_stmt_inside_except_star_block = old_return_stmt_flag
538+
514539
#
515540
# Preparing module (performed before semantic analysis)
516541
#
@@ -877,7 +902,8 @@ def visit_func_def(self, defn: FuncDef) -> None:
877902
return
878903

879904
with self.scope.function_scope(defn):
880-
self.analyze_func_def(defn)
905+
with self.inside_except_star_block_set(value=False):
906+
self.analyze_func_def(defn)
881907

882908
def function_fullname(self, fullname: str) -> str:
883909
if self.current_overload_item is None:
@@ -5264,6 +5290,8 @@ def visit_return_stmt(self, s: ReturnStmt) -> None:
52645290
self.statement = s
52655291
if not self.is_func_scope():
52665292
self.fail('"return" outside function', s)
5293+
if self.return_stmt_inside_except_star_block:
5294+
self.fail('"return" not allowed in except* block', s, serious=True)
52675295
if s.expr:
52685296
s.expr.accept(self)
52695297

@@ -5297,7 +5325,8 @@ def visit_while_stmt(self, s: WhileStmt) -> None:
52975325
self.statement = s
52985326
s.expr.accept(self)
52995327
self.loop_depth[-1] += 1
5300-
s.body.accept(self)
5328+
with self.inside_except_star_block_set(value=False, entering_loop=True):
5329+
s.body.accept(self)
53015330
self.loop_depth[-1] -= 1
53025331
self.visit_block_maybe(s.else_body)
53035332

@@ -5321,20 +5350,24 @@ def visit_for_stmt(self, s: ForStmt) -> None:
53215350
s.index_type = analyzed
53225351

53235352
self.loop_depth[-1] += 1
5324-
self.visit_block(s.body)
5353+
with self.inside_except_star_block_set(value=False, entering_loop=True):
5354+
self.visit_block(s.body)
53255355
self.loop_depth[-1] -= 1
5326-
53275356
self.visit_block_maybe(s.else_body)
53285357

53295358
def visit_break_stmt(self, s: BreakStmt) -> None:
53305359
self.statement = s
53315360
if self.loop_depth[-1] == 0:
53325361
self.fail('"break" outside loop', s, serious=True, blocker=True)
5362+
if self.inside_except_star_block:
5363+
self.fail('"break" not allowed in except* block', s, serious=True)
53335364

53345365
def visit_continue_stmt(self, s: ContinueStmt) -> None:
53355366
self.statement = s
53365367
if self.loop_depth[-1] == 0:
53375368
self.fail('"continue" outside loop', s, serious=True, blocker=True)
5369+
if self.inside_except_star_block:
5370+
self.fail('"continue" not allowed in except* block', s, serious=True)
53385371

53395372
def visit_if_stmt(self, s: IfStmt) -> None:
53405373
self.statement = s
@@ -5355,7 +5388,8 @@ def analyze_try_stmt(self, s: TryStmt, visitor: NodeVisitor[None]) -> None:
53555388
type.accept(visitor)
53565389
if var:
53575390
self.analyze_lvalue(var)
5358-
handler.accept(visitor)
5391+
with self.inside_except_star_block_set(self.inside_except_star_block or s.is_star):
5392+
handler.accept(visitor)
53595393
if s.else_body:
53605394
s.else_body.accept(visitor)
53615395
if s.finally_body:

Diff for: test-data/unit/check-python311.test

+86
Original file line numberDiff line numberDiff line change
@@ -173,3 +173,89 @@ Alias4 = Callable[[*IntList], int] # E: "List[int]" cannot be unpacked (must be
173173
x4: Alias4[int] # E: Bad number of arguments for type alias, expected 0, given 1
174174
reveal_type(x4) # N: Revealed type is "def (*Any) -> builtins.int"
175175
[builtins fixtures/tuple.pyi]
176+
177+
[case testReturnInExceptStarBlock1]
178+
# flags: --python-version 3.11
179+
def foo() -> None:
180+
try:
181+
pass
182+
except* Exception:
183+
return # E: "return" not allowed in except* block
184+
finally:
185+
return
186+
[builtins fixtures/exception.pyi]
187+
188+
[case testReturnInExceptStarBlock2]
189+
# flags: --python-version 3.11
190+
def foo():
191+
while True:
192+
try:
193+
pass
194+
except* Exception:
195+
while True:
196+
return # E: "return" not allowed in except* block
197+
[builtins fixtures/exception.pyi]
198+
199+
[case testContinueInExceptBlockNestedInExceptStarBlock]
200+
# flags: --python-version 3.11
201+
while True:
202+
try:
203+
...
204+
except* Exception:
205+
try:
206+
...
207+
except Exception:
208+
continue # E: "continue" not allowed in except* block
209+
continue # E: "continue" not allowed in except* block
210+
[builtins fixtures/exception.pyi]
211+
212+
[case testReturnInExceptBlockNestedInExceptStarBlock]
213+
# flags: --python-version 3.11
214+
def foo():
215+
try:
216+
...
217+
except* Exception:
218+
try:
219+
...
220+
except Exception:
221+
return # E: "return" not allowed in except* block
222+
return # E: "return" not allowed in except* block
223+
[builtins fixtures/exception.pyi]
224+
225+
[case testBreakContinueReturnInExceptStarBlock1]
226+
# flags: --python-version 3.11
227+
from typing import Iterable
228+
def foo(x: Iterable[int]) -> None:
229+
for _ in x:
230+
try:
231+
pass
232+
except* Exception:
233+
continue # E: "continue" not allowed in except* block
234+
except* Exception:
235+
for _ in x:
236+
continue
237+
break # E: "break" not allowed in except* block
238+
except* Exception:
239+
return # E: "return" not allowed in except* block
240+
[builtins fixtures/exception.pyi]
241+
242+
[case testBreakContinueReturnInExceptStarBlock2]
243+
# flags: --python-version 3.11
244+
def foo():
245+
while True:
246+
try:
247+
pass
248+
except* Exception:
249+
def inner():
250+
while True:
251+
if 1 < 1:
252+
continue
253+
else:
254+
break
255+
return
256+
if 1 < 2:
257+
break # E: "break" not allowed in except* block
258+
if 1 < 2:
259+
continue # E: "continue" not allowed in except* block
260+
return # E: "return" not allowed in except* block
261+
[builtins fixtures/exception.pyi]

0 commit comments

Comments
 (0)