check-safety/lint.go
Alex Dunmow cd88c808b0 initial: standalone check-safety module hoisted from CMS
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>
2026-06-06 13:04:02 +08:00

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
}