check-safety/colors_format.go
Alex Dunmow cd88c808b0 initial: standalone check-safety module hoisted from CMS
Static safety/lint runner for the BlockNinja codebase. ~25 invariant
checks across Go and frontend sources. Was at git.dev.alexdunmow.com:block/ninja
in backend/cmd/check-safety/ until the 2026-06-06 consolidation moved
the BlockNinja repos under a shared ~/src/blockninja/ parent.

This repo is the standalone extraction:
- Own go.mod (git.dev.alexdunmow.com/block/check-safety, go 1.26.4)
- Vendored internal/{helpers,theme} from CMS (Go's internal/ rule
  blocks cross-module imports; vendoring is the workaround)
- CLI contract unchanged: `check-safety <target-dir> [--flags]`
- CMS Makefile shells into ../check-safety for safety-check /
  install-safety-checker targets

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-06 13:04:02 +08:00

148 lines
4.4 KiB
Go

package main
import (
"fmt"
"sort"
"strings"
)
// hintForColor returns a likely semantic replacement for a hardcoded color snippet.
// This is intentionally approximate and is used only in the consolidated fix block.
func hintForColor(snippet string) string {
s := strings.ToLower(snippet)
for _, prefix := range []string{
"bg-", "text-", "border-", "ring-", "shadow-", "outline-",
"decoration-", "divide-", "from-", "to-", "via-", "fill-", "stroke-",
"accent-", "caret-", "placeholder-",
} {
_, after, ok := strings.Cut(s, prefix)
if !ok {
continue
}
twPrefix := prefix[:len(prefix)-1]
isLowShadeBg := false
if twPrefix == "bg" || twPrefix == "from" || twPrefix == "to" || twPrefix == "via" {
for _, low := range []string{"-50", "-100", "-200"} {
if strings.Contains(after, low) {
isLowShadeBg = true
break
}
}
}
switch {
case strings.HasPrefix(after, "green") || strings.HasPrefix(after, "emerald") || strings.HasPrefix(after, "lime"):
if isLowShadeBg {
return twPrefix + "-success/10"
}
return twPrefix + "-success"
case strings.HasPrefix(after, "red") || strings.HasPrefix(after, "rose"):
if isLowShadeBg {
return twPrefix + "-destructive/10"
}
return twPrefix + "-destructive"
case strings.HasPrefix(after, "amber") || strings.HasPrefix(after, "yellow") || strings.HasPrefix(after, "orange"):
if isLowShadeBg {
return twPrefix + "-warning/10"
}
return twPrefix + "-warning"
case strings.HasPrefix(after, "blue") || strings.HasPrefix(after, "sky") || strings.HasPrefix(after, "cyan"):
if isLowShadeBg {
return twPrefix + "-info/10"
}
return twPrefix + "-info"
case strings.HasPrefix(after, "indigo"):
return twPrefix + "-primary"
case strings.HasPrefix(after, "purple") || strings.HasPrefix(after, "violet") || strings.HasPrefix(after, "fuchsia") || strings.HasPrefix(after, "pink") || strings.HasPrefix(after, "teal"):
return twPrefix + "-accent"
case strings.HasPrefix(after, "gray") || strings.HasPrefix(after, "slate") || strings.HasPrefix(after, "zinc") || strings.HasPrefix(after, "neutral") || strings.HasPrefix(after, "stone"):
if twPrefix == "text" {
return "text-muted-foreground"
}
return twPrefix + "-muted"
}
}
if strings.Contains(s, "#") || strings.Contains(s, "rgb") || strings.Contains(s, "hsl") {
return "hsl(var(--token))"
}
return ""
}
func formatColorViolations(scope string, violations []colorViolation) string {
if len(violations) == 0 {
return ""
}
sorted := append([]colorViolation(nil), violations...)
sort.Slice(sorted, func(i, j int) bool {
if sorted[i].file != sorted[j].file {
return sorted[i].file < sorted[j].file
}
if sorted[i].line != sorted[j].line {
return sorted[i].line < sorted[j].line
}
if sorted[i].rule != sorted[j].rule {
return sorted[i].rule < sorted[j].rule
}
return sorted[i].snippet < sorted[j].snippet
})
var out strings.Builder
fmt.Fprintf(&out, " FAIL: %d hardcoded color(s) in %s:\n", len(sorted), scope)
currentFile := ""
for _, v := range sorted {
if v.file != currentFile {
fmt.Fprintf(&out, " %s\n", v.file)
currentFile = v.file
}
if v.line > 0 {
fmt.Fprintf(&out, " %d [%s] %s\n", v.line, v.rule, v.snippet)
continue
}
fmt.Fprintf(&out, " [%s] %s\n", v.rule, v.snippet)
}
return strings.TrimRight(out.String(), "\n")
}
func collectColorHints(violationGroups ...[]colorViolation) []string {
seen := make(map[string]bool)
var hints []string
for _, violations := range violationGroups {
for _, v := range violations {
hint := hintForColor(v.snippet)
if hint == "" || seen[hint] {
continue
}
seen[hint] = true
hints = append(hints, hint)
}
}
sort.Strings(hints)
if len(hints) > 5 {
hints = append(hints[:5], "...")
}
return hints
}
func printColorFixInstructions(violationGroups ...[]colorViolation) {
fmt.Println(" Fix:")
if hints := collectColorHints(violationGroups...); len(hints) > 0 {
fmt.Printf(" 1. Likely replacements for this batch: %s\n", strings.Join(hints, ", "))
fmt.Println(" 2. Replace literals with semantic tokens or CSS vars where needed.")
} else {
fmt.Println(" 1. Replace literals with semantic tokens or CSS vars.")
}
fmt.Println(" 3. Tailwind token surface: tools/tailwind-service/input.base.css")
fmt.Println(" 4. Runtime theme variable generation: internal/theme/css.go")
fmt.Println(" 5. Plugin CSS examples: internal/plugins/defaultplugin/assets/style.css")
}