Reporter is now a collector: checks declare a verdict (OK/Skip/Warn/Fail/ Fatal) plus findings, and a single central render() prints output. Default output is silent on pass/skip — only FAIL/WARN/ERR checks print, followed by one tally line, so a clean run is two lines. --verbose restores full per-check output. All ~30 checks were converted to this API; orphaned guidance/label helpers (printPerTargetOKLines, per-check *Help blocks, colors_format.go) were removed. The any-usage check (2e) now defaults to only the unstaged working-tree diff (changed lines), via a new per-repo git-diff index in changedlines.go; --all-any restores the full scan. Not-a-git-repo / no-diff warns on nothing. Golden fixtures regenerated; integration tests updated to the new format; added unit tests for the diff index. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
108 lines
3.6 KiB
Go
108 lines
3.6 KiB
Go
package main
|
|
|
|
import "sort"
|
|
|
|
func init() {
|
|
register(Check{
|
|
Seq: 60,
|
|
ID: "6",
|
|
Title: "No hardcoded colors in frontend (use theme tokens)",
|
|
Run: func(ctx *ScanContext, rep *Reporter) {
|
|
var colorViolations []colorViolation
|
|
for _, target := range ctx.frontendTargets {
|
|
for _, v := range checkColors(target.dir) {
|
|
v.file = prefixDisplayPath(target.label, v.file)
|
|
colorViolations = append(colorViolations, v)
|
|
}
|
|
}
|
|
|
|
// Also scan .templ files in backend and plugin roots (templ lives in backend/, not web/src/).
|
|
seenTemplRoots := make(map[string]bool)
|
|
var templColorViolations []colorViolation
|
|
for _, target := range ctx.backendTargets {
|
|
if seenTemplRoots[target.root] {
|
|
continue
|
|
}
|
|
seenTemplRoots[target.root] = true
|
|
for _, v := range checkTemplColors(target.root) {
|
|
v.file = prefixDisplayPath(target.display, v.file)
|
|
templColorViolations = append(templColorViolations, v)
|
|
}
|
|
}
|
|
for _, target := range ctx.pluginTargets {
|
|
if seenTemplRoots[target.root] {
|
|
continue
|
|
}
|
|
seenTemplRoots[target.root] = true
|
|
for _, v := range checkTemplColors(target.root) {
|
|
v.file = prefixDisplayPath(target.display, v.file)
|
|
templColorViolations = append(templColorViolations, v)
|
|
}
|
|
}
|
|
|
|
hasColorSources := len(ctx.frontendTargets) > 0 || len(templColorViolations) > 0 || len(seenTemplRoots) > 0
|
|
if !hasColorSources && len(ctx.backendTargets) == 0 {
|
|
rep.Skip("no frontend or templ sources found")
|
|
} else {
|
|
if len(colorViolations) > 0 {
|
|
reportColorViolations(rep, "frontend", colorViolations)
|
|
} else if len(ctx.frontendTargets) > 0 {
|
|
rep.OK("No hardcoded colors in frontend components")
|
|
}
|
|
|
|
if len(templColorViolations) > 0 {
|
|
reportColorViolations(rep, ".templ files", templColorViolations)
|
|
} else if len(seenTemplRoots) > 0 {
|
|
rep.OK("No hardcoded colors in .templ files")
|
|
}
|
|
|
|
// Scan .ninjatpl block templates in plugin roots for hardcoded colors.
|
|
// Use resolvedPluginTargets (pre-filter) so plugin-as-cwd roots aren't skipped.
|
|
seenNinjaTplRoots := make(map[string]bool)
|
|
var ninjaTplColorViolations []colorViolation
|
|
for _, target := range ctx.resolvedPluginTargets {
|
|
if seenNinjaTplRoots[target.root] {
|
|
continue
|
|
}
|
|
seenNinjaTplRoots[target.root] = true
|
|
for _, v := range checkNinjaTplColors(target.root) {
|
|
v.file = prefixDisplayPath(target.display, v.file)
|
|
ninjaTplColorViolations = append(ninjaTplColorViolations, v)
|
|
}
|
|
}
|
|
if len(ninjaTplColorViolations) > 0 {
|
|
reportColorViolations(rep, ".ninjatpl files", ninjaTplColorViolations)
|
|
} else if len(seenNinjaTplRoots) > 0 {
|
|
rep.OK("No hardcoded colors in .ninjatpl files")
|
|
}
|
|
}
|
|
},
|
|
})
|
|
}
|
|
|
|
// reportColorViolations emits a Fail plus one finding per hardcoded-color hit,
|
|
// sorted for deterministic output.
|
|
func reportColorViolations(rep *Reporter, scope string, violations []colorViolation) {
|
|
rep.Fail("%d hardcoded color(s) in %s — use semantic tokens/CSS vars", len(violations), scope)
|
|
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
|
|
})
|
|
for _, v := range sorted {
|
|
if v.line > 0 {
|
|
rep.Findingf("%s:%d [%s] %s", v.file, v.line, v.rule, v.snippet)
|
|
} else {
|
|
rep.Findingf("%s [%s] %s", v.file, v.rule, v.snippet)
|
|
}
|
|
}
|
|
}
|