Files

78 lines
2.2 KiB
Go
Raw Permalink Normal View History

// SPDX-FileCopyrightText: (C) Copyright 2026 Pim van Pelt <[email protected]>
// SPDX-License-Identifier: Apache-2.0
package cli
import (
"errors"
"fmt"
"strings"
)
// Validate reports common authoring faults in a command tree. Walk tolerates
// all of them, so Validate is optional — but calling it once at startup, or
// from a unit test, catches mistakes that otherwise cause silent misdispatch.
// It returns all problems found, joined, or nil if the tree is clean.
//
// It reports:
// - a node with more than one slot child (Dynamic != nil): only the first is
// ever reachable (FR-1.7);
// - a non-root node with an empty Word: it can never be matched or displayed;
// - duplicate fixed (non-slot) words among a node's children: the second is
// shadowed by the first;
// - a node that neither runs nor has children: a dead end that can only print
// "<no completions>".
//
// Circular slots (a slot that is its own descendant, e.g. a watch-options node)
// are traversed once, not looped.
func Validate[C any](root *Node[C]) error {
var errs []error
validateNode(root, "", true, make(map[*Node[C]]bool), &errs)
return errors.Join(errs...)
}
func validateNode[C any](n *Node[C], path string, isRoot bool, seen map[*Node[C]]bool, errs *[]error) {
if seen[n] {
return
}
seen[n] = true
where := path
if where == "" {
where = "<root>"
}
if !isRoot && n.Word == "" {
*errs = append(*errs, fmt.Errorf("%s: node has an empty Word", where))
}
if !isRoot && n.Run == nil && len(n.Children) == 0 {
*errs = append(*errs, fmt.Errorf("%s: node neither runs nor has children (dead end)", where))
}
slots := 0
fixedWords := make(map[string]bool)
for _, c := range n.Children {
if c.Dynamic != nil {
slots++
continue
}
if c.Word != "" {
if fixedWords[c.Word] {
*errs = append(*errs, fmt.Errorf("%s: duplicate child word %q", where, c.Word))
}
fixedWords[c.Word] = true
}
}
if slots > 1 {
*errs = append(*errs, fmt.Errorf("%s: %d slot children, but only the first is reachable", where, slots))
}
for _, c := range n.Children {
cp := c.Word
if path != "" {
cp = path + " " + c.Word
}
validateNode(c, strings.TrimSpace(cp), false, seen, errs)
}
}