Skip to content

Commit f398374

Browse files
committed
Recognize non-string type expressions everywhere lazily. Optimize eager parsing to bail earlier.
Also: * No longer add "as_type" attribute to NameExpr and MemberExpr. Retain that attribute on StrExpr, IndexExpr, and OpExpr. * Recognize dotted type expressions like "typing.List".
1 parent a2c318b commit f398374

File tree

6 files changed

+431
-36
lines changed

6 files changed

+431
-36
lines changed

mypy/checker.py

Lines changed: 106 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@
131131
from mypy.scope import Scope
132132
from mypy.semanal import is_trivial_body, refers_to_fullname, set_callable_name
133133
from mypy.semanal_enum import ENUM_BASES, ENUM_SPECIAL_PROPS
134+
from mypy.semanal_shared import SemanticAnalyzerCoreInterface
134135
from mypy.sharedparse import BINARY_MAGIC_METHODS
135136
from mypy.state import state
136137
from mypy.subtypes import (
@@ -307,6 +308,8 @@ class TypeChecker(NodeVisitor[None], CheckerPluginInterface):
307308

308309
tscope: Scope
309310
scope: CheckerScope
311+
# Innermost enclosing type
312+
type: TypeInfo | None
310313
# Stack of function return types
311314
return_types: list[Type]
312315
# Flags; true for dynamically typed functions
@@ -378,6 +381,7 @@ def __init__(
378381
self.scope = CheckerScope(tree)
379382
self.binder = ConditionalTypeBinder()
380383
self.globals = tree.names
384+
self.type = None
381385
self.return_types = []
382386
self.dynamic_funcs = []
383387
self.partial_types = []
@@ -2556,7 +2560,9 @@ def visit_class_def(self, defn: ClassDef) -> None:
25562560
for base in typ.mro[1:]:
25572561
if base.is_final:
25582562
self.fail(message_registry.CANNOT_INHERIT_FROM_FINAL.format(base.name), defn)
2559-
with self.tscope.class_scope(defn.info), self.enter_partial_types(is_class=True):
2563+
with self.tscope.class_scope(defn.info), \
2564+
self.enter_partial_types(is_class=True), \
2565+
self.enter_class(defn.info):
25602566
old_binder = self.binder
25612567
self.binder = ConditionalTypeBinder()
25622568
with self.binder.top_frame_context():
@@ -2624,6 +2630,15 @@ def visit_class_def(self, defn: ClassDef) -> None:
26242630
self.check_enum(defn)
26252631
infer_class_variances(defn.info)
26262632

2633+
@contextmanager
2634+
def enter_class(self, type: TypeInfo) -> Iterator[None]:
2635+
original_type = self.type
2636+
self.type = type
2637+
try:
2638+
yield
2639+
finally:
2640+
self.type = original_type
2641+
26272642
def check_final_deletable(self, typ: TypeInfo) -> None:
26282643
# These checks are only for mypyc. Only perform some checks that are easier
26292644
# to implement here than in mypyc.
@@ -7923,6 +7938,96 @@ def visit_global_decl(self, o: GlobalDecl, /) -> None:
79237938
return None
79247939

79257940

7941+
class TypeCheckerAsSemanticAnalyzer(SemanticAnalyzerCoreInterface):
7942+
"""
7943+
Adapts TypeChecker to the SemanticAnalyzerCoreInterface,
7944+
allowing most type expressions to be parsed during the TypeChecker pass.
7945+
7946+
See ExpressionChecker.try_parse_as_type_expression() to understand how this
7947+
class is used.
7948+
"""
7949+
_chk: TypeChecker
7950+
_names: dict[str, SymbolTableNode]
7951+
did_fail: bool
7952+
7953+
def __init__(self, chk: TypeChecker, names: dict[str, SymbolTableNode]) -> None:
7954+
self._chk = chk
7955+
self._names = names
7956+
self.did_fail = False
7957+
7958+
def lookup_qualified(
7959+
self, name: str, ctx: Context, suppress_errors: bool = False
7960+
) -> SymbolTableNode | None:
7961+
sym = self._names.get(name)
7962+
# All names being looked up should have been previously gathered,
7963+
# even if the related SymbolTableNode does not refer to a valid SymbolNode
7964+
assert sym is not None, name
7965+
return sym
7966+
7967+
def lookup_fully_qualified(self, fullname: str, /) -> SymbolTableNode:
7968+
ret = self.lookup_fully_qualified_or_none(fullname)
7969+
assert ret is not None, fullname
7970+
return ret
7971+
7972+
def lookup_fully_qualified_or_none(self, fullname: str, /) -> SymbolTableNode | None:
7973+
try:
7974+
return self._chk.lookup_qualified(fullname)
7975+
except KeyError:
7976+
return None
7977+
7978+
def fail(
7979+
self,
7980+
msg: str,
7981+
ctx: Context,
7982+
serious: bool = False,
7983+
*,
7984+
blocker: bool = False,
7985+
code: ErrorCode | None = None,
7986+
) -> None:
7987+
self.did_fail = True
7988+
7989+
def note(self, msg: str, ctx: Context, *, code: ErrorCode | None = None) -> None:
7990+
pass
7991+
7992+
def incomplete_feature_enabled(self, feature: str, ctx: Context) -> bool:
7993+
if feature not in self._chk.options.enable_incomplete_feature:
7994+
self.fail('__ignored__', ctx)
7995+
return False
7996+
return True
7997+
7998+
def record_incomplete_ref(self) -> None:
7999+
pass
8000+
8001+
def defer(self, debug_context: Context | None = None, force_progress: bool = False) -> None:
8002+
pass
8003+
8004+
def is_incomplete_namespace(self, fullname: str) -> bool:
8005+
return False
8006+
8007+
@property
8008+
def final_iteration(self) -> bool:
8009+
return True
8010+
8011+
def is_future_flag_set(self, flag: str) -> bool:
8012+
return self._chk.tree.is_future_flag_set(flag)
8013+
8014+
@property
8015+
def is_stub_file(self) -> bool:
8016+
return self._chk.tree.is_stub
8017+
8018+
def is_func_scope(self) -> bool:
8019+
# Return arbitrary value.
8020+
#
8021+
# This method is currently only used to decide whether to pair
8022+
# a fail() message with a note() message or not. Both of those
8023+
# message types are ignored.
8024+
return False
8025+
8026+
@property
8027+
def type(self) -> TypeInfo | None:
8028+
return self._chk.type
8029+
8030+
79268031
class CollectArgTypeVarTypes(TypeTraverserVisitor):
79278032
"""Collects the non-nested argument types in a set."""
79288033

mypy/checkexpr.py

Lines changed: 79 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
freshen_all_functions_type_vars,
3131
freshen_function_type_vars,
3232
)
33+
from mypy.exprtotype import expr_to_unanalyzed_type, TypeTranslationError
3334
from mypy.infer import ArgumentInferContext, infer_function_type_arguments, infer_type_arguments
3435
from mypy.literals import literal
3536
from mypy.maptype import map_instance_to_supertype
@@ -66,6 +67,7 @@
6667
FloatExpr,
6768
FuncDef,
6869
GeneratorExpr,
70+
get_member_expr_fullname,
6971
IndexExpr,
7072
IntExpr,
7173
LambdaExpr,
@@ -91,6 +93,7 @@
9193
StrExpr,
9294
SuperExpr,
9395
SymbolNode,
96+
SymbolTableNode,
9497
TempNode,
9598
TupleExpr,
9699
TypeAlias,
@@ -102,6 +105,7 @@
102105
TypeVarExpr,
103106
TypeVarTupleExpr,
104107
UnaryExpr,
108+
UNBOUND_IMPORTED,
105109
Var,
106110
YieldExpr,
107111
YieldFromExpr,
@@ -123,14 +127,16 @@
123127
is_subtype,
124128
non_method_protocol_members,
125129
)
126-
from mypy.traverser import has_await_expression
130+
from mypy.traverser import all_name_and_member_expressions, has_await_expression, has_str_expression
131+
from mypy.tvar_scope import TypeVarLikeScope
127132
from mypy.typeanal import (
128133
check_for_explicit_any,
129134
fix_instance,
130135
has_any_from_unimported_type,
131136
instantiate_type_alias,
132137
make_optional_type,
133138
set_any_tvars,
139+
TypeAnalyser,
134140
validate_instance,
135141
)
136142
from mypy.typeops import (
@@ -5950,13 +5956,12 @@ def accept(
59505956
elif (
59515957
isinstance(p_type_context, TypeType)
59525958
and p_type_context.is_type_form
5953-
and isinstance(node, MaybeTypeExpression)
5954-
and node.as_type is not None
5959+
and (node_as_type := self.try_parse_as_type_expression(node)) is not None
59555960
):
59565961
typ = TypeType.make_normalized(
5957-
node.as_type,
5958-
line=node.as_type.line,
5959-
column=node.as_type.column,
5962+
node_as_type,
5963+
line=node_as_type.line,
5964+
column=node_as_type.column,
59605965
is_type_form=True,
59615966
)
59625967
else:
@@ -6313,6 +6318,74 @@ def has_abstract_type(self, caller_type: ProperType, callee_type: ProperType) ->
63136318
and not self.chk.allow_abstract_call
63146319
)
63156320

6321+
def try_parse_as_type_expression(self, maybe_type_expr: Expression) -> Type | None:
6322+
"""Try to parse a value Expression as a type expression.
6323+
If success then return the type that it spells.
6324+
If fails then return None.
6325+
6326+
A value expression that is parsable as a type expression may be used
6327+
where a TypeForm is expected to represent the spelled type.
6328+
6329+
Unlike SemanticAnalyzer.try_parse_as_type_expression()
6330+
(used in the earlier SemanticAnalyzer pass), this function can only
6331+
recognize type expressions which contain no string annotations."""
6332+
if not isinstance(maybe_type_expr, MaybeTypeExpression):
6333+
return None
6334+
6335+
# Check whether has already been parsed as a type expression
6336+
# by SemanticAnalyzer.try_parse_as_type_expression(),
6337+
# perhaps containing a string annotation
6338+
if (isinstance(maybe_type_expr, (StrExpr, IndexExpr, OpExpr))
6339+
and maybe_type_expr.as_type is not Ellipsis):
6340+
return maybe_type_expr.as_type
6341+
6342+
# If is potentially a type expression containing a string annotation,
6343+
# don't try to parse it because there isn't enough information
6344+
# available to the TypeChecker pass to resolve string annotations
6345+
if has_str_expression(maybe_type_expr):
6346+
self.chk.note(
6347+
'TypeForm containing a string annotation cannot be recognized here. '
6348+
'Try assigning the TypeForm to a variable and use the variable here instead.',
6349+
maybe_type_expr,
6350+
)
6351+
return None
6352+
6353+
# Collect symbols targeted by NameExprs and MemberExprs,
6354+
# to be looked up by TypeAnalyser when binding the
6355+
# UnboundTypes corresponding to those expressions.
6356+
(name_exprs, member_exprs) = all_name_and_member_expressions(maybe_type_expr)
6357+
sym_for_name = {
6358+
e.name:
6359+
SymbolTableNode(UNBOUND_IMPORTED, e.node)
6360+
for e in name_exprs
6361+
} | {
6362+
e_name:
6363+
SymbolTableNode(UNBOUND_IMPORTED, e.node)
6364+
for e in member_exprs
6365+
if (e_name := get_member_expr_fullname(e)) is not None
6366+
}
6367+
6368+
chk_sem = mypy.checker.TypeCheckerAsSemanticAnalyzer(self.chk, sym_for_name)
6369+
tpan = TypeAnalyser(
6370+
chk_sem,
6371+
TypeVarLikeScope(), # empty scope
6372+
self.plugin,
6373+
self.chk.options,
6374+
self.chk.tree,
6375+
self.chk.is_typeshed_stub,
6376+
)
6377+
6378+
try:
6379+
typ1 = expr_to_unanalyzed_type(
6380+
maybe_type_expr, self.chk.options, self.chk.is_typeshed_stub,
6381+
)
6382+
typ2 = typ1.accept(tpan)
6383+
if chk_sem.did_fail:
6384+
return None
6385+
return typ2
6386+
except TypeTranslationError:
6387+
return None
6388+
63166389

63176390
def has_any_type(t: Type, ignore_in_type_obj: bool = False) -> bool:
63186391
"""Whether t contains an Any type"""

mypy/nodes.py

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import os
66
from abc import abstractmethod
7+
import builtins
78
from collections import defaultdict
89
from collections.abc import Iterator, Sequence
910
from enum import Enum, unique
@@ -20,6 +21,9 @@
2021
if TYPE_CHECKING:
2122
from mypy.patterns import Pattern
2223

24+
EllipsisType = builtins.ellipsis
25+
else:
26+
EllipsisType = Any
2327

2428
class Context:
2529
"""Base type for objects that are valid as error message locations."""
@@ -1723,12 +1727,13 @@ class StrExpr(Expression):
17231727
value: str # '' by default
17241728
# If this value expression can also be parsed as a valid type expression,
17251729
# represents the type denoted by the type expression.
1726-
as_type: mypy.types.Type | None
1730+
# Ellipsis means "not parsed" and None means "is not a type expression".
1731+
as_type: EllipsisType | mypy.types.Type | None
17271732

17281733
def __init__(self, value: str) -> None:
17291734
super().__init__()
17301735
self.value = value
1731-
self.as_type = None
1736+
self.as_type = Ellipsis
17321737

17331738
def accept(self, visitor: ExpressionVisitor[T]) -> T:
17341739
return visitor.visit_str_expr(self)
@@ -1879,20 +1884,15 @@ class NameExpr(RefExpr):
18791884
This refers to a local name, global name or a module.
18801885
"""
18811886

1882-
__slots__ = ("name", "is_special_form", "as_type")
1887+
__slots__ = ("name", "is_special_form")
18831888

18841889
__match_args__ = ("name", "node")
18851890

1886-
# If this value expression can also be parsed as a valid type expression,
1887-
# represents the type denoted by the type expression.
1888-
as_type: mypy.types.Type | None
1889-
18901891
def __init__(self, name: str) -> None:
18911892
super().__init__()
18921893
self.name = name # Name referred to
18931894
# Is this a l.h.s. of a special form assignment like typed dict or type variable?
18941895
self.is_special_form = False
1895-
self.as_type = None
18961896

18971897
def accept(self, visitor: ExpressionVisitor[T]) -> T:
18981898
return visitor.visit_name_expr(self)
@@ -2045,15 +2045,16 @@ class IndexExpr(Expression):
20452045
analyzed: TypeApplication | TypeAliasExpr | None
20462046
# If this value expression can also be parsed as a valid type expression,
20472047
# represents the type denoted by the type expression.
2048-
as_type: mypy.types.Type | None
2048+
# Ellipsis means "not parsed" and None means "is not a type expression".
2049+
as_type: EllipsisType | mypy.types.Type | None
20492050

20502051
def __init__(self, base: Expression, index: Expression) -> None:
20512052
super().__init__()
20522053
self.base = base
20532054
self.index = index
20542055
self.method_type = None
20552056
self.analyzed = None
2056-
self.as_type = None
2057+
self.as_type = Ellipsis
20572058

20582059
def accept(self, visitor: ExpressionVisitor[T]) -> T:
20592060
return visitor.visit_index_expr(self)
@@ -2129,7 +2130,8 @@ class OpExpr(Expression):
21292130
analyzed: TypeAliasExpr | None
21302131
# If this value expression can also be parsed as a valid type expression,
21312132
# represents the type denoted by the type expression.
2132-
as_type: mypy.types.Type | None
2133+
# Ellipsis means "not parsed" and None means "is not a type expression".
2134+
as_type: EllipsisType | mypy.types.Type | None
21332135

21342136
def __init__(
21352137
self, op: str, left: Expression, right: Expression, analyzed: TypeAliasExpr | None = None
@@ -2142,16 +2144,17 @@ def __init__(
21422144
self.right_always = False
21432145
self.right_unreachable = False
21442146
self.analyzed = analyzed
2145-
self.as_type = None
2147+
self.as_type = Ellipsis
21462148

21472149
def accept(self, visitor: ExpressionVisitor[T]) -> T:
21482150
return visitor.visit_op_expr(self)
21492151

21502152

21512153
# Expression subtypes that could represent the root of a valid type expression.
2152-
# Always contains an "as_type" attribute.
2153-
# TODO: Make this into a Protocol if mypyc is OK with that.
2154-
MaybeTypeExpression = (IndexExpr, NameExpr, OpExpr, StrExpr)
2154+
#
2155+
# May have an "as_type" attribute to hold the type for a type expression parsed
2156+
# during the SemanticAnalyzer pass.
2157+
MaybeTypeExpression = (IndexExpr, MemberExpr, NameExpr, OpExpr, StrExpr)
21552158

21562159

21572160
class ComparisonExpr(Expression):

0 commit comments

Comments
 (0)