diff --git a/doc/operators_precedence.rst b/doc/operators_precedence.rst index e681d752d..6791e8b25 100644 --- a/doc/operators_precedence.rst +++ b/doc/operators_precedence.rst @@ -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 diff --git a/src/ExpressionParser/Infix/ArgumentsTrait.php b/src/ExpressionParser/Infix/ArgumentsTrait.php index 185ec51a0..1c2ae49dd 100644 --- a/src/ExpressionParser/Infix/ArgumentsTrait.php +++ b/src/ExpressionParser/Infix/ArgumentsTrait.php @@ -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; diff --git a/src/ExpressionParser/Prefix/LiteralExpressionParser.php b/src/ExpressionParser/Prefix/LiteralExpressionParser.php index 67bae6c32..188b92445 100644 --- a/src/ExpressionParser/Prefix/LiteralExpressionParser.php +++ b/src/ExpressionParser/Prefix/LiteralExpressionParser.php @@ -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; } diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index 89bc2cf6f..039a36f43 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -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), diff --git a/src/Lexer.php b/src/Lexer.php index c9f2f0adc..34715f5ea 100644 --- a/src/Lexer.php +++ b/src/Lexer.php @@ -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); diff --git a/src/Node/Expression/ArrayExpression.php b/src/Node/Expression/ArrayExpression.php index 61a5063f3..c9b3a3ec1 100644 --- a/src/Node/Expression/ArrayExpression.php +++ b/src/Node/Expression/ArrayExpression.php @@ -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(']'); } } diff --git a/src/NodeVisitor/SandboxNodeVisitor.php b/src/NodeVisitor/SandboxNodeVisitor.php index 7e89ef83a..9dd48f5be 100644 --- a/src/NodeVisitor/SandboxNodeVisitor.php +++ b/src/NodeVisitor/SandboxNodeVisitor.php @@ -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) { diff --git a/src/Token.php b/src/Token.php index 0d8b385eb..73fd02e1d 100644 --- a/src/Token.php +++ b/src/Token.php @@ -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) { diff --git a/tests/ExpressionParserTest.php b/tests/ExpressionParserTest.php index bed218cc0..f13405292 100644 --- a/tests/ExpressionParserTest.php +++ b/tests/ExpressionParserTest.php @@ -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 */ diff --git a/tests/LexerTest.php b/tests/LexerTest.php index 806b65591..3a5ff988f 100644 --- a/tests/LexerTest.php +++ b/tests/LexerTest.php @@ -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()));