Reject a repeated alternation arm

An alternation picks its arms evenly, so {a|a|b} was a covert 3:1 skew — a
third spelling of what a choice's weight is for, reachable only by
experiment. The check runs over the arms as written, before a reference arm
is let through, so {..a|..a} is caught too.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
M
2026-08-30 23:05:00 +02:00
committed by lilleman-tw
parent cc76d300e7
commit b09bc56e87
+23 -1
View File
@@ -133,7 +133,11 @@ func checkTokens(format string, fields map[string]node) error {
if strings.IndexByte(t.body, '(') >= 0 { // a function token, not a field
return checkFunc(t.body, fields)
}
for _, name := range strings.Split(t.body, "|") {
names := strings.Split(t.body, "|")
if err := checkNoRepeatedArm(t.body, names); err != nil {
return err
}
for _, name := range names {
if isRef(name) {
if name == refPrefix {
return fmt.Errorf("token {%s}: reference has no path", t.body)
@@ -159,6 +163,24 @@ func checkTokens(format string, fields map[string]node) error {
})
}
// checkNoRepeatedArm rejects {a|a|b}. An alternation picks its arms evenly, so a
// repeated one skews the odds — a third spelling of what weight is for, and one an
// author is far likelier to have typed by accident than meant. The error names the
// spelling that does skew a pick.
func checkNoRepeatedArm(body string, names []string) error {
if len(names) < 2 {
return nil
}
seen := make(map[string]bool, len(names))
for _, name := range names {
if seen[name] {
return fmt.Errorf("token {%s}: arm %q is repeated; an alternation picks its arms evenly, so skew the odds with a choice's weights instead", body, name)
}
seen[name] = true
}
return nil
}
// fieldTokens returns the field and reference names a format renders via {name}
// or {a|..b} tokens (function tokens, which carry no field edges, are excluded).
// These are exactly the child nodes expand's resolve recurses into.