213 lines
8.1 KiB
Python
213 lines
8.1 KiB
Python
#!/usr/bin/env python3
|
|
# Copyright (c) 2025 Patrick Motsch
|
|
# All rights reserved.
|
|
"""
|
|
Script to find unused functions in Python files.
|
|
Analyzes all .py files in the codebase and reports functions that are never called.
|
|
"""
|
|
|
|
import os
|
|
import re
|
|
import ast
|
|
from pathlib import Path
|
|
from typing import Dict, List, Set, Tuple
|
|
import logging
|
|
|
|
# Configure logging
|
|
logging.basicConfig(level=logging.INFO, format='%(levelname)s: %(message)s')
|
|
logger = logging.getLogger(__name__)
|
|
|
|
class FunctionUsageAnalyzer:
|
|
def __init__(self, root_dir: str):
|
|
self.root_dir = Path(root_dir)
|
|
self.all_functions: Dict[str, List[Tuple[str, str]]] = {} # function_name -> [(file_path, line_number)]
|
|
self.function_calls: Set[str] = set()
|
|
self.imports: Dict[str, Set[str]] = {} # file_path -> set of imported modules/classes
|
|
|
|
def find_python_files(self) -> List[Path]:
|
|
"""Find all Python files in the codebase."""
|
|
python_files = []
|
|
for py_file in self.root_dir.rglob("*.py"):
|
|
if "venv" not in str(py_file) and "env" not in str(py_file):
|
|
python_files.append(py_file)
|
|
return python_files
|
|
|
|
def extract_functions_from_file(self, file_path: Path) -> List[Tuple[str, int]]:
|
|
"""Extract function definitions from a Python file."""
|
|
functions = []
|
|
try:
|
|
with open(file_path, 'r', encoding='utf-8') as f:
|
|
content = f.read()
|
|
|
|
tree = ast.parse(content)
|
|
|
|
for node in ast.walk(tree):
|
|
if isinstance(node, ast.FunctionDef):
|
|
functions.append((node.name, node.lineno))
|
|
elif isinstance(node, ast.AsyncFunctionDef):
|
|
functions.append((node.name, node.lineno))
|
|
|
|
except Exception as e:
|
|
logger.warning(f"Error parsing {file_path}: {e}")
|
|
|
|
return functions
|
|
|
|
def extract_function_calls_from_file(self, file_path: Path) -> Set[str]:
|
|
"""Extract function calls from a Python file."""
|
|
calls = set()
|
|
try:
|
|
with open(file_path, 'r', encoding='utf-8') as f:
|
|
content = f.read()
|
|
|
|
tree = ast.parse(content)
|
|
|
|
for node in ast.walk(tree):
|
|
if isinstance(node, ast.Call):
|
|
if isinstance(node.func, ast.Name):
|
|
calls.add(node.func.id)
|
|
elif isinstance(node.func, ast.Attribute):
|
|
calls.add(node.func.attr)
|
|
|
|
except Exception as e:
|
|
logger.warning(f"Error parsing {file_path}: {e}")
|
|
|
|
return calls
|
|
|
|
def extract_imports_from_file(self, file_path: Path) -> Set[str]:
|
|
"""Extract imports from a Python file."""
|
|
imports = set()
|
|
try:
|
|
with open(file_path, 'r', encoding='utf-8') as f:
|
|
content = f.read()
|
|
|
|
tree = ast.parse(content)
|
|
|
|
for node in ast.walk(tree):
|
|
if isinstance(node, ast.Import):
|
|
for alias in node.names:
|
|
imports.add(alias.name)
|
|
elif isinstance(node, ast.ImportFrom):
|
|
if node.module:
|
|
imports.add(node.module)
|
|
for alias in node.names:
|
|
if alias.name != "*":
|
|
imports.add(alias.name)
|
|
|
|
except Exception as e:
|
|
logger.warning(f"Error parsing {file_path}: {e}")
|
|
|
|
return imports
|
|
|
|
def analyze_codebase(self):
|
|
"""Analyze the entire codebase for function definitions and usage."""
|
|
python_files = self.find_python_files()
|
|
logger.info(f"Found {len(python_files)} Python files")
|
|
|
|
# First pass: collect all function definitions
|
|
for file_path in python_files:
|
|
relative_path = file_path.relative_to(self.root_dir)
|
|
functions = self.extract_functions_from_file(file_path)
|
|
|
|
for func_name, line_num in functions:
|
|
if func_name not in self.all_functions:
|
|
self.all_functions[func_name] = []
|
|
self.all_functions[func_name].append((str(relative_path), line_num))
|
|
|
|
logger.info(f"Found {len(self.all_functions)} unique function definitions")
|
|
|
|
# Second pass: collect all function calls
|
|
for file_path in python_files:
|
|
calls = self.extract_function_calls_from_file(file_path)
|
|
self.function_calls.update(calls)
|
|
|
|
imports = self.extract_imports_from_file(file_path)
|
|
self.imports[str(file_path.relative_to(self.root_dir))] = imports
|
|
|
|
logger.info(f"Found {len(self.function_calls)} unique function calls")
|
|
|
|
def find_unused_functions(self) -> Dict[str, List[Tuple[str, str]]]:
|
|
"""Find functions that are never called."""
|
|
unused_functions = {}
|
|
|
|
for func_name, locations in self.all_functions.items():
|
|
# Skip special methods and common patterns
|
|
if (func_name.startswith('_') or
|
|
func_name.startswith('__') or
|
|
func_name in ['main', 'if __name__', 'test_', 'setup', 'teardown']):
|
|
continue
|
|
|
|
# Check if function is called anywhere
|
|
if func_name not in self.function_calls:
|
|
unused_functions[func_name] = locations
|
|
|
|
return unused_functions
|
|
|
|
def generate_report(self) -> str:
|
|
"""Generate a comprehensive report of unused functions."""
|
|
unused_functions = self.find_unused_functions()
|
|
|
|
report = []
|
|
report.append("=" * 80)
|
|
report.append("UNUSED FUNCTIONS REPORT")
|
|
report.append("=" * 80)
|
|
report.append(f"Total functions found: {len(self.all_functions)}")
|
|
report.append(f"Unused functions: {len(unused_functions)}")
|
|
report.append(f"Usage rate: {((len(self.all_functions) - len(unused_functions)) / len(self.all_functions) * 100):.1f}%")
|
|
report.append("")
|
|
|
|
if not unused_functions:
|
|
report.append("🎉 All functions are being used!")
|
|
return "\n".join(report)
|
|
|
|
# Group by file
|
|
by_file = {}
|
|
for func_name, locations in unused_functions.items():
|
|
for file_path, line_num in locations:
|
|
if file_path not in by_file:
|
|
by_file[file_path] = []
|
|
by_file[file_path].append((func_name, line_num))
|
|
|
|
# Sort by file path
|
|
for file_path in sorted(by_file.keys()):
|
|
report.append(f"📁 {file_path}")
|
|
report.append("-" * 60)
|
|
|
|
functions_in_file = by_file[file_path]
|
|
functions_in_file.sort(key=lambda x: x[1]) # Sort by line number
|
|
|
|
for func_name, line_num in functions_in_file:
|
|
report.append(f" Line {line_num:3d}: {func_name}")
|
|
|
|
report.append("")
|
|
|
|
# Summary by function name
|
|
report.append("SUMMARY BY FUNCTION NAME:")
|
|
report.append("-" * 60)
|
|
for func_name in sorted(unused_functions.keys()):
|
|
locations = unused_functions[func_name]
|
|
report.append(f"{func_name:<30} ({len(locations)} location{'s' if len(locations) > 1 else ''})")
|
|
|
|
return "\n".join(report)
|
|
|
|
def main():
|
|
"""Main function to run the analysis."""
|
|
# Get the gateway directory for a full codebase scan
|
|
scriptPath = Path(__file__).resolve()
|
|
gatewayPath = scriptPath.parent.parent
|
|
logger.info(f"Analyzing codebase in: {gatewayPath}")
|
|
|
|
analyzer = FunctionUsageAnalyzer(gatewayPath)
|
|
analyzer.analyze_codebase()
|
|
|
|
report = analyzer.generate_report()
|
|
print(report)
|
|
|
|
# Save report to file
|
|
report_file = gatewayPath / "unused_functions_report.txt"
|
|
with open(report_file, 'w', encoding='utf-8') as f:
|
|
f.write(report)
|
|
|
|
logger.info(f"Report saved to: {report_file}")
|
|
|
|
if __name__ == "__main__":
|
|
main()
|