mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-01-06 20:39:40 +07:00
(cherry picked from commit 9444416aa25d4188bcafa47253f9c9bd7e40932e) GitOrigin-RevId: 6d5f0a70cb34378a11341bfd68f05abe338c37d9
310 lines
10 KiB
Python
310 lines
10 KiB
Python
"""Bytecode analysing utils. Originally added for using in smart step into."""
|
|
import dis
|
|
import inspect
|
|
from collections import namedtuple
|
|
|
|
from _pydevd_bundle.pydevd_constants import IS_PY3K, IS_CPYTHON
|
|
|
|
__all__ = ["get_smart_step_into_candidates"]
|
|
|
|
_LOAD_OPNAMES = {
|
|
'LOAD_BUILD_CLASS',
|
|
'LOAD_CONST',
|
|
'LOAD_NAME',
|
|
'LOAD_ATTR',
|
|
'LOAD_GLOBAL',
|
|
'LOAD_FAST',
|
|
'LOAD_CLOSURE',
|
|
'LOAD_DEREF',
|
|
}
|
|
|
|
_CALL_OPNAMES = {
|
|
'CALL_FUNCTION',
|
|
'CALL_FUNCTION_KW',
|
|
}
|
|
|
|
if IS_PY3K:
|
|
for opname in ('LOAD_CLASSDEREF', 'LOAD_METHOD'):
|
|
_LOAD_OPNAMES.add(opname)
|
|
for opname in ('CALL_FUNCTION_EX', 'CALL_METHOD'):
|
|
_CALL_OPNAMES.add(opname)
|
|
else:
|
|
_LOAD_OPNAMES.add('LOAD_LOCALS')
|
|
for opname in ('CALL_FUNCTION_VAR', 'CALL_FUNCTION_VAR_KW'):
|
|
_CALL_OPNAMES.add(opname)
|
|
|
|
_BINARY_OPS = set([opname for opname in dis.opname if opname.startswith('BINARY_')])
|
|
|
|
_BINARY_OP_MAP = {
|
|
'BINARY_POWER': '__pow__',
|
|
'BINARY_MULTIPLY': '__mul__',
|
|
'BINARY_MATRIX_MULTIPLY': '__matmul__',
|
|
'BINARY_FLOOR_DIVIDE': '__floordiv__',
|
|
'BINARY_TRUE_DIVIDE': '__div__',
|
|
'BINARY_MODULO': '__mod__',
|
|
'BINARY_ADD': '__add__',
|
|
'BINARY_SUBTRACT': '__sub__',
|
|
'BINARY_LSHIFT': '__lshift__',
|
|
'BINARY_RSHIFT': '__rshift__',
|
|
'BINARY_AND': '__and__',
|
|
'BINARY_OR': '__or__',
|
|
'BINARY_XOR': '__xor__',
|
|
'BINARY_SUBSCR': '__getitem__',
|
|
}
|
|
|
|
if not IS_PY3K:
|
|
_BINARY_OP_MAP['BINARY_DIVIDE'] = '__div__'
|
|
|
|
_UNARY_OPS = set([opname for opname in dis.opname if opname.startswith('UNARY_') and opname != 'UNARY_NOT'])
|
|
|
|
_UNARY_OP_MAP = {
|
|
'UNARY_POSITIVE': '__pos__',
|
|
'UNARY_NEGATIVE': '__neg__',
|
|
'UNARY_INVERT': '__invert__',
|
|
}
|
|
|
|
_MAKE_OPS = set([opname for opname in dis.opname if opname.startswith('MAKE_')])
|
|
|
|
_COMP_OP_MAP = {
|
|
'<': '__lt__',
|
|
'<=': '__le__',
|
|
'==': '__eq__',
|
|
'!=': '__ne__',
|
|
'>': '__gt__',
|
|
'>=': '__ge__',
|
|
'in': '__contains__',
|
|
'not in': '__contains__',
|
|
}
|
|
|
|
|
|
def _is_load_opname(opname):
|
|
return opname in _LOAD_OPNAMES
|
|
|
|
|
|
def _is_call_opname(opname):
|
|
return opname in _CALL_OPNAMES
|
|
|
|
|
|
def _is_binary_opname(opname):
|
|
return opname in _BINARY_OPS
|
|
|
|
|
|
def _is_unary_opname(opname):
|
|
return opname in _UNARY_OPS
|
|
|
|
|
|
def _is_make_opname(opname):
|
|
return opname in _MAKE_OPS
|
|
|
|
|
|
# Similar to :py:class:`dis._Instruction` but without fields we don't use. Also :py:class:`dis._Instruction`
|
|
# is not available in Python 2.
|
|
Instruction = namedtuple("Instruction", ["opname", "opcode", "arg", "argval", "lineno", "offset"])
|
|
|
|
if IS_PY3K:
|
|
_unpack_opargs = dis._unpack_opargs
|
|
long = int
|
|
else:
|
|
def _unpack_opargs(code):
|
|
n = len(code)
|
|
i = 0
|
|
extended_arg = 0
|
|
while i < n:
|
|
c = code[i]
|
|
op = ord(c)
|
|
offset = i
|
|
arg = None
|
|
i += 1
|
|
if op >= dis.HAVE_ARGUMENT:
|
|
arg = ord(code[i]) + ord(code[i + 1]) * 256 + extended_arg
|
|
extended_arg = 0
|
|
i += 2
|
|
if op == dis.EXTENDED_ARG:
|
|
extended_arg = arg * long(65536)
|
|
yield (offset, op, arg)
|
|
|
|
|
|
def _code_to_name(inst):
|
|
"""If thw instruction's ``argval`` is :py:class:`types.CodeType`, replace it with the name and return the updated instruction.
|
|
|
|
:type inst: :py:class:`Instruction`
|
|
:rtype: :py:class:`Instruction`
|
|
"""
|
|
if inspect.iscode(inst.argval):
|
|
return inst._replace(argval=inst.argval.co_name)
|
|
return inst
|
|
|
|
|
|
def get_smart_step_into_candidates(code):
|
|
"""Iterate through the bytecode and return a list of instructions which can be smart step into candidates.
|
|
|
|
:param code: A code object where we searching for calls.
|
|
:type code: :py:class:`types.CodeType`
|
|
:return: list of :py:class:`~Instruction` that represents the objects that were called
|
|
by one of the Python call instructions.
|
|
:raise: :py:class:`RuntimeError` if failed to parse the bytecode.
|
|
"""
|
|
if not IS_CPYTHON:
|
|
# For implementations other than CPython we fall back to simple step into.
|
|
return []
|
|
|
|
linestarts = dict(dis.findlinestarts(code))
|
|
varnames = code.co_varnames
|
|
names = code.co_names
|
|
constants = code.co_consts
|
|
freevars = code.co_freevars
|
|
lineno = None
|
|
stk = [] # only the instructions related to calls are pushed in the stack
|
|
result = []
|
|
|
|
for offset, op, arg in _unpack_opargs(code.co_code):
|
|
try:
|
|
if linestarts is not None:
|
|
lineno = linestarts.get(offset, None) or lineno
|
|
opname = dis.opname[op]
|
|
argval = None
|
|
if arg is None:
|
|
if _is_binary_opname(opname):
|
|
stk.pop()
|
|
result.append(Instruction(opname, op, arg, _BINARY_OP_MAP[opname], lineno, offset))
|
|
elif _is_unary_opname(opname):
|
|
result.append(Instruction(opname, op, arg, _UNARY_OP_MAP[opname], lineno, offset))
|
|
if opname == 'COMPARE_OP':
|
|
stk.pop()
|
|
cmp_op = dis.cmp_op[arg]
|
|
if cmp_op not in ('exception match', 'BAD'):
|
|
result.append(Instruction(opname, op, arg, _COMP_OP_MAP[cmp_op], lineno, offset))
|
|
if _is_load_opname(opname):
|
|
if opname == 'LOAD_CONST':
|
|
argval = constants[arg]
|
|
elif opname == 'LOAD_NAME' or opname == 'LOAD_GLOBAL':
|
|
argval = names[arg]
|
|
elif opname == 'LOAD_ATTR':
|
|
stk.pop()
|
|
argval = names[arg]
|
|
elif opname == 'LOAD_FAST':
|
|
argval = varnames[arg]
|
|
elif IS_PY3K and opname == 'LOAD_METHOD':
|
|
stk.pop()
|
|
argval = names[arg]
|
|
elif opname == 'LOAD_DEREF':
|
|
argval = freevars[arg]
|
|
stk.append(Instruction(opname, op, arg, argval, lineno, offset))
|
|
elif _is_make_opname(opname):
|
|
tos = stk.pop() # qualified name of the function or function code in Python 2
|
|
argc = 0
|
|
if IS_PY3K:
|
|
stk.pop() # function code
|
|
for flag in (0x01, 0x02, 0x04, 0x08):
|
|
if arg & flag:
|
|
argc += 1 # each flag means one extra element to pop
|
|
else:
|
|
argc = arg
|
|
tos = _code_to_name(tos)
|
|
while argc > 0:
|
|
stk.pop()
|
|
argc -= 1
|
|
stk.append(tos)
|
|
elif _is_call_opname(opname):
|
|
argc = arg # the number of the function or method arguments
|
|
if opname == 'CALL_FUNCTION_KW' or not IS_PY3K and opname == 'CALL_FUNCTION_VAR':
|
|
stk.pop() # pop the mapping or iterable with arguments or parameters
|
|
elif not IS_PY3K and opname == 'CALL_FUNCTION_VAR_KW':
|
|
stk.pop() # pop the mapping with arguments
|
|
stk.pop() # pop the iterable with parameters
|
|
elif not IS_PY3K and opname == 'CALL_FUNCTION':
|
|
argc = arg & 0xff # positional args
|
|
argc += ((arg >> 8) * 2) # keyword args
|
|
elif opname == 'CALL_FUNCTION_EX':
|
|
has_keyword_args = arg & 0x01
|
|
if has_keyword_args:
|
|
stk.pop()
|
|
stk.pop() # positional args
|
|
argc = 0
|
|
while argc > 0:
|
|
stk.pop() # popping args from the stack
|
|
argc -= 1
|
|
tos = _code_to_name(stk[-1])
|
|
if tos.opname == 'LOAD_BUILD_CLASS':
|
|
# an internal `CALL_FUNCTION` for building a class
|
|
continue
|
|
result.append(tos._replace(offset=offset)) # the actual offset is not when a function was loaded but when it was called
|
|
except:
|
|
err_msg = "Bytecode parsing error at: offset(%d), opname(%s), arg(%d)" % (offset, dis.opname[op], arg)
|
|
raise RuntimeError(err_msg)
|
|
return result
|
|
|
|
|
|
Variant = namedtuple('Variant', ['name', 'is_visited'])
|
|
|
|
|
|
def calculate_smart_step_into_variants(frame, start_line, end_line):
|
|
"""
|
|
Calculate smart step into variants for the given line range.
|
|
:param frame:
|
|
:type frame: :py:class:`types.FrameType`
|
|
:param start_line:
|
|
:param end_line:
|
|
:return: A list of call names from the first to the last.
|
|
:raise: :py:class:`RuntimeError` if failed to parse the bytecode.
|
|
"""
|
|
variants = []
|
|
is_context_reached = False
|
|
code = frame.f_code
|
|
lasti = frame.f_lasti
|
|
for inst in get_smart_step_into_candidates(code):
|
|
if inst.lineno and inst.lineno > end_line:
|
|
break
|
|
if not is_context_reached and inst.lineno is not None and inst.lineno >= start_line:
|
|
is_context_reached = True
|
|
if not is_context_reached:
|
|
continue
|
|
variants.append(Variant(inst.argval, inst.offset <= lasti))
|
|
return variants
|
|
|
|
|
|
def find_last_func_call_order(frame, start_line):
|
|
"""Find the call order of the last function call between ``start_line`` and last executed instruction.
|
|
|
|
:param frame: A frame inside which we are looking the function call.
|
|
:type frame: :py:class:`types.FrameType`
|
|
:param start_line:
|
|
:return: call order or -1 if we fail to find the call order for some
|
|
reason.
|
|
:rtype: int
|
|
:raise: :py:class:`RuntimeError` if failed to parse the bytecode.
|
|
"""
|
|
code = frame.f_code
|
|
lasti = frame.f_lasti
|
|
cache = {}
|
|
call_order = -1
|
|
for inst in get_smart_step_into_candidates(code):
|
|
if inst.offset > lasti:
|
|
break
|
|
if inst.lineno >= start_line:
|
|
name = inst.argval
|
|
call_order = cache.setdefault(name, -1)
|
|
call_order += 1
|
|
cache[name] = call_order
|
|
return call_order
|
|
|
|
|
|
def find_last_call_name(frame):
|
|
"""Find the name of the last call made in the frame.
|
|
|
|
:param frame: A frame inside which we are looking the last call.
|
|
:type frame: :py:class:`types.FrameType`
|
|
:return: The name of a function or method that has been called last.
|
|
:rtype: str
|
|
:raise: :py:class:`RuntimeError` if failed to parse the bytecode.
|
|
"""
|
|
last_call_name = None
|
|
code = frame.f_code
|
|
lasti = frame.f_lasti
|
|
for inst in get_smart_step_into_candidates(code):
|
|
if inst.offset > lasti:
|
|
break
|
|
last_call_name = inst.argval
|
|
|
|
return last_call_name
|