feature #4874 Normalize destructuring assignment targets (fabpot)

This PR was merged into the 3.x branch.

Discussion
----------

Normalize destructuring assignment targets

Commits
-------

6ce5beb7c2 Normalize destructuring assignment targets
This commit is contained in:
Fabien Potencier
2026-07-27 18:57:15 +02:00
5 changed files with 45 additions and 4 deletions
+1
View File
@@ -1,5 +1,6 @@
# 3.29.0 (2026-XX-XX)
* Normalize destructuring variable AST nodes as assignment targets
* Fix `IntlExtension` ignoring explicit date/time formats and the `format_date`/`format_time` filters when a date formatter prototype is configured
* Add a `format_list` filter to `IntlExtension` to format a list of strings using PHP 8.5's `IntlListFormatter`
* Fix array access with a `Stringable` key coercing the key to string for `ArrayAccess` objects that use object keys (such as `SplObjectStorage`)
@@ -19,6 +19,7 @@ use Twig\Node\Expression\Binary\AbstractBinary;
use Twig\Node\Expression\Binary\ObjectDestructuringSetBinary;
use Twig\Node\Expression\Binary\SequenceDestructuringSetBinary;
use Twig\Node\Expression\Binary\SetBinary;
use Twig\Node\Expression\Variable\AssignContextVariable;
use Twig\Node\Expression\Variable\ContextVariable;
use Twig\Parser;
use Twig\Token;
@@ -49,6 +50,12 @@ class AssignmentExpressionParser extends BinaryOperatorExpressionParser
};
if ($left instanceof ArrayExpression) {
foreach ($left->getKeyValuePairs() as $i => $pair) {
if ($pair['value'] instanceof ContextVariable && !$pair['value'] instanceof AssignContextVariable) {
$left->setNode(2 * $i + 1, new AssignContextVariable($pair['value']->getAttribute('name'), $pair['value']->getTemplateLine()));
}
}
if ($left->isSequence()) {
return new SequenceDestructuringSetBinary($left, $right, $token->getLine());
}
@@ -16,7 +16,7 @@ use Twig\Error\SyntaxError;
use Twig\Extension\SandboxExtension;
use Twig\Node\Expression\AbstractExpression;
use Twig\Node\Expression\ArrayExpression;
use Twig\Node\Expression\Variable\ContextVariable;
use Twig\Node\Expression\Variable\AssignContextVariable;
use Twig\Node\Node;
/**
@@ -37,7 +37,7 @@ class ObjectDestructuringSetBinary extends AbstractBinary
throw new \LogicException('Left side must be ArrayExpression for object/mapping destructuring.');
}
foreach ($left->getKeyValuePairs() as $pair) {
if (!$pair['value'] instanceof ContextVariable) {
if (!$pair['value'] instanceof AssignContextVariable) {
throw new SyntaxError(\sprintf('Cannot assign to "%s", only variables can be assigned in object/mapping destructuring.', $pair['value']::class), $lineno);
}
@@ -16,7 +16,7 @@ use Twig\Error\SyntaxError;
use Twig\Node\Expression\AbstractExpression;
use Twig\Node\Expression\ArrayExpression;
use Twig\Node\Expression\EmptyExpression;
use Twig\Node\Expression\Variable\ContextVariable;
use Twig\Node\Expression\Variable\AssignContextVariable;
use Twig\Node\Node;
/**
@@ -35,7 +35,7 @@ class SequenceDestructuringSetBinary extends AbstractBinary
foreach ($left->getKeyValuePairs() as $pair) {
if ($pair['value'] instanceof EmptyExpression) {
$this->variables[] = null;
} elseif ($pair['value'] instanceof ContextVariable) {
} elseif ($pair['value'] instanceof AssignContextVariable) {
$this->variables[] = $pair['value']->getAttribute('name');
} else {
throw new SyntaxError(\sprintf('Cannot assign to "%s", only variables can be assigned in sequence destructuring.', $pair['value']::class), $lineno);
+33
View File
@@ -37,12 +37,16 @@ use Twig\Extension\AbstractExtension;
use Twig\Loader\ArrayLoader;
use Twig\Node\Expression\ArrayExpression;
use Twig\Node\Expression\Binary\ConcatBinary;
use Twig\Node\Expression\Binary\ObjectDestructuringSetBinary;
use Twig\Node\Expression\Binary\SequenceDestructuringSetBinary;
use Twig\Node\Expression\ConstantExpression;
use Twig\Node\Expression\EmptyExpression;
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\AssignContextVariable;
use Twig\Node\Expression\Variable\ContextVariable;
use Twig\Node\Node;
use Twig\Parser;
@@ -249,6 +253,35 @@ class ExpressionParserTest extends TestCase
$env->compileSource(new Source('{{ [1,,2] }}', 'index'));
}
public function testSequenceDestructuringUsesAssignmentTargets(): void
{
$env = new Environment(new ArrayLoader(), ['cache' => false, 'autoescape' => false]);
$parser = new Parser($env);
$node = $parser->parse($env->tokenize(new Source('{{ ([first, , third] = values) }}', 'index')))->getNode('body')->getNode('0')->getNode('expr');
$this->assertInstanceOf(SequenceDestructuringSetBinary::class, $node);
$pairs = $node->getNode('left')->getKeyValuePairs();
$this->assertSame(AssignContextVariable::class, $pairs[0]['value']::class);
$this->assertSame('first', $pairs[0]['value']->getAttribute('name'));
$this->assertSame(EmptyExpression::class, $pairs[1]['value']::class);
$this->assertSame(AssignContextVariable::class, $pairs[2]['value']::class);
$this->assertSame('third', $pairs[2]['value']->getAttribute('name'));
}
public function testObjectDestructuringUsesAssignmentTargets(): void
{
$env = new Environment(new ArrayLoader(), ['cache' => false, 'autoescape' => false]);
$parser = new Parser($env);
$node = $parser->parse($env->tokenize(new Source('{{ ({name: user_name} = user) }}', 'index')))->getNode('body')->getNode('0')->getNode('expr');
$this->assertInstanceOf(ObjectDestructuringSetBinary::class, $node);
$pair = $node->getNode('left')->getKeyValuePairs()[0];
$this->assertSame(ConstantExpression::class, $pair['key']::class);
$this->assertSame('name', $pair['key']->getAttribute('value'));
$this->assertSame(AssignContextVariable::class, $pair['value']::class);
$this->assertSame('user_name', $pair['value']->getAttribute('name'));
}
/**
* @dataProvider getTestsForString
*/