blob: 1f21e080d0989281f548835dfd2c11199292a15d [file] [log] [blame] [edit]
from typing import Dict, List, Set
from mypy.nodes import (
Decorator, Expression, FuncDef, FuncItem, LambdaExpr, NameExpr, SymbolNode, Var, MemberExpr
)
from mypy.traverser import TraverserVisitor
class PreBuildVisitor(TraverserVisitor):
"""
Class used to visit a mypy file before building the IR for that program. This is done as a
first pass so that nested functions, encapsulating functions, lambda functions, decorated
functions, and free variables can be determined before instantiating the IRBuilder.
"""
def __init__(self) -> None:
super().__init__()
# Mapping from FuncItem instances to sets of variables. The FuncItem instances are where
# these variables were first declared, and these variables are free in any functions that
# are nested within the FuncItem from which they are mapped.
self.free_variables = {} # type: Dict[FuncItem, Set[SymbolNode]]
# Intermediate data structure used to map SymbolNode instances to the FuncDef in which they
# were first visited.
self.symbols_to_funcs = {} # type: Dict[SymbolNode, FuncItem]
# Stack representing the function call stack.
self.funcs = [] # type: List[FuncItem]
# The set of property setters
self.prop_setters = set() # type: Set[FuncDef]
# A map from any function that contains nested functions to
# a set of all the functions that are nested within it.
self.encapsulating_funcs = {} # type: Dict[FuncItem, List[FuncItem]]
# A map from a nested func to it's parent/encapsulating func.
self.nested_funcs = {} # type: Dict[FuncItem, FuncItem]
self.funcs_to_decorators = {} # type: Dict[FuncDef, List[Expression]]
def add_free_variable(self, symbol: SymbolNode) -> None:
# Get the FuncItem instance where the free symbol was first declared, and map that FuncItem
# to the SymbolNode representing the free symbol.
func = self.symbols_to_funcs[symbol]
self.free_variables.setdefault(func, set()).add(symbol)
def visit_decorator(self, dec: Decorator) -> None:
if dec.decorators:
# Only add the function being decorated if there exist decorators in the decorator
# list. Note that meaningful decorators (@property, @abstractmethod) are removed from
# this list by mypy, but functions decorated by those decorators (in addition to
# property setters) do not need to be added to the set of decorated functions for
# the IRBuilder, because they are handled in a special way.
if isinstance(dec.decorators[0], MemberExpr) and dec.decorators[0].name == 'setter':
self.prop_setters.add(dec.func)
else:
self.funcs_to_decorators[dec.func] = dec.decorators
super().visit_decorator(dec)
def visit_func(self, func: FuncItem) -> None:
# If there were already functions or lambda expressions defined in the function stack, then
# note the previous FuncItem as containing a nested function and the current FuncItem as
# being a nested function.
if self.funcs:
# Add the new func to the set of nested funcs within the func at top of the func stack.
self.encapsulating_funcs.setdefault(self.funcs[-1], []).append(func)
# Add the func at top of the func stack as the parent of new func.
self.nested_funcs[func] = self.funcs[-1]
self.funcs.append(func)
super().visit_func(func)
self.funcs.pop()
def visit_func_def(self, fdef: FuncItem) -> None:
self.visit_func(fdef)
def visit_lambda_expr(self, expr: LambdaExpr) -> None:
self.visit_func(expr)
def visit_name_expr(self, expr: NameExpr) -> None:
if isinstance(expr.node, (Var, FuncDef)):
self.visit_symbol_node(expr.node)
# Check if child is contained within fdef (possibly indirectly within
# multiple nested functions).
def is_parent(self, fitem: FuncItem, child: FuncItem) -> bool:
if child in self.nested_funcs:
parent = self.nested_funcs[child]
if parent == fitem:
return True
return self.is_parent(fitem, parent)
return False
def visit_symbol_node(self, symbol: SymbolNode) -> None:
if not self.funcs:
# If the list of FuncDefs is empty, then we are not inside of a function and hence do
# not need to do anything regarding free variables.
return
if symbol in self.symbols_to_funcs:
orig_func = self.symbols_to_funcs[symbol]
if self.is_parent(self.funcs[-1], orig_func):
# If the function in which the symbol was originally seen is nested
# within the function currently being visited, fix the free_variable
# and symbol_to_funcs dictionaries.
self.symbols_to_funcs[symbol] = self.funcs[-1]
self.free_variables.setdefault(self.funcs[-1], set()).add(symbol)
elif self.is_parent(orig_func, self.funcs[-1]):
# If the SymbolNode instance has already been visited before,
# and it was declared in a FuncDef not nested within the current
# FuncDef being visited, then it is a free symbol because it is
# being visited again.
self.add_free_variable(symbol)
else:
# Otherwise, this is the first time the SymbolNode is being visited. We map the
# SymbolNode to the current FuncDef being visited to note where it was first visited.
self.symbols_to_funcs[symbol] = self.funcs[-1]
def visit_var(self, var: Var) -> None:
self.visit_symbol_node(var)