diff --git a/docs/source/command_line.rst b/docs/source/command_line.rst index ad7555bb2759..5b41a490102b 100644 --- a/docs/source/command_line.rst +++ b/docs/source/command_line.rst @@ -278,7 +278,7 @@ Here are some more useful flags: - ``--disallow-any`` disallows various types of ``Any`` in a module. The option takes a comma-separated list of the following values: - ``unimported``, ``unannotated``. + ``unimported``, ``unannotated``, ``expr``. ``unimported`` disallows usage of types that come from unfollowed imports (such types become aliases for ``Any``). Unfollowed imports occur either @@ -290,6 +290,15 @@ Here are some more useful flags: of the parameters or the return type). ``unannotated`` option is interchangeable with ``--disallow-untyped-defs``. + ``expr`` disallows all expressions in the module that have type ``Any``. + If an expression of type ``Any`` appears anywhere in the module + mypy will output an error unless the expression is immediately + used as an argument to ``cast`` or assigned to a variable with an + explicit type annotation. In addition, declaring a variable of type ``Any`` + or casting to type ``Any`` is not allowed. Note that calling functions + that take parameters of type ``Any`` is still allowed. + + - ``--disallow-untyped-defs`` reports an error whenever it encounters a function definition without type annotations. diff --git a/docs/source/config_file.rst b/docs/source/config_file.rst index 07db822df8a1..6fbcfbb2db9a 100644 --- a/docs/source/config_file.rst +++ b/docs/source/config_file.rst @@ -150,7 +150,7 @@ overridden by the pattern sections matching the module name. - ``disallow_any`` (Comma-separated list, default empty) is an option to disallow various types of ``Any`` in a module. The flag takes a comma-separated list of the following arguments: ``unimported``, - ``unannotated``. For explanations see the discussion for the + ``unannotated``, ``expr``. For explanations see the discussion for the :ref:`--disallow-any ` option. - ``disallow_untyped_calls`` (Boolean, default False) disallows diff --git a/mypy/checker.py b/mypy/checker.py index 98bb35474bf9..9562e5bc7fd8 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -1736,7 +1736,9 @@ def check_simple_assignment(self, lvalue_type: Type, rvalue: Expression, # '...' is always a valid initializer in a stub. return AnyType() else: - rvalue_type = self.expr_checker.accept(rvalue, lvalue_type) + always_allow_any = lvalue_type is not None and not isinstance(lvalue_type, AnyType) + rvalue_type = self.expr_checker.accept(rvalue, lvalue_type, + always_allow_any=always_allow_any) if isinstance(rvalue_type, DeletedType): self.msg.deleted_as_rvalue(rvalue_type, context) if isinstance(lvalue_type, DeletedType): @@ -1855,7 +1857,7 @@ def try_infer_partial_type_from_indexed_assignment( del partial_types[var] def visit_expression_stmt(self, s: ExpressionStmt) -> None: - self.expr_checker.accept(s.expr, allow_none_return=True) + self.expr_checker.accept(s.expr, allow_none_return=True, always_allow_any=True) def visit_return_stmt(self, s: ReturnStmt) -> None: """Type check a return statement.""" diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 027c0d85a854..2f4246cffd92 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -204,7 +204,7 @@ def visit_call_expr(self, e: CallExpr, allow_none_return: bool = False) -> Type: or isinstance(typ, NameExpr) and node and node.kind == nodes.TYPE_ALIAS): self.msg.type_arguments_not_allowed(e) self.try_infer_partial_type(e) - callee_type = self.accept(e.callee) + callee_type = self.accept(e.callee, always_allow_any=True) if (self.chk.options.disallow_untyped_calls and self.chk.in_checked_function() and isinstance(callee_type, CallableType) @@ -1670,7 +1670,8 @@ def visit_enum_index_expr(self, enum_type: TypeInfo, index: Expression, def visit_cast_expr(self, expr: CastExpr) -> Type: """Type check a cast expression.""" - source_type = self.accept(expr.expr, type_context=AnyType(), allow_none_return=True) + source_type = self.accept(expr.expr, type_context=AnyType(), allow_none_return=True, + always_allow_any=True) target_type = expr.type options = self.chk.options if options.warn_redundant_casts and is_same_type(source_type, target_type): @@ -2191,7 +2192,8 @@ def visit_backquote_expr(self, e: BackquoteExpr) -> Type: def accept(self, node: Expression, type_context: Type = None, - allow_none_return: bool = False + allow_none_return: bool = False, + always_allow_any: bool = False, ) -> Type: """Type check a node in the given type context. If allow_none_return is True and this expression is a call, allow it to return None. This @@ -2211,6 +2213,14 @@ def accept(self, self.type_context.pop() assert typ is not None self.chk.store_type(node, typ) + + if ('expr' in self.chk.options.disallow_any and + not always_allow_any and + not self.chk.is_stub and + self.chk.in_checked_function() and + has_any_type(typ)): + self.msg.disallowed_any_type(typ, node) + if not self.chk.in_checked_function(): return AnyType() else: @@ -2429,6 +2439,19 @@ def narrow_type_from_binder(self, expr: Expression, known_type: Type) -> Type: return known_type +def has_any_type(t: Type) -> bool: + """Whether t contains an Any type""" + return t.accept(HasAnyType()) + + +class HasAnyType(types.TypeQuery[bool]): + def __init__(self) -> None: + super().__init__(any) + + def visit_any(self, t: AnyType) -> bool: + return True + + def has_coroutine_decorator(t: Type) -> bool: """Whether t came from a function decorated with `@coroutine`.""" return isinstance(t, Instance) and t.type.fullname() == 'typing.AwaitableGenerator' diff --git a/mypy/main.py b/mypy/main.py index a31feb2ff6be..56aa4bdd8738 100644 --- a/mypy/main.py +++ b/mypy/main.py @@ -97,7 +97,7 @@ def type_check_only(sources: List[BuildSource], bin_dir: str, options: Options) options=options) -disallow_any_options = ['unimported', 'unannotated'] +disallow_any_options = ['unimported', 'expr', 'unannotated'] def disallow_any_argument_type(raw_options: str) -> List[str]: diff --git a/mypy/messages.py b/mypy/messages.py index 223118c40fff..067cbd37f744 100644 --- a/mypy/messages.py +++ b/mypy/messages.py @@ -904,6 +904,13 @@ def typeddict_item_name_not_found(self, def type_arguments_not_allowed(self, context: Context) -> None: self.fail('Parameterized generics cannot be used with class or instance checks', context) + def disallowed_any_type(self, typ: Type, context: Context) -> None: + if isinstance(typ, AnyType): + message = 'Expression has type "Any"' + else: + message = 'Expression type contains "Any" (has type {})'.format(self.format(typ)) + self.fail(message, context) + def capitalize(s: str) -> str: """Capitalize the first character of a string.""" diff --git a/test-data/unit/check-flags.test b/test-data/unit/check-flags.test index 6ee0247c9bf4..08c5a40aaec3 100644 --- a/test-data/unit/check-flags.test +++ b/test-data/unit/check-flags.test @@ -515,3 +515,78 @@ x, y = 1, 2 # type: Unchecked, Unchecked [out] main:4: error: Type of variable becomes "Any" due to an unfollowed import main:6: error: A type on this line becomes "Any" due to an unfollowed import + +[case testDisallowAnyExprSimple] +# flags: --disallow-any=expr +from typing import Any +def f(s): + yield s + +x = f(0) # E: Expression has type "Any" +for x in f(0): # E: Expression has type "Any" + g(x) # E: Expression has type "Any" + +def g(x) -> Any: + yield x # E: Expression has type "Any" + +l = [1, 2, 3] +l[f(0)] # E: Expression has type "Any" +f(l) +f(f(0)) # E: Expression has type "Any" +[builtins fixtures/list.pyi] + +[case testDisallowAnyExprUnannotatedFunction] +# flags: --disallow-any=expr +def g(s): + return s + +g(0) +w: int = g(1) + +[case testDisallowAnyExprExplicitAnyParam] +# flags: --disallow-any=expr +from typing import Any, List +def f(s: Any) -> None: + pass + +def g(s: List[Any]) -> None: + pass + +f(0) + +# type of list below is inferred with expected type of List[Any], so that becomes it's type +# instead of List[str] +g(['']) # E: Expression type contains "Any" (has type List[Any]) +[builtins fixtures/list.pyi] + +[case testDisallowAnyExprAllowsAnyInCast] +# flags: --disallow-any=expr +from typing import Any, cast +class Foo: + g: Any = 2 + +z = cast(int, Foo().g) +m = cast(Any, Foo().g) # E: Expression has type "Any" +k = Foo.g # E: Expression has type "Any" +[builtins fixtures/list.pyi] + +[case testDisallowAnyExprAllowsAnyInVariableAssignmentWithExplicitTypeAnnotation] +# flags: --disallow-any=expr +from typing import Any +class Foo: + g: Any = 2 + +z: int = Foo().g +x = Foo().g # type: int +m: Any = Foo().g # E: Expression has type "Any" +n = Foo().g # type: Any # E: Expression has type "Any" +[builtins fixtures/list.pyi] + +[case testDisallowAnyExprGeneric] +# flags: --disallow-any=expr +from typing import List + +l: List = [] +l.append(1) # E: Expression type contains "Any" (has type List[Any]) +k = l[0] # E: Expression type contains "Any" (has type List[Any]) # E: Expression has type "Any" +[builtins fixtures/list.pyi]