Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit d7d8cd2

Browse files
committedFeb 12, 2025
Upgrade to Go 1.24
Fixes gohugoio#13381
1 parent 9b5f786 commit d7d8cd2

37 files changed

+655
-569
lines changed
 

‎.circleci/config.yml

+2-2
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ parameters:
44
defaults: &defaults
55
resource_class: large
66
docker:
7-
- image: bepsays/ci-hugoreleaser:1.22301.20401
7+
- image: bepsays/ci-hugoreleaser:1.22400.20000
88
environment: &buildenv
99
GOMODCACHE: /root/project/gomodcache
1010
version: 2
@@ -58,7 +58,7 @@ jobs:
5858
environment:
5959
<<: [*buildenv]
6060
docker:
61-
- image: bepsays/ci-hugoreleaser-linux-arm64:1.22301.20401
61+
- image: bepsays/ci-hugoreleaser-linux-arm64:1.22400.20000
6262
steps:
6363
- *restore-cache
6464
- &attach-workspace

‎.github/workflows/test.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ jobs:
1616
test:
1717
strategy:
1818
matrix:
19-
go-version: [1.22.x, 1.23.x]
19+
go-version: [1.23.x, 1.24.x]
2020
os: [ubuntu-latest, windows-latest] # macos disabled for now because of disk space issues.
2121
runs-on: ${{ matrix.os }}
2222
steps:

‎go.mod

+1-1
Original file line numberDiff line numberDiff line change
@@ -170,4 +170,4 @@ require (
170170
software.sslmate.com/src/go-pkcs12 v0.2.0 // indirect
171171
)
172172

173-
go 1.22.6
173+
go 1.23

‎scripts/fork_go_templates/main.go

+2-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import (
1616
)
1717

1818
func main() {
19-
// The current is built with 6885bad7dd86880be6929c02085e5c7a67ff2887 go1.23.0
19+
// The current is built with 3901409b5d [release-branch.go1.24] go1.24.0
2020
// TODO(bep) preserve the staticcheck.conf file.
2121
fmt.Println("Forking ...")
2222
defer fmt.Println("Done ...")
@@ -216,6 +216,7 @@ func rewrite(filename, rule string) {
216216
}
217217

218218
func goimports(dir string) {
219+
// Needs go install golang.org/x/tools/cmd/goimports@latest
219220
cmf, _ := hexec.SafeCommand("goimports", "-w", dir)
220221
out, err := cmf.CombinedOutput()
221222
if err != nil {

‎tpl/internal/go_templates/cfg/cfg.go

+2
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,14 @@ const KnownEnv = `
3737
GOARCH
3838
GOARM
3939
GOARM64
40+
GOAUTH
4041
GOBIN
4142
GOCACHE
4243
GOCACHEPROG
4344
GOENV
4445
GOEXE
4546
GOEXPERIMENT
47+
GOFIPS140
4648
GOFLAGS
4749
GOGCCFLAGS
4850
GOHOSTARCH

‎tpl/internal/go_templates/htmltemplate/clone_test.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
// Use of this source code is governed by a BSD-style
33
// license that can be found in the LICENSE file.
44

5-
//go:build !windows
6-
// +build !windows
5+
//go:build go1.13 && !windows
6+
// +build go1.13,!windows
77

88
package template
99

‎tpl/internal/go_templates/htmltemplate/content_test.go

+3-3
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
// Use of this source code is governed by a BSD-style
33
// license that can be found in the LICENSE file.
44

5-
//go:build !windows
6-
// +build !windows
5+
//go:build go1.13 && !windows
6+
// +build go1.13,!windows
77

88
package template
99

@@ -428,7 +428,7 @@ func TestStringer(t *testing.T) {
428428
if err := tmpl.Execute(b, s); err != nil {
429429
t.Fatal(err)
430430
}
431-
expect := "string=3"
431+
var expect = "string=3"
432432
if b.String() != expect {
433433
t.Errorf("expected %q got %q", expect, b.String())
434434
}

‎tpl/internal/go_templates/htmltemplate/css_test.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
// Use of this source code is governed by a BSD-style
33
// license that can be found in the LICENSE file.
44

5-
//go:build !windows
6-
// +build !windows
5+
//go:build go1.13 && !windows
6+
// +build go1.13,!windows
77

88
package template
99

‎tpl/internal/go_templates/htmltemplate/escape.go

+6-13
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"fmt"
1010
"html"
1111
"io"
12+
"maps"
1213
"regexp"
1314

1415
template "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate"
@@ -145,7 +146,7 @@ func (e *escaper) escape(c context, n parse.Node) context {
145146
return c
146147
case *parse.ContinueNode:
147148
c.n = n
148-
e.rangeContext.continues = append(e.rangeContext.breaks, c)
149+
e.rangeContext.continues = append(e.rangeContext.continues, c)
149150
return context{state: stateDead}
150151
case *parse.IfNode:
151152
return e.escapeBranch(c, &n.BranchNode, "if")
@@ -588,22 +589,14 @@ func (e *escaper) escapeListConditionally(c context, n *parse.ListNode, filter f
588589
e1 := makeEscaper(e.ns)
589590
e1.rangeContext = e.rangeContext
590591
// Make type inferences available to f.
591-
for k, v := range e.output {
592-
e1.output[k] = v
593-
}
592+
maps.Copy(e1.output, e.output)
594593
c = e1.escapeList(c, n)
595594
ok := filter != nil && filter(&e1, c)
596595
if ok {
597596
// Copy inferences and edits from e1 back into e.
598-
for k, v := range e1.output {
599-
e.output[k] = v
600-
}
601-
for k, v := range e1.derived {
602-
e.derived[k] = v
603-
}
604-
for k, v := range e1.called {
605-
e.called[k] = v
606-
}
597+
maps.Copy(e.output, e1.output)
598+
maps.Copy(e.derived, e1.derived)
599+
maps.Copy(e.called, e1.called)
607600
for k, v := range e1.actionNodeEdits {
608601
e.editActionNode(k, v)
609602
}

‎tpl/internal/go_templates/htmltemplate/escape_test.go

+8-2
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
// Use of this source code is governed by a BSD-style
33
// license that can be found in the LICENSE file.
44

5-
//go:build !windows
6-
// +build !windows
5+
//go:build go1.13 && !windows
6+
// +build go1.13,!windows
77

88
package template
99

@@ -944,6 +944,7 @@ func TestEscapeSet(t *testing.T) {
944944
t.Errorf("want\n\t%q\ngot\n\t%q", test.want, got)
945945
}
946946
}
947+
947948
}
948949

949950
func TestErrors(t *testing.T) {
@@ -1064,6 +1065,10 @@ func TestErrors(t *testing.T) {
10641065
"{{range .Items}}<a{{if .X}}{{continue}}{{end}}>{{end}}",
10651066
"z:1:29: at range loop continue: {{range}} branches end in different contexts",
10661067
},
1068+
{
1069+
"{{range .Items}}{{if .X}}{{break}}{{end}}<a{{if .Y}}{{continue}}{{end}}>{{if .Z}}{{continue}}{{end}}{{end}}",
1070+
"z:1:54: at range loop continue: {{range}} branches end in different contexts",
1071+
},
10671072
{
10681073
"<a b=1 c={{.H}}",
10691074
"z: ends in a non-text context: {stateAttr delimSpaceOrTagEnd",
@@ -1193,6 +1198,7 @@ func TestErrors(t *testing.T) {
11931198
// Check that we get the same error if we call Execute again.
11941199
if err := tmpl.Execute(buf, nil); err == nil || err.Error() != got {
11951200
t.Errorf("input=%q: unexpected error on second call %q", test.input, err)
1201+
11961202
}
11971203
}
11981204
}

‎tpl/internal/go_templates/htmltemplate/example_test.go

+5
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
// Use of this source code is governed by a BSD-style
33
// license that can be found in the LICENSE file.
44

5+
//go:build go1.13
6+
// +build go1.13
7+
58
package template_test
69

710
import (
@@ -80,6 +83,7 @@ func Example() {
8083
// <div><strong>no rows</strong></div>
8184
// </body>
8285
// </html>
86+
8387
}
8488

8589
func Example_autoescaping() {
@@ -120,6 +124,7 @@ func Example_escape() {
120124
// \"Fran \u0026 Freddie\'s Diner\" \u003Ctasty@example.com\u003E
121125
// \"Fran \u0026 Freddie\'s Diner\"32\u003Ctasty@example.com\u003E
122126
// %22Fran+%26+Freddie%27s+Diner%2232%3Ctasty%40example.com%3E
127+
123128
}
124129

125130
func ExampleTemplate_Delims() {

‎tpl/internal/go_templates/htmltemplate/examplefiles_test.go

+3
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
// Use of this source code is governed by a BSD-style
33
// license that can be found in the LICENSE file.
44

5+
//go:build go1.13
6+
// +build go1.13
7+
58
package template_test
69

710
import (

‎tpl/internal/go_templates/htmltemplate/exec_test.go

+20-34
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44

55
// Tests for template execution, copied from text/template.
66

7-
//go:build !windows
8-
// +build !windows
7+
//go:build go1.13 && !windows
8+
// +build go1.13,!windows
99

1010
package template
1111

@@ -325,16 +325,12 @@ var execTests = []execTest{
325325
{"$.U.V", "{{$.U.V}}", "v", tVal, true},
326326
{"declare in action", "{{$x := $.U.V}}{{$x}}", "v", tVal, true},
327327
{"simple assignment", "{{$x := 2}}{{$x = 3}}{{$x}}", "3", tVal, true},
328-
{
329-
"nested assignment",
328+
{"nested assignment",
330329
"{{$x := 2}}{{if true}}{{$x = 3}}{{end}}{{$x}}",
331-
"3", tVal, true,
332-
},
333-
{
334-
"nested assignment changes the last declaration",
330+
"3", tVal, true},
331+
{"nested assignment changes the last declaration",
335332
"{{$x := 1}}{{if true}}{{$x := 2}}{{if true}}{{$x = 3}}{{end}}{{end}}{{$x}}",
336-
"1", tVal, true,
337-
},
333+
"1", tVal, true},
338334

339335
// Type with String method.
340336
{"V{6666}.String()", "-{{.V0}}-", "-{6666}-", tVal, true}, // NOTE: -<6666>- in text/template
@@ -381,21 +377,15 @@ var execTests = []execTest{
381377
{".Method3(nil constant)", "-{{.Method3 nil}}-", "-Method3: &lt;nil&gt;-", tVal, true},
382378
{".Method3(nil value)", "-{{.Method3 .MXI.unset}}-", "-Method3: &lt;nil&gt;-", tVal, true},
383379
{"method on var", "{{if $x := .}}-{{$x.Method2 .U16 $x.X}}{{end}}-", "-Method2: 16 x-", tVal, true},
384-
{
385-
"method on chained var",
380+
{"method on chained var",
386381
"{{range .MSIone}}{{if $.U.TrueFalse $.True}}{{$.U.TrueFalse $.True}}{{else}}WRONG{{end}}{{end}}",
387-
"true", tVal, true,
388-
},
389-
{
390-
"chained method",
382+
"true", tVal, true},
383+
{"chained method",
391384
"{{range .MSIone}}{{if $.GetU.TrueFalse $.True}}{{$.U.TrueFalse $.True}}{{else}}WRONG{{end}}{{end}}",
392-
"true", tVal, true,
393-
},
394-
{
395-
"chained method on variable",
385+
"true", tVal, true},
386+
{"chained method on variable",
396387
"{{with $x := .}}{{with .SI}}{{$.GetU.TrueFalse $.True}}{{end}}{{end}}",
397-
"true", tVal, true,
398-
},
388+
"true", tVal, true},
399389
{".NilOKFunc not nil", "{{call .NilOKFunc .PI}}", "false", tVal, true},
400390
{".NilOKFunc nil", "{{call .NilOKFunc nil}}", "true", tVal, true},
401391
{"method on nil value from slice", "-{{range .}}{{.Method1 1234}}{{end}}-", "-1234-", tSliceOfNil, true},
@@ -481,14 +471,10 @@ var execTests = []execTest{
481471
{"printf lots", `{{printf "%d %s %g %s" 127 "hello" 7-3i .Method0}}`, "127 hello (7-3i) M0", tVal, true},
482472

483473
// HTML.
484-
{
485-
"html", `{{html "<script>alert(\"XSS\");</script>"}}`,
486-
"&lt;script&gt;alert(&#34;XSS&#34;);&lt;/script&gt;", nil, true,
487-
},
488-
{
489-
"html pipeline", `{{printf "<script>alert(\"XSS\");</script>" | html}}`,
490-
"&lt;script&gt;alert(&#34;XSS&#34;);&lt;/script&gt;", nil, true,
491-
},
474+
{"html", `{{html "<script>alert(\"XSS\");</script>"}}`,
475+
"&lt;script&gt;alert(&#34;XSS&#34;);&lt;/script&gt;", nil, true},
476+
{"html pipeline", `{{printf "<script>alert(\"XSS\");</script>" | html}}`,
477+
"&lt;script&gt;alert(&#34;XSS&#34;);&lt;/script&gt;", nil, true},
492478
{"html", `{{html .PS}}`, "a string", tVal, true},
493479
{"html typed nil", `{{html .NIL}}`, "&lt;nil&gt;", tVal, true},
494480
{"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{
854840

855841
func TestDelims(t *testing.T) {
856842
const hello = "Hello, world"
857-
value := struct{ Str string }{hello}
843+
var value = struct{ Str string }{hello}
858844
for i := 0; i < len(delimPairs); i += 2 {
859845
text := ".Str"
860846
left := delimPairs[i+0]
@@ -877,7 +863,7 @@ func TestDelims(t *testing.T) {
877863
if err != nil {
878864
t.Fatalf("delim %q text %q parse err %s", left, text, err)
879865
}
880-
b := new(strings.Builder)
866+
var b = new(strings.Builder)
881867
err = tmpl.Execute(b, value)
882868
if err != nil {
883869
t.Fatalf("delim %q exec err %s", left, err)
@@ -978,7 +964,7 @@ const treeTemplate = `
978964
`
979965

980966
func TestTree(t *testing.T) {
981-
tree := &Tree{
967+
var tree = &Tree{
982968
1,
983969
&Tree{
984970
2, &Tree{
@@ -1229,7 +1215,7 @@ var cmpTests = []cmpTest{
12291215

12301216
func TestComparison(t *testing.T) {
12311217
b := new(strings.Builder)
1232-
cmpStruct := struct {
1218+
var cmpStruct = struct {
12331219
Uthree, Ufour uint
12341220
NegOne, Three int
12351221
Ptr, NilPtr *int

‎tpl/internal/go_templates/htmltemplate/html_test.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
// Use of this source code is governed by a BSD-style
33
// license that can be found in the LICENSE file.
44

5-
//go:build !windows
6-
// +build !windows
5+
//go:build go1.13 && !windows
6+
// +build go1.13,!windows
77

88
package template
99

‎tpl/internal/go_templates/htmltemplate/js.go

+7-4
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"fmt"
1111
htmltemplate "html/template"
1212
"reflect"
13+
"regexp"
1314
"strings"
1415
"unicode/utf8"
1516
)
@@ -145,6 +146,8 @@ func indirectToJSONMarshaler(a any) any {
145146
return v.Interface()
146147
}
147148

149+
var scriptTagRe = regexp.MustCompile("(?i)<(/?)script")
150+
148151
// jsValEscaper escapes its inputs to a JS Expression (section 11.14) that has
149152
// neither side-effects nor free variables outside (NaN, Infinity).
150153
func jsValEscaper(args ...any) string {
@@ -182,9 +185,9 @@ func jsValEscaper(args ...any) string {
182185
// In particular we:
183186
// * replace "*/" comment end tokens with "* /", which does not
184187
// terminate the comment
185-
// * replace "</script" with "\x3C/script", and "<!--" with
186-
// "\x3C!--", which prevents confusing script block termination
187-
// semantics
188+
// * replace "<script" and "</script" with "\x3Cscript" and "\x3C/script"
189+
// (case insensitively), and "<!--" with "\x3C!--", which prevents
190+
// confusing script block termination semantics
188191
//
189192
// We also put a space before the comment so that if it is flush against
190193
// a division operator it is not turned into a line comment:
@@ -193,8 +196,8 @@ func jsValEscaper(args ...any) string {
193196
// x//* error marshaling y:
194197
// second line of error message */null
195198
errStr := err.Error()
199+
errStr = string(scriptTagRe.ReplaceAll([]byte(errStr), []byte(`\x3C${1}script`)))
196200
errStr = strings.ReplaceAll(errStr, "*/", "* /")
197-
errStr = strings.ReplaceAll(errStr, "</script", `\x3C/script`)
198201
errStr = strings.ReplaceAll(errStr, "<!--", `\x3C!--`)
199202
return fmt.Sprintf(" /* %s */null ", errStr)
200203
}

‎tpl/internal/go_templates/htmltemplate/js_test.go

+5-6
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
// Use of this source code is governed by a BSD-style
33
// license that can be found in the LICENSE file.
44

5-
//go:build !windows
6-
// +build !windows
5+
//go:build go1.13 && !windows
6+
// +build go1.13,!windows
77

88
package template
99

@@ -110,7 +110,7 @@ func TestNextJsCtx(t *testing.T) {
110110
type jsonErrType struct{}
111111

112112
func (e *jsonErrType) MarshalJSON() ([]byte, error) {
113-
return nil, errors.New("beep */ boop </script blip <!--")
113+
return nil, errors.New("a */ b <script c </script d <!-- e <sCrIpT f </sCrIpT")
114114
}
115115

116116
func TestJSValEscaper(t *testing.T) {
@@ -163,7 +163,7 @@ func TestJSValEscaper(t *testing.T) {
163163
{"</script", `"\u003c/script"`, false},
164164
{"\U0001D11E", "\"\U0001D11E\"", false}, // or "\uD834\uDD1E"
165165
{nil, " null ", false},
166-
{&jsonErrType{}, " /* json: error calling MarshalJSON for type *template.jsonErrType: beep * / boop \\x3C/script blip \\x3C!-- */null ", true},
166+
{&jsonErrType{}, " /* json: error calling MarshalJSON for type *template.jsonErrType: a * / b \\x3Cscript c \\x3C/script d \\x3C!-- e \\x3Cscript f \\x3C/script */null ", true},
167167
}
168168

169169
for _, test := range tests {
@@ -221,8 +221,7 @@ func TestJSStrEscaper(t *testing.T) {
221221
{"<!--", `\u003c!--`},
222222
{"-->", `--\u003e`},
223223
// From https://code.google.com/p/doctype/wiki/ArticleUtf7
224-
{
225-
"+ADw-script+AD4-alert(1)+ADw-/script+AD4-",
224+
{"+ADw-script+AD4-alert(1)+ADw-/script+AD4-",
226225
`\u002bADw-script\u002bAD4-alert(1)\u002bADw-\/script\u002bAD4-`,
227226
},
228227
// Invalid UTF-8 sequence

‎tpl/internal/go_templates/htmltemplate/multi_test.go

+3-3
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44

55
// Tests for multiple-template execution, copied from text/template.
66

7-
//go:build !windows
8-
// +build !windows
7+
//go:build go1.13 && !windows
8+
// +build go1.13,!windows
99

1010
package template
1111

@@ -268,7 +268,7 @@ func TestIssue19294(t *testing.T) {
268268
// by the contents of "stylesheet", but if the internal map associating
269269
// names with templates is built in the wrong order, the empty block
270270
// looks non-empty and this doesn't happen.
271-
inlined := map[string]string{
271+
var inlined = map[string]string{
272272
"stylesheet": `{{define "stylesheet"}}stylesheet{{end}}`,
273273
"xhtml": `{{block "stylesheet" .}}{{end}}`,
274274
}

‎tpl/internal/go_templates/htmltemplate/template_test.go

+4
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
// Use of this source code is governed by a BSD-style
33
// license that can be found in the LICENSE file.
44

5+
//go:build go1.13
6+
// +build go1.13
7+
58
package template_test
69

710
import (
@@ -15,6 +18,7 @@ import (
1518
)
1619

1720
func TestTemplateClone(t *testing.T) {
21+
1822
orig := New("name")
1923
clone, err := orig.Clone()
2024
if err != nil {

‎tpl/internal/go_templates/htmltemplate/transition_test.go

+3-2
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
// Use of this source code is governed by a BSD-style
33
// license that can be found in the LICENSE file.
44

5-
//go:build !windows
6-
// +build !windows
5+
//go:build go1.13 && !windows
6+
// +build go1.13,!windows
77

88
package template
99

@@ -43,6 +43,7 @@ func TestFindEndTag(t *testing.T) {
4343
}
4444

4545
func BenchmarkTemplateSpecialTags(b *testing.B) {
46+
4647
r := struct {
4748
Name, Gift string
4849
}{"Aunt Mildred", "bone china tea set"}

‎tpl/internal/go_templates/htmltemplate/url_test.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
// Use of this source code is governed by a BSD-style
33
// license that can be found in the LICENSE file.
44

5-
//go:build !windows
6-
// +build !windows
5+
//go:build go1.13 && !windows
6+
// +build go1.13,!windows
77

88
package template
99

‎tpl/internal/go_templates/testenv/exec.go

+32-12
Original file line numberDiff line numberDiff line change
@@ -31,20 +31,17 @@ import (
3131
// If exec is not supported, testenv.SyscallIsNotSupported will return true
3232
// for the resulting error.
3333
func MustHaveExec(t testing.TB) {
34-
tryExecOnce.Do(func() {
35-
tryExecErr = tryExec()
36-
})
37-
if tryExecErr != nil {
38-
t.Skipf("skipping test: cannot exec subprocess on %s/%s: %v", runtime.GOOS, runtime.GOARCH, tryExecErr)
34+
if err := tryExec(); err != nil {
35+
msg := fmt.Sprintf("cannot exec subprocess on %s/%s: %v", runtime.GOOS, runtime.GOARCH, err)
36+
if t == nil {
37+
panic(msg)
38+
}
39+
t.Helper()
40+
t.Skip("skipping test:", msg)
3941
}
4042
}
4143

42-
var (
43-
tryExecOnce sync.Once
44-
tryExecErr error
45-
)
46-
47-
func tryExec() error {
44+
var tryExec = sync.OnceValue(func() error {
4845
switch runtime.GOOS {
4946
case "wasip1", "js", "ios":
5047
default:
@@ -70,15 +67,37 @@ func tryExec() error {
7067

7168
// We know that this is a test executable. We should be able to run it with a
7269
// no-op flag to check for overall exec support.
73-
exe, err := os.Executable()
70+
exe, err := exePath()
7471
if err != nil {
7572
return fmt.Errorf("can't probe for exec support: %w", err)
7673
}
7774
cmd := exec.Command(exe, "-test.list=^$")
7875
cmd.Env = origEnv
7976
return cmd.Run()
77+
})
78+
79+
// Executable is a wrapper around [MustHaveExec] and [os.Executable].
80+
// It returns the path name for the executable that started the current process,
81+
// or skips the test if the current system can't start new processes,
82+
// or fails the test if the path can not be obtained.
83+
func Executable(t testing.TB) string {
84+
MustHaveExec(t)
85+
86+
exe, err := exePath()
87+
if err != nil {
88+
msg := fmt.Sprintf("os.Executable error: %v", err)
89+
if t == nil {
90+
panic(msg)
91+
}
92+
t.Fatal(msg)
93+
}
94+
return exe
8095
}
8196

97+
var exePath = sync.OnceValues(func() (string, error) {
98+
return os.Executable()
99+
})
100+
82101
var execPaths sync.Map // path -> error
83102

84103
// MustHaveExecPath checks that the current system can start the named executable
@@ -93,6 +112,7 @@ func MustHaveExecPath(t testing.TB, path string) {
93112
err, _ = execPaths.LoadOrStore(path, err)
94113
}
95114
if err != nil {
115+
t.Helper()
96116
t.Skipf("skipping test: %s: %s", path, err)
97117
}
98118
}

‎tpl/internal/go_templates/testenv/testenv.go

+106-118
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ package testenv
1212

1313
import (
1414
"bytes"
15-
"errors"
1615
"flag"
1716
"fmt"
1817
"os"
@@ -43,15 +42,22 @@ func Builder() string {
4342

4443
// HasGoBuild reports whether the current system can build programs with “go build”
4544
// and then run them with os.StartProcess or exec.Command.
46-
// Modified by Hugo (not needed)
4745
func HasGoBuild() bool {
48-
return false
46+
if os.Getenv("GO_GCFLAGS") != "" {
47+
// It's too much work to require every caller of the go command
48+
// to pass along "-gcflags="+os.Getenv("GO_GCFLAGS").
49+
// For now, if $GO_GCFLAGS is set, report that we simply can't
50+
// run go build.
51+
return false
52+
}
53+
54+
return tryGoBuild() == nil
4955
}
5056

51-
var (
52-
goBuildOnce sync.Once
53-
goBuildErr error
54-
)
57+
var tryGoBuild = sync.OnceValue(func() error {
58+
// Removed by Hugo, not used.
59+
return nil
60+
})
5561

5662
// MustHaveGoBuild checks that the current system can build programs with “go build”
5763
// and then run them with os.StartProcess or exec.Command.
@@ -63,7 +69,7 @@ func MustHaveGoBuild(t testing.TB) {
6369
}
6470
if !HasGoBuild() {
6571
t.Helper()
66-
t.Skipf("skipping test: 'go build' unavailable: %v", goBuildErr)
72+
t.Skipf("skipping test: 'go build' unavailable: %v", tryGoBuild())
6773
}
6874
}
6975

@@ -77,6 +83,7 @@ func HasGoRun() bool {
7783
// If not, MustHaveGoRun calls t.Skip with an explanation.
7884
func MustHaveGoRun(t testing.TB) {
7985
if !HasGoRun() {
86+
t.Helper()
8087
t.Skipf("skipping test: 'go run' not available on %s/%s", runtime.GOOS, runtime.GOARCH)
8188
}
8289
}
@@ -96,6 +103,7 @@ func HasParallelism() bool {
96103
// threads in parallel. If not, MustHaveParallelism calls t.Skip with an explanation.
97104
func MustHaveParallelism(t testing.TB) {
98105
if !HasParallelism() {
106+
t.Helper()
99107
t.Skipf("skipping test: no parallelism available on %s/%s", runtime.GOOS, runtime.GOARCH)
100108
}
101109
}
@@ -119,82 +127,67 @@ func GoToolPath(t testing.TB) string {
119127
return path
120128
}
121129

122-
var (
123-
gorootOnce sync.Once
124-
gorootPath string
125-
gorootErr error
126-
)
127-
128-
func findGOROOT() (string, error) {
129-
gorootOnce.Do(func() {
130-
gorootPath = runtime.GOROOT()
131-
if gorootPath != "" {
132-
// If runtime.GOROOT() is non-empty, assume that it is valid.
133-
//
134-
// (It might not be: for example, the user may have explicitly set GOROOT
135-
// to the wrong directory. But this case is
136-
// rare, and if that happens the user can fix what they broke.)
137-
return
138-
}
139-
140-
// runtime.GOROOT doesn't know where GOROOT is (perhaps because the test
141-
// binary was built with -trimpath).
130+
var findGOROOT = sync.OnceValues(func() (path string, err error) {
131+
if path := runtime.GOROOT(); path != "" {
132+
// If runtime.GOROOT() is non-empty, assume that it is valid.
142133
//
143-
// Since this is internal/testenv, we can cheat and assume that the caller
144-
// is a test of some package in a subdirectory of GOROOT/src. ('go test'
145-
// runs the test in the directory containing the packaged under test.) That
146-
// means that if we start walking up the tree, we should eventually find
147-
// GOROOT/src/go.mod, and we can report the parent directory of that.
148-
//
149-
// Notably, this works even if we can't run 'go env GOROOT' as a
150-
// subprocess.
134+
// (It might not be: for example, the user may have explicitly set GOROOT
135+
// to the wrong directory. But this case is
136+
// rare, and if that happens the user can fix what they broke.)
137+
return path, nil
138+
}
151139

152-
cwd, err := os.Getwd()
153-
if err != nil {
154-
gorootErr = fmt.Errorf("finding GOROOT: %w", err)
155-
return
140+
// runtime.GOROOT doesn't know where GOROOT is (perhaps because the test
141+
// binary was built with -trimpath).
142+
//
143+
// Since this is internal/testenv, we can cheat and assume that the caller
144+
// is a test of some package in a subdirectory of GOROOT/src. ('go test'
145+
// runs the test in the directory containing the packaged under test.) That
146+
// means that if we start walking up the tree, we should eventually find
147+
// GOROOT/src/go.mod, and we can report the parent directory of that.
148+
//
149+
// Notably, this works even if we can't run 'go env GOROOT' as a
150+
// subprocess.
151+
152+
cwd, err := os.Getwd()
153+
if err != nil {
154+
return "", fmt.Errorf("finding GOROOT: %w", err)
155+
}
156+
157+
dir := cwd
158+
for {
159+
parent := filepath.Dir(dir)
160+
if parent == dir {
161+
// dir is either "." or only a volume name.
162+
return "", fmt.Errorf("failed to locate GOROOT/src in any parent directory")
156163
}
157164

158-
dir := cwd
159-
for {
160-
parent := filepath.Dir(dir)
161-
if parent == dir {
162-
// dir is either "." or only a volume name.
163-
gorootErr = fmt.Errorf("failed to locate GOROOT/src in any parent directory")
164-
return
165-
}
165+
if base := filepath.Base(dir); base != "src" {
166+
dir = parent
167+
continue // dir cannot be GOROOT/src if it doesn't end in "src".
168+
}
166169

167-
if base := filepath.Base(dir); base != "src" {
170+
b, err := os.ReadFile(filepath.Join(dir, "go.mod"))
171+
if err != nil {
172+
if os.IsNotExist(err) {
168173
dir = parent
169-
continue // dir cannot be GOROOT/src if it doesn't end in "src".
170-
}
171-
172-
b, err := os.ReadFile(filepath.Join(dir, "go.mod"))
173-
if err != nil {
174-
if os.IsNotExist(err) {
175-
dir = parent
176-
continue
177-
}
178-
gorootErr = fmt.Errorf("finding GOROOT: %w", err)
179-
return
174+
continue
180175
}
181-
goMod := string(b)
182-
183-
for goMod != "" {
184-
var line string
185-
line, goMod, _ = strings.Cut(goMod, "\n")
186-
fields := strings.Fields(line)
187-
if len(fields) >= 2 && fields[0] == "module" && fields[1] == "std" {
188-
// Found "module std", which is the module declaration in GOROOT/src!
189-
gorootPath = parent
190-
return
191-
}
176+
return "", fmt.Errorf("finding GOROOT: %w", err)
177+
}
178+
goMod := string(b)
179+
180+
for goMod != "" {
181+
var line string
182+
line, goMod, _ = strings.Cut(goMod, "\n")
183+
fields := strings.Fields(line)
184+
if len(fields) >= 2 && fields[0] == "module" && fields[1] == "std" {
185+
// Found "module std", which is the module declaration in GOROOT/src!
186+
return parent, nil
192187
}
193188
}
194-
})
195-
196-
return gorootPath, gorootErr
197-
}
189+
}
190+
})
198191

199192
// GOROOT reports the path to the directory containing the root of the Go
200193
// project source tree. This is normally equivalent to runtime.GOROOT, but
@@ -217,28 +210,22 @@ func GOROOT(t testing.TB) string {
217210

218211
// GoTool reports the path to the Go tool.
219212
func GoTool() (string, error) {
220-
if !HasGoBuild() {
221-
return "", errors.New("platform cannot run go tool")
222-
}
223-
goToolOnce.Do(func() {
224-
goToolPath, goToolErr = exec.LookPath("go")
225-
})
226-
return goToolPath, goToolErr
213+
// Removed by Hugo, not used.
214+
return "", nil
227215
}
228216

229-
var (
230-
goToolOnce sync.Once
231-
goToolPath string
232-
goToolErr error
233-
)
217+
var goTool = sync.OnceValues(func() (string, error) {
218+
return exec.LookPath("go")
219+
})
234220

235-
// HasSrc reports whether the entire source tree is available under GOROOT.
236-
func HasSrc() bool {
221+
// MustHaveSource checks that the entire source tree is available under GOROOT.
222+
// If not, it calls t.Skip with an explanation.
223+
func MustHaveSource(t testing.TB) {
237224
switch runtime.GOOS {
238225
case "ios":
239-
return false
226+
t.Helper()
227+
t.Skip("skipping test: no source tree on " + runtime.GOOS)
240228
}
241-
return true
242229
}
243230

244231
// HasExternalNetwork reports whether the current system can use
@@ -263,41 +250,39 @@ func MustHaveExternalNetwork(t testing.TB) {
263250

264251
// HasCGO reports whether the current system can use cgo.
265252
func HasCGO() bool {
266-
hasCgoOnce.Do(func() {
267-
goTool, err := GoTool()
268-
if err != nil {
269-
return
270-
}
271-
cmd := exec.Command(goTool, "env", "CGO_ENABLED")
272-
cmd.Env = origEnv
273-
out, err := cmd.Output()
274-
if err != nil {
275-
panic(fmt.Sprintf("%v: %v", cmd, out))
276-
}
277-
hasCgo, err = strconv.ParseBool(string(bytes.TrimSpace(out)))
278-
if err != nil {
279-
panic(fmt.Sprintf("%v: non-boolean output %q", cmd, out))
280-
}
281-
})
282-
return hasCgo
253+
return hasCgo()
283254
}
284255

285-
var (
286-
hasCgoOnce sync.Once
287-
hasCgo bool
288-
)
256+
var hasCgo = sync.OnceValue(func() bool {
257+
goTool, err := goTool()
258+
if err != nil {
259+
return false
260+
}
261+
cmd := exec.Command(goTool, "env", "CGO_ENABLED")
262+
cmd.Env = origEnv
263+
out, err := cmd.Output()
264+
if err != nil {
265+
panic(fmt.Sprintf("%v: %v", cmd, out))
266+
}
267+
ok, err := strconv.ParseBool(string(bytes.TrimSpace(out)))
268+
if err != nil {
269+
panic(fmt.Sprintf("%v: non-boolean output %q", cmd, out))
270+
}
271+
return ok
272+
})
289273

290274
// MustHaveCGO calls t.Skip if cgo is not available.
291275
func MustHaveCGO(t testing.TB) {
292276
if !HasCGO() {
277+
t.Helper()
293278
t.Skipf("skipping test: no cgo")
294279
}
295280
}
296281

297282
// CanInternalLink reports whether the current system can link programs with
298283
// internal linking.
299-
// Modified by Hugo (not needed)
300284
func CanInternalLink(withCgo bool) bool {
285+
// Removed by Hugo, not used.
301286
return false
302287
}
303288

@@ -306,6 +291,7 @@ func CanInternalLink(withCgo bool) bool {
306291
// If not, MustInternalLink calls t.Skip with an explanation.
307292
func MustInternalLink(t testing.TB, withCgo bool) {
308293
if !CanInternalLink(withCgo) {
294+
t.Helper()
309295
if withCgo && CanInternalLink(false) {
310296
t.Skipf("skipping test: internal linking on %s/%s is not supported with cgo", runtime.GOOS, runtime.GOARCH)
311297
}
@@ -316,15 +302,15 @@ func MustInternalLink(t testing.TB, withCgo bool) {
316302
// MustInternalLinkPIE checks whether the current system can link PIE binary using
317303
// internal linking.
318304
// If not, MustInternalLinkPIE calls t.Skip with an explanation.
319-
// Modified by Hugo (not needed)
320305
func MustInternalLinkPIE(t testing.TB) {
306+
// Removed by Hugo, not used.
321307
}
322308

323309
// MustHaveBuildMode reports whether the current system can build programs in
324310
// the given build mode.
325311
// If not, MustHaveBuildMode calls t.Skip with an explanation.
326-
// Modified by Hugo (not needed)
327312
func MustHaveBuildMode(t testing.TB, buildmode string) {
313+
// Removed by Hugo, not used.
328314
}
329315

330316
// HasSymlink reports whether the current system can use os.Symlink.
@@ -338,6 +324,7 @@ func HasSymlink() bool {
338324
func MustHaveSymlink(t testing.TB) {
339325
ok, reason := hasSymlink()
340326
if !ok {
327+
t.Helper()
341328
t.Skipf("skipping test: cannot make symlinks on %s/%s: %s", runtime.GOOS, runtime.GOARCH, reason)
342329
}
343330
}
@@ -354,22 +341,23 @@ func HasLink() bool {
354341
// If not, MustHaveLink calls t.Skip with an explanation.
355342
func MustHaveLink(t testing.TB) {
356343
if !HasLink() {
344+
t.Helper()
357345
t.Skipf("skipping test: hardlinks are not supported on %s/%s", runtime.GOOS, runtime.GOARCH)
358346
}
359347
}
360348

361349
var flaky = flag.Bool("flaky", false, "run known-flaky tests too")
362350

363351
func SkipFlaky(t testing.TB, issue int) {
364-
t.Helper()
365352
if !*flaky {
353+
t.Helper()
366354
t.Skipf("skipping known flaky test without the -flaky flag; see golang.org/issue/%d", issue)
367355
}
368356
}
369357

370358
func SkipFlakyNet(t testing.TB) {
371-
t.Helper()
372359
if v, _ := strconv.ParseBool(os.Getenv("GO_BUILDER_FLAKY_NET")); v {
360+
t.Helper()
373361
t.Skip("skipping test on builder known to have frequent network failures")
374362
}
375363
}
@@ -455,6 +443,6 @@ func SyscallIsNotSupported(err error) bool {
455443
// ParallelOn64Bit calls t.Parallel() unless there is a case that cannot be parallel.
456444
// This function should be used when it is necessary to avoid t.Parallel on
457445
// 32-bit machines, typically because the test uses lots of memory.
458-
// Disabled by Hugo.
459446
func ParallelOn64Bit(t *testing.T) {
447+
// Removed by Hugo, not used.
460448
}

‎tpl/internal/go_templates/testenv/testenv_notwin.go

+3-2
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,10 @@ import (
1111
"os"
1212
"path/filepath"
1313
"runtime"
14+
"sync"
1415
)
1516

16-
func hasSymlink() (ok bool, reason string) {
17+
var hasSymlink = sync.OnceValues(func() (ok bool, reason string) {
1718
switch runtime.GOOS {
1819
case "plan9":
1920
return false, ""
@@ -43,4 +44,4 @@ func hasSymlink() (ok bool, reason string) {
4344
}
4445

4546
return true, ""
46-
}
47+
})

‎tpl/internal/go_templates/testenv/testenv_test.go

+77-1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
)
1616

1717
func TestGoToolLocation(t *testing.T) {
18+
t.Skip("This test is not relevant for Hugo")
1819
testenv.MustHaveGoBuild(t)
1920

2021
var exeSuffix string
@@ -54,8 +55,83 @@ func TestGoToolLocation(t *testing.T) {
5455
}
5556
}
5657

57-
// Modified by Hugo (not needed)
5858
func TestHasGoBuild(t *testing.T) {
59+
if !testenv.HasGoBuild() {
60+
switch runtime.GOOS {
61+
case "js", "wasip1":
62+
// No exec syscall, so these shouldn't be able to 'go build'.
63+
t.Logf("HasGoBuild is false on %s", runtime.GOOS)
64+
return
65+
}
66+
67+
b := testenv.Builder()
68+
if b == "" {
69+
// We shouldn't make assumptions about what kind of sandbox or build
70+
// environment external Go users may be running in.
71+
t.Skipf("skipping: 'go build' unavailable")
72+
}
73+
74+
// Since we control the Go builders, we know which ones ought
75+
// to be able to run 'go build'. Check that they can.
76+
//
77+
// (Note that we don't verify that any builders *can't* run 'go build'.
78+
// If a builder starts running 'go build' tests when it shouldn't,
79+
// we will presumably find out about it when those tests fail.)
80+
switch runtime.GOOS {
81+
case "ios":
82+
if isCorelliumBuilder(b) {
83+
// The corellium environment is self-hosting, so it should be able
84+
// to build even though real "ios" devices can't exec.
85+
} else {
86+
// The usual iOS sandbox does not allow the app to start another
87+
// process. If we add builders on stock iOS devices, they presumably
88+
// will not be able to exec, so we may as well allow that now.
89+
t.Logf("HasGoBuild is false on %s", b)
90+
return
91+
}
92+
case "android":
93+
panic("Removed by Hugo, should not be used")
94+
}
95+
96+
if strings.Contains(b, "-noopt") {
97+
// The -noopt builder sets GO_GCFLAGS, which causes tests of 'go build' to
98+
// be skipped.
99+
t.Logf("HasGoBuild is false on %s", b)
100+
return
101+
}
102+
103+
t.Fatalf("HasGoBuild unexpectedly false on %s", b)
104+
}
105+
106+
t.Logf("HasGoBuild is true; checking consistency with other functions")
107+
108+
hasExec := false
109+
hasExecGo := false
110+
t.Run("MustHaveExec", func(t *testing.T) {
111+
testenv.MustHaveExec(t)
112+
hasExec = true
113+
})
114+
t.Run("MustHaveExecPath", func(t *testing.T) {
115+
testenv.MustHaveExecPath(t, "go")
116+
hasExecGo = true
117+
})
118+
if !hasExec {
119+
t.Errorf(`MustHaveExec(t) skipped unexpectedly`)
120+
}
121+
if !hasExecGo {
122+
t.Errorf(`MustHaveExecPath(t, "go") skipped unexpectedly`)
123+
}
124+
125+
dir := t.TempDir()
126+
mainGo := filepath.Join(dir, "main.go")
127+
if err := os.WriteFile(mainGo, []byte("package main\nfunc main() {}\n"), 0o644); err != nil {
128+
t.Fatal(err)
129+
}
130+
cmd := testenv.Command(t, "go", "build", "-o", os.DevNull, mainGo)
131+
out, err := cmd.CombinedOutput()
132+
if err != nil {
133+
t.Fatalf("%v: %v\n%s", cmd, err, out)
134+
}
59135
}
60136

61137
func TestMustHaveExec(t *testing.T) {

‎tpl/internal/go_templates/testenv/testenv_windows.go

+7-22
Original file line numberDiff line numberDiff line change
@@ -5,43 +5,28 @@
55
package testenv
66

77
import (
8+
"errors"
89
"os"
910
"path/filepath"
1011
"sync"
1112
"syscall"
1213
)
1314

14-
var symlinkOnce sync.Once
15-
var winSymlinkErr error
16-
17-
func initWinHasSymlink() {
15+
var hasSymlink = sync.OnceValues(func() (bool, string) {
1816
tmpdir, err := os.MkdirTemp("", "symtest")
1917
if err != nil {
2018
panic("failed to create temp directory: " + err.Error())
2119
}
2220
defer os.RemoveAll(tmpdir)
2321

2422
err = os.Symlink("target", filepath.Join(tmpdir, "symlink"))
25-
if err != nil {
26-
err = err.(*os.LinkError).Err
27-
switch err {
28-
case syscall.EWINDOWS, syscall.ERROR_PRIVILEGE_NOT_HELD:
29-
winSymlinkErr = err
30-
}
31-
}
32-
}
33-
34-
func hasSymlink() (ok bool, reason string) {
35-
symlinkOnce.Do(initWinHasSymlink)
36-
37-
switch winSymlinkErr {
38-
case nil:
23+
switch {
24+
case err == nil:
3925
return true, ""
40-
case syscall.EWINDOWS:
26+
case errors.Is(err, syscall.EWINDOWS):
4127
return false, ": symlinks are not supported on your version of Windows"
42-
case syscall.ERROR_PRIVILEGE_NOT_HELD:
28+
case errors.Is(err, syscall.ERROR_PRIVILEGE_NOT_HELD):
4329
return false, ": you don't have enough privileges to create symlinks"
4430
}
45-
4631
return false, ""
47-
}
32+
})

‎tpl/internal/go_templates/texttemplate/doc.go

+28-2
Original file line numberDiff line numberDiff line change
@@ -98,15 +98,17 @@ data, defined in detail in the corresponding sections that follow.
9898
{{if pipeline}} T1 {{else}}{{if pipeline}} T0 {{end}}{{end}}
9999
100100
{{range pipeline}} T1 {{end}}
101-
The value of the pipeline must be an array, slice, map, or channel.
101+
The value of the pipeline must be an array, slice, map, iter.Seq,
102+
iter.Seq2, integer or channel.
102103
If the value of the pipeline has length zero, nothing is output;
103104
otherwise, dot is set to the successive elements of the array,
104105
slice, or map and T1 is executed. If the value is a map and the
105106
keys are of basic type with a defined order, the elements will be
106107
visited in sorted key order.
107108
108109
{{range pipeline}} T1 {{else}} T0 {{end}}
109-
The value of the pipeline must be an array, slice, map, or channel.
110+
The value of the pipeline must be an array, slice, map, iter.Seq,
111+
iter.Seq2, integer or channel.
110112
If the value of the pipeline has length zero, dot is unaffected and
111113
T0 is executed; otherwise, dot is set to the successive elements
112114
of the array, slice, or map and T1 is executed.
@@ -162,54 +164,78 @@ An argument is a simple value, denoted by one of the following.
162164
the host machine's ints are 32 or 64 bits.
163165
- The keyword nil, representing an untyped Go nil.
164166
- The character '.' (period):
167+
165168
.
169+
166170
The result is the value of dot.
167171
- A variable name, which is a (possibly empty) alphanumeric string
168172
preceded by a dollar sign, such as
173+
169174
$piOver2
175+
170176
or
177+
171178
$
179+
172180
The result is the value of the variable.
173181
Variables are described below.
174182
- The name of a field of the data, which must be a struct, preceded
175183
by a period, such as
184+
176185
.Field
186+
177187
The result is the value of the field. Field invocations may be
178188
chained:
189+
179190
.Field1.Field2
191+
180192
Fields can also be evaluated on variables, including chaining:
193+
181194
$x.Field1.Field2
182195
- The name of a key of the data, which must be a map, preceded
183196
by a period, such as
197+
184198
.Key
199+
185200
The result is the map element value indexed by the key.
186201
Key invocations may be chained and combined with fields to any
187202
depth:
203+
188204
.Field1.Key1.Field2.Key2
205+
189206
Although the key must be an alphanumeric identifier, unlike with
190207
field names they do not need to start with an upper case letter.
191208
Keys can also be evaluated on variables, including chaining:
209+
192210
$x.key1.key2
193211
- The name of a niladic method of the data, preceded by a period,
194212
such as
213+
195214
.Method
215+
196216
The result is the value of invoking the method with dot as the
197217
receiver, dot.Method(). Such a method must have one return value (of
198218
any type) or two return values, the second of which is an error.
199219
If it has two and the returned error is non-nil, execution terminates
200220
and an error is returned to the caller as the value of Execute.
201221
Method invocations may be chained and combined with fields and keys
202222
to any depth:
223+
203224
.Field1.Key1.Method1.Field2.Key2.Method2
225+
204226
Methods can also be evaluated on variables, including chaining:
227+
205228
$x.Method1.Field
206229
- The name of a niladic function, such as
230+
207231
fun
232+
208233
The result is the value of invoking the function, fun(). The return
209234
types and values behave as in methods. Functions and function
210235
names are described below.
211236
- A parenthesized instance of one the above, for grouping. The result
212237
may be accessed by a field or map key invocation.
238+
213239
print (.F1 arg1) (.F2 arg2)
214240
(.StructValuedMethod "arg").Field
215241

‎tpl/internal/go_templates/texttemplate/example_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ Josie
3535
Name, Gift string
3636
Attended bool
3737
}
38-
recipients := []Recipient{
38+
var recipients = []Recipient{
3939
{"Aunt Mildred", "bone china tea set", true},
4040
{"Uncle John", "moleskin pants", false},
4141
{"Cousin Rodney", "", false},

‎tpl/internal/go_templates/texttemplate/examplefiles_test.go

+3
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
// Use of this source code is governed by a BSD-style
33
// license that can be found in the LICENSE file.
44

5+
//go:build go1.13
6+
// +build go1.13
7+
58
package template_test
69

710
import (

‎tpl/internal/go_templates/texttemplate/exec.go

+61-2
Original file line numberDiff line numberDiff line change
@@ -395,6 +395,22 @@ func (s *state) walkRange(dot reflect.Value, r *parse.RangeNode) {
395395
s.walk(elem, r.List)
396396
}
397397
switch val.Kind() {
398+
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
399+
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
400+
if len(r.Pipe.Decl) > 1 {
401+
s.errorf("can't use %v to iterate over more than one variable", val)
402+
break
403+
}
404+
run := false
405+
for v := range val.Seq() {
406+
run = true
407+
// Pass element as second value, as we do for channels.
408+
oneIteration(reflect.Value{}, v)
409+
}
410+
if !run {
411+
break
412+
}
413+
return
398414
case reflect.Array, reflect.Slice:
399415
if val.Len() == 0 {
400416
break
@@ -434,6 +450,43 @@ func (s *state) walkRange(dot reflect.Value, r *parse.RangeNode) {
434450
return
435451
case reflect.Invalid:
436452
break // An invalid value is likely a nil map, etc. and acts like an empty map.
453+
case reflect.Func:
454+
if val.Type().CanSeq() {
455+
if len(r.Pipe.Decl) > 1 {
456+
s.errorf("can't use %v iterate over more than one variable", val)
457+
break
458+
}
459+
run := false
460+
for v := range val.Seq() {
461+
run = true
462+
// Pass element as second value,
463+
// as we do for channels.
464+
oneIteration(reflect.Value{}, v)
465+
}
466+
if !run {
467+
break
468+
}
469+
return
470+
}
471+
if val.Type().CanSeq2() {
472+
run := false
473+
for i, v := range val.Seq2() {
474+
run = true
475+
if len(r.Pipe.Decl) > 1 {
476+
oneIteration(i, v)
477+
} else {
478+
// If there is only one range variable,
479+
// oneIteration will use the
480+
// second value.
481+
oneIteration(reflect.Value{}, i)
482+
}
483+
}
484+
if !run {
485+
break
486+
}
487+
return
488+
}
489+
fallthrough
437490
default:
438491
s.errorf("range can't iterate over %v", val)
439492
}
@@ -757,7 +810,7 @@ func (s *state) evalCallOld(dot, fun reflect.Value, isBuiltin bool, node parse.N
757810
return v
758811
}
759812
}
760-
if final != missingVal {
813+
if !final.Equal(missingVal) {
761814
// The last argument to and/or is coming from
762815
// the pipeline. We didn't short circuit on an earlier
763816
// 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
803856
// Special case for the "call" builtin.
804857
// Insert the name of the callee function as the first argument.
805858
if isBuiltin && name == "call" {
806-
calleeName := args[0].String()
859+
var calleeName string
860+
if len(args) == 0 {
861+
// final must be present or we would have errored out above.
862+
calleeName = final.String()
863+
} else {
864+
calleeName = args[0].String()
865+
}
807866
argv = append([]reflect.Value{reflect.ValueOf(calleeName)}, argv...)
808867
fun = reflect.ValueOf(call)
809868
}

‎tpl/internal/go_templates/texttemplate/exec_test.go

+59
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"flag"
1414
"fmt"
1515
"io"
16+
"iter"
1617
"reflect"
1718
"strings"
1819
"sync"
@@ -412,6 +413,9 @@ var execTests = []execTest{
412413
{"Interface Call", `{{stringer .S}}`, "foozle", map[string]any{"S": bytes.NewBufferString("foozle")}, true},
413414
{".ErrFunc", "{{call .ErrFunc}}", "bla", tVal, true},
414415
{"call nil", "{{call nil}}", "", tVal, false},
416+
{"empty call", "{{call}}", "", tVal, false},
417+
{"empty call after pipe valid", "{{.ErrFunc | call}}", "bla", tVal, true},
418+
{"empty call after pipe invalid", "{{1 | call}}", "", tVal, false},
415419

416420
// Erroneous function calls (check args).
417421
{".BinaryFuncTooFew", "{{call .BinaryFunc `1`}}", "", tVal, false},
@@ -618,6 +622,30 @@ var execTests = []execTest{
618622
{"declare in range", "{{range $x := .PSI}}<{{$foo:=$x}}{{$x}}>{{end}}", "<21><22><23>", tVal, true},
619623
{"range count", `{{range $i, $x := count 5}}[{{$i}}]{{$x}}{{end}}`, "[0]a[1]b[2]c[3]d[4]e", tVal, true},
620624
{"range nil count", `{{range $i, $x := count 0}}{{else}}empty{{end}}`, "empty", tVal, true},
625+
{"range iter.Seq[int]", `{{range $i := .}}{{$i}}{{end}}`, "01", fVal1(2), true},
626+
{"i = range iter.Seq[int]", `{{$i := 0}}{{range $i = .}}{{$i}}{{end}}`, "01", fVal1(2), true},
627+
{"range iter.Seq[int] over two var", `{{range $i, $c := .}}{{$c}}{{end}}`, "", fVal1(2), false},
628+
{"i, c := range iter.Seq2[int,int]", `{{range $i, $c := .}}{{$i}}{{$c}}{{end}}`, "0112", fVal2(2), true},
629+
{"i, c = range iter.Seq2[int,int]", `{{$i := 0}}{{$c := 0}}{{range $i, $c = .}}{{$i}}{{$c}}{{end}}`, "0112", fVal2(2), true},
630+
{"i = range iter.Seq2[int,int]", `{{$i := 0}}{{range $i = .}}{{$i}}{{end}}`, "01", fVal2(2), true},
631+
{"i := range iter.Seq2[int,int]", `{{range $i := .}}{{$i}}{{end}}`, "01", fVal2(2), true},
632+
{"i,c,x range iter.Seq2[int,int]", `{{$i := 0}}{{$c := 0}}{{$x := 0}}{{range $i, $c = .}}{{$i}}{{$c}}{{end}}`, "0112", fVal2(2), true},
633+
{"i,x range iter.Seq[int]", `{{$i := 0}}{{$x := 0}}{{range $i = .}}{{$i}}{{end}}`, "01", fVal1(2), true},
634+
{"range iter.Seq[int] else", `{{range $i := .}}{{$i}}{{else}}empty{{end}}`, "empty", fVal1(0), true},
635+
{"range iter.Seq2[int,int] else", `{{range $i := .}}{{$i}}{{else}}empty{{end}}`, "empty", fVal2(0), true},
636+
{"range int8", rangeTestInt, rangeTestData[int8](), int8(5), true},
637+
{"range int16", rangeTestInt, rangeTestData[int16](), int16(5), true},
638+
{"range int32", rangeTestInt, rangeTestData[int32](), int32(5), true},
639+
{"range int64", rangeTestInt, rangeTestData[int64](), int64(5), true},
640+
{"range int", rangeTestInt, rangeTestData[int](), int(5), true},
641+
{"range uint8", rangeTestInt, rangeTestData[uint8](), uint8(5), true},
642+
{"range uint16", rangeTestInt, rangeTestData[uint16](), uint16(5), true},
643+
{"range uint32", rangeTestInt, rangeTestData[uint32](), uint32(5), true},
644+
{"range uint64", rangeTestInt, rangeTestData[uint64](), uint64(5), true},
645+
{"range uint", rangeTestInt, rangeTestData[uint](), uint(5), true},
646+
{"range uintptr", rangeTestInt, rangeTestData[uintptr](), uintptr(5), true},
647+
{"range uintptr(0)", `{{range $v := .}}{{print $v}}{{else}}empty{{end}}`, "empty", uintptr(0), true},
648+
{"range 5", `{{range $v := 5}}{{printf "%T%d" $v $v}}{{end}}`, rangeTestData[int](), nil, true},
621649

622650
// Cute examples.
623651
{"or as if true", `{{or .SI "slice is empty"}}`, "[3 4 5]", tVal, true},
@@ -722,6 +750,37 @@ var execTests = []execTest{
722750
{"issue60801", "{{$k := 0}}{{$v := 0}}{{range $k, $v = .AI}}{{$k}}={{$v}} {{end}}", "0=3 1=4 2=5 ", tVal, true},
723751
}
724752

753+
func fVal1(i int) iter.Seq[int] {
754+
return func(yield func(int) bool) {
755+
for v := range i {
756+
if !yield(v) {
757+
break
758+
}
759+
}
760+
}
761+
}
762+
763+
func fVal2(i int) iter.Seq2[int, int] {
764+
return func(yield func(int, int) bool) {
765+
for v := range i {
766+
if !yield(v, v+1) {
767+
break
768+
}
769+
}
770+
}
771+
}
772+
773+
const rangeTestInt = `{{range $v := .}}{{printf "%T%d" $v $v}}{{end}}`
774+
775+
func rangeTestData[T int | int8 | int16 | int32 | int64 | uint | uint8 | uint16 | uint32 | uint64 | uintptr]() string {
776+
I := T(5)
777+
var buf strings.Builder
778+
for i := T(0); i < I; i++ {
779+
fmt.Fprintf(&buf, "%T%d", i, i)
780+
}
781+
return buf.String()
782+
}
783+
725784
func zeroArgs() string {
726785
return "zeroArgs"
727786
}

‎tpl/internal/go_templates/texttemplate/hugo_template.go

+12-6
Original file line numberDiff line numberDiff line change
@@ -304,14 +304,14 @@ func (s *state) evalCall(dot, fun reflect.Value, isBuiltin bool, node parse.Node
304304
}
305305
}()
306306
}
307+
307308
if args != nil {
308309
args = args[1:] // Zeroth arg is function name/node; not passed to function.
309310
}
310-
311311
typ := fun.Type()
312-
numFirst := len(first)
312+
numFirst := len(first) // Added for Hugo
313313
numIn := len(args) + numFirst // Added for Hugo
314-
if final != missingVal {
314+
if !isMissing(final) {
315315
numIn++
316316
}
317317
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
346346
return v
347347
}
348348
}
349-
if final != missingVal {
349+
if !final.Equal(missingVal) {
350350
// The last argument to and/or is coming from
351351
// the pipeline. We didn't short circuit on an earlier
352352
// 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
373373
}
374374
}
375375
// Add final value if necessary.
376-
if final != missingVal {
376+
if !isMissing(final) {
377377
t := typ.In(typ.NumIn() - 1)
378378
if typ.IsVariadic() {
379379
if numIn-1 < numFixed {
@@ -392,7 +392,13 @@ func (s *state) evalCall(dot, fun reflect.Value, isBuiltin bool, node parse.Node
392392
// Special case for the "call" builtin.
393393
// Insert the name of the callee function as the first argument.
394394
if isBuiltin && name == "call" {
395-
calleeName := args[0].String()
395+
var calleeName string
396+
if len(args) == 0 {
397+
// final must be present or we would have errored out above.
398+
calleeName = final.String()
399+
} else {
400+
calleeName = args[0].String()
401+
}
396402
argv = append([]reflect.Value{reflect.ValueOf(calleeName)}, argv...)
397403
fun = reflect.ValueOf(call)
398404
}

‎tpl/internal/go_templates/texttemplate/link_test.go

+5-3
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,18 @@
22
// Use of this source code is governed by a BSD-style
33
// license that can be found in the LICENSE file.
44

5+
//go:build go1.13
6+
// +build go1.13
7+
58
package template_test
69

710
import (
811
"bytes"
12+
"github.com/gohugoio/hugo/tpl/internal/go_templates/testenv"
913
"os"
1014
"os/exec"
1115
"path/filepath"
1216
"testing"
13-
14-
"github.com/gohugoio/hugo/tpl/internal/go_templates/testenv"
1517
)
1618

1719
// Issue 36021: verify that text/template doesn't prevent the linker from removing
@@ -42,7 +44,7 @@ func main() {
4244
`
4345
td := t.TempDir()
4446

45-
if err := os.WriteFile(filepath.Join(td, "x.go"), []byte(prog), 0o644); err != nil {
47+
if err := os.WriteFile(filepath.Join(td, "x.go"), []byte(prog), 0644); err != nil {
4648
t.Fatal(err)
4749
}
4850
cmd := exec.Command(testenv.GoToolPath(t), "build", "-o", "x.exe", "x.go")

‎tpl/internal/go_templates/texttemplate/multi_test.go

+14-25
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,19 @@
22
// Use of this source code is governed by a BSD-style
33
// license that can be found in the LICENSE file.
44

5-
//go:build !windows
6-
// +build !windows
5+
//go:build go1.13 && !windows
6+
// +build go1.13,!windows
77

88
package template
99

1010
// Tests for multiple-template parsing and execution.
1111

1212
import (
1313
"fmt"
14+
"github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate/parse"
1415
"os"
1516
"strings"
1617
"testing"
17-
18-
"github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate/parse"
1918
)
2019

2120
const (
@@ -32,32 +31,22 @@ type multiParseTest struct {
3231
}
3332

3433
var multiParseTests = []multiParseTest{
35-
{
36-
"empty", "", noError,
37-
nil,
34+
{"empty", "", noError,
3835
nil,
39-
},
40-
{
41-
"one", `{{define "foo"}} FOO {{end}}`, noError,
36+
nil},
37+
{"one", `{{define "foo"}} FOO {{end}}`, noError,
4238
[]string{"foo"},
43-
[]string{" FOO "},
44-
},
45-
{
46-
"two", `{{define "foo"}} FOO {{end}}{{define "bar"}} BAR {{end}}`, noError,
39+
[]string{" FOO "}},
40+
{"two", `{{define "foo"}} FOO {{end}}{{define "bar"}} BAR {{end}}`, noError,
4741
[]string{"foo", "bar"},
48-
[]string{" FOO ", " BAR "},
49-
},
42+
[]string{" FOO ", " BAR "}},
5043
// errors
51-
{
52-
"missing end", `{{define "foo"}} FOO `, hasError,
53-
nil,
54-
nil,
55-
},
56-
{
57-
"malformed name", `{{define "foo}} FOO `, hasError,
44+
{"missing end", `{{define "foo"}} FOO `, hasError,
5845
nil,
46+
nil},
47+
{"malformed name", `{{define "foo}} FOO `, hasError,
5948
nil,
60-
},
49+
nil},
6150
}
6251

6352
func TestMultiParse(t *testing.T) {
@@ -443,7 +432,7 @@ func TestIssue19294(t *testing.T) {
443432
// by the contents of "stylesheet", but if the internal map associating
444433
// names with templates is built in the wrong order, the empty block
445434
// looks non-empty and this doesn't happen.
446-
inlined := map[string]string{
435+
var inlined = map[string]string{
447436
"stylesheet": `{{define "stylesheet"}}stylesheet{{end}}`,
448437
"xhtml": `{{block "stylesheet" .}}{{end}}`,
449438
}

‎tpl/internal/go_templates/texttemplate/parse/lex.go

+1
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,7 @@ func lexComment(l *lexer) stateFn {
352352
if !delim {
353353
return l.errorf("comment ends before closing delimiter")
354354
}
355+
l.line += strings.Count(l.input[l.start:l.pos], "\n")
355356
i := l.thisItem(itemComment)
356357
if trimSpace {
357358
l.pos += trimMarkerLen

‎tpl/internal/go_templates/texttemplate/parse/lex_test.go

+10
Original file line numberDiff line numberDiff line change
@@ -548,6 +548,16 @@ var lexPosTests = []lexTest{
548548
{itemRightDelim, 11, "}}", 2},
549549
{itemEOF, 13, "", 2},
550550
}},
551+
{"longcomment", "{{/*\n*/}}\n{{undefinedFunction \"test\"}}", []item{
552+
{itemComment, 2, "/*\n*/", 1},
553+
{itemText, 9, "\n", 2},
554+
{itemLeftDelim, 10, "{{", 3},
555+
{itemIdentifier, 12, "undefinedFunction", 3},
556+
{itemSpace, 29, " ", 3},
557+
{itemString, 30, "\"test\"", 3},
558+
{itemRightDelim, 36, "}}", 3},
559+
{itemEOF, 38, "", 3},
560+
}},
551561
}
552562

553563
// The other tests don't check position, to make the test cases easier to construct.

‎tpl/internal/go_templates/texttemplate/parse/parse_test.go

+150-289
Large diffs are not rendered by default.

‎tpl/internal/go_templates/texttemplate/template.go

+3-6
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package template
66

77
import (
88
"github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate/parse"
9+
"maps"
910
"reflect"
1011
"sync"
1112
)
@@ -102,12 +103,8 @@ func (t *Template) Clone() (*Template, error) {
102103
}
103104
t.muFuncs.RLock()
104105
defer t.muFuncs.RUnlock()
105-
for k, v := range t.parseFuncs {
106-
nt.parseFuncs[k] = v
107-
}
108-
for k, v := range t.execFuncs {
109-
nt.execFuncs[k] = v
110-
}
106+
maps.Copy(nt.parseFuncs, t.parseFuncs)
107+
maps.Copy(nt.execFuncs, t.execFuncs)
111108
return nt, nil
112109
}
113110

0 commit comments

Comments
 (0)
Please sign in to comment.