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>
156 lines
3.0 KiB
Go
156 lines
3.0 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"slices"
|
|
"sort"
|
|
"strings"
|
|
)
|
|
|
|
func expandPath(path string) (string, error) {
|
|
if path == "~" || strings.HasPrefix(path, "~/") {
|
|
home, err := os.UserHomeDir()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if path == "~" {
|
|
return home, nil
|
|
}
|
|
return filepath.Join(home, strings.TrimPrefix(path, "~/")), nil
|
|
}
|
|
|
|
return path, nil
|
|
}
|
|
|
|
func dirLooksLikeFrontendSource(path string) bool {
|
|
info, err := os.Stat(path)
|
|
if err != nil || !info.IsDir() {
|
|
return false
|
|
}
|
|
|
|
entries, err := os.ReadDir(path)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
|
|
for _, entry := range entries {
|
|
if entry.IsDir() {
|
|
continue
|
|
}
|
|
|
|
name := entry.Name()
|
|
if strings.HasSuffix(name, ".ts") || strings.HasSuffix(name, ".tsx") || strings.HasSuffix(name, ".js") || strings.HasSuffix(name, ".jsx") {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func dirExists(path string) bool {
|
|
info, err := os.Stat(path)
|
|
return err == nil && info.IsDir()
|
|
}
|
|
|
|
func samePath(a, b string) bool {
|
|
if a == "" || b == "" {
|
|
return false
|
|
}
|
|
|
|
absA, err := filepath.Abs(a)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
absB, err := filepath.Abs(b)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
|
|
return absA == absB
|
|
}
|
|
|
|
func normalizeDisplayLabel(label string) string {
|
|
if label == "." {
|
|
return ""
|
|
}
|
|
return label
|
|
}
|
|
|
|
func fileExists(path string) bool {
|
|
info, err := os.Stat(path)
|
|
return err == nil && !info.IsDir()
|
|
}
|
|
|
|
func prefixDisplayPath(prefix, path string) string {
|
|
if prefix == "" || prefix == "." {
|
|
return path
|
|
}
|
|
if path == "" {
|
|
return prefix
|
|
}
|
|
return prefix + "/" + filepath.ToSlash(path)
|
|
}
|
|
|
|
func shortPluginLabel(path string) string {
|
|
cleaned := filepath.Clean(path)
|
|
base := filepath.Base(cleaned)
|
|
if base == "." || base == string(filepath.Separator) || base == "" {
|
|
return path
|
|
}
|
|
return base
|
|
}
|
|
|
|
func isPluginModuleRoot(root string) bool {
|
|
modulePath, err := readModulePath(root)
|
|
if err != nil {
|
|
return fileExists(filepath.Join(root, "plugin.mod"))
|
|
}
|
|
return strings.Contains(modulePath, "/internal/plugins/") || fileExists(filepath.Join(root, "plugin.mod"))
|
|
}
|
|
|
|
func containsString(values []string, target string) bool {
|
|
return slices.Contains(values, target)
|
|
}
|
|
|
|
func uniqueSortedStrings(values []string) []string {
|
|
seen := make(map[string]bool)
|
|
var out []string
|
|
for _, value := range values {
|
|
if value == "" || seen[value] {
|
|
continue
|
|
}
|
|
seen[value] = true
|
|
out = append(out, value)
|
|
}
|
|
sort.Strings(out)
|
|
return out
|
|
}
|
|
|
|
func printPerTargetOKLines(labels []string) {
|
|
labels = uniqueSortedStrings(labels)
|
|
for _, label := range labels {
|
|
if label == "" || label == "." {
|
|
continue
|
|
}
|
|
fmt.Printf(" OK: %s\n", label)
|
|
}
|
|
}
|
|
|
|
func frontendTargetLabels(targets []frontendScanTarget) []string {
|
|
labels := make([]string, 0, len(targets))
|
|
for _, target := range targets {
|
|
labels = append(labels, target.label)
|
|
}
|
|
return labels
|
|
}
|
|
|
|
func pluginTargetLabels(targets []pluginScanTarget) []string {
|
|
labels := make([]string, 0, len(targets))
|
|
for _, target := range targets {
|
|
labels = append(labels, target.display)
|
|
}
|
|
return labels
|
|
}
|