mirror of
https://github.com/twigphp/Twig.git
synced 2026-10-04 02:47:14 +00:00
Reject the spread operator outside sequences, mappings, and call arguments
This commit is contained in:
@@ -5,6 +5,7 @@
|
||||
* Fix the non-selected branch of a `guard` tag registering its parent, blocks, and macros
|
||||
* Speed up reading object attributes backed by getters or class constants
|
||||
* Fix the sandbox not reporting the line of a rejected `guard` tag
|
||||
* Fix the spread operator compiling to invalid PHP outside sequences, mappings, and call arguments
|
||||
|
||||
# 3.30.0 (2026-09-25)
|
||||
|
||||
|
||||
@@ -52,11 +52,14 @@ trait ArgumentsTrait
|
||||
}
|
||||
}
|
||||
|
||||
$value = $parser->parseExpression();
|
||||
if ($value instanceof SpreadUnary) {
|
||||
if ($token = $stream->nextIf(Token::OPERATOR_TYPE, '...')) {
|
||||
$value = new SpreadUnary($parser->parseExpression(), $token->getLine());
|
||||
$hasSpread = true;
|
||||
} elseif ($hasSpread) {
|
||||
throw new SyntaxError('Normal arguments must be placed before argument unpacking.', $stream->getCurrent()->getLine(), $stream->getSourceContext());
|
||||
} else {
|
||||
$value = $parser->parseExpression();
|
||||
if ($hasSpread) {
|
||||
throw new SyntaxError('Normal arguments must be placed before argument unpacking.', $stream->getCurrent()->getLine(), $stream->getSourceContext());
|
||||
}
|
||||
}
|
||||
|
||||
$name = null;
|
||||
|
||||
@@ -21,6 +21,7 @@ use Twig\Node\Expression\ArrayExpression;
|
||||
use Twig\Node\Expression\Binary\ConcatBinary;
|
||||
use Twig\Node\Expression\ConstantExpression;
|
||||
use Twig\Node\Expression\EmptyExpression;
|
||||
use Twig\Node\Expression\Unary\SpreadUnary;
|
||||
use Twig\Node\Expression\Variable\ContextVariable;
|
||||
use Twig\Parser;
|
||||
use Twig\Token;
|
||||
@@ -163,6 +164,8 @@ final class LiteralExpressionParser extends AbstractExpressionParser implements
|
||||
// Check for empty slots (comma with no expression)
|
||||
if ($stream->test(Token::PUNCTUATION_TYPE, ',')) {
|
||||
$node->addElement(new EmptyExpression($stream->getCurrent()->getLine()));
|
||||
} elseif ($token = $stream->nextIf(Token::OPERATOR_TYPE, '...')) {
|
||||
$node->addElement(new SpreadUnary($parser->parseExpression(), $token->getLine()));
|
||||
} else {
|
||||
$node->addElement($parser->parseExpression());
|
||||
}
|
||||
@@ -190,8 +193,8 @@ final class LiteralExpressionParser extends AbstractExpressionParser implements
|
||||
}
|
||||
$first = false;
|
||||
|
||||
if ($stream->test(Token::OPERATOR_TYPE, '...')) {
|
||||
$node->addElement($parser->parseExpression());
|
||||
if ($token = $stream->nextIf(Token::OPERATOR_TYPE, '...')) {
|
||||
$node->addElement(new SpreadUnary($parser->parseExpression(), $token->getLine()));
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
<?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\ExpressionParser\Prefix;
|
||||
|
||||
use Twig\Error\SyntaxError;
|
||||
use Twig\ExpressionParser\AbstractExpressionParser;
|
||||
use Twig\ExpressionParser\ExpressionParserDescriptionInterface;
|
||||
use Twig\ExpressionParser\PrefixExpressionParserInterface;
|
||||
use Twig\Node\Expression\AbstractExpression;
|
||||
use Twig\Parser;
|
||||
use Twig\Token;
|
||||
|
||||
/**
|
||||
* Sequences, mappings, and call arguments parse the spread operator themselves; it is invalid anywhere else.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final class SpreadExpressionParser extends AbstractExpressionParser implements PrefixExpressionParserInterface, ExpressionParserDescriptionInterface
|
||||
{
|
||||
public function parse(Parser $parser, Token $token): AbstractExpression
|
||||
{
|
||||
throw new SyntaxError('The spread operator can only be used on sequence elements, mapping elements, and call arguments.', $token->getLine(), $parser->getStream()->getSourceContext());
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return '...';
|
||||
}
|
||||
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Spread operator';
|
||||
}
|
||||
|
||||
public function getPrecedence(): int
|
||||
{
|
||||
return 512;
|
||||
}
|
||||
}
|
||||
@@ -30,6 +30,7 @@ use Twig\ExpressionParser\InfixAssociativity;
|
||||
use Twig\ExpressionParser\PrecedenceChange;
|
||||
use Twig\ExpressionParser\Prefix\GroupingExpressionParser;
|
||||
use Twig\ExpressionParser\Prefix\LiteralExpressionParser;
|
||||
use Twig\ExpressionParser\Prefix\SpreadExpressionParser;
|
||||
use Twig\ExpressionParser\Prefix\UnaryOperatorExpressionParser;
|
||||
use Twig\MacroNamespace;
|
||||
use Twig\Markup;
|
||||
@@ -84,7 +85,6 @@ use Twig\Node\Expression\Test\TrueTest;
|
||||
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\NodeVisitor\CorrectnessNodeVisitor;
|
||||
use Twig\Parser;
|
||||
@@ -341,7 +341,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', operandPrecedence: 0),
|
||||
new SpreadExpressionParser(),
|
||||
new UnaryOperatorExpressionParser(NegUnary::class, '-', 500),
|
||||
new UnaryOperatorExpressionParser(PosUnary::class, '+', 500),
|
||||
|
||||
|
||||
@@ -126,6 +126,41 @@ class ExpressionParserTest extends TestCase
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider getMisplacedSpreadTests
|
||||
*/
|
||||
#[DataProvider('getMisplacedSpreadTests')]
|
||||
public function testMisplacedSpreadIsASyntaxError(string $template): void
|
||||
{
|
||||
$env = new Environment(new ArrayLoader(), ['cache' => false, 'autoescape' => false]);
|
||||
$parser = new Parser($env);
|
||||
|
||||
$this->expectException(SyntaxError::class);
|
||||
$this->expectExceptionMessage('The spread operator can only be used on sequence elements, mapping elements, and call arguments in "index" at line 1.');
|
||||
$parser->parse($env->tokenize(new Source($template, 'index')));
|
||||
}
|
||||
|
||||
public static function getMisplacedSpreadTests()
|
||||
{
|
||||
return [
|
||||
['{{ ...foo }}'],
|
||||
['{{ (...foo) }}'],
|
||||
['{{ -...foo }}'],
|
||||
['{{ 1 + ...foo }}'],
|
||||
['{% set bar = ...foo %}'],
|
||||
['{% do ...foo %}'],
|
||||
['{% with ...foo %}{% endwith %}'],
|
||||
['{{ (bar => ...foo)(1) }}'],
|
||||
['{{ [(...foo)] }}'],
|
||||
['{{ [1, -...foo] }}'],
|
||||
['{{ {a: ...foo} }}'],
|
||||
['{{ {(...foo): 1} }}'],
|
||||
['{{ bar(-...foo) }}'],
|
||||
['{{ bar(a: ...foo) }}'],
|
||||
['{{ bar(a = ...foo) }}'],
|
||||
];
|
||||
}
|
||||
|
||||
public static function getTestsForSequence()
|
||||
{
|
||||
return [
|
||||
|
||||
Reference in New Issue
Block a user