check-safety/codegen.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

131 lines
3.5 KiB
Go

package main
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"sort"
)
type codegenTarget struct {
label string
workdir string
stage string
binary string
args []string
}
type codegenFailure struct {
target string
stage string
output string
}
func discoverCodegenTargets(repoRoot, backendDir string, backendTargets []backendScanTarget, pluginTargets []pluginScanTarget, includeCoreTargets bool) []codegenTarget {
var targets []codegenTarget
seen := make(map[string]bool)
addTarget := func(workdir, label, stage, binary string, args ...string) {
key := workdir + "|" + stage
if workdir == "" || seen[key] {
return
}
seen[key] = true
targets = append(targets, codegenTarget{
label: label,
workdir: workdir,
stage: stage,
binary: binary,
args: append([]string{}, args...),
})
}
if includeCoreTargets && samePath(repoRoot, blockNinjaRepoRoot()) {
if fileExists(filepath.Join(backendDir, "sqlc.yaml")) {
addTarget(backendDir, "backend", "sqlc compile", "sqlc", "compile")
}
if fileExists(filepath.Join(repoRoot, "buf.yaml")) {
addTarget(repoRoot, "repo", "buf generate", "buf", "generate", "--path", "proto/blockninja/v1", "--path", "proto/orchestrator/v1")
}
}
for _, target := range pluginTargets {
if fileExists(filepath.Join(target.root, "sqlc.yaml")) {
addTarget(target.root, target.display, "sqlc compile", "sqlc", "compile")
}
if fileExists(filepath.Join(target.root, "buf.yaml")) {
addTarget(target.root, target.display, "buf generate", "buf", "generate")
}
}
for _, target := range backendTargets {
if !shouldCheckStandalonePluginImports(target.root) {
continue
}
if fileExists(filepath.Join(target.root, "sqlc.yaml")) {
addTarget(target.root, target.displayOrRoot(), "sqlc compile", "sqlc", "compile")
}
if fileExists(filepath.Join(target.root, "buf.yaml")) {
addTarget(target.root, target.displayOrRoot(), "buf generate", "buf", "generate")
}
}
sort.Slice(targets, func(i, j int) bool {
if targets[i].label == targets[j].label {
return targets[i].stage < targets[j].stage
}
return targets[i].label < targets[j].label
})
return targets
}
func runCodegenChecks(targets []codegenTarget) (completed []string, failures []codegenFailure, err error) {
sqlcBin, err := exec.LookPath("sqlc")
if err != nil {
return nil, nil, fmt.Errorf("sqlc not found in PATH")
}
bufBin, err := exec.LookPath("buf")
if err != nil {
return nil, nil, fmt.Errorf("buf not found in PATH")
}
return runCodegenChecksWithBinaries(targets, sqlcBin, bufBin)
}
func runCodegenChecksWithBinaries(targets []codegenTarget, sqlcBin, bufBin string) (completed []string, failures []codegenFailure, err error) {
for _, target := range targets {
binary := target.binary
args := append([]string{}, target.args...)
cleanup := func() {}
switch target.binary {
case "sqlc":
binary = sqlcBin
case "buf":
binary = bufBin
tempOutputDir, mkErr := os.MkdirTemp("", "check-safety-buf-*")
if mkErr != nil {
return nil, nil, fmt.Errorf("create temp output dir for %s: %w", target.label, mkErr)
}
args = append(args, "-o", tempOutputDir)
cleanup = func() {
_ = os.RemoveAll(tempOutputDir)
}
}
output, runErr := runCommand(target.workdir, binary, args...)
cleanup()
if runErr != nil {
failures = append(failures, codegenFailure{
target: target.label,
stage: target.stage,
output: output,
})
continue
}
completed = append(completed, target.label+" ["+target.stage+"]")
}
sort.Strings(completed)
return completed, failures, nil
}