Make embeds deterministic

This commit is contained in:
itsalmostchristmas
2026-05-06 14:55:26 +02:00
committed by Fabien Potencier
parent 99d95dd6b8
commit 2b4aa4458a
4 changed files with 40 additions and 5 deletions
+2 -2
View File
@@ -1,6 +1,6 @@
# 3.24.1 (2026-XX-XX)
# 3.25.0 (2026-XX-XX)
* n/a
* Use deterministic suffixes for generated embed classes
# 3.24.0 (2026-03-17)
+16 -1
View File
@@ -794,6 +794,21 @@ operand is contained in the right:
You can use this operator to perform a containment test on strings,
sequences, mappings, or objects implementing the ``Traversable`` interface.
.. note::
For sequences, mappings, and ``Traversable`` objects, ``in`` uses a loose
comparison (similar to ``==``); use :doc:`same as <tests/sameas>` for a
strict comparison. Like PHP's ``in_array()``, this can yield unexpected
results when the left operand is a boolean:
.. code-block:: twig
{# returns true because true == 'foo' under PHP loose comparison #}
{{ true in ['foo', 'bar'] }}
Containment on strings only accepts string, integer, and float operands on
the left; other types always return ``false``.
To perform a negative test, use the ``not in`` operator:
.. code-block:: twig
@@ -1221,7 +1236,7 @@ Twig supports two modifiers:
* *Line whitespace trimming* via the ``~`` modifier: Removes all whitespace
(excluding newlines). Using this modifier on the right disables the default
removal of the first newline inherited from PHP.
removal of the first newline mentioned above.
The modifiers can be used on either side of the tags like in ``{%-`` or ``-%}``
and they consume all whitespace for that side of the tag. It is possible to use
+8 -2
View File
@@ -54,6 +54,7 @@ class Parser
private $importedSymbols;
private $traits;
private $embeddedTemplates = [];
private int $lastEmbedIndex = 0;
private $varNameSalt = 0;
private $ignoreUnknownTwigCallables = false;
private ExpressionParsers $parsers;
@@ -81,8 +82,13 @@ class Parser
*/
public function parse(TokenStream $stream, $test = null, bool $dropNeedle = false): ModuleNode
{
// reset on root parse() calls only, so the counter spans nested/reentrant parses
if (!$this->stack) {
$this->lastEmbedIndex = 0;
}
$vars = get_object_vars($this);
unset($vars['stack'], $vars['env'], $vars['handlers'], $vars['visitors'], $vars['expressionParser'], $vars['reservedMacroNames'], $vars['varNameSalt']);
unset($vars['stack'], $vars['env'], $vars['handlers'], $vars['visitors'], $vars['expressionParser'], $vars['reservedMacroNames'], $vars['lastEmbedIndex'], $vars['varNameSalt']);
$this->stack[] = $vars;
// node visitors
@@ -319,7 +325,7 @@ class Parser
*/
public function embedTemplate(ModuleNode $template)
{
$template->setIndex(mt_rand());
$template->setIndex(++$this->lastEmbedIndex);
$this->embeddedTemplates[] = $template;
}
+14
View File
@@ -206,6 +206,20 @@ EOF, 'index')));
$this->assertTrue($argumentNodes->getNode(3)->getAttribute('value'));
}
public function testEmbeddedTemplatesHaveSequentialIndices(): void
{
$template = new Source('{% embed "first" %}{% endembed %}{% embed "second" %}{% endembed %}', 'index');
$lexer = new Lexer(new Environment(new ArrayLoader()));
$stream = $lexer->tokenize($template);
$embeds = $this->getParser()
->parse($stream)
->getAttribute('embedded_templates');
$this->assertSame(1, $embeds->getNode(0)->getAttribute('index'));
$this->assertSame(2, $embeds->getNode(1)->getAttribute('index'));
}
protected function getParser()
{
$parser = new Parser(new Environment(new ArrayLoader()));