def get_module_seg(module: str, cut: str): """ ARGS: module cut: str. 'l0'/'l1'/'r0'/'r1' 假设要切分的 module 为 'A.B.C' 'l0': 取第一个片段 -> 'A' 'l1': 取非末尾片段 -> 'A.B' 'r0': 取末尾片段 -> 'C' 'r1': 取非第一个片段 -> 'B.C' """ if '.' not in module: return '' if cut == 'l0': return module.split('.', 1)[0] elif cut == 'l1': return module.rsplit('.', 1)[0] elif cut == 'r0': return module.rsplit('.', 1)[1] elif cut == 'r1': return module.split('.', 1)[1] else: lk.logt('[E4544]', 'the `cut` must be one of the following values: ' '"l0", "l1", "r0" or "r1"', cut, h='parent') raise ValueError
def parse_attribute(self, call: str) -> str: """ IN: call: e.g. 'downloader.Downloader' call 的值是类似于 module 的写法, 可以按照点号切成多个片段, 其中第一个片段是 var, 可在 self.vars_holder 中发现它, 进而得到它的真实 module; 其余则是 该 module 级别以下的调用, 简单加在该 module 末尾即可, 即 'downloader .Downloader' -> self.assign_reached: {'downloader': 'testflight .downloader'} -> 'testflight.downloader' -> 'testflight .downloader.Downloader' -> 更新到 self.call_chain 中. OT: (updated) self.call_chain """ if call.startswith('self.'): module = call.replace('self', self.vars_holder.get('self'), 1) # 'self.main' -> 'src.app.Init.main' else: if '.' in call: head, tail = call.split('.', 1) else: head, tail = call, '' module = self.vars_holder.get(head) lk.logt('[D0521]', call, module) if module is None: # var = 'os' return '' else: # var = 'downloader.Downloader' if tail: module += '.' + tail return module
def parse_assign(self, assign: dict): """ IN: assign: {(str)new_var: (str)known_var}. e.g. {"init": "Init"} 键是新变量, 值来自 self.assign_reachables. OT: (updated) self.assign_reached """ out = [] for new_var, known_var in assign.items(): if known_var.startswith('self.'): module = known_var.replace( 'self', self.vars_holder.get('self'), 1) # 'self.main' -> 'src.app.Init.main' else: module = self.vars_holder.get(known_var.split('.', 1)[0]) lk.logt('[D0505]', known_var, module) """ case 1: known_var = "downloader.Downloader" -> known_var.split('.', 1)[0] = "downloader" -> module = 'testflight.downloader' """ if module is None: # source_line = 'a = os.path(...)' -> known_var = 'os.path' continue else: out.append(module) # source_line = 'a = Init()' -> known_var = 'Init' self.vars_holder.update(new_var, module) return out
def parse_call(self, data: str): # related to "<class '_ast.Call'>" """ data: """ lk.logt('[I3516]', 'parsing call', data, self.module_hooks.get(data)) if data in self.module_hooks: # e.g. data = 'child_method' # -> self.module_hooks[data] = 'src.app.main.child_method' self.calls.append(self.module_hooks[data])
def parse_function_def(self, data: str): """ IN: data: str. e.g. "main" OT: "src.app.main" """ var = data # -> 'main' module = self.top_module + '.' + var # -> 'src.app.main' lk.logt('[D3902]', 'parse_function_def', var, module) self.vars_holder.update(var, module)
def main(self): """ PS: 请配合 src.utils.ast_helper.test2() 的输出结果 (ast_helper_result.json) 完成本方法的制作. """ start = self.module_analyser.get_top_module() + '.' + 'module' calls = self.run_block(start) lk.logt('[I4413]', len(calls), calls) self.recurse_module_called(calls)
def parse_class_def(self, data: str): """ IN: data: str. e.g. "Init" OT: "src.app.Init" """ var = data # -> 'Init' module = self.top_module + '.' + var # | module = self.top_module + '.' + var + '.__init__' # | -> 'src.app.Init.__init__' lk.logt('[D3903]', 'parse_class_def', var, module) self.vars_holder.update(var, module)
def run_block(self, current_module: str): """ IN: module: str OT: self.calls: list """ lk.logd('run block', current_module, style='■') if current_module not in self.module_linos: # 说明此 module 是从外部导入的模块, 如 module = 'testflight.downloader'. assert self.module_analyser.is_prj_module(current_module) self.outer_calls.append(current_module) # return module_path # update hooks # self.module_hooks 需要在每次更新 self.run_block() 时同步更新. 这是因为, 不同 # 的 block 定义的区间范围不同, 而不同的区间范围包含的变量指配 (assigns) 也可能是不同 # 的. # 例如在 module = testflight.test_app_launcher.module 层级, self.module # _hooks = {'main': 'testflight.test_app_launcher.main'}. 到了 module = # testflight.test_app_launcher.Init 来运行 run_block 的时候, self.module # _hooks 就变成了 {'main': 'testflight.test_app_launcher.Init.main'}. 也就 # 是说在不同的 block 区间, 'main' 指配的 module 对象发生了变化, 因此必须更新 self # .module_hooks 才能适应最新变化. self.module_hooks = self.assign_analyser.indexing_assign_reachables( current_module, self.module_linos) lk.logt('[I4252]', 'update module hooks', self.module_hooks) # reset hooks self.var_hooks.clear() self.calls.clear() linos = self.module_linos[current_module] # the linos is in ordered. lk.loga(current_module, linos) for lino in linos: self.run_line(lino) # | for index, lino in enumerate(linos): # | # noinspection PyBroadException # | try: # | self.run_line(lino) # | except Exception: # | if index == 0: # | continue # | else: # | raise Exception return self.calls
def show(self, runtime_module): """ IN: self.tile_view: dict. {module: [call1, call2, ...]} e.g. res/sample/pycallchain_tile_view.json OT: self.cascade_view: dict. {runtime_module: {module1: {module11: {... }, module12: {...}, ...}}} e.g. res/sample/pycallchain_cascade_view.json """ node = self.cascade_view.setdefault(runtime_module, {}) calls = self.tile_view.get(runtime_module) self.recurse(node, calls) lk.logt('[D3619]', self.stacks) # lk.logt('[I3316]', self.cascade_view) # TEST output from lk_utils.read_and_write_basic import write_json write_json(self.cascade_view, '../temp/out.json') write_json(self.tile_view, '../temp/out2.json')
def find_global_vars(self): """ 哪些是全局变量: runtime 层级的 Import, ImportFrom runtime 层级的 Assign 行内的 `global xxx` IN: module_linos: provided by src.module_analyser.ModuleIndexing #indexing_module_linos self.module_helper self.ast_tree self.ast_indents OT: dict. {var: module} """ top_linos = tuple( lino for lino, indent in self.ast_indents.items() if indent == 0 ) lk.logt('[D3743]', self.top_module, top_linos) # ------------------------------------------------ # runtime 层级的 Import, ImportFrom & runtime 层级的 Assign line_parser = LineParser(self.top_module) for lino in top_linos: ast_line = self.ast_tree[lino] lk.logt('[TEMPRINT]_20190811_214127', lino, ast_line) line_parser.main(ast_line) # line_parser 会自动帮我们处理 ast_line 涉及的 Import, ImportFrom, # Assign 等的变量与 module 的对照关系. # ------------------------------------------------ # 行内的 `global xxx` for lino in self.ast_indents: if lino in top_linos: continue pass # TODO return line_parser.get_vars()
def analyse_module(self, module, linos): """ 发现该 module 下的与其他 module 之间的调用关系. """ lk.logd('analyse_module', module, style='■') # lk.logt('[TEMPRINT]_20190811_214927', # self.line_parser.get_global_vars()) related_calls = [] for lino in linos: ast_line = self.ast_tree[lino] module_called = self.analyse_line(ast_line) # lk.logt('[D3233]', module_called) for m in module_called: if m not in related_calls: related_calls.append(m) lk.logt('[I3259]', related_calls) self.module_calls.update({module: tuple(related_calls)})
def parse_call(self, call: str): """ IN: data: e.g. 'child_method' OT: (updated) self.call_chain NOTE: parse_call 与 parse_attribute 方法无区别. """ if '.' in call: head, tail = call.split('.', 1) else: head, tail = call, '' module = self.vars_holder.get(head) lk.logt('[D0521]', call, module) if module is None: # var = 'os' return '' else: # var = 'downloader.Downloader' if tail: module += '.' + tail return module
def parse_class_def(data): # related to "<class '_ast.ClassDef'>" lk.logt( '[E1036]', 'a class def found in block region, this should not ' 'be happend', data) raise Exception
def indexing_module_linos(self, master_module='', linos=None): """ 获取 pyfile 内每个 module 对应的行号范围. 根据 {lino:indent} 和 ast_tree 创建 {module:linos} 的字典. 注: 1. 每个 module (无论是父子关系还是兄弟关系) 之间的范围互不重叠. 2. AClass.__init__ 被认作 AClass IN: master_module: str. 当为空时, 将使用默认值 self.top_module (e.g. 'src.app') 不为空时, 请注意传入的是当前要观察的 module 的上一级 module. 例如我们要编译 src.app.main.child_method 所在的层级, 则 top_module 应传入 src .app.main. 用例参考: src.analyser.AssignAnalyser#update _assigns linos: None/list. 您可以自定义要读取的 module 范围, 本方法会仅针对这个区间进行 编译. 例如: 1 | def aaa(): 2 | def bbb(): # <- start 3 | def ccc(): 4 | pass 5 | # <- end 6 | def ddd(): 7 | pass 则本方法只编译 range(2, 5) 范围内的数据, 并返回 {'src.app.aaa.bbb': [ 2, 5], 'src.app.aaa.bbb.ccc': [3, 4]} 作为编译结果. 注意: 指定的范围的开始位置的缩进必须小于等于结束位置的缩进 (空行除外). 如果该参数为 None, 则默认使用所有行号 (`range(0, len(code_lines))`). self.ast_tree: dict. {lino: [(obj_type, obj_val), ...], ...} lino: int. 行号, 从 1 开始数. obj_type: str. 对象类型, 例如 "<class 'FunctionDef'>" 等. 完整的支持 列表参考 src.utils.ast_helper.AstHelper#eval_node(). obj_val: str/dict. 对象的值, 目前仅存在 str 或 dict 类型的数据. 示例: (str) 'print' (dict) {'src.downloader.Downloader': 'src.downloader.Downloader'} (多用于描述 Import) self.ast_indents: dict. {lino: indent, ...}. 由 AstHelper#create _lino_indent_dict() 提供. lino: int. 行号, 从 1 开始数. indent: int. 该行的列缩进位置, 为 4 的整数倍数, 如 0, 4, 8, 12 等. self.top_module: str. e.g. 'src.app' OT: module_linos: dict. {module: [lino, ...]} module: str. 模块的路径名. lino_list: list. 模块所涉及的行号列表, 已经过排序, 行号从 1 开始数, 最大 不超过当前 pyfile 的总代码行数. e.g. { 'src.app.module': [1, 2, 3, 9, 10], 'src.app.main': [4, 5, 8], 'src.app.main.child_method': [6, 7], ... } 有了 module_linos 以后, 我们就可以在已知 module 的调用关系的情况下, 专注 于读取该 module 对应的区间范围, 逐行分析每条语句, 并进一步发现新的调用关系, 以此产生裂变效应. 详见 src.analyser.VirtualRunner#main(). """ if master_module == '': master_module = self.top_module assert linos is None linos = self.prj_linos # the linos are already sorted. else: assert linos is not None lk.logd('indexing module linos', master_module) # ------------------------------------------------ # ast_defs: ast definitions ast_defs = ("<class '_ast.FunctionDef'>", "<class '_ast.ClassDef'>") ast_args = ("<class '_ast.arg'>",) indent_module_holder = {-4: self.top_module} # format: {indent: module} module_linos = {} # format: {module: linos} last_module = '' last_indent = 0 # last_indent 初始化值不影响方法的正常执行. 因为我们事先能保证 linos 参数的第一个 # lino 的 indent 一定是正常的, 而伴随着第一个 lino 的循环结尾, last_indent 就能 # 得到安全的更新, 因此 last_indent 的初始值无论是几都是安全的. for lino in linos: obj_type, obj_val = self.eval_ast_line(lino) # -> "<class '_ast.FunctionDef'>", 'main' indent = self.ast_indents.get(lino, -1) parent_indent = indent - 4 # lk.loga(lino, indent, last_module, obj_type, obj_val) if parent_indent in indent_module_holder: parent_module = indent_module_holder[parent_indent] if obj_type in ast_defs: # obj_type = "<class 'FunctionDef'>", obj_val = 'main' if obj_val == '__init__': # source_code = `def __init__(self):` current_module = parent_module else: current_module = parent_module + '.' + obj_val # -> 'src.app.main' elif obj_type in ast_args: current_module = last_module elif indent == 0 \ or last_module == self.runtime_module: current_module = self.runtime_module # | current_module = parent_module + '.module' else: # indent > 0 and last_parent_module not in ( # master_module, self # .runtime_module current_module = parent_module # update indent_module_holder indent_module_holder.update({indent: current_module}) # -> {0: 'src.app.main'}, {4: 'src.app.main.child_method'}, ... else: lk.loga(lino, indent, last_module) indent = last_indent current_module = last_module """ case 1: source_code = ``` 1 | def main(): # <- cursor 2 | pass ``` indent = 0, obj_val = 'main' -> indent - 4 = -4 -> parent_module = 'src.app' -> current_module = 'src.app.main' case 2: source_code = ``` 1 | def main(): 2 | def child_method(): # <- cursor 3 | pass ``` indent = 4, obj_val = 'child_method' -> indent - 4 = 0 -> parent_module = 'src.app.main' -> current_module = 'src.app.main.child_method' case 3: source_code = ``` 1 | def main(): 2 | def child_method(): 3 | print('aaa', 'bbb' 4 | 'ccc') # <- cursor ``` indent = 14, obj_val = "'ccc'" -> indent - 4 = 10 -> 10 not in indent_module_holder.keys(), so return the fallback value: 'src.app.main.child_method' -> current_module = 'src.app.main.child_method' """ # update module_linos node = module_linos.setdefault(current_module, []) node.append(lino) # NOTE: the lino is in ordered # update last vars last_module = current_module last_indent = indent # TEST show # lk.logt('[D3421]', self.top_module, indent_module_holder) show_modules_lightly = tuple( self.module_helper.get_module_seg(x, 'r0') for x in module_linos ) lk.logt('[I4204]', self.top_module, show_modules_lightly) """ -> module_linos = { 'testflight.test_app_launcher.module': [1, 3, 4, 38, 39], 'testflight.test_app_launcher.main' : [8, 11, 12, 21, 22, 24, 25, 27, 28], 'testflight.test_app_launcher.main.child_method' : [14, 15], 'testflight.test_app_launcher.main.child_method2': [17, 18, 19], 'testflight.test_app_launcher.Init' : [31], 'testflight.test_app_launcher.Init.main' : [33, 35] } """ # raise Exception # TEST return module_linos
def indexing_assign_reachables( self, target_module, module_linos ): """ IN: target_module: str. 'src.app.Init.main' module_linos OT: (<dict var_reachables>, <str parent_module>) """ if self.module_helper.is_runtime_module(target_module): # 相当于返回 self.find_global_vars() 的结果. return self.top_assigns.copy(), '' if target_module not in module_linos: lk.logt('[E2459]', target_module, module_linos) raise Exception lk.logt('[I0114]', target_module) # ------------------------------------------------ """ workflow: 1. 以 target_module 的 linos[0] 为起点, 向前找到第一个 indent 为 0 的 lino 2. 以 target_module 的 linos[-1] 为起点, 向后找到第一个 indent 为 0 的 lino 3. 在此区间内, 将所有 ast_defs 进行解析, 并认定为 var_reachables """ # ------------------------------------------------ lino reachables target_linos = module_linos[target_module] curr_module_lino = target_linos[0] start_offset, end_offset = target_linos[0], target_linos[-1] + 1 # the start lino reachable indent = self.ast_indents[start_offset] # lk.logt('[TEMPRINT]20190811182309', target_module, start_offset, # indent) if indent == 0: parent_module = '' else: while True: parent_module = self.module_helper.get_parent_module( target_module ) # lk.logt('[TEMPRINT]20190811182549', target_module, # parent_module) parent_linos = module_linos[parent_module] start_offset, end_offset = parent_linos[0], parent_linos[-1] + 1 parent_indent = self.ast_indents[start_offset] if parent_indent == 0: break else: continue # -> parent_module = 'src.app.Init' # the end lino reachable while end_offset < self.max_lino: if end_offset in self.ast_indents: indent = self.ast_indents[end_offset] if indent == 0: break end_offset += 1 # get lino reachalbes lino_reachables = [ lino for lino in range(start_offset, end_offset) if lino in self.ast_indents and lino != curr_module_lino ] """ 这里为什么要判断 `lino != curr_module_lino`? 因为 target_module 不能指任自身, 所以应去除. 例如 target_module = 'src.app.module', 在源码中, 不能因此自动产生 `module: src.app.module` 的对应关系. 所以不能加入到 assigns 中. """ # parse vars line_parser = LineParser(self.top_module) ast_defs = ("<class '_ast.FunctionDef'>", "<class '_ast.ClassDef'>") for lino in lino_reachables: ast_line = self.eval_ast_line(lino) if ast_line[0] in ast_defs: line_parser.main(ast_line) return line_parser.get_vars(), parent_module
def recurse_module_called(self, calls): for i in calls: child_calls = self.run_block(i) lk.logt('[D4429]', len(child_calls), child_calls) return self.recurse_module_called(child_calls)
def parse_function_def(data): # related to "<class '_ast.FunctionDef'>" lk.logt( '[E1036]', 'a function def found in block region, this should ' 'not be happend', data) raise Exception
def indexing_module_linos(self, top_module='', linos=None): """ 根据 {lino:indent} 和 ast_tree 创建 {module:linos} 的字典. ARGS: top_module linos: list. 您可以自定义要读取的 module 范围, 本方法会仅针对这个区间进行编译. 例如: 1 | def aaa(): 2 | def bbb(): # <- start 3 | def ccc(): 4 | pass 5 | # <- end 6 | def ddd(): 7 | pass 则本方法只编译 start=2 - end=5 范围内的数据, 并返回 {'src.app.aaa.bbb' : [2, 5], 'src.app.aaa.bbb.ccc': [3, 4]} 作为编译结果. 注意: 指定的范围的开始位置的缩进必须小于等于结束位置的缩进 (空行除外). IN: ast_tree: dict. {lino: [(obj_type, obj_val), ...], ...} lino: int. 行号, 从 1 开始数 obj_type: str. 对象类型, 例如 "<class 'FunctionDef'>" 等. 完整的支持 列表参考 src.utils.ast_helper.AstHelper#eval_node() obj_val: str/dict. 对象的值, 目前仅存在 str 或 dict 类型的数据. 示例: (str) 'print' (dict) (多用于描述 Import) {'src.downloader.Downloader': 'src.downloader.Downloader'} lino_indent: dict. {lino: indent, ...}. 由 AstHelper#create _lino_indent_dict() 提供 lino: int. 行号, 从 1 开始数 indent: int. 该行的列缩进位置, 为 4 的整数倍数, 如 0, 4, 8, 12 等 self.top_module: str. e.g. 'src.app' OT: module_linos: dict. {module: [lino, ...]} module: str. 模块的路径名. lino_list: list. 模块所涉及的行号列表, 已经过排序, 行号从 1 开始数, 最 大不超过当前 pyfile 的总代码行数. e.g. {'src.app.module': [1, 2, 3, 9, 10], 'src.app.main': [4, 5, 8], 'src.app.main.child_method': [6, 7], ...} 有了 module_linos 以后, 我们就可以在已知 module 的调用关系的情况下, 专注于 读取该 module 对应的区间范围, 逐行分析每条语句, 并进一步发现新的调用关系, 以 此产生裂变效应. 详见 src.analyser.VirtualRunner#main(). """ lk.logd('indexing module linos') if top_module == '': top_module = self.top_module if linos is None: linos = list(ast_tree.keys()) linos.sort() # ------------------------------------------------ def eval_ast_line(): ast_line = ast_tree[lino] # type: list # ast_line is type of list, assert it not empty. assert ast_line # here we only take its first element. return ast_line[0] # ast_defs: ast definitions ast_defs = ("<class '_ast.FunctionDef'>", "<class '_ast.ClassDef'>") indent_module_dict = {} # format: {indent: module} out = {} # format: {module: lino_list} last_indent = 0 for lino in linos: # lk.loga(lino) obj_type, obj_val = eval_ast_line() indent = ast_indents.get(lino, -1) """ special: indent 有一个特殊值 -1, 表示下面这种情况: def abc(): # -> indent = 0 print('aaa', # -> indent = 4 'bbb', # -> indent = 10 -> -1 'ccc') # -> indent = 10 -> -1 当 indent 为 -1 时, 则认为本行的 indent 保持与上次 indent 一致. """ if indent == -1: indent = last_indent assert obj_type not in ast_defs assert indent % 4 == 0, (lino, indent) """ 当 indent >= last_indent 时: 在 indent_holder 中开辟新键. 当 indent < last_indent 时: 从 indent_holder 更新并移除高缩进的键. """ lk.loga(lino, indent, obj_type, obj_val) # noinspection PyUnresolvedReferences parent_module = indent_module_dict.get(indent - 4, top_module) """ case 1: indent = 0, obj_val = 'main' -> indent - 4 = -4 -> -4 not in indent_module_dict. so assign default: parent_module = top_module = 'src.app' -> current_module = 'src.app.main' case 2: indent = 4, obj_val = 'child_method' -> indent - 4 = 0 -> parent_module = 'src.app.main' -> current_module = 'src.app.main.child_method' """ if obj_type in ast_defs: # obj_type = "<class 'FunctionDef'>", obj_val = 'main' current_module = parent_module + '.' + obj_val # lk.logt('[TEMPRINT]', current_module) # -> 'src.app.main' elif indent > 0: current_module = parent_module else: current_module = parent_module + '.' + 'module' # -> 'src.app.module' # lk.loga(parent_module, current_module) node = out.setdefault(current_module, []) node.append(lino) # NOTE: the lino is in ordered # update indent_module_dict indent_module_dict.update({indent: current_module}) # -> {0: 'src.app.main'}, {4: 'src.app.main.child_method'}, ... # update last_indent last_indent = indent # sort for lino_list in out.values(): lino_list.sort() # TEST print lk.loga(indent_module_dict) lk.logt('[I4204]', out) return out