From f5afaabf54290b920a1a13a890b027d4cb9415a4 Mon Sep 17 00:00:00 2001 From: Alexander Pankratov Date: Thu, 11 Jun 2026 15:33:21 +0200 Subject: [PATCH] Fix Markup truthiness in boolean expressions --- src/Node/Expression/Binary/AndBinary.php | 7 +++++++ src/Node/Expression/Binary/ElvisBinary.php | 3 ++- src/Node/Expression/Binary/OrBinary.php | 7 +++++++ src/Node/Expression/Binary/XorBinary.php | 7 +++++++ .../Expression/Ternary/ConditionalTernary.php | 8 +------- src/Node/Expression/Test/TrueTest.php | 12 ++++++++++++ src/Node/Expression/Unary/NotUnary.php | 10 +++++++++- src/Node/IfNode.php | 7 +------ tests/Fixtures/regression/markup_test.test | 16 ++++++++++++++++ 9 files changed, 62 insertions(+), 15 deletions(-) diff --git a/src/Node/Expression/Binary/AndBinary.php b/src/Node/Expression/Binary/AndBinary.php index 454ea70e5b2..7ccef174bdf 100644 --- a/src/Node/Expression/Binary/AndBinary.php +++ b/src/Node/Expression/Binary/AndBinary.php @@ -14,9 +14,16 @@ use Twig\Compiler; use Twig\Node\Expression\ReturnBoolInterface; +use Twig\Node\Expression\Test\TrueTest; +use Twig\Node\Node; class AndBinary extends AbstractBinary implements ReturnBoolInterface { + public function __construct(Node $left, Node $right, int $lineno) + { + parent::__construct(TrueTest::wrap($left), TrueTest::wrap($right), $lineno); + } + public function operator(Compiler $compiler): Compiler { return $compiler->raw('&&'); diff --git a/src/Node/Expression/Binary/ElvisBinary.php b/src/Node/Expression/Binary/ElvisBinary.php index 25522240b64..1340a4f0cb1 100644 --- a/src/Node/Expression/Binary/ElvisBinary.php +++ b/src/Node/Expression/Binary/ElvisBinary.php @@ -14,6 +14,7 @@ use Twig\Compiler; use Twig\Node\Expression\AbstractExpression; use Twig\Node\Expression\OperatorEscapeInterface; +use Twig\Node\Expression\Test\TrueTest; use Twig\Node\Node; final class ElvisBinary extends AbstractBinary implements OperatorEscapeInterface @@ -26,7 +27,7 @@ public function __construct(Node $left, Node $right, int $lineno) { parent::__construct($left, $right, $lineno); - $this->setNode('test', clone $left); + $this->setNode('test', TrueTest::wrap(clone $left)); $left->setAttribute('always_defined', true); } diff --git a/src/Node/Expression/Binary/OrBinary.php b/src/Node/Expression/Binary/OrBinary.php index 82dcb7e953b..cacffdaa332 100644 --- a/src/Node/Expression/Binary/OrBinary.php +++ b/src/Node/Expression/Binary/OrBinary.php @@ -14,9 +14,16 @@ use Twig\Compiler; use Twig\Node\Expression\ReturnBoolInterface; +use Twig\Node\Expression\Test\TrueTest; +use Twig\Node\Node; class OrBinary extends AbstractBinary implements ReturnBoolInterface { + public function __construct(Node $left, Node $right, int $lineno) + { + parent::__construct(TrueTest::wrap($left), TrueTest::wrap($right), $lineno); + } + public function operator(Compiler $compiler): Compiler { return $compiler->raw('||'); diff --git a/src/Node/Expression/Binary/XorBinary.php b/src/Node/Expression/Binary/XorBinary.php index 6f412d22fe2..0e9173aba78 100644 --- a/src/Node/Expression/Binary/XorBinary.php +++ b/src/Node/Expression/Binary/XorBinary.php @@ -14,9 +14,16 @@ use Twig\Compiler; use Twig\Node\Expression\ReturnBoolInterface; +use Twig\Node\Expression\Test\TrueTest; +use Twig\Node\Node; class XorBinary extends AbstractBinary implements ReturnBoolInterface { + public function __construct(Node $left, Node $right, int $lineno) + { + parent::__construct(TrueTest::wrap($left), TrueTest::wrap($right), $lineno); + } + public function operator(Compiler $compiler): Compiler { return $compiler->raw('xor'); diff --git a/src/Node/Expression/Ternary/ConditionalTernary.php b/src/Node/Expression/Ternary/ConditionalTernary.php index 7a09189acfe..3972755e01b 100644 --- a/src/Node/Expression/Ternary/ConditionalTernary.php +++ b/src/Node/Expression/Ternary/ConditionalTernary.php @@ -14,19 +14,13 @@ use Twig\Compiler; use Twig\Node\Expression\AbstractExpression; use Twig\Node\Expression\OperatorEscapeInterface; -use Twig\Node\Expression\ReturnPrimitiveTypeInterface; use Twig\Node\Expression\Test\TrueTest; -use Twig\TwigTest; final class ConditionalTernary extends AbstractExpression implements OperatorEscapeInterface { public function __construct(AbstractExpression $test, AbstractExpression $left, AbstractExpression $right, int $lineno) { - if (!$test instanceof ReturnPrimitiveTypeInterface) { - $test = new TrueTest($test, new TwigTest('true', null, ['always_allowed_in_sandbox' => true]), null, $test->getTemplateLine()); - } - - parent::__construct(['test' => $test, 'left' => $left, 'right' => $right], [], $lineno); + parent::__construct(['test' => TrueTest::wrap($test), 'left' => $left, 'right' => $right], [], $lineno); } public function compile(Compiler $compiler): void diff --git a/src/Node/Expression/Test/TrueTest.php b/src/Node/Expression/Test/TrueTest.php index f830b8f2e57..f0c49d476fc 100644 --- a/src/Node/Expression/Test/TrueTest.php +++ b/src/Node/Expression/Test/TrueTest.php @@ -12,7 +12,10 @@ namespace Twig\Node\Expression\Test; use Twig\Compiler; +use Twig\Node\Expression\ReturnPrimitiveTypeInterface; use Twig\Node\Expression\TestExpression; +use Twig\Node\Node; +use Twig\TwigTest; /** * Checks that an expression is true. @@ -23,6 +26,15 @@ */ class TrueTest extends TestExpression { + public static function wrap(Node $node): Node + { + if ($node instanceof ReturnPrimitiveTypeInterface) { + return $node; + } + + return new self($node, new TwigTest('true', null, ['always_allowed_in_sandbox' => true]), null, $node->getTemplateLine()); + } + public function compile(Compiler $compiler): void { $compiler diff --git a/src/Node/Expression/Unary/NotUnary.php b/src/Node/Expression/Unary/NotUnary.php index 55c11bacf18..6bdf8800a6d 100644 --- a/src/Node/Expression/Unary/NotUnary.php +++ b/src/Node/Expression/Unary/NotUnary.php @@ -13,9 +13,17 @@ namespace Twig\Node\Expression\Unary; use Twig\Compiler; +use Twig\Node\Expression\ReturnBoolInterface; +use Twig\Node\Expression\Test\TrueTest; +use Twig\Node\Node; -class NotUnary extends AbstractUnary +class NotUnary extends AbstractUnary implements ReturnBoolInterface { + public function __construct(Node $node, int $lineno) + { + parent::__construct(TrueTest::wrap($node), $lineno); + } + public function operator(Compiler $compiler): Compiler { return $compiler->raw('!'); diff --git a/src/Node/IfNode.php b/src/Node/IfNode.php index 1f91e005810..c7a7f4b9942 100644 --- a/src/Node/IfNode.php +++ b/src/Node/IfNode.php @@ -14,9 +14,7 @@ use Twig\Attribute\YieldReady; use Twig\Compiler; -use Twig\Node\Expression\ReturnPrimitiveTypeInterface; use Twig\Node\Expression\Test\TrueTest; -use Twig\TwigTest; /** * Represents an if node. @@ -29,10 +27,7 @@ class IfNode extends Node public function __construct(Node $tests, ?Node $else, int $lineno) { for ($i = 0, $count = \count($tests); $i < $count; $i += 2) { - $test = $tests->getNode((string) $i); - if (!$test instanceof ReturnPrimitiveTypeInterface) { - $tests->setNode($i, new TrueTest($test, new TwigTest('true', null, ['always_allowed_in_sandbox' => true]), null, $test->getTemplateLine())); - } + $tests->setNode($i, TrueTest::wrap($tests->getNode((string) $i))); } $nodes = ['tests' => $tests]; if (null !== $else) { diff --git a/tests/Fixtures/regression/markup_test.test b/tests/Fixtures/regression/markup_test.test index e03c5291b91..fa8bc3958aa 100644 --- a/tests/Fixtures/regression/markup_test.test +++ b/tests/Fixtures/regression/markup_test.test @@ -8,6 +8,14 @@ Twig outputs 0 nodes correctly {% if spaces|trim %}KO{% else %}ok{% endif %} {% set bar %} {% endset %}{{ bar|trim ? 'KO' : 'ok' }} +{% if bar|trim and bar|trim %}KO{% else %}ok{% endif %}{{- "\n" -}} +{% if bar|trim or empty|trim %}KO{% else %}ok{% endif %}{{- "\n" -}} +{% if bar|trim xor empty|trim %}KO{% else %}ok{% endif %}{{- "\n" -}} +{% if not bar|trim %}ok{% else %}KO{% endif %}{{- "\n" -}} +{{ bar|trim ?: 'ok' }} +{{ (bar|trim and bar|trim) ? 'KO' : 'ok' }} +{{ (bar|trim and bar|trim) ?: 'ok' }} +{{ (bar|trim and bar|trim) ?: (bar|trim ?: 'ok') }} --DATA-- return ['spaces' => new Twig\Markup(' ', 'UTF-8'), 'empty' => new Twig\Markup('', 'UTF-8')] --EXPECT-- @@ -16,3 +24,11 @@ ok ok ok ok +ok +ok +ok +ok +ok +ok +ok +ok