Search code examples
pythonvisual-studio-codedead-code

List all python functions within a vscode workspace that are uncalled?


Context

Since I have a workspace with multiple python/github repositories, vulture does not return all dead Python code. So to find all uncalled functions, I use the following steps:

  1. Search all functions, with:
  2. CTRL+SHIFT+F, Alt+R, ^(\s*)(def [\w_]+\()
  3. Then I open all those search results in a new tab with: Alt+Enter.
  4. Then manually, for each function, I search with CTRL+SHIFT+F how often that function occurs in the search results, and if it occurs only once, I know it is dead code.

Since this is an iterative process, where deleting one function can sometimes make other functions uncalled, it becomes quite time consuming.

Question

How can one automatically return a list of all python functions (within a workspace in vscode)/(across a set of folders), that occur only once (at its creation with def function_name(.., whilst never being called)?

Assumption

I assume no duplicate function names exist within these projects.


Solution

  • As suggested by rioV8, here is a Python script that lists all uncalled functions in all .py scripts within the current directory and within any of its children*.

    • It skips files in build/lib directories. It ignores functions that start with: test_.
    import os
    from pprint import pprint
    from typing import List
    
    from typeguard import typechecked
    
    
    @typechecked
    def get_all_py_filepaths(root_dir: str) -> List[str]:
        """Returns a list of file paths for all Python files in root_dir and its
        subdirectories.
    
        Args:
            root_dir: The root directory to search for Python files.
    
        Returns:
            A list of file paths for all Python files in root_dir and its
            subdirectories.
        """
        filepaths = []
        for dir_name, subdir_list, file_list in os.walk(root_dir):
            for file_name in file_list:
                if file_name.endswith(".py"):
                    full_path = os.path.join(dir_name, file_name)
                    if "build/lib" not in full_path:
                        filepaths.append(full_path)
        return list(set(filepaths))
    
    
    @typechecked
    def find_function_calls(
        filepath: str, found_function_calls: List[str]
    ) -> List[str]:
        """Finds all function calls in the Python file at filepath and adds them to
        found_function_calls.
    
        Args:
            filepath: The file path of the Python file to search for function
            calls.
            found_function_calls: A list of function calls that have already been
            found.
    
        Returns:
            A list of function calls found in the Python file at filepath.
        """
        with open(filepath) as f:
            code = f.read()
    
        tree = ast.parse(code)
        for node in ast.walk(tree):
            if isinstance(node, ast.Call):
                if "func" in node.__dict__:
                    if "id" in node.func.__dict__:
                        if node.func.id not in found_function_calls:
                            found_function_calls.append(node.func.id)
                    if "attr" in node.func.__dict__:
                        if node.func.attr not in found_function_calls:
                            found_function_calls.append(node.func.attr)
        return found_function_calls
    
    
    @typechecked
    def get_func_declarations(filepath: str) -> List[str]:
        """Returns a list of function names declared in the Python file at
        filepath.
    
        Args:
            filepath: The file path of the Python file to search for function
            declarations.
    
        Returns:
            A list of function names declared in the Python file at filepath.
        """
        declared_functions = []
        with open(filepath) as f:
            tree = ast.parse(f.read())
    
        for node in ast.walk(tree):
            if isinstance(node, ast.FunctionDef):
                if node.name not in ["__init__", "get"]:
                    declared_functions.append(node.name)
        return declared_functions
    
    
    # Get a list of file paths for all Python files in the current directory and
    # its sub
    filepaths = sorted(get_all_py_filepaths(os.getcwd()))
    
    # Get a list of declared functions from all Python files
    declared_functions = []
    for filepath in filepaths:
        declared_functions.extend(get_func_declarations(filepath))
    declared_functions = sorted(list(set(declared_functions)))
    
    # Get a list of found function calls from all Python files
    found_function_calls = []
    for filepath in filepaths:
        found_function_calls = find_function_calls(filepath, found_function_calls)
    found_function_calls = sorted(list(set(found_function_calls)))
    
    # Remove any remaining duplicates.
    declared = set(declared_functions)
    found = set(found_function_calls)
    
    # Get a list of uncalled functions
    uncalled_functions = list(declared - found)
    
    # Filter out functions that are test functions (i.e. start with "test_")
    dead_functions = [
        uncalled_function
        for uncalled_function in uncalled_functions
        if uncalled_function[:5] != "test_"
    ]
    
    # Print the list of dead functions
    pprint(dead_functions)