Upgrade to Go 1.24

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

View file

@ -4,7 +4,7 @@ parameters:
defaults: &defaults defaults: &defaults
resource_class: large resource_class: large
docker: docker:
- image: bepsays/ci-hugoreleaser:1.22301.20401 - image: bepsays/ci-hugoreleaser:1.22400.20000
environment: &buildenv environment: &buildenv
GOMODCACHE: /root/project/gomodcache GOMODCACHE: /root/project/gomodcache
version: 2 version: 2
@ -58,7 +58,7 @@ jobs:
environment: environment:
<<: [*buildenv] <<: [*buildenv]
docker: docker:
- image: bepsays/ci-hugoreleaser-linux-arm64:1.22301.20401 - image: bepsays/ci-hugoreleaser-linux-arm64:1.22400.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.22.x, 1.23.x] go-version: [1.23.x, 1.24.x]
os: [ubuntu-latest, windows-latest] # macos disabled for now because of disk space issues. os: [ubuntu-latest, windows-latest] # macos disabled for now because of disk space issues.
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.22.6 go 1.23

View file

@ -16,7 +16,7 @@ import (
) )
func main() { func main() {
// The current is built with 6885bad7dd86880be6929c02085e5c7a67ff2887 go1.23.0 // The current is built with 3901409b5d [release-branch.go1.24] go1.24.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 ...")
@ -216,6 +216,7 @@ func rewrite(filename, rule string) {
} }
func goimports(dir string) { func goimports(dir string) {
// Needs go install golang.org/x/tools/cmd/goimports@latest
cmf, _ := hexec.SafeCommand("goimports", "-w", dir) cmf, _ := hexec.SafeCommand("goimports", "-w", dir)
out, err := cmf.CombinedOutput() out, err := cmf.CombinedOutput()
if err != nil { if err != nil {

View file

@ -37,12 +37,14 @@ const KnownEnv = `
GOARCH GOARCH
GOARM GOARM
GOARM64 GOARM64
GOAUTH
GOBIN GOBIN
GOCACHE GOCACHE
GOCACHEPROG GOCACHEPROG
GOENV GOENV
GOEXE GOEXE
GOEXPERIMENT GOEXPERIMENT
GOFIPS140
GOFLAGS GOFLAGS
GOGCCFLAGS GOGCCFLAGS
GOHOSTARCH GOHOSTARCH

View file

@ -2,8 +2,8 @@
// 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 //go:build go1.13 && !windows
// +build !windows // +build go1.13,!windows
package template package template

View file

@ -2,8 +2,8 @@
// 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 //go:build go1.13 && !windows
// +build !windows // +build go1.13,!windows
package template package template
@ -428,7 +428,7 @@ func TestStringer(t *testing.T) {
if err := tmpl.Execute(b, s); err != nil { if err := tmpl.Execute(b, s); err != nil {
t.Fatal(err) t.Fatal(err)
} }
expect := "string=3" var expect = "string=3"
if b.String() != expect { if b.String() != expect {
t.Errorf("expected %q got %q", expect, b.String()) t.Errorf("expected %q got %q", expect, b.String())
} }

View file

@ -2,8 +2,8 @@
// 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 //go:build go1.13 && !windows
// +build !windows // +build go1.13,!windows
package template package template

View file

@ -9,6 +9,7 @@ import (
"fmt" "fmt"
"html" "html"
"io" "io"
"maps"
"regexp" "regexp"
template "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate" template "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate"
@ -145,7 +146,7 @@ func (e *escaper) escape(c context, n parse.Node) context {
return c return c
case *parse.ContinueNode: case *parse.ContinueNode:
c.n = n c.n = n
e.rangeContext.continues = append(e.rangeContext.breaks, c) e.rangeContext.continues = append(e.rangeContext.continues, c)
return context{state: stateDead} return context{state: stateDead}
case *parse.IfNode: case *parse.IfNode:
return e.escapeBranch(c, &n.BranchNode, "if") return e.escapeBranch(c, &n.BranchNode, "if")
@ -588,22 +589,14 @@ func (e *escaper) escapeListConditionally(c context, n *parse.ListNode, filter f
e1 := makeEscaper(e.ns) e1 := makeEscaper(e.ns)
e1.rangeContext = e.rangeContext e1.rangeContext = e.rangeContext
// Make type inferences available to f. // Make type inferences available to f.
for k, v := range e.output { maps.Copy(e1.output, e.output)
e1.output[k] = v
}
c = e1.escapeList(c, n) c = e1.escapeList(c, n)
ok := filter != nil && filter(&e1, c) ok := filter != nil && filter(&e1, c)
if ok { if ok {
// Copy inferences and edits from e1 back into e. // Copy inferences and edits from e1 back into e.
for k, v := range e1.output { maps.Copy(e.output, e1.output)
e.output[k] = v maps.Copy(e.derived, e1.derived)
} maps.Copy(e.called, e1.called)
for k, v := range e1.derived {
e.derived[k] = v
}
for k, v := range e1.called {
e.called[k] = v
}
for k, v := range e1.actionNodeEdits { for k, v := range e1.actionNodeEdits {
e.editActionNode(k, v) e.editActionNode(k, v)
} }

View file

@ -2,8 +2,8 @@
// 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 //go:build go1.13 && !windows
// +build !windows // +build go1.13,!windows
package template package template
@ -944,6 +944,7 @@ func TestEscapeSet(t *testing.T) {
t.Errorf("want\n\t%q\ngot\n\t%q", test.want, got) t.Errorf("want\n\t%q\ngot\n\t%q", test.want, got)
} }
} }
} }
func TestErrors(t *testing.T) { func TestErrors(t *testing.T) {
@ -1064,6 +1065,10 @@ func TestErrors(t *testing.T) {
"{{range .Items}}<a{{if .X}}{{continue}}{{end}}>{{end}}", "{{range .Items}}<a{{if .X}}{{continue}}{{end}}>{{end}}",
"z:1:29: at range loop continue: {{range}} branches end in different contexts", "z:1:29: at range loop continue: {{range}} branches end in different contexts",
}, },
{
"{{range .Items}}{{if .X}}{{break}}{{end}}<a{{if .Y}}{{continue}}{{end}}>{{if .Z}}{{continue}}{{end}}{{end}}",
"z:1:54: at range loop continue: {{range}} branches end in different contexts",
},
{ {
"<a b=1 c={{.H}}", "<a b=1 c={{.H}}",
"z: ends in a non-text context: {stateAttr delimSpaceOrTagEnd", "z: ends in a non-text context: {stateAttr delimSpaceOrTagEnd",
@ -1193,6 +1198,7 @@ func TestErrors(t *testing.T) {
// Check that we get the same error if we call Execute again. // Check that we get the same error if we call Execute again.
if err := tmpl.Execute(buf, nil); err == nil || err.Error() != got { if err := tmpl.Execute(buf, nil); err == nil || err.Error() != got {
t.Errorf("input=%q: unexpected error on second call %q", test.input, err) t.Errorf("input=%q: unexpected error on second call %q", test.input, err)
} }
} }
} }

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 go1.13
// +build go1.13
package template_test package template_test
import ( import (
@ -80,6 +83,7 @@ func Example() {
// <div><strong>no rows</strong></div> // <div><strong>no rows</strong></div>
// </body> // </body>
// </html> // </html>
} }
func Example_autoescaping() { func Example_autoescaping() {
@ -120,6 +124,7 @@ func Example_escape() {
// \"Fran \u0026 Freddie\'s Diner\" \u003Ctasty@example.com\u003E // \"Fran \u0026 Freddie\'s Diner\" \u003Ctasty@example.com\u003E
// \"Fran \u0026 Freddie\'s Diner\"32\u003Ctasty@example.com\u003E // \"Fran \u0026 Freddie\'s Diner\"32\u003Ctasty@example.com\u003E
// %22Fran+%26+Freddie%27s+Diner%2232%3Ctasty%40example.com%3E // %22Fran+%26+Freddie%27s+Diner%2232%3Ctasty%40example.com%3E
} }
func ExampleTemplate_Delims() { func ExampleTemplate_Delims() {

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 go1.13
// +build go1.13
package template_test package template_test
import ( import (

View file

@ -325,16 +325,12 @@ var execTests = []execTest{
{"$.U.V", "{{$.U.V}}", "v", tVal, true}, {"$.U.V", "{{$.U.V}}", "v", tVal, true},
{"declare in action", "{{$x := $.U.V}}{{$x}}", "v", tVal, true}, {"declare in action", "{{$x := $.U.V}}{{$x}}", "v", tVal, true},
{"simple assignment", "{{$x := 2}}{{$x = 3}}{{$x}}", "3", tVal, true}, {"simple assignment", "{{$x := 2}}{{$x = 3}}{{$x}}", "3", tVal, true},
{ {"nested assignment",
"nested assignment",
"{{$x := 2}}{{if true}}{{$x = 3}}{{end}}{{$x}}", "{{$x := 2}}{{if true}}{{$x = 3}}{{end}}{{$x}}",
"3", tVal, true, "3", tVal, true},
}, {"nested assignment changes the last declaration",
{
"nested assignment changes the last declaration",
"{{$x := 1}}{{if true}}{{$x := 2}}{{if true}}{{$x = 3}}{{end}}{{end}}{{$x}}", "{{$x := 1}}{{if true}}{{$x := 2}}{{if true}}{{$x = 3}}{{end}}{{end}}{{$x}}",
"1", tVal, true, "1", tVal, true},
},
// Type with String method. // Type with String method.
{"V{6666}.String()", "-{{.V0}}-", "-{6666}-", tVal, true}, // NOTE: -<6666>- in text/template {"V{6666}.String()", "-{{.V0}}-", "-{6666}-", tVal, true}, // NOTE: -<6666>- in text/template
@ -381,21 +377,15 @@ var execTests = []execTest{
{".Method3(nil constant)", "-{{.Method3 nil}}-", "-Method3: &lt;nil&gt;-", tVal, true}, {".Method3(nil constant)", "-{{.Method3 nil}}-", "-Method3: &lt;nil&gt;-", tVal, true},
{".Method3(nil value)", "-{{.Method3 .MXI.unset}}-", "-Method3: &lt;nil&gt;-", tVal, true}, {".Method3(nil value)", "-{{.Method3 .MXI.unset}}-", "-Method3: &lt;nil&gt;-", tVal, true},
{"method on var", "{{if $x := .}}-{{$x.Method2 .U16 $x.X}}{{end}}-", "-Method2: 16 x-", tVal, true}, {"method on var", "{{if $x := .}}-{{$x.Method2 .U16 $x.X}}{{end}}-", "-Method2: 16 x-", tVal, true},
{ {"method on chained var",
"method on chained var",
"{{range .MSIone}}{{if $.U.TrueFalse $.True}}{{$.U.TrueFalse $.True}}{{else}}WRONG{{end}}{{end}}", "{{range .MSIone}}{{if $.U.TrueFalse $.True}}{{$.U.TrueFalse $.True}}{{else}}WRONG{{end}}{{end}}",
"true", tVal, true, "true", tVal, true},
}, {"chained method",
{
"chained method",
"{{range .MSIone}}{{if $.GetU.TrueFalse $.True}}{{$.U.TrueFalse $.True}}{{else}}WRONG{{end}}{{end}}", "{{range .MSIone}}{{if $.GetU.TrueFalse $.True}}{{$.U.TrueFalse $.True}}{{else}}WRONG{{end}}{{end}}",
"true", tVal, true, "true", tVal, true},
}, {"chained method on variable",
{
"chained method on variable",
"{{with $x := .}}{{with .SI}}{{$.GetU.TrueFalse $.True}}{{end}}{{end}}", "{{with $x := .}}{{with .SI}}{{$.GetU.TrueFalse $.True}}{{end}}{{end}}",
"true", tVal, true, "true", tVal, true},
},
{".NilOKFunc not nil", "{{call .NilOKFunc .PI}}", "false", tVal, true}, {".NilOKFunc not nil", "{{call .NilOKFunc .PI}}", "false", tVal, true},
{".NilOKFunc nil", "{{call .NilOKFunc nil}}", "true", tVal, true}, {".NilOKFunc nil", "{{call .NilOKFunc nil}}", "true", tVal, true},
{"method on nil value from slice", "-{{range .}}{{.Method1 1234}}{{end}}-", "-1234-", tSliceOfNil, true}, {"method on nil value from slice", "-{{range .}}{{.Method1 1234}}{{end}}-", "-1234-", tSliceOfNil, true},
@ -481,14 +471,10 @@ var execTests = []execTest{
{"printf lots", `{{printf "%d %s %g %s" 127 "hello" 7-3i .Method0}}`, "127 hello (7-3i) M0", tVal, true}, {"printf lots", `{{printf "%d %s %g %s" 127 "hello" 7-3i .Method0}}`, "127 hello (7-3i) M0", tVal, true},
// HTML. // HTML.
{ {"html", `{{html "<script>alert(\"XSS\");</script>"}}`,
"html", `{{html "<script>alert(\"XSS\");</script>"}}`, "&lt;script&gt;alert(&#34;XSS&#34;);&lt;/script&gt;", nil, true},
"&lt;script&gt;alert(&#34;XSS&#34;);&lt;/script&gt;", nil, true, {"html pipeline", `{{printf "<script>alert(\"XSS\");</script>" | html}}`,
}, "&lt;script&gt;alert(&#34;XSS&#34;);&lt;/script&gt;", nil, true},
{
"html pipeline", `{{printf "<script>alert(\"XSS\");</script>" | html}}`,
"&lt;script&gt;alert(&#34;XSS&#34;);&lt;/script&gt;", nil, true,
},
{"html", `{{html .PS}}`, "a string", tVal, true}, {"html", `{{html .PS}}`, "a string", tVal, true},
{"html typed nil", `{{html .NIL}}`, "&lt;nil&gt;", tVal, true}, {"html typed nil", `{{html .NIL}}`, "&lt;nil&gt;", tVal, true},
{"html untyped nil", `{{html .Empty0}}`, "&lt;nil&gt;", tVal, true}, // NOTE: "&lt;no value&gt;" in text/template {"html untyped nil", `{{html .Empty0}}`, "&lt;nil&gt;", tVal, true}, // NOTE: "&lt;no value&gt;" in text/template
@ -854,7 +840,7 @@ var delimPairs = []string{
func TestDelims(t *testing.T) { func TestDelims(t *testing.T) {
const hello = "Hello, world" const hello = "Hello, world"
value := struct{ Str string }{hello} var value = struct{ Str string }{hello}
for i := 0; i < len(delimPairs); i += 2 { for i := 0; i < len(delimPairs); i += 2 {
text := ".Str" text := ".Str"
left := delimPairs[i+0] left := delimPairs[i+0]
@ -877,7 +863,7 @@ func TestDelims(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("delim %q text %q parse err %s", left, text, err) t.Fatalf("delim %q text %q parse err %s", left, text, err)
} }
b := new(strings.Builder) var b = new(strings.Builder)
err = tmpl.Execute(b, value) err = tmpl.Execute(b, value)
if err != nil { if err != nil {
t.Fatalf("delim %q exec err %s", left, err) t.Fatalf("delim %q exec err %s", left, err)
@ -978,7 +964,7 @@ const treeTemplate = `
` `
func TestTree(t *testing.T) { func TestTree(t *testing.T) {
tree := &Tree{ var tree = &Tree{
1, 1,
&Tree{ &Tree{
2, &Tree{ 2, &Tree{
@ -1229,7 +1215,7 @@ var cmpTests = []cmpTest{
func TestComparison(t *testing.T) { func TestComparison(t *testing.T) {
b := new(strings.Builder) b := new(strings.Builder)
cmpStruct := struct { var cmpStruct = struct {
Uthree, Ufour uint Uthree, Ufour uint
NegOne, Three int NegOne, Three int
Ptr, NilPtr *int Ptr, NilPtr *int

View file

@ -2,8 +2,8 @@
// 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 //go:build go1.13 && !windows
// +build !windows // +build go1.13,!windows
package template package template

View file

@ -10,6 +10,7 @@ import (
"fmt" "fmt"
htmltemplate "html/template" htmltemplate "html/template"
"reflect" "reflect"
"regexp"
"strings" "strings"
"unicode/utf8" "unicode/utf8"
) )
@ -145,6 +146,8 @@ func indirectToJSONMarshaler(a any) any {
return v.Interface() return v.Interface()
} }
var scriptTagRe = regexp.MustCompile("(?i)<(/?)script")
// jsValEscaper escapes its inputs to a JS Expression (section 11.14) that has // jsValEscaper escapes its inputs to a JS Expression (section 11.14) that has
// neither side-effects nor free variables outside (NaN, Infinity). // neither side-effects nor free variables outside (NaN, Infinity).
func jsValEscaper(args ...any) string { func jsValEscaper(args ...any) string {
@ -182,9 +185,9 @@ func jsValEscaper(args ...any) string {
// In particular we: // In particular we:
// * replace "*/" comment end tokens with "* /", which does not // * replace "*/" comment end tokens with "* /", which does not
// terminate the comment // terminate the comment
// * replace "</script" with "\x3C/script", and "<!--" with // * replace "<script" and "</script" with "\x3Cscript" and "\x3C/script"
// "\x3C!--", which prevents confusing script block termination // (case insensitively), and "<!--" with "\x3C!--", which prevents
// semantics // confusing script block termination semantics
// //
// We also put a space before the comment so that if it is flush against // We also put a space before the comment so that if it is flush against
// a division operator it is not turned into a line comment: // a division operator it is not turned into a line comment:
@ -193,8 +196,8 @@ func jsValEscaper(args ...any) string {
// x//* error marshaling y: // x//* error marshaling y:
// second line of error message */null // second line of error message */null
errStr := err.Error() errStr := err.Error()
errStr = string(scriptTagRe.ReplaceAll([]byte(errStr), []byte(`\x3C${1}script`)))
errStr = strings.ReplaceAll(errStr, "*/", "* /") errStr = strings.ReplaceAll(errStr, "*/", "* /")
errStr = strings.ReplaceAll(errStr, "</script", `\x3C/script`)
errStr = strings.ReplaceAll(errStr, "<!--", `\x3C!--`) errStr = strings.ReplaceAll(errStr, "<!--", `\x3C!--`)
return fmt.Sprintf(" /* %s */null ", errStr) return fmt.Sprintf(" /* %s */null ", errStr)
} }

View file

@ -2,8 +2,8 @@
// 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 //go:build go1.13 && !windows
// +build !windows // +build go1.13,!windows
package template package template
@ -110,7 +110,7 @@ func TestNextJsCtx(t *testing.T) {
type jsonErrType struct{} type jsonErrType struct{}
func (e *jsonErrType) MarshalJSON() ([]byte, error) { func (e *jsonErrType) MarshalJSON() ([]byte, error) {
return nil, errors.New("beep */ boop </script blip <!--") return nil, errors.New("a */ b <script c </script d <!-- e <sCrIpT f </sCrIpT")
} }
func TestJSValEscaper(t *testing.T) { func TestJSValEscaper(t *testing.T) {
@ -163,7 +163,7 @@ func TestJSValEscaper(t *testing.T) {
{"</script", `"\u003c/script"`, false}, {"</script", `"\u003c/script"`, false},
{"\U0001D11E", "\"\U0001D11E\"", false}, // or "\uD834\uDD1E" {"\U0001D11E", "\"\U0001D11E\"", false}, // or "\uD834\uDD1E"
{nil, " null ", false}, {nil, " null ", false},
{&jsonErrType{}, " /* json: error calling MarshalJSON for type *template.jsonErrType: beep * / boop \\x3C/script blip \\x3C!-- */null ", true}, {&jsonErrType{}, " /* json: error calling MarshalJSON for type *template.jsonErrType: a * / b \\x3Cscript c \\x3C/script d \\x3C!-- e \\x3Cscript f \\x3C/script */null ", true},
} }
for _, test := range tests { for _, test := range tests {
@ -221,8 +221,7 @@ func TestJSStrEscaper(t *testing.T) {
{"<!--", `\u003c!--`}, {"<!--", `\u003c!--`},
{"-->", `--\u003e`}, {"-->", `--\u003e`},
// From https://code.google.com/p/doctype/wiki/ArticleUtf7 // From https://code.google.com/p/doctype/wiki/ArticleUtf7
{ {"+ADw-script+AD4-alert(1)+ADw-/script+AD4-",
"+ADw-script+AD4-alert(1)+ADw-/script+AD4-",
`\u002bADw-script\u002bAD4-alert(1)\u002bADw-\/script\u002bAD4-`, `\u002bADw-script\u002bAD4-alert(1)\u002bADw-\/script\u002bAD4-`,
}, },
// Invalid UTF-8 sequence // Invalid UTF-8 sequence

View file

@ -4,8 +4,8 @@
// Tests for multiple-template execution, copied from text/template. // Tests for multiple-template execution, copied from text/template.
//go:build !windows //go:build go1.13 && !windows
// +build !windows // +build go1.13,!windows
package template package template
@ -268,7 +268,7 @@ func TestIssue19294(t *testing.T) {
// by the contents of "stylesheet", but if the internal map associating // by the contents of "stylesheet", but if the internal map associating
// names with templates is built in the wrong order, the empty block // names with templates is built in the wrong order, the empty block
// looks non-empty and this doesn't happen. // looks non-empty and this doesn't happen.
inlined := map[string]string{ var inlined = map[string]string{
"stylesheet": `{{define "stylesheet"}}stylesheet{{end}}`, "stylesheet": `{{define "stylesheet"}}stylesheet{{end}}`,
"xhtml": `{{block "stylesheet" .}}{{end}}`, "xhtml": `{{block "stylesheet" .}}{{end}}`,
} }

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 go1.13
// +build go1.13
package template_test package template_test
import ( import (
@ -15,6 +18,7 @@ import (
) )
func TestTemplateClone(t *testing.T) { func TestTemplateClone(t *testing.T) {
orig := New("name") orig := New("name")
clone, err := orig.Clone() clone, err := orig.Clone()
if err != nil { if err != nil {

View file

@ -2,8 +2,8 @@
// 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 //go:build go1.13 && !windows
// +build !windows // +build go1.13,!windows
package template package template
@ -43,6 +43,7 @@ func TestFindEndTag(t *testing.T) {
} }
func BenchmarkTemplateSpecialTags(b *testing.B) { func BenchmarkTemplateSpecialTags(b *testing.B) {
r := struct { r := struct {
Name, Gift string Name, Gift string
}{"Aunt Mildred", "bone china tea set"} }{"Aunt Mildred", "bone china tea set"}

View file

@ -2,8 +2,8 @@
// 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 //go:build go1.13 && !windows
// +build !windows // +build go1.13,!windows
package template package template

View file

@ -31,20 +31,17 @@ import (
// If exec is not supported, testenv.SyscallIsNotSupported will return true // If exec is not supported, testenv.SyscallIsNotSupported will return true
// for the resulting error. // for the resulting error.
func MustHaveExec(t testing.TB) { func MustHaveExec(t testing.TB) {
tryExecOnce.Do(func() { if err := tryExec(); err != nil {
tryExecErr = tryExec() msg := fmt.Sprintf("cannot exec subprocess on %s/%s: %v", runtime.GOOS, runtime.GOARCH, err)
}) if t == nil {
if tryExecErr != nil { panic(msg)
t.Skipf("skipping test: cannot exec subprocess on %s/%s: %v", runtime.GOOS, runtime.GOARCH, tryExecErr) }
t.Helper()
t.Skip("skipping test:", msg)
} }
} }
var ( var tryExec = sync.OnceValue(func() error {
tryExecOnce sync.Once
tryExecErr error
)
func tryExec() error {
switch runtime.GOOS { switch runtime.GOOS {
case "wasip1", "js", "ios": case "wasip1", "js", "ios":
default: default:
@ -70,15 +67,37 @@ func tryExec() error {
// We know that this is a test executable. We should be able to run it with a // We know that this is a test executable. We should be able to run it with a
// no-op flag to check for overall exec support. // no-op flag to check for overall exec support.
exe, err := os.Executable() exe, err := exePath()
if err != nil { if err != nil {
return fmt.Errorf("can't probe for exec support: %w", err) return fmt.Errorf("can't probe for exec support: %w", err)
} }
cmd := exec.Command(exe, "-test.list=^$") cmd := exec.Command(exe, "-test.list=^$")
cmd.Env = origEnv cmd.Env = origEnv
return cmd.Run() return cmd.Run()
})
// Executable is a wrapper around [MustHaveExec] and [os.Executable].
// It returns the path name for the executable that started the current process,
// or skips the test if the current system can't start new processes,
// or fails the test if the path can not be obtained.
func Executable(t testing.TB) string {
MustHaveExec(t)
exe, err := exePath()
if err != nil {
msg := fmt.Sprintf("os.Executable error: %v", err)
if t == nil {
panic(msg)
}
t.Fatal(msg)
}
return exe
} }
var exePath = sync.OnceValues(func() (string, error) {
return os.Executable()
})
var execPaths sync.Map // path -> error var execPaths sync.Map // path -> error
// MustHaveExecPath checks that the current system can start the named executable // MustHaveExecPath checks that the current system can start the named executable
@ -93,6 +112,7 @@ func MustHaveExecPath(t testing.TB, path string) {
err, _ = execPaths.LoadOrStore(path, err) err, _ = execPaths.LoadOrStore(path, err)
} }
if err != nil { if err != nil {
t.Helper()
t.Skipf("skipping test: %s: %s", path, err) t.Skipf("skipping test: %s: %s", path, err)
} }
} }

View file

@ -12,7 +12,6 @@ package testenv
import ( import (
"bytes" "bytes"
"errors"
"flag" "flag"
"fmt" "fmt"
"os" "os"
@ -43,15 +42,22 @@ func Builder() string {
// HasGoBuild reports whether the current system can build programs with “go build” // HasGoBuild reports whether the current system can build programs with “go build”
// and then run them with os.StartProcess or exec.Command. // and then run them with os.StartProcess or exec.Command.
// Modified by Hugo (not needed)
func HasGoBuild() bool { func HasGoBuild() bool {
return false if os.Getenv("GO_GCFLAGS") != "" {
// It's too much work to require every caller of the go command
// to pass along "-gcflags="+os.Getenv("GO_GCFLAGS").
// For now, if $GO_GCFLAGS is set, report that we simply can't
// run go build.
return false
}
return tryGoBuild() == nil
} }
var ( var tryGoBuild = sync.OnceValue(func() error {
goBuildOnce sync.Once // Removed by Hugo, not used.
goBuildErr error return nil
) })
// MustHaveGoBuild checks that the current system can build programs with “go build” // MustHaveGoBuild checks that the current system can build programs with “go build”
// and then run them with os.StartProcess or exec.Command. // and then run them with os.StartProcess or exec.Command.
@ -63,7 +69,7 @@ func MustHaveGoBuild(t testing.TB) {
} }
if !HasGoBuild() { if !HasGoBuild() {
t.Helper() t.Helper()
t.Skipf("skipping test: 'go build' unavailable: %v", goBuildErr) t.Skipf("skipping test: 'go build' unavailable: %v", tryGoBuild())
} }
} }
@ -77,6 +83,7 @@ func HasGoRun() bool {
// If not, MustHaveGoRun calls t.Skip with an explanation. // If not, MustHaveGoRun calls t.Skip with an explanation.
func MustHaveGoRun(t testing.TB) { func MustHaveGoRun(t testing.TB) {
if !HasGoRun() { if !HasGoRun() {
t.Helper()
t.Skipf("skipping test: 'go run' not available on %s/%s", runtime.GOOS, runtime.GOARCH) t.Skipf("skipping test: 'go run' not available on %s/%s", runtime.GOOS, runtime.GOARCH)
} }
} }
@ -96,6 +103,7 @@ func HasParallelism() bool {
// threads in parallel. If not, MustHaveParallelism calls t.Skip with an explanation. // threads in parallel. If not, MustHaveParallelism calls t.Skip with an explanation.
func MustHaveParallelism(t testing.TB) { func MustHaveParallelism(t testing.TB) {
if !HasParallelism() { if !HasParallelism() {
t.Helper()
t.Skipf("skipping test: no parallelism available on %s/%s", runtime.GOOS, runtime.GOARCH) t.Skipf("skipping test: no parallelism available on %s/%s", runtime.GOOS, runtime.GOARCH)
} }
} }
@ -119,82 +127,67 @@ func GoToolPath(t testing.TB) string {
return path return path
} }
var ( var findGOROOT = sync.OnceValues(func() (path string, err error) {
gorootOnce sync.Once if path := runtime.GOROOT(); path != "" {
gorootPath string // If runtime.GOROOT() is non-empty, assume that it is valid.
gorootErr error //
) // (It might not be: for example, the user may have explicitly set GOROOT
// to the wrong directory. But this case is
// rare, and if that happens the user can fix what they broke.)
return path, nil
}
func findGOROOT() (string, error) { // runtime.GOROOT doesn't know where GOROOT is (perhaps because the test
gorootOnce.Do(func() { // binary was built with -trimpath).
gorootPath = runtime.GOROOT() //
if gorootPath != "" { // Since this is internal/testenv, we can cheat and assume that the caller
// If runtime.GOROOT() is non-empty, assume that it is valid. // is a test of some package in a subdirectory of GOROOT/src. ('go test'
// // runs the test in the directory containing the packaged under test.) That
// (It might not be: for example, the user may have explicitly set GOROOT // means that if we start walking up the tree, we should eventually find
// to the wrong directory. But this case is // GOROOT/src/go.mod, and we can report the parent directory of that.
// rare, and if that happens the user can fix what they broke.) //
return // Notably, this works even if we can't run 'go env GOROOT' as a
// subprocess.
cwd, err := os.Getwd()
if err != nil {
return "", fmt.Errorf("finding GOROOT: %w", err)
}
dir := cwd
for {
parent := filepath.Dir(dir)
if parent == dir {
// dir is either "." or only a volume name.
return "", fmt.Errorf("failed to locate GOROOT/src in any parent directory")
} }
// runtime.GOROOT doesn't know where GOROOT is (perhaps because the test if base := filepath.Base(dir); base != "src" {
// binary was built with -trimpath). dir = parent
// continue // dir cannot be GOROOT/src if it doesn't end in "src".
// 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'
// runs the test in the directory containing the packaged under test.) That
// means that if we start walking up the tree, we should eventually find
// GOROOT/src/go.mod, and we can report the parent directory of that.
//
// Notably, this works even if we can't run 'go env GOROOT' as a
// subprocess.
cwd, err := os.Getwd() b, err := os.ReadFile(filepath.Join(dir, "go.mod"))
if err != nil { if err != nil {
gorootErr = fmt.Errorf("finding GOROOT: %w", err) if os.IsNotExist(err) {
return
}
dir := cwd
for {
parent := filepath.Dir(dir)
if parent == dir {
// dir is either "." or only a volume name.
gorootErr = fmt.Errorf("failed to locate GOROOT/src in any parent directory")
return
}
if base := filepath.Base(dir); base != "src" {
dir = parent dir = parent
continue // dir cannot be GOROOT/src if it doesn't end in "src". continue
} }
return "", fmt.Errorf("finding GOROOT: %w", err)
}
goMod := string(b)
b, err := os.ReadFile(filepath.Join(dir, "go.mod")) for goMod != "" {
if err != nil { var line string
if os.IsNotExist(err) { line, goMod, _ = strings.Cut(goMod, "\n")
dir = parent fields := strings.Fields(line)
continue if len(fields) >= 2 && fields[0] == "module" && fields[1] == "std" {
} // Found "module std", which is the module declaration in GOROOT/src!
gorootErr = fmt.Errorf("finding GOROOT: %w", err) return parent, nil
return
}
goMod := string(b)
for goMod != "" {
var line string
line, goMod, _ = strings.Cut(goMod, "\n")
fields := strings.Fields(line)
if len(fields) >= 2 && fields[0] == "module" && fields[1] == "std" {
// Found "module std", which is the module declaration in GOROOT/src!
gorootPath = parent
return
}
} }
} }
}) }
})
return gorootPath, gorootErr
}
// GOROOT reports the path to the directory containing the root of the Go // GOROOT reports the path to the directory containing the root of the Go
// project source tree. This is normally equivalent to runtime.GOROOT, but // project source tree. This is normally equivalent to runtime.GOROOT, but
@ -217,28 +210,22 @@ func GOROOT(t testing.TB) string {
// GoTool reports the path to the Go tool. // GoTool reports the path to the Go tool.
func GoTool() (string, error) { func GoTool() (string, error) {
if !HasGoBuild() { // Removed by Hugo, not used.
return "", errors.New("platform cannot run go tool") return "", nil
}
goToolOnce.Do(func() {
goToolPath, goToolErr = exec.LookPath("go")
})
return goToolPath, goToolErr
} }
var ( var goTool = sync.OnceValues(func() (string, error) {
goToolOnce sync.Once return exec.LookPath("go")
goToolPath string })
goToolErr error
)
// HasSrc reports whether the entire source tree is available under GOROOT. // MustHaveSource checks that the entire source tree is available under GOROOT.
func HasSrc() bool { // If not, it calls t.Skip with an explanation.
func MustHaveSource(t testing.TB) {
switch runtime.GOOS { switch runtime.GOOS {
case "ios": case "ios":
return false t.Helper()
t.Skip("skipping test: no source tree on " + runtime.GOOS)
} }
return true
} }
// HasExternalNetwork reports whether the current system can use // HasExternalNetwork reports whether the current system can use
@ -263,41 +250,39 @@ func MustHaveExternalNetwork(t testing.TB) {
// HasCGO reports whether the current system can use cgo. // HasCGO reports whether the current system can use cgo.
func HasCGO() bool { func HasCGO() bool {
hasCgoOnce.Do(func() { return hasCgo()
goTool, err := GoTool()
if err != nil {
return
}
cmd := exec.Command(goTool, "env", "CGO_ENABLED")
cmd.Env = origEnv
out, err := cmd.Output()
if err != nil {
panic(fmt.Sprintf("%v: %v", cmd, out))
}
hasCgo, err = strconv.ParseBool(string(bytes.TrimSpace(out)))
if err != nil {
panic(fmt.Sprintf("%v: non-boolean output %q", cmd, out))
}
})
return hasCgo
} }
var ( var hasCgo = sync.OnceValue(func() bool {
hasCgoOnce sync.Once goTool, err := goTool()
hasCgo bool if err != nil {
) return false
}
cmd := exec.Command(goTool, "env", "CGO_ENABLED")
cmd.Env = origEnv
out, err := cmd.Output()
if err != nil {
panic(fmt.Sprintf("%v: %v", cmd, out))
}
ok, err := strconv.ParseBool(string(bytes.TrimSpace(out)))
if err != nil {
panic(fmt.Sprintf("%v: non-boolean output %q", cmd, out))
}
return ok
})
// MustHaveCGO calls t.Skip if cgo is not available. // MustHaveCGO calls t.Skip if cgo is not available.
func MustHaveCGO(t testing.TB) { func MustHaveCGO(t testing.TB) {
if !HasCGO() { if !HasCGO() {
t.Helper()
t.Skipf("skipping test: no cgo") t.Skipf("skipping test: no cgo")
} }
} }
// CanInternalLink reports whether the current system can link programs with // CanInternalLink reports whether the current system can link programs with
// internal linking. // internal linking.
// Modified by Hugo (not needed)
func CanInternalLink(withCgo bool) bool { func CanInternalLink(withCgo bool) bool {
// Removed by Hugo, not used.
return false return false
} }
@ -306,6 +291,7 @@ func CanInternalLink(withCgo bool) bool {
// If not, MustInternalLink calls t.Skip with an explanation. // If not, MustInternalLink calls t.Skip with an explanation.
func MustInternalLink(t testing.TB, withCgo bool) { func MustInternalLink(t testing.TB, withCgo bool) {
if !CanInternalLink(withCgo) { if !CanInternalLink(withCgo) {
t.Helper()
if withCgo && CanInternalLink(false) { if withCgo && CanInternalLink(false) {
t.Skipf("skipping test: internal linking on %s/%s is not supported with cgo", runtime.GOOS, runtime.GOARCH) t.Skipf("skipping test: internal linking on %s/%s is not supported with cgo", runtime.GOOS, runtime.GOARCH)
} }
@ -316,15 +302,15 @@ func MustInternalLink(t testing.TB, withCgo bool) {
// MustInternalLinkPIE checks whether the current system can link PIE binary using // MustInternalLinkPIE checks whether the current system can link PIE binary using
// internal linking. // internal linking.
// If not, MustInternalLinkPIE calls t.Skip with an explanation. // If not, MustInternalLinkPIE calls t.Skip with an explanation.
// Modified by Hugo (not needed)
func MustInternalLinkPIE(t testing.TB) { func MustInternalLinkPIE(t testing.TB) {
// Removed by Hugo, not used.
} }
// 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)
func MustHaveBuildMode(t testing.TB, buildmode string) { func MustHaveBuildMode(t testing.TB, buildmode string) {
// Removed by Hugo, not used.
} }
// HasSymlink reports whether the current system can use os.Symlink. // HasSymlink reports whether the current system can use os.Symlink.
@ -338,6 +324,7 @@ func HasSymlink() bool {
func MustHaveSymlink(t testing.TB) { func MustHaveSymlink(t testing.TB) {
ok, reason := hasSymlink() ok, reason := hasSymlink()
if !ok { if !ok {
t.Helper()
t.Skipf("skipping test: cannot make symlinks on %s/%s: %s", runtime.GOOS, runtime.GOARCH, reason) t.Skipf("skipping test: cannot make symlinks on %s/%s: %s", runtime.GOOS, runtime.GOARCH, reason)
} }
} }
@ -354,6 +341,7 @@ func HasLink() bool {
// If not, MustHaveLink calls t.Skip with an explanation. // If not, MustHaveLink calls t.Skip with an explanation.
func MustHaveLink(t testing.TB) { func MustHaveLink(t testing.TB) {
if !HasLink() { if !HasLink() {
t.Helper()
t.Skipf("skipping test: hardlinks are not supported on %s/%s", runtime.GOOS, runtime.GOARCH) t.Skipf("skipping test: hardlinks are not supported on %s/%s", runtime.GOOS, runtime.GOARCH)
} }
} }
@ -361,15 +349,15 @@ func MustHaveLink(t testing.TB) {
var flaky = flag.Bool("flaky", false, "run known-flaky tests too") var flaky = flag.Bool("flaky", false, "run known-flaky tests too")
func SkipFlaky(t testing.TB, issue int) { func SkipFlaky(t testing.TB, issue int) {
t.Helper()
if !*flaky { if !*flaky {
t.Helper()
t.Skipf("skipping known flaky test without the -flaky flag; see golang.org/issue/%d", issue) t.Skipf("skipping known flaky test without the -flaky flag; see golang.org/issue/%d", issue)
} }
} }
func SkipFlakyNet(t testing.TB) { func SkipFlakyNet(t testing.TB) {
t.Helper()
if v, _ := strconv.ParseBool(os.Getenv("GO_BUILDER_FLAKY_NET")); v { if v, _ := strconv.ParseBool(os.Getenv("GO_BUILDER_FLAKY_NET")); v {
t.Helper()
t.Skip("skipping test on builder known to have frequent network failures") t.Skip("skipping test on builder known to have frequent network failures")
} }
} }
@ -455,6 +443,6 @@ func SyscallIsNotSupported(err error) bool {
// ParallelOn64Bit calls t.Parallel() unless there is a case that cannot be parallel. // 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 // 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. // 32-bit machines, typically because the test uses lots of memory.
// Disabled by Hugo.
func ParallelOn64Bit(t *testing.T) { func ParallelOn64Bit(t *testing.T) {
// Removed by Hugo, not used.
} }

View file

@ -11,9 +11,10 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"runtime" "runtime"
"sync"
) )
func hasSymlink() (ok bool, reason string) { var hasSymlink = sync.OnceValues(func() (ok bool, reason string) {
switch runtime.GOOS { switch runtime.GOOS {
case "plan9": case "plan9":
return false, "" return false, ""
@ -43,4 +44,4 @@ func hasSymlink() (ok bool, reason string) {
} }
return true, "" return true, ""
} })

View file

@ -15,6 +15,7 @@ import (
) )
func TestGoToolLocation(t *testing.T) { func TestGoToolLocation(t *testing.T) {
t.Skip("This test is not relevant for Hugo")
testenv.MustHaveGoBuild(t) testenv.MustHaveGoBuild(t)
var exeSuffix string var exeSuffix string
@ -54,8 +55,83 @@ func TestGoToolLocation(t *testing.T) {
} }
} }
// Modified by Hugo (not needed)
func TestHasGoBuild(t *testing.T) { func TestHasGoBuild(t *testing.T) {
if !testenv.HasGoBuild() {
switch runtime.GOOS {
case "js", "wasip1":
// No exec syscall, so these shouldn't be able to 'go build'.
t.Logf("HasGoBuild is false on %s", runtime.GOOS)
return
}
b := testenv.Builder()
if b == "" {
// We shouldn't make assumptions about what kind of sandbox or build
// environment external Go users may be running in.
t.Skipf("skipping: 'go build' unavailable")
}
// Since we control the Go builders, we know which ones ought
// to be able to run 'go build'. Check that they can.
//
// (Note that we don't verify that any builders *can't* run 'go build'.
// If a builder starts running 'go build' tests when it shouldn't,
// we will presumably find out about it when those tests fail.)
switch runtime.GOOS {
case "ios":
if isCorelliumBuilder(b) {
// The corellium environment is self-hosting, so it should be able
// to build even though real "ios" devices can't exec.
} else {
// The usual iOS sandbox does not allow the app to start another
// process. If we add builders on stock iOS devices, they presumably
// will not be able to exec, so we may as well allow that now.
t.Logf("HasGoBuild is false on %s", b)
return
}
case "android":
panic("Removed by Hugo, should not be used")
}
if strings.Contains(b, "-noopt") {
// The -noopt builder sets GO_GCFLAGS, which causes tests of 'go build' to
// be skipped.
t.Logf("HasGoBuild is false on %s", b)
return
}
t.Fatalf("HasGoBuild unexpectedly false on %s", b)
}
t.Logf("HasGoBuild is true; checking consistency with other functions")
hasExec := false
hasExecGo := false
t.Run("MustHaveExec", func(t *testing.T) {
testenv.MustHaveExec(t)
hasExec = true
})
t.Run("MustHaveExecPath", func(t *testing.T) {
testenv.MustHaveExecPath(t, "go")
hasExecGo = true
})
if !hasExec {
t.Errorf(`MustHaveExec(t) skipped unexpectedly`)
}
if !hasExecGo {
t.Errorf(`MustHaveExecPath(t, "go") skipped unexpectedly`)
}
dir := t.TempDir()
mainGo := filepath.Join(dir, "main.go")
if err := os.WriteFile(mainGo, []byte("package main\nfunc main() {}\n"), 0o644); err != nil {
t.Fatal(err)
}
cmd := testenv.Command(t, "go", "build", "-o", os.DevNull, mainGo)
out, err := cmd.CombinedOutput()
if err != nil {
t.Fatalf("%v: %v\n%s", cmd, err, out)
}
} }
func TestMustHaveExec(t *testing.T) { func TestMustHaveExec(t *testing.T) {

View file

@ -5,16 +5,14 @@
package testenv package testenv
import ( import (
"errors"
"os" "os"
"path/filepath" "path/filepath"
"sync" "sync"
"syscall" "syscall"
) )
var symlinkOnce sync.Once var hasSymlink = sync.OnceValues(func() (bool, string) {
var winSymlinkErr error
func initWinHasSymlink() {
tmpdir, err := os.MkdirTemp("", "symtest") tmpdir, err := os.MkdirTemp("", "symtest")
if err != nil { if err != nil {
panic("failed to create temp directory: " + err.Error()) panic("failed to create temp directory: " + err.Error())
@ -22,26 +20,13 @@ func initWinHasSymlink() {
defer os.RemoveAll(tmpdir) defer os.RemoveAll(tmpdir)
err = os.Symlink("target", filepath.Join(tmpdir, "symlink")) err = os.Symlink("target", filepath.Join(tmpdir, "symlink"))
if err != nil { switch {
err = err.(*os.LinkError).Err case err == nil:
switch err {
case syscall.EWINDOWS, syscall.ERROR_PRIVILEGE_NOT_HELD:
winSymlinkErr = err
}
}
}
func hasSymlink() (ok bool, reason string) {
symlinkOnce.Do(initWinHasSymlink)
switch winSymlinkErr {
case nil:
return true, "" return true, ""
case syscall.EWINDOWS: case errors.Is(err, syscall.EWINDOWS):
return false, ": symlinks are not supported on your version of Windows" return false, ": symlinks are not supported on your version of Windows"
case syscall.ERROR_PRIVILEGE_NOT_HELD: case errors.Is(err, syscall.ERROR_PRIVILEGE_NOT_HELD):
return false, ": you don't have enough privileges to create symlinks" return false, ": you don't have enough privileges to create symlinks"
} }
return false, "" return false, ""
} })

View file

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

View file

@ -35,7 +35,7 @@ Josie
Name, Gift string Name, Gift string
Attended bool Attended bool
} }
recipients := []Recipient{ var 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,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 go1.13
// +build go1.13
package template_test package template_test
import ( import (

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

@ -2,6 +2,9 @@
// Use of this source code is governed by a BSD-style // 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 parse package parse
import ( import (
@ -33,9 +36,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, 0o73, 0o73, 0o73, 0}, {"073", 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}, {"0O73", true, true, true, false, 073, 073, 073, 0},
{"0x73", true, true, true, false, 0x73, 0x73, 0x73, 0}, {"0x73", true, true, true, false, 0x73, 0x73, 0x73, 0},
{"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 +64,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, 0o123, 0o123, 0o123, 0}, {"0123", true, true, true, false, 0123, 0123, 0123, 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,150 +179,78 @@ const (
) )
var parseTests = []parseTest{ var parseTests = []parseTest{
{ {"empty", "", noError,
"empty", "", noError, ``},
``, {"comment", "{{/*\n\n\n*/}}", noError,
}, ``},
{ {"spaces", " \t\n", noError,
"comment", "{{/*\n\n\n*/}}", noError, `" \t\n"`},
``, {"text", "some text", noError,
}, `"some text"`},
{ {"emptyAction", "{{}}", hasError,
"spaces", " \t\n", noError, `{{}}`},
`" \t\n"`, {"field", "{{.X}}", noError,
}, `{{.X}}`},
{ {"simple command", "{{printf}}", noError,
"text", "some text", noError, `{{printf}}`},
`"some text"`, {"$ invocation", "{{$}}", noError,
}, "{{$}}"},
{ {"variable invocation", "{{with $x := 3}}{{$x 23}}{{end}}", noError,
"emptyAction", "{{}}", hasError, "{{with $x := 3}}{{$x 23}}{{end}}"},
`{{}}`, {"variable with fields", "{{$.I}}", noError,
}, "{{$.I}}"},
{ {"multi-word command", "{{printf `%d` 23}}", noError,
"field", "{{.X}}", noError, "{{printf `%d` 23}}"},
`{{.X}}`, {"pipeline", "{{.X|.Y}}", noError,
}, `{{.X | .Y}}`},
{ {"pipeline with decl", "{{$x := .X|.Y}}", noError,
"simple command", "{{printf}}", noError, `{{$x := .X | .Y}}`},
`{{printf}}`, {"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,
"$ invocation", "{{$}}", 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,
"variable invocation", "{{with $x := 3}}{{$x 23}}{{end}}", noError, `{{if .X}}"true"{{else}}"false"{{end}}`},
"{{with $x := 3}}{{$x 23}}{{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,
"variable with fields", "{{$.I}}", noError, `"+"{{if .X}}"X"{{else}}{{if .Y}}"Y"{{else}}{{if .Z}}"Z"{{end}}{{end}}{{end}}"+"`},
"{{$.I}}", {"simple range", "{{range .X}}hello{{end}}", noError,
}, `{{range .X}}"hello"{{end}}`},
{ {"chained field range", "{{range .X.Y.Z}}hello{{end}}", noError,
"multi-word command", "{{printf `%d` 23}}", noError, `{{range .X.Y.Z}}"hello"{{end}}`},
"{{printf `%d` 23}}", {"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,
"pipeline", "{{.X|.Y}}", noError, `{{range .X}}"true"{{else}}"false"{{end}}`},
`{{.X | .Y}}`, {"range over pipeline", "{{range .X|.M}}true{{else}}false{{end}}", noError,
}, `{{range .X | .M}}"true"{{else}}"false"{{end}}`},
{ {"range []int", "{{range .SI}}{{.}}{{end}}", noError,
"pipeline with decl", "{{$x := .X|.Y}}", noError, `{{range .SI}}{{.}}{{end}}`},
`{{$x := .X | .Y}}`, {"range 1 var", "{{range $x := .SI}}{{.}}{{end}}", noError,
}, `{{range $x := .SI}}{{.}}{{end}}`},
{ {"range 2 vars", "{{range $x, $y := .SI}}{{.}}{{end}}", noError,
"nested pipeline", "{{.X (.Y .Z) (.A | .B .C) (.E)}}", noError, `{{range $x, $y := .SI}}{{.}}{{end}}`},
`{{.X (.Y .Z) (.A | .B .C) (.E)}}`, {"range with break", "{{range .SI}}{{.}}{{break}}{{end}}", noError,
}, `{{range .SI}}{{.}}{{break}}{{end}}`},
{ {"range with continue", "{{range .SI}}{{.}}{{continue}}{{end}}", noError,
"field applied to parentheses", "{{(.Y .Z).Field}}", noError, `{{range .SI}}{{.}}{{continue}}{{end}}`},
`{{(.Y .Z).Field}}`, {"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,
"simple if", "{{if .X}}hello{{end}}", noError, `{{template "x"}}`},
`{{if .X}}"hello"{{end}}`, {"template with arg", "{{template `x` .Y}}", noError,
}, `{{template "x" .Y}}`},
{ {"with", "{{with .X}}hello{{end}}", noError,
"if with else", "{{if .X}}true{{else}}false{{end}}", noError, `{{with .X}}"hello"{{end}}`},
`{{if .X}}"true"{{else}}"false"{{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,
"if with else if", "{{if .X}}true{{else if .Y}}false{{end}}", noError, `{{with .X}}"hello"{{else}}{{with .Y}}"goodbye"{{end}}{{end}}`},
`{{if .X}}"true"{{else}}{{if .Y}}"false"{{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}}`},
{
"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"`},
@ -328,24 +259,18 @@ 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,
"block definition", `{{block "foo" .}}hello{{end}}`, noError, `{{template "foo" .}}`},
`{{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,
"spaces around continue", "{{range .SI}}{{.}}{{ continue }}{{end}}", noError, `{{range .SI}}{{.}}{{continue}}{{end}}`},
`{{range .SI}}{{.}}{{continue}}{{end}}`, {"spaces around break", "{{range .SI}}{{.}}{{ break }}{{end}}", noError,
}, `{{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, ""},
@ -487,7 +412,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.
funcsWithKeywordFunc := map[string]any{ var 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)
@ -574,168 +499,104 @@ 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

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