// Package guards turns rules that used to live only as prose in // AGENTS.md into tests. CARLOS's own rule for this: "A guard a shell // step can satisfy is a guard a shell step can be wrong about — put // guards inside the test binary." So these run as part of `go test // ./...`, not as separate shell greps that a CI step could get subtly // wrong (or that a shell rewrite could quietly stop running). // // This file holds the shared plumbing (repo-root discovery, file // walking, import parsing) that every rule's test file uses. It carries // no Test functions of its own. package guards import ( "go/parser" "go/token" "os" "path/filepath" "runtime" "strconv" "strings" "testing" ) // module is the module path declared in this repo's go.mod. Used to // confirm repoRoot found the right go.mod, not some unrelated one on a // parent path (e.g. if the worktree lives under another Go module). const module = "amadan.net/oficina/sheets" // repoRoot locates the module root by walking up from this source // file's own location (via runtime.Caller), not from the working // directory. `go test ./...` runs every package's tests with that // package's own directory as cwd, so cwd is not a reliable anchor — // runtime.Caller(0) always points back to this file, wherever it was // invoked from. func repoRoot(t *testing.T) string { t.Helper() _, thisFile, _, ok := runtime.Caller(0) if !ok { t.Fatal("guards: runtime.Caller(0) failed") } dir := filepath.Dir(thisFile) for { if b, err := os.ReadFile(filepath.Join(dir, "go.mod")); err == nil { if strings.Contains(string(b), "module "+module+"\n") { return dir } } parent := filepath.Dir(dir) if parent == dir { t.Fatalf("guards: no go.mod for module %q found above %s", module, filepath.Dir(thisFile)) } dir = parent } } // skipDirs names directories goFiles never descends into: version // control, dependency/vendor trees, and dotfiles-style tooling // directories that hold no Go source of ours. func skipDir(name string) bool { if strings.HasPrefix(name, ".") { return true } switch name { case "vendor", "node_modules", "testdata": return true } return false } // goFiles returns the repo-relative path of every *.go file under root. func goFiles(t *testing.T, root string) []string { t.Helper() var files []string err := filepath.WalkDir(root, func(path string, d os.DirEntry, err error) error { if err != nil { return err } if d.IsDir() { if path != root && skipDir(d.Name()) { return filepath.SkipDir } return nil } if strings.HasSuffix(path, ".go") { files = append(files, path) } return nil }) if err != nil { t.Fatalf("guards: walk %s: %v", root, err) } return files } // relPath is filepath.Rel with the guards-package error-handling // boilerplate folded in, since every rule needs a repo-relative path // for its failure messages. func relPath(t *testing.T, root, path string) string { t.Helper() rel, err := filepath.Rel(root, path) if err != nil { t.Fatalf("guards: rel %s: %v", path, err) } return filepath.ToSlash(rel) } // fileImports returns the import paths a Go source file declares. It // parses in ImportsOnly mode — the cheapest mode that still sees the // import block — so walking every file in the tree on every test run // stays fast. func fileImports(t *testing.T, path string) []string { t.Helper() fset := token.NewFileSet() f, err := parser.ParseFile(fset, path, nil, parser.ImportsOnly) if err != nil { t.Fatalf("guards: parse %s: %v", path, err) } imports := make([]string, 0, len(f.Imports)) for _, imp := range f.Imports { p, err := strconv.Unquote(imp.Path.Value) if err != nil { t.Fatalf("guards: unquote import in %s: %v", path, err) } imports = append(imports, p) } return imports } // inTree reports whether rel — a repo-relative file path — lives inside // the directory tree rooted at dir (dir itself, or any subdirectory of // it, at any depth). Used instead of comparing filepath.Dir(rel) == dir // so a rule means "this whole package tree" and not just "files whose // immediate parent happens to match" — a future subpackage (e.g. // calc/functions) must inherit the rule, not silently fall outside it. func inTree(rel, dir string) bool { return strings.HasPrefix(rel, dir+"/") } // importUnder reports whether imp is base itself, or an import path // nested under base (base plus "/something"). Used instead of plain // equality so a forbidden import stays forbidden if it grows // subpackages later (e.g. a future .../store/schema), rather than the // check silently narrowing to just the package's root import path. func importUnder(imp, base string) bool { return imp == base || strings.HasPrefix(imp, base+"/") }