Skip to content

Changes how unreachable statements are detected, refs #11437 #11443

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

Closed
wants to merge 8 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions docs/source/common_issues.rst
Original file line number Diff line number Diff line change
Expand Up @@ -786,6 +786,14 @@ False:
If you use the :option:`--warn-unreachable <mypy --warn-unreachable>` flag, mypy will generate
an error about each unreachable code block.

There are several cases that we always treat as reachable:

- ``reveal_type`` and ``reveal_locals`` helper functions, because they are only useful for type checking and can be used for debugging
- ``assert False, 'unreahable'`` and similar constructs, because they can be used to guard places that should not be reached during runtime execution
- functions that return ``NoReturn``, like ``sys.exit`` that can also be used to guard things
- ``raise ...``, with the same reasoning as above
- ``pass`` and ``...``, because they do nothing

Narrowing and inner functions
-----------------------------

Expand Down
31 changes: 25 additions & 6 deletions mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@
ARG_POS, ARG_STAR, LITERAL_TYPE, LDEF, MDEF, GDEF,
CONTRAVARIANT, COVARIANT, INVARIANT, TypeVarExpr, AssignmentExpr,
is_final_node,
ARG_NAMED)
ARG_NAMED, RevealExpr,
)
from mypy import nodes
from mypy import operators
from mypy.literals import literal, literal_hash, Key
Expand All @@ -37,7 +38,8 @@
UnionType, TypeVarId, TypeVarType, PartialType, DeletedType, UninhabitedType,
is_named_instance, union_items, TypeQuery, LiteralType,
is_optional, remove_optional, TypeTranslator, StarType, get_proper_type, ProperType,
get_proper_types, is_literal_type, TypeAliasType, TypeGuardedType)
get_proper_types, is_literal_type, TypeAliasType, TypeGuardedType
)
from mypy.sametypes import is_same_type
from mypy.messages import (
MessageBuilder, make_inferred_type_note, append_invariance_notes, pretty_seq,
Expand Down Expand Up @@ -306,10 +308,11 @@ def check_first_pass(self) -> None:
self.errors.set_file(self.path, self.tree.fullname, scope=self.tscope)
with self.tscope.module_scope(self.tree.fullname):
with self.enter_partial_types(), self.binder.top_frame_context():
for d in self.tree.defs:
for index, d in enumerate(self.tree.defs):
if (self.binder.is_unreachable()
and self.should_report_unreachable_issues()
and not self.is_raising_or_empty(d)):
and self.has_regular_unreachable_statements(
self.tree.defs, index)):
self.msg.unreachable_statement(d)
break
self.accept(d)
Expand Down Expand Up @@ -2017,9 +2020,10 @@ def visit_block(self, b: Block) -> None:
# as unreachable -- so we don't display an error.
self.binder.unreachable()
return
for s in b.body:
for index, s in enumerate(b.body):
if self.binder.is_unreachable():
if self.should_report_unreachable_issues() and not self.is_raising_or_empty(s):
if (self.should_report_unreachable_issues()
and self.has_regular_unreachable_statements(b.body, index)):
self.msg.unreachable_statement(s)
break
self.accept(s)
Expand All @@ -2046,6 +2050,9 @@ def is_raising_or_empty(self, s: Statement) -> bool:
if isinstance(s.expr, EllipsisExpr):
return True
elif isinstance(s.expr, CallExpr):
if isinstance(s.expr.analyzed, RevealExpr):
return True # `reveal_type` and `reveal_locals` are no-op

with self.expr_checker.msg.disable_errors():
typ = get_proper_type(self.expr_checker.accept(
s.expr, allow_none_return=True, always_allow_any=True))
Expand All @@ -2054,6 +2061,18 @@ def is_raising_or_empty(self, s: Statement) -> bool:
return True
return False

def has_regular_unreachable_statements(self, block: Sequence[Statement],
index: int) -> bool:
"""Helps to identify cases when our body has some regular unreachable statements.

We call statements "regular" when they are not covered by our special rules
defined in `is_raising_or_empty` function.
"""
for ind in range(index, len(block)):
if not self.is_raising_or_empty(block[ind]):
return True
return False

def visit_assignment_stmt(self, s: AssignmentStmt) -> None:
"""Type check an assignment statement.

Expand Down
10 changes: 5 additions & 5 deletions test-data/unit/check-enum.test
Original file line number Diff line number Diff line change
Expand Up @@ -1143,14 +1143,14 @@ reveal_type(y) # N: Revealed type is "__main__.Foo"

# The standard output when we end up inferring two disjoint facts about the same expr
if x is Foo.A and x is Foo.B:
reveal_type(x) # E: Statement is unreachable
x # E: Statement is unreachable
else:
reveal_type(x) # N: Revealed type is "__main__.Foo"
reveal_type(x) # N: Revealed type is "__main__.Foo"

# ..and we get the same result if we have two disjoint groups within the same comp expr
if x is Foo.A < x is Foo.B:
reveal_type(x) # E: Statement is unreachable
x # E: Statement is unreachable
else:
reveal_type(x) # N: Revealed type is "__main__.Foo"
reveal_type(x) # N: Revealed type is "__main__.Foo"
Expand All @@ -1168,23 +1168,23 @@ class Foo(Enum):

x: Foo
if x is Foo.A is Foo.B:
reveal_type(x) # E: Statement is unreachable
x # E: Statement is unreachable
else:
reveal_type(x) # N: Revealed type is "__main__.Foo"
reveal_type(x) # N: Revealed type is "__main__.Foo"

literal_a: Literal[Foo.A]
literal_b: Literal[Foo.B]
if x is literal_a is literal_b:
reveal_type(x) # E: Statement is unreachable
x # E: Statement is unreachable
else:
reveal_type(x) # N: Revealed type is "__main__.Foo"
reveal_type(x) # N: Revealed type is "__main__.Foo"

final_a: Final = Foo.A
final_b: Final = Foo.B
if x is final_a is final_b:
reveal_type(x) # E: Statement is unreachable
x # E: Statement is unreachable
else:
reveal_type(x) # N: Revealed type is "__main__.Foo"
reveal_type(x) # N: Revealed type is "__main__.Foo"
Expand Down
14 changes: 7 additions & 7 deletions test-data/unit/check-isinstance.test
Original file line number Diff line number Diff line change
Expand Up @@ -2336,16 +2336,16 @@ class C:

class Example(A, B): pass # E: Definition of "f" in base class "A" is incompatible with definition in base class "B"
x: A
if isinstance(x, B): # E: Subclass of "A" and "B" cannot exist: would have incompatible method signatures
reveal_type(x) # E: Statement is unreachable
if isinstance(x, B): # E: Subclass of "A" and "B" cannot exist: would have incompatible method signatures
x # E: Statement is unreachable
else:
reveal_type(x) # N: Revealed type is "__main__.A"
reveal_type(x) # N: Revealed type is "__main__.A"

y: C
if isinstance(y, B):
reveal_type(y) # N: Revealed type is "__main__.<subclass of "C" and "B">"
if isinstance(y, A): # E: Subclass of "C", "B", and "A" cannot exist: would have incompatible method signatures
reveal_type(y) # E: Statement is unreachable
y # E: Statement is unreachable
[builtins fixtures/isinstance.pyi]

[case testIsInstanceAdHocIntersectionReversed]
Expand Down Expand Up @@ -2410,7 +2410,7 @@ class B:

x: A[int]
if isinstance(x, B): # E: Subclass of "A[int]" and "B" cannot exist: would have incompatible method signatures
reveal_type(x) # E: Statement is unreachable
x # E: Statement is unreachable
else:
reveal_type(x) # N: Revealed type is "__main__.A[builtins.int]"

Expand Down Expand Up @@ -2580,7 +2580,7 @@ class B(Y, X): pass

foo: A
if isinstance(foo, B): # E: Subclass of "A" and "B" cannot exist: would have inconsistent method resolution order
reveal_type(foo) # E: Statement is unreachable
foo # E: Statement is unreachable
[builtins fixtures/isinstance.pyi]

[case testIsInstanceAdHocIntersectionAmbiguousClass]
Expand Down Expand Up @@ -2614,7 +2614,7 @@ x: Type[A]
if issubclass(x, B):
reveal_type(x) # N: Revealed type is "Type[__main__.<subclass of "A" and "B">]"
if issubclass(x, C): # E: Subclass of "A", "B", and "C" cannot exist: would have incompatible method signatures
reveal_type(x) # E: Statement is unreachable
x # E: Statement is unreachable
else:
reveal_type(x) # N: Revealed type is "Type[__main__.<subclass of "A" and "B">]"
else:
Expand Down
2 changes: 1 addition & 1 deletion test-data/unit/check-literal.test
Original file line number Diff line number Diff line change
Expand Up @@ -3298,7 +3298,7 @@ w: Union[Truth, AlsoTruth]
if w:
reveal_type(w) # N: Revealed type is "Union[__main__.Truth, __main__.AlsoTruth]"
else:
reveal_type(w) # E: Statement is unreachable
w # E: Statement is unreachable

[builtins fixtures/bool.pyi]

Expand Down
25 changes: 11 additions & 14 deletions test-data/unit/check-narrowing.test
Original file line number Diff line number Diff line change
Expand Up @@ -267,7 +267,7 @@ else:
reveal_type(x) # N: Revealed type is "Union[__main__.Object1, __main__.Object2]"

if x.key is Key.D:
reveal_type(x) # E: Statement is unreachable
x # E: Statement is unreachable
else:
reveal_type(x) # N: Revealed type is "Union[__main__.Object1, __main__.Object2]"
[builtins fixtures/tuple.pyi]
Expand All @@ -294,7 +294,7 @@ else:
reveal_type(x) # N: Revealed type is "Union[TypedDict('__main__.TypedDict1', {'key': Union[Literal['A'], Literal['C']]}), TypedDict('__main__.TypedDict2', {'key': Union[Literal['B'], Literal['C']]})]"

if x['key'] == 'D':
reveal_type(x) # E: Statement is unreachable
x # E: Statement is unreachable
else:
reveal_type(x) # N: Revealed type is "Union[TypedDict('__main__.TypedDict1', {'key': Union[Literal['A'], Literal['C']]}), TypedDict('__main__.TypedDict2', {'key': Union[Literal['B'], Literal['C']]})]"
[builtins fixtures/primitives.pyi]
Expand All @@ -321,7 +321,7 @@ else:
reveal_type(x) # N: Revealed type is "Union[TypedDict('__main__.TypedDict1', {'key'?: Union[Literal['A'], Literal['C']]}), TypedDict('__main__.TypedDict2', {'key'?: Union[Literal['B'], Literal['C']]})]"

if x['key'] == 'D':
reveal_type(x) # E: Statement is unreachable
x # E: Statement is unreachable
else:
reveal_type(x) # N: Revealed type is "Union[TypedDict('__main__.TypedDict1', {'key'?: Union[Literal['A'], Literal['C']]}), TypedDict('__main__.TypedDict2', {'key'?: Union[Literal['B'], Literal['C']]})]"
[builtins fixtures/primitives.pyi]
Expand Down Expand Up @@ -612,8 +612,7 @@ else:

y: Union[Parent1, Parent2]
if y["model"]["key"] is Key.C:
reveal_type(y) # E: Statement is unreachable
reveal_type(y["model"])
y # E: Statement is unreachable
else:
reveal_type(y) # N: Revealed type is "Union[TypedDict('__main__.Parent1', {'model': TypedDict('__main__.Model1', {'key': Literal[__main__.Key.A]}), 'foo': builtins.int}), TypedDict('__main__.Parent2', {'model': TypedDict('__main__.Model2', {'key': Literal[__main__.Key.B]}), 'bar': builtins.str})]"
reveal_type(y["model"]) # N: Revealed type is "Union[TypedDict('__main__.Model1', {'key': Literal[__main__.Key.A]}), TypedDict('__main__.Model2', {'key': Literal[__main__.Key.B]})]"
Expand Down Expand Up @@ -648,8 +647,7 @@ else:

y: Union[Parent1, Parent2]
if y["model"]["key"] == 'C':
reveal_type(y) # E: Statement is unreachable
reveal_type(y["model"])
y # E: Statement is unreachable
else:
reveal_type(y) # N: Revealed type is "Union[TypedDict('__main__.Parent1', {'model': TypedDict('__main__.Model1', {'key': Literal['A']}), 'foo': builtins.int}), TypedDict('__main__.Parent2', {'model': TypedDict('__main__.Model2', {'key': Literal['B']}), 'bar': builtins.str})]"
reveal_type(y["model"]) # N: Revealed type is "Union[TypedDict('__main__.Model1', {'key': Literal['A']}), TypedDict('__main__.Model2', {'key': Literal['B']})]"
Expand Down Expand Up @@ -728,7 +726,7 @@ def test2(switch: FlipFlopEnum) -> None:
switch.mutate()

assert switch.state is State.B # E: Non-overlapping identity check (left operand type: "Literal[State.A]", right operand type: "Literal[State.B]")
reveal_type(switch.state) # E: Statement is unreachable
switch.state # E: Statement is unreachable

def test3(switch: FlipFlopStr) -> None:
# This is the same thing as 'test1', except we try using str literals.
Expand Down Expand Up @@ -895,8 +893,7 @@ else:

# No contamination here
if 1 == x == z: # E: Non-overlapping equality check (left operand type: "Union[Literal[1], Literal[2], None]", right operand type: "Default")
reveal_type(x) # E: Statement is unreachable
reveal_type(z)
x # E: Statement is unreachable
else:
reveal_type(x) # N: Revealed type is "Union[Literal[1], Literal[2], None]"
reveal_type(z) # N: Revealed type is "__main__.Default"
Expand All @@ -912,7 +909,7 @@ b: Literal[1, 2]
c: Literal[2, 3]

if a == b == c:
reveal_type(a) # E: Statement is unreachable
a # E: Statement is unreachable
reveal_type(b)
reveal_type(c)
else:
Expand All @@ -923,7 +920,7 @@ else:
if a == a == a:
reveal_type(a) # N: Revealed type is "Literal[1]"
else:
reveal_type(a) # E: Statement is unreachable
a # E: Statement is unreachable

if a == a == b:
reveal_type(a) # N: Revealed type is "Literal[1]"
Expand Down Expand Up @@ -986,8 +983,8 @@ elif a == a == 4:
else:
# In contrast, this branch must be unreachable: we assume (maybe naively)
# that 'a' won't be mutated in the middle of the expression.
reveal_type(a) # E: Statement is unreachable
reveal_type(b)
a # E: Statement is unreachable
b
[builtins fixtures/primitives.pyi]

[case testNarrowingLiteralTruthiness]
Expand Down
18 changes: 9 additions & 9 deletions test-data/unit/check-python38.test
Original file line number Diff line number Diff line change
Expand Up @@ -455,11 +455,11 @@ def check_partial_list() -> None:
# flags: --warn-unreachable

if (x := 0):
reveal_type(x) # E: Statement is unreachable
x # E: Statement is unreachable
else:
reveal_type(x) # N: Revealed type is "builtins.int"

reveal_type(x) # N: Revealed type is "builtins.int"
reveal_type(x) # N: Revealed type is "builtins.int"

[case testWalrusAssignmentAndConditionScopeForProperty]
# flags: --warn-unreachable
Expand All @@ -479,14 +479,14 @@ if x := wrapper.f:
else:
reveal_type(x) # N: Revealed type is "builtins.str"

reveal_type(x) # N: Revealed type is "builtins.str"
reveal_type(x) # N: Revealed type is "builtins.str"

if y := wrapper.always_false:
reveal_type(y) # E: Statement is unreachable
y # E: Statement is unreachable
else:
reveal_type(y) # N: Revealed type is "Literal[False]"

reveal_type(y) # N: Revealed type is "Literal[False]"
reveal_type(y) # N: Revealed type is "Literal[False]"
[builtins fixtures/property.pyi]

[case testWalrusAssignmentAndConditionScopeForFunction]
Expand All @@ -501,21 +501,21 @@ if x := f():
else:
reveal_type(x) # N: Revealed type is "builtins.str"

reveal_type(x) # N: Revealed type is "builtins.str"
reveal_type(x) # N: Revealed type is "builtins.str"

def always_false() -> Literal[False]: ...

if y := always_false():
reveal_type(y) # E: Statement is unreachable
y # E: Statement is unreachable
else:
reveal_type(y) # N: Revealed type is "Literal[False]"

reveal_type(y) # N: Revealed type is "Literal[False]"
reveal_type(y) # N: Revealed type is "Literal[False]"

def always_false_with_parameter(x: int) -> Literal[False]: ...

if z := always_false_with_parameter(5):
reveal_type(z) # E: Statement is unreachable
z # E: Statement is unreachable
else:
reveal_type(z) # N: Revealed type is "Literal[False]"

Expand Down
Loading