Reject weight 0, a never-numeric calc operand and a repeat product past the cap; the CLI classifies ErrNoData
Tests / vet + fmt + tests (pull_request) Successful in 52s

This commit is contained in:
2026-09-02 18:23:03 +02:00
parent 34c933b2b3
commit 1b1b93b35f
8 changed files with 89 additions and 17 deletions
+29 -1
View File
@@ -70,9 +70,13 @@ func checkCalc(fields map[string]node, args []string) error {
return fmt.Errorf("calc(%q): %w", args[0], err)
}
for _, name := range calcVars(expr) {
if _, ok := fields[name]; !ok {
operand, ok := fields[name]
if !ok {
return fmt.Errorf("calc(%q): no field %q", args[0], name)
}
if text, never := neverNumeric(operand); never {
return fmt.Errorf("calc(%q): operand %q is never a number: it renders %q", args[0], name, text)
}
}
if len(args) == 2 {
if dp, err := strconv.Atoi(args[1]); err != nil || dp < 0 || dp > maxDecimals {
@@ -82,6 +86,30 @@ func checkCalc(fields map[string]node, args []string) error {
return nil
}
// neverNumeric reports a node no render of which is a number: fixed text that does
// not parse, or a choice of only such items. text is one such render.
func neverNumeric(n node) (text string, never bool) {
switch n := n.(type) {
case *template:
if !n.fixed || n.repeat > 1 {
return "", false
}
if _, err := strconv.ParseFloat(strings.TrimSpace(n.lit), 64); err != nil {
return n.lit, true
}
case *choice:
for _, it := range n.items {
t, itemNever := neverNumeric(it)
if !itemNever {
return "", false
}
text = t
}
return text, true
}
return "", false
}
// calcPrep parses the expression and decimals once, at compile time, and places each
// operand name at the position expand will read it into. checkCalc proved both args
// valid, so no step here can fail; dp -1 prints the minimal form.