Skip to content

Fix falsy computed object keys dropped on re-render (#901) - #977

Merged
RobLoach merged 1 commit into
twigjs:masterfrom
youdie006:fix/901-falsy-computed-object-key
Aug 18, 2026
Merged

Fix falsy computed object keys dropped on re-render (#901)#977
RobLoach merged 1 commit into
twigjs:masterfrom
youdie006:fix/901-falsy-computed-object-key

Conversation

@youdie006

Copy link
Copy Markdown
Contributor

Fixes #901.

Root cause

A computed object key -- {(expr): value} -- that evaluates to a falsy value (0, '', false) is treated as "no key" because three sites in src/twig.expression.js test the truthiness of token.key instead of its presence:

  1. The : operator's parse (if (token.key)). The first render evaluates the key expression, caches the result on token.key, and -- outside a loop -- deletes token.params. On the second render of the same compiled template the cached key is 0, so if (token.key) is false and the else if (token.params) branch is also false (params were already deleted). The token falls through to the plain-operator branch and is dropped from the eval stack, which later surfaces as Unexpected end of object.

  2. The object builder in object.end's parse (... && token.key). On the first render a falsy-key pair isn't recognised as a key/value pair, so the key token is misfiled as a value and the entry silently disappears (m[0] comes back empty).

  3. The loop fixup (loopTokenFixup.params && loopTokenFixup.key) has to use the same presence test, so a cached falsy key inside a loop is re-evaluated per iteration instead of being pinned to its first value.

The literal-key path ({0: 'x'}) already works because those keys are stringified up front (the existing "allow int 0 as a key" test, from #186). Only the computed-key path is affected: #284 added expression-as-key support and #337 hardened object-key stack handling, but neither covered a computed key that resolves to a falsy value.

Reproduction

const t = Twig.twig({data: "{% set m = {(a): 'valA', (b): 'valB'} %}[{{ m[0] }}|{{ m[10] }}]"});
t.render({a: 0, b: 10}); // "[|valB]"  <- m[0] already lost on the first render
t.render({a: 0, b: 10}); // throws: Unexpected end of object.

Fix

Change the three truthiness checks to presence checks ('key' in token / 'key' in loopTokenFixup). This is strictly more inclusive than the old test: regular operator tokens never carry a key property, so they're unaffected; only falsy computed keys are newly retained.

Tests

Two regression tests in test/test.core.js (computed 0 key and computed empty-string key), each rendering the same compiled template twice and asserting the map is correct on the second render. Reverting the source with the tests kept turns both red; the fix turns them green. Full suite: 533 passing, eslint clean.


Implemented with AI assistance; I reviewed and verified the diagnosis, fix, and tests.

A computed object key {(expr): value} that evaluates to a falsy value (0, '',
false) was treated as 'no key' because three sites in twig.expression.js tested
the truthiness of token.key instead of its presence. On the second render of a
compiled template the cached falsy key made the ':' operator drop the token from
the eval stack (params were already deleted), surfacing as 'Unexpected end of
object'; the object builder also misfiled a falsy-key pair as a value on the
first render.

Change the three truthiness checks to presence checks ('key' in token). Regular
operator tokens never carry a key property so they are unaffected; only falsy
computed keys are newly retained. The literal-key path already worked because
those keys are stringified up front.

Fixes twigjs#901

@willrowe willrowe left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This looks correct to me. Good catch @youdie006!

Go ahead and merge if you don't see any issues @RobLoach.

@willrowe
willrowe requested a review from RobLoach August 18, 2026 14:39
@RobLoach
RobLoach merged commit e545910 into twigjs:master Aug 18, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

TwigException "Unexpected end of object" when re-render compiled template

3 participants