]> git.maquefel.me Git - brevno-suite/hugo/commitdiff
tpl: Fix partial decorator panic when partial returns falsy
authorSimon Heimlicher <simon.github@heimlicher.com>
Sat, 24 Jan 2026 15:59:18 +0000 (16:59 +0100)
committerGitHub <noreply@github.com>
Sat, 24 Jan 2026 15:59:18 +0000 (16:59 +0100)
When {{ with partial "foo" }} returns a falsy value (false, nil, ""),
the with block is skipped but _popPartialDecorator was only called
inside the with block. This left an orphan entry on the decorator
stack, causing "partial decorator ID mismatch" panic on subsequent
partial calls.

Add _popPartialDecorator call to the else branch to ensure the
decorator stack is always balanced regardless of the partial's
return value.

Fixes #14419

tpl/templates/decorator_falsy_test.go [new file with mode: 0644]
tpl/tplimpl/templatetransform.go

diff --git a/tpl/templates/decorator_falsy_test.go b/tpl/templates/decorator_falsy_test.go
new file mode 100644 (file)
index 0000000..ede758a
--- /dev/null
@@ -0,0 +1,63 @@
+// Copyright 2025 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package templates_test
+
+import (
+       "testing"
+
+       "github.com/gohugoio/hugo/hugolib"
+)
+
+// TestDecoratorPartialFalsyReturn tests that partial decorators work correctly
+// when a partial returns a falsy value (false, nil, ""). This was a bug in
+// v0.154.0-v0.154.5 where the decorator stack would become unbalanced.
+// See https://github.com/gohugoio/hugo/issues/14419
+func TestDecoratorPartialFalsyReturn(t *testing.T) {
+       t.Parallel()
+
+       files := `
+-- hugo.toml --
+disableKinds = ["section", "taxonomy", "term", "sitemap", "RSS"]
+-- content/p1.md --
+---
+title: "Page 1"
+---
+-- layouts/_partials/a.html --
+{{ $result := dict }}
+{{ with partialCached "b.html" . .RelPermalink }}
+  {{ $result = . }}
+{{ end }}
+{{ return $result }}
+-- layouts/_partials/b.html --
+{{ $result := dict }}
+{{ with partialCached "c.html" . "key1" }}
+  {{ $result = merge $result (dict "c" .) }}
+{{ end }}
+{{ with partialCached "d.html" . "key2" }}
+  {{ $result = merge $result (dict "d" .) }}
+{{ end }}
+{{ return $result }}
+-- layouts/_partials/c.html --
+{{ return false }}
+-- layouts/_partials/d.html --
+{{ return "truthy" }}
+-- layouts/home.html --
+{{ range site.RegularPages }}
+{{ with partialCached "a.html" (dict "Page" . "RelPermalink" .RelPermalink) .RelPermalink }}d:{{ .d }}{{ end }}
+{{ end }}$
+`
+       b := hugolib.Test(t, files)
+
+       b.AssertFileContent("public/index.html", "d:truthy", "$")
+}
index ffb859b3ea9000e754d153cfeef12789c4217ac6..bab71feb85ed5b9c8cdf43056a86113233458682 100644 (file)
@@ -110,13 +110,18 @@ const (
        // _pushPartialDecorator is always falsy.
        pushPartialDecoratorTempl = `{{ if or (_pushPartialDecorator ("PLACEHOLDER")) }}{{ end }}`
        popPartialDecoratorTempl  = `{{ if (_popPartialDecorator ("PLACEHOLDER1")) }}{{ . }}{{ else }}("PLACEHOLDER2"){{ end }}`
+
+       // popPartialDecoratorElseTempl is used when partial returns falsy and the with block is skipped.
+       // We still need to pop the decorator ID from the stack.
+       popPartialDecoratorElseTempl = `{{ _popPartialDecorator ("PLACEHOLDER") }}`
 )
 
 var (
-       partialReturnWrapper *parse.ListNode
-       doDefer              *parse.ListNode
-       popPartialDecorator  *parse.ListNode
-       pushPartialDecorator *parse.ListNode
+       partialReturnWrapper    *parse.ListNode
+       doDefer                 *parse.ListNode
+       popPartialDecorator     *parse.ListNode
+       pushPartialDecorator    *parse.ListNode
+       popPartialDecoratorElse *parse.ListNode
 )
 
 func init() {
@@ -143,6 +148,12 @@ func init() {
                panic(err)
        }
        pushPartialDecorator = templ.Tree.Root
+
+       templ, err = texttemplate.New("").Funcs(texttemplate.FuncMap{"_popPartialDecorator": func(string) string { return "" }}).Parse(popPartialDecoratorElseTempl)
+       if err != nil {
+               panic(err)
+       }
+       popPartialDecoratorElse = templ.Tree.Root
 }
 
 // wrapInPartialReturnWrapper copies and modifies the parsed nodes of a
@@ -376,6 +387,23 @@ func (c *templateTransformContext) handleWithPartial(withNode *parse.WithNode) {
        withNode.Pipe.Cmds = append(orNode.Pipe.Cmds, withNode.Pipe.Cmds...)
 
        withNode.List = newInner
+
+       // When the partial returns a falsy value, the with block is skipped,
+       // but we still need to pop the decorator ID from the stack.
+       // Add a pop call to the else branch.
+       popElse := popPartialDecoratorElse.CopyList()
+       popElseAction := popElse.Nodes[0].(*parse.ActionNode)
+       popElsePipe := popElseAction.Pipe.Cmds[0].Args[1].(*parse.PipeNode)
+       popElseString := popElsePipe.Cmds[0].Args[0].(*parse.StringNode)
+       popElseString.Text = innerHash
+       popElseString.Quoted = fmt.Sprintf("%q", popElseString.Text)
+
+       if withNode.ElseList == nil {
+               withNode.ElseList = popElse
+       } else {
+               // Prepend the pop to the existing else list
+               withNode.ElseList.Nodes = append(popElse.Nodes, withNode.ElseList.Nodes...)
+       }
 }
 
 func (c *templateTransformContext) handleWith(withNode *parse.WithNode) {