Skip to content

Commit 3b92e79

Browse files
rwbartonJukkaL
authored andcommitted
Add --warn-no-return, and major binder refactoring (#1748)
* Replace the binder's breaking_out by an 'unreachable' property of Frame Also add some comments to the binder. * Minor type cleanup in the binder and explanation of Key This also fixes a bug where the index expression x['m'] was being treated by the binder as equivalent to the member expression x.m. * Add warn-no-return flag to give a note on missing return statements * Learn that loop condition is False on exit from while loop * Use bool() in tests when we want an unspecified bool in a conditional * Handle 'while True', 'assert False' * Reduce duplication with find_isinstance_check and binder.push The resulting simplification of visit_if_expr revealed a bug in the binder: if a frame was marked as unreachable, but then a new frame context was begun immediately, the new frame was not marked as unreachable, and a block visited inside the new frame would be type checked. For now we check all frames for reachability; probably there is a more efficient way to do this. This commit also cleans up check_boolean_op slightly. * Process try statements in top-to-bottom order Fixes #1289 (again). * Fix return type of binder.unreachable * Skip docstring when determining whether function body is trivial * Remove None return values from check_return_stmt * Explain why two try frames are needed for a finally clause; add a test * Decouple checker and binder by adding more options to frame_context No more poking around by the checker in loop_frames and try_frames.
1 parent 7533533 commit 3b92e79

19 files changed

+624
-230
lines changed

mypy/binder.py

Lines changed: 111 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,28 @@
1-
from typing import (Any, Dict, List, Set, Iterator, Union)
1+
from typing import (Dict, List, Set, Iterator, Union)
22
from contextlib import contextmanager
33

44
from mypy.types import Type, AnyType, PartialType
5-
from mypy.nodes import (Node, Expression, Var, RefExpr, SymbolTableNode)
5+
from mypy.nodes import (Key, Node, Expression, Var, RefExpr, SymbolTableNode)
66

77
from mypy.subtypes import is_subtype
88
from mypy.join import join_simple
99
from mypy.sametypes import is_same_type
1010

1111

12-
class Frame(Dict[Any, Type]):
13-
pass
12+
class Frame(Dict[Key, Type]):
13+
"""A Frame represents a specific point in the execution of a program.
14+
It carries information about the current types of expressions at
15+
that point, arising either from assignments to those expressions
16+
or the result of isinstance checks. It also records whether it is
17+
possible to reach that point at all.
1418
19+
This information is not copied into a new Frame when it is pushed
20+
onto the stack, so a given Frame only has information about types
21+
that were assigned in that frame.
22+
"""
1523

16-
class Key(AnyType):
17-
pass
24+
def __init__(self) -> None:
25+
self.unreachable = False
1826

1927

2028
class ConditionalTypeBinder:
@@ -39,13 +47,19 @@ class A:
3947
"""
4048

4149
def __init__(self) -> None:
42-
# The set of frames currently used. These map
50+
# The stack of frames currently used. These map
4351
# expr.literal_hash -- literals like 'foo.bar' --
44-
# to types.
52+
# to types. The last element of this list is the
53+
# top-most, current frame. Each earlier element
54+
# records the state as of when that frame was last
55+
# on top of the stack.
4556
self.frames = [Frame()]
4657

4758
# For frames higher in the stack, we record the set of
48-
# Frames that can escape there
59+
# Frames that can escape there, either by falling off
60+
# the end of the frame or by a loop control construct
61+
# or raised exception. The last element of self.frames
62+
# has no corresponding element in this list.
4963
self.options_on_return = [] # type: List[List[Frame]]
5064

5165
# Maps expr.literal_hash] to get_declaration(expr)
@@ -55,27 +69,20 @@ def __init__(self) -> None:
5569
# Whenever a new key (e.g. x.a.b) is added, we update this
5670
self.dependencies = {} # type: Dict[Key, Set[Key]]
5771

58-
# breaking_out is set to True on return/break/continue/raise
59-
# It is cleared on pop_frame() and placed in last_pop_breaking_out
60-
# Lines of code after breaking_out = True are unreachable and not
61-
# typechecked.
62-
self.breaking_out = False
63-
6472
# Whether the last pop changed the newly top frame on exit
6573
self.last_pop_changed = False
66-
# Whether the last pop was necessarily breaking out, and couldn't fall through
67-
self.last_pop_breaking_out = False
6874

6975
self.try_frames = set() # type: Set[int]
70-
self.loop_frames = [] # type: List[int]
76+
self.break_frames = [] # type: List[int]
77+
self.continue_frames = [] # type: List[int]
7178

7279
def _add_dependencies(self, key: Key, value: Key = None) -> None:
7380
if value is None:
7481
value = key
7582
else:
7683
self.dependencies.setdefault(key, set()).add(value)
77-
if isinstance(key, tuple):
78-
for elt in key:
84+
for elt in key:
85+
if isinstance(elt, Key):
7986
self._add_dependencies(elt, value)
8087

8188
def push_frame(self) -> Frame:
@@ -105,9 +112,17 @@ def push(self, node: Node, typ: Type) -> None:
105112
self._add_dependencies(key)
106113
self._push(key, typ)
107114

115+
def unreachable(self) -> None:
116+
self.frames[-1].unreachable = True
117+
108118
def get(self, expr: Union[Expression, Var]) -> Type:
109119
return self._get(expr.literal_hash)
110120

121+
def is_unreachable(self) -> bool:
122+
# TODO: Copy the value of unreachable into new frames to avoid
123+
# this traversal on every statement?
124+
return any(f.unreachable for f in self.frames)
125+
111126
def cleanse(self, expr: Expression) -> None:
112127
"""Remove all references to a Node from the binder."""
113128
self._cleanse_key(expr.literal_hash)
@@ -126,13 +141,17 @@ def update_from_options(self, frames: List[Frame]) -> bool:
126141
options are the same.
127142
"""
128143

144+
frames = [f for f in frames if not f.unreachable]
129145
changed = False
130146
keys = set(key for f in frames for key in f)
131147

132148
for key in keys:
133149
current_value = self._get(key)
134150
resulting_values = [f.get(key, current_value) for f in frames]
135151
if any(x is None for x in resulting_values):
152+
# We didn't know anything about key before
153+
# (current_value must be None), and we still don't
154+
# know anything about key in at least one possible frame.
136155
continue
137156

138157
if isinstance(self.declarations.get(key), AnyType):
@@ -147,27 +166,32 @@ def update_from_options(self, frames: List[Frame]) -> bool:
147166
self._push(key, type)
148167
changed = True
149168

169+
self.frames[-1].unreachable = not frames
170+
150171
return changed
151172

152-
def pop_frame(self, fall_through: int = 0) -> Frame:
173+
def pop_frame(self, can_skip: bool, fall_through: int) -> Frame:
153174
"""Pop a frame and return it.
154175
155176
See frame_context() for documentation of fall_through.
156177
"""
157-
if fall_through and not self.breaking_out:
178+
179+
if fall_through > 0:
158180
self.allow_jump(-fall_through)
159181

160182
result = self.frames.pop()
161183
options = self.options_on_return.pop()
162184

185+
if can_skip:
186+
options.insert(0, self.frames[-1])
187+
163188
self.last_pop_changed = self.update_from_options(options)
164-
self.last_pop_breaking_out = self.breaking_out
165189

166190
return result
167191

168-
def get_declaration(self, node: Node) -> Type:
169-
if isinstance(node, (RefExpr, SymbolTableNode)) and isinstance(node.node, Var):
170-
type = node.node.type
192+
def get_declaration(self, expr: Node) -> Type:
193+
if isinstance(expr, RefExpr) and isinstance(expr.node, Var):
194+
type = expr.node.type
171195
if isinstance(type, PartialType):
172196
return None
173197
return type
@@ -239,25 +263,74 @@ def allow_jump(self, index: int) -> None:
239263
frame = Frame()
240264
for f in self.frames[index + 1:]:
241265
frame.update(f)
266+
if f.unreachable:
267+
frame.unreachable = True
242268
self.options_on_return[index].append(frame)
243269

244-
def push_loop_frame(self) -> None:
245-
self.loop_frames.append(len(self.frames) - 1)
270+
def handle_break(self) -> None:
271+
self.allow_jump(self.break_frames[-1])
272+
self.unreachable()
246273

247-
def pop_loop_frame(self) -> None:
248-
self.loop_frames.pop()
274+
def handle_continue(self) -> None:
275+
self.allow_jump(self.continue_frames[-1])
276+
self.unreachable()
249277

250278
@contextmanager
251-
def frame_context(self, fall_through: int = 0) -> Iterator[Frame]:
279+
def frame_context(self, *, can_skip: bool, fall_through: int = 1,
280+
break_frame: int = 0, continue_frame: int = 0,
281+
try_frame: bool = False) -> Iterator[Frame]:
252282
"""Return a context manager that pushes/pops frames on enter/exit.
253283
254-
If fall_through > 0, then it will allow the frame to escape to
255-
its ancestor `fall_through` levels higher.
284+
If can_skip is True, control flow is allowed to bypass the
285+
newly-created frame.
286+
287+
If fall_through > 0, then it will allow control flow that
288+
falls off the end of the frame to escape to its ancestor
289+
`fall_through` levels higher. Otherwise control flow ends
290+
at the end of the frame.
291+
292+
If break_frame > 0, then 'break' statements within this frame
293+
will jump out to the frame break_frame levels higher than the
294+
frame created by this call to frame_context. Similarly for
295+
continue_frame and 'continue' statements.
296+
297+
If try_frame is true, then execution is allowed to jump at any
298+
point within the newly created frame (or its descendents) to
299+
its parent (i.e., to the frame that was on top before this
300+
call to frame_context).
256301
257-
A simple 'with binder.frame_context(): pass' will change the
258-
last_pop_* flags but nothing else.
302+
After the context manager exits, self.last_pop_changed indicates
303+
whether any types changed in the newly-topmost frame as a result
304+
of popping this frame.
305+
"""
306+
assert len(self.frames) > 1
307+
308+
if break_frame:
309+
self.break_frames.append(len(self.frames) - break_frame)
310+
if continue_frame:
311+
self.continue_frames.append(len(self.frames) - continue_frame)
312+
if try_frame:
313+
self.try_frames.add(len(self.frames) - 1)
314+
315+
new_frame = self.push_frame()
316+
if try_frame:
317+
# An exception may occur immediately
318+
self.allow_jump(-1)
319+
yield new_frame
320+
self.pop_frame(can_skip, fall_through)
321+
322+
if break_frame:
323+
self.break_frames.pop()
324+
if continue_frame:
325+
self.continue_frames.pop()
326+
if try_frame:
327+
self.try_frames.remove(len(self.frames) - 1)
328+
329+
@contextmanager
330+
def top_frame_context(self) -> Iterator[Frame]:
331+
"""A variant of frame_context for use at the top level of
332+
a namespace (module, function, or class).
259333
"""
260-
was_breaking_out = self.breaking_out
334+
assert len(self.frames) == 1
261335
yield self.push_frame()
262-
self.pop_frame(fall_through)
263-
self.breaking_out = was_breaking_out
336+
self.pop_frame(True, 0)

0 commit comments

Comments
 (0)