}
 
 type templateContext struct {
-       decl  decl
-       templ *template.Template
+       decl    decl
+       templ   *template.Template
+       visited map[string]bool
+}
+
+func (c templateContext) getIfNotVisited(name string) *template.Template {
+       if c.visited[name] {
+               return nil
+       }
+       c.visited[name] = true
+       return c.templ.Lookup(name)
 }
 
 func newTemplateContext(templ *template.Template) *templateContext {
-       return &templateContext{templ: templ, decl: make(map[string]string)}
+       return &templateContext{templ: templ, decl: make(map[string]string), visited: make(map[string]bool)}
 
 }
 
 // paramsKeysToLower is made purposely non-generic to make it not so tempting
 // to do more of these hard-to-maintain AST transformations.
 func (c *templateContext) paramsKeysToLower(n parse.Node) {
-
        switch x := n.(type) {
        case *parse.ListNode:
                if x != nil {
        case *parse.RangeNode:
                c.paramsKeysToLowerForNodes(x.Pipe, x.List, x.ElseList)
        case *parse.TemplateNode:
-               subTempl := c.templ.Lookup(x.Name)
+               subTempl := c.getIfNotVisited(x.Name)
                if subTempl != nil {
                        c.paramsKeysToLowerForNodes(subTempl.Tree.Root)
                }
 
        require.Contains(t, result, "P1: P1L")
        require.Contains(t, result, "P2: P1L")
 }
+
+// Issue #2927
+func TestTransformRecursiveTemplate(t *testing.T) {
+
+       recursive := `
+{{ define "menu-nodes" }}
+{{ template "menu-node" }}
+{{ end }}
+{{ define "menu-node" }}
+{{ template "menu-node" }}
+{{ end }}
+{{ template "menu-nodes" }}
+`
+
+       templ, err := template.New("foo").Parse(recursive)
+       require.NoError(t, err)
+
+       c := newTemplateContext(templ)
+       c.paramsKeysToLower(templ.Tree.Root)
+
+}