diff --git a/data.go b/data.go index 4e07211..ce69695 100644 --- a/data.go +++ b/data.go @@ -62,19 +62,13 @@ func loadData(sources []dataSource) (map[string]node, error) { if len(root) == 0 { return nil, fmt.Errorf("no .json data found") } - // The fences here and the scoped ones [Generator.NewTemplate] runs are one - // sequence split across two entry points; a new fence lands a twin below and - // in NewTemplate (see its comment for the one omission, checkNoCycles). if err := linkRefs(root); err != nil { return nil, err } if err := checkNoCycles(root); err != nil { return nil, err } - if err := checkRepeatReach(root); err != nil { - return nil, err - } - if err := checkBoundLevelsHeld(root); err != nil { + if err := checkScope(treeScope(root)); err != nil { return nil, err } return root, nil diff --git a/graph.go b/graph.go index a5b8868..d3a8258 100644 --- a/graph.go +++ b/graph.go @@ -165,24 +165,28 @@ func pathLeaves(n node, tail []string) []node { return out } -// checkRepeatReach bounds the renders a repeat multiplies to along any root-to-leaf -// path, so nested repeats cannot build what one repeat may not. It runs after -// checkNoCycles, whose guarantee is what lets the walk terminate. -func checkRepeatReach(root map[string]node) error { - mem := reachMemo{} - return walkNodes(root, func(path string, n node) error { - return repeatCheck(path, n, mem) - }) +// nodeScope is the set of nodes one validation pass covers: a whole loaded tree, +// or a single inline node. +type nodeScope func(fn func(path string, n node) error) error + +func treeScope(root map[string]node) nodeScope { + return func(fn func(path string, n node) error) error { return walkNodes(root, fn) } } -// checkNodeRepeatReach is checkRepeatReach for one inline node: each template in -// it is bounded, following reference edges into the already-validated tree the same -// way a repeat through a reference multiplies along a path. -func checkNodeRepeatReach(n node) error { +func inlineScope(n node) nodeScope { + return func(fn func(path string, m node) error) error { return eachNode(n, "template", fn) } +} + +// checkScope runs the per-node fences over a scope, each over the whole scope +// before the next, so which of several broken nodes is reported does not depend on +// the walk. It runs after checkNoCycles, whose guarantee is what lets the walks +// terminate. +func checkScope(s nodeScope) error { mem := reachMemo{} - return eachNode(n, "template", func(path string, m node) error { - return repeatCheck(path, m, mem) - }) + if err := s(func(path string, n node) error { return repeatCheck(path, n, mem) }); err != nil { + return err + } + return s(heldCheck) } type reachMemo map[node]int @@ -204,6 +208,8 @@ func (m reachMemo) of(n node) int { return r } +// repeatCheck bounds the renders a repeat multiplies to along any root-to-leaf +// path, so nested repeats cannot build what one repeat may not. func repeatCheck(path string, n node, mem reachMemo) error { if t, ok := n.(*template); ok && t.repeat > 1 && mem.of(n) > MaxRepeat { return fmt.Errorf("%s: repeat %d multiplies to %d renders along one path, above the maximum %d", path, t.repeat, mem.of(n), MaxRepeat) diff --git a/hold.go b/hold.go index 53a0a8f..908939f 100644 --- a/hold.go +++ b/hold.go @@ -6,27 +6,11 @@ import ( "strings" ) -// checkBoundLevelsHeld rejects every route to a held name except the ones that read -// its draw. An expansion holds one draw of that name; anything else that renders it -// draws again, and the two disagree. checkNoOverlap settles the spellings within one +// heldCheck rejects every route to a held name except the ones that read its draw. +// An expansion holds one draw of that name; anything else that renders it draws +// again, and the two disagree. checkNoOverlap settles the spellings within one // format (a token, an operand); this settles the rest — a reference, whether it // sits in that format or in anything the format renders, however deep. -// -// It runs after checkNoCycles, whose guarantee is what lets the walk terminate. -func checkBoundLevelsHeld(root map[string]node) error { - return walkNodes(root, func(path string, n node) error { - return heldCheck(path, n) - }) -} - -// checkNodeBoundLevelsHeld is checkBoundLevelsHeld for one inline node: the same -// held fence, over its own templates and the shared tree they reference. -func checkNodeBoundLevelsHeld(n node) error { - return eachNode(n, "template", func(path string, m node) error { - return heldCheck(path, m) - }) -} - func heldCheck(path string, n node) error { t, ok := n.(*template) if !ok || len(t.held) == 0 { diff --git a/reference.go b/reference.go index 1327d83..73bd4f2 100644 --- a/reference.go +++ b/reference.go @@ -111,14 +111,23 @@ func linkTemplateRefs(folder []string, path string, t *template, root map[string } // linkNodeRefs binds the references in an inline node's templates against the -// loaded tree. The node has no folder of its own, so every sigil is root-relative: -// / and . mean the root, .. has no folder above it. +// loaded tree. An inline template sits in no folder, so . and .. name nothing and +// are rejected for the root spelling they would otherwise silently mean. func linkNodeRefs(n node, root map[string]node) error { return eachNode(n, "template", func(path string, m node) error { t, ok := m.(*template) if !ok { return nil } + for _, name := range refTokens(t.format) { + sigil, rest, err := refShape(name) + if err != nil { + return fmt.Errorf("%s: reference {%s}: %w", path, name, err) + } + if sigil != "/" { + return fmt.Errorf("%s: reference {%s}: an inline template has no folder; write {/%s}", path, name, rest) + } + } return linkTemplateRefs(nil, path, t, root) }) } diff --git a/render.go b/render.go index 9d13801..b33ddbb 100644 --- a/render.go +++ b/render.go @@ -51,10 +51,10 @@ func (t *Template) Fake() string { // binds its references against the loaded tree, so repeated renders pay the // compile and validation once. It shares [New]'s guarantees: a bad template errors // here, and rendering cannot fail. -// NewTemplate runs the same fences loadData does for a category, scoped to one -// inline node with everything but checkNoCycles: a reference binds only into the -// loaded tree, which has no path into this node, so rendering it cannot reach -// itself. A new fence belongs in both places (loadData and here). +// +// checkNoCycles is the one fence loadData runs that an inline node does not need: +// the loaded tree is proven acyclic at [New], the node is a finite tree, and no +// tree node can reference it, so nothing it renders can reach itself. func (f *Generator) NewTemplate(input string) (*Template, error) { n, err := compileInput(input) if err != nil { @@ -63,10 +63,7 @@ func (f *Generator) NewTemplate(input string) (*Template, error) { if err := linkNodeRefs(n, f.categories); err != nil { return nil, fmt.Errorf("fejkdata: %w", err) } - if err := checkNodeRepeatReach(n); err != nil { - return nil, fmt.Errorf("fejkdata: %w", err) - } - if err := checkNodeBoundLevelsHeld(n); err != nil { + if err := checkScope(inlineScope(n)); err != nil { return nil, fmt.Errorf("fejkdata: %w", err) } return &Template{g: f, n: n}, nil