Skip to content

fix(typescript): follow Jinja2 truthiness and filter coercion in the nunjucks renderer - #584

Open
Arfath Ahmed Syed (ArfiArfath21) wants to merge 1 commit into
microsoft:mainfrom
ArfiArfath21:fix/ts-nunjucks-jinja-parity
Open

Arfath Ahmed Syed (ArfiArfath21) wants to merge 1 commit into
microsoft:mainfrom
ArfiArfath21:fix/ts-nunjucks-jinja-parity

Conversation

@ArfiArfath21

@ArfiArfath21 Arfath Ahmed Syed (ArfiArfath21) commented Oct 18, 2026 •

Copy link
Copy Markdown

Fixes #583

The TypeScript jinja2 key resolves to NunjucksRenderer, which diverged from the Jinja2 reference on two REQUIRED subset behaviours:

  • {% if docs %} took the true branch for [] and {}, because nunjucks compiles conditions to JavaScript truthiness (spec/jinja-subset.md §2.2).
  • trim / join on an omitted input and upper / lower / trim on a number threw TypeError, where Jinja2 renders "" or the stringified value (§2.1, §2.5).

What changed, all in runtime/typescript/packages/core/src/renderers/nunjucks.ts:

  • While renderSafely compiles and runs a template, if, inline if, not, and and or are emitted around a runtime.truthy check that follows Jinja2 (empty list, map and string are falsy). and / or still return an operand, as in Jinja2. The compiler methods and runtime.truthy are put back in a finally, the same way the existing memberLookup / callWrap hooks already are, so other nunjucks users in the process are untouched.
  • upper, lower, trim and join are overridden through env.addFilter to coerce first (undefined / null become "", numbers and booleans stringify, join accepts strings and maps the way Jinja2 does).

The sandbox hooks, the renderer key and the nonce handling are unchanged.

Tests (tests/renderers.test.ts): 23 new cases for truthiness (every falsy and truthy shape, elif, not / and / or, inline if, the guard-a-loop scenario), filter coercion, and restoring nunjucks afterwards, including after a template error. Their expected values are what Python's jinja2 renders. 9 of them fail on main and all pass here.

Verification:

  • vitest in packages/core: 1257 passed. tsc --noEmit and npm run format:check are clean.
  • Differential fuzz against Python jinja2: 6,000 random subset templates, then 10,000 more with empty-collection conditions, all identical after the change (before it, 167 of the 6,000 in the narrowest run differed). The Python and TS jinja_subset engines already matched jinja2 on all of them.
  • I could not run the Java, C#, Swift or Go renderers here.

Happy to follow up with shared render vectors for empty-collection truthiness and undefined-through-filter, so the other runtimes are held to the same behaviour, if you want them.

…nunjucks renderer

Nunjucks evaluates conditions with JavaScript truthiness, so an empty list or
map took the true branch, unlike jinja2 and the subset spec (section 2.2).
Its upper/lower/trim/join filters also threw TypeError on omitted inputs and
on numbers, where jinja2 renders an empty string or the stringified value.

Emit runtime.truthy around if/inline-if/not/and/or while a template is being
compiled (restored afterwards, like the existing runtime hooks) and override
the four filters to coerce first.

Verified against jinja2 with a 10,000 template differential fuzz run.

This branch has not been deployed

No deployments
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.

TypeScript nunjucks renderer diverges from Jinja2: empty list/map are truthy, filters throw on missing or numeric values

1 participant