Skip to content

Commit

Permalink
Merge branch '3.x' into 4.x
Browse files Browse the repository at this point in the history
* 3.x:
  Enforce AbstractBinary for all binary operators
  Fix CHANGELOG
  Bump version
  Prepare the 3.16.0 release
  • Loading branch information
fabpot committed Nov 29, 2024
2 parents af8c63d + d1d8e3c commit 81c7e8f
Show file tree
Hide file tree
Showing 18 changed files with 343 additions and 43 deletions.
23 changes: 9 additions & 14 deletions src/ExpressionParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,11 @@
use Twig\Node\Expression\ArrowFunctionExpression;
use Twig\Node\Expression\Binary\AbstractBinary;
use Twig\Node\Expression\Binary\ConcatBinary;
use Twig\Node\Expression\ConditionalExpression;
use Twig\Node\Expression\ConstantExpression;
use Twig\Node\Expression\GetAttrExpression;
use Twig\Node\Expression\MacroReferenceExpression;
use Twig\Node\Expression\NameExpression;
use Twig\Node\Expression\Ternary\ConditionalTernary;
use Twig\Node\Expression\TestExpression;
use Twig\Node\Expression\Unary\AbstractUnary;
use Twig\Node\Expression\Unary\NegUnary;
Expand Down Expand Up @@ -259,22 +260,16 @@ private function getPrimary(): AbstractExpression
private function parseConditionalExpression($expr): AbstractExpression
{
while ($this->parser->getStream()->nextIf(Token::PUNCTUATION_TYPE, '?')) {
if (!$this->parser->getStream()->nextIf(Token::PUNCTUATION_TYPE, ':')) {
$expr2 = $this->parseExpression();
if ($this->parser->getStream()->nextIf(Token::PUNCTUATION_TYPE, ':')) {
// Ternary operator (expr ? expr2 : expr3)
$expr3 = $this->parseExpression();
} else {
// Ternary without else (expr ? expr2)
$expr3 = new ConstantExpression('', $this->parser->getCurrentToken()->getLine());
}
} else {
// Ternary without then (expr ?: expr3)
$expr2 = $expr;
$expr2 = $this->parseExpression();
if ($this->parser->getStream()->nextIf(Token::PUNCTUATION_TYPE, ':')) {
// Ternary operator (expr ? expr2 : expr3)
$expr3 = $this->parseExpression();
} else {
// Ternary without else (expr ? expr2)
$expr3 = new ConstantExpression('', $this->parser->getCurrentToken()->getLine());
}

$expr = new ConditionalExpression($expr, $expr2, $expr3, $this->parser->getCurrentToken()->getLine());
$expr = new ConditionalTernary($expr, $expr2, $expr3, $this->parser->getCurrentToken()->getLine());
}

return $expr;
Expand Down
3 changes: 3 additions & 0 deletions src/Extension/CoreExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
use Twig\Node\Expression\Binary\BitwiseXorBinary;
use Twig\Node\Expression\Binary\ConcatBinary;
use Twig\Node\Expression\Binary\DivBinary;
use Twig\Node\Expression\Binary\ElvisBinary;
use Twig\Node\Expression\Binary\EndsWithBinary;
use Twig\Node\Expression\Binary\EqualBinary;
use Twig\Node\Expression\Binary\FloorDivBinary;
Expand All @@ -42,6 +43,7 @@
use Twig\Node\Expression\Binary\MulBinary;
use Twig\Node\Expression\Binary\NotEqualBinary;
use Twig\Node\Expression\Binary\NotInBinary;
use Twig\Node\Expression\Binary\NullCoalesceBinary;
use Twig\Node\Expression\Binary\OrBinary;
use Twig\Node\Expression\Binary\PowerBinary;
use Twig\Node\Expression\Binary\RangeBinary;
Expand Down Expand Up @@ -323,6 +325,7 @@ public function getOperators(): array
'+' => ['precedence' => 500, 'class' => PosUnary::class],
],
[
'?:' => ['precedence' => 5, 'class' => ElvisBinary::class, 'associativity' => ExpressionParser::OPERATOR_RIGHT],
'??' => ['precedence' => 5, 'class' => NullCoalesceExpression::class, 'associativity' => ExpressionParser::OPERATOR_RIGHT],
'or' => ['precedence' => 10, 'class' => OrBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT],
'xor' => ['precedence' => 12, 'class' => XorBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT],
Expand Down
50 changes: 50 additions & 0 deletions src/Node/Expression/Binary/ElvisBinary.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?php

/*
* This file is part of Twig.
*
* (c) Fabien Potencier
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Twig\Node\Expression\Binary;

use Twig\Compiler;
use Twig\Node\Expression\AbstractExpression;
use Twig\Node\Expression\OperatorEscapeInterface;

final class ElvisBinary extends AbstractBinary implements OperatorEscapeInterface
{
public function __construct(AbstractExpression $left, AbstractExpression $right, int $lineno)
{
parent::__construct($left, $right, $lineno);

$this->setNode('test', clone $left);
$left->setAttribute('always_defined', true);
}

public function compile(Compiler $compiler): void
{
$compiler
->raw('((')
->subcompile($this->getNode('test'))
->raw(') ? (')
->subcompile($this->getNode('left'))
->raw(') : (')
->subcompile($this->getNode('right'))
->raw('))')
;
}

public function operator(Compiler $compiler): Compiler
{
return $compiler->raw('?:');
}

public function getOperandNamesToEscape(): array
{
return ['left', 'right'];
}
}
84 changes: 84 additions & 0 deletions src/Node/Expression/Binary/NullCoalesceBinary.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
<?php

/*
* This file is part of Twig.
*
* (c) Fabien Potencier
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Twig\Node\Expression\Binary;

use Twig\Compiler;
use Twig\Node\EmptyNode;
use Twig\Node\Expression\AbstractExpression;
use Twig\Node\Expression\BlockReferenceExpression;
use Twig\Node\Expression\NameExpression;
use Twig\Node\Expression\OperatorEscapeInterface;
use Twig\Node\Expression\Test\DefinedTest;
use Twig\Node\Expression\Test\NullTest;
use Twig\Node\Expression\Unary\NotUnary;
use Twig\TwigTest;

final class NullCoalesceBinary extends AbstractBinary implements OperatorEscapeInterface
{
public function __construct(AbstractExpression $left, AbstractExpression $right, int $lineno)
{
parent::__construct($left, $right, $lineno);

if (!$left instanceof NameExpression) {
$left = clone $left;
$test = new DefinedTest($left, new TwigTest('defined'), new EmptyNode(), $left->getTemplateLine());
// for "block()", we don't need the null test as the return value is always a string
if (!$left instanceof BlockReferenceExpression) {
$test = new AndBinary(
$test,
new NotUnary(new NullTest($left, new TwigTest('null'), new EmptyNode(), $left->getTemplateLine()), $left->getTemplateLine()),
$left->getTemplateLine(),
);
}

$this->setNode('test', $test);
} else {
$left->setAttribute('always_defined', true);
}
}

public function compile(Compiler $compiler): void
{
/*
* This optimizes only one case. PHP 7 also supports more complex expressions
* that can return null. So, for instance, if log is defined, log("foo") ?? "..." works,
* but log($a["foo"]) ?? "..." does not if $a["foo"] is not defined. More advanced
* cases might be implemented as an optimizer node visitor, but has not been done
* as benefits are probably not worth the added complexity.
*/
if ($this->hasNode('test')) {
$compiler
->raw('((')
->subcompile($this->getNode('test'))
->raw(') ? (')
->subcompile($this->getNode('left'))
->raw(') : (')
->subcompile($this->getNode('right'))
->raw('))')
;

return;
}

parent::compile($compiler);
}

public function operator(Compiler $compiler): Compiler
{
return $compiler->raw('??');
}

public function getOperandNamesToEscape(): array
{
return $this->hasNode('test') ? ['left', 'right'] : ['right'];
}
}
11 changes: 10 additions & 1 deletion src/Node/Expression/ConditionalExpression.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,15 @@
namespace Twig\Node\Expression;

use Twig\Compiler;
use Twig\Node\Expression\OperatorEscapeInterface;
use Twig\Node\Expression\Ternary\ConditionalTernary;

class ConditionalExpression extends AbstractExpression
class ConditionalExpression extends AbstractExpression implements OperatorEscapeInterface
{
public function __construct(AbstractExpression $expr1, AbstractExpression $expr2, AbstractExpression $expr3, int $lineno)
{
trigger_deprecation('twig/twig', '3.16', \sprintf('"%s" is deprecated; use "%s" instead.', __CLASS__, ConditionalTernary::class));

parent::__construct(['expr1' => $expr1, 'expr2' => $expr2, 'expr3' => $expr3], [], $lineno);
}

Expand All @@ -42,4 +46,9 @@ public function compile(Compiler $compiler): void
->raw('))');
}
}

public function getOperandNamesToEscape(): array
{
return ['expr2', 'expr3'];
}
}
5 changes: 3 additions & 2 deletions src/Node/Expression/Filter/DefaultFilter.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,11 @@
use Twig\Compiler;
use Twig\Node\EmptyNode;
use Twig\Node\Expression\AbstractExpression;
use Twig\Node\Expression\ConditionalExpression;
use Twig\Node\Expression\ConstantExpression;
use Twig\Node\Expression\FilterExpression;
use Twig\Node\Expression\GetAttrExpression;
use Twig\Node\Expression\NameExpression;
use Twig\Node\Expression\Ternary\ConditionalTernary;
use Twig\Node\Expression\Test\DefinedTest;
use Twig\Node\Expression\Variable\ContextVariable;
use Twig\Node\Node;
Expand All @@ -44,7 +45,7 @@ public function __construct(AbstractExpression $node, TwigFilter $filter, Node $
$test = new DefinedTest(clone $node, new TwigTest('defined'), new EmptyNode(), $node->getTemplateLine());
$false = \count($arguments) ? $arguments->getNode(0) : new ConstantExpression('', $node->getTemplateLine());

$node = new ConditionalExpression($test, $default, $false, $node->getTemplateLine());
$node = new ConditionalTernary($test, $default, $false, $node->getTemplateLine());
} else {
$node = $default;
}
Expand Down
3 changes: 3 additions & 0 deletions src/Node/Expression/NullCoalesceExpression.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
use Twig\Compiler;
use Twig\Node\EmptyNode;
use Twig\Node\Expression\Binary\AndBinary;
use Twig\Node\Expression\Binary\NullCoalesceBinary;
use Twig\Node\Expression\Test\DefinedTest;
use Twig\Node\Expression\Test\NullTest;
use Twig\Node\Expression\Unary\NotUnary;
Expand All @@ -24,6 +25,8 @@ class NullCoalesceExpression extends ConditionalExpression
{
public function __construct(AbstractExpression $left, AbstractExpression $right, int $lineno)
{
trigger_deprecation('twig/twig', '3.16', \sprintf('"%s" is deprecated; use "%s" instead.', __CLASS__, NullCoalesceBinary::class));

$test = new DefinedTest(clone $left, new TwigTest('defined'), new EmptyNode(), $left->getTemplateLine());
// for "block()", we don't need the null test as the return value is always a string
if (!$left instanceof BlockReferenceExpression) {
Expand Down
25 changes: 25 additions & 0 deletions src/Node/Expression/OperatorEscapeInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

/*
* This file is part of Twig.
*
* (c) Fabien Potencier
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Twig\Node\Expression;

/**
* Interface implemented by n-ary operators for n > 1.
*
* @author Fabien Potencier <[email protected]>
*/
interface OperatorEscapeInterface
{
/**
* @return string[]
*/
public function getOperandNamesToEscape(): array;
}
42 changes: 42 additions & 0 deletions src/Node/Expression/Ternary/ConditionalTernary.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?php

/*
* This file is part of Twig.
*
* (c) Fabien Potencier
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Twig\Node\Expression\Ternary;

use Twig\Compiler;
use Twig\Node\Expression\AbstractExpression;
use Twig\Node\Expression\OperatorEscapeInterface;

final class ConditionalTernary extends AbstractExpression implements OperatorEscapeInterface
{
public function __construct(AbstractExpression $test, AbstractExpression $left, AbstractExpression $right, int $lineno)
{
parent::__construct(['test' => $test, 'left' => $left, 'right' => $right], [], $lineno);
}

public function compile(Compiler $compiler): void
{
$compiler
->raw('((')
->subcompile($this->getNode('test'))
->raw(') ? (')
->subcompile($this->getNode('left'))
->raw(') : (')
->subcompile($this->getNode('right'))
->raw('))')
;
}

public function getOperandNamesToEscape(): array
{
return ['left', 'right'];
}
}
28 changes: 13 additions & 15 deletions src/NodeVisitor/EscaperNodeVisitor.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@
use Twig\Node\BlockNode;
use Twig\Node\BlockReferenceNode;
use Twig\Node\Expression\AbstractExpression;
use Twig\Node\Expression\ConditionalExpression;
use Twig\Node\Expression\ConstantExpression;
use Twig\Node\Expression\FilterExpression;
use Twig\Node\Expression\OperatorEscapeInterface;
use Twig\Node\ImportNode;
use Twig\Node\ModuleNode;
use Twig\Node\Node;
Expand Down Expand Up @@ -81,7 +81,7 @@ public function leaveNode(Node $node, Environment $env): ?Node
return $this->preEscapeFilterNode($node, $env);
} elseif ($node instanceof PrintNode && false !== $type = $this->needEscaping()) {
$expression = $node->getNode('expr');
if ($expression instanceof ConditionalExpression) {
if ($expression instanceof OperatorEscapeInterface) {
$this->escapeConditional($expression, $env, $type);
} else {
$node->setNode('expr', $this->escapeExpression($expression, $env, $type));
Expand All @@ -99,20 +99,18 @@ public function leaveNode(Node $node, Environment $env): ?Node
return $node;
}

private function escapeConditional(ConditionalExpression $expression, Environment $env, string $type): void
/**
* @param AbstractExpression&OperatorEscapeInterface $expression
*/
private function escapeConditional($expression, Environment $env, string $type): void
{
$expr2 = $expression->getNode('expr2');
if ($expr2 instanceof ConditionalExpression) {
$this->escapeConditional($expr2, $env, $type);
} else {
$expression->setNode('expr2', $this->escapeExpression($expr2, $env, $type));
}

$expr3 = $expression->getNode('expr3');
if ($expr3 instanceof ConditionalExpression) {
$this->escapeConditional($expr3, $env, $type);
} else {
$expression->setNode('expr3', $this->escapeExpression($expr3, $env, $type));
foreach ($expression->getOperandNamesToEscape() as $name) {
$operand = $expression->getNode($name);
if ($operand instanceof OperatorEscapeInterface) {
$this->escapeConditional($operand, $env, $type);
} else {
$expression->setNode($name, $this->escapeExpression($operand, $env, $type));
}
}
}

Expand Down
Loading

0 comments on commit 81c7e8f

Please sign in to comment.