diff --git a/mypy/main.py b/mypy/main.py index 3b9667488728..c9b1e28f57e8 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', 'expr', 'unannotated', 'decorated', 'explicit'] +disallow_any_options = ['unimported', 'expr', 'unannotated', 'decorated', 'explicit', 'generics'] def disallow_any_argument_type(raw_options: str) -> List[str]: diff --git a/mypy/messages.py b/mypy/messages.py index 9c9fdea5ec49..6375f4d0ba7b 100644 --- a/mypy/messages.py +++ b/mypy/messages.py @@ -90,6 +90,8 @@ GENERIC_INSTANCE_VAR_CLASS_ACCESS = 'Access to generic instance variables via class is ambiguous' CANNOT_ISINSTANCE_TYPEDDICT = 'Cannot use isinstance() with a TypedDict type' CANNOT_ISINSTANCE_NEWTYPE = 'Cannot use isinstance() with a NewType type' +BARE_GENERIC = 'Missing type parameters for generic type' +IMPLICIT_GENERIC_ANY_BUILTIN = 'Implicit generic "Any". Use \'{}\' and specify generic parameters' ARG_CONSTRUCTOR_NAMES = { ARG_POS: "Arg", diff --git a/mypy/semanal.py b/mypy/semanal.py index ff0f29864fc6..3d44a2aed84e 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -77,21 +77,20 @@ from mypy.errors import Errors, report_internal_error 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, - TupleType, UnionType, StarType, EllipsisType, function_type, TypedDictType, + FunctionLike, UnboundType, TypeVarDef, TypeType, TupleType, UnionType, StarType, function_type, + TypedDictType, NoneTyp, CallableType, Overloaded, Instance, Type, TypeVarType, AnyType, TypeTranslator, ) from mypy.nodes import implicit_module_attrs from mypy.typeanal import ( TypeAnalyser, TypeAnalyserPass3, analyze_type_alias, no_subscript_builtin_alias, TypeVariableQuery, TypeVarList, remove_dups, has_any_from_unimported_type, - check_for_explicit_any + check_for_explicit_any, collect_any_types, ) from mypy.exprtotype import expr_to_unanalyzed_type, TypeTranslationError from mypy.sametypes import is_same_type from mypy.options import Options -from mypy import experiments +from mypy import experiments, messages from mypy.plugin import Plugin from mypy import join @@ -231,7 +230,7 @@ class SemanticAnalyzer(NodeVisitor): loop_depth = 0 # Depth of breakable loops cur_mod_id = '' # Current module id (or None) (phase 2) is_stub_file = False # Are we analyzing a stub file? - is_typeshed_stub_file = False # Are we analyzing a typeshed stub file? + is_typeshed_stub_file = False # Are we analyzing a typeshed stub file? imports = None # type: Set[str] # Imported modules (during phase 2 analysis) errors = None # type: Errors # Keeps track of generated errors plugin = None # type: Plugin # Mypy plugin for special casing of library features @@ -1535,6 +1534,8 @@ def type_analyzer(self, *, tvar_scope, self.fail, self.plugin, + self.options, + self.is_typeshed_stub_file, aliasing=aliasing, allow_tuple_literal=allow_tuple_literal, allow_unnormalized=self.is_stub_file) @@ -1574,6 +1575,8 @@ def visit_assignment_stmt(self, s: AssignmentStmt) -> None: self.tvar_scope, self.fail, self.plugin, + self.options, + self.is_typeshed_stub_file, allow_unnormalized=True) if res and (not isinstance(res, Instance) or res.args): # TODO: What if this gets reassigned? @@ -3199,6 +3202,8 @@ def visit_index_expr(self, expr: IndexExpr) -> None: self.tvar_scope, self.fail, self.plugin, + self.options, + self.is_typeshed_stub_file, allow_unnormalized=self.is_stub_file) expr.analyzed = TypeAliasExpr(res, fallback=self.alias_fallback(res), in_runtime=True) @@ -3886,6 +3891,7 @@ def __init__(self, modules: Dict[str, MypyFile], errors: Errors) -> None: def visit_file(self, file_node: MypyFile, fnam: str, options: Options) -> None: self.errors.set_file(fnam, file_node.fullname()) self.options = options + self.is_typeshed_file = self.errors.is_typeshed_file(fnam) with experiments.strict_optional_set(options.strict_optional): self.accept(file_node) @@ -4017,8 +4023,17 @@ def visit_type_application(self, e: TypeApplication) -> None: def analyze(self, type: Optional[Type]) -> None: if type: - analyzer = TypeAnalyserPass3(self.fail) + analyzer = TypeAnalyserPass3(self.fail, self.options, self.is_typeshed_file) type.accept(analyzer) + self.check_for_omitted_generics(type) + + def check_for_omitted_generics(self, typ: Type) -> None: + if 'generics' not in self.options.disallow_any or self.is_typeshed_file: + return + + for t in collect_any_types(typ): + if t.from_omitted_generics: + self.fail(messages.BARE_GENERIC, t) def fail(self, msg: str, ctx: Context, *, blocker: bool = False) -> None: self.errors.report(ctx.get_line(), ctx.get_column(), msg) diff --git a/mypy/typeanal.py b/mypy/typeanal.py index 9d12d912e480..a087ed54e4fc 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -17,18 +17,16 @@ ) from mypy.nodes import ( - TVAR, TYPE_ALIAS, UNBOUND_IMPORTED, - TypeInfo, Context, SymbolTableNode, Var, Expression, - IndexExpr, RefExpr, nongen_builtins, check_arg_names, check_arg_kinds, - ARG_POS, ARG_NAMED, ARG_OPT, ARG_NAMED_OPT, ARG_STAR, ARG_STAR2, TypeVarExpr + TVAR, TYPE_ALIAS, UNBOUND_IMPORTED, TypeInfo, Context, SymbolTableNode, Var, Expression, + IndexExpr, RefExpr, nongen_builtins, check_arg_names, check_arg_kinds, ARG_POS, ARG_NAMED, + ARG_OPT, ARG_NAMED_OPT, ARG_STAR, ARG_STAR2, TypeVarExpr ) from mypy.tvar_scope import TypeVarScope from mypy.sametypes import is_same_type from mypy.exprtotype import expr_to_unanalyzed_type, TypeTranslationError from mypy.subtypes import is_subtype from mypy.plugin import Plugin, AnalyzerPluginInterface, AnalyzeTypeContext -from mypy import nodes -from mypy import experiments +from mypy import nodes, messages T = TypeVar('T') @@ -58,6 +56,8 @@ def analyze_type_alias(node: Expression, tvar_scope: TypeVarScope, fail_func: Callable[[str, Context], None], plugin: Plugin, + options: Options, + is_typeshed_stub: bool, allow_unnormalized: bool = False) -> Optional[Type]: """Return type if node is valid as a type alias rvalue. @@ -100,8 +100,8 @@ def analyze_type_alias(node: Expression, except TypeTranslationError: fail_func('Invalid type alias', node) return None - analyzer = TypeAnalyser(lookup_func, lookup_fqn_func, tvar_scope, fail_func, plugin, - aliasing=True, allow_unnormalized=allow_unnormalized) + analyzer = TypeAnalyser(lookup_func, lookup_fqn_func, tvar_scope, fail_func, plugin, options, + is_typeshed_stub, aliasing=True, allow_unnormalized=allow_unnormalized) return type.accept(analyzer) @@ -124,7 +124,9 @@ def __init__(self, lookup_fqn_func: Callable[[str], SymbolTableNode], tvar_scope: TypeVarScope, fail_func: Callable[[str, Context], None], - plugin: Plugin, *, + plugin: Plugin, + options: Options, + is_typeshed_stub: bool, *, aliasing: bool = False, allow_tuple_literal: bool = False, allow_unnormalized: bool = False) -> None: @@ -138,6 +140,8 @@ def __init__(self, self.nesting_level = 0 self.allow_unnormalized = allow_unnormalized self.plugin = plugin + self.options = options + self.is_typeshed_stub = is_typeshed_stub def visit_unbound_type(self, t: UnboundType) -> Type: if t.optional: @@ -172,7 +176,11 @@ def visit_unbound_type(self, t: UnboundType) -> Type: elif fullname == 'typing.Tuple': if len(t.args) == 0 and not t.empty_tuple_index: # Bare 'Tuple' is same as 'tuple' - return self.named_type('builtins.tuple') + if 'generics' in self.options.disallow_any and not self.is_typeshed_stub: + self.fail(messages.BARE_GENERIC, t) + typ = self.named_type('builtins.tuple', line=t.line, column=t.column) + typ.from_generic_builtin = True + return typ if len(t.args) == 2 and isinstance(t.args[1], EllipsisType): # Tuple[T, ...] (uniform, variable-length tuple) instance = self.named_type('builtins.tuple', [self.anal_type(t.args[0])]) @@ -192,7 +200,8 @@ def visit_unbound_type(self, t: UnboundType) -> Type: return self.analyze_callable_type(t) elif fullname == 'typing.Type': if len(t.args) == 0: - return TypeType(AnyType(), line=t.line) + any_type = AnyType(from_omitted_generics=True, line=t.line, column=t.column) + return TypeType(any_type, line=t.line, column=t.column) if len(t.args) != 1: self.fail('Type[...] must have exactly one type argument', t) item = self.anal_type(t.args[0]) @@ -221,7 +230,8 @@ def visit_unbound_type(self, t: UnboundType) -> Type: act_len = len(an_args) if exp_len > 0 and act_len == 0: # Interpret bare Alias same as normal generic, i.e., Alias[Any, Any, ...] - return self.replace_alias_tvars(override, all_vars, [AnyType()] * exp_len, + any_type = AnyType(from_omitted_generics=True, line=t.line, column=t.column) + return self.replace_alias_tvars(override, all_vars, [any_type] * exp_len, t.line, t.column) if exp_len == 0 and act_len == 0: return override @@ -257,6 +267,7 @@ def visit_unbound_type(self, t: UnboundType) -> Type: # valid count at this point. Thus we may construct an # Instance with an invalid number of type arguments. instance = Instance(info, self.anal_array(t.args), t.line, t.column) + instance.from_generic_builtin = sym.normalized tup = info.tuple_type if tup is not None: # The class has a Tuple[...] base class so it will be @@ -397,10 +408,11 @@ def analyze_callable_type(self, t: UnboundType) -> Type: fallback = self.named_type('builtins.function') if len(t.args) == 0: # Callable (bare). Treat as Callable[..., Any]. - ret = CallableType([AnyType(), AnyType()], + any_type = AnyType(from_omitted_generics=True, line=t.line, column=t.column) + ret = CallableType([any_type, any_type], [nodes.ARG_STAR, nodes.ARG_STAR2], [None, None], - ret_type=AnyType(), + ret_type=any_type, fallback=fallback, is_ellipsis_args=True) elif len(t.args) == 2: @@ -555,10 +567,14 @@ def anal_var_defs(self, var_defs: List[TypeVarDef]) -> List[TypeVarDef]: vd.line)) return a - def named_type(self, fully_qualified_name: str, args: List[Type] = None) -> Instance: + def named_type(self, fully_qualified_name: str, + args: List[Type] = None, + line: int = -1, + column: int = -1) -> Instance: node = self.lookup_fqn_func(fully_qualified_name) assert isinstance(node.node, TypeInfo) - return Instance(node.node, args or []) + return Instance(node.node, args or [AnyType()] * len(node.node.defn.type_vars), + line=line, column=column) def tuple_type(self, items: List[Type]) -> TupleType: return TupleType(items, fallback=self.named_type('builtins.tuple', [AnyType()])) @@ -584,16 +600,29 @@ class TypeAnalyserPass3(TypeVisitor[None]): to types. """ - def __init__(self, fail_func: Callable[[str, Context], None]) -> None: + def __init__(self, + fail_func: Callable[[str, Context], None], + options: Options, + is_typeshed_stub: bool) -> None: self.fail = fail_func + self.options = options + self.is_typeshed_stub = is_typeshed_stub def visit_instance(self, t: Instance) -> None: info = t.type # Check type argument count. if len(t.args) != len(info.type_vars): if len(t.args) == 0: + from_builtins = t.type.fullname() in nongen_builtins and not t.from_generic_builtin + if ('generics' in self.options.disallow_any and + not self.is_typeshed_stub and + from_builtins): + alternative = nongen_builtins[t.type.fullname()] + self.fail(messages.IMPLICIT_GENERIC_ANY_BUILTIN.format(alternative), t) # Insert implicit 'Any' type arguments. - t.args = [AnyType()] * len(info.type_vars) + any_type = AnyType(from_omitted_generics=not from_builtins, line=t.line, + column=t.line) + t.args = [any_type] * len(info.type_vars) return # Invalid number of type parameters. n = len(info.type_vars) @@ -809,6 +838,26 @@ def visit_typeddict_type(self, t: TypedDictType) -> bool: return False +def collect_any_types(t: Type) -> List[AnyType]: + """Return all inner `AnyType`s of type t""" + return t.accept(CollectAnyTypesQuery()) + + +class CollectAnyTypesQuery(TypeQuery[List[AnyType]]): + def __init__(self) -> None: + super().__init__(self.combine_lists_strategy) + + def visit_any(self, t: AnyType) -> List[AnyType]: + return [t] + + @classmethod + def combine_lists_strategy(cls, it: Iterable[List[AnyType]]) -> List[AnyType]: + result = [] # type: List[AnyType] + for l in it: + result.extend(l) + return result + + 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 7114403308d1..74bf177a1d13 100644 --- a/mypy/types.py +++ b/mypy/types.py @@ -1,22 +1,20 @@ """Classes for representing mypy types.""" -from abc import abstractmethod import copy +from abc import abstractmethod from collections import OrderedDict from typing import ( - Any, TypeVar, Dict, List, Tuple, cast, Generic, Set, Sequence, Optional, Union, Iterable, - NamedTuple, Callable, + Any, TypeVar, Dict, List, Tuple, cast, Generic, Set, Optional, Union, Iterable, NamedTuple, + Callable, ) import mypy.nodes +from mypy import experiments from mypy.nodes import ( - INVARIANT, SymbolNode, - ARG_POS, ARG_OPT, ARG_STAR, ARG_STAR2, ARG_NAMED, ARG_NAMED_OPT, + INVARIANT, SymbolNode, ARG_POS, ARG_OPT, ARG_STAR, ARG_STAR2, ARG_NAMED, ARG_NAMED_OPT, ) from mypy.sharedparse import argument_elide_name from mypy.util import IdMapper -from mypy import experiments - T = TypeVar('T') @@ -259,6 +257,7 @@ def __init__(self, implicit: bool = False, from_unimported_type: bool = False, explicit: bool = False, + from_omitted_generics: bool = False, line: int = -1, column: int = -1) -> None: super().__init__(line, column) @@ -271,6 +270,8 @@ def __init__(self, self.from_unimported_type = from_unimported_type # Does this Any come from an explicit type annotation? self.explicit = explicit + # Does this type come from omitted generics? + self.from_omitted_generics = from_omitted_generics def accept(self, visitor: 'TypeVisitor[T]') -> T: return visitor.visit_any(self) @@ -279,6 +280,7 @@ def copy_modified(self, implicit: bool = _dummy, from_unimported_type: bool = _dummy, explicit: bool = _dummy, + from_omitted_generics: bool = _dummy, ) -> 'AnyType': if implicit is _dummy: implicit = self.implicit @@ -286,8 +288,11 @@ def copy_modified(self, from_unimported_type = self.from_unimported_type if explicit is _dummy: explicit = self.explicit + if from_omitted_generics is _dummy: + from_omitted_generics = self.from_omitted_generics return AnyType(implicit=implicit, from_unimported_type=from_unimported_type, - explicit=explicit, line=self.line, column=self.column) + explicit=explicit, from_omitted_generics=from_omitted_generics, + line=self.line, column=self.column) def serialize(self) -> JsonDict: return {'.class': 'AnyType'} @@ -408,6 +413,7 @@ class Instance(Type): args = None # type: List[Type] erased = False # True if result of type variable substitution invalid = False # True if recovered after incorrect number of type arguments error + from_generic_builtin = False # True if created from a generic builtin (e.g. list() or set()) def __init__(self, typ: mypy.nodes.TypeInfo, args: List[Type], line: int = -1, column: int = -1, erased: bool = False) -> None: diff --git a/test-data/unit/cmdline.test b/test-data/unit/cmdline.test index 9fdf059c356b..667a75c900ad 100644 --- a/test-data/unit/cmdline.test +++ b/test-data/unit/cmdline.test @@ -909,3 +909,191 @@ M = TypedDict('M', {'x': str, 'y': List[Any]}) # error N = TypedDict('N', {'x': str, 'y': List}) # no error [out] m.py:4: error: Explicit "Any" is not allowed + +[case testDisallowAnyGenericsTupleNoTypeParams] +# cmd: mypy m.py +[file mypy.ini] +[[mypy] +[[mypy-m] +disallow_any = generics + +[file m.py] +from typing import Tuple + +def f(s: Tuple) -> None: pass # error +def g(s) -> Tuple: # error + return 'a', 'b' +def h(s) -> Tuple[str, str]: # no error + return 'a', 'b' +x: Tuple = () # error +[out] +m.py:3: error: Missing type parameters for generic type +m.py:4: error: Missing type parameters for generic type +m.py:8: error: Missing type parameters for generic type + +[case testDisallowAnyGenericsTupleWithNoTypeParamsGeneric] +# cmd: mypy m.py +[file mypy.ini] +[[mypy] +[[mypy-m] +disallow_any = generics + +[file m.py] +from typing import Tuple, List + +def f(s: List[Tuple]) -> None: pass # error +def g(s: List[Tuple[str, str]]) -> None: pass # no error +[out] +m.py:3: error: Missing type parameters for generic type + +[case testDisallowAnyGenericsTypeType] +# cmd: mypy m.py +[file mypy.ini] +[[mypy] +[[mypy-m] +disallow_any = generics + +[file m.py] +from typing import Type, Any + +def f(s: Type[Any]) -> None: pass # no error +def g(s) -> Type: # error + return s +def h(s) -> Type[str]: # no error + return s +x: Type = g(0) # error +[out] +m.py:4: error: Missing type parameters for generic type +m.py:8: error: Missing type parameters for generic type + +[case testDisallowAnyGenericsAliasGenericType] +# cmd: mypy m.py +[file mypy.ini] +[[mypy] +[[mypy-m] +disallow_any = generics + +[file m.py] +from typing import List + +L = List # no error + +def f(l: L) -> None: pass # error +def g(l: L[str]) -> None: pass # no error +[out] +m.py:5: error: Missing type parameters for generic type + +[case testDisallowAnyGenericsGenericAlias] +# cmd: mypy m.py +[file mypy.ini] +[[mypy] +[[mypy-m] +disallow_any = generics + +[file m.py] +from typing import List, TypeVar, Tuple + +T = TypeVar('T') +A = Tuple[T, str, T] + +def f(s: A) -> None: pass # error +def g(s) -> A: # error + return 'a', 'b', 1 +def h(s) -> A[str]: # no error + return 'a', 'b', 'c' +x: A = ('a', 'b', 1) # error +[out] +m.py:6: error: Missing type parameters for generic type +m.py:7: error: Missing type parameters for generic type +m.py:11: error: Missing type parameters for generic type + +[case testDisallowAnyGenericsPlainList] +# cmd: mypy m.py +[file mypy.ini] +[[mypy] +[[mypy-m] +disallow_any = generics + +[file m.py] +from typing import List + +def f(l: List) -> None: pass # error +def g(l: List[str]) -> None: pass # no error +def h(l: List[List]) -> None: pass # error +def i(l: List[List[List[List]]]) -> None: pass # error + +x = [] # error: need type annotation +y: List = [] # error +[out] +m.py:3: error: Missing type parameters for generic type +m.py:5: error: Missing type parameters for generic type +m.py:6: error: Missing type parameters for generic type +m.py:8: error: Need type annotation for variable +m.py:9: error: Missing type parameters for generic type + +[case testDisallowAnyGenericsCustomGenericClass] +# cmd: mypy m.py +[file mypy.ini] +[[mypy] +[[mypy-m] +disallow_any = generics + +[file m.py] +from typing import Generic, TypeVar, Any + +T = TypeVar('T') +class G(Generic[T]): pass + +def f() -> G: # error + return G() + +x: G[Any] = G() # no error +y: G = x # error + +[out] +m.py:6: error: Missing type parameters for generic type +m.py:10: error: Missing type parameters for generic type + +[case testDisallowAnyGenericsBuiltinCollections] +# cmd: mypy m.py +[file mypy.ini] +[[mypy] +[[mypy-m] +disallow_any = generics + +[file m.py] +s = tuple([1, 2, 3]) # no error + +def f(t: tuple) -> None: pass +def g() -> list: pass +def h(s: dict) -> None: pass +def i(s: set) -> None: pass +def j(s: frozenset) -> None: pass +[out] +m.py:3: error: Implicit generic "Any". Use 'typing.Tuple' and specify generic parameters +m.py:4: error: Implicit generic "Any". Use 'typing.List' and specify generic parameters +m.py:5: error: Implicit generic "Any". Use 'typing.Dict' and specify generic parameters +m.py:6: error: Implicit generic "Any". Use 'typing.Set' and specify generic parameters +m.py:7: error: Implicit generic "Any". Use 'typing.FrozenSet' and specify generic parameters + +[case testDisallowAnyGenericsTypingCollections] +# cmd: mypy m.py +[file mypy.ini] +[[mypy] +[[mypy-m] +disallow_any = generics + +[file m.py] +from typing import Tuple, List, Dict, Set, FrozenSet + +def f(t: Tuple) -> None: pass +def g() -> List: pass +def h(s: Dict) -> None: pass +def i(s: Set) -> None: pass +def j(s: FrozenSet) -> None: pass +[out] +m.py:3: error: Missing type parameters for generic type +m.py:4: error: Missing type parameters for generic type +m.py:5: error: Missing type parameters for generic type +m.py:6: error: Missing type parameters for generic type +m.py:7: error: Missing type parameters for generic type