package guards import ( "go/ast" "go/parser" "go/token" "strconv" "strings" "testing" ) // Gate 9 (§12) scans what this app logs through slog. Anything that // writes somewhere else is not scanned and cannot be — so the rule is // that nothing else may write. // // This is not theoretical. chi's own middleware.Logger builds // log.New(os.Stdout, "", log.LstdFlags) at package init and writes the // request line — URL included — straight to stdout, touching neither // slog nor the log package default. middleware.Recoverer writes the // panic and stack to os.Stderr the same way. Either one is the most // natural thing for someone to reach for when adding request logging in // step 2, and either one would carry an address past a green gate. const chiMiddleware = "github.com/go-chi/chi/v5/middleware" // forbiddenSelectors are names that write outside slog, keyed by IMPORT // PATH rather than by the identifier they happen to be spelled with. // os.Stdout/os.Stderr are the raw sinks; the log package's own printers // go to its default writer, which is not the recorder either. // // Keying on the path is what makes `import goos "os"; goos.Stderr` fail // too. An earlier version compared the identifier to the literal "os", // which an alias walks straight past — and which also fired on a local // variable that happened to be named log. var forbiddenSelectors = map[string]map[string]string{ "os": { "Stdout": "writes outside slog, so gate 9 cannot see it", "Stderr": "writes outside slog, so gate 9 cannot see it", }, "log": { "New": "builds a logger gate 9 cannot capture; use slog", "Print": "writes to the log default, not slog", "Printf": "writes to the log default, not slog", "Println": "writes to the log default, not slog", "Fatal": "writes to the log default, not slog", "Fatalf": "writes to the log default, not slog", }, } // TestNoLogSinkOutsideSlog fails on any non-test file that imports chi's // middleware package or names a writer gate 9 cannot observe. // // cmd/home/main.go is in scope: it is where a stray fmt.Fprintln(os.Stderr) // would most plausibly land. os.Exit and os.Getenv are untouched by this // rule — only the two stream variables are named. func TestNoLogSinkOutsideSlog(t *testing.T) { root := repoRoot(t) for _, path := range goFiles(t, root) { if strings.HasSuffix(path, "_test.go") { continue } rel := relPath(t, root, path) for _, imp := range fileImports(t, path) { if imp == chiMiddleware { t.Errorf("%s: imports %q, whose Logger writes to os.Stdout and Recoverer to os.Stderr — neither is visible to gate 9", rel, chiMiddleware) } } fset := token.NewFileSet() f, err := parser.ParseFile(fset, path, nil, 0) if err != nil { t.Fatalf("guards: parse %s: %v", rel, err) } // Local identifier -> import path, so an alias resolves to the // package it actually names. local := map[string]string{} for _, imp := range f.Imports { ipath, err := strconv.Unquote(imp.Path.Value) if err != nil { t.Fatalf("guards: unquote import in %s: %v", rel, err) } name := ipath[strings.LastIndex(ipath, "/")+1:] if imp.Name != nil { name = imp.Name.Name } // A dot-import puts the package's names in file scope // unqualified, so os.Stderr becomes a bare Stderr with no // selector to match. Refuse the import rather than try to // resolve it. if name == "." { if _, forbidden := forbiddenSelectors[ipath]; forbidden { t.Errorf("%s:%d: dot-imports %q, which hides its sinks from this guard", rel, fset.Position(imp.Pos()).Line, ipath) } continue } local[name] = ipath } // The separate native CLI returns the approved structured public result. // Permit only that argument, not other raw writes in the same file. structuredResult := map[*ast.SelectorExpr]bool{} if rel == "cmd/oficina/main_linux.go" { ast.Inspect(f, func(n ast.Node) bool { call, ok := n.(*ast.CallExpr) if !ok || len(call.Args) != 2 { return true } fun, ok := call.Fun.(*ast.SelectorExpr) if !ok || fun.Sel.Name != "Run" { return true } pkg, ok := fun.X.(*ast.Ident) if !ok || local[pkg.Name] != "amadan.net/oficina/home/internal/provisioner" { return true } sink, ok := call.Args[1].(*ast.SelectorExpr) if !ok || sink.Sel.Name != "Stdout" { return true } owner, ok := sink.X.(*ast.Ident) if ok && local[owner.Name] == "os" { structuredResult[sink] = true } return true }) } ast.Inspect(f, func(n ast.Node) bool { sel, ok := n.(*ast.SelectorExpr) if !ok { return true } ident, ok := sel.X.(*ast.Ident) if !ok { return true } ipath, isPackage := local[ident.Name] if !isPackage { return true } if why, bad := forbiddenSelectors[ipath][sel.Sel.Name]; bad && !structuredResult[sel] { t.Errorf("%s:%d: %s.%s (%s) — %s", rel, fset.Position(sel.Pos()).Line, ident.Name, sel.Sel.Name, ipath, why) } return true }) } }