Find a format's bound readers in one ordered scan
This commit is contained in:
@@ -214,6 +214,18 @@ func TestASharedNodeIsWalkedOnce(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestTheEarlierReaderIsNamed(t *testing.T) {
|
||||||
|
// A token and a calc operand can name one level. The one the format writes
|
||||||
|
// first is the one reported, so the error points at the same place a reader
|
||||||
|
// looking at the format would start.
|
||||||
|
_, err := New([]string{writeData(t, map[string]string{
|
||||||
|
"cat": `{"format":"{calc(p * 1)} {p} {p.first}","p":[{"format":"{first}","first":["1","2"]}]}`,
|
||||||
|
})})
|
||||||
|
if err == nil || !strings.Contains(err.Error(), `calc operand "p"`) {
|
||||||
|
t.Fatalf("New = %v, want the calc operand named, being written first", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestReferenceToAMatchingStringIsAccepted(t *testing.T) {
|
func TestReferenceToAMatchingStringIsAccepted(t *testing.T) {
|
||||||
// A literal renders one fixed string, so no draw of it can disagree with a
|
// A literal renders one fixed string, so no draw of it can disagree with a
|
||||||
// held one. Two unrelated literals that merely spell the same text must not
|
// held one. Two unrelated literals that merely spell the same text must not
|
||||||
|
|||||||
@@ -101,19 +101,29 @@ func calcPrep(args []string) callFn {
|
|||||||
func calcOperands(format string) []string {
|
func calcOperands(format string) []string {
|
||||||
var names []string
|
var names []string
|
||||||
_ = eachToken(format, func(t ftoken) error {
|
_ = eachToken(format, func(t ftoken) error {
|
||||||
if t.kind != 'b' {
|
if t.kind == 'b' {
|
||||||
return nil
|
names = append(names, calcTokenOperands(t.body)...)
|
||||||
}
|
|
||||||
if name, args, ok := funcCall(t.body); ok && name == "calc" && len(args) >= 1 {
|
|
||||||
if expr, err := parseCalc(args[0]); err == nil {
|
|
||||||
names = append(names, calcVars(expr)...)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
return names
|
return names
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// calcTokenOperands lists the sibling-field names one {token} body reads, empty
|
||||||
|
// for anything that is not a {calc(...)}. checkCalc reports an expression that does
|
||||||
|
// not parse, so one that does not simply names nothing here.
|
||||||
|
func calcTokenOperands(body string) []string {
|
||||||
|
name, args, ok := funcCall(body)
|
||||||
|
if !ok || name != "calc" || len(args) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
expr, err := parseCalc(args[0])
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return calcVars(expr)
|
||||||
|
}
|
||||||
|
|
||||||
// calcVars lists the field names an expression references, for the existence
|
// calcVars lists the field names an expression references, for the existence
|
||||||
// check in checkCalc.
|
// check in checkCalc.
|
||||||
func calcVars(n calcNode) []string {
|
func calcVars(n calcNode) []string {
|
||||||
|
|||||||
@@ -82,3 +82,18 @@ func TestCalcReproducible(t *testing.T) {
|
|||||||
t.Fatalf("calc not reproducible: %q != %q", a, b)
|
t.Fatalf("calc not reproducible: %q != %q", a, b)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestCalcTokenOperandsReadsOnlyACalc pins what the helper answers for a body that
|
||||||
|
// is not a calc, and for one whose expression does not parse: nothing, either way.
|
||||||
|
// checkCalc is what reports a bad expression, so the callers that run after it
|
||||||
|
// never meet one — but they must not have to depend on that order to be safe.
|
||||||
|
func TestCalcTokenOperandsReadsOnlyACalc(t *testing.T) {
|
||||||
|
for _, body := range []string{"plain", "luhn()", "calc()", "calc(1 +)", "calc(()"} {
|
||||||
|
if got := calcTokenOperands(body); got != nil {
|
||||||
|
t.Errorf("calcTokenOperands(%q) = %v, want none", body, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if got := calcTokenOperands("calc(net * qty)"); len(got) != 2 {
|
||||||
|
t.Errorf("calcTokenOperands(calc(net * qty)) = %v, want both operands", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -161,7 +161,7 @@ func compileTemplate(m map[string]any) (node, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
t.ops, t.grow, t.bound = compileOps(format)
|
t.ops, t.grow, t.bound = compileOps(format)
|
||||||
if err := checkNoOverlap(t.ops, t.bound, format); err != nil {
|
if err := checkNoOverlap(format, t.bound); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return t, nil
|
return t, nil
|
||||||
|
|||||||
+31
-20
@@ -211,26 +211,10 @@ func splitArm(name string) arm {
|
|||||||
// expands it afresh, so their values disagree. One spelling, so there is nothing
|
// expands it afresh, so their values disagree. One spelling, so there is nothing
|
||||||
// to get wrong. Names are compared in sorted order, so which pair is reported
|
// to get wrong. Names are compared in sorted order, so which pair is reported
|
||||||
// does not depend on where the tokens sit.
|
// does not depend on where the tokens sit.
|
||||||
func checkNoOverlap(ops []op, bound map[string]string, format string) error {
|
func checkNoOverlap(format string, bound map[string]string) error {
|
||||||
var names []reader
|
names := boundReaders(format, bound)
|
||||||
for i := range ops {
|
// Stable over one format-order scan, so two readers of one name (a token and a
|
||||||
if ops[i].kind != 'f' {
|
// calc operand both naming "p") are reported as the format writes them.
|
||||||
continue
|
|
||||||
}
|
|
||||||
for _, a := range ops[i].arms {
|
|
||||||
if _, isBound := bound[a.key]; isBound {
|
|
||||||
names = append(names, reader{a.name, "token {" + a.name + "}"})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// A calc operand renders its field, so it names a level exactly as a token does.
|
|
||||||
for _, name := range calcOperands(format) {
|
|
||||||
if _, isBound := bound[name]; isBound {
|
|
||||||
names = append(names, reader{name, fmt.Sprintf("calc operand %q", name)})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Stable, so two readers of one name (a token and a calc operand both naming
|
|
||||||
// "p") are reported in the order 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].name < names[j].name })
|
||||||
for i, level := range names {
|
for i, level := range names {
|
||||||
for _, path := range names[i+1:] {
|
for _, path := range names[i+1:] {
|
||||||
@@ -245,6 +229,33 @@ func checkNoOverlap(ops []op, bound map[string]string, format string) error {
|
|||||||
// 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, and how to name that spelling.
|
||||||
type reader struct{ name, label string }
|
type reader struct{ name, label string }
|
||||||
|
|
||||||
|
// boundReaders lists every way a format reaches a bound field, in the order the
|
||||||
|
// format writes them. A calc operand renders its field, so it names a level exactly
|
||||||
|
// as a token does; one scan finds both, which is what puts them in one order.
|
||||||
|
func boundReaders(format string, bound map[string]string) []reader {
|
||||||
|
var names []reader
|
||||||
|
_ = eachToken(format, func(t ftoken) error {
|
||||||
|
if t.kind != 'b' {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if _, _, isFunc := funcCall(t.body); isFunc {
|
||||||
|
for _, operand := range calcTokenOperands(t.body) {
|
||||||
|
if _, isBound := bound[operand]; isBound {
|
||||||
|
names = append(names, reader{operand, fmt.Sprintf("calc operand %q", operand)})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
for _, a := range splitArms(t.body) {
|
||||||
|
if _, isBound := bound[a.key]; isBound {
|
||||||
|
names = append(names, reader{a.name, "token {" + a.name + "}"})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
return names
|
||||||
|
}
|
||||||
|
|
||||||
// checkSegments rejects an unfinished path: "{a.}", "{.b}" and "{a..b}" each have
|
// checkSegments rejects an unfinished path: "{a.}", "{.b}" and "{a..b}" each have
|
||||||
// a segment naming nothing. A field really named "" would otherwise make them
|
// 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.
|
||||||
|
|||||||
Reference in New Issue
Block a user