Skip to content

Commit 5d52eac

Browse files
authored
New semantic analyzer: Make assignment analysis logic more straightforward (#6527)
Fixes #6412 Fixes #6453 In this PR: * Wait until we can classify r.h.s. of an assignment as normal, type alias, named tuple etc. * Choose the corresponding branch instead of performing all of them (as it used to be before) * Never create (or rely on creation of) temporary `Var`s * Add every symbol to symbol table only once (as it is done for classes) * Don't "patch" existing symbols (with one exception for type variables, see below) * Add and update docstrings and comments Notes: * I allow placeholders for type alias targets and NewTypes targets, they essentially behave as base classes. In general I make logic for type aliases and NewTypes much more similar to logic of class definitions (especially NewTypes), this allows to support a whole bunch of tricky forward references * I don't store types with placeholders in annotations, and instead if an alias target got updated (e.g. placeholders are resolved), I set `progress=True` Follow-ups: * There is still a small hack to keep compatibility with old analyzer in type checker, but now it is basically "don't use binder for l.h.s. of special forms" * Type variable declarations still have a bug that duplicate type variable definition is not reported as error, second one silently wins, this will be fixed in a separate PR * This PR doesn't aim to fix aspects of assignment analysis related to #6422, that will be also a separate PR Test updates: * Add and enable more tests for type aliases and NewTypes * Add test for cyclic definition error messages * Change order of error messages on the same line in few tests * Enable one test file where previously only some tests were opted in (I think it was not enabled by mistake in one of the previous PRs, I enable it here since order of errors in one test in that file is affected by this PR.)
1 parent 69a5a02 commit 5d52eac

15 files changed

+570
-237
lines changed

mypy/checker.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1715,6 +1715,11 @@ def visit_assignment_stmt(self, s: AssignmentStmt) -> None:
17151715
with self.enter_final_context(s.is_final_def):
17161716
self.check_assignment(s.lvalues[-1], s.rvalue, s.type is None, s.new_syntax)
17171717

1718+
if s.is_alias_def:
1719+
# We do this mostly for compatibility with old semantic analyzer.
1720+
# TODO: should we get rid of this?
1721+
self.store_type(s.lvalues[-1], self.expr_checker.accept(s.rvalue))
1722+
17181723
if (s.type is not None and
17191724
self.options.disallow_any_unimported and
17201725
has_any_from_unimported_type(s.type)):
@@ -1824,7 +1829,9 @@ def check_assignment(self, lvalue: Lvalue, rvalue: Expression, infer_lvalue_type
18241829
self.msg.concrete_only_assign(lvalue_type, rvalue)
18251830
return
18261831
if rvalue_type and infer_lvalue_type and not isinstance(lvalue_type, PartialType):
1827-
self.binder.assign_type(lvalue, rvalue_type, lvalue_type, False)
1832+
# Don't use type binder for definitions of special forms, like named tuples.
1833+
if not (isinstance(lvalue, NameExpr) and lvalue.is_special_form):
1834+
self.binder.assign_type(lvalue, rvalue_type, lvalue_type, False)
18281835

18291836
elif index_lvalue:
18301837
self.check_indexed_assignment(index_lvalue, rvalue, lvalue)

mypy/newsemanal/semanal.py

Lines changed: 275 additions & 108 deletions
Large diffs are not rendered by default.

mypy/newsemanal/semanal_enum.py

Lines changed: 25 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -19,20 +19,22 @@ def __init__(self, options: Options, api: SemanticAnalyzerInterface) -> None:
1919
self.options = options
2020
self.api = api
2121

22-
def process_enum_call(self, s: AssignmentStmt, is_func_scope: bool) -> None:
23-
"""Check if s defines an Enum; if yes, store the definition in symbol table."""
22+
def process_enum_call(self, s: AssignmentStmt, is_func_scope: bool) -> bool:
23+
"""Check if s defines an Enum; if yes, store the definition in symbol table.
24+
25+
Return True if this looks like an Enum definition (but maybe with errors),
26+
otherwise return False.
27+
"""
2428
if len(s.lvalues) != 1 or not isinstance(s.lvalues[0], NameExpr):
25-
return
29+
return False
2630
lvalue = s.lvalues[0]
2731
name = lvalue.name
2832
enum_call = self.check_enum_call(s.rvalue, name, is_func_scope)
2933
if enum_call is None:
30-
return
34+
return False
3135
# Yes, it's a valid Enum definition. Add it to the symbol table.
32-
node = self.api.lookup(name, s)
33-
if node:
34-
node.kind = GDEF # TODO locally defined Enum
35-
node.node = enum_call
36+
self.api.add_symbol(name, enum_call, s)
37+
return True
3638

3739
def check_enum_call(self,
3840
node: Expression,
@@ -62,18 +64,19 @@ class A(enum.Enum):
6264
items, values, ok = self.parse_enum_call_args(call, fullname.split('.')[-1])
6365
if not ok:
6466
# Error. Construct dummy return value.
65-
return self.build_enum_call_typeinfo(var_name, [], fullname)
66-
name = cast(Union[StrExpr, UnicodeExpr], call.args[0]).value
67-
if name != var_name or is_func_scope:
68-
# Give it a unique name derived from the line number.
69-
name += '@' + str(call.line)
70-
info = self.build_enum_call_typeinfo(name, items, fullname)
71-
# Store it as a global just in case it would remain anonymous.
72-
# (Or in the nearest class if there is one.)
73-
stnode = SymbolTableNode(GDEF, info)
74-
self.api.add_symbol_table_node(name, stnode)
67+
info = self.build_enum_call_typeinfo(var_name, [], fullname)
68+
else:
69+
name = cast(Union[StrExpr, UnicodeExpr], call.args[0]).value
70+
if name != var_name or is_func_scope:
71+
# Give it a unique name derived from the line number.
72+
name += '@' + str(call.line)
73+
info = self.build_enum_call_typeinfo(name, items, fullname)
74+
# Store generated TypeInfo under both names, see semanal_namedtuple for more details.
75+
if name != var_name or is_func_scope:
76+
self.api.add_symbol_skip_local(name, info)
7577
call.analyzed = EnumCallExpr(info, items, values)
7678
call.analyzed.set_line(call.line, call.column)
79+
info.line = node.line
7780
return info
7881

7982
def build_enum_call_typeinfo(self, name: str, items: List[str], fullname: str) -> TypeInfo:
@@ -93,6 +96,10 @@ def build_enum_call_typeinfo(self, name: str, items: List[str], fullname: str) -
9396
def parse_enum_call_args(self, call: CallExpr,
9497
class_name: str) -> Tuple[List[str],
9598
List[Optional[Expression]], bool]:
99+
"""Parse arguments of an Enum call.
100+
101+
Return a tuple of fields, values, was there an error.
102+
"""
96103
args = call.args
97104
if len(args) < 2:
98105
return self.fail_enum_call_arg("Too few arguments for %s()" % class_name, call)

mypy/newsemanal/semanal_newtype.py

Lines changed: 75 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,14 @@
55

66
from typing import Tuple, Optional
77

8-
from mypy.types import Type, Instance, CallableType, NoneTyp, TupleType, AnyType, TypeOfAny
8+
from mypy.types import (
9+
Type, Instance, CallableType, NoneTyp, TupleType, AnyType, PlaceholderType,
10+
TypeOfAny
11+
)
912
from mypy.nodes import (
1013
AssignmentStmt, NewTypeExpr, CallExpr, NameExpr, RefExpr, Context, StrExpr, BytesExpr,
11-
UnicodeExpr, Block, FuncDef, Argument, TypeInfo, Var, SymbolTableNode, GDEF, MDEF, ARG_POS
14+
UnicodeExpr, Block, FuncDef, Argument, TypeInfo, Var, SymbolTableNode, MDEF, ARG_POS,
15+
PlaceholderNode
1216
)
1317
from mypy.newsemanal.semanal_shared import SemanticAnalyzerInterface
1418
from mypy.options import Options
@@ -26,17 +30,36 @@ def __init__(self,
2630
self.api = api
2731
self.msg = msg
2832

29-
def process_newtype_declaration(self, s: AssignmentStmt) -> None:
30-
"""Check if s declares a NewType; if yes, store it in symbol table."""
31-
# Extract and check all information from newtype declaration
33+
def process_newtype_declaration(self, s: AssignmentStmt) -> bool:
34+
"""Check if s declares a NewType; if yes, store it in symbol table.
35+
36+
Return True if it's a NewType declaration. The current target may be
37+
deferred as a side effect if the base type is not ready, even if
38+
the return value is True.
39+
40+
The logic in this function mostly copies the logic for visit_class_def()
41+
with a single (non-Generic) base.
42+
"""
3243
name, call = self.analyze_newtype_declaration(s)
3344
if name is None or call is None:
34-
return
35-
36-
old_type = self.check_newtype_args(name, call, s)
37-
call.analyzed = NewTypeExpr(name, old_type, line=call.line)
45+
return False
46+
# OK, now we know this is a NewType. But the base type may be not ready yet,
47+
# add placeholder as we do for ClassDef.
48+
49+
fullname = self.api.qualified_name(name)
50+
if (not call.analyzed or
51+
isinstance(call.analyzed, NewTypeExpr) and not call.analyzed.info):
52+
# Start from labeling this as a future class, as we do for normal ClassDefs.
53+
self.api.add_symbol(name, PlaceholderNode(fullname, s, becomes_typeinfo=True), s)
54+
55+
old_type, should_defer = self.check_newtype_args(name, call, s)
56+
if not call.analyzed:
57+
call.analyzed = NewTypeExpr(name, old_type, line=call.line)
3858
if old_type is None:
39-
return
59+
if should_defer:
60+
# Base type is not ready.
61+
self.api.defer()
62+
return True
4063

4164
# Create the corresponding class definition if the aliased type is subtypeable
4265
if isinstance(old_type, TupleType):
@@ -48,9 +71,14 @@ def process_newtype_declaration(self, s: AssignmentStmt) -> None:
4871
self.fail("NewType cannot be used with protocol classes", s)
4972
newtype_class_info = self.build_newtype_typeinfo(name, old_type, old_type)
5073
else:
51-
message = "Argument 2 to NewType(...) must be subclassable (got {})"
52-
self.fail(message.format(self.msg.format(old_type)), s)
53-
return
74+
if old_type is not None:
75+
message = "Argument 2 to NewType(...) must be subclassable (got {})"
76+
self.fail(message.format(self.msg.format(old_type)), s)
77+
# Otherwise the error was already reported.
78+
old_type = AnyType(TypeOfAny.from_error)
79+
object_type = self.api.named_type('__builtins__.object')
80+
newtype_class_info = self.build_newtype_typeinfo(name, old_type, object_type)
81+
newtype_class_info.fallback_to_any = True
5482

5583
check_for_explicit_any(old_type, self.options, self.api.is_typeshed_stub_file, self.msg,
5684
context=s)
@@ -59,13 +87,16 @@ def process_newtype_declaration(self, s: AssignmentStmt) -> None:
5987
self.msg.unimported_type_becomes_any("Argument 2 to NewType(...)", old_type, s)
6088

6189
# If so, add it to the symbol table.
62-
node = self.api.lookup(name, s)
63-
if node is None:
64-
self.fail("Could not find {} in current namespace".format(name), s)
65-
return
66-
# TODO: why does NewType work in local scopes despite always being of kind GDEF?
67-
node.kind = GDEF
68-
call.analyzed.info = node.node = newtype_class_info
90+
assert isinstance(call.analyzed, NewTypeExpr)
91+
# As we do for normal classes, create the TypeInfo only once, then just
92+
# update base classes on next iterations (to get rid of placeholders there).
93+
if not call.analyzed.info:
94+
call.analyzed.info = newtype_class_info
95+
else:
96+
call.analyzed.info.bases = newtype_class_info.bases
97+
self.api.add_symbol(name, call.analyzed.info, s)
98+
newtype_class_info.line = s.line
99+
return True
69100

70101
def analyze_newtype_declaration(self,
71102
s: AssignmentStmt) -> Tuple[Optional[str], Optional[CallExpr]]:
@@ -76,13 +107,18 @@ def analyze_newtype_declaration(self,
76107
and isinstance(s.rvalue, CallExpr)
77108
and isinstance(s.rvalue.callee, RefExpr)
78109
and s.rvalue.callee.fullname == 'typing.NewType'):
79-
lvalue = s.lvalues[0]
80110
name = s.lvalues[0].name
81-
if not lvalue.is_inferred_def:
82-
if s.type:
83-
self.fail("Cannot declare the type of a NewType declaration", s)
84-
else:
85-
self.fail("Cannot redefine '%s' as a NewType" % name, s)
111+
112+
if s.type:
113+
self.fail("Cannot declare the type of a NewType declaration", s)
114+
115+
names = self.api.current_symbol_table()
116+
existing = names.get(name)
117+
# Give a better error message than generic "Name already defined",
118+
# like the old semantic analyzer does.
119+
if (existing and
120+
not isinstance(existing.node, PlaceholderNode) and not s.rvalue.analyzed):
121+
self.fail("Cannot redefine '%s' as a NewType" % name, s)
86122

87123
# This dummy NewTypeExpr marks the call as sufficiently analyzed; it will be
88124
# overwritten later with a fully complete NewTypeExpr if there are no other
@@ -91,12 +127,17 @@ def analyze_newtype_declaration(self,
91127

92128
return name, call
93129

94-
def check_newtype_args(self, name: str, call: CallExpr, context: Context) -> Optional[Type]:
130+
def check_newtype_args(self, name: str, call: CallExpr,
131+
context: Context) -> Tuple[Optional[Type], bool]:
132+
"""Ananlyze base type in NewType call.
133+
134+
Return a tuple (type, should defer).
135+
"""
95136
has_failed = False
96137
args, arg_kinds = call.args, call.arg_kinds
97138
if len(args) != 2 or arg_kinds[0] != ARG_POS or arg_kinds[1] != ARG_POS:
98139
self.fail("NewType(...) expects exactly two positional arguments", context)
99-
return None
140+
return None, False
100141

101142
# Check first argument
102143
if not isinstance(args[0], (StrExpr, BytesExpr, UnicodeExpr)):
@@ -113,19 +154,22 @@ def check_newtype_args(self, name: str, call: CallExpr, context: Context) -> Opt
113154
unanalyzed_type = expr_to_unanalyzed_type(args[1])
114155
except TypeTranslationError:
115156
self.fail(msg, context)
116-
return None
157+
return None, False
117158

118159
# We want to use our custom error message (see above), so we suppress
119160
# the default error message for invalid types here.
120161
old_type = self.api.anal_type(unanalyzed_type, report_invalid_types=False)
162+
should_defer = False
163+
if old_type is None or isinstance(old_type, PlaceholderType):
164+
should_defer = True
121165

122166
# The caller of this function assumes that if we return a Type, it's always
123167
# a valid one. So, we translate AnyTypes created from errors into None.
124168
if isinstance(old_type, AnyType) and old_type.is_from_error:
125169
self.fail(msg, context)
126-
return None
170+
return None, False
127171

128-
return None if has_failed else old_type
172+
return None if has_failed else old_type, should_defer
129173

130174
def build_newtype_typeinfo(self, name: str, old_type: Type, base_type: Instance) -> TypeInfo:
131175
info = self.api.basic_new_typeinfo(name, base_type)

mypy/newsemanal/semanal_shared.py

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

77
from mypy.nodes import (
88
Context, SymbolTableNode, MypyFile, ImportedName, FuncDef, Node, TypeInfo, Expression, GDEF,
9-
SymbolNode
9+
SymbolNode, SymbolTable
1010
)
1111
from mypy.util import correct_relative_import
1212
from mypy.types import Type, FunctionLike, Instance, TupleType
@@ -123,6 +123,14 @@ def add_symbol_table_node(self, name: str, stnode: SymbolTableNode) -> bool:
123123
"""Add node to the current symbol table."""
124124
raise NotImplementedError
125125

126+
@abstractmethod
127+
def current_symbol_table(self) -> SymbolTable:
128+
"""Get currently active symbol table.
129+
130+
May be module, class, or local namespace.
131+
"""
132+
raise NotImplementedError
133+
126134
@abstractmethod
127135
def add_symbol(self, name: str, node: SymbolNode, context: Optional[Context],
128136
module_public: bool = True, module_hidden: bool = False) -> bool:

mypy/newsemanal/typeanal.py

Lines changed: 3 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -75,48 +75,6 @@ def analyze_type_alias(node: Expression,
7575
full names of type aliases it depends on (directly or indirectly).
7676
Return None otherwise. 'node' must have been semantically analyzed.
7777
"""
78-
# Quickly return None if the expression doesn't look like a type. Note
79-
# that we don't support straight string literals as type aliases
80-
# (only string literals within index expressions).
81-
if isinstance(node, RefExpr):
82-
# Note that this misses the case where someone tried to use a
83-
# class-referenced type variable as a type alias. It's easier to catch
84-
# that one in checkmember.py
85-
if isinstance(node.node, TypeVarExpr):
86-
api.fail('Type variable "{}" is invalid as target for type alias'.format(
87-
node.fullname), node)
88-
return None
89-
if not (isinstance(node.node, TypeInfo) or
90-
node.fullname in ('typing.Any', 'typing.Tuple', 'typing.Callable') or
91-
isinstance(node.node, TypeAlias)):
92-
return None
93-
elif isinstance(node, IndexExpr):
94-
base = node.base
95-
if isinstance(base, RefExpr):
96-
if not (isinstance(base.node, TypeInfo) or
97-
base.fullname in type_constructors or
98-
isinstance(base.node, TypeAlias)):
99-
return None
100-
# Enums can't be generic, and without this check we may incorrectly interpret indexing
101-
# an Enum class as creating a type alias.
102-
if isinstance(base.node, TypeInfo) and base.node.is_enum:
103-
return None
104-
else:
105-
return None
106-
elif isinstance(node, CallExpr):
107-
if (isinstance(node.callee, NameExpr) and len(node.args) == 1 and
108-
isinstance(node.args[0], NameExpr)):
109-
call = api.lookup_qualified(node.callee.name, node.callee)
110-
arg = api.lookup_qualified(node.args[0].name, node.args[0])
111-
if (call is not None and call.node and call.node.fullname() == 'builtins.type' and
112-
arg is not None and arg.node and arg.node.fullname() == 'builtins.None'):
113-
return NoneTyp(), set()
114-
return None
115-
return None
116-
else:
117-
return None
118-
119-
# It's a type alias (though it may be an invalid one).
12078
try:
12179
type = expr_to_unanalyzed_type(node)
12280
except TypeTranslationError:
@@ -414,14 +372,10 @@ def analyze_unbound_type_without_type_info(self, t: UnboundType, sym: SymbolTabl
414372
(not self.tvar_scope or self.tvar_scope.get_binding(sym) is None))
415373
if self.allow_unbound_tvars and unbound_tvar and not self.third_pass:
416374
return t
375+
417376
# None of the above options worked, we give up.
418-
# NOTE: 'final_iteration' is iteration when we hit the maximum number of iterations limit.
419-
if unbound_tvar or self.api.final_iteration:
420-
# TODO: This is problematic, since we will have to wait until the maximum number
421-
# of iterations to report an invalid type.
422-
self.fail('Invalid type "{}"'.format(name), t)
423-
else:
424-
self.api.defer()
377+
self.fail('Invalid type "{}"'.format(name), t)
378+
425379
if self.third_pass and isinstance(sym.node, TypeVarExpr):
426380
self.note_func("Forward references to type variables are prohibited", t)
427381
return AnyType(TypeOfAny.from_error)

mypy/nodes.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1418,11 +1418,13 @@ class NameExpr(RefExpr):
14181418
This refers to a local name, global name or a module.
14191419
"""
14201420

1421-
__slots__ = ('name',)
1421+
__slots__ = ('name', 'is_special_form')
14221422

14231423
def __init__(self, name: str) -> None:
14241424
super().__init__()
14251425
self.name = name # Name referred to (may be qualified)
1426+
# Is this a l.h.s. of a special form assignment like typed dict or type variable?
1427+
self.is_special_form = False
14261428

14271429
def accept(self, visitor: ExpressionVisitor[T]) -> T:
14281430
return visitor.visit_name_expr(self)

mypy/strconv.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -345,7 +345,9 @@ def visit_star_expr(self, o: 'mypy.nodes.StarExpr') -> str:
345345
return self.dump([o.expr], o)
346346

347347
def visit_name_expr(self, o: 'mypy.nodes.NameExpr') -> str:
348-
pretty = self.pretty_name(o.name, o.kind, o.fullname, o.is_inferred_def, o.node)
348+
pretty = self.pretty_name(o.name, o.kind, o.fullname,
349+
o.is_inferred_def or o.is_special_form,
350+
o.node)
349351
if isinstance(o.node, mypy.nodes.Var) and o.node.is_final:
350352
pretty += ' = {}'.format(o.node.final_value)
351353
return short_type(o) + '(' + pretty + ')'

mypy/test/hacks.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,8 @@
1515
'check-incremental.test',
1616
'check-literal.test',
1717
'check-modules.test',
18-
'check-newtype.test',
1918
'check-overloading.test',
2019
'check-python2.test',
21-
'check-semanal-error.test',
2220
'check-statements.test',
2321
'check-unions.test',
2422
'check-unreachable-code.test',

0 commit comments

Comments
 (0)