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) } } }