mirror of
https://github.com/twigphp/Twig.git
synced 2026-08-18 15:55:28 +00:00
Add a proper prefix spread operator
This commit is contained in:
@@ -2,7 +2,8 @@
|
||||
=========== ================ ======= ============= ===========
|
||||
Precedence Operator Type Associativity Description
|
||||
=========== ================ ======= ============= ===========
|
||||
512 => 300 ``|`` infix Left Twig filter call
|
||||
512 ``...`` prefix n/a Spread operator
|
||||
=> 300 ``|`` infix Left Twig filter call
|
||||
``(`` Twig function call
|
||||
``.`` Get an attribute on a variable
|
||||
``[`` Array access
|
||||
@@ -56,7 +57,8 @@ Here is the same table for Twig 4.0 with adjusted precedences:
|
||||
=========== ================ ======= ============= ===========
|
||||
Precedence Operator Type Associativity Description
|
||||
=========== ================ ======= ============= ===========
|
||||
512 ``(`` infix Left Twig function call
|
||||
512 ``...`` prefix n/a Spread operator
|
||||
``(`` infix Left Twig function call
|
||||
``.`` Get an attribute on a variable
|
||||
``[`` Array access
|
||||
500 ``-`` prefix n/a
|
||||
|
||||
@@ -50,13 +50,11 @@ trait ArgumentsTrait
|
||||
}
|
||||
}
|
||||
|
||||
if ($stream->nextIf(Token::SPREAD_TYPE)) {
|
||||
$value = $parser->parseExpression();
|
||||
if ($value instanceof SpreadUnary) {
|
||||
$hasSpread = true;
|
||||
$value = new SpreadUnary($parser->parseExpression(), $stream->getCurrent()->getLine());
|
||||
} elseif ($hasSpread) {
|
||||
throw new SyntaxError('Normal arguments must be placed before argument unpacking.', $stream->getCurrent()->getLine(), $stream->getSourceContext());
|
||||
} else {
|
||||
$value = $parser->parseExpression();
|
||||
}
|
||||
|
||||
$name = null;
|
||||
|
||||
@@ -20,6 +20,7 @@ use Twig\Node\Expression\AbstractExpression;
|
||||
use Twig\Node\Expression\ArrayExpression;
|
||||
use Twig\Node\Expression\Binary\ConcatBinary;
|
||||
use Twig\Node\Expression\ConstantExpression;
|
||||
use Twig\Node\Expression\Unary\SpreadUnary;
|
||||
use Twig\Node\Expression\Variable\ContextVariable;
|
||||
use Twig\Parser;
|
||||
use Twig\Token;
|
||||
@@ -174,13 +175,7 @@ final class LiteralExpressionParser extends AbstractExpressionParser implements
|
||||
}
|
||||
$first = false;
|
||||
|
||||
if ($stream->nextIf(Token::SPREAD_TYPE)) {
|
||||
$expr = $parser->parseExpression();
|
||||
$expr->setAttribute('spread', true);
|
||||
$node->addElement($expr);
|
||||
} else {
|
||||
$node->addElement($parser->parseExpression());
|
||||
}
|
||||
$node->addElement($parser->parseExpression());
|
||||
}
|
||||
$stream->expect(Token::PUNCTUATION_TYPE, ']', 'An opened sequence is not properly closed');
|
||||
|
||||
@@ -207,10 +202,9 @@ final class LiteralExpressionParser extends AbstractExpressionParser implements
|
||||
}
|
||||
$first = false;
|
||||
|
||||
if ($stream->nextIf(Token::SPREAD_TYPE)) {
|
||||
$value = $parser->parseExpression();
|
||||
$value->setAttribute('spread', true);
|
||||
$node->addElement($value);
|
||||
if ($stream->test(Token::OPERATOR_TYPE, '...')) {
|
||||
$node->addElement($parser->parseExpression());
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -79,6 +79,7 @@ use Twig\Node\Expression\Test\SameasTest;
|
||||
use Twig\Node\Expression\Unary\NegUnary;
|
||||
use Twig\Node\Expression\Unary\NotUnary;
|
||||
use Twig\Node\Expression\Unary\PosUnary;
|
||||
use Twig\Node\Expression\Unary\SpreadUnary;
|
||||
use Twig\Node\Node;
|
||||
use Twig\Parser;
|
||||
use Twig\Sandbox\SecurityNotAllowedMethodError;
|
||||
@@ -330,6 +331,7 @@ final class CoreExtension extends AbstractExtension
|
||||
return [
|
||||
// unary operators
|
||||
new UnaryOperatorExpressionParser(NotUnary::class, 'not', 50, new PrecedenceChange('twig/twig', '3.15', 70)),
|
||||
new UnaryOperatorExpressionParser(SpreadUnary::class, '...', 512, description: 'Spread operator'),
|
||||
new UnaryOperatorExpressionParser(NegUnary::class, '-', 500),
|
||||
new UnaryOperatorExpressionParser(PosUnary::class, '+', 500),
|
||||
|
||||
|
||||
+1
-6
@@ -334,13 +334,8 @@ class Lexer
|
||||
}
|
||||
}
|
||||
|
||||
// spread operator
|
||||
if ('.' === $this->code[$this->cursor] && ($this->cursor + 2 < $this->end) && '.' === $this->code[$this->cursor + 1] && '.' === $this->code[$this->cursor + 2]) {
|
||||
$this->pushToken(Token::SPREAD_TYPE, '...');
|
||||
$this->moveCursor('...');
|
||||
}
|
||||
// operators
|
||||
elseif (preg_match($this->regexes['operator'], $this->code, $match, 0, $this->cursor)) {
|
||||
if (preg_match($this->regexes['operator'], $this->code, $match, 0, $this->cursor)) {
|
||||
$operator = preg_replace('/\s+/', ' ', $match[0]);
|
||||
if (\in_array($operator, $this->openingBrackets)) {
|
||||
$this->checkBrackets($operator);
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
namespace Twig\Node\Expression;
|
||||
|
||||
use Twig\Compiler;
|
||||
use Twig\Node\Expression\Unary\SpreadUnary;
|
||||
use Twig\Node\Expression\Unary\StringCastUnary;
|
||||
use Twig\Node\Expression\Variable\ContextVariable;
|
||||
|
||||
@@ -68,76 +69,37 @@ class ArrayExpression extends AbstractExpression
|
||||
|
||||
public function compile(Compiler $compiler): void
|
||||
{
|
||||
$keyValuePairs = $this->getKeyValuePairs();
|
||||
$needsArrayMergeSpread = \PHP_VERSION_ID < 80100 && $this->hasSpreadItem($keyValuePairs);
|
||||
|
||||
if ($needsArrayMergeSpread) {
|
||||
$compiler->raw('CoreExtension::merge(');
|
||||
}
|
||||
$compiler->raw('[');
|
||||
$first = true;
|
||||
$reopenAfterMergeSpread = false;
|
||||
$nextIndex = 0;
|
||||
foreach ($keyValuePairs as $pair) {
|
||||
if ($reopenAfterMergeSpread) {
|
||||
$compiler->raw(', [');
|
||||
$reopenAfterMergeSpread = false;
|
||||
}
|
||||
|
||||
if ($needsArrayMergeSpread && $pair['value']->hasAttribute('spread')) {
|
||||
$compiler->raw('], ')->subcompile($pair['value']);
|
||||
$first = true;
|
||||
$reopenAfterMergeSpread = true;
|
||||
continue;
|
||||
}
|
||||
foreach ($this->getKeyValuePairs() as $pair) {
|
||||
if (!$first) {
|
||||
$compiler->raw(', ');
|
||||
}
|
||||
$first = false;
|
||||
|
||||
if ($pair['value']->hasAttribute('spread') && !$needsArrayMergeSpread) {
|
||||
$compiler->raw('...')->subcompile($pair['value']);
|
||||
++$nextIndex;
|
||||
} else {
|
||||
$key = null;
|
||||
if ($pair['key'] instanceof ContextVariable) {
|
||||
$pair['key'] = new StringCastUnary($pair['key'], $pair['key']->getTemplateLine());
|
||||
}
|
||||
if ($pair['key'] instanceof TempNameExpression) {
|
||||
$key = $pair['key']->getAttribute('name');
|
||||
$pair['key'] = new ConstantExpression($key, $pair['key']->getTemplateLine());
|
||||
}
|
||||
if ($pair['key'] instanceof ConstantExpression) {
|
||||
$key = $pair['key']->getAttribute('value');
|
||||
}
|
||||
|
||||
if ($nextIndex !== $key) {
|
||||
$compiler
|
||||
->subcompile($pair['key'])
|
||||
->raw(' => ')
|
||||
;
|
||||
}
|
||||
++$nextIndex;
|
||||
|
||||
$compiler->subcompile($pair['value']);
|
||||
$key = null;
|
||||
if ($pair['key'] instanceof ContextVariable) {
|
||||
$pair['key'] = new StringCastUnary($pair['key'], $pair['key']->getTemplateLine());
|
||||
}
|
||||
}
|
||||
if (!$reopenAfterMergeSpread) {
|
||||
$compiler->raw(']');
|
||||
}
|
||||
if ($needsArrayMergeSpread) {
|
||||
$compiler->raw(')');
|
||||
}
|
||||
}
|
||||
|
||||
private function hasSpreadItem(array $pairs): bool
|
||||
{
|
||||
foreach ($pairs as $pair) {
|
||||
if ($pair['value']->hasAttribute('spread')) {
|
||||
return true;
|
||||
if ($pair['key'] instanceof TempNameExpression) {
|
||||
$key = $pair['key']->getAttribute('name');
|
||||
$pair['key'] = new ConstantExpression($key, $pair['key']->getTemplateLine());
|
||||
}
|
||||
if ($pair['key'] instanceof ConstantExpression) {
|
||||
$key = $pair['key']->getAttribute('value');
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
if ($nextIndex !== $key && !$pair['value'] instanceof SpreadUnary) {
|
||||
$compiler
|
||||
->subcompile($pair['key'])
|
||||
->raw(' => ')
|
||||
;
|
||||
}
|
||||
++$nextIndex;
|
||||
|
||||
$compiler->subcompile($pair['value']);
|
||||
}
|
||||
$compiler->raw(']');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,12 +123,7 @@ final class SandboxNodeVisitor implements NodeVisitorInterface
|
||||
{
|
||||
$expr = $node->getNode($name);
|
||||
if (($expr instanceof ContextVariable || $expr instanceof GetAttrExpression) && !$expr->isGenerator()) {
|
||||
// Simplify in 4.0 as the spread attribute has been removed there
|
||||
$new = new CheckToStringNode($expr);
|
||||
if ($expr->hasAttribute('spread')) {
|
||||
$new->setAttribute('spread', $expr->getAttribute('spread'));
|
||||
}
|
||||
$node->setNode($name, $new);
|
||||
$node->setNode($name, new CheckToStringNode($expr));
|
||||
} elseif ($expr instanceof SpreadUnary) {
|
||||
$this->wrapNode($expr, 'node');
|
||||
} elseif ($expr instanceof ArrayExpression) {
|
||||
|
||||
@@ -34,6 +34,9 @@ final class Token
|
||||
* @deprecated since Twig 3.21, "arrow" is now an operator
|
||||
*/
|
||||
public const ARROW_TYPE = 12;
|
||||
/**
|
||||
* @deprecated since Twig 3.21, "spread" is now an operator
|
||||
*/
|
||||
public const SPREAD_TYPE = 13;
|
||||
|
||||
public function __construct(
|
||||
@@ -44,6 +47,9 @@ final class Token
|
||||
if (self::ARROW_TYPE === $type) {
|
||||
trigger_deprecation('twig/twig', '3.21', 'The "%s" token type is deprecated, "arrow" is now an operator.', self::ARROW_TYPE);
|
||||
}
|
||||
if (self::SPREAD_TYPE === $type) {
|
||||
trigger_deprecation('twig/twig', '3.21', 'The "%s" token type is deprecated, "spread" is now an operator.', self::SPREAD_TYPE);
|
||||
}
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
@@ -74,6 +80,11 @@ final class Token
|
||||
|
||||
return self::OPERATOR_TYPE === $this->type && '=>' === $this->value;
|
||||
}
|
||||
if (self::SPREAD_TYPE === $type) {
|
||||
trigger_deprecation('twig/twig', '3.21', 'The "%s" token type is deprecated, "spread" is now an operator.', self::typeToEnglish(self::SPREAD_TYPE));
|
||||
|
||||
return self::OPERATOR_TYPE === $this->type && '...' === $this->value;
|
||||
}
|
||||
|
||||
$typeMatches = $this->type === $type;
|
||||
if ($typeMatches && self::PUNCTUATION_TYPE === $type && \in_array($this->value, ['(', '[', '|', '.', '?', '?:']) && $values) {
|
||||
|
||||
@@ -27,6 +27,7 @@ use Twig\Node\Expression\FilterExpression;
|
||||
use Twig\Node\Expression\FunctionExpression;
|
||||
use Twig\Node\Expression\TestExpression;
|
||||
use Twig\Node\Expression\Unary\AbstractUnary;
|
||||
use Twig\Node\Expression\Unary\SpreadUnary;
|
||||
use Twig\Node\Expression\Variable\ContextVariable;
|
||||
use Twig\Node\Node;
|
||||
use Twig\Parser;
|
||||
@@ -193,7 +194,7 @@ class ExpressionParserTest extends TestCase
|
||||
new ConstantExpression(2, 1),
|
||||
|
||||
new ConstantExpression(2, 1),
|
||||
self::createContextVariable('foo', ['spread' => true]),
|
||||
new SpreadUnary(new ContextVariable('foo', 1), 1),
|
||||
], 1)],
|
||||
|
||||
// mapping with spread operator
|
||||
@@ -206,7 +207,7 @@ class ExpressionParserTest extends TestCase
|
||||
new ConstantExpression('c', 1),
|
||||
|
||||
new ConstantExpression(0, 1),
|
||||
self::createContextVariable('otherLetters', ['spread' => true]),
|
||||
new SpreadUnary(new ContextVariable('otherLetters', 1), 1),
|
||||
], 1)],
|
||||
];
|
||||
}
|
||||
@@ -591,16 +592,6 @@ class ExpressionParserTest extends TestCase
|
||||
$this->expectNotToPerformAssertions();
|
||||
}
|
||||
|
||||
private static function createContextVariable(string $name, array $attributes): ContextVariable
|
||||
{
|
||||
$expression = new ContextVariable($name, 1);
|
||||
foreach ($attributes as $key => $value) {
|
||||
$expression->setAttribute($key, $value);
|
||||
}
|
||||
|
||||
return $expression;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider getBindingPowerTests
|
||||
*/
|
||||
|
||||
@@ -54,16 +54,6 @@ class LexerTest extends TestCase
|
||||
$this->assertEquals(2, $this->countToken($template, Token::PUNCTUATION_TYPE, '}'));
|
||||
}
|
||||
|
||||
public function testSpreadOperator()
|
||||
{
|
||||
$template = '{{ { a: "a", ...{ b: "b" } } }}';
|
||||
|
||||
$this->assertEquals(1, $this->countToken($template, Token::SPREAD_TYPE, '...'));
|
||||
// sanity check on lexing after spread
|
||||
$this->assertEquals(2, $this->countToken($template, Token::PUNCTUATION_TYPE, '{'));
|
||||
$this->assertEquals(2, $this->countToken($template, Token::PUNCTUATION_TYPE, '}'));
|
||||
}
|
||||
|
||||
protected function countToken($template, $type, $value = null)
|
||||
{
|
||||
$lexer = new Lexer(new Environment(new ArrayLoader()));
|
||||
|
||||
Reference in New Issue
Block a user