185 lines
7.1 KiB
Bash
Executable File
185 lines
7.1 KiB
Bash
Executable File
#!/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."
|