gateway/tool_showUnusedFunctions.py

210 lines
8 KiB
Python

#!/usr/bin/env python3
"""
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 directory where this script is located
script_dir = Path(__file__).parent
logger.info(f"Analyzing codebase in: {script_dir}")
analyzer = FunctionUsageAnalyzer(script_dir)
analyzer.analyze_codebase()
report = analyzer.generate_report()
print(report)
# Save report to file
report_file = script_dir / "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()