Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enforce AbstractBinary for all binary operators #4482

Merged
merged 1 commit into from
Nov 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

# 3.16.0 (2024-11-29)

* Deprecate `ConditionalExpression` and `NullCoalesceExpression` (use `ConditionalTernary` and `NullCoalesceBinary` instead)
* Deprecate `InlinePrint`
* Fix having macro variables starting with an underscore
* Deprecate not passing a `Source` instance to `TokenStream`
Expand Down
10 changes: 9 additions & 1 deletion doc/deprecated.rst
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,15 @@ Nodes
made ready for ``yield``; the ``use_yield`` Environment option can be turned
on when all nodes use the ``#[\Twig\Attribute\YieldReady]`` attribute.

* The ``InlinePrint`` class is deprecated as of Twig 3.16 with no replacement.
* The ``Twig\Node\InlinePrint`` class is deprecated as of Twig 3.16 with no
replacement.

* The ``Twig\Node\Expression\NullCoalesceExpression`` class is deprecated as
of Twig 3.16, use ``Twig\Node\Expression\Binary\NullCoalesceBinary``
instead.

* The ``Twig\Node\Expression\ConditionalExpression`` class is deprecated as of
Twig 3.16, use ``Twig\Node\Expression\Ternary\ConditionalTernary`` instead.

Node Visitors
-------------
Expand Down
22 changes: 8 additions & 14 deletions src/ExpressionParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,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 @@ -269,22 +269,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)
fabpot marked this conversation as resolved.
Show resolved Hide resolved
$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
5 changes: 4 additions & 1 deletion src/Extension/CoreExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,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 @@ -41,6 +42,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 @@ -320,6 +322,8 @@ public function getOperators(): array
'+' => ['precedence' => 500, 'class' => PosUnary::class],
],
[
'?:' => ['precedence' => 5, 'class' => ElvisBinary::class, 'associativity' => ExpressionParser::OPERATOR_RIGHT],
'??' => ['precedence' => 300, 'precedence_change' => new OperatorPrecedenceChange('twig/twig', '3.15', 5), 'class' => NullCoalesceBinary::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],
'and' => ['precedence' => 15, 'class' => AndBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT],
Expand Down Expand Up @@ -351,7 +355,6 @@ public function getOperators(): array
'is' => ['precedence' => 100, 'associativity' => ExpressionParser::OPERATOR_LEFT],
'is not' => ['precedence' => 100, 'associativity' => ExpressionParser::OPERATOR_LEFT],
'**' => ['precedence' => 200, 'class' => PowerBinary::class, 'associativity' => ExpressionParser::OPERATOR_RIGHT],
'??' => ['precedence' => 300, 'precedence_change' => new OperatorPrecedenceChange('twig/twig', '3.15', 5), 'class' => NullCoalesceExpression::class, 'associativity' => ExpressionParser::OPERATOR_RIGHT],
],
];
}
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'];
fabpot marked this conversation as resolved.
Show resolved Hide resolved
}
}
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'];
}
}
4 changes: 2 additions & 2 deletions src/Node/Expression/Filter/DefaultFilter.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,11 @@
use Twig\Extension\CoreExtension;
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\Node;
use Twig\TwigFilter;
Expand Down Expand Up @@ -57,7 +57,7 @@ public function __construct(Node $node, TwigFilter|ConstantExpression $filter, N
$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 @@ -28,6 +29,8 @@ class NullCoalesceExpression extends ConditionalExpression
*/
public function __construct(Node $left, Node $right, int $lineno)
{
trigger_deprecation('twig/twig', '3.16', \sprintf('"%s" is deprecated; use "%s" instead.', __CLASS__, NullCoalesceBinary::class));

if (!$left instanceof AbstractExpression) {
trigger_deprecation('twig/twig', '3.15', 'Not passing a "%s" instance to the "left" argument of "%s" is deprecated ("%s" given).', AbstractExpression::class, static::class, get_class($left));
}
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.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This interface can be defined on 1-ary operators, but that would not make sense.

*
* @author Fabien Potencier <[email protected]>
*/
interface OperatorEscapeInterface
{
/**
* @return string[]
*/
public function getOperandNamesToEscape(): array;
fabpot marked this conversation as resolved.
Show resolved Hide resolved
}
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'];
}
}
Loading
Loading