#!/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()