]> git.maquefel.me Git - brevno-suite/hugo/commitdiff
Upgrade to Go 1.24
authorBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Wed, 12 Feb 2025 09:35:51 +0000 (10:35 +0100)
committerBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Wed, 12 Feb 2025 21:01:25 +0000 (22:01 +0100)
Fixes #13381

37 files changed:
.circleci/config.yml
.github/workflows/test.yml
go.mod
scripts/fork_go_templates/main.go
tpl/internal/go_templates/cfg/cfg.go
tpl/internal/go_templates/htmltemplate/clone_test.go
tpl/internal/go_templates/htmltemplate/content_test.go
tpl/internal/go_templates/htmltemplate/css_test.go
tpl/internal/go_templates/htmltemplate/escape.go
tpl/internal/go_templates/htmltemplate/escape_test.go
tpl/internal/go_templates/htmltemplate/example_test.go
tpl/internal/go_templates/htmltemplate/examplefiles_test.go
tpl/internal/go_templates/htmltemplate/exec_test.go
tpl/internal/go_templates/htmltemplate/html_test.go
tpl/internal/go_templates/htmltemplate/js.go
tpl/internal/go_templates/htmltemplate/js_test.go
tpl/internal/go_templates/htmltemplate/multi_test.go
tpl/internal/go_templates/htmltemplate/template_test.go
tpl/internal/go_templates/htmltemplate/transition_test.go
tpl/internal/go_templates/htmltemplate/url_test.go
tpl/internal/go_templates/testenv/exec.go
tpl/internal/go_templates/testenv/testenv.go
tpl/internal/go_templates/testenv/testenv_notwin.go
tpl/internal/go_templates/testenv/testenv_test.go
tpl/internal/go_templates/testenv/testenv_windows.go
tpl/internal/go_templates/texttemplate/doc.go
tpl/internal/go_templates/texttemplate/example_test.go
tpl/internal/go_templates/texttemplate/examplefiles_test.go
tpl/internal/go_templates/texttemplate/exec.go
tpl/internal/go_templates/texttemplate/exec_test.go
tpl/internal/go_templates/texttemplate/hugo_template.go
tpl/internal/go_templates/texttemplate/link_test.go
tpl/internal/go_templates/texttemplate/multi_test.go
tpl/internal/go_templates/texttemplate/parse/lex.go
tpl/internal/go_templates/texttemplate/parse/lex_test.go
tpl/internal/go_templates/texttemplate/parse/parse_test.go
tpl/internal/go_templates/texttemplate/template.go

index f7a47a64b3a2c8a87face5890f66c0fe3adde746..06e643bdd7657476eec8bc8bd68ce00473e7fbe7 100644 (file)
@@ -4,7 +4,7 @@ parameters:
 defaults: &defaults
   resource_class: large
   docker:
-      - image: bepsays/ci-hugoreleaser:1.22301.20401
+      - image: bepsays/ci-hugoreleaser:1.22400.20000
 environment: &buildenv
       GOMODCACHE: /root/project/gomodcache
 version: 2
@@ -58,7 +58,7 @@ jobs:
     environment:
         <<: [*buildenv]
     docker:
-    - image: bepsays/ci-hugoreleaser-linux-arm64:1.22301.20401
+    - image: bepsays/ci-hugoreleaser-linux-arm64:1.22400.20000
     steps:
       - *restore-cache
       - &attach-workspace
index fb9d71f06fd02d798768d7af2a1d501a0c24dd50..c49c12371a471d4339f5435730442945557506c9 100644 (file)
@@ -16,7 +16,7 @@ jobs:
   test:
     strategy:
       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.
     runs-on: ${{ matrix.os }}
     steps:
diff --git a/go.mod b/go.mod
index 3c9a07193979d16bf8399a29a8d2aaf560e6c67f..f375cbe1a09cb6fd031cb93f2c052a32f670f5dc 100644 (file)
--- a/go.mod
+++ b/go.mod
@@ -170,4 +170,4 @@ require (
        software.sslmate.com/src/go-pkcs12 v0.2.0 // indirect
 )
 
-go 1.22.6
+go 1.23
index c4dad3224051018b50ed2f38d43f4aecca7a336e..cd461043c1a28412a5e0c7ef8d43ce82c88c7649 100644 (file)
@@ -16,7 +16,7 @@ import (
 )
 
 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.
        fmt.Println("Forking ...")
        defer fmt.Println("Done ...")
@@ -216,6 +216,7 @@ func rewrite(filename, rule string) {
 }
 
 func goimports(dir string) {
+       // Needs go install golang.org/x/tools/cmd/goimports@latest
        cmf, _ := hexec.SafeCommand("goimports", "-w", dir)
        out, err := cmf.CombinedOutput()
        if err != nil {
index 08d210b797385b26ef4a652ec8b17bdf99768b3e..9329769721b7debfa20f3a2f04b6361939c7ea62 100644 (file)
@@ -37,12 +37,14 @@ const KnownEnv = `
        GOARCH
        GOARM
        GOARM64
+       GOAUTH
        GOBIN
        GOCACHE
        GOCACHEPROG
        GOENV
        GOEXE
        GOEXPERIMENT
+       GOFIPS140
        GOFLAGS
        GOGCCFLAGS
        GOHOSTARCH
index e42177a6b32e46274214125c37eeeb26e0d371db..7db335b5b7dea0fcd0066b8cd9f5c1dd744211ef 100644 (file)
@@ -2,8 +2,8 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
-//go:build !windows
-// +build !windows
+//go:build go1.13 && !windows
+// +build go1.13,!windows
 
 package template
 
index e886ee8ff66500129f9c9651877102caf109f988..fac4774cc840ca9b67a1dc5d71e5a654a7903b32 100644 (file)
@@ -2,8 +2,8 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
-//go:build !windows
-// +build !windows
+//go:build go1.13 && !windows
+// +build go1.13,!windows
 
 package template
 
@@ -428,7 +428,7 @@ func TestStringer(t *testing.T) {
        if err := tmpl.Execute(b, s); err != nil {
                t.Fatal(err)
        }
-       expect := "string=3"
+       var expect = "string=3"
        if b.String() != expect {
                t.Errorf("expected %q got %q", expect, b.String())
        }
index f2b2add3a700ff556cdc5f363ee38d5a29b50579..f44568930ddfc14857558afcc565050fe41db3f8 100644 (file)
@@ -2,8 +2,8 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
-//go:build !windows
-// +build !windows
+//go:build go1.13 && !windows
+// +build go1.13,!windows
 
 package template
 
index 334bbce0f3af5cfbc7fe6c95fd45e52614a44797..f5d5674fae88fd02a0e97a47f41c49726b52137f 100644 (file)
@@ -9,6 +9,7 @@ import (
        "fmt"
        "html"
        "io"
+       "maps"
        "regexp"
 
        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
        case *parse.ContinueNode:
                c.n = n
-               e.rangeContext.continues = append(e.rangeContext.breaks, c)
+               e.rangeContext.continues = append(e.rangeContext.continues, c)
                return context{state: stateDead}
        case *parse.IfNode:
                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.rangeContext = e.rangeContext
        // Make type inferences available to f.
-       for k, v := range e.output {
-               e1.output[k] = v
-       }
+       maps.Copy(e1.output, e.output)
        c = e1.escapeList(c, n)
        ok := filter != nil && filter(&e1, c)
        if ok {
                // Copy inferences and edits from e1 back into e.
-               for k, v := range e1.output {
-                       e.output[k] = v
-               }
-               for k, v := range e1.derived {
-                       e.derived[k] = v
-               }
-               for k, v := range e1.called {
-                       e.called[k] = v
-               }
+               maps.Copy(e.output, e1.output)
+               maps.Copy(e.derived, e1.derived)
+               maps.Copy(e.called, e1.called)
                for k, v := range e1.actionNodeEdits {
                        e.editActionNode(k, v)
                }
index 66bf50203e2aaae938e1c25d1236b8ecdceced92..a5a810ffccf807d8d281a01fde326c1480d3eed8 100644 (file)
@@ -2,8 +2,8 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
-//go:build !windows
-// +build !windows
+//go:build go1.13 && !windows
+// +build go1.13,!windows
 
 package template
 
@@ -944,6 +944,7 @@ func TestEscapeSet(t *testing.T) {
                        t.Errorf("want\n\t%q\ngot\n\t%q", test.want, got)
                }
        }
+
 }
 
 func TestErrors(t *testing.T) {
@@ -1064,6 +1065,10 @@ func TestErrors(t *testing.T) {
                        "{{range .Items}}<a{{if .X}}{{continue}}{{end}}>{{end}}",
                        "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}}",
                        "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.
                if err := tmpl.Execute(buf, nil); err == nil || err.Error() != got {
                        t.Errorf("input=%q: unexpected error on second call %q", test.input, err)
+
                }
        }
 }
index de81132c2514a06ac3cf399ab5255fd0e4e53b20..6485c7cfbec1cbf4312841b078de677407ba6203 100644 (file)
@@ -2,6 +2,9 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
+//go:build go1.13
+// +build go1.13
+
 package template_test
 
 import (
@@ -80,6 +83,7 @@ func Example() {
        //              <div><strong>no rows</strong></div>
        //      </body>
        // </html>
+
 }
 
 func Example_autoescaping() {
@@ -120,6 +124,7 @@ func Example_escape() {
        // \"Fran \u0026 Freddie\'s Diner\" \u003Ctasty@example.com\u003E
        // \"Fran \u0026 Freddie\'s Diner\"32\u003Ctasty@example.com\u003E
        // %22Fran+%26+Freddie%27s+Diner%2232%3Ctasty%40example.com%3E
+
 }
 
 func ExampleTemplate_Delims() {
index 24b22d984a2a82d15e40c406078ac2a02ff272eb..43cc3bf011ff5be4f5cc59d53c304b29eb686cca 100644 (file)
@@ -2,6 +2,9 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
+//go:build go1.13
+// +build go1.13
+
 package template_test
 
 import (
index e01813e68523a44e7071ee593735bf22796155d0..e349ca7cdb8ee66fa375f2c50acbb974d39a4ebf 100644 (file)
@@ -325,16 +325,12 @@ var execTests = []execTest{
        {"$.U.V", "{{$.U.V}}", "v", tVal, true},
        {"declare in action", "{{$x := $.U.V}}{{$x}}", "v", tVal, true},
        {"simple assignment", "{{$x := 2}}{{$x = 3}}{{$x}}", "3", tVal, true},
-       {
-               "nested assignment",
+       {"nested assignment",
                "{{$x := 2}}{{if true}}{{$x = 3}}{{end}}{{$x}}",
-               "3", tVal, true,
-       },
-       {
-               "nested assignment changes the last declaration",
+               "3", tVal, true},
+       {"nested assignment changes the last declaration",
                "{{$x := 1}}{{if true}}{{$x := 2}}{{if true}}{{$x = 3}}{{end}}{{end}}{{$x}}",
-               "1", tVal, true,
-       },
+               "1", tVal, true},
 
        // Type with String method.
        {"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 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 chained var",
+       {"method on chained var",
                "{{range .MSIone}}{{if $.U.TrueFalse $.True}}{{$.U.TrueFalse $.True}}{{else}}WRONG{{end}}{{end}}",
-               "true", tVal, true,
-       },
-       {
-               "chained method",
+               "true", tVal, true},
+       {"chained method",
                "{{range .MSIone}}{{if $.GetU.TrueFalse $.True}}{{$.U.TrueFalse $.True}}{{else}}WRONG{{end}}{{end}}",
-               "true", tVal, true,
-       },
-       {
-               "chained method on variable",
+               "true", tVal, true},
+       {"chained method on variable",
                "{{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 nil", "{{call .NilOKFunc nil}}", "true", tVal, 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},
 
        // HTML.
-       {
-               "html", `{{html "<script>alert(\"XSS\");</script>"}}`,
-               "&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 "<script>alert(\"XSS\");</script>"}}`,
+               "&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 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
@@ -854,7 +840,7 @@ var delimPairs = []string{
 
 func TestDelims(t *testing.T) {
        const hello = "Hello, world"
-       value := struct{ Str string }{hello}
+       var value = struct{ Str string }{hello}
        for i := 0; i < len(delimPairs); i += 2 {
                text := ".Str"
                left := delimPairs[i+0]
@@ -877,7 +863,7 @@ func TestDelims(t *testing.T) {
                if err != nil {
                        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)
                if err != nil {
                        t.Fatalf("delim %q exec err %s", left, err)
@@ -978,7 +964,7 @@ const treeTemplate = `
 `
 
 func TestTree(t *testing.T) {
-       tree := &Tree{
+       var tree = &Tree{
                1,
                &Tree{
                        2, &Tree{
@@ -1229,7 +1215,7 @@ var cmpTests = []cmpTest{
 
 func TestComparison(t *testing.T) {
        b := new(strings.Builder)
-       cmpStruct := struct {
+       var cmpStruct = struct {
                Uthree, Ufour  uint
                NegOne, Three  int
                Ptr, NilPtr    *int
index bea668ef9c9e96eb8c0846810790b0562dc9d3f6..2809ee1e2a6321a1130cce223f153a96b185bb85 100644 (file)
@@ -2,8 +2,8 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
-//go:build !windows
-// +build !windows
+//go:build go1.13 && !windows
+// +build go1.13,!windows
 
 package template
 
index 1b56abeb108608db8c7bba8e3492a9463a6764e2..b658aeabcbfa423f27f681f6bc7e58ab37a74e84 100644 (file)
@@ -10,6 +10,7 @@ import (
        "fmt"
        htmltemplate "html/template"
        "reflect"
+       "regexp"
        "strings"
        "unicode/utf8"
 )
@@ -145,6 +146,8 @@ func indirectToJSONMarshaler(a any) any {
        return v.Interface()
 }
 
+var scriptTagRe = regexp.MustCompile("(?i)<(/?)script")
+
 // jsValEscaper escapes its inputs to a JS Expression (section 11.14) that has
 // neither side-effects nor free variables outside (NaN, Infinity).
 func jsValEscaper(args ...any) string {
@@ -182,9 +185,9 @@ func jsValEscaper(args ...any) string {
                // In particular we:
                //   * replace "*/" comment end tokens with "* /", which does not
                //     terminate the comment
-               //   * replace "</script" with "\x3C/script", and "<!--" with
-               //     "\x3C!--", which prevents confusing script block termination
-               //     semantics
+               //   * replace "<script" and "</script" with "\x3Cscript" and "\x3C/script"
+               //     (case insensitively), and "<!--" with "\x3C!--", which prevents
+               //     confusing script block termination semantics
                //
                // 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:
@@ -193,8 +196,8 @@ func jsValEscaper(args ...any) string {
                //     x//* error marshaling y:
                //          second line of error message */null
                errStr := err.Error()
+               errStr = string(scriptTagRe.ReplaceAll([]byte(errStr), []byte(`\x3C${1}script`)))
                errStr = strings.ReplaceAll(errStr, "*/", "* /")
-               errStr = strings.ReplaceAll(errStr, "</script", `\x3C/script`)
                errStr = strings.ReplaceAll(errStr, "<!--", `\x3C!--`)
                return fmt.Sprintf(" /* %s */null ", errStr)
        }
index 61a601b028b9f0f86193ac4d36d13eb8c2f7f925..b24fd69fddda6c25371e79bc09636c2c5a34a073 100644 (file)
@@ -2,8 +2,8 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
-//go:build !windows
-// +build !windows
+//go:build go1.13 && !windows
+// +build go1.13,!windows
 
 package template
 
@@ -110,7 +110,7 @@ func TestNextJsCtx(t *testing.T) {
 type jsonErrType struct{}
 
 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) {
@@ -163,7 +163,7 @@ func TestJSValEscaper(t *testing.T) {
                {"</script", `"\u003c/script"`, false},
                {"\U0001D11E", "\"\U0001D11E\"", false}, // or "\uD834\uDD1E"
                {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 {
@@ -221,8 +221,7 @@ func TestJSStrEscaper(t *testing.T) {
                {"<!--", `\u003c!--`},
                {"-->", `--\u003e`},
                // 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-`,
                },
                // Invalid UTF-8 sequence
index c320c4353a264a3e2c55717ad87e6f01dd67ef61..f3e629b8864f3474b2ad4e7d8f0bbc802663231f 100644 (file)
@@ -4,8 +4,8 @@
 
 // Tests for multiple-template execution, copied from text/template.
 
-//go:build !windows
-// +build !windows
+//go:build go1.13 && !windows
+// +build go1.13,!windows
 
 package template
 
@@ -268,7 +268,7 @@ func TestIssue19294(t *testing.T) {
        // by the contents of "stylesheet", but if the internal map associating
        // names with templates is built in the wrong order, the empty block
        // looks non-empty and this doesn't happen.
-       inlined := map[string]string{
+       var inlined = map[string]string{
                "stylesheet": `{{define "stylesheet"}}stylesheet{{end}}`,
                "xhtml":      `{{block "stylesheet" .}}{{end}}`,
        }
index 3a0f7463e0a5dd1e031b96b48ada9ae6ba50cd17..0838308a146e8559aa023a580d96279899250f38 100644 (file)
@@ -2,6 +2,9 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
+//go:build go1.13
+// +build go1.13
+
 package template_test
 
 import (
@@ -15,6 +18,7 @@ import (
 )
 
 func TestTemplateClone(t *testing.T) {
+
        orig := New("name")
        clone, err := orig.Clone()
        if err != nil {
index 9eb36558b009655fd117f536e34d1ebbab1b78e4..0bd38800f1a996bc2ec4f1652bbe67ca592af0ea 100644 (file)
@@ -2,8 +2,8 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
-//go:build !windows
-// +build !windows
+//go:build go1.13 && !windows
+// +build go1.13,!windows
 
 package template
 
@@ -43,6 +43,7 @@ func TestFindEndTag(t *testing.T) {
 }
 
 func BenchmarkTemplateSpecialTags(b *testing.B) {
+
        r := struct {
                Name, Gift string
        }{"Aunt Mildred", "bone china tea set"}
index c7e67c211e494d9e3fb21ea62c8f2d0cd78ace5d..72c8a4fe9be9251261c451aace19d4f382bace31 100644 (file)
@@ -2,8 +2,8 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
-//go:build !windows
-// +build !windows
+//go:build go1.13 && !windows
+// +build go1.13,!windows
 
 package template
 
index 7f6ad5cac4e39b603c0f59b797ff84c926a5bc5d..7b251b602237f3e9d8145d58cce432fd2e5a543e 100644 (file)
@@ -31,20 +31,17 @@ import (
 // If exec is not supported, testenv.SyscallIsNotSupported will return true
 // for the resulting error.
 func MustHaveExec(t testing.TB) {
-       tryExecOnce.Do(func() {
-               tryExecErr = tryExec()
-       })
-       if tryExecErr != nil {
-               t.Skipf("skipping test: cannot exec subprocess on %s/%s: %v", runtime.GOOS, runtime.GOARCH, tryExecErr)
+       if err := tryExec(); err != nil {
+               msg := fmt.Sprintf("cannot exec subprocess on %s/%s: %v", runtime.GOOS, runtime.GOARCH, err)
+               if t == nil {
+                       panic(msg)
+               }
+               t.Helper()
+               t.Skip("skipping test:", msg)
        }
 }
 
-var (
-       tryExecOnce sync.Once
-       tryExecErr  error
-)
-
-func tryExec() error {
+var tryExec = sync.OnceValue(func() error {
        switch runtime.GOOS {
        case "wasip1", "js", "ios":
        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
        // no-op flag to check for overall exec support.
-       exe, err := os.Executable()
+       exe, err := exePath()
        if err != nil {
                return fmt.Errorf("can't probe for exec support: %w", err)
        }
        cmd := exec.Command(exe, "-test.list=^$")
        cmd.Env = origEnv
        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
 
 // 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)
        }
        if err != nil {
+               t.Helper()
                t.Skipf("skipping test: %s: %s", path, err)
        }
 }
index e8577d2eae800a495a69c072a910364c70698ff9..a46c8f5a82d23187bb6d85d43436c67b567d3bb5 100644 (file)
@@ -12,7 +12,6 @@ package testenv
 
 import (
        "bytes"
-       "errors"
        "flag"
        "fmt"
        "os"
@@ -43,15 +42,22 @@ func Builder() string {
 
 // HasGoBuild reports whether the current system can build programs with “go build”
 // and then run them with os.StartProcess or exec.Command.
-// Modified by Hugo (not needed)
 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 (
-       goBuildOnce sync.Once
-       goBuildErr  error
-)
+var tryGoBuild = sync.OnceValue(func() error {
+       // Removed by Hugo, not used.
+       return nil
+})
 
 // MustHaveGoBuild checks that the current system can build programs with “go build”
 // and then run them with os.StartProcess or exec.Command.
@@ -63,7 +69,7 @@ func MustHaveGoBuild(t testing.TB) {
        }
        if !HasGoBuild() {
                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.
 func MustHaveGoRun(t testing.TB) {
        if !HasGoRun() {
+               t.Helper()
                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.
 func MustHaveParallelism(t testing.TB) {
        if !HasParallelism() {
+               t.Helper()
                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
 }
 
-var (
-       gorootOnce sync.Once
-       gorootPath string
-       gorootErr  error
-)
-
-func findGOROOT() (string, error) {
-       gorootOnce.Do(func() {
-               gorootPath = runtime.GOROOT()
-               if gorootPath != "" {
-                       // 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. 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).
+var findGOROOT = sync.OnceValues(func() (path string, err error) {
+       if path := runtime.GOROOT(); path != "" {
+               // If runtime.GOROOT() is non-empty, assume that it is valid.
                //
-               // 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.
+               // (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
+       }
 
-               cwd, err := os.Getwd()
-               if err != nil {
-                       gorootErr = fmt.Errorf("finding GOROOT: %w", err)
-                       return
+       // runtime.GOROOT doesn't know where GOROOT is (perhaps because the test
+       // 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'
+       // 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()
+       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")
                }
 
-               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
+                       continue // dir cannot be GOROOT/src if it doesn't end in "src".
+               }
 
-                       if base := filepath.Base(dir); base != "src" {
+               b, err := os.ReadFile(filepath.Join(dir, "go.mod"))
+               if err != nil {
+                       if os.IsNotExist(err) {
                                dir = parent
-                               continue // dir cannot be GOROOT/src if it doesn't end in "src".
-                       }
-
-                       b, err := os.ReadFile(filepath.Join(dir, "go.mod"))
-                       if err != nil {
-                               if os.IsNotExist(err) {
-                                       dir = parent
-                                       continue
-                               }
-                               gorootErr = fmt.Errorf("finding GOROOT: %w", err)
-                               return
+                               continue
                        }
-                       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 "", fmt.Errorf("finding GOROOT: %w", err)
+               }
+               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!
+                               return parent, nil
                        }
                }
-       })
-
-       return gorootPath, gorootErr
-}
+       }
+})
 
 // GOROOT reports the path to the directory containing the root of the Go
 // 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.
 func GoTool() (string, error) {
-       if !HasGoBuild() {
-               return "", errors.New("platform cannot run go tool")
-       }
-       goToolOnce.Do(func() {
-               goToolPath, goToolErr = exec.LookPath("go")
-       })
-       return goToolPath, goToolErr
+       // Removed by Hugo, not used.
+       return "", nil
 }
 
-var (
-       goToolOnce sync.Once
-       goToolPath string
-       goToolErr  error
-)
+var goTool = sync.OnceValues(func() (string, error) {
+       return exec.LookPath("go")
+})
 
-// HasSrc reports whether the entire source tree is available under GOROOT.
-func HasSrc() bool {
+// MustHaveSource checks that the entire source tree is available under GOROOT.
+// If not, it calls t.Skip with an explanation.
+func MustHaveSource(t testing.TB) {
        switch runtime.GOOS {
        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
@@ -263,41 +250,39 @@ func MustHaveExternalNetwork(t testing.TB) {
 
 // HasCGO reports whether the current system can use cgo.
 func HasCGO() bool {
-       hasCgoOnce.Do(func() {
-               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
+       return hasCgo()
 }
 
-var (
-       hasCgoOnce sync.Once
-       hasCgo     bool
-)
+var hasCgo = sync.OnceValue(func() bool {
+       goTool, err := goTool()
+       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.
 func MustHaveCGO(t testing.TB) {
        if !HasCGO() {
+               t.Helper()
                t.Skipf("skipping test: no cgo")
        }
 }
 
 // CanInternalLink reports whether the current system can link programs with
 // internal linking.
-// Modified by Hugo (not needed)
 func CanInternalLink(withCgo bool) bool {
+       // Removed by Hugo, not used.
        return false
 }
 
@@ -306,6 +291,7 @@ func CanInternalLink(withCgo bool) bool {
 // If not, MustInternalLink calls t.Skip with an explanation.
 func MustInternalLink(t testing.TB, withCgo bool) {
        if !CanInternalLink(withCgo) {
+               t.Helper()
                if withCgo && CanInternalLink(false) {
                        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
 // internal linking.
 // If not, MustInternalLinkPIE calls t.Skip with an explanation.
-// Modified by Hugo (not needed)
 func MustInternalLinkPIE(t testing.TB) {
+       // Removed by Hugo, not used.
 }
 
 // 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) {
+       // Removed by Hugo, not used.
 }
 
 // HasSymlink reports whether the current system can use os.Symlink.
@@ -338,6 +324,7 @@ func HasSymlink() bool {
 func MustHaveSymlink(t testing.TB) {
        ok, reason := hasSymlink()
        if !ok {
+               t.Helper()
                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.
 func MustHaveLink(t testing.TB) {
        if !HasLink() {
+               t.Helper()
                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")
 
 func SkipFlaky(t testing.TB, issue int) {
-       t.Helper()
        if !*flaky {
+               t.Helper()
                t.Skipf("skipping known flaky test without the -flaky flag; see golang.org/issue/%d", issue)
        }
 }
 
 func SkipFlakyNet(t testing.TB) {
-       t.Helper()
        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")
        }
 }
@@ -455,6 +443,6 @@ func SyscallIsNotSupported(err error) bool {
 // 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) {
+       // Removed by Hugo, not used.
 }
index 30e159a6ecd4a2d2902639e33378f3a603d36352..9dddea94d05798982aaf14e9e8dc2c63bb70dacf 100644 (file)
@@ -11,9 +11,10 @@ import (
        "os"
        "path/filepath"
        "runtime"
+       "sync"
 )
 
-func hasSymlink() (ok bool, reason string) {
+var hasSymlink = sync.OnceValues(func() (ok bool, reason string) {
        switch runtime.GOOS {
        case "plan9":
                return false, ""
@@ -43,4 +44,4 @@ func hasSymlink() (ok bool, reason string) {
        }
 
        return true, ""
-}
+})
index 4aabc00a61885369e52c683e6e050a2c92575e3c..afe1595319e16ff51a67a85c611d6ea0d855c6cf 100644 (file)
@@ -15,6 +15,7 @@ import (
 )
 
 func TestGoToolLocation(t *testing.T) {
+       t.Skip("This test is not relevant for Hugo")
        testenv.MustHaveGoBuild(t)
 
        var exeSuffix string
@@ -54,8 +55,83 @@ func TestGoToolLocation(t *testing.T) {
        }
 }
 
-// Modified by Hugo (not needed)
 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) {
index 4802b139518e3e24bc7a428168707028540daea6..eed53cdfb2b9065833373e1d74f5fa6e63221d27 100644 (file)
@@ -5,16 +5,14 @@
 package testenv
 
 import (
+       "errors"
        "os"
        "path/filepath"
        "sync"
        "syscall"
 )
 
-var symlinkOnce sync.Once
-var winSymlinkErr error
-
-func initWinHasSymlink() {
+var hasSymlink = sync.OnceValues(func() (bool, string) {
        tmpdir, err := os.MkdirTemp("", "symtest")
        if err != nil {
                panic("failed to create temp directory: " + err.Error())
@@ -22,26 +20,13 @@ func initWinHasSymlink() {
        defer os.RemoveAll(tmpdir)
 
        err = os.Symlink("target", filepath.Join(tmpdir, "symlink"))
-       if err != nil {
-               err = err.(*os.LinkError).Err
-               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:
+       switch {
+       case err == nil:
                return true, ""
-       case syscall.EWINDOWS:
+       case errors.Is(err, syscall.EWINDOWS):
                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, ""
-}
+})
index b3ffaabb153c9deac544aa00405321b2f340600e..7b63bb76ae4c5d0fa99a72490416d38fbdf730cd 100644 (file)
@@ -98,7 +98,8 @@ data, defined in detail in the corresponding sections that follow.
                        {{if pipeline}} T1 {{else}}{{if pipeline}} T0 {{end}}{{end}}
 
        {{range pipeline}} T1 {{end}}
-               The value of the pipeline must be an array, slice, map, or channel.
+               The value of the pipeline must be an array, slice, map, iter.Seq,
+               iter.Seq2, integer or channel.
                If the value of the pipeline has length zero, nothing is output;
                otherwise, dot is set to the successive elements of the array,
                slice, or map and T1 is executed. If the value is a map and the
@@ -106,7 +107,8 @@ data, defined in detail in the corresponding sections that follow.
                visited in sorted key order.
 
        {{range pipeline}} T1 {{else}} T0 {{end}}
-               The value of the pipeline must be an array, slice, map, or channel.
+               The value of the pipeline must be an array, slice, map, iter.Seq,
+               iter.Seq2, integer or channel.
                If the value of the pipeline has length zero, dot is unaffected and
                T0 is executed; otherwise, dot is set to the successive elements
                of the array, slice, or map and T1 is executed.
@@ -162,37 +164,55 @@ An argument is a simple value, denoted by one of the following.
          the host machine's ints are 32 or 64 bits.
        - The keyword nil, representing an untyped Go nil.
        - The character '.' (period):
+
                .
+
          The result is the value of dot.
        - A variable name, which is a (possibly empty) alphanumeric string
          preceded by a dollar sign, such as
+
                $piOver2
+
          or
+
                $
+
          The result is the value of the variable.
          Variables are described below.
        - The name of a field of the data, which must be a struct, preceded
          by a period, such as
+
                .Field
+
          The result is the value of the field. Field invocations may be
          chained:
+
            .Field1.Field2
+
          Fields can also be evaluated on variables, including chaining:
+
            $x.Field1.Field2
        - The name of a key of the data, which must be a map, preceded
          by a period, such as
+
                .Key
+
          The result is the map element value indexed by the key.
          Key invocations may be chained and combined with fields to any
          depth:
+
            .Field1.Key1.Field2.Key2
+
          Although the key must be an alphanumeric identifier, unlike with
          field names they do not need to start with an upper case letter.
          Keys can also be evaluated on variables, including chaining:
+
            $x.key1.key2
        - The name of a niladic method of the data, preceded by a period,
          such as
+
                .Method
+
          The result is the value of invoking the method with dot as the
          receiver, dot.Method(). Such a method must have one return value (of
          any type) or two return values, the second of which is an error.
@@ -200,16 +220,22 @@ An argument is a simple value, denoted by one of the following.
          and an error is returned to the caller as the value of Execute.
          Method invocations may be chained and combined with fields and keys
          to any depth:
+
            .Field1.Key1.Method1.Field2.Key2.Method2
+
          Methods can also be evaluated on variables, including chaining:
+
            $x.Method1.Field
        - The name of a niladic function, such as
+
                fun
+
          The result is the value of invoking the function, fun(). The return
          types and values behave as in methods. Functions and function
          names are described below.
        - A parenthesized instance of one the above, for grouping. The result
          may be accessed by a field or map key invocation.
+
                print (.F1 arg1) (.F2 arg2)
                (.StructValuedMethod "arg").Field
 
index 975ceea930629202cb028e5c3d97ea5d996de7bb..295a810b853e5f12071a2f5567a230199e0708f4 100644 (file)
@@ -35,7 +35,7 @@ Josie
                Name, Gift string
                Attended   bool
        }
-       recipients := []Recipient{
+       var recipients = []Recipient{
                {"Aunt Mildred", "bone china tea set", true},
                {"Uncle John", "moleskin pants", false},
                {"Cousin Rodney", "", false},
index 6534ee33158890d4b1a0cd1565db1bfc2e32fb6a..bc91e87f99ba263b59a4b7c1e137b2c7387134d5 100644 (file)
@@ -2,6 +2,9 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
+//go:build go1.13
+// +build go1.13
+
 package template_test
 
 import (
index bd8c82bd705934924ed0b760a936ab13c0102c08..f710a25ec2c37c36370647a18417755a66df9d11 100644 (file)
@@ -395,6 +395,22 @@ func (s *state) walkRange(dot reflect.Value, r *parse.RangeNode) {
                s.walk(elem, r.List)
        }
        switch val.Kind() {
+       case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
+               reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
+               if len(r.Pipe.Decl) > 1 {
+                       s.errorf("can't use %v to iterate over more than one variable", val)
+                       break
+               }
+               run := false
+               for v := range val.Seq() {
+                       run = true
+                       // Pass element as second value, as we do for channels.
+                       oneIteration(reflect.Value{}, v)
+               }
+               if !run {
+                       break
+               }
+               return
        case reflect.Array, reflect.Slice:
                if val.Len() == 0 {
                        break
@@ -434,6 +450,43 @@ func (s *state) walkRange(dot reflect.Value, r *parse.RangeNode) {
                return
        case reflect.Invalid:
                break // An invalid value is likely a nil map, etc. and acts like an empty map.
+       case reflect.Func:
+               if val.Type().CanSeq() {
+                       if len(r.Pipe.Decl) > 1 {
+                               s.errorf("can't use %v iterate over more than one variable", val)
+                               break
+                       }
+                       run := false
+                       for v := range val.Seq() {
+                               run = true
+                               // Pass element as second value,
+                               // as we do for channels.
+                               oneIteration(reflect.Value{}, v)
+                       }
+                       if !run {
+                               break
+                       }
+                       return
+               }
+               if val.Type().CanSeq2() {
+                       run := false
+                       for i, v := range val.Seq2() {
+                               run = true
+                               if len(r.Pipe.Decl) > 1 {
+                                       oneIteration(i, v)
+                               } else {
+                                       // If there is only one range variable,
+                                       // oneIteration will use the
+                                       // second value.
+                                       oneIteration(reflect.Value{}, i)
+                               }
+                       }
+                       if !run {
+                               break
+                       }
+                       return
+               }
+               fallthrough
        default:
                s.errorf("range can't iterate over %v", val)
        }
@@ -757,7 +810,7 @@ func (s *state) evalCallOld(dot, fun reflect.Value, isBuiltin bool, node parse.N
                                return v
                        }
                }
-               if final != missingVal {
+               if !final.Equal(missingVal) {
                        // The last argument to and/or is coming from
                        // the pipeline. We didn't short circuit on an earlier
                        // argument, so we are going to return this one.
@@ -803,7 +856,13 @@ func (s *state) evalCallOld(dot, fun reflect.Value, isBuiltin bool, node parse.N
        // Special case for the "call" builtin.
        // Insert the name of the callee function as the first argument.
        if isBuiltin && name == "call" {
-               calleeName := args[0].String()
+               var calleeName string
+               if len(args) == 0 {
+                       // final must be present or we would have errored out above.
+                       calleeName = final.String()
+               } else {
+                       calleeName = args[0].String()
+               }
                argv = append([]reflect.Value{reflect.ValueOf(calleeName)}, argv...)
                fun = reflect.ValueOf(call)
        }
index efbaa9eec47a8f714e67fc5395181ef4ed01fbe5..742e85605a7347727e54d32b1223d2c25707ba1e 100644 (file)
@@ -13,6 +13,7 @@ import (
        "flag"
        "fmt"
        "io"
+       "iter"
        "reflect"
        "strings"
        "sync"
@@ -412,6 +413,9 @@ var execTests = []execTest{
        {"Interface Call", `{{stringer .S}}`, "foozle", map[string]any{"S": bytes.NewBufferString("foozle")}, true},
        {".ErrFunc", "{{call .ErrFunc}}", "bla", tVal, true},
        {"call nil", "{{call nil}}", "", tVal, false},
+       {"empty call", "{{call}}", "", tVal, false},
+       {"empty call after pipe valid", "{{.ErrFunc | call}}", "bla", tVal, true},
+       {"empty call after pipe invalid", "{{1 | call}}", "", tVal, false},
 
        // Erroneous function calls (check args).
        {".BinaryFuncTooFew", "{{call .BinaryFunc `1`}}", "", tVal, false},
@@ -618,6 +622,30 @@ var execTests = []execTest{
        {"declare in range", "{{range $x := .PSI}}<{{$foo:=$x}}{{$x}}>{{end}}", "<21><22><23>", tVal, true},
        {"range count", `{{range $i, $x := count 5}}[{{$i}}]{{$x}}{{end}}`, "[0]a[1]b[2]c[3]d[4]e", tVal, true},
        {"range nil count", `{{range $i, $x := count 0}}{{else}}empty{{end}}`, "empty", tVal, true},
+       {"range iter.Seq[int]", `{{range $i := .}}{{$i}}{{end}}`, "01", fVal1(2), true},
+       {"i = range iter.Seq[int]", `{{$i := 0}}{{range $i = .}}{{$i}}{{end}}`, "01", fVal1(2), true},
+       {"range iter.Seq[int] over two var", `{{range $i, $c := .}}{{$c}}{{end}}`, "", fVal1(2), false},
+       {"i, c := range iter.Seq2[int,int]", `{{range $i, $c := .}}{{$i}}{{$c}}{{end}}`, "0112", fVal2(2), true},
+       {"i, c = range iter.Seq2[int,int]", `{{$i := 0}}{{$c := 0}}{{range $i, $c = .}}{{$i}}{{$c}}{{end}}`, "0112", fVal2(2), true},
+       {"i = range iter.Seq2[int,int]", `{{$i := 0}}{{range $i = .}}{{$i}}{{end}}`, "01", fVal2(2), true},
+       {"i := range iter.Seq2[int,int]", `{{range $i := .}}{{$i}}{{end}}`, "01", fVal2(2), true},
+       {"i,c,x range iter.Seq2[int,int]", `{{$i := 0}}{{$c := 0}}{{$x := 0}}{{range $i, $c = .}}{{$i}}{{$c}}{{end}}`, "0112", fVal2(2), true},
+       {"i,x range iter.Seq[int]", `{{$i := 0}}{{$x := 0}}{{range $i = .}}{{$i}}{{end}}`, "01", fVal1(2), true},
+       {"range iter.Seq[int] else", `{{range $i := .}}{{$i}}{{else}}empty{{end}}`, "empty", fVal1(0), true},
+       {"range iter.Seq2[int,int] else", `{{range $i := .}}{{$i}}{{else}}empty{{end}}`, "empty", fVal2(0), true},
+       {"range int8", rangeTestInt, rangeTestData[int8](), int8(5), true},
+       {"range int16", rangeTestInt, rangeTestData[int16](), int16(5), true},
+       {"range int32", rangeTestInt, rangeTestData[int32](), int32(5), true},
+       {"range int64", rangeTestInt, rangeTestData[int64](), int64(5), true},
+       {"range int", rangeTestInt, rangeTestData[int](), int(5), true},
+       {"range uint8", rangeTestInt, rangeTestData[uint8](), uint8(5), true},
+       {"range uint16", rangeTestInt, rangeTestData[uint16](), uint16(5), true},
+       {"range uint32", rangeTestInt, rangeTestData[uint32](), uint32(5), true},
+       {"range uint64", rangeTestInt, rangeTestData[uint64](), uint64(5), true},
+       {"range uint", rangeTestInt, rangeTestData[uint](), uint(5), true},
+       {"range uintptr", rangeTestInt, rangeTestData[uintptr](), uintptr(5), true},
+       {"range uintptr(0)", `{{range $v := .}}{{print $v}}{{else}}empty{{end}}`, "empty", uintptr(0), true},
+       {"range 5", `{{range $v := 5}}{{printf "%T%d" $v $v}}{{end}}`, rangeTestData[int](), nil, true},
 
        // Cute examples.
        {"or as if true", `{{or .SI "slice is empty"}}`, "[3 4 5]", tVal, true},
@@ -722,6 +750,37 @@ var execTests = []execTest{
        {"issue60801", "{{$k := 0}}{{$v := 0}}{{range $k, $v = .AI}}{{$k}}={{$v}} {{end}}", "0=3 1=4 2=5 ", tVal, true},
 }
 
+func fVal1(i int) iter.Seq[int] {
+       return func(yield func(int) bool) {
+               for v := range i {
+                       if !yield(v) {
+                               break
+                       }
+               }
+       }
+}
+
+func fVal2(i int) iter.Seq2[int, int] {
+       return func(yield func(int, int) bool) {
+               for v := range i {
+                       if !yield(v, v+1) {
+                               break
+                       }
+               }
+       }
+}
+
+const rangeTestInt = `{{range $v := .}}{{printf "%T%d" $v $v}}{{end}}`
+
+func rangeTestData[T int | int8 | int16 | int32 | int64 | uint | uint8 | uint16 | uint32 | uint64 | uintptr]() string {
+       I := T(5)
+       var buf strings.Builder
+       for i := T(0); i < I; i++ {
+               fmt.Fprintf(&buf, "%T%d", i, i)
+       }
+       return buf.String()
+}
+
 func zeroArgs() string {
        return "zeroArgs"
 }
index 36962c444b8f85f972eba909dd87b59b770dabbf..d179cb8c92db4274b4557d48426a1e5ed4bc7188 100644 (file)
@@ -304,14 +304,14 @@ func (s *state) evalCall(dot, fun reflect.Value, isBuiltin bool, node parse.Node
                        }
                }()
        }
+
        if args != nil {
                args = args[1:] // Zeroth arg is function name/node; not passed to function.
        }
-
        typ := fun.Type()
-       numFirst := len(first)
+       numFirst := len(first)        // Added for Hugo
        numIn := len(args) + numFirst // Added for Hugo
-       if final != missingVal {
+       if !isMissing(final) {
                numIn++
        }
        numFixed := len(args) + len(first) // Adjusted for Hugo
@@ -346,7 +346,7 @@ func (s *state) evalCall(dot, fun reflect.Value, isBuiltin bool, node parse.Node
                                return v
                        }
                }
-               if final != missingVal {
+               if !final.Equal(missingVal) {
                        // The last argument to and/or is coming from
                        // the pipeline. We didn't short circuit on an earlier
                        // argument, so we are going to return this one.
@@ -373,7 +373,7 @@ func (s *state) evalCall(dot, fun reflect.Value, isBuiltin bool, node parse.Node
                }
        }
        // Add final value if necessary.
-       if final != missingVal {
+       if !isMissing(final) {
                t := typ.In(typ.NumIn() - 1)
                if typ.IsVariadic() {
                        if numIn-1 < numFixed {
@@ -392,7 +392,13 @@ func (s *state) evalCall(dot, fun reflect.Value, isBuiltin bool, node parse.Node
        // Special case for the "call" builtin.
        // Insert the name of the callee function as the first argument.
        if isBuiltin && name == "call" {
-               calleeName := args[0].String()
+               var calleeName string
+               if len(args) == 0 {
+                       // final must be present or we would have errored out above.
+                       calleeName = final.String()
+               } else {
+                       calleeName = args[0].String()
+               }
                argv = append([]reflect.Value{reflect.ValueOf(calleeName)}, argv...)
                fun = reflect.ValueOf(call)
        }
index 63418cd90ce3ba2836744c6d733df430ef1b599e..23f6a31fade53afb7188aed40776a3147b497d6a 100644 (file)
@@ -2,16 +2,18 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
+//go:build go1.13
+// +build go1.13
+
 package template_test
 
 import (
        "bytes"
+       "github.com/gohugoio/hugo/tpl/internal/go_templates/testenv"
        "os"
        "os/exec"
        "path/filepath"
        "testing"
-
-       "github.com/gohugoio/hugo/tpl/internal/go_templates/testenv"
 )
 
 // Issue 36021: verify that text/template doesn't prevent the linker from removing
@@ -42,7 +44,7 @@ func main() {
 `
        td := t.TempDir()
 
-       if err := os.WriteFile(filepath.Join(td, "x.go"), []byte(prog), 0o644); err != nil {
+       if err := os.WriteFile(filepath.Join(td, "x.go"), []byte(prog), 0644); err != nil {
                t.Fatal(err)
        }
        cmd := exec.Command(testenv.GoToolPath(t), "build", "-o", "x.exe", "x.go")
index 6358383e415a3baf2b6bb61a3ff25080aa86acdf..6e383c131f8367abb691eb202864615fbff4f92e 100644 (file)
@@ -2,8 +2,8 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
-//go:build !windows
-// +build !windows
+//go:build go1.13 && !windows
+// +build go1.13,!windows
 
 package template
 
@@ -11,11 +11,10 @@ package template
 
 import (
        "fmt"
+       "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate/parse"
        "os"
        "strings"
        "testing"
-
-       "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate/parse"
 )
 
 const (
@@ -32,32 +31,22 @@ type multiParseTest struct {
 }
 
 var multiParseTests = []multiParseTest{
-       {
-               "empty", "", noError,
-               nil,
+       {"empty", "", noError,
                nil,
-       },
-       {
-               "one", `{{define "foo"}} FOO {{end}}`, noError,
+               nil},
+       {"one", `{{define "foo"}} FOO {{end}}`, noError,
                []string{"foo"},
-               []string{" FOO "},
-       },
-       {
-               "two", `{{define "foo"}} FOO {{end}}{{define "bar"}} BAR {{end}}`, noError,
+               []string{" FOO "}},
+       {"two", `{{define "foo"}} FOO {{end}}{{define "bar"}} BAR {{end}}`, noError,
                []string{"foo", "bar"},
-               []string{" FOO ", " BAR "},
-       },
+               []string{" FOO ", " BAR "}},
        // errors
-       {
-               "missing end", `{{define "foo"}} FOO `, hasError,
-               nil,
-               nil,
-       },
-       {
-               "malformed name", `{{define "foo}} FOO `, hasError,
+       {"missing end", `{{define "foo"}} FOO `, hasError,
                nil,
+               nil},
+       {"malformed name", `{{define "foo}} FOO `, hasError,
                nil,
-       },
+               nil},
 }
 
 func TestMultiParse(t *testing.T) {
@@ -443,7 +432,7 @@ func TestIssue19294(t *testing.T) {
        // by the contents of "stylesheet", but if the internal map associating
        // names with templates is built in the wrong order, the empty block
        // looks non-empty and this doesn't happen.
-       inlined := map[string]string{
+       var inlined = map[string]string{
                "stylesheet": `{{define "stylesheet"}}stylesheet{{end}}`,
                "xhtml":      `{{block "stylesheet" .}}{{end}}`,
        }
index 70fc86b63cccdc2deea9b1bc5dc7d8f523706f4f..a00f48e658ab64827ce51441787ac5af0f6921c2 100644 (file)
@@ -352,6 +352,7 @@ func lexComment(l *lexer) stateFn {
        if !delim {
                return l.errorf("comment ends before closing delimiter")
        }
+       l.line += strings.Count(l.input[l.start:l.pos], "\n")
        i := l.thisItem(itemComment)
        if trimSpace {
                l.pos += trimMarkerLen
index 11e88792e3e1888c94d4880fd129464320a97ef2..b1ea699673be33c405a851e00c3c2eea8b828a53 100644 (file)
@@ -548,6 +548,16 @@ var lexPosTests = []lexTest{
                {itemRightDelim, 11, "}}", 2},
                {itemEOF, 13, "", 2},
        }},
+       {"longcomment", "{{/*\n*/}}\n{{undefinedFunction \"test\"}}", []item{
+               {itemComment, 2, "/*\n*/", 1},
+               {itemText, 9, "\n", 2},
+               {itemLeftDelim, 10, "{{", 3},
+               {itemIdentifier, 12, "undefinedFunction", 3},
+               {itemSpace, 29, " ", 3},
+               {itemString, 30, "\"test\"", 3},
+               {itemRightDelim, 36, "}}", 3},
+               {itemEOF, 38, "", 3},
+       }},
 }
 
 // The other tests don't check position, to make the test cases easier to construct.
index 80e7f53faddcd623e8b121af07e619b8a0533cc8..47951a9c9d788031763db642195533f53d1a7dce 100644 (file)
@@ -2,6 +2,9 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
+//go:build go1.13
+// +build go1.13
+
 package parse
 
 import (
@@ -33,9 +36,9 @@ var numberTests = []numberTest{
        {"7_3", true, true, true, false, 73, 73, 73, 0},
        {"0b10_010_01", true, true, true, false, 73, 73, 73, 0},
        {"0B10_010_01", true, true, true, false, 73, 73, 73, 0},
-       {"073", true, true, true, false, 0o73, 0o73, 0o73, 0},
-       {"0o73", true, true, true, false, 0o73, 0o73, 0o73, 0},
-       {"0O73", true, true, true, false, 0o73, 0o73, 0o73, 0},
+       {"073", true, true, true, false, 073, 073, 073, 0},
+       {"0o73", true, true, true, false, 073, 073, 073, 0},
+       {"0O73", true, true, true, false, 073, 073, 073, 0},
        {"0x73", true, true, true, false, 0x73, 0x73, 0x73, 0},
        {"0X73", true, true, true, false, 0x73, 0x73, 0x73, 0},
        {"0x7_3", true, true, true, false, 0x73, 0x73, 0x73, 0},
@@ -61,7 +64,7 @@ var numberTests = []numberTest{
        {"-12+0i", true, false, true, true, -12, 0, -12, -12},
        {"13+0i", true, true, true, true, 13, 13, 13, 13},
        // funny bases
-       {"0123", true, true, true, false, 0o123, 0o123, 0o123, 0},
+       {"0123", true, true, true, false, 0123, 0123, 0123, 0},
        {"-0x0", true, true, true, false, 0, 0, 0, 0},
        {"0xdeadbeef", true, true, true, false, 0xdeadbeef, 0xdeadbeef, 0xdeadbeef, 0},
        // character constants
@@ -176,150 +179,78 @@ const (
 )
 
 var parseTests = []parseTest{
-       {
-               "empty", "", noError,
-               ``,
-       },
-       {
-               "comment", "{{/*\n\n\n*/}}", noError,
-               ``,
-       },
-       {
-               "spaces", " \t\n", noError,
-               `" \t\n"`,
-       },
-       {
-               "text", "some text", noError,
-               `"some text"`,
-       },
-       {
-               "emptyAction", "{{}}", hasError,
-               `{{}}`,
-       },
-       {
-               "field", "{{.X}}", noError,
-               `{{.X}}`,
-       },
-       {
-               "simple command", "{{printf}}", noError,
-               `{{printf}}`,
-       },
-       {
-               "$ invocation", "{{$}}", noError,
-               "{{$}}",
-       },
-       {
-               "variable invocation", "{{with $x := 3}}{{$x 23}}{{end}}", noError,
-               "{{with $x := 3}}{{$x 23}}{{end}}",
-       },
-       {
-               "variable with fields", "{{$.I}}", noError,
-               "{{$.I}}",
-       },
-       {
-               "multi-word command", "{{printf `%d` 23}}", noError,
-               "{{printf `%d` 23}}",
-       },
-       {
-               "pipeline", "{{.X|.Y}}", noError,
-               `{{.X | .Y}}`,
-       },
-       {
-               "pipeline with decl", "{{$x := .X|.Y}}", noError,
-               `{{$x := .X | .Y}}`,
-       },
-       {
-               "nested pipeline", "{{.X (.Y .Z) (.A | .B .C) (.E)}}", noError,
-               `{{.X (.Y .Z) (.A | .B .C) (.E)}}`,
-       },
-       {
-               "field applied to parentheses", "{{(.Y .Z).Field}}", noError,
-               `{{(.Y .Z).Field}}`,
-       },
-       {
-               "simple if", "{{if .X}}hello{{end}}", noError,
-               `{{if .X}}"hello"{{end}}`,
-       },
-       {
-               "if with else", "{{if .X}}true{{else}}false{{end}}", noError,
-               `{{if .X}}"true"{{else}}"false"{{end}}`,
-       },
-       {
-               "if with else if", "{{if .X}}true{{else if .Y}}false{{end}}", noError,
-               `{{if .X}}"true"{{else}}{{if .Y}}"false"{{end}}{{end}}`,
-       },
-       {
-               "if else chain", "+{{if .X}}X{{else if .Y}}Y{{else if .Z}}Z{{end}}+", noError,
-               `"+"{{if .X}}"X"{{else}}{{if .Y}}"Y"{{else}}{{if .Z}}"Z"{{end}}{{end}}{{end}}"+"`,
-       },
-       {
-               "simple range", "{{range .X}}hello{{end}}", noError,
-               `{{range .X}}"hello"{{end}}`,
-       },
-       {
-               "chained field range", "{{range .X.Y.Z}}hello{{end}}", noError,
-               `{{range .X.Y.Z}}"hello"{{end}}`,
-       },
-       {
-               "nested range", "{{range .X}}hello{{range .Y}}goodbye{{end}}{{end}}", noError,
-               `{{range .X}}"hello"{{range .Y}}"goodbye"{{end}}{{end}}`,
-       },
-       {
-               "range with else", "{{range .X}}true{{else}}false{{end}}", noError,
-               `{{range .X}}"true"{{else}}"false"{{end}}`,
-       },
-       {
-               "range over pipeline", "{{range .X|.M}}true{{else}}false{{end}}", noError,
-               `{{range .X | .M}}"true"{{else}}"false"{{end}}`,
-       },
-       {
-               "range []int", "{{range .SI}}{{.}}{{end}}", noError,
-               `{{range .SI}}{{.}}{{end}}`,
-       },
-       {
-               "range 1 var", "{{range $x := .SI}}{{.}}{{end}}", noError,
-               `{{range $x := .SI}}{{.}}{{end}}`,
-       },
-       {
-               "range 2 vars", "{{range $x, $y := .SI}}{{.}}{{end}}", noError,
-               `{{range $x, $y := .SI}}{{.}}{{end}}`,
-       },
-       {
-               "range with break", "{{range .SI}}{{.}}{{break}}{{end}}", noError,
-               `{{range .SI}}{{.}}{{break}}{{end}}`,
-       },
-       {
-               "range with continue", "{{range .SI}}{{.}}{{continue}}{{end}}", noError,
-               `{{range .SI}}{{.}}{{continue}}{{end}}`,
-       },
-       {
-               "constants", "{{range .SI 1 -3.2i true false 'a' nil}}{{end}}", noError,
-               `{{range .SI 1 -3.2i true false 'a' nil}}{{end}}`,
-       },
-       {
-               "template", "{{template `x`}}", noError,
-               `{{template "x"}}`,
-       },
-       {
-               "template with arg", "{{template `x` .Y}}", noError,
-               `{{template "x" .Y}}`,
-       },
-       {
-               "with", "{{with .X}}hello{{end}}", noError,
-               `{{with .X}}"hello"{{end}}`,
-       },
-       {
-               "with with else", "{{with .X}}hello{{else}}goodbye{{end}}", noError,
-               `{{with .X}}"hello"{{else}}"goodbye"{{end}}`,
-       },
-       {
-               "with with else with", "{{with .X}}hello{{else with .Y}}goodbye{{end}}", noError,
-               `{{with .X}}"hello"{{else}}{{with .Y}}"goodbye"{{end}}{{end}}`,
-       },
-       {
-               "with else chain", "{{with .X}}X{{else with .Y}}Y{{else with .Z}}Z{{end}}", noError,
-               `{{with .X}}"X"{{else}}{{with .Y}}"Y"{{else}}{{with .Z}}"Z"{{end}}{{end}}{{end}}`,
-       },
+       {"empty", "", noError,
+               ``},
+       {"comment", "{{/*\n\n\n*/}}", noError,
+               ``},
+       {"spaces", " \t\n", noError,
+               `" \t\n"`},
+       {"text", "some text", noError,
+               `"some text"`},
+       {"emptyAction", "{{}}", hasError,
+               `{{}}`},
+       {"field", "{{.X}}", noError,
+               `{{.X}}`},
+       {"simple command", "{{printf}}", noError,
+               `{{printf}}`},
+       {"$ invocation", "{{$}}", noError,
+               "{{$}}"},
+       {"variable invocation", "{{with $x := 3}}{{$x 23}}{{end}}", noError,
+               "{{with $x := 3}}{{$x 23}}{{end}}"},
+       {"variable with fields", "{{$.I}}", noError,
+               "{{$.I}}"},
+       {"multi-word command", "{{printf `%d` 23}}", noError,
+               "{{printf `%d` 23}}"},
+       {"pipeline", "{{.X|.Y}}", noError,
+               `{{.X | .Y}}`},
+       {"pipeline with decl", "{{$x := .X|.Y}}", noError,
+               `{{$x := .X | .Y}}`},
+       {"nested pipeline", "{{.X (.Y .Z) (.A | .B .C) (.E)}}", noError,
+               `{{.X (.Y .Z) (.A | .B .C) (.E)}}`},
+       {"field applied to parentheses", "{{(.Y .Z).Field}}", noError,
+               `{{(.Y .Z).Field}}`},
+       {"simple if", "{{if .X}}hello{{end}}", noError,
+               `{{if .X}}"hello"{{end}}`},
+       {"if with else", "{{if .X}}true{{else}}false{{end}}", noError,
+               `{{if .X}}"true"{{else}}"false"{{end}}`},
+       {"if with else if", "{{if .X}}true{{else if .Y}}false{{end}}", noError,
+               `{{if .X}}"true"{{else}}{{if .Y}}"false"{{end}}{{end}}`},
+       {"if else chain", "+{{if .X}}X{{else if .Y}}Y{{else if .Z}}Z{{end}}+", noError,
+               `"+"{{if .X}}"X"{{else}}{{if .Y}}"Y"{{else}}{{if .Z}}"Z"{{end}}{{end}}{{end}}"+"`},
+       {"simple range", "{{range .X}}hello{{end}}", noError,
+               `{{range .X}}"hello"{{end}}`},
+       {"chained field range", "{{range .X.Y.Z}}hello{{end}}", noError,
+               `{{range .X.Y.Z}}"hello"{{end}}`},
+       {"nested range", "{{range .X}}hello{{range .Y}}goodbye{{end}}{{end}}", noError,
+               `{{range .X}}"hello"{{range .Y}}"goodbye"{{end}}{{end}}`},
+       {"range with else", "{{range .X}}true{{else}}false{{end}}", noError,
+               `{{range .X}}"true"{{else}}"false"{{end}}`},
+       {"range over pipeline", "{{range .X|.M}}true{{else}}false{{end}}", noError,
+               `{{range .X | .M}}"true"{{else}}"false"{{end}}`},
+       {"range []int", "{{range .SI}}{{.}}{{end}}", noError,
+               `{{range .SI}}{{.}}{{end}}`},
+       {"range 1 var", "{{range $x := .SI}}{{.}}{{end}}", noError,
+               `{{range $x := .SI}}{{.}}{{end}}`},
+       {"range 2 vars", "{{range $x, $y := .SI}}{{.}}{{end}}", noError,
+               `{{range $x, $y := .SI}}{{.}}{{end}}`},
+       {"range with break", "{{range .SI}}{{.}}{{break}}{{end}}", noError,
+               `{{range .SI}}{{.}}{{break}}{{end}}`},
+       {"range with continue", "{{range .SI}}{{.}}{{continue}}{{end}}", noError,
+               `{{range .SI}}{{.}}{{continue}}{{end}}`},
+       {"constants", "{{range .SI 1 -3.2i true false 'a' nil}}{{end}}", noError,
+               `{{range .SI 1 -3.2i true false 'a' nil}}{{end}}`},
+       {"template", "{{template `x`}}", noError,
+               `{{template "x"}}`},
+       {"template with arg", "{{template `x` .Y}}", noError,
+               `{{template "x" .Y}}`},
+       {"with", "{{with .X}}hello{{end}}", noError,
+               `{{with .X}}"hello"{{end}}`},
+       {"with with else", "{{with .X}}hello{{else}}goodbye{{end}}", noError,
+               `{{with .X}}"hello"{{else}}"goodbye"{{end}}`},
+       {"with with else with", "{{with .X}}hello{{else with .Y}}goodbye{{end}}", noError,
+               `{{with .X}}"hello"{{else}}{{with .Y}}"goodbye"{{end}}{{end}}`},
+       {"with else chain", "{{with .X}}X{{else with .Y}}Y{{else with .Z}}Z{{end}}", noError,
+               `{{with .X}}"X"{{else}}{{with .Y}}"Y"{{else}}{{with .Z}}"Z"{{end}}{{end}}{{end}}`},
        // Trimming spaces.
        {"trim left", "x \r\n\t{{- 3}}", noError, `"x"{{3}}`},
        {"trim right", "{{3 -}}\n\n\ty", noError, `{{3}}"y"`},
@@ -328,24 +259,18 @@ var parseTests = []parseTest{
        {"comment trim left", "x \r\n\t{{- /* hi */}}", noError, `"x"`},
        {"comment trim right", "{{/* hi */ -}}\n\n\ty", noError, `"y"`},
        {"comment trim left and right", "x \r\n\t{{- /* */ -}}\n\n\ty", noError, `"x""y"`},
-       {
-               "block definition", `{{block "foo" .}}hello{{end}}`, noError,
-               `{{template "foo" .}}`,
-       },
+       {"block definition", `{{block "foo" .}}hello{{end}}`, noError,
+               `{{template "foo" .}}`},
 
        {"newline in assignment", "{{ $x \n := \n 1 \n }}", noError, "{{$x := 1}}"},
        {"newline in empty action", "{{\n}}", hasError, "{{\n}}"},
        {"newline in pipeline", "{{\n\"x\"\n|\nprintf\n}}", noError, `{{"x" | printf}}`},
        {"newline in comment", "{{/*\nhello\n*/}}", noError, ""},
        {"newline in comment", "{{-\n/*\nhello\n*/\n-}}", noError, ""},
-       {
-               "spaces around continue", "{{range .SI}}{{.}}{{ continue }}{{end}}", noError,
-               `{{range .SI}}{{.}}{{continue}}{{end}}`,
-       },
-       {
-               "spaces around break", "{{range .SI}}{{.}}{{ break }}{{end}}", noError,
-               `{{range .SI}}{{.}}{{break}}{{end}}`,
-       },
+       {"spaces around continue", "{{range .SI}}{{.}}{{ continue }}{{end}}", noError,
+               `{{range .SI}}{{.}}{{continue}}{{end}}`},
+       {"spaces around break", "{{range .SI}}{{.}}{{ break }}{{end}}", noError,
+               `{{range .SI}}{{.}}{{break}}{{end}}`},
 
        // Errors.
        {"unclosed action", "hello{{range", hasError, ""},
@@ -487,7 +412,7 @@ func TestKeywordsAndFuncs(t *testing.T) {
        {
                // 'break' is a defined function, don't treat it as a keyword: it should
                // accept an argument successfully.
-               funcsWithKeywordFunc := map[string]any{
+               var funcsWithKeywordFunc = map[string]any{
                        "break": func(in any) any { return in },
                }
                tmpl, err := New("").Parse(inp, "", "", make(map[string]*Tree), funcsWithKeywordFunc)
@@ -574,168 +499,104 @@ func TestErrorContextWithTreeCopy(t *testing.T) {
 // All failures, and the result is a string that must appear in the error message.
 var errorTests = []parseTest{
        // Check line numbers are accurate.
-       {
-               "unclosed1",
+       {"unclosed1",
                "line1\n{{",
-               hasError, `unclosed1:2: unclosed action`,
-       },
-       {
-               "unclosed2",
+               hasError, `unclosed1:2: unclosed action`},
+       {"unclosed2",
                "line1\n{{define `x`}}line2\n{{",
-               hasError, `unclosed2:3: unclosed action`,
-       },
-       {
-               "unclosed3",
+               hasError, `unclosed2:3: unclosed action`},
+       {"unclosed3",
                "line1\n{{\"x\"\n\"y\"\n",
-               hasError, `unclosed3:4: unclosed action started at unclosed3:2`,
-       },
-       {
-               "unclosed4",
+               hasError, `unclosed3:4: unclosed action started at unclosed3:2`},
+       {"unclosed4",
                "{{\n\n\n\n\n",
-               hasError, `unclosed4:6: unclosed action started at unclosed4:1`,
-       },
-       {
-               "var1",
+               hasError, `unclosed4:6: unclosed action started at unclosed4:1`},
+       {"var1",
                "line1\n{{\nx\n}}",
-               hasError, `var1:3: function "x" not defined`,
-       },
+               hasError, `var1:3: function "x" not defined`},
        // Specific errors.
-       {
-               "function",
+       {"function",
                "{{foo}}",
-               hasError, `function "foo" not defined`,
-       },
-       {
-               "comment1",
+               hasError, `function "foo" not defined`},
+       {"comment1",
                "{{/*}}",
-               hasError, `comment1:1: unclosed comment`,
-       },
-       {
-               "comment2",
+               hasError, `comment1:1: unclosed comment`},
+       {"comment2",
                "{{/*\nhello\n}}",
-               hasError, `comment2:1: unclosed comment`,
-       },
-       {
-               "lparen",
+               hasError, `comment2:1: unclosed comment`},
+       {"lparen",
                "{{.X (1 2 3}}",
-               hasError, `unclosed left paren`,
-       },
-       {
-               "rparen",
+               hasError, `unclosed left paren`},
+       {"rparen",
                "{{.X 1 2 3 ) }}",
-               hasError, "unexpected right paren",
-       },
-       {
-               "rparen2",
+               hasError, "unexpected right paren"},
+       {"rparen2",
                "{{(.X 1 2 3",
-               hasError, `unclosed action`,
-       },
-       {
-               "space",
+               hasError, `unclosed action`},
+       {"space",
                "{{`x`3}}",
-               hasError, `in operand`,
-       },
-       {
-               "idchar",
+               hasError, `in operand`},
+       {"idchar",
                "{{a#}}",
-               hasError, `'#'`,
-       },
-       {
-               "charconst",
+               hasError, `'#'`},
+       {"charconst",
                "{{'a}}",
-               hasError, `unterminated character constant`,
-       },
-       {
-               "stringconst",
+               hasError, `unterminated character constant`},
+       {"stringconst",
                `{{"a}}`,
-               hasError, `unterminated quoted string`,
-       },
-       {
-               "rawstringconst",
+               hasError, `unterminated quoted string`},
+       {"rawstringconst",
                "{{`a}}",
-               hasError, `unterminated raw quoted string`,
-       },
-       {
-               "number",
+               hasError, `unterminated raw quoted string`},
+       {"number",
                "{{0xi}}",
-               hasError, `number syntax`,
-       },
-       {
-               "multidefine",
+               hasError, `number syntax`},
+       {"multidefine",
                "{{define `a`}}a{{end}}{{define `a`}}b{{end}}",
-               hasError, `multiple definition of template`,
-       },
-       {
-               "eof",
+               hasError, `multiple definition of template`},
+       {"eof",
                "{{range .X}}",
-               hasError, `unexpected EOF`,
-       },
-       {
-               "variable",
+               hasError, `unexpected EOF`},
+       {"variable",
                // Declare $x so it's defined, to avoid that error, and then check we don't parse a declaration.
                "{{$x := 23}}{{with $x.y := 3}}{{$x 23}}{{end}}",
-               hasError, `unexpected ":="`,
-       },
-       {
-               "multidecl",
+               hasError, `unexpected ":="`},
+       {"multidecl",
                "{{$a,$b,$c := 23}}",
-               hasError, `too many declarations`,
-       },
-       {
-               "undefvar",
+               hasError, `too many declarations`},
+       {"undefvar",
                "{{$a}}",
-               hasError, `undefined variable`,
-       },
-       {
-               "wrongdot",
+               hasError, `undefined variable`},
+       {"wrongdot",
                "{{true.any}}",
-               hasError, `unexpected . after term`,
-       },
-       {
-               "wrongpipeline",
+               hasError, `unexpected . after term`},
+       {"wrongpipeline",
                "{{12|false}}",
-               hasError, `non executable command in pipeline`,
-       },
-       {
-               "emptypipeline",
+               hasError, `non executable command in pipeline`},
+       {"emptypipeline",
                `{{ ( ) }}`,
-               hasError, `missing value for parenthesized pipeline`,
-       },
-       {
-               "multilinerawstring",
+               hasError, `missing value for parenthesized pipeline`},
+       {"multilinerawstring",
                "{{ $v := `\n` }} {{",
-               hasError, `multilinerawstring:2: unclosed action`,
-       },
-       {
-               "rangeundefvar",
+               hasError, `multilinerawstring:2: unclosed action`},
+       {"rangeundefvar",
                "{{range $k}}{{end}}",
-               hasError, `undefined variable`,
-       },
-       {
-               "rangeundefvars",
+               hasError, `undefined variable`},
+       {"rangeundefvars",
                "{{range $k, $v}}{{end}}",
-               hasError, `undefined variable`,
-       },
-       {
-               "rangemissingvalue1",
+               hasError, `undefined variable`},
+       {"rangemissingvalue1",
                "{{range $k,}}{{end}}",
-               hasError, `missing value for range`,
-       },
-       {
-               "rangemissingvalue2",
+               hasError, `missing value for range`},
+       {"rangemissingvalue2",
                "{{range $k, $v := }}{{end}}",
-               hasError, `missing value for range`,
-       },
-       {
-               "rangenotvariable1",
+               hasError, `missing value for range`},
+       {"rangenotvariable1",
                "{{range $k, .}}{{end}}",
-               hasError, `range can only initialize variables`,
-       },
-       {
-               "rangenotvariable2",
+               hasError, `range can only initialize variables`},
+       {"rangenotvariable2",
                "{{range $k, 123 := .}}{{end}}",
-               hasError, `range can only initialize variables`,
-       },
+               hasError, `range can only initialize variables`},
 }
 
 func TestErrors(t *testing.T) {
index 536932a60bdc057defc6b0803d6d4501594c4221..0be64eee2d902a0a7081e55ccc125307cf2621d0 100644 (file)
@@ -6,6 +6,7 @@ package template
 
 import (
        "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate/parse"
+       "maps"
        "reflect"
        "sync"
 )
@@ -102,12 +103,8 @@ func (t *Template) Clone() (*Template, error) {
        }
        t.muFuncs.RLock()
        defer t.muFuncs.RUnlock()
-       for k, v := range t.parseFuncs {
-               nt.parseFuncs[k] = v
-       }
-       for k, v := range t.execFuncs {
-               nt.execFuncs[k] = v
-       }
+       maps.Copy(nt.parseFuncs, t.parseFuncs)
+       maps.Copy(nt.execFuncs, t.execFuncs)
        return nt, nil
 }