tpl: Sync go_templates for Go 1.18

Using Go tag go1.18 4aa1efed4853ea067d665a952eee77c52faac774

Updates #9677
This commit is contained in:
Bjørn Erik Pedersen 2022-03-16 08:48:16 +01:00
parent 4d6d1d08da
commit 65a78cae1e
No known key found for this signature in database
GPG key ID: 330E6E2BD4859D8F
48 changed files with 697 additions and 223 deletions

View file

@ -17,8 +17,7 @@ import (
) )
func main() { func main() {
// TODO(bep) git checkout tag // The current is built with Go tag go1.18 4aa1efed4853ea067d665a952eee77c52faac774
// The current is built with Go version 2f0da6d9e29d9b9d5a4d10427ca9f71d12bbacc8 / go1.16
fmt.Println("Forking ...") fmt.Println("Forking ...")
defer fmt.Println("Done ...") defer fmt.Println("Done ...")
@ -40,7 +39,7 @@ func main() {
const ( const (
// TODO(bep) // TODO(bep)
goSource = "/Users/bep/dev/go/dump/go/src" goSource = "/Users/bep/dev/go/misc/go/src"
forkRoot = "../../tpl/internal/go_templates" forkRoot = "../../tpl/internal/go_templates"
) )

View file

@ -33,12 +33,14 @@ const KnownEnv = `
GCCGO GCCGO
GO111MODULE GO111MODULE
GO386 GO386
GOAMD64
GOARCH GOARCH
GOARM GOARM
GOBIN GOBIN
GOCACHE GOCACHE
GOENV GOENV
GOEXE GOEXE
GOEXPERIMENT
GOFLAGS GOFLAGS
GOGCCFLAGS GOGCCFLAGS
GOHOSTARCH GOHOSTARCH
@ -60,6 +62,7 @@ const KnownEnv = `
GOTOOLDIR GOTOOLDIR
GOVCS GOVCS
GOWASM GOWASM
GOWORK
GO_EXTLINK_ENABLED GO_EXTLINK_ENABLED
PKG_CONFIG PKG_CONFIG
` `

View file

@ -130,7 +130,7 @@ func compare(aVal, bVal reflect.Value) int {
default: default:
return -1 return -1
} }
case reflect.Ptr, reflect.UnsafePointer: case reflect.Pointer, reflect.UnsafePointer:
a, b := aVal.Pointer(), bVal.Pointer() a, b := aVal.Pointer(), bVal.Pointer()
switch { switch {
case a < b: case a < b:

View file

@ -9,6 +9,7 @@ import (
"github.com/gohugoio/hugo/tpl/internal/go_templates/fmtsort" "github.com/gohugoio/hugo/tpl/internal/go_templates/fmtsort"
"math" "math"
"reflect" "reflect"
"sort"
"strings" "strings"
"testing" "testing"
"unsafe" "unsafe"
@ -37,12 +38,12 @@ var compareTests = [][]reflect.Value{
ct(reflect.TypeOf(chans[0]), chans[0], chans[1], chans[2]), 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(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([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)) value := make([]reflect.Value, len(args))
for i, v := range args { for i, v := range args {
x := reflect.ValueOf(v) x := reflect.ValueOf(v)
@ -83,7 +84,7 @@ func TestCompare(t *testing.T) {
} }
type sortTest struct { type sortTest struct {
data interface{} // Always a map. data any // Always a map.
print string // Printed result using our custom printer. print string // Printed result using our custom printer.
} }
@ -134,7 +135,7 @@ var sortTests = []sortTest{
}, },
} }
func sprint(data interface{}) string { func sprint(data any) string {
om := fmtsort.Sort(reflect.ValueOf(data)) om := fmtsort.Sort(reflect.ValueOf(data))
if om == nil { if om == nil {
return "nil" return "nil"
@ -188,9 +189,19 @@ func sprintKey(key reflect.Value) string {
var ( var (
ints [3]int 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 { func pointerMap() map[*int]string {
m := make(map[*int]string) m := make(map[*int]string)
for i := 2; i >= 0; i-- { 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, // A map containing multiple concrete types should be sorted by type,
// then value. However, the relative ordering of types is unspecified, // then value. However, the relative ordering of types is unspecified,
// so test this by checking the presence of sorted subgroups. // so test this by checking the presence of sorted subgroups.
m := map[interface{}]string{ m := map[any]string{
[2]int{1, 0}: "", [2]int{1, 0}: "",
[2]int{0, 1}: "", [2]int{0, 1}: "",
true: "", true: "",

View file

@ -143,12 +143,12 @@ func attrType(name string) contentType {
// widely applied. // widely applied.
// Treat data-action as URL below. // Treat data-action as URL below.
name = name[5:] name = name[5:]
} else if colon := strings.IndexRune(name, ':'); colon != -1 { } else if prefix, short, ok := strings.Cut(name, ":"); ok {
if name[:colon] == "xmlns" { if prefix == "xmlns" {
return contentTypeURL return contentTypeURL
} }
// Treat svg:href and xlink:href as href below. // Treat svg:href and xlink:href as href below.
name = name[colon+1:] name = short
} }
if t, ok := attrTypeMap[name]; ok { if t, ok := attrTypeMap[name]; ok {
return t return t

View file

@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style // Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
//go:build go1.13 && !windows
// +build go1.13,!windows // +build go1.13,!windows
package template package template

View file

@ -29,16 +29,16 @@ const (
// indirect returns the value, after dereferencing as many times // indirect returns the value, after dereferencing as many times
// as necessary to reach the base type (or nil). // as necessary to reach the base type (or nil).
func indirect(a interface{}) interface{} { func indirect(a any) any {
if a == nil { if a == nil {
return 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. // Avoid creating a reflect.Value if it's not a pointer.
return a return a
} }
v := reflect.ValueOf(a) v := reflect.ValueOf(a)
for v.Kind() == reflect.Ptr && !v.IsNil() { for v.Kind() == reflect.Pointer && !v.IsNil() {
v = v.Elem() v = v.Elem()
} }
return v.Interface() return v.Interface()
@ -52,12 +52,12 @@ var (
// indirectToStringerOrError returns the value, after dereferencing as many times // indirectToStringerOrError returns the value, after dereferencing as many times
// as necessary to reach the base type (or nil) or an implementation of fmt.Stringer // as necessary to reach the base type (or nil) or an implementation of fmt.Stringer
// or error, // or error,
func indirectToStringerOrError(a interface{}) interface{} { func indirectToStringerOrError(a any) any {
if a == nil { if a == nil {
return nil return nil
} }
v := reflect.ValueOf(a) 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() v = v.Elem()
} }
return v.Interface() 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. // stringify converts its arguments to a string and the type of the content.
// All pointers are dereferenced, as in the text/template package. // 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 { if len(args) == 1 {
switch s := indirect(args[0]).(type) { switch s := indirect(args[0]).(type) {
case string: case string:

View file

@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style // Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
//go:build go1.13 && !windows
// +build go1.13,!windows // +build go1.13,!windows
package template package template
@ -15,7 +16,7 @@ import (
) )
func TestTypedContent(t *testing.T) { func TestTypedContent(t *testing.T) {
data := []interface{}{ data := []any{
`<b> "foo%" O'Reilly &bar;`, `<b> "foo%" O'Reilly &bar;`,
htmltemplate.CSS(`a[href =~ "//example.com"]#foo`), htmltemplate.CSS(`a[href =~ "//example.com"]#foo`),
htmltemplate.HTML(`Hello, <b>World</b> &amp;tc!`), htmltemplate.HTML(`Hello, <b>World</b> &amp;tc!`),
@ -452,7 +453,7 @@ func TestEscapingNilNonemptyInterfaces(t *testing.T) {
// A non-empty interface should print like an empty interface. // A non-empty interface should print like an empty interface.
want := new(bytes.Buffer) want := new(bytes.Buffer)
data := struct{ E interface{} }{} data := struct{ E any }{}
tmpl.Execute(want, data) tmpl.Execute(want, data)
if !bytes.Equal(want.Bytes(), got.Bytes()) { if !bytes.Equal(want.Bytes(), got.Bytes()) {

View file

@ -4,7 +4,11 @@
package template 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 // context describes the state an HTML parser must be in when it reaches the
// portion of HTML produced by evaluating a particular template node. // portion of HTML produced by evaluating a particular template node.
@ -20,6 +24,7 @@ type context struct {
jsCtx jsCtx jsCtx jsCtx
attr attr attr attr
element element element element
n parse.Node // for range break/continue
err *Error err *Error
} }
@ -139,6 +144,8 @@ const (
// stateError is an infectious error state outside any valid // stateError is an infectious error state outside any valid
// HTML/CSS/JS construct. // HTML/CSS/JS construct.
stateError stateError
// stateDead marks unreachable code after a {{break}} or {{continue}}.
stateDead
) )
// isComment is true for any state that contains content meant for template // isComment is true for any state that contains content meant for template

View file

@ -155,7 +155,7 @@ func isCSSSpace(b byte) bool {
} }
// cssEscaper escapes HTML and CSS special characters using \<hex>+ escapes. // cssEscaper escapes HTML and CSS special characters using \<hex>+ escapes.
func cssEscaper(args ...interface{}) string { func cssEscaper(args ...any) string {
s, _ := stringify(args...) s, _ := stringify(args...)
var b strings.Builder var b strings.Builder
r, w, written := rune(0), 0, 0 r, w, written := rune(0), 0, 0
@ -218,7 +218,7 @@ var mozBindingBytes = []byte("mozbinding")
// (inherit, blue), and colors (#888). // (inherit, blue), and colors (#888).
// It filters out unsafe values, such as those that affect token boundaries, // It filters out unsafe values, such as those that affect token boundaries,
// and anything that might execute scripts. // and anything that might execute scripts.
func cssValueFilter(args ...interface{}) string { func cssValueFilter(args ...any) string {
s, t := stringify(args...) s, t := stringify(args...)
if t == contentTypeCSS { if t == contentTypeCSS {
return s return s

View file

@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style // Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
//go:build go1.13 && !windows
// +build go1.13,!windows // +build go1.13,!windows
package template package template

View file

@ -229,6 +229,6 @@ func (e *Error) Error() string {
// errorf creates an error given a format string f and args. // errorf creates an error given a format string f and args.
// The template Name still needs to be supplied. // 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...)} return &Error{k, node, "", line, fmt.Sprintf(f, args...)}
} }

View file

@ -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 // evalArgs formats the list of arguments into a string. It is equivalent to
// fmt.Sprint(args...), except that it deferences all pointers. // 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. // Optimization for simple common case of a single string argument.
if len(args) == 1 { if len(args) == 1 {
if s, ok := args[0].(string); ok { if s, ok := args[0].(string); ok {
@ -98,6 +98,15 @@ type escaper struct {
actionNodeEdits map[*parse.ActionNode][]string actionNodeEdits map[*parse.ActionNode][]string
templateNodeEdits map[*parse.TemplateNode]string templateNodeEdits map[*parse.TemplateNode]string
textNodeEdits map[*parse.TextNode][]byte 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. // makeEscaper creates a blank escaper for the given set.
@ -110,6 +119,7 @@ func makeEscaper(n *nameSpace) escaper {
map[*parse.ActionNode][]string{}, map[*parse.ActionNode][]string{},
map[*parse.TemplateNode]string{}, map[*parse.TemplateNode]string{},
map[*parse.TextNode][]byte{}, map[*parse.TextNode][]byte{},
nil,
} }
} }
@ -125,8 +135,16 @@ func (e *escaper) escape(c context, n parse.Node) context {
switch n := n.(type) { switch n := n.(type) {
case *parse.ActionNode: case *parse.ActionNode:
return e.escapeAction(c, n) 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: case *parse.CommentNode:
return c return c
case *parse.ContinueNode:
c.n = n
e.rangeContext.continues = append(e.rangeContext.breaks, c)
return context{state: stateDead}
case *parse.IfNode: case *parse.IfNode:
return e.escapeBranch(c, &n.BranchNode, "if") return e.escapeBranch(c, &n.BranchNode, "if")
case *parse.ListNode: case *parse.ListNode:
@ -428,6 +446,12 @@ func join(a, b context, node parse.Node, nodeName string) context {
if b.state == stateError { if b.state == stateError {
return b return b
} }
if a.state == stateDead {
return b
}
if b.state == stateDead {
return a
}
if a.eq(b) { if a.eq(b) {
return a 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". // escapeBranch escapes a branch template node: "if", "range" and "with".
func (e *escaper) escapeBranch(c context, n *parse.BranchNode, nodeName string) context { 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) 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. // The "true" branch of a "range" node can execute multiple times.
// We check that executing n.List once results in the same context // We check that executing n.List once results in the same context
// as executing n.List twice. // as executing n.List twice.
e.rangeContext = &rangeContext{outer: e.rangeContext}
c1, _ := e.escapeListConditionally(c0, n.List, nil) c1, _ := e.escapeListConditionally(c0, n.List, nil)
c0 = join(c0, c1, n, nodeName) c0 = join(c0, c1, n, nodeName)
if c0.state == stateError { if c0.state == stateError {
e.rangeContext = e.rangeContext.outer
// Make clear that this is a problem on loop re-entry // Make clear that this is a problem on loop re-entry
// since developers tend to overlook that branch when // since developers tend to overlook that branch when
// debugging templates. // 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 c0.err.Description = "on range loop re-entry: " + c0.err.Description
return c0 return c0
} }
c0 = joinRange(c0, e.rangeContext)
e.rangeContext = e.rangeContext.outer
if c0.state == stateError {
return c0
}
} }
c1 := e.escapeList(c, n.ElseList) c1 := e.escapeList(c, n.ElseList)
return join(c0, c1, n, nodeName) 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. // escapeList escapes a list template node.
func (e *escaper) escapeList(c context, n *parse.ListNode) context { func (e *escaper) escapeList(c context, n *parse.ListNode) context {
if n == nil { if n == nil {
@ -494,6 +559,9 @@ func (e *escaper) escapeList(c context, n *parse.ListNode) context {
} }
for _, m := range n.Nodes { for _, m := range n.Nodes {
c = e.escape(c, m) c = e.escape(c, m)
if c.state == stateDead {
break
}
} }
return c 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. // 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) { func (e *escaper) escapeListConditionally(c context, n *parse.ListNode, filter func(*escaper, context) bool) (context, bool) {
e1 := makeEscaper(e.ns) e1 := makeEscaper(e.ns)
e1.rangeContext = e.rangeContext
// Make type inferences available to f. // Make type inferences available to f.
for k, v := range e.output { for k, v := range e.output {
e1.output[k] = v e1.output[k] = v
@ -866,7 +935,7 @@ func HTMLEscapeString(s string) string {
// HTMLEscaper returns the escaped HTML equivalent of the textual // HTMLEscaper returns the escaped HTML equivalent of the textual
// representation of its arguments. // representation of its arguments.
func HTMLEscaper(args ...interface{}) string { func HTMLEscaper(args ...any) string {
return template.HTMLEscaper(args...) return template.HTMLEscaper(args...)
} }
@ -882,12 +951,12 @@ func JSEscapeString(s string) string {
// JSEscaper returns the escaped JavaScript equivalent of the textual // JSEscaper returns the escaped JavaScript equivalent of the textual
// representation of its arguments. // representation of its arguments.
func JSEscaper(args ...interface{}) string { func JSEscaper(args ...any) string {
return template.JSEscaper(args...) return template.JSEscaper(args...)
} }
// URLQueryEscaper returns the escaped value of the textual representation of // URLQueryEscaper returns the escaped value of the textual representation of
// its arguments in a form suitable for embedding in a URL query. // 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...) return template.URLQueryEscaper(args...)
} }

View file

@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style // Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
//go:build go1.13 && !windows
// +build go1.13,!windows // +build go1.13,!windows
package template package template
@ -39,7 +40,7 @@ func TestEscape(t *testing.T) {
A, E []string A, E []string
B, M json.Marshaler B, M json.Marshaler
N int N int
U interface{} // untyped nil U any // untyped nil
Z *int // typed nil Z *int // typed nil
W htmltemplate.HTML W htmltemplate.HTML
}{ }{
@ -862,7 +863,7 @@ func TestEscapeSet(t *testing.T) {
// pred is a template function that returns the predecessor of a // pred is a template function that returns the predecessor of a
// natural number for testing recursive templates. // 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 len(a) == 1 {
if i, _ := a[0].(int); i > 0 { if i, _ := a[0].(int); i > 0 {
return i - 1, nil return i - 1, nil
@ -924,6 +925,22 @@ func TestErrors(t *testing.T) {
"<a href='/foo?{{range .Items}}&{{.K}}={{.V}}{{end}}'>", "<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. // Error cases.
{ {
"{{if .Cond}}<a{{end}}", "{{if .Cond}}<a{{end}}",
@ -959,6 +976,14 @@ func TestErrors(t *testing.T) {
"\n{{range .Items}} x='<a{{end}}", "\n{{range .Items}} x='<a{{end}}",
"z:2:8: on range loop re-entry: {{range}} branches", "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}}", "<a b=1 c={{.H}}",
"z: ends in a non-text context: {stateAttr delimSpaceOrTagEnd", "z: ends in a non-text context: {stateAttr delimSpaceOrTagEnd",
@ -1768,7 +1793,7 @@ func TestEscapeSetErrorsNotIgnorable(t *testing.T) {
} }
func TestRedundantFuncs(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" + "\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" + "\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 { for n0, m := range redundantFuncs {
f0 := funcMap[n0].(func(...interface{}) string) f0 := funcMap[n0].(func(...any) string)
for n1 := range m { for n1 := range m {
f1 := funcMap[n1].(func(...interface{}) string) f1 := funcMap[n1].(func(...any) string)
for _, input := range inputs { for _, input := range inputs {
want := f0(input) want := f0(input)
if got := f1(want); want != got { if got := f1(want); want != got {

View file

@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style // Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
//go:build go1.13
// +build go1.13 // +build go1.13
package template_test package template_test
@ -101,7 +102,7 @@ func Example_autoescaping() {
func Example_escape() { func Example_escape() {
const s = `"Fran & Freddie's Diner" <tasty@example.com>` 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)) fmt.Println(template.HTMLEscapeString(s))
template.HTMLEscape(os.Stdout, []byte(s)) template.HTMLEscape(os.Stdout, []byte(s))

View file

@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style // Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
//go:build go1.13
// +build go1.13 // +build go1.13
package template_test package template_test

View file

@ -4,6 +4,7 @@
// Tests for template execution, copied from text/template. // Tests for template execution, copied from text/template.
//go:build go1.13 && !windows
// +build go1.13,!windows // +build go1.13,!windows
package template package template
@ -53,7 +54,7 @@ type T struct {
MSI map[string]int MSI map[string]int
MSIone map[string]int // one element, for deterministic output MSIone map[string]int // one element, for deterministic output
MSIEmpty map[string]int MSIEmpty map[string]int
MXI map[interface{}]int MXI map[any]int
MII map[int]int MII map[int]int
MI32S map[int32]string MI32S map[int32]string
MI64S map[int64]string MI64S map[int64]string
@ -63,11 +64,11 @@ type T struct {
MUI8S map[uint8]string MUI8S map[uint8]string
SMSI []map[string]int SMSI []map[string]int
// Empty interfaces; used to see if we can dig inside one. // Empty interfaces; used to see if we can dig inside one.
Empty0 interface{} // nil Empty0 any // nil
Empty1 interface{} Empty1 any
Empty2 interface{} Empty2 any
Empty3 interface{} Empty3 any
Empty4 interface{} Empty4 any
// Non-empty interfaces. // Non-empty interfaces.
NonEmptyInterface I NonEmptyInterface I
NonEmptyInterfacePtS *I NonEmptyInterfacePtS *I
@ -145,7 +146,7 @@ var tVal = &T{
SB: []bool{true, false}, SB: []bool{true, false},
MSI: map[string]int{"one": 1, "two": 2, "three": 3}, MSI: map[string]int{"one": 1, "two": 2, "three": 3},
MSIone: map[string]int{"one": 1}, MSIone: map[string]int{"one": 1},
MXI: map[interface{}]int{"one": 1}, MXI: map[any]int{"one": 1},
MII: map[int]int{1: 1}, MII: map[int]int{1: 1},
MI32S: map[int32]string{1: "one", 2: "two"}, MI32S: map[int32]string{1: "one", 2: "two"},
MI64S: map[int64]string{2: "i642", 3: "i643"}, 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) 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) return fmt.Sprintf("Method3: %v", v)
} }
@ -256,7 +257,7 @@ func (u *U) TrueFalse(b bool) string {
return "" return ""
} }
func typeOf(arg interface{}) string { func typeOf(arg any) string {
return fmt.Sprintf("%T", arg) return fmt.Sprintf("%T", arg)
} }
@ -264,7 +265,7 @@ type execTest struct {
name string name string
input string input string
output string output string
data interface{} data any
ok bool ok bool
} }
@ -397,7 +398,7 @@ var execTests = []execTest{
{".VariadicFuncInt", "{{call .VariadicFuncInt 33 `he` `llo`}}", "33=&lt;he&#43;llo&gt;", tVal, true}, {".VariadicFuncInt", "{{call .VariadicFuncInt 33 `he` `llo`}}", "33=&lt;he&#43;llo&gt;", tVal, true},
{"if .BinaryFunc call", "{{ if .BinaryFunc}}{{call .BinaryFunc `1` `2`}}{{end}}", "[1=2]", 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}, {"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}, {".ErrFunc", "{{call .ErrFunc}}", "bla", tVal, true},
{"call nil", "{{call nil}}", "", tVal, false}, {"call nil", "{{call nil}}", "", tVal, false},
@ -571,6 +572,8 @@ var execTests = []execTest{
{"range empty no else", "{{range .SIEmpty}}-{{.}}-{{end}}", "", tVal, true}, {"range empty no else", "{{range .SIEmpty}}-{{.}}-{{end}}", "", tVal, true},
{"range []int else", "{{range .SI}}-{{.}}-{{else}}EMPTY{{end}}", "-3--4--5-", 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 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 []bool", "{{range .SB}}-{{.}}-{{end}}", "-true--false-", tVal, true},
{"range []int method", "{{range .SI | .MAdd .I}}-{{.}}-{{end}}", "-20--21--22-", 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}, {"range map", "{{range .MSI}}-{{.}}-{{end}}", "-1--3--2-", tVal, true},
@ -742,7 +745,7 @@ func add(args ...int) int {
return sum return sum
} }
func echo(arg interface{}) interface{} { func echo(arg any) any {
return arg return arg
} }
@ -761,7 +764,7 @@ func stringer(s fmt.Stringer) string {
return s.String() return s.String()
} }
func mapOfThree() interface{} { func mapOfThree() any {
return map[string]int{"three": 3} return map[string]int{"three": 3}
} }
@ -1440,7 +1443,7 @@ func TestBlock(t *testing.T) {
func TestEvalFieldErrors(t *testing.T) { func TestEvalFieldErrors(t *testing.T) {
tests := []struct { tests := []struct {
name, src string name, src string
value interface{} value any
want string want string
}{ }{
{ {
@ -1583,7 +1586,7 @@ func TestInterfaceValues(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
tmpl := Must(New("tmpl").Parse(tt.text)) tmpl := Must(New("tmpl").Parse(tt.text))
var buf bytes.Buffer var buf bytes.Buffer
err := tmpl.Execute(&buf, map[string]interface{}{ err := tmpl.Execute(&buf, map[string]any{
"PlusOne": func(n int) int { "PlusOne": func(n int) int {
return n + 1 return n + 1
}, },
@ -1612,7 +1615,7 @@ func TestInterfaceValues(t *testing.T) {
// Check that panics during calls are recovered and returned as errors. // Check that panics during calls are recovered and returned as errors.
func TestExecutePanicDuringCall(t *testing.T) { func TestExecutePanicDuringCall(t *testing.T) {
funcs := map[string]interface{}{ funcs := map[string]any{
"doPanic": func() string { "doPanic": func() string {
panic("custom panic string") panic("custom panic string")
}, },
@ -1620,7 +1623,7 @@ func TestExecutePanicDuringCall(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
input string input string
data interface{} data any
wantErr string wantErr string
}{ }{
{ {
@ -1724,8 +1727,6 @@ var v = "v";
` `
func TestEscapeRace(t *testing.T) { func TestEscapeRace(t *testing.T) {
// t.Skip("this test currently fails with -race; see issue #39807")
tmpl := New("") tmpl := New("")
_, err := tmpl.New("templ.html").Parse(raceText) _, err := tmpl.New("templ.html").Parse(raceText)
if err != nil { if err != nil {
@ -1820,7 +1821,7 @@ func TestRecursiveExecuteViaMethod(t *testing.T) {
func TestTemplateFuncsAfterClone(t *testing.T) { func TestTemplateFuncsAfterClone(t *testing.T) {
s := `{{ f . }}` s := `{{ f . }}`
want := "test" want := "test"
orig := New("orig").Funcs(map[string]interface{}{ orig := New("orig").Funcs(map[string]any{
"f": func(in string) string { "f": func(in string) string {
return in return in
}, },

View file

@ -12,7 +12,7 @@ import (
) )
// htmlNospaceEscaper escapes for inclusion in unquoted attribute values. // htmlNospaceEscaper escapes for inclusion in unquoted attribute values.
func htmlNospaceEscaper(args ...interface{}) string { func htmlNospaceEscaper(args ...any) string {
s, t := stringify(args...) s, t := stringify(args...)
if t == contentTypeHTML { if t == contentTypeHTML {
return htmlReplacer(stripTags(s), htmlNospaceNormReplacementTable, false) return htmlReplacer(stripTags(s), htmlNospaceNormReplacementTable, false)
@ -21,7 +21,7 @@ func htmlNospaceEscaper(args ...interface{}) string {
} }
// attrEscaper escapes for inclusion in quoted attribute values. // attrEscaper escapes for inclusion in quoted attribute values.
func attrEscaper(args ...interface{}) string { func attrEscaper(args ...any) string {
s, t := stringify(args...) s, t := stringify(args...)
if t == contentTypeHTML { if t == contentTypeHTML {
return htmlReplacer(stripTags(s), htmlNormReplacementTable, true) return htmlReplacer(stripTags(s), htmlNormReplacementTable, true)
@ -30,7 +30,7 @@ func attrEscaper(args ...interface{}) string {
} }
// rcdataEscaper escapes for inclusion in an RCDATA element body. // rcdataEscaper escapes for inclusion in an RCDATA element body.
func rcdataEscaper(args ...interface{}) string { func rcdataEscaper(args ...any) string {
s, t := stringify(args...) s, t := stringify(args...)
if t == contentTypeHTML { if t == contentTypeHTML {
return htmlReplacer(s, htmlNormReplacementTable, true) return htmlReplacer(s, htmlNormReplacementTable, true)
@ -39,7 +39,7 @@ func rcdataEscaper(args ...interface{}) string {
} }
// htmlEscaper escapes for inclusion in HTML text. // htmlEscaper escapes for inclusion in HTML text.
func htmlEscaper(args ...interface{}) string { func htmlEscaper(args ...any) string {
s, t := stringify(args...) s, t := stringify(args...)
if t == contentTypeHTML { if t == contentTypeHTML {
return s return s
@ -225,7 +225,7 @@ func stripTags(html string) string {
// htmlNameFilter accepts valid parts of an HTML attribute or tag name or // htmlNameFilter accepts valid parts of an HTML attribute or tag name or
// a known-safe HTML attribute. // a known-safe HTML attribute.
func htmlNameFilter(args ...interface{}) string { func htmlNameFilter(args ...any) string {
s, t := stringify(args...) s, t := stringify(args...)
if t == contentTypeHTMLAttr { if t == contentTypeHTMLAttr {
return s return s
@ -260,6 +260,6 @@ func htmlNameFilter(args ...interface{}) string {
// content interpolated into comments. // content interpolated into comments.
// This approach is equally valid whether or not static comment content is // This approach is equally valid whether or not static comment content is
// removed from the template. // removed from the template.
func commentEscaper(args ...interface{}) string { func commentEscaper(args ...any) string {
return "" return ""
} }

View file

@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style // Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
//go:build go1.13 && !windows
// +build go1.13,!windows // +build go1.13,!windows
package template package template

View file

@ -123,7 +123,7 @@ var jsonMarshalType = reflect.TypeOf((*json.Marshaler)(nil)).Elem()
// indirectToJSONMarshaler returns the value, after dereferencing as many times // indirectToJSONMarshaler returns the value, after dereferencing as many times
// as necessary to reach the base type (or nil) or an implementation of json.Marshal. // 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 // text/template now supports passing untyped nil as a func call
// argument, so we must support it. Otherwise we'd panic below, as one // argument, so we must support it. Otherwise we'd panic below, as one
// cannot call the Type or Interface methods on an invalid // cannot call the Type or Interface methods on an invalid
@ -133,7 +133,7 @@ func indirectToJSONMarshaler(a interface{}) interface{} {
} }
v := reflect.ValueOf(a) 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() v = v.Elem()
} }
return v.Interface() 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 // jsValEscaper escapes its inputs to a JS Expression (section 11.14) that has
// neither side-effects nor free variables outside (NaN, Infinity). // neither side-effects nor free variables outside (NaN, Infinity).
func jsValEscaper(args ...interface{}) string { func jsValEscaper(args ...any) string {
var a interface{} var a any
if len(args) == 1 { if len(args) == 1 {
a = indirectToJSONMarshaler(args[0]) a = indirectToJSONMarshaler(args[0])
switch t := a.(type) { switch t := a.(type) {
@ -225,7 +225,7 @@ func jsValEscaper(args ...interface{}) string {
// jsStrEscaper produces a string that can be included between quotes in // jsStrEscaper produces a string that can be included between quotes in
// JavaScript source, in JavaScript embedded in an HTML5 <script> element, // JavaScript source, in JavaScript embedded in an HTML5 <script> element,
// or in an HTML5 event handler attribute such as onclick. // or in an HTML5 event handler attribute such as onclick.
func jsStrEscaper(args ...interface{}) string { func jsStrEscaper(args ...any) string {
s, t := stringify(args...) s, t := stringify(args...)
if t == contentTypeJSStr { if t == contentTypeJSStr {
return replace(s, jsStrNormReplacementTable) 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 // specials so the result is treated literally when included in a regular
// expression literal. /foo{{.X}}bar/ matches the string "foo" followed by // expression literal. /foo{{.X}}bar/ matches the string "foo" followed by
// the literal text of {{.X}} followed by the string "bar". // the literal text of {{.X}} followed by the string "bar".
func jsRegexpEscaper(args ...interface{}) string { func jsRegexpEscaper(args ...any) string {
s, _ := stringify(args...) s, _ := stringify(args...)
s = replace(s, jsRegexpReplacementTable) s = replace(s, jsRegexpReplacementTable)
if s == "" { if s == "" {
@ -399,9 +399,7 @@ func isJSType(mimeType string) bool {
// https://tools.ietf.org/html/rfc4329#section-3 // https://tools.ietf.org/html/rfc4329#section-3
// https://www.ietf.org/rfc/rfc4627.txt // https://www.ietf.org/rfc/rfc4627.txt
// discard parameters // discard parameters
if i := strings.Index(mimeType, ";"); i >= 0 { mimeType, _, _ = strings.Cut(mimeType, ";")
mimeType = mimeType[:i]
}
mimeType = strings.ToLower(mimeType) mimeType = strings.ToLower(mimeType)
mimeType = strings.TrimSpace(mimeType) mimeType = strings.TrimSpace(mimeType)
switch mimeType { switch mimeType {

View file

@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style // Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
//go:build go1.13 && !windows
// +build go1.13,!windows // +build go1.13,!windows
package template package template
@ -105,7 +106,7 @@ func TestNextJsCtx(t *testing.T) {
func TestJSValEscaper(t *testing.T) { func TestJSValEscaper(t *testing.T) {
tests := []struct { tests := []struct {
x interface{} x any
js string js string
}{ }{
{int(42), " 42 "}, {int(42), " 42 "},
@ -142,8 +143,8 @@ func TestJSValEscaper(t *testing.T) {
// "\v" == "v" on IE 6 so use "\u000b" instead. // "\v" == "v" on IE 6 so use "\u000b" instead.
{"\t\x0b", `"\t\u000b"`}, {"\t\x0b", `"\t\u000b"`},
{struct{ X, Y int }{1, 2}, `{"X":1,"Y":2}`}, {struct{ X, Y int }{1, 2}, `{"X":1,"Y":2}`},
{[]interface{}{}, "[]"}, {[]any{}, "[]"},
{[]interface{}{42, "foo", nil}, `[42,"foo",null]`}, {[]any{42, "foo", nil}, `[42,"foo",null]`},
{[]string{"<!--", "</script>", "-->"}, `["\u003c!--","\u003c/script\u003e","--\u003e"]`}, {[]string{"<!--", "</script>", "-->"}, `["\u003c!--","\u003c/script\u003e","--\u003e"]`},
{"<!--", `"\u003c!--"`}, {"<!--", `"\u003c!--"`},
{"-->", `"--\u003e"`}, {"-->", `"--\u003e"`},
@ -160,7 +161,7 @@ func TestJSValEscaper(t *testing.T) {
} }
// Make sure that escaping corner cases are not broken // Make sure that escaping corner cases are not broken
// by nesting. // by nesting.
a := []interface{}{test.x} a := []any{test.x}
want := "[" + strings.TrimSpace(test.js) + "]" want := "[" + strings.TrimSpace(test.js) + "]"
if js := jsValEscaper(a); js != want { if js := jsValEscaper(a); js != want {
t.Errorf("%+v: want\n\t%q\ngot\n\t%q", a, want, js) 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) { func TestJSStrEscaper(t *testing.T) {
tests := []struct { tests := []struct {
x interface{} x any
esc string esc string
}{ }{
{"", ``}, {"", ``},
@ -225,7 +226,7 @@ func TestJSStrEscaper(t *testing.T) {
func TestJSRegexpEscaper(t *testing.T) { func TestJSRegexpEscaper(t *testing.T) {
tests := []struct { tests := []struct {
x interface{} x any
esc string esc string
}{ }{
{"", `(?:)`}, {"", `(?:)`},
@ -280,7 +281,7 @@ func TestEscapersOnLower7AndSelectHighCodepoints(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
escaper func(...interface{}) string escaper func(...any) string
escaped string escaped string
}{ }{
{ {

View file

@ -4,6 +4,7 @@
// Tests for multiple-template execution, copied from text/template. // Tests for multiple-template execution, copied from text/template.
//go:build go1.13 && !windows
// +build go1.13,!windows // +build go1.13,!windows
package template package template

View file

@ -118,7 +118,7 @@ func (t *Template) escape() error {
// the output writer. // the output writer.
// A template may be executed safely in parallel, although if parallel // A template may be executed safely in parallel, although if parallel
// executions share a Writer the output may be interleaved. // 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 { if err := t.escape(); err != nil {
return err return err
} }
@ -132,7 +132,7 @@ func (t *Template) Execute(wr io.Writer, data interface{}) error {
// the output writer. // the output writer.
// A template may be executed safely in parallel, although if parallel // A template may be executed safely in parallel, although if parallel
// executions share a Writer the output may be interleaved. // 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) tmpl, err := t.lookupAndEscapeTemplate(name)
if err != nil { if err != nil {
return err return err
@ -336,7 +336,7 @@ func (t *Template) Name() string {
// terminates and Execute returns that error. FuncMap has the same base type // terminates and Execute returns that error. FuncMap has the same base type
// as FuncMap in "text/template", copied here so clients need not import // as FuncMap in "text/template", copied here so clients need not import
// "text/template". // "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. // Funcs adds the elements of the argument map to the template's function map.
// It must be called before the template is parsed. // 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, // 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 // and whether the value has a meaningful truth value. This is the definition of
// truth used by if and other such actions. // 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) return template.IsTrue(val)
} }

View file

@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style // Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
//go:build go1.13
// +build go1.13 // +build go1.13
package template_test 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 var buf bytes.Buffer
err := t.Execute(&buf, val) err := t.Execute(&buf, val)
if err != nil { if err != nil {

View file

@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style // Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
//go:build go1.13 && !windows
// +build go1.13,!windows // +build go1.13,!windows
package template package template

View file

@ -32,7 +32,7 @@ import (
// To allow URLs containing other schemes to bypass this filter, developers must // 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 // explicitly indicate that such a URL is expected and safe by encapsulating it
// in a template.URL value. // in a template.URL value.
func urlFilter(args ...interface{}) string { func urlFilter(args ...any) string {
s, t := stringify(args...) s, t := stringify(args...)
if t == contentTypeURL { if t == contentTypeURL {
return s 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 // isSafeURL is true if s is a relative URL or if URL has a protocol in
// (http, https, mailto). // (http, https, mailto).
func isSafeURL(s string) bool { func isSafeURL(s string) bool {
if i := strings.IndexRune(s, ':'); i >= 0 && !strings.ContainsRune(s[:i], '/') { if protocol, _, ok := strings.Cut(s, ":"); ok && !strings.Contains(protocol, "/") {
protocol := s[:i]
if !strings.EqualFold(protocol, "http") && !strings.EqualFold(protocol, "https") && !strings.EqualFold(protocol, "mailto") { if !strings.EqualFold(protocol, "http") && !strings.EqualFold(protocol, "https") && !strings.EqualFold(protocol, "mailto") {
return false return false
} }
@ -58,7 +56,7 @@ func isSafeURL(s string) bool {
// urlEscaper produces an output that can be embedded in a URL query. // urlEscaper produces an output that can be embedded in a URL query.
// The output can be embedded in an HTML attribute without further escaping. // 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...) 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 // The normalizer does not encode all HTML specials. Specifically, it does not
// encode '&' so correct embedding in an HTML attribute requires escaping of // encode '&' so correct embedding in an HTML attribute requires escaping of
// '&' to '&amp;'. // '&' to '&amp;'.
func urlNormalizer(args ...interface{}) string { func urlNormalizer(args ...any) string {
return urlProcessor(true, args...) return urlProcessor(true, args...)
} }
// urlProcessor normalizes (when norm is true) or escapes its input to produce // urlProcessor normalizes (when norm is true) or escapes its input to produce
// a valid hierarchical or opaque URL part. // 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...) s, t := stringify(args...)
if t == contentTypeURL { if t == contentTypeURL {
norm = true 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 // Filters and normalizes srcset values which are comma separated
// URLs followed by metadata. // URLs followed by metadata.
func srcsetFilterAndEscaper(args ...interface{}) string { func srcsetFilterAndEscaper(args ...any) string {
s, t := stringify(args...) s, t := stringify(args...)
switch t { switch t {
case contentTypeSrcset: case contentTypeSrcset:

View file

@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style // Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
//go:build go1.13 && !windows
// +build go1.13,!windows // +build go1.13,!windows
package template package template
@ -50,7 +51,7 @@ func TestURLFilters(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
escaper func(...interface{}) string escaper func(...any) string
escaped string escaped string
}{ }{
{ {

View file

@ -11,6 +11,7 @@
package testenv package testenv
import ( import (
"bytes"
"errors" "errors"
"flag" "flag"
"github.com/gohugoio/hugo/tpl/internal/go_templates/cfg" "github.com/gohugoio/hugo/tpl/internal/go_templates/cfg"
@ -22,6 +23,7 @@ import (
"strings" "strings"
"sync" "sync"
"testing" "testing"
"time"
) )
// Builder reports the name of the builder running this test // 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) 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
}

View file

@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style // Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
// +build cgo //go:build cgo
package testenv package testenv

View file

@ -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

View file

@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style // Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
// +build !windows //go:build !windows
package testenv package testenv

View file

@ -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

View file

@ -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 T0 is executed; otherwise, dot is set to the successive elements
of the array, slice, or map and T1 is executed. of the array, slice, or map and T1 is executed.
{{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"}} {{template "name"}}
The template with the specified name is executed with nil data. The template with the specified name is executed with nil data.
@ -307,9 +315,10 @@ Predefined global functions are named as follows.
and and
Returns the boolean AND of its arguments by returning the Returns the boolean AND of its arguments by returning the
first empty argument or the last argument, that is, first empty argument or the last argument. That is,
"and x y" behaves as "if x then y else x". All the "and x y" behaves as "if x then y else x."
arguments are evaluated. Evaluation proceeds through the arguments left to right
and returns when the result is determined.
call call
Returns the result of calling the first argument, which Returns the result of calling the first argument, which
must be a function, with the remaining arguments as parameters. must be a function, with the remaining arguments as parameters.
@ -344,8 +353,9 @@ Predefined global functions are named as follows.
or or
Returns the boolean OR of its arguments by returning the Returns the boolean OR of its arguments by returning the
first non-empty argument or the last argument, that is, first non-empty argument or the last argument, that is,
"or x y" behaves as "if x then x else y". All the "or x y" behaves as "if x then x else y".
arguments are evaluated. Evaluation proceeds through the arguments left to right
and returns when the result is determined.
print print
An alias for fmt.Sprint An alias for fmt.Sprint
printf printf

View file

@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style // Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
//go:build go1.13
// +build go1.13 // +build go1.13
package template_test package template_test

View file

@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style // Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
//go:build go1.13
// +build go1.13 // +build go1.13
package template_test package template_test

View file

@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style // Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
//go:build go1.13
// +build go1.13 // +build go1.13
package template_test package template_test

View file

@ -5,14 +5,14 @@
package template package template
import ( import (
"errors"
"fmt" "fmt"
"github.com/gohugoio/hugo/tpl/internal/go_templates/fmtsort"
"github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate/parse"
"io" "io"
"reflect" "reflect"
"runtime" "runtime"
"strings" "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 // 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. // 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()) name := doublePercent(s.tmpl.Name())
if s.node == nil { if s.node == nil {
format = fmt.Sprintf("template: %s: %s", name, format) format = fmt.Sprintf("template: %s: %s", name, format)
@ -179,7 +179,7 @@ func errRecover(errp *error) {
// the output writer. // the output writer.
// A template may be executed safely in parallel, although if parallel // A template may be executed safely in parallel, although if parallel
// executions share a Writer the output may be interleaved. // 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) tmpl := t.Lookup(name)
if tmpl == nil { if tmpl == nil {
return fmt.Errorf("template: no template %q associated with template %q", name, t.name) 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 // If data is a reflect.Value, the template applies to the concrete
// value that the reflect.Value holds, as in fmt.Print. // 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) 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) defer errRecover(&err)
value, ok := data.(reflect.Value) value, ok := data.(reflect.Value)
if !ok { if !ok {
@ -228,7 +228,6 @@ func (t *Template) DefinedTemplates() string {
return "" return ""
} }
var b strings.Builder var b strings.Builder
// temporary Hugo-fix
t.muTmpl.RLock() t.muTmpl.RLock()
defer t.muTmpl.RUnlock() defer t.muTmpl.RUnlock()
for name, tmpl := range t.tmpl { for name, tmpl := range t.tmpl {
@ -245,6 +244,12 @@ func (t *Template) DefinedTemplates() string {
return b.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, // Walk functions step through the major pieces of the template structure,
// generating output as they go. // generating output as they go.
func (s *state) walk(dot reflect.Value, node parse.Node) { 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 { if len(node.Pipe.Decl) == 0 {
s.printValue(node, val) s.printValue(node, val)
} }
case *parse.BreakNode:
panic(walkBreak)
case *parse.CommentNode: case *parse.CommentNode:
case *parse.ContinueNode:
panic(walkContinue)
case *parse.IfNode: case *parse.IfNode:
s.walkIfOrWith(parse.NodeIf, dot, node.Pipe, node.List, node.ElseList) s.walkIfOrWith(parse.NodeIf, dot, node.Pipe, node.List, node.ElseList)
case *parse.ListNode: 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, // 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 // and whether the value has a meaningful truth value. This is the definition of
// truth used by if and other such actions. // 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)) return isTrue(reflect.ValueOf(val))
} }
@ -318,7 +327,7 @@ func isTrueOld(val reflect.Value) (truth, ok bool) {
truth = val.Bool() truth = val.Bool()
case reflect.Complex64, reflect.Complex128: case reflect.Complex64, reflect.Complex128:
truth = val.Complex() != 0 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() truth = !val.IsNil()
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
truth = val.Int() != 0 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) { func (s *state) walkRange(dot reflect.Value, r *parse.RangeNode) {
s.at(r) s.at(r)
defer func() {
if r := recover(); r != nil && r != walkBreak {
panic(r)
}
}()
defer s.pop(s.mark()) defer s.pop(s.mark())
val, _ := indirect(s.evalPipeline(dot, r.Pipe)) val, _ := indirect(s.evalPipeline(dot, r.Pipe))
// mark top of stack before any variables in the body are pushed. // 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 { if len(r.Pipe.Decl) > 1 {
s.setTopVar(2, index) 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.walk(elem, r.List)
s.pop(mark)
} }
switch val.Kind() { switch val.Kind() {
case reflect.Array, reflect.Slice: 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 { func (s *state) evalFunctionOld(dot reflect.Value, node *parse.IdentifierNode, cmd parse.Node, args []parse.Node, final reflect.Value) reflect.Value {
s.at(node) s.at(node)
name := node.Ident name := node.Ident
function, ok := findFunction(name, s.tmpl) function, isBuiltin, ok := findFunction(name, s.tmpl)
if !ok { if !ok {
s.errorf("%q is not a defined function", name) 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). // 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 // Unless it's an interface, need to get to a value of type *T to guarantee
// we see all methods of T and *T. // we see all methods of T and *T.
ptr := receiver 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() ptr = ptr.Addr()
} }
if method := ptr.MethodByName(fieldName); method.IsValid() { 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 hasArgs := len(args) > 1 || final != missingVal
// It's not a method; must be a field of a struct or an element of a map. // 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: case reflect.Struct:
tField, ok := receiver.Type().FieldByName(fieldName) tField, ok := receiver.Type().FieldByName(fieldName)
if ok { if ok {
field := receiver.FieldByIndex(tField.Index) field, err := receiver.FieldByIndexErr(tField.Index)
if tField.PkgPath != "" { // field is unexported if !tField.IsExported() {
s.errorf("%s is an unexported field of struct type %s", fieldName, typ) 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 it's a function, we must call it.
if hasArgs { if hasArgs {
s.errorf("%s has arguments but cannot be invoked as function", fieldName) 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 return result
} }
case reflect.Ptr: case reflect.Pointer:
etyp := receiver.Type().Elem() etyp := receiver.Type().Elem()
if etyp.Kind() == reflect.Struct { if etyp.Kind() == reflect.Struct {
if _, ok := etyp.FieldByName(fieldName); !ok { 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 // 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] // 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. // 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 { if args != nil {
args = args[1:] // Zeroth arg is function name/node; not passed to function. args = args[1:] // Zeroth arg is function name/node; not passed to function.
} }
@ -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. // 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()) 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. // Build the arg list.
argv := make([]reflect.Value, numIn) argv := make([]reflect.Value, numIn)
// Args must be evaluated. Fixed args first. // 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. // error to the caller.
if err != nil { if err != nil {
s.at(node) s.at(node)
s.errorf("error calling %s: %v", name, err) s.errorf("error calling %s: %w", name, err)
} }
if v.Type() == reflectValueType { return unwrap(v)
v = v.Interface().(reflect.Value)
}
return v
} }
// canBeNil reports whether an untyped nil can be assigned to the type. See reflect.Zero. // canBeNil reports whether an untyped nil can be assigned to the type. See reflect.Zero.
func canBeNil(typ reflect.Type) bool { func canBeNil(typ reflect.Type) bool {
switch typ.Kind() { 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 return true
case reflect.Struct: case reflect.Struct:
return typ == reflectValueType 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. // are much more constrained, so it makes more sense there than here.
// Besides, one is almost always all you need. // Besides, one is almost always all you need.
switch { switch {
case value.Kind() == reflect.Ptr && value.Type().Elem().AssignableTo(typ): case value.Kind() == reflect.Pointer && value.Type().Elem().AssignableTo(typ):
value = value.Elem() value = value.Elem()
if !value.IsValid() { if !value.IsValid() {
s.errorf("dereference of nil pointer of type %s", typ) 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() value = value.Addr()
case value.IsZero():
s.errorf("got <nil>, expected %s", typ)
default: default:
s.errorf("wrong type for value; expected %s; got %s", typ, value.Type()) 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 // if it's nil. If the returned bool is true, the returned value's kind will be
// either a pointer or interface. // either a pointer or interface.
func indirect(v reflect.Value) (rv reflect.Value, isNil bool) { 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() { if v.IsNil() {
return v, true 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 // printableValue returns the, possibly indirected, interface value inside v that
// is best for a call to formatted printer. // is best for a call to formatted printer.
func printableValue(v reflect.Value) (interface{}, bool) { func printableValue(v reflect.Value) (any, bool) {
if v.Kind() == reflect.Ptr { if v.Kind() == reflect.Pointer {
v, _ = indirect(v) // fmt.Fprint handles nil. v, _ = indirect(v) // fmt.Fprint handles nil.
} }
if !v.IsValid() { 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.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() v = v.Addr()
} else { } else {
switch v.Kind() { switch v.Kind() {

View file

@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style // Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
//go:build go1.13 && !windows
// +build go1.13,!windows // +build go1.13,!windows
package template package template
@ -14,6 +15,7 @@ import (
"io" "io"
"reflect" "reflect"
"strings" "strings"
"sync"
"testing" "testing"
) )
@ -47,7 +49,7 @@ type T struct {
MSI map[string]int MSI map[string]int
MSIone map[string]int // one element, for deterministic output MSIone map[string]int // one element, for deterministic output
MSIEmpty map[string]int MSIEmpty map[string]int
MXI map[interface{}]int MXI map[any]int
MII map[int]int MII map[int]int
MI32S map[int32]string MI32S map[int32]string
MI64S map[int64]string MI64S map[int64]string
@ -57,11 +59,11 @@ type T struct {
MUI8S map[uint8]string MUI8S map[uint8]string
SMSI []map[string]int SMSI []map[string]int
// Empty interfaces; used to see if we can dig inside one. // Empty interfaces; used to see if we can dig inside one.
Empty0 interface{} // nil Empty0 any // nil
Empty1 interface{} Empty1 any
Empty2 interface{} Empty2 any
Empty3 interface{} Empty3 any
Empty4 interface{} Empty4 any
// Non-empty interfaces. // Non-empty interfaces.
NonEmptyInterface I NonEmptyInterface I
NonEmptyInterfacePtS *I NonEmptyInterfacePtS *I
@ -139,7 +141,7 @@ var tVal = &T{
SB: []bool{true, false}, SB: []bool{true, false},
MSI: map[string]int{"one": 1, "two": 2, "three": 3}, MSI: map[string]int{"one": 1, "two": 2, "three": 3},
MSIone: map[string]int{"one": 1}, MSIone: map[string]int{"one": 1},
MXI: map[interface{}]int{"one": 1}, MXI: map[any]int{"one": 1},
MII: map[int]int{1: 1}, MII: map[int]int{1: 1},
MI32S: map[int32]string{1: "one", 2: "two"}, MI32S: map[int32]string{1: "one", 2: "two"},
MI64S: map[int64]string{2: "i642", 3: "i643"}, 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) 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) return fmt.Sprintf("Method3: %v", v)
} }
@ -250,7 +252,7 @@ func (u *U) TrueFalse(b bool) string {
return "" return ""
} }
func typeOf(arg interface{}) string { func typeOf(arg any) string {
return fmt.Sprintf("%T", arg) return fmt.Sprintf("%T", arg)
} }
@ -258,7 +260,7 @@ type execTest struct {
name string name string
input string input string
output string output string
data interface{} data any
ok bool ok bool
} }
@ -391,7 +393,7 @@ var execTests = []execTest{
{".VariadicFuncInt", "{{call .VariadicFuncInt 33 `he` `llo`}}", "33=<he+llo>", tVal, true}, {".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 .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}, {"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}, {".ErrFunc", "{{call .ErrFunc}}", "bla", tVal, true},
{"call nil", "{{call nil}}", "", tVal, false}, {"call nil", "{{call nil}}", "", tVal, false},
@ -482,8 +484,19 @@ var execTests = []execTest{
{"not", "{{not true}} {{not false}}", "false true", nil, true}, {"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}, {"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", "{{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", "{{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 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. // Indexing.
{"slice[0]", "{{index .SI 0}}", "3", tVal, true}, {"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 empty no else", "{{range .SIEmpty}}-{{.}}-{{end}}", "", tVal, true},
{"range []int else", "{{range .SI}}-{{.}}-{{else}}EMPTY{{end}}", "-3--4--5-", 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 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 []bool", "{{range .SB}}-{{.}}-{{end}}", "-true--false-", tVal, true},
{"range []int method", "{{range .SI | .MAdd .I}}-{{.}}-{{end}}", "-20--21--22-", 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}, {"range map", "{{range .MSI}}-{{.}}-{{end}}", "-1--3--2-", tVal, true},
@ -736,7 +751,7 @@ func add(args ...int) int {
return sum return sum
} }
func echo(arg interface{}) interface{} { func echo(arg any) any {
return arg return arg
} }
@ -755,7 +770,7 @@ func stringer(s fmt.Stringer) string {
return s.String() return s.String()
} }
func mapOfThree() interface{} { func mapOfThree() any {
return map[string]int{"three": 3} return map[string]int{"three": 3}
} }
@ -765,6 +780,7 @@ func testExecute(execTests []execTest, template *Template, t *testing.T) {
"add": add, "add": add,
"count": count, "count": count,
"dddArg": dddArg, "dddArg": dddArg,
"die": func() bool { panic("die") },
"echo": echo, "echo": echo,
"makemap": makemap, "makemap": makemap,
"mapOfThree": mapOfThree, "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) { func TestJSEscaping(t *testing.T) {
testCases := []struct { testCases := []struct {
in, exp string in, exp string
@ -1180,8 +1218,11 @@ var cmpTests = []cmpTest{
{"eq .Ptr .NilPtr", "false", true}, {"eq .Ptr .NilPtr", "false", true},
{"eq .NilPtr .NilPtr", "true", true}, {"eq .NilPtr .NilPtr", "true", true},
{"eq .Iface1 .Iface1", "true", true}, {"eq .Iface1 .Iface1", "true", true},
{"eq .Iface1 .Iface2", "false", true}, {"eq .Iface1 .NilIface", "false", true},
{"eq .Iface2 .Iface2", "true", true}, {"eq .NilIface .NilIface", "true", true},
{"eq .NilIface .Iface1", "false", true},
{"eq .NilIface 0", "false", true},
{"eq 0 .NilIface", "false", true},
// Errors // Errors
{"eq `xy` 1", "", false}, // Different types. {"eq `xy` 1", "", false}, // Different types.
{"eq 2 2.0", "", false}, // Different types. {"eq 2 2.0", "", false}, // Different types.
@ -1201,7 +1242,7 @@ func TestComparison(t *testing.T) {
Ptr, NilPtr *int Ptr, NilPtr *int
Map map[int]int Map map[int]int
V1, V2 V V1, V2 V
Iface1, Iface2 fmt.Stringer Iface1, NilIface fmt.Stringer
}{ }{
Uthree: 3, Uthree: 3,
Ufour: 4, Ufour: 4,
@ -1430,7 +1471,7 @@ func TestBlock(t *testing.T) {
func TestEvalFieldErrors(t *testing.T) { func TestEvalFieldErrors(t *testing.T) {
tests := []struct { tests := []struct {
name, src string name, src string
value interface{} value any
want string want string
}{ }{
{ {
@ -1573,7 +1614,7 @@ func TestInterfaceValues(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
tmpl := Must(New("tmpl").Parse(tt.text)) tmpl := Must(New("tmpl").Parse(tt.text))
var buf bytes.Buffer var buf bytes.Buffer
err := tmpl.Execute(&buf, map[string]interface{}{ err := tmpl.Execute(&buf, map[string]any{
"PlusOne": func(n int) int { "PlusOne": func(n int) int {
return n + 1 return n + 1
}, },
@ -1602,7 +1643,7 @@ func TestInterfaceValues(t *testing.T) {
// Check that panics during calls are recovered and returned as errors. // Check that panics during calls are recovered and returned as errors.
func TestExecutePanicDuringCall(t *testing.T) { func TestExecutePanicDuringCall(t *testing.T) {
funcs := map[string]interface{}{ funcs := map[string]any{
"doPanic": func() string { "doPanic": func() string {
panic("custom panic string") panic("custom panic string")
}, },
@ -1610,7 +1651,7 @@ func TestExecutePanicDuringCall(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
input string input string
data interface{} data any
wantErr string wantErr string
}{ }{
{ {
@ -1712,3 +1753,63 @@ func TestIssue43065(t *testing.T) {
t.Errorf("%s", err) 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)
}
}

View file

@ -23,12 +23,15 @@ import (
// return value evaluates to non-nil during execution, execution terminates and // return value evaluates to non-nil during execution, execution terminates and
// Execute returns that error. // 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 // When template execution invokes a function with an argument list, that list
// must be assignable to the function's parameter types. Functions meant to // 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 // 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 // of type reflect.Value. Similarly, functions meant to return a result of arbitrary
// type can return interface{} or reflect.Value. // type can return interface{} or reflect.Value.
type FuncMap map[string]interface{} type FuncMap map[string]any
// builtins returns the FuncMap. // builtins returns the FuncMap.
// It is not a global variable so the linker can dead code eliminate // 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. // 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 { if tmpl != nil && tmpl.common != nil {
tmpl.muFuncs.RLock() tmpl.muFuncs.RLock()
defer tmpl.muFuncs.RUnlock() defer tmpl.muFuncs.RUnlock()
if fn := tmpl.execFuncs[name]; fn.IsValid() { if fn := tmpl.execFuncs[name]; fn.IsValid() {
return fn, true return fn, false, true
} }
} }
if fn := builtinFuncs()[name]; fn.IsValid() { 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 // 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 var err error
if argv[i], err = prepareArg(arg, argType); err != nil { 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) return safeCall(fn, argv)
@ -379,31 +382,13 @@ func truth(arg reflect.Value) bool {
// and computes the Boolean AND of its arguments, returning // and computes the Boolean AND of its arguments, returning
// the first false argument it encounters, or the last argument. // the first false argument it encounters, or the last argument.
func and(arg0 reflect.Value, args ...reflect.Value) reflect.Value { func and(arg0 reflect.Value, args ...reflect.Value) reflect.Value {
if !truth(arg0) { panic("unreachable") // implemented as a special case in evalCall
return arg0
}
for i := range args {
arg0 = args[i]
if !truth(arg0) {
break
}
}
return arg0
} }
// or computes the Boolean OR of its arguments, returning // or computes the Boolean OR of its arguments, returning
// the first true argument it encounters, or the last argument. // the first true argument it encounters, or the last argument.
func or(arg0 reflect.Value, args ...reflect.Value) reflect.Value { func or(arg0 reflect.Value, args ...reflect.Value) reflect.Value {
if truth(arg0) { panic("unreachable") // implemented as a special case in evalCall
return arg0
}
for i := range args {
arg0 = args[i]
if truth(arg0) {
break
}
}
return arg0
} }
// not returns the Boolean negation of its argument. // not returns the Boolean negation of its argument.
@ -475,8 +460,10 @@ func eq(arg1 reflect.Value, arg2 ...reflect.Value) (bool, error) {
case k1 == uintKind && k2 == intKind: case k1 == uintKind && k2 == intKind:
truth = arg.Int() >= 0 && arg1.Uint() == uint64(arg.Int()) truth = arg.Int() >= 0 && arg1.Uint() == uint64(arg.Int())
default: default:
if arg1 != zero && arg != zero {
return false, errBadComparison return false, errBadComparison
} }
}
} else { } else {
switch k1 { switch k1 {
case boolKind: case boolKind:
@ -492,7 +479,7 @@ func eq(arg1 reflect.Value, arg2 ...reflect.Value) (bool, error) {
case uintKind: case uintKind:
truth = arg1.Uint() == arg.Uint() truth = arg1.Uint() == arg.Uint()
default: default:
if arg == zero { if arg == zero || arg1 == zero {
truth = arg1 == arg truth = arg1 == arg
} else { } else {
if t2 := arg.Type(); !t2.Comparable() { if t2 := arg.Type(); !t2.Comparable() {
@ -640,7 +627,7 @@ func HTMLEscapeString(s string) string {
// HTMLEscaper returns the escaped HTML equivalent of the textual // HTMLEscaper returns the escaped HTML equivalent of the textual
// representation of its arguments. // representation of its arguments.
func HTMLEscaper(args ...interface{}) string { func HTMLEscaper(args ...any) string {
return HTMLEscapeString(evalArgs(args)) return HTMLEscapeString(evalArgs(args))
} }
@ -731,13 +718,13 @@ func jsIsSpecial(r rune) bool {
// JSEscaper returns the escaped JavaScript equivalent of the textual // JSEscaper returns the escaped JavaScript equivalent of the textual
// representation of its arguments. // representation of its arguments.
func JSEscaper(args ...interface{}) string { func JSEscaper(args ...any) string {
return JSEscapeString(evalArgs(args)) return JSEscapeString(evalArgs(args))
} }
// URLQueryEscaper returns the escaped value of the textual representation of // URLQueryEscaper returns the escaped value of the textual representation of
// its arguments in a form suitable for embedding in a URL query. // 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)) 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, // except that each argument is indirected (if a pointer), as required,
// using the same rules as the default string evaluation during template // using the same rules as the default string evaluation during template
// execution. // execution.
func evalArgs(args []interface{}) string { func evalArgs(args []any) string {
ok := false ok := false
var s string var s string
// Fast path for simple common case. // Fast path for simple common case.

View file

@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style // Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
//go:build go1.13
// +build go1.13 // +build go1.13
package template_test package template_test
@ -41,11 +42,7 @@ func main() {
t.Used() t.Used()
} }
` `
td, err := os.MkdirTemp("", "text_template_TestDeadCodeElimination") td := t.TempDir()
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(td)
if err := os.WriteFile(filepath.Join(td, "x.go"), []byte(prog), 0644); err != nil { if err := os.WriteFile(filepath.Join(td, "x.go"), []byte(prog), 0644); err != nil {
t.Fatal(err) t.Fatal(err)

View file

@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style // Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
//go:build go1.13 && !windows
// +build go1.13,!windows // +build go1.13,!windows
package template 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"])
}

View file

@ -51,13 +51,11 @@ func (t *Template) setOption(opt string) {
if opt == "" { if opt == "" {
panic("empty option string") panic("empty option string")
} }
elems := strings.Split(opt, "=")
switch len(elems) {
case 2:
// key=value // key=value
switch elems[0] { if key, value, ok := strings.Cut(opt, "="); ok {
switch key {
case "missingkey": case "missingkey":
switch elems[1] { switch value {
case "invalid", "default": case "invalid", "default":
t.option.missingKey = mapInvalid t.option.missingKey = mapInvalid
return return

View file

@ -62,6 +62,8 @@ const (
// Keywords appear after all the rest. // Keywords appear after all the rest.
itemKeyword // used only to delimit the keywords itemKeyword // used only to delimit the keywords
itemBlock // block keyword itemBlock // block keyword
itemBreak // break keyword
itemContinue // continue keyword
itemDot // the cursor, spelled '.' itemDot // the cursor, spelled '.'
itemDefine // define keyword itemDefine // define keyword
itemElse // else keyword itemElse // else keyword
@ -76,6 +78,8 @@ const (
var key = map[string]itemType{ var key = map[string]itemType{
".": itemDot, ".": itemDot,
"block": itemBlock, "block": itemBlock,
"break": itemBreak,
"continue": itemContinue,
"define": itemDefine, "define": itemDefine,
"else": itemElse, "else": itemElse,
"end": itemEnd, "end": itemEnd,
@ -119,6 +123,8 @@ type lexer struct {
parenDepth int // nesting depth of ( ) exprs parenDepth int // nesting depth of ( ) exprs
line int // 1+number of newlines seen line int // 1+number of newlines seen
startLine int // start line of this item 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. // 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 // errorf returns an error token and terminates the scan by passing
// back a nil pointer that will be the next state, terminating l.nextItem. // 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} l.items <- item{itemError, l.start, fmt.Sprintf(format, args...), l.startLine}
return nil return nil
} }
@ -461,7 +467,12 @@ Loop:
} }
switch { switch {
case key[word] > itemKeyword: 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] == '.': case word[0] == '.':
l.emit(itemField) l.emit(itemField)
case word == "true", word == "false": case word == "true", word == "false":

View file

@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style // Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
//go:build go1.13
// +build go1.13 // +build go1.13
package parse package parse
@ -37,6 +38,8 @@ var itemName = map[itemType]string{
// keywords // keywords
itemDot: ".", itemDot: ".",
itemBlock: "block", itemBlock: "block",
itemBreak: "break",
itemContinue: "continue",
itemDefine: "define", itemDefine: "define",
itemElse: "else", itemElse: "else",
itemIf: "if", itemIf: "if",

View file

@ -71,6 +71,8 @@ const (
NodeVariable // A $ variable. NodeVariable // A $ variable.
NodeWith // A with action. NodeWith // A with action.
NodeComment // A comment. NodeComment // A comment.
NodeBreak // A break action.
NodeContinue // A continue action.
) )
// Nodes. // 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()) 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. // RangeNode represents a {{range}} action and its commands.
type RangeNode struct { type RangeNode struct {
BranchNode BranchNode

View file

@ -24,14 +24,14 @@ type Tree struct {
Mode Mode // parsing mode. Mode Mode // parsing mode.
text string // text parsed to create the template (or its parent) text string // text parsed to create the template (or its parent)
// Parsing only; cleared after parse. // Parsing only; cleared after parse.
funcs []map[string]interface{} funcs []map[string]any
lex *lexer lex *lexer
token [3]item // three-token lookahead for parser. token [3]item // three-token lookahead for parser.
peekCount int peekCount int
vars []string // variables defined at the moment. vars []string // variables defined at the moment.
treeSet map[string]*Tree treeSet map[string]*Tree
actionLine int // line of left delim starting action 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. // A mode value is a set of flags (or 0). Modes control parser behavior.
@ -39,6 +39,7 @@ type Mode uint
const ( const (
ParseComments Mode = 1 << iota // parse comments and add them to AST 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. // 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 // 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 // given the specified name. If an error is encountered, parsing stops and an
// empty map is returned with the error. // 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) treeSet := make(map[string]*Tree)
t := New(name) t := New(name)
t.text = text t.text = text
@ -127,7 +128,7 @@ func (t *Tree) peekNonSpace() item {
// Parsing. // Parsing.
// New allocates a new parse tree with the given name. // 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{ return &Tree{
Name: name, Name: name,
funcs: funcs, funcs: funcs,
@ -157,7 +158,7 @@ func (t *Tree) ErrorContext(n Node) (location, context string) {
} }
// errorf formats the error and terminates processing. // 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 t.Root = nil
format = fmt.Sprintf("template: %s:%d: %s", t.ParseName, t.token[0].line, format) format = fmt.Sprintf("template: %s:%d: %s", t.ParseName, t.token[0].line, format)
panic(fmt.Errorf(format, args...)) panic(fmt.Errorf(format, args...))
@ -217,12 +218,14 @@ func (t *Tree) recover(errp *error) {
} }
// startParse initializes the parser, using the lexer. // 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.Root = nil
t.lex = lex t.lex = lex
t.vars = []string{"$"} t.vars = []string{"$"}
t.funcs = funcs t.funcs = funcs
t.treeSet = treeSet t.treeSet = treeSet
lex.breakOK = !t.hasFunction("break")
lex.continueOK = !t.hasFunction("continue")
} }
// stopParse terminates parsing. // stopParse terminates parsing.
@ -237,7 +240,7 @@ func (t *Tree) stopParse() {
// the template for execution. If either action delimiter string is empty, the // the template for execution. If either action delimiter string is empty, the
// default ("{{" or "}}") is used. Embedded template definitions are added to // default ("{{" or "}}") is used. Embedded template definitions are added to
// the treeSet map. // 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) defer t.recover(&err)
t.ParseName = t.Name t.ParseName = t.Name
emitComment := t.Mode&ParseComments != 0 emitComment := t.Mode&ParseComments != 0
@ -385,6 +388,10 @@ func (t *Tree) action() (n Node) {
switch token := t.nextNonSpace(); token.typ { switch token := t.nextNonSpace(); token.typ {
case itemBlock: case itemBlock:
return t.blockControl() return t.blockControl()
case itemBreak:
return t.breakControl(token.pos, token.line)
case itemContinue:
return t.continueControl(token.pos, token.line)
case itemElse: case itemElse:
return t.elseControl() return t.elseControl()
case itemEnd: case itemEnd:
@ -404,6 +411,32 @@ func (t *Tree) action() (n Node) {
return t.newAction(token.pos, token.line, t.pipeline("command", itemRightDelim)) 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: // Pipeline:
// declarations? command ('|' command)* // declarations? command ('|' command)*
func (t *Tree) pipeline(context string, end itemType) (pipe *PipeNode) { 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) { func (t *Tree) parseControl(allowElseIf bool, context string) (pos Pos, line int, pipe *PipeNode, list, elseList *ListNode) {
defer t.popVars(len(t.vars)) defer t.popVars(len(t.vars))
pipe = t.pipeline(context, itemRightDelim) pipe = t.pipeline(context, itemRightDelim)
if context == "range" {
t.rangeDepth++
}
var next Node var next Node
list, next = t.itemList() list, next = t.itemList()
if context == "range" {
t.rangeDepth--
}
switch next.Type() { switch next.Type() {
case nodeEnd: //done case nodeEnd: //done
case nodeElse: case nodeElse:
@ -522,7 +561,8 @@ func (t *Tree) ifControl() Node {
// {{range pipeline}} itemList {{else}} itemList {{end}} // {{range pipeline}} itemList {{else}} itemList {{end}}
// Range keyword is past. // Range keyword is past.
func (t *Tree) rangeControl() Node { func (t *Tree) rangeControl() Node {
return t.newRange(t.parseControl(false, "range")) r := t.newRange(t.parseControl(false, "range"))
return r
} }
// With: // With:
@ -689,7 +729,8 @@ func (t *Tree) operand() Node {
func (t *Tree) term() Node { func (t *Tree) term() Node {
switch token := t.nextNonSpace(); token.typ { switch token := t.nextNonSpace(); token.typ {
case itemIdentifier: 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) t.errorf("function %q not defined", token.val)
} }
return NewIdentifier(token.val).SetTree(t).SetPos(token.pos) return NewIdentifier(token.val).SetTree(t).SetPos(token.pos)

View file

@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style // Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
//go:build go1.13
// +build go1.13 // +build go1.13
package parse package parse
@ -232,6 +233,10 @@ var parseTests = []parseTest{
`{{range $x := .SI}}{{.}}{{end}}`}, `{{range $x := .SI}}{{.}}{{end}}`},
{"range 2 vars", "{{range $x, $y := .SI}}{{.}}{{end}}", noError, {"range 2 vars", "{{range $x, $y := .SI}}{{.}}{{end}}", noError,
`{{range $x, $y := .SI}}{{.}}{{end}}`}, `{{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, {"constants", "{{range .SI 1 -3.2i true false 'a' nil}}{{end}}", noError,
`{{range .SI 1 -3.2i true false 'a' nil}}{{end}}`}, `{{range .SI 1 -3.2i true false 'a' nil}}{{end}}`},
{"template", "{{template `x`}}", noError, {"template", "{{template `x`}}", noError,
@ -281,6 +286,10 @@ var parseTests = []parseTest{
{"adjacent args", "{{printf 3`x`}}", hasError, ""}, {"adjacent args", "{{printf 3`x`}}", hasError, ""},
{"adjacent args with .", "{{printf `x`.}}", hasError, ""}, {"adjacent args with .", "{{printf `x`.}}", hasError, ""},
{"extra end after if", "{{if .X}}a{{else if .Y}}b{{end}}{{end}}", 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. // Other kinds of assignments and operators aren't available yet.
{"bug0a", "{{$x := 0}}{{$x}}", noError, "{{$x := 0}}{{$x}}"}, {"bug0a", "{{$x := 0}}{{$x}}", noError, "{{$x := 0}}{{$x}}"},
{"bug0b", "{{$x += 1}}{{$x}}", hasError, ""}, {"bug0b", "{{$x += 1}}{{$x}}", hasError, ""},
@ -312,7 +321,7 @@ var parseTests = []parseTest{
{"block definition", `{{block "foo"}}hello{{end}}`, hasError, ""}, {"block definition", `{{block "foo"}}hello{{end}}`, hasError, ""},
} }
var builtins = map[string]interface{}{ var builtins = map[string]any{
"printf": fmt.Sprintf, "printf": fmt.Sprintf,
"contains": strings.Contains, "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 { type isEmptyTest struct {
name string name string
input string input string

View file

@ -5,16 +5,15 @@
package template package template
import ( import (
"github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate/parse"
"reflect" "reflect"
"sync" "sync"
"github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate/parse"
) )
// common holds the information shared by related templates. // common holds the information shared by related templates.
type common struct { type common struct {
muTmpl sync.RWMutex // protects tmpl (temporary Hugo-fix)
tmpl map[string]*Template // Map from name to defined templates. tmpl map[string]*Template // Map from name to defined templates.
muTmpl sync.RWMutex // protects tmpl
option option option option
// We use two maps, one for parsing and one for execution. // We use two maps, one for parsing and one for execution.
// This separation makes the API cleaner since it doesn't // This separation makes the API cleaner since it doesn't
@ -90,7 +89,6 @@ func (t *Template) Clone() (*Template, error) {
if t.common == nil { if t.common == nil {
return nt, nil return nt, nil
} }
// temporary Hugo-fix
t.muTmpl.RLock() t.muTmpl.RLock()
defer t.muTmpl.RUnlock() defer t.muTmpl.RUnlock()
for k, v := range t.tmpl { 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 // 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. // definition is replaced; otherwise a new template is created, defined, and returned.
func (t *Template) AddParseTree(name string, tree *parse.Tree) (*Template, error) { func (t *Template) AddParseTree(name string, tree *parse.Tree) (*Template, error) {
// temporary Hugo-fix t.init()
t.muTmpl.Lock() t.muTmpl.Lock()
defer t.muTmpl.Unlock() defer t.muTmpl.Unlock()
t.init()
nt := t nt := t
if name != t.name { if name != t.name {
nt = t.New(name) nt = t.New(name)
@ -150,7 +147,6 @@ func (t *Template) Templates() []*Template {
return nil return nil
} }
// Return a slice so we don't expose the map. // Return a slice so we don't expose the map.
// temporary Hugo-fix
t.muTmpl.RLock() t.muTmpl.RLock()
defer t.muTmpl.RUnlock() defer t.muTmpl.RUnlock()
m := make([]*Template, 0, len(t.tmpl)) m := make([]*Template, 0, len(t.tmpl))
@ -193,7 +189,6 @@ func (t *Template) Lookup(name string) *Template {
if t.common == nil { if t.common == nil {
return nil return nil
} }
// temporary Hugo-fix
t.muTmpl.RLock() t.muTmpl.RLock()
defer t.muTmpl.RUnlock() defer t.muTmpl.RUnlock()
return t.tmpl[name] return t.tmpl[name]