audit(#622): sweep hardcoded colors + add checkColorLiterals guard (#626)

Removed docs/color-hardcoded-audit.md per request. Replaced 2 hardcoded
Color literals with theme tokens; added CI guard blocking new literals.

Co-authored-by: Hy4ri <hy4ri@users.noreply.github.com>
This commit is contained in:
M57
2026-07-18 16:15:45 +03:00
committed by GitHub
co-authored by Hy4ri
parent 0018e5d172
commit 9661246497
4 changed files with 75 additions and 3 deletions
+3
View File
@@ -65,6 +65,9 @@ jobs:
- name: Run ktlintCheck
run: ./gradlew ktlintCheck
- name: Run checkColorLiterals (hardcoded Color guard, issue #622)
run: ./gradlew checkColorLiterals
- name: Upload ktlint reports
if: always()
uses: actions/upload-artifact@v7
+69
View File
@@ -209,6 +209,75 @@ tasks.register<Exec>("ktlintFormat") {
commandLine("sh", "-c", "ktlint -F \"app/src/**/*.kt\"")
}
// ── Hardcoded Color Guard (issue #622) ────────────────────────────────────
//
// Fails the build if a hardcoded Color literal (Color(0x...) or named
// Color.White/Black/Red/Green/Gray/etc.) is introduced outside the theme
// module, *Preview.kt files, and _test sources. Pairing/Auth screens are
// exempt via the per-path overlay list below.
//
// Acceptable replacements (see docs/color-hardcoded-audit.md):
// - MaterialTheme.colorScheme.<token>
// - LocalHermesStatusColors.current.<semantic>
// - Color.Transparent / Color.Unspecified (intentional)
// Resolve paths at configuration time (config-cache compatible).
val colorGuardSrcDir = layout.projectDirectory
.dir("src/main/java/com/m57/hermescontrol")
val colorGuardExemptions = listOf(
"/theme/",
"PairingScreen.kt",
"AuthLoginScreen.kt",
"Preview.kt",
)
val colorGuardHexPattern = Regex("""Color\(\s*0x[0-9A-Fa-f]{6,8}\s*\)""")
val colorGuardNamedPattern = Regex("""Color\.(White|Black|Red|Green|Gray|LightGray|DarkGray|Yellow|Blue|Cyan|Magenta)\b""")
tasks.register("checkColorLiterals") {
group = "verification"
description = "Fails if hardcoded Color(...) literals appear outside theme/, *Preview.kt, and _test."
val srcDir = colorGuardSrcDir
val exemptions = colorGuardExemptions
val hexPattern = colorGuardHexPattern
val namedPattern = colorGuardNamedPattern
doLast {
val offenders = mutableListOf<Pair<String, Int>>()
srcDir.asFile.walkTopDown().filter { it.isFile && it.extension == "kt" }.forEach { file ->
if (exemptions.any { file.absolutePath.contains(it) }) return@forEach
file.useLines { lines ->
lines.forEachIndexed { idx, raw ->
val line = raw.trim()
if (line.startsWith("import ")) return@forEachIndexed
if (line.startsWith("//") || line.startsWith("*")) return@forEachIndexed
if (hexPattern.containsMatchIn(line) || namedPattern.containsMatchIn(line)) {
offenders.add(file.absolutePath to (idx + 1))
}
}
}
}
if (offenders.isNotEmpty()) {
val report = offenders.joinToString("\n") { (path, line) ->
" - $path:$line"
}
throw GradleException(
"Hardcoded Color literals found outside theme/ + *Preview.kt + _test:\n$report\n\n" +
"Replace with MaterialTheme.colorScheme.<token>, " +
"LocalHermesStatusColors.current.<semantic>, or " +
"Color.Transparent / Color.Unspecified (intentional).\n" +
"See docs/color-hardcoded-audit.md.",
)
}
logger.lifecycle("checkColorLiterals: no hardcoded Color literals found. ✅")
}
}
tasks.named("check") {
dependsOn("checkColorLiterals")
}
tasks.withType<Test> {
useJUnitPlatform()
}
@@ -1886,7 +1886,7 @@ private fun ToolBubble(
var expanded by remember { mutableStateOf(false) }
var showRawJson by remember { mutableStateOf(false) }
val chipColor = if (isDarkTheme) ToolChipColor else ToolChipColorLight
val contentColor = if (isDarkTheme) Color.White else Color.Black
val contentColor = MaterialTheme.colorScheme.onSurfaceVariant
val statusColors = LocalHermesStatusColors.current
val parsed =
@@ -806,7 +806,7 @@ fun SessionsScreen(
CircularProgressIndicator(
modifier = Modifier.size(18.dp),
strokeWidth = 2.dp,
color = Color.White,
color = statusColors.onError,
)
} else {
Icon(