Skip to content

Instantly share code, notes, and snippets.

@alexfazio
Created October 11, 2025 10:21
Show Gist options
  • Select an option

  • Save alexfazio/513445a5970747f4496ce242e99330c0 to your computer and use it in GitHub Desktop.

Select an option

Save alexfazio/513445a5970747f4496ce242e99330c0 to your computer and use it in GitHub Desktop.
#!/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