1

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.

a.t.
  • 2,002
  • 3
  • 26
  • 66
  • 1
    how do you discriminate between `A.draw()` and `B.draw()`? Go to a function and Context Click and select **Go to References**, but what if the function is called outside the module – rioV8 Dec 09 '22 at 15:24
  • 1
    write a Python script that does these searches and counting for you – rioV8 Dec 09 '22 at 15:25
  • 1
    Q: So is it the case that you cannot checkout out all your repos locally then analyze them in a single call to vulture with all the source directories passed as arguments? – The Lazy Graybeard Dec 09 '22 at 16:12
  • Yes, it only returned 2 functions with a 60% confidence (and no others), whereas in the initial manual sweeps I identified over 13 functions that were uncalled. – a.t. Dec 09 '22 at 16:32
  • Search in [**OUTLINE**](https://imgur.com/fjQHEXw.png) view? – JialeDu Dec 12 '22 at 07:05
  • @JialeDu Thank you for the suggestion, the Outline returns: active editor cannot provide outline information if no file is opened. There is no 1 file that calls/spans all files and functions. Do you have a suggestion on how to perform the query anyway? – a.t. Dec 12 '22 at 14:25
  • With ***Lint*** enabled, unused functions will appear as warnings in the **PRONLEMS** panel. or change `"python.analysis.typeCheckingMode"` to `"strict"`. – JialeDu Dec 14 '22 at 06:49

1 Answers1

0

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)

a.t.
  • 2,002
  • 3
  • 26
  • 66