diff --git a/mypy/checker.py b/mypy/checker.py index 870c561852b6..4aa5d81d1086 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -29,6 +29,7 @@ ARG_POS, MDEF, CONTRAVARIANT, COVARIANT) from mypy import nodes +from mypy.typeanal import has_any_from_unimported_type from mypy.types import ( Type, AnyType, CallableType, FunctionLike, Overloaded, TupleType, TypedDictType, Instance, NoneTyp, strip_type, TypeType, @@ -611,7 +612,15 @@ def is_implicit_any(t: Type) -> bool: self.fail(messages.RETURN_TYPE_EXPECTED, fdef) if any(is_implicit_any(t) for t in fdef.type.arg_types): self.fail(messages.ARGUMENT_TYPE_EXPECTED, fdef) - + if 'unimported' in self.options.disallow_any: + if fdef.type and isinstance(fdef.type, CallableType): + ret_type = fdef.type.ret_type + if has_any_from_unimported_type(ret_type): + self.msg.unimported_type_becomes_any("Return type", ret_type, fdef) + for idx, arg_type in enumerate(fdef.type.arg_types): + if has_any_from_unimported_type(arg_type): + prefix = "Argument {} to \"{}\"".format(idx + 1, fdef.name()) + self.msg.unimported_type_becomes_any(prefix, arg_type, fdef) if name in nodes.reverse_op_method_set: self.check_reverse_op_method(item, typ, name) elif name in ('__getattr__', '__getattribute__'): @@ -1179,6 +1188,16 @@ def visit_assignment_stmt(self, s: AssignmentStmt) -> None: """ self.check_assignment(s.lvalues[-1], s.rvalue, s.type is None, s.new_syntax) + if (s.type is not None and + 'unimported' in self.options.disallow_any and + has_any_from_unimported_type(s.type)): + if isinstance(s.lvalues[-1], TupleExpr): + # This is a multiple assignment. Instead of figuring out which type is problematic, + # give a generic error message. + self.msg.unimported_type_becomes_any("A type on this line", AnyType(), s) + else: + self.msg.unimported_type_becomes_any("Type of variable", s.type, s) + if len(s.lvalues) > 1: # Chained assignment (e.g. x = y = ...). # Make sure that rvalue type will not be reinferred. diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index b19a0a1bcfd2..4add26e68081 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -4,6 +4,7 @@ from typing import cast, Dict, Set, List, Tuple, Callable, Union, Optional from mypy.errors import report_internal_error +from mypy.typeanal import has_any_from_unimported_type from mypy.types import ( Type, AnyType, CallableType, Overloaded, NoneTyp, TypeVarDef, TupleType, TypedDictType, Instance, TypeVarType, ErasedType, UnionType, @@ -1543,8 +1544,11 @@ 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) target_type = expr.type - if self.chk.options.warn_redundant_casts and is_same_type(source_type, target_type): + options = self.chk.options + if options.warn_redundant_casts and is_same_type(source_type, target_type): self.msg.redundant_cast(target_type, expr) + if 'unimported' in options.disallow_any and has_any_from_unimported_type(target_type): + self.msg.unimported_type_becomes_any("Target type of cast", target_type, expr) return target_type def visit_reveal_type_expr(self, expr: RevealTypeExpr) -> Type: @@ -2229,6 +2233,11 @@ def visit_newtype_expr(self, e: NewTypeExpr) -> Type: return AnyType() def visit_namedtuple_expr(self, e: NamedTupleExpr) -> Type: + tuple_type = e.info.tuple_type + if tuple_type: + if ('unimported' in self.chk.options.disallow_any and + has_any_from_unimported_type(tuple_type)): + self.msg.unimported_type_becomes_any("NamedTuple type", tuple_type, e) # TODO: Perhaps return a type object type? return AnyType() diff --git a/mypy/main.py b/mypy/main.py index a5511671c966..5229071773ba 100644 --- a/mypy/main.py +++ b/mypy/main.py @@ -179,6 +179,7 @@ def process_options(args: List[str], strict_flag_names = [] # type: List[str] strict_flag_assignments = [] # type: List[Tuple[str, bool]] + disallow_any_options = ['unimported'] def add_invertible_flag(flag: str, *, @@ -203,6 +204,17 @@ def add_invertible_flag(flag: str, strict_flag_names.append(flag) strict_flag_assignments.append((dest, not default)) + def disallow_any_argument_type(raw_options: str) -> List[str]: + flag_options = raw_options.split(',') + for option in flag_options: + if option not in disallow_any_options: + formatted_valid_options = ', '.join( + "'{}'".format(o) for o in disallow_any_options) + message = "Invalid '--disallow-any' option '{}' (valid options are: {}).".format( + option, formatted_valid_options) + raise argparse.ArgumentError(None, message) + return flag_options + # Unless otherwise specified, arguments will be parsed directly onto an # Options object. Options that require further processing should have # their `dest` prefixed with `special-opts:`, which will cause them to be @@ -222,6 +234,10 @@ def add_invertible_flag(flag: str, help="silently ignore imports of missing modules") parser.add_argument('--follow-imports', choices=['normal', 'silent', 'skip', 'error'], default='normal', help="how to treat imports (default normal)") + parser.add_argument('--disallow-any', type=disallow_any_argument_type, default=[], + metavar='{{{}}}'.format(', '.join(disallow_any_options)), + help="disallow various types of Any in a module. Takes a comma-separated " + "list of options (defaults to all options disabled)") add_invertible_flag('--disallow-untyped-calls', default=False, strict_flag=True, help="disallow calling functions without type annotations" " from functions with type annotations") diff --git a/mypy/messages.py b/mypy/messages.py index 5c2f5d16fdc7..c63b44905d51 100644 --- a/mypy/messages.py +++ b/mypy/messages.py @@ -855,6 +855,10 @@ def unsupported_type_type(self, item: Type, context: Context) -> None: def redundant_cast(self, typ: Type, context: Context) -> None: self.note('Redundant cast to {}'.format(self.format(typ)), context) + def unimported_type_becomes_any(self, prefix: str, typ: Type, ctx: Context) -> None: + self.fail("{} becomes {} due to an unfollowed import".format(prefix, self.format(typ)), + ctx) + def typeddict_instantiated_with_unexpected_items(self, expected_item_names: List[str], actual_item_names: List[str], diff --git a/mypy/options.py b/mypy/options.py index 8c8764200800..b558470520f3 100644 --- a/mypy/options.py +++ b/mypy/options.py @@ -2,7 +2,7 @@ import pprint import sys -from typing import Any, Mapping, Optional, Tuple, List, Pattern, Dict +from typing import Mapping, Optional, Tuple, List, Pattern, Dict from mypy import defaults @@ -19,6 +19,7 @@ class Options: PER_MODULE_OPTIONS = { "ignore_missing_imports", "follow_imports", + "disallow_any", "disallow_untyped_calls", "disallow_untyped_defs", "check_untyped_defs", @@ -44,6 +45,7 @@ def __init__(self) -> None: self.report_dirs = {} # type: Dict[str, str] self.ignore_missing_imports = False self.follow_imports = 'normal' # normal|silent|skip|error + self.disallow_any = [] # type: List[str] # Disallow calling untyped functions from typed ones self.disallow_untyped_calls = False diff --git a/mypy/semanal.py b/mypy/semanal.py index b14e3dd9076e..8272efeb6152 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -75,7 +75,7 @@ from mypy.visitor import NodeVisitor from mypy.traverser import TraverserVisitor from mypy.errors import Errors, report_internal_error -from mypy.messages import CANNOT_ASSIGN_TO_TYPE +from mypy.messages import CANNOT_ASSIGN_TO_TYPE, MessageBuilder from mypy.types import ( NoneTyp, CallableType, Overloaded, Instance, Type, TypeVarType, AnyType, FunctionLike, UnboundType, TypeList, TypeVarDef, TypeType, @@ -85,7 +85,7 @@ from mypy.nodes import implicit_module_attrs from mypy.typeanal import ( TypeAnalyser, TypeAnalyserPass3, analyze_type_alias, no_subscript_builtin_alias, - TypeVariableQuery, TypeVarList, remove_dups, + TypeVariableQuery, TypeVarList, remove_dups, has_any_from_unimported_type ) from mypy.exprtotype import expr_to_unanalyzed_type, TypeTranslationError from mypy.sametypes import is_same_type @@ -236,6 +236,7 @@ def __init__(self, self.lib_path = lib_path self.errors = errors self.modules = modules + self.msg = MessageBuilder(errors, modules) self.missing_modules = missing_modules self.postpone_nested_functions_stack = [FUNCTION_BOTH_PHASES] self.postponed_functions_stack = [] @@ -974,6 +975,12 @@ def analyze_base_classes(self, defn: ClassDef) -> None: else: self.fail('Invalid base class', base_expr) info.fallback_to_any = True + if 'unimported' in self.options.disallow_any and has_any_from_unimported_type(base): + if isinstance(base_expr, (NameExpr, MemberExpr)): + prefix = "Base type {}".format(base_expr.name) + else: + prefix = "Base type" + self.msg.unimported_type_becomes_any(prefix, base, base_expr) # Add 'object' as implicit base if there is no other base class. if (not base_types and defn.fullname != 'builtins.object'): @@ -1428,7 +1435,7 @@ def add_unknown_symbol(self, name: str, context: Context, is_import: bool = Fals else: var._fullname = self.qualified_name(name) var.is_ready = True - var.type = AnyType() + var.type = AnyType(from_unimported_type=is_import) var.is_suppressed_import = is_import self.add_symbol(name, SymbolTableNode(GDEF, var, self.cur_mod_id), context) @@ -1875,6 +1882,16 @@ def process_typevar_declaration(self, s: AssignmentStmt) -> None: return variance, upper_bound = res + if 'unimported' in self.options.disallow_any: + for idx, constraint in enumerate(values, start=1): + if has_any_from_unimported_type(constraint): + prefix = "Constraint {}".format(idx) + self.msg.unimported_type_becomes_any(prefix, constraint, s) + + if has_any_from_unimported_type(upper_bound): + prefix = "Upper bound of type variable" + self.msg.unimported_type_becomes_any(prefix, upper_bound, s) + # Yes, it's a valid type variable definition! Add it to the symbol table. node = self.lookup(name, s) node.kind = TVAR diff --git a/mypy/typeanal.py b/mypy/typeanal.py index 2a506b7a5378..f9ff0ab66fe3 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -232,7 +232,7 @@ def visit_unbound_type(self, t: UnboundType) -> Type: # context. This is slightly problematic as it allows using the type 'Any' # as a base class -- however, this will fail soon at runtime so the problem # is pretty minor. - return AnyType() + return AnyType(from_unimported_type=True) # Allow unbound type variables when defining an alias if not (self.aliasing and sym.kind == TVAR and self.tvar_scope.get_binding(sym) is None): @@ -731,6 +731,23 @@ def visit_callable_type(self, t: CallableType) -> TypeVarList: return [] +def has_any_from_unimported_type(t: Type) -> bool: + """Return true if this type is Any because an import was not followed. + + If type t is such Any type or has type arguments that contain such Any type + this function will return true. + """ + return t.accept(HasAnyFromUnimportedType()) + + +class HasAnyFromUnimportedType(TypeQuery[bool]): + def __init__(self) -> None: + super().__init__(any) + + def visit_any(self, t: AnyType) -> bool: + return t.from_unimported_type + + def make_optional_type(t: Type) -> Type: """Return the type corresponding to Optional[t]. diff --git a/mypy/types.py b/mypy/types.py index c17bb063f5fb..0cf44d724bb7 100644 --- a/mypy/types.py +++ b/mypy/types.py @@ -272,9 +272,16 @@ def serialize(self) -> JsonDict: class AnyType(Type): """The type 'Any'.""" - def __init__(self, implicit: bool = False, line: int = -1, column: int = -1) -> None: + def __init__(self, + implicit: bool = False, + from_unimported_type: bool = False, + line: int = -1, + column: int = -1) -> None: super().__init__(line, column) + # Was this Any type was inferred without a type annotation? self.implicit = implicit + # Does this come from an unfollowed import? See --disallow-any=unimported option + self.from_unimported_type = from_unimported_type def accept(self, visitor: 'TypeVisitor[T]') -> T: return visitor.visit_any(self) diff --git a/test-data/unit/check-flags.test b/test-data/unit/check-flags.test index 2b0e5549589a..6f8dcdd4a94b 100644 --- a/test-data/unit/check-flags.test +++ b/test-data/unit/check-flags.test @@ -376,3 +376,136 @@ if y: # E: Condition must be a boolean if z: # E: Condition must be a boolean pass [builtins fixtures/bool.pyi] + +[case testDisallowImplicitTypesIgnoreMissingTypes] +# flags: --ignore-missing-imports --disallow-any=unimported +from missing import MyType + +def f(x: MyType) -> None: # E: Argument 1 to "f" becomes "Any" due to an unfollowed import + pass + +[case testDisallowImplicitTypes] +# flags: --disallow-any=unimported +from missing import MyType + +def f(x: MyType) -> None: + pass +[out] +main:2: error: Cannot find module named 'missing' +main:2: note: (Perhaps setting MYPYPATH or using the "--ignore-missing-imports" flag would help) +main:4: error: Argument 1 to "f" becomes "Any" due to an unfollowed import + +[case testDisallowImplicitAnyVariableDefinition] +# flags: --ignore-missing-imports --disallow-any=unimported +from missing import Unchecked + +t: Unchecked = 12 # E: Type of variable becomes "Any" due to an unfollowed import + +[case testDisallowImplicitAnyGeneric] +# flags: --ignore-missing-imports --disallow-any=unimported +from missing import Unchecked +from typing import List + +def foo(l: List[Unchecked]) -> List[Unchecked]: + t = [] # type: List[Unchecked] + return l +[builtins fixtures/list.pyi] +[out] +main:5: error: Return type becomes List[Any] due to an unfollowed import +main:5: error: Argument 1 to "foo" becomes List[Any] due to an unfollowed import +main:6: error: Type of variable becomes List[Any] due to an unfollowed import + +[case testDisallowImplicitAnyInherit] +# flags: --ignore-missing-imports --disallow-any=unimported +from missing import Unchecked +from typing import List + +class C(Unchecked): # E: Base type Unchecked becomes "Any" due to an unfollowed import + pass + +class A(List[Unchecked]): # E: Base type becomes List[Any] due to an unfollowed import + pass +[builtins fixtures/list.pyi] + +[case testDisallowImplicitAnyAlias] +# flags: --ignore-missing-imports --disallow-any=unimported +from missing import Unchecked +from typing import List + +X = List[Unchecked] + +def f(x: X) -> None: # E: Argument 1 to "f" becomes List[Any] due to an unfollowed import + pass +[builtins fixtures/list.pyi] + +[case testDisallowImplicitAnyCast] +# flags: --ignore-missing-imports --disallow-any=unimported +from missing import Unchecked +from typing import List, cast + + +foo = [1, 2, 3] +cast(List[Unchecked], foo) # E: Target type of cast becomes List[Any] due to an unfollowed import +cast(Unchecked, foo) # E: Target type of cast becomes "Any" due to an unfollowed import +[builtins fixtures/list.pyi] + +[case testDisallowImplicitAnyNamedTuple] +# flags: --ignore-missing-imports --disallow-any=unimported +from typing import List, NamedTuple +from missing import Unchecked + +Point = NamedTuple('Point', [('x', List[Unchecked]), + ('y', Unchecked)]) +[builtins fixtures/list.pyi] +[out] +main:5: error: NamedTuple type becomes "Tuple[List[Any], Any]" due to an unfollowed import + +[case testDisallowImplicitAnyTypeVarConstraints] +# flags: --ignore-missing-imports --disallow-any=unimported +from typing import List, NamedTuple, TypeVar, Any +from missing import Unchecked + +T = TypeVar('T', Unchecked, List[Unchecked], str) +[builtins fixtures/list.pyi] +[out] +main:5: error: Constraint 1 becomes "Any" due to an unfollowed import +main:5: error: Constraint 2 becomes List[Any] due to an unfollowed import + +[case testDisallowImplicitAnyNewType] +# flags: --ignore-missing-imports --disallow-any=unimported +from typing import NewType +from missing import Unchecked + +Baz = NewType('Baz', Unchecked) +[out] +main:5: error: Argument 2 to NewType(...) must be subclassable (got Any) + +[case testDisallowImplicitAnyCallableAndTuple] +# flags: --ignore-missing-imports --disallow-any=unimported +from typing import Callable, Tuple +from missing import Unchecked + +def foo(f: Callable[[], Unchecked]) -> Tuple[Unchecked]: + return f() +[builtins fixtures/list.pyi] +[out] +main:5: error: Return type becomes "Tuple[Any]" due to an unfollowed import +main:5: error: Argument 1 to "foo" becomes Callable[[], Any] due to an unfollowed import + +[case testDisallowImplicitAnySubclassingExplicitAny] +# flags: --ignore-missing-imports --disallow-any=unimported --disallow-subclassing-any +from typing import Any + +class C(Any): # E: Class cannot subclass 'Any' (has type 'Any') + pass + +[case testDisallowImplicitAnyVarDeclaration] +# flags: --ignore-missing-imports --disallow-any=unimported +from missing import Unchecked + +foo: Unchecked = "" +foo = "" +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