Skip to content

Commit b7636cb

Browse files
authored
Option to disallow explicit Any types (--disallow-any=explicit) (#3579)
Type aliases are a little tricky to deal with because they get "inlined". The solution was to unmark Anys as explicit right after we check a type alias for explicit Anys.
1 parent e74ce8d commit b7636cb

File tree

10 files changed

+338
-13
lines changed

10 files changed

+338
-13
lines changed

docs/source/command_line.rst

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -278,7 +278,7 @@ Here are some more useful flags:
278278

279279
- ``--disallow-any`` disallows various types of ``Any`` in a module.
280280
The option takes a comma-separated list of the following values:
281-
``unimported``, ``unannotated``, ``expr``, ``decorated``.
281+
``unimported``, ``unannotated``, ``expr``, ``decorated``, ``explicit``.
282282

283283
``unimported`` disallows usage of types that come from unfollowed imports
284284
(such types become aliases for ``Any``). Unfollowed imports occur either
@@ -301,6 +301,8 @@ Here are some more useful flags:
301301
``decorated`` disallows functions that have ``Any`` in their signature
302302
after decorator transformation.
303303

304+
``explicit`` disallows explicit ``Any`` in type positions such as type
305+
annotations and generic type parameters.
304306

305307
- ``--disallow-untyped-defs`` reports an error whenever it encounters
306308
a function definition without type annotations.

docs/source/config_file.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -150,8 +150,8 @@ overridden by the pattern sections matching the module name.
150150
- ``disallow_any`` (Comma-separated list, default empty) is an option to
151151
disallow various types of ``Any`` in a module. The flag takes a
152152
comma-separated list of the following arguments: ``unimported``,
153-
``unannotated``, ``expr``. For explanations see the discussion for the
154-
:ref:`--disallow-any <disallow-any>` option.
153+
``unannotated``, ``expr``, ``decorated``, ``explicit``. For explanations
154+
see the discussion for the :ref:`--disallow-any <disallow-any>` option.
155155

156156
- ``disallow_untyped_calls`` (Boolean, default False) disallows
157157
calling functions without type annotations from functions with type

mypy/checker.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
ARG_POS, MDEF,
3030
CONTRAVARIANT, COVARIANT)
3131
from mypy import nodes
32-
from mypy.typeanal import has_any_from_unimported_type
32+
from mypy.typeanal import has_any_from_unimported_type, check_for_explicit_any
3333
from mypy.types import (
3434
Type, AnyType, CallableType, FunctionLike, Overloaded, TupleType, TypedDictType,
3535
Instance, NoneTyp, strip_type, TypeType,
@@ -629,6 +629,8 @@ def is_implicit_any(t: Type) -> bool:
629629
if has_any_from_unimported_type(arg_type):
630630
prefix = "Argument {} to \"{}\"".format(idx + 1, fdef.name())
631631
self.msg.unimported_type_becomes_any(prefix, arg_type, fdef)
632+
check_for_explicit_any(fdef.type, self.options, self.is_typeshed_stub,
633+
self.msg, context=fdef)
632634
if name in nodes.reverse_op_method_set:
633635
self.check_reverse_op_method(item, typ, name)
634636
elif name in ('__getattr__', '__getattribute__'):
@@ -1215,6 +1217,7 @@ def visit_assignment_stmt(self, s: AssignmentStmt) -> None:
12151217
self.msg.unimported_type_becomes_any("A type on this line", AnyType(), s)
12161218
else:
12171219
self.msg.unimported_type_becomes_any("Type of variable", s.type, s)
1220+
check_for_explicit_any(s.type, self.options, self.is_typeshed_stub, self.msg, context=s)
12181221

12191222
if len(s.lvalues) > 1:
12201223
# Chained assignment (e.g. x = y = ...).

mypy/checkexpr.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from typing import cast, Dict, Set, List, Tuple, Callable, Union, Optional
55

66
from mypy.errors import report_internal_error
7-
from mypy.typeanal import has_any_from_unimported_type
7+
from mypy.typeanal import has_any_from_unimported_type, check_for_explicit_any
88
from mypy.types import (
99
Type, AnyType, CallableType, Overloaded, NoneTyp, TypeVarDef,
1010
TupleType, TypedDictType, Instance, TypeVarType, ErasedType, UnionType,
@@ -1692,6 +1692,8 @@ def visit_cast_expr(self, expr: CastExpr) -> Type:
16921692
self.msg.redundant_cast(target_type, expr)
16931693
if 'unimported' in options.disallow_any and has_any_from_unimported_type(target_type):
16941694
self.msg.unimported_type_becomes_any("Target type of cast", target_type, expr)
1695+
check_for_explicit_any(target_type, self.chk.options, self.chk.is_typeshed_stub, self.msg,
1696+
context=expr)
16951697
return target_type
16961698

16971699
def visit_reveal_type_expr(self, expr: RevealTypeExpr) -> Type:
@@ -2412,6 +2414,8 @@ def visit_namedtuple_expr(self, e: NamedTupleExpr) -> Type:
24122414
if ('unimported' in self.chk.options.disallow_any and
24132415
has_any_from_unimported_type(tuple_type)):
24142416
self.msg.unimported_type_becomes_any("NamedTuple type", tuple_type, e)
2417+
check_for_explicit_any(tuple_type, self.chk.options, self.chk.is_typeshed_stub,
2418+
self.msg, context=e)
24152419
# TODO: Perhaps return a type object type?
24162420
return AnyType()
24172421

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']
100+
disallow_any_options = ['unimported', 'expr', 'unannotated', 'decorated', 'explicit']
101101

102102

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

mypy/messages.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -879,6 +879,9 @@ def unimported_type_becomes_any(self, prefix: str, typ: Type, ctx: Context) -> N
879879
self.fail("{} becomes {} due to an unfollowed import".format(prefix, self.format(typ)),
880880
ctx)
881881

882+
def explicit_any(self, ctx: Context) -> None:
883+
self.fail('Explicit "Any" is not allowed', ctx)
884+
882885
def unexpected_typeddict_keys(
883886
self,
884887
typ: TypedDictType,

mypy/semanal.py

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@
4747
from contextlib import contextmanager
4848

4949
from typing import (
50-
List, Dict, Set, Tuple, cast, TypeVar, Union, Optional, Callable, Iterator,
50+
List, Dict, Set, Tuple, cast, TypeVar, Union, Optional, Callable, Iterator, Iterable
5151
)
5252

5353
from mypy.nodes import (
@@ -80,12 +80,13 @@
8080
NoneTyp, CallableType, Overloaded, Instance, Type, TypeVarType, AnyType,
8181
FunctionLike, UnboundType, TypeList, TypeVarDef, TypeType,
8282
TupleType, UnionType, StarType, EllipsisType, function_type, TypedDictType,
83-
TypeQuery
83+
TypeTranslator,
8484
)
8585
from mypy.nodes import implicit_module_attrs
8686
from mypy.typeanal import (
8787
TypeAnalyser, TypeAnalyserPass3, analyze_type_alias, no_subscript_builtin_alias,
88-
TypeVariableQuery, TypeVarList, remove_dups, has_any_from_unimported_type
88+
TypeVariableQuery, TypeVarList, remove_dups, has_any_from_unimported_type,
89+
check_for_explicit_any
8990
)
9091
from mypy.exprtotype import expr_to_unanalyzed_type, TypeTranslationError
9192
from mypy.sametypes import is_same_type
@@ -230,6 +231,7 @@ class SemanticAnalyzer(NodeVisitor):
230231
loop_depth = 0 # Depth of breakable loops
231232
cur_mod_id = '' # Current module id (or None) (phase 2)
232233
is_stub_file = False # Are we analyzing a stub file?
234+
is_typeshed_stub_file = False # Are we analyzing a typeshed stub file?
233235
imports = None # type: Set[str] # Imported modules (during phase 2 analysis)
234236
errors = None # type: Errors # Keeps track of generated errors
235237
plugin = None # type: Plugin # Mypy plugin for special casing of library features
@@ -274,6 +276,7 @@ def visit_file(self, file_node: MypyFile, fnam: str, options: Options,
274276
self.cur_mod_node = file_node
275277
self.cur_mod_id = file_node.fullname()
276278
self.is_stub_file = fnam.lower().endswith('.pyi')
279+
self.is_typeshed_stub_file = self.errors.is_typeshed_file(file_node.path)
277280
self.globals = file_node.names
278281
self.patches = patches
279282

@@ -339,6 +342,7 @@ def file_context(self, file_node: MypyFile, fnam: str, options: Options,
339342
self.cur_mod_node = file_node
340343
self.cur_mod_id = file_node.fullname()
341344
self.is_stub_file = fnam.lower().endswith('.pyi')
345+
self.is_typeshed_stub_file = self.errors.is_typeshed_file(file_node.path)
342346
self.globals = file_node.names
343347
if active_type:
344348
self.enter_class(active_type.defn.info)
@@ -1017,6 +1021,8 @@ def analyze_base_classes(self, defn: ClassDef) -> None:
10171021
else:
10181022
prefix = "Base type"
10191023
self.msg.unimported_type_becomes_any(prefix, base, base_expr)
1024+
check_for_explicit_any(base, self.options, self.is_typeshed_stub_file, self.msg,
1025+
context=base_expr)
10201026

10211027
# Add 'object' as implicit base if there is no other base class.
10221028
if (not base_types and defn.fullname != 'builtins.object'):
@@ -1571,6 +1577,12 @@ def visit_assignment_stmt(self, s: AssignmentStmt) -> None:
15711577
allow_unnormalized=True)
15721578
if res and (not isinstance(res, Instance) or res.args):
15731579
# TODO: What if this gets reassigned?
1580+
check_for_explicit_any(res, self.options, self.is_typeshed_stub_file, self.msg,
1581+
context=s)
1582+
# when this type alias gets "inlined", the Any is not explicit anymore,
1583+
# so we need to replace it with non-explicit Anys
1584+
res = make_any_non_explicit(res)
1585+
15741586
name = s.lvalues[0]
15751587
node = self.lookup(name.name, name)
15761588
node.kind = TYPE_ALIAS
@@ -1835,6 +1847,9 @@ def process_newtype_declaration(self, s: AssignmentStmt) -> None:
18351847
self.fail(message.format(old_type), s)
18361848
return
18371849

1850+
check_for_explicit_any(old_type, self.options, self.is_typeshed_stub_file, self.msg,
1851+
context=s)
1852+
18381853
if 'unimported' in self.options.disallow_any and has_any_from_unimported_type(old_type):
18391854
self.msg.unimported_type_becomes_any("Argument 2 to NewType(...)", old_type, s)
18401855

@@ -1959,6 +1974,9 @@ def process_typevar_declaration(self, s: AssignmentStmt) -> None:
19591974
prefix = "Upper bound of type variable"
19601975
self.msg.unimported_type_becomes_any(prefix, upper_bound, s)
19611976

1977+
for t in values + [upper_bound]:
1978+
check_for_explicit_any(t, self.options, self.is_typeshed_stub_file, self.msg,
1979+
context=s)
19621980
# Yes, it's a valid type variable definition! Add it to the symbol table.
19631981
node = self.lookup(name, s)
19641982
node.kind = TVAR
@@ -2393,6 +2411,10 @@ def parse_typeddict_args(self, call: CallExpr,
23932411
'TypedDict() "total" argument must be True or False', call)
23942412
dictexpr = args[1]
23952413
items, types, ok = self.parse_typeddict_fields_with_types(dictexpr.items, call)
2414+
for t in types:
2415+
check_for_explicit_any(t, self.options, self.is_typeshed_stub_file, self.msg,
2416+
context=call)
2417+
23962418
if 'unimported' in self.options.disallow_any:
23972419
for t in types:
23982420
if has_any_from_unimported_type(t):
@@ -4390,3 +4412,13 @@ def find_fixed_callable_return(expr: Expression) -> Optional[CallableType]:
43904412
if isinstance(t.ret_type, CallableType):
43914413
return t.ret_type
43924414
return None
4415+
4416+
4417+
def make_any_non_explicit(t: Type) -> Type:
4418+
"""Replace all Any types within in with Any that has attribute 'explicit' set to False"""
4419+
return t.accept(MakeAnyNonExplicit())
4420+
4421+
4422+
class MakeAnyNonExplicit(TypeTranslator):
4423+
def visit_any(self, t: AnyType) -> Type:
4424+
return t.copy_modified(explicit=False)

mypy/typeanal.py

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66

77
from contextlib import contextmanager
88

9+
from mypy.messages import MessageBuilder
10+
from mypy.options import Options
911
from mypy.types import (
1012
Type, UnboundType, TypeVarType, TupleType, TypedDictType, UnionType, Instance,
1113
AnyType, CallableType, NoneTyp, DeletedType, TypeList, TypeVarDef, TypeVisitor,
@@ -166,7 +168,7 @@ def visit_unbound_type(self, t: UnboundType) -> Type:
166168
elif fullname == 'builtins.None':
167169
return NoneTyp()
168170
elif fullname == 'typing.Any' or fullname == 'builtins.Any':
169-
return AnyType()
171+
return AnyType(explicit=True)
170172
elif fullname == 'typing.Tuple':
171173
if len(t.args) == 0 and not t.empty_tuple_index:
172174
# Bare 'Tuple' is same as 'tuple'
@@ -754,6 +756,37 @@ def visit_callable_type(self, t: CallableType) -> TypeVarList:
754756
return []
755757

756758

759+
def check_for_explicit_any(typ: Optional[Type],
760+
options: Options,
761+
is_typeshed_stub: bool,
762+
msg: MessageBuilder,
763+
context: Context) -> None:
764+
if ('explicit' in options.disallow_any and
765+
not is_typeshed_stub and
766+
typ and
767+
has_explicit_any(typ)):
768+
msg.explicit_any(context)
769+
770+
771+
def has_explicit_any(t: Type) -> bool:
772+
"""
773+
Whether this type is or type it contains is an Any coming from explicit type annotation
774+
"""
775+
return t.accept(HasExplicitAny())
776+
777+
778+
class HasExplicitAny(TypeQuery[bool]):
779+
def __init__(self) -> None:
780+
super().__init__(any)
781+
782+
def visit_any(self, t: AnyType) -> bool:
783+
return t.explicit
784+
785+
def visit_typeddict_type(self, t: TypedDictType) -> bool:
786+
# typeddict is checked during TypedDict declaration, so don't typecheck it here.
787+
return False
788+
789+
757790
def has_any_from_unimported_type(t: Type) -> bool:
758791
"""Return true if this type is Any because an import was not followed.
759792

mypy/types.py

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -249,23 +249,46 @@ def serialize(self) -> JsonDict:
249249
assert False, "Sythetic types don't serialize"
250250

251251

252+
_dummy = object() # type: Any
253+
254+
252255
class AnyType(Type):
253256
"""The type 'Any'."""
254257

255258
def __init__(self,
256259
implicit: bool = False,
257260
from_unimported_type: bool = False,
261+
explicit: bool = False,
258262
line: int = -1,
259263
column: int = -1) -> None:
260264
super().__init__(line, column)
261265
# Was this Any type was inferred without a type annotation?
266+
# Note that this is not always the opposite of explicit.
267+
# For instance, if "Any" comes from an unimported type,
268+
# both explicit and implicit will be False
262269
self.implicit = implicit
263270
# Does this come from an unfollowed import? See --disallow-any=unimported option
264271
self.from_unimported_type = from_unimported_type
272+
# Does this Any come from an explicit type annotation?
273+
self.explicit = explicit
265274

266275
def accept(self, visitor: 'TypeVisitor[T]') -> T:
267276
return visitor.visit_any(self)
268277

278+
def copy_modified(self,
279+
implicit: bool = _dummy,
280+
from_unimported_type: bool = _dummy,
281+
explicit: bool = _dummy,
282+
) -> 'AnyType':
283+
if implicit is _dummy:
284+
implicit = self.implicit
285+
if from_unimported_type is _dummy:
286+
from_unimported_type = self.from_unimported_type
287+
if explicit is _dummy:
288+
explicit = self.explicit
289+
return AnyType(implicit=implicit, from_unimported_type=from_unimported_type,
290+
explicit=explicit, line=self.line, column=self.column)
291+
269292
def serialize(self) -> JsonDict:
270293
return {'.class': 'AnyType'}
271294

@@ -509,9 +532,6 @@ def get_name(self) -> Optional[str]: pass
509532
fallback = None # type: Instance
510533

511534

512-
_dummy = object() # type: Any
513-
514-
515535
FormalArgument = NamedTuple('FormalArgument', [
516536
('name', Optional[str]),
517537
('pos', Optional[int]),

0 commit comments

Comments
 (0)