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>
176 lines
5.5 KiB
Go
176 lines
5.5 KiB
Go
package main
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestCheckTemplColorsFlagsHardcodedColorsAndInlineStyles(t *testing.T) {
|
|
root := t.TempDir()
|
|
file := filepath.Join(root, "sample.templ")
|
|
content := `package sample
|
|
|
|
templ Example() {
|
|
<div class="text-blue-600 bg-[#fff]"></div>
|
|
<div class="border border-border" style="color: #fff; background: rgb(255, 0, 0)"></div>
|
|
<div style={ "background-color: hsl(12 100% 50%)" }></div>
|
|
}
|
|
`
|
|
if err := os.WriteFile(file, []byte(content), 0644); err != nil {
|
|
t.Fatalf("write sample templ: %v", err)
|
|
}
|
|
|
|
violations := checkTemplColors(root)
|
|
if len(violations) < 5 {
|
|
t.Fatalf("checkTemplColors() returned %d violations, want at least 5: %#v", len(violations), violations)
|
|
}
|
|
|
|
rules := map[string]bool{}
|
|
for _, violation := range violations {
|
|
rules[violation.rule] = true
|
|
}
|
|
|
|
for _, rule := range []string{
|
|
"no-hardcoded-tw-color",
|
|
"no-hex-in-class",
|
|
"no-tw-arbitrary-hex",
|
|
"no-inline-hex-style",
|
|
"no-inline-rgb-style",
|
|
} {
|
|
if !rules[rule] {
|
|
t.Fatalf("missing rule %q in violations: %#v", rule, violations)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCheckTemplColorsAllowsSemanticTokensAndCSSVariables(t *testing.T) {
|
|
root := t.TempDir()
|
|
file := filepath.Join(root, "sample.templ")
|
|
content := `package sample
|
|
|
|
templ Example() {
|
|
<div class="bg-success/10 text-success border-border"></div>
|
|
<div style="background: hsl(var(--background)); color: hsl(var(--foreground))"></div>
|
|
<div style={ "background: hsl(var(--primary)); color: hsl(var(--primary-foreground))" }></div>
|
|
}
|
|
`
|
|
if err := os.WriteFile(file, []byte(content), 0644); err != nil {
|
|
t.Fatalf("write sample templ: %v", err)
|
|
}
|
|
|
|
violations := checkTemplColors(root)
|
|
if len(violations) != 0 {
|
|
t.Fatalf("checkTemplColors() returned %d violations, want 0: %#v", len(violations), violations)
|
|
}
|
|
}
|
|
|
|
func TestCheckNinjaTplColorsFlagsHardcodedColors(t *testing.T) {
|
|
root := t.TempDir()
|
|
tplDir := filepath.Join(root, "templates", "blocks")
|
|
if err := os.MkdirAll(tplDir, 0755); err != nil {
|
|
t.Fatalf("mkdir: %v", err)
|
|
}
|
|
file := filepath.Join(tplDir, "hero.ninjatpl")
|
|
content := `<section style="background:hsl(232,55%,4%);min-height:28rem;">
|
|
<h1 class="text-[hsl(210,18%,96%)] text-5xl">{{ headline }}</h1>
|
|
<p class="text-[rgba(220,232,248,0.75)]">{{ subtext }}</p>
|
|
<a style="background:hsl(168,82%,46%)">{{ primary_cta }}</a>
|
|
<div style="background:#0a0e24">hardcoded hex</div>
|
|
<span class="bg-[rgba(130,70,220,0.1)]">badge</span>
|
|
<span class="text-blue-500">named color</span>
|
|
</section>
|
|
`
|
|
if err := os.WriteFile(file, []byte(content), 0644); err != nil {
|
|
t.Fatalf("write ninjatpl: %v", err)
|
|
}
|
|
|
|
violations := checkNinjaTplColors(root)
|
|
if len(violations) < 6 {
|
|
t.Fatalf("checkNinjaTplColors() returned %d violations, want at least 6: %#v", len(violations), violations)
|
|
}
|
|
|
|
rules := map[string]bool{}
|
|
for _, v := range violations {
|
|
rules[v.rule] = true
|
|
}
|
|
for _, rule := range []string{
|
|
"no-hardcoded-hsl",
|
|
"no-tw-arbitrary-color",
|
|
"no-inline-hex",
|
|
"no-hardcoded-tw-color",
|
|
} {
|
|
if !rules[rule] {
|
|
t.Fatalf("missing rule %q in violations: %#v", rule, violations)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCheckNinjaTplColorsAllowsCSSVariablesAndNeutralOverlays(t *testing.T) {
|
|
root := t.TempDir()
|
|
tplDir := filepath.Join(root, "templates", "blocks")
|
|
if err := os.MkdirAll(tplDir, 0755); err != nil {
|
|
t.Fatalf("mkdir: %v", err)
|
|
}
|
|
file := filepath.Join(tplDir, "hero.ninjatpl")
|
|
content := `<section class="bg-background text-foreground">
|
|
<h1 class="text-primary text-5xl">{{ headline }}</h1>
|
|
<p class="text-muted-foreground">{{ subtext }}</p>
|
|
<a style="background:hsl(var(--primary));color:hsl(var(--primary-foreground))">{{ cta }}</a>
|
|
<div style="background:rgba(0,0,0,0.5)">dark overlay</div>
|
|
<div style="background:rgba(255,255,255,0.1)">light overlay</div>
|
|
<span class="bg-primary/10 text-accent">badge</span>
|
|
</section>
|
|
`
|
|
if err := os.WriteFile(file, []byte(content), 0644); err != nil {
|
|
t.Fatalf("write ninjatpl: %v", err)
|
|
}
|
|
|
|
violations := checkNinjaTplColors(root)
|
|
if len(violations) != 0 {
|
|
t.Fatalf("checkNinjaTplColors() returned %d violations, want 0: %#v", len(violations), violations)
|
|
}
|
|
}
|
|
|
|
func TestFormatColorViolationsGroupsByFile(t *testing.T) {
|
|
report := formatColorViolations(".ninjatpl files", []colorViolation{
|
|
{file: "b.ninjatpl", line: 12, rule: "no-hardcoded-rgb", snippet: "rgba(1,2,3,0.5)"},
|
|
{file: "a.ninjatpl", line: 9, rule: "no-hardcoded-hsl", snippet: "hsl(10,20%,30%)"},
|
|
{file: "a.ninjatpl", line: 9, rule: "no-tw-arbitrary-color", snippet: "text-[hsl(10,20%,30%)]"},
|
|
{file: "a.ninjatpl", line: 4, rule: "no-inline-hex", snippet: "#fff"},
|
|
})
|
|
|
|
expected := strings.Join([]string{
|
|
" FAIL: 4 hardcoded color(s) in .ninjatpl files:",
|
|
" a.ninjatpl",
|
|
" 4 [no-inline-hex] #fff",
|
|
" 9 [no-hardcoded-hsl] hsl(10,20%,30%)",
|
|
" 9 [no-tw-arbitrary-color] text-[hsl(10,20%,30%)]",
|
|
" b.ninjatpl",
|
|
" 12 [no-hardcoded-rgb] rgba(1,2,3,0.5)",
|
|
}, "\n")
|
|
|
|
if report != expected {
|
|
t.Fatalf("formatColorViolations() mismatch\nwant:\n%s\n\ngot:\n%s", expected, report)
|
|
}
|
|
}
|
|
|
|
func TestCollectColorHintsDeduplicatesAndSorts(t *testing.T) {
|
|
hints := collectColorHints(
|
|
[]colorViolation{
|
|
{snippet: "text-blue-500"},
|
|
{snippet: "bg-amber-100"},
|
|
},
|
|
[]colorViolation{
|
|
{snippet: "text-blue-500"},
|
|
{snippet: "text-[hsl(38,52%,54%)]"},
|
|
},
|
|
)
|
|
|
|
expected := []string{"bg-warning/10", "hsl(var(--token))", "text-info"}
|
|
if strings.Join(hints, "|") != strings.Join(expected, "|") {
|
|
t.Fatalf("collectColorHints() = %#v, want %#v", hints, expected)
|
|
}
|
|
}
|