Files
zip/binding_test.go
T
zeekayandHanzo Dev 41f9a5d0fc use: a handler that answers an address is refused, at every depth
Two things, both closing a gap between what walk.go states and what it checks.

A HANDLER MAY NEVER TERMINATE A REQUEST. Enforced now. The inert check only ever
asked something strictly weaker — that a definition carrying middleware has
routes beneath it — which app.Use(zip.Static(assets)) passes on any app with a
route, and its depth-0 skip meant the app being SERVED was never asked at all.
That is exactly where a wiring file writes it, and the static handler then ran
before every route in the composition and answered each one with a file, or a
404 where no file existed. walk.go's closing comment said so plainly: the rule
condemned no form anyone writes.

Terminality is now a property the walk can read. A leaf constructor returns its
handler through zip.Terminal — exported, so wsx.Upgrade and third-party leaves
come through the same door — and the identity is the closure's code pointer,
never a name match. AdaptNetHTTP and AdaptNetHTTPMiddleware return closures with
the same body from the same file, and only their authors can know which one
answers. The handler comes back unwrapped, so registering a leaf at an address
is byte for byte the registration it always was. The depth-0 exemption stays
where it belongs, in the inert check alone: root middleware is router middleware
and covers requests that match nothing, so it is never inert.

ONE INTERPRETER. routesUnder re-walked App.entries per definition with a cycle
guard of its own — a second authority that could disagree with the walk about
the program it was asked about, and that read a definition's LIVE entry list
rather than the one the generation was built from. It is now one pass over the
occurrence slice, because a trail cell carries its *App, which makes an
occurrence's breadcrumb its ancestor chain; a cyclic composition never reaches a
reducer, so there is no guard to need. materialise reads its Handler through the
third accessor the other two always documented, instead of asserting the
payload.

The two AST guards in binding_test.go become one that is strictly stronger:
function granularity rather than file, bare type assertions as well as switches,
and every allow-listed site must still be HIT — so deleting or renaming one
fails the test rather than leaving it quietly vacuous.

derived() already owns operation-id collisions; nothing here touches it.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-02 14:32:27 -07:00

190 lines
6.7 KiB
Go

package zip
import (
"go/ast"
"go/parser"
"go/token"
"os"
"path/filepath"
"sort"
"strings"
"testing"
)
// The binding requirement, enforced mechanically rather than by review.
//
// "One walk, and every projection is a reducer over its result" is the claim the
// whole design rests on, and it is the kind of claim that decays silently: a
// second reader of App.entries is four lines, looks harmless in review, reads
// exactly like the first one, and immediately becomes a second authority on what
// a composition means. That is how routesUnder came to re-walk the tree with a
// cycle guard of its own, able to disagree with the walk about the program it
// was asked about.
//
// Prose cannot hold the line and neither can review, so the line is a test. It
// parses this package's own source and refuses any read of an entry's payload,
// and any traversal of an entry list, outside the sites named below.
//
// It is deliberately SYNTACTIC. A type-checked version would be more precise and
// would need the whole dependency graph loaded to say anything at all; the four
// shapes below are the only ones the offence can take, they are cheap to
// recognise, and a test that runs in milliseconds with no toolchain
// prerequisites is a test that keeps running.
//
// x.n.(type) a type switch over a payload
// x.n.(T) a type assertion on a payload
// case route: a switch that names a payload type, however it was reached
// range x.entries a traversal of a program's entry list
//
// What is NOT an offence: switching over [Component] (the Use surface is a
// different closed set, and compose is entitled to know it — deciding what to
// APPEND is construction, not reduction), and reading an occurrence's kind
// through [occurrence.kind] and the accessors, which is what every reducer does.
func TestOneInterpreter_NothingOutsideTheWalkReadsAPayloadOrWalksTheTree(t *testing.T) {
// file/func -> why this one site is allowed. Every entry must be HIT, so
// deleting the walk cannot make this test pass by having nothing to find.
allowed := map[string]string{
"walk.go/descend": "the ONE type switch over node, and the one traversal of an entry list",
"walk.go/kind": "the normaliser: turns the payload into the discriminator reducers switch on",
"walk.go/middleware": "sanctioned accessor: one assertion, no traversal",
"walk.go/route": "sanctioned accessor: one assertion, no traversal",
"walk.go/app": "sanctioned accessor: one assertion, no traversal",
"host.go/Drop": "the WRITER of an entry list: filters the receiver's own entries " +
"between generations, and recurses nowhere",
"host.go/Reload": "the WRITER of an entry list: swaps a definition for a new version " +
"of itself by name, and recurses nowhere",
}
hit := map[string]bool{}
var offences []string
for _, path := range packageFiles(t) {
fset := token.NewFileSet()
file, err := parser.ParseFile(fset, path, nil, 0)
if err != nil {
t.Fatalf("parse %s: %v", path, err)
}
base := filepath.Base(path)
for _, decl := range file.Decls {
fn, ok := decl.(*ast.FuncDecl)
if !ok || fn.Body == nil {
continue
}
site := base + "/" + fn.Name.Name
report := func(n ast.Node, what string) {
if _, ok := allowed[site]; ok {
hit[site] = true
return
}
offences = append(offences, fset.Position(n.Pos()).String()+"\n\t\t"+what+
"\n\t\tin "+site+" — reduce walk's []occurrence instead")
}
ast.Inspect(fn.Body, func(n ast.Node) bool {
switch s := n.(type) {
case *ast.TypeSwitchStmt:
if isPayload(typeSwitchSubject(s)) {
report(s, "type switch over an entry's payload (x.n.(type))")
} else if namesPayloadType(s) {
report(s, "type switch naming a node payload type (case route:)")
}
case *ast.TypeAssertExpr:
// A nil Type is the `x.(type)` inside the switch above,
// already reported as the switch it belongs to.
if s.Type != nil && isPayload(s.X) {
report(s, "type assertion on an entry's payload (x.n.(T))")
}
case *ast.RangeStmt:
if isEntries(s.X) {
report(s, "traversal of an App's entry list (range x.entries)")
}
}
return true
})
}
}
if len(offences) > 0 {
sort.Strings(offences)
t.Errorf("the walk is no longer the only interpreter — %d second authority/authorities:\n\t%s",
len(offences), strings.Join(offences, "\n\t"))
}
for site, why := range allowed {
if !hit[site] {
t.Errorf("%s no longer does what it is allow-listed for (%s) — "+
"if it moved, move the allowance; if it is gone, delete it, but do not leave this test "+
"passing because there is nothing left to check", site, why)
}
}
}
// packageFiles is this package's own source, tests excluded: a test file is
// allowed to take a program apart any way it likes, because a test asserts on
// the AST rather than interpreting it for a running server.
func packageFiles(t *testing.T) []string {
t.Helper()
ents, err := os.ReadDir(".")
if err != nil {
t.Fatalf("read package dir: %v", err)
}
var out []string
for _, e := range ents {
name := e.Name()
if e.IsDir() || !strings.HasSuffix(name, ".go") || strings.HasSuffix(name, "_test.go") {
continue
}
out = append(out, name)
}
if len(out) < 20 {
t.Fatalf("found %d source files; the scan is not seeing the package", len(out))
}
sort.Strings(out)
return out
}
// typeSwitchSubject is the expression a type switch switches ON, for both
// spellings — `switch x := e.(type)` and `switch e.(type)`.
func typeSwitchSubject(s *ast.TypeSwitchStmt) ast.Expr {
var e ast.Expr
switch a := s.Assign.(type) {
case *ast.AssignStmt:
if len(a.Rhs) == 1 {
e = a.Rhs[0]
}
case *ast.ExprStmt:
e = a.X
}
if ta, ok := e.(*ast.TypeAssertExpr); ok {
return ta.X
}
return nil
}
// namesPayloadType catches a switch over a payload reached under any other name,
// by looking at what it decides: `route` is a node payload and belongs to no
// other closed set in this package, so a case for it is a case over node.
func namesPayloadType(s *ast.TypeSwitchStmt) bool {
for _, stmt := range s.Body.List {
cc, ok := stmt.(*ast.CaseClause)
if !ok {
continue
}
for _, texp := range cc.List {
if id, ok := texp.(*ast.Ident); ok && id.Name == "route" {
return true
}
}
}
return false
}
// isPayload reports whether e reads an entry's or an occurrence's payload field.
// Both spell it `n`, which is what makes this one predicate.
func isPayload(e ast.Expr) bool { return fieldNamed(e, "n") }
// isEntries reports whether e is an App's program.
func isEntries(e ast.Expr) bool { return fieldNamed(e, "entries") }
func fieldNamed(e ast.Expr, name string) bool {
sel, ok := e.(*ast.SelectorExpr)
return ok && sel.Sel != nil && sel.Sel.Name == name
}