Skip to content

Commit f03ad70

Browse files
authored
Copy semantic analyzer module to mypy.newsemanal (#6234)
Copy the existing semantic analyzer to a new package with minimal changes (only update imports). The new package is not used anywhere yet. Once the new semantic analyzer is good enough, the plan is to replace the existing semantic analyzer with it and remove the `mypy.newsemanal` package.
1 parent d2f1d34 commit f03ad70

File tree

9 files changed

+7340
-0
lines changed

9 files changed

+7340
-0
lines changed

mypy/newsemanal/__init__.py

Whitespace-only changes.

mypy/newsemanal/semanal.py

Lines changed: 3889 additions & 0 deletions
Large diffs are not rendered by default.

mypy/newsemanal/semanal_enum.py

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
"""Semantic analysis of call-based Enum definitions.
2+
3+
This is conceptually part of mypy.semanal (semantic analyzer pass 2).
4+
"""
5+
6+
from typing import List, Tuple, Optional, Union, cast
7+
8+
from mypy.nodes import (
9+
Expression, Context, TypeInfo, AssignmentStmt, NameExpr, CallExpr, RefExpr, StrExpr,
10+
UnicodeExpr, TupleExpr, ListExpr, DictExpr, Var, SymbolTableNode, GDEF, MDEF, ARG_POS,
11+
EnumCallExpr
12+
)
13+
from mypy.semanal_shared import SemanticAnalyzerInterface
14+
from mypy.options import Options
15+
16+
17+
class EnumCallAnalyzer:
18+
def __init__(self, options: Options, api: SemanticAnalyzerInterface) -> None:
19+
self.options = options
20+
self.api = api
21+
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."""
24+
if len(s.lvalues) != 1 or not isinstance(s.lvalues[0], NameExpr):
25+
return
26+
lvalue = s.lvalues[0]
27+
name = lvalue.name
28+
enum_call = self.check_enum_call(s.rvalue, name, is_func_scope)
29+
if enum_call is None:
30+
return
31+
# 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+
37+
def check_enum_call(self,
38+
node: Expression,
39+
var_name: str,
40+
is_func_scope: bool) -> Optional[TypeInfo]:
41+
"""Check if a call defines an Enum.
42+
43+
Example:
44+
45+
A = enum.Enum('A', 'foo bar')
46+
47+
is equivalent to:
48+
49+
class A(enum.Enum):
50+
foo = 1
51+
bar = 2
52+
"""
53+
if not isinstance(node, CallExpr):
54+
return None
55+
call = node
56+
callee = call.callee
57+
if not isinstance(callee, RefExpr):
58+
return None
59+
fullname = callee.fullname
60+
if fullname not in ('enum.Enum', 'enum.IntEnum', 'enum.Flag', 'enum.IntFlag'):
61+
return None
62+
items, values, ok = self.parse_enum_call_args(call, fullname.split('.')[-1])
63+
if not ok:
64+
# 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)
75+
call.analyzed = EnumCallExpr(info, items, values)
76+
call.analyzed.set_line(call.line, call.column)
77+
return info
78+
79+
def build_enum_call_typeinfo(self, name: str, items: List[str], fullname: str) -> TypeInfo:
80+
base = self.api.named_type_or_none(fullname)
81+
assert base is not None
82+
info = self.api.basic_new_typeinfo(name, base)
83+
info.metaclass_type = info.calculate_metaclass_type()
84+
info.is_enum = True
85+
for item in items:
86+
var = Var(item)
87+
var.info = info
88+
var.is_property = True
89+
var._fullname = '{}.{}'.format(self.api.qualified_name(name), item)
90+
info.names[item] = SymbolTableNode(MDEF, var)
91+
return info
92+
93+
def parse_enum_call_args(self, call: CallExpr,
94+
class_name: str) -> Tuple[List[str],
95+
List[Optional[Expression]], bool]:
96+
args = call.args
97+
if len(args) < 2:
98+
return self.fail_enum_call_arg("Too few arguments for %s()" % class_name, call)
99+
if len(args) > 2:
100+
return self.fail_enum_call_arg("Too many arguments for %s()" % class_name, call)
101+
if call.arg_kinds != [ARG_POS, ARG_POS]:
102+
return self.fail_enum_call_arg("Unexpected arguments to %s()" % class_name, call)
103+
if not isinstance(args[0], (StrExpr, UnicodeExpr)):
104+
return self.fail_enum_call_arg(
105+
"%s() expects a string literal as the first argument" % class_name, call)
106+
items = []
107+
values = [] # type: List[Optional[Expression]]
108+
if isinstance(args[1], (StrExpr, UnicodeExpr)):
109+
fields = args[1].value
110+
for field in fields.replace(',', ' ').split():
111+
items.append(field)
112+
elif isinstance(args[1], (TupleExpr, ListExpr)):
113+
seq_items = args[1].items
114+
if all(isinstance(seq_item, (StrExpr, UnicodeExpr)) for seq_item in seq_items):
115+
items = [cast(Union[StrExpr, UnicodeExpr], seq_item).value
116+
for seq_item in seq_items]
117+
elif all(isinstance(seq_item, (TupleExpr, ListExpr))
118+
and len(seq_item.items) == 2
119+
and isinstance(seq_item.items[0], (StrExpr, UnicodeExpr))
120+
for seq_item in seq_items):
121+
for seq_item in seq_items:
122+
assert isinstance(seq_item, (TupleExpr, ListExpr))
123+
name, value = seq_item.items
124+
assert isinstance(name, (StrExpr, UnicodeExpr))
125+
items.append(name.value)
126+
values.append(value)
127+
else:
128+
return self.fail_enum_call_arg(
129+
"%s() with tuple or list expects strings or (name, value) pairs" %
130+
class_name,
131+
call)
132+
elif isinstance(args[1], DictExpr):
133+
for key, value in args[1].items:
134+
if not isinstance(key, (StrExpr, UnicodeExpr)):
135+
return self.fail_enum_call_arg(
136+
"%s() with dict literal requires string literals" % class_name, call)
137+
items.append(key.value)
138+
values.append(value)
139+
else:
140+
# TODO: Allow dict(x=1, y=2) as a substitute for {'x': 1, 'y': 2}?
141+
return self.fail_enum_call_arg(
142+
"%s() expects a string, tuple, list or dict literal as the second argument" %
143+
class_name,
144+
call)
145+
if len(items) == 0:
146+
return self.fail_enum_call_arg("%s() needs at least one item" % class_name, call)
147+
if not values:
148+
values = [None] * len(items)
149+
assert len(items) == len(values)
150+
return items, values, True
151+
152+
def fail_enum_call_arg(self, message: str,
153+
context: Context) -> Tuple[List[str],
154+
List[Optional[Expression]], bool]:
155+
self.fail(message, context)
156+
return [], [], False
157+
158+
# Helpers
159+
160+
def fail(self, msg: str, ctx: Context) -> None:
161+
self.api.fail(msg, ctx)

0 commit comments

Comments
 (0)