oficina / sheets Public

Clone
git clone https://amadan.net/oficina/sheets

Plain git — no account needed to clone.

Download

Download this file

1// Package guards turns rules that used to live only as prose in
2// AGENTS.md into tests. CARLOS's own rule for this: "A guard a shell
3// step can satisfy is a guard a shell step can be wrong about — put
4// guards inside the test binary." So these run as part of `go test
5// ./...`, not as separate shell greps that a CI step could get subtly
6// wrong (or that a shell rewrite could quietly stop running).
7//
8// This file holds the shared plumbing (repo-root discovery, file
9// walking, import parsing) that every rule's test file uses. It carries
10// no Test functions of its own.
11package guards
12
13import (
14 "go/parser"
15 "go/token"
16 "os"
17 "path/filepath"
18 "runtime"
19 "strconv"
20 "strings"
21 "testing"
22)
23
24// module is the module path declared in this repo's go.mod. Used to
25// confirm repoRoot found the right go.mod, not some unrelated one on a
26// parent path (e.g. if the worktree lives under another Go module).
27const module = "amadan.net/oficina/sheets"
28
29// repoRoot locates the module root by walking up from this source
30// file's own location (via runtime.Caller), not from the working
31// directory. `go test ./...` runs every package's tests with that
32// package's own directory as cwd, so cwd is not a reliable anchor —
33// runtime.Caller(0) always points back to this file, wherever it was
34// invoked from.
35func repoRoot(t *testing.T) string {
36 t.Helper()
37 _, thisFile, _, ok := runtime.Caller(0)
38 if !ok {
39 t.Fatal("guards: runtime.Caller(0) failed")
40 }
41 dir := filepath.Dir(thisFile)
42 for {
43 if b, err := os.ReadFile(filepath.Join(dir, "go.mod")); err == nil {
44 if strings.Contains(string(b), "module "+module+"\n") {
45 return dir
46 }
47 }
48 parent := filepath.Dir(dir)
49 if parent == dir {
50 t.Fatalf("guards: no go.mod for module %q found above %s", module, filepath.Dir(thisFile))
51 }
52 dir = parent
53 }
54}
55
56// skipDirs names directories goFiles never descends into: version
57// control, dependency/vendor trees, and dotfiles-style tooling
58// directories that hold no Go source of ours.
59func skipDir(name string) bool {
60 if strings.HasPrefix(name, ".") {
61 return true
62 }
63 switch name {
64 case "vendor", "node_modules", "testdata":
65 return true
66 }
67 return false
68}
69
70// goFiles returns the repo-relative path of every *.go file under root.
71func goFiles(t *testing.T, root string) []string {
72 t.Helper()
73 var files []string
74 err := filepath.WalkDir(root, func(path string, d os.DirEntry, err error) error {
75 if err != nil {
76 return err
77 }
78 if d.IsDir() {
79 if path != root && skipDir(d.Name()) {
80 return filepath.SkipDir
81 }
82 return nil
83 }
84 if strings.HasSuffix(path, ".go") {
85 files = append(files, path)
86 }
87 return nil
88 })
89 if err != nil {
90 t.Fatalf("guards: walk %s: %v", root, err)
91 }
92 return files
93}
94
95// relPath is filepath.Rel with the guards-package error-handling
96// boilerplate folded in, since every rule needs a repo-relative path
97// for its failure messages.
98func relPath(t *testing.T, root, path string) string {
99 t.Helper()
100 rel, err := filepath.Rel(root, path)
101 if err != nil {
102 t.Fatalf("guards: rel %s: %v", path, err)
103 }
104 return filepath.ToSlash(rel)
105}
106
107// fileImports returns the import paths a Go source file declares. It
108// parses in ImportsOnly mode — the cheapest mode that still sees the
109// import block — so walking every file in the tree on every test run
110// stays fast.
111func fileImports(t *testing.T, path string) []string {
112 t.Helper()
113 fset := token.NewFileSet()
114 f, err := parser.ParseFile(fset, path, nil, parser.ImportsOnly)
115 if err != nil {
116 t.Fatalf("guards: parse %s: %v", path, err)
117 }
118 imports := make([]string, 0, len(f.Imports))
119 for _, imp := range f.Imports {
120 p, err := strconv.Unquote(imp.Path.Value)
121 if err != nil {
122 t.Fatalf("guards: unquote import in %s: %v", path, err)
123 }
124 imports = append(imports, p)
125 }
126 return imports
127}
128
129// inTree reports whether rel — a repo-relative file path — lives inside
130// the directory tree rooted at dir (dir itself, or any subdirectory of
131// it, at any depth). Used instead of comparing filepath.Dir(rel) == dir
132// so a rule means "this whole package tree" and not just "files whose
133// immediate parent happens to match" — a future subpackage (e.g.
134// calc/functions) must inherit the rule, not silently fall outside it.
135func inTree(rel, dir string) bool {
136 return strings.HasPrefix(rel, dir+"/")
137}
138
139// importUnder reports whether imp is base itself, or an import path
140// nested under base (base plus "/something"). Used instead of plain
141// equality so a forbidden import stays forbidden if it grows
142// subpackages later (e.g. a future .../store/schema), rather than the
143// check silently narrowing to just the package's root import path.
144func importUnder(imp, base string) bool {
145 return imp == base || strings.HasPrefix(imp, base+"/")
146}
147