diff options
author | Bjørn Erik Pedersen <[email protected]> | 2024-08-14 11:34:21 +0200 |
---|---|---|
committer | Bjørn Erik Pedersen <[email protected]> | 2024-08-15 10:18:19 +0200 |
commit | 2168c5b125020a1841450730edc1b0ed2141d239 (patch) | |
tree | 7edfa01d9f1e0c921f08a468f9e5202e5569c529 /tpl | |
parent | b3ad58fa04fb2447d1a788d1fe61b2ed581a403e (diff) | |
download | hugo-2168c5b125020a1841450730edc1b0ed2141d239.tar.gz hugo-2168c5b125020a1841450730edc1b0ed2141d239.zip |
Upgrade to Go 1.23
Fixes #12763
Diffstat (limited to 'tpl')
25 files changed, 604 insertions, 383 deletions
diff --git a/tpl/internal/go_templates/cfg/cfg.go b/tpl/internal/go_templates/cfg/cfg.go index 2af0ec707..08d210b79 100644 --- a/tpl/internal/go_templates/cfg/cfg.go +++ b/tpl/internal/go_templates/cfg/cfg.go @@ -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 diff --git a/tpl/internal/go_templates/fmtsort/sort.go b/tpl/internal/go_templates/fmtsort/sort.go index 278a89bd7..f51cdc708 100644 --- a/tpl/internal/go_templates/fmtsort/sort.go +++ b/tpl/internal/go_templates/fmtsort/sort.go @@ -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 := &SortedMap{ - Key: key, - Value: value, + sorted = append(sorted, KeyValue{iter.Key(), iter.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 -} diff --git a/tpl/internal/go_templates/fmtsort/sort_test.go b/tpl/internal/go_templates/fmtsort/sort_test.go index e86b4c673..0986dbb6d 100644 --- a/tpl/internal/go_templates/fmtsort/sort_test.go +++ b/tpl/internal/go_templates/fmtsort/sort_test.go @@ -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 } diff --git a/tpl/internal/go_templates/htmltemplate/content.go b/tpl/internal/go_templates/htmltemplate/content.go index 9c61cfac0..d19b1ec12 100644 --- a/tpl/internal/go_templates/htmltemplate/content.go +++ b/tpl/internal/go_templates/htmltemplate/content.go @@ -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 diff --git a/tpl/internal/go_templates/htmltemplate/doc.go b/tpl/internal/go_templates/htmltemplate/doc.go index 6be5e0f84..7442194f7 100644 --- a/tpl/internal/go_templates/htmltemplate/doc.go +++ b/tpl/internal/go_templates/htmltemplate/doc.go @@ -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 diff --git a/tpl/internal/go_templates/htmltemplate/examplefiles_test.go b/tpl/internal/go_templates/htmltemplate/examplefiles_test.go index 43cc3bf01..24b22d984 100644 --- a/tpl/internal/go_templates/htmltemplate/examplefiles_test.go +++ b/tpl/internal/go_templates/htmltemplate/examplefiles_test.go @@ -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 ( diff --git a/tpl/internal/go_templates/htmltemplate/exec_test.go b/tpl/internal/go_templates/htmltemplate/exec_test.go index 428cddc0d..e01813e68 100644 --- a/tpl/internal/go_templates/htmltemplate/exec_test.go +++ b/tpl/internal/go_templates/htmltemplate/exec_test.go @@ -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}, diff --git a/tpl/internal/go_templates/htmltemplate/js.go b/tpl/internal/go_templates/htmltemplate/js.go index cc80d2b64..1b56abeb1 100644 --- a/tpl/internal/go_templates/htmltemplate/js.go +++ b/tpl/internal/go_templates/htmltemplate/js.go @@ -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`, diff --git a/tpl/internal/go_templates/htmltemplate/template.go b/tpl/internal/go_templates/htmltemplate/template.go index d7454f101..4582ddd5f 100644 --- a/tpl/internal/go_templates/htmltemplate/template.go +++ b/tpl/internal/go_templates/htmltemplate/template.go @@ -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.) diff --git a/tpl/internal/go_templates/htmltemplate/transition.go b/tpl/internal/go_templates/htmltemplate/transition.go index d5a05f66d..c430389a3 100644 --- a/tpl/internal/go_templates/htmltemplate/transition.go +++ b/tpl/internal/go_templates/htmltemplate/transition.go @@ -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 diff --git a/tpl/internal/go_templates/testenv/testenv.go b/tpl/internal/go_templates/testenv/testenv.go index 277df2255..e8577d2ea 100644 --- a/tpl/internal/go_templates/testenv/testenv.go +++ b/tpl/internal/go_templates/testenv/testenv.go @@ -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) { +} diff --git a/tpl/internal/go_templates/texttemplate/doc.go b/tpl/internal/go_templates/texttemplate/doc.go index 032784bc3..b3ffaabb1 100644 --- a/tpl/internal/go_templates/texttemplate/doc.go +++ b/tpl/internal/go_templates/texttemplate/doc.go @@ -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. diff --git a/tpl/internal/go_templates/texttemplate/example_test.go b/tpl/internal/go_templates/texttemplate/example_test.go index 295a810b8..975ceea93 100644 --- a/tpl/internal/go_templates/texttemplate/example_test.go +++ b/tpl/internal/go_templates/texttemplate/example_test.go @@ -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}, diff --git a/tpl/internal/go_templates/texttemplate/examplefiles_test.go b/tpl/internal/go_templates/texttemplate/examplefiles_test.go index bc91e87f9..6534ee331 100644 --- a/tpl/internal/go_templates/texttemplate/examplefiles_test.go +++ b/tpl/internal/go_templates/texttemplate/examplefiles_test.go @@ -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 ( diff --git a/tpl/internal/go_templates/texttemplate/exec.go b/tpl/internal/go_templates/texttemplate/exec.go index 73153c764..bd8c82bd7 100644 --- a/tpl/internal/go_templates/texttemplate/exec.go +++ b/tpl/internal/go_templates/texttemplate/exec.go @@ -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. diff --git a/tpl/internal/go_templates/texttemplate/exec_test.go b/tpl/internal/go_templates/texttemplate/exec_test.go index ed75adacb..efbaa9eec 100644 --- a/tpl/internal/go_templates/texttemplate/exec_test.go +++ b/tpl/internal/go_templates/texttemplate/exec_test.go @@ -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 ( @@ -75,12 +78,15 @@ type T struct { PSI *[]int NIL *int // Function (not method) - BinaryFunc func(string, string) string - VariadicFunc func(...string) string - VariadicFuncInt func(int, ...string) string - NilOKFunc func(*int) bool - ErrFunc func() (string, error) - PanicFunc func() string + BinaryFunc func(string, string) string + VariadicFunc func(...string) string + VariadicFuncInt func(int, ...string) string + 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. diff --git a/tpl/internal/go_templates/texttemplate/funcs.go b/tpl/internal/go_templates/texttemplate/funcs.go index a949f896f..7d63cf8b7 100644 --- a/tpl/internal/go_templates/texttemplate/funcs.go +++ b/tpl/internal/go_templates/texttemplate/funcs.go @@ -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)) diff --git a/tpl/internal/go_templates/texttemplate/helper.go b/tpl/internal/go_templates/texttemplate/helper.go index 48af3928b..81b55538e 100644 --- a/tpl/internal/go_templates/texttemplate/helper.go +++ b/tpl/internal/go_templates/texttemplate/helper.go @@ -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() diff --git a/tpl/internal/go_templates/texttemplate/hugo_template.go b/tpl/internal/go_templates/texttemplate/hugo_template.go index 276367a7c..12dbe0412 100644 --- a/tpl/internal/go_templates/texttemplate/hugo_template.go +++ b/tpl/internal/go_templates/texttemplate/hugo_template.go @@ -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)) diff --git a/tpl/internal/go_templates/texttemplate/link_test.go b/tpl/internal/go_templates/texttemplate/link_test.go index 23f6a31fa..63418cd90 100644 --- a/tpl/internal/go_templates/texttemplate/link_test.go +++ b/tpl/internal/go_templates/texttemplate/link_test.go @@ -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") diff --git a/tpl/internal/go_templates/texttemplate/parse/node.go b/tpl/internal/go_templates/texttemplate/parse/node.go index c36688825..a31309874 100644 --- a/tpl/internal/go_templates/texttemplate/parse/node.go +++ b/tpl/internal/go_templates/texttemplate/parse/node.go @@ -217,7 +217,11 @@ func (p *PipeNode) writeTo(sb *strings.Builder) { } v.writeTo(sb) } - sb.WriteString(" := ") + if p.IsAssign { + sb.WriteString(" = ") + } else { + sb.WriteString(" := ") + } } for i, c := range p.Cmds { if i > 0 { @@ -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 { diff --git a/tpl/internal/go_templates/texttemplate/parse/parse.go b/tpl/internal/go_templates/texttemplate/parse/parse.go index d43d5334b..27c84f31e 100644 --- a/tpl/internal/go_templates/texttemplate/parse/parse.go +++ b/tpl/internal/go_templates/texttemplate/parse/parse.go @@ -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,27 +535,30 @@ 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 - // {{if a}}_{{else if 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 { - 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 + // 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}} + // {{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()) + } 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 } @@ -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") diff --git a/tpl/internal/go_templates/texttemplate/parse/parse_test.go b/tpl/internal/go_templates/texttemplate/parse/parse_test.go index 59e0a1741..80e7f53fa 100644 --- a/tpl/internal/go_templates/texttemplate/parse/parse_test.go +++ b/tpl/internal/go_templates/texttemplate/parse/parse_test.go @@ -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) { diff --git a/tpl/internal/go_templates/texttemplate/template.go b/tpl/internal/go_templates/texttemplate/template.go index 1ba72c194..536932a60 100644 --- a/tpl/internal/go_templates/texttemplate/template.go +++ b/tpl/internal/go_templates/texttemplate/template.go @@ -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. diff --git a/tpl/tplimpl/tplimpl_integration_test.go b/tpl/tplimpl/tplimpl_integration_test.go index 41257912d..a8599bbad 100644 --- a/tpl/tplimpl/tplimpl_integration_test.go +++ b/tpl/tplimpl/tplimpl_integration_test.go @@ -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() |