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>
359 lines
9.0 KiB
Go
359 lines
9.0 KiB
Go
package main
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
)
|
|
|
|
type pluginScanTarget struct {
|
|
root string
|
|
display string
|
|
frontendDir string
|
|
frontendLabel string
|
|
}
|
|
|
|
type backendScanTarget struct {
|
|
root string
|
|
display string
|
|
protoRoot string
|
|
allowedPackagePrefixes []string
|
|
}
|
|
|
|
type frontendScanTarget struct {
|
|
dir string
|
|
label string
|
|
pluginRules bool
|
|
managedConfigs bool
|
|
}
|
|
|
|
type rbacTargetSummary struct {
|
|
label string
|
|
protoMethods []string
|
|
roleByMethod map[string]string
|
|
missingCount int
|
|
staleCount int
|
|
requiresRPCs bool
|
|
}
|
|
|
|
func (t backendScanTarget) displayOrRoot() string {
|
|
if t.display != "" {
|
|
return t.display
|
|
}
|
|
return t.root
|
|
}
|
|
|
|
func resolveScanRoots(target string) (backendDir string, repoRoot string, err error) {
|
|
absTarget, err := filepath.Abs(target)
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
|
|
if dirExists(filepath.Join(absTarget, "backend", "cmd", "check-safety")) {
|
|
return filepath.Join(absTarget, "backend"), absTarget, nil
|
|
}
|
|
|
|
if dirExists(filepath.Join(absTarget, "cmd", "check-safety")) {
|
|
parent := filepath.Clean(filepath.Join(absTarget, ".."))
|
|
if fileExists(filepath.Join(parent, "buf.yaml")) {
|
|
return absTarget, parent, nil
|
|
}
|
|
return absTarget, absTarget, nil
|
|
}
|
|
|
|
return absTarget, absTarget, nil
|
|
}
|
|
|
|
func defaultScanTargetDir(targetDir string, pluginRoots []string, cwd string) string {
|
|
if len(pluginRoots) > 0 || targetDir != "." {
|
|
return targetDir
|
|
}
|
|
if fileExists(filepath.Join(cwd, "plugin.mod")) {
|
|
return cwd
|
|
}
|
|
return filepath.Join(blockNinjaRepoRoot(), "backend")
|
|
}
|
|
|
|
func resolvePluginTargets(repoRoot string, roots []string) (targets []pluginScanTarget, unresolved []string) {
|
|
seenRoots := make(map[string]bool)
|
|
seenFrontends := make(map[string]bool)
|
|
|
|
for _, root := range roots {
|
|
resolvedRoot, err := expandPath(root)
|
|
if err != nil {
|
|
unresolved = append(unresolved, shortPluginLabel(root))
|
|
continue
|
|
}
|
|
|
|
absRoot, err := filepath.Abs(resolvedRoot)
|
|
if err != nil {
|
|
unresolved = append(unresolved, shortPluginLabel(root))
|
|
continue
|
|
}
|
|
|
|
info, err := os.Stat(absRoot)
|
|
if err != nil || !info.IsDir() {
|
|
unresolved = append(unresolved, shortPluginLabel(root))
|
|
continue
|
|
}
|
|
if seenRoots[absRoot] {
|
|
continue
|
|
}
|
|
seenRoots[absRoot] = true
|
|
|
|
target := pluginScanTarget{
|
|
root: absRoot,
|
|
display: shortPluginLabel(absRoot),
|
|
}
|
|
|
|
if frontendDir, ok := findPluginFrontendDir(absRoot); ok {
|
|
if !seenFrontends[frontendDir] {
|
|
target.frontendDir = frontendDir
|
|
target.frontendLabel = target.display
|
|
seenFrontends[frontendDir] = true
|
|
}
|
|
}
|
|
|
|
targets = append(targets, target)
|
|
}
|
|
|
|
sort.Slice(targets, func(i, j int) bool {
|
|
return targets[i].display < targets[j].display
|
|
})
|
|
return targets, unresolved
|
|
}
|
|
|
|
func collectBackendScanTargets(repoRoot, backendDir string, includeCoreTargets bool) []backendScanTarget {
|
|
var targets []backendScanTarget
|
|
seen := make(map[string]bool)
|
|
|
|
if !includeCoreTargets {
|
|
return nil
|
|
}
|
|
|
|
addTarget := func(root string, allowedPrefixes []string) {
|
|
if root == "" || seen[root] || !dirExists(root) {
|
|
return
|
|
}
|
|
seen[root] = true
|
|
targets = append(targets, backendScanTarget{
|
|
root: root,
|
|
display: normalizeDisplayLabel(displayLintPath(repoRoot, root)),
|
|
protoRoot: resolveProtoScanRoot(root),
|
|
allowedPackagePrefixes: allowedPrefixes,
|
|
})
|
|
}
|
|
|
|
addTarget(backendDir, inferAllowedPackagePrefixes(repoRoot, backendDir))
|
|
|
|
sort.Slice(targets, func(i, j int) bool {
|
|
return targets[i].displayOrRoot() < targets[j].displayOrRoot()
|
|
})
|
|
return targets
|
|
}
|
|
|
|
func filterPluginTargets(targets []pluginScanTarget, backendDir string) []pluginScanTarget {
|
|
filtered := make([]pluginScanTarget, 0, len(targets))
|
|
for _, target := range targets {
|
|
if samePath(target.root, backendDir) {
|
|
continue
|
|
}
|
|
filtered = append(filtered, target)
|
|
}
|
|
return filtered
|
|
}
|
|
|
|
func collectFrontendScanTargets(repoRoot, backendDir string, pluginTargets []pluginScanTarget, includeCoreTargets bool) []frontendScanTarget {
|
|
var targets []frontendScanTarget
|
|
seen := make(map[string]bool)
|
|
|
|
addTarget := func(dir, label string, pluginRules bool, managedConfigs bool) {
|
|
if dir == "" || seen[dir] || !dirExists(dir) {
|
|
return
|
|
}
|
|
seen[dir] = true
|
|
targets = append(targets, frontendScanTarget{
|
|
dir: dir,
|
|
label: normalizeDisplayLabel(label),
|
|
pluginRules: pluginRules,
|
|
managedConfigs: managedConfigs,
|
|
})
|
|
}
|
|
|
|
if includeCoreTargets && samePath(backendDir, filepath.Join(repoRoot, "backend")) {
|
|
addTarget(filepath.Join(repoRoot, "web", "src"), "web/src", false, false)
|
|
addTarget(filepath.Join(orchestratorRepoRoot(), "frontend", "src"), "orchestrator/frontend/src", false, true)
|
|
for _, pluginWebDir := range discoverInternalPluginWebDirs(backendDir) {
|
|
rel, _ := filepath.Rel(repoRoot, pluginWebDir)
|
|
addTarget(pluginWebDir, rel, true, true)
|
|
}
|
|
} else if includeCoreTargets {
|
|
if frontendDir, ok := findPluginFrontendDir(backendDir); ok {
|
|
addTarget(frontendDir, displayLintPath(repoRoot, frontendDir), true, true)
|
|
}
|
|
}
|
|
|
|
for _, target := range pluginTargets {
|
|
if target.frontendDir == "" {
|
|
continue
|
|
}
|
|
addTarget(target.frontendDir, target.frontendLabel, true, true)
|
|
}
|
|
|
|
sort.Slice(targets, func(i, j int) bool {
|
|
return targets[i].label < targets[j].label
|
|
})
|
|
|
|
return targets
|
|
}
|
|
|
|
func shouldIncludeCoreTargets(targetDir, backendDir string, pluginRoots []string, pluginPageDirs []string) bool {
|
|
if len(pluginRoots) == 0 && len(pluginPageDirs) == 0 {
|
|
return true
|
|
}
|
|
|
|
blockNinjaRepo := blockNinjaRepoRoot()
|
|
if samePath(targetDir, blockNinjaRepo) || samePath(targetDir, filepath.Join(blockNinjaRepo, "backend")) {
|
|
return false
|
|
}
|
|
if samePath(backendDir, filepath.Join(blockNinjaRepo, "backend")) &&
|
|
(samePath(targetDir, filepath.Join(blockNinjaRepo, "backend")) || samePath(targetDir, blockNinjaRepo)) {
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
func appendManualFrontendTargets(repoRoot string, targets []frontendScanTarget, dirs []string) []frontendScanTarget {
|
|
if len(dirs) == 0 {
|
|
return targets
|
|
}
|
|
|
|
seen := make(map[string]bool, len(targets))
|
|
for _, target := range targets {
|
|
if target.dir == "" {
|
|
continue
|
|
}
|
|
absDir, err := filepath.Abs(target.dir)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
seen[absDir] = true
|
|
}
|
|
|
|
for _, dir := range dirs {
|
|
absDir, err := filepath.Abs(dir)
|
|
if err != nil || seen[absDir] {
|
|
continue
|
|
}
|
|
seen[absDir] = true
|
|
targets = append(targets, frontendScanTarget{
|
|
dir: absDir,
|
|
label: normalizeDisplayLabel(displayLintPath(repoRoot, absDir)),
|
|
pluginRules: true,
|
|
managedConfigs: true,
|
|
})
|
|
}
|
|
|
|
sort.Slice(targets, func(i, j int) bool {
|
|
return targets[i].label < targets[j].label
|
|
})
|
|
return targets
|
|
}
|
|
|
|
func collectESLintDirectiveTargets(repoRoot string, frontendTargets []frontendScanTarget) []frontendScanTarget {
|
|
var targets []frontendScanTarget
|
|
seen := make(map[string]bool)
|
|
|
|
addTarget := func(dir, label string) {
|
|
if dir == "" || !dirExists(dir) {
|
|
return
|
|
}
|
|
absDir, err := filepath.Abs(dir)
|
|
if err != nil || seen[absDir] {
|
|
return
|
|
}
|
|
seen[absDir] = true
|
|
targets = append(targets, frontendScanTarget{
|
|
dir: absDir,
|
|
label: normalizeDisplayLabel(label),
|
|
})
|
|
}
|
|
|
|
for _, target := range frontendTargets {
|
|
addTarget(target.dir, target.label)
|
|
}
|
|
|
|
if samePath(repoRoot, blockNinjaRepoRoot()) {
|
|
addTarget(filepath.Join(repoRoot, "packages", "ui", "src"), "packages/ui/src")
|
|
}
|
|
|
|
sort.Slice(targets, func(i, j int) bool {
|
|
return targets[i].label < targets[j].label
|
|
})
|
|
return targets
|
|
}
|
|
|
|
func discoverInternalPluginWebDirs(backendDir string) []string {
|
|
pluginsDir := filepath.Join(backendDir, "internal", "plugins")
|
|
entries, err := os.ReadDir(pluginsDir)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
var dirs []string
|
|
for _, entry := range entries {
|
|
if !entry.IsDir() {
|
|
continue
|
|
}
|
|
webDir := filepath.Join(pluginsDir, entry.Name(), "web")
|
|
if dirLooksLikeFrontendSource(webDir) {
|
|
abs, err := filepath.Abs(webDir)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
dirs = append(dirs, abs)
|
|
}
|
|
}
|
|
return dirs
|
|
}
|
|
|
|
func findPluginFrontendDir(root string) (string, bool) {
|
|
for _, candidate := range []string{
|
|
root,
|
|
filepath.Join(root, "src"),
|
|
filepath.Join(root, "web"),
|
|
filepath.Join(root, "web", "src"),
|
|
filepath.Join(root, "frontend"),
|
|
filepath.Join(root, "frontend", "src"),
|
|
} {
|
|
if !dirLooksLikeFrontendSource(candidate) {
|
|
continue
|
|
}
|
|
absCandidate, err := filepath.Abs(candidate)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
return absCandidate, true
|
|
}
|
|
return "", false
|
|
}
|
|
|
|
func inferAllowedPackagePrefixes(repoRoot, backendDir string) []string {
|
|
switch {
|
|
case samePath(repoRoot, orchestratorRepoRoot()):
|
|
return []string{"orchestrator."}
|
|
case samePath(backendDir, filepath.Join(repoRoot, "backend")):
|
|
return []string{"blockninja."}
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func resolveProtoScanRoot(backendDir string) string {
|
|
parent := filepath.Clean(filepath.Join(backendDir, ".."))
|
|
if fileExists(filepath.Join(parent, "buf.yaml")) || dirExists(filepath.Join(parent, "proto")) {
|
|
return parent
|
|
}
|
|
return backendDir
|
|
}
|