Beispiel #1
0
 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
Beispiel #2
0
    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
Beispiel #3
0
 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])
Beispiel #5
0
 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)
Beispiel #7
0
 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
Beispiel #9
0
 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')
Beispiel #10
0
    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()
Beispiel #11
0
    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)})
Beispiel #12
0
    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
Beispiel #14
0
 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
Beispiel #15
0
    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