Created
October 11, 2025 10:21
-
-
Save alexfazio/513445a5970747f4496ce242e99330c0 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/bin/bash | |
| # Enhanced Vulture Dead Code Analysis Script with Dynamic Analysis Insights | |
| # Combines Vulture with ast-grep and optional Python analysis for comprehensive dead code detection | |
| # Configuration | |
| VULTURE_CONFIDENCE=${VULTURE_CONFIDENCE:-100} | |
| SRC_DIR=${SRC_DIR:-./src} | |
| TEST_PATTERNS="test_|_test\.py|tests/|test/|conftest\.py" | |
| VERBOSE=${VERBOSE:-0} | |
| # Verbosity levels: | |
| # 0 = Minimal (just verdicts) | |
| # 1 = Show commands | |
| # 2 = Add code snippets and relationships | |
| # 3 = Full context with dynamic analysis insights | |
| # Color codes for better readability (disabled if not terminal) | |
| if [ -t 1 ]; then | |
| BOLD='\033[1m' | |
| DIM='\033[2m' | |
| RED='\033[0;31m' | |
| GREEN='\033[0;32m' | |
| YELLOW='\033[0;33m' | |
| BLUE='\033[0;34m' | |
| MAGENTA='\033[0;35m' | |
| CYAN='\033[0;36m' | |
| RESET='\033[0m' | |
| else | |
| BOLD='' | |
| DIM='' | |
| RED='' | |
| GREEN='' | |
| YELLOW='' | |
| BLUE='' | |
| MAGENTA='' | |
| CYAN='' | |
| RESET='' | |
| fi | |
| # Function to execute commands with optional verbose output | |
| run_cmd() { | |
| if [ "$VERBOSE" -ge 1 ]; then | |
| echo -e "${DIM} CMD: $*${RESET}" >&2 | |
| fi | |
| eval "$@" | |
| } | |
| # Function to print section headers | |
| print_header() { | |
| echo "" | |
| echo -e "${BOLD}$1${RESET}" | |
| echo "$(echo "$1" | sed 's/./=/g')" | |
| } | |
| # Function to print subsection headers | |
| print_subheader() { | |
| echo "" | |
| echo -e "${BOLD}$1${RESET}" | |
| echo "$(echo "$1" | sed 's/./-/g')" | |
| } | |
| # Function to show code context | |
| show_code_context() { | |
| local file=$1 | |
| local line_num=$2 | |
| local symbol=$3 | |
| local context_lines=${4:-2} | |
| if [ "$VERBOSE" -ge 2 ] && [ -f "$file" ]; then | |
| echo "" | |
| echo -e "${CYAN}Code Context:${RESET}" | |
| # Ensure line_num is numeric | |
| if ! [[ "$line_num" =~ ^[0-9]+$ ]]; then | |
| echo " [Error: Invalid line number: $line_num]" | |
| return | |
| fi | |
| # Calculate line range | |
| start_line=$((line_num - context_lines)) | |
| [ "$start_line" -lt 1 ] && start_line=1 | |
| end_line=$((line_num + context_lines)) | |
| # Get total lines in file | |
| total_lines=$(wc -l < "$file" | tr -d ' ') | |
| [ "$end_line" -gt "$total_lines" ] && end_line=$total_lines | |
| # Show the code with line numbers, highlighting the target line | |
| sed -n "${start_line},${end_line}p" "$file" | nl -v "$start_line" | while IFS= read -r line; do | |
| line_no=$(echo "$line" | awk '{print $1}') | |
| # Skip if no line number extracted | |
| [ -z "$line_no" ] && continue | |
| code_line=$(echo "$line" | cut -d$'\t' -f2-) | |
| if [ "$line_no" -eq "$line_num" ]; then | |
| echo -e "${YELLOW}>> $line_no: $code_line${RESET}" | |
| else | |
| echo " $line_no: $code_line" | |
| fi | |
| done | |
| fi | |
| } | |
| # Function to find containing function/class (portable version) | |
| find_containing_context() { | |
| local file=$1 | |
| local line_num=$2 | |
| local symbol=$3 | |
| local item_type=${4:-""} | |
| if [ "$VERBOSE" -ge 2 ] && [ -f "$file" ]; then | |
| # Ensure line_num is numeric | |
| if ! [[ "$line_num" =~ ^[0-9]+$ ]]; then | |
| return | |
| fi | |
| # For parameters, look for the function definition very close to the line | |
| if [ "$item_type" = "parameter" ]; then | |
| # Check lines immediately before for a function definition | |
| local check_start=$((line_num - 5)) | |
| [ "$check_start" -lt 1 ] && check_start=1 | |
| # Look for function definition near the parameter | |
| local func_def=$(sed -n "${check_start},${line_num}p" "$file" | grep -E "^[[:space:]]*(def |class |async def )" | tail -1) | |
| if [ -n "$func_def" ]; then | |
| local name=$(echo "$func_def" | sed -E 's/^[[:space:]]*(def |class |async def )([a-zA-Z_][a-zA-Z0-9_]*).*/\2/') | |
| if [ -n "$name" ]; then | |
| # Find the actual line number of this definition | |
| local def_line=$(grep -n "$func_def" "$file" 2>/dev/null | head -1 | cut -d: -f1) | |
| [ -n "$def_line" ] && echo -e "${CYAN}Defined within:${RESET} $name (line $def_line)" | |
| return | |
| fi | |
| fi | |
| fi | |
| # For non-parameters, find the nearest containing function/class | |
| local context_info="" | |
| local context_line="" | |
| local current_line=1 | |
| while IFS= read -r line; do | |
| if [ "$current_line" -lt "$line_num" ]; then | |
| if echo "$line" | grep -E "^[[:space:]]*(def |class |async def )" >/dev/null 2>&1; then | |
| # Extract function/class name | |
| local name=$(echo "$line" | sed -E 's/^[[:space:]]*(def |class |async def )([a-zA-Z_][a-zA-Z0-9_]*).*/\2/') | |
| if [ -n "$name" ]; then | |
| context_info="$name" | |
| context_line="$current_line" | |
| fi | |
| fi | |
| fi | |
| current_line=$((current_line + 1)) | |
| done < "$file" | |
| if [ -n "$context_info" ]; then | |
| echo -e "${CYAN}Defined within:${RESET} $context_info (line $context_line)" | |
| fi | |
| fi | |
| } | |
| # Function to detect if symbol is a parameter | |
| is_function_parameter() { | |
| local file=$1 | |
| local line_num=$2 | |
| local symbol=$3 | |
| if [ -f "$file" ] && [[ "$line_num" =~ ^[0-9]+$ ]]; then | |
| # Get the line and check if it's part of a function definition | |
| local target_line=$(sed -n "${line_num}p" "$file" 2>/dev/null) | |
| # Check if this line or nearby lines contain a function definition with this parameter | |
| local check_start=$((line_num - 2)) | |
| [ "$check_start" -lt 1 ] && check_start=1 | |
| local check_end=$((line_num + 2)) | |
| # Extract the function definition area and check for the symbol as a parameter | |
| local func_def=$(sed -n "${check_start},${check_end}p" "$file" 2>/dev/null | tr '\n' ' ') | |
| if echo "$func_def" | grep -E "(def |async def )[^(]*\([^)]*\b$symbol\b[^)]*\)" >/dev/null 2>&1; then | |
| return 0 # It's a parameter | |
| fi | |
| fi | |
| return 1 # Not a parameter | |
| } | |
| # Function to find import relationships | |
| find_import_relationships() { | |
| local symbol=$1 | |
| if [ "$VERBOSE" -ge 2 ]; then | |
| echo "" | |
| echo -e "${CYAN}Import Relationships:${RESET}" | |
| # Check if symbol is imported elsewhere | |
| import_count=$(grep -r "from .* import.*$symbol\|^import.*$symbol" "$SRC_DIR" --include="*.py" 2>/dev/null | wc -l | tr -d ' ') | |
| if [ "$import_count" -gt 0 ]; then | |
| echo " Found $import_count import statement(s):" | |
| grep -r "from .* import.*$symbol\|^import.*$symbol" "$SRC_DIR" --include="*.py" 2>/dev/null | head -3 | while IFS= read -r line; do | |
| echo " - $line" | |
| done | |
| [ "$import_count" -gt 3 ] && echo " ... and $((import_count - 3)) more" | |
| else | |
| echo " No imports found" | |
| fi | |
| # Check if symbol itself is an import | |
| if [ -f "$2" ]; then | |
| is_import=$(grep "^from .* import.*$symbol\|^import.*$symbol" "$2" 2>/dev/null | head -1) | |
| [ -n "$is_import" ] && echo -e " ${YELLOW}Symbol is imported:${RESET} $is_import" | |
| fi | |
| fi | |
| } | |
| # Function to detect mock usage (simple grep-based) | |
| detect_mock_usage() { | |
| local symbol=$1 | |
| local test_dir="${SRC_DIR}" | |
| # Look for mock patterns in test files | |
| local mock_patterns=( | |
| "@patch.*['\"].*$symbol['\"]" | |
| "@mock\.patch.*['\"].*$symbol" | |
| "Mock.*name=['\"]$symbol" | |
| "MagicMock.*name=['\"]$symbol" | |
| "@patch\.object.*['\"]$symbol['\"]" | |
| ) | |
| local total_mocks=0 | |
| local mock_locations="" | |
| for pattern in "${mock_patterns[@]}"; do | |
| local count=$(grep -r "$pattern" "$test_dir" --include="*test*.py" 2>/dev/null | wc -l | tr -d ' ') | |
| total_mocks=$((total_mocks + count)) | |
| if [ "$count" -gt 0 ] && [ "$VERBOSE" -ge 3 ]; then | |
| local locations=$(grep -r "$pattern" "$test_dir" --include="*test*.py" 2>/dev/null | head -2) | |
| [ -n "$locations" ] && mock_locations="${mock_locations}\n${locations}" | |
| fi | |
| done | |
| echo "$total_mocks" | |
| [ "$VERBOSE" -ge 3 ] && [ -n "$mock_locations" ] && echo -e "$mock_locations" >&2 | |
| } | |
| # Function to run Python dynamic analysis if available | |
| run_python_analysis() { | |
| local symbol=$1 | |
| local file=$2 | |
| local line_num=$3 | |
| local item_type=$4 | |
| # Check if Python is available | |
| if ! command -v python3 >/dev/null 2>&1; then | |
| return 1 | |
| fi | |
| # Create temporary Python script for analysis | |
| cat > /tmp/vulture_dynamic_$$.py << 'PYTHON_SCRIPT' | |
| import sys | |
| import json | |
| import ast | |
| import os | |
| import re | |
| def analyze_symbol(symbol, file_path, line_num, item_type): | |
| """Analyze symbol for dynamic patterns without changing verdicts""" | |
| insights = { | |
| 'observations': [], | |
| 'patterns_found': [], | |
| 'questions_for_reviewer': [], | |
| 'search_suggestions': [] | |
| } | |
| try: | |
| with open(file_path, 'r') as f: | |
| source = f.read() | |
| tree = ast.parse(source, filename=file_path) | |
| # Check for dynamic access patterns | |
| class DynamicPatternVisitor(ast.NodeVisitor): | |
| def visit_Call(self, node): | |
| # Check for getattr/hasattr/setattr | |
| if isinstance(node.func, ast.Name): | |
| if node.func.id in ('getattr', 'hasattr', 'setattr'): | |
| if len(node.args) >= 2: | |
| if isinstance(node.args[1], ast.Constant): | |
| if node.args[1].value == symbol: | |
| insights['observations'].append( | |
| f"Dynamic attribute access: {node.func.id}(obj, '{symbol}')" | |
| ) | |
| # Check for locals()/vars() usage | |
| if isinstance(node.func, ast.Name): | |
| if node.func.id in ('locals', 'vars', 'globals'): | |
| insights['observations'].append( | |
| f"Function uses {node.func.id}() - may access '{symbol}' dynamically" | |
| ) | |
| insights['questions_for_reviewer'].append( | |
| f"Check if {node.func.id}() is used to access '{symbol}'" | |
| ) | |
| self.generic_visit(node) | |
| DynamicPatternVisitor().visit(tree) | |
| # Look for decorators on the containing function | |
| for node in ast.walk(tree): | |
| if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)): | |
| if hasattr(node, 'lineno') and abs(node.lineno - int(line_num)) < 5: | |
| if node.decorator_list: | |
| decorator_names = [] | |
| for dec in node.decorator_list: | |
| if isinstance(dec, ast.Name): | |
| decorator_names.append(dec.id) | |
| elif isinstance(dec, ast.Attribute): | |
| decorator_names.append(dec.attr) | |
| if decorator_names: | |
| insights['observations'].append( | |
| f"Function has decorators: {', '.join(decorator_names)}" | |
| ) | |
| # Check for common framework decorators | |
| framework_decorators = ['route', 'app', 'api', 'task', 'action', 'receiver'] | |
| if any(d in str(decorator_names).lower() for d in framework_decorators): | |
| insights['questions_for_reviewer'].append( | |
| "Framework decorator detected - parameter signature may be required" | |
| ) | |
| # Check if symbol appears in string literals (possible dynamic usage) | |
| string_pattern = re.compile(r'["\']' + re.escape(symbol) + r'["\']') | |
| if string_pattern.search(source): | |
| insights['patterns_found'].append({ | |
| 'type': 'string_literal', | |
| 'description': f"'{symbol}' appears in string literals" | |
| }) | |
| insights['search_suggestions'].append( | |
| f"grep -n 'getattr.*{symbol}\\|hasattr.*{symbol}' {file_path}" | |
| ) | |
| except Exception as e: | |
| insights['observations'].append(f"Analysis error: {str(e)}") | |
| return insights | |
| # Main execution | |
| if len(sys.argv) != 5: | |
| print(json.dumps({'error': 'Invalid arguments'})) | |
| sys.exit(1) | |
| symbol = sys.argv[1] | |
| file_path = sys.argv[2] | |
| line_num = sys.argv[3] | |
| item_type = sys.argv[4] | |
| insights = analyze_symbol(symbol, file_path, line_num, item_type) | |
| print(json.dumps(insights)) | |
| PYTHON_SCRIPT | |
| # Run Python analysis | |
| local result=$(python3 /tmp/vulture_dynamic_$$.py "$symbol" "$file" "$line_num" "$item_type" 2>/dev/null) | |
| rm -f /tmp/vulture_dynamic_$$.py | |
| echo "$result" | |
| } | |
| # Function to display dynamic analysis insights | |
| display_dynamic_insights() { | |
| local symbol=$1 | |
| local file=$2 | |
| local line_num=$3 | |
| local item_type=$4 | |
| echo "" | |
| echo -e "${CYAN}══════ Dynamic Analysis Insights ══════${RESET}" | |
| echo -e "${DIM}(Advisory only - does not affect verdict)${RESET}" | |
| echo "" | |
| # Check for mock usage | |
| local mock_count=$(detect_mock_usage "$symbol") | |
| if [ "$mock_count" -gt 0 ]; then | |
| echo -e "${BLUE}Mock Detection:${RESET}" | |
| echo " • Symbol is mocked in $mock_count test location(s)" | |
| if [ "$VERBOSE" -ge 3 ]; then | |
| echo " ? Consider: Is this a test seam or interface requirement?" | |
| fi | |
| echo "" | |
| fi | |
| # Check for decorators (simple grep approach) | |
| if [ -f "$file" ]; then | |
| local decorators=$(grep -B3 "def $symbol\|class $symbol" "$file" 2>/dev/null | grep "^[[:space:]]*@" | sed 's/^[[:space:]]*//' | tr '\n' ', ' | sed 's/, $//') | |
| if [ -n "$decorators" ]; then | |
| echo -e "${BLUE}Decorator Analysis:${RESET}" | |
| echo " • Has decorators: $decorators" | |
| echo " ? Consider: Do these decorators require specific signatures?" | |
| echo "" | |
| fi | |
| fi | |
| # Run Python analysis if available and verbose >= 3 | |
| if [ "$VERBOSE" -ge 3 ]; then | |
| local python_result=$(run_python_analysis "$symbol" "$file" "$line_num" "$item_type") | |
| if [ -n "$python_result" ] && [ "$python_result" != "" ]; then | |
| # Parse and display Python insights | |
| echo -e "${BLUE}Advanced Analysis:${RESET}" | |
| # Extract observations | |
| local observations=$(echo "$python_result" | python3 -c " | |
| import sys, json | |
| try: | |
| data = json.load(sys.stdin) | |
| for obs in data.get('observations', []): | |
| print(f' • {obs}') | |
| except: pass | |
| " 2>/dev/null) | |
| [ -n "$observations" ] && echo "$observations" | |
| # Extract patterns | |
| local patterns=$(echo "$python_result" | python3 -c " | |
| import sys, json | |
| try: | |
| data = json.load(sys.stdin) | |
| for p in data.get('patterns_found', []): | |
| print(f\" ◦ {p.get('type', 'unknown')}: {p.get('description', '')}\") | |
| except: pass | |
| " 2>/dev/null) | |
| [ -n "$patterns" ] && echo "$patterns" | |
| # Extract questions | |
| local questions=$(echo "$python_result" | python3 -c " | |
| import sys, json | |
| try: | |
| data = json.load(sys.stdin) | |
| for q in data.get('questions_for_reviewer', []): | |
| print(f' ? {q}') | |
| except: pass | |
| " 2>/dev/null) | |
| [ -n "$questions" ] && echo -e "\n${YELLOW}Review Questions:${RESET}\n$questions" | |
| # Extract search suggestions | |
| local suggestions=$(echo "$python_result" | python3 -c " | |
| import sys, json | |
| try: | |
| data = json.load(sys.stdin) | |
| for s in data.get('search_suggestions', []): | |
| print(f' $ {s}') | |
| except: pass | |
| " 2>/dev/null) | |
| [ -n "$suggestions" ] && echo -e "\n${DIM}Suggested Commands:${RESET}\n$suggestions" | |
| fi | |
| fi | |
| # Additional quick checks | |
| echo "" | |
| echo -e "${BLUE}Quick Checks:${RESET}" | |
| # Check if symbol name suggests it's required | |
| if echo "$symbol" | grep -qE "^(callback|handler|listener|observer|_)"; then | |
| echo " • Symbol name suggests it might be a required interface" | |
| fi | |
| # Check file location | |
| if echo "$file" | grep -qE "(handlers?|callbacks?|signals?|events?|listeners?)"; then | |
| echo " • File path suggests event/callback handling" | |
| fi | |
| # For parameters, check if function name suggests callbacks | |
| if [ "$item_type" = "parameter" ]; then | |
| local func_context=$(grep -B1 "$symbol" "$file" 2>/dev/null | grep -E "(def |async def )" | head -1) | |
| if echo "$func_context" | grep -qE "(handle|process|on_|callback|signal)"; then | |
| echo " • Function name suggests this is a handler/callback" | |
| fi | |
| fi | |
| echo -e "${CYAN}════════════════════════════════════════${RESET}" | |
| } | |
| # Function to analyze call chain | |
| analyze_call_chain() { | |
| local symbol=$1 | |
| local file=$2 | |
| local depth=${3:-0} | |
| local max_depth=3 | |
| if [ "$VERBOSE" -ge 3 ] && [ "$depth" -lt "$max_depth" ]; then | |
| # Find what this symbol calls | |
| local indent="" | |
| for ((i=0; i<depth; i++)); do indent=" $indent"; done | |
| # Look for function calls within this symbol's definition | |
| if [ -f "$file" ]; then | |
| # Get the function body (simplified - gets next 20 lines after definition) | |
| local def_line=$(grep -n "def $symbol\|class $symbol" "$file" 2>/dev/null | head -1 | cut -d: -f1) | |
| if [ -n "$def_line" ] && [[ "$def_line" =~ ^[0-9]+$ ]]; then | |
| local end_line=$((def_line + 20)) | |
| # Extract potential function calls | |
| sed -n "${def_line},${end_line}p" "$file" 2>/dev/null | grep -o '[a-zA-Z_][a-zA-Z0-9_]*(' | sed 's/($//' | sort -u | while read -r called_func; do | |
| # Check if the called function is also in the dead code list | |
| if grep -q "^$called_func$" potential_dead.txt 2>/dev/null; then | |
| echo -e "${indent}${YELLOW}└── calls $called_func() [ALSO DEAD]${RESET}" | |
| # Recursive call for chain analysis | |
| analyze_call_chain "$called_func" "$file" $((depth + 1)) | |
| elif [ "$depth" -eq 0 ]; then | |
| # Only show active calls at the first level | |
| echo -e "${indent}${GREEN}└── calls $called_func() [ACTIVE]${RESET}" | head -5 | |
| fi | |
| done | |
| fi | |
| fi | |
| fi | |
| } | |
| # Function to find similar symbols | |
| find_similar_symbols() { | |
| local symbol=$1 | |
| local file=$2 | |
| if [ "$VERBOSE" -ge 2 ]; then | |
| echo "" | |
| echo -e "${CYAN}Related Symbols:${RESET}" | |
| # Extract base name (remove common suffixes/prefixes) | |
| local base_name=$(echo "$symbol" | sed 's/_test$//' | sed 's/^test_//' | sed 's/_helper$//' | sed 's/^_//') | |
| # Find similar symbols | |
| similar_count=0 | |
| ast-grep --pattern "${base_name}" --lang python 2>/dev/null | grep -o "${base_name}[a-zA-Z0-9_]*" | sort -u | while read -r similar; do | |
| if [ "$similar" != "$symbol" ] && [ -n "$similar" ]; then | |
| # Check if similar symbol is also dead | |
| if grep -q "^$similar$" potential_dead.txt 2>/dev/null; then | |
| echo -e " - ${YELLOW}$similar [ALSO DEAD]${RESET}" | |
| similar_count=$((similar_count + 1)) | |
| elif [ "$similar_count" -lt 3 ]; then | |
| echo " - $similar" | |
| similar_count=$((similar_count + 1)) | |
| fi | |
| fi | |
| done | |
| [ "$similar_count" -eq 0 ] && echo " None found" | |
| fi | |
| } | |
| # Function to show usage examples | |
| show_usage_examples() { | |
| local symbol=$1 | |
| local limit=${2:-3} | |
| if [ "$VERBOSE" -ge 2 ]; then | |
| echo "" | |
| echo -e "${CYAN}Usage Examples:${RESET}" | |
| # Find actual usage (excluding definitions) | |
| usage_found=0 | |
| ast-grep --pattern "$symbol" --lang python 2>/dev/null | grep -v "def.*$symbol\|class.*$symbol" | head -"$limit" | while IFS=: read -r file line_num content; do | |
| if [ -n "$file" ] && [ -n "$line_num" ]; then | |
| usage_found=$((usage_found + 1)) | |
| echo -e " ${MAGENTA}$file:$line_num${RESET}" | |
| echo " $content" | head -c 100 | |
| echo "" | |
| fi | |
| done | |
| if [ "$usage_found" -eq 0 ]; then | |
| echo " No usage found outside of definition" | |
| fi | |
| fi | |
| } | |
| # Main header | |
| echo "" | |
| echo "======================================================" | |
| echo " VULTURE DEAD CODE ANALYSIS WITH DYNAMIC INSIGHTS " | |
| echo "======================================================" | |
| echo "Configuration:" | |
| echo " Source directory: $SRC_DIR" | |
| echo " Confidence level: $VULTURE_CONFIDENCE%" | |
| echo " Verbose level: $VERBOSE (0=minimal, 1=commands, 2=context, 3=full+dynamic)" | |
| echo "" | |
| # Step 1: Run Vulture | |
| print_header "STEP 1: Running Vulture Analysis" | |
| cmd="uv run vulture \"$SRC_DIR\" --min-confidence \"$VULTURE_CONFIDENCE\"" | |
| [ "$VERBOSE" -ge 1 ] && echo "Executing: $cmd" | |
| echo "" | |
| uv run vulture "$SRC_DIR" --min-confidence "$VULTURE_CONFIDENCE" > vulture_raw.txt 2>&1 | |
| if [ ! -s vulture_raw.txt ]; then | |
| echo -e "${GREEN}No dead code detected by Vulture${RESET}" | |
| rm -f vulture_raw.txt | |
| exit 0 | |
| fi | |
| # Filter out syntax errors and other non-dead-code messages | |
| grep -E "unused (function|method|class|variable|property|attribute)" vulture_raw.txt > vulture_output.txt 2>/dev/null || true | |
| if [ ! -s vulture_output.txt ]; then | |
| echo "Vulture found only syntax issues or no dead code with confidence >= ${VULTURE_CONFIDENCE}%" | |
| echo "" | |
| [ "$VERBOSE" -ge 1 ] && echo "Vulture output:" && cat vulture_raw.txt | head -10 | sed 's/^/ /' | |
| rm -f vulture_raw.txt vulture_output.txt | |
| exit 0 | |
| fi | |
| line_count=$(wc -l < vulture_output.txt | tr -d ' ') | |
| echo -e "${YELLOW}Vulture found $line_count potential dead code items${RESET}" | |
| echo "" | |
| [ "$VERBOSE" -ge 1 ] && echo "Sample findings:" && head -3 vulture_output.txt | sed 's/^/ /' | |
| # Step 2: Extract symbols | |
| print_header "STEP 2: Extracting and Categorizing Symbols" | |
| # Parse Vulture output more carefully | |
| while IFS= read -r line; do | |
| # Skip empty lines | |
| [ -z "$line" ] && continue | |
| # Extract filename (everything before first colon) | |
| file=$(echo "$line" | cut -d: -f1) | |
| # Extract line number (between first and second colon) | |
| line_num=$(echo "$line" | cut -d: -f2) | |
| # Extract symbol (text between single quotes) | |
| symbol=$(echo "$line" | sed -n "s/.*'\([^']*\)'.*/\1/p") | |
| # Skip if symbol is empty or looks like a syntax error | |
| if [ -n "$symbol" ] && [ -n "$file" ] && [ "$symbol" != ")" ] && [ "$symbol" != "(" ]; then | |
| # Extract the type of unused item | |
| item_type="unknown" | |
| echo "$line" | grep -q "unused function" && item_type="function" | |
| echo "$line" | grep -q "unused method" && item_type="method" | |
| echo "$line" | grep -q "unused class" && item_type="class" | |
| echo "$line" | grep -q "unused variable" && item_type="variable" | |
| echo "$line" | grep -q "unused property" && item_type="property" | |
| echo "$line" | grep -q "unused attribute" && item_type="attribute" | |
| # Check if it's actually a parameter | |
| if [ "$item_type" = "variable" ] && is_function_parameter "$file" "$line_num" "$symbol"; then | |
| item_type="parameter" | |
| fi | |
| echo "${symbol}|${file}|${line_num}|${item_type}" | |
| fi | |
| done < vulture_output.txt | sort -u > symbols_with_context.txt | |
| symbol_count=$(wc -l < symbols_with_context.txt | tr -d ' ') | |
| echo "Extracted $symbol_count unique symbols for analysis" | |
| echo "" | |
| # Show categorized symbols | |
| echo "Breakdown by type:" | |
| for type in function method class variable parameter property attribute; do | |
| count=$(grep "|${type}$" symbols_with_context.txt 2>/dev/null | wc -l | tr -d ' ') | |
| [ "$count" -gt 0 ] && echo " - ${type}s: $count" | |
| done | |
| # Build dead symbol list | |
| cut -d'|' -f1 symbols_with_context.txt | sort -u > potential_dead.txt | |
| # Step 3: Detailed Analysis | |
| print_header "STEP 3: Detailed Symbol Analysis" | |
| counter=0 | |
| total_safe_to_remove=0 | |
| total_needs_review=0 | |
| total_false_positives=0 | |
| # Create temporary results file | |
| > analysis_results.txt | |
| while IFS='|' read -r symbol source_file line_num item_type; do | |
| [ -z "$symbol" ] && continue | |
| counter=$((counter + 1)) | |
| # Special handling for parameters | |
| if [ "$item_type" = "parameter" ]; then | |
| print_subheader "[$counter/$symbol_count] $symbol (function parameter)" | |
| else | |
| print_subheader "[$counter/$symbol_count] $symbol ($item_type)" | |
| fi | |
| # Convert to absolute path | |
| if command -v realpath >/dev/null 2>&1; then | |
| abs_path=$(realpath "$source_file" 2>/dev/null || echo "$source_file") | |
| else | |
| abs_path=$(cd "$(dirname "$source_file")" 2>/dev/null && pwd)/$(basename "$source_file") || echo "$source_file" | |
| fi | |
| echo "Location: ${abs_path}:${line_num}" | |
| # Show containing context (function/class) | |
| find_containing_context "$source_file" "$line_num" "$symbol" "$item_type" | |
| # Show code context | |
| show_code_context "$source_file" "$line_num" "$symbol" 2 | |
| # Initialize analysis variables | |
| verdict="UNUSED" | |
| verdict_reasons="" | |
| risk_level="LOW" | |
| checks_performed="" | |
| # Special handling for parameters | |
| if [ "$item_type" = "parameter" ]; then | |
| echo "" | |
| echo -e "${YELLOW}Note: This is a function parameter${RESET}" | |
| # Check if it's a signal handler or callback | |
| if echo "$symbol" | grep -qE "^(frame|event|context|request|response|args|kwargs|_)$"; then | |
| verdict="CONVENTION" | |
| verdict_reasons="Common parameter name for callbacks/handlers. " | |
| risk_level="HIGH" | |
| fi | |
| # Check if function is decorated (might be required by framework) | |
| if [ -f "$source_file" ]; then | |
| func_line=$(grep -B2 "$symbol" "$source_file" | grep -E "^[[:space:]]*(def |async def )" | head -1) | |
| if [ -n "$func_line" ]; then | |
| decorator_above=$(grep -B1 "$func_line" "$source_file" | head -1 | grep "^[[:space:]]*@") | |
| if [ -n "$decorator_above" ]; then | |
| verdict="FRAMEWORK_CODE" | |
| verdict_reasons="${verdict_reasons}Function is decorated (parameter may be required). " | |
| risk_level="HIGH" | |
| fi | |
| fi | |
| fi | |
| fi | |
| # Check 1: String literal usage | |
| checks_performed="${checks_performed}string_literals," | |
| string_usage=$(run_cmd "grep -r \"\\\"$symbol\\\"|'$symbol'\" \"$SRC_DIR\" --include=\"*.py\" 2>/dev/null | grep -v \"^[[:space:]]*#\" | wc -l | tr -d ' '") | |
| if [ "$string_usage" -gt 0 ]; then | |
| verdict="DYNAMIC_USAGE" | |
| verdict_reasons="${verdict_reasons}Found in $string_usage string literal(s) (possible dynamic usage). " | |
| risk_level="HIGH" | |
| [ "$VERBOSE" -ge 1 ] && echo " [!] Found in string literals: $string_usage times" | |
| if [ "$VERBOSE" -ge 2 ]; then | |
| echo -e "${CYAN} String literal usage:${RESET}" | |
| grep -r "\"$symbol\"|'$symbol'" "$SRC_DIR" --include="*.py" 2>/dev/null | grep -v "^[[:space:]]*#" | head -2 | while read -r line; do | |
| echo " ${line:0:100}..." | |
| done | |
| fi | |
| fi | |
| # Check 2: Decorator usage | |
| checks_performed="${checks_performed}decorators," | |
| decorator_check=$(run_cmd "ast-grep --pattern \"@$symbol\" --lang python 2>/dev/null | wc -l | tr -d ' '") | |
| if [ "$decorator_check" -gt 0 ]; then | |
| verdict="FRAMEWORK_CODE" | |
| verdict_reasons="${verdict_reasons}Used as decorator $decorator_check time(s). " | |
| risk_level="HIGH" | |
| [ "$VERBOSE" -ge 1 ] && echo " [!] Used as decorator" | |
| fi | |
| # Check 3: Check if symbol is decorated | |
| if [ -f "$source_file" ] && [ "$item_type" != "parameter" ]; then | |
| checks_performed="${checks_performed}is_decorated," | |
| symbol_line=$(run_cmd "grep -n \"\\(def\\|class\\) $symbol\" \"$source_file\" 2>/dev/null | head -1 | cut -d: -f1") | |
| if [ -n "$symbol_line" ] && [[ "$symbol_line" =~ ^[0-9]+$ ]] && [ "$symbol_line" -gt 1 ]; then | |
| prev_line=$((symbol_line - 1)) | |
| decorator_above=$(run_cmd "sed -n \"${prev_line}p\" \"$source_file\" 2>/dev/null | grep \"^[[:space:]]*@\"") | |
| if [ -n "$decorator_above" ]; then | |
| verdict="FRAMEWORK_CODE" | |
| decorator_name=$(echo "$decorator_above" | sed 's/^[[:space:]]*@//' | cut -d'(' -f1) | |
| verdict_reasons="${verdict_reasons}Has decorator @${decorator_name}. " | |
| risk_level="HIGH" | |
| [ "$VERBOSE" -ge 1 ] && echo " [!] Decorated with: $decorator_above" | |
| fi | |
| fi | |
| fi | |
| # Check 4: __all__ exports | |
| checks_performed="${checks_performed}exports," | |
| in_all=$(run_cmd "grep -r \"__all__\" \"$SRC_DIR\" --include=\"*.py\" 2>/dev/null | grep \"['\\\"]$symbol['\\\"]\" | wc -l | tr -d ' '") | |
| if [ "$in_all" -gt 0 ]; then | |
| verdict="PUBLIC_API" | |
| verdict_reasons="${verdict_reasons}Exported in __all__. " | |
| risk_level="HIGH" | |
| [ "$VERBOSE" -ge 1 ] && echo " [!] Exported in __all__" | |
| fi | |
| # Check if in __init__.py | |
| if [[ "$source_file" == *"__init__.py" ]]; then | |
| verdict="PUBLIC_API" | |
| verdict_reasons="${verdict_reasons}Defined in __init__.py. " | |
| risk_level="HIGH" | |
| fi | |
| # Import relationships | |
| find_import_relationships "$symbol" "$source_file" | |
| # Check 5: Usage statistics | |
| checks_performed="${checks_performed}usage_stats" | |
| total_usage=$(run_cmd "ast-grep --pattern \"$symbol\" --lang python 2>/dev/null | wc -l | tr -d ' '") | |
| test_usage=$(run_cmd "ast-grep --pattern \"$symbol\" --lang python 2>/dev/null | grep -E \"$TEST_PATTERNS\" | wc -l | tr -d ' '") | |
| prod_usage=$((total_usage - test_usage)) | |
| # Count definitions and handle variables/parameters specially | |
| if [ "$item_type" = "variable" ] || [ "$item_type" = "parameter" ] || [ "$item_type" = "attribute" ] || [ "$item_type" = "property" ]; then | |
| # For variables/parameters, check if symbol appears at the exact location Vulture reported | |
| occurrences_at_definition=$(run_cmd "ast-grep --pattern \"$symbol\" --lang python 2>/dev/null | grep \"^${source_file}:${line_num}:\" | wc -l | tr -d ' '") | |
| if [ "$occurrences_at_definition" -gt 0 ]; then | |
| definitions=1 | |
| else | |
| # Count simple assignments as definitions | |
| definitions=$(run_cmd "ast-grep --pattern \"$symbol = \$_\" --lang python 2>/dev/null | wc -l | tr -d ' '") | |
| # Also count augmented assignments | |
| aug_assignments=$(run_cmd "ast-grep --pattern \"$symbol += \$_\" --lang python 2>/dev/null | wc -l | tr -d ' '") | |
| definitions=$((definitions + aug_assignments)) | |
| # If still 0 but Vulture found it, there must be at least 1 definition | |
| [ "$definitions" -eq 0 ] && [ "$total_usage" -gt 0 ] && definitions=1 | |
| fi | |
| else | |
| # For functions/classes, look for def/class declarations | |
| definitions=$(run_cmd "ast-grep --pattern \"$symbol\" --lang python 2>/dev/null | grep -E \"^[^:]+:[0-9]+:.*\\(def\\|class\\|async def\\)\" | wc -l | tr -d ' '") | |
| fi | |
| # Calculate actual usage (total minus definitions) | |
| actual_prod_usage=$((prod_usage - definitions)) | |
| actual_test_usage=$test_usage | |
| # Ensure we don't have negative usage | |
| [ "$actual_prod_usage" -lt 0 ] && actual_prod_usage=0 | |
| echo "" | |
| echo "Usage Analysis:" | |
| echo " Total occurrences: $total_usage" | |
| echo " Definitions: $definitions" | |
| echo " Production references: $actual_prod_usage" | |
| echo " Test references: $actual_test_usage" | |
| # Show usage examples | |
| show_usage_examples "$symbol" 3 | |
| # Determine verdict based on usage | |
| if [ "$actual_test_usage" -gt 0 ] && [ "$actual_prod_usage" -eq 0 ]; then | |
| verdict="TEST_ONLY" | |
| verdict_reasons="${verdict_reasons}Only used in tests. " | |
| risk_level="MEDIUM" | |
| elif [ "$actual_prod_usage" -gt 0 ] && [ "$verdict" = "UNUSED" ]; then | |
| # Check for dead code chains | |
| callers=$(run_cmd "ast-grep --pattern \"$symbol\" --lang python 2>/dev/null | grep -v \"\\(def\\|class\\).*$symbol\" | cut -d: -f1 | sort -u") | |
| if [ -n "$callers" ]; then | |
| dead_chain=true | |
| for caller_file in $callers; do | |
| # Simple check: if any caller is not in our dead code list, it's not a dead chain | |
| if ! grep -q "$(basename \"$caller_file\")" symbols_with_context.txt 2>/dev/null; then | |
| dead_chain=false | |
| break | |
| fi | |
| done | |
| if [ "$dead_chain" = true ]; then | |
| verdict="DEAD_CHAIN" | |
| verdict_reasons="${verdict_reasons}Part of dead code chain. " | |
| risk_level="LOW" | |
| # Show call chain analysis | |
| if [ "$VERBOSE" -ge 3 ]; then | |
| echo "" | |
| echo -e "${CYAN}Call Chain Analysis:${RESET}" | |
| analyze_call_chain "$symbol" "$source_file" 0 | |
| fi | |
| else | |
| verdict="IN_USE" | |
| risk_level="HIGH" | |
| fi | |
| fi | |
| fi | |
| # Find similar symbols | |
| find_similar_symbols "$symbol" "$source_file" | |
| # Check 6: Framework patterns | |
| if [[ "$symbol" == *"View" ]] || [[ "$symbol" == *"Model" ]] || [[ "$symbol" == *"Serializer" ]] || [[ "$symbol" == *"Form" ]]; then | |
| risk_level="HIGH" | |
| verdict_reasons="${verdict_reasons}Follows framework naming convention. " | |
| fi | |
| # Check for route definitions in file | |
| if [ -f "$source_file" ]; then | |
| route_count=$(grep -c "@.*route\|@app\.\|@api\.\|@router\." "$source_file" 2>/dev/null | head -1 || echo 0) | |
| if [ "$route_count" -gt 0 ]; then | |
| risk_level="HIGH" | |
| verdict_reasons="${verdict_reasons}File contains route definitions. " | |
| fi | |
| fi | |
| # Final verdict output | |
| echo "" | |
| echo "Verdict: $verdict" | |
| echo "Risk Level: $risk_level" | |
| [ -n "$verdict_reasons" ] && [ "$VERBOSE" -ge 1 ] && echo "Details: $verdict_reasons" | |
| # Show dynamic analysis insights if verbose >= 2 | |
| # This is advisory only and doesn't change the verdict | |
| if [ "$VERBOSE" -ge 2 ]; then | |
| display_dynamic_insights "$symbol" "$source_file" "$line_num" "$item_type" | |
| fi | |
| echo "" | |
| echo -n "Recommendation: " | |
| case "$verdict" in | |
| UNUSED) | |
| if [ "$risk_level" = "LOW" ]; then | |
| if [ "$item_type" = "parameter" ]; then | |
| echo -e "${YELLOW}REVIEW${RESET} - Unused parameter (may be required by interface)" | |
| total_needs_review=$((total_needs_review + 1)) | |
| echo "$symbol|REVIEW|$source_file|$line_num|$item_type" >> analysis_results.txt | |
| else | |
| echo -e "${GREEN}SAFE TO REMOVE${RESET}" | |
| total_safe_to_remove=$((total_safe_to_remove + 1)) | |
| echo "$symbol|REMOVE|$source_file|$line_num|$item_type" >> analysis_results.txt | |
| fi | |
| else | |
| echo -e "${YELLOW}REVIEW CAREFULLY${RESET}" | |
| total_needs_review=$((total_needs_review + 1)) | |
| echo "$symbol|REVIEW|$source_file|$line_num|$item_type" >> analysis_results.txt | |
| fi | |
| ;; | |
| CONVENTION) | |
| echo -e "${YELLOW}REVIEW${RESET} - Common parameter convention" | |
| total_needs_review=$((total_needs_review + 1)) | |
| echo "$symbol|CONVENTION|$source_file|$line_num|$item_type" >> analysis_results.txt | |
| ;; | |
| DYNAMIC_USAGE|FRAMEWORK_CODE|PUBLIC_API) | |
| echo -e "${RED}DO NOT REMOVE${RESET} - Framework/Dynamic/Public API" | |
| total_false_positives=$((total_false_positives + 1)) | |
| echo "$symbol|KEEP|$source_file|$line_num|$item_type" >> analysis_results.txt | |
| ;; | |
| TEST_ONLY) | |
| echo -e "${YELLOW}REVIEW${RESET} - Test-only code" | |
| total_needs_review=$((total_needs_review + 1)) | |
| echo "$symbol|TEST_ONLY|$source_file|$line_num|$item_type" >> analysis_results.txt | |
| ;; | |
| DEAD_CHAIN) | |
| echo -e "${GREEN}REMOVE WITH CHAIN${RESET} - Part of dead code chain" | |
| total_safe_to_remove=$((total_safe_to_remove + 1)) | |
| echo "$symbol|REMOVE_CHAIN|$source_file|$line_num|$item_type" >> analysis_results.txt | |
| ;; | |
| IN_USE) | |
| echo -e "${RED}FALSE POSITIVE${RESET} - Code is actively used" | |
| total_false_positives=$((total_false_positives + 1)) | |
| echo "$symbol|FALSE_POSITIVE|$source_file|$line_num|$item_type" >> analysis_results.txt | |
| ;; | |
| *) | |
| echo -e "${YELLOW}MANUAL REVIEW REQUIRED${RESET}" | |
| total_needs_review=$((total_needs_review + 1)) | |
| echo "$symbol|UNCLEAR|$source_file|$line_num|$item_type" >> analysis_results.txt | |
| ;; | |
| esac | |
| [ "$VERBOSE" -ge 1 ] && echo "Checks performed: ${checks_performed}" | |
| done < symbols_with_context.txt | |
| # Step 4: Summary Report | |
| print_header "SUMMARY REPORT" | |
| echo "" | |
| echo "Analysis Statistics:" | |
| echo " Total symbols analyzed: $symbol_count" | |
| echo -e " ${GREEN}Safe to remove: $total_safe_to_remove${RESET}" | |
| echo -e " ${YELLOW}Needs review: $total_needs_review${RESET}" | |
| echo -e " ${RED}False positives: $total_false_positives${RESET}" | |
| if [ -s analysis_results.txt ]; then | |
| echo "" | |
| echo "Actionable Items:" | |
| echo "" | |
| # Safe to remove | |
| if grep -q "|REMOVE|" analysis_results.txt 2>/dev/null; then | |
| echo -e "${GREEN}[SAFE TO REMOVE]${RESET}" | |
| grep "|REMOVE|" analysis_results.txt | while IFS='|' read -r sym action file line_no type; do | |
| echo " - $sym ($type) in $file:$line_no" | |
| done | |
| fi | |
| # Remove as chain | |
| if grep -q "|REMOVE_CHAIN|" analysis_results.txt 2>/dev/null; then | |
| echo "" | |
| echo -e "${GREEN}[REMOVE AS DEAD CODE CHAIN]${RESET}" | |
| grep "|REMOVE_CHAIN|" analysis_results.txt | while IFS='|' read -r sym action file line_no type; do | |
| echo " - $sym ($type) in $file:$line_no" | |
| done | |
| fi | |
| # Parameter conventions | |
| if grep -q "|CONVENTION|" analysis_results.txt 2>/dev/null; then | |
| echo "" | |
| echo -e "${YELLOW}[PARAMETER CONVENTIONS - REVIEW]${RESET}" | |
| grep "|CONVENTION|" analysis_results.txt | while IFS='|' read -r sym action file line_no type; do | |
| echo " - $sym ($type) in $file:$line_no" | |
| done | |
| fi | |
| # Test only | |
| if grep -q "|TEST_ONLY|" analysis_results.txt 2>/dev/null; then | |
| echo "" | |
| echo -e "${YELLOW}[TEST-ONLY CODE]${RESET}" | |
| grep "|TEST_ONLY|" analysis_results.txt | while IFS='|' read -r sym action file line_no type; do | |
| echo " - $sym ($type) in $file:$line_no" | |
| done | |
| fi | |
| # Needs review | |
| if grep -q "|REVIEW|" analysis_results.txt 2>/dev/null; then | |
| echo "" | |
| echo -e "${YELLOW}[NEEDS MANUAL REVIEW]${RESET}" | |
| grep "|REVIEW|" analysis_results.txt | while IFS='|' read -r sym action file line_no type; do | |
| echo " - $sym ($type) in $file:$line_no" | |
| done | |
| fi | |
| # False positives / Keep | |
| if grep -q -E "\\|(KEEP|FALSE_POSITIVE)\\|" analysis_results.txt 2>/dev/null; then | |
| echo "" | |
| echo -e "${RED}[FALSE POSITIVES - DO NOT REMOVE]${RESET}" | |
| grep -E "\\|(KEEP|FALSE_POSITIVE)\\|" analysis_results.txt | while IFS='|' read -r sym action file line_no type; do | |
| echo " - $sym ($type) in $file:$line_no" | |
| done | |
| fi | |
| fi | |
| # Cleanup | |
| rm -f vulture_raw.txt vulture_output.txt symbols_with_context.txt potential_dead.txt analysis_results.txt | |
| echo "" | |
| echo "======================================================" | |
| echo "Analysis complete!" | |
| echo "" | |
| echo "Verbosity levels:" | |
| echo " VERBOSE=0 - Minimal output (default)" | |
| echo " VERBOSE=1 - Show commands and details" | |
| echo " VERBOSE=2 - Add code context and basic dynamic insights" | |
| echo " VERBOSE=3 - Full analysis with advanced dynamic insights" | |
| echo "" | |
| echo "Dynamic insights are advisory only and do not affect verdicts." | |
| echo "They provide additional context for manual review." | |
| echo "" | |
| echo "Example: VERBOSE=3 $0" | |
| echo "======================================================" | |
| echo "" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment