Skip to content

Commit 324f68f

Browse files
authored
Option to disallow omitting type parameters of generic types (#3637)
(--disallow-any=generics) This code is based on #3141 by pkch. This option disallows implicit Anys from omitted type parameters to generic types. For instance, `def x() -> List` would produce an error while `def x() -> List[Any]` is allowed. Note that with the flag enabled builtin generic types such as `list` and `set` are also forbidden.
1 parent 138f478 commit 324f68f

File tree

6 files changed

+294
-34
lines changed

6 files changed

+294
-34
lines changed

mypy/main.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ def type_check_only(sources: List[BuildSource], bin_dir: str, options: Options)
9797
options=options)
9898

9999

100-
disallow_any_options = ['unimported', 'expr', 'unannotated', 'decorated', 'explicit']
100+
disallow_any_options = ['unimported', 'expr', 'unannotated', 'decorated', 'explicit', 'generics']
101101

102102

103103
def disallow_any_argument_type(raw_options: str) -> List[str]:

mypy/messages.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,8 @@
9090
GENERIC_INSTANCE_VAR_CLASS_ACCESS = 'Access to generic instance variables via class is ambiguous'
9191
CANNOT_ISINSTANCE_TYPEDDICT = 'Cannot use isinstance() with a TypedDict type'
9292
CANNOT_ISINSTANCE_NEWTYPE = 'Cannot use isinstance() with a NewType type'
93+
BARE_GENERIC = 'Missing type parameters for generic type'
94+
IMPLICIT_GENERIC_ANY_BUILTIN = 'Implicit generic "Any". Use \'{}\' and specify generic parameters'
9395

9496
ARG_CONSTRUCTOR_NAMES = {
9597
ARG_POS: "Arg",

mypy/semanal.py

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -77,21 +77,20 @@
7777
from mypy.errors import Errors, report_internal_error
7878
from mypy.messages import CANNOT_ASSIGN_TO_TYPE, MessageBuilder
7979
from mypy.types import (
80-
NoneTyp, CallableType, Overloaded, Instance, Type, TypeVarType, AnyType,
81-
FunctionLike, UnboundType, TypeList, TypeVarDef, TypeType,
82-
TupleType, UnionType, StarType, EllipsisType, function_type, TypedDictType,
80+
FunctionLike, UnboundType, TypeVarDef, TypeType, TupleType, UnionType, StarType, function_type,
81+
TypedDictType, NoneTyp, CallableType, Overloaded, Instance, Type, TypeVarType, AnyType,
8382
TypeTranslator,
8483
)
8584
from mypy.nodes import implicit_module_attrs
8685
from mypy.typeanal import (
8786
TypeAnalyser, TypeAnalyserPass3, analyze_type_alias, no_subscript_builtin_alias,
8887
TypeVariableQuery, TypeVarList, remove_dups, has_any_from_unimported_type,
89-
check_for_explicit_any
88+
check_for_explicit_any, collect_any_types,
9089
)
9190
from mypy.exprtotype import expr_to_unanalyzed_type, TypeTranslationError
9291
from mypy.sametypes import is_same_type
9392
from mypy.options import Options
94-
from mypy import experiments
93+
from mypy import experiments, messages
9594
from mypy.plugin import Plugin
9695
from mypy import join
9796

@@ -231,7 +230,7 @@ class SemanticAnalyzer(NodeVisitor):
231230
loop_depth = 0 # Depth of breakable loops
232231
cur_mod_id = '' # Current module id (or None) (phase 2)
233232
is_stub_file = False # Are we analyzing a stub file?
234-
is_typeshed_stub_file = False # Are we analyzing a typeshed stub file?
233+
is_typeshed_stub_file = False # Are we analyzing a typeshed stub file?
235234
imports = None # type: Set[str] # Imported modules (during phase 2 analysis)
236235
errors = None # type: Errors # Keeps track of generated errors
237236
plugin = None # type: Plugin # Mypy plugin for special casing of library features
@@ -1535,6 +1534,8 @@ def type_analyzer(self, *,
15351534
tvar_scope,
15361535
self.fail,
15371536
self.plugin,
1537+
self.options,
1538+
self.is_typeshed_stub_file,
15381539
aliasing=aliasing,
15391540
allow_tuple_literal=allow_tuple_literal,
15401541
allow_unnormalized=self.is_stub_file)
@@ -1574,6 +1575,8 @@ def visit_assignment_stmt(self, s: AssignmentStmt) -> None:
15741575
self.tvar_scope,
15751576
self.fail,
15761577
self.plugin,
1578+
self.options,
1579+
self.is_typeshed_stub_file,
15771580
allow_unnormalized=True)
15781581
if res and (not isinstance(res, Instance) or res.args):
15791582
# TODO: What if this gets reassigned?
@@ -3199,6 +3202,8 @@ def visit_index_expr(self, expr: IndexExpr) -> None:
31993202
self.tvar_scope,
32003203
self.fail,
32013204
self.plugin,
3205+
self.options,
3206+
self.is_typeshed_stub_file,
32023207
allow_unnormalized=self.is_stub_file)
32033208
expr.analyzed = TypeAliasExpr(res, fallback=self.alias_fallback(res),
32043209
in_runtime=True)
@@ -3886,6 +3891,7 @@ def __init__(self, modules: Dict[str, MypyFile], errors: Errors) -> None:
38863891
def visit_file(self, file_node: MypyFile, fnam: str, options: Options) -> None:
38873892
self.errors.set_file(fnam, file_node.fullname())
38883893
self.options = options
3894+
self.is_typeshed_file = self.errors.is_typeshed_file(fnam)
38893895
with experiments.strict_optional_set(options.strict_optional):
38903896
self.accept(file_node)
38913897

@@ -4017,8 +4023,17 @@ def visit_type_application(self, e: TypeApplication) -> None:
40174023

40184024
def analyze(self, type: Optional[Type]) -> None:
40194025
if type:
4020-
analyzer = TypeAnalyserPass3(self.fail)
4026+
analyzer = TypeAnalyserPass3(self.fail, self.options, self.is_typeshed_file)
40214027
type.accept(analyzer)
4028+
self.check_for_omitted_generics(type)
4029+
4030+
def check_for_omitted_generics(self, typ: Type) -> None:
4031+
if 'generics' not in self.options.disallow_any or self.is_typeshed_file:
4032+
return
4033+
4034+
for t in collect_any_types(typ):
4035+
if t.from_omitted_generics:
4036+
self.fail(messages.BARE_GENERIC, t)
40224037

40234038
def fail(self, msg: str, ctx: Context, *, blocker: bool = False) -> None:
40244039
self.errors.report(ctx.get_line(), ctx.get_column(), msg)

mypy/typeanal.py

Lines changed: 67 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -17,18 +17,16 @@
1717
)
1818

1919
from mypy.nodes import (
20-
TVAR, TYPE_ALIAS, UNBOUND_IMPORTED,
21-
TypeInfo, Context, SymbolTableNode, Var, Expression,
22-
IndexExpr, RefExpr, nongen_builtins, check_arg_names, check_arg_kinds,
23-
ARG_POS, ARG_NAMED, ARG_OPT, ARG_NAMED_OPT, ARG_STAR, ARG_STAR2, TypeVarExpr
20+
TVAR, TYPE_ALIAS, UNBOUND_IMPORTED, TypeInfo, Context, SymbolTableNode, Var, Expression,
21+
IndexExpr, RefExpr, nongen_builtins, check_arg_names, check_arg_kinds, ARG_POS, ARG_NAMED,
22+
ARG_OPT, ARG_NAMED_OPT, ARG_STAR, ARG_STAR2, TypeVarExpr
2423
)
2524
from mypy.tvar_scope import TypeVarScope
2625
from mypy.sametypes import is_same_type
2726
from mypy.exprtotype import expr_to_unanalyzed_type, TypeTranslationError
2827
from mypy.subtypes import is_subtype
2928
from mypy.plugin import Plugin, AnalyzerPluginInterface, AnalyzeTypeContext
30-
from mypy import nodes
31-
from mypy import experiments
29+
from mypy import nodes, messages
3230

3331

3432
T = TypeVar('T')
@@ -58,6 +56,8 @@ def analyze_type_alias(node: Expression,
5856
tvar_scope: TypeVarScope,
5957
fail_func: Callable[[str, Context], None],
6058
plugin: Plugin,
59+
options: Options,
60+
is_typeshed_stub: bool,
6161
allow_unnormalized: bool = False) -> Optional[Type]:
6262
"""Return type if node is valid as a type alias rvalue.
6363
@@ -100,8 +100,8 @@ def analyze_type_alias(node: Expression,
100100
except TypeTranslationError:
101101
fail_func('Invalid type alias', node)
102102
return None
103-
analyzer = TypeAnalyser(lookup_func, lookup_fqn_func, tvar_scope, fail_func, plugin,
104-
aliasing=True, allow_unnormalized=allow_unnormalized)
103+
analyzer = TypeAnalyser(lookup_func, lookup_fqn_func, tvar_scope, fail_func, plugin, options,
104+
is_typeshed_stub, aliasing=True, allow_unnormalized=allow_unnormalized)
105105
return type.accept(analyzer)
106106

107107

@@ -124,7 +124,9 @@ def __init__(self,
124124
lookup_fqn_func: Callable[[str], SymbolTableNode],
125125
tvar_scope: TypeVarScope,
126126
fail_func: Callable[[str, Context], None],
127-
plugin: Plugin, *,
127+
plugin: Plugin,
128+
options: Options,
129+
is_typeshed_stub: bool, *,
128130
aliasing: bool = False,
129131
allow_tuple_literal: bool = False,
130132
allow_unnormalized: bool = False) -> None:
@@ -138,6 +140,8 @@ def __init__(self,
138140
self.nesting_level = 0
139141
self.allow_unnormalized = allow_unnormalized
140142
self.plugin = plugin
143+
self.options = options
144+
self.is_typeshed_stub = is_typeshed_stub
141145

142146
def visit_unbound_type(self, t: UnboundType) -> Type:
143147
if t.optional:
@@ -172,7 +176,11 @@ def visit_unbound_type(self, t: UnboundType) -> Type:
172176
elif fullname == 'typing.Tuple':
173177
if len(t.args) == 0 and not t.empty_tuple_index:
174178
# Bare 'Tuple' is same as 'tuple'
175-
return self.named_type('builtins.tuple')
179+
if 'generics' in self.options.disallow_any and not self.is_typeshed_stub:
180+
self.fail(messages.BARE_GENERIC, t)
181+
typ = self.named_type('builtins.tuple', line=t.line, column=t.column)
182+
typ.from_generic_builtin = True
183+
return typ
176184
if len(t.args) == 2 and isinstance(t.args[1], EllipsisType):
177185
# Tuple[T, ...] (uniform, variable-length tuple)
178186
instance = self.named_type('builtins.tuple', [self.anal_type(t.args[0])])
@@ -192,7 +200,8 @@ def visit_unbound_type(self, t: UnboundType) -> Type:
192200
return self.analyze_callable_type(t)
193201
elif fullname == 'typing.Type':
194202
if len(t.args) == 0:
195-
return TypeType(AnyType(), line=t.line)
203+
any_type = AnyType(from_omitted_generics=True, line=t.line, column=t.column)
204+
return TypeType(any_type, line=t.line, column=t.column)
196205
if len(t.args) != 1:
197206
self.fail('Type[...] must have exactly one type argument', t)
198207
item = self.anal_type(t.args[0])
@@ -221,7 +230,8 @@ def visit_unbound_type(self, t: UnboundType) -> Type:
221230
act_len = len(an_args)
222231
if exp_len > 0 and act_len == 0:
223232
# Interpret bare Alias same as normal generic, i.e., Alias[Any, Any, ...]
224-
return self.replace_alias_tvars(override, all_vars, [AnyType()] * exp_len,
233+
any_type = AnyType(from_omitted_generics=True, line=t.line, column=t.column)
234+
return self.replace_alias_tvars(override, all_vars, [any_type] * exp_len,
225235
t.line, t.column)
226236
if exp_len == 0 and act_len == 0:
227237
return override
@@ -257,6 +267,7 @@ def visit_unbound_type(self, t: UnboundType) -> Type:
257267
# valid count at this point. Thus we may construct an
258268
# Instance with an invalid number of type arguments.
259269
instance = Instance(info, self.anal_array(t.args), t.line, t.column)
270+
instance.from_generic_builtin = sym.normalized
260271
tup = info.tuple_type
261272
if tup is not None:
262273
# The class has a Tuple[...] base class so it will be
@@ -397,10 +408,11 @@ def analyze_callable_type(self, t: UnboundType) -> Type:
397408
fallback = self.named_type('builtins.function')
398409
if len(t.args) == 0:
399410
# Callable (bare). Treat as Callable[..., Any].
400-
ret = CallableType([AnyType(), AnyType()],
411+
any_type = AnyType(from_omitted_generics=True, line=t.line, column=t.column)
412+
ret = CallableType([any_type, any_type],
401413
[nodes.ARG_STAR, nodes.ARG_STAR2],
402414
[None, None],
403-
ret_type=AnyType(),
415+
ret_type=any_type,
404416
fallback=fallback,
405417
is_ellipsis_args=True)
406418
elif len(t.args) == 2:
@@ -555,10 +567,14 @@ def anal_var_defs(self, var_defs: List[TypeVarDef]) -> List[TypeVarDef]:
555567
vd.line))
556568
return a
557569

558-
def named_type(self, fully_qualified_name: str, args: List[Type] = None) -> Instance:
570+
def named_type(self, fully_qualified_name: str,
571+
args: List[Type] = None,
572+
line: int = -1,
573+
column: int = -1) -> Instance:
559574
node = self.lookup_fqn_func(fully_qualified_name)
560575
assert isinstance(node.node, TypeInfo)
561-
return Instance(node.node, args or [])
576+
return Instance(node.node, args or [AnyType()] * len(node.node.defn.type_vars),
577+
line=line, column=column)
562578

563579
def tuple_type(self, items: List[Type]) -> TupleType:
564580
return TupleType(items, fallback=self.named_type('builtins.tuple', [AnyType()]))
@@ -584,16 +600,29 @@ class TypeAnalyserPass3(TypeVisitor[None]):
584600
to types.
585601
"""
586602

587-
def __init__(self, fail_func: Callable[[str, Context], None]) -> None:
603+
def __init__(self,
604+
fail_func: Callable[[str, Context], None],
605+
options: Options,
606+
is_typeshed_stub: bool) -> None:
588607
self.fail = fail_func
608+
self.options = options
609+
self.is_typeshed_stub = is_typeshed_stub
589610

590611
def visit_instance(self, t: Instance) -> None:
591612
info = t.type
592613
# Check type argument count.
593614
if len(t.args) != len(info.type_vars):
594615
if len(t.args) == 0:
616+
from_builtins = t.type.fullname() in nongen_builtins and not t.from_generic_builtin
617+
if ('generics' in self.options.disallow_any and
618+
not self.is_typeshed_stub and
619+
from_builtins):
620+
alternative = nongen_builtins[t.type.fullname()]
621+
self.fail(messages.IMPLICIT_GENERIC_ANY_BUILTIN.format(alternative), t)
595622
# Insert implicit 'Any' type arguments.
596-
t.args = [AnyType()] * len(info.type_vars)
623+
any_type = AnyType(from_omitted_generics=not from_builtins, line=t.line,
624+
column=t.line)
625+
t.args = [any_type] * len(info.type_vars)
597626
return
598627
# Invalid number of type parameters.
599628
n = len(info.type_vars)
@@ -809,6 +838,26 @@ def visit_typeddict_type(self, t: TypedDictType) -> bool:
809838
return False
810839

811840

841+
def collect_any_types(t: Type) -> List[AnyType]:
842+
"""Return all inner `AnyType`s of type t"""
843+
return t.accept(CollectAnyTypesQuery())
844+
845+
846+
class CollectAnyTypesQuery(TypeQuery[List[AnyType]]):
847+
def __init__(self) -> None:
848+
super().__init__(self.combine_lists_strategy)
849+
850+
def visit_any(self, t: AnyType) -> List[AnyType]:
851+
return [t]
852+
853+
@classmethod
854+
def combine_lists_strategy(cls, it: Iterable[List[AnyType]]) -> List[AnyType]:
855+
result = [] # type: List[AnyType]
856+
for l in it:
857+
result.extend(l)
858+
return result
859+
860+
812861
def make_optional_type(t: Type) -> Type:
813862
"""Return the type corresponding to Optional[t].
814863

mypy/types.py

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,20 @@
11
"""Classes for representing mypy types."""
22

3-
from abc import abstractmethod
43
import copy
4+
from abc import abstractmethod
55
from collections import OrderedDict
66
from typing import (
7-
Any, TypeVar, Dict, List, Tuple, cast, Generic, Set, Sequence, Optional, Union, Iterable,
8-
NamedTuple, Callable,
7+
Any, TypeVar, Dict, List, Tuple, cast, Generic, Set, Optional, Union, Iterable, NamedTuple,
8+
Callable,
99
)
1010

1111
import mypy.nodes
12+
from mypy import experiments
1213
from mypy.nodes import (
13-
INVARIANT, SymbolNode,
14-
ARG_POS, ARG_OPT, ARG_STAR, ARG_STAR2, ARG_NAMED, ARG_NAMED_OPT,
14+
INVARIANT, SymbolNode, ARG_POS, ARG_OPT, ARG_STAR, ARG_STAR2, ARG_NAMED, ARG_NAMED_OPT,
1515
)
1616
from mypy.sharedparse import argument_elide_name
1717
from mypy.util import IdMapper
18-
from mypy import experiments
19-
2018

2119
T = TypeVar('T')
2220

@@ -259,6 +257,7 @@ def __init__(self,
259257
implicit: bool = False,
260258
from_unimported_type: bool = False,
261259
explicit: bool = False,
260+
from_omitted_generics: bool = False,
262261
line: int = -1,
263262
column: int = -1) -> None:
264263
super().__init__(line, column)
@@ -271,6 +270,8 @@ def __init__(self,
271270
self.from_unimported_type = from_unimported_type
272271
# Does this Any come from an explicit type annotation?
273272
self.explicit = explicit
273+
# Does this type come from omitted generics?
274+
self.from_omitted_generics = from_omitted_generics
274275

275276
def accept(self, visitor: 'TypeVisitor[T]') -> T:
276277
return visitor.visit_any(self)
@@ -279,15 +280,19 @@ def copy_modified(self,
279280
implicit: bool = _dummy,
280281
from_unimported_type: bool = _dummy,
281282
explicit: bool = _dummy,
283+
from_omitted_generics: bool = _dummy,
282284
) -> 'AnyType':
283285
if implicit is _dummy:
284286
implicit = self.implicit
285287
if from_unimported_type is _dummy:
286288
from_unimported_type = self.from_unimported_type
287289
if explicit is _dummy:
288290
explicit = self.explicit
291+
if from_omitted_generics is _dummy:
292+
from_omitted_generics = self.from_omitted_generics
289293
return AnyType(implicit=implicit, from_unimported_type=from_unimported_type,
290-
explicit=explicit, line=self.line, column=self.column)
294+
explicit=explicit, from_omitted_generics=from_omitted_generics,
295+
line=self.line, column=self.column)
291296

292297
def serialize(self) -> JsonDict:
293298
return {'.class': 'AnyType'}
@@ -408,6 +413,7 @@ class Instance(Type):
408413
args = None # type: List[Type]
409414
erased = False # True if result of type variable substitution
410415
invalid = False # True if recovered after incorrect number of type arguments error
416+
from_generic_builtin = False # True if created from a generic builtin (e.g. list() or set())
411417

412418
def __init__(self, typ: mypy.nodes.TypeInfo, args: List[Type],
413419
line: int = -1, column: int = -1, erased: bool = False) -> None:

0 commit comments

Comments
 (0)