hugo

Unnamed repository; edit this file 'description' to name the repository.

git clone git://git.shimmy1996.com/hugo.git
commit 42cc5f88b6e38d94e1a7d146d7f53210d94c0a35
parent a6488e7bad883c71fb08655948d83574d7f703f9
Author: Bjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Date:   Wed, 16 Mar 2022 09:04:30 +0100

tpl: Adjustments and an integration test for Go 1.18

Updates #9677

Diffstat:
Mtpl/internal/go_templates/texttemplate/hugo_template.go | 84+++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------------
Mtpl/tplimpl/integration_test.go | 53+++++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 114 insertions(+), 23 deletions(-)
diff --git a/tpl/internal/go_templates/texttemplate/hugo_template.go b/tpl/internal/go_templates/texttemplate/hugo_template.go
@@ -161,21 +161,23 @@ func (s *state) evalFunction(dot reflect.Value, node *parse.IdentifierNode, cmd 
 	// Added for Hugo.
 	var first reflect.Value
 	var ok bool
+	var isBuiltin bool
 	if s.helper != nil {
+		isBuiltin = name == "and" || name == "or"
 		function, first, ok = s.helper.GetFunc(s.ctx, s.prep, name)
 	}
 
 	if !ok {
-		function, ok = findFunction(name, s.tmpl)
+		function, isBuiltin, ok = findFunction(name, s.tmpl)
 	}
 
 	if !ok {
 		s.errorf("%q is not a defined function", name)
 	}
 	if first != zero {
-		return s.evalCall(dot, function, cmd, name, args, final, first)
+		return s.evalCall(dot, function, isBuiltin, cmd, name, args, final, first)
 	}
-	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).
@@ -200,9 +202,10 @@ func (s *state) evalField(dot reflect.Value, fieldName string, node parse.Node, 
 	// Unless it's an interface, need to get to a value of type *T to guarantee
 	// we see all methods of T and *T.
 	ptr := receiver
-	if ptr.Kind() != reflect.Interface && ptr.Kind() != reflect.Ptr && ptr.CanAddr() {
+	if ptr.Kind() != reflect.Interface && ptr.Kind() != reflect.Pointer && ptr.CanAddr() {
 		ptr = ptr.Addr()
 	}
+
 	// Added for Hugo.
 	var first reflect.Value
 	var method reflect.Value
@@ -214,22 +217,28 @@ func (s *state) evalField(dot reflect.Value, fieldName string, node parse.Node, 
 
 	if method.IsValid() {
 		if first != zero {
-			return s.evalCall(dot, method, node, fieldName, args, final, first)
+			return s.evalCall(dot, method, false, node, fieldName, args, final, first)
 		}
 
-		return s.evalCall(dot, method, node, fieldName, args, final)
+		return s.evalCall(dot, method, false, node, fieldName, args, final)
 	}
 
+	if method := ptr.MethodByName(fieldName); method.IsValid() {
+		return s.evalCall(dot, method, false, node, fieldName, args, final)
+	}
 	hasArgs := len(args) > 1 || final != missingVal
 	// It's not a method; must be a field of a struct or an element of a map.
 	switch receiver.Kind() {
 	case reflect.Struct:
 		tField, ok := receiver.Type().FieldByName(fieldName)
 		if ok {
-			field := receiver.FieldByIndex(tField.Index)
-			if tField.PkgPath != "" { // field is unexported
+			field, err := receiver.FieldByIndexErr(tField.Index)
+			if !tField.IsExported() {
 				s.errorf("%s is an unexported field of struct type %s", fieldName, typ)
 			}
+			if err != nil {
+				s.errorf("%v", err)
+			}
 			// If it's a function, we must call it.
 			if hasArgs {
 				s.errorf("%s has arguments but cannot be invoked as function", fieldName)
@@ -262,7 +271,7 @@ func (s *state) evalField(dot reflect.Value, fieldName string, node parse.Node, 
 			}
 			return result
 		}
-	case reflect.Ptr:
+	case reflect.Pointer:
 		etyp := receiver.Type().Elem()
 		if etyp.Kind() == reflect.Struct {
 			if _, ok := etyp.FieldByName(fieldName); !ok {
@@ -282,17 +291,18 @@ func (s *state) evalField(dot reflect.Value, fieldName string, node parse.Node, 
 // evalCall executes a function or method call. If it's a method, fun already has the receiver bound, so
 // it looks just like a function call. The arg list, if non-nil, includes (in the manner of the shell), arg[0]
 // as the function itself.
-func (s *state) evalCall(dot, fun reflect.Value, node parse.Node, name string, args []parse.Node, final reflect.Value, first ...reflect.Value) reflect.Value {
+func (s *state) evalCall(dot, fun reflect.Value, isBuiltin bool, node parse.Node, name string, args []parse.Node, final reflect.Value, first ...reflect.Value) reflect.Value {
 	if args != nil {
 		args = args[1:] // Zeroth arg is function name/node; not passed to function.
 	}
+
 	typ := fun.Type()
 	numFirst := len(first)
-	numIn := len(args) + numFirst // // Added for Hugo
+	numIn := len(args) + numFirst // Added for Hugo
 	if final != missingVal {
 		numIn++
 	}
-	numFixed := len(args) + len(first)
+	numFixed := len(args) + len(first) // Adjusted for Hugo
 	if typ.IsVariadic() {
 		numFixed = typ.NumIn() - 1 // last arg is the variadic one.
 		if numIn < numFixed {
@@ -305,20 +315,51 @@ func (s *state) evalCall(dot, fun reflect.Value, node parse.Node, name string, a
 		// TODO: This could still be a confusing error; maybe goodFunc should provide info.
 		s.errorf("can't call method/function %q with %d results", name, typ.NumOut())
 	}
+
+	unwrap := func(v reflect.Value) reflect.Value {
+		if v.Type() == reflectValueType {
+			v = v.Interface().(reflect.Value)
+		}
+		return v
+	}
+
+	// Special case for builtin and/or, which short-circuit.
+	if isBuiltin && (name == "and" || name == "or") {
+		argType := typ.In(0)
+		var v reflect.Value
+		for _, arg := range args {
+			v = s.evalArg(dot, argType, arg).Interface().(reflect.Value)
+			if truth(v) == (name == "or") {
+				// This value was already unwrapped
+				// by the .Interface().(reflect.Value).
+				return v
+			}
+		}
+		if final != missingVal {
+			// The last argument to and/or is coming from
+			// the pipeline. We didn't short circuit on an earlier
+			// argument, so we are going to return this one.
+			// We don't have to evaluate final, but we do
+			// have to check its type. Then, since we are
+			// going to return it, we have to unwrap it.
+			v = unwrap(s.validateType(final, argType))
+		}
+		return v
+	}
+
 	// Build the arg list.
 	argv := make([]reflect.Value, numIn)
 	// Args must be evaluated. Fixed args first.
-	i := len(first)
-	for ; i < numFixed && i < len(args)+numFirst; i++ {
-		argv[i] = s.evalArg(dot, typ.In(i), args[i-numFirst])
+	i := len(first)                                     // Adjusted for Hugo.
+	for ; i < numFixed && i < len(args)+numFirst; i++ { // Adjusted for Hugo.
+		argv[i] = s.evalArg(dot, typ.In(i), args[i-numFirst]) // Adjusted for Hugo.
 	}
 	// Now the ... args.
 	if typ.IsVariadic() {
 		argType := typ.In(typ.NumIn() - 1).Elem() // Argument is a slice.
-		for ; i < len(args)+numFirst; i++ {
-			argv[i] = s.evalArg(dot, argType, args[i-numFirst])
+		for ; i < len(args)+numFirst; i++ {       // Adjusted for Hugo.
+			argv[i] = s.evalArg(dot, argType, args[i-numFirst]) // Adjusted for Hugo.
 		}
-
 	}
 	// Add final value if necessary.
 	if final != missingVal {
@@ -347,12 +388,9 @@ func (s *state) evalCall(dot, fun reflect.Value, node parse.Node, name string, a
 	// error to the caller.
 	if err != nil {
 		s.at(node)
-		s.errorf("error calling %s: %v", name, err)
-	}
-	if v.Type() == reflectValueType {
-		v = v.Interface().(reflect.Value)
+		s.errorf("error calling %s: %w", name, err)
 	}
-	return v
+	return unwrap(v)
 }
 
 func isTrue(val reflect.Value) (truth, ok bool) {
diff --git a/tpl/tplimpl/integration_test.go b/tpl/tplimpl/integration_test.go
@@ -59,3 +59,56 @@ title: "P1"
 	b.Assert(names, qt.DeepEquals, []string{"_default/single.json", "baseof.json", "partials/unusedpartial.html", "post/single.html", "shortcodes/unusedshortcode.html"})
 	b.Assert(unused[0].Filename(), qt.Equals, filepath.Join(b.Cfg.WorkingDir, "layouts/_default/single.json"))
 }
+
+// Verify that the new keywords in Go 1.18 is available.
+func TestGo18Constructs(t *testing.T) {
+	t.Parallel()
+
+	files := `
+-- config.toml --
+baseURL = 'http://example.com/'
+disableKinds = ["section", "home", "rss", "taxonomy",  "term", "rss"]
+-- content/p1.md --
+---
+title: "P1"
+---
+-- layouts/partials/counter.html --
+{{ if .Scratch.Get "counter" }}{{ .Scratch.Add "counter" 1 }}{{ else }}{{ .Scratch.Set "counter" 1 }}{{ end }}{{ return true }}
+-- layouts/_default/single.html --
+{{/* Note no spaces in {{continue}} or {{break}}, see https://github.com/golang/go/issues/51670 */}}
+continue:{{ range seq 5 }}{{ if eq . 2 }}{{continue}}{{ end }}{{ . }}{{ end }}:END:
+break:{{ range seq 5 }}{{ if eq . 2 }}{{break}}{{ end }}{{ . }}{{ end }}:END:
+
+counter1: {{ partial "counter.html" . }}/{{ .Scratch.Get "counter" }}
+and1: {{ if (and false (partial "counter.html" .)) }}true{{ else }}false{{ end }}
+or1: {{ if (or true (partial "counter.html" .)) }}true{{ else }}false{{ end }}
+and2: {{ if (and true (partial "counter.html" .)) }}true{{ else }}false{{ end }}
+or2: {{ if (or false (partial "counter.html" .)) }}true{{ else }}false{{ end }}
+
+
+counter2: {{ .Scratch.Get "counter" }}
+
+
+	`
+
+	b := hugolib.NewIntegrationTestBuilder(
+		hugolib.IntegrationTestConfig{
+			T:           t,
+			TxtarString: files,
+			NeedsOsFS:   true,
+		},
+	)
+	b.Build()
+
+	b.AssertFileContent("public/p1/index.html", `
+continue:1345:END:
+break:1:END:
+counter1: true/1
+and1: false
+or1: true
+and2: true
+or2: true
+counter2: 3
+`)
+
+}