Closer/scripts/theme-scan.sh

185 lines
7.1 KiB
Bash
Raw Normal View History

#!/bin/bash
#
# CloserApp — automated theme-mismatch scanner (Pass C pre-check)
#
# ⛔ CLAUDE: You may improve this script whenever you discover a new failure mode
# for light/dark theme mismatches. Keep the script self-contained, runnable from
# the project root, and write findings to stdout + SCAN_OUTPUT. Update this header
# with any new patterns or exclusions you add.
#
# Exclusions:
# - Color.Transparent (intentional)
# - Theme.kt (the theme definition itself)
# - @Preview composables — a hardcoded color inside an `@Preview` function only affects
# Android Studio's design-time preview pane, never the shipped app, so it is NOT a theme
# defect. (Added 2026-06-28 after WheelCompleteScreen.kt `WheelRevealPreview` was filed as
# C-THEME-003, a false positive — the scanner now skips any hit whose enclosing `fun` is
# annotated `@Preview`.) See in_preview().
#
# Usage:
# ./scripts/theme-scan.sh > /tmp/claude-theme-scan-$(date +%Y%m%d).md
# ./scripts/theme-scan.sh --json # (future improvement — machine-readable)
#
set -euo pipefail
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
UI_DIR="$PROJECT_ROOT/app/src/main/java/app/closer/ui"
THEME_FILE="$PROJECT_ROOT/app/src/main/java/app/closer/ui/theme/Theme.kt"
SCAN_OUTPUT="${1:-}"
if [[ -z "$SCAN_OUTPUT" ]]; then
SCAN_OUTPUT="/tmp/claude-theme-scan-$(date +%Y%m%d).md"
fi
: > "$SCAN_OUTPUT"
log() {
echo "$1" | tee -a "$SCAN_OUTPUT"
}
# -----------------------------------------------------------------------------
# Helper: is file:lineno inside an @Preview composable? (design-time only → not a defect)
# Finds the nearest `fun ` at or above lineno, then checks the 4 lines above that `fun`
# for an @Preview annotation. Returns 0 (true) if in a preview.
# -----------------------------------------------------------------------------
in_preview() {
local file="$1" lineno="$2"
[[ -f "$file" ]] || return 1
local fun_line
fun_line=$(awk -v L="$lineno" 'NR<=L && /^[[:space:]]*(private |internal |public )?(suspend )?fun /{ln=NR} END{print ln}' "$file")
[[ -z "$fun_line" ]] && return 1
local start=$(( fun_line > 4 ? fun_line - 4 : 1 ))
sed -n "${start},${fun_line}p" "$file" | grep -q '@Preview' && return 0 || return 1
}
# Filter stdin (grep -rn formatted lines: file:lineno:...) → drop lines inside @Preview.
drop_previews() {
local line file lineno
while IFS= read -r line; do
file=$(echo "$line" | cut -d: -f1)
lineno=$(echo "$line" | cut -d: -f2)
in_preview "$file" "$lineno" && continue
echo "$line"
done
}
log "# CloserApp Theme-Mismatch Scan — $(date)"
log "Project: $PROJECT_ROOT"
log ""
# -----------------------------------------------------------------------------
# Helper: find lines that are within N lines after a container composable starts.
# $1 = regex for container opening
# $2 = severity emoji + label
# $3 = grep-invert exclusions (optional)
# -----------------------------------------------------------------------------
scan_after_container() {
local pattern="$1"
local label="$2"
local exclude="${3:-Color\.Transparent}"
local tmp_containers=$(mktemp)
grep -rnE "$pattern" "$UI_DIR" --include="*.kt" > "$tmp_containers" || true
while IFS= read -r container; do
local file=$(echo "$container" | cut -d: -f1)
local lineno=$(echo "$container" | cut -d: -f2)
if [[ ! -f "$file" ]]; then continue; fi
if in_preview "$file" "$lineno"; then continue; fi
local segment=$(sed -n "${lineno},$((lineno+7))p" "$file")
local match=$(echo "$segment" | grep -E '^\s*(color|containerColor|background)\s*=\s*Color(\.|\()' | grep -ivE "$exclude" | head -1 || true)
if [[ -n "$match" ]]; then
log "${label} ${file}:${lineno}"
log " ${match}"
fi
done < "$tmp_containers"
rm "$tmp_containers"
}
log "## Tier 1A — Container/surface colors that won't adapt (CRITICAL)"
log "Looks at Surface, Card, Dialog, AlertDialog, ModalBottomSheet, BottomSheet, Scaffold, LazyVerticalGrid, Box."
log ""
scan_after_container '(Surface|Card|Dialog|AlertDialog|ModalBottomSheet|BottomSheet|Scaffold|LazyVerticalGrid|Box)\s*\(' '🔴 CRITICAL' 'Color\.Transparent'
log ""
log "## Tier 1B — Modifier.background with hardcoded color (CRITICAL)"
grep -rnE 'Modifier\.background\(Color(\.|\()' "$UI_DIR" --include="*.kt" \
| grep -ivE 'Color\.Transparent' \
| grep -ivE 'Theme\.kt' \
| drop_previews \
| while IFS= read -r line; do
log "🔴 CRITICAL $line"
done || true
log ""
log "## Tier 1C — Component color overrides that won't adapt (MAJOR)"
grep -rnE '(buttonColors|TextFieldDefaults\.colors|TabRowDefaults\.colors|SwitchDefaults\.colors)\s*\(' "$UI_DIR" --include="*.kt" \
| grep -iE 'Color(\.|\()' \
| grep -ivE 'Color\.Transparent' \
| drop_previews \
| while IFS= read -r line; do
log "🟠 MAJOR $line"
done || true
grep -rnE '(Divider|HorizontalDivider)\s*\(' "$UI_DIR" --include="*.kt" \
| grep -iE 'color = Color(\.|\()' \
| drop_previews \
| while IFS= read -r line; do
log "🟠 MAJOR $line"
done || true
log ""
log "## Tier 1D — Text/Icon color hardcoded on themed surfaces (REVIEW)"
grep -rnE '^\s*color = Color\.(White|Black|(0x[0-9A-F]{8}))' "$UI_DIR" --include="*.kt" \
| grep -ivE 'Theme\.kt' \
| drop_previews \
| while IFS= read -r line; do
log "🟡 REVIEW $line"
done || true
log ""
log "## Tier 1E — Direct painterResource that bypasses BrandIllustration (MAJOR)"
grep -rnE 'painterResource\(R\.drawable\.(illustration_|pack_art_)' "$UI_DIR" --include="*.kt" \
| drop_previews \
| while IFS= read -r line; do
log "🟠 MAJOR $line"
done || true
log ""
log "## Tier 1F — Hardcoded border colors (REVIEW)"
grep -rnE 'Modifier\.border\([^)]*Color(\.|\()' "$UI_DIR" --include="*.kt" \
| grep -ivE 'Color\.Transparent' \
| drop_previews \
| while IFS= read -r line; do
log "🟡 REVIEW $line"
done || true
log ""
log "## Tier 1G — Hardcoded Brush/gradient stops (REVIEW)"
grep -rnE 'Brush\.(linear|vertical|horizontal|radial)Gradient' "$UI_DIR" --include="*.kt" \
| grep -vE 'BrandIllustration\.kt' \
| drop_previews \
| while IFS= read -r line; do
log "🟡 REVIEW $line"
done || true
log ""
log "## Summary"
critical=$(grep -c "🔴 CRITICAL" "$SCAN_OUTPUT" || true)
major=$(grep -c "🟠 MAJOR" "$SCAN_OUTPUT" || true)
review=$(grep -c "🟡 REVIEW" "$SCAN_OUTPUT" || true)
log "- 🔴 CRITICAL: $critical"
log "- 🟠 MAJOR: $major"
log "- 🟡 REVIEW: $review"
log ""
log "**Severity guide:**"
log "- 🔴 CRITICAL = container/surface/background set to a hardcoded color. Will produce visible light/dark mismatches."
log "- 🟠 MAJOR = component override or direct painterResource that likely bypasses theme adaptation or decoupled-theme art."
log "- 🟡 REVIEW = hardcoded text/icon/border/gradient color that may be correct on a branded container but must be verified in both themes."
log "- (@Preview composables are excluded — design-time only, never shipped.)"
log ""
log "---"
log "End of scan. Next step: file all CRITICAL and any MAJOR that break themes to ClaudeReport.md, update the Pass C"
log "coverage matrix in ClaudeQACoverage.md, then run the visual sweep. If you fixed hardcoded colors, re-run this"
log "script and confirm the count dropped."