Skip to content

Fix: standalone inverse tags ({{^helper args}}) drop params/hash arguments - #1188

Open
tomasbjerre wants to merge 1 commit into
jknack:masterfrom
tomasbjerre:fix/unless-hash-params
Open

tomasbjerre wants to merge 1 commit into
jknack:masterfrom
tomasbjerre:fix/unless-hash-params

Conversation

@tomasbjerre

Copy link
Copy Markdown

Bug

visitUnless() in TemplateBuilder.java (which handles a standalone
inverse tag, {{^helper args}}...{{/helper}}) always constructs its
Block with Collections.emptyList()/Collections.emptyMap() for
params/hash, discarding whatever ctx.sexpr() actually parsed:

Block block =
    new Block(
        handlebars,
        name,
        true,
        "^",
        Collections.emptyList(),   // <- parsed params thrown away
        Collections.emptyMap(),    // <- parsed hash thrown away
        blockParams(ctx.blockParams()),
        source(ctx));

The sibling visitBlock(), which handles the positive form
({{#helper args}}...{{/helper}}) and shares the exact same sexpr
grammar rule (sexpr : QID param* hash*), correctly does:

params(sexpr.param()),
hash(sexpr.hash()),

So the grammar parses hash/param arguments on a standalone inverse tag
just fine (unless : UNLESS sexpr blockParams? END body END_BLOCK nameEnd=QID END) - the visitor just never wires them through for that
one code path.

Impact

For a helper that only checks truthiness of a plain value this is
invisible, since most such usages don't pass extra args. But a custom
helper that relies on its own hash argument, e.g.

{{^ifSomething . scope="x"}}...{{/ifSomething}}

silently gets options.hash("scope") == null instead of "x".
Depending on what the helper does with that, this can surface as a
confusing NullPointerException deep inside otherwise-correct helper
code, with no indication that the argument was dropped by the parser
rather than by the helper.

I ran into this in a Handlebars template that used
{{^ifCommitScope . scope="deps"}}...{{/ifCommitScope}} (a custom
helper mirroring the existing ifCommitScope positive-form pattern)
and got an NPE from inside Options.hash(String) that took a while to
trace back to this.

Fix

Mirror visitBlock() exactly: params(sexpr.param()) /
hash(sexpr.hash()) instead of the hardcoded empty collections.

Tests

Added UnlessBlockHelperArgsTest with two cases:

  • the first positional param becomes the helper's context argument
    (matching how visitBlock()/the positive form behaves)
  • an additional positional param and a hash argument are both
    available via options.param(0) / options.hash(...)

Ran the full handlebars module test suite locally:
1030 tests, 0 failures, 0 errors (3 pre-existing skips, unrelated).

No other files are touched; I intentionally left the spotless:apply
formatting drift on unrelated pre-existing files (Handlebars.java,
StringHelpers.java, Text.java, a few test files) out of this PR to
keep the diff scoped to the actual fix.

visitUnless() (handling standalone {{^helper args}}...{{/helper}}
tags) always built its Block with Collections.emptyList()/emptyMap()
for params/hash, discarding whatever the parser had actually parsed
from ctx.sexpr(). The sibling visitBlock() (handling {{#helper
args}}...{{/helper}}), which shares the same sexpr grammar rule,
correctly does params(sexpr.param()) / hash(sexpr.hash()) - visitUnless
just never got the same treatment.

For a helper without a body context binding, this is mostly invisible.
But for a custom helper that relies on its own hash arguments (e.g.
{{^ifSomething . scope="x"}}...{{/ifSomething}}), Options.hash("scope")
silently comes back null. Depending on what the helper does with the
missing value, this can surface as a confusing NullPointerException
deep inside the helper rather than any indication that the arguments
were dropped.

Fix mirrors visitBlock() exactly: params(sexpr.param()) and
hash(sexpr.hash()) instead of the hardcoded empty collections.

Added UnlessBlockHelperArgsTest covering both the context (first
positional param) and hash argument pass-through on a standalone
inverse tag. Ran the full handlebars module test suite
(1030 tests, 0 failures/errors) to confirm no regressions.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N4LemHUKaqfXULRkmxfxwx

@edgar-espina-wpp edgar-espina-wpp left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thank you

@jknack jknack added the bug label Sep 18, 2026
@jknack jknack added this to the 4.5.6 milestone Sep 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants