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

View file

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

2
go.mod
View file

@ -170,4 +170,4 @@ require (
software.sslmate.com/src/go-pkcs12 v0.2.0 // indirect 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. // TODO(bep) move this to a more suitable place.
func MakeReadableAndRemoveAllModulePkgDir(fs afero.Fs, dir string) (int, error) { func MakeReadableAndRemoveAllModulePkgDir(fs afero.Fs, dir string) (int, error) {
// Safe guard // 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)) 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 { 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 { 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) return fmt.Errorf("failed to get %q: %w", args, err)
} }

View file

@ -16,7 +16,7 @@ import (
) )
func main() { 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. // TODO(bep) preserve the staticcheck.conf file.
fmt.Println("Forking ...") fmt.Println("Forking ...")
defer fmt.Println("Done ...") defer fmt.Println("Done ...")

View file

@ -18,7 +18,8 @@ hugo mod clean
! stderr . ! stderr .
stdout 'hugo: removed 1 dirs in module cache for \"github.com/bep/empty-hugo-module\"' stdout 'hugo: removed 1 dirs in module cache for \"github.com/bep/empty-hugo-module\"'
hugo mod clean --all 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 cd submod
hugo mod init testsubmod hugo mod init testsubmod
cmpenv go.mod $WORK/golden/go.mod.testsubmod cmpenv go.mod $WORK/golden/go.mod.testsubmod

View file

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

View file

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

View file

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

View file

@ -9,25 +9,23 @@
package fmtsort package fmtsort
import ( import (
"cmp"
"reflect" "reflect"
"sort" "slices"
) )
// Note: Throughout this package we avoid calling reflect.Value.Interface as // 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. // 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 // SortedMap is a slice of KeyValue pairs that simplifies sorting
// aligned in index order: Value[i] is the value in the map corresponding to Key[i]. // and iterating over map entries.
type SortedMap struct { //
Key []reflect.Value // Each KeyValue pair contains a map key and its corresponding value.
Value []reflect.Value type SortedMap []KeyValue
}
func (o *SortedMap) Len() int { return len(o.Key) } // KeyValue holds a single key and value pair found in a map.
func (o *SortedMap) Less(i, j int) bool { return compare(o.Key[i], o.Key[j]) < 0 } type KeyValue struct {
func (o *SortedMap) Swap(i, j int) { Key, Value reflect.Value
o.Key[i], o.Key[j] = o.Key[j], o.Key[i]
o.Value[i], o.Value[j] = o.Value[j], o.Value[i]
} }
// Sort accepts a map and returns a SortedMap that has the same keys and // 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. // Otherwise identical arrays compare by length.
// - interface values compare first by reflect.Type describing the concrete type // - interface values compare first by reflect.Type describing the concrete type
// and then by concrete value as described in the previous rules. // 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 { if mapValue.Type().Kind() != reflect.Map {
return nil return nil
} }
@ -56,18 +54,14 @@ func Sort(mapValue reflect.Value) *SortedMap {
// of a concurrent map update. The runtime is responsible for // of a concurrent map update. The runtime is responsible for
// yelling loudly if that happens. See issue 33275. // yelling loudly if that happens. See issue 33275.
n := mapValue.Len() n := mapValue.Len()
key := make([]reflect.Value, 0, n) sorted := make(SortedMap, 0, n)
value := make([]reflect.Value, 0, n)
iter := mapValue.MapRange() iter := mapValue.MapRange()
for iter.Next() { for iter.Next() {
key = append(key, iter.Key()) sorted = append(sorted, KeyValue{iter.Key(), iter.Value()})
value = append(value, iter.Value())
} }
sorted := &SortedMap{ slices.SortStableFunc(sorted, func(a, b KeyValue) int {
Key: key, return compare(a.Key, b.Key)
Value: value, })
}
sort.Stable(sorted)
return sorted return sorted
} }
@ -82,43 +76,19 @@ func compare(aVal, bVal reflect.Value) int {
} }
switch aVal.Kind() { switch aVal.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
a, b := aVal.Int(), bVal.Int() return cmp.Compare(aVal.Int(), bVal.Int())
switch {
case a < b:
return -1
case a > b:
return 1
default:
return 0
}
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
a, b := aVal.Uint(), bVal.Uint() return cmp.Compare(aVal.Uint(), bVal.Uint())
switch {
case a < b:
return -1
case a > b:
return 1
default:
return 0
}
case reflect.String: case reflect.String:
a, b := aVal.String(), bVal.String() return cmp.Compare(aVal.String(), bVal.String())
switch {
case a < b:
return -1
case a > b:
return 1
default:
return 0
}
case reflect.Float32, reflect.Float64: case reflect.Float32, reflect.Float64:
return floatCompare(aVal.Float(), bVal.Float()) return cmp.Compare(aVal.Float(), bVal.Float())
case reflect.Complex64, reflect.Complex128: case reflect.Complex64, reflect.Complex128:
a, b := aVal.Complex(), bVal.Complex() 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 c
} }
return floatCompare(imag(a), imag(b)) return cmp.Compare(imag(a), imag(b))
case reflect.Bool: case reflect.Bool:
a, b := aVal.Bool(), bVal.Bool() a, b := aVal.Bool(), bVal.Bool()
switch { switch {
@ -130,28 +100,12 @@ func compare(aVal, bVal reflect.Value) int {
return -1 return -1
} }
case reflect.Pointer, reflect.UnsafePointer: case reflect.Pointer, reflect.UnsafePointer:
a, b := aVal.Pointer(), bVal.Pointer() return cmp.Compare(aVal.Pointer(), bVal.Pointer())
switch {
case a < b:
return -1
case a > b:
return 1
default:
return 0
}
case reflect.Chan: case reflect.Chan:
if c, ok := nilCompare(aVal, bVal); ok { if c, ok := nilCompare(aVal, bVal); ok {
return c return c
} }
ap, bp := aVal.Pointer(), bVal.Pointer() return cmp.Compare(aVal.Pointer(), bVal.Pointer())
switch {
case ap < bp:
return -1
case ap > bp:
return 1
default:
return 0
}
case reflect.Struct: case reflect.Struct:
for i := 0; i < aVal.NumField(); i++ { for i := 0; i < aVal.NumField(); i++ {
if c := compare(aVal.Field(i), bVal.Field(i)); c != 0 { 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 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 package fmtsort_test
import ( import (
"cmp"
"fmt" "fmt"
"github.com/gohugoio/hugo/tpl/internal/go_templates/fmtsort" "github.com/gohugoio/hugo/tpl/internal/go_templates/fmtsort"
"math" "math"
"reflect" "reflect"
"runtime" "runtime"
"sort" "slices"
"strings" "strings"
"testing" "testing"
"unsafe" "unsafe"
@ -67,10 +68,6 @@ func TestCompare(t *testing.T) {
switch { switch {
case i == j: case i == j:
expect = 0 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: case i < j:
expect = -1 expect = -1
case i > j: case i > j:
@ -142,13 +139,13 @@ func sprint(data any) string {
return "nil" return "nil"
} }
b := new(strings.Builder) b := new(strings.Builder)
for i, key := range om.Key { for i, m := range om {
if i > 0 { if i > 0 {
b.WriteRune(' ') b.WriteRune(' ')
} }
b.WriteString(sprintKey(key)) b.WriteString(sprintKey(m.Key))
b.WriteRune(':') b.WriteRune(':')
fmt.Fprint(b, om.Value[i]) fmt.Fprint(b, m.Value)
} }
return b.String() return b.String()
} }
@ -200,8 +197,8 @@ func makeChans() []chan int {
for i := range cs { for i := range cs {
pin.Pin(reflect.ValueOf(cs[i]).UnsafePointer()) pin.Pin(reflect.ValueOf(cs[i]).UnsafePointer())
} }
sort.Slice(cs, func(i, j int) bool { slices.SortFunc(cs, func(a, b chan int) int {
return uintptr(reflect.ValueOf(cs[i]).UnsafePointer()) < uintptr(reflect.ValueOf(cs[j]).UnsafePointer()) return cmp.Compare(reflect.ValueOf(a).Pointer(), reflect.ValueOf(b).Pointer())
}) })
return cs return cs
} }

View file

@ -29,7 +29,6 @@ const (
// indirect returns the value, after dereferencing as many times // indirect returns the value, after dereferencing as many times
// as necessary to reach the base type (or nil). // as necessary to reach the base type (or nil).
// Signature modified by Hugo. TODO(bep) script this.
func doIndirect(a any) any { func doIndirect(a any) any {
if a == nil { if a == nil {
return nil return nil
@ -46,8 +45,8 @@ func doIndirect(a any) any {
} }
var ( var (
errorType = reflect.TypeOf((*error)(nil)).Elem() errorType = reflect.TypeFor[error]()
fmtStringerType = reflect.TypeOf((*fmt.Stringer)(nil)).Elem() fmtStringerType = reflect.TypeFor[fmt.Stringer]()
) )
// indirectToStringerOrError returns the value, after dereferencing as many times // 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 {{.}} knows that contextual autoescaping happens should be able to look at a {{.}}
and correctly infer what sanitization happens." and correctly infer what sanitization happens."
As a consequence of the Least Surprise Property, template actions within an Previously, ECMAScript 6 template literal were disabled by default, and could be
ECMAScript 6 template literal are disabled by default. enabled with the GODEBUG=jstmpllitinterp=1 environment variable. Template
Handling string interpolation within these literals is rather complex resulting literals are now supported by default, and setting jstmpllitinterp has no
in no clear safe way to support it. effect.
To re-enable template actions within ECMAScript 6 template literals, use the
GODEBUG=jstmpllitinterp=1 environment variable.
*/ */
package template package template

View file

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

View file

@ -273,8 +273,8 @@ type execTest struct {
// of the max int boundary. // of the max int boundary.
// We do it this way so the test doesn't depend on ints being 32 bits. // We do it this way so the test doesn't depend on ints being 32 bits.
var ( var (
bigInt = fmt.Sprintf("0x%x", int(1<<uint(reflect.TypeOf(0).Bits()-1)-1)) bigInt = fmt.Sprintf("0x%x", int(1<<uint(reflect.TypeFor[int]().Bits()-1)-1))
bigUint = fmt.Sprintf("0x%x", uint(1<<uint(reflect.TypeOf(0).Bits()-1))) bigUint = fmt.Sprintf("0x%x", uint(1<<uint(reflect.TypeFor[int]().Bits()-1)))
) )
var execTests = []execTest{ 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 $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 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 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.
{"range []int", "{{range .SI}}-{{.}}-{{end}}", "-3--4--5-", tVal, true}, {"range []int", "{{range .SI}}-{{.}}-{{end}}", "-3--4--5-", tVal, true},

View file

@ -125,7 +125,7 @@ var regexpPrecederKeywords = map[string]bool{
"void": true, "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 // indirectToJSONMarshaler returns the value, after dereferencing as many times
// as necessary to reach the base type (or nil) or an implementation of json.Marshal. // 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. // cyclic data. This may be an unacceptable DoS risk.
b, err := json.Marshal(a) b, err := json.Marshal(a)
if err != nil { 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, // 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 content of the error message is not guaranteed. Since we insert
// the error into the template, as part of a comment, we attempt to // the error into the template, as part of a comment, we attempt to
@ -393,7 +393,6 @@ var jsStrNormReplacementTable = []string{
'<': `\u003c`, '<': `\u003c`,
'>': `\u003e`, '>': `\u003e`,
} }
var jsRegexpReplacementTable = []string{ var jsRegexpReplacementTable = []string{
0: `\u0000`, 0: `\u0000`,
'\t': `\t`, '\t': `\t`,

View file

@ -179,7 +179,7 @@ func (t *Template) DefinedTemplates() string {
// definition of t itself. // definition of t itself.
// //
// Templates can be redefined in successive calls to Parse, // 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 // A template definition with a body containing only white space and comments
// is considered empty and will not replace an existing template's body. // is considered empty and will not replace an existing template's body.
// This allows using Parse to add new named template definitions without // 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 // Clone returns a duplicate of the template, including all associated
// templates. The actual representation is not copied, but the name space of // 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 // 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 // common templates and use them with variant definitions for other templates
// by adding the variants after the clone is made. // 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 // 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 // definitions will inherit the settings. An empty delimiter stands for the
// corresponding default: {{ or }}. // corresponding default: {{ or }}.
// The return value is the template, so calls can be chained. // 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] 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 // and panics if the error is non-nil. It is intended for use in variable initializations
// such as // such as
// //
@ -371,10 +371,10 @@ func Must(t *Template, err error) *Template {
return t 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 // 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. // (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, // When parsing multiple files with the same name in different directories,
// the last one mentioned will be the one that results. // 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 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 // 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. // 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 // The returned template will have the (base) name and (parsed) contents of the
// first file matched by the pattern. ParseGlob is equivalent to calling // 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, // When parsing multiple files with the same name in different directories,
// the last one mentioned will be the one that results. // 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) 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. // instead of the host operating system's file system.
// It accepts a list of glob patterns. // It accepts a list of glob patterns.
// (Note that most file names serve as glob patterns matching only themselves.) // (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) 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. // instead of the host operating system's file system.
// It accepts a list of glob patterns. // It accepts a list of glob patterns.
// (Note that most file names serve as glob patterns matching only themselves.) // (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 // If "</script" appears in a regex literal, the '/' should not
// close the regex literal, and it will later be escaped to // close the regex literal, and it will later be escaped to
// "\x3C/script" in escapeText. // "\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++ i++
} else if !inCharset { } else if !inCharset {
c.state, c.jsCtx = stateJS, jsCtxDivOp 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. // If runtime.GOROOT() is non-empty, assume that it is valid.
// //
// (It might not be: for example, the user may have explicitly set GOROOT // (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 // to the wrong directory. But this case is
// and hasn't moved the tree to GOROOT_FINAL yet. But those cases are
// rare, and if that happens the user can fix what they broke.) // rare, and if that happens the user can fix what they broke.)
return return
} }
// runtime.GOROOT doesn't know where GOROOT is (perhaps because the test // runtime.GOROOT doesn't know where GOROOT is (perhaps because the test
// binary was built with -trimpath, or perhaps because GOROOT_FINAL was set // binary was built with -trimpath).
// without GOROOT and the tree hasn't been moved there yet).
// //
// Since this is internal/testenv, we can cheat and assume that the caller // 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' // 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 // MustHaveBuildMode reports whether the current system can build programs in
// the given build mode. // the given build mode.
// If not, MustHaveBuildMode calls t.Skip with an explanation. // If not, MustHaveBuildMode calls t.Skip with an explanation.
// Modified by Hugo (not needed) // Modified by Hugo (not needed)
func MustHaveBuildMode(t testing.TB, buildmode string) { func MustHaveBuildMode(t testing.TB, buildmode string) {
return
} }
// HasSymlink reports whether the current system can use os.Symlink. // 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 { func SyscallIsNotSupported(err error) bool {
return syscallIsNotSupported(err) 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 is executed; otherwise, dot is set to the value of the pipeline
and T1 is executed. 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 Arguments
An argument is a simple value, denoted by one of the following. An argument is a simple value, denoted by one of the following.

View file

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

View file

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

View file

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

View file

@ -2,6 +2,9 @@
// Use of this source code is governed by a BSD-style // Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
//go:build !windows
// +build !windows
package template package template
import ( import (
@ -75,12 +78,15 @@ type T struct {
PSI *[]int PSI *[]int
NIL *int NIL *int
// Function (not method) // Function (not method)
BinaryFunc func(string, string) string BinaryFunc func(string, string) string
VariadicFunc func(...string) string VariadicFunc func(...string) string
VariadicFuncInt func(int, ...string) string VariadicFuncInt func(int, ...string) string
NilOKFunc func(*int) bool NilOKFunc func(*int) bool
ErrFunc func() (string, error) ErrFunc func() (string, error)
PanicFunc func() string PanicFunc func() string
TooFewReturnCountFunc func()
TooManyReturnCountFunc func() (string, error, int)
InvalidReturnTypeFunc func() (string, bool)
// Template to test evaluation of templates. // Template to test evaluation of templates.
Tmpl *Template Tmpl *Template
// Unexported field; cannot be accessed by template. // Unexported field; cannot be accessed by template.
@ -168,6 +174,9 @@ var tVal = &T{
NilOKFunc: func(s *int) bool { return s == nil }, NilOKFunc: func(s *int) bool { return s == nil },
ErrFunc: func() (string, error) { return "bla", nil }, ErrFunc: func() (string, error) { return "bla", nil },
PanicFunc: func() string { panic("test panic") }, 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 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. // of the max int boundary.
// We do it this way so the test doesn't depend on ints being 32 bits. // We do it this way so the test doesn't depend on ints being 32 bits.
var ( var (
bigInt = fmt.Sprintf("0x%x", int(1<<uint(reflect.TypeOf(0).Bits()-1)-1)) bigInt = fmt.Sprintf("0x%x", int(1<<uint(reflect.TypeFor[int]().Bits()-1)-1))
bigUint = fmt.Sprintf("0x%x", uint(1<<uint(reflect.TypeOf(0).Bits()-1))) bigUint = fmt.Sprintf("0x%x", uint(1<<uint(reflect.TypeFor[int]().Bits()-1)))
) )
var execTests = []execTest{ 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 $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 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 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.
{"range []int", "{{range .SI}}-{{.}}-{{end}}", "-3--4--5-", tVal, true}, {"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. // Issue 31810. Check that a parenthesized first argument behaves properly.
func TestIssue31810(t *testing.T) { func TestIssue31810(t *testing.T) {
// A simple value with no arguments is fine. // 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 // return value evaluates to non-nil during execution, execution terminates and
// Execute returns that error. // 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. // unwrap them.
// //
// When template execution invokes a function with an argument list, that list // When template execution invokes a function with an argument list, that list
// must be assignable to the function's parameter types. Functions meant to // 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 // 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 // of type [reflect.Value]. Similarly, functions meant to return a result of arbitrary
// type can return interface{} or reflect.Value. // type can return interface{} or [reflect.Value].
type FuncMap map[string]any type FuncMap map[string]any
// builtins returns the FuncMap. // builtins returns the FuncMap.
@ -39,7 +39,7 @@ type FuncMap map[string]any
func builtins() FuncMap { func builtins() FuncMap {
return FuncMap{ return FuncMap{
"and": and, "and": and,
"call": call, "call": emptyCall,
"html": HTMLEscaper, "html": HTMLEscaper,
"index": index, "index": index,
"slice": slice, "slice": slice,
@ -93,8 +93,8 @@ func addValueFuncs(out map[string]reflect.Value, in FuncMap) {
if v.Kind() != reflect.Func { if v.Kind() != reflect.Func {
panic("value for " + name + " not a function") panic("value for " + name + " not a function")
} }
if !goodFunc(v.Type()) { if err := goodFunc(name, v.Type()); err != nil {
panic(fmt.Errorf("can't install method/function %q with %d results", name, v.Type().NumOut())) panic(err)
} }
out[name] = v out[name] = v
} }
@ -109,15 +109,18 @@ func addFuncs(out, in FuncMap) {
} }
// goodFunc reports whether the function or method has the right result signature. // 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. // We allow functions with 1 result or 2 results where the second is an error.
switch { switch numOut := typ.NumOut(); {
case typ.NumOut() == 1: case numOut == 1:
return true return nil
case typ.NumOut() == 2 && typ.Out(1) == errorType: case numOut == 2 && typ.Out(1) == errorType:
return true 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. // goodName reports whether the function name is a valid identifier.
@ -309,30 +312,35 @@ func length(item reflect.Value) (int, error) {
// Function invocation // 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. // 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. // 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) fn = indirectInterface(fn)
if !fn.IsValid() { if !fn.IsValid() {
return reflect.Value{}, fmt.Errorf("call of nil") return reflect.Value{}, fmt.Errorf("call of nil")
} }
typ := fn.Type() typ := fn.Type()
if typ.Kind() != reflect.Func { 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() numIn := typ.NumIn()
var dddType reflect.Type var dddType reflect.Type
if typ.IsVariadic() { if typ.IsVariadic() {
if len(args) < numIn-1 { 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() dddType = typ.In(numIn - 1).Elem()
} else { } else {
if len(args) != numIn { 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)) argv := make([]reflect.Value, len(args))

View file

@ -16,7 +16,7 @@ import (
// Functions and methods to parse templates. // 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 // and panics if the error is non-nil. It is intended for use in variable
// initializations such as // initializations such as
// //
@ -28,7 +28,7 @@ func Must(t *Template, err error) *Template {
return t 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 // 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. // 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.
@ -45,9 +45,9 @@ func ParseFiles(filenames ...string) (*Template, error) {
// t. If an error occurs, parsing stops and the returned template is nil; // t. If an error occurs, parsing stops and the returned template is nil;
// otherwise it is t. There must be at least one file. // otherwise it is t. There must be at least one file.
// Since the templates created by ParseFiles are named by the base // Since the templates created by ParseFiles are named by the base
// names of the argument files, t should usually have the name of one // (see [filepath.Base]) names of the argument files, t should usually have the
// of the (base) names of the files. If it does not, depending on t's // name of one of the (base) names of the files. If it does not, depending on
// contents before calling ParseFiles, t.Execute may fail. In that // t's contents before calling ParseFiles, t.Execute may fail. In that
// case use t.ExecuteTemplate to execute a valid template. // case use t.ExecuteTemplate to execute a valid template.
// //
// When parsing multiple files with the same name in different directories, // 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 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 // 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. // 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 // The returned template will have the [filepath.Base] name and (parsed)
// first file matched by the pattern. ParseGlob is equivalent to calling // contents of the first file matched by the pattern. ParseGlob is equivalent to
// ParseFiles with the list of files matched by the pattern. // calling [ParseFiles] with the list of files matched by the pattern.
// //
// When parsing multiple files with the same name in different directories, // When parsing multiple files with the same name in different directories,
// the last one mentioned will be the one that results. // 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 // ParseGlob parses the template definitions in the files identified by the
// pattern and associates the resulting templates with t. The files are matched // 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 // 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 // least one file. ParseGlob is equivalent to calling [Template.ParseFiles] with
// list of files matched by the pattern. // the list of files matched by the pattern.
// //
// When parsing multiple files with the same name in different directories, // When parsing multiple files with the same name in different directories,
// the last one mentioned will be the one that results. // 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...) 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. // 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.) // (Note that most file names serve as glob patterns matching only themselves.)
func ParseFS(fsys fs.FS, patterns ...string) (*Template, error) { func ParseFS(fsys fs.FS, patterns ...string) (*Template, error) {
return parseFS(nil, fsys, patterns) 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. // 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.) // (Note that most file names serve as glob patterns matching only themselves.)
func (t *Template) ParseFS(fsys fs.FS, patterns ...string) (*Template, error) { func (t *Template) ParseFS(fsys fs.FS, patterns ...string) (*Template, error) {
t.init() 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() { } else if numIn != typ.NumIn() {
s.errorf("wrong number of args for %s: want %d got %d", name, typ.NumIn(), numIn) s.errorf("wrong number of args for %s: want %d got %d", name, typ.NumIn(), numIn)
} }
if !goodFunc(typ) { if err := goodFunc(name, typ); err != nil {
// TODO: This could still be a confusing error; maybe goodFunc should provide info. s.errorf("%v", err)
s.errorf("can't call method/function %q with %d results", name, typ.NumOut())
} }
unwrap := func(v reflect.Value) reflect.Value { 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) 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 // Added for Hugo
for i := 0; i < len(first); i++ { for i := 0; i < len(first); i++ {
argv[i] = s.validateType(first[i], typ.In(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 // Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
//go:build go1.13
// +build go1.13
package template_test package template_test
import ( import (
"bytes" "bytes"
"github.com/gohugoio/hugo/tpl/internal/go_templates/testenv"
"os" "os"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
"testing" "testing"
"github.com/gohugoio/hugo/tpl/internal/go_templates/testenv"
) )
// Issue 36021: verify that text/template doesn't prevent the linker from removing // Issue 36021: verify that text/template doesn't prevent the linker from removing
@ -44,7 +42,7 @@ func main() {
` `
td := t.TempDir() 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) t.Fatal(err)
} }
cmd := exec.Command(testenv.GoToolPath(t), "build", "-o", "x.exe", "x.go") cmd := exec.Command(testenv.GoToolPath(t), "build", "-o", "x.exe", "x.go")

View file

@ -217,7 +217,11 @@ func (p *PipeNode) writeTo(sb *strings.Builder) {
} }
v.writeTo(sb) v.writeTo(sb)
} }
sb.WriteString(" := ") if p.IsAssign {
sb.WriteString(" = ")
} else {
sb.WriteString(" := ")
}
} }
for i, c := range p.Cmds { for i, c := range p.Cmds {
if i > 0 { if i > 0 {
@ -346,12 +350,12 @@ type IdentifierNode struct {
Ident string // The identifier's name. 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 { func NewIdentifier(ident string) *IdentifierNode {
return &IdentifierNode{NodeType: NodeIdentifier, Ident: ident} 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. // Chained for convenience.
// TODO: fix one day? // TODO: fix one day?
func (i *IdentifierNode) SetPos(pos Pos) *IdentifierNode { func (i *IdentifierNode) SetPos(pos Pos) *IdentifierNode {
@ -359,7 +363,7 @@ func (i *IdentifierNode) SetPos(pos Pos) *IdentifierNode {
return i 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. // Chained for convenience.
// TODO: fix one day? // TODO: fix one day?
func (i *IdentifierNode) SetTree(t *Tree) *IdentifierNode { func (i *IdentifierNode) SetTree(t *Tree) *IdentifierNode {

View file

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

View file

@ -33,9 +33,9 @@ var numberTests = []numberTest{
{"7_3", true, true, true, false, 73, 73, 73, 0}, {"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},
{"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}, {"073", true, true, true, false, 0o73, 0o73, 0o73, 0},
{"0o73", true, true, true, false, 073, 073, 073, 0}, {"0o73", true, true, true, false, 0o73, 0o73, 0o73, 0},
{"0O73", true, true, true, false, 073, 073, 073, 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},
{"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}, {"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}, {"-12+0i", true, false, true, true, -12, 0, -12, -12},
{"13+0i", true, true, true, true, 13, 13, 13, 13}, {"13+0i", true, true, true, true, 13, 13, 13, 13},
// funny bases // 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}, {"-0x0", true, true, true, false, 0, 0, 0, 0},
{"0xdeadbeef", true, true, true, false, 0xdeadbeef, 0xdeadbeef, 0xdeadbeef, 0}, {"0xdeadbeef", true, true, true, false, 0xdeadbeef, 0xdeadbeef, 0xdeadbeef, 0},
// character constants // character constants
@ -176,74 +176,150 @@ const (
) )
var parseTests = []parseTest{ var parseTests = []parseTest{
{"empty", "", noError, {
``}, "empty", "", noError,
{"comment", "{{/*\n\n\n*/}}", noError, ``,
``}, },
{"spaces", " \t\n", noError, {
`" \t\n"`}, "comment", "{{/*\n\n\n*/}}", noError,
{"text", "some text", noError, ``,
`"some text"`}, },
{"emptyAction", "{{}}", hasError, {
`{{}}`}, "spaces", " \t\n", noError,
{"field", "{{.X}}", noError, `" \t\n"`,
`{{.X}}`}, },
{"simple command", "{{printf}}", noError, {
`{{printf}}`}, "text", "some text", noError,
{"$ invocation", "{{$}}", noError, `"some text"`,
"{{$}}"}, },
{"variable invocation", "{{with $x := 3}}{{$x 23}}{{end}}", noError, {
"{{with $x := 3}}{{$x 23}}{{end}}"}, "emptyAction", "{{}}", hasError,
{"variable with fields", "{{$.I}}", noError, `{{}}`,
"{{$.I}}"}, },
{"multi-word command", "{{printf `%d` 23}}", noError, {
"{{printf `%d` 23}}"}, "field", "{{.X}}", noError,
{"pipeline", "{{.X|.Y}}", noError, `{{.X}}`,
`{{.X | .Y}}`}, },
{"pipeline with decl", "{{$x := .X|.Y}}", noError, {
`{{$x := .X | .Y}}`}, "simple command", "{{printf}}", noError,
{"nested pipeline", "{{.X (.Y .Z) (.A | .B .C) (.E)}}", noError, `{{printf}}`,
`{{.X (.Y .Z) (.A | .B .C) (.E)}}`}, },
{"field applied to parentheses", "{{(.Y .Z).Field}}", noError, {
`{{(.Y .Z).Field}}`}, "$ invocation", "{{$}}", noError,
{"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}}`}, "variable invocation", "{{with $x := 3}}{{$x 23}}{{end}}", noError,
{"if with else if", "{{if .X}}true{{else if .Y}}false{{end}}", noError, "{{with $x := 3}}{{$x 23}}{{end}}",
`{{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}}"+"`}, "variable with fields", "{{$.I}}", noError,
{"simple range", "{{range .X}}hello{{end}}", noError, "{{$.I}}",
`{{range .X}}"hello"{{end}}`}, },
{"chained field range", "{{range .X.Y.Z}}hello{{end}}", noError, {
`{{range .X.Y.Z}}"hello"{{end}}`}, "multi-word command", "{{printf `%d` 23}}", noError,
{"nested range", "{{range .X}}hello{{range .Y}}goodbye{{end}}{{end}}", noError, "{{printf `%d` 23}}",
`{{range .X}}"hello"{{range .Y}}"goodbye"{{end}}{{end}}`}, },
{"range with else", "{{range .X}}true{{else}}false{{end}}", noError, {
`{{range .X}}"true"{{else}}"false"{{end}}`}, "pipeline", "{{.X|.Y}}", noError,
{"range over pipeline", "{{range .X|.M}}true{{else}}false{{end}}", noError, `{{.X | .Y}}`,
`{{range .X | .M}}"true"{{else}}"false"{{end}}`}, },
{"range []int", "{{range .SI}}{{.}}{{end}}", noError, {
`{{range .SI}}{{.}}{{end}}`}, "pipeline with decl", "{{$x := .X|.Y}}", noError,
{"range 1 var", "{{range $x := .SI}}{{.}}{{end}}", noError, `{{$x := .X | .Y}}`,
`{{range $x := .SI}}{{.}}{{end}}`}, },
{"range 2 vars", "{{range $x, $y := .SI}}{{.}}{{end}}", noError, {
`{{range $x, $y := .SI}}{{.}}{{end}}`}, "nested pipeline", "{{.X (.Y .Z) (.A | .B .C) (.E)}}", noError,
{"range with break", "{{range .SI}}{{.}}{{break}}{{end}}", noError, `{{.X (.Y .Z) (.A | .B .C) (.E)}}`,
`{{range .SI}}{{.}}{{break}}{{end}}`}, },
{"range with continue", "{{range .SI}}{{.}}{{continue}}{{end}}", noError, {
`{{range .SI}}{{.}}{{continue}}{{end}}`}, "field applied to parentheses", "{{(.Y .Z).Field}}", noError,
{"constants", "{{range .SI 1 -3.2i true false 'a' nil}}{{end}}", noError, `{{(.Y .Z).Field}}`,
`{{range .SI 1 -3.2i true false 'a' nil}}{{end}}`}, },
{"template", "{{template `x`}}", noError, {
`{{template "x"}}`}, "simple if", "{{if .X}}hello{{end}}", noError,
{"template with arg", "{{template `x` .Y}}", noError, `{{if .X}}"hello"{{end}}`,
`{{template "x" .Y}}`}, },
{"with", "{{with .X}}hello{{end}}", noError, {
`{{with .X}}"hello"{{end}}`}, "if with else", "{{if .X}}true{{else}}false{{end}}", noError,
{"with with else", "{{with .X}}hello{{else}}goodbye{{end}}", noError, `{{if .X}}"true"{{else}}"false"{{end}}`,
`{{with .X}}"hello"{{else}}"goodbye"{{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. // Trimming spaces.
{"trim left", "x \r\n\t{{- 3}}", noError, `"x"{{3}}`}, {"trim left", "x \r\n\t{{- 3}}", noError, `"x"{{3}}`},
{"trim right", "{{3 -}}\n\n\ty", noError, `{{3}}"y"`}, {"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 left", "x \r\n\t{{- /* hi */}}", noError, `"x"`},
{"comment trim right", "{{/* hi */ -}}\n\n\ty", noError, `"y"`}, {"comment trim right", "{{/* hi */ -}}\n\n\ty", noError, `"y"`},
{"comment trim left and right", "x \r\n\t{{- /* */ -}}\n\n\ty", noError, `"x""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 assignment", "{{ $x \n := \n 1 \n }}", noError, "{{$x := 1}}"},
{"newline in empty action", "{{\n}}", hasError, "{{\n}}"}, {"newline in empty action", "{{\n}}", hasError, "{{\n}}"},
{"newline in pipeline", "{{\n\"x\"\n|\nprintf\n}}", noError, `{{"x" | printf}}`}, {"newline in pipeline", "{{\n\"x\"\n|\nprintf\n}}", noError, `{{"x" | printf}}`},
{"newline in comment", "{{/*\nhello\n*/}}", noError, ""}, {"newline in comment", "{{/*\nhello\n*/}}", noError, ""},
{"newline in comment", "{{-\n/*\nhello\n*/\n-}}", noError, ""}, {"newline in comment", "{{-\n/*\nhello\n*/\n-}}", noError, ""},
{"spaces around continue", "{{range .SI}}{{.}}{{ continue }}{{end}}", noError, {
`{{range .SI}}{{.}}{{continue}}{{end}}`}, "spaces around continue", "{{range .SI}}{{.}}{{ continue }}{{end}}", noError,
{"spaces around break", "{{range .SI}}{{.}}{{ break }}{{end}}", noError, `{{range .SI}}{{.}}{{continue}}{{end}}`,
`{{range .SI}}{{.}}{{break}}{{end}}`}, },
{
"spaces around break", "{{range .SI}}{{.}}{{ break }}{{end}}", noError,
`{{range .SI}}{{.}}{{break}}{{end}}`,
},
// Errors. // Errors.
{"unclosed action", "hello{{range", hasError, ""}, {"unclosed action", "hello{{range", hasError, ""},
@ -302,6 +384,9 @@ var parseTests = []parseTest{
{"bug1a", "{{$x:=.}}{{$x!2}}", hasError, ""}, // ! is just illegal here. {"bug1a", "{{$x:=.}}{{$x!2}}", hasError, ""}, // ! is just illegal here.
{"bug1b", "{{$x:=.}}{{$x+2}}", hasError, ""}, // $x+2 should not parse as ($x) (+2). {"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. {"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 following a literal value
{"dot after integer", "{{1.E}}", hasError, ""}, {"dot after integer", "{{1.E}}", hasError, ""},
{"dot after float", "{{0.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 // 'break' is a defined function, don't treat it as a keyword: it should
// accept an argument successfully. // accept an argument successfully.
var funcsWithKeywordFunc = map[string]any{ funcsWithKeywordFunc := map[string]any{
"break": func(in any) any { return in }, "break": func(in any) any { return in },
} }
tmpl, err := New("").Parse(inp, "", "", make(map[string]*Tree), funcsWithKeywordFunc) 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. // All failures, and the result is a string that must appear in the error message.
var errorTests = []parseTest{ var errorTests = []parseTest{
// Check line numbers are accurate. // Check line numbers are accurate.
{"unclosed1", {
"unclosed1",
"line1\n{{", "line1\n{{",
hasError, `unclosed1:2: unclosed action`}, hasError, `unclosed1:2: unclosed action`,
{"unclosed2", },
{
"unclosed2",
"line1\n{{define `x`}}line2\n{{", "line1\n{{define `x`}}line2\n{{",
hasError, `unclosed2:3: unclosed action`}, hasError, `unclosed2:3: unclosed action`,
{"unclosed3", },
{
"unclosed3",
"line1\n{{\"x\"\n\"y\"\n", "line1\n{{\"x\"\n\"y\"\n",
hasError, `unclosed3:4: unclosed action started at unclosed3:2`}, hasError, `unclosed3:4: unclosed action started at unclosed3:2`,
{"unclosed4", },
{
"unclosed4",
"{{\n\n\n\n\n", "{{\n\n\n\n\n",
hasError, `unclosed4:6: unclosed action started at unclosed4:1`}, hasError, `unclosed4:6: unclosed action started at unclosed4:1`,
{"var1", },
{
"var1",
"line1\n{{\nx\n}}", "line1\n{{\nx\n}}",
hasError, `var1:3: function "x" not defined`}, hasError, `var1:3: function "x" not defined`,
},
// Specific errors. // Specific errors.
{"function", {
"function",
"{{foo}}", "{{foo}}",
hasError, `function "foo" not defined`}, hasError, `function "foo" not defined`,
{"comment1", },
{
"comment1",
"{{/*}}", "{{/*}}",
hasError, `comment1:1: unclosed comment`}, hasError, `comment1:1: unclosed comment`,
{"comment2", },
{
"comment2",
"{{/*\nhello\n}}", "{{/*\nhello\n}}",
hasError, `comment2:1: unclosed comment`}, hasError, `comment2:1: unclosed comment`,
{"lparen", },
{
"lparen",
"{{.X (1 2 3}}", "{{.X (1 2 3}}",
hasError, `unclosed left paren`}, hasError, `unclosed left paren`,
{"rparen", },
{
"rparen",
"{{.X 1 2 3 ) }}", "{{.X 1 2 3 ) }}",
hasError, "unexpected right paren"}, hasError, "unexpected right paren",
{"rparen2", },
{
"rparen2",
"{{(.X 1 2 3", "{{(.X 1 2 3",
hasError, `unclosed action`}, hasError, `unclosed action`,
{"space", },
{
"space",
"{{`x`3}}", "{{`x`3}}",
hasError, `in operand`}, hasError, `in operand`,
{"idchar", },
{
"idchar",
"{{a#}}", "{{a#}}",
hasError, `'#'`}, hasError, `'#'`,
{"charconst", },
{
"charconst",
"{{'a}}", "{{'a}}",
hasError, `unterminated character constant`}, hasError, `unterminated character constant`,
{"stringconst", },
{
"stringconst",
`{{"a}}`, `{{"a}}`,
hasError, `unterminated quoted string`}, hasError, `unterminated quoted string`,
{"rawstringconst", },
{
"rawstringconst",
"{{`a}}", "{{`a}}",
hasError, `unterminated raw quoted string`}, hasError, `unterminated raw quoted string`,
{"number", },
{
"number",
"{{0xi}}", "{{0xi}}",
hasError, `number syntax`}, hasError, `number syntax`,
{"multidefine", },
{
"multidefine",
"{{define `a`}}a{{end}}{{define `a`}}b{{end}}", "{{define `a`}}a{{end}}{{define `a`}}b{{end}}",
hasError, `multiple definition of template`}, hasError, `multiple definition of template`,
{"eof", },
{
"eof",
"{{range .X}}", "{{range .X}}",
hasError, `unexpected EOF`}, hasError, `unexpected EOF`,
{"variable", },
{
"variable",
// Declare $x so it's defined, to avoid that error, and then check we don't parse a declaration. // 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}}", "{{$x := 23}}{{with $x.y := 3}}{{$x 23}}{{end}}",
hasError, `unexpected ":="`}, hasError, `unexpected ":="`,
{"multidecl", },
{
"multidecl",
"{{$a,$b,$c := 23}}", "{{$a,$b,$c := 23}}",
hasError, `too many declarations`}, hasError, `too many declarations`,
{"undefvar", },
{
"undefvar",
"{{$a}}", "{{$a}}",
hasError, `undefined variable`}, hasError, `undefined variable`,
{"wrongdot", },
{
"wrongdot",
"{{true.any}}", "{{true.any}}",
hasError, `unexpected . after term`}, hasError, `unexpected . after term`,
{"wrongpipeline", },
{
"wrongpipeline",
"{{12|false}}", "{{12|false}}",
hasError, `non executable command in pipeline`}, hasError, `non executable command in pipeline`,
{"emptypipeline", },
{
"emptypipeline",
`{{ ( ) }}`, `{{ ( ) }}`,
hasError, `missing value for parenthesized pipeline`}, hasError, `missing value for parenthesized pipeline`,
{"multilinerawstring", },
{
"multilinerawstring",
"{{ $v := `\n` }} {{", "{{ $v := `\n` }} {{",
hasError, `multilinerawstring:2: unclosed action`}, hasError, `multilinerawstring:2: unclosed action`,
{"rangeundefvar", },
{
"rangeundefvar",
"{{range $k}}{{end}}", "{{range $k}}{{end}}",
hasError, `undefined variable`}, hasError, `undefined variable`,
{"rangeundefvars", },
{
"rangeundefvars",
"{{range $k, $v}}{{end}}", "{{range $k, $v}}{{end}}",
hasError, `undefined variable`}, hasError, `undefined variable`,
{"rangemissingvalue1", },
{
"rangemissingvalue1",
"{{range $k,}}{{end}}", "{{range $k,}}{{end}}",
hasError, `missing value for range`}, hasError, `missing value for range`,
{"rangemissingvalue2", },
{
"rangemissingvalue2",
"{{range $k, $v := }}{{end}}", "{{range $k, $v := }}{{end}}",
hasError, `missing value for range`}, hasError, `missing value for range`,
{"rangenotvariable1", },
{
"rangenotvariable1",
"{{range $k, .}}{{end}}", "{{range $k, .}}{{end}}",
hasError, `range can only initialize variables`}, hasError, `range can only initialize variables`,
{"rangenotvariable2", },
{
"rangenotvariable2",
"{{range $k, 123 := .}}{{end}}", "{{range $k, 123 := .}}{{end}}",
hasError, `range can only initialize variables`}, hasError, `range can only initialize variables`,
},
} }
func TestErrors(t *testing.T) { 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 // 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. // as unexported by all other clients.
type Template struct { type Template struct {
name string name string
@ -79,7 +79,7 @@ func (t *Template) init() {
// Clone returns a duplicate of the template, including all associated // Clone returns a duplicate of the template, including all associated
// templates. The actual representation is not copied, but the name space of // 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 // 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 // common templates and use them with variant definitions for other templates
// by adding the variants after the clone is made. // 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 // 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 // definitions will inherit the settings. An empty delimiter stands for the
// corresponding default: {{ or }}. // corresponding default: {{ or }}.
// The return value is the template, so calls can be chained. // 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 // Issue 10495
func TestCommentsBeforeBlockDefinition(t *testing.T) { func TestCommentsBeforeBlockDefinition(t *testing.T) {
t.Parallel() t.Parallel()