Compare and cache a read by its one spelling, so two spellings of one reference are one level; reject a slash after a sigil naming the spelling
Tests / vet + fmt + tests (pull_request) Successful in 55s
Tests / vet + fmt + tests (pull_request) Successful in 55s
This commit is contained in:
@@ -7,6 +7,7 @@
|
|||||||
- One spelling per result: reject the other at `New`, and let the error name the spelling to use.
|
- One spelling per result: reject the other at `New`, and let the error name the spelling to use.
|
||||||
- A standing choice a reader would relitigate goes under Decisions in the README, not in a comment.
|
- A standing choice a reader would relitigate goes under Decisions in the README, not in a comment.
|
||||||
- A README example is a `json` block that loads and renders as a category; `readme_test.go` runs every one.
|
- A README example is a `json` block that loads and renders as a category; `readme_test.go` runs every one.
|
||||||
|
- Cyclomatic complexity is gated at 14: the table-shaped dispatches (`eachToken`, `calc.factor`, `walkPath`) sit at 13–14 and stay whole; anything else that reaches 14 is decomposed.
|
||||||
|
|
||||||
# Deferred
|
# Deferred
|
||||||
|
|
||||||
|
|||||||
@@ -179,60 +179,20 @@ func renders(n node, want, seen map[node]bool) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// arm is one alternative of a {a|b} token or one operand, split into the key
|
|
||||||
// naming the node in a template's fields (a sibling field, or the head a
|
|
||||||
// reference is bound under) and the tail of a dotted path into it. A non-empty
|
|
||||||
// tail is what makes the arm a bound draw: its head is drawn once per expansion
|
|
||||||
// (see compileOps).
|
|
||||||
type arm struct {
|
|
||||||
name string // as written, and the key a bound draw's value is held under
|
|
||||||
key string
|
|
||||||
tail []string
|
|
||||||
steps []string // key per level passed through; the head and leaf hold their own
|
|
||||||
}
|
|
||||||
|
|
||||||
// splitArm splits one name into key and tail. refs maps a reference to what
|
|
||||||
// linkRefs bound it to; before linking, a reference is whole.
|
|
||||||
func splitArm(name string, refs map[string]refBinding) arm {
|
|
||||||
if isRef(name) {
|
|
||||||
b, bound := refs[name]
|
|
||||||
if !bound || len(b.tail) == 0 {
|
|
||||||
key := name
|
|
||||||
if bound {
|
|
||||||
key = b.key
|
|
||||||
}
|
|
||||||
return arm{name: name, key: key}
|
|
||||||
}
|
|
||||||
return pathArm(name, b.key, b.tail)
|
|
||||||
}
|
|
||||||
head, tail, dotted := strings.Cut(name, ".")
|
|
||||||
if !dotted {
|
|
||||||
return arm{name: name, key: name}
|
|
||||||
}
|
|
||||||
return pathArm(name, head, strings.Split(tail, "."))
|
|
||||||
}
|
|
||||||
|
|
||||||
func pathArm(name, key string, segs []string) arm {
|
|
||||||
var steps []string
|
|
||||||
for i := 0; i < len(segs)-1; i++ { // every level except the leaf's own
|
|
||||||
steps = append(steps, key+"."+strings.Join(segs[:i+1], "."))
|
|
||||||
}
|
|
||||||
return arm{name: name, key: key, tail: segs, steps: steps}
|
|
||||||
}
|
|
||||||
|
|
||||||
// checkNoOverlap rejects a format that both renders a level and reads a path into
|
// checkNoOverlap rejects a format that both renders a level and reads a path into
|
||||||
// it — {p} beside {p.first}, or {p.addr} beside {p.addr.city}. The path reads the
|
// it — {p} beside {p.first}, {p.addr} beside {p.addr.city}, {.p} beside
|
||||||
// level's held draw while rendering the level expands it afresh, so their values
|
// {/sv_SE.p.first}. The path reads the level's held draw while rendering the level
|
||||||
// would disagree. Names are compared in sorted order, so which pair is reported
|
// expands it afresh, so their values would disagree. Reads are compared by their
|
||||||
// does not depend on where the tokens sit.
|
// 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 {
|
func checkNoOverlap(format string, bound map[string]string, refs map[string]refBinding) error {
|
||||||
names := boundReaders(format, bound, refs)
|
names := boundReaders(format, bound, refs)
|
||||||
// Stable over one format-order scan, so two readers of one name (a token and a
|
// 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.
|
// calc operand both naming "p") are reported as the format writes them.
|
||||||
sort.SliceStable(names, func(i, j int) bool { return names[i].name < names[j].name })
|
sort.SliceStable(names, func(i, j int) bool { return names[i].path < names[j].path })
|
||||||
for i, level := range names {
|
for i, level := range names {
|
||||||
for _, path := range names[i+1:] {
|
for _, path := range names[i+1:] {
|
||||||
if strings.HasPrefix(path.name, level.name+".") {
|
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 fmt.Errorf("%s renders a level that {%s} reads a path into; name the fields you want instead", level.label, path.name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -240,8 +200,9 @@ func checkNoOverlap(format string, bound map[string]string, refs map[string]refB
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// reader is one way a format reaches a bound field, and how to name that spelling.
|
// reader is one way a format reaches a bound field: as written, by its one
|
||||||
type reader struct{ name, label string }
|
// 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
|
// 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
|
// format writes them. An operand renders its field, so it names a level exactly
|
||||||
@@ -256,14 +217,14 @@ func boundReaders(format string, bound map[string]string, refs map[string]refBin
|
|||||||
for _, operand := range tokenOperands(t.body) {
|
for _, operand := range tokenOperands(t.body) {
|
||||||
a := splitArm(operand, refs)
|
a := splitArm(operand, refs)
|
||||||
if _, isBound := bound[a.key]; isBound {
|
if _, isBound := bound[a.key]; isBound {
|
||||||
names = append(names, reader{a.name, fmt.Sprintf("%s operand %q", fn, operand)})
|
names = append(names, reader{a.name, a.path, fmt.Sprintf("%s operand %q", fn, operand)})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
for _, a := range splitArms(t.body, refs) {
|
for _, a := range splitArms(t.body, refs) {
|
||||||
if _, isBound := bound[a.key]; isBound {
|
if _, isBound := bound[a.key]; isBound {
|
||||||
names = append(names, reader{a.name, "token {" + a.name + "}"})
|
names = append(names, reader{a.name, a.path, "token {" + a.name + "}"})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@@ -271,16 +232,6 @@ func boundReaders(format string, bound map[string]string, refs map[string]refBin
|
|||||||
return names
|
return names
|
||||||
}
|
}
|
||||||
|
|
||||||
// splitArms splits a token body's '|' alternatives.
|
|
||||||
func splitArms(body string, refs map[string]refBinding) []arm {
|
|
||||||
parts := strings.Split(body, "|")
|
|
||||||
arms := make([]arm, len(parts))
|
|
||||||
for i, p := range parts {
|
|
||||||
arms[i] = splitArm(p, refs)
|
|
||||||
}
|
|
||||||
return arms
|
|
||||||
}
|
|
||||||
|
|
||||||
// checkNoRepeatedRead rejects a bare token repeated on a held name: {w} {w} beside
|
// 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
|
// {uppercase(w)} would read one draw twice, where {w} {w} alone draws twice. The
|
||||||
// error names the single-token spelling.
|
// error names the single-token spelling.
|
||||||
@@ -306,8 +257,8 @@ func checkNoRepeatedRead(format string, c formatOps, refs map[string]refBinding)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// draws is what an expansion has already drawn for its held names: the variant each
|
// 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, so
|
// was drawn as, so every path under it reads one row, and the value each read, by
|
||||||
// the same name read twice reads one value.
|
// its one spelling, so the same read written twice reads one value.
|
||||||
type draws struct {
|
type draws struct {
|
||||||
variant map[string]node
|
variant map[string]node
|
||||||
value map[string]string
|
value map[string]string
|
||||||
@@ -324,7 +275,7 @@ func readField(s *session, t *template, held *draws, a arm) string {
|
|||||||
if !t.held[a.key] {
|
if !t.held[a.key] {
|
||||||
return render(s, t.fields[a.key])
|
return render(s, t.fields[a.key])
|
||||||
}
|
}
|
||||||
if v, read := held.value[a.name]; read {
|
if v, read := held.value[a.path]; read {
|
||||||
return v
|
return v
|
||||||
}
|
}
|
||||||
var v string
|
var v string
|
||||||
@@ -345,7 +296,7 @@ func readField(s *session, t *template, held *draws, a arm) string {
|
|||||||
},
|
},
|
||||||
leaf: func(n node) error { v = render(s, n); return nil },
|
leaf: func(n node) error { v = render(s, n); return nil },
|
||||||
})
|
})
|
||||||
held.value[a.name] = v
|
held.value[a.path] = v
|
||||||
return v
|
return v
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,8 @@ type pathWalk struct {
|
|||||||
|
|
||||||
// walkPath descends tail from n: a group or template by its next segment, a
|
// walkPath descends tail from n: a group or template by its next segment, a
|
||||||
// choice by w.choice, which consumes no segment. A missing segment is an error,
|
// choice by w.choice, which consumes no segment. A missing segment is an error,
|
||||||
// so no walk reaches past what the data holds.
|
// so no walk reaches past what the data holds. A table-shaped dispatch, one case
|
||||||
|
// per node kind, kept whole on purpose.
|
||||||
func walkPath(n node, tail []string, w pathWalk) error {
|
func walkPath(n node, tail []string, w pathWalk) error {
|
||||||
if len(tail) == 0 {
|
if len(tail) == 0 {
|
||||||
if w.leaf != nil {
|
if w.leaf != nil {
|
||||||
|
|||||||
+6
-1
@@ -29,6 +29,9 @@ func refShape(name string) (sigil, rest string, err error) {
|
|||||||
if rest == "" {
|
if rest == "" {
|
||||||
return "", "", fmt.Errorf("reference has no path")
|
return "", "", fmt.Errorf("reference has no path")
|
||||||
}
|
}
|
||||||
|
if strings.HasPrefix(rest, "/") {
|
||||||
|
return "", "", fmt.Errorf("the path after %s starts at a name, not a /; write {%s%s}", sigil, sigil, rest[1:])
|
||||||
|
}
|
||||||
if strings.HasPrefix(rest, ".") {
|
if strings.HasPrefix(rest, ".") {
|
||||||
return "", "", fmt.Errorf("a reference starts with / (the root), . (this folder) or .. (the folder above)")
|
return "", "", fmt.Errorf("a reference starts with / (the root), . (this folder) or .. (the folder above)")
|
||||||
}
|
}
|
||||||
@@ -137,7 +140,9 @@ func eachTemplate(root map[string]node, fn func(folder []string, path string, t
|
|||||||
}
|
}
|
||||||
|
|
||||||
// resolveRef walks a reference path through the folders to the category it names,
|
// resolveRef walks a reference path through the folders to the category it names,
|
||||||
// returning that head, the node, and the tail left to read into it.
|
// returning that head, the node, and the tail left to read into it. A descent of
|
||||||
|
// its own rather than a walkPath: it walks groups only and returns where they end,
|
||||||
|
// not a leaf.
|
||||||
func resolveRef(root map[string]node, segments []string) (head []string, target node, tail []string, err error) {
|
func resolveRef(root map[string]node, segments []string) (head []string, target node, tail []string, err error) {
|
||||||
var n node = &group{children: root}
|
var n node = &group{children: root}
|
||||||
i := 0
|
i := 0
|
||||||
|
|||||||
+54
-2
@@ -229,8 +229,60 @@ func fieldTokens(format string) []string {
|
|||||||
return names
|
return names
|
||||||
}
|
}
|
||||||
|
|
||||||
// checkSegments rejects an unfinished path: "{a.}", "{.b}" and "{a..b}" each have
|
// arm is one alternative of a {a|b} token or one operand, split into the key
|
||||||
// a segment naming nothing. A field really named "" would otherwise make them
|
// naming the node in a template's fields (a sibling field, or the head a
|
||||||
|
// reference is bound under) and the tail of a dotted path into it. A non-empty
|
||||||
|
// tail is what makes the arm a bound draw: its head is drawn once per expansion
|
||||||
|
// (see compileOps).
|
||||||
|
type arm struct {
|
||||||
|
name string // as written, for messages
|
||||||
|
key string
|
||||||
|
tail []string
|
||||||
|
steps []string // key per level passed through; the head and leaf hold their own
|
||||||
|
path string // key and tail, the one spelling every way of writing this read shares
|
||||||
|
}
|
||||||
|
|
||||||
|
// splitArm splits one name into key and tail. refs maps a reference to what
|
||||||
|
// linkRefs bound it to; before linking, a reference is whole.
|
||||||
|
func splitArm(name string, refs map[string]refBinding) arm {
|
||||||
|
if isRef(name) {
|
||||||
|
b, bound := refs[name]
|
||||||
|
if !bound || len(b.tail) == 0 {
|
||||||
|
key := name
|
||||||
|
if bound {
|
||||||
|
key = b.key
|
||||||
|
}
|
||||||
|
return arm{name: name, key: key, path: key}
|
||||||
|
}
|
||||||
|
return pathArm(name, b.key, b.tail)
|
||||||
|
}
|
||||||
|
head, tail, dotted := strings.Cut(name, ".")
|
||||||
|
if !dotted {
|
||||||
|
return arm{name: name, key: name, path: name}
|
||||||
|
}
|
||||||
|
return pathArm(name, head, strings.Split(tail, "."))
|
||||||
|
}
|
||||||
|
|
||||||
|
func pathArm(name, key string, segs []string) arm {
|
||||||
|
var steps []string
|
||||||
|
for i := 0; i < len(segs)-1; i++ { // every level except the leaf's own
|
||||||
|
steps = append(steps, key+"."+strings.Join(segs[:i+1], "."))
|
||||||
|
}
|
||||||
|
return arm{name: name, key: key, tail: segs, steps: steps, path: key + "." + strings.Join(segs, ".")}
|
||||||
|
}
|
||||||
|
|
||||||
|
// splitArms splits a token body's '|' alternatives.
|
||||||
|
func splitArms(body string, refs map[string]refBinding) []arm {
|
||||||
|
parts := strings.Split(body, "|")
|
||||||
|
arms := make([]arm, len(parts))
|
||||||
|
for i, p := range parts {
|
||||||
|
arms[i] = splitArm(p, refs)
|
||||||
|
}
|
||||||
|
return arms
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkSegments rejects an unfinished path: "{a.}" and "{a..b}" each have a
|
||||||
|
// segment naming nothing. A field really named "" would otherwise make them
|
||||||
// resolve, so a typo would read as a path that worked.
|
// resolve, so a typo would read as a path that worked.
|
||||||
func checkSegments(a arm) error {
|
func checkSegments(a arm) error {
|
||||||
if len(a.tail) == 0 {
|
if len(a.tail) == 0 {
|
||||||
|
|||||||
Reference in New Issue
Block a user