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>
116 lines
2.8 KiB
Go
116 lines
2.8 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"git.dev.alexdunmow.com/block/check-safety/internal/theme"
|
|
)
|
|
|
|
type presetViolation struct {
|
|
file string
|
|
message string
|
|
}
|
|
|
|
type presetJSON struct {
|
|
ID string `json:"id"`
|
|
Name string `json:"name"`
|
|
Description string `json:"description"`
|
|
Theme theme.Theme `json:"theme"`
|
|
}
|
|
|
|
func checkPresets(root string) []presetViolation {
|
|
var violations []presetViolation
|
|
|
|
presetsFiles := findPresetsFiles(root)
|
|
for _, path := range presetsFiles {
|
|
relPath, _ := filepath.Rel(root, path)
|
|
if relPath == "" {
|
|
relPath = path
|
|
}
|
|
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
violations = append(violations, presetViolation{
|
|
file: relPath,
|
|
message: fmt.Sprintf("cannot read file: %v", err),
|
|
})
|
|
continue
|
|
}
|
|
|
|
// Strict unmarshal: catches type mismatches (e.g., int where string expected)
|
|
var presets []presetJSON
|
|
decoder := json.NewDecoder(bytes.NewReader(data))
|
|
decoder.DisallowUnknownFields()
|
|
if err := decoder.Decode(&presets); err != nil {
|
|
// Distinguish between unknown fields (warning-worthy) and hard parse errors
|
|
if isUnknownFieldError(err) {
|
|
// Re-try without strict mode to get the actual parse result
|
|
var lenient []presetJSON
|
|
if lenientErr := json.Unmarshal(data, &lenient); lenientErr != nil {
|
|
violations = append(violations, presetViolation{
|
|
file: relPath,
|
|
message: fmt.Sprintf("invalid JSON: %v", lenientErr),
|
|
})
|
|
} else {
|
|
violations = append(violations, presetViolation{
|
|
file: relPath,
|
|
message: fmt.Sprintf("unknown fields (stale preset data): %v", err),
|
|
})
|
|
}
|
|
} else {
|
|
violations = append(violations, presetViolation{
|
|
file: relPath,
|
|
message: fmt.Sprintf("invalid JSON: %v", err),
|
|
})
|
|
}
|
|
continue
|
|
}
|
|
|
|
for i, p := range presets {
|
|
if p.ID == "" {
|
|
violations = append(violations, presetViolation{
|
|
file: relPath,
|
|
message: fmt.Sprintf("preset[%d]: missing \"id\" field", i),
|
|
})
|
|
}
|
|
if p.Name == "" {
|
|
violations = append(violations, presetViolation{
|
|
file: relPath,
|
|
message: fmt.Sprintf("preset[%d]: missing \"name\" field", i),
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
return violations
|
|
}
|
|
|
|
func findPresetsFiles(root string) []string {
|
|
var files []string
|
|
_ = filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
|
|
if err != nil || info.IsDir() {
|
|
if info != nil && info.IsDir() {
|
|
switch info.Name() {
|
|
case ".git", "vendor", "node_modules", "web", "dist":
|
|
return filepath.SkipDir
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
if info.Name() == "presets.json" {
|
|
files = append(files, path)
|
|
}
|
|
return nil
|
|
})
|
|
return files
|
|
}
|
|
|
|
func isUnknownFieldError(err error) bool {
|
|
return strings.Contains(err.Error(), "unknown field")
|
|
}
|