Upgrade to Go 1.23

Fixes #12763
This commit is contained in:
Bjørn Erik Pedersen 2024-08-14 11:34:21 +02:00
parent b3ad58fa04
commit 2168c5b125
34 changed files with 616 additions and 402 deletions

View file

@ -4,7 +4,7 @@ parameters:
defaults: &defaults
resource_class: large
docker:
- image: bepsays/ci-hugoreleaser:1.22200.20501
- image: bepsays/ci-hugoreleaser:1.22300.20000
environment: &buildenv
GOMODCACHE: /root/project/gomodcache
version: 2
@ -60,7 +60,7 @@ jobs:
environment:
<<: [*buildenv]
docker:
- image: bepsays/ci-hugoreleaser-linux-arm64:1.22200.20501
- image: bepsays/ci-hugoreleaser-linux-arm64:1.22300.20000
steps:
- *restore-cache
- &attach-workspace

View file

@ -16,7 +16,7 @@ jobs:
test:
strategy:
matrix:
go-version: [1.21.x, 1.22.x]
go-version: [1.22.x, 1.23.x]
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:

2
go.mod
View file

@ -170,4 +170,4 @@ require (
software.sslmate.com/src/go-pkcs12 v0.2.0 // indirect
)
go 1.21.8
go 1.22.6

View file

@ -147,7 +147,8 @@ func isWrite(flag int) bool {
// TODO(bep) move this to a more suitable place.
func MakeReadableAndRemoveAllModulePkgDir(fs afero.Fs, dir string) (int, error) {
// Safe guard
if !strings.Contains(dir, "pkg") {
// Note that the base directory changed from pkg to gomod_cache in Go 1.23.
if !strings.Contains(dir, "pkg") && !strings.Contains(dir, "gomod") {
panic(fmt.Sprint("invalid dir:", dir))
}

View file

@ -365,18 +365,6 @@ func (c *Client) Get(args ...string) error {
}
func (c *Client) get(args ...string) error {
var hasD bool
for _, arg := range args {
if arg == "-d" {
hasD = true
break
}
}
if !hasD {
// go get without the -d flag does not make sense to us, as
// it will try to build and install go packages.
args = append([]string{"-d"}, args...)
}
if err := c.runGo(context.Background(), c.logger.Out(), append([]string{"get"}, args...)...); err != nil {
return fmt.Errorf("failed to get %q: %w", args, err)
}

View file

@ -16,7 +16,7 @@ import (
)
func main() {
// The current is built with 8e1fdea8316d840fd07e9d6e026048e53290948b go1.22.5
// The current is built with 6885bad7dd86880be6929c02085e5c7a67ff2887 go1.23.0
// TODO(bep) preserve the staticcheck.conf file.
fmt.Println("Forking ...")
defer fmt.Println("Done ...")

View file

@ -18,7 +18,8 @@ hugo mod clean
! stderr .
stdout 'hugo: removed 1 dirs in module cache for \"github.com/bep/empty-hugo-module\"'
hugo mod clean --all
stdout 'Deleted 2\d{2} files from module cache\.'
# Currently this is 299 on MacOS and 301 on Linux.
stdout 'Deleted (2|3)\d{2} files from module cache\.'
cd submod
hugo mod init testsubmod
cmpenv go.mod $WORK/golden/go.mod.testsubmod

View file

@ -2,6 +2,7 @@
dostounix golden/package.json
hugo mod npm pack
cmp package.json golden/package.json
@ -41,3 +42,4 @@ path="github.com/gohugoio/hugoTestModule2"
}
-- go.mod --
module github.com/gohugoio/hugoTestModule
go 1.20

View file

@ -55,3 +55,4 @@ path="github.com/gohugoio/hugoTestModule2"
}
-- go.mod --
module github.com/gohugoio/hugoTestModule
go 1.20

View file

@ -36,6 +36,7 @@ const KnownEnv = `
GOAMD64
GOARCH
GOARM
GOARM64
GOBIN
GOCACHE
GOCACHEPROG
@ -57,6 +58,7 @@ const KnownEnv = `
GOPPC64
GOPRIVATE
GOPROXY
GORISCV64
GOROOT
GOSUMDB
GOTMPDIR

View file

@ -9,25 +9,23 @@
package fmtsort
import (
"cmp"
"reflect"
"sort"
"slices"
)
// Note: Throughout this package we avoid calling reflect.Value.Interface as
// it is not always legal to do so and it's easier to avoid the issue than to face it.
// SortedMap represents a map's keys and values. The keys and values are
// aligned in index order: Value[i] is the value in the map corresponding to Key[i].
type SortedMap struct {
Key []reflect.Value
Value []reflect.Value
}
// SortedMap is a slice of KeyValue pairs that simplifies sorting
// and iterating over map entries.
//
// Each KeyValue pair contains a map key and its corresponding value.
type SortedMap []KeyValue
func (o *SortedMap) Len() int { return len(o.Key) }
func (o *SortedMap) Less(i, j int) bool { return compare(o.Key[i], o.Key[j]) < 0 }
func (o *SortedMap) Swap(i, j int) {
o.Key[i], o.Key[j] = o.Key[j], o.Key[i]
o.Value[i], o.Value[j] = o.Value[j], o.Value[i]
// KeyValue holds a single key and value pair found in a map.
type KeyValue struct {
Key, Value reflect.Value
}
// Sort accepts a map and returns a SortedMap that has the same keys and
@ -48,7 +46,7 @@ func (o *SortedMap) Swap(i, j int) {
// Otherwise identical arrays compare by length.
// - interface values compare first by reflect.Type describing the concrete type
// and then by concrete value as described in the previous rules.
func Sort(mapValue reflect.Value) *SortedMap {
func Sort(mapValue reflect.Value) SortedMap {
if mapValue.Type().Kind() != reflect.Map {
return nil
}
@ -56,18 +54,14 @@ func Sort(mapValue reflect.Value) *SortedMap {
// of a concurrent map update. The runtime is responsible for
// yelling loudly if that happens. See issue 33275.
n := mapValue.Len()
key := make([]reflect.Value, 0, n)
value := make([]reflect.Value, 0, n)
sorted := make(SortedMap, 0, n)
iter := mapValue.MapRange()
for iter.Next() {
key = append(key, iter.Key())
value = append(value, iter.Value())
sorted = append(sorted, KeyValue{iter.Key(), iter.Value()})
}
sorted := &SortedMap{
Key: key,
Value: value,
}
sort.Stable(sorted)
slices.SortStableFunc(sorted, func(a, b KeyValue) int {
return compare(a.Key, b.Key)
})
return sorted
}
@ -82,43 +76,19 @@ func compare(aVal, bVal reflect.Value) int {
}
switch aVal.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
a, b := aVal.Int(), bVal.Int()
switch {
case a < b:
return -1
case a > b:
return 1
default:
return 0
}
return cmp.Compare(aVal.Int(), bVal.Int())
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
a, b := aVal.Uint(), bVal.Uint()
switch {
case a < b:
return -1
case a > b:
return 1
default:
return 0
}
return cmp.Compare(aVal.Uint(), bVal.Uint())
case reflect.String:
a, b := aVal.String(), bVal.String()
switch {
case a < b:
return -1
case a > b:
return 1
default:
return 0
}
return cmp.Compare(aVal.String(), bVal.String())
case reflect.Float32, reflect.Float64:
return floatCompare(aVal.Float(), bVal.Float())
return cmp.Compare(aVal.Float(), bVal.Float())
case reflect.Complex64, reflect.Complex128:
a, b := aVal.Complex(), bVal.Complex()
if c := floatCompare(real(a), real(b)); c != 0 {
if c := cmp.Compare(real(a), real(b)); c != 0 {
return c
}
return floatCompare(imag(a), imag(b))
return cmp.Compare(imag(a), imag(b))
case reflect.Bool:
a, b := aVal.Bool(), bVal.Bool()
switch {
@ -130,28 +100,12 @@ func compare(aVal, bVal reflect.Value) int {
return -1
}
case reflect.Pointer, reflect.UnsafePointer:
a, b := aVal.Pointer(), bVal.Pointer()
switch {
case a < b:
return -1
case a > b:
return 1
default:
return 0
}
return cmp.Compare(aVal.Pointer(), bVal.Pointer())
case reflect.Chan:
if c, ok := nilCompare(aVal, bVal); ok {
return c
}
ap, bp := aVal.Pointer(), bVal.Pointer()
switch {
case ap < bp:
return -1
case ap > bp:
return 1
default:
return 0
}
return cmp.Compare(aVal.Pointer(), bVal.Pointer())
case reflect.Struct:
for i := 0; i < aVal.NumField(); i++ {
if c := compare(aVal.Field(i), bVal.Field(i)); c != 0 {
@ -198,22 +152,3 @@ func nilCompare(aVal, bVal reflect.Value) (int, bool) {
}
return 0, false
}
// floatCompare compares two floating-point values. NaNs compare low.
func floatCompare(a, b float64) int {
switch {
case isNaN(a):
return -1 // No good answer if b is a NaN so don't bother checking.
case isNaN(b):
return 1
case a < b:
return -1
case a > b:
return 1
}
return 0
}
func isNaN(a float64) bool {
return a != a
}

View file

@ -5,12 +5,13 @@
package fmtsort_test
import (
"cmp"
"fmt"
"github.com/gohugoio/hugo/tpl/internal/go_templates/fmtsort"
"math"
"reflect"
"runtime"
"sort"
"slices"
"strings"
"testing"
"unsafe"
@ -67,10 +68,6 @@ func TestCompare(t *testing.T) {
switch {
case i == j:
expect = 0
// NaNs are tricky.
if typ := v0.Type(); (typ.Kind() == reflect.Float32 || typ.Kind() == reflect.Float64) && math.IsNaN(v0.Float()) {
expect = -1
}
case i < j:
expect = -1
case i > j:
@ -142,13 +139,13 @@ func sprint(data any) string {
return "nil"
}
b := new(strings.Builder)
for i, key := range om.Key {
for i, m := range om {
if i > 0 {
b.WriteRune(' ')
}
b.WriteString(sprintKey(key))
b.WriteString(sprintKey(m.Key))
b.WriteRune(':')
fmt.Fprint(b, om.Value[i])
fmt.Fprint(b, m.Value)
}
return b.String()
}
@ -200,8 +197,8 @@ func makeChans() []chan int {
for i := range cs {
pin.Pin(reflect.ValueOf(cs[i]).UnsafePointer())
}
sort.Slice(cs, func(i, j int) bool {
return uintptr(reflect.ValueOf(cs[i]).UnsafePointer()) < uintptr(reflect.ValueOf(cs[j]).UnsafePointer())
slices.SortFunc(cs, func(a, b chan int) int {
return cmp.Compare(reflect.ValueOf(a).Pointer(), reflect.ValueOf(b).Pointer())
})
return cs
}

View file

@ -29,7 +29,6 @@ const (
// indirect returns the value, after dereferencing as many times
// as necessary to reach the base type (or nil).
// Signature modified by Hugo. TODO(bep) script this.
func doIndirect(a any) any {
if a == nil {
return nil
@ -46,8 +45,8 @@ func doIndirect(a any) any {
}
var (
errorType = reflect.TypeOf((*error)(nil)).Elem()
fmtStringerType = reflect.TypeOf((*fmt.Stringer)(nil)).Elem()
errorType = reflect.TypeFor[error]()
fmtStringerType = reflect.TypeFor[fmt.Stringer]()
)
// indirectToStringerOrError returns the value, after dereferencing as many times

View file

@ -232,11 +232,9 @@ Least Surprise Property:
knows that contextual autoescaping happens should be able to look at a {{.}}
and correctly infer what sanitization happens."
As a consequence of the Least Surprise Property, template actions within an
ECMAScript 6 template literal are disabled by default.
Handling string interpolation within these literals is rather complex resulting
in no clear safe way to support it.
To re-enable template actions within ECMAScript 6 template literals, use the
GODEBUG=jstmpllitinterp=1 environment variable.
Previously, ECMAScript 6 template literal were disabled by default, and could be
enabled with the GODEBUG=jstmpllitinterp=1 environment variable. Template
literals are now supported by default, and setting jstmpllitinterp has no
effect.
*/
package template

View file

@ -2,9 +2,6 @@
// 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

@ -273,8 +273,8 @@ type execTest struct {
// of the max int boundary.
// We do it this way so the test doesn't depend on ints being 32 bits.
var (
bigInt = fmt.Sprintf("0x%x", int(1<<uint(reflect.TypeOf(0).Bits()-1)-1))
bigUint = fmt.Sprintf("0x%x", uint(1<<uint(reflect.TypeOf(0).Bits()-1)))
bigInt = fmt.Sprintf("0x%x", int(1<<uint(reflect.TypeFor[int]().Bits()-1)-1))
bigUint = fmt.Sprintf("0x%x", uint(1<<uint(reflect.TypeFor[int]().Bits()-1)))
)
var execTests = []execTest{
@ -580,6 +580,8 @@ var execTests = []execTest{
{"with $x struct.U.V", "{{with $x := $}}{{$x.U.V}}{{end}}", "v", tVal, true},
{"with variable and action", "{{with $x := $}}{{$y := $.U.V}}{{$y}}{{end}}", "v", tVal, true},
{"with on typed nil interface value", "{{with .NonEmptyInterfaceTypedNil}}TRUE{{ end }}", "", tVal, true},
{"with else with", "{{with 0}}{{.}}{{else with true}}{{.}}{{end}}", "true", tVal, true},
{"with else with chain", "{{with 0}}{{.}}{{else with false}}{{.}}{{else with `notempty`}}{{.}}{{end}}", "notempty", tVal, true},
// Range.
{"range []int", "{{range .SI}}-{{.}}-{{end}}", "-3--4--5-", tVal, true},

View file

@ -125,7 +125,7 @@ var regexpPrecederKeywords = map[string]bool{
"void": true,
}
var jsonMarshalType = reflect.TypeOf((*json.Marshaler)(nil)).Elem()
var jsonMarshalType = reflect.TypeFor[json.Marshaler]()
// indirectToJSONMarshaler returns the value, after dereferencing as many times
// as necessary to reach the base type (or nil) or an implementation of json.Marshal.
@ -172,7 +172,7 @@ func jsValEscaper(args ...any) string {
// cyclic data. This may be an unacceptable DoS risk.
b, err := json.Marshal(a)
if err != nil {
// While the standard JSON marshaller does not include user controlled
// While the standard JSON marshaler does not include user controlled
// information in the error message, if a type has a MarshalJSON method,
// the content of the error message is not guaranteed. Since we insert
// the error into the template, as part of a comment, we attempt to
@ -393,7 +393,6 @@ var jsStrNormReplacementTable = []string{
'<': `\u003c`,
'>': `\u003e`,
}
var jsRegexpReplacementTable = []string{
0: `\u0000`,
'\t': `\t`,

View file

@ -179,7 +179,7 @@ func (t *Template) DefinedTemplates() string {
// definition of t itself.
//
// Templates can be redefined in successive calls to Parse,
// before the first use of Execute on t or any associated template.
// before the first use of [Template.Execute] on t or any associated template.
// A template definition with a body containing only white space and comments
// is considered empty and will not replace an existing template's body.
// This allows using Parse to add new named template definitions without
@ -238,8 +238,8 @@ func (t *Template) AddParseTree(name string, tree *parse.Tree) (*Template, error
// Clone returns a duplicate of the template, including all associated
// templates. The actual representation is not copied, but the name space of
// associated templates is, so further calls to Parse in the copy will add
// templates to the copy but not to the original. Clone can be used to prepare
// associated templates is, so further calls to [Template.Parse] in the copy will add
// templates to the copy but not to the original. [Template.Clone] can be used to prepare
// common templates and use them with variant definitions for other templates
// by adding the variants after the clone is made.
//
@ -342,7 +342,7 @@ func (t *Template) Funcs(funcMap FuncMap) *Template {
}
// Delims sets the action delimiters to the specified strings, to be used in
// subsequent calls to Parse, ParseFiles, or ParseGlob. Nested template
// subsequent calls to [Template.Parse], [ParseFiles], or [ParseGlob]. Nested template
// definitions will inherit the settings. An empty delimiter stands for the
// corresponding default: {{ or }}.
// The return value is the template, so calls can be chained.
@ -359,7 +359,7 @@ func (t *Template) Lookup(name string) *Template {
return t.set[name]
}
// Must is a helper that wraps a call to a function returning (*Template, error)
// Must is a helper that wraps a call to a function returning ([*Template], error)
// and panics if the error is non-nil. It is intended for use in variable initializations
// such as
//
@ -371,10 +371,10 @@ func Must(t *Template, err error) *Template {
return t
}
// ParseFiles creates a new Template and parses the template definitions from
// ParseFiles creates a new [Template] and parses the template definitions from
// the named files. The returned template's name will have the (base) name and
// (parsed) contents of the first file. There must be at least one file.
// If an error occurs, parsing stops and the returned *Template is nil.
// If an error occurs, parsing stops and the returned [*Template] is nil.
//
// When parsing multiple files with the same name in different directories,
// the last one mentioned will be the one that results.
@ -436,12 +436,12 @@ func parseFiles(t *Template, readFile func(string) (string, []byte, error), file
return t, nil
}
// ParseGlob creates a new Template and parses the template definitions from
// ParseGlob creates a new [Template] and parses the template definitions from
// the files identified by the pattern. The files are matched according to the
// semantics of filepath.Match, and the pattern must match at least one file.
// The returned template will have the (base) name and (parsed) contents of the
// first file matched by the pattern. ParseGlob is equivalent to calling
// ParseFiles with the list of files matched by the pattern.
// [ParseFiles] with the list of files matched by the pattern.
//
// When parsing multiple files with the same name in different directories,
// the last one mentioned will be the one that results.
@ -485,7 +485,7 @@ func IsTrue(val any) (truth, ok bool) {
return template.IsTrue(val)
}
// ParseFS is like ParseFiles or ParseGlob but reads from the file system fs
// ParseFS is like [ParseFiles] or [ParseGlob] but reads from the file system fs
// instead of the host operating system's file system.
// It accepts a list of glob patterns.
// (Note that most file names serve as glob patterns matching only themselves.)
@ -493,7 +493,7 @@ func ParseFS(fs fs.FS, patterns ...string) (*Template, error) {
return parseFS(nil, fs, patterns)
}
// ParseFS is like ParseFiles or ParseGlob but reads from the file system fs
// ParseFS is like [Template.ParseFiles] or [Template.ParseGlob] but reads from the file system fs
// instead of the host operating system's file system.
// It accepts a list of glob patterns.
// (Note that most file names serve as glob patterns matching only themselves.)

View file

@ -414,7 +414,7 @@ func tJSDelimited(c context, s []byte) (context, int) {
// If "</script" appears in a regex literal, the '/' should not
// close the regex literal, and it will later be escaped to
// "\x3C/script" in escapeText.
if i > 0 && i+7 <= len(s) && bytes.Compare(bytes.ToLower(s[i-1:i+7]), []byte("</script")) == 0 {
if i > 0 && i+7 <= len(s) && bytes.Equal(bytes.ToLower(s[i-1:i+7]), []byte("</script")) {
i++
} else if !inCharset {
c.state, c.jsCtx = stateJS, jsCtxDivOp

View file

@ -132,15 +132,13 @@ func findGOROOT() (string, error) {
// If runtime.GOROOT() is non-empty, assume that it is valid.
//
// (It might not be: for example, the user may have explicitly set GOROOT
// to the wrong directory, or explicitly set GOROOT_FINAL but not GOROOT
// and hasn't moved the tree to GOROOT_FINAL yet. But those cases are
// to the wrong directory. But this case is
// rare, and if that happens the user can fix what they broke.)
return
}
// runtime.GOROOT doesn't know where GOROOT is (perhaps because the test
// binary was built with -trimpath, or perhaps because GOROOT_FINAL was set
// without GOROOT and the tree hasn't been moved there yet).
// binary was built with -trimpath).
//
// Since this is internal/testenv, we can cheat and assume that the caller
// is a test of some package in a subdirectory of GOROOT/src. ('go test'
@ -315,12 +313,18 @@ func MustInternalLink(t testing.TB, withCgo bool) {
}
}
// MustInternalLinkPIE checks whether the current system can link PIE binary using
// internal linking.
// If not, MustInternalLinkPIE calls t.Skip with an explanation.
// Modified by Hugo (not needed)
func MustInternalLinkPIE(t testing.TB) {
}
// MustHaveBuildMode reports whether the current system can build programs in
// the given build mode.
// If not, MustHaveBuildMode calls t.Skip with an explanation.
// Modified by Hugo (not needed)
func MustHaveBuildMode(t testing.TB, buildmode string) {
return
}
// HasSymlink reports whether the current system can use os.Symlink.
@ -447,3 +451,10 @@ func WriteImportcfg(t testing.TB, dstPath string, packageFiles map[string]string
func SyscallIsNotSupported(err error) bool {
return syscallIsNotSupported(err)
}
// ParallelOn64Bit calls t.Parallel() unless there is a case that cannot be parallel.
// This function should be used when it is necessary to avoid t.Parallel on
// 32-bit machines, typically because the test uses lots of memory.
// Disabled by Hugo.
func ParallelOn64Bit(t *testing.T) {
}

View file

@ -144,6 +144,13 @@ data, defined in detail in the corresponding sections that follow.
is executed; otherwise, dot is set to the value of the pipeline
and T1 is executed.
{{with pipeline}} T1 {{else with pipeline}} T0 {{end}}
To simplify the appearance of with-else chains, the else action
of a with may include another with directly; the effect is exactly
the same as writing
{{with pipeline}} T1 {{else}}{{with pipeline}} T0 {{end}}{{end}}
Arguments
An argument is a simple value, denoted by one of the following.

View file

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

View file

@ -2,9 +2,6 @@
// 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

@ -7,13 +7,12 @@ package template
import (
"errors"
"fmt"
"github.com/gohugoio/hugo/tpl/internal/go_templates/fmtsort"
"github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate/parse"
"io"
"reflect"
"runtime"
"strings"
"github.com/gohugoio/hugo/tpl/internal/go_templates/fmtsort"
"github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate/parse"
)
// maxExecDepth specifies the maximum stack depth of templates within
@ -95,7 +94,7 @@ type missingValType struct{}
var missingVal = reflect.ValueOf(missingValType{})
var missingValReflectType = reflect.TypeOf(missingValType{})
var missingValReflectType = reflect.TypeFor[missingValType]()
func isMissing(v reflect.Value) bool {
return v.IsValid() && v.Type() == missingValReflectType
@ -202,8 +201,8 @@ func (t *Template) ExecuteTemplate(wr io.Writer, name string, data any) error {
// A template may be executed safely in parallel, although if parallel
// executions share a Writer the output may be interleaved.
//
// If data is a reflect.Value, the template applies to the concrete
// value that the reflect.Value holds, as in fmt.Print.
// If data is a [reflect.Value], the template applies to the concrete
// value that the reflect.Value holds, as in [fmt.Print].
func (t *Template) Execute(wr io.Writer, data any) error {
return t.execute(wr, data)
}
@ -229,7 +228,7 @@ func (t *Template) execute(wr io.Writer, data any) (err error) {
// DefinedTemplates returns a string listing the defined templates,
// prefixed by the string "; defined templates are: ". If there are none,
// it returns the empty string. For generating an error message here
// and in html/template.
// and in [html/template].
func (t *Template) DefinedTemplates() string {
if t.common == nil {
return ""
@ -409,8 +408,8 @@ func (s *state) walkRange(dot reflect.Value, r *parse.RangeNode) {
break
}
om := fmtsort.Sort(val)
for i, key := range om.Key {
oneIteration(key, om.Value[i])
for _, m := range om {
oneIteration(m.Key, m.Value)
}
return
case reflect.Chan:
@ -480,7 +479,7 @@ func (s *state) evalPipeline(dot reflect.Value, pipe *parse.PipeNode) (value ref
value = s.evalCommand(dot, cmd, value) // previous value is this one's final arg.
// If the object has type interface{}, dig down one level to the thing inside.
if value.Kind() == reflect.Interface && value.Type().NumMethod() == 0 {
value = reflect.ValueOf(value.Interface()) // lovely!
value = value.Elem()
}
}
for _, variable := range pipe.Decl {
@ -709,9 +708,9 @@ func (s *state) evalFieldOld(dot reflect.Value, fieldName string, node parse.Nod
}
var (
errorType = reflect.TypeOf((*error)(nil)).Elem()
fmtStringerType = reflect.TypeOf((*fmt.Stringer)(nil)).Elem()
reflectValueType = reflect.TypeOf((*reflect.Value)(nil)).Elem()
errorType = reflect.TypeFor[error]()
fmtStringerType = reflect.TypeFor[fmt.Stringer]()
reflectValueType = reflect.TypeFor[reflect.Value]()
)
// evalCall executes a function or method call. If it's a method, fun already has the receiver bound, so
@ -735,9 +734,8 @@ func (s *state) evalCallOld(dot, fun reflect.Value, isBuiltin bool, node parse.N
} else if numIn != typ.NumIn() {
s.errorf("wrong number of args for %s: want %d got %d", name, typ.NumIn(), numIn)
}
if !goodFunc(typ) {
// TODO: This could still be a confusing error; maybe goodFunc should provide info.
s.errorf("can't call method/function %q with %d results", name, typ.NumOut())
if err := goodFunc(name, typ); err != nil {
s.errorf("%v", err)
}
unwrap := func(v reflect.Value) reflect.Value {
@ -801,6 +799,15 @@ func (s *state) evalCallOld(dot, fun reflect.Value, isBuiltin bool, node parse.N
}
argv[i] = s.validateType(final, t)
}
// 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()
argv = append([]reflect.Value{reflect.ValueOf(calleeName)}, argv...)
fun = reflect.ValueOf(call)
}
v, err := safeCall(fun, argv)
// If we have an error that is not nil, stop execution and return that
// error to the caller.

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 !windows
// +build !windows
package template
import (
@ -81,6 +84,9 @@ type T struct {
NilOKFunc func(*int) bool
ErrFunc func() (string, error)
PanicFunc func() string
TooFewReturnCountFunc func()
TooManyReturnCountFunc func() (string, error, int)
InvalidReturnTypeFunc func() (string, bool)
// Template to test evaluation of templates.
Tmpl *Template
// Unexported field; cannot be accessed by template.
@ -168,6 +174,9 @@ var tVal = &T{
NilOKFunc: func(s *int) bool { return s == nil },
ErrFunc: func() (string, error) { return "bla", nil },
PanicFunc: func() string { panic("test panic") },
TooFewReturnCountFunc: func() {},
TooManyReturnCountFunc: func() (string, error, int) { return "", nil, 0 },
InvalidReturnTypeFunc: func() (string, bool) { return "", false },
Tmpl: Must(New("x").Parse("test template")), // "x" is the value of .X
}
@ -265,8 +274,8 @@ type execTest struct {
// of the max int boundary.
// We do it this way so the test doesn't depend on ints being 32 bits.
var (
bigInt = fmt.Sprintf("0x%x", int(1<<uint(reflect.TypeOf(0).Bits()-1)-1))
bigUint = fmt.Sprintf("0x%x", uint(1<<uint(reflect.TypeOf(0).Bits()-1)))
bigInt = fmt.Sprintf("0x%x", int(1<<uint(reflect.TypeFor[int]().Bits()-1)-1))
bigUint = fmt.Sprintf("0x%x", uint(1<<uint(reflect.TypeFor[int]().Bits()-1)))
)
var execTests = []execTest{
@ -583,6 +592,8 @@ var execTests = []execTest{
{"with $x struct.U.V", "{{with $x := $}}{{$x.U.V}}{{end}}", "v", tVal, true},
{"with variable and action", "{{with $x := $}}{{$y := $.U.V}}{{$y}}{{end}}", "v", tVal, true},
{"with on typed nil interface value", "{{with .NonEmptyInterfaceTypedNil}}TRUE{{ end }}", "", tVal, true},
{"with else with", "{{with 0}}{{.}}{{else with true}}{{.}}{{end}}", "true", tVal, true},
{"with else with chain", "{{with 0}}{{.}}{{else with false}}{{.}}{{else with `notempty`}}{{.}}{{end}}", "notempty", tVal, true},
// Range.
{"range []int", "{{range .SI}}-{{.}}-{{end}}", "-3--4--5-", tVal, true},
@ -1723,6 +1734,81 @@ func TestExecutePanicDuringCall(t *testing.T) {
}
}
func TestFunctionCheckDuringCall(t *testing.T) {
tests := []struct {
name string
input string
data any
wantErr string
}{
{
name: "call nothing",
input: `{{call}}`,
data: tVal,
wantErr: "wrong number of args for call: want at least 1 got 0",
},
{
name: "call non-function",
input: "{{call .True}}",
data: tVal,
wantErr: "error calling call: non-function .True of type bool",
},
{
name: "call func with wrong argument",
input: "{{call .BinaryFunc 1}}",
data: tVal,
wantErr: "error calling call: wrong number of args for .BinaryFunc: got 1 want 2",
},
{
name: "call variadic func with wrong argument",
input: `{{call .VariadicFuncInt}}`,
data: tVal,
wantErr: "error calling call: wrong number of args for .VariadicFuncInt: got 0 want at least 1",
},
{
name: "call too few return number func",
input: `{{call .TooFewReturnCountFunc}}`,
data: tVal,
wantErr: "error calling call: function .TooFewReturnCountFunc has 0 return values; should be 1 or 2",
},
{
name: "call too many return number func",
input: `{{call .TooManyReturnCountFunc}}`,
data: tVal,
wantErr: "error calling call: function .TooManyReturnCountFunc has 3 return values; should be 1 or 2",
},
{
name: "call invalid return type func",
input: `{{call .InvalidReturnTypeFunc}}`,
data: tVal,
wantErr: "error calling call: invalid function signature for .InvalidReturnTypeFunc: second return value should be error; is bool",
},
{
name: "call pipeline",
input: `{{call (len "test")}}`,
data: nil,
wantErr: "error calling call: non-function len \"test\" of type int",
},
}
for _, tc := range tests {
b := new(bytes.Buffer)
tmpl, err := New("t").Parse(tc.input)
if err != nil {
t.Fatalf("parse error: %s", err)
}
err = tmpl.Execute(b, tc.data)
if err == nil {
t.Errorf("%s: expected error; got none", tc.name)
} else if tc.wantErr == "" || !strings.Contains(err.Error(), tc.wantErr) {
if *debug {
fmt.Printf("%s: test execute error: %s\n", tc.name, err)
}
t.Errorf("%s: expected error:\n%s\ngot:\n%s", tc.name, tc.wantErr, err)
}
}
}
// Issue 31810. Check that a parenthesized first argument behaves properly.
func TestIssue31810(t *testing.T) {
// A simple value with no arguments is fine.

View file

@ -22,14 +22,14 @@ import (
// return value evaluates to non-nil during execution, execution terminates and
// Execute returns that error.
//
// Errors returned by Execute wrap the underlying error; call errors.As to
// Errors returned by Execute wrap the underlying error; call [errors.As] to
// unwrap them.
//
// When template execution invokes a function with an argument list, that list
// must be assignable to the function's parameter types. Functions meant to
// apply to arguments of arbitrary type can use parameters of type interface{} or
// of type reflect.Value. Similarly, functions meant to return a result of arbitrary
// type can return interface{} or reflect.Value.
// of type [reflect.Value]. Similarly, functions meant to return a result of arbitrary
// type can return interface{} or [reflect.Value].
type FuncMap map[string]any
// builtins returns the FuncMap.
@ -39,7 +39,7 @@ type FuncMap map[string]any
func builtins() FuncMap {
return FuncMap{
"and": and,
"call": call,
"call": emptyCall,
"html": HTMLEscaper,
"index": index,
"slice": slice,
@ -93,8 +93,8 @@ func addValueFuncs(out map[string]reflect.Value, in FuncMap) {
if v.Kind() != reflect.Func {
panic("value for " + name + " not a function")
}
if !goodFunc(v.Type()) {
panic(fmt.Errorf("can't install method/function %q with %d results", name, v.Type().NumOut()))
if err := goodFunc(name, v.Type()); err != nil {
panic(err)
}
out[name] = v
}
@ -109,15 +109,18 @@ func addFuncs(out, in FuncMap) {
}
// goodFunc reports whether the function or method has the right result signature.
func goodFunc(typ reflect.Type) bool {
func goodFunc(name string, typ reflect.Type) error {
// We allow functions with 1 result or 2 results where the second is an error.
switch {
case typ.NumOut() == 1:
return true
case typ.NumOut() == 2 && typ.Out(1) == errorType:
return true
switch numOut := typ.NumOut(); {
case numOut == 1:
return nil
case numOut == 2 && typ.Out(1) == errorType:
return nil
case numOut == 2:
return fmt.Errorf("invalid function signature for %s: second return value should be error; is %s", name, typ.Out(1))
default:
return fmt.Errorf("function %s has %d return values; should be 1 or 2", name, typ.NumOut())
}
return false
}
// goodName reports whether the function name is a valid identifier.
@ -309,30 +312,35 @@ func length(item reflect.Value) (int, error) {
// Function invocation
func emptyCall(fn reflect.Value, args ...reflect.Value) reflect.Value {
panic("unreachable") // implemented as a special case in evalCall
}
// call returns the result of evaluating the first argument as a function.
// The function must return 1 result, or 2 results, the second of which is an error.
func call(fn reflect.Value, args ...reflect.Value) (reflect.Value, error) {
func call(name string, fn reflect.Value, args ...reflect.Value) (reflect.Value, error) {
fn = indirectInterface(fn)
if !fn.IsValid() {
return reflect.Value{}, fmt.Errorf("call of nil")
}
typ := fn.Type()
if typ.Kind() != reflect.Func {
return reflect.Value{}, fmt.Errorf("non-function of type %s", typ)
return reflect.Value{}, fmt.Errorf("non-function %s of type %s", name, typ)
}
if !goodFunc(typ) {
return reflect.Value{}, fmt.Errorf("function called with %d args; should be 1 or 2", typ.NumOut())
if err := goodFunc(name, typ); err != nil {
return reflect.Value{}, err
}
numIn := typ.NumIn()
var dddType reflect.Type
if typ.IsVariadic() {
if len(args) < numIn-1 {
return reflect.Value{}, fmt.Errorf("wrong number of args: got %d want at least %d", len(args), numIn-1)
return reflect.Value{}, fmt.Errorf("wrong number of args for %s: got %d want at least %d", name, len(args), numIn-1)
}
dddType = typ.In(numIn - 1).Elem()
} else {
if len(args) != numIn {
return reflect.Value{}, fmt.Errorf("wrong number of args: got %d want %d", len(args), numIn)
return reflect.Value{}, fmt.Errorf("wrong number of args for %s: got %d want %d", name, len(args), numIn)
}
}
argv := make([]reflect.Value, len(args))

View file

@ -16,7 +16,7 @@ import (
// Functions and methods to parse templates.
// Must is a helper that wraps a call to a function returning (*Template, error)
// Must is a helper that wraps a call to a function returning ([*Template], error)
// and panics if the error is non-nil. It is intended for use in variable
// initializations such as
//
@ -28,7 +28,7 @@ func Must(t *Template, err error) *Template {
return t
}
// ParseFiles creates a new Template and parses the template definitions from
// ParseFiles creates a new [Template] and parses the template definitions from
// the named files. The returned template's name will have the base name and
// parsed contents of the first file. There must be at least one file.
// If an error occurs, parsing stops and the returned *Template is nil.
@ -45,9 +45,9 @@ func ParseFiles(filenames ...string) (*Template, error) {
// t. If an error occurs, parsing stops and the returned template is nil;
// otherwise it is t. There must be at least one file.
// Since the templates created by ParseFiles are named by the base
// names of the argument files, t should usually have the name of one
// of the (base) names of the files. If it does not, depending on t's
// contents before calling ParseFiles, t.Execute may fail. In that
// (see [filepath.Base]) names of the argument files, t should usually have the
// name of one of the (base) names of the files. If it does not, depending on
// t's contents before calling ParseFiles, t.Execute may fail. In that
// case use t.ExecuteTemplate to execute a valid template.
//
// When parsing multiple files with the same name in different directories,
@ -93,12 +93,12 @@ func parseFiles(t *Template, readFile func(string) (string, []byte, error), file
return t, nil
}
// ParseGlob creates a new Template and parses the template definitions from
// ParseGlob creates a new [Template] and parses the template definitions from
// the files identified by the pattern. The files are matched according to the
// semantics of filepath.Match, and the pattern must match at least one file.
// The returned template will have the (base) name and (parsed) contents of the
// first file matched by the pattern. ParseGlob is equivalent to calling
// ParseFiles with the list of files matched by the pattern.
// semantics of [filepath.Match], and the pattern must match at least one file.
// The returned template will have the [filepath.Base] name and (parsed)
// contents of the first file matched by the pattern. ParseGlob is equivalent to
// calling [ParseFiles] with the list of files matched by the pattern.
//
// When parsing multiple files with the same name in different directories,
// the last one mentioned will be the one that results.
@ -108,9 +108,9 @@ func ParseGlob(pattern string) (*Template, error) {
// ParseGlob parses the template definitions in the files identified by the
// pattern and associates the resulting templates with t. The files are matched
// according to the semantics of filepath.Match, and the pattern must match at
// least one file. ParseGlob is equivalent to calling t.ParseFiles with the
// list of files matched by the pattern.
// according to the semantics of [filepath.Match], and the pattern must match at
// least one file. ParseGlob is equivalent to calling [Template.ParseFiles] with
// the list of files matched by the pattern.
//
// When parsing multiple files with the same name in different directories,
// the last one mentioned will be the one that results.
@ -131,17 +131,17 @@ func parseGlob(t *Template, pattern string) (*Template, error) {
return parseFiles(t, readFileOS, filenames...)
}
// ParseFS is like ParseFiles or ParseGlob but reads from the file system fsys
// ParseFS is like [Template.ParseFiles] or [Template.ParseGlob] but reads from the file system fsys
// instead of the host operating system's file system.
// It accepts a list of glob patterns.
// It accepts a list of glob patterns (see [path.Match]).
// (Note that most file names serve as glob patterns matching only themselves.)
func ParseFS(fsys fs.FS, patterns ...string) (*Template, error) {
return parseFS(nil, fsys, patterns)
}
// ParseFS is like ParseFiles or ParseGlob but reads from the file system fsys
// ParseFS is like [Template.ParseFiles] or [Template.ParseGlob] but reads from the file system fsys
// instead of the host operating system's file system.
// It accepts a list of glob patterns.
// It accepts a list of glob patterns (see [path.Match]).
// (Note that most file names serve as glob patterns matching only themselves.)
func (t *Template) ParseFS(fsys fs.FS, patterns ...string) (*Template, error) {
t.init()

View file

@ -278,9 +278,8 @@ func (s *state) evalCall(dot, fun reflect.Value, isBuiltin bool, node parse.Node
} else if numIn != typ.NumIn() {
s.errorf("wrong number of args for %s: want %d got %d", name, typ.NumIn(), numIn)
}
if !goodFunc(typ) {
// TODO: This could still be a confusing error; maybe goodFunc should provide info.
s.errorf("can't call method/function %q with %d results", name, typ.NumOut())
if err := goodFunc(name, typ); err != nil {
s.errorf("%v", err)
}
unwrap := func(v reflect.Value) reflect.Value {
@ -345,6 +344,14 @@ func (s *state) evalCall(dot, fun reflect.Value, isBuiltin bool, node parse.Node
argv[i] = s.validateType(final, t)
}
// 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()
argv = append([]reflect.Value{reflect.ValueOf(calleeName)}, argv...)
fun = reflect.ValueOf(call)
}
// Added for Hugo
for i := 0; i < len(first); i++ {
argv[i] = s.validateType(first[i], typ.In(i))

View file

@ -2,18 +2,16 @@
// 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
@ -44,7 +42,7 @@ func main() {
`
td := t.TempDir()
if err := os.WriteFile(filepath.Join(td, "x.go"), []byte(prog), 0644); err != nil {
if err := os.WriteFile(filepath.Join(td, "x.go"), []byte(prog), 0o644); err != nil {
t.Fatal(err)
}
cmd := exec.Command(testenv.GoToolPath(t), "build", "-o", "x.exe", "x.go")

View file

@ -217,8 +217,12 @@ func (p *PipeNode) writeTo(sb *strings.Builder) {
}
v.writeTo(sb)
}
if p.IsAssign {
sb.WriteString(" = ")
} else {
sb.WriteString(" := ")
}
}
for i, c := range p.Cmds {
if i > 0 {
sb.WriteString(" | ")
@ -346,12 +350,12 @@ type IdentifierNode struct {
Ident string // The identifier's name.
}
// NewIdentifier returns a new IdentifierNode with the given identifier name.
// NewIdentifier returns a new [IdentifierNode] with the given identifier name.
func NewIdentifier(ident string) *IdentifierNode {
return &IdentifierNode{NodeType: NodeIdentifier, Ident: ident}
}
// SetPos sets the position. NewIdentifier is a public method so we can't modify its signature.
// SetPos sets the position. [NewIdentifier] is a public method so we can't modify its signature.
// Chained for convenience.
// TODO: fix one day?
func (i *IdentifierNode) SetPos(pos Pos) *IdentifierNode {
@ -359,7 +363,7 @@ func (i *IdentifierNode) SetPos(pos Pos) *IdentifierNode {
return i
}
// SetTree sets the parent tree for the node. NewIdentifier is a public method so we can't modify its signature.
// SetTree sets the parent tree for the node. [NewIdentifier] is a public method so we can't modify its signature.
// Chained for convenience.
// TODO: fix one day?
func (i *IdentifierNode) SetTree(t *Tree) *IdentifierNode {

View file

@ -42,7 +42,7 @@ const (
SkipFuncCheck // do not check that functions are defined
)
// Copy returns a copy of the Tree. Any parsing state is discarded.
// Copy returns a copy of the [Tree]. Any parsing state is discarded.
func (t *Tree) Copy() *Tree {
if t == nil {
return nil
@ -55,7 +55,7 @@ func (t *Tree) Copy() *Tree {
}
}
// Parse returns a map from template name to parse.Tree, created by parsing the
// Parse returns a map from template name to [Tree], created by parsing the
// templates described in the argument string. The top-level template will be
// given the specified name. If an error is encountered, parsing stops and an
// empty map is returned with the error.
@ -521,7 +521,7 @@ func (t *Tree) checkPipeline(pipe *PipeNode, context string) {
}
}
func (t *Tree) parseControl(allowElseIf bool, context string) (pos Pos, line int, pipe *PipeNode, list, elseList *ListNode) {
func (t *Tree) parseControl(context string) (pos Pos, line int, pipe *PipeNode, list, elseList *ListNode) {
defer t.popVars(len(t.vars))
pipe = t.pipeline(context, itemRightDelim)
if context == "range" {
@ -535,28 +535,31 @@ func (t *Tree) parseControl(allowElseIf bool, context string) (pos Pos, line int
switch next.Type() {
case nodeEnd: //done
case nodeElse:
if allowElseIf {
// Special case for "else if". If the "else" is followed immediately by an "if",
// the elseControl will have left the "if" token pending. Treat
// Special case for "else if" and "else with".
// If the "else" is followed immediately by an "if" or "with",
// the elseControl will have left the "if" or "with" token pending. Treat
// {{if a}}_{{else if b}}_{{end}}
// {{with a}}_{{else with b}}_{{end}}
// as
// {{if a}}_{{else}}{{if b}}_{{end}}{{end}}.
// To do this, parse the if as usual and stop at it {{end}}; the subsequent{{end}}
// is assumed. This technique works even for long if-else-if chains.
// TODO: Should we allow else-if in with and range?
if t.peek().typ == itemIf {
// {{if a}}_{{else}}{{if b}}_{{end}}{{end}}
// {{with a}}_{{else}}{{with b}}_{{end}}{{end}}.
// To do this, parse the "if" or "with" as usual and stop at it {{end}};
// the subsequent{{end}} is assumed. This technique works even for long if-else-if chains.
if context == "if" && t.peek().typ == itemIf {
t.next() // Consume the "if" token.
elseList = t.newList(next.Position())
elseList.append(t.ifControl())
// Do not consume the next item - only one {{end}} required.
break
}
}
} else if context == "with" && t.peek().typ == itemWith {
t.next()
elseList = t.newList(next.Position())
elseList.append(t.withControl())
} else {
elseList, next = t.itemList()
if next.Type() != nodeEnd {
t.errorf("expected end; found %s", next)
}
}
}
return pipe.Position(), pipe.Line, pipe, list, elseList
}
@ -567,7 +570,7 @@ func (t *Tree) parseControl(allowElseIf bool, context string) (pos Pos, line int
//
// If keyword is past.
func (t *Tree) ifControl() Node {
return t.newIf(t.parseControl(true, "if"))
return t.newIf(t.parseControl("if"))
}
// Range:
@ -577,7 +580,7 @@ func (t *Tree) ifControl() Node {
//
// Range keyword is past.
func (t *Tree) rangeControl() Node {
r := t.newRange(t.parseControl(false, "range"))
r := t.newRange(t.parseControl("range"))
return r
}
@ -588,7 +591,7 @@ func (t *Tree) rangeControl() Node {
//
// If keyword is past.
func (t *Tree) withControl() Node {
return t.newWith(t.parseControl(false, "with"))
return t.newWith(t.parseControl("with"))
}
// End:
@ -606,10 +609,11 @@ func (t *Tree) endControl() Node {
//
// Else keyword is past.
func (t *Tree) elseControl() Node {
// Special case for "else if".
peek := t.peekNonSpace()
if peek.typ == itemIf {
// We see "{{else if ... " but in effect rewrite it to {{else}}{{if ... ".
// The "{{else if ... " and "{{else with ..." will be
// treated as "{{else}}{{if ..." and "{{else}}{{with ...".
// So return the else node here.
if peek.typ == itemIf || peek.typ == itemWith {
return t.newElse(peek.pos, peek.line)
}
token := t.expect(itemRightDelim, "else")

View file

@ -33,9 +33,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, 073, 073, 073, 0},
{"0o73", true, true, true, false, 073, 073, 073, 0},
{"0O73", true, true, true, false, 073, 073, 073, 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},
{"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 +61,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, 0123, 0123, 0123, 0},
{"0123", true, true, true, false, 0o123, 0o123, 0o123, 0},
{"-0x0", true, true, true, false, 0, 0, 0, 0},
{"0xdeadbeef", true, true, true, false, 0xdeadbeef, 0xdeadbeef, 0xdeadbeef, 0},
// character constants
@ -176,74 +176,150 @@ 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}}`},
{
"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"`},
@ -252,18 +328,24 @@ 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, ""},
@ -302,6 +384,9 @@ var parseTests = []parseTest{
{"bug1a", "{{$x:=.}}{{$x!2}}", hasError, ""}, // ! is just illegal here.
{"bug1b", "{{$x:=.}}{{$x+2}}", hasError, ""}, // $x+2 should not parse as ($x) (+2).
{"bug1c", "{{$x:=.}}{{$x +2}}", noError, "{{$x := .}}{{$x +2}}"}, // It's OK with a space.
// Check the range handles assignment vs. declaration properly.
{"bug2a", "{{range $x := 0}}{{$x}}{{end}}", noError, "{{range $x := 0}}{{$x}}{{end}}"},
{"bug2b", "{{range $x = 0}}{{$x}}{{end}}", noError, "{{range $x = 0}}{{$x}}{{end}}"},
// dot following a literal value
{"dot after integer", "{{1.E}}", hasError, ""},
{"dot after float", "{{0.1.E}}", hasError, ""},
@ -402,7 +487,7 @@ func TestKeywordsAndFuncs(t *testing.T) {
{
// 'break' is a defined function, don't treat it as a keyword: it should
// accept an argument successfully.
var funcsWithKeywordFunc = map[string]any{
funcsWithKeywordFunc := map[string]any{
"break": func(in any) any { return in },
}
tmpl, err := New("").Parse(inp, "", "", make(map[string]*Tree), funcsWithKeywordFunc)
@ -489,104 +574,168 @@ 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

@ -24,7 +24,7 @@ type common struct {
}
// Template is the representation of a parsed template. The *parse.Tree
// field is exported only for use by html/template and should be treated
// field is exported only for use by [html/template] and should be treated
// as unexported by all other clients.
type Template struct {
name string
@ -79,7 +79,7 @@ func (t *Template) init() {
// Clone returns a duplicate of the template, including all associated
// templates. The actual representation is not copied, but the name space of
// associated templates is, so further calls to Parse in the copy will add
// associated templates is, so further calls to [Template.Parse] in the copy will add
// templates to the copy but not to the original. Clone can be used to prepare
// common templates and use them with variant definitions for other templates
// by adding the variants after the clone is made.
@ -157,7 +157,7 @@ func (t *Template) Templates() []*Template {
}
// Delims sets the action delimiters to the specified strings, to be used in
// subsequent calls to Parse, ParseFiles, or ParseGlob. Nested template
// subsequent calls to [Template.Parse], [Template.ParseFiles], or [Template.ParseGlob]. Nested template
// definitions will inherit the settings. An empty delimiter stands for the
// corresponding default: {{ or }}.
// The return value is the template, so calls can be chained.

View file

@ -116,6 +116,20 @@ counter2: 3
`)
}
func TestGo23ElseWith(t *testing.T) {
t.Parallel()
files := `
-- hugo.toml --
title = "Hugo"
-- layouts/index.html --
{{ with false }}{{ else with .Site }}{{ .Title }}{{ end }}|
`
b := hugolib.Test(t, files)
b.AssertFileContent("public/index.html", "Hugo|")
}
// Issue 10495
func TestCommentsBeforeBlockDefinition(t *testing.T) {
t.Parallel()