Skip to content

Commit ae548ef

Browse files
committed
Turn DeletedType into a status in the type binder Deleted
This got a little bit more complicated than expected, and might be considered optional. I like having all the logic about deleted variables in one place, though.
1 parent 53ad5cf commit ae548ef

File tree

6 files changed

+93
-49
lines changed

6 files changed

+93
-49
lines changed

mypy/binder.py

Lines changed: 66 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import (Dict, List, Set, Iterator)
1+
from typing import (Dict, List, Set, Iterator, Union, cast)
22
from contextlib import contextmanager
33

44
from mypy.types import Type, AnyType, PartialType
@@ -9,7 +9,22 @@
99
from mypy.sametypes import is_same_type
1010

1111

12-
class Frame(Dict[Key, Type]):
12+
class Deleted:
13+
"""Frame entry representing a deleted variable."""
14+
15+
source = '' # May be None; name that generated this value
16+
17+
def __init__(self, source: str, as_lvalue: bool = False) -> None:
18+
self.source = source
19+
self.as_lvalue = as_lvalue
20+
21+
22+
# What's known about an expression at a specific moment:
23+
# either its current type, or that it has been deleted.
24+
Status = Union[Type, Deleted]
25+
26+
27+
class Frame(Dict[Key, Status]):
1328
"""A Frame represents a specific point in the execution of a program.
1429
It carries information about the current types of expressions at
1530
that point, arising either from assignments to those expressions
@@ -64,7 +79,7 @@ def __init__(self) -> None:
6479

6580
# Maps expr.literal_hash] to get_declaration(expr)
6681
# for every expr stored in the binder
67-
self.declarations = Frame()
82+
self.declarations = {} # type: Dict[Key, Type]
6883
# Set of other keys to invalidate if a key is changed, e.g. x -> {x.a, x[0]}
6984
# Whenever a new key (e.g. x.a.b) is added, we update this
7085
self.dependencies = {} # type: Dict[Key, Set[Key]]
@@ -92,18 +107,18 @@ def push_frame(self) -> Frame:
92107
self.options_on_return.append([])
93108
return f
94109

95-
def _push(self, key: Key, type: Type, index: int=-1) -> None:
110+
def _push(self, key: Key, type: Status, index: int=-1) -> None:
96111
self.frames[index][key] = type
97112

98-
def _get(self, key: Key, index: int=-1) -> Type:
113+
def _get(self, key: Key, index: int=-1) -> Status:
99114
if index < 0:
100115
index += len(self.frames)
101116
for i in range(index, -1, -1):
102117
if key in self.frames[i]:
103118
return self.frames[i][key]
104119
return None
105120

106-
def push(self, expr: Node, typ: Type) -> None:
121+
def push(self, expr: Node, typ: Status) -> None:
107122
if not expr.literal:
108123
return
109124
key = expr.literal_hash
@@ -112,12 +127,23 @@ def push(self, expr: Node, typ: Type) -> None:
112127
self._add_dependencies(key)
113128
self._push(key, typ)
114129

130+
def not_deleted(self, expr: Node) -> None:
131+
if not expr.literal:
132+
return
133+
if isinstance(self.get(expr), Deleted):
134+
self.push(expr, self.get_declaration(expr))
135+
115136
def unreachable(self) -> None:
116137
self.frames[-1].unreachable = True
117138

118-
def get(self, expr: Node) -> Type:
139+
def get(self, expr: Node) -> Status:
119140
return self._get(expr.literal_hash)
120141

142+
def get_lvalue(self, expr: Node) -> Status:
143+
if expr.literal:
144+
return self.declarations.get(expr.literal_hash)
145+
return None
146+
121147
def is_unreachable(self) -> bool:
122148
# TODO: Copy the value of unreachable into new frames to avoid
123149
# this traversal on every statement?
@@ -148,21 +174,28 @@ def update_from_options(self, frames: List[Frame]) -> bool:
148174
for key in keys:
149175
current_value = self._get(key)
150176
resulting_values = [f.get(key, current_value) for f in frames]
177+
151178
if any(x is None for x in resulting_values):
152179
# We didn't know anything about key before
153180
# (current_value must be None), and we still don't
154181
# know anything about key in at least one possible frame.
182+
# It might be deleted though.
183+
deleted_values = [x for x in resulting_values if isinstance(x, Deleted)]
184+
if deleted_values:
185+
self._push(key, deleted_values[0])
186+
changed = True
155187
continue
156188

157-
if isinstance(self.declarations.get(key), AnyType):
189+
declaration = self.declarations.get(key)
190+
if isinstance(declaration, AnyType):
158191
type = resulting_values[0]
159-
if not all(is_same_type(type, t) for t in resulting_values[1:]):
192+
if not all(is_same_status(type, t) for t in resulting_values[1:]):
160193
type = AnyType()
161194
else:
162195
type = resulting_values[0]
163196
for other in resulting_values[1:]:
164-
type = join_simple(self.declarations[key], type, other)
165-
if not is_same_type(type, current_value):
197+
type = join_status(declaration, type, other)
198+
if not is_same_status(type, current_value):
166199
self._push(key, type)
167200
changed = True
168201

@@ -199,7 +232,7 @@ def get_declaration(self, expr: Node) -> Type:
199232
return None
200233

201234
def assign_type(self, expr: Node,
202-
type: Type,
235+
type: Status,
203236
declared_type: Type,
204237
restrict_any: bool = False) -> None:
205238
if not expr.literal:
@@ -210,7 +243,7 @@ def assign_type(self, expr: Node,
210243
# Not sure why this happens. It seems to mainly happen in
211244
# member initialization.
212245
return
213-
if not is_subtype(type, declared_type):
246+
if isinstance(type, Type) and not is_subtype(type, declared_type):
214247
# Pretty sure this is only happens when there's a type error.
215248

216249
# Ideally this function wouldn't be called if the
@@ -247,13 +280,13 @@ def invalidate_dependencies(self, expr: Node) -> None:
247280
for dep in self.dependencies.get(expr.literal_hash, set()):
248281
self._cleanse_key(dep)
249282

250-
def most_recent_enclosing_type(self, expr: Node, type: Type) -> Type:
283+
def most_recent_enclosing_type(self, expr: Node, type: Status) -> Status:
251284
if isinstance(type, AnyType):
252285
return self.get_declaration(expr)
253286
key = expr.literal_hash
254-
enclosers = ([self.get_declaration(expr)] +
287+
enclosers = ([cast(Status, self.get_declaration(expr))] +
255288
[f[key] for f in self.frames
256-
if key in f and is_subtype(type, f[key])])
289+
if key in f])
257290
return enclosers[-1]
258291

259292
def allow_jump(self, index: int) -> None:
@@ -335,3 +368,20 @@ def top_frame_context(self) -> Iterator[Frame]:
335368
assert len(self.frames) == 1
336369
yield self.push_frame()
337370
self.pop_frame(True, 0)
371+
372+
373+
def join_status(declaration: Type, s: Status, t: Status) -> Status:
374+
if isinstance(s, Deleted):
375+
return s
376+
377+
if isinstance(t, Deleted):
378+
return t
379+
380+
return join_simple(declaration, s, t)
381+
382+
383+
def is_same_status(s: Status, t: Status) -> bool:
384+
if isinstance(s, Deleted) or isinstance(t, Deleted):
385+
return isinstance(s, Deleted) and isinstance(t, Deleted) and s.as_lvalue == t.as_lvalue
386+
387+
return is_same_type(s, t)

mypy/checker.py

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@
5252
from mypy.join import join_types
5353
from mypy.treetransform import TransformVisitor
5454
from mypy.meet import meet_simple, nearest_builtin_ancestor, is_overlapping_types
55-
from mypy.binder import ConditionalTypeBinder
55+
from mypy.binder import ConditionalTypeBinder, Deleted
5656

5757
from mypy import experiments
5858

@@ -972,6 +972,7 @@ def check_assignment(self, lvalue: Node, rvalue: Node, infer_lvalue_type: bool =
972972
infer_lvalue_type)
973973
else:
974974
lvalue_type, index_lvalue, inferred = self.check_lvalue(lvalue)
975+
self.binder.not_deleted(lvalue)
975976
if lvalue_type:
976977
if isinstance(lvalue_type, PartialType) and lvalue_type.type is None:
977978
# Try to infer a proper type for a variable with a partial None type.
@@ -1211,6 +1212,10 @@ def check_lvalue(self, lvalue: Node) -> Tuple[Type, IndexExpr, Var]:
12111212
self.store_type(lvalue, lvalue_type)
12121213
elif isinstance(lvalue, NameExpr):
12131214
lvalue_type = self.expr_checker.analyze_ref_expr(lvalue, lvalue=True)
1215+
binder_type = self.binder.get(lvalue)
1216+
if isinstance(binder_type, Deleted) and binder_type.as_lvalue:
1217+
self.msg.deleted_as_lvalue(binder_type.source, lvalue)
1218+
lvalue_type = AnyType() # Don't produce a subsequent type error also.
12141219
self.store_type(lvalue, lvalue_type)
12151220
elif isinstance(lvalue, TupleExpr) or isinstance(lvalue, ListExpr):
12161221
lv = cast(Union[TupleExpr, ListExpr], lvalue)
@@ -1246,8 +1251,6 @@ def infer_variable_type(self, name: Var, lvalue: Node,
12461251
elif isinstance(init_type, Void):
12471252
self.check_not_void(init_type, context)
12481253
self.set_inference_error_fallback_type(name, lvalue, init_type, context)
1249-
elif isinstance(init_type, DeletedType):
1250-
self.msg.deleted_as_rvalue(init_type, context)
12511254
elif not is_valid_inferred_type(init_type):
12521255
# We cannot use the type of the initialization expression for full type
12531256
# inference (it's not specific enough), but we might be able to give
@@ -1312,6 +1315,10 @@ def narrow_type_from_binder(self, expr: Node, known_type: Type) -> Type:
13121315
if expr.literal >= LITERAL_TYPE:
13131316
restriction = self.binder.get(expr)
13141317
if restriction:
1318+
if isinstance(restriction, Deleted):
1319+
self.msg.deleted_as_rvalue(restriction.source, expr)
1320+
return AnyType() # Don't produce a subsequent type error also.
1321+
13151322
ans = meet_simple(known_type, restriction)
13161323
return ans
13171324
return known_type
@@ -1326,16 +1333,11 @@ def check_simple_assignment(self, lvalue_type: Type, rvalue: Node,
13261333
return AnyType()
13271334
else:
13281335
rvalue_type = self.accept(rvalue, lvalue_type)
1329-
if isinstance(rvalue_type, DeletedType):
1330-
self.msg.deleted_as_rvalue(rvalue_type, context)
13311336
if self.typing_mode_weak():
13321337
return rvalue_type
1333-
if isinstance(lvalue_type, DeletedType):
1334-
self.msg.deleted_as_lvalue(lvalue_type, context)
1335-
else:
1336-
self.check_subtype(rvalue_type, lvalue_type, context, msg,
1337-
'{} has type'.format(rvalue_name),
1338-
'{} has type'.format(lvalue_name))
1338+
self.check_subtype(rvalue_type, lvalue_type, context, msg,
1339+
'{} has type'.format(rvalue_name),
1340+
'{} has type'.format(lvalue_name))
13391341
return rvalue_type
13401342

13411343
def check_indexed_assignment(self, lvalue: IndexExpr,
@@ -1597,6 +1599,7 @@ def visit_try_without_finally(self, s: TryStmt, try_frame: bool) -> None:
15971599
# causing assignment to set the variable's type.
15981600
s.vars[i].is_def = True
15991601
self.check_assignment(s.vars[i], self.temp_node(t, s.vars[i]))
1602+
self.binder.push(s.vars[i], t)
16001603
self.accept(s.handlers[i])
16011604
if s.vars[i]:
16021605
# Exception variables are deleted in python 3 but not python 2.
@@ -1611,9 +1614,10 @@ def visit_try_without_finally(self, s: TryStmt, try_frame: bool) -> None:
16111614
source = ('(exception variable "{}", which we do not '
16121615
'accept outside except: blocks even in '
16131616
'python 2)'.format(s.vars[i].name))
1614-
var = cast(Var, s.vars[i].node)
1615-
var.type = DeletedType(source=source)
1616-
self.binder.cleanse(s.vars[i])
1617+
self.binder.assign_type(s.vars[i],
1618+
Deleted(source=source, as_lvalue=True),
1619+
self.binder.get_declaration(s.vars[i]),
1620+
self.typing_mode_weak())
16171621
if s.else_body:
16181622
self.accept(s.else_body)
16191623

@@ -1711,7 +1715,7 @@ def flatten(t: Node) -> List[Node]:
17111715
for elt in flatten(s.expr):
17121716
if isinstance(elt, NameExpr):
17131717
self.binder.assign_type(elt,
1714-
DeletedType(source=elt.name),
1718+
Deleted(source=elt.name),
17151719
self.binder.get_declaration(elt),
17161720
self.typing_mode_weak())
17171721
return None

mypy/checkexpr.py

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -121,12 +121,7 @@ def analyze_var_ref(self, var: Var, context: Context) -> Type:
121121
# Implicit 'Any' type.
122122
return AnyType()
123123
else:
124-
# Look up local type of variable with type (inferred or explicit).
125-
val = self.chk.binder.get(var)
126-
if val is None:
127-
return var.type
128-
else:
129-
return val
124+
return var.type
130125

131126
def visit_call_expr(self, e: CallExpr) -> Type:
132127
"""Type check a call expression."""
@@ -691,8 +686,6 @@ def check_arg(self, caller_type: Type, original_caller_type: Type,
691686
"""Check the type of a single argument in a call."""
692687
if isinstance(caller_type, Void):
693688
messages.does_not_return_value(caller_type, context)
694-
elif isinstance(caller_type, DeletedType):
695-
messages.deleted_as_rvalue(caller_type, context)
696689
elif not is_subtype(caller_type, callee_type):
697690
messages.incompatible_argument(n, m, callee, original_caller_type,
698691
caller_kind, context)

mypy/checkmember.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -110,9 +110,6 @@ def analyze_member_access(name: str, typ: Type, node: Context, is_lvalue: bool,
110110
return analyze_member_access(name, typ.upper_bound, node, is_lvalue, is_super,
111111
builtin_type, not_ready_callback, msg,
112112
report_type=report_type)
113-
elif isinstance(typ, DeletedType):
114-
msg.deleted_as_rvalue(typ, node)
115-
return AnyType()
116113
elif isinstance(typ, TypeType):
117114
# Similar to FunctionLike + is_type_obj() above.
118115
item = None

mypy/messages.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -562,24 +562,24 @@ def does_not_return_value(self, void_type: Type, context: Context) -> None:
562562
self.fail('{} does not return a value'.format(
563563
capitalize((cast(Void, void_type)).source)), context)
564564

565-
def deleted_as_rvalue(self, typ: DeletedType, context: Context) -> None:
565+
def deleted_as_rvalue(self, source: str, context: Context) -> None:
566566
"""Report an error about using an deleted type as an rvalue."""
567-
if typ.source is None:
567+
if source is None:
568568
s = ""
569569
else:
570-
s = " '{}'".format(typ.source)
570+
s = " '{}'".format(source)
571571
self.fail('Trying to read deleted variable{}'.format(s), context)
572572

573-
def deleted_as_lvalue(self, typ: DeletedType, context: Context) -> None:
573+
def deleted_as_lvalue(self, source: str, context: Context) -> None:
574574
"""Report an error about using an deleted type as an lvalue.
575575
576576
Currently, this only occurs when trying to assign to an
577577
exception variable outside the local except: blocks.
578578
"""
579-
if typ.source is None:
579+
if source is None:
580580
s = ""
581581
else:
582-
s = " '{}'".format(typ.source)
582+
s = " '{}'".format(source)
583583
self.fail('Assignment to variable{} outside except: block'.format(s), context)
584584

585585
def no_variant_matches_arguments(self, overload: Overloaded, arg_types: List[Type],

test-data/unit/check-statements.test

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -656,7 +656,7 @@ e = 1
656656
try: pass
657657
except E1 as e: pass
658658
e = 1 # E: Assignment to variable 'e' outside except: block
659-
e = E1() # E: Assignment to variable 'e' outside except: block
659+
e = E1()
660660
[builtins fixtures/exception.py]
661661

662662
[case testArbitraryExpressionAsExceptionType]

0 commit comments

Comments
 (0)