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>
128 lines
2.5 KiB
Go
128 lines
2.5 KiB
Go
package main
|
|
|
|
import (
|
|
"bufio"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"sort"
|
|
|
|
"git.dev.alexdunmow.com/block/check-safety/internal/helpers"
|
|
"strings"
|
|
)
|
|
|
|
type todoViolation struct {
|
|
file string
|
|
line int
|
|
snippet string
|
|
}
|
|
|
|
type todoScanRoot struct {
|
|
root string
|
|
displayPrefix string
|
|
}
|
|
|
|
var todoFileExts = map[string]bool{
|
|
".css": true,
|
|
".go": true,
|
|
".html": true,
|
|
".js": true,
|
|
".jsx": true,
|
|
".scss": true,
|
|
".sql": true,
|
|
".templ": true,
|
|
".ts": true,
|
|
".tsx": true,
|
|
}
|
|
|
|
var todoDirSkips = map[string]bool{
|
|
".git": true,
|
|
"dist": true,
|
|
"node_modules": true,
|
|
"vendor": true,
|
|
}
|
|
|
|
var reTODO = regexp.MustCompile(`(?i)\bTODO(?:\([^)]*\))?\b`)
|
|
|
|
func checkTODOs(roots []todoScanRoot) []todoViolation {
|
|
var violations []todoViolation
|
|
|
|
for _, scanRoot := range roots {
|
|
if scanRoot.root == "" {
|
|
continue
|
|
}
|
|
|
|
if err := filepath.Walk(scanRoot.root, func(path string, info os.FileInfo, err error) error {
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
if info.IsDir() {
|
|
if todoDirSkips[info.Name()] {
|
|
return filepath.SkipDir
|
|
}
|
|
return nil
|
|
}
|
|
|
|
slashPath := filepath.ToSlash(path)
|
|
ext := filepath.Ext(path)
|
|
if !todoFileExts[ext] {
|
|
return nil
|
|
}
|
|
if strings.Contains(slashPath, "cmd/check-safety/") {
|
|
return nil
|
|
}
|
|
if strings.HasSuffix(path, "_test.go") ||
|
|
strings.HasSuffix(path, ".test.ts") ||
|
|
strings.HasSuffix(path, ".test.tsx") ||
|
|
strings.HasSuffix(path, ".spec.ts") ||
|
|
strings.HasSuffix(path, ".spec.tsx") {
|
|
return nil
|
|
}
|
|
|
|
f, openErr := os.Open(path)
|
|
if openErr != nil {
|
|
return nil
|
|
}
|
|
defer helpers.LogDeferredError(nil, "close TODO scan file", f.Close, "path", path)
|
|
|
|
relPath, _ := filepath.Rel(scanRoot.root, path)
|
|
if relPath == "" {
|
|
relPath = path
|
|
}
|
|
relPath = filepath.ToSlash(relPath)
|
|
if scanRoot.displayPrefix != "" {
|
|
relPath = scanRoot.displayPrefix + "/" + relPath
|
|
}
|
|
|
|
scanner := bufio.NewScanner(f)
|
|
lineNum := 0
|
|
for scanner.Scan() {
|
|
lineNum++
|
|
line := scanner.Text()
|
|
if !reTODO.MatchString(line) {
|
|
continue
|
|
}
|
|
|
|
violations = append(violations, todoViolation{
|
|
file: relPath,
|
|
line: lineNum,
|
|
snippet: strings.TrimSpace(line),
|
|
})
|
|
}
|
|
|
|
return nil
|
|
}); err != nil {
|
|
_ = err // Walk errors are non-fatal for TODO checks
|
|
}
|
|
}
|
|
|
|
sort.Slice(violations, func(i, j int) bool {
|
|
if violations[i].file == violations[j].file {
|
|
return violations[i].line < violations[j].line
|
|
}
|
|
return violations[i].file < violations[j].file
|
|
})
|
|
|
|
return violations
|
|
}
|