Repository: python/mypy
Pull request: Add flag to warn about unreachable branches and exprs
This diff adds a `--warn-unreachable` flag that reports an error if:

1. Any statement is inferred to be unreachable
2. Any subexpression in boolean expressions, inline if statements, and list/set/generator/dict comprehensions are always unreachable or redundant.

This only takes into account the results of *type* analysis. We do not report errors for statements that are inferred to be unreachable in the semantic analysis phase (e.g. due to `sys.platform` checks and the like).

Those checks are all intentional/deliberately flag certain branches as unreachable, so error messages would be annoying in those cases.

A few additional notes:

1. I didn't spend a huge amount of time trying to come up with error messages. They could probably do with some polishing/rewording.

2. I thought about enabling this flag for default with mypy, but unfortunately that ended up producing ~40 to 50 (apparently legitimate) error messages.

   About a fourth of them were due to runtime checks checking to see if some type is None (despite not being declared to be Optional), about a fourth seemed to be due to mypy not quite understanding how to handle things like traits and Bogus[...], and about a fourth seemed to be legitimately unnecessary checks we could safely remove.

   The final fourth was a mixture of typeshed bugs and misc errors with the reachability checks. (e.g. see https://github.com/python/mypy/pull/7048)

3. For these reasons, I decided against adding this flag to `--strict` and against documenting it yet. We'll probably need to flush out a few bugs in mypy first/get mypy to the state where we can at least honestly dogfood this.

Resolves https://github.com/python/mypy/issues/2395; partially addresses https://github.com/python/mypy/issues/7008.

Problem statement:
Warn about always true/false isinstance tests
These may result in code that mypy considers unreachable and silently doesn't type check. However, they may happen quite frequently in innocent things like `assert isinstance(x, str)` so finding a good heuristic for what to report can be tricky.

Hints from issue discussion:
Initial reactions:
- Don't warn about an assertion that always passes
- Do warn about an assertion that always fails
- Do warn about a branch of an `if` that is unreachable

To add to Reid's comment: but don't warn about an assertion that always fails if it's an explicit `assert False`.

@ddfisher Or any false-y builtin constant, since I've seen `assert 0` and `assert None` before.

@ilinum This issue might be interesting given your recent quest for flagging unreachable code.
I have found five other issues on the tracker that are some variations of this, so I think this is high priority.
Also from my experience there is definitely no need in a warning about always True asserts. So 100% with @rwbarton [above](https://github.com/python/mypy/issues/2395#issuecomment-258235424).

Commit message: Add flag to warn about unreachable branches and exprs

This diff adds a `--disallow-inferred-unreachable` flag that
reports an error if:

1. Any branch is inferred to be unreachable
2. Any subexpression in boolean expressions, inline if statements,
   and list/set/generator/dict comprehensions are always unreachable
   or redundant.

This only takes into account the results of *type* analysis. We do not
report errors for branches that are inferred to be unreachable in the
semantic analysis phase (e.g. due to `sys.platform` checks and the
like).

Those checks are all intentional/deliberately flag certain branches
as unreachable, so error messages would be annoying in those cases.

A few additional notes:

1. I didn't spend a huge amount of time trying to come up with error
   messages. They could probably do with some polishing/rewording.

2. I thought about enabling this flag for default with mypy, but
   unfortunately that ended up producing ~40 to 50 (apparently
   legitimate) error messages.

   About a fourth of them were due to runtime checks checking to see
   if some type is None (despite not being declared to be Optional),
   about a fourth seemed to be due to mypy not quite understanding how
   to handle things like traits and Bogus[...], and about a fourth
   seemed to be legitimately unnecessary checks we could safely remove.

   The final checks were a mixture of typeshed bugs and misc errors with
   the reachability checks. (e.g. see https://github.com/python/mypy/pull/7048)

3. For these reasons, I decided against adding this flag to `--strict`
   and against documenting it yet. We'll probably need to flush out
   a few bugs in mypy first/get mypy to the state where we can at least
   honestly dogfood this.

Resolves https://github.com/python/mypy/issues/2395; partially
addresses https://github.com/python/mypy/issues/7008.

Patch to review:
diff --git a/mypy/checker.py b/mypy/checker.py
index 8720b4995f36..a2f066749fa6 100644
--- a/mypy/checker.py
+++ b/mypy/checker.py
@@ -1777,13 +1777,36 @@ def check_import(self, node: ImportBase) -> None:
 
     def visit_block(self, b: Block) -> None:
         if b.is_unreachable:
+            # This block was marked as being unreachable during semantic analysis.
+            # It turns out any blocks marked in this way are *intentionally* marked
+            # as unreachable -- so we don't display an error.
             self.binder.unreachable()
             return
         for s in b.body:
             if self.binder.is_unreachable():
+                if self.options.disallow_inferred_unreachable and not self.is_raising_or_empty(s):
+                    self.msg.unreachable_branch(s)
                 break
             self.accept(s)
 
+    def is_raising_or_empty(self, s: Statement) -> bool:
+        if isinstance(s, AssertStmt) and is_false_literal(s.expr):
+            return True
+        elif isinstance(s, (RaiseStmt, PassStmt)):
+            return True
+        elif isinstance(s, ExpressionStmt):
+            if isinstance(s.expr, EllipsisExpr):
+                return True
+            elif isinstance(s.expr, CallExpr):
+                self.expr_checker.msg.disable_errors()
+                typ = self.expr_checker.accept(
+                    s.expr, allow_none_return=True, always_allow_any=True)
+                self.expr_checker.msg.enable_errors()
+
+                if isinstance(typ, UninhabitedType):
+                    return True
+        return False
+
     def visit_assignment_stmt(self, s: AssignmentStmt) -> None:
         """Type check an assignment statement.
 
@@ -2956,7 +2979,8 @@ def visit_try_stmt(self, s: TryStmt) -> None:
             # type checks in both contexts, but only the resulting types
             # from the latter context affect the type state in the code
             # that follows the try statement.)
-            self.accept(s.finally_body)
+            if not self.binder.is_unreachable():
+                self.accept(s.finally_body)
 
     def visit_try_without_finally(self, s: TryStmt, try_frame: bool) -> None:
         """Type check a try statement, ignoring the finally block.
diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py
index 3c2306de6fdc..f0a460605ad8 100644
--- a/mypy/checkexpr.py
+++ b/mypy/checkexpr.py
@@ -2367,14 +2367,25 @@ def check_boolean_op(self, e: OpExpr, context: Context) -> Type:
             restricted_left_type = true_only(left_type)
             result_is_left = not left_type.can_be_false
 
+        # If right_map is None then we know mypy considers the right branch
+        # to be unreachable and therefore any errors found in the right branch
+        # should be suppressed.
+        #
+        # Note that we perform these checks *before* we take into account
+        # the analysis from the semanal phase below. We assume that nodes
+        # marked as unreachable during semantic analysis were done so intentionally.
+        # So, we shouldn't report an error.
+        if self.chk.options.disallow_inferred_unreachable:
+            if left_map is None:
+                self.msg.always_same_truth_value_left_operand(e.op, e.left)
+            if right_map is None:
+                self.msg.unreachable_right_operand(e.op, e.right)
+
         if e.right_unreachable:
             right_map = None
         elif e.right_always:
             left_map = None
 
-        # If right_map is None then we know mypy considers the right branch
-        # to be unreachable and therefore any errors found in the right branch
-        # should be suppressed.
         if right_map is None:
             self.msg.disable_errors()
         try:
@@ -3174,12 +3185,18 @@ def check_for_comp(self, e: Union[GeneratorExpr, DictionaryComprehension]) -> No
                 self.accept(condition)
 
                 # values are only part of the comprehension when all conditions are true
-                true_map, _ = self.chk.find_isinstance_check(condition)
+                true_map, false_map = self.chk.find_isinstance_check(condition)
 
                 if true_map:
                     for var, type in true_map.items():
                         self.chk.binder.put(var, type)
 
+                if self.chk.options.disallow_inferred_unreachable:
+                    if true_map is None:
+                        self.msg.comprehension_cond_always_same(False, condition)
+                    elif false_map is None:
+                        self.msg.comprehension_cond_always_same(True, condition)
+
     def visit_conditional_expr(self, e: ConditionalExpr) -> Type:
         self.accept(e.cond)
         ctx = self.type_context[-1]
@@ -3187,6 +3204,11 @@ def visit_conditional_expr(self, e: ConditionalExpr) -> Type:
         # Gain type information from isinstance if it is there
         # but only for the current expression
         if_map, else_map = self.chk.find_isinstance_check(e.cond)
+        if self.chk.options.disallow_inferred_unreachable:
+            if if_map is None:
+                self.msg.unreachable_branch_in_inline_if('if', e.cond)
+            elif else_map is None:
+                self.msg.unreachable_branch_in_inline_if('else', e.cond)
 
         if_type = self.analyze_cond_branch(if_map, e.if_expr, context=ctx)
 
diff --git a/mypy/main.py b/mypy/main.py
index 14fbee845516..7bca8c58022c 100644
--- a/mypy/main.py
+++ b/mypy/main.py
@@ -492,6 +492,10 @@ def add_invertible_flag(flag: str,
                         help="Treat imports as private unless aliased",
                         group=strictness_group)
 
+    add_invertible_flag('--disallow-inferred-unreachable', default=False, strict_flag=False,
+                        help="Disallow branches inferred to be unreachable after type analysis",
+                        group=strictness_group)
+
     incremental_group = parser.add_argument_group(
         title='Incremental mode',
         description="Adjust how mypy incrementally type checks and caches modules. "
diff --git a/mypy/messages.py b/mypy/messages.py
index 08812acaabcc..2fdc654aaabb 100644
--- a/mypy/messages.py
+++ b/mypy/messages.py
@@ -1228,6 +1228,30 @@ def note_call(self, subtype: Type, call: Type, context: Context) -> None:
         self.note('"{}.__call__" has type {}'.format(self.format_bare(subtype),
                                                      self.format(call, verbosity=1)), context)
 
+    def unreachable_branch(self, context: Context) -> None:
+        self.fail("This branch is inferred to be unreachable", context)
+
+    def always_same_truth_value_left_operand(self, op_name: str, context: Context) -> None:
+        value = 'true' if op_name == 'and' else 'false'
+        self.fail(
+            "The left operand of this '{}' expression is always {}".format(op_name, value),
+            context,
+        )
+
+    def unreachable_right_operand(self, op_name: str, context: Context) -> None:
+        self.fail(
+            "The right operand of this '{}' expression is never evaluated".format(op_name),
+            context,
+        )
+
+    def comprehension_cond_always_same(self, simplified_result: bool, context: Context) -> None:
+        template = "The conditional check in this comprehension is always {}"
+        self.fail(template.format(str(simplified_result).lower()), context)
+
+    def unreachable_branch_in_inline_if(self, sub_expr_name: str, context: Context) -> None:
+        template = "The '{}' expression in this inline if expression is never evaluated"
+        self.fail(template.format(sub_expr_name), context)
+
     def report_protocol_problems(self, subtype: Union[Instance, TupleType, TypedDictType],
                                  supertype: Instance, context: Context) -> None:
         """Report possible protocol conflicts between 'subtype' and 'supertype'.
diff --git a/mypy/options.py b/mypy/options.py
index 68cf3446d5da..7f83fbc16ffa 100644
--- a/mypy/options.py
+++ b/mypy/options.py
@@ -31,6 +31,7 @@ class BuildType:
     "disallow_any_generics",
     "disallow_any_unimported",
     "disallow_incomplete_defs",
+    "disallow_inferred_unreachable",
     "disallow_subclassing_any",
     "disallow_untyped_calls",
     "disallow_untyped_decorators",
@@ -164,6 +165,10 @@ def __init__(self) -> None:
         # This makes 1 == '1', 1 in ['1'], and 1 is '1' errors.
         self.strict_equality = False
 
+        # Report an error for any branches inferred to be unreachable as a result of
+        # type analysis.
+        self.disallow_inferred_unreachable = False
+
         # Variable names considered True
         self.always_true = []  # type: List[str]
 
diff --git a/test-data/unit/check-unreachable-code.test b/test-data/unit/check-unreachable-code.test
index 593f24523242..36f429d1981a 100644
--- a/test-data/unit/check-unreachable-code.test
+++ b/test-data/unit/check-unreachable-code.test
@@ -715,3 +715,179 @@ if sys.version_info[0] >= 2:
     reveal_type('')  # N: Revealed type is 'builtins.str'
 reveal_type('')  # N: Revealed type is 'builtins.str'
 [builtins fixtures/ops.pyi]
+
+[case testUnreachableFlagWithBadControlFlow]
+# flags: --disallow-inferred-unreachable
+a: int
+if isinstance(a, int):
+    reveal_type(a)  # N: Revealed type is 'builtins.int'
+else:
+    reveal_type(a)  # E: This branch is inferred to be unreachable
+
+b: int
+while isinstance(b, int):
+    reveal_type(b)  # N: Revealed type is 'builtins.int'
+else:
+    reveal_type(b)  # E: This branch is inferred to be unreachable
+
+def foo(c: int) -> None:
+    reveal_type(c)  # N: Revealed type is 'builtins.int'
+    assert not isinstance(c, int)
+    reveal_type(c)  # E: This branch is inferred to be unreachable
+
+d: int
+if False:
+    reveal_type(d)  # E: This branch is inferred to be unreachable
+
+e: int
+if True:
+    reveal_type(e)  # N: Revealed type is 'builtins.int'
+else:
+    reveal_type(e)  # E: This branch is inferred to be unreachable
+
+[builtins fixtures/isinstancelist.pyi]
+
+[case testUnreachableFlagTryBlocks]
+# flags: --disallow-inferred-unreachable
+
+def foo(x: int) -> int:
+    try:
+        reveal_type(x)      # N: Revealed type is 'builtins.int'
+        return x
+        reveal_type(x)      # E: This branch is inferred to be unreachable
+    finally:
+        reveal_type(x)      # N: Revealed type is 'builtins.int'
+        if True:
+            reveal_type(x)  # N: Revealed type is 'builtins.int'
+        else:
+            reveal_type(x)  # E: This branch is inferred to be unreachable
+
+def bar(x: int) -> int:
+    try:
+        if True:
+            raise Exception()
+        reveal_type(x)      # E: This branch is inferred to be unreachable
+    except:
+        reveal_type(x)      # N: Revealed type is 'builtins.int'
+        return x
+    else:
+        reveal_type(x)      # E: This branch is inferred to be unreachable
+
+def baz(x: int) -> int:
+    try:
+        reveal_type(x)      # N: Revealed type is 'builtins.int'
+    except:
+        # Mypy assumes all lines could throw an exception
+        reveal_type(x)      # N: Revealed type is 'builtins.int'
+        return x
+    else:
+        reveal_type(x)      # N: Revealed type is 'builtins.int'
+        return x
+[builtins fixtures/exception.pyi]
+
+[case testUnreachableFlagIgnoresSemanticAnalysisUnreachable]
+# flags: --disallow-inferred-unreachable --python-version 3.7 --platform win32 --always-false FOOBAR
+import sys
+from typing import TYPE_CHECKING
+
+x: int
+if TYPE_CHECKING:
+    reveal_type(x)  # N: Revealed type is 'builtins.int'
+
+if sys.platform == 'darwin':
+    reveal_type(x)
+
+if sys.version_info == (2, 7):
+    reveal_type(x)
+
+FOOBAR = ""
+if FOOBAR:
+    reveal_type(x)
+else:
+    reveal_type(x)  # N: Revealed type is 'builtins.int'
+[builtins fixtures/ops.pyi]
+
+[case testUnreachableFlagIgnoresSemanticAnalysisExprUnreachable]
+# flags: --disallow-inferred-unreachable --always-false FOOBAR
+import sys
+from typing import TYPE_CHECKING
+
+FOOBAR = ""
+
+def foo() -> bool: ...
+
+lst = [1, 2, 3]
+
+a = FOOBAR and foo()
+b = (not FOOBAR) or foo()
+c = 1 if FOOBAR else 2
+d = [x for x in lst if FOOBAR]
+[builtins fixtures/list.pyi]
+
+[case testUnreachableFlagOkWithDeadStatements]
+# flags: --disallow-inferred-unreachable
+from typing import NoReturn
+def assert_never(x: NoReturn) -> NoReturn:
+    assert False
+
+def nonthrowing_assert_never(x: NoReturn) -> None: ...
+
+def expect_str(x: str) -> str: pass
+
+x: int
+if False:
+    assert False
+    reveal_type(x)
+
+if False:
+    raise Exception()
+    reveal_type(x)
+
+if False:
+    assert_never(x)
+    reveal_type(x)
+
+if False:
+    nonthrowing_assert_never(x)  # E: This branch is inferred to be unreachable
+    reveal_type(x)
+
+if False:
+    # Ignore obvious type errors
+    assert_never(expect_str(x))
+    reveal_type(x)
+[builtins fixtures/exception.pyi]
+
+[case testUnreachableFlagExpressions]
+# flags: --disallow-inferred-unreachable
+def foo() -> bool: ...
+
+lst = [1, 2, 3, 4]
+
+a = True or foo()                        # E: The right operand of this 'or' expression is never evaluated
+b = False or foo()                       # E: The left operand of this 'or' expression is always false
+c = True and foo()                       # E: The left operand of this 'and' expression is always true
+d = False and foo()                      # E: The right operand of this 'and' expression is never evaluated
+e = True or (True or (True or foo()))    # E: The right operand of this 'or' expression is never evaluated
+f = (True or foo()) or (True or foo())   # E: The right operand of this 'or' expression is never evaluated
+g = 3 if True else 4                     # E: The 'else' expression in this inline if expression is never evaluated
+h = 3 if False else 4                    # E: The 'if' expression in this inline if expression is never evaluated
+i = [x for x in lst if True]             # E: The conditional check in this comprehension is always true
+j = [x for x in lst if False]            # E: The conditional check in this comprehension is always false
+
+k = [x for x in lst if isinstance(x, int) or foo()]  # E: The conditional check in this comprehension is always true \
+                                                     # E: The right operand of this 'or' expression is never evaluated
+[builtins fixtures/isinstancelist.pyi]
+
+[case testUnreachableFlagMiscTestCaseMissingMethod]
+# flags: --disallow-inferred-unreachable
+
+class Case1:
+    def test1(self) -> bool:
+        return False and self.missing()  # E: The right operand of this 'and' expression is never evaluated
+
+    def test2(self) -> bool:
+        return not self.property_decorator_missing and self.missing()  # E: The right operand of this 'and' expression is never evaluated
+
+    def property_decorator_missing(self) -> bool:
+        return True
+[builtins fixtures/bool.pyi]


Code review comments:
diff_hunk: @@ -715,3 +715,179 @@ if sys.version_info[0] >= 2:
     reveal_type('')  # N: Revealed type is 'builtins.str'
 reveal_type('')  # N: Revealed type is 'builtins.str'
 [builtins fixtures/ops.pyi]
+
+[case testUnreachableFlagWithBadControlFlow]
+# flags: --disallow-inferred-unreachable
+a: int
+if isinstance(a, int):
+    reveal_type(a)  # N: Revealed type is 'builtins.int'
+else:
+    reveal_type(a)  # E: This branch is inferred to be unreachable
+
+b: int
+while isinstance(b, int):
+    reveal_type(b)  # N: Revealed type is 'builtins.int'
+else:
+    reveal_type(b)  # E: This branch is inferred to be unreachable
+
+def foo(c: int) -> None:
+    reveal_type(c)  # N: Revealed type is 'builtins.int'
+    assert not isinstance(c, int)
+    reveal_type(c)  # E: This branch is inferred to be unreachable
+
+d: int
+if False:
+    reveal_type(d)  # E: This branch is inferred to be unreachable
+
+e: int
+if True:
+    reveal_type(e)  # N: Revealed type is 'builtins.int'
+else:
+    reveal_type(e)  # E: This branch is inferred to be unreachable
+
+[builtins fixtures/isinstancelist.pyi]
+
+[case testUnreachableFlagTryBlocks]
+# flags: --disallow-inferred-unreachable
+
+def foo(x: int) -> int:
+    try:
+        reveal_type(x)      # N: Revealed type is 'builtins.int'
+        return x
+        reveal_type(x)      # E: This branch is inferred to be unreachable
+    finally:
+        reveal_type(x)      # N: Revealed type is 'builtins.int'
+        if True:
+            reveal_type(x)  # N: Revealed type is 'builtins.int'
+        else:
+            reveal_type(x)  # E: This branch is inferred to be unreachable
+
+def bar(x: int) -> int:
+    try:
+        if True:
+            raise Exception()
+        reveal_type(x)      # E: This branch is inferred to be unreachable
+    except:
+        reveal_type(x)      # N: Revealed type is 'builtins.int'
+        return x
+    else:
+        reveal_type(x)      # E: This branch is inferred to be unreachable
+
+def baz(x: int) -> int:
+    try:
+        reveal_type(x)      # N: Revealed type is 'builtins.int'
+    except:
+        # Mypy assumes all lines could throw an exception
+        reveal_type(x)      # N: Revealed type is 'builtins.int'
+        return x
+    else:
+        reveal_type(x)      # N: Revealed type is 'builtins.int'
+        return x
+[builtins fixtures/exception.pyi]
+
+[case testUnreachableFlagIgnoresSemanticAnalysisUnreachable]
+# flags: --disallow-inferred-unreachable --python-version 3.7 --platform win32 --always-false FOOBAR
+import sys
+from typing import TYPE_CHECKING
+
+x: int
+if TYPE_CHECKING:
+    reveal_type(x)  # N: Revealed type is 'builtins.int'
+
+if sys.platform == 'darwin':
+    reveal_type(x)
+
+if sys.version_info == (2, 7):
+    reveal_type(x)
+
+FOOBAR = ""
+if FOOBAR:
+    reveal_type(x)
+else:
+    reveal_type(x)  # N: Revealed type is 'builtins.int'
+[builtins fixtures/ops.pyi]
+
+[case testUnreachableFlagIgnoresSemanticAnalysisExprUnreachable]
+# flags: --disallow-inferred-unreachable --always-false FOOBAR
+import sys
+from typing import TYPE_CHECKING
+
+FOOBAR = ""
+
+def foo() -> bool: ...
+
+lst = [1, 2, 3]
+
+a = FOOBAR and foo()
+b = (not FOOBAR) or foo()
+c = 1 if FOOBAR else 2
+d = [x for x in lst if FOOBAR]
+[builtins fixtures/list.pyi]
+
+[case testUnreachableFlagOkWithDeadStatements]
+# flags: --disallow-inferred-unreachable
+from typing import NoReturn
+def assert_never(x: NoReturn) -> NoReturn:
+    assert False
+
+def nonthrowing_assert_never(x: NoReturn) -> None: ...
+
+def expect_str(x: str) -> str: pass
+
+x: int
+if False:
+    assert False
+    reveal_type(x)
+
+if False:
+    raise Exception()
+    reveal_type(x)
+
+if False:
+    assert_never(x)
+    reveal_type(x)
+
+if False:
+    nonthrowing_assert_never(x)  # E: This branch is inferred to be unreachable
+    reveal_type(x)
+
+if False:
+    # Ignore obvious type errors
+    assert_never(expect_str(x))
+    reveal_type(x)
+[builtins fixtures/exception.pyi]
+
+[case testUnreachableFlagExpressions]
+# flags: --disallow-inferred-unreachable
+def foo() -> bool: ...
+
+lst = [1, 2, 3, 4]
+
+a = True or foo()                        # E: The right operand of this 'or' expression is never evaluated
+b = False or foo()                       # E: The left operand of this 'or' expression is always false
+c = True and foo()                       # E: The left operand of this 'and' expression is always true
+d = False and foo()                      # E: The right operand of this 'and' expression is never evaluated
+e = True or (True or (True or foo()))    # E: The right operand of this 'or' expression is never evaluated
+f = (True or foo()) or (True or foo())   # E: The right operand of this 'or' expression is never evaluated
+g = 3 if True else 4                     # E: The 'else' expression in this inline if expression is never evaluated
path: test-data/unit/check-unreachable-code.test
text: @user1:
Idea for message: `If condition is always true`

diff_hunk: @@ -715,3 +715,179 @@ if sys.version_info[0] >= 2:
     reveal_type('')  # N: Revealed type is 'builtins.str'
 reveal_type('')  # N: Revealed type is 'builtins.str'
 [builtins fixtures/ops.pyi]
+
+[case testUnreachableFlagWithBadControlFlow]
+# flags: --disallow-inferred-unreachable
+a: int
+if isinstance(a, int):
+    reveal_type(a)  # N: Revealed type is 'builtins.int'
+else:
+    reveal_type(a)  # E: This branch is inferred to be unreachable
+
+b: int
+while isinstance(b, int):
+    reveal_type(b)  # N: Revealed type is 'builtins.int'
+else:
+    reveal_type(b)  # E: This branch is inferred to be unreachable
+
+def foo(c: int) -> None:
+    reveal_type(c)  # N: Revealed type is 'builtins.int'
+    assert not isinstance(c, int)
+    reveal_type(c)  # E: This branch is inferred to be unreachable
+
+d: int
+if False:
+    reveal_type(d)  # E: This branch is inferred to be unreachable
+
+e: int
+if True:
+    reveal_type(e)  # N: Revealed type is 'builtins.int'
+else:
+    reveal_type(e)  # E: This branch is inferred to be unreachable
+
+[builtins fixtures/isinstancelist.pyi]
+
+[case testUnreachableFlagTryBlocks]
+# flags: --disallow-inferred-unreachable
+
+def foo(x: int) -> int:
+    try:
+        reveal_type(x)      # N: Revealed type is 'builtins.int'
+        return x
+        reveal_type(x)      # E: This branch is inferred to be unreachable
+    finally:
+        reveal_type(x)      # N: Revealed type is 'builtins.int'
+        if True:
+            reveal_type(x)  # N: Revealed type is 'builtins.int'
+        else:
+            reveal_type(x)  # E: This branch is inferred to be unreachable
+
+def bar(x: int) -> int:
+    try:
+        if True:
+            raise Exception()
+        reveal_type(x)      # E: This branch is inferred to be unreachable
+    except:
+        reveal_type(x)      # N: Revealed type is 'builtins.int'
+        return x
+    else:
+        reveal_type(x)      # E: This branch is inferred to be unreachable
+
+def baz(x: int) -> int:
+    try:
+        reveal_type(x)      # N: Revealed type is 'builtins.int'
+    except:
+        # Mypy assumes all lines could throw an exception
+        reveal_type(x)      # N: Revealed type is 'builtins.int'
+        return x
+    else:
+        reveal_type(x)      # N: Revealed type is 'builtins.int'
+        return x
+[builtins fixtures/exception.pyi]
+
+[case testUnreachableFlagIgnoresSemanticAnalysisUnreachable]
+# flags: --disallow-inferred-unreachable --python-version 3.7 --platform win32 --always-false FOOBAR
+import sys
+from typing import TYPE_CHECKING
+
+x: int
+if TYPE_CHECKING:
+    reveal_type(x)  # N: Revealed type is 'builtins.int'
+
+if sys.platform == 'darwin':
+    reveal_type(x)
+
+if sys.version_info == (2, 7):
+    reveal_type(x)
+
+FOOBAR = ""
+if FOOBAR:
+    reveal_type(x)
+else:
+    reveal_type(x)  # N: Revealed type is 'builtins.int'
+[builtins fixtures/ops.pyi]
+
+[case testUnreachableFlagIgnoresSemanticAnalysisExprUnreachable]
+# flags: --disallow-inferred-unreachable --always-false FOOBAR
+import sys
+from typing import TYPE_CHECKING
+
+FOOBAR = ""
+
+def foo() -> bool: ...
+
+lst = [1, 2, 3]
+
+a = FOOBAR and foo()
+b = (not FOOBAR) or foo()
+c = 1 if FOOBAR else 2
+d = [x for x in lst if FOOBAR]
+[builtins fixtures/list.pyi]
+
+[case testUnreachableFlagOkWithDeadStatements]
+# flags: --disallow-inferred-unreachable
+from typing import NoReturn
+def assert_never(x: NoReturn) -> NoReturn:
+    assert False
+
+def nonthrowing_assert_never(x: NoReturn) -> None: ...
+
+def expect_str(x: str) -> str: pass
+
+x: int
+if False:
+    assert False
+    reveal_type(x)
+
+if False:
+    raise Exception()
+    reveal_type(x)
+
+if False:
+    assert_never(x)
+    reveal_type(x)
+
+if False:
+    nonthrowing_assert_never(x)  # E: This branch is inferred to be unreachable
+    reveal_type(x)
+
+if False:
+    # Ignore obvious type errors
+    assert_never(expect_str(x))
+    reveal_type(x)
+[builtins fixtures/exception.pyi]
+
+[case testUnreachableFlagExpressions]
+# flags: --disallow-inferred-unreachable
+def foo() -> bool: ...
+
+lst = [1, 2, 3, 4]
+
+a = True or foo()                        # E: The right operand of this 'or' expression is never evaluated
+b = False or foo()                       # E: The left operand of this 'or' expression is always false
path: test-data/unit/check-unreachable-code.test
text: @user1:
Similar to above, maybe rephrase as `Left operand of 'or' is always false`

diff_hunk: @@ -1228,6 +1228,30 @@ def note_call(self, subtype: Type, call: Type, context: Context) -> None:
         self.note('"{}.__call__" has type {}'.format(self.format_bare(subtype),
                                                      self.format(call, verbosity=1)), context)
 
+    def unreachable_branch(self, context: Context) -> None:
path: mypy/messages.py
text: @user1:
What do you think about renaming this to `unreachable_statement`? Would it be clearer (and correct)?

@author:
Good idea! I think it'd be more correct, especially since we're also reporting these errors after things like asserts and returns. (I also added a test case for the latter).

diff_hunk: @@ -715,3 +715,179 @@ if sys.version_info[0] >= 2:
     reveal_type('')  # N: Revealed type is 'builtins.str'
 reveal_type('')  # N: Revealed type is 'builtins.str'
 [builtins fixtures/ops.pyi]
+
+[case testUnreachableFlagWithBadControlFlow]
+# flags: --disallow-inferred-unreachable
+a: int
+if isinstance(a, int):
+    reveal_type(a)  # N: Revealed type is 'builtins.int'
+else:
+    reveal_type(a)  # E: This branch is inferred to be unreachable
+
+b: int
+while isinstance(b, int):
+    reveal_type(b)  # N: Revealed type is 'builtins.int'
+else:
+    reveal_type(b)  # E: This branch is inferred to be unreachable
+
+def foo(c: int) -> None:
+    reveal_type(c)  # N: Revealed type is 'builtins.int'
+    assert not isinstance(c, int)
+    reveal_type(c)  # E: This branch is inferred to be unreachable
+
+d: int
+if False:
+    reveal_type(d)  # E: This branch is inferred to be unreachable
+
+e: int
+if True:
+    reveal_type(e)  # N: Revealed type is 'builtins.int'
+else:
+    reveal_type(e)  # E: This branch is inferred to be unreachable
+
+[builtins fixtures/isinstancelist.pyi]
+
+[case testUnreachableFlagTryBlocks]
+# flags: --disallow-inferred-unreachable
+
+def foo(x: int) -> int:
+    try:
+        reveal_type(x)      # N: Revealed type is 'builtins.int'
+        return x
+        reveal_type(x)      # E: This branch is inferred to be unreachable
+    finally:
+        reveal_type(x)      # N: Revealed type is 'builtins.int'
+        if True:
+            reveal_type(x)  # N: Revealed type is 'builtins.int'
+        else:
+            reveal_type(x)  # E: This branch is inferred to be unreachable
+
+def bar(x: int) -> int:
+    try:
+        if True:
+            raise Exception()
+        reveal_type(x)      # E: This branch is inferred to be unreachable
+    except:
+        reveal_type(x)      # N: Revealed type is 'builtins.int'
+        return x
+    else:
+        reveal_type(x)      # E: This branch is inferred to be unreachable
+
+def baz(x: int) -> int:
+    try:
+        reveal_type(x)      # N: Revealed type is 'builtins.int'
+    except:
+        # Mypy assumes all lines could throw an exception
+        reveal_type(x)      # N: Revealed type is 'builtins.int'
+        return x
+    else:
+        reveal_type(x)      # N: Revealed type is 'builtins.int'
+        return x
+[builtins fixtures/exception.pyi]
+
+[case testUnreachableFlagIgnoresSemanticAnalysisUnreachable]
+# flags: --disallow-inferred-unreachable --python-version 3.7 --platform win32 --always-false FOOBAR
+import sys
+from typing import TYPE_CHECKING
+
+x: int
+if TYPE_CHECKING:
+    reveal_type(x)  # N: Revealed type is 'builtins.int'
+
+if sys.platform == 'darwin':
+    reveal_type(x)
+
+if sys.version_info == (2, 7):
+    reveal_type(x)
+
+FOOBAR = ""
+if FOOBAR:
+    reveal_type(x)
+else:
+    reveal_type(x)  # N: Revealed type is 'builtins.int'
+[builtins fixtures/ops.pyi]
+
+[case testUnreachableFlagIgnoresSemanticAnalysisExprUnreachable]
+# flags: --disallow-inferred-unreachable --always-false FOOBAR
+import sys
+from typing import TYPE_CHECKING
+
+FOOBAR = ""
+
+def foo() -> bool: ...
+
+lst = [1, 2, 3]
+
+a = FOOBAR and foo()
+b = (not FOOBAR) or foo()
+c = 1 if FOOBAR else 2
+d = [x for x in lst if FOOBAR]
+[builtins fixtures/list.pyi]
+
+[case testUnreachableFlagOkWithDeadStatements]
+# flags: --disallow-inferred-unreachable
+from typing import NoReturn
+def assert_never(x: NoReturn) -> NoReturn:
+    assert False
+
+def nonthrowing_assert_never(x: NoReturn) -> None: ...
+
+def expect_str(x: str) -> str: pass
+
+x: int
+if False:
+    assert False
+    reveal_type(x)
+
+if False:
+    raise Exception()
+    reveal_type(x)
+
+if False:
+    assert_never(x)
+    reveal_type(x)
+
+if False:
+    nonthrowing_assert_never(x)  # E: This branch is inferred to be unreachable
+    reveal_type(x)
+
+if False:
+    # Ignore obvious type errors
+    assert_never(expect_str(x))
+    reveal_type(x)
+[builtins fixtures/exception.pyi]
+
+[case testUnreachableFlagExpressions]
+# flags: --disallow-inferred-unreachable
+def foo() -> bool: ...
+
+lst = [1, 2, 3, 4]
+
+a = True or foo()                        # E: The right operand of this 'or' expression is never evaluated
path: test-data/unit/check-unreachable-code.test
text: @user1:
Idea for message: `Right operand of 'or' is never evaluated`

diff_hunk: @@ -715,3 +715,179 @@ if sys.version_info[0] >= 2:
     reveal_type('')  # N: Revealed type is 'builtins.str'
 reveal_type('')  # N: Revealed type is 'builtins.str'
 [builtins fixtures/ops.pyi]
+
+[case testUnreachableFlagWithBadControlFlow]
+# flags: --disallow-inferred-unreachable
+a: int
+if isinstance(a, int):
+    reveal_type(a)  # N: Revealed type is 'builtins.int'
+else:
+    reveal_type(a)  # E: This branch is inferred to be unreachable
path: test-data/unit/check-unreachable-code.test
text: @user1:
Idea for message: `Statement is unreachable`

Even better, maybe we can somehow detect that this is caused by an always-true `isinstance` check and generate an error on the line with the `isinstance` check.

@author:
I experimented a bit with this idea. I think switching to reporting an error on the check might be a bit messy -- it'd actually be much easier to report *both* error message.

Do you think that's ok? If so, I can work on submitting a separate PR with this change after this one is landed. (I'll probably need to do some mild refactoring so we can reuse the "should we report a warning with this line?" logic, and I'd prefer to do that in a separate branch.)

diff_hunk: @@ -492,6 +492,10 @@ def add_invertible_flag(flag: str,
                         help="Treat imports as private unless aliased",
                         group=strictness_group)
 
+    add_invertible_flag('--disallow-inferred-unreachable', default=False, strict_flag=False,
path: mypy/main.py
text: @user1:
Maybe we can make the flag name a bit clearer. What about `--warn-unreachable`? We still allow some unreachable code, but I think that we don't need to make this distinction in the flag name. I think that it's sufficient to describe the semantics in the documentation.

diff_hunk: @@ -715,3 +715,179 @@ if sys.version_info[0] >= 2:
     reveal_type('')  # N: Revealed type is 'builtins.str'
 reveal_type('')  # N: Revealed type is 'builtins.str'
 [builtins fixtures/ops.pyi]
+
+[case testUnreachableFlagWithBadControlFlow]
+# flags: --disallow-inferred-unreachable
+a: int
+if isinstance(a, int):
+    reveal_type(a)  # N: Revealed type is 'builtins.int'
+else:
+    reveal_type(a)  # E: This branch is inferred to be unreachable
+
+b: int
+while isinstance(b, int):
+    reveal_type(b)  # N: Revealed type is 'builtins.int'
+else:
+    reveal_type(b)  # E: This branch is inferred to be unreachable
+
+def foo(c: int) -> None:
+    reveal_type(c)  # N: Revealed type is 'builtins.int'
+    assert not isinstance(c, int)
+    reveal_type(c)  # E: This branch is inferred to be unreachable
+
+d: int
+if False:
+    reveal_type(d)  # E: This branch is inferred to be unreachable
+
+e: int
+if True:
+    reveal_type(e)  # N: Revealed type is 'builtins.int'
+else:
+    reveal_type(e)  # E: This branch is inferred to be unreachable
+
+[builtins fixtures/isinstancelist.pyi]
+
+[case testUnreachableFlagTryBlocks]
+# flags: --disallow-inferred-unreachable
+
+def foo(x: int) -> int:
+    try:
+        reveal_type(x)      # N: Revealed type is 'builtins.int'
+        return x
+        reveal_type(x)      # E: This branch is inferred to be unreachable
+    finally:
+        reveal_type(x)      # N: Revealed type is 'builtins.int'
+        if True:
+            reveal_type(x)  # N: Revealed type is 'builtins.int'
+        else:
+            reveal_type(x)  # E: This branch is inferred to be unreachable
+
+def bar(x: int) -> int:
+    try:
+        if True:
+            raise Exception()
+        reveal_type(x)      # E: This branch is inferred to be unreachable
+    except:
+        reveal_type(x)      # N: Revealed type is 'builtins.int'
+        return x
+    else:
+        reveal_type(x)      # E: This branch is inferred to be unreachable
+
+def baz(x: int) -> int:
+    try:
+        reveal_type(x)      # N: Revealed type is 'builtins.int'
+    except:
+        # Mypy assumes all lines could throw an exception
+        reveal_type(x)      # N: Revealed type is 'builtins.int'
+        return x
+    else:
+        reveal_type(x)      # N: Revealed type is 'builtins.int'
+        return x
+[builtins fixtures/exception.pyi]
+
+[case testUnreachableFlagIgnoresSemanticAnalysisUnreachable]
+# flags: --disallow-inferred-unreachable --python-version 3.7 --platform win32 --always-false FOOBAR
+import sys
+from typing import TYPE_CHECKING
+
+x: int
+if TYPE_CHECKING:
+    reveal_type(x)  # N: Revealed type is 'builtins.int'
+
+if sys.platform == 'darwin':
+    reveal_type(x)
+
+if sys.version_info == (2, 7):
+    reveal_type(x)
+
+FOOBAR = ""
+if FOOBAR:
+    reveal_type(x)
+else:
+    reveal_type(x)  # N: Revealed type is 'builtins.int'
+[builtins fixtures/ops.pyi]
+
+[case testUnreachableFlagIgnoresSemanticAnalysisExprUnreachable]
+# flags: --disallow-inferred-unreachable --always-false FOOBAR
+import sys
+from typing import TYPE_CHECKING
+
+FOOBAR = ""
+
+def foo() -> bool: ...
+
+lst = [1, 2, 3]
+
+a = FOOBAR and foo()
+b = (not FOOBAR) or foo()
+c = 1 if FOOBAR else 2
+d = [x for x in lst if FOOBAR]
+[builtins fixtures/list.pyi]
+
+[case testUnreachableFlagOkWithDeadStatements]
+# flags: --disallow-inferred-unreachable
+from typing import NoReturn
+def assert_never(x: NoReturn) -> NoReturn:
+    assert False
+
+def nonthrowing_assert_never(x: NoReturn) -> None: ...
+
+def expect_str(x: str) -> str: pass
+
+x: int
+if False:
+    assert False
+    reveal_type(x)
+
+if False:
+    raise Exception()
+    reveal_type(x)
+
+if False:
+    assert_never(x)
+    reveal_type(x)
+
+if False:
+    nonthrowing_assert_never(x)  # E: This branch is inferred to be unreachable
+    reveal_type(x)
+
+if False:
+    # Ignore obvious type errors
+    assert_never(expect_str(x))
+    reveal_type(x)
+[builtins fixtures/exception.pyi]
+
+[case testUnreachableFlagExpressions]
+# flags: --disallow-inferred-unreachable
+def foo() -> bool: ...
+
+lst = [1, 2, 3, 4]
+
+a = True or foo()                        # E: The right operand of this 'or' expression is never evaluated
+b = False or foo()                       # E: The left operand of this 'or' expression is always false
+c = True and foo()                       # E: The left operand of this 'and' expression is always true
+d = False and foo()                      # E: The right operand of this 'and' expression is never evaluated
+e = True or (True or (True or foo()))    # E: The right operand of this 'or' expression is never evaluated
+f = (True or foo()) or (True or foo())   # E: The right operand of this 'or' expression is never evaluated
+g = 3 if True else 4                     # E: The 'else' expression in this inline if expression is never evaluated
+h = 3 if False else 4                    # E: The 'if' expression in this inline if expression is never evaluated
+i = [x for x in lst if True]             # E: The conditional check in this comprehension is always true
path: test-data/unit/check-unreachable-code.test
text: @user1:
Idea for message: `If condition in comprehension is always true`