Repository: python/mypy

Issue:
Code is not cheked after some function calls: binder/inference bug
Any code after an ``UninhabitedType`` is marked as unreachable in binder. This leads to dangerous behaviour together with how type inference currently works "in all uncertain situations infer uninhabited" (see also https://github.com/python/mypy/issues/3283 for discussion). For example no error is reported in this code:
```python
from typing import TypeVar
T = TypeVar('T')

def f() -> T:
    ...

class C:
    f()
    1 + 'this is not checked'

def fun() -> None:
    f()
    1 + 'can do everything here'

if True:
    f()
    1 + 'whatever'
```
There are other function signatures (all involve type variables) that lead to this behaviour: type checking stops after a call to such function.


Discussion:
I think that in a dynamic settings `()->T` should not be considered as returning `Uninhabited`. Similarly to how `T->T` is not necessarily the identity function.

Resolving patch:
diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py
--- a/mypy/checkexpr.py
+++ b/mypy/checkexpr.py
@@ -269,7 +269,7 @@ def visit_call_expr(self, e: CallExpr, allow_none_return: bool = False) -> Type:
                 self.check_runtime_protocol_test(e)
             if e.callee.fullname == 'builtins.issubclass':
                 self.check_protocol_issubclass(e)
-        if isinstance(ret_type, UninhabitedType):
+        if isinstance(ret_type, UninhabitedType) and not ret_type.ambiguous:
             self.chk.binder.unreachable()
         if not allow_none_return and isinstance(ret_type, NoneTyp):
             self.chk.msg.does_not_return_value(callee_type, e)
diff --git a/mypy/solve.py b/mypy/solve.py
--- a/mypy/solve.py
+++ b/mypy/solve.py
@@ -63,6 +63,7 @@ def solve_constraints(vars: List[TypeVarId], constraints: List[Constraint],
                 # No constraints for type variable -- 'UninhabitedType' is the most specific type.
                 if strict:
                     candidate = UninhabitedType()
+                    candidate.ambiguous = True
                 else:
                     candidate = AnyType(TypeOfAny.special_form)
         elif top is None:
diff --git a/mypy/types.py b/mypy/types.py
--- a/mypy/types.py
+++ b/mypy/types.py
@@ -357,6 +357,10 @@ class UninhabitedType(Type):
     can_be_true = False
     can_be_false = False
     is_noreturn = False  # Does this come from a NoReturn?  Purely for error messages.
+    # It is important to track whether this is an actual NoReturn type, or just a result
+    # of ambiguous type inference, in the latter case we don't want to mark a branch as
+    # unreachable in binder.
+    ambiguous = False  # Is this a result of inference for a variable without constraints?
 
     def __init__(self, is_noreturn: bool = False, line: int = -1, column: int = -1) -> None:
         super().__init__(line, column)


Test patch:
diff --git a/test-data/unit/check-inference.test b/test-data/unit/check-inference.test
--- a/test-data/unit/check-inference.test
+++ b/test-data/unit/check-inference.test
@@ -1942,3 +1942,52 @@ def main() -> None:
     reveal_type(b) # E: Revealed type is 'builtins.int'
 [builtins fixtures/tuple.pyi]
 [out]
+
+[case testDontMarkUnreachableAfterInferenceUninhabited]
+from typing import TypeVar
+T = TypeVar('T')
+def f() -> T: pass
+
+class C:
+    x = f()
+    def m(self) -> str:
+        return 42 # E: Incompatible return value type (got "int", expected "str")
+
+if bool():
+    f()
+    1 + '' # E: Unsupported left operand type for + ("int")
+[builtins fixtures/list.pyi]
+[out]
+
+[case testDontMarkUnreachableAfterInferenceUninhabited2]
+# flags: --strict-optional
+from typing import TypeVar, Optional
+T = TypeVar('T')
+def f(x: Optional[T] = None) -> T: pass
+
+class C:
+    x = f()
+    def m(self) -> str:
+        return 42 # E: Incompatible return value type (got "int", expected "str")
+
+if bool():
+    f()
+    1 + '' # E: Unsupported left operand type for + ("int")
+[builtins fixtures/list.pyi]
+[out]
+
+[case testDontMarkUnreachableAfterInferenceUninhabited3]
+from typing import TypeVar, List
+T = TypeVar('T')
+def f(x: List[T]) -> T: pass
+
+class C:
+    x = f([])
+    def m(self) -> str:
+        return 42 # E: Incompatible return value type (got "int", expected "str")
+
+if bool():
+    f([])
+    1 + '' # E: Unsupported left operand type for + ("int")
+[builtins fixtures/list.pyi]
+[out]