Upgrade to Go 1.24

Fixes #13381
This commit is contained in:
Bjørn Erik Pedersen
2025-02-12 10:35:51 +01:00
parent 9b5f786df8
commit fd8b0fbf8a
37 changed files with 652 additions and 566 deletions

View File

@@ -98,7 +98,8 @@ data, defined in detail in the corresponding sections that follow.
{{if pipeline}} T1 {{else}}{{if pipeline}} T0 {{end}}{{end}}
{{range pipeline}} T1 {{end}}
The value of the pipeline must be an array, slice, map, or channel.
The value of the pipeline must be an array, slice, map, iter.Seq,
iter.Seq2, integer or channel.
If the value of the pipeline has length zero, nothing is output;
otherwise, dot is set to the successive elements of the array,
slice, or map and T1 is executed. If the value is a map and the
@@ -106,7 +107,8 @@ data, defined in detail in the corresponding sections that follow.
visited in sorted key order.
{{range pipeline}} T1 {{else}} T0 {{end}}
The value of the pipeline must be an array, slice, map, or channel.
The value of the pipeline must be an array, slice, map, iter.Seq,
iter.Seq2, integer or channel.
If the value of the pipeline has length zero, dot is unaffected and
T0 is executed; otherwise, dot is set to the successive elements
of the array, slice, or map and T1 is executed.
@@ -162,37 +164,55 @@ An argument is a simple value, denoted by one of the following.
the host machine's ints are 32 or 64 bits.
- The keyword nil, representing an untyped Go nil.
- The character '.' (period):
.
The result is the value of dot.
- A variable name, which is a (possibly empty) alphanumeric string
preceded by a dollar sign, such as
$piOver2
or
$
The result is the value of the variable.
Variables are described below.
- The name of a field of the data, which must be a struct, preceded
by a period, such as
.Field
The result is the value of the field. Field invocations may be
chained:
.Field1.Field2
Fields can also be evaluated on variables, including chaining:
$x.Field1.Field2
- The name of a key of the data, which must be a map, preceded
by a period, such as
.Key
The result is the map element value indexed by the key.
Key invocations may be chained and combined with fields to any
depth:
.Field1.Key1.Field2.Key2
Although the key must be an alphanumeric identifier, unlike with
field names they do not need to start with an upper case letter.
Keys can also be evaluated on variables, including chaining:
$x.key1.key2
- The name of a niladic method of the data, preceded by a period,
such as
.Method
The result is the value of invoking the method with dot as the
receiver, dot.Method(). Such a method must have one return value (of
any type) or two return values, the second of which is an error.
@@ -200,16 +220,22 @@ An argument is a simple value, denoted by one of the following.
and an error is returned to the caller as the value of Execute.
Method invocations may be chained and combined with fields and keys
to any depth:
.Field1.Key1.Method1.Field2.Key2.Method2
Methods can also be evaluated on variables, including chaining:
$x.Method1.Field
- The name of a niladic function, such as
fun
The result is the value of invoking the function, fun(). The return
types and values behave as in methods. Functions and function
names are described below.
- A parenthesized instance of one the above, for grouping. The result
may be accessed by a field or map key invocation.
print (.F1 arg1) (.F2 arg2)
(.StructValuedMethod "arg").Field

View File

@@ -35,7 +35,7 @@ Josie
Name, Gift string
Attended bool
}
recipients := []Recipient{
var recipients = []Recipient{
{"Aunt Mildred", "bone china tea set", true},
{"Uncle John", "moleskin pants", false},
{"Cousin Rodney", "", false},

View File

@@ -2,6 +2,9 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build go1.13
// +build go1.13
package template_test
import (

View File

@@ -395,6 +395,22 @@ func (s *state) walkRange(dot reflect.Value, r *parse.RangeNode) {
s.walk(elem, r.List)
}
switch val.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
if len(r.Pipe.Decl) > 1 {
s.errorf("can't use %v to iterate over more than one variable", val)
break
}
run := false
for v := range val.Seq() {
run = true
// Pass element as second value, as we do for channels.
oneIteration(reflect.Value{}, v)
}
if !run {
break
}
return
case reflect.Array, reflect.Slice:
if val.Len() == 0 {
break
@@ -434,6 +450,43 @@ func (s *state) walkRange(dot reflect.Value, r *parse.RangeNode) {
return
case reflect.Invalid:
break // An invalid value is likely a nil map, etc. and acts like an empty map.
case reflect.Func:
if val.Type().CanSeq() {
if len(r.Pipe.Decl) > 1 {
s.errorf("can't use %v iterate over more than one variable", val)
break
}
run := false
for v := range val.Seq() {
run = true
// Pass element as second value,
// as we do for channels.
oneIteration(reflect.Value{}, v)
}
if !run {
break
}
return
}
if val.Type().CanSeq2() {
run := false
for i, v := range val.Seq2() {
run = true
if len(r.Pipe.Decl) > 1 {
oneIteration(i, v)
} else {
// If there is only one range variable,
// oneIteration will use the
// second value.
oneIteration(reflect.Value{}, i)
}
}
if !run {
break
}
return
}
fallthrough
default:
s.errorf("range can't iterate over %v", val)
}
@@ -757,7 +810,7 @@ func (s *state) evalCallOld(dot, fun reflect.Value, isBuiltin bool, node parse.N
return v
}
}
if final != missingVal {
if !final.Equal(missingVal) {
// The last argument to and/or is coming from
// the pipeline. We didn't short circuit on an earlier
// argument, so we are going to return this one.
@@ -803,7 +856,13 @@ func (s *state) evalCallOld(dot, fun reflect.Value, isBuiltin bool, node parse.N
// Special case for the "call" builtin.
// Insert the name of the callee function as the first argument.
if isBuiltin && name == "call" {
calleeName := args[0].String()
var calleeName string
if len(args) == 0 {
// final must be present or we would have errored out above.
calleeName = final.String()
} else {
calleeName = args[0].String()
}
argv = append([]reflect.Value{reflect.ValueOf(calleeName)}, argv...)
fun = reflect.ValueOf(call)
}

View File

@@ -13,6 +13,7 @@ import (
"flag"
"fmt"
"io"
"iter"
"reflect"
"strings"
"sync"
@@ -412,6 +413,9 @@ var execTests = []execTest{
{"Interface Call", `{{stringer .S}}`, "foozle", map[string]any{"S": bytes.NewBufferString("foozle")}, true},
{".ErrFunc", "{{call .ErrFunc}}", "bla", tVal, true},
{"call nil", "{{call nil}}", "", tVal, false},
{"empty call", "{{call}}", "", tVal, false},
{"empty call after pipe valid", "{{.ErrFunc | call}}", "bla", tVal, true},
{"empty call after pipe invalid", "{{1 | call}}", "", tVal, false},
// Erroneous function calls (check args).
{".BinaryFuncTooFew", "{{call .BinaryFunc `1`}}", "", tVal, false},
@@ -618,6 +622,30 @@ var execTests = []execTest{
{"declare in range", "{{range $x := .PSI}}<{{$foo:=$x}}{{$x}}>{{end}}", "<21><22><23>", tVal, true},
{"range count", `{{range $i, $x := count 5}}[{{$i}}]{{$x}}{{end}}`, "[0]a[1]b[2]c[3]d[4]e", tVal, true},
{"range nil count", `{{range $i, $x := count 0}}{{else}}empty{{end}}`, "empty", tVal, true},
{"range iter.Seq[int]", `{{range $i := .}}{{$i}}{{end}}`, "01", fVal1(2), true},
{"i = range iter.Seq[int]", `{{$i := 0}}{{range $i = .}}{{$i}}{{end}}`, "01", fVal1(2), true},
{"range iter.Seq[int] over two var", `{{range $i, $c := .}}{{$c}}{{end}}`, "", fVal1(2), false},
{"i, c := range iter.Seq2[int,int]", `{{range $i, $c := .}}{{$i}}{{$c}}{{end}}`, "0112", fVal2(2), true},
{"i, c = range iter.Seq2[int,int]", `{{$i := 0}}{{$c := 0}}{{range $i, $c = .}}{{$i}}{{$c}}{{end}}`, "0112", fVal2(2), true},
{"i = range iter.Seq2[int,int]", `{{$i := 0}}{{range $i = .}}{{$i}}{{end}}`, "01", fVal2(2), true},
{"i := range iter.Seq2[int,int]", `{{range $i := .}}{{$i}}{{end}}`, "01", fVal2(2), true},
{"i,c,x range iter.Seq2[int,int]", `{{$i := 0}}{{$c := 0}}{{$x := 0}}{{range $i, $c = .}}{{$i}}{{$c}}{{end}}`, "0112", fVal2(2), true},
{"i,x range iter.Seq[int]", `{{$i := 0}}{{$x := 0}}{{range $i = .}}{{$i}}{{end}}`, "01", fVal1(2), true},
{"range iter.Seq[int] else", `{{range $i := .}}{{$i}}{{else}}empty{{end}}`, "empty", fVal1(0), true},
{"range iter.Seq2[int,int] else", `{{range $i := .}}{{$i}}{{else}}empty{{end}}`, "empty", fVal2(0), true},
{"range int8", rangeTestInt, rangeTestData[int8](), int8(5), true},
{"range int16", rangeTestInt, rangeTestData[int16](), int16(5), true},
{"range int32", rangeTestInt, rangeTestData[int32](), int32(5), true},
{"range int64", rangeTestInt, rangeTestData[int64](), int64(5), true},
{"range int", rangeTestInt, rangeTestData[int](), int(5), true},
{"range uint8", rangeTestInt, rangeTestData[uint8](), uint8(5), true},
{"range uint16", rangeTestInt, rangeTestData[uint16](), uint16(5), true},
{"range uint32", rangeTestInt, rangeTestData[uint32](), uint32(5), true},
{"range uint64", rangeTestInt, rangeTestData[uint64](), uint64(5), true},
{"range uint", rangeTestInt, rangeTestData[uint](), uint(5), true},
{"range uintptr", rangeTestInt, rangeTestData[uintptr](), uintptr(5), true},
{"range uintptr(0)", `{{range $v := .}}{{print $v}}{{else}}empty{{end}}`, "empty", uintptr(0), true},
{"range 5", `{{range $v := 5}}{{printf "%T%d" $v $v}}{{end}}`, rangeTestData[int](), nil, true},
// Cute examples.
{"or as if true", `{{or .SI "slice is empty"}}`, "[3 4 5]", tVal, true},
@@ -722,6 +750,37 @@ var execTests = []execTest{
{"issue60801", "{{$k := 0}}{{$v := 0}}{{range $k, $v = .AI}}{{$k}}={{$v}} {{end}}", "0=3 1=4 2=5 ", tVal, true},
}
func fVal1(i int) iter.Seq[int] {
return func(yield func(int) bool) {
for v := range i {
if !yield(v) {
break
}
}
}
}
func fVal2(i int) iter.Seq2[int, int] {
return func(yield func(int, int) bool) {
for v := range i {
if !yield(v, v+1) {
break
}
}
}
}
const rangeTestInt = `{{range $v := .}}{{printf "%T%d" $v $v}}{{end}}`
func rangeTestData[T int | int8 | int16 | int32 | int64 | uint | uint8 | uint16 | uint32 | uint64 | uintptr]() string {
I := T(5)
var buf strings.Builder
for i := T(0); i < I; i++ {
fmt.Fprintf(&buf, "%T%d", i, i)
}
return buf.String()
}
func zeroArgs() string {
return "zeroArgs"
}

View File

@@ -304,14 +304,14 @@ func (s *state) evalCall(dot, fun reflect.Value, isBuiltin bool, node parse.Node
}
}()
}
if args != nil {
args = args[1:] // Zeroth arg is function name/node; not passed to function.
}
typ := fun.Type()
numFirst := len(first)
numFirst := len(first) // Added for Hugo
numIn := len(args) + numFirst // Added for Hugo
if final != missingVal {
if !isMissing(final) {
numIn++
}
numFixed := len(args) + len(first) // Adjusted for Hugo
@@ -346,7 +346,7 @@ func (s *state) evalCall(dot, fun reflect.Value, isBuiltin bool, node parse.Node
return v
}
}
if final != missingVal {
if !final.Equal(missingVal) {
// The last argument to and/or is coming from
// the pipeline. We didn't short circuit on an earlier
// argument, so we are going to return this one.
@@ -373,7 +373,7 @@ func (s *state) evalCall(dot, fun reflect.Value, isBuiltin bool, node parse.Node
}
}
// Add final value if necessary.
if final != missingVal {
if !isMissing(final) {
t := typ.In(typ.NumIn() - 1)
if typ.IsVariadic() {
if numIn-1 < numFixed {
@@ -392,7 +392,13 @@ func (s *state) evalCall(dot, fun reflect.Value, isBuiltin bool, node parse.Node
// Special case for the "call" builtin.
// Insert the name of the callee function as the first argument.
if isBuiltin && name == "call" {
calleeName := args[0].String()
var calleeName string
if len(args) == 0 {
// final must be present or we would have errored out above.
calleeName = final.String()
} else {
calleeName = args[0].String()
}
argv = append([]reflect.Value{reflect.ValueOf(calleeName)}, argv...)
fun = reflect.ValueOf(call)
}

View File

@@ -2,16 +2,18 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build go1.13
// +build go1.13
package template_test
import (
"bytes"
"github.com/gohugoio/hugo/tpl/internal/go_templates/testenv"
"os"
"os/exec"
"path/filepath"
"testing"
"github.com/gohugoio/hugo/tpl/internal/go_templates/testenv"
)
// Issue 36021: verify that text/template doesn't prevent the linker from removing
@@ -42,7 +44,7 @@ func main() {
`
td := t.TempDir()
if err := os.WriteFile(filepath.Join(td, "x.go"), []byte(prog), 0o644); err != nil {
if err := os.WriteFile(filepath.Join(td, "x.go"), []byte(prog), 0644); err != nil {
t.Fatal(err)
}
cmd := exec.Command(testenv.GoToolPath(t), "build", "-o", "x.exe", "x.go")

View File

@@ -2,8 +2,8 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build !windows
// +build !windows
//go:build go1.13 && !windows
// +build go1.13,!windows
package template
@@ -11,11 +11,10 @@ package template
import (
"fmt"
"github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate/parse"
"os"
"strings"
"testing"
"github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate/parse"
)
const (
@@ -32,32 +31,22 @@ type multiParseTest struct {
}
var multiParseTests = []multiParseTest{
{
"empty", "", noError,
{"empty", "", noError,
nil,
nil,
},
{
"one", `{{define "foo"}} FOO {{end}}`, noError,
nil},
{"one", `{{define "foo"}} FOO {{end}}`, noError,
[]string{"foo"},
[]string{" FOO "},
},
{
"two", `{{define "foo"}} FOO {{end}}{{define "bar"}} BAR {{end}}`, noError,
[]string{" FOO "}},
{"two", `{{define "foo"}} FOO {{end}}{{define "bar"}} BAR {{end}}`, noError,
[]string{"foo", "bar"},
[]string{" FOO ", " BAR "},
},
[]string{" FOO ", " BAR "}},
// errors
{
"missing end", `{{define "foo"}} FOO `, hasError,
{"missing end", `{{define "foo"}} FOO `, hasError,
nil,
nil},
{"malformed name", `{{define "foo}} FOO `, hasError,
nil,
},
{
"malformed name", `{{define "foo}} FOO `, hasError,
nil,
nil,
},
nil},
}
func TestMultiParse(t *testing.T) {
@@ -443,7 +432,7 @@ func TestIssue19294(t *testing.T) {
// by the contents of "stylesheet", but if the internal map associating
// names with templates is built in the wrong order, the empty block
// looks non-empty and this doesn't happen.
inlined := map[string]string{
var inlined = map[string]string{
"stylesheet": `{{define "stylesheet"}}stylesheet{{end}}`,
"xhtml": `{{block "stylesheet" .}}{{end}}`,
}

View File

@@ -352,6 +352,7 @@ func lexComment(l *lexer) stateFn {
if !delim {
return l.errorf("comment ends before closing delimiter")
}
l.line += strings.Count(l.input[l.start:l.pos], "\n")
i := l.thisItem(itemComment)
if trimSpace {
l.pos += trimMarkerLen

View File

@@ -548,6 +548,16 @@ var lexPosTests = []lexTest{
{itemRightDelim, 11, "}}", 2},
{itemEOF, 13, "", 2},
}},
{"longcomment", "{{/*\n*/}}\n{{undefinedFunction \"test\"}}", []item{
{itemComment, 2, "/*\n*/", 1},
{itemText, 9, "\n", 2},
{itemLeftDelim, 10, "{{", 3},
{itemIdentifier, 12, "undefinedFunction", 3},
{itemSpace, 29, " ", 3},
{itemString, 30, "\"test\"", 3},
{itemRightDelim, 36, "}}", 3},
{itemEOF, 38, "", 3},
}},
}
// The other tests don't check position, to make the test cases easier to construct.

View File

@@ -2,6 +2,9 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build go1.13
// +build go1.13
package parse
import (
@@ -33,9 +36,9 @@ var numberTests = []numberTest{
{"7_3", true, true, true, false, 73, 73, 73, 0},
{"0b10_010_01", true, true, true, false, 73, 73, 73, 0},
{"0B10_010_01", true, true, true, false, 73, 73, 73, 0},
{"073", true, true, true, false, 0o73, 0o73, 0o73, 0},
{"0o73", true, true, true, false, 0o73, 0o73, 0o73, 0},
{"0O73", true, true, true, false, 0o73, 0o73, 0o73, 0},
{"073", true, true, true, false, 073, 073, 073, 0},
{"0o73", true, true, true, false, 073, 073, 073, 0},
{"0O73", true, true, true, false, 073, 073, 073, 0},
{"0x73", true, true, true, false, 0x73, 0x73, 0x73, 0},
{"0X73", true, true, true, false, 0x73, 0x73, 0x73, 0},
{"0x7_3", true, true, true, false, 0x73, 0x73, 0x73, 0},
@@ -61,7 +64,7 @@ var numberTests = []numberTest{
{"-12+0i", true, false, true, true, -12, 0, -12, -12},
{"13+0i", true, true, true, true, 13, 13, 13, 13},
// funny bases
{"0123", true, true, true, false, 0o123, 0o123, 0o123, 0},
{"0123", true, true, true, false, 0123, 0123, 0123, 0},
{"-0x0", true, true, true, false, 0, 0, 0, 0},
{"0xdeadbeef", true, true, true, false, 0xdeadbeef, 0xdeadbeef, 0xdeadbeef, 0},
// character constants
@@ -176,150 +179,78 @@ const (
)
var parseTests = []parseTest{
{
"empty", "", noError,
``,
},
{
"comment", "{{/*\n\n\n*/}}", noError,
``,
},
{
"spaces", " \t\n", noError,
`" \t\n"`,
},
{
"text", "some text", noError,
`"some text"`,
},
{
"emptyAction", "{{}}", hasError,
`{{}}`,
},
{
"field", "{{.X}}", noError,
`{{.X}}`,
},
{
"simple command", "{{printf}}", noError,
`{{printf}}`,
},
{
"$ invocation", "{{$}}", noError,
"{{$}}",
},
{
"variable invocation", "{{with $x := 3}}{{$x 23}}{{end}}", noError,
"{{with $x := 3}}{{$x 23}}{{end}}",
},
{
"variable with fields", "{{$.I}}", noError,
"{{$.I}}",
},
{
"multi-word command", "{{printf `%d` 23}}", noError,
"{{printf `%d` 23}}",
},
{
"pipeline", "{{.X|.Y}}", noError,
`{{.X | .Y}}`,
},
{
"pipeline with decl", "{{$x := .X|.Y}}", noError,
`{{$x := .X | .Y}}`,
},
{
"nested pipeline", "{{.X (.Y .Z) (.A | .B .C) (.E)}}", noError,
`{{.X (.Y .Z) (.A | .B .C) (.E)}}`,
},
{
"field applied to parentheses", "{{(.Y .Z).Field}}", noError,
`{{(.Y .Z).Field}}`,
},
{
"simple if", "{{if .X}}hello{{end}}", noError,
`{{if .X}}"hello"{{end}}`,
},
{
"if with else", "{{if .X}}true{{else}}false{{end}}", noError,
`{{if .X}}"true"{{else}}"false"{{end}}`,
},
{
"if with else if", "{{if .X}}true{{else if .Y}}false{{end}}", noError,
`{{if .X}}"true"{{else}}{{if .Y}}"false"{{end}}{{end}}`,
},
{
"if else chain", "+{{if .X}}X{{else if .Y}}Y{{else if .Z}}Z{{end}}+", noError,
`"+"{{if .X}}"X"{{else}}{{if .Y}}"Y"{{else}}{{if .Z}}"Z"{{end}}{{end}}{{end}}"+"`,
},
{
"simple range", "{{range .X}}hello{{end}}", noError,
`{{range .X}}"hello"{{end}}`,
},
{
"chained field range", "{{range .X.Y.Z}}hello{{end}}", noError,
`{{range .X.Y.Z}}"hello"{{end}}`,
},
{
"nested range", "{{range .X}}hello{{range .Y}}goodbye{{end}}{{end}}", noError,
`{{range .X}}"hello"{{range .Y}}"goodbye"{{end}}{{end}}`,
},
{
"range with else", "{{range .X}}true{{else}}false{{end}}", noError,
`{{range .X}}"true"{{else}}"false"{{end}}`,
},
{
"range over pipeline", "{{range .X|.M}}true{{else}}false{{end}}", noError,
`{{range .X | .M}}"true"{{else}}"false"{{end}}`,
},
{
"range []int", "{{range .SI}}{{.}}{{end}}", noError,
`{{range .SI}}{{.}}{{end}}`,
},
{
"range 1 var", "{{range $x := .SI}}{{.}}{{end}}", noError,
`{{range $x := .SI}}{{.}}{{end}}`,
},
{
"range 2 vars", "{{range $x, $y := .SI}}{{.}}{{end}}", noError,
`{{range $x, $y := .SI}}{{.}}{{end}}`,
},
{
"range with break", "{{range .SI}}{{.}}{{break}}{{end}}", noError,
`{{range .SI}}{{.}}{{break}}{{end}}`,
},
{
"range with continue", "{{range .SI}}{{.}}{{continue}}{{end}}", noError,
`{{range .SI}}{{.}}{{continue}}{{end}}`,
},
{
"constants", "{{range .SI 1 -3.2i true false 'a' nil}}{{end}}", noError,
`{{range .SI 1 -3.2i true false 'a' nil}}{{end}}`,
},
{
"template", "{{template `x`}}", noError,
`{{template "x"}}`,
},
{
"template with arg", "{{template `x` .Y}}", noError,
`{{template "x" .Y}}`,
},
{
"with", "{{with .X}}hello{{end}}", noError,
`{{with .X}}"hello"{{end}}`,
},
{
"with with else", "{{with .X}}hello{{else}}goodbye{{end}}", noError,
`{{with .X}}"hello"{{else}}"goodbye"{{end}}`,
},
{
"with with else with", "{{with .X}}hello{{else with .Y}}goodbye{{end}}", noError,
`{{with .X}}"hello"{{else}}{{with .Y}}"goodbye"{{end}}{{end}}`,
},
{
"with else chain", "{{with .X}}X{{else with .Y}}Y{{else with .Z}}Z{{end}}", noError,
`{{with .X}}"X"{{else}}{{with .Y}}"Y"{{else}}{{with .Z}}"Z"{{end}}{{end}}{{end}}`,
},
{"empty", "", noError,
``},
{"comment", "{{/*\n\n\n*/}}", noError,
``},
{"spaces", " \t\n", noError,
`" \t\n"`},
{"text", "some text", noError,
`"some text"`},
{"emptyAction", "{{}}", hasError,
`{{}}`},
{"field", "{{.X}}", noError,
`{{.X}}`},
{"simple command", "{{printf}}", noError,
`{{printf}}`},
{"$ invocation", "{{$}}", noError,
"{{$}}"},
{"variable invocation", "{{with $x := 3}}{{$x 23}}{{end}}", noError,
"{{with $x := 3}}{{$x 23}}{{end}}"},
{"variable with fields", "{{$.I}}", noError,
"{{$.I}}"},
{"multi-word command", "{{printf `%d` 23}}", noError,
"{{printf `%d` 23}}"},
{"pipeline", "{{.X|.Y}}", noError,
`{{.X | .Y}}`},
{"pipeline with decl", "{{$x := .X|.Y}}", noError,
`{{$x := .X | .Y}}`},
{"nested pipeline", "{{.X (.Y .Z) (.A | .B .C) (.E)}}", noError,
`{{.X (.Y .Z) (.A | .B .C) (.E)}}`},
{"field applied to parentheses", "{{(.Y .Z).Field}}", noError,
`{{(.Y .Z).Field}}`},
{"simple if", "{{if .X}}hello{{end}}", noError,
`{{if .X}}"hello"{{end}}`},
{"if with else", "{{if .X}}true{{else}}false{{end}}", noError,
`{{if .X}}"true"{{else}}"false"{{end}}`},
{"if with else if", "{{if .X}}true{{else if .Y}}false{{end}}", noError,
`{{if .X}}"true"{{else}}{{if .Y}}"false"{{end}}{{end}}`},
{"if else chain", "+{{if .X}}X{{else if .Y}}Y{{else if .Z}}Z{{end}}+", noError,
`"+"{{if .X}}"X"{{else}}{{if .Y}}"Y"{{else}}{{if .Z}}"Z"{{end}}{{end}}{{end}}"+"`},
{"simple range", "{{range .X}}hello{{end}}", noError,
`{{range .X}}"hello"{{end}}`},
{"chained field range", "{{range .X.Y.Z}}hello{{end}}", noError,
`{{range .X.Y.Z}}"hello"{{end}}`},
{"nested range", "{{range .X}}hello{{range .Y}}goodbye{{end}}{{end}}", noError,
`{{range .X}}"hello"{{range .Y}}"goodbye"{{end}}{{end}}`},
{"range with else", "{{range .X}}true{{else}}false{{end}}", noError,
`{{range .X}}"true"{{else}}"false"{{end}}`},
{"range over pipeline", "{{range .X|.M}}true{{else}}false{{end}}", noError,
`{{range .X | .M}}"true"{{else}}"false"{{end}}`},
{"range []int", "{{range .SI}}{{.}}{{end}}", noError,
`{{range .SI}}{{.}}{{end}}`},
{"range 1 var", "{{range $x := .SI}}{{.}}{{end}}", noError,
`{{range $x := .SI}}{{.}}{{end}}`},
{"range 2 vars", "{{range $x, $y := .SI}}{{.}}{{end}}", noError,
`{{range $x, $y := .SI}}{{.}}{{end}}`},
{"range with break", "{{range .SI}}{{.}}{{break}}{{end}}", noError,
`{{range .SI}}{{.}}{{break}}{{end}}`},
{"range with continue", "{{range .SI}}{{.}}{{continue}}{{end}}", noError,
`{{range .SI}}{{.}}{{continue}}{{end}}`},
{"constants", "{{range .SI 1 -3.2i true false 'a' nil}}{{end}}", noError,
`{{range .SI 1 -3.2i true false 'a' nil}}{{end}}`},
{"template", "{{template `x`}}", noError,
`{{template "x"}}`},
{"template with arg", "{{template `x` .Y}}", noError,
`{{template "x" .Y}}`},
{"with", "{{with .X}}hello{{end}}", noError,
`{{with .X}}"hello"{{end}}`},
{"with with else", "{{with .X}}hello{{else}}goodbye{{end}}", noError,
`{{with .X}}"hello"{{else}}"goodbye"{{end}}`},
{"with with else with", "{{with .X}}hello{{else with .Y}}goodbye{{end}}", noError,
`{{with .X}}"hello"{{else}}{{with .Y}}"goodbye"{{end}}{{end}}`},
{"with else chain", "{{with .X}}X{{else with .Y}}Y{{else with .Z}}Z{{end}}", noError,
`{{with .X}}"X"{{else}}{{with .Y}}"Y"{{else}}{{with .Z}}"Z"{{end}}{{end}}{{end}}`},
// Trimming spaces.
{"trim left", "x \r\n\t{{- 3}}", noError, `"x"{{3}}`},
{"trim right", "{{3 -}}\n\n\ty", noError, `{{3}}"y"`},
@@ -328,24 +259,18 @@ var parseTests = []parseTest{
{"comment trim left", "x \r\n\t{{- /* hi */}}", noError, `"x"`},
{"comment trim right", "{{/* hi */ -}}\n\n\ty", noError, `"y"`},
{"comment trim left and right", "x \r\n\t{{- /* */ -}}\n\n\ty", noError, `"x""y"`},
{
"block definition", `{{block "foo" .}}hello{{end}}`, noError,
`{{template "foo" .}}`,
},
{"block definition", `{{block "foo" .}}hello{{end}}`, noError,
`{{template "foo" .}}`},
{"newline in assignment", "{{ $x \n := \n 1 \n }}", noError, "{{$x := 1}}"},
{"newline in empty action", "{{\n}}", hasError, "{{\n}}"},
{"newline in pipeline", "{{\n\"x\"\n|\nprintf\n}}", noError, `{{"x" | printf}}`},
{"newline in comment", "{{/*\nhello\n*/}}", noError, ""},
{"newline in comment", "{{-\n/*\nhello\n*/\n-}}", noError, ""},
{
"spaces around continue", "{{range .SI}}{{.}}{{ continue }}{{end}}", noError,
`{{range .SI}}{{.}}{{continue}}{{end}}`,
},
{
"spaces around break", "{{range .SI}}{{.}}{{ break }}{{end}}", noError,
`{{range .SI}}{{.}}{{break}}{{end}}`,
},
{"spaces around continue", "{{range .SI}}{{.}}{{ continue }}{{end}}", noError,
`{{range .SI}}{{.}}{{continue}}{{end}}`},
{"spaces around break", "{{range .SI}}{{.}}{{ break }}{{end}}", noError,
`{{range .SI}}{{.}}{{break}}{{end}}`},
// Errors.
{"unclosed action", "hello{{range", hasError, ""},
@@ -487,7 +412,7 @@ func TestKeywordsAndFuncs(t *testing.T) {
{
// 'break' is a defined function, don't treat it as a keyword: it should
// accept an argument successfully.
funcsWithKeywordFunc := map[string]any{
var funcsWithKeywordFunc = map[string]any{
"break": func(in any) any { return in },
}
tmpl, err := New("").Parse(inp, "", "", make(map[string]*Tree), funcsWithKeywordFunc)
@@ -574,168 +499,104 @@ func TestErrorContextWithTreeCopy(t *testing.T) {
// All failures, and the result is a string that must appear in the error message.
var errorTests = []parseTest{
// Check line numbers are accurate.
{
"unclosed1",
{"unclosed1",
"line1\n{{",
hasError, `unclosed1:2: unclosed action`,
},
{
"unclosed2",
hasError, `unclosed1:2: unclosed action`},
{"unclosed2",
"line1\n{{define `x`}}line2\n{{",
hasError, `unclosed2:3: unclosed action`,
},
{
"unclosed3",
hasError, `unclosed2:3: unclosed action`},
{"unclosed3",
"line1\n{{\"x\"\n\"y\"\n",
hasError, `unclosed3:4: unclosed action started at unclosed3:2`,
},
{
"unclosed4",
hasError, `unclosed3:4: unclosed action started at unclosed3:2`},
{"unclosed4",
"{{\n\n\n\n\n",
hasError, `unclosed4:6: unclosed action started at unclosed4:1`,
},
{
"var1",
hasError, `unclosed4:6: unclosed action started at unclosed4:1`},
{"var1",
"line1\n{{\nx\n}}",
hasError, `var1:3: function "x" not defined`,
},
hasError, `var1:3: function "x" not defined`},
// Specific errors.
{
"function",
{"function",
"{{foo}}",
hasError, `function "foo" not defined`,
},
{
"comment1",
hasError, `function "foo" not defined`},
{"comment1",
"{{/*}}",
hasError, `comment1:1: unclosed comment`,
},
{
"comment2",
hasError, `comment1:1: unclosed comment`},
{"comment2",
"{{/*\nhello\n}}",
hasError, `comment2:1: unclosed comment`,
},
{
"lparen",
hasError, `comment2:1: unclosed comment`},
{"lparen",
"{{.X (1 2 3}}",
hasError, `unclosed left paren`,
},
{
"rparen",
hasError, `unclosed left paren`},
{"rparen",
"{{.X 1 2 3 ) }}",
hasError, "unexpected right paren",
},
{
"rparen2",
hasError, "unexpected right paren"},
{"rparen2",
"{{(.X 1 2 3",
hasError, `unclosed action`,
},
{
"space",
hasError, `unclosed action`},
{"space",
"{{`x`3}}",
hasError, `in operand`,
},
{
"idchar",
hasError, `in operand`},
{"idchar",
"{{a#}}",
hasError, `'#'`,
},
{
"charconst",
hasError, `'#'`},
{"charconst",
"{{'a}}",
hasError, `unterminated character constant`,
},
{
"stringconst",
hasError, `unterminated character constant`},
{"stringconst",
`{{"a}}`,
hasError, `unterminated quoted string`,
},
{
"rawstringconst",
hasError, `unterminated quoted string`},
{"rawstringconst",
"{{`a}}",
hasError, `unterminated raw quoted string`,
},
{
"number",
hasError, `unterminated raw quoted string`},
{"number",
"{{0xi}}",
hasError, `number syntax`,
},
{
"multidefine",
hasError, `number syntax`},
{"multidefine",
"{{define `a`}}a{{end}}{{define `a`}}b{{end}}",
hasError, `multiple definition of template`,
},
{
"eof",
hasError, `multiple definition of template`},
{"eof",
"{{range .X}}",
hasError, `unexpected EOF`,
},
{
"variable",
hasError, `unexpected EOF`},
{"variable",
// Declare $x so it's defined, to avoid that error, and then check we don't parse a declaration.
"{{$x := 23}}{{with $x.y := 3}}{{$x 23}}{{end}}",
hasError, `unexpected ":="`,
},
{
"multidecl",
hasError, `unexpected ":="`},
{"multidecl",
"{{$a,$b,$c := 23}}",
hasError, `too many declarations`,
},
{
"undefvar",
hasError, `too many declarations`},
{"undefvar",
"{{$a}}",
hasError, `undefined variable`,
},
{
"wrongdot",
hasError, `undefined variable`},
{"wrongdot",
"{{true.any}}",
hasError, `unexpected . after term`,
},
{
"wrongpipeline",
hasError, `unexpected . after term`},
{"wrongpipeline",
"{{12|false}}",
hasError, `non executable command in pipeline`,
},
{
"emptypipeline",
hasError, `non executable command in pipeline`},
{"emptypipeline",
`{{ ( ) }}`,
hasError, `missing value for parenthesized pipeline`,
},
{
"multilinerawstring",
hasError, `missing value for parenthesized pipeline`},
{"multilinerawstring",
"{{ $v := `\n` }} {{",
hasError, `multilinerawstring:2: unclosed action`,
},
{
"rangeundefvar",
hasError, `multilinerawstring:2: unclosed action`},
{"rangeundefvar",
"{{range $k}}{{end}}",
hasError, `undefined variable`,
},
{
"rangeundefvars",
hasError, `undefined variable`},
{"rangeundefvars",
"{{range $k, $v}}{{end}}",
hasError, `undefined variable`,
},
{
"rangemissingvalue1",
hasError, `undefined variable`},
{"rangemissingvalue1",
"{{range $k,}}{{end}}",
hasError, `missing value for range`,
},
{
"rangemissingvalue2",
hasError, `missing value for range`},
{"rangemissingvalue2",
"{{range $k, $v := }}{{end}}",
hasError, `missing value for range`,
},
{
"rangenotvariable1",
hasError, `missing value for range`},
{"rangenotvariable1",
"{{range $k, .}}{{end}}",
hasError, `range can only initialize variables`,
},
{
"rangenotvariable2",
hasError, `range can only initialize variables`},
{"rangenotvariable2",
"{{range $k, 123 := .}}{{end}}",
hasError, `range can only initialize variables`,
},
hasError, `range can only initialize variables`},
}
func TestErrors(t *testing.T) {

View File

@@ -6,6 +6,7 @@ package template
import (
"github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate/parse"
"maps"
"reflect"
"sync"
)
@@ -102,12 +103,8 @@ func (t *Template) Clone() (*Template, error) {
}
t.muFuncs.RLock()
defer t.muFuncs.RUnlock()
for k, v := range t.parseFuncs {
nt.parseFuncs[k] = v
}
for k, v := range t.execFuncs {
nt.execFuncs[k] = v
}
maps.Copy(nt.parseFuncs, t.parseFuncs)
maps.Copy(nt.execFuncs, t.execFuncs)
return nt, nil
}