Skip to content

Commit 9406886

Browse files
ddfisherJukkaL
authored andcommitted
Teach mypy about NoReturn (#2798)
* Add basic NoReturn functionality * Ensure body of NoReturn functions does not return * Add tests
1 parent 88ceec0 commit 9406886

File tree

8 files changed

+90
-6
lines changed

8 files changed

+90
-6
lines changed

mypy/checker.py

+9-1
Original file line numberDiff line numberDiff line change
@@ -648,7 +648,11 @@ def is_implicit_any(t: Type) -> bool:
648648
if self.is_trivial_body(defn.body):
649649
pass
650650
else:
651-
self.msg.note(messages.MISSING_RETURN_STATEMENT, defn)
651+
if isinstance(self.return_types[-1], UninhabitedType):
652+
# This is a NoReturn function
653+
self.msg.note(messages.INVALID_IMPLICIT_RETURN, defn)
654+
else:
655+
self.msg.note(messages.MISSING_RETURN_STATEMENT, defn)
652656

653657
self.return_types.pop()
654658

@@ -1723,6 +1727,10 @@ def check_return_stmt(self, s: ReturnStmt) -> None:
17231727
else:
17241728
return_type = self.return_types[-1]
17251729

1730+
if isinstance(return_type, UninhabitedType):
1731+
self.fail(messages.NO_RETURN_EXPECTED, s)
1732+
return
1733+
17261734
if s.expr:
17271735
# Return with a value.
17281736
typ = self.accept(s.expr, return_type)

mypy/checkexpr.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,10 @@ def visit_call_expr(self, e: CallExpr) -> Type:
184184
isinstance(callee_type, CallableType)
185185
and callee_type.implicit):
186186
return self.msg.untyped_function_call(callee_type, e)
187-
return self.check_call_expr_with_callee_type(callee_type, e)
187+
ret_type = self.check_call_expr_with_callee_type(callee_type, e)
188+
if isinstance(ret_type, UninhabitedType):
189+
self.chk.binder.unreachable()
190+
return ret_type
188191

189192
def check_typeddict_call(self, callee: TypedDictType,
190193
arg_kinds: List[int],

mypy/messages.py

+9-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@
1111
from mypy.errors import Errors
1212
from mypy.types import (
1313
Type, CallableType, Instance, TypeVarType, TupleType, TypedDictType,
14-
UnionType, Void, NoneTyp, AnyType, Overloaded, FunctionLike, DeletedType, TypeType
14+
UnionType, Void, NoneTyp, AnyType, Overloaded, FunctionLike, DeletedType, TypeType,
15+
UninhabitedType
1516
)
1617
from mypy.nodes import (
1718
TypeInfo, Context, MypyFile, op_methods, FuncDef, reverse_type_aliases,
@@ -24,8 +25,10 @@
2425

2526
NO_RETURN_VALUE_EXPECTED = 'No return value expected'
2627
MISSING_RETURN_STATEMENT = 'Missing return statement'
28+
INVALID_IMPLICIT_RETURN = 'Implicit return in function which does not return'
2729
INCOMPATIBLE_RETURN_VALUE_TYPE = 'Incompatible return value type'
2830
RETURN_VALUE_EXPECTED = 'Return value expected'
31+
NO_RETURN_EXPECTED = 'Return statement in function which does not return'
2932
INVALID_EXCEPTION = 'Exception must be derived from BaseException'
3033
INVALID_EXCEPTION_TYPE = 'Exception type must be derived from BaseException'
3134
INVALID_RETURN_TYPE_FOR_GENERATOR = \
@@ -319,6 +322,11 @@ def format_simple(self, typ: Type, verbosity: int = 0) -> str:
319322
return '"Any"'
320323
elif isinstance(typ, DeletedType):
321324
return '<deleted>'
325+
elif isinstance(typ, UninhabitedType):
326+
if typ.is_noreturn:
327+
return 'NoReturn'
328+
else:
329+
return '<uninhabited>'
322330
elif isinstance(typ, TypeType):
323331
return 'Type[{}]'.format(
324332
strip_quotes(self.format_simple(typ.item, verbosity)))

mypy/typeanal.py

+2
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,8 @@ def visit_unbound_type(self, t: UnboundType) -> Type:
148148
items = self.anal_array(t.args)
149149
item = items[0]
150150
return TypeType(item, line=t.line)
151+
elif fullname == 'mypy_extensions.NoReturn':
152+
return UninhabitedType(is_noreturn=True)
151153
elif sym.kind == TYPE_ALIAS:
152154
override = sym.type_override
153155
an_args = self.anal_array(t.args)

mypy/types.py

+6-3
Original file line numberDiff line numberDiff line change
@@ -323,20 +323,23 @@ class UninhabitedType(Type):
323323

324324
can_be_true = False
325325
can_be_false = False
326+
is_noreturn = False # Does this come from a NoReturn? Purely for error messages.
326327

327-
def __init__(self, line: int = -1, column: int = -1) -> None:
328+
def __init__(self, is_noreturn: bool = False, line: int = -1, column: int = -1) -> None:
328329
super().__init__(line, column)
330+
self.is_noreturn = is_noreturn
329331

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

333335
def serialize(self) -> JsonDict:
334-
return {'.class': 'UninhabitedType'}
336+
return {'.class': 'UninhabitedType',
337+
'is_noreturn': self.is_noreturn}
335338

336339
@classmethod
337340
def deserialize(cls, data: JsonDict) -> 'UninhabitedType':
338341
assert data['.class'] == 'UninhabitedType'
339-
return UninhabitedType()
342+
return UninhabitedType(is_noreturn=data['is_noreturn'])
340343

341344

342345
class NoneTyp(Type):

test-data/unit/check-flags.test

+57
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,63 @@ def f() -> int:
136136
return 0
137137
[builtins fixtures/bool.pyi]
138138

139+
[case testNoReturnDisallowsReturn]
140+
# flags: --warn-no-return
141+
from mypy_extensions import NoReturn
142+
143+
def f() -> NoReturn:
144+
if bool():
145+
return 5 # E: Return statement in function which does not return
146+
else:
147+
return # E: Return statement in function which does not return
148+
[builtins fixtures/dict.pyi]
149+
150+
[case testNoReturnWithoutImplicitReturn]
151+
# flags: --warn-no-return
152+
from mypy_extensions import NoReturn
153+
154+
def no_return() -> NoReturn: pass
155+
def f() -> NoReturn:
156+
no_return()
157+
[builtins fixtures/dict.pyi]
158+
159+
[case testNoReturnDisallowsImplicitReturn]
160+
# flags: --warn-no-return
161+
from mypy_extensions import NoReturn
162+
163+
def f() -> NoReturn: # N: Implicit return in function which does not return
164+
non_trivial_function = 1
165+
[builtins fixtures/dict.pyi]
166+
167+
[case testNoReturnNoWarnNoReturn]
168+
# flags: --warn-no-return
169+
from mypy_extensions import NoReturn
170+
171+
def no_return() -> NoReturn: pass
172+
def f() -> int:
173+
if bool():
174+
return 0
175+
else:
176+
no_return()
177+
[builtins fixtures/dict.pyi]
178+
179+
[case testNoReturnInExpr]
180+
# flags: --warn-no-return
181+
from mypy_extensions import NoReturn
182+
183+
def no_return() -> NoReturn: pass
184+
def f() -> int:
185+
return 0
186+
reveal_type(f() or no_return()) # E: Revealed type is 'builtins.int'
187+
[builtins fixtures/dict.pyi]
188+
189+
[case testNoReturnVariable]
190+
# flags: --warn-no-return
191+
from mypy_extensions import NoReturn
192+
193+
x = 0 # type: NoReturn # E: Incompatible types in assignment (expression has type "int", variable has type NoReturn)
194+
[builtins fixtures/dict.pyi]
195+
139196
[case testShowErrorContextFunction]
140197
# flags: --show-error-context
141198
def f() -> None:

test-data/unit/fixtures/dict.pyi

+1
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,4 @@ class list(Iterable[T], Generic[T]): # needed by some test cases
3333
class tuple: pass
3434
class function: pass
3535
class float: pass
36+
class bool: pass

test-data/unit/lib-stub/mypy_extensions.pyi

+2
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,5 @@ T = TypeVar('T')
44

55

66
def TypedDict(typename: str, fields: Dict[str, Type[T]]) -> Type[dict]: pass
7+
8+
class NoReturn: pass

0 commit comments

Comments
 (0)