diff options
author | Bjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com> | 2022-03-16 10:48:16 +0300 |
---|---|---|
committer | Bjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com> | 2022-03-16 10:54:25 +0300 |
commit | 65a78cae1ecdcfcb0701d78fed812083a1a6e82b (patch) | |
tree | b57295e36c91cbfb76bdd606657d0c1f6c7fdf2b | |
parent | 4d6d1d08dad5538f4710fc4b56e09419aa0d854b (diff) |
tpl: Sync go_templates for Go 1.18
Using Go tag go1.18 4aa1efed4853ea067d665a952eee77c52faac774
Updates #9677
48 files changed, 697 insertions, 223 deletions
diff --git a/scripts/fork_go_templates/main.go b/scripts/fork_go_templates/main.go index f550efbbb..4ffba018f 100644 --- a/scripts/fork_go_templates/main.go +++ b/scripts/fork_go_templates/main.go @@ -17,8 +17,7 @@ import ( ) func main() { - // TODO(bep) git checkout tag - // The current is built with Go version 2f0da6d9e29d9b9d5a4d10427ca9f71d12bbacc8 / go1.16 + // The current is built with Go tag go1.18 4aa1efed4853ea067d665a952eee77c52faac774 fmt.Println("Forking ...") defer fmt.Println("Done ...") @@ -40,7 +39,7 @@ func main() { const ( // TODO(bep) - goSource = "/Users/bep/dev/go/dump/go/src" + goSource = "/Users/bep/dev/go/misc/go/src" forkRoot = "../../tpl/internal/go_templates" ) diff --git a/tpl/internal/go_templates/cfg/cfg.go b/tpl/internal/go_templates/cfg/cfg.go index 553021374..78664d7a9 100644 --- a/tpl/internal/go_templates/cfg/cfg.go +++ b/tpl/internal/go_templates/cfg/cfg.go @@ -33,12 +33,14 @@ const KnownEnv = ` GCCGO GO111MODULE GO386 + GOAMD64 GOARCH GOARM GOBIN GOCACHE GOENV GOEXE + GOEXPERIMENT GOFLAGS GOGCCFLAGS GOHOSTARCH @@ -60,6 +62,7 @@ const KnownEnv = ` GOTOOLDIR GOVCS GOWASM + GOWORK GO_EXTLINK_ENABLED PKG_CONFIG ` diff --git a/tpl/internal/go_templates/fmtsort/sort.go b/tpl/internal/go_templates/fmtsort/sort.go index 7127ba6ac..34c1f477f 100644 --- a/tpl/internal/go_templates/fmtsort/sort.go +++ b/tpl/internal/go_templates/fmtsort/sort.go @@ -130,7 +130,7 @@ func compare(aVal, bVal reflect.Value) int { default: return -1 } - case reflect.Ptr, reflect.UnsafePointer: + case reflect.Pointer, reflect.UnsafePointer: a, b := aVal.Pointer(), bVal.Pointer() switch { case a < b: diff --git a/tpl/internal/go_templates/fmtsort/sort_test.go b/tpl/internal/go_templates/fmtsort/sort_test.go index 5205a6413..a05e8a3c3 100644 --- a/tpl/internal/go_templates/fmtsort/sort_test.go +++ b/tpl/internal/go_templates/fmtsort/sort_test.go @@ -9,6 +9,7 @@ import ( "github.com/gohugoio/hugo/tpl/internal/go_templates/fmtsort" "math" "reflect" + "sort" "strings" "testing" "unsafe" @@ -37,12 +38,12 @@ var compareTests = [][]reflect.Value{ ct(reflect.TypeOf(chans[0]), chans[0], chans[1], chans[2]), ct(reflect.TypeOf(toy{}), toy{0, 1}, toy{0, 2}, toy{1, -1}, toy{1, 1}), ct(reflect.TypeOf([2]int{}), [2]int{1, 1}, [2]int{1, 2}, [2]int{2, 0}), - ct(reflect.TypeOf(interface{}(interface{}(0))), iFace, 1, 2, 3), + ct(reflect.TypeOf(any(any(0))), iFace, 1, 2, 3), } -var iFace interface{} +var iFace any -func ct(typ reflect.Type, args ...interface{}) []reflect.Value { +func ct(typ reflect.Type, args ...any) []reflect.Value { value := make([]reflect.Value, len(args)) for i, v := range args { x := reflect.ValueOf(v) @@ -83,8 +84,8 @@ func TestCompare(t *testing.T) { } type sortTest struct { - data interface{} // Always a map. - print string // Printed result using our custom printer. + data any // Always a map. + print string // Printed result using our custom printer. } var sortTests = []sortTest{ @@ -134,7 +135,7 @@ var sortTests = []sortTest{ }, } -func sprint(data interface{}) string { +func sprint(data any) string { om := fmtsort.Sort(reflect.ValueOf(data)) if om == nil { return "nil" @@ -188,9 +189,19 @@ func sprintKey(key reflect.Value) string { var ( ints [3]int - chans = [3]chan int{make(chan int), make(chan int), make(chan int)} + chans = makeChans() ) +func makeChans() []chan int { + cs := []chan int{make(chan int), make(chan int), make(chan int)} + // Order channels by address. See issue #49431. + // TODO: pin these pointers once pinning is available (#46787). + sort.Slice(cs, func(i, j int) bool { + return uintptr(reflect.ValueOf(cs[i]).UnsafePointer()) < uintptr(reflect.ValueOf(cs[j]).UnsafePointer()) + }) + return cs +} + func pointerMap() map[*int]string { m := make(map[*int]string) for i := 2; i >= 0; i-- { @@ -233,7 +244,7 @@ func TestInterface(t *testing.T) { // A map containing multiple concrete types should be sorted by type, // then value. However, the relative ordering of types is unspecified, // so test this by checking the presence of sorted subgroups. - m := map[interface{}]string{ + m := map[any]string{ [2]int{1, 0}: "", [2]int{0, 1}: "", true: "", diff --git a/tpl/internal/go_templates/htmltemplate/attr.go b/tpl/internal/go_templates/htmltemplate/attr.go index 22922e603..6c52211fe 100644 --- a/tpl/internal/go_templates/htmltemplate/attr.go +++ b/tpl/internal/go_templates/htmltemplate/attr.go @@ -143,12 +143,12 @@ func attrType(name string) contentType { // widely applied. // Treat data-action as URL below. name = name[5:] - } else if colon := strings.IndexRune(name, ':'); colon != -1 { - if name[:colon] == "xmlns" { + } else if prefix, short, ok := strings.Cut(name, ":"); ok { + if prefix == "xmlns" { return contentTypeURL } // Treat svg:href and xlink:href as href below. - name = name[colon+1:] + name = short } if t, ok := attrTypeMap[name]; ok { return t diff --git a/tpl/internal/go_templates/htmltemplate/clone_test.go b/tpl/internal/go_templates/htmltemplate/clone_test.go index 7c774e923..553f656b5 100644 --- a/tpl/internal/go_templates/htmltemplate/clone_test.go +++ b/tpl/internal/go_templates/htmltemplate/clone_test.go @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. +//go:build go1.13 && !windows // +build go1.13,!windows package template diff --git a/tpl/internal/go_templates/htmltemplate/content.go b/tpl/internal/go_templates/htmltemplate/content.go index bc32dc813..65cc3086c 100644 --- a/tpl/internal/go_templates/htmltemplate/content.go +++ b/tpl/internal/go_templates/htmltemplate/content.go @@ -29,16 +29,16 @@ const ( // indirect returns the value, after dereferencing as many times // as necessary to reach the base type (or nil). -func indirect(a interface{}) interface{} { +func indirect(a any) any { if a == nil { return nil } - if t := reflect.TypeOf(a); t.Kind() != reflect.Ptr { + if t := reflect.TypeOf(a); t.Kind() != reflect.Pointer { // Avoid creating a reflect.Value if it's not a pointer. return a } v := reflect.ValueOf(a) - for v.Kind() == reflect.Ptr && !v.IsNil() { + for v.Kind() == reflect.Pointer && !v.IsNil() { v = v.Elem() } return v.Interface() @@ -52,12 +52,12 @@ var ( // indirectToStringerOrError returns the value, after dereferencing as many times // as necessary to reach the base type (or nil) or an implementation of fmt.Stringer // or error, -func indirectToStringerOrError(a interface{}) interface{} { +func indirectToStringerOrError(a any) any { if a == nil { return nil } v := reflect.ValueOf(a) - for !v.Type().Implements(fmtStringerType) && !v.Type().Implements(errorType) && v.Kind() == reflect.Ptr && !v.IsNil() { + for !v.Type().Implements(fmtStringerType) && !v.Type().Implements(errorType) && v.Kind() == reflect.Pointer && !v.IsNil() { v = v.Elem() } return v.Interface() @@ -65,7 +65,7 @@ func indirectToStringerOrError(a interface{}) interface{} { // stringify converts its arguments to a string and the type of the content. // All pointers are dereferenced, as in the text/template package. -func stringify(args ...interface{}) (string, contentType) { +func stringify(args ...any) (string, contentType) { if len(args) == 1 { switch s := indirect(args[0]).(type) { case string: diff --git a/tpl/internal/go_templates/htmltemplate/content_test.go b/tpl/internal/go_templates/htmltemplate/content_test.go index 909a24bc0..29221a4ad 100644 --- a/tpl/internal/go_templates/htmltemplate/content_test.go +++ b/tpl/internal/go_templates/htmltemplate/content_test.go @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. +//go:build go1.13 && !windows // +build go1.13,!windows package template @@ -15,7 +16,7 @@ import ( ) func TestTypedContent(t *testing.T) { - data := []interface{}{ + data := []any{ `<b> "foo%" O'Reilly &bar;`, htmltemplate.CSS(`a[href =~ "//example.com"]#foo`), htmltemplate.HTML(`Hello, <b>World</b> &tc!`), @@ -452,7 +453,7 @@ func TestEscapingNilNonemptyInterfaces(t *testing.T) { // A non-empty interface should print like an empty interface. want := new(bytes.Buffer) - data := struct{ E interface{} }{} + data := struct{ E any }{} tmpl.Execute(want, data) if !bytes.Equal(want.Bytes(), got.Bytes()) { diff --git a/tpl/internal/go_templates/htmltemplate/context.go b/tpl/internal/go_templates/htmltemplate/context.go index 006b870ec..c28e08dce 100644 --- a/tpl/internal/go_templates/htmltemplate/context.go +++ b/tpl/internal/go_templates/htmltemplate/context.go @@ -4,7 +4,11 @@ package template -import "fmt" +import ( + "fmt" + + "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate/parse" +) // context describes the state an HTML parser must be in when it reaches the // portion of HTML produced by evaluating a particular template node. @@ -20,6 +24,7 @@ type context struct { jsCtx jsCtx attr attr element element + n parse.Node // for range break/continue err *Error } @@ -139,6 +144,8 @@ const ( // stateError is an infectious error state outside any valid // HTML/CSS/JS construct. stateError + // stateDead marks unreachable code after a {{break}} or {{continue}}. + stateDead ) // isComment is true for any state that contains content meant for template diff --git a/tpl/internal/go_templates/htmltemplate/css.go b/tpl/internal/go_templates/htmltemplate/css.go index eb92fc92b..890a0c6b2 100644 --- a/tpl/internal/go_templates/htmltemplate/css.go +++ b/tpl/internal/go_templates/htmltemplate/css.go @@ -155,7 +155,7 @@ func isCSSSpace(b byte) bool { } // cssEscaper escapes HTML and CSS special characters using \<hex>+ escapes. -func cssEscaper(args ...interface{}) string { +func cssEscaper(args ...any) string { s, _ := stringify(args...) var b strings.Builder r, w, written := rune(0), 0, 0 @@ -218,7 +218,7 @@ var mozBindingBytes = []byte("mozbinding") // (inherit, blue), and colors (#888). // It filters out unsafe values, such as those that affect token boundaries, // and anything that might execute scripts. -func cssValueFilter(args ...interface{}) string { +func cssValueFilter(args ...any) string { s, t := stringify(args...) if t == contentTypeCSS { return s diff --git a/tpl/internal/go_templates/htmltemplate/css_test.go b/tpl/internal/go_templates/htmltemplate/css_test.go index afed58c29..7d8ad8b59 100644 --- a/tpl/internal/go_templates/htmltemplate/css_test.go +++ b/tpl/internal/go_templates/htmltemplate/css_test.go @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. +//go:build go1.13 && !windows // +build go1.13,!windows package template diff --git a/tpl/internal/go_templates/htmltemplate/error.go b/tpl/internal/go_templates/htmltemplate/error.go index f8b3f1cea..21c86a9ef 100644 --- a/tpl/internal/go_templates/htmltemplate/error.go +++ b/tpl/internal/go_templates/htmltemplate/error.go @@ -229,6 +229,6 @@ func (e *Error) Error() string { // errorf creates an error given a format string f and args. // The template Name still needs to be supplied. -func errorf(k ErrorCode, node parse.Node, line int, f string, args ...interface{}) *Error { +func errorf(k ErrorCode, node parse.Node, line int, f string, args ...any) *Error { return &Error{k, node, "", line, fmt.Sprintf(f, args...)} } diff --git a/tpl/internal/go_templates/htmltemplate/escape.go b/tpl/internal/go_templates/htmltemplate/escape.go index 5f4c92a17..5382c4288 100644 --- a/tpl/internal/go_templates/htmltemplate/escape.go +++ b/tpl/internal/go_templates/htmltemplate/escape.go @@ -46,7 +46,7 @@ func escapeTemplate(tmpl *Template, node parse.Node, name string) error { // evalArgs formats the list of arguments into a string. It is equivalent to // fmt.Sprint(args...), except that it deferences all pointers. -func evalArgs(args ...interface{}) string { +func evalArgs(args ...any) string { // Optimization for simple common case of a single string argument. if len(args) == 1 { if s, ok := args[0].(string); ok { @@ -98,6 +98,15 @@ type escaper struct { actionNodeEdits map[*parse.ActionNode][]string templateNodeEdits map[*parse.TemplateNode]string textNodeEdits map[*parse.TextNode][]byte + // rangeContext holds context about the current range loop. + rangeContext *rangeContext +} + +// rangeContext holds information about the current range loop. +type rangeContext struct { + outer *rangeContext // outer loop + breaks []context // context at each break action + continues []context // context at each continue action } // makeEscaper creates a blank escaper for the given set. @@ -110,6 +119,7 @@ func makeEscaper(n *nameSpace) escaper { map[*parse.ActionNode][]string{}, map[*parse.TemplateNode]string{}, map[*parse.TextNode][]byte{}, + nil, } } @@ -125,8 +135,16 @@ func (e *escaper) escape(c context, n parse.Node) context { switch n := n.(type) { case *parse.ActionNode: return e.escapeAction(c, n) + case *parse.BreakNode: + c.n = n + e.rangeContext.breaks = append(e.rangeContext.breaks, c) + return context{state: stateDead} case *parse.CommentNode: return c + case *parse.ContinueNode: + c.n = n + e.rangeContext.continues = append(e.rangeContext.breaks, c) + return context{state: stateDead} case *parse.IfNode: return e.escapeBranch(c, &n.BranchNode, "if") case *parse.ListNode: @@ -428,6 +446,12 @@ func join(a, b context, node parse.Node, nodeName string) context { if b.state == stateError { return b } + if a.state == stateDead { + return b + } + if b.state == stateDead { + return a + } if a.eq(b) { return a } @@ -467,14 +491,27 @@ func join(a, b context, node parse.Node, nodeName string) context { // escapeBranch escapes a branch template node: "if", "range" and "with". func (e *escaper) escapeBranch(c context, n *parse.BranchNode, nodeName string) context { + if nodeName == "range" { + e.rangeContext = &rangeContext{outer: e.rangeContext} + } c0 := e.escapeList(c, n.List) - if nodeName == "range" && c0.state != stateError { + if nodeName == "range" { + if c0.state != stateError { + c0 = joinRange(c0, e.rangeContext) + } + e.rangeContext = e.rangeContext.outer + if c0.state == stateError { + return c0 + } + // The "true" branch of a "range" node can execute multiple times. // We check that executing n.List once results in the same context // as executing n.List twice. + e.rangeContext = &rangeContext{outer: e.rangeContext} c1, _ := e.escapeListConditionally(c0, n.List, nil) c0 = join(c0, c1, n, nodeName) if c0.state == stateError { + e.rangeContext = e.rangeContext.outer // Make clear that this is a problem on loop re-entry // since developers tend to overlook that branch when // debugging templates. @@ -482,11 +519,39 @@ func (e *escaper) escapeBranch(c context, n *parse.BranchNode, nodeName string) c0.err.Description = "on range loop re-entry: " + c0.err.Description return c0 } + c0 = joinRange(c0, e.rangeContext) + e.rangeContext = e.rangeContext.outer + if c0.state == stateError { + return c0 + } } c1 := e.escapeList(c, n.ElseList) return join(c0, c1, n, nodeName) } +func joinRange(c0 context, rc *rangeContext) context { + // Merge contexts at break and continue statements into overall body context. + // In theory we could treat breaks differently from continues, but for now it is + // enough to treat them both as going back to the start of the loop (which may then stop). + for _, c := range rc.breaks { + c0 = join(c0, c, c.n, "range") + if c0.state == stateError { + c0.err.Line = c.n.(*parse.BreakNode).Line + c0.err.Description = "at range loop break: " + c0.err.Description + return c0 + } + } + for _, c := range rc.continues { + c0 = join(c0, c, c.n, "range") + if c0.state == stateError { + c0.err.Line = c.n.(*parse.ContinueNode).Line + c0.err.Description = "at range loop continue: " + c0.err.Description + return c0 + } + } + return c0 +} + // escapeList escapes a list template node. func (e *escaper) escapeList(c context, n *parse.ListNode) context { if n == nil { @@ -494,6 +559,9 @@ func (e *escaper) escapeList(c context, n *parse.ListNode) context { } for _, m := range n.Nodes { c = e.escape(c, m) + if c.state == stateDead { + break + } } return c } @@ -504,6 +572,7 @@ func (e *escaper) escapeList(c context, n *parse.ListNode) context { // which is the same as whether e was updated. func (e *escaper) escapeListConditionally(c context, n *parse.ListNode, filter func(*escaper, context) bool) (context, bool) { e1 := makeEscaper(e.ns) + e1.rangeContext = e.rangeContext // Make type inferences available to f. for k, v := range e.output { e1.output[k] = v @@ -866,7 +935,7 @@ func HTMLEscapeString(s string) string { // HTMLEscaper returns the escaped HTML equivalent of the textual // representation of its arguments. -func HTMLEscaper(args ...interface{}) string { +func HTMLEscaper(args ...any) string { return template.HTMLEscaper(args...) } @@ -882,12 +951,12 @@ func JSEscapeString(s string) string { // JSEscaper returns the escaped JavaScript equivalent of the textual // representation of its arguments. -func JSEscaper(args ...interface{}) string { +func JSEscaper(args ...any) string { return template.JSEscaper(args...) } // URLQueryEscaper returns the escaped value of the textual representation of // its arguments in a form suitable for embedding in a URL query. -func URLQueryEscaper(args ...interface{}) string { +func URLQueryEscaper(args ...any) string { return template.URLQueryEscaper(args...) } diff --git a/tpl/internal/go_templates/htmltemplate/escape_test.go b/tpl/internal/go_templates/htmltemplate/escape_test.go index c569a9391..adf160b5d 100644 --- a/tpl/internal/go_templates/htmltemplate/escape_test.go +++ b/tpl/internal/go_templates/htmltemplate/escape_test.go @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. +//go:build go1.13 && !windows // +build go1.13,!windows package template @@ -39,8 +40,8 @@ func TestEscape(t *testing.T) { A, E []string B, M json.Marshaler N int - U interface{} // untyped nil - Z *int // typed nil + U any // untyped nil + Z *int // typed nil W htmltemplate.HTML }{ F: false, @@ -862,7 +863,7 @@ func TestEscapeSet(t *testing.T) { // pred is a template function that returns the predecessor of a // natural number for testing recursive templates. - fns := FuncMap{"pred": func(a ...interface{}) (interface{}, error) { + fns := FuncMap{"pred": func(a ...any) (any, error) { if len(a) == 1 { if i, _ := a[0].(int); i > 0 { return i - 1, nil @@ -924,6 +925,22 @@ func TestErrors(t *testing.T) { "<a href='/foo?{{range .Items}}&{{.K}}={{.V}}{{end}}'>", "", }, + { + "{{range .Items}}<a{{if .X}}{{end}}>{{end}}", + "", + }, + { + "{{range .Items}}<a{{if .X}}{{end}}>{{continue}}{{end}}", + "", + }, + { + "{{range .Items}}<a{{if .X}}{{end}}>{{break}}{{end}}", + "", + }, + { + "{{range .Items}}<a{{if .X}}{{end}}>{{if .X}}{{break}}{{end}}{{end}}", + "", + }, // Error cases. { "{{if .Cond}}<a{{end}}", @@ -960,6 +977,14 @@ func TestErrors(t *testing.T) { "z:2:8: on range loop re-entry: {{range}} branches", }, { + "{{range .Items}}<a{{if .X}}{{break}}{{end}}>{{end}}", + "z:1:29: at range loop break: {{range}} branches end in different contexts", + }, + { + "{{range .Items}}<a{{if .X}}{{continue}}{{end}}>{{end}}", + "z:1:29: at range loop continue: {{range}} branches end in different contexts", + }, + { "<a b=1 c={{.H}}", "z: ends in a non-text context: {stateAttr delimSpaceOrTagEnd", }, @@ -1768,7 +1793,7 @@ func TestEscapeSetErrorsNotIgnorable(t *testing.T) { } func TestRedundantFuncs(t *testing.T) { - inputs := []interface{}{ + inputs := []any{ "\x00\x01\x02\x03\x04\x05\x06\x07\x08\t\n\x0b\x0c\r\x0e\x0f" + "\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f" + ` !"#$%&'()*+,-./` + @@ -1788,9 +1813,9 @@ func TestRedundantFuncs(t *testing.T) { } for n0, m := range redundantFuncs { - f0 := funcMap[n0].(func(...interface{}) string) + f0 := funcMap[n0].(func(...any) string) for n1 := range m { - f1 := funcMap[n1].(func(...interface{}) string) + f1 := funcMap[n1].(func(...any) string) for _, input := range inputs { want := f0(input) if got := f1(want); want != got { diff --git a/tpl/internal/go_templates/htmltemplate/example_test.go b/tpl/internal/go_templates/htmltemplate/example_test.go index a93b8d2fb..6485c7cfb 100644 --- a/tpl/internal/go_templates/htmltemplate/example_test.go +++ b/tpl/internal/go_templates/htmltemplate/example_test.go @@ -2,6 +2,7 @@ // 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 @@ -101,7 +102,7 @@ func Example_autoescaping() { func Example_escape() { const s = `"Fran & Freddie's Diner" <tasty@example.com>` - v := []interface{}{`"Fran & Freddie's Diner"`, ' ', `<tasty@example.com>`} + v := []any{`"Fran & Freddie's Diner"`, ' ', `<tasty@example.com>`} fmt.Println(template.HTMLEscapeString(s)) template.HTMLEscape(os.Stdout, []byte(s)) diff --git a/tpl/internal/go_templates/htmltemplate/examplefiles_test.go b/tpl/internal/go_templates/htmltemplate/examplefiles_test.go index 4c693fba3..43cc3bf01 100644 --- a/tpl/internal/go_templates/htmltemplate/examplefiles_test.go +++ b/tpl/internal/go_templates/htmltemplate/examplefiles_test.go @@ -2,6 +2,7 @@ // 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 diff --git a/tpl/internal/go_templates/htmltemplate/exec_test.go b/tpl/internal/go_templates/htmltemplate/exec_test.go index 3e8b022d7..08195af0e 100644 --- a/tpl/internal/go_templates/htmltemplate/exec_test.go +++ b/tpl/internal/go_templates/htmltemplate/exec_test.go @@ -4,6 +4,7 @@ // Tests for template execution, copied from text/template. +//go:build go1.13 && !windows // +build go1.13,!windows package template @@ -53,7 +54,7 @@ type T struct { MSI map[string]int MSIone map[string]int // one element, for deterministic output MSIEmpty map[string]int - MXI map[interface{}]int + MXI map[any]int MII map[int]int MI32S map[int32]string MI64S map[int64]string @@ -63,11 +64,11 @@ type T struct { MUI8S map[uint8]string SMSI []map[string]int // Empty interfaces; used to see if we can dig inside one. - Empty0 interface{} // nil - Empty1 interface{} - Empty2 interface{} - Empty3 interface{} - Empty4 interface{} + Empty0 any // nil + Empty1 any + Empty2 any + Empty3 any + Empty4 any // Non-empty interfaces. NonEmptyInterface I NonEmptyInterfacePtS *I @@ -145,7 +146,7 @@ var tVal = &T{ SB: []bool{true, false}, MSI: map[string]int{"one": 1, "two": 2, "three": 3}, MSIone: map[string]int{"one": 1}, - MXI: map[interface{}]int{"one": 1}, + MXI: map[any]int{"one": 1}, MII: map[int]int{1: 1}, MI32S: map[int32]string{1: "one", 2: "two"}, MI64S: map[int64]string{2: "i642", 3: "i643"}, @@ -216,7 +217,7 @@ func (t *T) Method2(a uint16, b string) string { return fmt.Sprintf("Method2: %d %s", a, b) } -func (t *T) Method3(v interface{}) string { +func (t *T) Method3(v any) string { return fmt.Sprintf("Method3: %v", v) } @@ -256,7 +257,7 @@ func (u *U) TrueFalse(b bool) string { return "" } -func typeOf(arg interface{}) string { +func typeOf(arg any) string { return fmt.Sprintf("%T", arg) } @@ -264,7 +265,7 @@ type execTest struct { name string input string output string - data interface{} + data any ok bool } @@ -397,7 +398,7 @@ var execTests = []execTest{ {".VariadicFuncInt", "{{call .VariadicFuncInt 33 `he` `llo`}}", "33=<he+llo>", tVal, true}, {"if .BinaryFunc call", "{{ if .BinaryFunc}}{{call .BinaryFunc `1` `2`}}{{end}}", "[1=2]", tVal, true}, {"if not .BinaryFunc call", "{{ if not .BinaryFunc}}{{call .BinaryFunc `1` `2`}}{{else}}No{{end}}", "No", tVal, true}, - {"Interface Call", `{{stringer .S}}`, "foozle", map[string]interface{}{"S": bytes.NewBufferString("foozle")}, true}, + {"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}, @@ -571,6 +572,8 @@ var execTests = []execTest{ {"range empty no else", "{{range .SIEmpty}}-{{.}}-{{end}}", "", tVal, true}, {"range []int else", "{{range .SI}}-{{.}}-{{else}}EMPTY{{end}}", "-3--4--5-", tVal, true}, {"range empty else", "{{range .SIEmpty}}-{{.}}-{{else}}EMPTY{{end}}", "EMPTY", tVal, true}, + {"range []int break else", "{{range .SI}}-{{.}}-{{break}}NOTREACHED{{else}}EMPTY{{end}}", "-3-", tVal, true}, + {"range []int continue else", "{{range .SI}}-{{.}}-{{continue}}NOTREACHED{{else}}EMPTY{{end}}", "-3--4--5-", tVal, true}, {"range []bool", "{{range .SB}}-{{.}}-{{end}}", "-true--false-", tVal, true}, {"range []int method", "{{range .SI | .MAdd .I}}-{{.}}-{{end}}", "-20--21--22-", tVal, true}, {"range map", "{{range .MSI}}-{{.}}-{{end}}", "-1--3--2-", tVal, true}, @@ -742,7 +745,7 @@ func add(args ...int) int { return sum } -func echo(arg interface{}) interface{} { +func echo(arg any) any { return arg } @@ -761,7 +764,7 @@ func stringer(s fmt.Stringer) string { return s.String() } -func mapOfThree() interface{} { +func mapOfThree() any { return map[string]int{"three": 3} } @@ -1440,7 +1443,7 @@ func TestBlock(t *testing.T) { func TestEvalFieldErrors(t *testing.T) { tests := []struct { name, src string - value interface{} + value any want string }{ { @@ -1583,7 +1586,7 @@ func TestInterfaceValues(t *testing.T) { for _, tt := range tests { tmpl := Must(New("tmpl").Parse(tt.text)) var buf bytes.Buffer - err := tmpl.Execute(&buf, map[string]interface{}{ + err := tmpl.Execute(&buf, map[string]any{ "PlusOne": func(n int) int { return n + 1 }, @@ -1612,7 +1615,7 @@ func TestInterfaceValues(t *testing.T) { // Check that panics during calls are recovered and returned as errors. func TestExecutePanicDuringCall(t *testing.T) { - funcs := map[string]interface{}{ + funcs := map[string]any{ "doPanic": func() string { panic("custom panic string") }, @@ -1620,7 +1623,7 @@ func TestExecutePanicDuringCall(t *testing.T) { tests := []struct { name string input string - data interface{} + data any wantErr string }{ { @@ -1724,8 +1727,6 @@ var v = "v"; ` func TestEscapeRace(t *testing.T) { - // t.Skip("this test currently fails with -race; see issue #39807") - tmpl := New("") _, err := tmpl.New("templ.html").Parse(raceText) if err != nil { @@ -1820,7 +1821,7 @@ func TestRecursiveExecuteViaMethod(t *testing.T) { func TestTemplateFuncsAfterClone(t *testing.T) { s := `{{ f . }}` want := "test" - orig := New("orig").Funcs(map[string]interface{}{ + orig := New("orig").Funcs(map[string]any{ "f": func(in string) string { return in }, diff --git a/tpl/internal/go_templates/htmltemplate/html.go b/tpl/internal/go_templates/htmltemplate/html.go index 356b8298a..19bd0ccb2 100644 --- a/tpl/internal/go_templates/htmltemplate/html.go +++ b/tpl/internal/go_templates/htmltemplate/html.go @@ -12,7 +12,7 @@ import ( ) // htmlNospaceEscaper escapes for inclusion in unquoted attribute values. -func htmlNospaceEscaper(args ...interface{}) string { +func htmlNospaceEscaper(args ...any) string { s, t := stringify(args...) if t == contentTypeHTML { return htmlReplacer(stripTags(s), htmlNospaceNormReplacementTable, false) @@ -21,7 +21,7 @@ func htmlNospaceEscaper(args ...interface{}) string { } // attrEscaper escapes for inclusion in quoted attribute values. -func attrEscaper(args ...interface{}) string { +func attrEscaper(args ...any) string { s, t := stringify(args...) if t == contentTypeHTML { return htmlReplacer(stripTags(s), htmlNormReplacementTable, true) @@ -30,7 +30,7 @@ func attrEscaper(args ...interface{}) string { } // rcdataEscaper escapes for inclusion in an RCDATA element body. -func rcdataEscaper(args ...interface{}) string { +func rcdataEscaper(args ...any) string { s, t := stringify(args...) if t == contentTypeHTML { return htmlReplacer(s, htmlNormReplacementTable, true) @@ -39,7 +39,7 @@ func rcdataEscaper(args ...interface{}) string { } // htmlEscaper escapes for inclusion in HTML text. -func htmlEscaper(args ...interface{}) string { +func htmlEscaper(args ...any) string { s, t := stringify(args...) if t == contentTypeHTML { return s @@ -225,7 +225,7 @@ func stripTags(html string) string { // htmlNameFilter accepts valid parts of an HTML attribute or tag name or // a known-safe HTML attribute. -func htmlNameFilter(args ...interface{}) string { +func htmlNameFilter(args ...any) string { s, t := stringify(args...) if t == contentTypeHTMLAttr { return s @@ -260,6 +260,6 @@ func htmlNameFilter(args ...interface{}) string { // content interpolated into comments. // This approach is equally valid whether or not static comment content is // removed from the template. -func commentEscaper(args ...interface{}) string { +func commentEscaper(args ...any) string { return "" } diff --git a/tpl/internal/go_templates/htmltemplate/html_test.go b/tpl/internal/go_templates/htmltemplate/html_test.go index 946221822..2809ee1e2 100644 --- a/tpl/internal/go_templates/htmltemplate/html_test.go +++ b/tpl/internal/go_templates/htmltemplate/html_test.go @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. +//go:build go1.13 && !windows // +build go1.13,!windows package template diff --git a/tpl/internal/go_templates/htmltemplate/js.go b/tpl/internal/go_templates/htmltemplate/js.go index cfd413461..6187dc036 100644 --- a/tpl/internal/go_templates/htmltemplate/js.go +++ b/tpl/internal/go_templates/htmltemplate/js.go @@ -123,7 +123,7 @@ var jsonMarshalType = reflect.TypeOf((*json.Marshaler)(nil)).Elem() // indirectToJSONMarshaler returns the value, after dereferencing as many times // as necessary to reach the base type (or nil) or an implementation of json.Marshal. -func indirectToJSONMarshaler(a interface{}) interface{} { +func indirectToJSONMarshaler(a any) any { // text/template now supports passing untyped nil as a func call // argument, so we must support it. Otherwise we'd panic below, as one // cannot call the Type or Interface methods on an invalid @@ -133,7 +133,7 @@ func indirectToJSONMarshaler(a interface{}) interface{} { } v := reflect.ValueOf(a) - for !v.Type().Implements(jsonMarshalType) && v.Kind() == reflect.Ptr && !v.IsNil() { + for !v.Type().Implements(jsonMarshalType) && v.Kind() == reflect.Pointer && !v.IsNil() { v = v.Elem() } return v.Interface() @@ -141,8 +141,8 @@ func indirectToJSONMarshaler(a interface{}) interface{} { // 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 ...interface{}) string { - var a interface{} +func jsValEscaper(args ...any) string { + var a any if len(args) == 1 { a = indirectToJSONMarshaler(args[0]) switch t := a.(type) { @@ -225,7 +225,7 @@ func jsValEscaper(args ...interface{}) string { // jsStrEscaper produces a string that can be included between quotes in // JavaScript source, in JavaScript embedded in an HTML5 <script> element, // or in an HTML5 event handler attribute such as onclick. -func jsStrEscaper(args ...interface{}) string { +func jsStrEscaper(args ...any) string { s, t := stringify(args...) if t == contentTypeJSStr { return replace(s, jsStrNormReplacementTable) @@ -237,7 +237,7 @@ func jsStrEscaper(args ...interface{}) string { // specials so the result is treated literally when included in a regular // expression literal. /foo{{.X}}bar/ matches the string "foo" followed by // the literal text of {{.X}} followed by the string "bar". -func jsRegexpEscaper(args ...interface{}) string { +func jsRegexpEscaper(args ...any) string { s, _ := stringify(args...) s = replace(s, jsRegexpReplacementTable) if s == "" { @@ -399,9 +399,7 @@ func isJSType(mimeType string) bool { // https://tools.ietf.org/html/rfc4329#section-3 // https://www.ietf.org/rfc/rfc4627.txt // discard parameters - if i := strings.Index(mimeType, ";"); i >= 0 { - mimeType = mimeType[:i] - } + mimeType, _, _ = strings.Cut(mimeType, ";") mimeType = strings.ToLower(mimeType) mimeType = strings.TrimSpace(mimeType) switch mimeType { diff --git a/tpl/internal/go_templates/htmltemplate/js_test.go b/tpl/internal/go_templates/htmltemplate/js_test.go index e15087f0f..92073b37a 100644 --- a/tpl/internal/go_templates/htmltemplate/js_test.go +++ b/tpl/internal/go_templates/htmltemplate/js_test.go @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. +//go:build go1.13 && !windows // +build go1.13,!windows package template @@ -105,7 +106,7 @@ func TestNextJsCtx(t *testing.T) { func TestJSValEscaper(t *testing.T) { tests := []struct { - x interface{} + x any js string }{ {int(42), " 42 "}, @@ -142,8 +143,8 @@ func TestJSValEscaper(t *testing.T) { // "\v" == "v" on IE 6 so use "\u000b" instead. {"\t\x0b", `"\t\u000b"`}, {struct{ X, Y int }{1, 2}, `{"X":1,"Y":2}`}, - {[]interface{}{}, "[]"}, - {[]interface{}{42, "foo", nil}, `[42,"foo",null]`}, + {[]any{}, "[]"}, + {[]any{42, "foo", nil}, `[42,"foo",null]`}, {[]string{"<!--", "</script>", "-->"}, `["\u003c!--","\u003c/script\u003e","--\u003e"]`}, {"<!--", `"\u003c!--"`}, {"-->", `"--\u003e"`}, @@ -160,7 +161,7 @@ func TestJSValEscaper(t *testing.T) { } // Make sure that escaping corner cases are not broken // by nesting. - a := []interface{}{test.x} + a := []any{test.x} want := "[" + strings.TrimSpace(test.js) + "]" if js := jsValEscaper(a); js != want { t.Errorf("%+v: want\n\t%q\ngot\n\t%q", a, want, js) @@ -170,7 +171,7 @@ func TestJSValEscaper(t *testing.T) { func TestJSStrEscaper(t *testing.T) { tests := []struct { - x interface{} + x any esc string }{ {"", ``}, @@ -225,7 +226,7 @@ func TestJSStrEscaper(t *testing.T) { func TestJSRegexpEscaper(t *testing.T) { tests := []struct { - x interface{} + x any esc string }{ {"", `(?:)`}, @@ -280,7 +281,7 @@ func TestEscapersOnLower7AndSelectHighCodepoints(t *testing.T) { tests := []struct { name string - escaper func(...interface{}) string + escaper func(...any) string escaped string }{ { diff --git a/tpl/internal/go_templates/htmltemplate/multi_test.go b/tpl/internal/go_templates/htmltemplate/multi_test.go index fd61c3f13..14cd7c766 100644 --- a/tpl/internal/go_templates/htmltemplate/multi_test.go +++ b/tpl/internal/go_templates/htmltemplate/multi_test.go @@ -4,6 +4,7 @@ // Tests for multiple-template execution, copied from text/template. +//go:build go1.13 && !windows // +build go1.13,!windows package template diff --git a/tpl/internal/go_templates/htmltemplate/template.go b/tpl/internal/go_templates/htmltemplate/template.go index 263018969..b4ccaa648 100644 --- a/tpl/internal/go_templates/htmltemplate/template.go +++ b/tpl/internal/go_templates/htmltemplate/template.go @@ -118,7 +118,7 @@ func (t *Template) escape() error { // the output writer. // A template may be executed safely in parallel, although if parallel // executions share a Writer the output may be interleaved. -func (t *Template) Execute(wr io.Writer, data interface{}) error { +func (t *Template) Execute(wr io.Writer, data any) error { if err := t.escape(); err != nil { return err } @@ -132,7 +132,7 @@ func (t *Template) Execute(wr io.Writer, data interface{}) error { // the output writer. // A template may be executed safely in parallel, although if parallel // executions share a Writer the output may be interleaved. -func (t *Template) ExecuteTemplate(wr io.Writer, name string, data interface{}) error { +func (t *Template) ExecuteTemplate(wr io.Writer, name string, data any) error { tmpl, err := t.lookupAndEscapeTemplate(name) if err != nil { return err @@ -336,7 +336,7 @@ func (t *Template) Name() string { // terminates and Execute returns that error. FuncMap has the same base type // as FuncMap in "text/template", copied here so clients need not import // "text/template". -type FuncMap map[string]interface{} +type FuncMap map[string]any // Funcs adds the elements of the argument map to the template's function map. // It must be called before the template is parsed. @@ -487,7 +487,7 @@ func parseGlob(t *Template, pattern string) (*Template, error) { // IsTrue reports whether the value is 'true', in the sense of not the zero of its type, // and whether the value has a meaningful truth value. This is the definition of // truth used by if and other such actions. -func IsTrue(val interface{}) (truth, ok bool) { +func IsTrue(val any) (truth, ok bool) { return template.IsTrue(val) } diff --git a/tpl/internal/go_templates/htmltemplate/template_test.go b/tpl/internal/go_templates/htmltemplate/template_test.go index 562d50b22..8a8f2f38c 100644 --- a/tpl/internal/go_templates/htmltemplate/template_test.go +++ b/tpl/internal/go_templates/htmltemplate/template_test.go @@ -2,6 +2,7 @@ // 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 @@ -209,7 +210,7 @@ func (c *testCase) mustNotParse(t *Template, text string) { } } -func (c *testCase) mustExecute(t *Template, val interface{}, want string) { +func (c *testCase) mustExecute(t *Template, val any, want string) { var buf bytes.Buffer err := t.Execute(&buf, val) if err != nil { diff --git a/tpl/internal/go_templates/htmltemplate/transition_test.go b/tpl/internal/go_templates/htmltemplate/transition_test.go index 00b0ff6ca..0bd38800f 100644 --- a/tpl/internal/go_templates/htmltemplate/transition_test.go +++ b/tpl/internal/go_templates/htmltemplate/transition_test.go @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. +//go:build go1.13 && !windows // +build go1.13,!windows package template diff --git a/tpl/internal/go_templates/htmltemplate/url.go b/tpl/internal/go_templates/htmltemplate/url.go index 6f8185a4e..93905586a 100644 --- a/tpl/internal/go_templates/htmltemplate/url.go +++ b/tpl/internal/go_templates/htmltemplate/url.go @@ -32,7 +32,7 @@ import ( // To allow URLs containing other schemes to bypass this filter, developers must // explicitly indicate that such a URL is expected and safe by encapsulating it // in a template.URL value. -func urlFilter(args ...interface{}) string { +func urlFilter(args ...any) string { s, t := stringify(args...) if t == contentTypeURL { return s @@ -46,9 +46,7 @@ func urlFilter(args ...interface{}) string { // isSafeURL is true if s is a relative URL or if URL has a protocol in // (http, https, mailto). func isSafeURL(s string) bool { - if i := strings.IndexRune(s, ':'); i >= 0 && !strings.ContainsRune(s[:i], '/') { - - protocol := s[:i] + if protocol, _, ok := strings.Cut(s, ":"); ok && !strings.Contains(protocol, "/") { if !strings.EqualFold(protocol, "http") && !strings.EqualFold(protocol, "https") && !strings.EqualFold(protocol, "mailto") { return false } @@ -58,7 +56,7 @@ func isSafeURL(s string) bool { // urlEscaper produces an output that can be embedded in a URL query. // The output can be embedded in an HTML attribute without further escaping. -func urlEscaper(args ...interface{}) string { +func urlEscaper(args ...any) string { return urlProcessor(false, args...) } @@ -67,13 +65,13 @@ func urlEscaper(args ...interface{}) string { // The normalizer does not encode all HTML specials. Specifically, it does not // encode '&' so correct embedding in an HTML attribute requires escaping of // '&' to '&'. -func urlNormalizer(args ...interface{}) string { +func urlNormalizer(args ...any) string { return urlProcessor(true, args...) } // urlProcessor normalizes (when norm is true) or escapes its input to produce // a valid hierarchical or opaque URL part. -func urlProcessor(norm bool, args ...interface{}) string { +func urlProcessor(norm bool, args ...any) string { s, t := stringify(args...) if t == contentTypeURL { norm = true @@ -143,7 +141,7 @@ func processURLOnto(s string, norm bool, b *bytes.Buffer) bool { // Filters and normalizes srcset values which are comma separated // URLs followed by metadata. -func srcsetFilterAndEscaper(args ...interface{}) string { +func srcsetFilterAndEscaper(args ...any) string { s, t := stringify(args...) switch t { case contentTypeSrcset: diff --git a/tpl/internal/go_templates/htmltemplate/url_test.go b/tpl/internal/go_templates/htmltemplate/url_test.go index ff0459ffd..72c8a4fe9 100644 --- a/tpl/internal/go_templates/htmltemplate/url_test.go +++ b/tpl/internal/go_templates/htmltemplate/url_test.go @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. +//go:build go1.13 && !windows // +build go1.13,!windows package template @@ -50,7 +51,7 @@ func TestURLFilters(t *testing.T) { tests := []struct { name string - escaper func(...interface{}) string + escaper func(...any) string escaped string }{ { diff --git a/tpl/internal/go_templates/testenv/testenv.go b/tpl/internal/go_templates/testenv/testenv.go index e0a0b4ec6..510b5406e 100644 --- a/tpl/internal/go_templates/testenv/testenv.go +++ b/tpl/internal/go_templates/testenv/testenv.go @@ -11,6 +11,7 @@ package testenv import ( + "bytes" "errors" "flag" "github.com/gohugoio/hugo/tpl/internal/go_templates/cfg" @@ -22,6 +23,7 @@ import ( "strings" "sync" "testing" + "time" ) // Builder reports the name of the builder running this test @@ -306,3 +308,59 @@ func SkipIfShortAndSlow(t testing.TB) { t.Skipf("skipping test in -short mode on %s", runtime.GOARCH) } } + +// RunWithTimeout runs cmd and returns its combined output. If the +// subprocess exits with a non-zero status, it will log that status +// and return a non-nil error, but this is not considered fatal. +func RunWithTimeout(t testing.TB, cmd *exec.Cmd) ([]byte, error) { + args := cmd.Args + if args == nil { + args = []string{cmd.Path} + } + + var b bytes.Buffer + cmd.Stdout = &b + cmd.Stderr = &b + if err := cmd.Start(); err != nil { + t.Fatalf("starting %s: %v", args, err) + } + + // If the process doesn't complete within 1 minute, + // assume it is hanging and kill it to get a stack trace. + p := cmd.Process + done := make(chan bool) + go func() { + scale := 1 + // This GOARCH/GOOS test is copied from cmd/dist/test.go. + // TODO(iant): Have cmd/dist update the environment variable. + if runtime.GOARCH == "arm" || runtime.GOOS == "windows" { + scale = 2 + } + if s := os.Getenv("GO_TEST_TIMEOUT_SCALE"); s != "" { + if sc, err := strconv.Atoi(s); err == nil { + scale = sc + } + } + + select { + case <-done: + case <-time.After(time.Duration(scale) * time.Minute): + p.Signal(Sigquit) + // If SIGQUIT doesn't do it after a little + // while, kill the process. + select { + case <-done: + case <-time.After(time.Duration(scale) * 30 * time.Second): + p.Signal(os.Kill) + } + } + }() + + err := cmd.Wait() + if err != nil { + t.Logf("%s exit status: %v", args, err) + } + close(done) + + return b.Bytes(), err +} diff --git a/tpl/internal/go_templates/testenv/testenv_cgo.go b/tpl/internal/go_templates/testenv/testenv_cgo.go index e3d4d16b3..7426a29c1 100644 --- a/tpl/internal/go_templates/testenv/testenv_cgo.go +++ b/tpl/internal/go_templates/testenv/testenv_cgo.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -// +build cgo +//go:build cgo package testenv diff --git a/tpl/internal/go_templates/testenv/testenv_notunix.go b/tpl/internal/go_templates/testenv/testenv_notunix.go new file mode 100644 index 000000000..180206bc9 --- /dev/null +++ b/tpl/internal/go_templates/testenv/testenv_notunix.go @@ -0,0 +1,13 @@ +// Copyright 2021 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build windows || plan9 || (js && wasm) + +package testenv + +import "os" + +// Sigquit is the signal to send to kill a hanging subprocess. +// On Unix we send SIGQUIT, but on non-Unix we only have os.Kill. +var Sigquit = os.Kill diff --git a/tpl/internal/go_templates/testenv/testenv_notwin.go b/tpl/internal/go_templates/testenv/testenv_notwin.go index ccb5d5585..81171fd19 100644 --- a/tpl/internal/go_templates/testenv/testenv_notwin.go +++ b/tpl/internal/go_templates/testenv/testenv_notwin.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -// +build !windows +//go:build !windows package testenv diff --git a/tpl/internal/go_templates/testenv/testenv_unix.go b/tpl/internal/go_templates/testenv/testenv_unix.go new file mode 100644 index 000000000..3dc5daf45 --- /dev/null +++ b/tpl/internal/go_templates/testenv/testenv_unix.go @@ -0,0 +1,13 @@ +// Copyright 2021 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris + +package testenv + +import "syscall" + +// Sigquit is the signal to send to kill a hanging subprocess. +// Send SIGQUIT to get a stack trace. +var Sigquit = syscall.SIGQUIT diff --git a/tpl/internal/go_templates/texttemplate/doc.go b/tpl/internal/go_templates/texttemplate/doc.go index 7b3029433..10093881f 100644 --- a/tpl/internal/go_templates/texttemplate/doc.go +++ b/tpl/internal/go_templates/texttemplate/doc.go @@ -112,6 +112,14 @@ data, defined in detail in the corresponding sections that follow. T0 is executed; otherwise, dot is set to the successive elements of the array, slice, or map and T1 is executed. + {{break}} + The innermost {{range pipeline}} loop is ended early, stopping the + current iteration and bypassing all remaining iterations. + + {{continue}} + The current iteration of the innermost {{range pipeline}} loop is + stopped, and the loop starts the next iteration. + {{template "name"}} The template with the specified name is executed with nil data. @@ -307,9 +315,10 @@ Predefined global functions are named as follows. and Returns the boolean AND of its arguments by returning the - first empty argument or the last argument, that is, - "and x y" behaves as "if x then y else x". All the - arguments are evaluated. + first empty argument or the last argument. That is, + "and x y" behaves as "if x then y else x." + Evaluation proceeds through the arguments left to right + and returns when the result is determined. call Returns the result of calling the first argument, which must be a function, with the remaining arguments as parameters. @@ -344,8 +353,9 @@ Predefined global functions are named as follows. or Returns the boolean OR of its arguments by returning the first non-empty argument or the last argument, that is, - "or x y" behaves as "if x then x else y". All the - arguments are evaluated. + "or x y" behaves as "if x then x else y". + Evaluation proceeds through the arguments left to right + and returns when the result is determined. print An alias for fmt.Sprint printf diff --git a/tpl/internal/go_templates/texttemplate/example_test.go b/tpl/internal/go_templates/texttemplate/example_test.go index f192cac4f..295a810b8 100644 --- a/tpl/internal/go_templates/texttemplate/example_test.go +++ b/tpl/internal/go_templates/texttemplate/example_test.go @@ -2,6 +2,7 @@ // 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 diff --git a/tpl/internal/go_templates/texttemplate/examplefiles_test.go b/tpl/internal/go_templates/texttemplate/examplefiles_test.go index 5a95b7078..bc91e87f9 100644 --- a/tpl/internal/go_templates/texttemplate/examplefiles_test.go +++ b/tpl/internal/go_templates/texttemplate/examplefiles_test.go @@ -2,6 +2,7 @@ // 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 diff --git a/tpl/internal/go_templates/texttemplate/examplefunc_test.go b/tpl/internal/go_templates/texttemplate/examplefunc_test.go index 62aab02fb..4a13b1f9a 100644 --- a/tpl/internal/go_templates/texttemplate/examplefunc_test.go +++ b/tpl/internal/go_templates/texttemplate/examplefunc_test.go @@ -2,6 +2,7 @@ // 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 diff --git a/tpl/internal/go_templates/texttemplate/exec.go b/tpl/internal/go_templates/texttemplate/exec.go index 1775d0554..4460771cb 100644 --- a/tpl/internal/go_templates/texttemplate/exec.go +++ b/tpl/internal/go_templates/texttemplate/exec.go @@ -5,14 +5,14 @@ package template import ( + "errors" "fmt" + "github.com/gohugoio/hugo/tpl/internal/go_templates/fmtsort" + "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate/parse" "io" "reflect" "runtime" "strings" - - "github.com/gohugoio/hugo/tpl/internal/go_templates/fmtsort" - "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate/parse" ) // maxExecDepth specifies the maximum stack depth of templates within @@ -126,7 +126,7 @@ func (e ExecError) Unwrap() error { } // errorf records an ExecError and terminates processing. -func (s *state) errorf(format string, args ...interface{}) { +func (s *state) errorf(format string, args ...any) { name := doublePercent(s.tmpl.Name()) if s.node == nil { format = fmt.Sprintf("template: %s: %s", name, format) @@ -179,7 +179,7 @@ func errRecover(errp *error) { // the output writer. // A template may be executed safely in parallel, although if parallel // executions share a Writer the output may be interleaved. -func (t *Template) ExecuteTemplate(wr io.Writer, name string, data interface{}) error { +func (t *Template) ExecuteTemplate(wr io.Writer, name string, data any) error { tmpl := t.Lookup(name) if tmpl == nil { return fmt.Errorf("template: no template %q associated with template %q", name, t.name) @@ -197,11 +197,11 @@ func (t *Template) ExecuteTemplate(wr io.Writer, name string, data interface{}) // // If data is a reflect.Value, the template applies to the concrete // value that the reflect.Value holds, as in fmt.Print. -func (t *Template) Execute(wr io.Writer, data interface{}) error { +func (t *Template) Execute(wr io.Writer, data any) error { return t.execute(wr, data) } -func (t *Template) execute(wr io.Writer, data interface{}) (err error) { +func (t *Template) execute(wr io.Writer, data any) (err error) { defer errRecover(&err) value, ok := data.(reflect.Value) if !ok { @@ -228,7 +228,6 @@ func (t *Template) DefinedTemplates() string { return "" } var b strings.Builder - // temporary Hugo-fix t.muTmpl.RLock() defer t.muTmpl.RUnlock() for name, tmpl := range t.tmpl { @@ -245,6 +244,12 @@ func (t *Template) DefinedTemplates() string { return b.String() } +// Sentinel errors for use with panic to signal early exits from range loops. +var ( + walkBreak = errors.New("break") + walkContinue = errors.New("continue") +) + // Walk functions step through the major pieces of the template structure, // generating output as they go. func (s *state) walk(dot reflect.Value, node parse.Node) { @@ -257,7 +262,11 @@ func (s *state) walk(dot reflect.Value, node parse.Node) { if len(node.Pipe.Decl) == 0 { s.printValue(node, val) } + case *parse.BreakNode: + panic(walkBreak) case *parse.CommentNode: + case *parse.ContinueNode: + panic(walkContinue) case *parse.IfNode: s.walkIfOrWith(parse.NodeIf, dot, node.Pipe, node.List, node.ElseList) case *parse.ListNode: @@ -302,7 +311,7 @@ func (s *state) walkIfOrWith(typ parse.NodeType, dot reflect.Value, pipe *parse. // IsTrue reports whether the value is 'true', in the sense of not the zero of its type, // and whether the value has a meaningful truth value. This is the definition of // truth used by if and other such actions. -func IsTrue(val interface{}) (truth, ok bool) { +func IsTrue(val any) (truth, ok bool) { return isTrue(reflect.ValueOf(val)) } @@ -318,7 +327,7 @@ func isTrueOld(val reflect.Value) (truth, ok bool) { truth = val.Bool() case reflect.Complex64, reflect.Complex128: truth = val.Complex() != 0 - case reflect.Chan, reflect.Func, reflect.Ptr, reflect.Interface: + case reflect.Chan, reflect.Func, reflect.Pointer, reflect.Interface: truth = !val.IsNil() case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: truth = val.Int() != 0 @@ -336,6 +345,11 @@ func isTrueOld(val reflect.Value) (truth, ok bool) { func (s *state) walkRange(dot reflect.Value, r *parse.RangeNode) { s.at(r) + defer func() { + if r := recover(); r != nil && r != walkBreak { + panic(r) + } + }() defer s.pop(s.mark()) val, _ := indirect(s.evalPipeline(dot, r.Pipe)) // mark top of stack before any variables in the body are pushed. @@ -349,8 +363,14 @@ func (s *state) walkRange(dot reflect.Value, r *parse.RangeNode) { if len(r.Pipe.Decl) > 1 { s.setTopVar(2, index) } + defer s.pop(mark) + defer func() { + // Consume panic(walkContinue) + if r := recover(); r != nil && r != walkContinue { + panic(r) + } + }() s.walk(elem, r.List) - s.pop(mark) } switch val.Kind() { case reflect.Array, reflect.Slice: @@ -574,11 +594,11 @@ func (s *state) evalFieldChain(dot, receiver reflect.Value, node parse.Node, ide func (s *state) evalFunctionOld(dot reflect.Value, node *parse.IdentifierNode, cmd parse.Node, args []parse.Node, final reflect.Value) reflect.Value { s.at(node) name := node.Ident - function, ok := findFunction(name, s.tmpl) + function, isBuiltin, ok := findFunction(name, s.tmpl) if !ok { s.errorf("%q is not a defined function", name) } - return s.evalCall(dot, function, cmd, name, args, final) + return s.evalCall(dot, function, isBuiltin, cmd, name, args, final) } // evalField evaluates an expression like (.Field) or (.Field arg1 arg2). @@ -603,11 +623,11 @@ func (s *state) evalFieldOld(dot reflect.Value, fieldName string, node parse.Nod // Unless it's an interface, need to get to a value of type *T to guarantee // we see all methods of T and *T. ptr := receiver - if ptr.Kind() != reflect.Interface && ptr.Kind() != reflect.Ptr && ptr.CanAddr() { + if ptr.Kind() != reflect.Interface && ptr.Kind() != reflect.Pointer && ptr.CanAddr() { ptr = ptr.Addr() } if method := ptr.MethodByName(fieldName); method.IsValid() { - return s.evalCall(dot, method, node, fieldName, args, final) + return s.evalCall(dot, method, false, node, fieldName, args, final) } hasArgs := len(args) > 1 || final != missingVal // It's not a method; must be a field of a struct or an element of a map. @@ -615,10 +635,13 @@ func (s *state) evalFieldOld(dot reflect.Value, fieldName string, node parse.Nod case reflect.Struct: tField, ok := receiver.Type().FieldByName(fieldName) if ok { - field := receiver.FieldByIndex(tField.Index) - if tField.PkgPath != "" { // field is unexported + field, err := receiver.FieldByIndexErr(tField.Index) + if !tField.IsExported() { s.errorf("%s is an unexported field of struct type %s", fieldName, typ) } + if err != nil { + s.errorf("%v", err) + } // If it's a function, we must call it. if hasArgs { s.errorf("%s has arguments but cannot be invoked as function", fieldName) @@ -645,7 +668,7 @@ func (s *state) evalFieldOld(dot reflect.Value, fieldName string, node parse.Nod } return result } - case reflect.Ptr: + case reflect.Pointer: etyp := receiver.Type().Elem() if etyp.Kind() == reflect.Struct { if _, ok := etyp.FieldByName(fieldName); !ok { @@ -671,7 +694,7 @@ var ( // evalCall executes a function or method call. If it's a method, fun already has the receiver bound, so // it looks just like a function call. The arg list, if non-nil, includes (in the manner of the shell), arg[0] // as the function itself. -func (s *state) evalCallOld(dot, fun reflect.Value, node parse.Node, name string, args []parse.Node, final reflect.Value) reflect.Value { +func (s *state) evalCallOld(dot, fun reflect.Value, isBuiltin bool, node parse.Node, name string, args []parse.Node, final reflect.Value) reflect.Value { if args != nil { args = args[1:] // Zeroth arg is function name/node; not passed to function. } @@ -693,6 +716,38 @@ func (s *state) evalCallOld(dot, fun reflect.Value, node parse.Node, name string // TODO: This could still be a confusing error; maybe goodFunc should provide info. s.errorf("can't call method/function %q with %d results", name, typ.NumOut()) } + + unwrap := func(v reflect.Value) reflect.Value { + if v.Type() == reflectValueType { + v = v.Interface().(reflect.Value) + } + return v + } + + // Special case for builtin and/or, which short-circuit. + if isBuiltin && (name == "and" || name == "or") { + argType := typ.In(0) + var v reflect.Value + for _, arg := range args { + v = s.evalArg(dot, argType, arg).Interface().(reflect.Value) + if truth(v) == (name == "or") { + // This value was already unwrapped + // by the .Interface().(reflect.Value). + return v + } + } + if final != 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. + // We don't have to evaluate final, but we do + // have to check its type. Then, since we are + // going to return it, we have to unwrap it. + v = unwrap(s.validateType(final, argType)) + } + return v + } + // Build the arg list. argv := make([]reflect.Value, numIn) // Args must be evaluated. Fixed args first. @@ -728,18 +783,15 @@ func (s *state) evalCallOld(dot, fun reflect.Value, node parse.Node, name string // error to the caller. if err != nil { s.at(node) - s.errorf("error calling %s: %v", name, err) - } - if v.Type() == reflectValueType { - v = v.Interface().(reflect.Value) + s.errorf("error calling %s: %w", name, err) } - return v + return unwrap(v) } // canBeNil reports whether an untyped nil can be assigned to the type. See reflect.Zero. func canBeNil(typ reflect.Type) bool { switch typ.Kind() { - case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice: + case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Pointer, reflect.Slice: return true case reflect.Struct: return typ == reflectValueType @@ -776,15 +828,13 @@ func (s *state) validateType(value reflect.Value, typ reflect.Type) reflect.Valu // are much more constrained, so it makes more sense there than here. // Besides, one is almost always all you need. switch { - case value.Kind() == reflect.Ptr && value.Type().Elem().AssignableTo(typ): + case value.Kind() == reflect.Pointer && value.Type().Elem().AssignableTo(typ): value = value.Elem() if !value.IsValid() { s.errorf("dereference of nil pointer of type %s", typ) } - case reflect.PtrTo(value.Type()).AssignableTo(typ) && value.CanAddr(): + case reflect.PointerTo(value.Type()).AssignableTo(typ) && value.CanAddr(): value = value.Addr() - case value.IsZero(): - s.errorf("got <nil>, expected %s", typ) default: s.errorf("wrong type for value; expected %s; got %s", typ, value.Type()) } @@ -935,7 +985,7 @@ func (s *state) evalEmptyInterface(dot reflect.Value, n parse.Node) reflect.Valu // if it's nil. If the returned bool is true, the returned value's kind will be // either a pointer or interface. func indirect(v reflect.Value) (rv reflect.Value, isNil bool) { - for ; v.Kind() == reflect.Ptr || v.Kind() == reflect.Interface; v = v.Elem() { + for ; v.Kind() == reflect.Pointer || v.Kind() == reflect.Interface; v = v.Elem() { if v.IsNil() { return v, true } @@ -973,8 +1023,8 @@ func (s *state) printValue(n parse.Node, v reflect.Value) { // printableValue returns the, possibly indirected, interface value inside v that // is best for a call to formatted printer. -func printableValue(v reflect.Value) (interface{}, bool) { - if v.Kind() == reflect.Ptr { +func printableValue(v reflect.Value) (any, bool) { + if v.Kind() == reflect.Pointer { v, _ = indirect(v) // fmt.Fprint handles nil. } if !v.IsValid() { @@ -982,7 +1032,7 @@ func printableValue(v reflect.Value) (interface{}, bool) { } if !v.Type().Implements(errorType) && !v.Type().Implements(fmtStringerType) { - if v.CanAddr() && (reflect.PtrTo(v.Type()).Implements(errorType) || reflect.PtrTo(v.Type()).Implements(fmtStringerType)) { + if v.CanAddr() && (reflect.PointerTo(v.Type()).Implements(errorType) || reflect.PointerTo(v.Type()).Implements(fmtStringerType)) { v = v.Addr() } else { switch v.Kind() { diff --git a/tpl/internal/go_templates/texttemplate/exec_test.go b/tpl/internal/go_templates/texttemplate/exec_test.go index 88de0fd49..64cb87ec6 100644 --- a/tpl/internal/go_templates/texttemplate/exec_test.go +++ b/tpl/internal/go_templates/texttemplate/exec_test.go @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. +//go:build go1.13 && !windows // +build go1.13,!windows package template @@ -14,6 +15,7 @@ import ( "io" "reflect" "strings" + "sync" "testing" ) @@ -47,7 +49,7 @@ type T struct { MSI map[string]int MSIone map[string]int // one element, for deterministic output MSIEmpty map[string]int - MXI map[interface{}]int + MXI map[any]int MII map[int]int MI32S map[int32]string MI64S map[int64]string @@ -57,11 +59,11 @@ type T struct { MUI8S map[uint8]string SMSI []map[string]int // Empty interfaces; used to see if we can dig inside one. - Empty0 interface{} // nil - Empty1 interface{} - Empty2 interface{} - Empty3 interface{} - Empty4 interface{} + Empty0 any // nil + Empty1 any + Empty2 any + Empty3 any + Empty4 any // Non-empty interfaces. NonEmptyInterface I NonEmptyInterfacePtS *I @@ -139,7 +141,7 @@ var tVal = &T{ SB: []bool{true, false}, MSI: map[string]int{"one": 1, "two": 2, "three": 3}, MSIone: map[string]int{"one": 1}, - MXI: map[interface{}]int{"one": 1}, + MXI: map[any]int{"one": 1}, MII: map[int]int{1: 1}, MI32S: map[int32]string{1: "one", 2: "two"}, MI64S: map[int64]string{2: "i642", 3: "i643"}, @@ -210,7 +212,7 @@ func (t *T) Method2(a uint16, b string) string { return fmt.Sprintf("Method2: %d %s", a, b) } -func (t *T) Method3(v interface{}) string { +func (t *T) Method3(v any) string { return fmt.Sprintf("Method3: %v", v) } @@ -250,7 +252,7 @@ func (u *U) TrueFalse(b bool) string { return "" } -func typeOf(arg interface{}) string { +func typeOf(arg any) string { return fmt.Sprintf("%T", arg) } @@ -258,7 +260,7 @@ type execTest struct { name string input string output string - data interface{} + data any ok bool } @@ -391,7 +393,7 @@ var execTests = []execTest{ {".VariadicFuncInt", "{{call .VariadicFuncInt 33 `he` `llo`}}", "33=<he+llo>", tVal, true}, {"if .BinaryFunc call", "{{ if .BinaryFunc}}{{call .BinaryFunc `1` `2`}}{{end}}", "[1=2]", tVal, true}, {"if not .BinaryFunc call", "{{ if not .BinaryFunc}}{{call .BinaryFunc `1` `2`}}{{else}}No{{end}}", "No", tVal, true}, - {"Interface Call", `{{stringer .S}}`, "foozle", map[string]interface{}{"S": bytes.NewBufferString("foozle")}, true}, + {"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}, @@ -482,8 +484,19 @@ var execTests = []execTest{ {"not", "{{not true}} {{not false}}", "false true", nil, true}, {"and", "{{and false 0}} {{and 1 0}} {{and 0 true}} {{and 1 1}}", "false 0 0 1", nil, true}, {"or", "{{or 0 0}} {{or 1 0}} {{or 0 true}} {{or 1 1}}", "0 1 true 1", nil, true}, + {"or short-circuit", "{{or 0 1 (die)}}", "1", nil, true}, + {"and short-circuit", "{{and 1 0 (die)}}", "0", nil, true}, + {"or short-circuit2", "{{or 0 0 (die)}}", "", nil, false}, + {"and short-circuit2", "{{and 1 1 (die)}}", "", nil, false}, + {"and pipe-true", "{{1 | and 1}}", "1", nil, true}, + {"and pipe-false", "{{0 | and 1}}", "0", nil, true}, + {"or pipe-true", "{{1 | or 0}}", "1", nil, true}, + {"or pipe-false", "{{0 | or 0}}", "0", nil, true}, + {"and undef", "{{and 1 .Unknown}}", "<no value>", nil, true}, + {"or undef", "{{or 0 .Unknown}}", "<no value>", nil, true}, {"boolean if", "{{if and true 1 `hi`}}TRUE{{else}}FALSE{{end}}", "TRUE", tVal, true}, {"boolean if not", "{{if and true 1 `hi` | not}}TRUE{{else}}FALSE{{end}}", "FALSE", nil, true}, + {"boolean if pipe", "{{if true | not | and 1}}TRUE{{else}}FALSE{{end}}", "FALSE", nil, true}, // Indexing. {"slice[0]", "{{index .SI 0}}", "3", tVal, true}, @@ -565,6 +578,8 @@ var execTests = []execTest{ {"range empty no else", "{{range .SIEmpty}}-{{.}}-{{end}}", "", tVal, true}, {"range []int else", "{{range .SI}}-{{.}}-{{else}}EMPTY{{end}}", "-3--4--5-", tVal, true}, {"range empty else", "{{range .SIEmpty}}-{{.}}-{{else}}EMPTY{{end}}", "EMPTY", tVal, true}, + {"range []int break else", "{{range .SI}}-{{.}}-{{break}}NOTREACHED{{else}}EMPTY{{end}}", "-3-", tVal, true}, + {"range []int continue else", "{{range .SI}}-{{.}}-{{continue}}NOTREACHED{{else}}EMPTY{{end}}", "-3--4--5-", tVal, true}, {"range []bool", "{{range .SB}}-{{.}}-{{end}}", "-true--false-", tVal, true}, {"range []int method", "{{range .SI | .MAdd .I}}-{{.}}-{{end}}", "-20--21--22-", tVal, true}, {"range map", "{{range .MSI}}-{{.}}-{{end}}", "-1--3--2-", tVal, true}, @@ -736,7 +751,7 @@ func add(args ...int) int { return sum } -func echo(arg interface{}) interface{} { +func echo(arg any) any { return arg } @@ -755,7 +770,7 @@ func stringer(s fmt.Stringer) string { return s.String() } -func mapOfThree() interface{} { +func mapOfThree() any { return map[string]int{"three": 3} } @@ -765,6 +780,7 @@ func testExecute(execTests []execTest, template *Template, t *testing.T) { "add": add, "count": count, "dddArg": dddArg, + "die": func() bool { panic("die") }, "echo": echo, "makemap": makemap, "mapOfThree": mapOfThree, @@ -904,6 +920,28 @@ func TestExecError(t *testing.T) { } } +type CustomError struct{} + +func (*CustomError) Error() string { return "heyo !" } + +// Check that a custom error can be returned. +func TestExecError_CustomError(t *testing.T) { + failingFunc := func() (string, error) { + return "", &CustomError{} + } + tmpl := Must(New("top").Funcs(FuncMap{ + "err": failingFunc, + }).Parse("{{ err }}")) + + var b bytes.Buffer + err := tmpl.Execute(&b, nil) + + var e *CustomError + if !errors.As(err, &e) { + t.Fatalf("expected custom error; got %s", err) + } +} + func TestJSEscaping(t *testing.T) { testCases := []struct { in, exp string @@ -1180,8 +1218,11 @@ var cmpTests = []cmpTest{ {"eq .Ptr .NilPtr", "false", true}, {"eq .NilPtr .NilPtr", "true", true}, {"eq .Iface1 .Iface1", "true", true}, - {"eq .Iface1 .Iface2", "false", true}, - {"eq .Iface2 .Iface2", "true", true}, + {"eq .Iface1 .NilIface", "false", true}, + {"eq .NilIface .NilIface", "true", true}, + {"eq .NilIface .Iface1", "false", true}, + {"eq .NilIface 0", "false", true}, + {"eq 0 .NilIface", "false", true}, // Errors {"eq `xy` 1", "", false}, // Different types. {"eq 2 2.0", "", false}, // Different types. @@ -1196,12 +1237,12 @@ var cmpTests = []cmpTest{ func TestComparison(t *testing.T) { b := new(bytes.Buffer) var cmpStruct = struct { - Uthree, Ufour uint - NegOne, Three int - Ptr, NilPtr *int - Map map[int]int - V1, V2 V - Iface1, Iface2 fmt.Stringer + Uthree, Ufour uint + NegOne, Three int + Ptr, NilPtr *int + Map map[int]int + V1, V2 V + Iface1, NilIface fmt.Stringer }{ Uthree: 3, Ufour: 4, @@ -1430,7 +1471,7 @@ func TestBlock(t *testing.T) { func TestEvalFieldErrors(t *testing.T) { tests := []struct { name, src string - value interface{} + value any want string }{ { @@ -1573,7 +1614,7 @@ func TestInterfaceValues(t *testing.T) { for _, tt := range tests { tmpl := Must(New("tmpl").Parse(tt.text)) var buf bytes.Buffer - err := tmpl.Execute(&buf, map[string]interface{}{ + err := tmpl.Execute(&buf, map[string]any{ "PlusOne": func(n int) int { return n + 1 }, @@ -1602,7 +1643,7 @@ func TestInterfaceValues(t *testing.T) { // Check that panics during calls are recovered and returned as errors. func TestExecutePanicDuringCall(t *testing.T) { - funcs := map[string]interface{}{ + funcs := map[string]any{ "doPanic": func() string { panic("custom panic string") }, @@ -1610,7 +1651,7 @@ func TestExecutePanicDuringCall(t *testing.T) { tests := []struct { name string input string - data interface{} + data any wantErr string }{ { @@ -1712,3 +1753,63 @@ func TestIssue43065(t *testing.T) { t.Errorf("%s", err) } } + +// Issue 39807: data race in html/template & text/template +func TestIssue39807(t *testing.T) { + var wg sync.WaitGroup + + tplFoo, err := New("foo").Parse(`{{ template "bar" . }}`) + if err != nil { + t.Error(err) + } + + tplBar, err := New("bar").Parse("bar") + if err != nil { + t.Error(err) + } + + gofuncs := 10 + numTemplates := 10 + + for i := 1; i <= gofuncs; i++ { + wg.Add(1) + go func() { + defer wg.Done() + for j := 0; j < numTemplates; j++ { + _, err := tplFoo.AddParseTree(tplBar.Name(), tplBar.Tree) + if err != nil { + t.Error(err) + } + err = tplFoo.Execute(io.Discard, nil) + if err != nil { + t.Error(err) + } + } + }() + } + + wg.Wait() +} + +// Issue 48215: embedded nil pointer causes panic. +// Fixed by adding FieldByIndexErr to the reflect package. +func TestIssue48215(t *testing.T) { + type A struct { + S string + } + type B struct { + *A + } + tmpl, err := New("").Parse(`{{ .S }}`) + if err != nil { + t.Fatal(err) + } + err = tmpl.Execute(io.Discard, B{}) + // We expect an error, not a panic. + if err == nil { + t.Fatal("did not get error for nil embedded struct") + } + if !strings.Contains(err.Error(), "reflect: indirection through nil pointer to embedded struct field A") { + t.Fatal(err) + } +} diff --git a/tpl/internal/go_templates/texttemplate/funcs.go b/tpl/internal/go_templates/texttemplate/funcs.go index 1b6940a84..dca5ed28d 100644 --- a/tpl/internal/go_templates/texttemplate/funcs.go +++ b/tpl/internal/go_templates/texttemplate/funcs.go @@ -23,12 +23,15 @@ import ( // return value evaluates to non-nil during execution, execution terminates and // Execute returns that error. // +// Errors returned by Execute wrap the underlying error; call errors.As to +// uncover them. +// // When template execution invokes a function with an argument list, that list // must be assignable to the function's parameter types. Functions meant to // apply to arguments of arbitrary type can use parameters of type interface{} or // of type reflect.Value. Similarly, functions meant to return a result of arbitrary // type can return interface{} or reflect.Value. -type FuncMap map[string]interface{} +type FuncMap map[string]any // builtins returns the FuncMap. // It is not a global variable so the linker can dead code eliminate @@ -136,18 +139,18 @@ func goodName(name string) bool { } // findFunction looks for a function in the template, and global map. -func findFunction(name string, tmpl *Template) (reflect.Value, bool) { +func findFunction(name string, tmpl *Template) (v reflect.Value, isBuiltin, ok bool) { if tmpl != nil && tmpl.common != nil { tmpl.muFuncs.RLock() defer tmpl.muFuncs.RUnlock() if fn := tmpl.execFuncs[name]; fn.IsValid() { - return fn, true + return fn, false, true } } if fn := builtinFuncs()[name]; fn.IsValid() { - return fn, true + return fn, true, true } - return reflect.Value{}, false + return reflect.Value{}, false, false } // prepareArg checks if value can be used as an argument of type argType, and @@ -344,7 +347,7 @@ func call(fn reflect.Value, args ...reflect.Value) (reflect.Value, error) { var err error if argv[i], err = prepareArg(arg, argType); err != nil { - return reflect.Value{}, fmt.Errorf("arg %d: %s", i, err) + return reflect.Value{}, fmt.Errorf("arg %d: %w", i, err) } } return safeCall(fn, argv) @@ -379,31 +382,13 @@ func truth(arg reflect.Value) bool { // and computes the Boolean AND of its arguments, returning // the first false argument it encounters, or the last argument. func and(arg0 reflect.Value, args ...reflect.Value) reflect.Value { - if !truth(arg0) { - return arg0 - } - for i := range args { - arg0 = args[i] - if !truth(arg0) { - break - } - } - return arg0 + panic("unreachable") // implemented as a special case in evalCall } // or computes the Boolean OR of its arguments, returning // the first true argument it encounters, or the last argument. func or(arg0 reflect.Value, args ...reflect.Value) reflect.Value { - if truth(arg0) { - return arg0 - } - for i := range args { - arg0 = args[i] - if truth(arg0) { - break - } - } - return arg0 + panic("unreachable") // implemented as a special case in evalCall } // not returns the Boolean negation of its argument. @@ -475,7 +460,9 @@ func eq(arg1 reflect.Value, arg2 ...reflect.Value) (bool, error) { case k1 == uintKind && k2 == intKind: truth = arg.Int() >= 0 && arg1.Uint() == uint64(arg.Int()) default: - return false, errBadComparison + if arg1 != zero && arg != zero { + return false, errBadComparison + } } } else { switch k1 { @@ -492,7 +479,7 @@ func eq(arg1 reflect.Value, arg2 ...reflect.Value) (bool, error) { case uintKind: truth = arg1.Uint() == arg.Uint() default: - if arg == zero { + if arg == zero || arg1 == zero { truth = arg1 == arg } else { if t2 := arg.Type(); !t2.Comparable() { @@ -640,7 +627,7 @@ func HTMLEscapeString(s string) string { // HTMLEscaper returns the escaped HTML equivalent of the textual // representation of its arguments. -func HTMLEscaper(args ...interface{}) string { +func HTMLEscaper(args ...any) string { return HTMLEscapeString(evalArgs(args)) } @@ -731,13 +718,13 @@ func jsIsSpecial(r rune) bool { // JSEscaper returns the escaped JavaScript equivalent of the textual // representation of its arguments. -func JSEscaper(args ...interface{}) string { +func JSEscaper(args ...any) string { return JSEscapeString(evalArgs(args)) } // URLQueryEscaper returns the escaped value of the textual representation of // its arguments in a form suitable for embedding in a URL query. -func URLQueryEscaper(args ...interface{}) string { +func URLQueryEscaper(args ...any) string { return url.QueryEscape(evalArgs(args)) } @@ -746,7 +733,7 @@ func URLQueryEscaper(args ...interface{}) string { // except that each argument is indirected (if a pointer), as required, // using the same rules as the default string evaluation during template // execution. -func evalArgs(args []interface{}) string { +func evalArgs(args []any) string { ok := false var s string // Fast path for simple common case. diff --git a/tpl/internal/go_templates/texttemplate/link_test.go b/tpl/internal/go_templates/texttemplate/link_test.go index e602b0e7e..23f6a31fa 100644 --- a/tpl/internal/go_templates/texttemplate/link_test.go +++ b/tpl/internal/go_templates/texttemplate/link_test.go @@ -2,6 +2,7 @@ // 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 @@ -41,11 +42,7 @@ func main() { t.Used() } ` - td, err := os.MkdirTemp("", "text_template_TestDeadCodeElimination") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(td) + td := t.TempDir() if err := os.WriteFile(filepath.Join(td, "x.go"), []byte(prog), 0644); err != nil { t.Fatal(err) diff --git a/tpl/internal/go_templates/texttemplate/multi_test.go b/tpl/internal/go_templates/texttemplate/multi_test.go index 99c2f7759..e3c9ec3ae 100644 --- a/tpl/internal/go_templates/texttemplate/multi_test.go +++ b/tpl/internal/go_templates/texttemplate/multi_test.go @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. +//go:build go1.13 && !windows // +build go1.13,!windows package template @@ -454,3 +455,13 @@ func TestIssue19294(t *testing.T) { } } } + +// Issue 48436 +func TestAddToZeroTemplate(t *testing.T) { + tree, err := parse.Parse("c", cloneText3, "", "", nil, builtins()) + if err != nil { + t.Fatal(err) + } + var tmpl Template + tmpl.AddParseTree("x", tree["c"]) +} diff --git a/tpl/internal/go_templates/texttemplate/option.go b/tpl/internal/go_templates/texttemplate/option.go index addce2d89..1035afad7 100644 --- a/tpl/internal/go_templates/texttemplate/option.go +++ b/tpl/internal/go_templates/texttemplate/option.go @@ -51,13 +51,11 @@ func (t *Template) setOption(opt string) { if opt == "" { panic("empty option string") } - elems := strings.Split(opt, "=") - switch len(elems) { - case 2: - // key=value - switch elems[0] { + // key=value + if key, value, ok := strings.Cut(opt, "="); ok { + switch key { case "missingkey": - switch elems[1] { + switch value { case "invalid", "default": t.option.missingKey = mapInvalid return diff --git a/tpl/internal/go_templates/texttemplate/parse/lex.go b/tpl/internal/go_templates/texttemplate/parse/lex.go index 6784071b1..40d041112 100644 --- a/tpl/internal/go_templates/texttemplate/parse/lex.go +++ b/tpl/internal/go_templates/texttemplate/parse/lex.go @@ -62,6 +62,8 @@ const ( // Keywords appear after all the rest. itemKeyword // used only to delimit the keywords itemBlock // block keyword + itemBreak // break keyword + itemContinue // continue keyword itemDot // the cursor, spelled '.' itemDefine // define keyword itemElse // else keyword @@ -76,6 +78,8 @@ const ( var key = map[string]itemType{ ".": itemDot, "block": itemBlock, + "break": itemBreak, + "continue": itemContinue, "define": itemDefine, "else": itemElse, "end": itemEnd, @@ -119,6 +123,8 @@ type lexer struct { parenDepth int // nesting depth of ( ) exprs line int // 1+number of newlines seen startLine int // start line of this item + breakOK bool // break keyword allowed + continueOK bool // continue keyword allowed } // next returns the next rune in the input. @@ -184,7 +190,7 @@ func (l *lexer) acceptRun(valid string) { // errorf returns an error token and terminates the scan by passing // back a nil pointer that will be the next state, terminating l.nextItem. -func (l *lexer) errorf(format string, args ...interface{}) stateFn { +func (l *lexer) errorf(format string, args ...any) stateFn { l.items <- item{itemError, l.start, fmt.Sprintf(format, args...), l.startLine} return nil } @@ -461,7 +467,12 @@ Loop: } switch { case key[word] > itemKeyword: - l.emit(key[word]) + item := key[word] + if item == itemBreak && !l.breakOK || item == itemContinue && !l.continueOK { + l.emit(itemIdentifier) + } else { + l.emit(item) + } case word[0] == '.': l.emit(itemField) case word == "true", word == "false": diff --git a/tpl/internal/go_templates/texttemplate/parse/lex_test.go b/tpl/internal/go_templates/texttemplate/parse/lex_test.go index 7ba4ee2b8..9189035fe 100644 --- a/tpl/internal/go_templates/texttemplate/parse/lex_test.go +++ b/tpl/internal/go_templates/texttemplate/parse/lex_test.go @@ -2,6 +2,7 @@ // 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 @@ -37,6 +38,8 @@ var itemName = map[itemType]string{ // keywords itemDot: ".", itemBlock: "block", + itemBreak: "break", + itemContinue: "continue", itemDefine: "define", itemElse: "else", itemIf: "if", diff --git a/tpl/internal/go_templates/texttemplate/parse/node.go b/tpl/internal/go_templates/texttemplate/parse/node.go index 177482f9b..47268225c 100644 --- a/tpl/internal/go_templates/texttemplate/parse/node.go +++ b/tpl/internal/go_templates/texttemplate/parse/node.go @@ -71,6 +71,8 @@ const ( NodeVariable // A $ variable. NodeWith // A with action. NodeComment // A comment. + NodeBreak // A break action. + NodeContinue // A continue action. ) // Nodes. @@ -907,6 +909,40 @@ func (i *IfNode) Copy() Node { return i.tr.newIf(i.Pos, i.Line, i.Pipe.CopyPipe(), i.List.CopyList(), i.ElseList.CopyList()) } +// BreakNode represents a {{break}} action. +type BreakNode struct { + tr *Tree + NodeType + Pos + Line int +} + +func (t *Tree) newBreak(pos Pos, line int) *BreakNode { + return &BreakNode{tr: t, NodeType: NodeBreak, Pos: pos, Line: line} +} + +func (b *BreakNode) Copy() Node { return b.tr.newBreak(b.Pos, b.Line) } +func (b *BreakNode) String() string { return "{{break}}" } +func (b *BreakNode) tree() *Tree { return b.tr } +func (b *BreakNode) writeTo(sb *strings.Builder) { sb.WriteString("{{break}}") } + +// ContinueNode represents a {{continue}} action. +type ContinueNode struct { + tr *Tree + NodeType + Pos + Line int +} + +func (t *Tree) newContinue(pos Pos, line int) *ContinueNode { + return &ContinueNode{tr: t, NodeType: NodeContinue, Pos: pos, Line: line} +} + +func (c *ContinueNode) Copy() Node { return c.tr.newContinue(c.Pos, c.Line) } +func (c *ContinueNode) String() string { return "{{continue}}" } +func (c *ContinueNode) tree() *Tree { return c.tr } +func (c *ContinueNode) writeTo(sb *strings.Builder) { sb.WriteString("{{continue}}") } + // RangeNode represents a {{range}} action and its commands. type RangeNode struct { BranchNode diff --git a/tpl/internal/go_templates/texttemplate/parse/parse.go b/tpl/internal/go_templates/texttemplate/parse/parse.go index 5e6e512eb..b0cbe9dfc 100644 --- a/tpl/internal/go_templates/texttemplate/parse/parse.go +++ b/tpl/internal/go_templates/texttemplate/parse/parse.go @@ -24,14 +24,14 @@ type Tree struct { Mode Mode // parsing mode. text string // text parsed to create the template (or its parent) // Parsing only; cleared after parse. - funcs []map[string]interface{} + funcs []map[string]any lex *lexer token [3]item // three-token lookahead for parser. peekCount int vars []string // variables defined at the moment. treeSet map[string]*Tree actionLine int // line of left delim starting action - mode Mode + rangeDepth int } // A mode value is a set of flags (or 0). Modes control parser behavior. @@ -39,6 +39,7 @@ type Mode uint const ( ParseComments Mode = 1 << iota // parse comments and add them to AST + SkipFuncCheck // do not check that functions are defined ) // Copy returns a copy of the Tree. Any parsing state is discarded. @@ -58,7 +59,7 @@ func (t *Tree) Copy() *Tree { // templates described in the argument string. The top-level template will be // given the specified name. If an error is encountered, parsing stops and an // empty map is returned with the error. -func Parse(name, text, leftDelim, rightDelim string, funcs ...map[string]interface{}) (map[string]*Tree, error) { +func Parse(name, text, leftDelim, rightDelim string, funcs ...map[string]any) (map[string]*Tree, error) { treeSet := make(map[string]*Tree) t := New(name) t.text = text @@ -127,7 +128,7 @@ func (t *Tree) peekNonSpace() item { // Parsing. // New allocates a new parse tree with the given name. -func New(name string, funcs ...map[string]interface{}) *Tree { +func New(name string, funcs ...map[string]any) *Tree { return &Tree{ Name: name, funcs: funcs, @@ -157,7 +158,7 @@ func (t *Tree) ErrorContext(n Node) (location, context string) { } // errorf formats the error and terminates processing. -func (t *Tree) errorf(format string, args ...interface{}) { +func (t *Tree) errorf(format string, args ...any) { t.Root = nil format = fmt.Sprintf("template: %s:%d: %s", t.ParseName, t.token[0].line, format) panic(fmt.Errorf(format, args...)) @@ -217,12 +218,14 @@ func (t *Tree) recover(errp *error) { } // startParse initializes the parser, using the lexer. -func (t *Tree) startParse(funcs []map[string]interface{}, lex *lexer, treeSet map[string]*Tree) { +func (t *Tree) startParse(funcs []map[string]any, lex *lexer, treeSet map[string]*Tree) { t.Root = nil t.lex = lex t.vars = []string{"$"} t.funcs = funcs t.treeSet = treeSet + lex.breakOK = !t.hasFunction("break") + lex.continueOK = !t.hasFunction("continue") } // stopParse terminates parsing. @@ -237,7 +240,7 @@ func (t *Tree) stopParse() { // the template for execution. If either action delimiter string is empty, the // default ("{{" or "}}") is used. Embedded template definitions are added to // the treeSet map. -func (t *Tree) Parse(text, leftDelim, rightDelim string, treeSet map[string]*Tree, funcs ...map[string]interface{}) (tree *Tree, err error) { +func (t *Tree) Parse(text, leftDelim, rightDelim string, treeSet map[string]*Tree, funcs ...map[string]any) (tree *Tree, err error) { defer t.recover(&err) t.ParseName = t.Name emitComment := t.Mode&ParseComments != 0 @@ -385,6 +388,10 @@ func (t *Tree) action() (n Node) { switch token := t.nextNonSpace(); token.typ { case itemBlock: return t.blockControl() + case itemBreak: + return t.breakControl(token.pos, token.line) + case itemContinue: + return t.continueControl(token.pos, token.line) case itemElse: return t.elseControl() case itemEnd: @@ -404,6 +411,32 @@ func (t *Tree) action() (n Node) { return t.newAction(token.pos, token.line, t.pipeline("command", itemRightDelim)) } +// Break: +// {{break}} +// Break keyword is past. +func (t *Tree) breakControl(pos Pos, line int) Node { + if token := t.next(); token.typ != itemRightDelim { + t.unexpected(token, "in {{break}}") + } + if t.rangeDepth == 0 { + t.errorf("{{break}} outside {{range}}") + } + return t.newBreak(pos, line) +} + +// Continue: +// {{continue}} +// Continue keyword is past. +func (t *Tree) continueControl(pos Pos, line int) Node { + if token := t.next(); token.typ != itemRightDelim { + t.unexpected(token, "in {{continue}}") + } + if t.rangeDepth == 0 { + t.errorf("{{continue}} outside {{range}}") + } + return t.newContinue(pos, line) +} + // Pipeline: // declarations? command ('|' command)* func (t *Tree) pipeline(context string, end itemType) (pipe *PipeNode) { @@ -479,8 +512,14 @@ func (t *Tree) checkPipeline(pipe *PipeNode, context string) { func (t *Tree) parseControl(allowElseIf bool, context string) (pos Pos, line int, pipe *PipeNode, list, elseList *ListNode) { defer t.popVars(len(t.vars)) pipe = t.pipeline(context, itemRightDelim) + if context == "range" { + t.rangeDepth++ + } var next Node list, next = t.itemList() + if context == "range" { + t.rangeDepth-- + } switch next.Type() { case nodeEnd: //done case nodeElse: @@ -522,7 +561,8 @@ func (t *Tree) ifControl() Node { // {{range pipeline}} itemList {{else}} itemList {{end}} // Range keyword is past. func (t *Tree) rangeControl() Node { - return t.newRange(t.parseControl(false, "range")) + r := t.newRange(t.parseControl(false, "range")) + return r } // With: @@ -689,7 +729,8 @@ func (t *Tree) operand() Node { func (t *Tree) term() Node { switch token := t.nextNonSpace(); token.typ { case itemIdentifier: - if !t.hasFunction(token.val) { + checkFunc := t.Mode&SkipFuncCheck == 0 + if checkFunc && !t.hasFunction(token.val) { t.errorf("function %q not defined", token.val) } return NewIdentifier(token.val).SetTree(t).SetPos(token.pos) diff --git a/tpl/internal/go_templates/texttemplate/parse/parse_test.go b/tpl/internal/go_templates/texttemplate/parse/parse_test.go index 0433a87db..b0e75afa9 100644 --- a/tpl/internal/go_templates/texttemplate/parse/parse_test.go +++ b/tpl/internal/go_templates/texttemplate/parse/parse_test.go @@ -2,6 +2,7 @@ // 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 @@ -232,6 +233,10 @@ var parseTests = []parseTest{ `{{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, @@ -281,6 +286,10 @@ var parseTests = []parseTest{ {"adjacent args", "{{printf 3`x`}}", hasError, ""}, {"adjacent args with .", "{{printf `x`.}}", hasError, ""}, {"extra end after if", "{{if .X}}a{{else if .Y}}b{{end}}{{end}}", hasError, ""}, + {"break outside range", "{{range .}}{{end}} {{break}}", hasError, ""}, + {"continue outside range", "{{range .}}{{end}} {{continue}}", hasError, ""}, + {"break in range else", "{{range .}}{{else}}{{break}}{{end}}", hasError, ""}, + {"continue in range else", "{{range .}}{{else}}{{continue}}{{end}}", hasError, ""}, // Other kinds of assignments and operators aren't available yet. {"bug0a", "{{$x := 0}}{{$x}}", noError, "{{$x := 0}}{{$x}}"}, {"bug0b", "{{$x += 1}}{{$x}}", hasError, ""}, @@ -312,7 +321,7 @@ var parseTests = []parseTest{ {"block definition", `{{block "foo"}}hello{{end}}`, hasError, ""}, } -var builtins = map[string]interface{}{ +var builtins = map[string]any{ "printf": fmt.Sprintf, "contains": strings.Contains, } @@ -381,6 +390,22 @@ func TestParseWithComments(t *testing.T) { } } +func TestSkipFuncCheck(t *testing.T) { + oldTextFormat := textFormat + textFormat = "%q" + defer func() { textFormat = oldTextFormat }() + tr := New("skip func check") + tr.Mode = SkipFuncCheck + tmpl, err := tr.Parse("{{fn 1 2}}", "", "", make(map[string]*Tree)) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + expected := "{{fn 1 2}}" + if result := tmpl.Root.String(); result != expected { + t.Errorf("got\n\t%v\nexpected\n\t%v", result, expected) + } +} + type isEmptyTest struct { name string input string diff --git a/tpl/internal/go_templates/texttemplate/template.go b/tpl/internal/go_templates/texttemplate/template.go index 15f4a2f26..1ba72c194 100644 --- a/tpl/internal/go_templates/texttemplate/template.go +++ b/tpl/internal/go_templates/texttemplate/template.go @@ -5,16 +5,15 @@ package template import ( + "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate/parse" "reflect" "sync" - - "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate/parse" ) // common holds the information shared by related templates. type common struct { - muTmpl sync.RWMutex // protects tmpl (temporary Hugo-fix) tmpl map[string]*Template // Map from name to defined templates. + muTmpl sync.RWMutex // protects tmpl option option // We use two maps, one for parsing and one for execution. // This separation makes the API cleaner since it doesn't @@ -90,7 +89,6 @@ func (t *Template) Clone() (*Template, error) { if t.common == nil { return nt, nil } - // temporary Hugo-fix t.muTmpl.RLock() defer t.muTmpl.RUnlock() for k, v := range t.tmpl { @@ -129,10 +127,9 @@ func (t *Template) copy(c *common) *Template { // its definition. If it has been defined and already has that name, the existing // definition is replaced; otherwise a new template is created, defined, and returned. func (t *Template) AddParseTree(name string, tree *parse.Tree) (*Template, error) { - // temporary Hugo-fix + t.init() t.muTmpl.Lock() defer t.muTmpl.Unlock() - t.init() nt := t if name != t.name { nt = t.New(name) @@ -150,7 +147,6 @@ func (t *Template) Templates() []*Template { return nil } // Return a slice so we don't expose the map. - // temporary Hugo-fix t.muTmpl.RLock() defer t.muTmpl.RUnlock() m := make([]*Template, 0, len(t.tmpl)) @@ -193,7 +189,6 @@ func (t *Template) Lookup(name string) *Template { if t.common == nil { return nil } - // temporary Hugo-fix t.muTmpl.RLock() defer t.muTmpl.RUnlock() return t.tmpl[name] |