#!/usr/bin/env bash # ============================================================================== # Python Linting Script - Run ALL linters with aggressive settings # ============================================================================== # Usage: # ./lint_python.sh # Lint all Python files # ./lint_python.sh --fix # Lint and auto-fix where possible # ./lint_python.sh # Lint specific file # ./lint_python.sh --quick # Quick lint (ruff + mypy only) # ./lint_python.sh --report # Generate detailed reports # ============================================================================== set -euo pipefail # Colors for output RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' MAGENTA='\033[0;35m' CYAN='\033[0;36m' NC='\033[0m' # No Color BOLD='\033[1m' # Configuration SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_ROOT="${SCRIPT_DIR}" PYTHON_PATHS=( "PYTHON" "articles" "poker-modifier-app" "tests" ) EXCLUDE_PATHS=( ".venv" "__pycache__" ".git" "Bash/ffmpeg-build" ".pytest_cache" ".ruff_cache" ".mypy_cache" ) # Build exclude pattern for find EXCLUDE_PATTERN="" for path in "${EXCLUDE_PATHS[@]}"; do EXCLUDE_PATTERN="${EXCLUDE_PATTERN} -path '*/${path}/*' -prune -o" done # Parse arguments FIX_MODE=false QUICK_MODE=false REPORT_MODE=false TARGET_FILES="" while [[ $# -gt 0 ]]; do case $1 in --fix|-f) FIX_MODE=true shift ;; --quick|-q) QUICK_MODE=true shift ;; --report|-r) REPORT_MODE=true shift ;; --help|-h) echo "Usage: $0 [OPTIONS] [FILES...]" echo "" echo "Options:" echo " --fix, -f Auto-fix issues where possible" echo " --quick, -q Quick mode (ruff + mypy only)" echo " --report, -r Generate detailed reports to ./lint-reports/" echo " --help, -h Show this help message" echo "" echo "Examples:" echo " $0 # Lint all Python files" echo " $0 --fix # Lint and auto-fix" echo " $0 PYTHON/ # Lint specific directory" echo " $0 --quick --fix # Quick lint with auto-fix" exit 0 ;; *) TARGET_FILES="${TARGET_FILES} $1" shift ;; esac done # If no target specified, use default paths if [[ -z "${TARGET_FILES}" ]]; then TARGET_FILES="${PYTHON_PATHS[*]}" fi # Create reports directory if needed if [[ "${REPORT_MODE}" == true ]]; then mkdir -p "${PROJECT_ROOT}/lint-reports" fi # Track overall status OVERALL_STATUS=0 FAILED_TOOLS=() # ============================================================================== # Helper functions # ============================================================================== print_header() { echo "" echo -e "${BOLD}${BLUE}══════════════════════════════════════════════════════════════${NC}" echo -e "${BOLD}${BLUE} $1${NC}" echo -e "${BOLD}${BLUE}══════════════════════════════════════════════════════════════${NC}" } print_subheader() { echo "" echo -e "${CYAN}──────────────────────────────────────────────────────────────${NC}" echo -e "${CYAN} $1${NC}" echo -e "${CYAN}──────────────────────────────────────────────────────────────${NC}" } print_success() { echo -e "${GREEN}✓${NC} $1" } print_warning() { echo -e "${YELLOW}⚠${NC} $1" } print_error() { echo -e "${RED}✗${NC} $1" } print_info() { echo -e "${BLUE}ℹ${NC} $1" } run_tool() { local tool_name="$1" local tool_cmd="$2" local report_file="${PROJECT_ROOT}/lint-reports/${tool_name}.txt" print_subheader "Running ${tool_name}..." if [[ "${REPORT_MODE}" == true ]]; then if eval "${tool_cmd}" 2>&1 | tee "${report_file}"; then print_success "${tool_name} passed" return 0 else print_error "${tool_name} found issues (see ${report_file})" FAILED_TOOLS+=("${tool_name}") return 1 fi else if eval "${tool_cmd}"; then print_success "${tool_name} passed" return 0 else print_error "${tool_name} found issues" FAILED_TOOLS+=("${tool_name}") return 1 fi fi } check_tool() { if command -v "$1" &> /dev/null; then return 0 else print_warning "$1 not found, skipping..." return 1 fi } # ============================================================================== # Main linting process # ============================================================================== print_header "Python Linting Suite - Aggressive Mode" echo "" print_info "Target: ${TARGET_FILES}" print_info "Fix mode: ${FIX_MODE}" print_info "Quick mode: ${QUICK_MODE}" print_info "Report mode: ${REPORT_MODE}" cd "${PROJECT_ROOT}" # ============================================================================== # RUFF - Primary linter and formatter # ============================================================================== if check_tool ruff; then if [[ "${FIX_MODE}" == true ]]; then run_tool "ruff-lint" "ruff check --fix --show-fixes ${TARGET_FILES}" || OVERALL_STATUS=1 run_tool "ruff-format" "ruff format ${TARGET_FILES}" || OVERALL_STATUS=1 else run_tool "ruff-lint" "ruff check ${TARGET_FILES}" || OVERALL_STATUS=1 run_tool "ruff-format-check" "ruff format --check ${TARGET_FILES}" || OVERALL_STATUS=1 fi fi # ============================================================================== # MYPY - Static type checking # ============================================================================== if check_tool mypy; then run_tool "mypy" "mypy --strict --ignore-missing-imports ${TARGET_FILES}" || OVERALL_STATUS=1 fi # Quick mode exits here if [[ "${QUICK_MODE}" == true ]]; then print_header "Quick Lint Complete" if [[ ${#FAILED_TOOLS[@]} -gt 0 ]]; then print_error "Failed tools: ${FAILED_TOOLS[*]}" exit 1 else print_success "All quick checks passed!" exit 0 fi fi # ============================================================================== # PYLINT - Comprehensive linting # ============================================================================== if check_tool pylint; then run_tool "pylint" "pylint --rcfile=pyproject.toml --jobs=0 --fail-under=0 ${TARGET_FILES}" || OVERALL_STATUS=1 fi # ============================================================================== # BANDIT - Security linting # ============================================================================== if check_tool bandit; then run_tool "bandit" "bandit -c pyproject.toml -r ${TARGET_FILES} --severity-level low --confidence-level low" || OVERALL_STATUS=1 fi # ============================================================================== # VULTURE - Dead code detection # ============================================================================== if check_tool vulture; then run_tool "vulture" "vulture --min-confidence 80 ${TARGET_FILES}" || OVERALL_STATUS=1 fi # ============================================================================== # FLAKE8 - Traditional linter # ============================================================================== if check_tool flake8; then run_tool "flake8" "flake8 --max-line-length=88 --extend-ignore=E203,W503 --max-complexity=10 ${TARGET_FILES}" || OVERALL_STATUS=1 fi # ============================================================================== # PYCODESTYLE - PEP 8 style checker # ============================================================================== if check_tool pycodestyle; then run_tool "pycodestyle" "pycodestyle --max-line-length=88 --ignore=E203,W503 ${TARGET_FILES}" || OVERALL_STATUS=1 fi # ============================================================================== # PYDOCSTYLE - Docstring style checker # ============================================================================== if check_tool pydocstyle; then run_tool "pydocstyle" "pydocstyle --convention=google ${TARGET_FILES}" || OVERALL_STATUS=1 fi # ============================================================================== # RADON - Complexity metrics # ============================================================================== if check_tool radon; then print_subheader "Running radon (complexity analysis)..." echo "" echo -e "${MAGENTA}Cyclomatic Complexity:${NC}" radon cc -a -s ${TARGET_FILES} || true echo "" echo -e "${MAGENTA}Maintainability Index:${NC}" radon mi -s ${TARGET_FILES} || true if [[ "${REPORT_MODE}" == true ]]; then radon cc -a -s ${TARGET_FILES} > "${PROJECT_ROOT}/lint-reports/radon-cc.txt" 2>&1 || true radon mi -s ${TARGET_FILES} > "${PROJECT_ROOT}/lint-reports/radon-mi.txt" 2>&1 || true fi fi # ============================================================================== # INTERROGATE - Docstring coverage # ============================================================================== if check_tool interrogate; then run_tool "interrogate" "interrogate -v --fail-under=0 ${TARGET_FILES}" || OVERALL_STATUS=1 fi # ============================================================================== # PYRIGHT - Microsoft's type checker (optional, very strict) # ============================================================================== if check_tool pyright; then run_tool "pyright" "pyright ${TARGET_FILES}" || OVERALL_STATUS=1 fi # ============================================================================== # AUTOFLAKE - Unused imports/variables (fix mode only) # ============================================================================== if [[ "${FIX_MODE}" == true ]] && check_tool autoflake; then print_subheader "Running autoflake (removing unused imports)..." find ${TARGET_FILES} -name "*.py" -type f -exec autoflake --in-place --remove-all-unused-imports --remove-unused-variables {} \; print_success "autoflake completed" fi # ============================================================================== # PYUPGRADE - Upgrade Python syntax (fix mode only) # ============================================================================== if [[ "${FIX_MODE}" == true ]] && check_tool pyupgrade; then print_subheader "Running pyupgrade (upgrading syntax to Python 3.10+)..." find ${TARGET_FILES} -name "*.py" -type f -exec pyupgrade --py310-plus {} \; print_success "pyupgrade completed" fi # ============================================================================== # CODESPELL - Spell checking # ============================================================================== if check_tool codespell; then if [[ "${FIX_MODE}" == true ]]; then run_tool "codespell" "codespell -w --skip='*.json,*.lock,.git,__pycache__,.venv' ${TARGET_FILES}" || OVERALL_STATUS=1 else run_tool "codespell" "codespell --skip='*.json,*.lock,.git,__pycache__,.venv' ${TARGET_FILES}" || OVERALL_STATUS=1 fi fi # ============================================================================== # Summary # ============================================================================== print_header "Linting Summary" echo "" if [[ ${OVERALL_STATUS} -ne 0 ]]; then print_error "The following tools reported issues:" for tool in "${FAILED_TOOLS[@]}"; do echo " - ${tool}" done echo "" if [[ "${REPORT_MODE}" == true ]]; then print_info "Detailed reports saved to: ${PROJECT_ROOT}/lint-reports/" fi print_info "Run with --fix to auto-fix issues where possible" exit ${OVERALL_STATUS} else print_success "All linting checks passed!" exit 0 fi