Add a proper prefix spread operator

This commit is contained in:
Fabien Potencier
2025-02-14 10:34:17 +01:00
parent 2f3150579f
commit 3964aeba78
10 changed files with 51 additions and 111 deletions
+4 -2
View File
@@ -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;
}
+2
View File
@@ -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
View File
@@ -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);
+22 -60
View File
@@ -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(']');
}
}
+1 -6
View File
@@ -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) {
+11
View File
@@ -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) {
+3 -12
View File
@@ -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
*/
-10
View File
@@ -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()));