Skip to content

Teach mypy about NoReturn #2798

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

Merged
merged 4 commits into from
Feb 7, 2017
Merged
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
10 changes: 9 additions & 1 deletion mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -648,7 +648,11 @@ def is_implicit_any(t: Type) -> bool:
if self.is_trivial_body(defn.body):
pass
else:
self.msg.note(messages.MISSING_RETURN_STATEMENT, defn)
if isinstance(self.return_types[-1], UninhabitedType):
# This is a NoReturn function
self.msg.note(messages.INVALID_IMPLICIT_RETURN, defn)
else:
self.msg.note(messages.MISSING_RETURN_STATEMENT, defn)

self.return_types.pop()

Expand Down Expand Up @@ -1721,6 +1725,10 @@ def check_return_stmt(self, s: ReturnStmt) -> None:
else:
return_type = self.return_types[-1]

if isinstance(return_type, UninhabitedType):
self.fail(messages.NO_RETURN_EXPECTED, s)
return

if s.expr:
# Return with a value.
typ = self.accept(s.expr, return_type)
Expand Down
5 changes: 4 additions & 1 deletion mypy/checkexpr.py
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,10 @@ def visit_call_expr(self, e: CallExpr) -> Type:
isinstance(callee_type, CallableType)
and callee_type.implicit):
return self.msg.untyped_function_call(callee_type, e)
return self.check_call_expr_with_callee_type(callee_type, e)
ret_type = self.check_call_expr_with_callee_type(callee_type, e)
if isinstance(ret_type, UninhabitedType):
self.chk.binder.unreachable()
return ret_type

def check_typeddict_call(self, callee: TypedDictType,
arg_kinds: List[int],
Expand Down
10 changes: 9 additions & 1 deletion mypy/messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
from mypy.errors import Errors
from mypy.types import (
Type, CallableType, Instance, TypeVarType, TupleType, TypedDictType,
UnionType, Void, NoneTyp, AnyType, Overloaded, FunctionLike, DeletedType, TypeType
UnionType, Void, NoneTyp, AnyType, Overloaded, FunctionLike, DeletedType, TypeType,
UninhabitedType
)
from mypy.nodes import (
TypeInfo, Context, MypyFile, op_methods, FuncDef, reverse_type_aliases,
Expand All @@ -24,8 +25,10 @@

NO_RETURN_VALUE_EXPECTED = 'No return value expected'
MISSING_RETURN_STATEMENT = 'Missing return statement'
INVALID_IMPLICIT_RETURN = 'Implicit return in function which does not return'
INCOMPATIBLE_RETURN_VALUE_TYPE = 'Incompatible return value type'
RETURN_VALUE_EXPECTED = 'Return value expected'
NO_RETURN_EXPECTED = 'Return statement in function which does not return'
INVALID_EXCEPTION = 'Exception must be derived from BaseException'
INVALID_EXCEPTION_TYPE = 'Exception type must be derived from BaseException'
INVALID_RETURN_TYPE_FOR_GENERATOR = \
Expand Down Expand Up @@ -319,6 +322,11 @@ def format_simple(self, typ: Type, verbosity: int = 0) -> str:
return '"Any"'
elif isinstance(typ, DeletedType):
return '<deleted>'
elif isinstance(typ, UninhabitedType):
if typ.is_noreturn:
return 'NoReturn'
else:
return '<uninhabited>'
elif isinstance(typ, TypeType):
return 'Type[{}]'.format(
strip_quotes(self.format_simple(typ.item, verbosity)))
Expand Down
2 changes: 2 additions & 0 deletions mypy/typeanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,8 @@ def visit_unbound_type(self, t: UnboundType) -> Type:
items = self.anal_array(t.args)
item = items[0]
return TypeType(item, line=t.line)
elif fullname == 'mypy_extensions.NoReturn':
return UninhabitedType(is_noreturn=True)
elif sym.kind == TYPE_ALIAS:
override = sym.type_override
an_args = self.anal_array(t.args)
Expand Down
9 changes: 6 additions & 3 deletions mypy/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -323,20 +323,23 @@ class UninhabitedType(Type):

can_be_true = False
can_be_false = False
is_noreturn = False # Does this come from a NoReturn? Purely for error messages.

def __init__(self, line: int = -1, column: int = -1) -> None:
def __init__(self, is_noreturn: bool = False, line: int = -1, column: int = -1) -> None:
super().__init__(line, column)
self.is_noreturn = is_noreturn

def accept(self, visitor: 'TypeVisitor[T]') -> T:
return visitor.visit_uninhabited_type(self)

def serialize(self) -> JsonDict:
return {'.class': 'UninhabitedType'}
return {'.class': 'UninhabitedType',
'is_noreturn': self.is_noreturn}

@classmethod
def deserialize(cls, data: JsonDict) -> 'UninhabitedType':
assert data['.class'] == 'UninhabitedType'
return UninhabitedType()
return UninhabitedType(is_noreturn=data['is_noreturn'])


class NoneTyp(Type):
Expand Down
57 changes: 57 additions & 0 deletions test-data/unit/check-flags.test
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,63 @@ def f() -> int:
return 0
[builtins fixtures/bool.pyi]

[case testNoReturnDisallowsReturn]
# flags: --warn-no-return
from mypy_extensions import NoReturn

def f() -> NoReturn:
if bool():
return 5 # E: Return statement in function which does not return
else:
return # E: Return statement in function which does not return
[builtins fixtures/dict.pyi]

[case testNoReturnWithoutImplicitReturn]
# flags: --warn-no-return
from mypy_extensions import NoReturn

def no_return() -> NoReturn: pass
def f() -> NoReturn:
no_return()
[builtins fixtures/dict.pyi]

[case testNoReturnDisallowsImplicitReturn]
# flags: --warn-no-return
from mypy_extensions import NoReturn

def f() -> NoReturn: # N: Implicit return in function which does not return
non_trivial_function = 1
[builtins fixtures/dict.pyi]

[case testNoReturnNoWarnNoReturn]
# flags: --warn-no-return
from mypy_extensions import NoReturn

def no_return() -> NoReturn: pass
def f() -> int:
if bool():
return 0
else:
no_return()
[builtins fixtures/dict.pyi]

[case testNoReturnInExpr]
# flags: --warn-no-return
from mypy_extensions import NoReturn

def no_return() -> NoReturn: pass
def f() -> int:
return 0
reveal_type(f() or no_return()) # E: Revealed type is 'builtins.int'
[builtins fixtures/dict.pyi]

[case testNoReturnVariable]
# flags: --warn-no-return
from mypy_extensions import NoReturn

x = 0 # type: NoReturn # E: Incompatible types in assignment (expression has type "int", variable has type NoReturn)
[builtins fixtures/dict.pyi]

[case testShowErrorContextFunction]
# flags: --show-error-context
def f() -> None:
Expand Down
1 change: 1 addition & 0 deletions test-data/unit/fixtures/dict.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,4 @@ class list(Iterable[T], Generic[T]): # needed by some test cases
class tuple: pass
class function: pass
class float: pass
class bool: pass
2 changes: 2 additions & 0 deletions test-data/unit/lib-stub/mypy_extensions.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,5 @@ T = TypeVar('T')


def TypedDict(typename: str, fields: Dict[str, Type[T]]) -> Type[dict]: pass

class NoReturn: pass