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

This commit is contained in:
2026-09-02 18:45:31 +02:00
parent 512696722d
commit fdbbf94d2c
5 changed files with 79 additions and 69 deletions
+54 -2
View File
@@ -229,8 +229,60 @@ func fieldTokens(format string) []string {
return names
}
// checkSegments rejects an unfinished path: "{a.}", "{.b}" and "{a..b}" each have
// a segment naming nothing. A field really named "" would otherwise make them
// 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, 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.
func checkSegments(a arm) error {
if len(a.tail) == 0 {