Skip to content

Commit 3bf8521

Browse files
authored
Consistently avoid type-checking unreachable code (#15386)
- On module-level, now we'll skip remaining statements once unreachable. This brings the behavior in line with function-level behavior. - For module and function code, if `--warn-unreachable` is enabled, we'll emit an error, just once, on the first unreachable statement that's not a no-op statement. Previously a no-op statement would not have the "Unreachable statement" error, but the subsequent statements did not have the error either, e.g. ```diff raise Exception assert False # no error since it's a "no-op statement" -foo = 42 +foo = 42 # E: Unreachable statement spam = "ham" # no error since we warn just once ```
1 parent dfea43f commit 3bf8521

11 files changed

+120
-80
lines changed

mypy/checker.py

+16-13
Original file line numberDiff line numberDiff line change
@@ -464,14 +464,14 @@ def check_first_pass(self) -> None:
464464
with self.tscope.module_scope(self.tree.fullname):
465465
with self.enter_partial_types(), self.binder.top_frame_context():
466466
for d in self.tree.defs:
467-
if (
468-
self.binder.is_unreachable()
469-
and self.should_report_unreachable_issues()
470-
and not self.is_raising_or_empty(d)
471-
):
472-
self.msg.unreachable_statement(d)
473-
break
474-
self.accept(d)
467+
if self.binder.is_unreachable():
468+
if not self.should_report_unreachable_issues():
469+
break
470+
if not self.is_noop_for_reachability(d):
471+
self.msg.unreachable_statement(d)
472+
break
473+
else:
474+
self.accept(d)
475475

476476
assert not self.current_node_deferred
477477

@@ -2706,10 +2706,13 @@ def visit_block(self, b: Block) -> None:
27062706
return
27072707
for s in b.body:
27082708
if self.binder.is_unreachable():
2709-
if self.should_report_unreachable_issues() and not self.is_raising_or_empty(s):
2709+
if not self.should_report_unreachable_issues():
2710+
break
2711+
if not self.is_noop_for_reachability(s):
27102712
self.msg.unreachable_statement(s)
2711-
break
2712-
self.accept(s)
2713+
break
2714+
else:
2715+
self.accept(s)
27132716

27142717
def should_report_unreachable_issues(self) -> bool:
27152718
return (
@@ -2719,11 +2722,11 @@ def should_report_unreachable_issues(self) -> bool:
27192722
and not self.binder.is_unreachable_warning_suppressed()
27202723
)
27212724

2722-
def is_raising_or_empty(self, s: Statement) -> bool:
2725+
def is_noop_for_reachability(self, s: Statement) -> bool:
27232726
"""Returns 'true' if the given statement either throws an error of some kind
27242727
or is a no-op.
27252728
2726-
We use this function mostly while handling the '--warn-unreachable' flag. When
2729+
We use this function while handling the '--warn-unreachable' flag. When
27272730
that flag is present, we normally report an error on any unreachable statement.
27282731
But if that statement is just something like a 'pass' or a just-in-case 'assert False',
27292732
reporting an error would be annoying.

mypyc/test-data/run-misc.test

+6-17
Original file line numberDiff line numberDiff line change
@@ -1108,25 +1108,14 @@ assert not C
11081108
# make the initial import fail
11091109
assert False
11101110

1111-
class C:
1112-
def __init__(self):
1113-
self.x = 1
1114-
self.y = 2
1115-
def test() -> None:
1116-
a = C()
11171111
[file driver.py]
11181112
# load native, cause PyInit to be run, create the module but don't finish initializing the globals
1119-
try:
1120-
import native
1121-
except:
1122-
pass
1123-
try:
1124-
# try accessing those globals that were never properly initialized
1125-
import native
1126-
native.test()
1127-
# should fail with AssertionError due to `assert False` in other function
1128-
except AssertionError:
1129-
pass
1113+
for _ in range(2):
1114+
try:
1115+
import native
1116+
raise RuntimeError('exception expected')
1117+
except AssertionError:
1118+
pass
11301119

11311120
[case testRepeatedUnderscoreFunctions]
11321121
def _(arg): pass

test-data/unit/check-classes.test

+19-8
Original file line numberDiff line numberDiff line change
@@ -7725,10 +7725,14 @@ class D:
77257725
def __new__(cls) -> NoReturn: ...
77267726
def __init__(self) -> NoReturn: ...
77277727

7728-
reveal_type(A()) # N: Revealed type is "<nothing>"
7729-
reveal_type(B()) # N: Revealed type is "<nothing>"
7730-
reveal_type(C()) # N: Revealed type is "<nothing>"
7731-
reveal_type(D()) # N: Revealed type is "<nothing>"
7728+
if object():
7729+
reveal_type(A()) # N: Revealed type is "<nothing>"
7730+
if object():
7731+
reveal_type(B()) # N: Revealed type is "<nothing>"
7732+
if object():
7733+
reveal_type(C()) # N: Revealed type is "<nothing>"
7734+
if object():
7735+
reveal_type(D()) # N: Revealed type is "<nothing>"
77327736

77337737
[case testOverloadedNewAndInitNoReturn]
77347738
from typing import NoReturn, overload
@@ -7767,13 +7771,20 @@ class D:
77677771
def __init__(self, a: int) -> None: ...
77687772
def __init__(self, a: int = ...) -> None: ...
77697773

7770-
reveal_type(A()) # N: Revealed type is "<nothing>"
7774+
if object():
7775+
reveal_type(A()) # N: Revealed type is "<nothing>"
77717776
reveal_type(A(1)) # N: Revealed type is "__main__.A"
7772-
reveal_type(B()) # N: Revealed type is "<nothing>"
7777+
7778+
if object():
7779+
reveal_type(B()) # N: Revealed type is "<nothing>"
77737780
reveal_type(B(1)) # N: Revealed type is "__main__.B"
7774-
reveal_type(C()) # N: Revealed type is "<nothing>"
7781+
7782+
if object():
7783+
reveal_type(C()) # N: Revealed type is "<nothing>"
77757784
reveal_type(C(1)) # N: Revealed type is "__main__.C"
7776-
reveal_type(D()) # N: Revealed type is "<nothing>"
7785+
7786+
if object():
7787+
reveal_type(D()) # N: Revealed type is "<nothing>"
77777788
reveal_type(D(1)) # N: Revealed type is "__main__.D"
77787789

77797790
[case testClassScopeImportWithWrapperAndError]

test-data/unit/check-fastparse.test

+1-1
Original file line numberDiff line numberDiff line change
@@ -228,8 +228,8 @@ def g(): # E: Type signature has too many arguments
228228
assert 1, 2
229229
assert (1, 2) # E: Assertion is always true, perhaps remove parentheses?
230230
assert (1, 2), 3 # E: Assertion is always true, perhaps remove parentheses?
231-
assert ()
232231
assert (1,) # E: Assertion is always true, perhaps remove parentheses?
232+
assert ()
233233
[builtins fixtures/tuple.pyi]
234234

235235
[case testFastParseAssertMessage]

test-data/unit/check-incremental.test

+4-2
Original file line numberDiff line numberDiff line change
@@ -5413,7 +5413,8 @@ reveal_type(z)
54135413
[out]
54145414
tmp/c.py:2: note: Revealed type is "a.<subclass of "A" and "B">"
54155415
[out2]
5416-
tmp/c.py:2: note: Revealed type is "a.A"
5416+
tmp/b.py:2: error: Cannot determine type of "y"
5417+
tmp/c.py:2: note: Revealed type is "Any"
54175418

54185419
[case testIsInstanceAdHocIntersectionIncrementalUnreachaableToIntersection]
54195420
import c
@@ -5444,7 +5445,8 @@ from b import z
54445445
reveal_type(z)
54455446
[builtins fixtures/isinstance.pyi]
54465447
[out]
5447-
tmp/c.py:2: note: Revealed type is "a.A"
5448+
tmp/b.py:2: error: Cannot determine type of "y"
5449+
tmp/c.py:2: note: Revealed type is "Any"
54485450
[out2]
54495451
tmp/c.py:2: note: Revealed type is "a.<subclass of "A" and "B">"
54505452

test-data/unit/check-inference-context.test

+4-2
Original file line numberDiff line numberDiff line change
@@ -625,8 +625,10 @@ reveal_type((lambda x, y: x + y)(1, 2)) # N: Revealed type is "builtins.int"
625625
reveal_type((lambda s, i: s)(i=0, s='x')) # N: Revealed type is "Literal['x']?"
626626
reveal_type((lambda s, i: i)(i=0, s='x')) # N: Revealed type is "Literal[0]?"
627627
reveal_type((lambda x, s, i: x)(1.0, i=0, s='x')) # N: Revealed type is "builtins.float"
628-
(lambda x, s, i: x)() # E: Too few arguments
629-
(lambda: 0)(1) # E: Too many arguments
628+
if object():
629+
(lambda x, s, i: x)() # E: Too few arguments
630+
if object():
631+
(lambda: 0)(1) # E: Too many arguments
630632
-- varargs are not handled, but it should not crash
631633
reveal_type((lambda *k, s, i: i)(type, i=0, s='x')) # N: Revealed type is "Any"
632634
reveal_type((lambda s, *k, i: i)(i=0, s='x')) # N: Revealed type is "Any"

test-data/unit/check-native-int.test

+8-4
Original file line numberDiff line numberDiff line change
@@ -87,8 +87,10 @@ reveal_type(meet(f32, f)) # N: Revealed type is "mypy_extensions.i32"
8787
reveal_type(meet(f, f32)) # N: Revealed type is "mypy_extensions.i32"
8888
reveal_type(meet(f64, f)) # N: Revealed type is "mypy_extensions.i64"
8989
reveal_type(meet(f, f64)) # N: Revealed type is "mypy_extensions.i64"
90-
reveal_type(meet(f32, f64)) # N: Revealed type is "<nothing>"
91-
reveal_type(meet(f64, f32)) # N: Revealed type is "<nothing>"
90+
if object():
91+
reveal_type(meet(f32, f64)) # N: Revealed type is "<nothing>"
92+
if object():
93+
reveal_type(meet(f64, f32)) # N: Revealed type is "<nothing>"
9294

9395
reveal_type(meet(f, fa)) # N: Revealed type is "builtins.int"
9496
reveal_type(meet(f32, fa)) # N: Revealed type is "mypy_extensions.i32"
@@ -148,8 +150,10 @@ def meet(c1: Callable[[T], None], c2: Callable[[T], None]) -> T:
148150
def ff(x: float) -> None: pass
149151
def fi32(x: i32) -> None: pass
150152

151-
reveal_type(meet(ff, fi32)) # N: Revealed type is "<nothing>"
152-
reveal_type(meet(fi32, ff)) # N: Revealed type is "<nothing>"
153+
if object():
154+
reveal_type(meet(ff, fi32)) # N: Revealed type is "<nothing>"
155+
if object():
156+
reveal_type(meet(fi32, ff)) # N: Revealed type is "<nothing>"
153157
[builtins fixtures/dict.pyi]
154158

155159
[case testNativeIntForLoopRange]

test-data/unit/check-statements.test

+52-26
Original file line numberDiff line numberDiff line change
@@ -409,11 +409,16 @@ main:5: error: Exception must be derived from BaseException
409409
class A: pass
410410
class MyError(BaseException): pass
411411
def f(): pass
412-
raise BaseException
413-
raise MyError
414-
raise A # E: Exception must be derived from BaseException
415-
raise object # E: Exception must be derived from BaseException
416-
raise f # E: Exception must be derived from BaseException
412+
if object():
413+
raise BaseException
414+
if object():
415+
raise MyError
416+
if object():
417+
raise A # E: Exception must be derived from BaseException
418+
if object():
419+
raise object # E: Exception must be derived from BaseException
420+
if object():
421+
raise f # E: Exception must be derived from BaseException
417422
[builtins fixtures/exception.pyi]
418423

419424
[case testRaiseClassObjectCustomInit]
@@ -429,18 +434,30 @@ class MyKwError(Exception):
429434
class MyErrorWithDefault(Exception):
430435
def __init__(self, optional=1) -> None:
431436
...
432-
raise BaseException
433-
raise Exception
434-
raise BaseException(1)
435-
raise Exception(2)
436-
raise MyBaseError(4)
437-
raise MyError(5, 6)
438-
raise MyKwError(kwonly=7)
439-
raise MyErrorWithDefault(8)
440-
raise MyErrorWithDefault
441-
raise MyBaseError # E: Too few arguments for "MyBaseError"
442-
raise MyError # E: Too few arguments for "MyError"
443-
raise MyKwError # E: Missing named argument "kwonly" for "MyKwError"
437+
if object():
438+
raise BaseException
439+
if object():
440+
raise Exception
441+
if object():
442+
raise BaseException(1)
443+
if object():
444+
raise Exception(2)
445+
if object():
446+
raise MyBaseError(4)
447+
if object():
448+
raise MyError(5, 6)
449+
if object():
450+
raise MyKwError(kwonly=7)
451+
if object():
452+
raise MyErrorWithDefault(8)
453+
if object():
454+
raise MyErrorWithDefault
455+
if object():
456+
raise MyBaseError # E: Too few arguments for "MyBaseError"
457+
if object():
458+
raise MyError # E: Too few arguments for "MyError"
459+
if object():
460+
raise MyKwError # E: Missing named argument "kwonly" for "MyKwError"
444461
[builtins fixtures/exception.pyi]
445462

446463
[case testRaiseExceptionType]
@@ -473,10 +490,14 @@ f: MyError
473490
a: A
474491
x: BaseException
475492
del x
476-
raise e from a # E: Exception must be derived from BaseException
477-
raise e from e
478-
raise e from f
479-
raise e from x # E: Trying to read deleted variable "x"
493+
if object():
494+
raise e from a # E: Exception must be derived from BaseException
495+
if object():
496+
raise e from e
497+
if object():
498+
raise e from f
499+
if object():
500+
raise e from x # E: Trying to read deleted variable "x"
480501
class A: pass
481502
class MyError(BaseException): pass
482503
[builtins fixtures/exception.pyi]
@@ -486,11 +507,16 @@ import typing
486507
class A: pass
487508
class MyError(BaseException): pass
488509
def f(): pass
489-
raise BaseException from BaseException
490-
raise BaseException from MyError
491-
raise BaseException from A # E: Exception must be derived from BaseException
492-
raise BaseException from object # E: Exception must be derived from BaseException
493-
raise BaseException from f # E: Exception must be derived from BaseException
510+
if object():
511+
raise BaseException from BaseException
512+
if object():
513+
raise BaseException from MyError
514+
if object():
515+
raise BaseException from A # E: Exception must be derived from BaseException
516+
if object():
517+
raise BaseException from object # E: Exception must be derived from BaseException
518+
if object():
519+
raise BaseException from f # E: Exception must be derived from BaseException
494520
[builtins fixtures/exception.pyi]
495521

496522
[case testTryFinallyStatement]

test-data/unit/check-typevar-tuple.test

+2-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ reveal_type(f(args)) # N: Revealed type is "Tuple[builtins.int, builtins.str]"
1717

1818
reveal_type(f(varargs)) # N: Revealed type is "builtins.tuple[builtins.int, ...]"
1919

20-
f(0) # E: Argument 1 to "f" has incompatible type "int"; expected <nothing>
20+
if object():
21+
f(0) # E: Argument 1 to "f" has incompatible type "int"; expected <nothing>
2122

2223
def g(a: Tuple[Unpack[Ts]], b: Tuple[Unpack[Ts]]) -> Tuple[Unpack[Ts]]:
2324
return a

test-data/unit/check-unreachable-code.test

+4-4
Original file line numberDiff line numberDiff line change
@@ -873,15 +873,15 @@ def expect_str(x: str) -> str: pass
873873
x: int
874874
if False:
875875
assert False
876-
reveal_type(x)
876+
reveal_type(x) # E: Statement is unreachable
877877

878878
if False:
879879
raise Exception()
880-
reveal_type(x)
880+
reveal_type(x) # E: Statement is unreachable
881881

882882
if False:
883883
assert_never(x)
884-
reveal_type(x)
884+
reveal_type(x) # E: Statement is unreachable
885885

886886
if False:
887887
nonthrowing_assert_never(x) # E: Statement is unreachable
@@ -890,7 +890,7 @@ if False:
890890
if False:
891891
# Ignore obvious type errors
892892
assert_never(expect_str(x))
893-
reveal_type(x)
893+
reveal_type(x) # E: Statement is unreachable
894894
[builtins fixtures/exception.pyi]
895895

896896
[case testNeverVariants]

test-data/unit/fine-grained.test

+4-2
Original file line numberDiff line numberDiff line change
@@ -9667,7 +9667,8 @@ reveal_type(z)
96679667
[out]
96689668
c.py:2: note: Revealed type is "a.<subclass of "A" and "B">"
96699669
==
9670-
c.py:2: note: Revealed type is "a.A"
9670+
c.py:2: note: Revealed type is "Any"
9671+
b.py:2: error: Cannot determine type of "y"
96719672

96729673
[case testIsInstanceAdHocIntersectionFineGrainedIncrementalUnreachaableToIntersection]
96739674
import c
@@ -9698,7 +9699,8 @@ from b import z
96989699
reveal_type(z)
96999700
[builtins fixtures/isinstance.pyi]
97009701
[out]
9701-
c.py:2: note: Revealed type is "a.A"
9702+
b.py:2: error: Cannot determine type of "y"
9703+
c.py:2: note: Revealed type is "Any"
97029704
==
97039705
c.py:2: note: Revealed type is "a.<subclass of "A" and "B">"
97049706

0 commit comments

Comments
 (0)