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>
266 lines
5.9 KiB
Go
266 lines
5.9 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
)
|
|
|
|
type goLintTarget struct {
|
|
label string
|
|
moduleRoot string
|
|
}
|
|
|
|
type goLintFailure struct {
|
|
target string
|
|
stage string
|
|
output string
|
|
}
|
|
|
|
type frontendLintTarget struct {
|
|
label string
|
|
packageRoot string
|
|
sourceDir string
|
|
sourceArg string
|
|
configPath string
|
|
pluginRules bool
|
|
managedConfigs bool
|
|
tsconfigPath string
|
|
hasTypeScript bool
|
|
}
|
|
|
|
type frontendLintFailure struct {
|
|
target string
|
|
stage string
|
|
output string
|
|
}
|
|
|
|
func discoverGoLintTargets(repoRoot string, backendDirs []string, pluginRoots []string) ([]goLintTarget, []string, error) {
|
|
var targets []goLintTarget
|
|
var skipped []string
|
|
seen := make(map[string]bool)
|
|
|
|
candidates := append(append([]string{}, backendDirs...), pluginRoots...)
|
|
for _, candidate := range candidates {
|
|
moduleRoot, ok := findNearestGoModuleRoot(candidate)
|
|
if !ok {
|
|
skipped = append(skipped, fmt.Sprintf("%s (no go.mod found)", displayLintPath(repoRoot, candidate)))
|
|
continue
|
|
}
|
|
if seen[moduleRoot] {
|
|
continue
|
|
}
|
|
seen[moduleRoot] = true
|
|
targets = append(targets, goLintTarget{
|
|
label: displayLintPath(repoRoot, moduleRoot),
|
|
moduleRoot: moduleRoot,
|
|
})
|
|
}
|
|
|
|
sort.Slice(targets, func(i, j int) bool {
|
|
return targets[i].label < targets[j].label
|
|
})
|
|
return targets, skipped, nil
|
|
}
|
|
|
|
func discoverFrontendLintTargets(repoRoot string, scanTargets []frontendScanTarget) ([]frontendLintTarget, []string, error) {
|
|
var targets []frontendLintTarget
|
|
var skipped []string
|
|
seen := make(map[string]bool)
|
|
|
|
for _, scanTarget := range scanTargets {
|
|
absCandidate, err := filepath.Abs(scanTarget.dir)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
if seen[absCandidate] {
|
|
continue
|
|
}
|
|
seen[absCandidate] = true
|
|
|
|
info, err := os.Stat(absCandidate)
|
|
if err != nil || !info.IsDir() {
|
|
continue
|
|
}
|
|
|
|
packageRoot, ok := findNearestPackageRoot(absCandidate)
|
|
if !ok {
|
|
skipped = append(skipped, fmt.Sprintf("%s (no package.json found)", displayLintPath(repoRoot, absCandidate)))
|
|
continue
|
|
}
|
|
|
|
if scanTarget.pluginRules || scanTarget.managedConfigs {
|
|
if err := ensureDefaultFrontendConfigs(repoRoot, packageRoot); err != nil {
|
|
return nil, nil, err
|
|
}
|
|
}
|
|
|
|
configPath, ok := findNearestESLintConfig(absCandidate)
|
|
if !ok {
|
|
skipped = append(skipped, fmt.Sprintf("%s (no ESLint config found)", displayLintPath(repoRoot, absCandidate)))
|
|
continue
|
|
}
|
|
|
|
sourceArg, err := filepath.Rel(packageRoot, absCandidate)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
if sourceArg == "" {
|
|
sourceArg = "."
|
|
}
|
|
hasTypeScript := dirContainsTypeScript(absCandidate)
|
|
tsconfigPath := ""
|
|
if hasTypeScript {
|
|
tsconfigPath, _ = findNearestTSConfig(packageRoot)
|
|
}
|
|
|
|
targets = append(targets, frontendLintTarget{
|
|
label: displayLintPath(repoRoot, absCandidate),
|
|
packageRoot: packageRoot,
|
|
sourceDir: absCandidate,
|
|
sourceArg: sourceArg,
|
|
configPath: configPath,
|
|
pluginRules: scanTarget.pluginRules,
|
|
managedConfigs: scanTarget.managedConfigs,
|
|
tsconfigPath: tsconfigPath,
|
|
hasTypeScript: hasTypeScript,
|
|
})
|
|
}
|
|
|
|
sort.Slice(targets, func(i, j int) bool {
|
|
return targets[i].label < targets[j].label
|
|
})
|
|
|
|
return targets, skipped, nil
|
|
}
|
|
|
|
func usesFlatESLintConfig(configPath string) bool {
|
|
base := filepath.Base(configPath)
|
|
return strings.HasPrefix(base, "eslint.config.")
|
|
}
|
|
|
|
func displayLintPath(repoRoot, path string) string {
|
|
if rel, err := filepath.Rel(repoRoot, path); err == nil && !strings.HasPrefix(rel, "..") {
|
|
return rel
|
|
}
|
|
return path
|
|
}
|
|
|
|
func findNearestPackageRoot(start string) (string, bool) {
|
|
for dir := start; ; dir = filepath.Dir(dir) {
|
|
info, err := os.Stat(filepath.Join(dir, "package.json"))
|
|
if err == nil && !info.IsDir() {
|
|
return dir, true
|
|
}
|
|
parent := filepath.Dir(dir)
|
|
if parent == dir {
|
|
return "", false
|
|
}
|
|
}
|
|
}
|
|
|
|
func findNearestGoModuleRoot(start string) (string, bool) {
|
|
for dir := start; ; dir = filepath.Dir(dir) {
|
|
info, err := os.Stat(filepath.Join(dir, "go.mod"))
|
|
if err == nil && !info.IsDir() {
|
|
return dir, true
|
|
}
|
|
parent := filepath.Dir(dir)
|
|
if parent == dir {
|
|
return "", false
|
|
}
|
|
}
|
|
}
|
|
|
|
func findNearestESLintConfig(start string) (string, bool) {
|
|
for dir := start; ; dir = filepath.Dir(dir) {
|
|
for _, name := range []string{
|
|
"eslint.config.js",
|
|
"eslint.config.mjs",
|
|
"eslint.config.cjs",
|
|
".eslintrc",
|
|
".eslintrc.js",
|
|
".eslintrc.cjs",
|
|
".eslintrc.json",
|
|
".eslintrc.yaml",
|
|
".eslintrc.yml",
|
|
} {
|
|
path := filepath.Join(dir, name)
|
|
info, err := os.Stat(path)
|
|
if err == nil && !info.IsDir() {
|
|
return path, true
|
|
}
|
|
}
|
|
|
|
if packageJSONHasESLintConfig(filepath.Join(dir, "package.json")) {
|
|
return filepath.Join(dir, "package.json"), true
|
|
}
|
|
|
|
parent := filepath.Dir(dir)
|
|
if parent == dir {
|
|
return "", false
|
|
}
|
|
}
|
|
}
|
|
|
|
func findNearestTSConfig(start string) (string, bool) {
|
|
for dir := start; ; dir = filepath.Dir(dir) {
|
|
for _, name := range []string{
|
|
"tsconfig.json",
|
|
"tsconfig.app.json",
|
|
"tsconfig.build.json",
|
|
} {
|
|
path := filepath.Join(dir, name)
|
|
info, err := os.Stat(path)
|
|
if err == nil && !info.IsDir() {
|
|
return path, true
|
|
}
|
|
}
|
|
|
|
parent := filepath.Dir(dir)
|
|
if parent == dir {
|
|
return "", false
|
|
}
|
|
}
|
|
}
|
|
|
|
func dirContainsTypeScript(root string) bool {
|
|
found := false
|
|
_ = filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
|
|
if err != nil || found {
|
|
return nil
|
|
}
|
|
if info.IsDir() {
|
|
switch info.Name() {
|
|
case "node_modules", "dist", "build", ".git":
|
|
return filepath.SkipDir
|
|
}
|
|
return nil
|
|
}
|
|
|
|
if strings.HasSuffix(path, ".ts") || strings.HasSuffix(path, ".tsx") {
|
|
found = true
|
|
}
|
|
return nil
|
|
})
|
|
return found
|
|
}
|
|
|
|
func packageJSONHasESLintConfig(path string) bool {
|
|
content, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
|
|
var payload map[string]json.RawMessage
|
|
if err := json.Unmarshal(content, &payload); err != nil {
|
|
return false
|
|
}
|
|
|
|
_, ok := payload["eslintConfig"]
|
|
return ok
|
|
}
|