code: def _compute_function_attributes(cfg):
          
          # the attributes we use are the number of basic blocks, number of edges, and number of subfunction calls
          attributes = dict()
          all_funcs = set(cfg.kb.callgraph.nodes())
          for function_addr in cfg.kb.functions:
              # skip syscalls and functions which are None in the cfg
              if cfg.kb.functions.function(function_addr) is None or cfg.kb.functions.function(function_addr).is_syscall:
                  continue
              if cfg.kb.functions.function(function_addr) is not None:
                  normalized_funtion = NormalizedFunction(cfg.kb.functions.function(function_addr))
                  number_of_basic_blocks = len(normalized_funtion.graph.nodes())
                  number_of_edges = len(normalized_funtion.graph.edges())
              else:
                  number_of_basic_blocks = 0
                  number_of_edges = 0
              if function_addr in all_funcs:
                  number_of_subfunction_calls = len(list(cfg.kb.callgraph.successors(function_addr)))
              else:
                  number_of_subfunction_calls = 0
              attributes[function_addr] = (number_of_basic_blocks, number_of_edges, number_of_subfunction_calls)
  
          return attributes
docs: :param cfg: An angr CFG object
          :returns:    a dictionary of function addresses to tuples of attributes