Repository: gohugoio/hugo (language: go)

Issue:
Some issues with the index func
I'm doing some documentation work where I have a backing data file which is a nested map structure.

The `index` func is incredibly useful, but it 1) Has some subtle bugs (esp. when the indices given are a slice and not variadic) and 2) Is a little eager to return errors in situations where most people would be happy with an empty value ("not found"). 

I will follow up with a PR with test cases that show what I mean.


Discussion / hints:

Fix patch:
diff --git a/tpl/collections/index.go b/tpl/collections/index.go
index 43fccbc6bff..13dd386725b 100644
--- a/tpl/collections/index.go
+++ b/tpl/collections/index.go
@@ -35,12 +35,9 @@ import (
 func (ns *Namespace) Index(item any, args ...any) (any, error) {
 	v := reflect.ValueOf(item)
 	if !v.IsValid() {
-		return nil, errors.New("index of untyped nil")
-	}
-
-	lowerm, ok := item.(maps.Params)
-	if ok {
-		return lowerm.Get(cast.ToStringSlice(args)...), nil
+		// See issue 10489
+		// This used to be an error.
+		return nil, nil
 	}
 
 	var indices []any
@@ -51,18 +48,25 @@ func (ns *Namespace) Index(item any, args ...any) (any, error) {
 			for i := 0; i < v.Len(); i++ {
 				indices = append(indices, v.Index(i).Interface())
 			}
+		} else {
+			indices = append(indices, args[0])
 		}
+	} else {
+		indices = args
 	}
 
-	if indices == nil {
-		indices = args
+	lowerm, ok := item.(maps.Params)
+	if ok {
+		return lowerm.Get(cast.ToStringSlice(indices)...), nil
 	}
 
 	for _, i := range indices {
 		index := reflect.ValueOf(i)
 		var isNil bool
 		if v, isNil = indirect(v); isNil {
-			return nil, errors.New("index of nil pointer")
+			// See issue 10489
+			// This used to be an error.
+			return nil, nil
 		}
 		switch v.Kind() {
 		case reflect.Array, reflect.Slice, reflect.String:
diff --git a/tpl/diagrams/diagrams.go b/tpl/diagrams/diagrams.go
index 7f0a171b2cb..9abe9e24c7e 100644
--- a/tpl/diagrams/diagrams.go
+++ b/tpl/diagrams/diagrams.go
@@ -59,11 +59,12 @@ func (d goatDiagram) Height() int {
 	return d.d.Height
 }
 
-type Diagrams struct {
+// Namespace provides template functions for the diagrams namespace.
+type Namespace struct {
 	d *deps.Deps
 }
 
-func (d *Diagrams) Goat(v any) SVGDiagram {
+func (d *Namespace) Goat(v any) SVGDiagram {
 	var r io.Reader
 
 	switch vv := v.(type) {
diff --git a/tpl/diagrams/init.go b/tpl/diagrams/init.go
index fed85e6a358..005e942b720 100644
--- a/tpl/diagrams/init.go
+++ b/tpl/diagrams/init.go
@@ -22,7 +22,7 @@ const name = "diagrams"
 
 func init() {
 	f := func(d *deps.Deps) *internal.TemplateFuncsNamespace {
-		ctx := &Diagrams{
+		ctx := &Namespace{
 			d: d,
 		}
 
diff --git a/tpl/internal/templatefuncsRegistry.go b/tpl/internal/templatefuncsRegistry.go
index d06b55b4d06..da9aa5d292b 100644
--- a/tpl/internal/templatefuncsRegistry.go
+++ b/tpl/internal/templatefuncsRegistry.go
@@ -141,6 +141,22 @@ func (t goDocFunc) toJSON() ([]byte, error) {
 	return buf.Bytes(), nil
 }
 
+// ToMap returns a limited map representation of the namespaces.
+func (namespaces TemplateFuncsNamespaces) ToMap() map[string]any {
+	m := make(map[string]any)
+	for _, ns := range namespaces {
+		mm := make(map[string]any)
+		for name, mapping := range ns.MethodMappings {
+			mm[name] = map[string]any{
+				"Examples": mapping.Examples,
+				"Aliases":  mapping.Aliases,
+			}
+		}
+		m[ns.Name] = mm
+	}
+	return m
+}
+
 // MarshalJSON returns the JSON encoding of namespaces.
 func (namespaces TemplateFuncsNamespaces) MarshalJSON() ([]byte, error) {
 	var buf bytes.Buffer


Test patch:
diff --git a/tpl/collections/index_test.go b/tpl/collections/index_test.go
index 662fe6f3331..7c917c4431d 100644
--- a/tpl/collections/index_test.go
+++ b/tpl/collections/index_test.go
@@ -30,6 +30,11 @@ func TestIndex(t *testing.T) {
 	c := qt.New(t)
 	ns := New(&deps.Deps{Language: langs.NewDefaultLanguage(config.New())})
 
+	var (
+		emptyInterface any
+		nilPointer     *int
+	)
+
 	for i, test := range []struct {
 		item    any
 		indices []any
@@ -45,16 +50,23 @@ func TestIndex(t *testing.T) {
 		{map[string]map[string]string{"a": {"b": "c"}}, []any{"a", "b"}, "c", false},
 		{[]map[string]map[string]string{{"a": {"b": "c"}}}, []any{0, "a", "b"}, "c", false},
 		{map[string]map[string]any{"a": {"b": []string{"c", "d"}}}, []any{"a", "b", 1}, "d", false},
-		{map[string]map[string]string{"a": {"b": "c"}}, []any{[]string{"a", "b"}}, "c", false},
 		{maps.Params{"a": "av"}, []any{"A"}, "av", false},
 		{maps.Params{"a": map[string]any{"b": "bv"}}, []any{"A", "B"}, "bv", false},
+
+		// These used to be errors.
+		// See issue 10489.
+		{nil, nil, nil, false},
+		{nil, []any{0}, nil, false},
+		{emptyInterface, []any{0}, nil, false},
+		{nilPointer, []any{0}, nil, false},
+
 		// errors
-		{nil, nil, nil, true},
 		{[]int{0, 1}, []any{"1"}, nil, true},
 		{[]int{0, 1}, []any{nil}, nil, true},
 		{tstNoStringer{}, []any{0}, nil, true},
 	} {
-		c.Run(fmt.Sprint(i), func(c *qt.C) {
+
+		c.Run(fmt.Sprintf("vararg %d", i), func(c *qt.C) {
 			errMsg := qt.Commentf("[%d] %v", i, test)
 
 			result, err := ns.Index(test.item, test.indices...)
@@ -66,5 +78,18 @@ func TestIndex(t *testing.T) {
 			c.Assert(err, qt.IsNil, errMsg)
 			c.Assert(result, qt.DeepEquals, test.expect, errMsg)
 		})
+
+		c.Run(fmt.Sprintf("slice %d", i), func(c *qt.C) {
+			errMsg := qt.Commentf("[%d] %v", i, test)
+
+			result, err := ns.Index(test.item, test.indices)
+
+			if test.isErr {
+				c.Assert(err, qt.Not(qt.IsNil), errMsg)
+				return
+			}
+			c.Assert(err, qt.IsNil, errMsg)
+			c.Assert(result, qt.DeepEquals, test.expect, errMsg)
+		})
 	}
 }