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>
162 lines
4.2 KiB
Go
162 lines
4.2 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
|
|
"golang.org/x/mod/modfile"
|
|
)
|
|
|
|
type pluginGoModViolation struct {
|
|
file string
|
|
line int
|
|
rule string
|
|
detail string
|
|
}
|
|
|
|
func currentCMSCoreSDKVersion() (string, error) {
|
|
return readRequiredModuleVersion(filepath.Join(blockNinjaRepoRoot(), "backend", "go.mod"), blockCoreImportPrefix)
|
|
}
|
|
|
|
func parseGoModForSafety(goModPath string) (*modfile.File, []pluginGoModViolation) {
|
|
if !fileExists(goModPath) {
|
|
return nil, []pluginGoModViolation{{
|
|
file: filepath.Base(goModPath),
|
|
rule: "missing-go-mod",
|
|
detail: fmt.Sprintf("missing %s", filepath.Base(goModPath)),
|
|
}}
|
|
}
|
|
|
|
data, err := os.ReadFile(goModPath)
|
|
if err != nil {
|
|
return nil, []pluginGoModViolation{{
|
|
file: filepath.Base(goModPath),
|
|
rule: "read-go-mod",
|
|
detail: fmt.Sprintf("failed to read go.mod: %v", err),
|
|
}}
|
|
}
|
|
|
|
parsed, err := modfile.Parse(goModPath, data, nil)
|
|
if err != nil {
|
|
return nil, []pluginGoModViolation{{
|
|
file: filepath.Base(goModPath),
|
|
rule: "parse-go-mod",
|
|
detail: fmt.Sprintf("failed to parse go.mod: %v", err),
|
|
}}
|
|
}
|
|
|
|
return parsed, nil
|
|
}
|
|
|
|
func newGoModReplaceViolation(parsed *modfile.File, replace *modfile.Replace, rule string) pluginGoModViolation {
|
|
detail := fmt.Sprintf("replace directive present for %s", replace.Old.Path)
|
|
if replace.New.Path != "" {
|
|
detail += fmt.Sprintf(" -> %s", replace.New.Path)
|
|
}
|
|
if replace.New.Version != "" {
|
|
detail += fmt.Sprintf(" %s", replace.New.Version)
|
|
}
|
|
|
|
line := 0
|
|
if replace.Syntax != nil {
|
|
line = replace.Syntax.Start.Line
|
|
}
|
|
|
|
return pluginGoModViolation{
|
|
file: filepath.Base(parsed.Syntax.Name),
|
|
line: line,
|
|
rule: rule,
|
|
detail: detail,
|
|
}
|
|
}
|
|
|
|
func checkGoModForAnyReplaceDirectives(goModPath string) []pluginGoModViolation {
|
|
parsed, violations := parseGoModForSafety(goModPath)
|
|
if parsed == nil {
|
|
return violations
|
|
}
|
|
|
|
for _, replace := range parsed.Replace {
|
|
violations = append(violations, newGoModReplaceViolation(parsed, replace, "no-replace-directives"))
|
|
}
|
|
|
|
return violations
|
|
}
|
|
|
|
func checkGoModForModuleReplaceDirective(goModPath, modulePath string) []pluginGoModViolation {
|
|
parsed, violations := parseGoModForSafety(goModPath)
|
|
if parsed == nil {
|
|
return violations
|
|
}
|
|
|
|
for _, replace := range parsed.Replace {
|
|
if replace.Old.Path != modulePath {
|
|
continue
|
|
}
|
|
violations = append(violations, newGoModReplaceViolation(parsed, replace, "no-local-block-core-replace"))
|
|
}
|
|
|
|
return violations
|
|
}
|
|
|
|
func readRequiredModuleVersion(goModPath, modulePath string) (string, error) {
|
|
data, err := os.ReadFile(goModPath)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
parsed, err := modfile.Parse(goModPath, data, nil)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
for _, req := range parsed.Require {
|
|
if req.Mod.Path == modulePath {
|
|
return req.Mod.Version, nil
|
|
}
|
|
}
|
|
return "", fmt.Errorf("%s does not require %s", goModPath, modulePath)
|
|
}
|
|
|
|
func checkCMSCoreSDKGoMod(root string) []pluginGoModViolation {
|
|
return checkGoModForModuleReplaceDirective(filepath.Join(root, "go.mod"), blockCoreImportPrefix)
|
|
}
|
|
|
|
func checkStandalonePluginGoMod(root, requiredSDKVersion string) []pluginGoModViolation {
|
|
goModPath := filepath.Join(root, "go.mod")
|
|
parsed, violations := parseGoModForSafety(goModPath)
|
|
if parsed == nil {
|
|
if len(violations) == 1 && violations[0].rule == "missing-go-mod" {
|
|
violations[0].detail = "standalone plugins must be standalone Go modules with a repo-root go.mod"
|
|
}
|
|
return violations
|
|
}
|
|
|
|
violations = append(violations, checkGoModForAnyReplaceDirectives(goModPath)...)
|
|
|
|
currentSDKVersion := ""
|
|
for _, req := range parsed.Require {
|
|
if req.Mod.Path == blockCoreImportPrefix {
|
|
currentSDKVersion = req.Mod.Version
|
|
break
|
|
}
|
|
}
|
|
if currentSDKVersion == "" {
|
|
violations = append(violations, pluginGoModViolation{
|
|
file: "go.mod",
|
|
rule: "missing-block-core-require",
|
|
detail: fmt.Sprintf("missing required module %s", blockCoreImportPrefix),
|
|
})
|
|
return violations
|
|
}
|
|
|
|
if requiredSDKVersion != "" && currentSDKVersion != requiredSDKVersion {
|
|
violations = append(violations, pluginGoModViolation{
|
|
file: "go.mod",
|
|
rule: "block-core-version-mismatch",
|
|
detail: fmt.Sprintf("requires %s, want %s", currentSDKVersion, requiredSDKVersion),
|
|
})
|
|
}
|
|
|
|
return violations
|
|
}
|