diff --git a/mypy/checker.py b/mypy/checker.py index 76b229b8e405..b56ef2c9ccea 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -316,7 +316,10 @@ def get_generator_yield_type(self, return_type: Type, is_coroutine: bool) -> Typ ret_type = return_type.args[0] # TODO not best fix, better have dedicated yield token if isinstance(ret_type, NoneTyp): - return Void() + if experiments.STRICT_OPTIONAL: + return NoneTyp(is_ret_type=True) + else: + return Void() return ret_type else: # If the function's declared supertype of Generator has no type @@ -348,7 +351,10 @@ def get_generator_receive_type(self, return_type: Type, is_coroutine: bool) -> T else: # `return_type` is a supertype of Generator, so callers won't be able to send it # values. - return Void() + if experiments.STRICT_OPTIONAL: + return NoneTyp(is_ret_type=True) + else: + return Void() def get_generator_return_type(self, return_type: Type, is_coroutine: bool) -> Type: """Given the declared return type of a generator (t), return the type it returns (tr).""" @@ -492,7 +498,7 @@ def check_func_def(self, defn: FuncItem, typ: CallableType, name: str) -> None: if fdef: # Check if __init__ has an invalid, non-None return type. if (fdef.info and fdef.name() == '__init__' and - not isinstance(typ.ret_type, Void) and + not isinstance(typ.ret_type, (Void, NoneTyp)) and not self.dynamic_funcs[-1]): self.fail(messages.INIT_MUST_HAVE_NONE_RETURN_TYPE, item.type) @@ -531,8 +537,7 @@ def is_implicit_any(t: Type) -> bool: if (self.options.python_version[0] == 2 and isinstance(typ.ret_type, Instance) and typ.ret_type.type.fullname() == 'typing.Generator'): - if not (isinstance(typ.ret_type.args[2], Void) - or isinstance(typ.ret_type.args[2], AnyType)): + if not isinstance(typ.ret_type.args[2], (Void, NoneTyp, AnyType)): self.fail(messages.INVALID_GENERATOR_RETURN_ITEM_TYPE, typ) # Push return type. @@ -1270,8 +1275,8 @@ def infer_variable_type(self, name: Var, lvalue: Node, if self.typing_mode_weak(): self.set_inferred_type(name, lvalue, AnyType()) self.binder.assign_type(lvalue, init_type, self.binder.get_declaration(lvalue), True) - elif isinstance(init_type, Void): - self.check_not_void(init_type, context) + elif self.is_unusable_type(init_type): + self.check_usable_type(init_type, context) self.set_inference_error_fallback_type(name, lvalue, init_type, context) elif isinstance(init_type, DeletedType): self.msg.deleted_as_rvalue(init_type, context) @@ -1429,8 +1434,8 @@ def visit_return_stmt(self, s: ReturnStmt) -> Type: if isinstance(typ, AnyType): return None - if isinstance(return_type, Void): - # Lambdas are allowed to have a Void return. + if self.is_unusable_type(return_type): + # Lambdas are allowed to have a unusable returns. # Functions returning a value of type None are allowed to have a Void return. if isinstance(self.function_stack[-1], FuncExpr) or isinstance(typ, NoneTyp): return None @@ -1488,7 +1493,7 @@ def visit_if_stmt(self, s: IfStmt) -> Type: with self.binder.frame_context(): for e, b in zip(s.expr, s.body): t = self.accept(e) - self.check_not_void(t, e) + self.check_usable_type(t, e) if_map, else_map = find_isinstance_check( e, self.type_map, self.typing_mode_weak() @@ -1696,7 +1701,7 @@ def analyze_async_iterable_item_type(self, expr: Node) -> Type: """Analyse async iterable expression and return iterator item type.""" iterable = self.accept(expr) - self.check_not_void(iterable, expr) + self.check_usable_type(iterable, expr) self.check_subtype(iterable, self.named_generic_type('typing.AsyncIterable', @@ -1715,7 +1720,7 @@ def analyze_iterable_item_type(self, expr: Node) -> Type: """Analyse iterable expression and return iterator item type.""" iterable = self.accept(expr) - self.check_not_void(iterable, expr) + self.check_usable_type(iterable, expr) if isinstance(iterable, TupleType): if experiments.STRICT_OPTIONAL: joined = UninhabitedType() # type: Type @@ -1912,7 +1917,10 @@ def visit_yield_from_expr(self, e: YieldFromExpr) -> Type: if isinstance(actual_item_type, AnyType): return AnyType() else: - return Void() + if experiments.STRICT_OPTIONAL: + return NoneTyp(is_ret_type=True) + else: + return Void() def visit_member_expr(self, e: MemberExpr) -> Type: return self.expr_checker.visit_member_expr(e) @@ -2029,8 +2037,7 @@ def visit_yield_expr(self, e: YieldExpr) -> Type: return_type = self.return_types[-1] expected_item_type = self.get_generator_yield_type(return_type, False) if e.expr is None: - if (not (isinstance(expected_item_type, Void) or - isinstance(expected_item_type, AnyType)) + if (not isinstance(expected_item_type, (Void, NoneTyp, AnyType)) and self.typing_mode_full()): self.fail(messages.YIELD_VALUE_EXPECTED, e) else: @@ -2062,7 +2069,7 @@ def check_subtype(self, subtype: Type, supertype: Type, context: Context, if is_subtype(subtype, supertype): return True else: - if isinstance(subtype, Void): + if self.is_unusable_type(subtype): self.msg.does_not_return_value(subtype, context) else: extra_info = [] # type: List[str] @@ -2211,9 +2218,16 @@ def is_within_function(self) -> bool: """ return self.return_types != [] - def check_not_void(self, typ: Type, context: Context) -> None: - """Generate an error if the type is Void.""" - if isinstance(typ, Void): + def is_unusable_type(self, typ: Type): + """Is this type an unusable type? + + The two unusable types are Void and NoneTyp(is_ret_type=True). + """ + return isinstance(typ, Void) or (isinstance(typ, NoneTyp) and typ.is_ret_type) + + def check_usable_type(self, typ: Type, context: Context) -> None: + """Generate an error if the type is not a usable type.""" + if self.is_unusable_type(typ): self.msg.does_not_return_value(typ, context) def temp_node(self, t: Type, context: Context = None) -> Node: diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 92dfbf71214d..62835dda6a88 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -702,7 +702,7 @@ def check_arg(self, caller_type: Type, original_caller_type: Type, callee_type: Type, n: int, m: int, callee: CallableType, context: Context, messages: MessageBuilder) -> None: """Check the type of a single argument in a call.""" - if isinstance(caller_type, Void): + if self.chk.is_unusable_type(caller_type): messages.does_not_return_value(caller_type, context) elif isinstance(caller_type, DeletedType): messages.deleted_as_rvalue(caller_type, context) @@ -997,7 +997,7 @@ def visit_comparison_expr(self, e: ComparisonExpr) -> Type: result = sub_result else: # TODO: check on void needed? - self.check_not_void(sub_result, e) + self.check_usable_type(sub_result, e) result = join.join_types(result, sub_result) return result @@ -1116,8 +1116,8 @@ def check_boolean_op(self, e: OpExpr, context: Context) -> Type: right_type = self.accept(e.right, left_type) - self.check_not_void(left_type, context) - self.check_not_void(right_type, context) + self.check_usable_type(left_type, context) + self.check_usable_type(right_type, context) # If either of the type maps is None that means that result cannot happen. # If both of the type maps are None we just have no information. @@ -1148,7 +1148,7 @@ def visit_unary_expr(self, e: UnaryExpr) -> Type: operand_type = self.accept(e.expr) op = e.op if op == 'not': - self.check_not_void(operand_type, e) + self.check_usable_type(operand_type, e) result = self.chk.bool_type() # type: Type elif op == '-': method_type = self.analyze_external_member_access('__neg__', @@ -1340,7 +1340,7 @@ def visit_tuple_expr(self, e: TupleExpr) -> Type: # context? Counterargument: Why would anyone write # (1, *(2, 3)) instead of (1, 2, 3) except in a test? tt = self.accept(item.expr) - self.check_not_void(tt, e) + self.check_usable_type(tt, e) if isinstance(tt, TupleType): items.extend(tt.items) j += len(tt.items) @@ -1354,7 +1354,7 @@ def visit_tuple_expr(self, e: TupleExpr) -> Type: else: tt = self.accept(item, ctx.items[j]) j += 1 - self.check_not_void(tt, e) + self.check_usable_type(tt, e) items.append(tt) fallback_item = join.join_type_list(items) return TupleType(items, self.chk.named_generic_type('builtins.tuple', [fallback_item])) @@ -1581,7 +1581,7 @@ def check_for_comp(self, e: Union[GeneratorExpr, DictionaryComprehension]) -> No def visit_conditional_expr(self, e: ConditionalExpr) -> Type: cond_type = self.accept(e.cond) - self.check_not_void(cond_type, e) + self.check_usable_type(cond_type, e) ctx = self.chk.type_context[-1] # Gain type information from isinstance if it is there @@ -1634,9 +1634,9 @@ def accept(self, node: Node, context: Type = None) -> Type: """Type check a node. Alias for TypeChecker.accept.""" return self.chk.accept(node, context) - def check_not_void(self, typ: Type, context: Context) -> None: + def check_usable_type(self, typ: Type, context: Context) -> None: """Generate an error if type is Void.""" - self.chk.check_not_void(typ, context) + self.chk.check_usable_type(typ, context) def is_boolean(self, typ: Type) -> bool: """Is type compatible with bool?""" diff --git a/mypy/erasetype.py b/mypy/erasetype.py index 979b6bb0a223..3f53f75219b2 100644 --- a/mypy/erasetype.py +++ b/mypy/erasetype.py @@ -5,6 +5,7 @@ Instance, TypeVarType, CallableType, TupleType, UnionType, Overloaded, ErasedType, PartialType, DeletedType, TypeTranslator, TypeList, UninhabitedType, TypeType ) +from mypy import experiments def erase_type(typ: Type) -> Type: @@ -65,7 +66,11 @@ def visit_type_var(self, t: TypeVarType) -> Type: def visit_callable_type(self, t: CallableType) -> Type: # We must preserve the fallback type for overload resolution to work. - return CallableType([], [], [], Void(), t.fallback) + if experiments.STRICT_OPTIONAL: + ret_type = NoneTyp(is_ret_type=True) # type: Type + else: + ret_type = Void() + return CallableType([], [], [], ret_type, t.fallback) def visit_overloaded(self, t: Overloaded) -> Type: return t.items()[0].accept(self) diff --git a/mypy/messages.py b/mypy/messages.py index b4240f006dc1..d04050f6f343 100644 --- a/mypy/messages.py +++ b/mypy/messages.py @@ -320,8 +320,8 @@ def has_no_attr(self, typ: Type, member: str, context: Context) -> Type: if (isinstance(typ, Instance) and typ.type.has_readable_member(member)): self.fail('Member "{}" is not assignable'.format(member), context) - elif isinstance(typ, Void): - self.check_void(typ, context) + elif self.check_unusable_type(typ, context): + pass elif member == '__contains__': self.fail('Unsupported right operand type for in ({})'.format( self.format(typ)), context) @@ -376,9 +376,8 @@ def unsupported_operand_types(self, op: str, left_type: Any, Types can be Type objects or strings. """ - if isinstance(left_type, Void) or isinstance(right_type, Void): - self.check_void(left_type, context) - self.check_void(right_type, context) + if (self.check_unusable_type(left_type, context) or + self.check_unusable_type(right_type, context)): return left_str = '' if isinstance(left_type, str): @@ -401,7 +400,7 @@ def unsupported_operand_types(self, op: str, left_type: Any, def unsupported_left_operand(self, op: str, typ: Type, context: Context) -> None: - if not self.check_void(typ, context): + if not self.check_unusable_type(typ, context): if self.disable_type_names: msg = 'Unsupported left operand type for {} (some union)'.format(op) else: @@ -554,18 +553,17 @@ def duplicate_argument_value(self, callee: CallableType, index: int, format(capitalize(callable_name(callee)), callee.arg_names[index]), context) - def does_not_return_value(self, void_type: Type, context: Context) -> None: - """Report an error about a void type in a non-void context. + def does_not_return_value(self, unusable_type: Type, context: Context) -> None: + """Report an error about use of an unusable type. - The first argument must be a void type. If the void type has a - source in it, report it in the error message. This allows - giving messages such as 'Foo does not return a value'. + If the type is a Void type and has a source in it, report it in the error message. + This allows giving messages such as 'Foo does not return a value'. """ - if (cast(Void, void_type)).source is None: - self.fail('Function does not return a value', context) - else: + if isinstance(unusable_type, Void) and unusable_type.source is not None: self.fail('{} does not return a value'.format( - capitalize((cast(Void, void_type)).source)), context) + capitalize((cast(Void, unusable_type)).source)), context) + else: + self.fail('Function does not return a value', context) def deleted_as_rvalue(self, typ: DeletedType, context: Context) -> None: """Report an error about using an deleted type as an rvalue.""" @@ -602,7 +600,7 @@ def function_variants_overlap(self, n1: int, n2: int, def invalid_cast(self, target_type: Type, source_type: Type, context: Context) -> None: - if not self.check_void(source_type, context): + if not self.check_unusable_type(source_type, context): self.fail('Cannot cast from {} to {}'.format( self.format(source_type), self.format(target_type)), context) @@ -720,11 +718,13 @@ def not_implemented(self, msg: str, context: Context) -> Type: def undefined_in_superclass(self, member: str, context: Context) -> None: self.fail('"{}" undefined in superclass'.format(member), context) - def check_void(self, typ: Type, context: Context) -> bool: - """If type is void, report an error such as '.. does not + def check_unusable_type(self, typ: Type, context: Context) -> bool: + """If type is a type which is not meant to be used (like Void or + NoneTyp(is_ret_type=True)), report an error such as '.. does not return a value' and return True. Otherwise, return False. """ - if isinstance(typ, Void): + if (isinstance(typ, Void) or + (isinstance(typ, NoneTyp) and typ.is_ret_type)): self.does_not_return_value(typ, context) return True else: diff --git a/mypy/typeanal.py b/mypy/typeanal.py index 0493d3a15f6b..1723033631a4 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -95,10 +95,7 @@ def visit_unbound_type(self, t: UnboundType) -> Type: return TypeVarType(sym.tvar_def, t.line) elif fullname == 'builtins.None': if experiments.STRICT_OPTIONAL: - if t.is_ret_type: - return Void() - else: - return NoneTyp() + return NoneTyp(is_ret_type=t.is_ret_type) else: return Void() elif fullname == 'typing.Any': diff --git a/mypy/types.py b/mypy/types.py index 1f6d9574a595..59ed4f9b09e8 100644 --- a/mypy/types.py +++ b/mypy/types.py @@ -327,19 +327,22 @@ class NoneTyp(Type): of a function, where 'None' means Void. """ - def __init__(self, line: int = -1) -> None: + def __init__(self, is_ret_type: bool = False, line: int = -1) -> None: super().__init__(line) + self.is_ret_type = is_ret_type def accept(self, visitor: 'TypeVisitor[T]') -> T: return visitor.visit_none_type(self) def serialize(self) -> JsonDict: - return {'.class': 'NoneTyp'} + return {'.class': 'NoneTyp', + 'is_ret_type': self.is_ret_type, + } @classmethod def deserialize(self, data: JsonDict) -> 'NoneTyp': assert data['.class'] == 'NoneTyp' - return NoneTyp() + return NoneTyp(is_ret_type=data['is_ret_type']) class ErasedType(Type): diff --git a/test-data/unit/check-optional.test b/test-data/unit/check-optional.test index 6e896e043af6..0367116224fe 100644 --- a/test-data/unit/check-optional.test +++ b/test-data/unit/check-optional.test @@ -81,11 +81,6 @@ else: f = lambda: None x = f() -[case testFunctionStillVoid] -def f() -> None: pass -f() -x = f() # E: "f" does not return a value - [case testNoneArgumentType] def f(x: None) -> None: pass f(None) @@ -274,3 +269,32 @@ from typing import overload def f() -> None: ... @overload def f(o: object) -> None: ... + +[case testGenericSubclassReturningNone] +from typing import Generic, TypeVar + +T = TypeVar('T') + +class Base(Generic[T]): + def f(self) -> T: + pass + +class SubNone(Base[None]): + def f(self) -> None: + pass + +class SubInt(Base[int]): + def f(self) -> int: + return 1 + +[case testUseOfNoneReturningFunction] +from typing import Optional +def f() -> None: + pass + +def g(x: Optional[int]) -> int: + pass + +x = f() # E: Function does not return a value +f() + 1 # E: Function does not return a value +g(f()) # E: Function does not return a value