package fejkdata import ( "fmt" "sort" "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 // 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 { t, ok := n.(*template) if !ok || len(t.held) == 0 { return nil } readers := boundReaders(t.format, t.bound, t.refs) for _, head := range heldHeads(t) { if err := checkHeadHeld(t, head, readers); err != nil { return fmt.Errorf("%s: %w", path, err) } } return nil }) } // heldHeads lists a template's held names, operand heads first, then paths, each // in name order: a level read both ways is reported by the operand's fence, and // which overlap is reported does not vary. func heldHeads(t *template) []string { heads := make([]string, 0, len(t.held)) for head := range t.held { heads = append(heads, head) } sort.Slice(heads, func(i, j int) bool { _, pi := t.bound[heads[i]] _, pj := t.bound[heads[j]] if pi != pj { return !pi } return heads[i] < heads[j] }) return heads } // pinned collects what the hold of head answers for: a path pins the levels it // passes through and the leaf it lands on, an operand exactly the value its render // produces. func pinned(t *template, head string, readers []reader) map[node]bool { held := map[node]bool{} if _, isPath := t.bound[head]; !isPath { operandDraw(t.fields[head], held) return held } for _, r := range readers { if a := splitArm(r.name, t.refs); a.key == head { coverPath(t.fields[head], a.tail, held) } } return held } // checkHeadHeld rejects every route to what head's hold pins except the readers // holding it. One seen set across the edges: a node that cannot reach the level // cannot reach it by another route either, so it is walked once. func checkHeadHeld(t *template, head string, readers []reader) error { held := pinned(t, head, readers) if len(held) == 0 { return nil // a fixed head holds nothing to reach } reader, isPath := t.bound[head] seen := map[node]bool{} for _, e := range renderEdges(t) { if splitArm(e.label, t.refs).key == head || !renders(e.to, held, seen) { continue } if isPath { return fmt.Errorf("%s renders %q, which {%s} reads a path into; name the fields you want instead", e.reached(), head, reader) } return fmt.Errorf("%s renders %q, which a {%s()} also reads; reach it one way so it is drawn once", e.reached(), head, operandReader(t, head)) } return nil } // operandReader names the builtin whose operand holds head. func operandReader(t *template, head string) string { fn := "" _ = eachToken(t.format, func(tok ftoken) error { if tok.kind != 'b' || fn != "" { return nil } if name, _, isFunc := funcCall(tok.body); isFunc { for _, operand := range tokenOperands(tok.body) { if splitArm(operand, t.refs).key == head { fn = name } } } return nil }) return fn } // coverPath collects what holding one path pins: every choice level the path // passes through, whole, and the leaf it renders. func coverPath(n node, tail []string, into map[node]bool) { _ = walkPath(n, tail, pathWalk{ choice: func(c *choice, _ []string) ([]node, error) { cover(c, into, false); return nil, nil }, leaf: func(n node) error { cover(n, into, false); return nil }, }) } // cover collects a level and everything contained in it. A fixed string outside a // choice is left out — it cannot disagree with itself — but inside one each // variant carries its own, so there it counts. func cover(n node, into map[node]bool, inChoice bool) { if isFixed(n) && !inChoice { return } into[n] = true _, isChoice := n.(*choice) for _, c := range contained(n) { cover(c.node, into, inChoice || isChoice) } } // operandDraw collects what one held draw of an operand answers for: the operand // and what rendering it settles inside itself. The builtin renders its operand // whole, so that draw fixes every value the render produced, and a second route to // any of them disagrees with it. // // The walk stops at a reference edge, which is where the operand's own value ends // and a shared source begins: two names referencing one category are two draws, the // same rule {word} {word} follows. func operandDraw(n node, into map[node]bool) { if isFixed(n) { return } if into[n] { return } into[n] = true for _, e := range renderEdges(n) { if isRef(e.label) { continue } operandDraw(e.to, into) } } // isFixed is a string that varies nothing: fixed text with no fields to read into. func isFixed(n node) bool { t, ok := n.(*template) return ok && t.fixed && len(t.fields) == 0 } // renders reports whether rendering n can reach anything in want, following the // same edges expand does. seen keeps a node shared by several routes from being // walked twice; checkNoCycles has already proved the graph is a DAG, so the walk // ends. func renders(n node, want, seen map[node]bool) bool { if want[n] { return true } if seen[n] { return false } seen[n] = true for _, e := range renderEdges(n) { if renders(e.to, want, seen) { return true } } return false } // checkNoOverlap rejects a format that both renders a level and reads a path into // it — {p} beside {p.first}, {p.addr} beside {p.addr.city}, {.p} beside // {/sv_SE.p.first}. The path reads the level's held draw while rendering the level // expands it afresh, so their values would disagree. Reads are compared by their // one spelling, in sorted order, so which pair is reported depends neither on how // a reference was written nor on where the tokens sit. func checkNoOverlap(format string, bound map[string]string, refs map[string]refBinding) error { names := boundReaders(format, bound, refs) // Stable over one format-order scan, so two readers of one name (a token and a // calc operand both naming "p") are reported as the format writes them. sort.SliceStable(names, func(i, j int) bool { return names[i].path < names[j].path }) for i, level := range names { for _, path := range names[i+1:] { if strings.HasPrefix(path.path, level.path+".") { return fmt.Errorf("%s renders a level that {%s} reads a path into; name the fields you want instead", level.label, path.name) } } } return nil } // reader is one way a format reaches a bound field: as written, by its one // spelling, and how to name it. type reader struct{ name, path, label string } // boundReaders lists every way a format reaches a bound field, in the order the // format writes them. An operand renders its field, so it names a level exactly // as a token does; one scan finds both, which is what puts them in one order. func boundReaders(format string, bound map[string]string, refs map[string]refBinding) []reader { var names []reader _ = eachToken(format, func(t ftoken) error { if t.kind != 'b' { return nil } if fn, _, isFunc := funcCall(t.body); isFunc { for _, operand := range tokenOperands(t.body) { a := splitArm(operand, refs) if _, isBound := bound[a.key]; isBound { names = append(names, reader{a.name, a.path, fmt.Sprintf("%s operand %q", fn, operand)}) } } return nil } for _, a := range splitArms(t.body, refs) { if _, isBound := bound[a.key]; isBound { names = append(names, reader{a.name, a.path, "token {" + a.name + "}"}) } } return nil }) return names } // checkNoRepeatedRead rejects a bare token repeated on a held name: {w} {w} beside // {uppercase(w)} would read one draw twice, where {w} {w} alone draws twice. The // error names the single-token spelling. func checkNoRepeatedRead(format string, c formatOps, refs map[string]refBinding) error { count := map[string]int{} return eachToken(format, func(t ftoken) error { if t.kind != 'b' { return nil } if _, _, isFunc := funcCall(t.body); isFunc { return nil } for _, a := range splitArms(t.body, refs) { if len(a.tail) > 0 || !c.held[a.key] { continue } if count[a.key]++; count[a.key] > 1 { return fmt.Errorf("token {%s} is repeated, and %s holds %q to one draw per expansion; write {%s} once", a.name, c.holder[a.key], a.key, a.name) } } return nil }) } // draws is what an expansion has already drawn for its held names: the variant each // was drawn as, so every path under it reads one row, and the value each read, by // its one spelling, so the same read written twice reads one value. type draws struct { variant map[string]node value map[string]string } // readField renders one arm of a token. An arm's key is a sibling field or a // reference linkRefs bound into fields. A name the expansion holds — a level some // token addresses by dotted path, or a field an operand reads — is drawn once and // kept, so {place.postal-code} and {place.locality} read one row, either read twice // gives one value, and a shown operand is the operand computed. Every other name is // drawn afresh, so {word} {word} still draws twice. checkTokens, checkPath and // linkRefs prove every step, so the walk cannot fail. func readField(s *session, t *template, held *draws, a arm) string { if !t.held[a.key] { if len(a.tail) > 0 { panic(fmt.Sprintf("fejkdata: %q reads a path into %q, which the expansion does not hold", a.name, a.key)) } return render(s, t.fields[a.key]) } if v, read := held.value[a.path]; read { return v } var v string _ = walkPath(t.fields[a.key], a.tail, pathWalk{ // Hold the draw at every level passed through, so two paths sharing a // prefix share it. choice: func(c *choice, rest []string) ([]node, error) { key := a.key if consumed := len(a.tail) - len(rest); consumed > 0 { key = a.steps[consumed-1] } n, drew := held.variant[key] if !drew { n = drawn(s, c) held.variant[key] = n } return []node{n}, nil }, leaf: func(n node) error { v = render(s, n); return nil }, }) held.value[a.path] = v return v } // drawn resolves a choice to one variant, so a bound head is a concrete node the // rest of the expansion shares. Nested choices unwrap too: a draw is one value, not // another set to pick from. func drawn(s *session, n node) node { for c, ok := n.(*choice); ok; c, ok = n.(*choice) { n = pick(s, c) } return n }