blob: 9da9056c56b496ae2ec2eda8960c9a5396ec6a33 [file] [log] [blame]
"""Compare two versions of a module symbol table.
The goal is to find which AST nodes have externally visible changes, so
that we can fire triggers and re-type-check other parts of the program
that are stale because of the changes.
Only look at detail at definitions at the current module.
"""
from typing import Set, List, TypeVar
from mypy.nodes import SymbolTable, SymbolTableNode, FuncBase, TypeInfo, Var
from mypy.types import (
Type, TypeVisitor, UnboundType, TypeList, AnyType, NoneTyp, UninhabitedType,
ErasedType, DeletedType, Instance, TypeVarType, CallableType, TupleType, TypedDictType,
UnionType, Overloaded, PartialType, TypeType
)
def compare_symbol_tables(name_prefix: str, table1: SymbolTable, table2: SymbolTable) -> Set[str]:
"""Return names that are different in two versions of a symbol table.
Return a set of fully-qualified names (e.g., 'mod.func' or 'mod.Class.method').
"""
# Find names only defined only in one version.
names1 = {'%s.%s' % (name_prefix, name) for name in table1}
names2 = {'%s.%s' % (name_prefix, name) for name in table2}
triggers = names1 ^ names2
# Look for names defined in both versions that are different.
for name in set(table1.keys()) & set(table2.keys()):
if not is_similar_node_shallow(table1[name], table2[name]):
triggers.add('%s.%s' % (name_prefix, name))
else:
# Nodes are the same when using shallow comparison. Now look into contents of
# classes to find changed items.
node1 = table1[name].node
node2 = table2[name].node
if node1.fullname() and get_prefix(node1.fullname()) != name_prefix:
# Only look inside things defined in the current module.
# TODO: This probably doesn't work generally...
continue
if isinstance(node1, TypeInfo) and isinstance(node2, TypeInfo):
# TODO: Only do this is the class is defined in this module.
prefix = '%s.%s' % (name_prefix, node1.name())
triggers |= compare_symbol_tables(prefix, node1.names, node2.names)
return triggers
def is_similar_node_shallow(n: SymbolTableNode, m: SymbolTableNode) -> bool:
# TODO:
# cross_ref
# tvar_def
# type_override
if (n.kind != m.kind
or n.mod_id != m.mod_id
or n.module_public != m.module_public):
return False
if type(n.node) != type(m.node): # noqa
return False
if n.node.fullname() != m.node.fullname():
return False
if isinstance(n.node, FuncBase) and isinstance(m.node, FuncBase):
# TODO: info
return (n.node.is_property == m.node.is_property and
is_identical_type(n.node.type, m.node.type))
if isinstance(n.node, TypeInfo) and isinstance(m.node, TypeInfo):
# TODO:
# type_vars
# bases
# _promote
# tuple_type
# typeddict_type
nn = n.node
mn = m.node
return (nn.is_abstract == mn.is_abstract and
nn.is_enum == mn.is_enum and
nn.fallback_to_any == mn.fallback_to_any and
nn.is_named_tuple == mn.is_named_tuple and
nn.is_newtype == mn.is_newtype and
is_same_mro(nn.mro, mn.mro))
if isinstance(n.node, Var) and isinstance(m.node, Var):
return is_identical_type(n.node.type, m.node.type)
return True
def is_same_mro(mro1: List[TypeInfo], mro2: List[TypeInfo]) -> bool:
return (len(mro1) == len(mro2)
and all(x.fullname() == y.fullname() for x, y in zip(mro1, mro2)))
def get_prefix(id: str) -> str:
"""Drop the final component of a qualified name (e.g. ('x.y' -> 'x')."""
return id.rsplit('.', 1)[0]
def is_identical_type(t: Type, s: Type) -> bool:
return t.accept(IdenticalTypeVisitor(s))
TT = TypeVar('TT', bound=Type)
def is_identical_types(a: List[TT], b: List[TT]) -> bool:
return len(a) == len(b) and all(is_identical_type(t, s) for t, s in zip(a, b))
class IdenticalTypeVisitor(TypeVisitor[bool]):
"""Visitor for checking whether two types are identical.
This may be conservative -- it's okay for two types to be considered
different even if they are actually the same. The results are only
used to improve performance, not relied on for correctness.
Differences from mypy.sametypes:
* Types with the same name but different AST nodes are considered
identical.
* If one of the types is not valid for whatever reason, they are
considered different.
* Sometimes require types to be structurally identical, even if they
are semantically the same type.
"""
def __init__(self, right: Type) -> None:
self.right = right
# visit_x(left) means: is left (which is an instance of X) the same type as
# right?
def visit_unbound_type(self, left: UnboundType) -> bool:
return False
def visit_any(self, left: AnyType) -> bool:
return isinstance(self.right, AnyType)
def visit_none_type(self, left: NoneTyp) -> bool:
return isinstance(self.right, NoneTyp)
def visit_uninhabited_type(self, t: UninhabitedType) -> bool:
return isinstance(self.right, UninhabitedType)
def visit_erased_type(self, left: ErasedType) -> bool:
return False
def visit_deleted_type(self, left: DeletedType) -> bool:
return isinstance(self.right, DeletedType)
def visit_instance(self, left: Instance) -> bool:
return (isinstance(self.right, Instance) and
left.type.fullname() == self.right.type.fullname() and
is_identical_types(left.args, self.right.args))
def visit_type_var(self, left: TypeVarType) -> bool:
return (isinstance(self.right, TypeVarType) and
left.id == self.right.id)
def visit_callable_type(self, left: CallableType) -> bool:
# FIX generics
if isinstance(self.right, CallableType):
cright = self.right
return (is_identical_type(left.ret_type, cright.ret_type) and
is_identical_types(left.arg_types, cright.arg_types) and
left.arg_names == cright.arg_names and
left.arg_kinds == cright.arg_kinds and
left.is_type_obj() == cright.is_type_obj() and
left.is_ellipsis_args == cright.is_ellipsis_args)
return False
def visit_tuple_type(self, left: TupleType) -> bool:
if isinstance(self.right, TupleType):
return is_identical_types(left.items, self.right.items)
return False
def visit_typeddict_type(self, left: TypedDictType) -> bool:
if isinstance(self.right, TypedDictType):
if left.items.keys() != self.right.items.keys():
return False
for (_, left_item_type, right_item_type) in left.zip(self.right):
if not is_identical_type(left_item_type, right_item_type):
return False
return True
return False
def visit_union_type(self, left: UnionType) -> bool:
if isinstance(self.right, UnionType):
# Require structurally identical types.
return is_identical_types(left.items, self.right.items)
return False
def visit_overloaded(self, left: Overloaded) -> bool:
if isinstance(self.right, Overloaded):
return is_identical_types(left.items(), self.right.items())
return False
def visit_partial_type(self, left: PartialType) -> bool:
# A partial type is not fully defined, so the result is indeterminate. We shouldn't
# get here.
raise RuntimeError
def visit_type_type(self, left: TypeType) -> bool:
if isinstance(self.right, TypeType):
return is_identical_type(left.item, self.right.item)
return False