441 lines
17 KiB
Python
441 lines
17 KiB
Python
from contextlib import contextmanager
|
|
from collections import defaultdict
|
|
|
|
from typing import Dict, List, Set, Iterator, Union, Optional, Tuple, cast
|
|
from typing_extensions import DefaultDict, TypeAlias as _TypeAlias
|
|
|
|
from mypy.types import (
|
|
Type, AnyType, PartialType, UnionType, TypeOfAny, NoneType, get_proper_type
|
|
)
|
|
from mypy.subtypes import is_subtype
|
|
from mypy.join import join_simple
|
|
from mypy.sametypes import is_same_type
|
|
from mypy.erasetype import remove_instance_last_known_values
|
|
from mypy.nodes import Expression, Var, RefExpr
|
|
from mypy.literals import Key, literal, literal_hash, subkeys
|
|
from mypy.nodes import IndexExpr, MemberExpr, AssignmentExpr, NameExpr
|
|
|
|
|
|
BindableExpression: _TypeAlias = Union[IndexExpr, MemberExpr, AssignmentExpr, NameExpr]
|
|
|
|
|
|
class Frame:
|
|
"""A Frame represents a specific point in the execution of a program.
|
|
It carries information about the current types of expressions at
|
|
that point, arising either from assignments to those expressions
|
|
or the result of isinstance checks. It also records whether it is
|
|
possible to reach that point at all.
|
|
|
|
This information is not copied into a new Frame when it is pushed
|
|
onto the stack, so a given Frame only has information about types
|
|
that were assigned in that frame.
|
|
"""
|
|
|
|
def __init__(self, id: int, conditional_frame: bool = False) -> None:
|
|
self.id = id
|
|
self.types: Dict[Key, Type] = {}
|
|
self.unreachable = False
|
|
self.conditional_frame = conditional_frame
|
|
|
|
# Should be set only if we're entering a frame where it's not
|
|
# possible to accurately determine whether or not contained
|
|
# statements will be unreachable or not.
|
|
#
|
|
# Long-term, we should improve mypy to the point where we no longer
|
|
# need this field.
|
|
self.suppress_unreachable_warnings = False
|
|
|
|
|
|
Assigns = DefaultDict[Expression, List[Tuple[Type, Optional[Type]]]]
|
|
|
|
|
|
class ConditionalTypeBinder:
|
|
"""Keep track of conditional types of variables.
|
|
|
|
NB: Variables are tracked by literal expression, so it is possible
|
|
to confuse the binder; for example,
|
|
|
|
```
|
|
class A:
|
|
a = None # type: Union[int, str]
|
|
x = A()
|
|
lst = [x]
|
|
reveal_type(x.a) # Union[int, str]
|
|
x.a = 1
|
|
reveal_type(x.a) # int
|
|
reveal_type(lst[0].a) # Union[int, str]
|
|
lst[0].a = 'a'
|
|
reveal_type(x.a) # int
|
|
reveal_type(lst[0].a) # str
|
|
```
|
|
"""
|
|
# Stored assignments for situations with tuple/list lvalue and rvalue of union type.
|
|
# This maps an expression to a list of bound types for every item in the union type.
|
|
type_assignments: Optional[Assigns] = None
|
|
|
|
def __init__(self) -> None:
|
|
self.next_id = 1
|
|
|
|
# The stack of frames currently used. These map
|
|
# literal_hash(expr) -- literals like 'foo.bar' --
|
|
# to types. The last element of this list is the
|
|
# top-most, current frame. Each earlier element
|
|
# records the state as of when that frame was last
|
|
# on top of the stack.
|
|
self.frames = [Frame(self._get_id())]
|
|
|
|
# For frames higher in the stack, we record the set of
|
|
# Frames that can escape there, either by falling off
|
|
# the end of the frame or by a loop control construct
|
|
# or raised exception. The last element of self.frames
|
|
# has no corresponding element in this list.
|
|
self.options_on_return: List[List[Frame]] = []
|
|
|
|
# Maps literal_hash(expr) to get_declaration(expr)
|
|
# for every expr stored in the binder
|
|
self.declarations: Dict[Key, Optional[Type]] = {}
|
|
# Set of other keys to invalidate if a key is changed, e.g. x -> {x.a, x[0]}
|
|
# Whenever a new key (e.g. x.a.b) is added, we update this
|
|
self.dependencies: Dict[Key, Set[Key]] = {}
|
|
|
|
# Whether the last pop changed the newly top frame on exit
|
|
self.last_pop_changed = False
|
|
|
|
self.try_frames: Set[int] = set()
|
|
self.break_frames: List[int] = []
|
|
self.continue_frames: List[int] = []
|
|
|
|
def _get_id(self) -> int:
|
|
self.next_id += 1
|
|
return self.next_id
|
|
|
|
def _add_dependencies(self, key: Key, value: Optional[Key] = None) -> None:
|
|
if value is None:
|
|
value = key
|
|
else:
|
|
self.dependencies.setdefault(key, set()).add(value)
|
|
for elt in subkeys(key):
|
|
self._add_dependencies(elt, value)
|
|
|
|
def push_frame(self, conditional_frame: bool = False) -> Frame:
|
|
"""Push a new frame into the binder."""
|
|
f = Frame(self._get_id(), conditional_frame)
|
|
self.frames.append(f)
|
|
self.options_on_return.append([])
|
|
return f
|
|
|
|
def _put(self, key: Key, type: Type, index: int = -1) -> None:
|
|
self.frames[index].types[key] = type
|
|
|
|
def _get(self, key: Key, index: int = -1) -> Optional[Type]:
|
|
if index < 0:
|
|
index += len(self.frames)
|
|
for i in range(index, -1, -1):
|
|
if key in self.frames[i].types:
|
|
return self.frames[i].types[key]
|
|
return None
|
|
|
|
def put(self, expr: Expression, typ: Type) -> None:
|
|
if not isinstance(expr, (IndexExpr, MemberExpr, AssignmentExpr, NameExpr)):
|
|
return
|
|
if not literal(expr):
|
|
return
|
|
key = literal_hash(expr)
|
|
assert key is not None, 'Internal error: binder tried to put non-literal'
|
|
if key not in self.declarations:
|
|
self.declarations[key] = get_declaration(expr)
|
|
self._add_dependencies(key)
|
|
self._put(key, typ)
|
|
|
|
def unreachable(self) -> None:
|
|
self.frames[-1].unreachable = True
|
|
|
|
def suppress_unreachable_warnings(self) -> None:
|
|
self.frames[-1].suppress_unreachable_warnings = True
|
|
|
|
def get(self, expr: Expression) -> Optional[Type]:
|
|
key = literal_hash(expr)
|
|
assert key is not None, 'Internal error: binder tried to get non-literal'
|
|
return self._get(key)
|
|
|
|
def is_unreachable(self) -> bool:
|
|
# TODO: Copy the value of unreachable into new frames to avoid
|
|
# this traversal on every statement?
|
|
return any(f.unreachable for f in self.frames)
|
|
|
|
def is_unreachable_warning_suppressed(self) -> bool:
|
|
# TODO: See todo in 'is_unreachable'
|
|
return any(f.suppress_unreachable_warnings for f in self.frames)
|
|
|
|
def cleanse(self, expr: Expression) -> None:
|
|
"""Remove all references to a Node from the binder."""
|
|
key = literal_hash(expr)
|
|
assert key is not None, 'Internal error: binder tried cleanse non-literal'
|
|
self._cleanse_key(key)
|
|
|
|
def _cleanse_key(self, key: Key) -> None:
|
|
"""Remove all references to a key from the binder."""
|
|
for frame in self.frames:
|
|
if key in frame.types:
|
|
del frame.types[key]
|
|
|
|
def update_from_options(self, frames: List[Frame]) -> bool:
|
|
"""Update the frame to reflect that each key will be updated
|
|
as in one of the frames. Return whether any item changes.
|
|
|
|
If a key is declared as AnyType, only update it if all the
|
|
options are the same.
|
|
"""
|
|
|
|
frames = [f for f in frames if not f.unreachable]
|
|
changed = False
|
|
keys = {key for f in frames for key in f.types}
|
|
|
|
for key in keys:
|
|
current_value = self._get(key)
|
|
resulting_values = [f.types.get(key, current_value) for f in frames]
|
|
if any(x is None for x in resulting_values):
|
|
# We didn't know anything about key before
|
|
# (current_value must be None), and we still don't
|
|
# know anything about key in at least one possible frame.
|
|
continue
|
|
|
|
type = resulting_values[0]
|
|
assert type is not None
|
|
declaration_type = get_proper_type(self.declarations.get(key))
|
|
if isinstance(declaration_type, AnyType):
|
|
# At this point resulting values can't contain None, see continue above
|
|
if not all(is_same_type(type, cast(Type, t)) for t in resulting_values[1:]):
|
|
type = AnyType(TypeOfAny.from_another_any, source_any=declaration_type)
|
|
else:
|
|
for other in resulting_values[1:]:
|
|
assert other is not None
|
|
type = join_simple(self.declarations[key], type, other)
|
|
if current_value is None or not is_same_type(type, current_value):
|
|
self._put(key, type)
|
|
changed = True
|
|
|
|
self.frames[-1].unreachable = not frames
|
|
|
|
return changed
|
|
|
|
def pop_frame(self, can_skip: bool, fall_through: int) -> Frame:
|
|
"""Pop a frame and return it.
|
|
|
|
See frame_context() for documentation of fall_through.
|
|
"""
|
|
|
|
if fall_through > 0:
|
|
self.allow_jump(-fall_through)
|
|
|
|
result = self.frames.pop()
|
|
options = self.options_on_return.pop()
|
|
|
|
if can_skip:
|
|
options.insert(0, self.frames[-1])
|
|
|
|
self.last_pop_changed = self.update_from_options(options)
|
|
|
|
return result
|
|
|
|
@contextmanager
|
|
def accumulate_type_assignments(self) -> 'Iterator[Assigns]':
|
|
"""Push a new map to collect assigned types in multiassign from union.
|
|
|
|
If this map is not None, actual binding is deferred until all items in
|
|
the union are processed (a union of collected items is later bound
|
|
manually by the caller).
|
|
"""
|
|
old_assignments = None
|
|
if self.type_assignments is not None:
|
|
old_assignments = self.type_assignments
|
|
self.type_assignments = defaultdict(list)
|
|
yield self.type_assignments
|
|
self.type_assignments = old_assignments
|
|
|
|
def assign_type(self, expr: Expression,
|
|
type: Type,
|
|
declared_type: Optional[Type],
|
|
restrict_any: bool = False) -> None:
|
|
# We should erase last known value in binder, because if we are using it,
|
|
# it means that the target is not final, and therefore can't hold a literal.
|
|
type = remove_instance_last_known_values(type)
|
|
|
|
type = get_proper_type(type)
|
|
declared_type = get_proper_type(declared_type)
|
|
|
|
if self.type_assignments is not None:
|
|
# We are in a multiassign from union, defer the actual binding,
|
|
# just collect the types.
|
|
self.type_assignments[expr].append((type, declared_type))
|
|
return
|
|
if not isinstance(expr, (IndexExpr, MemberExpr, NameExpr)):
|
|
return None
|
|
if not literal(expr):
|
|
return
|
|
self.invalidate_dependencies(expr)
|
|
|
|
if declared_type is None:
|
|
# Not sure why this happens. It seems to mainly happen in
|
|
# member initialization.
|
|
return
|
|
if not is_subtype(type, declared_type):
|
|
# Pretty sure this is only happens when there's a type error.
|
|
|
|
# Ideally this function wouldn't be called if the
|
|
# expression has a type error, though -- do other kinds of
|
|
# errors cause this function to get called at invalid
|
|
# times?
|
|
return
|
|
|
|
enclosing_type = get_proper_type(self.most_recent_enclosing_type(expr, type))
|
|
if isinstance(enclosing_type, AnyType) and not restrict_any:
|
|
# If x is Any and y is int, after x = y we do not infer that x is int.
|
|
# This could be changed.
|
|
# Instead, since we narrowed type from Any in a recent frame (probably an
|
|
# isinstance check), but now it is reassigned, we broaden back
|
|
# to Any (which is the most recent enclosing type)
|
|
self.put(expr, enclosing_type)
|
|
# As a special case, when assigning Any to a variable with a
|
|
# declared Optional type that has been narrowed to None,
|
|
# replace all the Nones in the declared Union type with Any.
|
|
# This overrides the normal behavior of ignoring Any assignments to variables
|
|
# in order to prevent false positives.
|
|
# (See discussion in #3526)
|
|
elif (isinstance(type, AnyType)
|
|
and isinstance(declared_type, UnionType)
|
|
and any(isinstance(get_proper_type(item), NoneType) for item in declared_type.items)
|
|
and isinstance(get_proper_type(self.most_recent_enclosing_type(expr, NoneType())),
|
|
NoneType)):
|
|
# Replace any Nones in the union type with Any
|
|
new_items = [type if isinstance(get_proper_type(item), NoneType) else item
|
|
for item in declared_type.items]
|
|
self.put(expr, UnionType(new_items))
|
|
elif (isinstance(type, AnyType)
|
|
and not (isinstance(declared_type, UnionType)
|
|
and any(isinstance(get_proper_type(item), AnyType)
|
|
for item in declared_type.items))):
|
|
# Assigning an Any value doesn't affect the type to avoid false negatives, unless
|
|
# there is an Any item in a declared union type.
|
|
self.put(expr, declared_type)
|
|
else:
|
|
self.put(expr, type)
|
|
|
|
for i in self.try_frames:
|
|
# XXX This should probably not copy the entire frame, but
|
|
# just copy this variable into a single stored frame.
|
|
self.allow_jump(i)
|
|
|
|
def invalidate_dependencies(self, expr: BindableExpression) -> None:
|
|
"""Invalidate knowledge of types that include expr, but not expr itself.
|
|
|
|
For example, when expr is foo.bar, invalidate foo.bar.baz.
|
|
|
|
It is overly conservative: it invalidates globally, including
|
|
in code paths unreachable from here.
|
|
"""
|
|
key = literal_hash(expr)
|
|
assert key is not None
|
|
for dep in self.dependencies.get(key, set()):
|
|
self._cleanse_key(dep)
|
|
|
|
def most_recent_enclosing_type(self, expr: BindableExpression, type: Type) -> Optional[Type]:
|
|
type = get_proper_type(type)
|
|
if isinstance(type, AnyType):
|
|
return get_declaration(expr)
|
|
key = literal_hash(expr)
|
|
assert key is not None
|
|
enclosers = ([get_declaration(expr)] +
|
|
[f.types[key] for f in self.frames
|
|
if key in f.types and is_subtype(type, f.types[key])])
|
|
return enclosers[-1]
|
|
|
|
def allow_jump(self, index: int) -> None:
|
|
# self.frames and self.options_on_return have different lengths
|
|
# so make sure the index is positive
|
|
if index < 0:
|
|
index += len(self.options_on_return)
|
|
frame = Frame(self._get_id())
|
|
for f in self.frames[index + 1:]:
|
|
frame.types.update(f.types)
|
|
if f.unreachable:
|
|
frame.unreachable = True
|
|
self.options_on_return[index].append(frame)
|
|
|
|
def handle_break(self) -> None:
|
|
self.allow_jump(self.break_frames[-1])
|
|
self.unreachable()
|
|
|
|
def handle_continue(self) -> None:
|
|
self.allow_jump(self.continue_frames[-1])
|
|
self.unreachable()
|
|
|
|
@contextmanager
|
|
def frame_context(self, *, can_skip: bool, fall_through: int = 1,
|
|
break_frame: int = 0, continue_frame: int = 0,
|
|
conditional_frame: bool = False,
|
|
try_frame: bool = False) -> Iterator[Frame]:
|
|
"""Return a context manager that pushes/pops frames on enter/exit.
|
|
|
|
If can_skip is True, control flow is allowed to bypass the
|
|
newly-created frame.
|
|
|
|
If fall_through > 0, then it will allow control flow that
|
|
falls off the end of the frame to escape to its ancestor
|
|
`fall_through` levels higher. Otherwise control flow ends
|
|
at the end of the frame.
|
|
|
|
If break_frame > 0, then 'break' statements within this frame
|
|
will jump out to the frame break_frame levels higher than the
|
|
frame created by this call to frame_context. Similarly for
|
|
continue_frame and 'continue' statements.
|
|
|
|
If try_frame is true, then execution is allowed to jump at any
|
|
point within the newly created frame (or its descendants) to
|
|
its parent (i.e., to the frame that was on top before this
|
|
call to frame_context).
|
|
|
|
After the context manager exits, self.last_pop_changed indicates
|
|
whether any types changed in the newly-topmost frame as a result
|
|
of popping this frame.
|
|
"""
|
|
assert len(self.frames) > 1
|
|
|
|
if break_frame:
|
|
self.break_frames.append(len(self.frames) - break_frame)
|
|
if continue_frame:
|
|
self.continue_frames.append(len(self.frames) - continue_frame)
|
|
if try_frame:
|
|
self.try_frames.add(len(self.frames) - 1)
|
|
|
|
new_frame = self.push_frame(conditional_frame)
|
|
if try_frame:
|
|
# An exception may occur immediately
|
|
self.allow_jump(-1)
|
|
yield new_frame
|
|
self.pop_frame(can_skip, fall_through)
|
|
|
|
if break_frame:
|
|
self.break_frames.pop()
|
|
if continue_frame:
|
|
self.continue_frames.pop()
|
|
if try_frame:
|
|
self.try_frames.remove(len(self.frames) - 1)
|
|
|
|
@contextmanager
|
|
def top_frame_context(self) -> Iterator[Frame]:
|
|
"""A variant of frame_context for use at the top level of
|
|
a namespace (module, function, or class).
|
|
"""
|
|
assert len(self.frames) == 1
|
|
yield self.push_frame()
|
|
self.pop_frame(True, 0)
|
|
|
|
|
|
def get_declaration(expr: BindableExpression) -> Optional[Type]:
|
|
if isinstance(expr, RefExpr) and isinstance(expr.node, Var):
|
|
type = get_proper_type(expr.node.type)
|
|
if not isinstance(type, PartialType):
|
|
return type
|
|
return None
|