From b358df847d67a78a0b65d1c2448e020b313c75ee Mon Sep 17 00:00:00 2001 From: Apoorva Verma Date: Thu, 2 Jul 2026 07:49:17 +0530 Subject: [PATCH] Report a syntax error for unclosed brackets in expressions `{{{ foo }}` rendered as `[object Object]` instead of failing. The extra brace opens an object that never closes, so its group-start token was left on the shunting-yard stack, flushed into the output, and then handed straight back by the parser. Check the stack once the expression is consumed and throw if an opening `{`, `[` or `(` was never matched. A well formed expression always pops its start at the matching close, so this only fires on genuinely unbalanced input. --- src/twig.expression.js | 20 +++++++++++++++++++- test/test.regression.js | 18 ++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/src/twig.expression.js b/src/twig.expression.js index 0bd65f88..92419603 100644 --- a/src/twig.expression.js +++ b/src/twig.expression.js @@ -103,6 +103,15 @@ module.exports = function (Twig) { Twig.expression.type.slice ]); + // The closing bracket expected for each token that opens a group. Used to + // detect an unclosed group left on the stack after an expression is compiled. + Twig.expression.closingBracket = { + [Twig.expression.type.object.start]: '}', + [Twig.expression.type.array.start]: ']', + [Twig.expression.type.parameter.start]: ')', + [Twig.expression.type.subexpression.start]: ')' + }; + // Some commonly used compile and parse functions. Twig.expression.fn = { compile: { @@ -1323,8 +1332,17 @@ module.exports = function (Twig) { Twig.log.trace('Twig.expression.compile: ', 'Output is', output); } + // A well formed expression closes every group it opens, so once the + // input is consumed the stack should only hold operators. An opening + // bracket left here is an unclosed group and must be reported rather + // than silently flushed into the output. while (stack.length > 0) { - output.push(stack.pop()); + token = stack.pop(); + if (Object.prototype.hasOwnProperty.call(Twig.expression.closingBracket, token.type)) { + throw new Twig.Error('Expected closing \'' + Twig.expression.closingBracket[token.type] + '\' in expression \'' + rawToken.value + '\''); + } + + output.push(token); } Twig.log.trace('Twig.expression.compile: ', 'Final output is', output); diff --git a/test/test.regression.js b/test/test.regression.js index db9f571f..cc81d58b 100644 --- a/test/test.regression.js +++ b/test/test.regression.js @@ -36,4 +36,22 @@ describe('Twig.js Regression Tests ->', function () { const testTemplate = twig({data: str}); testTemplate.render().should.equal(expected); }); + + it('#896 should report a syntax error for an unclosed brace instead of rendering [object Object]', function () { + // '{{{ ... }}' opens an object with the extra '{' that is never closed. + // It used to render as '[object Object]' rather than failing. + (function () { + twig({rethrow: true, data: 'Hey {{{data.variables.aa | default(\'there\') }}'}).render({data: {variables: {}}}); + }).should.throw(/Expected closing '}'/); + + (function () { + twig({rethrow: true, data: 'Hey {{{data.variables.aa | default(\'there\') }}}'}).render({data: {variables: {}}}); + }).should.throw(/Expected closing '}'/); + }); + + it('#896 should still render a triple-open brace whose object is closed', function () { + twig({data: '{% for val in arr %}{{{(val.value):null}|json_encode}}{% endfor %}'}) + .render({arr: [{value: 'one'}, {value: 'two'}]}) + .should.equal('{"one":null}{"two":null}'); + }); });