'For each function in the function_manager, try to determine if it returns or not. A function does not return if
it calls another function that is known to be not returning, and this function does not have other exits.
We might as well analyze other features of functions in the future.
A function does not return if
a) it is a SimProcedure that has NO_RET being True,
or
b) it is completely recovered (i.e. every block of this function has been recovered, and no future block will
be added to it), and it does not have a ret or any equivalent instruction.
A function returns if any of its block contains a ret instruction or any equivalence.'

def _analyze_function_features(self):

    changes = {'functions_return': [], 'functions_do_not_return': []}
    if (self._changed_functions is not None):
        all_functions = self._changed_functions
        caller_functions = set()
        for func_addr in self._changed_functions:
            if (func_addr not in self.kb.functions.callgraph):
                continue
            callers = self.kb.functions.callgraph.predecessors(func_addr)
            for f in callers:
                caller_functions.add(f)
        all_functions |= caller_functions
        all_functions = [self.kb.functions.function(addr=f) for f in all_functions if (f in self.kb.functions)]
    else:
        all_functions = self.kb.functions.values()
    for func in all_functions:
        if (func.returning is not None):
            continue
        if func.has_return:
            changes['functions_return'].append(func)
            func.returning = True
            continue
        if self.project.is_hooked(func.addr):
            procedure = self.project.hooked_by(func.addr)
        else:
            try:
                procedure = self.project._simos.syscall_from_addr(func.addr, allow_unsupported=False)
            except AngrUnsupportedSyscallError:
                procedure = None
        if ((procedure is not None) and hasattr(procedure, 'NO_RET')):
            if procedure.NO_RET:
                func.returning = False
                changes['functions_do_not_return'].append(func)
            else:
                func.returning = True
                changes['functions_return'].append(func)
            continue
        if (func.addr not in self._completed_functions):
            continue
        if (not func.block_addrs_set):
            func.returning = True
            changes['functions_return'].append(func)
            continue
        bail_out = False
        for jump_out_site in func.jumpout_sites:
            if func.returning:
                continue
            jump_out_site_successors = jump_out_site.successors()
            if (not jump_out_site_successors):
                bail_out = True
                continue
            jump_out_target = jump_out_site_successors[0]
            if (not self.kb.functions.contains_addr(jump_out_target.addr)):
                bail_out = True
                continue
            target_func = self.kb.functions[jump_out_target.addr]
            if (target_func.returning is True):
                func.returning = True
                changes['functions_return'].append(func)
                bail_out = True
            elif (target_func.returning is None):
                bail_out = True
        if bail_out:
            continue
        func.returning = False
        changes['functions_do_not_return'].append(func)
    return changes
