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>
129 lines
3.2 KiB
Go
129 lines
3.2 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"go/ast"
|
|
"go/parser"
|
|
"go/token"
|
|
"os"
|
|
"path/filepath"
|
|
"slices"
|
|
"strings"
|
|
)
|
|
|
|
type errLeakViolation struct {
|
|
file string
|
|
line int
|
|
snippet string
|
|
}
|
|
|
|
// checkErrLeak finds places where err.Error() is sent directly to HTTP clients.
|
|
// These leak internal details. Use writeInternalServerError/writeServerError instead.
|
|
func checkErrLeak(root string) []errLeakViolation {
|
|
var violations []errLeakViolation
|
|
|
|
if err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
|
|
if err != nil || info.IsDir() || !strings.HasSuffix(path, ".go") {
|
|
return nil
|
|
}
|
|
if strings.Contains(path, "/vendor/") || strings.HasSuffix(path, "_test.go") {
|
|
return nil
|
|
}
|
|
fset := token.NewFileSet()
|
|
f, err := parser.ParseFile(fset, path, nil, 0)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
|
|
relPath, _ := filepath.Rel(root, path)
|
|
|
|
ast.Inspect(f, func(n ast.Node) bool {
|
|
call, ok := n.(*ast.CallExpr)
|
|
if !ok {
|
|
return true
|
|
}
|
|
|
|
// Pattern 1: http.Error(w, err.Error(), ...)
|
|
if sel, ok := call.Fun.(*ast.SelectorExpr); ok {
|
|
if ident, ok := sel.X.(*ast.Ident); ok && ident.Name == "http" && sel.Sel.Name == "Error" {
|
|
if len(call.Args) >= 2 && containsErrorCall(call.Args[1]) {
|
|
pos := fset.Position(call.Pos())
|
|
violations = append(violations, errLeakViolation{
|
|
file: relPath,
|
|
line: pos.Line,
|
|
snippet: "http.Error(w, err.Error(), ...) \u2014 leaks internal error",
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
// Pattern 2: fmt.Fprintf(w, ... err.Error() ...)
|
|
if sel, ok := call.Fun.(*ast.SelectorExpr); ok {
|
|
if ident, ok := sel.X.(*ast.Ident); ok && ident.Name == "fmt" {
|
|
if sel.Sel.Name == "Fprintf" || sel.Sel.Name == "Fprintln" {
|
|
if len(call.Args) > 1 {
|
|
if slices.ContainsFunc(call.Args[1:], containsErrorCall) {
|
|
pos := fset.Position(call.Pos())
|
|
violations = append(violations, errLeakViolation{
|
|
file: relPath,
|
|
line: pos.Line,
|
|
snippet: fmt.Sprintf("fmt.%s(w, ...err.Error()...) \u2014 leaks internal error", sel.Sel.Name),
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Pattern 3: w.Write([]byte(err.Error()))
|
|
if sel, ok := call.Fun.(*ast.SelectorExpr); ok {
|
|
if sel.Sel.Name == "Write" && len(call.Args) == 1 {
|
|
if containsErrorCall(call.Args[0]) {
|
|
pos := fset.Position(call.Pos())
|
|
violations = append(violations, errLeakViolation{
|
|
file: relPath,
|
|
line: pos.Line,
|
|
snippet: "w.Write([]byte(err.Error())) \u2014 leaks internal error",
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
return true
|
|
})
|
|
|
|
return nil
|
|
}); err != nil {
|
|
violations = append(violations, errLeakViolation{
|
|
file: filepath.ToSlash(root),
|
|
line: 0,
|
|
snippet: err.Error(),
|
|
})
|
|
}
|
|
|
|
return violations
|
|
}
|
|
|
|
// containsErrorCall checks if an expression contains a call to .Error() on any receiver identifier.
|
|
func containsErrorCall(expr ast.Expr) bool {
|
|
found := false
|
|
ast.Inspect(expr, func(n ast.Node) bool {
|
|
if found {
|
|
return false
|
|
}
|
|
call, ok := n.(*ast.CallExpr)
|
|
if !ok {
|
|
return true
|
|
}
|
|
sel, ok := call.Fun.(*ast.SelectorExpr)
|
|
if !ok || sel.Sel.Name != "Error" {
|
|
return true
|
|
}
|
|
if _, ok := sel.X.(*ast.Ident); ok {
|
|
found = true
|
|
}
|
|
return true
|
|
})
|
|
return found
|
|
}
|