From 009bb4fd82dbb4766c095e65de769457af0adaec Mon Sep 17 00:00:00 2001 From: "hubert.lenoir" Date: Thu, 24 Mar 2022 13:50:00 +0100 Subject: [PATCH 001/812] Add "some" and "every" filters --- src/ExpressionParser.php | 2 +- src/Extension/CoreExtension.php | 30 +++++++++++++++++ src/Node/Expression/Binary/HasEveryBinary.php | 33 +++++++++++++++++++ src/Node/Expression/Binary/HasSomeBinary.php | 33 +++++++++++++++++++ tests/Fixtures/expressions/has_every.test | 19 +++++++++++ tests/Fixtures/expressions/has_some.test | 19 +++++++++++ 6 files changed, 135 insertions(+), 1 deletion(-) create mode 100644 src/Node/Expression/Binary/HasEveryBinary.php create mode 100644 src/Node/Expression/Binary/HasSomeBinary.php create mode 100644 tests/Fixtures/expressions/has_every.test create mode 100644 tests/Fixtures/expressions/has_some.test diff --git a/src/ExpressionParser.php b/src/ExpressionParser.php index 66acddf6165..a2c258591c6 100644 --- a/src/ExpressionParser.php +++ b/src/ExpressionParser.php @@ -80,7 +80,7 @@ public function parseExpression($precedence = 0, $allowArrow = false) } elseif (isset($op['callable'])) { $expr = $op['callable']($this->parser, $expr); } else { - $expr1 = $this->parseExpression(self::OPERATOR_LEFT === $op['associativity'] ? $op['precedence'] + 1 : $op['precedence']); + $expr1 = $this->parseExpression(self::OPERATOR_LEFT === $op['associativity'] ? $op['precedence'] + 1 : $op['precedence'], true); $class = $op['class']; $expr = new $class($expr, $expr1, $token->getLine()); } diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index 88cd7545842..4eed76b7e3a 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -23,6 +23,8 @@ use Twig\Node\Expression\Binary\FloorDivBinary; use Twig\Node\Expression\Binary\GreaterBinary; use Twig\Node\Expression\Binary\GreaterEqualBinary; +use Twig\Node\Expression\Binary\HasEveryBinary; +use Twig\Node\Expression\Binary\HasSomeBinary; use Twig\Node\Expression\Binary\InBinary; use Twig\Node\Expression\Binary\LessBinary; use Twig\Node\Expression\Binary\LessEqualBinary; @@ -284,6 +286,8 @@ public function getOperators(): array 'matches' => ['precedence' => 20, 'class' => MatchesBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT], 'starts with' => ['precedence' => 20, 'class' => StartsWithBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT], 'ends with' => ['precedence' => 20, 'class' => EndsWithBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT], + 'has some' => ['precedence' => 20, 'class' => HasSomeBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT], + 'has every' => ['precedence' => 20, 'class' => HasEveryBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT], '..' => ['precedence' => 25, 'class' => RangeBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT], '+' => ['precedence' => 30, 'class' => AddBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT], '-' => ['precedence' => 30, 'class' => SubBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT], @@ -1678,6 +1682,32 @@ function twig_array_reduce(Environment $env, $array, $arrow, $initial = null) return array_reduce($array, $arrow, $initial); } +function twig_array_some(Environment $env, $array, $arrow) +{ + twig_check_arrow_in_sandbox($env, $arrow, 'some', 'filter'); + + foreach ($array as $k => $v) { + if ($arrow($v, $k)) { + return true; + } + } + + return false; +} + +function twig_array_every(Environment $env, $array, $arrow) +{ + twig_check_arrow_in_sandbox($env, $arrow, 'every', 'filter'); + + foreach ($array as $k => $v) { + if (!$arrow($v, $k)) { + return false; + } + } + + return true; +} + function twig_check_arrow_in_sandbox(Environment $env, $arrow, $thing, $type) { if (!$arrow instanceof Closure && $env->hasExtension('\Twig\Extension\SandboxExtension') && $env->getExtension('\Twig\Extension\SandboxExtension')->isSandboxed()) { diff --git a/src/Node/Expression/Binary/HasEveryBinary.php b/src/Node/Expression/Binary/HasEveryBinary.php new file mode 100644 index 00000000000..adfabd44c7f --- /dev/null +++ b/src/Node/Expression/Binary/HasEveryBinary.php @@ -0,0 +1,33 @@ +raw('twig_array_every($this->env, ') + ->subcompile($this->getNode('left')) + ->raw(', ') + ->subcompile($this->getNode('right')) + ->raw(')') + ; + } + + public function operator(Compiler $compiler): Compiler + { + return $compiler->raw(''); + } +} diff --git a/src/Node/Expression/Binary/HasSomeBinary.php b/src/Node/Expression/Binary/HasSomeBinary.php new file mode 100644 index 00000000000..270da369275 --- /dev/null +++ b/src/Node/Expression/Binary/HasSomeBinary.php @@ -0,0 +1,33 @@ +raw('twig_array_some($this->env, ') + ->subcompile($this->getNode('left')) + ->raw(', ') + ->subcompile($this->getNode('right')) + ->raw(')') + ; + } + + public function operator(Compiler $compiler): Compiler + { + return $compiler->raw(''); + } +} diff --git a/tests/Fixtures/expressions/has_every.test b/tests/Fixtures/expressions/has_every.test new file mode 100644 index 00000000000..dc43b95ca45 --- /dev/null +++ b/tests/Fixtures/expressions/has_every.test @@ -0,0 +1,19 @@ +--TEST-- +Twig supports the "has every" operator +--TEMPLATE-- +{% if [0, 2, 4] has every v => 0 == v % 2 %}Every{% else %}Not every{% endif %} items are even in array +{{ ([0, 2, 4] has every v => 0 == v % 2) ? 'Every' : 'Not every' }} items are even in array +{{ ({ a: 0, b: 2, c: 4 } has every v => 0 == v % 2) ? 'Every' : 'Not every' }} items are even in object +{{ ({ a: 0, b: 2, c: 4 } has every (v, k) => "d" > k)? 'Every' : 'Not every' }} keys are before "d" in object +{{ (it has every v => 0 == v % 2) ? 'Every' : 'Not every' }} items are even in iterator +{{ ([0, 1, 2] has every v => 0 == v % 2) ? 'Every' : 'Not every' }} items are even in array +--DATA-- +return ['it' => new \ArrayIterator([0, 2, 4])] +--EXPECT-- +Every items are even in array +Every items are even in array +Every items are even in object +Every keys are before "d" in object +Every items are even in iterator +Not every items are even in array + diff --git a/tests/Fixtures/expressions/has_some.test b/tests/Fixtures/expressions/has_some.test new file mode 100644 index 00000000000..c5d75c77d1e --- /dev/null +++ b/tests/Fixtures/expressions/has_some.test @@ -0,0 +1,19 @@ +--TEST-- +Twig supports the "has some" operator +--TEMPLATE-- +{% if [1, 2, 3] has some v => 0 == v % 2 %}At least one{% else %}No{% endif %} item is even in array +{{ ([1, 2, 3] has some v => 0 == v % 2) ? 'At least one' : 'No' }} item is even in array +{{ ({ a: 1, b: 2, c: 3 } has some v => 0 == v % 2) ? 'At least one' : 'No' }} item is even in object +{{ ({ a: 1, b: 2, c: 3 } has some (v, k) => "b" == k)? 'At least one' : 'No' }} key is "b" in object +{{ (it has some v => 0 == v % 2) ? 'At least one' : 'No' }} item is even in iterator +{{ ([1, 3, 5] has some v => 0 == v % 2) ? 'At least one' : 'No' }} item is even in array +--DATA-- +return ['it' => new \ArrayIterator([1, 2, 3])] +--EXPECT-- +At least one item is even in array +At least one item is even in array +At least one item is even in object +At least one key is "b" in object +At least one item is even in iterator +No item is even in array + From 1cf610bd2aecc5681a8e0ba53ebf397c0759dbb6 Mon Sep 17 00:00:00 2001 From: Mark Huot Date: Sat, 30 Apr 2022 04:49:45 -0400 Subject: [PATCH 002/812] Add @codeCoverageIgnore to untestable compiled methods --- src/Node/ModuleNode.php | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/Node/ModuleNode.php b/src/Node/ModuleNode.php index e972b6ba582..0f4df35410f 100644 --- a/src/Node/ModuleNode.php +++ b/src/Node/ModuleNode.php @@ -355,6 +355,9 @@ protected function compileMacros(Compiler $compiler) protected function compileGetTemplateName(Compiler $compiler) { $compiler + ->write("/**") + ->write(" * @codeCoverageIgnore") + ->write(" */") ->write("public function getTemplateName()\n", "{\n") ->indent() ->write('return ') @@ -409,6 +412,9 @@ protected function compileIsTraitable(Compiler $compiler) } $compiler + ->write("/**") + ->write(" * @codeCoverageIgnore") + ->write(" */") ->write("public function isTraitable()\n", "{\n") ->indent() ->write(sprintf("return %s;\n", $traitable ? 'true' : 'false')) @@ -420,6 +426,9 @@ protected function compileIsTraitable(Compiler $compiler) protected function compileDebugInfo(Compiler $compiler) { $compiler + ->write("/**") + ->write(" * @codeCoverageIgnore") + ->write(" */") ->write("public function getDebugInfo()\n", "{\n") ->indent() ->write(sprintf("return %s;\n", str_replace("\n", '', var_export(array_reverse($compiler->getDebugInfo(), true), true)))) From 6448ca061709be87e11bc25e96e3eb83bb0e6b1e Mon Sep 17 00:00:00 2001 From: naveen <172697+naveensrinivasan@users.noreply.github.com> Date: Mon, 27 Jun 2022 01:30:05 +0000 Subject: [PATCH 003/812] chore: Included githubactions in the dependabot config This should help with keeping the GitHub actions updated on new releases. This will also help with keeping it secure. Dependabot helps in keeping the supply chain secure https://docs.github.com/en/code-security/dependabot GitHub actions up to date https://docs.github.com/en/code-security/dependabot/working-with-dependabot/keeping-your-actions-up-to-date-with-dependabot https://github.com/ossf/scorecard/blob/main/docs/checks.md#dependency-update-tool Signed-off-by: naveen <172697+naveensrinivasan@users.noreply.github.com> --- .github/dependabot.yml | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000000..5ace4600a1f --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,6 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" From 6f206296224748d9fa35f92a99778e5720271949 Mon Sep 17 00:00:00 2001 From: Doeke Norg Date: Tue, 23 Aug 2022 21:10:06 +0200 Subject: [PATCH 004/812] Added configuration for commonmark use in twig-extra-bundle. --- .../DependencyInjection/Configuration.php | 58 +++++++++++++++++++ .../TwigExtraExtension.php | 6 ++ .../LeagueCommonMarkConverterFactory.php | 7 ++- .../TwigExtraExtensionTest.php | 24 +++++++- 4 files changed, 92 insertions(+), 3 deletions(-) diff --git a/extra/twig-extra-bundle/DependencyInjection/Configuration.php b/extra/twig-extra-bundle/DependencyInjection/Configuration.php index 447e6ac76fa..f0f119e463d 100644 --- a/extra/twig-extra-bundle/DependencyInjection/Configuration.php +++ b/extra/twig-extra-bundle/DependencyInjection/Configuration.php @@ -11,6 +11,7 @@ namespace Twig\Extra\TwigExtraBundle\DependencyInjection; +use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition; use Symfony\Component\Config\Definition\Builder\TreeBuilder; use Symfony\Component\Config\Definition\ConfigurationInterface; use Twig\Extra\TwigExtraBundle\Extensions; @@ -35,6 +36,63 @@ public function getConfigTreeBuilder() ; } + $this->addCommonMarkConfiguration($rootNode); + return $treeBuilder; } + + /** + * Full configuration from {@link https://commonmark.thephpleague.com/2.3/configuration}. + */ + private function addCommonMarkConfiguration(ArrayNodeDefinition $rootNode): void + { + $rootNode + ->children() + ->arrayNode('commonmark') + ->ignoreExtraKeys() + ->children() + ->arrayNode('renderer') + ->info('Array of options for rendering HTML.') + ->children() + ->scalarNode('block_separator')->end() + ->scalarNode('inner_separator')->end() + ->scalarNode('soft_break')->end() + ->end() + ->end() + ->enumNode('html_input') + ->info('How to handle HTML input.') + ->values(['strip','allow','escape']) + ->end() + ->booleanNode('allow_unsafe_links') + ->info('Remove risky link and image URLs by setting this to false.') + ->defaultTrue() + ->end() + ->integerNode('max_nesting_level') + ->info('The maximum nesting level for blocks.') + ->defaultValue(PHP_INT_MAX) + ->end() + ->arrayNode('slug_normalizer') + ->info('Array of options for configuring how URL-safe slugs are created.') + ->children() + ->variableNode('instance')->end() + ->integerNode('max_length')->defaultValue(255)->end() + ->variableNode('unique')->end() + ->end() + ->end() + ->arrayNode('commonmark') + ->info('Array of options for configuring the CommonMark core extension.') + ->children() + ->booleanNode('enable_em')->defaultTrue()->end() + ->booleanNode('enable_strong')->defaultTrue()->end() + ->booleanNode('use_asterisk')->defaultTrue()->end() + ->booleanNode('use_underscore')->defaultTrue()->end() + ->arrayNode('unordered_list_markers') + ->scalarPrototype()->end() + ->defaultValue([['-', '*', '+']])->end() + ->end() + ->end() + ->end() + ->end() + ->end(); + } } diff --git a/extra/twig-extra-bundle/DependencyInjection/TwigExtraExtension.php b/extra/twig-extra-bundle/DependencyInjection/TwigExtraExtension.php index 0f57d71f36a..eb4ee062430 100644 --- a/extra/twig-extra-bundle/DependencyInjection/TwigExtraExtension.php +++ b/extra/twig-extra-bundle/DependencyInjection/TwigExtraExtension.php @@ -39,6 +39,12 @@ public function load(array $configs, ContainerBuilder $container) if ('markdown' === $extension && \class_exists(CommonMarkConverter::class)) { $loader->load('markdown_league.php'); + + if ($container->hasDefinition('twig.markdown.league_common_mark_converter_factory')) { + $container + ->getDefinition('twig.markdown.league_common_mark_converter_factory') + ->setArgument('$config', $config['commonmark'] ?? []); + } } } } diff --git a/extra/twig-extra-bundle/LeagueCommonMarkConverterFactory.php b/extra/twig-extra-bundle/LeagueCommonMarkConverterFactory.php index a2b90a25f58..834ac9983a2 100644 --- a/extra/twig-extra-bundle/LeagueCommonMarkConverterFactory.php +++ b/extra/twig-extra-bundle/LeagueCommonMarkConverterFactory.php @@ -21,17 +21,20 @@ final class LeagueCommonMarkConverterFactory { private $extensions; + private $config; + /** * @param ExtensionInterface[] $extensions */ - public function __construct(iterable $extensions) + public function __construct(iterable $extensions, array $config = []) { $this->extensions = $extensions; + $this->config = $config; } public function __invoke(): CommonMarkConverter { - $converter = new CommonMarkConverter(); + $converter = new CommonMarkConverter($this->config); foreach ($this->extensions as $extension) { $converter->getEnvironment()->addExtension($extension); diff --git a/extra/twig-extra-bundle/Tests/DependencyInjection/TwigExtraExtensionTest.php b/extra/twig-extra-bundle/Tests/DependencyInjection/TwigExtraExtensionTest.php index 355b794d2b1..b17c040a7e6 100644 --- a/extra/twig-extra-bundle/Tests/DependencyInjection/TwigExtraExtensionTest.php +++ b/extra/twig-extra-bundle/Tests/DependencyInjection/TwigExtraExtensionTest.php @@ -26,7 +26,29 @@ public function testDefaultConfiguration() 'kernel.debug' => false, ])); $container->registerExtension(new TwigExtraExtension()); - $container->loadFromExtension('twig_extra'); + $container->loadFromExtension('twig_extra', [ + 'commonmark' => [ + 'extra_key' => true, + 'renderer' => [ + 'block_separator' => "\n", + 'inner_separator' => "\n", + 'soft_break' => "\n", + ], + 'commonmark' => [ + 'enable_em' => true, + 'enable_strong' => true, + 'use_asterisk' => true, + 'use_underscore' => true, + 'unordered_list_markers' => ['-', '*', '+'], + ], + 'html_input' => 'escape', + 'allow_unsafe_links' => false, + 'max_nesting_level' => PHP_INT_MAX, + 'slug_normalizer' => [ + 'max_length' => 255, + ], + ], + ]); $container->getCompilerPassConfig()->setOptimizationPasses([]); $container->getCompilerPassConfig()->setRemovingPasses([]); $container->getCompilerPassConfig()->setAfterRemovingPasses([]); From 849d6832b38dc84a0440ebf41fd34c2b451da157 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Anne?= Date: Wed, 27 Jul 2022 16:02:49 +0200 Subject: [PATCH 005/812] Update .gitattributes --- .gitattributes | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.gitattributes b/.gitattributes index 06bc3671340..86b9ef413df 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,4 +1,9 @@ +/.github/ export-ignore /doc/ export-ignore /extra/ export-ignore /tests/ export-ignore +/.editorconfig export-ignore +/.gitattributes export-ignore +/.gitignore export-ignore +/.php-cs-fixer.dist.php export-ignore /phpunit.xml.dist export-ignore From 4904f38e32c0ecb6bed33ad1ba45b7a121ac2faf Mon Sep 17 00:00:00 2001 From: Joshua Francis <61574468+Francoscopic@users.noreply.github.com> Date: Thu, 13 Oct 2022 21:15:06 +0100 Subject: [PATCH 006/812] Update templates.rst --- doc/templates.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/templates.rst b/doc/templates.rst index 121118f2e91..83f6b7d6c98 100644 --- a/doc/templates.rst +++ b/doc/templates.rst @@ -441,7 +441,7 @@ Escaping works by using the :doc:`escape` or ``e`` filter: {{ user.username|e }} By default, the ``escape`` filter uses the ``html`` strategy, but depending on -the escaping context, you might want to explicitly use an other strategy: +the escaping context, you might want to explicitly use another strategy: .. code-block:: twig From 406b3e5969752cfd01da58d61b7e28d62902ede6 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Mon, 12 Dec 2022 16:33:12 +0100 Subject: [PATCH 007/812] Fix optimizing closures callbacks --- src/Node/Expression/CallExpression.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Node/Expression/CallExpression.php b/src/Node/Expression/CallExpression.php index 79e9defdf0f..aeb38c42f89 100644 --- a/src/Node/Expression/CallExpression.php +++ b/src/Node/Expression/CallExpression.php @@ -304,7 +304,9 @@ private function reflectCallable($callable) if ($object = $r->getClosureThis()) { $callable = [$object, $r->name]; $callableName = (\function_exists('get_debug_type') ? get_debug_type($object) : \get_class($object)).'::'.$r->name; - } elseif ($class = $r->getClosureScopeClass()) { + } elseif (\PHP_VERSION_ID >= 80111 && $class = $r->getClosureCalledClass()) { + $callableName = $class->name.'::'.$r->name; + } elseif (\PHP_VERSION_ID < 80111 && $class = $r->getClosureScopeClass()) { $callableName = (\is_array($callable) ? $callable[0] : $class->name).'::'.$r->name; } else { $callable = $callableName = $r->name; From 883fb333b3a1adf643dbd389b8d8b8197c8353b7 Mon Sep 17 00:00:00 2001 From: Christophe Coevoet Date: Thu, 15 Dec 2022 10:44:37 +0100 Subject: [PATCH 008/812] Add missing argument for the cycle function --- doc/functions/cycle.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/functions/cycle.rst b/doc/functions/cycle.rst index 84cff6a1d5a..0aef3187078 100644 --- a/doc/functions/cycle.rst +++ b/doc/functions/cycle.rst @@ -25,4 +25,5 @@ The array can contain any number of values: Arguments --------- +* ``values``: The list of values to cycle on * ``position``: The cycle position From f3b8c01262f24088ba4d1eca987545647e636306 Mon Sep 17 00:00:00 2001 From: Christophe Coevoet Date: Thu, 15 Dec 2022 11:35:27 +0100 Subject: [PATCH 009/812] Fix the drupal testing script --- tests/drupal_test.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/drupal_test.sh b/tests/drupal_test.sh index a25d886f8fd..7141f3181fd 100644 --- a/tests/drupal_test.sh +++ b/tests/drupal_test.sh @@ -15,7 +15,7 @@ source output #echo '$config["system.logging"]["error_level"] = "verbose";' >> web/sites/default/settings.php wget https://get.symfony.com/cli/installer -O - | bash -export PATH="$HOME/.symfony/bin:$PATH" +export PATH="$HOME/.symfony5/bin:$PATH" symfony server:start -d --no-tls curl -OLsS https://get.blackfire.io/blackfire-player.phar From 1e3b126cf279b13848eb3fef8f2c21ac3d2b1d6e Mon Sep 17 00:00:00 2001 From: Katharina Luthe <120368052+kathi-at-datrycs@users.noreply.github.com> Date: Thu, 15 Dec 2022 08:58:22 +0100 Subject: [PATCH 010/812] Add output --- doc/functions/cycle.rst | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/doc/functions/cycle.rst b/doc/functions/cycle.rst index 0aef3187078..46f8f21cab9 100644 --- a/doc/functions/cycle.rst +++ b/doc/functions/cycle.rst @@ -11,6 +11,17 @@ The ``cycle`` function cycles on an array of values: {% for year in start_year..end_year %} {{ cycle(['odd', 'even'], loop.index0) }} {% endfor %} + + {# outputs + + odd + even + odd + even + odd + even + + #} The array can contain any number of values: @@ -21,6 +32,22 @@ The array can contain any number of values: {% for i in 0..10 %} {{ cycle(fruits, i) }} {% endfor %} + + {# outputs + + apple + orange + citrus + apple + orange + citrus + apple + orange + citrus + apple + orange + + #} Arguments --------- From ef7a5ef2e39e4299a64218ccb7623aa6bd9194e4 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Mon, 26 Dec 2022 12:42:02 +0100 Subject: [PATCH 011/812] Remove a test that was a regression test for the C extension --- tests/Fixtures/regression/issue_1143.test | 23 ----------------------- 1 file changed, 23 deletions(-) delete mode 100644 tests/Fixtures/regression/issue_1143.test diff --git a/tests/Fixtures/regression/issue_1143.test b/tests/Fixtures/regression/issue_1143.test deleted file mode 100644 index e2ab950e183..00000000000 --- a/tests/Fixtures/regression/issue_1143.test +++ /dev/null @@ -1,23 +0,0 @@ ---TEST-- -error in twig extension ---TEMPLATE-- -{{ object.region is not null ? object.regionChoices[object.region] }} ---DATA-- -class House -{ - const REGION_S = 1; - const REGION_P = 2; - - public static $regionChoices = [self::REGION_S => 'house.region.s', self::REGION_P => 'house.region.p']; - - public function getRegionChoices() - { - return self::$regionChoices; - } -} - -$object = new House(); -$object->region = 1; -return ['object' => $object] ---EXPECT-- -house.region.s From 7f9cad9972f0ae562760661517745ca0b5e9c3fd Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Mon, 26 Dec 2022 12:43:08 +0100 Subject: [PATCH 012/812] Add PHP 8.2 to the tests --- .github/workflows/ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 88aebc164d7..78e8482af0d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,6 +29,7 @@ jobs: - '7.4' - '8.0' - '8.1' + - '8.2' experimental: [false] steps: @@ -74,6 +75,7 @@ jobs: - '7.4' - '8.0' - '8.1' + - '8.2' extension: - 'extra/cssinliner-extra' - 'extra/html-extra' From e428a0b7015e04eeeddb2f4d8872f4f7bf308e37 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Mon, 26 Dec 2022 12:43:42 +0100 Subject: [PATCH 013/812] Use checkout actions v3 --- .github/workflows/ci.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 78e8482af0d..3f49259ca9a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,7 +34,7 @@ jobs: steps: - name: "Checkout code" - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: "Install PHP with extensions" uses: shivammathur/setup-php@v2 @@ -88,7 +88,7 @@ jobs: steps: - name: "Checkout code" - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: "Install PHP with extensions" uses: shivammathur/setup-php@v2 @@ -133,7 +133,7 @@ jobs: steps: - name: "Checkout code" - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: "Install PHP with extensions" uses: shivammathur/setup-php@v2 From dec30692abebfa05de933a19d470981abc2d962f Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Mon, 26 Dec 2022 12:47:02 +0100 Subject: [PATCH 014/812] Do more Github actions updates --- .github/workflows/documentation.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml index ee83b588749..3f7ae40a777 100644 --- a/.github/workflows/documentation.yml +++ b/.github/workflows/documentation.yml @@ -18,19 +18,19 @@ jobs: steps: - name: "Checkout code" - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: "Set-up PHP" uses: shivammathur/setup-php@v2 with: - php-version: 8.1 + php-version: 8.2 coverage: none tools: "composer:v2" - name: Get composer cache directory id: composercache working-directory: doc/_build - run: echo "::set-output name=dir::$(composer config cache-files-dir)" + run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT - name: Cache dependencies uses: actions/cache@v2 @@ -54,7 +54,7 @@ jobs: steps: - name: "Checkout code" - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: "Run DOCtor-RST" uses: docker://oskarstark/doctor-rst From 589655c1b930fa940beef243e760a21412e84834 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Mon, 26 Dec 2022 13:23:51 +0100 Subject: [PATCH 015/812] Re-enable Drupal integration tests --- .github/workflows/ci.yml | 63 +++++++++++++++++++--------------------- tests/drupal_test.sh | 2 +- 2 files changed, 31 insertions(+), 34 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a281b0ddcdf..d8af05234b4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -116,36 +116,33 @@ jobs: working-directory: ${{ matrix.extension}} run: ../../vendor/bin/simple-phpunit -# -# Drupal does not support Twig 3 now! -# -# integration-tests: -# needs: -# - 'tests' -# -# name: "Integration tests with PHP ${{ matrix.php-version }}" -# -# runs-on: 'ubuntu-20.04' -# -# continue-on-error: true -# -# strategy: -# matrix: -# php-version: -# - '7.3' -# -# steps: -# - name: "Checkout code" -# uses: actions/checkout@v3 -# -# - name: "Install PHP with extensions" -# uses: shivammathur/setup-php@2 -# with: -# coverage: "none" -# extensions: "gd, pdo_sqlite" -# php-version: ${{ matrix.php-version }} -# ini-values: memory_limit=-1 -# tools: composer:v2 -# -# - run: bash ./tests/drupal_test.sh -# shell: "bash" + integration-tests: + needs: + - 'tests' + + name: "Integration tests with PHP ${{ matrix.php-version }}" + + runs-on: 'ubuntu-latest' + + continue-on-error: true + + strategy: + matrix: + php-version: + - '8.2' + + steps: + - name: "Checkout code" + uses: actions/checkout@v3 + + - name: "Install PHP with extensions" + uses: shivammathur/setup-php@v2 + with: + coverage: "none" + extensions: "gd, pdo_sqlite" + php-version: ${{ matrix.php-version }} + ini-values: memory_limit=-1 + tools: composer:v2 + + - run: bash ./tests/drupal_test.sh + shell: "bash" diff --git a/tests/drupal_test.sh b/tests/drupal_test.sh index 7141f3181fd..e7fbf5dc7e9 100644 --- a/tests/drupal_test.sh +++ b/tests/drupal_test.sh @@ -6,7 +6,7 @@ set -e REPO=`pwd` cd /tmp rm -rf drupal-twig-test -composer create-project --no-interaction drupal/recommended-project:9.1.x-dev drupal-twig-test +composer create-project --no-interaction drupal/recommended-project:10.1.x-dev drupal-twig-test cd drupal-twig-test (cd vendor/twig && rm -rf twig && ln -sf $REPO twig) php ./web/core/scripts/drupal install --no-interaction demo_umami > output From 56b31224bdfb6dc6271c3e7eb060c8da7e723e3c Mon Sep 17 00:00:00 2001 From: Alex Henderson-Roche Date: Tue, 8 Nov 2022 18:11:59 +0200 Subject: [PATCH 016/812] Updates CoreExtension::twig_constant to check for definition first to avoid hard crash --- src/Extension/CoreExtension.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index 6ac36108137..5c4087ec217 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -1329,6 +1329,10 @@ function twig_constant($constant, $object = null) $constant = \get_class($object).'::'.$constant; } + if (!\defined($constant)) { + throw new RuntimeError(sprintf('Constant "%s" is undefined.', $constant)); + } + return \constant($constant); } From 61f55998d29d0af9071b791ed725f5ed841d0d13 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Mon, 26 Dec 2022 16:06:40 +0100 Subject: [PATCH 017/812] Fix tests --- extra/intl-extra/Tests/Fixtures/format_date.test | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/extra/intl-extra/Tests/Fixtures/format_date.test b/extra/intl-extra/Tests/Fixtures/format_date.test index 457f345d9d2..75844e1e4bb 100644 --- a/extra/intl-extra/Tests/Fixtures/format_date.test +++ b/extra/intl-extra/Tests/Fixtures/format_date.test @@ -15,10 +15,10 @@ return []; --EXPECT-- Aug 7, 2019, 11:39:12 PM -7 août 2019 à 23:39:12 +7 août 2019, 23:39:12 23:39 07/08/2019 -mercredi 7 août 2019 à 23:39:12 Temps universel coordonné +mercredi 7 août 2019 à 23:39:12 temps universel coordonné 11 oclock PM, Coordinated Universal Time Aug 7, 2019 From 824dbd9e21f721d7b9249a12842f238170134db8 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Mon, 26 Dec 2022 17:09:36 +0100 Subject: [PATCH 018/812] Add Compile::reset() --- src/Compiler.php | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/Compiler.php b/src/Compiler.php index 95e1f183b25..eb652c61a4e 100644 --- a/src/Compiler.php +++ b/src/Compiler.php @@ -46,7 +46,7 @@ public function getSource(): string /** * @return $this */ - public function compile(Node $node, int $indentation = 0) + public function reset(int $indentation = 0) { $this->lastLine = null; $this->source = ''; @@ -57,6 +57,15 @@ public function compile(Node $node, int $indentation = 0) $this->indentation = $indentation; $this->varNameSalt = 0; + return $this; + } + + /** + * @return $this + */ + public function compile(Node $node, int $indentation = 0) + { + $this->reset($indentation); $node->compile($this); return $this; From 3b6a2a691064520e8dd98d8ad8cc274d2113ffa9 Mon Sep 17 00:00:00 2001 From: naveen <172697+naveensrinivasan@users.noreply.github.com> Date: Mon, 27 Jun 2022 01:30:05 +0000 Subject: [PATCH 019/812] chore: Included githubactions in the dependabot config This should help with keeping the GitHub actions updated on new releases. This will also help with keeping it secure. Dependabot helps in keeping the supply chain secure https://docs.github.com/en/code-security/dependabot GitHub actions up to date https://docs.github.com/en/code-security/dependabot/working-with-dependabot/keeping-your-actions-up-to-date-with-dependabot https://github.com/ossf/scorecard/blob/main/docs/checks.md#dependency-update-tool Signed-off-by: naveen <172697+naveensrinivasan@users.noreply.github.com> --- .github/dependabot.yml | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000000..5ace4600a1f --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,6 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" From 6cd1473d65098aef575151d3fe78990987847506 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Mon, 26 Dec 2022 17:29:08 +0100 Subject: [PATCH 020/812] Bump version of actions/cache --- .github/workflows/documentation.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml index 3f7ae40a777..f2f46fc6d6e 100644 --- a/.github/workflows/documentation.yml +++ b/.github/workflows/documentation.yml @@ -33,7 +33,7 @@ jobs: run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT - name: Cache dependencies - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: ${{ steps.composercache.outputs.dir }} key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} From c8ec092ceb2ab68cc3cc2c87924b4a5973731502 Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Fri, 12 Aug 2022 19:15:06 +0200 Subject: [PATCH 021/812] do not clean up whitespace text nodes inside if tags --- src/Node/IfNode.php | 5 +++- tests/Fixtures/tags/if/empty_body.test | 32 ++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) create mode 100644 tests/Fixtures/tags/if/empty_body.test diff --git a/src/Node/IfNode.php b/src/Node/IfNode.php index 8ba23ddb60a..e74ca523b03 100644 --- a/src/Node/IfNode.php +++ b/src/Node/IfNode.php @@ -50,8 +50,11 @@ public function compile(Compiler $compiler) ->subcompile($this->getNode('tests')->getNode($i)) ->raw(") {\n") ->indent() - ->subcompile($this->getNode('tests')->getNode($i + 1)) ; + // The node might not exists if the content is empty + if ($this->getNode('tests')->hasNode($i + 1)) { + $compiler->subcompile($this->getNode('tests')->getNode($i + 1)); + } } if ($this->hasNode('else')) { diff --git a/tests/Fixtures/tags/if/empty_body.test b/tests/Fixtures/tags/if/empty_body.test new file mode 100644 index 00000000000..ba49f6e1ce5 --- /dev/null +++ b/tests/Fixtures/tags/if/empty_body.test @@ -0,0 +1,32 @@ +--TEST-- +empty "if" body in child template +--TEMPLATE-- +{% extends 'base.twig' %} + +{% set foo = '' %} + +{% if a is defined %} + +{% else %} + {% set foo = 'NOTHING' %} +{% endif %} + +{% if a is defined %} + +{% endif %} + +{% if a is defined %} + {% set foo = 'NOTHING' %} +{% else %} + +{% endif %} + +{% block content %} + {{ foo }} +{% endblock %} +--TEMPLATE(base.twig)-- +{% block content %}{% endblock %} +--DATA-- +return [] +--EXPECT-- + NOTHING From 61672c43f97dc2a9197211997e0ec2b1a797444c Mon Sep 17 00:00:00 2001 From: Matheo Daninos Date: Mon, 18 Apr 2022 22:03:37 +0200 Subject: [PATCH 022/812] throwing syntaxt error when the matches regexp is not valid --- src/Extension/CoreExtension.php | 21 ++++++++++++++++++++ src/Node/Expression/Binary/MatchesBinary.php | 2 +- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index ad7108c7587..d6ff1ce9777 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -310,6 +310,7 @@ public function getOperators(): array use Twig\Environment; use Twig\Error\LoaderError; use Twig\Error\RuntimeError; + use Twig\Error\SyntaxError; use Twig\Extension\CoreExtension; use Twig\Extension\SandboxExtension; use Twig\Markup; @@ -1019,6 +1020,26 @@ function twig_compare($a, $b) return $a <=> $b; } + /** + * @param string $pattern + * @param string $subject + * + * @return int + * + * @throws SyntaxError When an invalid pattern is used + */ +function twig_matches(string $regexp, string $str) +{ + set_error_handler(function ($t, $m) use ($regexp) { + throw new SyntaxError(sprintf('Regexp "%s" passed to "matches" is not valid', $regexp).substr($m, 12)); + }); + try { + return preg_match($regexp, $str); + } finally { + restore_error_handler(); + } +} + /** * Returns a trimmed string. * diff --git a/src/Node/Expression/Binary/MatchesBinary.php b/src/Node/Expression/Binary/MatchesBinary.php index bc97292cda5..a8bce6f4e75 100644 --- a/src/Node/Expression/Binary/MatchesBinary.php +++ b/src/Node/Expression/Binary/MatchesBinary.php @@ -18,7 +18,7 @@ class MatchesBinary extends AbstractBinary public function compile(Compiler $compiler): void { $compiler - ->raw('preg_match(') + ->raw('twig_matches(') ->subcompile($this->getNode('right')) ->raw(', ') ->subcompile($this->getNode('left')) From a82e94d911cf6aefca9dba01c68cdab501ceb375 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Tue, 27 Dec 2022 12:30:00 +0100 Subject: [PATCH 023/812] Add some tests --- src/Extension/CoreExtension.php | 19 +++++++++---------- tests/Fixtures/expressions/matches_error.test | 8 ++++++++ 2 files changed, 17 insertions(+), 10 deletions(-) create mode 100644 tests/Fixtures/expressions/matches_error.test diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index d6ff1ce9777..65caab31fe1 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -310,7 +310,6 @@ public function getOperators(): array use Twig\Environment; use Twig\Error\LoaderError; use Twig\Error\RuntimeError; - use Twig\Error\SyntaxError; use Twig\Extension\CoreExtension; use Twig\Extension\SandboxExtension; use Twig\Markup; @@ -1020,18 +1019,18 @@ function twig_compare($a, $b) return $a <=> $b; } - /** - * @param string $pattern - * @param string $subject - * - * @return int - * - * @throws SyntaxError When an invalid pattern is used - */ +/** + * @param string $pattern + * @param string $subject + * + * @return int + * + * @throws RuntimeError When an invalid pattern is used + */ function twig_matches(string $regexp, string $str) { set_error_handler(function ($t, $m) use ($regexp) { - throw new SyntaxError(sprintf('Regexp "%s" passed to "matches" is not valid', $regexp).substr($m, 12)); + throw new RuntimeError(sprintf('Regexp "%s" passed to "matches" is not valid', $regexp).substr($m, 12)); }); try { return preg_match($regexp, $str); diff --git a/tests/Fixtures/expressions/matches_error.test b/tests/Fixtures/expressions/matches_error.test new file mode 100644 index 00000000000..1220eb42212 --- /dev/null +++ b/tests/Fixtures/expressions/matches_error.test @@ -0,0 +1,8 @@ +--TEST-- +Twig supports the "matches" operator with a great error message +--TEMPLATE-- +{{ 'foo' matches '/o' }} +--DATA-- +return [] +--EXCEPTION-- +Twig\Error\RuntimeError: Regexp "/o" passed to "matches" is not valid: No ending delimiter '/' found in "index.twig" at line 2 From b376bdd0a9c8dd72d14d9db03f7d21d399e1dfd9 Mon Sep 17 00:00:00 2001 From: seb-jean Date: Fri, 16 Sep 2022 15:56:27 +0200 Subject: [PATCH 024/812] Add function twig *_name for intl list --- doc/functions/country_names.rst | 47 ++++++++++++++ doc/functions/currency_names.rst | 47 ++++++++++++++ doc/functions/language_names.rst | 47 ++++++++++++++ doc/functions/locale_names.rst | 47 ++++++++++++++ doc/functions/script_names.rst | 47 ++++++++++++++ doc/functions/timezone_names.rst | 47 ++++++++++++++ extra/intl-extra/IntlExtension.php | 63 ++++++++++++++++++- .../Tests/Fixtures/country_names.test | 12 ++++ .../Tests/Fixtures/currency_names.test | 12 ++++ .../Tests/Fixtures/language_names.test | 12 ++++ .../Tests/Fixtures/locale_names.test | 12 ++++ .../Tests/Fixtures/script_names.test | 12 ++++ .../Tests/Fixtures/timezone_names.test | 12 ++++ 13 files changed, 416 insertions(+), 1 deletion(-) create mode 100644 doc/functions/country_names.rst create mode 100644 doc/functions/currency_names.rst create mode 100644 doc/functions/language_names.rst create mode 100644 doc/functions/locale_names.rst create mode 100644 doc/functions/script_names.rst create mode 100644 doc/functions/timezone_names.rst create mode 100644 extra/intl-extra/Tests/Fixtures/country_names.test create mode 100644 extra/intl-extra/Tests/Fixtures/currency_names.test create mode 100644 extra/intl-extra/Tests/Fixtures/language_names.test create mode 100644 extra/intl-extra/Tests/Fixtures/locale_names.test create mode 100644 extra/intl-extra/Tests/Fixtures/script_names.test create mode 100644 extra/intl-extra/Tests/Fixtures/timezone_names.test diff --git a/doc/functions/country_names.rst b/doc/functions/country_names.rst new file mode 100644 index 00000000000..692137b0431 --- /dev/null +++ b/doc/functions/country_names.rst @@ -0,0 +1,47 @@ +``country_names`` +================= + +.. versionadded:: 3.5 + + The ``country_names`` function was added in Twig 3.5. + +The ``country_names`` function returns the names of the countries: + +.. code-block:: twig + + {# Afghanistan, Åland Islands, ... #} + {{ country_names()|join(', ') }} + +By default, the function uses the current locale. You can pass it explicitly: + +.. code-block:: twig + + {# Afghanistan, Afrique du Sud, ... #} + {{ country_names('fr')|join(', ') }} + +.. note:: + + The ``country_names`` function is part of the ``IntlExtension`` which is not + installed by default. Install it first: + + .. code-block:: bash + + $ composer require twig/intl-extra + + Then, on Symfony projects, install the ``twig/extra-bundle``: + + .. code-block:: bash + + $ composer require twig/extra-bundle + + Otherwise, add the extension explicitly on the Twig environment:: + + use Twig\Extra\Intl\IntlExtension; + + $twig = new \Twig\Environment(...); + $twig->addExtension(new IntlExtension()); + +Arguments +--------- + +* ``locale``: The locale diff --git a/doc/functions/currency_names.rst b/doc/functions/currency_names.rst new file mode 100644 index 00000000000..dfb446c8650 --- /dev/null +++ b/doc/functions/currency_names.rst @@ -0,0 +1,47 @@ +``currency_names`` +================== + +.. versionadded:: 3.5 + + The ``currency_names`` function was added in Twig 3.5. + +The ``currency_names`` function returns the names of the currencies: + +.. code-block:: twig + + {# Afghan Afghani, Afghan Afghani (1927–2002), ... #} + {{ currency_names()|join(', ') }} + +By default, the function uses the current locale. You can pass it explicitly: + +.. code-block:: twig + + {# afghani (1927–2002), afghani afghan, ... #} + {{ currency_names('fr')|join(', ') }} + +.. note:: + + The ``currency_names`` function is part of the ``IntlExtension`` which is not + installed by default. Install it first: + + .. code-block:: bash + + $ composer require twig/intl-extra + + Then, on Symfony projects, install the ``twig/extra-bundle``: + + .. code-block:: bash + + $ composer require twig/extra-bundle + + Otherwise, add the extension explicitly on the Twig environment:: + + use Twig\Extra\Intl\IntlExtension; + + $twig = new \Twig\Environment(...); + $twig->addExtension(new IntlExtension()); + +Arguments +--------- + +* ``locale``: The locale diff --git a/doc/functions/language_names.rst b/doc/functions/language_names.rst new file mode 100644 index 00000000000..f1cce488a73 --- /dev/null +++ b/doc/functions/language_names.rst @@ -0,0 +1,47 @@ +``language_names`` +================== + +.. versionadded:: 3.5 + + The ``language_names`` function was added in Twig 3.5. + +The ``language_names`` function returns the names of the languages: + +.. code-block:: twig + + {# Abkhazian, Achinese, ... #} + {{ language_names()|join(', ') }} + +By default, the function uses the current locale. You can pass it explicitly: + +.. code-block:: twig + + {# abkhaze, aceh, ... #} + {{ language_names('fr')|join(', ') }} + +.. note:: + + The ``language_names`` function is part of the ``IntlExtension`` which is not + installed by default. Install it first: + + .. code-block:: bash + + $ composer require twig/intl-extra + + Then, on Symfony projects, install the ``twig/extra-bundle``: + + .. code-block:: bash + + $ composer require twig/extra-bundle + + Otherwise, add the extension explicitly on the Twig environment:: + + use Twig\Extra\Intl\IntlExtension; + + $twig = new \Twig\Environment(...); + $twig->addExtension(new IntlExtension()); + +Arguments +--------- + +* ``locale``: The locale diff --git a/doc/functions/locale_names.rst b/doc/functions/locale_names.rst new file mode 100644 index 00000000000..320ab672470 --- /dev/null +++ b/doc/functions/locale_names.rst @@ -0,0 +1,47 @@ +``locale_names`` +================ + +.. versionadded:: 3.5 + + The ``locale_names`` function was added in Twig 3.5. + +The ``locale_names`` function returns the names of the locales: + +.. code-block:: twig + + {# Afrikaans, Afrikaans (Namibia), ... #} + {{ locale_names()|join(', ') }} + +By default, the function uses the current locale. You can pass it explicitly: + +.. code-block:: twig + + {# afrikaans, afrikaans (Afrique du Sud), ... #} + {{ locale_names('fr')|join(', ') }} + +.. note:: + + The ``locale_names`` function is part of the ``IntlExtension`` which is not + installed by default. Install it first: + + .. code-block:: bash + + $ composer require twig/intl-extra + + Then, on Symfony projects, install the ``twig/extra-bundle``: + + .. code-block:: bash + + $ composer require twig/extra-bundle + + Otherwise, add the extension explicitly on the Twig environment:: + + use Twig\Extra\Intl\IntlExtension; + + $twig = new \Twig\Environment(...); + $twig->addExtension(new IntlExtension()); + +Arguments +--------- + +* ``locale``: The locale diff --git a/doc/functions/script_names.rst b/doc/functions/script_names.rst new file mode 100644 index 00000000000..79b20c65fe2 --- /dev/null +++ b/doc/functions/script_names.rst @@ -0,0 +1,47 @@ +``script_names`` +================ + +.. versionadded:: 3.5 + + The ``script_names`` function was added in Twig 3.5. + +The ``script_names`` function returns the names of the scripts: + +.. code-block:: twig + + {# Adlam, Afaka, ... #} + {{ script_names()|join(', ') }} + +By default, the function uses the current locale. You can pass it explicitly: + +.. code-block:: twig + + {# Adlam, Afaka, ... #} + {{ script_names('fr')|join(', ') }} + +.. note:: + + The ``script_names`` function is part of the ``IntlExtension`` which is not + installed by default. Install it first: + + .. code-block:: bash + + $ composer require twig/intl-extra + + Then, on Symfony projects, install the ``twig/extra-bundle``: + + .. code-block:: bash + + $ composer require twig/extra-bundle + + Otherwise, add the extension explicitly on the Twig environment:: + + use Twig\Extra\Intl\IntlExtension; + + $twig = new \Twig\Environment(...); + $twig->addExtension(new IntlExtension()); + +Arguments +--------- + +* ``locale``: The locale diff --git a/doc/functions/timezone_names.rst b/doc/functions/timezone_names.rst new file mode 100644 index 00000000000..69db196fddd --- /dev/null +++ b/doc/functions/timezone_names.rst @@ -0,0 +1,47 @@ +``timezone_names`` +================== + +.. versionadded:: 3.5 + + The ``timezone_names`` function was added in Twig 3.5. + +The ``timezone_names`` function returns the names of the timezones: + +.. code-block:: twig + + {# Acre Time (Eirunepe), Acre Time (Rio Branco), ... #} + {{ timezone_names()|join(', ') }} + +By default, the function uses the current locale. You can pass it explicitly: + +.. code-block:: twig + + {# heure : Antarctique (Casey), heure : Canada (Montreal), ... #} + {{ timezone_names('fr')|join(', ') }} + +.. note:: + + The ``timezone_names`` function is part of the ``IntlExtension`` which is not + installed by default. Install it first: + + .. code-block:: bash + + $ composer require twig/intl-extra + + Then, on Symfony projects, install the ``twig/extra-bundle``: + + .. code-block:: bash + + $ composer require twig/extra-bundle + + Otherwise, add the extension explicitly on the Twig environment:: + + use Twig\Extra\Intl\IntlExtension; + + $twig = new \Twig\Environment(...); + $twig->addExtension(new IntlExtension()); + +Arguments +--------- + +* ``locale``: The locale diff --git a/extra/intl-extra/IntlExtension.php b/extra/intl-extra/IntlExtension.php index 1fce0c7888d..76c2b271e71 100644 --- a/extra/intl-extra/IntlExtension.php +++ b/extra/intl-extra/IntlExtension.php @@ -16,6 +16,7 @@ use Symfony\Component\Intl\Exception\MissingResourceException; use Symfony\Component\Intl\Languages; use Symfony\Component\Intl\Locales; +use Symfony\Component\Intl\Scripts; use Symfony\Component\Intl\Timezones; use Twig\Environment; use Twig\Error\RuntimeError; @@ -152,6 +153,12 @@ public function getFunctions() return [ // internationalized names new TwigFunction('country_timezones', [$this, 'getCountryTimezones']), + new TwigFunction('language_names', [$this, 'getLanguageNames']), + new TwigFunction('script_names', [$this, 'getScriptNames']), + new TwigFunction('country_names', [$this, 'getCountryNames']), + new TwigFunction('locale_names', [$this, 'getLocaleNames']), + new TwigFunction('currency_names', [$this, 'getCurrencyNames']), + new TwigFunction('timezone_names', [$this, 'getTimezoneNames']), ]; } @@ -242,6 +249,60 @@ public function getCountryTimezones(string $country): array } } + public function getLanguageNames(string $locale = null): array + { + try { + return Languages::getNames($locale); + } catch (MissingResourceException $exception) { + return []; + } + } + + public function getScriptNames(string $locale = null): array + { + try { + return Scripts::getNames($locale); + } catch (MissingResourceException $exception) { + return []; + } + } + + public function getCountryNames(string $locale = null): array + { + try { + return Countries::getNames($locale); + } catch (MissingResourceException $exception) { + return []; + } + } + + public function getLocaleNames(string $locale = null): array + { + try { + return Locales::getNames($locale); + } catch (MissingResourceException $exception) { + return []; + } + } + + public function getCurrencyNames(string $locale = null): array + { + try { + return Currencies::getNames($locale); + } catch (MissingResourceException $exception) { + return []; + } + } + + public function getTimezoneNames(string $locale = null): array + { + try { + return Timezones::getNames($locale); + } catch (MissingResourceException $exception) { + return []; + } + } + public function formatCurrency($amount, string $currency, array $attrs = [], string $locale = null): string { $formatter = $this->createNumberFormatter($locale, 'currency', $attrs); @@ -279,7 +340,7 @@ public function formatNumberStyle(string $style, $number, array $attrs = [], str */ public function formatDateTime(Environment $env, $date, ?string $dateFormat = 'medium', ?string $timeFormat = 'medium', string $pattern = '', $timezone = null, string $calendar = 'gregorian', string $locale = null): string { - $date = \twig_date_converter($env, $date, $timezone); + $date = twig_date_converter($env, $date, $timezone); $formatter = $this->createDateFormatter($locale, $dateFormat, $timeFormat, $pattern, $date->getTimezone(), $calendar); if (false === $ret = $formatter->format($date)) { diff --git a/extra/intl-extra/Tests/Fixtures/country_names.test b/extra/intl-extra/Tests/Fixtures/country_names.test new file mode 100644 index 00000000000..042c87ac996 --- /dev/null +++ b/extra/intl-extra/Tests/Fixtures/country_names.test @@ -0,0 +1,12 @@ +--TEST-- +"country_names" function +--TEMPLATE-- +{{ country_names('UNKNOWN')|length }} +{{ country_names()|join(', ') }} +{{ country_names('fr')|join(', ') }} +--DATA-- +return []; +--EXPECT-- +0 +Afghanistan, Åland Islands, Albania, Algeria, American Samoa, Andorra, Angola, Anguilla, Antarctica, Antigua & Barbuda, Argentina, Armenia, Aruba, Australia, Austria, Azerbaijan, Bahamas, Bahrain, Bangladesh, Barbados, Belarus, Belgium, Belize, Benin, Bermuda, Bhutan, Bolivia, Bosnia & Herzegovina, Botswana, Bouvet Island, Brazil, British Indian Ocean Territory, British Virgin Islands, Brunei, Bulgaria, Burkina Faso, Burundi, Cambodia, Cameroon, Canada, Cape Verde, Caribbean Netherlands, Cayman Islands, Central African Republic, Chad, Chile, China, Christmas Island, Cocos (Keeling) Islands, Colombia, Comoros, Congo - Brazzaville, Congo - Kinshasa, Cook Islands, Costa Rica, Côte d’Ivoire, Croatia, Cuba, Curaçao, Cyprus, Czechia, Denmark, Djibouti, Dominica, Dominican Republic, Ecuador, Egypt, El Salvador, Equatorial Guinea, Eritrea, Estonia, Eswatini, Ethiopia, Falkland Islands, Faroe Islands, Fiji, Finland, France, French Guiana, French Polynesia, French Southern Territories, Gabon, Gambia, Georgia, Germany, Ghana, Gibraltar, Greece, Greenland, Grenada, Guadeloupe, Guam, Guatemala, Guernsey, Guinea, Guinea-Bissau, Guyana, Haiti, Heard & McDonald Islands, Honduras, Hong Kong SAR China, Hungary, Iceland, India, Indonesia, Iran, Iraq, Ireland, Isle of Man, Israel, Italy, Jamaica, Japan, Jersey, Jordan, Kazakhstan, Kenya, Kiribati, Kuwait, Kyrgyzstan, Laos, Latvia, Lebanon, Lesotho, Liberia, Libya, Liechtenstein, Lithuania, Luxembourg, Macao SAR China, Madagascar, Malawi, Malaysia, Maldives, Mali, Malta, Marshall Islands, Martinique, Mauritania, Mauritius, Mayotte, Mexico, Micronesia, Moldova, Monaco, Mongolia, Montenegro, Montserrat, Morocco, Mozambique, Myanmar (Burma), Namibia, Nauru, Nepal, Netherlands, New Caledonia, New Zealand, Nicaragua, Niger, Nigeria, Niue, Norfolk Island, North Korea, North Macedonia, Northern Mariana Islands, Norway, Oman, Pakistan, Palau, Palestinian Territories, Panama, Papua New Guinea, Paraguay, Peru, Philippines, Pitcairn Islands, Poland, Portugal, Puerto Rico, Qatar, Réunion, Romania, Russia, Rwanda, Samoa, San Marino, São Tomé & Príncipe, Saudi Arabia, Senegal, Serbia, Seychelles, Sierra Leone, Singapore, Sint Maarten, Slovakia, Slovenia, Solomon Islands, Somalia, South Africa, South Georgia & South Sandwich Islands, South Korea, South Sudan, Spain, Sri Lanka, St. Barthélemy, St. Helena, St. Kitts & Nevis, St. Lucia, St. Martin, St. Pierre & Miquelon, St. Vincent & Grenadines, Sudan, Suriname, Svalbard & Jan Mayen, Sweden, Switzerland, Syria, Taiwan, Tajikistan, Tanzania, Thailand, Timor-Leste, Togo, Tokelau, Tonga, Trinidad & Tobago, Tunisia, Turkey, Turkmenistan, Turks & Caicos Islands, Tuvalu, U.S. Outlying Islands, U.S. Virgin Islands, Uganda, Ukraine, United Arab Emirates, United Kingdom, United States, Uruguay, Uzbekistan, Vanuatu, Vatican City, Venezuela, Vietnam, Wallis & Futuna, Western Sahara, Yemen, Zambia, Zimbabwe +Afghanistan, Afrique du Sud, Albanie, Algérie, Allemagne, Andorre, Angola, Anguilla, Antarctique, Antigua-et-Barbuda, Arabie saoudite, Argentine, Arménie, Aruba, Australie, Autriche, Azerbaïdjan, Bahamas, Bahreïn, Bangladesh, Barbade, Belgique, Belize, Bénin, Bermudes, Bhoutan, Biélorussie, Bolivie, Bosnie-Herzégovine, Botswana, Brésil, Brunei, Bulgarie, Burkina Faso, Burundi, Cambodge, Cameroun, Canada, Cap-Vert, Chili, Chine, Chypre, Colombie, Comores, Congo-Brazzaville, Congo-Kinshasa, Corée du Nord, Corée du Sud, Costa Rica, Côte d’Ivoire, Croatie, Cuba, Curaçao, Danemark, Djibouti, Dominique, Égypte, Émirats arabes unis, Équateur, Érythrée, Espagne, Estonie, Eswatini, État de la Cité du Vatican, États-Unis, Éthiopie, Fidji, Finlande, France, Gabon, Gambie, Géorgie, Géorgie du Sud-et-les Îles Sandwich du Sud, Ghana, Gibraltar, Grèce, Grenade, Groenland, Guadeloupe, Guam, Guatemala, Guernesey, Guinée, Guinée équatoriale, Guinée-Bissau, Guyana, Guyane française, Haïti, Honduras, Hongrie, Île Bouvet, Île Christmas, Île de Man, Île Norfolk, Îles Åland, Îles Caïmans, Îles Cocos, Îles Cook, Îles Féroé, Îles Heard-et-MacDonald, Îles Malouines, Îles Mariannes du Nord, Îles Marshall, Îles mineures éloignées des États-Unis, Îles Pitcairn, Îles Salomon, Îles Turques-et-Caïques, Îles Vierges britanniques, Îles Vierges des États-Unis, Inde, Indonésie, Irak, Iran, Irlande, Islande, Israël, Italie, Jamaïque, Japon, Jersey, Jordanie, Kazakhstan, Kenya, Kirghizstan, Kiribati, Koweït, La Réunion, Laos, Lesotho, Lettonie, Liban, Liberia, Libye, Liechtenstein, Lituanie, Luxembourg, Macédoine du Nord, Madagascar, Malaisie, Malawi, Maldives, Mali, Malte, Maroc, Martinique, Maurice, Mauritanie, Mayotte, Mexique, Micronésie, Moldavie, Monaco, Mongolie, Monténégro, Montserrat, Mozambique, Myanmar (Birmanie), Namibie, Nauru, Népal, Nicaragua, Niger, Nigeria, Niue, Norvège, Nouvelle-Calédonie, Nouvelle-Zélande, Oman, Ouganda, Ouzbékistan, Pakistan, Palaos, Panama, Papouasie-Nouvelle-Guinée, Paraguay, Pays-Bas, Pays-Bas caribéens, Pérou, Philippines, Pologne, Polynésie française, Porto Rico, Portugal, Qatar, R.A.S. chinoise de Hong Kong, R.A.S. chinoise de Macao, République centrafricaine, République dominicaine, Roumanie, Royaume-Uni, Russie, Rwanda, Sahara occidental, Saint-Barthélemy, Saint-Christophe-et-Niévès, Saint-Marin, Saint-Martin, Saint-Martin (partie néerlandaise), Saint-Pierre-et-Miquelon, Saint-Vincent-et-les Grenadines, Sainte-Hélène, Sainte-Lucie, Salvador, Samoa, Samoa américaines, Sao Tomé-et-Principe, Sénégal, Serbie, Seychelles, Sierra Leone, Singapour, Slovaquie, Slovénie, Somalie, Soudan, Soudan du Sud, Sri Lanka, Suède, Suisse, Suriname, Svalbard et Jan Mayen, Syrie, Tadjikistan, Taïwan, Tanzanie, Tchad, Tchéquie, Terres australes françaises, Territoire britannique de l’océan Indien, Territoires palestiniens, Thaïlande, Timor oriental, Togo, Tokelau, Tonga, Trinité-et-Tobago, Tunisie, Turkménistan, Turquie, Tuvalu, Ukraine, Uruguay, Vanuatu, Venezuela, Viêt Nam, Wallis-et-Futuna, Yémen, Zambie, Zimbabwe diff --git a/extra/intl-extra/Tests/Fixtures/currency_names.test b/extra/intl-extra/Tests/Fixtures/currency_names.test new file mode 100644 index 00000000000..47220290b00 --- /dev/null +++ b/extra/intl-extra/Tests/Fixtures/currency_names.test @@ -0,0 +1,12 @@ +--TEST-- +"currency_names" function +--TEMPLATE-- +{{ currency_names('UNKNOWN')|length }} +{{ currency_names()|join(', ') }} +{{ currency_names('fr')|join(', ') }} +--DATA-- +return []; +--EXPECT-- +0 +Afghan Afghani, Afghan Afghani (1927–2002), Albanian Lek, Albanian Lek (1946–1965), Algerian Dinar, Andorran Peseta, Angolan Kwanza, Angolan Kwanza (1977–1991), Angolan New Kwanza (1990–2000), Angolan Readjusted Kwanza (1995–1999), Argentine Austral, Argentine Peso, Argentine Peso (1881–1970), Argentine Peso (1983–1985), Argentine Peso Ley (1970–1983), Armenian Dram, Aruban Florin, Australian Dollar, Austrian Schilling, Azerbaijani Manat, Azerbaijani Manat (1993–2006), Bahamian Dollar, Bahraini Dinar, Bangladeshi Taka, Barbadian Dollar, Belarusian Ruble, Belarusian Ruble (1994–1999), Belarusian Ruble (2000–2016), Belgian Franc, Belgian Franc (convertible), Belgian Franc (financial), Belize Dollar, Bermudan Dollar, Bhutanese Ngultrum, Bolívar Soberano, Bolivian Boliviano, Bolivian Boliviano (1863–1963), Bolivian Mvdol, Bolivian Peso, Bosnia-Herzegovina Convertible Mark, Bosnia-Herzegovina Dinar (1992–1994), Bosnia-Herzegovina New Dinar (1994–1997), Botswanan Pula, Brazilian Cruzado (1986–1989), Brazilian Cruzeiro (1942–1967), Brazilian Cruzeiro (1990–1993), Brazilian Cruzeiro (1993–1994), Brazilian New Cruzado (1989–1990), Brazilian New Cruzeiro (1967–1986), Brazilian Real, British Pound, Brunei Dollar, Bulgarian Hard Lev, Bulgarian Lev, Bulgarian Lev (1879–1952), Bulgarian Socialist Lev, Burmese Kyat, Burundian Franc, Cambodian Riel, Canadian Dollar, Cape Verdean Escudo, Cayman Islands Dollar, Central African CFA Franc, CFP Franc, Chilean Escudo, Chilean Peso, Chilean Unit of Account (UF), Chinese People’s Bank Dollar, Chinese Yuan, Chinese Yuan (offshore), Colombian Peso, Colombian Real Value Unit, Comorian Franc, Congolese Franc, Costa Rican Colón, Croatian Dinar, Croatian Kuna, Cuban Convertible Peso, Cuban Peso, Cypriot Pound, Czech Koruna, Czechoslovak Hard Koruna, Danish Krone, Djiboutian Franc, Dominican Peso, Dutch Guilder, East Caribbean Dollar, East German Mark, Ecuadorian Sucre, Ecuadorian Unit of Constant Value, Egyptian Pound, Equatorial Guinean Ekwele, Eritrean Nakfa, Estonian Kroon, Ethiopian Birr, Euro, European Currency Unit, Falkland Islands Pound, Fijian Dollar, Finnish Markka, French Franc, French Gold Franc, French UIC-Franc, Gambian Dalasi, Georgian Kupon Larit, Georgian Lari, German Mark, Ghanaian Cedi, Ghanaian Cedi (1979–2007), Gibraltar Pound, Greek Drachma, Guatemalan Quetzal, Guinea-Bissau Peso, Guinean Franc, Guinean Syli, Guyanaese Dollar, Haitian Gourde, Honduran Lempira, Hong Kong Dollar, Hungarian Forint, Icelandic Króna, Icelandic Króna (1918–1981), Indian Rupee, Indonesian Rupiah, Iranian Rial, Iraqi Dinar, Irish Pound, Israeli New Shekel, Israeli Pound, Israeli Shekel (1980–1985), Italian Lira, Jamaican Dollar, Japanese Yen, Jordanian Dinar, Kazakhstani Tenge, Kenyan Shilling, Kuwaiti Dinar, Kyrgystani Som, Laotian Kip, Latvian Lats, Latvian Ruble, Lebanese Pound, Lesotho Loti, Liberian Dollar, Libyan Dinar, Lithuanian Litas, Lithuanian Talonas, Luxembourg Financial Franc, Luxembourgian Convertible Franc, Luxembourgian Franc, Macanese Pataca, Macedonian Denar, Macedonian Denar (1992–1993), Malagasy Ariary, Malagasy Franc, Malawian Kwacha, Malaysian Ringgit, Maldivian Rufiyaa, Maldivian Rupee (1947–1981), Malian Franc, Maltese Lira, Maltese Pound, Mauritanian Ouguiya, Mauritanian Ouguiya (1973–2017), Mauritian Rupee, Mexican Investment Unit, Mexican Peso, Mexican Silver Peso (1861–1992), Moldovan Cupon, Moldovan Leu, Monegasque Franc, Mongolian Tugrik, Moroccan Dirham, Moroccan Franc, Mozambican Escudo, Mozambican Metical, Mozambican Metical (1980–2006), Myanmar Kyat, Namibian Dollar, Nepalese Rupee, Netherlands Antillean Guilder, New Taiwan Dollar, New Zealand Dollar, Nicaraguan Córdoba, Nicaraguan Córdoba (1988–1991), Nigerian Naira, North Korean Won, Norwegian Krone, Omani Rial, Pakistani Rupee, Panamanian Balboa, Papua New Guinean Kina, Paraguayan Guarani, Peruvian Inti, Peruvian Sol, Peruvian Sol (1863–1965), Philippine Peso, Polish Zloty, Polish Zloty (1950–1995), Portuguese Escudo, Portuguese Guinea Escudo, Qatari Rial, Rhodesian Dollar, RINET Funds, Romanian Leu, Romanian Leu (1952–2006), Russian Ruble, Russian Ruble (1991–1998), Rwandan Franc, Salvadoran Colón, Samoan Tala, São Tomé & Príncipe Dobra, São Tomé & Príncipe Dobra (1977–2017), Saudi Riyal, Serbian Dinar, Serbian Dinar (2002–2006), Seychellois Rupee, Sierra Leonean Leone, Sierra Leonean New Leone, Singapore Dollar, Slovak Koruna, Slovenian Tolar, Solomon Islands Dollar, Somali Shilling, South African Rand, South African Rand (financial), South Korean Hwan (1953–1962), South Korean Won, South Korean Won (1945–1953), South Sudanese Pound, Soviet Rouble, Spanish Peseta, Spanish Peseta (A account), Spanish Peseta (convertible account), Sri Lankan Rupee, St. Helena Pound, Sudanese Dinar (1992–2007), Sudanese Pound, Sudanese Pound (1957–1998), Surinamese Dollar, Surinamese Guilder, Swazi Lilangeni, Swedish Krona, Swiss Franc, Syrian Pound, Tajikistani Ruble, Tajikistani Somoni, Tanzanian Shilling, Thai Baht, Timorese Escudo, Tongan Paʻanga, Trinidad & Tobago Dollar, Tunisian Dinar, Turkish Lira, Turkish Lira (1922–2005), Turkmenistani Manat, Turkmenistani Manat (1993–2009), Ugandan Shilling, Ugandan Shilling (1966–1987), Ukrainian Hryvnia, Ukrainian Karbovanets, United Arab Emirates Dirham, Uruguayan Nominal Wage Index Unit, Uruguayan Peso, Uruguayan Peso (1975–1993), Uruguayan Peso (Indexed Units), US Dollar, US Dollar (Next day), US Dollar (Same day), Uzbekistani Som, Vanuatu Vatu, Venezuelan Bolívar, Venezuelan Bolívar (1871–2008), Venezuelan Bolívar (2008–2018), Vietnamese Dong, Vietnamese Dong (1978–1985), West African CFA Franc, WIR Euro, WIR Franc, Yemeni Dinar, Yemeni Rial, Yugoslavian Convertible Dinar (1990–1992), Yugoslavian Hard Dinar (1966–1990), Yugoslavian New Dinar (1994–2002), Yugoslavian Reformed Dinar (1992–1993), Zairean New Zaire (1993–1998), Zairean Zaire (1971–1993), Zambian Kwacha, Zambian Kwacha (1968–2012), Zimbabwean Dollar (1980–2008), Zimbabwean Dollar (2008), Zimbabwean Dollar (2009) +afghani (1927–2002), afghani afghan, ancien leu roumain, Argentine Peso (1881–1970), Argentine Peso Ley (1970–1983), ariary malgache, austral argentin, baht thaïlandais, balboa panaméen, birr éthiopien, Bolívar Soberano, bolivar vénézuélien, bolivar vénézuélien (1871–2008), bolivar vénézuélien (2008–2018), Bolivian Boliviano (1863–1963), boliviano bolivien, Bosnia-Herzegovina New Dinar (1994–1997), Brazilian Cruzeiro (1942–1967), Bulgarian Lev (1879–1952), Bulgarian Socialist Lev, cédi, cédi ghanéen, Chilean Escudo, Chinese People’s Bank Dollar, colón costaricain, colón salvadorien, cordoba, córdoba oro nicaraguayen, coupon de lari géorgien, couronne danoise, couronne estonienne, couronne forte tchécoslovaque, couronne islandaise, couronne norvégienne, couronne slovaque, couronne suédoise, couronne tchèque, cruzado brésilien (1986–1989), cruzeiro, cruzeiro brésilien (1990–1993), dalasi gambien, denar macédonien, dinar algérien, dinar bahreïni, dinar bosniaque, dinar croate, dinar du Yémen, dinar irakien, dinar jordanien, dinar koweïtien, dinar libyen, dinar serbe, dinar serbo-monténégrin, dinar soudanais, dinar tunisien, dinar yougoslave convertible, dinar yougoslave Noviy, dirham des Émirats arabes unis, dirham marocain, dobra santoméen, dobra santoméen (1977–2017), dollar australien, dollar bahaméen, dollar barbadien, dollar bélizéen, dollar bermudien, dollar brunéien, dollar canadien, dollar de Hong Kong, dollar de Singapour, dollar de Trinité-et-Tobago, dollar des Caraïbes orientales, dollar des États-Unis, dollar des Etats-Unis (jour même), dollar des Etats-Unis (jour suivant), dollar des îles Caïmans, dollar des îles Salomon, dollar du Guyana, dollar fidjien, dollar jamaïcain, dollar libérien, dollar namibien, dollar néo-zélandais, dollar rhodésien, dollar surinamais, dollar zimbabwéen, dollar zimbabwéen (2008), dollar zimbabwéen (2009), dông vietnamien, drachme grecque, dram arménien, ekwélé équatoguinéen, escudo capverdien, escudo de Guinée portugaise, escudo mozambicain, escudo portugais, escudo timorais, euro, euro WIR, florin antillais, florin arubais, florin néerlandais, florin surinamais, forint hongrois, franc belge, franc belge (convertible), franc belge (financier), franc burundais, franc CFA (BCEAO), franc CFA (BEAC), franc CFP, franc comorien, franc congolais, franc convertible luxembourgeois, franc djiboutien, franc financier luxembourgeois, franc français, franc guinéen, franc luxembourgeois, franc malgache, franc malien, franc marocain, franc or, franc rwandais, franc suisse, franc UIC, franc WIR, gourde haïtienne, guaraní paraguayen, hryvnia ukrainienne, Icelandic Króna (1918–1981), inti péruvien, Israeli Shekel (1980–1985), karbovanetz, kina papouan-néo-guinéen, kip loatien, kuna croate, kwacha malawite, kwacha zambien, kwacha zambien (1968–2012), kwanza angolais, kwanza angolais (1977–1990), kwanza angolais réajusté (1995–1999), kyat birman, kyat myanmarais, lari géorgien, lats letton, lek albanais, lek albanais (1947–1961), lempira hondurien, leone sierra-léonais, leu moldave, leu roumain, lev bulgare, lev bulgare (1962–1999), lilangeni swazi, lire italienne, lire maltaise, litas lituanien, livre chypriote, livre de Gibraltar, livre de Sainte-Hélène, livre des îles Malouines, livre égyptienne, livre irlandaise, livre israélienne, livre libanaise, livre maltaise, livre soudanaise, livre soudanaise (1956–2007), livre sterling, livre sud-soudanaise, livre syrienne, livre turque, livre turque (1844–2005), loti lesothan, Macedonian Denar (1992–1993), Maldivian Rupee (1947–1981), manat azéri, manat azéri (1993–2006), manat turkmène, mark allemand, mark convertible bosniaque, mark est-allemand, mark finlandais, métical, metical mozambicain, Moldovan Cupon, Monegasque Franc, mvdol bolivien, nafka érythréen, naira nigérian, ngultrum bouthanais, nouveau cruzado, nouveau cruzeiro brésilien (1967–1986), nouveau dinar yougoslave, nouveau dollar taïwanais, nouveau kwanza angolais (1990–2000), nouveau manat turkmène, nouveau rouble biélorusse (1994–1999), nouveau shekel israélien, nouveau zaïre zaïrien, ouguiya mauritanien, ouguiya mauritanien (1973–2017), pa’anga tongan, pataca macanaise, peseta andorrane, peseta espagnole, peseta espagnole (compte A), peseta espagnole (compte convertible), peso argentin, peso argentin (1983–1985), peso bissau-guinéen, peso bolivien, peso chilien, peso colombien, peso cubain, peso cubain convertible, peso d’argent mexicain (1861–1992), peso dominicain, peso mexicain, peso philippin, peso uruguayen, peso uruguayen (1975–1993), peso uruguayen (unités indexées), pula botswanais, quetzal guatémaltèque, rand sud-africain, rand sud-africain (financier), réal brésilien, riel cambodgien, ringgit malais, riyal iranien, riyal omanais, riyal qatari, riyal saoudien, riyal yéménite, rouble biélorusse, rouble biélorusse (2000–2016), rouble letton, rouble russe, rouble russe (1991–1998), rouble soviétique, rouble tadjik, roupie des Seychelles, roupie indienne, roupie indonésienne, roupie mauricienne, roupie népalaise, roupie pakistanaise, roupie srilankaise, rufiyaa maldivien, schilling autrichien, shilling kényan, shilling ougandais, shilling ougandais (1966–1987), shilling somalien, shilling tanzanien, Sierra Leonean New Leone, sol péruvien, sol péruvien (1863–1985), som kirghize, somoni tadjik, South Korean Hwan (1953–1962), South Korean Won (1945–1953), sucre équatorien, sum ouzbek, syli guinéen, taka bangladeshi, tala samoan, talonas lituanien, tenge kazakh, tolar slovène, tugrik mongol, type de fonds RINET, unité d’investissement chilienne, unité de compte européenne (ECU), unité de conversion mexicaine (UDI), unité de valeur constante équatoriale (UVC), unité de valeur réelle colombienne, Uruguayan Nominal Wage Index Unit, vatu vanuatuan, Vietnamese Dong (1978–1985), won nord-coréen, won sud-coréen, yen japonais, yuan chinois (zone extracôtière), yuan renminbi chinois, Yugoslavian Reformed Dinar (1992–1993), zaïre zaïrois, zloty (1950–1995), zloty polonais diff --git a/extra/intl-extra/Tests/Fixtures/language_names.test b/extra/intl-extra/Tests/Fixtures/language_names.test new file mode 100644 index 00000000000..871a60991b8 --- /dev/null +++ b/extra/intl-extra/Tests/Fixtures/language_names.test @@ -0,0 +1,12 @@ +--TEST-- +"language_names" function +--TEMPLATE-- +{{ language_names('UNKNOWN')|length }} +{{ language_names()|join(', ') }} +{{ language_names('fr')|join(', ') }} +--DATA-- +return []; +--EXPECT-- +0 +Abkhazian, Achinese, Acoli, Adangme, Adyghe, Afar, Afrihili, Afrikaans, Aghem, Ainu, Akan, Akkadian, Akoose, Alabama, Albanian, Aleut, Algerian Arabic, American Sign Language, Amharic, Ancient Egyptian, Ancient Greek, Angika, Ao Naga, Arabic, Aragonese, Aramaic, Araona, Arapaho, Arawak, Armenian, Aromanian, Arpitan, Assamese, Asturian, Asu, Atikamekw, Atsam, Avaric, Avestan, Awadhi, Aymara, Azerbaijani, Badaga, Bafia, Bafut, Bakhtiari, Balinese, Baluchi, Bambara, Bamun, Bangla, Banjar, Basaa, Bashkir, Basque, Batak Toba, Bavarian, Beja, Belarusian, Bemba, Bena, Betawi, Bhojpuri, Bikol, Bini, Bishnupriya, Bislama, Blin, Blissymbols, Bodo, Bosnian, Brahui, Braj, Breton, Buginese, Bulgarian, Bulu, Buriat, Burmese, Caddo, Cajun French, Cantonese, Capiznon, Carib, Carolina Algonquian, Catalan, Cayuga, Cebuano, Central Atlas Tamazight, Central Dusun, Central Kurdish, Central Ojibwa, Central Yupik, Chadian Arabic, Chagatai, Chakma, Chamorro, Chechen, Cherokee, Cheyenne, Chibcha, Chickasaw, Chiga, Chilcotin, Chimborazo Highland Quichua, Chinese, Chinook Jargon, Chipewyan, Choctaw, Church Slavic, Chuukese, Chuvash, Classical Newari, Classical Syriac, Colognian, Comorian, Coptic, Cornish, Corsican, Cree, Crimean Tatar, Croatian, Czech, Dakota, Danish, Dargwa, Dazaga, Delaware, Dinka, Divehi, Dogri, Dogrib, Duala, Dutch, Dyula, Dzongkha, Eastern Canadian Inuktitut, Eastern Frisian, Eastern Ojibwa, Efik, Egyptian Arabic, Ekajuk, Elamite, Embu, Emilian, English, Erzya, Esperanto, Estonian, Ewe, Ewondo, Extremaduran, Fang, Fanti, Faroese, Fiji Hindi, Fijian, Filipino, Finnish, Fon, Frafra, French, Friulian, Fulah, Ga, Gagauz, Galician, Gan Chinese, Ganda, Gayo, Gbaya, Geez, Georgian, German, Gheg Albanian, Ghomala, Gilaki, Gilbertese, Goan Konkani, Gondi, Gorontalo, Gothic, Grebo, Greek, Guarani, Gujarati, Gusii, Gwichʼin, Haida, Haitian Creole, Hakka Chinese, Halkomelem, Hausa, Hawaiian, Hebrew, Herero, Hiligaynon, Hindi, Hiri Motu, Hittite, Hmong, Hmong Njua, Hungarian, Hupa, Iban, Ibibio, Icelandic, Ido, Igbo, Iloko, Inari Sami, Indonesian, Ingrian, Ingush, Innu-aimun, Interlingua, Interlingue, Inuktitut, Inupiaq, Irish, Italian, Jamaican Creole English, Japanese, Javanese, Jju, Jola-Fonyi, Judeo-Arabic, Judeo-Persian, Jutish, Kabardian, Kabuverdianu, Kabyle, Kachin, Kaingang, Kako, Kalaallisut, Kalenjin, Kalmyk, Kamba, Kanembu, Kannada, Kanuri, Kara-Kalpak, Karachay-Balkar, Karelian, Kashmiri, Kashubian, Kawi, Kazakh, Kenyang, Khasi, Khmer, Khotanese, Khowar, Kikuyu, Kimbundu, Kinaray-a, Kinyarwanda, Kirmanjki, Klingon, Kom, Komi, Komi-Permyak, Kongo, Konkani, Korean, Koro, Kosraean, Kotava, Koyra Chiini, Koyraboro Senni, Kpelle, Krio, Kuanyama, Kumyk, Kurdish, Kurukh, Kutenai, Kwakʼwala, Kwasio, Kyrgyz, Kʼicheʼ, Ladino, Lahnda, Lakota, Lamba, Langi, Lao, Latgalian, Latin, Latvian, Laz, Lezghian, Ligurian, Lillooet, Limburgish, Lingala, Lingua Franca Nova, Literary Chinese, Lithuanian, Livonian, Lojban, Lombard, Louisiana Creole, Low German, Lower Silesian, Lower Sorbian, Lozi, Luba-Katanga, Luba-Lulua, Luiseno, Lule Sami, Lunda, Luo, Luxembourgish, Luyia, Maba, Macedonian, Machame, Madurese, Mafa, Magahi, Main-Franconian, Maithili, Makasar, Makhuwa-Meetto, Makonde, Malagasy, Malay, Malayalam, Malecite, Maltese, Manchu, Mandar, Mandingo, Manipuri, Manx, Māori, Mapuche, Marathi, Mari, Marshallese, Marwari, Masai, Mazanderani, Medumba, Mende, Mentawai, Meru, Metaʼ, Mi'kmaq, Michif, Middle Dutch, Middle English, Middle French, Middle High German, Middle Irish, Min Nan Chinese, Minangkabau, Mingrelian, Mirandese, Mizo, Mohawk, Moksha, Mongo, Mongolian, Moose Cree, Morisyen, Moroccan Arabic, Mossi, Mundang, Muscogee, Muslim Tat, Myene, N’Ko, Najdi Arabic, Nama, Nauru, Navajo, Ndonga, Neapolitan, Nepali, Newari, Ngambay, Ngiemboon, Ngomba, Nheengatu, Nias, Nigerian Pidgin, Niuean, Nogai, North Ndebele, Northern East Cree, Northern Frisian, Northern Haida, Northern Luri, Northern Sami, Northern Sotho, Northern Tutchone, Northwestern Ojibwa, Norwegian, Norwegian Bokmål, Norwegian Nynorsk, Novial, Nuer, Nyamwezi, Nyanja, Nyankole, Nyasa Tonga, Nyoro, Nzima, Occitan, Odia, Oji-Cree, Ojibwa, Okanagan, Old English, Old French, Old High German, Old Irish, Old Norse, Old Persian, Old Provençal, Oromo, Osage, Ossetic, Ottoman Turkish, Pahlavi, Palatine German, Palauan, Pali, Pampanga, Pangasinan, Papiamento, Pashto, Pennsylvania German, Persian, Phoenician, Picard, Piedmontese, Plains Cree, Plautdietsch, Pohnpeian, Polish, Pontic, Portuguese, Prussian, Punjabi, Quechua, Rajasthani, Rapanui, Rarotongan, Riffian, Rohingya, Romagnol, Romanian, Romansh, Romany, Rombo, Rotuman, Roviana, Rundi, Russian, Rusyn, Rwa, Saho, Sakha, Samaritan Aramaic, Samburu, Samoan, Samogitian, Sandawe, Sango, Sangu, Sanskrit, Santali, Sardinian, Sasak, Sassarese Sardinian, Saterland Frisian, Saurashtra, Scots, Scottish Gaelic, Selayar, Selkup, Sena, Seneca, Serbian, Serbo-Croatian, Serer, Seri, Seselwa Creole French, Shambala, Shan, Shona, Sichuan Yi, Sicilian, Sidamo, Siksika, Silesian, Sindhi, Sinhala, Skolt Sami, Slave, Slovak, Slovenian, Soga, Sogdien, Somali, Soninke, South Ndebele, Southern Altai, Southern East Cree, Southern Haida, Southern Kurdish, Southern Lushootseed, Southern Sami, Southern Sotho, Southern Tutchone, Spanish, Sranan Tongo, Standard Moroccan Tamazight, Straits Salish, Sukuma, Sumerian, Sundanese, Susu, Swahili, Swampy Cree, Swati, Swedish, Swiss German, Syriac, Tachelhit, Tagalog, Tagish, Tahitian, Tahltan, Tai Dam, Taita, Tajik, Talysh, Tamashek, Tamil, Taroko, Tasawaq, Tatar, Telugu, Tereno, Teso, Tetum, Thai, Tibetan, Tigre, Tigrinya, Timne, Tiv, Tlingit, Tok Pisin, Tokelau, Tongan, Tornedalen Finnish, Torwali, Tsakhur, Tsakonian, Tsimshian, Tsonga, Tswana, Tulu, Tumbuka, Tunisian Arabic, Turkish, Turkmen, Turoyo, Tuvalu, Tuvinian, Twi, Tyap, Udmurt, Ugaritic, Ukrainian, Umbundu, Upper Sorbian, Urdu, Uyghur, Uzbek, Vai, Venda, Venetian, Veps, Vietnamese, Volapük, Võro, Votic, Vunjo, Walloon, Walser, Waray, Warlpiri, Washo, Wayuu, Welsh, West Flemish, Western Balochi, Western Canadian Inuktitut, Western Frisian, Western Mari, Western Ojibwa, Wolaytta, Wolof, Woods Cree, Wu Chinese, Xhosa, Xiang Chinese, Yangben, Yao, Yapese, Yemba, Yiddish, Yoruba, Zapotec, Zarma, Zaza, Zeelandic, Zenaga, Zhuang, Zoroastrian Dari, Zulu, Zuni +abkhaze, aceh, acoli, adangme, adyguéen, afar, afrihili, afrikaans, aghem, aïnou, akan, akkadien, akoose, alabama, albanais, aléoute, allemand, allemand palatin, altaï du Sud, amazighe de l’Atlas central, amazighe standard marocain, amharique, ancien anglais, ancien français, ancien haut allemand, ancien irlandais, angika, anglais, Ao, arabe, arabe algérien, arabe égyptien, arabe marocain, arabe najdi, arabe tchadien, arabe tunisien, aragonais, araméen, araméen samaritain, araona, arapaho, arawak, arménien, aroumain, assamais, asturien, asu, Atikamekw, atsam, avar, avestique, awadhi, aymara, azerbaïdjanais, bachkir, badaga, bafia, bafut, bakhtiari, balinais, baloutchi, baloutchi occidental, bambara, bamoun, banjar, bas-allemand, bas-prussien, bas-silésien, bas-sorabe, basque, bassa, batak toba, bavarois, bedja, bemba, bena, bengali, betawi, bhodjpouri, bichelamar, biélorusse, bikol, bini, birman, bishnupriya, blin, bodo, bosniaque, boulou, bouriate, brahoui, braj, breton, bugi, bulgare, cachemiri, caddo, caingangue, cantonais, capiznon, capverdien, carélien, caribe, Carolina Algonquian, catalan, cayuga, cebuano, Central Ojibwa, chamorro, changma kodha, cherokee, chewa, cheyenne, chibcha, Chickasaw, Chilcotin, chinois, chinois littéraire, chipewyan, chleuh, choctaw, chuuk, cingalais, cisena, comorien, copte, coréen, cornique, corse, cree, creek, créole haïtien, créole jamaïcain, créole louisianais, créole mauricien, créole seychellois, croate, dakota, danois, dargwa, dari zoroastrien, dazaga, delaware, dinka, diola-fogny, dioula, dogri, dogrib, douala, dusun central, dzongkha, Eastern Canadian Inuktitut, Eastern Ojibwa, écossais, éfik, égyptien ancien, ékadjouk, élamite, embu, émilien, erzya, esclave, espagnol, espéranto, estonien, estrémègne, éwé, éwondo, fang, fanti, féroïen, fidjien, filipino, finnois, finnois tornédalien, flamand occidental, fon, français, français cadien, franconien du Main, francoprovençal, frioulan, frison du Nord, frison occidental, frison oriental, ga, gaélique écossais, gagaouze, galicien, gallois, gan, ganda, gayo, gbaya, géorgien, ghomalaʼ, gilaki, gilbertin, gondi, gorontalo, gotique, goudjarati, grebo, grec, grec ancien, groenlandais, guarani, guègue, guèze, gurenne, gusii, gwichʼin, haida, hakka, Halkomelem, haoussa, haut-sorabe, hawaïen, hébreu, héréro, hiligaynon, hindi, hindi fidjien, hiri motu, hittite, hmong, Hmong Njua, hongrois, hupa, iakoute, iban, ibibio, ido, igbo, ilocano, indonésien, ingouche, ingrien, Innu-aimun, interlingua, interlingue, inuktitut, inupiaq, irlandais, isangu, islandais, italien, japonais, jargon chinook, javanais, jju, judéo-arabe, judéo-persan, jute, kabarde, kabyle, kachin, kachoube, kako, kalendjin, kalmouk, kamba, kanembou, kannada, kanouri, karakalpak, karatchaï balkar, kawi, kazakh, kényang, khasi, khmer, khotanais, khowar, kiga, kikongo, kikuyu, kimboundou, kinaray-a, kinyarwanda, kirghize, kirmanjki, klingon, kölsch, kom, komi, komi-permiak, konkani, konkani de Goa, koro, kosraéen, kotava, koumyk, kouroukh, koyra chiini, koyraboro senni, kpellé, krio, kuanyama, kurde, kurde du Sud, kutenai, Kwakʼwala, ladino, lahnda, lakota, lamba, langi, langue des signes américaine, lao, latgalien, latin, laze, letton, lezghien, ligure, Lillooet, limbourgeois, lingala, lingua franca nova, lituanien, livonien, lojban, lombard, lori du Nord, lozi, luba-kasaï (ciluba), luba-katanga (kiluba), luiseño, lunda, luo, lushaï, luxembourgeois, luyia, maasaï, maba, macédonien, madurais, mafa, magahi, maïthili, makassar, makondé, makua, malais, malayalam, maldivien, Malecite, malgache, maltais, mandar, mandchou, mandingue, manipuri, mannois, maori, mapuche, marathi, mari, mari occidental, marshallais, marwarî, matchamé, mazandérani, médumba, mendé, mentawaï, meru, metaʼ, Michif, micmac, minangkabau, mingrélien, minnan, mirandais, mohawk, mokcha, mongo, mongol, Moose Cree, moré, moundang, moyen anglais, moyen français, moyen haut-allemand, moyen irlandais, moyen néerlandais, myènè, n’ko, nama, napolitain, nauruan, navajo, ndébélé du Nord, ndébélé du Sud, ndonga, néerlandais, népalais, newari, newarî classique, ngambay, ngiemboon, ngomba, ngoumba, nheengatou, niha, niuéen, nogaï, Northern East Cree, Northern Haida, Northern Tutchone, Northwestern Ojibwa, norvégien, norvégien bokmål, norvégien nynorsk, novial, nuer, nyamwezi, nyankolé, nyoro, nzema, occitan, odia, Oji-Cree, ojibwa, Okanagan, oromo, osage, ossète, oudmourte, ougaritique, ouïghour, ourdou, ouzbek, pachto, pahlavi, palau, pali, pampangan, pangasinan, papiamento, pendjabi, pennsilfaanisch, persan, persan ancien, peul, phénicien, picard, pidgin nigérian, piémontais, Plains Cree, pohnpei, polonais, pontique, portugais, provençal ancien, prussien, quechua, quiché, quichua du Haut-Chimborazo, rajasthani, rapanui, rarotongien, rifain, rohingya, romagnol, romanche, romani, rombo, rotuman, roumain, roundi, roviana, russe, ruthène, rwa, saho, samburu, same d’Inari, same de Lule, same du Nord, same du Sud, same skolt, samoan, samogitien, sandawe, sango, sanskrit, santali, sarde, sarde sassarais, sasak, saterlandais, saurashtra, sélayar, selkoupe, seneca, serbe, serbo-croate, sérère, séri, shambala, shan, shona, sicilien, sidamo, siksika, silésien, sindhi, slavon d’église, slovaque, slovène, soga, sogdien, somali, soninké, sorani, sotho du Nord, sotho du Sud, soukouma, soundanais, soussou, Southern East Cree, Southern Haida, Southern Lushootseed, Southern Tutchone, sranan tongo, Straits Salish, suédois, suisse allemand, sumérien, swahili, Swampy Cree, swati, symboles Bliss, syriaque, syriaque classique, tadjik, tagalog, Tagish, tahitien, Tahltan, Tai Dam, taita, talysh, tamacheq, tamoul, taroko, tasawaq, tatar, tatar de Crimée, tati caucasien, tchaghataï, tchèque, tchétchène, tchouvache, télougou, tereno, teso, tétoum, thaï, tibétain, tigré, tigrigna, timné, tiv, tlingit, tok pisin, tokelau, tonga nyasa, tongien, Torwali, toulou, touroyo, touvain, tsakhour, tsakonien, tsimshian, tsonga, tswana, tumbuka, turc, turc ottoman, turkmène, tuvalu, twi, tyap, ukrainien, umbundu, vaï, venda, vénitien, vepse, vietnamien, vieux norrois, volapük, võro, vote, vunjo, walamo, wallon, walser, waray, warlpiri, washo, wayuu, Western Canadian Inuktitut, Western Ojibwa, wolof, Woods Cree, wu, xhosa, xiang, yangben, yao, yapois, yemba, yi du Sichuan, yiddish, yoruba, youpik central, zapotèque, zarma, zazaki, zélandais, zenaga, zhuang, zoulou, zuñi diff --git a/extra/intl-extra/Tests/Fixtures/locale_names.test b/extra/intl-extra/Tests/Fixtures/locale_names.test new file mode 100644 index 00000000000..bdf2e68b1db --- /dev/null +++ b/extra/intl-extra/Tests/Fixtures/locale_names.test @@ -0,0 +1,12 @@ +--TEST-- +"locale_names" function +--TEMPLATE-- +{{ locale_names('UNKNOWN')|length }} +{{ locale_names()|join(', ') }} +{{ locale_names('fr')|join(', ') }} +--DATA-- +return []; +--EXPECT-- +0 +Afrikaans, Afrikaans (Namibia), Afrikaans (South Africa), Akan, Akan (Ghana), Albanian, Albanian (Albania), Albanian (North Macedonia), Amharic, Amharic (Ethiopia), Arabic, Arabic (Algeria), Arabic (Bahrain), Arabic (Chad), Arabic (Comoros), Arabic (Djibouti), Arabic (Egypt), Arabic (Eritrea), Arabic (Iraq), Arabic (Israel), Arabic (Jordan), Arabic (Kuwait), Arabic (Lebanon), Arabic (Libya), Arabic (Mauritania), Arabic (Morocco), Arabic (Oman), Arabic (Palestinian Territories), Arabic (Qatar), Arabic (Saudi Arabia), Arabic (Somalia), Arabic (South Sudan), Arabic (Sudan), Arabic (Syria), Arabic (Tunisia), Arabic (United Arab Emirates), Arabic (Western Sahara), Arabic (world), Arabic (Yemen), Armenian, Armenian (Armenia), Assamese, Assamese (India), Azerbaijani, Azerbaijani (Azerbaijan), Azerbaijani (Cyrillic, Azerbaijan), Azerbaijani (Cyrillic), Azerbaijani (Latin, Azerbaijan), Azerbaijani (Latin), Bambara, Bambara (Mali), Bangla, Bangla (Bangladesh), Bangla (India), Basque, Basque (Spain), Belarusian, Belarusian (Belarus), Bosnian, Bosnian (Bosnia & Herzegovina), Bosnian (Cyrillic, Bosnia & Herzegovina), Bosnian (Cyrillic), Bosnian (Latin, Bosnia & Herzegovina), Bosnian (Latin), Breton, Breton (France), Bulgarian, Bulgarian (Bulgaria), Burmese, Burmese (Myanmar [Burma]), Catalan, Catalan (Andorra), Catalan (France), Catalan (Italy), Catalan (Spain), Chechen, Chechen (Russia), Chinese, Chinese (China), Chinese (Hong Kong SAR China), Chinese (Macao SAR China), Chinese (Simplified, China), Chinese (Simplified, Hong Kong SAR China), Chinese (Simplified, Macao SAR China), Chinese (Simplified, Singapore), Chinese (Simplified), Chinese (Singapore), Chinese (Taiwan), Chinese (Traditional, Hong Kong SAR China), Chinese (Traditional, Macao SAR China), Chinese (Traditional, Taiwan), Chinese (Traditional), Cornish, Cornish (United Kingdom), Croatian, Croatian (Bosnia & Herzegovina), Croatian (Croatia), Czech, Czech (Czechia), Danish, Danish (Denmark), Danish (Greenland), Dutch, Dutch (Aruba), Dutch (Belgium), Dutch (Caribbean Netherlands), Dutch (Curaçao), Dutch (Netherlands), Dutch (Sint Maarten), Dutch (Suriname), Dzongkha, Dzongkha (Bhutan), English, English (American Samoa), English (Anguilla), English (Antigua & Barbuda), English (Australia), English (Austria), English (Bahamas), English (Barbados), English (Belgium), English (Belize), English (Bermuda), English (Botswana), English (British Indian Ocean Territory), English (British Virgin Islands), English (Burundi), English (Cameroon), English (Canada), English (Cayman Islands), English (Christmas Island), English (Cocos [Keeling] Islands), English (Cook Islands), English (Cyprus), English (Denmark), English (Dominica), English (Eritrea), English (Eswatini), English (Europe), English (Falkland Islands), English (Fiji), English (Finland), English (Gambia), English (Germany), English (Ghana), English (Gibraltar), English (Grenada), English (Guam), English (Guernsey), English (Guyana), English (Hong Kong SAR China), English (India), English (Ireland), English (Isle of Man), English (Israel), English (Jamaica), English (Jersey), English (Kenya), English (Kiribati), English (Lesotho), English (Liberia), English (Macao SAR China), English (Madagascar), English (Malawi), English (Malaysia), English (Maldives), English (Malta), English (Marshall Islands), English (Mauritius), English (Micronesia), English (Montserrat), English (Namibia), English (Nauru), English (Netherlands), English (New Zealand), English (Nigeria), English (Niue), English (Norfolk Island), English (Northern Mariana Islands), English (Pakistan), English (Palau), English (Papua New Guinea), English (Philippines), English (Pitcairn Islands), English (Puerto Rico), English (Rwanda), English (Samoa), English (Seychelles), English (Sierra Leone), English (Singapore), English (Sint Maarten), English (Slovenia), English (Solomon Islands), English (South Africa), English (South Sudan), English (St. Helena), English (St. Kitts & Nevis), English (St. Lucia), English (St. Vincent & Grenadines), English (Sudan), English (Sweden), English (Switzerland), English (Tanzania), English (Tokelau), English (Tonga), English (Trinidad & Tobago), English (Turks & Caicos Islands), English (Tuvalu), English (U.S. Outlying Islands), English (U.S. Virgin Islands), English (Uganda), English (United Arab Emirates), English (United Kingdom), English (United States), English (Vanuatu), English (world), English (Zambia), English (Zimbabwe), Esperanto, Esperanto (world), Estonian, Estonian (Estonia), Ewe, Ewe (Ghana), Ewe (Togo), Faroese, Faroese (Denmark), Faroese (Faroe Islands), Finnish, Finnish (Finland), French, French (Algeria), French (Belgium), French (Benin), French (Burkina Faso), French (Burundi), French (Cameroon), French (Canada), French (Central African Republic), French (Chad), French (Comoros), French (Congo - Brazzaville), French (Congo - Kinshasa), French (Côte d’Ivoire), French (Djibouti), French (Equatorial Guinea), French (France), French (French Guiana), French (French Polynesia), French (Gabon), French (Guadeloupe), French (Guinea), French (Haiti), French (Luxembourg), French (Madagascar), French (Mali), French (Martinique), French (Mauritania), French (Mauritius), French (Mayotte), French (Monaco), French (Morocco), French (New Caledonia), French (Niger), French (Réunion), French (Rwanda), French (Senegal), French (Seychelles), French (St. Barthélemy), French (St. Martin), French (St. Pierre & Miquelon), French (Switzerland), French (Syria), French (Togo), French (Tunisia), French (Vanuatu), French (Wallis & Futuna), Fulah, Fulah (Adlam, Burkina Faso), Fulah (Adlam, Cameroon), Fulah (Adlam, Gambia), Fulah (Adlam, Ghana), Fulah (Adlam, Guinea-Bissau), Fulah (Adlam, Guinea), Fulah (Adlam, Liberia), Fulah (Adlam, Mauritania), Fulah (Adlam, Niger), Fulah (Adlam, Nigeria), Fulah (Adlam, Senegal), Fulah (Adlam, Sierra Leone), Fulah (Adlam), Fulah (Cameroon), Fulah (Guinea), Fulah (Latin, Burkina Faso), Fulah (Latin, Cameroon), Fulah (Latin, Gambia), Fulah (Latin, Ghana), Fulah (Latin, Guinea-Bissau), Fulah (Latin, Guinea), Fulah (Latin, Liberia), Fulah (Latin, Mauritania), Fulah (Latin, Niger), Fulah (Latin, Nigeria), Fulah (Latin, Senegal), Fulah (Latin, Sierra Leone), Fulah (Latin), Fulah (Mauritania), Fulah (Senegal), Galician, Galician (Spain), Ganda, Ganda (Uganda), Georgian, Georgian (Georgia), German, German (Austria), German (Belgium), German (Germany), German (Italy), German (Liechtenstein), German (Luxembourg), German (Switzerland), Greek, Greek (Cyprus), Greek (Greece), Gujarati, Gujarati (India), Hausa, Hausa (Ghana), Hausa (Niger), Hausa (Nigeria), Hebrew, Hebrew (Israel), Hindi, Hindi (India), Hindi (Latin, India), Hindi (Latin), Hungarian, Hungarian (Hungary), Icelandic, Icelandic (Iceland), Igbo, Igbo (Nigeria), Indonesian, Indonesian (Indonesia), Interlingua, Interlingua (world), Irish, Irish (Ireland), Irish (United Kingdom), Italian, Italian (Italy), Italian (San Marino), Italian (Switzerland), Italian (Vatican City), Japanese, Japanese (Japan), Javanese, Javanese (Indonesia), Kalaallisut, Kalaallisut (Greenland), Kannada, Kannada (India), Kashmiri, Kashmiri (Arabic, India), Kashmiri (Arabic), Kashmiri (Devanagari, India), Kashmiri (Devanagari), Kashmiri (India), Kazakh, Kazakh (Kazakhstan), Khmer, Khmer (Cambodia), Kikuyu, Kikuyu (Kenya), Kinyarwanda, Kinyarwanda (Rwanda), Korean, Korean (North Korea), Korean (South Korea), Kurdish, Kurdish (Turkey), Kyrgyz, Kyrgyz (Kyrgyzstan), Lao, Lao (Laos), Latvian, Latvian (Latvia), Lingala, Lingala (Angola), Lingala (Central African Republic), Lingala (Congo - Brazzaville), Lingala (Congo - Kinshasa), Lithuanian, Lithuanian (Lithuania), Luba-Katanga, Luba-Katanga (Congo - Kinshasa), Luxembourgish, Luxembourgish (Luxembourg), Macedonian, Macedonian (North Macedonia), Malagasy, Malagasy (Madagascar), Malay, Malay (Brunei), Malay (Indonesia), Malay (Malaysia), Malay (Singapore), Malayalam, Malayalam (India), Maltese, Maltese (Malta), Manx, Manx (Isle of Man), Māori, Māori (New Zealand), Marathi, Marathi (India), Mongolian, Mongolian (Mongolia), Nepali, Nepali (India), Nepali (Nepal), North Ndebele, North Ndebele (Zimbabwe), Northern Sami, Northern Sami (Finland), Northern Sami (Norway), Northern Sami (Sweden), Norwegian, Norwegian (Norway), Norwegian Bokmål, Norwegian Bokmål (Norway), Norwegian Bokmål (Svalbard & Jan Mayen), Norwegian Nynorsk, Norwegian Nynorsk (Norway), Odia, Odia (India), Oromo, Oromo (Ethiopia), Oromo (Kenya), Ossetic, Ossetic (Georgia), Ossetic (Russia), Pashto, Pashto (Afghanistan), Pashto (Pakistan), Persian, Persian (Afghanistan), Persian (Iran), Polish, Polish (Poland), Portuguese, Portuguese (Angola), Portuguese (Brazil), Portuguese (Cape Verde), Portuguese (Equatorial Guinea), Portuguese (Guinea-Bissau), Portuguese (Luxembourg), Portuguese (Macao SAR China), Portuguese (Mozambique), Portuguese (Portugal), Portuguese (São Tomé & Príncipe), Portuguese (Switzerland), Portuguese (Timor-Leste), Punjabi, Punjabi (Arabic, Pakistan), Punjabi (Arabic), Punjabi (Gurmukhi, India), Punjabi (Gurmukhi), Punjabi (India), Punjabi (Pakistan), Quechua, Quechua (Bolivia), Quechua (Ecuador), Quechua (Peru), Romanian, Romanian (Moldova), Romanian (Romania), Romansh, Romansh (Switzerland), Rundi, Rundi (Burundi), Russian, Russian (Belarus), Russian (Kazakhstan), Russian (Kyrgyzstan), Russian (Moldova), Russian (Russia), Russian (Ukraine), Sango, Sango (Central African Republic), Sanskrit, Sanskrit (India), Sardinian, Sardinian (Italy), Scottish Gaelic, Scottish Gaelic (United Kingdom), Serbian, Serbian (Bosnia & Herzegovina), Serbian (Cyrillic, Bosnia & Herzegovina), Serbian (Cyrillic, Montenegro), Serbian (Cyrillic, Serbia), Serbian (Cyrillic), Serbian (Latin, Bosnia & Herzegovina), Serbian (Latin, Montenegro), Serbian (Latin, Serbia), Serbian (Latin), Serbian (Montenegro), Serbian (Serbia), Serbo-Croatian, Serbo-Croatian (Bosnia & Herzegovina), Shona, Shona (Zimbabwe), Sichuan Yi, Sichuan Yi (China), Sindhi, Sindhi (Arabic, Pakistan), Sindhi (Arabic), Sindhi (Devanagari, India), Sindhi (Devanagari), Sindhi (Pakistan), Sinhala, Sinhala (Sri Lanka), Slovak, Slovak (Slovakia), Slovenian, Slovenian (Slovenia), Somali, Somali (Djibouti), Somali (Ethiopia), Somali (Kenya), Somali (Somalia), Spanish, Spanish (Argentina), Spanish (Belize), Spanish (Bolivia), Spanish (Brazil), Spanish (Chile), Spanish (Colombia), Spanish (Costa Rica), Spanish (Cuba), Spanish (Dominican Republic), Spanish (Ecuador), Spanish (El Salvador), Spanish (Equatorial Guinea), Spanish (Guatemala), Spanish (Honduras), Spanish (Latin America), Spanish (Mexico), Spanish (Nicaragua), Spanish (Panama), Spanish (Paraguay), Spanish (Peru), Spanish (Philippines), Spanish (Puerto Rico), Spanish (Spain), Spanish (United States), Spanish (Uruguay), Spanish (Venezuela), Sundanese, Sundanese (Indonesia), Sundanese (Latin, Indonesia), Sundanese (Latin), Swahili, Swahili (Congo - Kinshasa), Swahili (Kenya), Swahili (Tanzania), Swahili (Uganda), Swedish, Swedish (Åland Islands), Swedish (Finland), Swedish (Sweden), Tagalog, Tagalog (Philippines), Tajik, Tajik (Tajikistan), Tamil, Tamil (India), Tamil (Malaysia), Tamil (Singapore), Tamil (Sri Lanka), Tatar, Tatar (Russia), Telugu, Telugu (India), Thai, Thai (Thailand), Tibetan, Tibetan (China), Tibetan (India), Tigrinya, Tigrinya (Eritrea), Tigrinya (Ethiopia), Tongan, Tongan (Tonga), Turkish, Turkish (Cyprus), Turkish (Turkey), Turkmen, Turkmen (Turkmenistan), Ukrainian, Ukrainian (Ukraine), Urdu, Urdu (India), Urdu (Pakistan), Uyghur, Uyghur (China), Uzbek, Uzbek (Afghanistan), Uzbek (Arabic, Afghanistan), Uzbek (Arabic), Uzbek (Cyrillic, Uzbekistan), Uzbek (Cyrillic), Uzbek (Latin, Uzbekistan), Uzbek (Latin), Uzbek (Uzbekistan), Vietnamese, Vietnamese (Vietnam), Welsh, Welsh (United Kingdom), Western Frisian, Western Frisian (Netherlands), Wolof, Wolof (Senegal), Xhosa, Xhosa (South Africa), Yiddish, Yiddish (world), Yoruba, Yoruba (Benin), Yoruba (Nigeria), Zulu, Zulu (South Africa) +afrikaans, afrikaans (Afrique du Sud), afrikaans (Namibie), akan, akan (Ghana), albanais, albanais (Albanie), albanais (Macédoine du Nord), allemand, allemand (Allemagne), allemand (Autriche), allemand (Belgique), allemand (Italie), allemand (Liechtenstein), allemand (Luxembourg), allemand (Suisse), amharique, amharique (Éthiopie), anglais, anglais (Afrique du Sud), anglais (Allemagne), anglais (Anguilla), anglais (Antigua-et-Barbuda), anglais (Australie), anglais (Autriche), anglais (Bahamas), anglais (Barbade), anglais (Belgique), anglais (Belize), anglais (Bermudes), anglais (Botswana), anglais (Burundi), anglais (Cameroun), anglais (Canada), anglais (Chypre), anglais (Danemark), anglais (Dominique), anglais (Émirats arabes unis), anglais (Érythrée), anglais (Eswatini), anglais (États-Unis), anglais (Europe), anglais (Fidji), anglais (Finlande), anglais (Gambie), anglais (Ghana), anglais (Gibraltar), anglais (Grenade), anglais (Guam), anglais (Guernesey), anglais (Guyana), anglais (Île Christmas), anglais (Île de Man), anglais (Île Norfolk), anglais (Îles Caïmans), anglais (Îles Cocos), anglais (Îles Cook), anglais (Îles Malouines), anglais (Îles Mariannes du Nord), anglais (Îles Marshall), anglais (Îles mineures éloignées des États-Unis), anglais (Îles Pitcairn), anglais (Îles Salomon), anglais (Îles Turques-et-Caïques), anglais (Îles Vierges britanniques), anglais (Îles Vierges des États-Unis), anglais (Inde), anglais (Irlande), anglais (Israël), anglais (Jamaïque), anglais (Jersey), anglais (Kenya), anglais (Kiribati), anglais (Lesotho), anglais (Liberia), anglais (Madagascar), anglais (Malaisie), anglais (Malawi), anglais (Maldives), anglais (Malte), anglais (Maurice), anglais (Micronésie), anglais (Monde), anglais (Montserrat), anglais (Namibie), anglais (Nauru), anglais (Nigeria), anglais (Niue), anglais (Nouvelle-Zélande), anglais (Ouganda), anglais (Pakistan), anglais (Palaos), anglais (Papouasie-Nouvelle-Guinée), anglais (Pays-Bas), anglais (Philippines), anglais (Porto Rico), anglais (R.A.S. chinoise de Hong Kong), anglais (R.A.S. chinoise de Macao), anglais (Royaume-Uni), anglais (Rwanda), anglais (Saint-Christophe-et-Niévès), anglais (Saint-Martin [partie néerlandaise]), anglais (Saint-Vincent-et-les Grenadines), anglais (Sainte-Hélène), anglais (Sainte-Lucie), anglais (Samoa américaines), anglais (Samoa), anglais (Seychelles), anglais (Sierra Leone), anglais (Singapour), anglais (Slovénie), anglais (Soudan du Sud), anglais (Soudan), anglais (Suède), anglais (Suisse), anglais (Tanzanie), anglais (Territoire britannique de l’océan Indien), anglais (Tokelau), anglais (Tonga), anglais (Trinité-et-Tobago), anglais (Tuvalu), anglais (Vanuatu), anglais (Zambie), anglais (Zimbabwe), arabe, arabe (Algérie), arabe (Arabie saoudite), arabe (Bahreïn), arabe (Comores), arabe (Djibouti), arabe (Égypte), arabe (Émirats arabes unis), arabe (Érythrée), arabe (Irak), arabe (Israël), arabe (Jordanie), arabe (Koweït), arabe (Liban), arabe (Libye), arabe (Maroc), arabe (Mauritanie), arabe (Monde), arabe (Oman), arabe (Qatar), arabe (Sahara occidental), arabe (Somalie), arabe (Soudan du Sud), arabe (Soudan), arabe (Syrie), arabe (Tchad), arabe (Territoires palestiniens), arabe (Tunisie), arabe (Yémen), arménien, arménien (Arménie), assamais, assamais (Inde), azerbaïdjanais, azerbaïdjanais (Azerbaïdjan), azerbaïdjanais (cyrillique, Azerbaïdjan), azerbaïdjanais (cyrillique), azerbaïdjanais (latin, Azerbaïdjan), azerbaïdjanais (latin), bambara, bambara (Mali), basque, basque (Espagne), bengali, bengali (Bangladesh), bengali (Inde), biélorusse, biélorusse (Biélorussie), birman, birman (Myanmar [Birmanie]), bosniaque, bosniaque (Bosnie-Herzégovine), bosniaque (cyrillique, Bosnie-Herzégovine), bosniaque (cyrillique), bosniaque (latin, Bosnie-Herzégovine), bosniaque (latin), breton, breton (France), bulgare, bulgare (Bulgarie), cachemiri, cachemiri (arabe, Inde), cachemiri (arabe), cachemiri (dévanagari, Inde), cachemiri (dévanagari), cachemiri (Inde), catalan, catalan (Andorre), catalan (Espagne), catalan (France), catalan (Italie), chinois, chinois (Chine), chinois (R.A.S. chinoise de Hong Kong), chinois (R.A.S. chinoise de Macao), chinois (simplifié, Chine), chinois (simplifié, R.A.S. chinoise de Hong Kong), chinois (simplifié, R.A.S. chinoise de Macao), chinois (simplifié, Singapour), chinois (simplifié), chinois (Singapour), chinois (Taïwan), chinois (traditionnel, R.A.S. chinoise de Hong Kong), chinois (traditionnel, R.A.S. chinoise de Macao), chinois (traditionnel, Taïwan), chinois (traditionnel), cingalais, cingalais (Sri Lanka), coréen, coréen (Corée du Nord), coréen (Corée du Sud), cornique, cornique (Royaume-Uni), croate, croate (Bosnie-Herzégovine), croate (Croatie), danois, danois (Danemark), danois (Groenland), dzongkha, dzongkha (Bhoutan), espagnol, espagnol (Amérique latine), espagnol (Argentine), espagnol (Belize), espagnol (Bolivie), espagnol (Brésil), espagnol (Chili), espagnol (Colombie), espagnol (Costa Rica), espagnol (Cuba), espagnol (Équateur), espagnol (Espagne), espagnol (États-Unis), espagnol (Guatemala), espagnol (Guinée équatoriale), espagnol (Honduras), espagnol (Mexique), espagnol (Nicaragua), espagnol (Panama), espagnol (Paraguay), espagnol (Pérou), espagnol (Philippines), espagnol (Porto Rico), espagnol (République dominicaine), espagnol (Salvador), espagnol (Uruguay), espagnol (Venezuela), espéranto, espéranto (Monde), estonien, estonien (Estonie), éwé, éwé (Ghana), éwé (Togo), féroïen, féroïen (Danemark), féroïen (Îles Féroé), finnois, finnois (Finlande), français, français (Algérie), français (Belgique), français (Bénin), français (Burkina Faso), français (Burundi), français (Cameroun), français (Canada), français (Comores), français (Congo-Brazzaville), français (Congo-Kinshasa), français (Côte d’Ivoire), français (Djibouti), français (France), français (Gabon), français (Guadeloupe), français (Guinée équatoriale), français (Guinée), français (Guyane française), français (Haïti), français (La Réunion), français (Luxembourg), français (Madagascar), français (Mali), français (Maroc), français (Martinique), français (Maurice), français (Mauritanie), français (Mayotte), français (Monaco), français (Niger), français (Nouvelle-Calédonie), français (Polynésie française), français (République centrafricaine), français (Rwanda), français (Saint-Barthélemy), français (Saint-Martin), français (Saint-Pierre-et-Miquelon), français (Sénégal), français (Seychelles), français (Suisse), français (Syrie), français (Tchad), français (Togo), français (Tunisie), français (Vanuatu), français (Wallis-et-Futuna), frison occidental, frison occidental (Pays-Bas), Fulah (Adlam, Burkina Faso), Fulah (Adlam, Cameroon), Fulah (Adlam, Gambia), Fulah (Adlam, Ghana), Fulah (Adlam, Guinea-Bissau), Fulah (Adlam, Guinea), Fulah (Adlam, Liberia), Fulah (Adlam, Mauritania), Fulah (Adlam, Niger), Fulah (Adlam, Nigeria), Fulah (Adlam, Senegal), Fulah (Adlam, Sierra Leone), Fulah (Adlam), gaélique écossais, gaélique écossais (Royaume-Uni), galicien, galicien (Espagne), gallois, gallois (Royaume-Uni), ganda, ganda (Ouganda), géorgien, géorgien (Géorgie), goudjarati, goudjarati (Inde), grec, grec (Chypre), grec (Grèce), groenlandais, groenlandais (Groenland), haoussa, haoussa (Ghana), haoussa (Niger), haoussa (Nigeria), hébreu, hébreu (Israël), hindi, hindi (Inde), hindi (latin, Inde), hindi (latin), hongrois, hongrois (Hongrie), igbo, igbo (Nigeria), indonésien, indonésien (Indonésie), interlingua, interlingua (Monde), irlandais, irlandais (Irlande), irlandais (Royaume-Uni), islandais, islandais (Islande), italien, italien (État de la Cité du Vatican), italien (Italie), italien (Saint-Marin), italien (Suisse), japonais, japonais (Japon), javanais, javanais (Indonésie), kannada, kannada (Inde), kazakh, kazakh (Kazakhstan), khmer, khmer (Cambodge), kikuyu, kikuyu (Kenya), kinyarwanda, kinyarwanda (Rwanda), kirghize, kirghize (Kirghizstan), kurde, kurde (Turquie), lao, lao (Laos), letton, letton (Lettonie), lingala, lingala (Angola), lingala (Congo-Brazzaville), lingala (Congo-Kinshasa), lingala (République centrafricaine), lituanien, lituanien (Lituanie), luba-katanga [kiluba], luba-katanga [kiluba] (Congo-Kinshasa), luxembourgeois, luxembourgeois (Luxembourg), macédonien, macédonien (Macédoine du Nord), malais, malais (Brunei), malais (Indonésie), malais (Malaisie), malais (Singapour), malayalam, malayalam (Inde), malgache, malgache (Madagascar), maltais, maltais (Malte), mannois, mannois (Île de Man), maori, maori (Nouvelle-Zélande), marathi, marathi (Inde), mongol, mongol (Mongolie), ndébélé du Nord, ndébélé du Nord (Zimbabwe), néerlandais, néerlandais (Aruba), néerlandais (Belgique), néerlandais (Curaçao), néerlandais (Pays-Bas caribéens), néerlandais (Pays-Bas), néerlandais (Saint-Martin [partie néerlandaise]), néerlandais (Suriname), népalais, népalais (Inde), népalais (Népal), norvégien, norvégien (Norvège), norvégien bokmål, norvégien bokmål (Norvège), norvégien bokmål (Svalbard et Jan Mayen), norvégien nynorsk, norvégien nynorsk (Norvège), odia, odia (Inde), oromo, oromo (Éthiopie), oromo (Kenya), ossète, ossète (Géorgie), ossète (Russie), ouïghour, ouïghour (Chine), ourdou, ourdou (Inde), ourdou (Pakistan), ouzbek, ouzbek (Afghanistan), ouzbek (arabe, Afghanistan), ouzbek (arabe), ouzbek (cyrillique, Ouzbékistan), ouzbek (cyrillique), ouzbek (latin, Ouzbékistan), ouzbek (latin), ouzbek (Ouzbékistan), pachto, pachto (Afghanistan), pachto (Pakistan), pendjabi, pendjabi (arabe, Pakistan), pendjabi (arabe), pendjabi (gourmoukhî, Inde), pendjabi (gourmoukhî), pendjabi (Inde), pendjabi (Pakistan), persan, persan (Afghanistan), persan (Iran), peul, peul (Cameroun), peul (Guinée), peul (latin, Burkina Faso), peul (latin, Cameroun), peul (latin, Gambie), peul (latin, Ghana), peul (latin, Guinée-Bissau), peul (latin, Guinée), peul (latin, Liberia), peul (latin, Mauritanie), peul (latin, Niger), peul (latin, Nigeria), peul (latin, Sénégal), peul (latin, Sierra Leone), peul (latin), peul (Mauritanie), peul (Sénégal), polonais, polonais (Pologne), portugais, portugais (Angola), portugais (Brésil), portugais (Cap-Vert), portugais (Guinée équatoriale), portugais (Guinée-Bissau), portugais (Luxembourg), portugais (Mozambique), portugais (Portugal), portugais (R.A.S. chinoise de Macao), portugais (Sao Tomé-et-Principe), portugais (Suisse), portugais (Timor oriental), quechua, quechua (Bolivie), quechua (Équateur), quechua (Pérou), romanche, romanche (Suisse), roumain, roumain (Moldavie), roumain (Roumanie), roundi, roundi (Burundi), russe, russe (Biélorussie), russe (Kazakhstan), russe (Kirghizstan), russe (Moldavie), russe (Russie), russe (Ukraine), same du Nord, same du Nord (Finlande), same du Nord (Norvège), same du Nord (Suède), sango, sango (République centrafricaine), sanskrit, sanskrit (Inde), sarde, sarde (Italie), serbe, serbe (Bosnie-Herzégovine), serbe (cyrillique, Bosnie-Herzégovine), serbe (cyrillique, Monténégro), serbe (cyrillique, Serbie), serbe (cyrillique), serbe (latin, Bosnie-Herzégovine), serbe (latin, Monténégro), serbe (latin, Serbie), serbe (latin), serbe (Monténégro), serbe (Serbie), serbo-croate, serbo-croate (Bosnie-Herzégovine), shona, shona (Zimbabwe), sindhi, sindhi (arabe, Pakistan), sindhi (arabe), sindhi (dévanagari, Inde), sindhi (dévanagari), sindhi (Pakistan), slovaque, slovaque (Slovaquie), slovène, slovène (Slovénie), somali, somali (Djibouti), somali (Éthiopie), somali (Kenya), somali (Somalie), soundanais, soundanais (Indonésie), soundanais (latin, Indonésie), soundanais (latin), suédois, suédois (Finlande), suédois (Îles Åland), suédois (Suède), swahili, swahili (Congo-Kinshasa), swahili (Kenya), swahili (Ouganda), swahili (Tanzanie), tadjik, tadjik (Tadjikistan), tagalog, tagalog (Philippines), tamoul, tamoul (Inde), tamoul (Malaisie), tamoul (Singapour), tamoul (Sri Lanka), tatar, tatar (Russie), tchèque, tchèque (Tchéquie), tchétchène, tchétchène (Russie), télougou, télougou (Inde), thaï, thaï (Thaïlande), tibétain, tibétain (Chine), tibétain (Inde), tigrigna, tigrigna (Érythrée), tigrigna (Éthiopie), tongien, tongien (Tonga), turc, turc (Chypre), turc (Turquie), turkmène, turkmène (Turkménistan), ukrainien, ukrainien (Ukraine), vietnamien, vietnamien (Viêt Nam), wolof, wolof (Sénégal), xhosa, xhosa (Afrique du Sud), yi du Sichuan, yi du Sichuan (Chine), yiddish, yiddish (Monde), yoruba, yoruba (Bénin), yoruba (Nigeria), zoulou, zoulou (Afrique du Sud) diff --git a/extra/intl-extra/Tests/Fixtures/script_names.test b/extra/intl-extra/Tests/Fixtures/script_names.test new file mode 100644 index 00000000000..18baa84e6ab --- /dev/null +++ b/extra/intl-extra/Tests/Fixtures/script_names.test @@ -0,0 +1,12 @@ +--TEST-- +"script_names" function +--TEMPLATE-- +{{ script_names('UNKNOWN')|length }} +{{ script_names()|join(', ') }} +{{ script_names('fr')|join(', ') }} +--DATA-- +return []; +--EXPECT-- +0 +Adlam, Afaka, Ahom, Anatolian Hieroglyphs, Arabic, Armenian, Avestan, Balinese, Bamum, Bangla, Bassa Vah, Batak, Bhaiksuki, Blissymbols, Book Pahlavi, Bopomofo, Brahmi, Braille, Buginese, Buhid, Carian, Caucasian Albanian, Chakma, Cham, Cherokee, Chorasmian, Cirth, Common, Coptic, Cypriot, Cypro-Minoan, Cyrillic, Deseret, Devanagari, Dives Akuru, Dogra, Duployan shorthand, Eastern Syriac, Egyptian demotic, Egyptian hieratic, Egyptian hieroglyphs, Elbasan, Elymaic, Emoji, Estrangelo Syriac, Ethiopic, Fraktur Latin, Fraser, Gaelic Latin, Georgian, Georgian Khutsuri, Glagolitic, Gothic, Grantha, Greek, Gujarati, Gunjala Gondi, Gurmukhi, Han, Han with Bopomofo, Hangul, Hanifi, Hanunoo, Hatran, Hebrew, Hiragana, Imperial Aramaic, Indus, Inherited, Inscriptional Pahlavi, Inscriptional Parthian, Jamo, Japanese, Japanese syllabaries, Javanese, Jurchen, Kaithi, Kannada, Katakana, Kawi, Kayah Li, Kharoshthi, Khitan small script, Khmer, Khojki, Khudawadi, Korean, Kpelle, Lanna, Lao, Latin, Lepcha, Limbu, Linear A, Linear B, Loma, Lycian, Lydian, Mahajani, Makasar, Malayalam, Mandaean, Manichaean, Marchen, Masaram Gondi, Mathematical Notation, Mayan hieroglyphs, Medefaidrin, Meitei Mayek, Mende, Meroitic, Meroitic Cursive, Modi, Mongolian, Moon, Mro, Multani, Myanmar, N’Ko, Nabataean, Nag Mundari, Nandinagari, Nastaliq, Naxi Geba, New Tai Lue, Newa, Nüshu, Nyiakeng Puachue Hmong, Odia, Ogham, Ol Chiki, Old Church Slavonic Cyrillic, Old Hungarian, Old Italic, Old North Arabian, Old Permic, Old Persian, Old Sogdian, Old South Arabian, Old Uyghur, Orkhon, Osage, Osmanya, Pahawh Hmong, Palmyrene, Pau Cin Hau, Phags-pa, Phoenician, Pollard Phonetic, Psalter Pahlavi, Rejang, Rongorongo, Runic, Samaritan, Sarati, Saurashtra, Sharada, Shavian, Siddham, SignWriting, Simplified, Sinhala, Sogdian, Sora Sompeng, Soyombo, Sumero-Akkadian Cuneiform, Sundanese, Syloti Nagri, Symbols, Syriac, Tagalog, Tagbanwa, Tai Le, Tai Viet, Takri, Tamil, Tangsa, Tangut, Telugu, Tengwar, Thaana, Thai, Tibetan, Tifinagh, Tirhuta, Toto, Traditional, Ugaritic, Unified Canadian Aboriginal Syllabics, Unwritten, Vai, Varang Kshiti, Visible Speech, Vithkuqi, Wancho, Western Syriac, Woleai, Yezidi, Yi, Zanabazar Square, Zawgyi +Adlam, Afaka, Ahom, Anatolian Hieroglyphs, ancien hongrois, ancien italique, ancien permien, arabe, araméen impérial, arménien, avestique, balinais, Bamum, Bassa Vah, batak, bengali, Bhaiksuki, birman, bopomofo, bouguis, bouhide, brâhmî, braille, carien, Caucasian Albanian, chakma, cham, cherokee, Chorasmian, cingalais, cirth, commun, copte, coréen, cunéiforme persépolitain, cunéiforme suméro-akkadien, Cypro-Minoan, cyrillique, cyrillique (variante slavonne), démotique égyptien, déséret, dévanagari, Dives Akuru, Dogra, Duployan shorthand, écriture des signes, Elbasan, élymaïque, emoji, éthiopique, Fraser, géorgien, géorgien khoutsouri, glagolitique, gotique, goudjarâtî, gourmoukhî, Grantha, grec, Gunjala Gondi, han avec bopomofo, hangûl, Hanifi, hanounóo, Hatran, hébreu, hérité, hiératique égyptien, hiéroglyphes égyptiens, hiéroglyphes mayas, hiragana, indus, jamo, japonais, javanais, Jurchen, kaithî, kannara, katakana, katakana ou hiragana, Kawi, kayah li, kharochthî, Khitan small script, khmer, Khojki, Khudawadi, Kpelle, lanna, lao, latin, latin (variante brisée), latin (variante gaélique), lepcha, limbou, linéaire A, linéaire B, Loma, lycien, lydien, Mahajani, Makasar, malayalam, mandéen, manichéen, Marchen, Masaram Gondi, Medefaidrin, meitei mayek, Mende, Meroitic Cursive, méroïtique, Modi, mongol, moon, Mro, Multani, n’ko, Nabataean, Nag Mundari, nandinagari, nastaliq, Naxi Geba, Newa, non écrit, notation mathématique, nouveau taï-lue, Nüshu, nyiakeng puachue hmong, odia, ogam, ol tchiki, Old North Arabian, Old Sogdian, Old South Arabian, Old Uyghur, orkhon, Osage, osmanais, ougaritique, pahawh hmong, Palmyrene, parole visible, parthe des inscriptions, Pau Cin Hau, pehlevi des inscriptions, pehlevi des livres, pehlevi des psautiers, phags pa, phénicien, phonétique de Pollard, rejang, rongorongo, runique, samaritain, sarati, saurashtra, Sharada, shavien, Siddham, simplifié, sinogrammes, Sogdian, Sora Sompeng, Soyombo, sundanais, syllabaire autochtone canadien unifié, syllabaire chypriote, sylotî nâgrî, symboles, symboles Bliss, syriaque, syriaque estranghélo, syriaque occidental, syriaque oriental, tagal, tagbanoua, taï viêt, taï-le, Takri, tamoul, Tangsa, Tangut, télougou, tengwar, thaï, thâna, tibétain, tifinagh, Tirhuta, Toto, traditionnel, vaï, Varang Kshiti, Vithkuqi, wantcho, Woleai, Yezidi, yi, Zanabazar Square, zawgyi diff --git a/extra/intl-extra/Tests/Fixtures/timezone_names.test b/extra/intl-extra/Tests/Fixtures/timezone_names.test new file mode 100644 index 00000000000..bf0aafb91e6 --- /dev/null +++ b/extra/intl-extra/Tests/Fixtures/timezone_names.test @@ -0,0 +1,12 @@ +--TEST-- +"timezone_names" function +--TEMPLATE-- +{{ timezone_names('UNKNOWN')|length }} +{{ timezone_names()|join(', ') }} +{{ timezone_names('fr')|join(', ') }} +--DATA-- +return []; +--EXPECT-- +0 +Acre Time (Eirunepe), Acre Time (Rio Branco), Afghanistan Time (Kabul), Alaska Time (Anchorage), Alaska Time (Juneau), Alaska Time (Metlakatla), Alaska Time (Nome), Alaska Time (Sitka), Alaska Time (Yakutat), Amazon Time (Boa Vista), Amazon Time (Campo Grande), Amazon Time (Cuiaba), Amazon Time (Manaus), Amazon Time (Porto Velho), Anadyr Time, Apia Time, Arabian Time (Aden), Arabian Time (Baghdad), Arabian Time (Bahrain), Arabian Time (Kuwait), Arabian Time (Qatar), Arabian Time (Riyadh), Argentina Time (Buenos Aires), Argentina Time (Catamarca), Argentina Time (Cordoba), Argentina Time (Jujuy), Argentina Time (La Rioja), Argentina Time (Mendoza), Argentina Time (Rio Gallegos), Argentina Time (Salta), Argentina Time (San Juan), Argentina Time (San Luis), Argentina Time (Tucuman), Argentina Time (Ushuaia), Armenia Time (Yerevan), Atlantic Time (Anguilla), Atlantic Time (Antigua), Atlantic Time (Aruba), Atlantic Time (Barbados), Atlantic Time (Bermuda), Atlantic Time (Blanc-Sablon), Atlantic Time (Curaçao), Atlantic Time (Dominica), Atlantic Time (Glace Bay), Atlantic Time (Goose Bay), Atlantic Time (Grenada), Atlantic Time (Guadeloupe), Atlantic Time (Halifax), Atlantic Time (Kralendijk), Atlantic Time (Lower Prince’s Quarter), Atlantic Time (Marigot), Atlantic Time (Martinique), Atlantic Time (Moncton), Atlantic Time (Montserrat), Atlantic Time (Port of Spain), Atlantic Time (Puerto Rico), Atlantic Time (Santo Domingo), Atlantic Time (St. Barthélemy), Atlantic Time (St. Kitts), Atlantic Time (St. Lucia), Atlantic Time (St. Thomas), Atlantic Time (St. Vincent), Atlantic Time (Thule), Atlantic Time (Tortola), Australian Central Western Time (Eucla), Azerbaijan Time (Baku), Azores Time, Bangladesh Time (Dhaka), Bhutan Time (Thimphu), Bolivia Time (La Paz), Brasilia Time (Araguaina), Brasilia Time (Bahia), Brasilia Time (Belem), Brasilia Time (Fortaleza), Brasilia Time (Maceio), Brasilia Time (Recife), Brasilia Time (Santarem), Brasilia Time (Sao Paulo), Brunei Darussalam Time, Canada Time (Montreal), Cape Verde Time, Casey Time, Central Africa Time (Blantyre), Central Africa Time (Bujumbura), Central Africa Time (Gaborone), Central Africa Time (Harare), Central Africa Time (Juba), Central Africa Time (Khartoum), Central Africa Time (Kigali), Central Africa Time (Lubumbashi), Central Africa Time (Lusaka), Central Africa Time (Maputo), Central Africa Time (Windhoek), Central Australia Time (Adelaide), Central Australia Time (Broken Hill), Central Australia Time (Darwin), Central European Time (Algiers), Central European Time (Amsterdam), Central European Time (Andorra), Central European Time (Belgrade), Central European Time (Berlin), Central European Time (Bratislava), Central European Time (Brussels), Central European Time (Budapest), Central European Time (Busingen), Central European Time (Ceuta), Central European Time (Copenhagen), Central European Time (Gibraltar), Central European Time (Ljubljana), Central European Time (Longyearbyen), Central European Time (Luxembourg), Central European Time (Madrid), Central European Time (Malta), Central European Time (Monaco), Central European Time (Oslo), Central European Time (Paris), Central European Time (Podgorica), Central European Time (Prague), Central European Time (Rome), Central European Time (San Marino), Central European Time (Sarajevo), Central European Time (Skopje), Central European Time (Stockholm), Central European Time (Tirane), Central European Time (Tunis), Central European Time (Vaduz), Central European Time (Vatican), Central European Time (Vienna), Central European Time (Warsaw), Central European Time (Zagreb), Central European Time (Zurich), Central Indonesia Time (Makassar), Central Time, Central Time (Bahia Banderas), Central Time (Belize), Central Time (Beulah, North Dakota), Central Time (Center, North Dakota), Central Time (Chicago), Central Time (Costa Rica), Central Time (El Salvador), Central Time (Guatemala), Central Time (Knox, Indiana), Central Time (Managua), Central Time (Matamoros), Central Time (Menominee), Central Time (Merida), Central Time (Mexico City), Central Time (Monterrey), Central Time (New Salem, North Dakota), Central Time (Rainy River), Central Time (Rankin Inlet), Central Time (Regina), Central Time (Resolute), Central Time (Swift Current), Central Time (Tegucigalpa), Central Time (Tell City, Indiana), Central Time (Winnipeg), Chamorro Standard Time (Guam), Chamorro Standard Time (Saipan), Chatham Time, Chile Time (Palmer), Chile Time (Punta Arenas), Chile Time (Santiago), China Time (Macao), China Time (Shanghai), China Time (Urumqi), Christmas Island Time, Chuuk Time, Cocos Islands Time, Colombia Time (Bogota), Cook Islands Time (Rarotonga), Coordinated Universal Time, Cuba Time (Havana), Davis Time, Dumont-d’Urville Time, East Africa Time (Addis Ababa), East Africa Time (Antananarivo), East Africa Time (Asmara), East Africa Time (Comoro), East Africa Time (Dar es Salaam), East Africa Time (Djibouti), East Africa Time (Kampala), East Africa Time (Mayotte), East Africa Time (Mogadishu), East Africa Time (Nairobi), East Greenland Time (Ittoqqortoormiit), East Kazakhstan Time (Almaty), East Kazakhstan Time (Kostanay), East Timor Time (Dili), Easter Island Time, Eastern Australia Time (Brisbane), Eastern Australia Time (Currie), Eastern Australia Time (Hobart), Eastern Australia Time (Lindeman), Eastern Australia Time (Macquarie), Eastern Australia Time (Melbourne), Eastern Australia Time (Sydney), Eastern European Time (Amman), Eastern European Time (Athens), Eastern European Time (Beirut), Eastern European Time (Bucharest), Eastern European Time (Cairo), Eastern European Time (Chisinau), Eastern European Time (Damascus), Eastern European Time (Famagusta), Eastern European Time (Gaza), Eastern European Time (Hebron), Eastern European Time (Helsinki), Eastern European Time (Kaliningrad), Eastern European Time (Kyiv), Eastern European Time (Mariehamn), Eastern European Time (Nicosia), Eastern European Time (Riga), Eastern European Time (Sofia), Eastern European Time (Tallinn), Eastern European Time (Tripoli), Eastern European Time (Uzhhorod), Eastern European Time (Vilnius), Eastern European Time (Zaporozhye), Eastern Indonesia Time (Jayapura), Eastern Time, Eastern Time (Atikokan), Eastern Time (Cancun), Eastern Time (Cayman), Eastern Time (Detroit), Eastern Time (Grand Turk), Eastern Time (Indianapolis), Eastern Time (Iqaluit), Eastern Time (Jamaica), Eastern Time (Louisville), Eastern Time (Marengo, Indiana), Eastern Time (Monticello, Kentucky), Eastern Time (Nassau), Eastern Time (New York), Eastern Time (Nipigon), Eastern Time (Panama), Eastern Time (Pangnirtung), Eastern Time (Petersburg, Indiana), Eastern Time (Port-au-Prince), Eastern Time (Thunder Bay), Eastern Time (Toronto), Eastern Time (Vevay, Indiana), Eastern Time (Vincennes, Indiana), Eastern Time (Winamac, Indiana), Ecuador Time (Guayaquil), Falkland Islands Time (Stanley), Fernando de Noronha Time, Fiji Time, French Guiana Time (Cayenne), French Southern & Antarctic Time (Kerguelen), Galapagos Time, Gambier Time, Georgia Time (Tbilisi), Gilbert Islands Time (Tarawa), Greenwich Mean Time, Greenwich Mean Time (Abidjan), Greenwich Mean Time (Accra), Greenwich Mean Time (Bamako), Greenwich Mean Time (Banjul), Greenwich Mean Time (Bissau), Greenwich Mean Time (Conakry), Greenwich Mean Time (Dakar), Greenwich Mean Time (Danmarkshavn), Greenwich Mean Time (Dublin), Greenwich Mean Time (Freetown), Greenwich Mean Time (Guernsey), Greenwich Mean Time (Isle of Man), Greenwich Mean Time (Jersey), Greenwich Mean Time (Lome), Greenwich Mean Time (London), Greenwich Mean Time (Monrovia), Greenwich Mean Time (Nouakchott), Greenwich Mean Time (Ouagadougou), Greenwich Mean Time (Reykjavik), Greenwich Mean Time (São Tomé), Greenwich Mean Time (St. Helena), Greenwich Mean Time (Troll), Gulf Standard Time (Dubai), Gulf Standard Time (Muscat), Guyana Time, Hawaii-Aleutian Time (Adak), Hawaii-Aleutian Time (Honolulu), Hawaii-Aleutian Time (Johnston), Hong Kong Time, Hovd Time, India Standard Time (Colombo), India Standard Time (Kolkata), Indian Ocean Time (Chagos), Indochina Time (Bangkok), Indochina Time (Ho Chi Minh City), Indochina Time (Phnom Penh), Indochina Time (Vientiane), Iran Time (Tehran), Irkutsk Time, Israel Time (Jerusalem), Japan Time (Tokyo), Korean Time (Pyongyang), Korean Time (Seoul), Kosrae Time, Krasnoyarsk Time, Krasnoyarsk Time (Novokuznetsk), Kyrgyzstan Time (Bishkek), Line Islands Time (Kiritimati), Lord Howe Time, Magadan Time, Magadan Time (Srednekolymsk), Malaysia Time (Kuala Lumpur), Malaysia Time (Kuching), Maldives Time, Marquesas Time, Marshall Islands Time (Kwajalein), Marshall Islands Time (Majuro), Mauritius Time, Mawson Time, Mexican Pacific Time (Chihuahua), Mexican Pacific Time (Hermosillo), Mexican Pacific Time (Mazatlan), Moscow Time, Moscow Time (Astrakhan), Moscow Time (Minsk), Moscow Time (Saratov), Moscow Time (Simferopol), Moscow Time (Ulyanovsk), Mountain Time, Mountain Time (Boise), Mountain Time (Cambridge Bay), Mountain Time (Creston), Mountain Time (Dawson Creek), Mountain Time (Denver), Mountain Time (Edmonton), Mountain Time (Fort Nelson), Mountain Time (Inuvik), Mountain Time (Ojinaga), Mountain Time (Phoenix), Mountain Time (Yellowknife), Myanmar Time (Yangon), Nauru Time, Nepal Time (Kathmandu), New Caledonia Time (Noumea), New Zealand Time (Auckland), New Zealand Time (McMurdo), Newfoundland Time (St. John’s), Niue Time, Norfolk Island Time, Northwest Mexico Time (Santa Isabel), Novosibirsk Time, Omsk Time, Pacific Time, Pacific Time (Los Angeles), Pacific Time (Tijuana), Pacific Time (Vancouver), Pakistan Time (Karachi), Palau Time, Papua New Guinea Time (Bougainville), Papua New Guinea Time (Port Moresby), Paraguay Time (Asunción), Peru Time (Lima), Petropavlovsk-Kamchatski Time (Kamchatka), Philippine Time (Manila), Phoenix Islands Time (Enderbury), Pitcairn Time, Ponape Time (Pohnpei), Réunion Time, Rothera Time, Russia Time (Barnaul), Russia Time (Kirov), Russia Time (Tomsk), Sakhalin Time, Samara Time, Samoa Time (Midway), Samoa Time (Pago Pago), Seychelles Time (Mahe), Singapore Standard Time, Solomon Islands Time (Guadalcanal), South Africa Standard Time (Johannesburg), South Africa Standard Time (Maseru), South Africa Standard Time (Mbabane), South Georgia Time, St. Pierre & Miquelon Time, Suriname Time (Paramaribo), Syowa Time, Tahiti Time, Taipei Time, Tajikistan Time (Dushanbe), Tokelau Time (Fakaofo), Tonga Time (Tongatapu), Turkey Time (Istanbul), Turkmenistan Time (Ashgabat), Tuvalu Time (Funafuti), Ulaanbaatar Time, Ulaanbaatar Time (Choibalsan), Uruguay Time (Montevideo), Uzbekistan Time (Samarkand), Uzbekistan Time (Tashkent), Vanuatu Time (Efate), Venezuela Time (Caracas), Vladivostok Time, Vladivostok Time (Ust-Nera), Volgograd Time, Vostok Time, Wake Island Time, Wallis & Futuna Time, West Africa Time (Bangui), West Africa Time (Brazzaville), West Africa Time (Douala), West Africa Time (Kinshasa), West Africa Time (Lagos), West Africa Time (Libreville), West Africa Time (Luanda), West Africa Time (Malabo), West Africa Time (Ndjamena), West Africa Time (Niamey), West Africa Time (Porto-Novo), West Greenland Time (Nuuk), West Kazakhstan Time (Aqtau), West Kazakhstan Time (Aqtobe), West Kazakhstan Time (Atyrau), West Kazakhstan Time (Oral), West Kazakhstan Time (Qyzylorda), Western Australia Time (Perth), Western European Time (Canary), Western European Time (Casablanca), Western European Time (El Aaiun), Western European Time (Faroe), Western European Time (Lisbon), Western European Time (Madeira), Western Indonesia Time (Jakarta), Western Indonesia Time (Pontianak), Yakutsk Time, Yakutsk Time (Chita), Yakutsk Time (Khandyga), Yekaterinburg Time, Yukon Time (Dawson), Yukon Time (Whitehorse) +heure : Antarctique (Casey), heure : Canada (Montreal), heure : Chine (Ürümqi), heure : Russie (Barnaul), heure : Russie (Kirov), heure : Russie (Tomsk), heure : Turquie (Istanbul), heure d’Afrique de l’Ouest (Bangui), heure d’Afrique de l’Ouest (Brazzaville), heure d’Afrique de l’Ouest (Douala), heure d’Afrique de l’Ouest (Kinshasa), heure d’Afrique de l’Ouest (Lagos), heure d’Afrique de l’Ouest (Libreville), heure d’Afrique de l’Ouest (Luanda), heure d’Afrique de l’Ouest (Malabo), heure d’Afrique de l’Ouest (N’Djamena), heure d’Afrique de l’Ouest (Niamey), heure d’Afrique de l’Ouest (Porto-Novo), heure d’Anadyr, heure d’Apia, heure d’Ekaterinbourg, heure d’Europe centrale (Alger), heure d’Europe centrale (Amsterdam), heure d’Europe centrale (Andorre), heure d’Europe centrale (Belgrade), heure d’Europe centrale (Berlin), heure d’Europe centrale (Bratislava), heure d’Europe centrale (Bruxelles), heure d’Europe centrale (Budapest), heure d’Europe centrale (Büsingen), heure d’Europe centrale (Ceuta), heure d’Europe centrale (Copenhague), heure d’Europe centrale (Gibraltar), heure d’Europe centrale (Le Vatican), heure d’Europe centrale (Ljubljana), heure d’Europe centrale (Longyearbyen), heure d’Europe centrale (Luxembourg), heure d’Europe centrale (Madrid), heure d’Europe centrale (Malte), heure d’Europe centrale (Monaco), heure d’Europe centrale (Oslo), heure d’Europe centrale (Paris), heure d’Europe centrale (Podgorica), heure d’Europe centrale (Prague), heure d’Europe centrale (Rome), heure d’Europe centrale (Saint-Marin), heure d’Europe centrale (Sarajevo), heure d’Europe centrale (Skopje), heure d’Europe centrale (Stockholm), heure d’Europe centrale (Tirana), heure d’Europe centrale (Tunis), heure d’Europe centrale (Vaduz), heure d’Europe centrale (Varsovie), heure d’Europe centrale (Vienne), heure d’Europe centrale (Zagreb), heure d’Europe centrale (Zurich), heure d’Europe de l’Est (Amman), heure d’Europe de l’Est (Athènes), heure d’Europe de l’Est (Beyrouth), heure d’Europe de l’Est (Bucarest), heure d’Europe de l’Est (Chisinau), heure d’Europe de l’Est (Damas), heure d’Europe de l’Est (Famagouste), heure d’Europe de l’Est (Gaza), heure d’Europe de l’Est (Hébron), heure d’Europe de l’Est (Helsinki), heure d’Europe de l’Est (Kaliningrad), heure d’Europe de l’Est (Kiev), heure d’Europe de l’Est (Le Caire), heure d’Europe de l’Est (Mariehamn), heure d’Europe de l’Est (Nicosie), heure d’Europe de l’Est (Oujgorod), heure d’Europe de l’Est (Riga), heure d’Europe de l’Est (Sofia), heure d’Europe de l’Est (Tallinn), heure d’Europe de l’Est (Tripoli (Libye)), heure d’Europe de l’Est (Vilnius), heure d’Europe de l’Est (Zaporojie), heure d’Europe de l’Ouest (Casablanca), heure d’Europe de l’Ouest (Îles Canaries), heure d’Europe de l’Ouest (Îles Féroé), heure d’Europe de l’Ouest (Laâyoune), heure d’Europe de l’Ouest (Lisbonne), heure d’Europe de l’Ouest (Madère), heure d’Hawaii - Aléoutiennes (Adak), heure d’Hawaii - Aléoutiennes (Honolulu), heure d’Hawaii - Aléoutiennes (Johnston), heure d’Indochine (Bangkok), heure d’Indochine (Hô-Chi-Minh-Ville), heure d’Indochine (Phnom Penh), heure d’Indochine (Vientiane), heure d’Irkoutsk, heure d’Israël (Jérusalem), heure d’Oulan-Bator, heure d’Oulan-Bator (Tchoïbalsan), heure de Bolivie (La Paz), heure de Brasilia (Araguaína), heure de Brasilia (Bahia), heure de Brasilia (Belém), heure de Brasilia (Fortaleza), heure de Brasilia (Maceió), heure de Brasilia (Recife), heure de Brasilia (Santarém), heure de Brasilia (São Paulo), heure de Chuuk, heure de Colombie (Bogota), heure de Cuba (La Havane), heure de Davis, heure de Dumont-d’Urville, heure de Fernando de Noronha, heure de Géorgie du Sud, heure de Hong Kong, heure de Hovd, heure de Iakoutsk, heure de Iakoutsk (Khandyga), heure de Iakoutsk (Tchita), heure de Kosrae, heure de Krasnoïarsk, heure de Krasnoïarsk (Novokuznetsk), heure de l’Acre (Eirunepé), heure de l’Acre (Rio Branco), heure de l’Afghanistan (Kaboul), heure de l’Alaska (Anchorage), heure de l’Alaska (Juneau), heure de l’Alaska (Metlakatla), heure de l’Alaska (Nome), heure de l’Alaska (Sitka), heure de l’Alaska (Yakutat), heure de l’Amazonie (Boa Vista), heure de l’Amazonie (Campo Grande), heure de l’Amazonie (Cuiabá), heure de l’Amazonie (Manaos), heure de l’Amazonie (Porto Velho), heure de l’Arabie (Aden), heure de l’Arabie (Bagdad), heure de l’Arabie (Bahreïn), heure de l’Arabie (Koweït), heure de l’Arabie (Qatar), heure de l’Arabie (Riyad), heure de l’Argentine (Buenos Aires), heure de l’Argentine (Catamarca), heure de l’Argentine (Córdoba), heure de l’Argentine (Jujuy), heure de l’Argentine (La Rioja), heure de l’Argentine (Mendoza), heure de l’Argentine (Río Gallegos), heure de l’Argentine (Salta), heure de l’Argentine (San Juan), heure de l’Argentine (San Luis), heure de l’Argentine (Tucumán), heure de l’Argentine (Ushuaïa), heure de l’Arménie (Erevan), heure de l’Atlantique (Anguilla), heure de l’Atlantique (Antigua), heure de l’Atlantique (Aruba), heure de l’Atlantique (Bermudes), heure de l’Atlantique (Blanc-Sablon), heure de l’Atlantique (Curaçao), heure de l’Atlantique (Dominique), heure de l’Atlantique (Glace Bay), heure de l’Atlantique (Goose Bay), heure de l’Atlantique (Grenade), heure de l’Atlantique (Guadeloupe), heure de l’Atlantique (Halifax), heure de l’Atlantique (Kralendijk), heure de l’Atlantique (La Barbade), heure de l’Atlantique (Lower Prince’s Quarter), heure de l’Atlantique (Marigot), heure de l’Atlantique (Martinique), heure de l’Atlantique (Moncton), heure de l’Atlantique (Montserrat), heure de l’Atlantique (Port-d’Espagne), heure de l’Atlantique (Porto Rico), heure de l’Atlantique (Saint-Barthélemy), heure de l’Atlantique (Saint-Christophe), heure de l’Atlantique (Saint-Domingue), heure de l’Atlantique (Saint-Thomas), heure de l’Atlantique (Saint-Vincent), heure de l’Atlantique (Sainte-Lucie), heure de l’Atlantique (Thulé), heure de l’Atlantique (Tortola), heure de l’Azerbaïdjan (Bakou), heure de l’Équateur (Guayaquil), heure de l’Est de l’Australie (Brisbane), heure de l’Est de l’Australie (Currie), heure de l’Est de l’Australie (Hobart), heure de l’Est de l’Australie (Lindeman), heure de l’Est de l’Australie (Macquarie), heure de l’Est de l’Australie (Melbourne), heure de l’Est de l’Australie (Sydney), heure de l’Est du Groenland (Ittoqqortoormiit), heure de l’Est du Kazakhstan (Alma Ata), heure de l’Est du Kazakhstan (Kostanaï), heure de l’Est indonésien (Jayapura), heure de l’Est nord-américain, heure de l’Est nord-américain (Atikokan), heure de l’Est nord-américain (Caïmans), heure de l’Est nord-américain (Cancún), heure de l’Est nord-américain (Détroit), heure de l’Est nord-américain (Grand Turk), heure de l’Est nord-américain (Indianapolis), heure de l’Est nord-américain (Iqaluit), heure de l’Est nord-américain (Jamaïque), heure de l’Est nord-américain (Louisville), heure de l’Est nord-américain (Marengo [Indiana]), heure de l’Est nord-américain (Monticello [Kentucky]), heure de l’Est nord-américain (Nassau), heure de l’Est nord-américain (New York), heure de l’Est nord-américain (Nipigon), heure de l’Est nord-américain (Panama), heure de l’Est nord-américain (Pangnirtung), heure de l’Est nord-américain (Petersburg [Indiana]), heure de l’Est nord-américain (Port-au-Prince), heure de l’Est nord-américain (Thunder Bay), heure de l’Est nord-américain (Toronto), heure de l’Est nord-américain (Vevay [Indiana]), heure de l’Est nord-américain (Vincennes [Indiana]), heure de l’Est nord-américain (Winamac [Indiana]), heure de l’île Christmas, heure de l’île de Pâques, heure de l’île de Pohnpei, heure de l’île Norfolk, heure de l’île Wake, heure de l’Inde (Calcutta), heure de l’Inde (Colombo), heure de l’Iran (Téhéran), heure de l’Océan Indien (Chagos), heure de l’Ouest de l’Australie (Perth), heure de l’Ouest du Groenland (Nuuk), heure de l’Ouest du Kazakhstan (Aktaou), heure de l’Ouest du Kazakhstan (Aktioubinsk), heure de l’Ouest du Kazakhstan (Atyraou), heure de l’Ouest du Kazakhstan (Kzyl Orda), heure de l’Ouest du Kazakhstan (Ouralsk), heure de l’Ouest indonésien (Jakarta), heure de l’Ouest indonésien (Pontianak), heure de l’Ouzbékistan (Samarcande), heure de l’Ouzbékistan (Tachkent), heure de l’Uruguay (Montevideo), heure de la Chine (Macao), heure de la Chine (Shanghai), heure de la Corée (Pyongyang), heure de la Corée (Séoul), heure de la Géorgie (Tbilissi), heure de la Guyane française (Cayenne), heure de la Malaisie (Kuala Lumpur), heure de la Malaisie (Kuching), heure de la Nouvelle-Calédonie (Nouméa), heure de la Nouvelle-Zélande (Auckland), heure de la Nouvelle-Zélande (McMurdo), heure de la Papouasie-Nouvelle-Guinée (Bougainville), heure de la Papouasie-Nouvelle-Guinée (Port Moresby), heure de La Réunion, heure de Lord Howe, heure de Magadan, heure de Magadan (Srednekolymsk), heure de Maurice, heure de Mawson, heure de Moscou, heure de Moscou (Astrakhan), heure de Moscou (Minsk), heure de Moscou (Oulianovsk), heure de Moscou (Saratov), heure de Moscou (Simferopol), heure de Nauru, heure de Nioué (Niue), heure de Novossibirsk, heure de Omsk, heure de Petropavlovsk-Kamchatski (Kamtchatka), heure de Rothera, heure de Saint-Pierre-et-Miquelon, heure de Sakhaline, heure de Samara, heure de Singapour, heure de Syowa (Showa), heure de Tahiti, heure de Taipei, heure de Terre-Neuve (Saint-Jean de Terre-Neuve), heure de Tokelau (Fakaofo), heure de Vladivostok, heure de Vladivostok (Ust-Nera), heure de Volgograd, heure de Vostok, heure de Wallis-et-Futuna, heure des Açores, heure des Chamorro (Guam), heure des Chamorro (Saipan), heure des îles Chatham, heure des îles Cocos, heure des îles Cook (Rarotonga), heure des îles de la Ligne (Kiritimati), heure des îles Fidji, heure des îles Galápagos, heure des îles Gambier, heure des îles Gilbert (Tarawa), heure des îles Malouines (Stanley), heure des îles Marquises, heure des îles Marshall (Kwajalein), heure des îles Marshall (Majuro), heure des îles Phoenix (Enderbury), heure des îles Pitcairn, heure des îles Salomon (Guadalcanal), heure des Maldives, heure des Palaos (Palau), heure des Philippines (Manille), heure des Rocheuses, heure des Rocheuses (Boise), heure des Rocheuses (Cambridge Bay), heure des Rocheuses (Creston), heure des Rocheuses (Dawson Creek), heure des Rocheuses (Denver), heure des Rocheuses (Edmonton), heure des Rocheuses (Fort Nelson), heure des Rocheuses (Inuvik), heure des Rocheuses (Ojinaga), heure des Rocheuses (Phoenix), heure des Rocheuses (Yellowknife), heure des Samoa (Midway), heure des Samoa (Pago Pago), heure des Seychelles (Mahé), heure des Terres australes et antarctiques françaises (Kerguelen), heure des Tonga (Tongatapu), heure des Tuvalu (Funafuti), heure du Bangladesh (Dhaka), heure du Bhoutan (Thimphu), heure du Brunéi (Brunei), heure du Cap-Vert, heure du centre de l’Australie (Adélaïde), heure du centre de l’Australie (Broken Hill), heure du centre de l’Australie (Darwin), heure du Centre indonésien (Macassar), heure du centre nord-américain, heure du centre nord-américain (Bahia de Banderas), heure du centre nord-américain (Belize), heure du centre nord-américain (Beulah (Dakota du Nord)), heure du centre nord-américain (Center (Dakota du Nord)), heure du centre nord-américain (Chicago), heure du centre nord-américain (Costa Rica), heure du centre nord-américain (El Salvador), heure du centre nord-américain (Guatemala), heure du centre nord-américain (Knox [Indiana]), heure du centre nord-américain (Managua), heure du centre nord-américain (Matamoros), heure du centre nord-américain (Menominee), heure du centre nord-américain (Mérida), heure du centre nord-américain (Mexico), heure du centre nord-américain (Monterrey), heure du centre nord-américain (New Salem (Dakota du Nord)), heure du centre nord-américain (Rainy River), heure du centre nord-américain (Rankin Inlet), heure du centre nord-américain (Regina), heure du centre nord-américain (Resolute), heure du centre nord-américain (Swift Current), heure du centre nord-américain (Tégucigalpa), heure du centre nord-américain (Tell City [Indiana]), heure du centre nord-américain (Winnipeg), heure du centre-ouest de l’Australie (Eucla), heure du Chili (Palmer), heure du Chili (Punta Arenas), heure du Chili (Santiago), heure du Golfe (Dubaï), heure du Golfe (Mascate), heure du Guyana, heure du Japon (Tokyo), heure du Kirghizistan (Bichkek), heure du Myanmar (Rangoun), heure du Népal (Katmandou), heure du Nord-Ouest du Mexique (Santa Isabel), heure du Pacifique mexicain (Chihuahua), heure du Pacifique mexicain (Hermosillo), heure du Pacifique mexicain (Mazatlán), heure du Pacifique nord-américain, heure du Pacifique nord-américain (Los Angeles), heure du Pacifique nord-américain (Tijuana), heure du Pacifique nord-américain (Vancouver), heure du Pakistan (Karachi), heure du Paraguay (Asunción), heure du Pérou (Lima), heure du Suriname (Paramaribo), heure du Tadjikistan (Douchanbé), heure du Timor oriental (Dili), heure du Turkménistan (Achgabat), heure du Vanuatu (Éfaté), heure du Venezuela (Caracas), heure moyenne de Greenwich, heure moyenne de Greenwich (Abidjan), heure moyenne de Greenwich (Accra), heure moyenne de Greenwich (Bamako), heure moyenne de Greenwich (Banjul), heure moyenne de Greenwich (Bissau), heure moyenne de Greenwich (Conakry), heure moyenne de Greenwich (Dakar), heure moyenne de Greenwich (Danmarkshavn), heure moyenne de Greenwich (Dublin), heure moyenne de Greenwich (Freetown), heure moyenne de Greenwich (Guernesey), heure moyenne de Greenwich (Île de Man), heure moyenne de Greenwich (Jersey), heure moyenne de Greenwich (Lomé), heure moyenne de Greenwich (Londres), heure moyenne de Greenwich (Monrovia), heure moyenne de Greenwich (Nouakchott), heure moyenne de Greenwich (Ouagadougou), heure moyenne de Greenwich (Reykjavik), heure moyenne de Greenwich (Sainte-Hélène), heure moyenne de Greenwich (São Tomé), heure moyenne de Greenwich (Troll), heure normale d’Afrique centrale (Blantyre), heure normale d’Afrique centrale (Bujumbura), heure normale d’Afrique centrale (Gaborone), heure normale d’Afrique centrale (Harare), heure normale d’Afrique centrale (Juba), heure normale d’Afrique centrale (Khartoum), heure normale d’Afrique centrale (Kigali), heure normale d’Afrique centrale (Lubumbashi), heure normale d’Afrique centrale (Lusaka), heure normale d’Afrique centrale (Maputo), heure normale d’Afrique centrale (Windhoek), heure normale d’Afrique de l’Est (Addis-Abeba), heure normale d’Afrique de l’Est (Antananarivo), heure normale d’Afrique de l’Est (Asmara), heure normale d’Afrique de l’Est (Comores), heure normale d’Afrique de l’Est (Dar es Salaam), heure normale d’Afrique de l’Est (Djibouti), heure normale d’Afrique de l’Est (Kampala), heure normale d’Afrique de l’Est (Mayotte), heure normale d’Afrique de l’Est (Mogadiscio), heure normale d’Afrique de l’Est (Nairobi), heure normale d’Afrique méridionale (Johannesburg), heure normale d’Afrique méridionale (Maseru), heure normale d’Afrique méridionale (Mbabane), heure normale du Yukon (Dawson), heure normale du Yukon (Whitehorse), temps universel coordonné From 6f9c7c02aed9295cc3ccdc0020584c1f8389d151 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Tue, 27 Dec 2022 12:48:47 +0100 Subject: [PATCH 025/812] Fix tests --- .../Tests/Fixtures/country_names.test | 28 +++++++++++-------- .../Tests/Fixtures/currency_names.test | 28 +++++++++++-------- .../Tests/Fixtures/language_names.test | 28 +++++++++++-------- .../Tests/Fixtures/locale_names.test | 28 +++++++++++-------- .../Tests/Fixtures/script_names.test | 12 +++++--- .../Tests/Fixtures/timezone_names.test | 28 +++++++++++-------- 6 files changed, 88 insertions(+), 64 deletions(-) diff --git a/extra/intl-extra/Tests/Fixtures/country_names.test b/extra/intl-extra/Tests/Fixtures/country_names.test index 042c87ac996..f3cb079741d 100644 --- a/extra/intl-extra/Tests/Fixtures/country_names.test +++ b/extra/intl-extra/Tests/Fixtures/country_names.test @@ -1,12 +1,16 @@ ---TEST-- -"country_names" function ---TEMPLATE-- -{{ country_names('UNKNOWN')|length }} -{{ country_names()|join(', ') }} -{{ country_names('fr')|join(', ') }} ---DATA-- -return []; ---EXPECT-- -0 -Afghanistan, Åland Islands, Albania, Algeria, American Samoa, Andorra, Angola, Anguilla, Antarctica, Antigua & Barbuda, Argentina, Armenia, Aruba, Australia, Austria, Azerbaijan, Bahamas, Bahrain, Bangladesh, Barbados, Belarus, Belgium, Belize, Benin, Bermuda, Bhutan, Bolivia, Bosnia & Herzegovina, Botswana, Bouvet Island, Brazil, British Indian Ocean Territory, British Virgin Islands, Brunei, Bulgaria, Burkina Faso, Burundi, Cambodia, Cameroon, Canada, Cape Verde, Caribbean Netherlands, Cayman Islands, Central African Republic, Chad, Chile, China, Christmas Island, Cocos (Keeling) Islands, Colombia, Comoros, Congo - Brazzaville, Congo - Kinshasa, Cook Islands, Costa Rica, Côte d’Ivoire, Croatia, Cuba, Curaçao, Cyprus, Czechia, Denmark, Djibouti, Dominica, Dominican Republic, Ecuador, Egypt, El Salvador, Equatorial Guinea, Eritrea, Estonia, Eswatini, Ethiopia, Falkland Islands, Faroe Islands, Fiji, Finland, France, French Guiana, French Polynesia, French Southern Territories, Gabon, Gambia, Georgia, Germany, Ghana, Gibraltar, Greece, Greenland, Grenada, Guadeloupe, Guam, Guatemala, Guernsey, Guinea, Guinea-Bissau, Guyana, Haiti, Heard & McDonald Islands, Honduras, Hong Kong SAR China, Hungary, Iceland, India, Indonesia, Iran, Iraq, Ireland, Isle of Man, Israel, Italy, Jamaica, Japan, Jersey, Jordan, Kazakhstan, Kenya, Kiribati, Kuwait, Kyrgyzstan, Laos, Latvia, Lebanon, Lesotho, Liberia, Libya, Liechtenstein, Lithuania, Luxembourg, Macao SAR China, Madagascar, Malawi, Malaysia, Maldives, Mali, Malta, Marshall Islands, Martinique, Mauritania, Mauritius, Mayotte, Mexico, Micronesia, Moldova, Monaco, Mongolia, Montenegro, Montserrat, Morocco, Mozambique, Myanmar (Burma), Namibia, Nauru, Nepal, Netherlands, New Caledonia, New Zealand, Nicaragua, Niger, Nigeria, Niue, Norfolk Island, North Korea, North Macedonia, Northern Mariana Islands, Norway, Oman, Pakistan, Palau, Palestinian Territories, Panama, Papua New Guinea, Paraguay, Peru, Philippines, Pitcairn Islands, Poland, Portugal, Puerto Rico, Qatar, Réunion, Romania, Russia, Rwanda, Samoa, San Marino, São Tomé & Príncipe, Saudi Arabia, Senegal, Serbia, Seychelles, Sierra Leone, Singapore, Sint Maarten, Slovakia, Slovenia, Solomon Islands, Somalia, South Africa, South Georgia & South Sandwich Islands, South Korea, South Sudan, Spain, Sri Lanka, St. Barthélemy, St. Helena, St. Kitts & Nevis, St. Lucia, St. Martin, St. Pierre & Miquelon, St. Vincent & Grenadines, Sudan, Suriname, Svalbard & Jan Mayen, Sweden, Switzerland, Syria, Taiwan, Tajikistan, Tanzania, Thailand, Timor-Leste, Togo, Tokelau, Tonga, Trinidad & Tobago, Tunisia, Turkey, Turkmenistan, Turks & Caicos Islands, Tuvalu, U.S. Outlying Islands, U.S. Virgin Islands, Uganda, Ukraine, United Arab Emirates, United Kingdom, United States, Uruguay, Uzbekistan, Vanuatu, Vatican City, Venezuela, Vietnam, Wallis & Futuna, Western Sahara, Yemen, Zambia, Zimbabwe -Afghanistan, Afrique du Sud, Albanie, Algérie, Allemagne, Andorre, Angola, Anguilla, Antarctique, Antigua-et-Barbuda, Arabie saoudite, Argentine, Arménie, Aruba, Australie, Autriche, Azerbaïdjan, Bahamas, Bahreïn, Bangladesh, Barbade, Belgique, Belize, Bénin, Bermudes, Bhoutan, Biélorussie, Bolivie, Bosnie-Herzégovine, Botswana, Brésil, Brunei, Bulgarie, Burkina Faso, Burundi, Cambodge, Cameroun, Canada, Cap-Vert, Chili, Chine, Chypre, Colombie, Comores, Congo-Brazzaville, Congo-Kinshasa, Corée du Nord, Corée du Sud, Costa Rica, Côte d’Ivoire, Croatie, Cuba, Curaçao, Danemark, Djibouti, Dominique, Égypte, Émirats arabes unis, Équateur, Érythrée, Espagne, Estonie, Eswatini, État de la Cité du Vatican, États-Unis, Éthiopie, Fidji, Finlande, France, Gabon, Gambie, Géorgie, Géorgie du Sud-et-les Îles Sandwich du Sud, Ghana, Gibraltar, Grèce, Grenade, Groenland, Guadeloupe, Guam, Guatemala, Guernesey, Guinée, Guinée équatoriale, Guinée-Bissau, Guyana, Guyane française, Haïti, Honduras, Hongrie, Île Bouvet, Île Christmas, Île de Man, Île Norfolk, Îles Åland, Îles Caïmans, Îles Cocos, Îles Cook, Îles Féroé, Îles Heard-et-MacDonald, Îles Malouines, Îles Mariannes du Nord, Îles Marshall, Îles mineures éloignées des États-Unis, Îles Pitcairn, Îles Salomon, Îles Turques-et-Caïques, Îles Vierges britanniques, Îles Vierges des États-Unis, Inde, Indonésie, Irak, Iran, Irlande, Islande, Israël, Italie, Jamaïque, Japon, Jersey, Jordanie, Kazakhstan, Kenya, Kirghizstan, Kiribati, Koweït, La Réunion, Laos, Lesotho, Lettonie, Liban, Liberia, Libye, Liechtenstein, Lituanie, Luxembourg, Macédoine du Nord, Madagascar, Malaisie, Malawi, Maldives, Mali, Malte, Maroc, Martinique, Maurice, Mauritanie, Mayotte, Mexique, Micronésie, Moldavie, Monaco, Mongolie, Monténégro, Montserrat, Mozambique, Myanmar (Birmanie), Namibie, Nauru, Népal, Nicaragua, Niger, Nigeria, Niue, Norvège, Nouvelle-Calédonie, Nouvelle-Zélande, Oman, Ouganda, Ouzbékistan, Pakistan, Palaos, Panama, Papouasie-Nouvelle-Guinée, Paraguay, Pays-Bas, Pays-Bas caribéens, Pérou, Philippines, Pologne, Polynésie française, Porto Rico, Portugal, Qatar, R.A.S. chinoise de Hong Kong, R.A.S. chinoise de Macao, République centrafricaine, République dominicaine, Roumanie, Royaume-Uni, Russie, Rwanda, Sahara occidental, Saint-Barthélemy, Saint-Christophe-et-Niévès, Saint-Marin, Saint-Martin, Saint-Martin (partie néerlandaise), Saint-Pierre-et-Miquelon, Saint-Vincent-et-les Grenadines, Sainte-Hélène, Sainte-Lucie, Salvador, Samoa, Samoa américaines, Sao Tomé-et-Principe, Sénégal, Serbie, Seychelles, Sierra Leone, Singapour, Slovaquie, Slovénie, Somalie, Soudan, Soudan du Sud, Sri Lanka, Suède, Suisse, Suriname, Svalbard et Jan Mayen, Syrie, Tadjikistan, Taïwan, Tanzanie, Tchad, Tchéquie, Terres australes françaises, Territoire britannique de l’océan Indien, Territoires palestiniens, Thaïlande, Timor oriental, Togo, Tokelau, Tonga, Trinité-et-Tobago, Tunisie, Turkménistan, Turquie, Tuvalu, Ukraine, Uruguay, Vanuatu, Venezuela, Viêt Nam, Wallis-et-Futuna, Yémen, Zambie, Zimbabwe +--TEST-- +"country_names" function +--TEMPLATE-- +{{ country_names('UNKNOWN')|length }} +{{ country_names()|length }} +{{ country_names('fr')|length }} +{{ country_names()['BE'] }} +{{ country_names('fr')['BE'] }} +--DATA-- +return []; +--EXPECT-- +0 +249 +249 +Belgium +Belgique diff --git a/extra/intl-extra/Tests/Fixtures/currency_names.test b/extra/intl-extra/Tests/Fixtures/currency_names.test index 47220290b00..bc2c54d0274 100644 --- a/extra/intl-extra/Tests/Fixtures/currency_names.test +++ b/extra/intl-extra/Tests/Fixtures/currency_names.test @@ -1,12 +1,16 @@ ---TEST-- -"currency_names" function ---TEMPLATE-- -{{ currency_names('UNKNOWN')|length }} -{{ currency_names()|join(', ') }} -{{ currency_names('fr')|join(', ') }} ---DATA-- -return []; ---EXPECT-- -0 -Afghan Afghani, Afghan Afghani (1927–2002), Albanian Lek, Albanian Lek (1946–1965), Algerian Dinar, Andorran Peseta, Angolan Kwanza, Angolan Kwanza (1977–1991), Angolan New Kwanza (1990–2000), Angolan Readjusted Kwanza (1995–1999), Argentine Austral, Argentine Peso, Argentine Peso (1881–1970), Argentine Peso (1983–1985), Argentine Peso Ley (1970–1983), Armenian Dram, Aruban Florin, Australian Dollar, Austrian Schilling, Azerbaijani Manat, Azerbaijani Manat (1993–2006), Bahamian Dollar, Bahraini Dinar, Bangladeshi Taka, Barbadian Dollar, Belarusian Ruble, Belarusian Ruble (1994–1999), Belarusian Ruble (2000–2016), Belgian Franc, Belgian Franc (convertible), Belgian Franc (financial), Belize Dollar, Bermudan Dollar, Bhutanese Ngultrum, Bolívar Soberano, Bolivian Boliviano, Bolivian Boliviano (1863–1963), Bolivian Mvdol, Bolivian Peso, Bosnia-Herzegovina Convertible Mark, Bosnia-Herzegovina Dinar (1992–1994), Bosnia-Herzegovina New Dinar (1994–1997), Botswanan Pula, Brazilian Cruzado (1986–1989), Brazilian Cruzeiro (1942–1967), Brazilian Cruzeiro (1990–1993), Brazilian Cruzeiro (1993–1994), Brazilian New Cruzado (1989–1990), Brazilian New Cruzeiro (1967–1986), Brazilian Real, British Pound, Brunei Dollar, Bulgarian Hard Lev, Bulgarian Lev, Bulgarian Lev (1879–1952), Bulgarian Socialist Lev, Burmese Kyat, Burundian Franc, Cambodian Riel, Canadian Dollar, Cape Verdean Escudo, Cayman Islands Dollar, Central African CFA Franc, CFP Franc, Chilean Escudo, Chilean Peso, Chilean Unit of Account (UF), Chinese People’s Bank Dollar, Chinese Yuan, Chinese Yuan (offshore), Colombian Peso, Colombian Real Value Unit, Comorian Franc, Congolese Franc, Costa Rican Colón, Croatian Dinar, Croatian Kuna, Cuban Convertible Peso, Cuban Peso, Cypriot Pound, Czech Koruna, Czechoslovak Hard Koruna, Danish Krone, Djiboutian Franc, Dominican Peso, Dutch Guilder, East Caribbean Dollar, East German Mark, Ecuadorian Sucre, Ecuadorian Unit of Constant Value, Egyptian Pound, Equatorial Guinean Ekwele, Eritrean Nakfa, Estonian Kroon, Ethiopian Birr, Euro, European Currency Unit, Falkland Islands Pound, Fijian Dollar, Finnish Markka, French Franc, French Gold Franc, French UIC-Franc, Gambian Dalasi, Georgian Kupon Larit, Georgian Lari, German Mark, Ghanaian Cedi, Ghanaian Cedi (1979–2007), Gibraltar Pound, Greek Drachma, Guatemalan Quetzal, Guinea-Bissau Peso, Guinean Franc, Guinean Syli, Guyanaese Dollar, Haitian Gourde, Honduran Lempira, Hong Kong Dollar, Hungarian Forint, Icelandic Króna, Icelandic Króna (1918–1981), Indian Rupee, Indonesian Rupiah, Iranian Rial, Iraqi Dinar, Irish Pound, Israeli New Shekel, Israeli Pound, Israeli Shekel (1980–1985), Italian Lira, Jamaican Dollar, Japanese Yen, Jordanian Dinar, Kazakhstani Tenge, Kenyan Shilling, Kuwaiti Dinar, Kyrgystani Som, Laotian Kip, Latvian Lats, Latvian Ruble, Lebanese Pound, Lesotho Loti, Liberian Dollar, Libyan Dinar, Lithuanian Litas, Lithuanian Talonas, Luxembourg Financial Franc, Luxembourgian Convertible Franc, Luxembourgian Franc, Macanese Pataca, Macedonian Denar, Macedonian Denar (1992–1993), Malagasy Ariary, Malagasy Franc, Malawian Kwacha, Malaysian Ringgit, Maldivian Rufiyaa, Maldivian Rupee (1947–1981), Malian Franc, Maltese Lira, Maltese Pound, Mauritanian Ouguiya, Mauritanian Ouguiya (1973–2017), Mauritian Rupee, Mexican Investment Unit, Mexican Peso, Mexican Silver Peso (1861–1992), Moldovan Cupon, Moldovan Leu, Monegasque Franc, Mongolian Tugrik, Moroccan Dirham, Moroccan Franc, Mozambican Escudo, Mozambican Metical, Mozambican Metical (1980–2006), Myanmar Kyat, Namibian Dollar, Nepalese Rupee, Netherlands Antillean Guilder, New Taiwan Dollar, New Zealand Dollar, Nicaraguan Córdoba, Nicaraguan Córdoba (1988–1991), Nigerian Naira, North Korean Won, Norwegian Krone, Omani Rial, Pakistani Rupee, Panamanian Balboa, Papua New Guinean Kina, Paraguayan Guarani, Peruvian Inti, Peruvian Sol, Peruvian Sol (1863–1965), Philippine Peso, Polish Zloty, Polish Zloty (1950–1995), Portuguese Escudo, Portuguese Guinea Escudo, Qatari Rial, Rhodesian Dollar, RINET Funds, Romanian Leu, Romanian Leu (1952–2006), Russian Ruble, Russian Ruble (1991–1998), Rwandan Franc, Salvadoran Colón, Samoan Tala, São Tomé & Príncipe Dobra, São Tomé & Príncipe Dobra (1977–2017), Saudi Riyal, Serbian Dinar, Serbian Dinar (2002–2006), Seychellois Rupee, Sierra Leonean Leone, Sierra Leonean New Leone, Singapore Dollar, Slovak Koruna, Slovenian Tolar, Solomon Islands Dollar, Somali Shilling, South African Rand, South African Rand (financial), South Korean Hwan (1953–1962), South Korean Won, South Korean Won (1945–1953), South Sudanese Pound, Soviet Rouble, Spanish Peseta, Spanish Peseta (A account), Spanish Peseta (convertible account), Sri Lankan Rupee, St. Helena Pound, Sudanese Dinar (1992–2007), Sudanese Pound, Sudanese Pound (1957–1998), Surinamese Dollar, Surinamese Guilder, Swazi Lilangeni, Swedish Krona, Swiss Franc, Syrian Pound, Tajikistani Ruble, Tajikistani Somoni, Tanzanian Shilling, Thai Baht, Timorese Escudo, Tongan Paʻanga, Trinidad & Tobago Dollar, Tunisian Dinar, Turkish Lira, Turkish Lira (1922–2005), Turkmenistani Manat, Turkmenistani Manat (1993–2009), Ugandan Shilling, Ugandan Shilling (1966–1987), Ukrainian Hryvnia, Ukrainian Karbovanets, United Arab Emirates Dirham, Uruguayan Nominal Wage Index Unit, Uruguayan Peso, Uruguayan Peso (1975–1993), Uruguayan Peso (Indexed Units), US Dollar, US Dollar (Next day), US Dollar (Same day), Uzbekistani Som, Vanuatu Vatu, Venezuelan Bolívar, Venezuelan Bolívar (1871–2008), Venezuelan Bolívar (2008–2018), Vietnamese Dong, Vietnamese Dong (1978–1985), West African CFA Franc, WIR Euro, WIR Franc, Yemeni Dinar, Yemeni Rial, Yugoslavian Convertible Dinar (1990–1992), Yugoslavian Hard Dinar (1966–1990), Yugoslavian New Dinar (1994–2002), Yugoslavian Reformed Dinar (1992–1993), Zairean New Zaire (1993–1998), Zairean Zaire (1971–1993), Zambian Kwacha, Zambian Kwacha (1968–2012), Zimbabwean Dollar (1980–2008), Zimbabwean Dollar (2008), Zimbabwean Dollar (2009) -afghani (1927–2002), afghani afghan, ancien leu roumain, Argentine Peso (1881–1970), Argentine Peso Ley (1970–1983), ariary malgache, austral argentin, baht thaïlandais, balboa panaméen, birr éthiopien, Bolívar Soberano, bolivar vénézuélien, bolivar vénézuélien (1871–2008), bolivar vénézuélien (2008–2018), Bolivian Boliviano (1863–1963), boliviano bolivien, Bosnia-Herzegovina New Dinar (1994–1997), Brazilian Cruzeiro (1942–1967), Bulgarian Lev (1879–1952), Bulgarian Socialist Lev, cédi, cédi ghanéen, Chilean Escudo, Chinese People’s Bank Dollar, colón costaricain, colón salvadorien, cordoba, córdoba oro nicaraguayen, coupon de lari géorgien, couronne danoise, couronne estonienne, couronne forte tchécoslovaque, couronne islandaise, couronne norvégienne, couronne slovaque, couronne suédoise, couronne tchèque, cruzado brésilien (1986–1989), cruzeiro, cruzeiro brésilien (1990–1993), dalasi gambien, denar macédonien, dinar algérien, dinar bahreïni, dinar bosniaque, dinar croate, dinar du Yémen, dinar irakien, dinar jordanien, dinar koweïtien, dinar libyen, dinar serbe, dinar serbo-monténégrin, dinar soudanais, dinar tunisien, dinar yougoslave convertible, dinar yougoslave Noviy, dirham des Émirats arabes unis, dirham marocain, dobra santoméen, dobra santoméen (1977–2017), dollar australien, dollar bahaméen, dollar barbadien, dollar bélizéen, dollar bermudien, dollar brunéien, dollar canadien, dollar de Hong Kong, dollar de Singapour, dollar de Trinité-et-Tobago, dollar des Caraïbes orientales, dollar des États-Unis, dollar des Etats-Unis (jour même), dollar des Etats-Unis (jour suivant), dollar des îles Caïmans, dollar des îles Salomon, dollar du Guyana, dollar fidjien, dollar jamaïcain, dollar libérien, dollar namibien, dollar néo-zélandais, dollar rhodésien, dollar surinamais, dollar zimbabwéen, dollar zimbabwéen (2008), dollar zimbabwéen (2009), dông vietnamien, drachme grecque, dram arménien, ekwélé équatoguinéen, escudo capverdien, escudo de Guinée portugaise, escudo mozambicain, escudo portugais, escudo timorais, euro, euro WIR, florin antillais, florin arubais, florin néerlandais, florin surinamais, forint hongrois, franc belge, franc belge (convertible), franc belge (financier), franc burundais, franc CFA (BCEAO), franc CFA (BEAC), franc CFP, franc comorien, franc congolais, franc convertible luxembourgeois, franc djiboutien, franc financier luxembourgeois, franc français, franc guinéen, franc luxembourgeois, franc malgache, franc malien, franc marocain, franc or, franc rwandais, franc suisse, franc UIC, franc WIR, gourde haïtienne, guaraní paraguayen, hryvnia ukrainienne, Icelandic Króna (1918–1981), inti péruvien, Israeli Shekel (1980–1985), karbovanetz, kina papouan-néo-guinéen, kip loatien, kuna croate, kwacha malawite, kwacha zambien, kwacha zambien (1968–2012), kwanza angolais, kwanza angolais (1977–1990), kwanza angolais réajusté (1995–1999), kyat birman, kyat myanmarais, lari géorgien, lats letton, lek albanais, lek albanais (1947–1961), lempira hondurien, leone sierra-léonais, leu moldave, leu roumain, lev bulgare, lev bulgare (1962–1999), lilangeni swazi, lire italienne, lire maltaise, litas lituanien, livre chypriote, livre de Gibraltar, livre de Sainte-Hélène, livre des îles Malouines, livre égyptienne, livre irlandaise, livre israélienne, livre libanaise, livre maltaise, livre soudanaise, livre soudanaise (1956–2007), livre sterling, livre sud-soudanaise, livre syrienne, livre turque, livre turque (1844–2005), loti lesothan, Macedonian Denar (1992–1993), Maldivian Rupee (1947–1981), manat azéri, manat azéri (1993–2006), manat turkmène, mark allemand, mark convertible bosniaque, mark est-allemand, mark finlandais, métical, metical mozambicain, Moldovan Cupon, Monegasque Franc, mvdol bolivien, nafka érythréen, naira nigérian, ngultrum bouthanais, nouveau cruzado, nouveau cruzeiro brésilien (1967–1986), nouveau dinar yougoslave, nouveau dollar taïwanais, nouveau kwanza angolais (1990–2000), nouveau manat turkmène, nouveau rouble biélorusse (1994–1999), nouveau shekel israélien, nouveau zaïre zaïrien, ouguiya mauritanien, ouguiya mauritanien (1973–2017), pa’anga tongan, pataca macanaise, peseta andorrane, peseta espagnole, peseta espagnole (compte A), peseta espagnole (compte convertible), peso argentin, peso argentin (1983–1985), peso bissau-guinéen, peso bolivien, peso chilien, peso colombien, peso cubain, peso cubain convertible, peso d’argent mexicain (1861–1992), peso dominicain, peso mexicain, peso philippin, peso uruguayen, peso uruguayen (1975–1993), peso uruguayen (unités indexées), pula botswanais, quetzal guatémaltèque, rand sud-africain, rand sud-africain (financier), réal brésilien, riel cambodgien, ringgit malais, riyal iranien, riyal omanais, riyal qatari, riyal saoudien, riyal yéménite, rouble biélorusse, rouble biélorusse (2000–2016), rouble letton, rouble russe, rouble russe (1991–1998), rouble soviétique, rouble tadjik, roupie des Seychelles, roupie indienne, roupie indonésienne, roupie mauricienne, roupie népalaise, roupie pakistanaise, roupie srilankaise, rufiyaa maldivien, schilling autrichien, shilling kényan, shilling ougandais, shilling ougandais (1966–1987), shilling somalien, shilling tanzanien, Sierra Leonean New Leone, sol péruvien, sol péruvien (1863–1985), som kirghize, somoni tadjik, South Korean Hwan (1953–1962), South Korean Won (1945–1953), sucre équatorien, sum ouzbek, syli guinéen, taka bangladeshi, tala samoan, talonas lituanien, tenge kazakh, tolar slovène, tugrik mongol, type de fonds RINET, unité d’investissement chilienne, unité de compte européenne (ECU), unité de conversion mexicaine (UDI), unité de valeur constante équatoriale (UVC), unité de valeur réelle colombienne, Uruguayan Nominal Wage Index Unit, vatu vanuatuan, Vietnamese Dong (1978–1985), won nord-coréen, won sud-coréen, yen japonais, yuan chinois (zone extracôtière), yuan renminbi chinois, Yugoslavian Reformed Dinar (1992–1993), zaïre zaïrois, zloty (1950–1995), zloty polonais +--TEST-- +"currency_names" function +--TEMPLATE-- +{{ currency_names('UNKNOWN')|length }} +{{ currency_names()|length }} +{{ currency_names('fr')|length }} +{{ currency_names()['USD'] }} +{{ currency_names('fr')['USD'] }} +--DATA-- +return []; +--EXPECT-- +0 +292 +292 +US Dollar +dollar des États-Unis diff --git a/extra/intl-extra/Tests/Fixtures/language_names.test b/extra/intl-extra/Tests/Fixtures/language_names.test index 871a60991b8..bd30607f8a4 100644 --- a/extra/intl-extra/Tests/Fixtures/language_names.test +++ b/extra/intl-extra/Tests/Fixtures/language_names.test @@ -1,12 +1,16 @@ ---TEST-- -"language_names" function ---TEMPLATE-- -{{ language_names('UNKNOWN')|length }} -{{ language_names()|join(', ') }} -{{ language_names('fr')|join(', ') }} ---DATA-- -return []; ---EXPECT-- -0 -Abkhazian, Achinese, Acoli, Adangme, Adyghe, Afar, Afrihili, Afrikaans, Aghem, Ainu, Akan, Akkadian, Akoose, Alabama, Albanian, Aleut, Algerian Arabic, American Sign Language, Amharic, Ancient Egyptian, Ancient Greek, Angika, Ao Naga, Arabic, Aragonese, Aramaic, Araona, Arapaho, Arawak, Armenian, Aromanian, Arpitan, Assamese, Asturian, Asu, Atikamekw, Atsam, Avaric, Avestan, Awadhi, Aymara, Azerbaijani, Badaga, Bafia, Bafut, Bakhtiari, Balinese, Baluchi, Bambara, Bamun, Bangla, Banjar, Basaa, Bashkir, Basque, Batak Toba, Bavarian, Beja, Belarusian, Bemba, Bena, Betawi, Bhojpuri, Bikol, Bini, Bishnupriya, Bislama, Blin, Blissymbols, Bodo, Bosnian, Brahui, Braj, Breton, Buginese, Bulgarian, Bulu, Buriat, Burmese, Caddo, Cajun French, Cantonese, Capiznon, Carib, Carolina Algonquian, Catalan, Cayuga, Cebuano, Central Atlas Tamazight, Central Dusun, Central Kurdish, Central Ojibwa, Central Yupik, Chadian Arabic, Chagatai, Chakma, Chamorro, Chechen, Cherokee, Cheyenne, Chibcha, Chickasaw, Chiga, Chilcotin, Chimborazo Highland Quichua, Chinese, Chinook Jargon, Chipewyan, Choctaw, Church Slavic, Chuukese, Chuvash, Classical Newari, Classical Syriac, Colognian, Comorian, Coptic, Cornish, Corsican, Cree, Crimean Tatar, Croatian, Czech, Dakota, Danish, Dargwa, Dazaga, Delaware, Dinka, Divehi, Dogri, Dogrib, Duala, Dutch, Dyula, Dzongkha, Eastern Canadian Inuktitut, Eastern Frisian, Eastern Ojibwa, Efik, Egyptian Arabic, Ekajuk, Elamite, Embu, Emilian, English, Erzya, Esperanto, Estonian, Ewe, Ewondo, Extremaduran, Fang, Fanti, Faroese, Fiji Hindi, Fijian, Filipino, Finnish, Fon, Frafra, French, Friulian, Fulah, Ga, Gagauz, Galician, Gan Chinese, Ganda, Gayo, Gbaya, Geez, Georgian, German, Gheg Albanian, Ghomala, Gilaki, Gilbertese, Goan Konkani, Gondi, Gorontalo, Gothic, Grebo, Greek, Guarani, Gujarati, Gusii, Gwichʼin, Haida, Haitian Creole, Hakka Chinese, Halkomelem, Hausa, Hawaiian, Hebrew, Herero, Hiligaynon, Hindi, Hiri Motu, Hittite, Hmong, Hmong Njua, Hungarian, Hupa, Iban, Ibibio, Icelandic, Ido, Igbo, Iloko, Inari Sami, Indonesian, Ingrian, Ingush, Innu-aimun, Interlingua, Interlingue, Inuktitut, Inupiaq, Irish, Italian, Jamaican Creole English, Japanese, Javanese, Jju, Jola-Fonyi, Judeo-Arabic, Judeo-Persian, Jutish, Kabardian, Kabuverdianu, Kabyle, Kachin, Kaingang, Kako, Kalaallisut, Kalenjin, Kalmyk, Kamba, Kanembu, Kannada, Kanuri, Kara-Kalpak, Karachay-Balkar, Karelian, Kashmiri, Kashubian, Kawi, Kazakh, Kenyang, Khasi, Khmer, Khotanese, Khowar, Kikuyu, Kimbundu, Kinaray-a, Kinyarwanda, Kirmanjki, Klingon, Kom, Komi, Komi-Permyak, Kongo, Konkani, Korean, Koro, Kosraean, Kotava, Koyra Chiini, Koyraboro Senni, Kpelle, Krio, Kuanyama, Kumyk, Kurdish, Kurukh, Kutenai, Kwakʼwala, Kwasio, Kyrgyz, Kʼicheʼ, Ladino, Lahnda, Lakota, Lamba, Langi, Lao, Latgalian, Latin, Latvian, Laz, Lezghian, Ligurian, Lillooet, Limburgish, Lingala, Lingua Franca Nova, Literary Chinese, Lithuanian, Livonian, Lojban, Lombard, Louisiana Creole, Low German, Lower Silesian, Lower Sorbian, Lozi, Luba-Katanga, Luba-Lulua, Luiseno, Lule Sami, Lunda, Luo, Luxembourgish, Luyia, Maba, Macedonian, Machame, Madurese, Mafa, Magahi, Main-Franconian, Maithili, Makasar, Makhuwa-Meetto, Makonde, Malagasy, Malay, Malayalam, Malecite, Maltese, Manchu, Mandar, Mandingo, Manipuri, Manx, Māori, Mapuche, Marathi, Mari, Marshallese, Marwari, Masai, Mazanderani, Medumba, Mende, Mentawai, Meru, Metaʼ, Mi'kmaq, Michif, Middle Dutch, Middle English, Middle French, Middle High German, Middle Irish, Min Nan Chinese, Minangkabau, Mingrelian, Mirandese, Mizo, Mohawk, Moksha, Mongo, Mongolian, Moose Cree, Morisyen, Moroccan Arabic, Mossi, Mundang, Muscogee, Muslim Tat, Myene, N’Ko, Najdi Arabic, Nama, Nauru, Navajo, Ndonga, Neapolitan, Nepali, Newari, Ngambay, Ngiemboon, Ngomba, Nheengatu, Nias, Nigerian Pidgin, Niuean, Nogai, North Ndebele, Northern East Cree, Northern Frisian, Northern Haida, Northern Luri, Northern Sami, Northern Sotho, Northern Tutchone, Northwestern Ojibwa, Norwegian, Norwegian Bokmål, Norwegian Nynorsk, Novial, Nuer, Nyamwezi, Nyanja, Nyankole, Nyasa Tonga, Nyoro, Nzima, Occitan, Odia, Oji-Cree, Ojibwa, Okanagan, Old English, Old French, Old High German, Old Irish, Old Norse, Old Persian, Old Provençal, Oromo, Osage, Ossetic, Ottoman Turkish, Pahlavi, Palatine German, Palauan, Pali, Pampanga, Pangasinan, Papiamento, Pashto, Pennsylvania German, Persian, Phoenician, Picard, Piedmontese, Plains Cree, Plautdietsch, Pohnpeian, Polish, Pontic, Portuguese, Prussian, Punjabi, Quechua, Rajasthani, Rapanui, Rarotongan, Riffian, Rohingya, Romagnol, Romanian, Romansh, Romany, Rombo, Rotuman, Roviana, Rundi, Russian, Rusyn, Rwa, Saho, Sakha, Samaritan Aramaic, Samburu, Samoan, Samogitian, Sandawe, Sango, Sangu, Sanskrit, Santali, Sardinian, Sasak, Sassarese Sardinian, Saterland Frisian, Saurashtra, Scots, Scottish Gaelic, Selayar, Selkup, Sena, Seneca, Serbian, Serbo-Croatian, Serer, Seri, Seselwa Creole French, Shambala, Shan, Shona, Sichuan Yi, Sicilian, Sidamo, Siksika, Silesian, Sindhi, Sinhala, Skolt Sami, Slave, Slovak, Slovenian, Soga, Sogdien, Somali, Soninke, South Ndebele, Southern Altai, Southern East Cree, Southern Haida, Southern Kurdish, Southern Lushootseed, Southern Sami, Southern Sotho, Southern Tutchone, Spanish, Sranan Tongo, Standard Moroccan Tamazight, Straits Salish, Sukuma, Sumerian, Sundanese, Susu, Swahili, Swampy Cree, Swati, Swedish, Swiss German, Syriac, Tachelhit, Tagalog, Tagish, Tahitian, Tahltan, Tai Dam, Taita, Tajik, Talysh, Tamashek, Tamil, Taroko, Tasawaq, Tatar, Telugu, Tereno, Teso, Tetum, Thai, Tibetan, Tigre, Tigrinya, Timne, Tiv, Tlingit, Tok Pisin, Tokelau, Tongan, Tornedalen Finnish, Torwali, Tsakhur, Tsakonian, Tsimshian, Tsonga, Tswana, Tulu, Tumbuka, Tunisian Arabic, Turkish, Turkmen, Turoyo, Tuvalu, Tuvinian, Twi, Tyap, Udmurt, Ugaritic, Ukrainian, Umbundu, Upper Sorbian, Urdu, Uyghur, Uzbek, Vai, Venda, Venetian, Veps, Vietnamese, Volapük, Võro, Votic, Vunjo, Walloon, Walser, Waray, Warlpiri, Washo, Wayuu, Welsh, West Flemish, Western Balochi, Western Canadian Inuktitut, Western Frisian, Western Mari, Western Ojibwa, Wolaytta, Wolof, Woods Cree, Wu Chinese, Xhosa, Xiang Chinese, Yangben, Yao, Yapese, Yemba, Yiddish, Yoruba, Zapotec, Zarma, Zaza, Zeelandic, Zenaga, Zhuang, Zoroastrian Dari, Zulu, Zuni -abkhaze, aceh, acoli, adangme, adyguéen, afar, afrihili, afrikaans, aghem, aïnou, akan, akkadien, akoose, alabama, albanais, aléoute, allemand, allemand palatin, altaï du Sud, amazighe de l’Atlas central, amazighe standard marocain, amharique, ancien anglais, ancien français, ancien haut allemand, ancien irlandais, angika, anglais, Ao, arabe, arabe algérien, arabe égyptien, arabe marocain, arabe najdi, arabe tchadien, arabe tunisien, aragonais, araméen, araméen samaritain, araona, arapaho, arawak, arménien, aroumain, assamais, asturien, asu, Atikamekw, atsam, avar, avestique, awadhi, aymara, azerbaïdjanais, bachkir, badaga, bafia, bafut, bakhtiari, balinais, baloutchi, baloutchi occidental, bambara, bamoun, banjar, bas-allemand, bas-prussien, bas-silésien, bas-sorabe, basque, bassa, batak toba, bavarois, bedja, bemba, bena, bengali, betawi, bhodjpouri, bichelamar, biélorusse, bikol, bini, birman, bishnupriya, blin, bodo, bosniaque, boulou, bouriate, brahoui, braj, breton, bugi, bulgare, cachemiri, caddo, caingangue, cantonais, capiznon, capverdien, carélien, caribe, Carolina Algonquian, catalan, cayuga, cebuano, Central Ojibwa, chamorro, changma kodha, cherokee, chewa, cheyenne, chibcha, Chickasaw, Chilcotin, chinois, chinois littéraire, chipewyan, chleuh, choctaw, chuuk, cingalais, cisena, comorien, copte, coréen, cornique, corse, cree, creek, créole haïtien, créole jamaïcain, créole louisianais, créole mauricien, créole seychellois, croate, dakota, danois, dargwa, dari zoroastrien, dazaga, delaware, dinka, diola-fogny, dioula, dogri, dogrib, douala, dusun central, dzongkha, Eastern Canadian Inuktitut, Eastern Ojibwa, écossais, éfik, égyptien ancien, ékadjouk, élamite, embu, émilien, erzya, esclave, espagnol, espéranto, estonien, estrémègne, éwé, éwondo, fang, fanti, féroïen, fidjien, filipino, finnois, finnois tornédalien, flamand occidental, fon, français, français cadien, franconien du Main, francoprovençal, frioulan, frison du Nord, frison occidental, frison oriental, ga, gaélique écossais, gagaouze, galicien, gallois, gan, ganda, gayo, gbaya, géorgien, ghomalaʼ, gilaki, gilbertin, gondi, gorontalo, gotique, goudjarati, grebo, grec, grec ancien, groenlandais, guarani, guègue, guèze, gurenne, gusii, gwichʼin, haida, hakka, Halkomelem, haoussa, haut-sorabe, hawaïen, hébreu, héréro, hiligaynon, hindi, hindi fidjien, hiri motu, hittite, hmong, Hmong Njua, hongrois, hupa, iakoute, iban, ibibio, ido, igbo, ilocano, indonésien, ingouche, ingrien, Innu-aimun, interlingua, interlingue, inuktitut, inupiaq, irlandais, isangu, islandais, italien, japonais, jargon chinook, javanais, jju, judéo-arabe, judéo-persan, jute, kabarde, kabyle, kachin, kachoube, kako, kalendjin, kalmouk, kamba, kanembou, kannada, kanouri, karakalpak, karatchaï balkar, kawi, kazakh, kényang, khasi, khmer, khotanais, khowar, kiga, kikongo, kikuyu, kimboundou, kinaray-a, kinyarwanda, kirghize, kirmanjki, klingon, kölsch, kom, komi, komi-permiak, konkani, konkani de Goa, koro, kosraéen, kotava, koumyk, kouroukh, koyra chiini, koyraboro senni, kpellé, krio, kuanyama, kurde, kurde du Sud, kutenai, Kwakʼwala, ladino, lahnda, lakota, lamba, langi, langue des signes américaine, lao, latgalien, latin, laze, letton, lezghien, ligure, Lillooet, limbourgeois, lingala, lingua franca nova, lituanien, livonien, lojban, lombard, lori du Nord, lozi, luba-kasaï (ciluba), luba-katanga (kiluba), luiseño, lunda, luo, lushaï, luxembourgeois, luyia, maasaï, maba, macédonien, madurais, mafa, magahi, maïthili, makassar, makondé, makua, malais, malayalam, maldivien, Malecite, malgache, maltais, mandar, mandchou, mandingue, manipuri, mannois, maori, mapuche, marathi, mari, mari occidental, marshallais, marwarî, matchamé, mazandérani, médumba, mendé, mentawaï, meru, metaʼ, Michif, micmac, minangkabau, mingrélien, minnan, mirandais, mohawk, mokcha, mongo, mongol, Moose Cree, moré, moundang, moyen anglais, moyen français, moyen haut-allemand, moyen irlandais, moyen néerlandais, myènè, n’ko, nama, napolitain, nauruan, navajo, ndébélé du Nord, ndébélé du Sud, ndonga, néerlandais, népalais, newari, newarî classique, ngambay, ngiemboon, ngomba, ngoumba, nheengatou, niha, niuéen, nogaï, Northern East Cree, Northern Haida, Northern Tutchone, Northwestern Ojibwa, norvégien, norvégien bokmål, norvégien nynorsk, novial, nuer, nyamwezi, nyankolé, nyoro, nzema, occitan, odia, Oji-Cree, ojibwa, Okanagan, oromo, osage, ossète, oudmourte, ougaritique, ouïghour, ourdou, ouzbek, pachto, pahlavi, palau, pali, pampangan, pangasinan, papiamento, pendjabi, pennsilfaanisch, persan, persan ancien, peul, phénicien, picard, pidgin nigérian, piémontais, Plains Cree, pohnpei, polonais, pontique, portugais, provençal ancien, prussien, quechua, quiché, quichua du Haut-Chimborazo, rajasthani, rapanui, rarotongien, rifain, rohingya, romagnol, romanche, romani, rombo, rotuman, roumain, roundi, roviana, russe, ruthène, rwa, saho, samburu, same d’Inari, same de Lule, same du Nord, same du Sud, same skolt, samoan, samogitien, sandawe, sango, sanskrit, santali, sarde, sarde sassarais, sasak, saterlandais, saurashtra, sélayar, selkoupe, seneca, serbe, serbo-croate, sérère, séri, shambala, shan, shona, sicilien, sidamo, siksika, silésien, sindhi, slavon d’église, slovaque, slovène, soga, sogdien, somali, soninké, sorani, sotho du Nord, sotho du Sud, soukouma, soundanais, soussou, Southern East Cree, Southern Haida, Southern Lushootseed, Southern Tutchone, sranan tongo, Straits Salish, suédois, suisse allemand, sumérien, swahili, Swampy Cree, swati, symboles Bliss, syriaque, syriaque classique, tadjik, tagalog, Tagish, tahitien, Tahltan, Tai Dam, taita, talysh, tamacheq, tamoul, taroko, tasawaq, tatar, tatar de Crimée, tati caucasien, tchaghataï, tchèque, tchétchène, tchouvache, télougou, tereno, teso, tétoum, thaï, tibétain, tigré, tigrigna, timné, tiv, tlingit, tok pisin, tokelau, tonga nyasa, tongien, Torwali, toulou, touroyo, touvain, tsakhour, tsakonien, tsimshian, tsonga, tswana, tumbuka, turc, turc ottoman, turkmène, tuvalu, twi, tyap, ukrainien, umbundu, vaï, venda, vénitien, vepse, vietnamien, vieux norrois, volapük, võro, vote, vunjo, walamo, wallon, walser, waray, warlpiri, washo, wayuu, Western Canadian Inuktitut, Western Ojibwa, wolof, Woods Cree, wu, xhosa, xiang, yangben, yao, yapois, yemba, yi du Sichuan, yiddish, yoruba, youpik central, zapotèque, zarma, zazaki, zélandais, zenaga, zhuang, zoulou, zuñi +--TEST-- +"language_names" function +--TEMPLATE-- +{{ language_names('UNKNOWN')|length }} +{{ language_names()|length }} +{{ language_names('fr')|length }} +{{ language_names()['fr'] }} +{{ language_names('fr')['fr'] }} +--DATA-- +return []; +--EXPECT-- +0 +634 +634 +French +français diff --git a/extra/intl-extra/Tests/Fixtures/locale_names.test b/extra/intl-extra/Tests/Fixtures/locale_names.test index bdf2e68b1db..f7e830f6693 100644 --- a/extra/intl-extra/Tests/Fixtures/locale_names.test +++ b/extra/intl-extra/Tests/Fixtures/locale_names.test @@ -1,12 +1,16 @@ ---TEST-- -"locale_names" function ---TEMPLATE-- -{{ locale_names('UNKNOWN')|length }} -{{ locale_names()|join(', ') }} -{{ locale_names('fr')|join(', ') }} ---DATA-- -return []; ---EXPECT-- -0 -Afrikaans, Afrikaans (Namibia), Afrikaans (South Africa), Akan, Akan (Ghana), Albanian, Albanian (Albania), Albanian (North Macedonia), Amharic, Amharic (Ethiopia), Arabic, Arabic (Algeria), Arabic (Bahrain), Arabic (Chad), Arabic (Comoros), Arabic (Djibouti), Arabic (Egypt), Arabic (Eritrea), Arabic (Iraq), Arabic (Israel), Arabic (Jordan), Arabic (Kuwait), Arabic (Lebanon), Arabic (Libya), Arabic (Mauritania), Arabic (Morocco), Arabic (Oman), Arabic (Palestinian Territories), Arabic (Qatar), Arabic (Saudi Arabia), Arabic (Somalia), Arabic (South Sudan), Arabic (Sudan), Arabic (Syria), Arabic (Tunisia), Arabic (United Arab Emirates), Arabic (Western Sahara), Arabic (world), Arabic (Yemen), Armenian, Armenian (Armenia), Assamese, Assamese (India), Azerbaijani, Azerbaijani (Azerbaijan), Azerbaijani (Cyrillic, Azerbaijan), Azerbaijani (Cyrillic), Azerbaijani (Latin, Azerbaijan), Azerbaijani (Latin), Bambara, Bambara (Mali), Bangla, Bangla (Bangladesh), Bangla (India), Basque, Basque (Spain), Belarusian, Belarusian (Belarus), Bosnian, Bosnian (Bosnia & Herzegovina), Bosnian (Cyrillic, Bosnia & Herzegovina), Bosnian (Cyrillic), Bosnian (Latin, Bosnia & Herzegovina), Bosnian (Latin), Breton, Breton (France), Bulgarian, Bulgarian (Bulgaria), Burmese, Burmese (Myanmar [Burma]), Catalan, Catalan (Andorra), Catalan (France), Catalan (Italy), Catalan (Spain), Chechen, Chechen (Russia), Chinese, Chinese (China), Chinese (Hong Kong SAR China), Chinese (Macao SAR China), Chinese (Simplified, China), Chinese (Simplified, Hong Kong SAR China), Chinese (Simplified, Macao SAR China), Chinese (Simplified, Singapore), Chinese (Simplified), Chinese (Singapore), Chinese (Taiwan), Chinese (Traditional, Hong Kong SAR China), Chinese (Traditional, Macao SAR China), Chinese (Traditional, Taiwan), Chinese (Traditional), Cornish, Cornish (United Kingdom), Croatian, Croatian (Bosnia & Herzegovina), Croatian (Croatia), Czech, Czech (Czechia), Danish, Danish (Denmark), Danish (Greenland), Dutch, Dutch (Aruba), Dutch (Belgium), Dutch (Caribbean Netherlands), Dutch (Curaçao), Dutch (Netherlands), Dutch (Sint Maarten), Dutch (Suriname), Dzongkha, Dzongkha (Bhutan), English, English (American Samoa), English (Anguilla), English (Antigua & Barbuda), English (Australia), English (Austria), English (Bahamas), English (Barbados), English (Belgium), English (Belize), English (Bermuda), English (Botswana), English (British Indian Ocean Territory), English (British Virgin Islands), English (Burundi), English (Cameroon), English (Canada), English (Cayman Islands), English (Christmas Island), English (Cocos [Keeling] Islands), English (Cook Islands), English (Cyprus), English (Denmark), English (Dominica), English (Eritrea), English (Eswatini), English (Europe), English (Falkland Islands), English (Fiji), English (Finland), English (Gambia), English (Germany), English (Ghana), English (Gibraltar), English (Grenada), English (Guam), English (Guernsey), English (Guyana), English (Hong Kong SAR China), English (India), English (Ireland), English (Isle of Man), English (Israel), English (Jamaica), English (Jersey), English (Kenya), English (Kiribati), English (Lesotho), English (Liberia), English (Macao SAR China), English (Madagascar), English (Malawi), English (Malaysia), English (Maldives), English (Malta), English (Marshall Islands), English (Mauritius), English (Micronesia), English (Montserrat), English (Namibia), English (Nauru), English (Netherlands), English (New Zealand), English (Nigeria), English (Niue), English (Norfolk Island), English (Northern Mariana Islands), English (Pakistan), English (Palau), English (Papua New Guinea), English (Philippines), English (Pitcairn Islands), English (Puerto Rico), English (Rwanda), English (Samoa), English (Seychelles), English (Sierra Leone), English (Singapore), English (Sint Maarten), English (Slovenia), English (Solomon Islands), English (South Africa), English (South Sudan), English (St. Helena), English (St. Kitts & Nevis), English (St. Lucia), English (St. Vincent & Grenadines), English (Sudan), English (Sweden), English (Switzerland), English (Tanzania), English (Tokelau), English (Tonga), English (Trinidad & Tobago), English (Turks & Caicos Islands), English (Tuvalu), English (U.S. Outlying Islands), English (U.S. Virgin Islands), English (Uganda), English (United Arab Emirates), English (United Kingdom), English (United States), English (Vanuatu), English (world), English (Zambia), English (Zimbabwe), Esperanto, Esperanto (world), Estonian, Estonian (Estonia), Ewe, Ewe (Ghana), Ewe (Togo), Faroese, Faroese (Denmark), Faroese (Faroe Islands), Finnish, Finnish (Finland), French, French (Algeria), French (Belgium), French (Benin), French (Burkina Faso), French (Burundi), French (Cameroon), French (Canada), French (Central African Republic), French (Chad), French (Comoros), French (Congo - Brazzaville), French (Congo - Kinshasa), French (Côte d’Ivoire), French (Djibouti), French (Equatorial Guinea), French (France), French (French Guiana), French (French Polynesia), French (Gabon), French (Guadeloupe), French (Guinea), French (Haiti), French (Luxembourg), French (Madagascar), French (Mali), French (Martinique), French (Mauritania), French (Mauritius), French (Mayotte), French (Monaco), French (Morocco), French (New Caledonia), French (Niger), French (Réunion), French (Rwanda), French (Senegal), French (Seychelles), French (St. Barthélemy), French (St. Martin), French (St. Pierre & Miquelon), French (Switzerland), French (Syria), French (Togo), French (Tunisia), French (Vanuatu), French (Wallis & Futuna), Fulah, Fulah (Adlam, Burkina Faso), Fulah (Adlam, Cameroon), Fulah (Adlam, Gambia), Fulah (Adlam, Ghana), Fulah (Adlam, Guinea-Bissau), Fulah (Adlam, Guinea), Fulah (Adlam, Liberia), Fulah (Adlam, Mauritania), Fulah (Adlam, Niger), Fulah (Adlam, Nigeria), Fulah (Adlam, Senegal), Fulah (Adlam, Sierra Leone), Fulah (Adlam), Fulah (Cameroon), Fulah (Guinea), Fulah (Latin, Burkina Faso), Fulah (Latin, Cameroon), Fulah (Latin, Gambia), Fulah (Latin, Ghana), Fulah (Latin, Guinea-Bissau), Fulah (Latin, Guinea), Fulah (Latin, Liberia), Fulah (Latin, Mauritania), Fulah (Latin, Niger), Fulah (Latin, Nigeria), Fulah (Latin, Senegal), Fulah (Latin, Sierra Leone), Fulah (Latin), Fulah (Mauritania), Fulah (Senegal), Galician, Galician (Spain), Ganda, Ganda (Uganda), Georgian, Georgian (Georgia), German, German (Austria), German (Belgium), German (Germany), German (Italy), German (Liechtenstein), German (Luxembourg), German (Switzerland), Greek, Greek (Cyprus), Greek (Greece), Gujarati, Gujarati (India), Hausa, Hausa (Ghana), Hausa (Niger), Hausa (Nigeria), Hebrew, Hebrew (Israel), Hindi, Hindi (India), Hindi (Latin, India), Hindi (Latin), Hungarian, Hungarian (Hungary), Icelandic, Icelandic (Iceland), Igbo, Igbo (Nigeria), Indonesian, Indonesian (Indonesia), Interlingua, Interlingua (world), Irish, Irish (Ireland), Irish (United Kingdom), Italian, Italian (Italy), Italian (San Marino), Italian (Switzerland), Italian (Vatican City), Japanese, Japanese (Japan), Javanese, Javanese (Indonesia), Kalaallisut, Kalaallisut (Greenland), Kannada, Kannada (India), Kashmiri, Kashmiri (Arabic, India), Kashmiri (Arabic), Kashmiri (Devanagari, India), Kashmiri (Devanagari), Kashmiri (India), Kazakh, Kazakh (Kazakhstan), Khmer, Khmer (Cambodia), Kikuyu, Kikuyu (Kenya), Kinyarwanda, Kinyarwanda (Rwanda), Korean, Korean (North Korea), Korean (South Korea), Kurdish, Kurdish (Turkey), Kyrgyz, Kyrgyz (Kyrgyzstan), Lao, Lao (Laos), Latvian, Latvian (Latvia), Lingala, Lingala (Angola), Lingala (Central African Republic), Lingala (Congo - Brazzaville), Lingala (Congo - Kinshasa), Lithuanian, Lithuanian (Lithuania), Luba-Katanga, Luba-Katanga (Congo - Kinshasa), Luxembourgish, Luxembourgish (Luxembourg), Macedonian, Macedonian (North Macedonia), Malagasy, Malagasy (Madagascar), Malay, Malay (Brunei), Malay (Indonesia), Malay (Malaysia), Malay (Singapore), Malayalam, Malayalam (India), Maltese, Maltese (Malta), Manx, Manx (Isle of Man), Māori, Māori (New Zealand), Marathi, Marathi (India), Mongolian, Mongolian (Mongolia), Nepali, Nepali (India), Nepali (Nepal), North Ndebele, North Ndebele (Zimbabwe), Northern Sami, Northern Sami (Finland), Northern Sami (Norway), Northern Sami (Sweden), Norwegian, Norwegian (Norway), Norwegian Bokmål, Norwegian Bokmål (Norway), Norwegian Bokmål (Svalbard & Jan Mayen), Norwegian Nynorsk, Norwegian Nynorsk (Norway), Odia, Odia (India), Oromo, Oromo (Ethiopia), Oromo (Kenya), Ossetic, Ossetic (Georgia), Ossetic (Russia), Pashto, Pashto (Afghanistan), Pashto (Pakistan), Persian, Persian (Afghanistan), Persian (Iran), Polish, Polish (Poland), Portuguese, Portuguese (Angola), Portuguese (Brazil), Portuguese (Cape Verde), Portuguese (Equatorial Guinea), Portuguese (Guinea-Bissau), Portuguese (Luxembourg), Portuguese (Macao SAR China), Portuguese (Mozambique), Portuguese (Portugal), Portuguese (São Tomé & Príncipe), Portuguese (Switzerland), Portuguese (Timor-Leste), Punjabi, Punjabi (Arabic, Pakistan), Punjabi (Arabic), Punjabi (Gurmukhi, India), Punjabi (Gurmukhi), Punjabi (India), Punjabi (Pakistan), Quechua, Quechua (Bolivia), Quechua (Ecuador), Quechua (Peru), Romanian, Romanian (Moldova), Romanian (Romania), Romansh, Romansh (Switzerland), Rundi, Rundi (Burundi), Russian, Russian (Belarus), Russian (Kazakhstan), Russian (Kyrgyzstan), Russian (Moldova), Russian (Russia), Russian (Ukraine), Sango, Sango (Central African Republic), Sanskrit, Sanskrit (India), Sardinian, Sardinian (Italy), Scottish Gaelic, Scottish Gaelic (United Kingdom), Serbian, Serbian (Bosnia & Herzegovina), Serbian (Cyrillic, Bosnia & Herzegovina), Serbian (Cyrillic, Montenegro), Serbian (Cyrillic, Serbia), Serbian (Cyrillic), Serbian (Latin, Bosnia & Herzegovina), Serbian (Latin, Montenegro), Serbian (Latin, Serbia), Serbian (Latin), Serbian (Montenegro), Serbian (Serbia), Serbo-Croatian, Serbo-Croatian (Bosnia & Herzegovina), Shona, Shona (Zimbabwe), Sichuan Yi, Sichuan Yi (China), Sindhi, Sindhi (Arabic, Pakistan), Sindhi (Arabic), Sindhi (Devanagari, India), Sindhi (Devanagari), Sindhi (Pakistan), Sinhala, Sinhala (Sri Lanka), Slovak, Slovak (Slovakia), Slovenian, Slovenian (Slovenia), Somali, Somali (Djibouti), Somali (Ethiopia), Somali (Kenya), Somali (Somalia), Spanish, Spanish (Argentina), Spanish (Belize), Spanish (Bolivia), Spanish (Brazil), Spanish (Chile), Spanish (Colombia), Spanish (Costa Rica), Spanish (Cuba), Spanish (Dominican Republic), Spanish (Ecuador), Spanish (El Salvador), Spanish (Equatorial Guinea), Spanish (Guatemala), Spanish (Honduras), Spanish (Latin America), Spanish (Mexico), Spanish (Nicaragua), Spanish (Panama), Spanish (Paraguay), Spanish (Peru), Spanish (Philippines), Spanish (Puerto Rico), Spanish (Spain), Spanish (United States), Spanish (Uruguay), Spanish (Venezuela), Sundanese, Sundanese (Indonesia), Sundanese (Latin, Indonesia), Sundanese (Latin), Swahili, Swahili (Congo - Kinshasa), Swahili (Kenya), Swahili (Tanzania), Swahili (Uganda), Swedish, Swedish (Åland Islands), Swedish (Finland), Swedish (Sweden), Tagalog, Tagalog (Philippines), Tajik, Tajik (Tajikistan), Tamil, Tamil (India), Tamil (Malaysia), Tamil (Singapore), Tamil (Sri Lanka), Tatar, Tatar (Russia), Telugu, Telugu (India), Thai, Thai (Thailand), Tibetan, Tibetan (China), Tibetan (India), Tigrinya, Tigrinya (Eritrea), Tigrinya (Ethiopia), Tongan, Tongan (Tonga), Turkish, Turkish (Cyprus), Turkish (Turkey), Turkmen, Turkmen (Turkmenistan), Ukrainian, Ukrainian (Ukraine), Urdu, Urdu (India), Urdu (Pakistan), Uyghur, Uyghur (China), Uzbek, Uzbek (Afghanistan), Uzbek (Arabic, Afghanistan), Uzbek (Arabic), Uzbek (Cyrillic, Uzbekistan), Uzbek (Cyrillic), Uzbek (Latin, Uzbekistan), Uzbek (Latin), Uzbek (Uzbekistan), Vietnamese, Vietnamese (Vietnam), Welsh, Welsh (United Kingdom), Western Frisian, Western Frisian (Netherlands), Wolof, Wolof (Senegal), Xhosa, Xhosa (South Africa), Yiddish, Yiddish (world), Yoruba, Yoruba (Benin), Yoruba (Nigeria), Zulu, Zulu (South Africa) -afrikaans, afrikaans (Afrique du Sud), afrikaans (Namibie), akan, akan (Ghana), albanais, albanais (Albanie), albanais (Macédoine du Nord), allemand, allemand (Allemagne), allemand (Autriche), allemand (Belgique), allemand (Italie), allemand (Liechtenstein), allemand (Luxembourg), allemand (Suisse), amharique, amharique (Éthiopie), anglais, anglais (Afrique du Sud), anglais (Allemagne), anglais (Anguilla), anglais (Antigua-et-Barbuda), anglais (Australie), anglais (Autriche), anglais (Bahamas), anglais (Barbade), anglais (Belgique), anglais (Belize), anglais (Bermudes), anglais (Botswana), anglais (Burundi), anglais (Cameroun), anglais (Canada), anglais (Chypre), anglais (Danemark), anglais (Dominique), anglais (Émirats arabes unis), anglais (Érythrée), anglais (Eswatini), anglais (États-Unis), anglais (Europe), anglais (Fidji), anglais (Finlande), anglais (Gambie), anglais (Ghana), anglais (Gibraltar), anglais (Grenade), anglais (Guam), anglais (Guernesey), anglais (Guyana), anglais (Île Christmas), anglais (Île de Man), anglais (Île Norfolk), anglais (Îles Caïmans), anglais (Îles Cocos), anglais (Îles Cook), anglais (Îles Malouines), anglais (Îles Mariannes du Nord), anglais (Îles Marshall), anglais (Îles mineures éloignées des États-Unis), anglais (Îles Pitcairn), anglais (Îles Salomon), anglais (Îles Turques-et-Caïques), anglais (Îles Vierges britanniques), anglais (Îles Vierges des États-Unis), anglais (Inde), anglais (Irlande), anglais (Israël), anglais (Jamaïque), anglais (Jersey), anglais (Kenya), anglais (Kiribati), anglais (Lesotho), anglais (Liberia), anglais (Madagascar), anglais (Malaisie), anglais (Malawi), anglais (Maldives), anglais (Malte), anglais (Maurice), anglais (Micronésie), anglais (Monde), anglais (Montserrat), anglais (Namibie), anglais (Nauru), anglais (Nigeria), anglais (Niue), anglais (Nouvelle-Zélande), anglais (Ouganda), anglais (Pakistan), anglais (Palaos), anglais (Papouasie-Nouvelle-Guinée), anglais (Pays-Bas), anglais (Philippines), anglais (Porto Rico), anglais (R.A.S. chinoise de Hong Kong), anglais (R.A.S. chinoise de Macao), anglais (Royaume-Uni), anglais (Rwanda), anglais (Saint-Christophe-et-Niévès), anglais (Saint-Martin [partie néerlandaise]), anglais (Saint-Vincent-et-les Grenadines), anglais (Sainte-Hélène), anglais (Sainte-Lucie), anglais (Samoa américaines), anglais (Samoa), anglais (Seychelles), anglais (Sierra Leone), anglais (Singapour), anglais (Slovénie), anglais (Soudan du Sud), anglais (Soudan), anglais (Suède), anglais (Suisse), anglais (Tanzanie), anglais (Territoire britannique de l’océan Indien), anglais (Tokelau), anglais (Tonga), anglais (Trinité-et-Tobago), anglais (Tuvalu), anglais (Vanuatu), anglais (Zambie), anglais (Zimbabwe), arabe, arabe (Algérie), arabe (Arabie saoudite), arabe (Bahreïn), arabe (Comores), arabe (Djibouti), arabe (Égypte), arabe (Émirats arabes unis), arabe (Érythrée), arabe (Irak), arabe (Israël), arabe (Jordanie), arabe (Koweït), arabe (Liban), arabe (Libye), arabe (Maroc), arabe (Mauritanie), arabe (Monde), arabe (Oman), arabe (Qatar), arabe (Sahara occidental), arabe (Somalie), arabe (Soudan du Sud), arabe (Soudan), arabe (Syrie), arabe (Tchad), arabe (Territoires palestiniens), arabe (Tunisie), arabe (Yémen), arménien, arménien (Arménie), assamais, assamais (Inde), azerbaïdjanais, azerbaïdjanais (Azerbaïdjan), azerbaïdjanais (cyrillique, Azerbaïdjan), azerbaïdjanais (cyrillique), azerbaïdjanais (latin, Azerbaïdjan), azerbaïdjanais (latin), bambara, bambara (Mali), basque, basque (Espagne), bengali, bengali (Bangladesh), bengali (Inde), biélorusse, biélorusse (Biélorussie), birman, birman (Myanmar [Birmanie]), bosniaque, bosniaque (Bosnie-Herzégovine), bosniaque (cyrillique, Bosnie-Herzégovine), bosniaque (cyrillique), bosniaque (latin, Bosnie-Herzégovine), bosniaque (latin), breton, breton (France), bulgare, bulgare (Bulgarie), cachemiri, cachemiri (arabe, Inde), cachemiri (arabe), cachemiri (dévanagari, Inde), cachemiri (dévanagari), cachemiri (Inde), catalan, catalan (Andorre), catalan (Espagne), catalan (France), catalan (Italie), chinois, chinois (Chine), chinois (R.A.S. chinoise de Hong Kong), chinois (R.A.S. chinoise de Macao), chinois (simplifié, Chine), chinois (simplifié, R.A.S. chinoise de Hong Kong), chinois (simplifié, R.A.S. chinoise de Macao), chinois (simplifié, Singapour), chinois (simplifié), chinois (Singapour), chinois (Taïwan), chinois (traditionnel, R.A.S. chinoise de Hong Kong), chinois (traditionnel, R.A.S. chinoise de Macao), chinois (traditionnel, Taïwan), chinois (traditionnel), cingalais, cingalais (Sri Lanka), coréen, coréen (Corée du Nord), coréen (Corée du Sud), cornique, cornique (Royaume-Uni), croate, croate (Bosnie-Herzégovine), croate (Croatie), danois, danois (Danemark), danois (Groenland), dzongkha, dzongkha (Bhoutan), espagnol, espagnol (Amérique latine), espagnol (Argentine), espagnol (Belize), espagnol (Bolivie), espagnol (Brésil), espagnol (Chili), espagnol (Colombie), espagnol (Costa Rica), espagnol (Cuba), espagnol (Équateur), espagnol (Espagne), espagnol (États-Unis), espagnol (Guatemala), espagnol (Guinée équatoriale), espagnol (Honduras), espagnol (Mexique), espagnol (Nicaragua), espagnol (Panama), espagnol (Paraguay), espagnol (Pérou), espagnol (Philippines), espagnol (Porto Rico), espagnol (République dominicaine), espagnol (Salvador), espagnol (Uruguay), espagnol (Venezuela), espéranto, espéranto (Monde), estonien, estonien (Estonie), éwé, éwé (Ghana), éwé (Togo), féroïen, féroïen (Danemark), féroïen (Îles Féroé), finnois, finnois (Finlande), français, français (Algérie), français (Belgique), français (Bénin), français (Burkina Faso), français (Burundi), français (Cameroun), français (Canada), français (Comores), français (Congo-Brazzaville), français (Congo-Kinshasa), français (Côte d’Ivoire), français (Djibouti), français (France), français (Gabon), français (Guadeloupe), français (Guinée équatoriale), français (Guinée), français (Guyane française), français (Haïti), français (La Réunion), français (Luxembourg), français (Madagascar), français (Mali), français (Maroc), français (Martinique), français (Maurice), français (Mauritanie), français (Mayotte), français (Monaco), français (Niger), français (Nouvelle-Calédonie), français (Polynésie française), français (République centrafricaine), français (Rwanda), français (Saint-Barthélemy), français (Saint-Martin), français (Saint-Pierre-et-Miquelon), français (Sénégal), français (Seychelles), français (Suisse), français (Syrie), français (Tchad), français (Togo), français (Tunisie), français (Vanuatu), français (Wallis-et-Futuna), frison occidental, frison occidental (Pays-Bas), Fulah (Adlam, Burkina Faso), Fulah (Adlam, Cameroon), Fulah (Adlam, Gambia), Fulah (Adlam, Ghana), Fulah (Adlam, Guinea-Bissau), Fulah (Adlam, Guinea), Fulah (Adlam, Liberia), Fulah (Adlam, Mauritania), Fulah (Adlam, Niger), Fulah (Adlam, Nigeria), Fulah (Adlam, Senegal), Fulah (Adlam, Sierra Leone), Fulah (Adlam), gaélique écossais, gaélique écossais (Royaume-Uni), galicien, galicien (Espagne), gallois, gallois (Royaume-Uni), ganda, ganda (Ouganda), géorgien, géorgien (Géorgie), goudjarati, goudjarati (Inde), grec, grec (Chypre), grec (Grèce), groenlandais, groenlandais (Groenland), haoussa, haoussa (Ghana), haoussa (Niger), haoussa (Nigeria), hébreu, hébreu (Israël), hindi, hindi (Inde), hindi (latin, Inde), hindi (latin), hongrois, hongrois (Hongrie), igbo, igbo (Nigeria), indonésien, indonésien (Indonésie), interlingua, interlingua (Monde), irlandais, irlandais (Irlande), irlandais (Royaume-Uni), islandais, islandais (Islande), italien, italien (État de la Cité du Vatican), italien (Italie), italien (Saint-Marin), italien (Suisse), japonais, japonais (Japon), javanais, javanais (Indonésie), kannada, kannada (Inde), kazakh, kazakh (Kazakhstan), khmer, khmer (Cambodge), kikuyu, kikuyu (Kenya), kinyarwanda, kinyarwanda (Rwanda), kirghize, kirghize (Kirghizstan), kurde, kurde (Turquie), lao, lao (Laos), letton, letton (Lettonie), lingala, lingala (Angola), lingala (Congo-Brazzaville), lingala (Congo-Kinshasa), lingala (République centrafricaine), lituanien, lituanien (Lituanie), luba-katanga [kiluba], luba-katanga [kiluba] (Congo-Kinshasa), luxembourgeois, luxembourgeois (Luxembourg), macédonien, macédonien (Macédoine du Nord), malais, malais (Brunei), malais (Indonésie), malais (Malaisie), malais (Singapour), malayalam, malayalam (Inde), malgache, malgache (Madagascar), maltais, maltais (Malte), mannois, mannois (Île de Man), maori, maori (Nouvelle-Zélande), marathi, marathi (Inde), mongol, mongol (Mongolie), ndébélé du Nord, ndébélé du Nord (Zimbabwe), néerlandais, néerlandais (Aruba), néerlandais (Belgique), néerlandais (Curaçao), néerlandais (Pays-Bas caribéens), néerlandais (Pays-Bas), néerlandais (Saint-Martin [partie néerlandaise]), néerlandais (Suriname), népalais, népalais (Inde), népalais (Népal), norvégien, norvégien (Norvège), norvégien bokmål, norvégien bokmål (Norvège), norvégien bokmål (Svalbard et Jan Mayen), norvégien nynorsk, norvégien nynorsk (Norvège), odia, odia (Inde), oromo, oromo (Éthiopie), oromo (Kenya), ossète, ossète (Géorgie), ossète (Russie), ouïghour, ouïghour (Chine), ourdou, ourdou (Inde), ourdou (Pakistan), ouzbek, ouzbek (Afghanistan), ouzbek (arabe, Afghanistan), ouzbek (arabe), ouzbek (cyrillique, Ouzbékistan), ouzbek (cyrillique), ouzbek (latin, Ouzbékistan), ouzbek (latin), ouzbek (Ouzbékistan), pachto, pachto (Afghanistan), pachto (Pakistan), pendjabi, pendjabi (arabe, Pakistan), pendjabi (arabe), pendjabi (gourmoukhî, Inde), pendjabi (gourmoukhî), pendjabi (Inde), pendjabi (Pakistan), persan, persan (Afghanistan), persan (Iran), peul, peul (Cameroun), peul (Guinée), peul (latin, Burkina Faso), peul (latin, Cameroun), peul (latin, Gambie), peul (latin, Ghana), peul (latin, Guinée-Bissau), peul (latin, Guinée), peul (latin, Liberia), peul (latin, Mauritanie), peul (latin, Niger), peul (latin, Nigeria), peul (latin, Sénégal), peul (latin, Sierra Leone), peul (latin), peul (Mauritanie), peul (Sénégal), polonais, polonais (Pologne), portugais, portugais (Angola), portugais (Brésil), portugais (Cap-Vert), portugais (Guinée équatoriale), portugais (Guinée-Bissau), portugais (Luxembourg), portugais (Mozambique), portugais (Portugal), portugais (R.A.S. chinoise de Macao), portugais (Sao Tomé-et-Principe), portugais (Suisse), portugais (Timor oriental), quechua, quechua (Bolivie), quechua (Équateur), quechua (Pérou), romanche, romanche (Suisse), roumain, roumain (Moldavie), roumain (Roumanie), roundi, roundi (Burundi), russe, russe (Biélorussie), russe (Kazakhstan), russe (Kirghizstan), russe (Moldavie), russe (Russie), russe (Ukraine), same du Nord, same du Nord (Finlande), same du Nord (Norvège), same du Nord (Suède), sango, sango (République centrafricaine), sanskrit, sanskrit (Inde), sarde, sarde (Italie), serbe, serbe (Bosnie-Herzégovine), serbe (cyrillique, Bosnie-Herzégovine), serbe (cyrillique, Monténégro), serbe (cyrillique, Serbie), serbe (cyrillique), serbe (latin, Bosnie-Herzégovine), serbe (latin, Monténégro), serbe (latin, Serbie), serbe (latin), serbe (Monténégro), serbe (Serbie), serbo-croate, serbo-croate (Bosnie-Herzégovine), shona, shona (Zimbabwe), sindhi, sindhi (arabe, Pakistan), sindhi (arabe), sindhi (dévanagari, Inde), sindhi (dévanagari), sindhi (Pakistan), slovaque, slovaque (Slovaquie), slovène, slovène (Slovénie), somali, somali (Djibouti), somali (Éthiopie), somali (Kenya), somali (Somalie), soundanais, soundanais (Indonésie), soundanais (latin, Indonésie), soundanais (latin), suédois, suédois (Finlande), suédois (Îles Åland), suédois (Suède), swahili, swahili (Congo-Kinshasa), swahili (Kenya), swahili (Ouganda), swahili (Tanzanie), tadjik, tadjik (Tadjikistan), tagalog, tagalog (Philippines), tamoul, tamoul (Inde), tamoul (Malaisie), tamoul (Singapour), tamoul (Sri Lanka), tatar, tatar (Russie), tchèque, tchèque (Tchéquie), tchétchène, tchétchène (Russie), télougou, télougou (Inde), thaï, thaï (Thaïlande), tibétain, tibétain (Chine), tibétain (Inde), tigrigna, tigrigna (Érythrée), tigrigna (Éthiopie), tongien, tongien (Tonga), turc, turc (Chypre), turc (Turquie), turkmène, turkmène (Turkménistan), ukrainien, ukrainien (Ukraine), vietnamien, vietnamien (Viêt Nam), wolof, wolof (Sénégal), xhosa, xhosa (Afrique du Sud), yi du Sichuan, yi du Sichuan (Chine), yiddish, yiddish (Monde), yoruba, yoruba (Bénin), yoruba (Nigeria), zoulou, zoulou (Afrique du Sud) +--TEST-- +"locale_names" function +--TEMPLATE-- +{{ locale_names('UNKNOWN')|length }} +{{ locale_names()|length }} +{{ locale_names('fr')|length }} +{{ locale_names()['fr'] }} +{{ locale_names('fr')['fr'] }} +--DATA-- +return []; +--EXPECT-- +0 +637 +637 +French +français diff --git a/extra/intl-extra/Tests/Fixtures/script_names.test b/extra/intl-extra/Tests/Fixtures/script_names.test index 18baa84e6ab..c65daf55071 100644 --- a/extra/intl-extra/Tests/Fixtures/script_names.test +++ b/extra/intl-extra/Tests/Fixtures/script_names.test @@ -2,11 +2,15 @@ "script_names" function --TEMPLATE-- {{ script_names('UNKNOWN')|length }} -{{ script_names()|join(', ') }} -{{ script_names('fr')|join(', ') }} +{{ script_names()|length }} +{{ script_names('fr')|length }} +{{ script_names()['Marc'] }} +{{ script_names('fr')['Marc'] }} --DATA-- return []; --EXPECT-- 0 -Adlam, Afaka, Ahom, Anatolian Hieroglyphs, Arabic, Armenian, Avestan, Balinese, Bamum, Bangla, Bassa Vah, Batak, Bhaiksuki, Blissymbols, Book Pahlavi, Bopomofo, Brahmi, Braille, Buginese, Buhid, Carian, Caucasian Albanian, Chakma, Cham, Cherokee, Chorasmian, Cirth, Common, Coptic, Cypriot, Cypro-Minoan, Cyrillic, Deseret, Devanagari, Dives Akuru, Dogra, Duployan shorthand, Eastern Syriac, Egyptian demotic, Egyptian hieratic, Egyptian hieroglyphs, Elbasan, Elymaic, Emoji, Estrangelo Syriac, Ethiopic, Fraktur Latin, Fraser, Gaelic Latin, Georgian, Georgian Khutsuri, Glagolitic, Gothic, Grantha, Greek, Gujarati, Gunjala Gondi, Gurmukhi, Han, Han with Bopomofo, Hangul, Hanifi, Hanunoo, Hatran, Hebrew, Hiragana, Imperial Aramaic, Indus, Inherited, Inscriptional Pahlavi, Inscriptional Parthian, Jamo, Japanese, Japanese syllabaries, Javanese, Jurchen, Kaithi, Kannada, Katakana, Kawi, Kayah Li, Kharoshthi, Khitan small script, Khmer, Khojki, Khudawadi, Korean, Kpelle, Lanna, Lao, Latin, Lepcha, Limbu, Linear A, Linear B, Loma, Lycian, Lydian, Mahajani, Makasar, Malayalam, Mandaean, Manichaean, Marchen, Masaram Gondi, Mathematical Notation, Mayan hieroglyphs, Medefaidrin, Meitei Mayek, Mende, Meroitic, Meroitic Cursive, Modi, Mongolian, Moon, Mro, Multani, Myanmar, N’Ko, Nabataean, Nag Mundari, Nandinagari, Nastaliq, Naxi Geba, New Tai Lue, Newa, Nüshu, Nyiakeng Puachue Hmong, Odia, Ogham, Ol Chiki, Old Church Slavonic Cyrillic, Old Hungarian, Old Italic, Old North Arabian, Old Permic, Old Persian, Old Sogdian, Old South Arabian, Old Uyghur, Orkhon, Osage, Osmanya, Pahawh Hmong, Palmyrene, Pau Cin Hau, Phags-pa, Phoenician, Pollard Phonetic, Psalter Pahlavi, Rejang, Rongorongo, Runic, Samaritan, Sarati, Saurashtra, Sharada, Shavian, Siddham, SignWriting, Simplified, Sinhala, Sogdian, Sora Sompeng, Soyombo, Sumero-Akkadian Cuneiform, Sundanese, Syloti Nagri, Symbols, Syriac, Tagalog, Tagbanwa, Tai Le, Tai Viet, Takri, Tamil, Tangsa, Tangut, Telugu, Tengwar, Thaana, Thai, Tibetan, Tifinagh, Tirhuta, Toto, Traditional, Ugaritic, Unified Canadian Aboriginal Syllabics, Unwritten, Vai, Varang Kshiti, Visible Speech, Vithkuqi, Wancho, Western Syriac, Woleai, Yezidi, Yi, Zanabazar Square, Zawgyi -Adlam, Afaka, Ahom, Anatolian Hieroglyphs, ancien hongrois, ancien italique, ancien permien, arabe, araméen impérial, arménien, avestique, balinais, Bamum, Bassa Vah, batak, bengali, Bhaiksuki, birman, bopomofo, bouguis, bouhide, brâhmî, braille, carien, Caucasian Albanian, chakma, cham, cherokee, Chorasmian, cingalais, cirth, commun, copte, coréen, cunéiforme persépolitain, cunéiforme suméro-akkadien, Cypro-Minoan, cyrillique, cyrillique (variante slavonne), démotique égyptien, déséret, dévanagari, Dives Akuru, Dogra, Duployan shorthand, écriture des signes, Elbasan, élymaïque, emoji, éthiopique, Fraser, géorgien, géorgien khoutsouri, glagolitique, gotique, goudjarâtî, gourmoukhî, Grantha, grec, Gunjala Gondi, han avec bopomofo, hangûl, Hanifi, hanounóo, Hatran, hébreu, hérité, hiératique égyptien, hiéroglyphes égyptiens, hiéroglyphes mayas, hiragana, indus, jamo, japonais, javanais, Jurchen, kaithî, kannara, katakana, katakana ou hiragana, Kawi, kayah li, kharochthî, Khitan small script, khmer, Khojki, Khudawadi, Kpelle, lanna, lao, latin, latin (variante brisée), latin (variante gaélique), lepcha, limbou, linéaire A, linéaire B, Loma, lycien, lydien, Mahajani, Makasar, malayalam, mandéen, manichéen, Marchen, Masaram Gondi, Medefaidrin, meitei mayek, Mende, Meroitic Cursive, méroïtique, Modi, mongol, moon, Mro, Multani, n’ko, Nabataean, Nag Mundari, nandinagari, nastaliq, Naxi Geba, Newa, non écrit, notation mathématique, nouveau taï-lue, Nüshu, nyiakeng puachue hmong, odia, ogam, ol tchiki, Old North Arabian, Old Sogdian, Old South Arabian, Old Uyghur, orkhon, Osage, osmanais, ougaritique, pahawh hmong, Palmyrene, parole visible, parthe des inscriptions, Pau Cin Hau, pehlevi des inscriptions, pehlevi des livres, pehlevi des psautiers, phags pa, phénicien, phonétique de Pollard, rejang, rongorongo, runique, samaritain, sarati, saurashtra, Sharada, shavien, Siddham, simplifié, sinogrammes, Sogdian, Sora Sompeng, Soyombo, sundanais, syllabaire autochtone canadien unifié, syllabaire chypriote, sylotî nâgrî, symboles, symboles Bliss, syriaque, syriaque estranghélo, syriaque occidental, syriaque oriental, tagal, tagbanoua, taï viêt, taï-le, Takri, tamoul, Tangsa, Tangut, télougou, tengwar, thaï, thâna, tibétain, tifinagh, Tirhuta, Toto, traditionnel, vaï, Varang Kshiti, Vithkuqi, wantcho, Woleai, Yezidi, yi, Zanabazar Square, zawgyi +201 +201 +Marchen +Marchen diff --git a/extra/intl-extra/Tests/Fixtures/timezone_names.test b/extra/intl-extra/Tests/Fixtures/timezone_names.test index bf0aafb91e6..51b50704c38 100644 --- a/extra/intl-extra/Tests/Fixtures/timezone_names.test +++ b/extra/intl-extra/Tests/Fixtures/timezone_names.test @@ -1,12 +1,16 @@ ---TEST-- -"timezone_names" function ---TEMPLATE-- -{{ timezone_names('UNKNOWN')|length }} -{{ timezone_names()|join(', ') }} -{{ timezone_names('fr')|join(', ') }} ---DATA-- -return []; ---EXPECT-- -0 -Acre Time (Eirunepe), Acre Time (Rio Branco), Afghanistan Time (Kabul), Alaska Time (Anchorage), Alaska Time (Juneau), Alaska Time (Metlakatla), Alaska Time (Nome), Alaska Time (Sitka), Alaska Time (Yakutat), Amazon Time (Boa Vista), Amazon Time (Campo Grande), Amazon Time (Cuiaba), Amazon Time (Manaus), Amazon Time (Porto Velho), Anadyr Time, Apia Time, Arabian Time (Aden), Arabian Time (Baghdad), Arabian Time (Bahrain), Arabian Time (Kuwait), Arabian Time (Qatar), Arabian Time (Riyadh), Argentina Time (Buenos Aires), Argentina Time (Catamarca), Argentina Time (Cordoba), Argentina Time (Jujuy), Argentina Time (La Rioja), Argentina Time (Mendoza), Argentina Time (Rio Gallegos), Argentina Time (Salta), Argentina Time (San Juan), Argentina Time (San Luis), Argentina Time (Tucuman), Argentina Time (Ushuaia), Armenia Time (Yerevan), Atlantic Time (Anguilla), Atlantic Time (Antigua), Atlantic Time (Aruba), Atlantic Time (Barbados), Atlantic Time (Bermuda), Atlantic Time (Blanc-Sablon), Atlantic Time (Curaçao), Atlantic Time (Dominica), Atlantic Time (Glace Bay), Atlantic Time (Goose Bay), Atlantic Time (Grenada), Atlantic Time (Guadeloupe), Atlantic Time (Halifax), Atlantic Time (Kralendijk), Atlantic Time (Lower Prince’s Quarter), Atlantic Time (Marigot), Atlantic Time (Martinique), Atlantic Time (Moncton), Atlantic Time (Montserrat), Atlantic Time (Port of Spain), Atlantic Time (Puerto Rico), Atlantic Time (Santo Domingo), Atlantic Time (St. Barthélemy), Atlantic Time (St. Kitts), Atlantic Time (St. Lucia), Atlantic Time (St. Thomas), Atlantic Time (St. Vincent), Atlantic Time (Thule), Atlantic Time (Tortola), Australian Central Western Time (Eucla), Azerbaijan Time (Baku), Azores Time, Bangladesh Time (Dhaka), Bhutan Time (Thimphu), Bolivia Time (La Paz), Brasilia Time (Araguaina), Brasilia Time (Bahia), Brasilia Time (Belem), Brasilia Time (Fortaleza), Brasilia Time (Maceio), Brasilia Time (Recife), Brasilia Time (Santarem), Brasilia Time (Sao Paulo), Brunei Darussalam Time, Canada Time (Montreal), Cape Verde Time, Casey Time, Central Africa Time (Blantyre), Central Africa Time (Bujumbura), Central Africa Time (Gaborone), Central Africa Time (Harare), Central Africa Time (Juba), Central Africa Time (Khartoum), Central Africa Time (Kigali), Central Africa Time (Lubumbashi), Central Africa Time (Lusaka), Central Africa Time (Maputo), Central Africa Time (Windhoek), Central Australia Time (Adelaide), Central Australia Time (Broken Hill), Central Australia Time (Darwin), Central European Time (Algiers), Central European Time (Amsterdam), Central European Time (Andorra), Central European Time (Belgrade), Central European Time (Berlin), Central European Time (Bratislava), Central European Time (Brussels), Central European Time (Budapest), Central European Time (Busingen), Central European Time (Ceuta), Central European Time (Copenhagen), Central European Time (Gibraltar), Central European Time (Ljubljana), Central European Time (Longyearbyen), Central European Time (Luxembourg), Central European Time (Madrid), Central European Time (Malta), Central European Time (Monaco), Central European Time (Oslo), Central European Time (Paris), Central European Time (Podgorica), Central European Time (Prague), Central European Time (Rome), Central European Time (San Marino), Central European Time (Sarajevo), Central European Time (Skopje), Central European Time (Stockholm), Central European Time (Tirane), Central European Time (Tunis), Central European Time (Vaduz), Central European Time (Vatican), Central European Time (Vienna), Central European Time (Warsaw), Central European Time (Zagreb), Central European Time (Zurich), Central Indonesia Time (Makassar), Central Time, Central Time (Bahia Banderas), Central Time (Belize), Central Time (Beulah, North Dakota), Central Time (Center, North Dakota), Central Time (Chicago), Central Time (Costa Rica), Central Time (El Salvador), Central Time (Guatemala), Central Time (Knox, Indiana), Central Time (Managua), Central Time (Matamoros), Central Time (Menominee), Central Time (Merida), Central Time (Mexico City), Central Time (Monterrey), Central Time (New Salem, North Dakota), Central Time (Rainy River), Central Time (Rankin Inlet), Central Time (Regina), Central Time (Resolute), Central Time (Swift Current), Central Time (Tegucigalpa), Central Time (Tell City, Indiana), Central Time (Winnipeg), Chamorro Standard Time (Guam), Chamorro Standard Time (Saipan), Chatham Time, Chile Time (Palmer), Chile Time (Punta Arenas), Chile Time (Santiago), China Time (Macao), China Time (Shanghai), China Time (Urumqi), Christmas Island Time, Chuuk Time, Cocos Islands Time, Colombia Time (Bogota), Cook Islands Time (Rarotonga), Coordinated Universal Time, Cuba Time (Havana), Davis Time, Dumont-d’Urville Time, East Africa Time (Addis Ababa), East Africa Time (Antananarivo), East Africa Time (Asmara), East Africa Time (Comoro), East Africa Time (Dar es Salaam), East Africa Time (Djibouti), East Africa Time (Kampala), East Africa Time (Mayotte), East Africa Time (Mogadishu), East Africa Time (Nairobi), East Greenland Time (Ittoqqortoormiit), East Kazakhstan Time (Almaty), East Kazakhstan Time (Kostanay), East Timor Time (Dili), Easter Island Time, Eastern Australia Time (Brisbane), Eastern Australia Time (Currie), Eastern Australia Time (Hobart), Eastern Australia Time (Lindeman), Eastern Australia Time (Macquarie), Eastern Australia Time (Melbourne), Eastern Australia Time (Sydney), Eastern European Time (Amman), Eastern European Time (Athens), Eastern European Time (Beirut), Eastern European Time (Bucharest), Eastern European Time (Cairo), Eastern European Time (Chisinau), Eastern European Time (Damascus), Eastern European Time (Famagusta), Eastern European Time (Gaza), Eastern European Time (Hebron), Eastern European Time (Helsinki), Eastern European Time (Kaliningrad), Eastern European Time (Kyiv), Eastern European Time (Mariehamn), Eastern European Time (Nicosia), Eastern European Time (Riga), Eastern European Time (Sofia), Eastern European Time (Tallinn), Eastern European Time (Tripoli), Eastern European Time (Uzhhorod), Eastern European Time (Vilnius), Eastern European Time (Zaporozhye), Eastern Indonesia Time (Jayapura), Eastern Time, Eastern Time (Atikokan), Eastern Time (Cancun), Eastern Time (Cayman), Eastern Time (Detroit), Eastern Time (Grand Turk), Eastern Time (Indianapolis), Eastern Time (Iqaluit), Eastern Time (Jamaica), Eastern Time (Louisville), Eastern Time (Marengo, Indiana), Eastern Time (Monticello, Kentucky), Eastern Time (Nassau), Eastern Time (New York), Eastern Time (Nipigon), Eastern Time (Panama), Eastern Time (Pangnirtung), Eastern Time (Petersburg, Indiana), Eastern Time (Port-au-Prince), Eastern Time (Thunder Bay), Eastern Time (Toronto), Eastern Time (Vevay, Indiana), Eastern Time (Vincennes, Indiana), Eastern Time (Winamac, Indiana), Ecuador Time (Guayaquil), Falkland Islands Time (Stanley), Fernando de Noronha Time, Fiji Time, French Guiana Time (Cayenne), French Southern & Antarctic Time (Kerguelen), Galapagos Time, Gambier Time, Georgia Time (Tbilisi), Gilbert Islands Time (Tarawa), Greenwich Mean Time, Greenwich Mean Time (Abidjan), Greenwich Mean Time (Accra), Greenwich Mean Time (Bamako), Greenwich Mean Time (Banjul), Greenwich Mean Time (Bissau), Greenwich Mean Time (Conakry), Greenwich Mean Time (Dakar), Greenwich Mean Time (Danmarkshavn), Greenwich Mean Time (Dublin), Greenwich Mean Time (Freetown), Greenwich Mean Time (Guernsey), Greenwich Mean Time (Isle of Man), Greenwich Mean Time (Jersey), Greenwich Mean Time (Lome), Greenwich Mean Time (London), Greenwich Mean Time (Monrovia), Greenwich Mean Time (Nouakchott), Greenwich Mean Time (Ouagadougou), Greenwich Mean Time (Reykjavik), Greenwich Mean Time (São Tomé), Greenwich Mean Time (St. Helena), Greenwich Mean Time (Troll), Gulf Standard Time (Dubai), Gulf Standard Time (Muscat), Guyana Time, Hawaii-Aleutian Time (Adak), Hawaii-Aleutian Time (Honolulu), Hawaii-Aleutian Time (Johnston), Hong Kong Time, Hovd Time, India Standard Time (Colombo), India Standard Time (Kolkata), Indian Ocean Time (Chagos), Indochina Time (Bangkok), Indochina Time (Ho Chi Minh City), Indochina Time (Phnom Penh), Indochina Time (Vientiane), Iran Time (Tehran), Irkutsk Time, Israel Time (Jerusalem), Japan Time (Tokyo), Korean Time (Pyongyang), Korean Time (Seoul), Kosrae Time, Krasnoyarsk Time, Krasnoyarsk Time (Novokuznetsk), Kyrgyzstan Time (Bishkek), Line Islands Time (Kiritimati), Lord Howe Time, Magadan Time, Magadan Time (Srednekolymsk), Malaysia Time (Kuala Lumpur), Malaysia Time (Kuching), Maldives Time, Marquesas Time, Marshall Islands Time (Kwajalein), Marshall Islands Time (Majuro), Mauritius Time, Mawson Time, Mexican Pacific Time (Chihuahua), Mexican Pacific Time (Hermosillo), Mexican Pacific Time (Mazatlan), Moscow Time, Moscow Time (Astrakhan), Moscow Time (Minsk), Moscow Time (Saratov), Moscow Time (Simferopol), Moscow Time (Ulyanovsk), Mountain Time, Mountain Time (Boise), Mountain Time (Cambridge Bay), Mountain Time (Creston), Mountain Time (Dawson Creek), Mountain Time (Denver), Mountain Time (Edmonton), Mountain Time (Fort Nelson), Mountain Time (Inuvik), Mountain Time (Ojinaga), Mountain Time (Phoenix), Mountain Time (Yellowknife), Myanmar Time (Yangon), Nauru Time, Nepal Time (Kathmandu), New Caledonia Time (Noumea), New Zealand Time (Auckland), New Zealand Time (McMurdo), Newfoundland Time (St. John’s), Niue Time, Norfolk Island Time, Northwest Mexico Time (Santa Isabel), Novosibirsk Time, Omsk Time, Pacific Time, Pacific Time (Los Angeles), Pacific Time (Tijuana), Pacific Time (Vancouver), Pakistan Time (Karachi), Palau Time, Papua New Guinea Time (Bougainville), Papua New Guinea Time (Port Moresby), Paraguay Time (Asunción), Peru Time (Lima), Petropavlovsk-Kamchatski Time (Kamchatka), Philippine Time (Manila), Phoenix Islands Time (Enderbury), Pitcairn Time, Ponape Time (Pohnpei), Réunion Time, Rothera Time, Russia Time (Barnaul), Russia Time (Kirov), Russia Time (Tomsk), Sakhalin Time, Samara Time, Samoa Time (Midway), Samoa Time (Pago Pago), Seychelles Time (Mahe), Singapore Standard Time, Solomon Islands Time (Guadalcanal), South Africa Standard Time (Johannesburg), South Africa Standard Time (Maseru), South Africa Standard Time (Mbabane), South Georgia Time, St. Pierre & Miquelon Time, Suriname Time (Paramaribo), Syowa Time, Tahiti Time, Taipei Time, Tajikistan Time (Dushanbe), Tokelau Time (Fakaofo), Tonga Time (Tongatapu), Turkey Time (Istanbul), Turkmenistan Time (Ashgabat), Tuvalu Time (Funafuti), Ulaanbaatar Time, Ulaanbaatar Time (Choibalsan), Uruguay Time (Montevideo), Uzbekistan Time (Samarkand), Uzbekistan Time (Tashkent), Vanuatu Time (Efate), Venezuela Time (Caracas), Vladivostok Time, Vladivostok Time (Ust-Nera), Volgograd Time, Vostok Time, Wake Island Time, Wallis & Futuna Time, West Africa Time (Bangui), West Africa Time (Brazzaville), West Africa Time (Douala), West Africa Time (Kinshasa), West Africa Time (Lagos), West Africa Time (Libreville), West Africa Time (Luanda), West Africa Time (Malabo), West Africa Time (Ndjamena), West Africa Time (Niamey), West Africa Time (Porto-Novo), West Greenland Time (Nuuk), West Kazakhstan Time (Aqtau), West Kazakhstan Time (Aqtobe), West Kazakhstan Time (Atyrau), West Kazakhstan Time (Oral), West Kazakhstan Time (Qyzylorda), Western Australia Time (Perth), Western European Time (Canary), Western European Time (Casablanca), Western European Time (El Aaiun), Western European Time (Faroe), Western European Time (Lisbon), Western European Time (Madeira), Western Indonesia Time (Jakarta), Western Indonesia Time (Pontianak), Yakutsk Time, Yakutsk Time (Chita), Yakutsk Time (Khandyga), Yekaterinburg Time, Yukon Time (Dawson), Yukon Time (Whitehorse) -heure : Antarctique (Casey), heure : Canada (Montreal), heure : Chine (Ürümqi), heure : Russie (Barnaul), heure : Russie (Kirov), heure : Russie (Tomsk), heure : Turquie (Istanbul), heure d’Afrique de l’Ouest (Bangui), heure d’Afrique de l’Ouest (Brazzaville), heure d’Afrique de l’Ouest (Douala), heure d’Afrique de l’Ouest (Kinshasa), heure d’Afrique de l’Ouest (Lagos), heure d’Afrique de l’Ouest (Libreville), heure d’Afrique de l’Ouest (Luanda), heure d’Afrique de l’Ouest (Malabo), heure d’Afrique de l’Ouest (N’Djamena), heure d’Afrique de l’Ouest (Niamey), heure d’Afrique de l’Ouest (Porto-Novo), heure d’Anadyr, heure d’Apia, heure d’Ekaterinbourg, heure d’Europe centrale (Alger), heure d’Europe centrale (Amsterdam), heure d’Europe centrale (Andorre), heure d’Europe centrale (Belgrade), heure d’Europe centrale (Berlin), heure d’Europe centrale (Bratislava), heure d’Europe centrale (Bruxelles), heure d’Europe centrale (Budapest), heure d’Europe centrale (Büsingen), heure d’Europe centrale (Ceuta), heure d’Europe centrale (Copenhague), heure d’Europe centrale (Gibraltar), heure d’Europe centrale (Le Vatican), heure d’Europe centrale (Ljubljana), heure d’Europe centrale (Longyearbyen), heure d’Europe centrale (Luxembourg), heure d’Europe centrale (Madrid), heure d’Europe centrale (Malte), heure d’Europe centrale (Monaco), heure d’Europe centrale (Oslo), heure d’Europe centrale (Paris), heure d’Europe centrale (Podgorica), heure d’Europe centrale (Prague), heure d’Europe centrale (Rome), heure d’Europe centrale (Saint-Marin), heure d’Europe centrale (Sarajevo), heure d’Europe centrale (Skopje), heure d’Europe centrale (Stockholm), heure d’Europe centrale (Tirana), heure d’Europe centrale (Tunis), heure d’Europe centrale (Vaduz), heure d’Europe centrale (Varsovie), heure d’Europe centrale (Vienne), heure d’Europe centrale (Zagreb), heure d’Europe centrale (Zurich), heure d’Europe de l’Est (Amman), heure d’Europe de l’Est (Athènes), heure d’Europe de l’Est (Beyrouth), heure d’Europe de l’Est (Bucarest), heure d’Europe de l’Est (Chisinau), heure d’Europe de l’Est (Damas), heure d’Europe de l’Est (Famagouste), heure d’Europe de l’Est (Gaza), heure d’Europe de l’Est (Hébron), heure d’Europe de l’Est (Helsinki), heure d’Europe de l’Est (Kaliningrad), heure d’Europe de l’Est (Kiev), heure d’Europe de l’Est (Le Caire), heure d’Europe de l’Est (Mariehamn), heure d’Europe de l’Est (Nicosie), heure d’Europe de l’Est (Oujgorod), heure d’Europe de l’Est (Riga), heure d’Europe de l’Est (Sofia), heure d’Europe de l’Est (Tallinn), heure d’Europe de l’Est (Tripoli (Libye)), heure d’Europe de l’Est (Vilnius), heure d’Europe de l’Est (Zaporojie), heure d’Europe de l’Ouest (Casablanca), heure d’Europe de l’Ouest (Îles Canaries), heure d’Europe de l’Ouest (Îles Féroé), heure d’Europe de l’Ouest (Laâyoune), heure d’Europe de l’Ouest (Lisbonne), heure d’Europe de l’Ouest (Madère), heure d’Hawaii - Aléoutiennes (Adak), heure d’Hawaii - Aléoutiennes (Honolulu), heure d’Hawaii - Aléoutiennes (Johnston), heure d’Indochine (Bangkok), heure d’Indochine (Hô-Chi-Minh-Ville), heure d’Indochine (Phnom Penh), heure d’Indochine (Vientiane), heure d’Irkoutsk, heure d’Israël (Jérusalem), heure d’Oulan-Bator, heure d’Oulan-Bator (Tchoïbalsan), heure de Bolivie (La Paz), heure de Brasilia (Araguaína), heure de Brasilia (Bahia), heure de Brasilia (Belém), heure de Brasilia (Fortaleza), heure de Brasilia (Maceió), heure de Brasilia (Recife), heure de Brasilia (Santarém), heure de Brasilia (São Paulo), heure de Chuuk, heure de Colombie (Bogota), heure de Cuba (La Havane), heure de Davis, heure de Dumont-d’Urville, heure de Fernando de Noronha, heure de Géorgie du Sud, heure de Hong Kong, heure de Hovd, heure de Iakoutsk, heure de Iakoutsk (Khandyga), heure de Iakoutsk (Tchita), heure de Kosrae, heure de Krasnoïarsk, heure de Krasnoïarsk (Novokuznetsk), heure de l’Acre (Eirunepé), heure de l’Acre (Rio Branco), heure de l’Afghanistan (Kaboul), heure de l’Alaska (Anchorage), heure de l’Alaska (Juneau), heure de l’Alaska (Metlakatla), heure de l’Alaska (Nome), heure de l’Alaska (Sitka), heure de l’Alaska (Yakutat), heure de l’Amazonie (Boa Vista), heure de l’Amazonie (Campo Grande), heure de l’Amazonie (Cuiabá), heure de l’Amazonie (Manaos), heure de l’Amazonie (Porto Velho), heure de l’Arabie (Aden), heure de l’Arabie (Bagdad), heure de l’Arabie (Bahreïn), heure de l’Arabie (Koweït), heure de l’Arabie (Qatar), heure de l’Arabie (Riyad), heure de l’Argentine (Buenos Aires), heure de l’Argentine (Catamarca), heure de l’Argentine (Córdoba), heure de l’Argentine (Jujuy), heure de l’Argentine (La Rioja), heure de l’Argentine (Mendoza), heure de l’Argentine (Río Gallegos), heure de l’Argentine (Salta), heure de l’Argentine (San Juan), heure de l’Argentine (San Luis), heure de l’Argentine (Tucumán), heure de l’Argentine (Ushuaïa), heure de l’Arménie (Erevan), heure de l’Atlantique (Anguilla), heure de l’Atlantique (Antigua), heure de l’Atlantique (Aruba), heure de l’Atlantique (Bermudes), heure de l’Atlantique (Blanc-Sablon), heure de l’Atlantique (Curaçao), heure de l’Atlantique (Dominique), heure de l’Atlantique (Glace Bay), heure de l’Atlantique (Goose Bay), heure de l’Atlantique (Grenade), heure de l’Atlantique (Guadeloupe), heure de l’Atlantique (Halifax), heure de l’Atlantique (Kralendijk), heure de l’Atlantique (La Barbade), heure de l’Atlantique (Lower Prince’s Quarter), heure de l’Atlantique (Marigot), heure de l’Atlantique (Martinique), heure de l’Atlantique (Moncton), heure de l’Atlantique (Montserrat), heure de l’Atlantique (Port-d’Espagne), heure de l’Atlantique (Porto Rico), heure de l’Atlantique (Saint-Barthélemy), heure de l’Atlantique (Saint-Christophe), heure de l’Atlantique (Saint-Domingue), heure de l’Atlantique (Saint-Thomas), heure de l’Atlantique (Saint-Vincent), heure de l’Atlantique (Sainte-Lucie), heure de l’Atlantique (Thulé), heure de l’Atlantique (Tortola), heure de l’Azerbaïdjan (Bakou), heure de l’Équateur (Guayaquil), heure de l’Est de l’Australie (Brisbane), heure de l’Est de l’Australie (Currie), heure de l’Est de l’Australie (Hobart), heure de l’Est de l’Australie (Lindeman), heure de l’Est de l’Australie (Macquarie), heure de l’Est de l’Australie (Melbourne), heure de l’Est de l’Australie (Sydney), heure de l’Est du Groenland (Ittoqqortoormiit), heure de l’Est du Kazakhstan (Alma Ata), heure de l’Est du Kazakhstan (Kostanaï), heure de l’Est indonésien (Jayapura), heure de l’Est nord-américain, heure de l’Est nord-américain (Atikokan), heure de l’Est nord-américain (Caïmans), heure de l’Est nord-américain (Cancún), heure de l’Est nord-américain (Détroit), heure de l’Est nord-américain (Grand Turk), heure de l’Est nord-américain (Indianapolis), heure de l’Est nord-américain (Iqaluit), heure de l’Est nord-américain (Jamaïque), heure de l’Est nord-américain (Louisville), heure de l’Est nord-américain (Marengo [Indiana]), heure de l’Est nord-américain (Monticello [Kentucky]), heure de l’Est nord-américain (Nassau), heure de l’Est nord-américain (New York), heure de l’Est nord-américain (Nipigon), heure de l’Est nord-américain (Panama), heure de l’Est nord-américain (Pangnirtung), heure de l’Est nord-américain (Petersburg [Indiana]), heure de l’Est nord-américain (Port-au-Prince), heure de l’Est nord-américain (Thunder Bay), heure de l’Est nord-américain (Toronto), heure de l’Est nord-américain (Vevay [Indiana]), heure de l’Est nord-américain (Vincennes [Indiana]), heure de l’Est nord-américain (Winamac [Indiana]), heure de l’île Christmas, heure de l’île de Pâques, heure de l’île de Pohnpei, heure de l’île Norfolk, heure de l’île Wake, heure de l’Inde (Calcutta), heure de l’Inde (Colombo), heure de l’Iran (Téhéran), heure de l’Océan Indien (Chagos), heure de l’Ouest de l’Australie (Perth), heure de l’Ouest du Groenland (Nuuk), heure de l’Ouest du Kazakhstan (Aktaou), heure de l’Ouest du Kazakhstan (Aktioubinsk), heure de l’Ouest du Kazakhstan (Atyraou), heure de l’Ouest du Kazakhstan (Kzyl Orda), heure de l’Ouest du Kazakhstan (Ouralsk), heure de l’Ouest indonésien (Jakarta), heure de l’Ouest indonésien (Pontianak), heure de l’Ouzbékistan (Samarcande), heure de l’Ouzbékistan (Tachkent), heure de l’Uruguay (Montevideo), heure de la Chine (Macao), heure de la Chine (Shanghai), heure de la Corée (Pyongyang), heure de la Corée (Séoul), heure de la Géorgie (Tbilissi), heure de la Guyane française (Cayenne), heure de la Malaisie (Kuala Lumpur), heure de la Malaisie (Kuching), heure de la Nouvelle-Calédonie (Nouméa), heure de la Nouvelle-Zélande (Auckland), heure de la Nouvelle-Zélande (McMurdo), heure de la Papouasie-Nouvelle-Guinée (Bougainville), heure de la Papouasie-Nouvelle-Guinée (Port Moresby), heure de La Réunion, heure de Lord Howe, heure de Magadan, heure de Magadan (Srednekolymsk), heure de Maurice, heure de Mawson, heure de Moscou, heure de Moscou (Astrakhan), heure de Moscou (Minsk), heure de Moscou (Oulianovsk), heure de Moscou (Saratov), heure de Moscou (Simferopol), heure de Nauru, heure de Nioué (Niue), heure de Novossibirsk, heure de Omsk, heure de Petropavlovsk-Kamchatski (Kamtchatka), heure de Rothera, heure de Saint-Pierre-et-Miquelon, heure de Sakhaline, heure de Samara, heure de Singapour, heure de Syowa (Showa), heure de Tahiti, heure de Taipei, heure de Terre-Neuve (Saint-Jean de Terre-Neuve), heure de Tokelau (Fakaofo), heure de Vladivostok, heure de Vladivostok (Ust-Nera), heure de Volgograd, heure de Vostok, heure de Wallis-et-Futuna, heure des Açores, heure des Chamorro (Guam), heure des Chamorro (Saipan), heure des îles Chatham, heure des îles Cocos, heure des îles Cook (Rarotonga), heure des îles de la Ligne (Kiritimati), heure des îles Fidji, heure des îles Galápagos, heure des îles Gambier, heure des îles Gilbert (Tarawa), heure des îles Malouines (Stanley), heure des îles Marquises, heure des îles Marshall (Kwajalein), heure des îles Marshall (Majuro), heure des îles Phoenix (Enderbury), heure des îles Pitcairn, heure des îles Salomon (Guadalcanal), heure des Maldives, heure des Palaos (Palau), heure des Philippines (Manille), heure des Rocheuses, heure des Rocheuses (Boise), heure des Rocheuses (Cambridge Bay), heure des Rocheuses (Creston), heure des Rocheuses (Dawson Creek), heure des Rocheuses (Denver), heure des Rocheuses (Edmonton), heure des Rocheuses (Fort Nelson), heure des Rocheuses (Inuvik), heure des Rocheuses (Ojinaga), heure des Rocheuses (Phoenix), heure des Rocheuses (Yellowknife), heure des Samoa (Midway), heure des Samoa (Pago Pago), heure des Seychelles (Mahé), heure des Terres australes et antarctiques françaises (Kerguelen), heure des Tonga (Tongatapu), heure des Tuvalu (Funafuti), heure du Bangladesh (Dhaka), heure du Bhoutan (Thimphu), heure du Brunéi (Brunei), heure du Cap-Vert, heure du centre de l’Australie (Adélaïde), heure du centre de l’Australie (Broken Hill), heure du centre de l’Australie (Darwin), heure du Centre indonésien (Macassar), heure du centre nord-américain, heure du centre nord-américain (Bahia de Banderas), heure du centre nord-américain (Belize), heure du centre nord-américain (Beulah (Dakota du Nord)), heure du centre nord-américain (Center (Dakota du Nord)), heure du centre nord-américain (Chicago), heure du centre nord-américain (Costa Rica), heure du centre nord-américain (El Salvador), heure du centre nord-américain (Guatemala), heure du centre nord-américain (Knox [Indiana]), heure du centre nord-américain (Managua), heure du centre nord-américain (Matamoros), heure du centre nord-américain (Menominee), heure du centre nord-américain (Mérida), heure du centre nord-américain (Mexico), heure du centre nord-américain (Monterrey), heure du centre nord-américain (New Salem (Dakota du Nord)), heure du centre nord-américain (Rainy River), heure du centre nord-américain (Rankin Inlet), heure du centre nord-américain (Regina), heure du centre nord-américain (Resolute), heure du centre nord-américain (Swift Current), heure du centre nord-américain (Tégucigalpa), heure du centre nord-américain (Tell City [Indiana]), heure du centre nord-américain (Winnipeg), heure du centre-ouest de l’Australie (Eucla), heure du Chili (Palmer), heure du Chili (Punta Arenas), heure du Chili (Santiago), heure du Golfe (Dubaï), heure du Golfe (Mascate), heure du Guyana, heure du Japon (Tokyo), heure du Kirghizistan (Bichkek), heure du Myanmar (Rangoun), heure du Népal (Katmandou), heure du Nord-Ouest du Mexique (Santa Isabel), heure du Pacifique mexicain (Chihuahua), heure du Pacifique mexicain (Hermosillo), heure du Pacifique mexicain (Mazatlán), heure du Pacifique nord-américain, heure du Pacifique nord-américain (Los Angeles), heure du Pacifique nord-américain (Tijuana), heure du Pacifique nord-américain (Vancouver), heure du Pakistan (Karachi), heure du Paraguay (Asunción), heure du Pérou (Lima), heure du Suriname (Paramaribo), heure du Tadjikistan (Douchanbé), heure du Timor oriental (Dili), heure du Turkménistan (Achgabat), heure du Vanuatu (Éfaté), heure du Venezuela (Caracas), heure moyenne de Greenwich, heure moyenne de Greenwich (Abidjan), heure moyenne de Greenwich (Accra), heure moyenne de Greenwich (Bamako), heure moyenne de Greenwich (Banjul), heure moyenne de Greenwich (Bissau), heure moyenne de Greenwich (Conakry), heure moyenne de Greenwich (Dakar), heure moyenne de Greenwich (Danmarkshavn), heure moyenne de Greenwich (Dublin), heure moyenne de Greenwich (Freetown), heure moyenne de Greenwich (Guernesey), heure moyenne de Greenwich (Île de Man), heure moyenne de Greenwich (Jersey), heure moyenne de Greenwich (Lomé), heure moyenne de Greenwich (Londres), heure moyenne de Greenwich (Monrovia), heure moyenne de Greenwich (Nouakchott), heure moyenne de Greenwich (Ouagadougou), heure moyenne de Greenwich (Reykjavik), heure moyenne de Greenwich (Sainte-Hélène), heure moyenne de Greenwich (São Tomé), heure moyenne de Greenwich (Troll), heure normale d’Afrique centrale (Blantyre), heure normale d’Afrique centrale (Bujumbura), heure normale d’Afrique centrale (Gaborone), heure normale d’Afrique centrale (Harare), heure normale d’Afrique centrale (Juba), heure normale d’Afrique centrale (Khartoum), heure normale d’Afrique centrale (Kigali), heure normale d’Afrique centrale (Lubumbashi), heure normale d’Afrique centrale (Lusaka), heure normale d’Afrique centrale (Maputo), heure normale d’Afrique centrale (Windhoek), heure normale d’Afrique de l’Est (Addis-Abeba), heure normale d’Afrique de l’Est (Antananarivo), heure normale d’Afrique de l’Est (Asmara), heure normale d’Afrique de l’Est (Comores), heure normale d’Afrique de l’Est (Dar es Salaam), heure normale d’Afrique de l’Est (Djibouti), heure normale d’Afrique de l’Est (Kampala), heure normale d’Afrique de l’Est (Mayotte), heure normale d’Afrique de l’Est (Mogadiscio), heure normale d’Afrique de l’Est (Nairobi), heure normale d’Afrique méridionale (Johannesburg), heure normale d’Afrique méridionale (Maseru), heure normale d’Afrique méridionale (Mbabane), heure normale du Yukon (Dawson), heure normale du Yukon (Whitehorse), temps universel coordonné +--TEST-- +"timezone_names" function +--TEMPLATE-- +{{ timezone_names('UNKNOWN')|length }} +{{ timezone_names()|length }} +{{ timezone_names('fr')|length }} +{{ timezone_names()['Europe/Paris'] }} +{{ timezone_names('fr')['Europe/Paris'] }} +--DATA-- +return []; +--EXPECT-- +0 +434 +434 +Central European Time (Paris) +heure d’Europe centrale (Paris) From de55785d8530ed072cce7f4d17ad0bc1b64553eb Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Tue, 27 Dec 2022 13:19:29 +0100 Subject: [PATCH 026/812] Update CHANGELOG --- CHANGELOG | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index dc279ff84d4..9509a599303 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,8 @@ # 2.15.4 (2022-XX-XX) - * n/a + * Fix optimizing closures callbacks + * Add a better exception when getting an undefined constant via `constant` + * Fix `if` nodes when outside of a block and with an empty body # 2.15.3 (2022-09-28) From 0c8f5b2ea43ed9e7e6ef5f71a4741d018ef32a83 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Tue, 27 Dec 2022 13:23:36 +0100 Subject: [PATCH 027/812] Bump version to 3.5 --- CHANGELOG | 2 +- composer.json | 2 +- extra/cache-extra/composer.json | 2 +- extra/cssinliner-extra/composer.json | 2 +- extra/html-extra/composer.json | 2 +- extra/inky-extra/composer.json | 2 +- extra/intl-extra/composer.json | 2 +- extra/markdown-extra/composer.json | 2 +- extra/string-extra/composer.json | 2 +- extra/twig-extra-bundle/composer.json | 2 +- src/Environment.php | 8 ++++---- 11 files changed, 14 insertions(+), 14 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 0a3efca44b5..b3cc5877b0e 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,4 +1,4 @@ -# 3.4.4 (2022-XX-XX) +# 3.5.0 (2022-XX-XX) * Fix optimizing closures callbacks * Add a better exception when getting an undefined constant via `constant` diff --git a/composer.json b/composer.json index 33e46405c93..18d3135bb4b 100644 --- a/composer.json +++ b/composer.json @@ -44,7 +44,7 @@ }, "extra": { "branch-alias": { - "dev-master": "3.4-dev" + "dev-master": "3.5-dev" } } } diff --git a/extra/cache-extra/composer.json b/extra/cache-extra/composer.json index 462b0f48b30..05d75afd5f4 100644 --- a/extra/cache-extra/composer.json +++ b/extra/cache-extra/composer.json @@ -30,7 +30,7 @@ }, "extra": { "branch-alias": { - "dev-master": "3.2-dev" + "dev-master": "3.5-dev" } } } diff --git a/extra/cssinliner-extra/composer.json b/extra/cssinliner-extra/composer.json index b1c15249b7a..1866f417ef9 100644 --- a/extra/cssinliner-extra/composer.json +++ b/extra/cssinliner-extra/composer.json @@ -30,7 +30,7 @@ }, "extra": { "branch-alias": { - "dev-master": "3.2-dev" + "dev-master": "3.5-dev" } } } diff --git a/extra/html-extra/composer.json b/extra/html-extra/composer.json index 97d87f7e1b1..e5a545cf086 100644 --- a/extra/html-extra/composer.json +++ b/extra/html-extra/composer.json @@ -30,7 +30,7 @@ }, "extra": { "branch-alias": { - "dev-master": "3.2-dev" + "dev-master": "3.5-dev" } } } diff --git a/extra/inky-extra/composer.json b/extra/inky-extra/composer.json index abd7d399aa6..0a813c7bf2a 100644 --- a/extra/inky-extra/composer.json +++ b/extra/inky-extra/composer.json @@ -30,7 +30,7 @@ }, "extra": { "branch-alias": { - "dev-master": "3.2-dev" + "dev-master": "3.5-dev" } } } diff --git a/extra/intl-extra/composer.json b/extra/intl-extra/composer.json index abd60d38606..24f3f0577e3 100644 --- a/extra/intl-extra/composer.json +++ b/extra/intl-extra/composer.json @@ -30,7 +30,7 @@ }, "extra": { "branch-alias": { - "dev-master": "3.2-dev" + "dev-master": "3.5-dev" } } } diff --git a/extra/markdown-extra/composer.json b/extra/markdown-extra/composer.json index 45382b62f7a..f26143525a8 100644 --- a/extra/markdown-extra/composer.json +++ b/extra/markdown-extra/composer.json @@ -33,7 +33,7 @@ }, "extra": { "branch-alias": { - "dev-master": "3.2-dev" + "dev-master": "3.5-dev" } } } diff --git a/extra/string-extra/composer.json b/extra/string-extra/composer.json index e422f7c0c5f..2b4a6950e6e 100644 --- a/extra/string-extra/composer.json +++ b/extra/string-extra/composer.json @@ -31,7 +31,7 @@ }, "extra": { "branch-alias": { - "dev-master": "3.2-dev" + "dev-master": "3.5-dev" } } } diff --git a/extra/twig-extra-bundle/composer.json b/extra/twig-extra-bundle/composer.json index a946ecc3441..3e183704493 100644 --- a/extra/twig-extra-bundle/composer.json +++ b/extra/twig-extra-bundle/composer.json @@ -39,7 +39,7 @@ }, "extra": { "branch-alias": { - "dev-master": "3.2-dev" + "dev-master": "3.5-dev" } } } diff --git a/src/Environment.php b/src/Environment.php index 12db4bb2375..139b62eb709 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -40,11 +40,11 @@ */ class Environment { - public const VERSION = '3.4.4-DEV'; - public const VERSION_ID = 30404; + public const VERSION = '3.5.0-DEV'; + public const VERSION_ID = 30500; public const MAJOR_VERSION = 3; - public const MINOR_VERSION = 4; - public const RELEASE_VERSION = 4; + public const MINOR_VERSION = 5; + public const RELEASE_VERSION = 0; public const EXTRA_VERSION = 'DEV'; private $charset; From 0e5d37e59c6ce6af7d30c74924ec4075d97e0e9d Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Tue, 27 Dec 2022 13:25:09 +0100 Subject: [PATCH 028/812] Update CHANGELOG --- CHANGELOG | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index b3cc5877b0e..1c93c02d198 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,10 @@ # 3.5.0 (2022-XX-XX) + * Make Twig\ExpressionParser non internal + * Add "some" and "every" filters + * Add Compile::reset( + * Throw a better runtime error when the "matches" regexp is not valid + * Add "twig *_names" intl functions * Fix optimizing closures callbacks * Add a better exception when getting an undefined constant via `constant` * Fix `if` nodes when outside of a block and with an empty body From 3e059001d6d597dd50ea7c74dd2464b4adea48d3 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Tue, 27 Dec 2022 13:26:20 +0100 Subject: [PATCH 029/812] Prepare the 2.15.4 release --- CHANGELOG | 2 +- src/Environment.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 9509a599303..db87b236046 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,4 +1,4 @@ -# 2.15.4 (2022-XX-XX) +# 2.15.4 (2022-12-27) * Fix optimizing closures callbacks * Add a better exception when getting an undefined constant via `constant` diff --git a/src/Environment.php b/src/Environment.php index 463e4bc45a5..96f725106f3 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -38,12 +38,12 @@ */ class Environment { - public const VERSION = '2.15.4-DEV'; + public const VERSION = '2.15.4'; public const VERSION_ID = 21504; public const MAJOR_VERSION = 2; public const MINOR_VERSION = 15; public const RELEASE_VERSION = 4; - public const EXTRA_VERSION = 'DEV'; + public const EXTRA_VERSION = ''; private $charset; private $loader; From d573914760bd21f19ede028bfcd53a9a866a69e5 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Tue, 27 Dec 2022 13:27:28 +0100 Subject: [PATCH 030/812] Bump version --- CHANGELOG | 4 ++++ src/Environment.php | 8 ++++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index db87b236046..0b5344858b5 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +# 2.15.5 (2023-XX-XX) + + * n/a + # 2.15.4 (2022-12-27) * Fix optimizing closures callbacks diff --git a/src/Environment.php b/src/Environment.php index 96f725106f3..c5ab53f4656 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -38,12 +38,12 @@ */ class Environment { - public const VERSION = '2.15.4'; - public const VERSION_ID = 21504; + public const VERSION = '2.15.5-DEV'; + public const VERSION_ID = 21505; public const MAJOR_VERSION = 2; public const MINOR_VERSION = 15; - public const RELEASE_VERSION = 4; - public const EXTRA_VERSION = ''; + public const RELEASE_VERSION = 5; + public const EXTRA_VERSION = 'DEV'; private $charset; private $loader; From 3ffcf4b7d890770466da3b2666f82ac054e7ec72 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Tue, 27 Dec 2022 13:28:18 +0100 Subject: [PATCH 031/812] Prepare the 3.5.0 release --- CHANGELOG | 2 +- src/Environment.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 1c93c02d198..b86b9e5fc08 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,4 +1,4 @@ -# 3.5.0 (2022-XX-XX) +# 3.5.0 (2022-12-27) * Make Twig\ExpressionParser non internal * Add "some" and "every" filters diff --git a/src/Environment.php b/src/Environment.php index 139b62eb709..c4f2885849d 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -40,12 +40,12 @@ */ class Environment { - public const VERSION = '3.5.0-DEV'; + public const VERSION = '3.5.0'; public const VERSION_ID = 30500; public const MAJOR_VERSION = 3; public const MINOR_VERSION = 5; public const RELEASE_VERSION = 0; - public const EXTRA_VERSION = 'DEV'; + public const EXTRA_VERSION = ''; private $charset; private $loader; From 7d326d512faf0e493a59a283e0313b013b4237d5 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Tue, 27 Dec 2022 13:29:16 +0100 Subject: [PATCH 032/812] Bump version --- CHANGELOG | 3 +++ src/Environment.php | 8 ++++---- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index b86b9e5fc08..c65ae265e5f 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,6 @@ +# 3.5.1 (2023-XX-XX) + + * n/a # 3.5.0 (2022-12-27) * Make Twig\ExpressionParser non internal diff --git a/src/Environment.php b/src/Environment.php index c4f2885849d..1d999a33511 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -40,12 +40,12 @@ */ class Environment { - public const VERSION = '3.5.0'; - public const VERSION_ID = 30500; + public const VERSION = '3.5.1-DEV'; + public const VERSION_ID = 30501; public const MAJOR_VERSION = 3; public const MINOR_VERSION = 5; - public const RELEASE_VERSION = 0; - public const EXTRA_VERSION = ''; + public const RELEASE_VERSION = 1; + public const EXTRA_VERSION = 'DEV'; private $charset; private $loader; From 8f8d67b4898d30a71334f12c286a0feca9f664d9 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Tue, 27 Dec 2022 13:34:33 +0100 Subject: [PATCH 033/812] Add some missing functions in docs --- doc/functions/index.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/doc/functions/index.rst b/doc/functions/index.rst index 97465ed0395..eebcd61c7ab 100644 --- a/doc/functions/index.rst +++ b/doc/functions/index.rst @@ -19,4 +19,10 @@ Functions range source country_timezones + country_names + currency_names + language_names + locale_names + script_names + timezone_names template_from_string From 459ed67e4889f2e8ec4f7073757bda378e63da31 Mon Sep 17 00:00:00 2001 From: "hubert.lenoir" Date: Wed, 28 Dec 2022 14:34:31 +0100 Subject: [PATCH 034/812] Add "has some" and "has every" expressions --- doc/templates.rst | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/doc/templates.rst b/doc/templates.rst index 380d42b4f37..580458cf732 100644 --- a/doc/templates.rst +++ b/doc/templates.rst @@ -505,7 +505,7 @@ Twig allows expressions everywhere. The operator precedence is as follows, with the lowest-precedence operators listed first: ``?:`` (ternary operator), ``b-and``, ``b-xor``, ``b-or``, ``or``, ``and``, ``==``, ``!=``, ``<=>``, ``<``, ``>``, ``>=``, ``<=``, - ``in``, ``matches``, ``starts with``, ``ends with``, ``..``, ``+``, ``-``, + ``in``, ``matches``, ``starts with``, ``ends with``, ``has every``, ``has some``, ``..``, ``+``, ``-``, ``~``, ``*``, ``/``, ``//``, ``%``, ``is`` (tests), ``**``, ``??``, ``|`` (filters), ``[]``, and ``.``: @@ -661,6 +661,20 @@ next section). {% if phone matches '/^[\\d\\.]+$/' %} {% endif %} +Check that a sequence or a mapping ``has every`` or ``has some`` of its elements +``true`` using an arrow function. The arrow function receives the value of the +sequence or mapping. + +.. code-block:: twig + + {% set sizes = [34, 36, 38, 40, 42] %} + + {% set hasOnlyOver38 = sizes has every v => v > 38 %} + {# hasOnlyOver38 is false #} + + {% set hasOver38 = sizes has some v => v > 38 %} + {# hasOver38 is true #} + Containment Operator ~~~~~~~~~~~~~~~~~~~~ From dc693725ea61b4a5179e72f002e2968e55b0690c Mon Sep 17 00:00:00 2001 From: Jacob Dreesen Date: Tue, 27 Dec 2022 13:58:24 +0100 Subject: [PATCH 035/812] Fix CHANGELOG --- CHANGELOG | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index c65ae265e5f..68518ce96cf 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -3,9 +3,9 @@ * n/a # 3.5.0 (2022-12-27) - * Make Twig\ExpressionParser non internal - * Add "some" and "every" filters - * Add Compile::reset( + * Make Twig\ExpressionParser non-internal + * Add "some" and "every" tests + * Add Compile::reset() * Throw a better runtime error when the "matches" regexp is not valid * Add "twig *_names" intl functions * Fix optimizing closures callbacks From 7b4d2a69f3a80dfaa6e9aaf17c14bde7eaf5fb96 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Wed, 28 Dec 2022 14:50:47 +0100 Subject: [PATCH 036/812] Tweak docs --- CHANGELOG | 2 +- doc/templates.rst | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 68518ce96cf..131bdadb148 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -4,7 +4,7 @@ # 3.5.0 (2022-12-27) * Make Twig\ExpressionParser non-internal - * Add "some" and "every" tests + * Add "has some" and "has every" operators * Add Compile::reset() * Throw a better runtime error when the "matches" regexp is not valid * Add "twig *_names" intl functions diff --git a/doc/templates.rst b/doc/templates.rst index 580458cf732..5e52326e442 100644 --- a/doc/templates.rst +++ b/doc/templates.rst @@ -505,7 +505,8 @@ Twig allows expressions everywhere. The operator precedence is as follows, with the lowest-precedence operators listed first: ``?:`` (ternary operator), ``b-and``, ``b-xor``, ``b-or``, ``or``, ``and``, ``==``, ``!=``, ``<=>``, ``<``, ``>``, ``>=``, ``<=``, - ``in``, ``matches``, ``starts with``, ``ends with``, ``has every``, ``has some``, ``..``, ``+``, ``-``, + ``in``, ``matches``, ``starts with``, ``ends with``, ``has every``, ``has + some``, ``..``, ``+``, ``-``, ``~``, ``*``, ``/``, ``//``, ``%``, ``is`` (tests), ``**``, ``??``, ``|`` (filters), ``[]``, and ``.``: @@ -661,9 +662,9 @@ next section). {% if phone matches '/^[\\d\\.]+$/' %} {% endif %} -Check that a sequence or a mapping ``has every`` or ``has some`` of its elements -``true`` using an arrow function. The arrow function receives the value of the -sequence or mapping. +Check that a sequence or a mapping ``has every`` or ``has some`` of its +elements return ``true`` using an arrow function. The arrow function receives +the value of the sequence or mapping: .. code-block:: twig From eb8bde3c41402fa97edc197fa98dd921f163b461 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sun, 1 Jan 2023 09:42:51 +0100 Subject: [PATCH 037/812] Bump LICENSE year --- LICENSE | 2 +- extra/cssinliner-extra/LICENSE | 2 +- extra/html-extra/LICENSE | 2 +- extra/inky-extra/LICENSE | 2 +- extra/intl-extra/LICENSE | 2 +- extra/markdown-extra/LICENSE | 2 +- extra/string-extra/LICENSE | 2 +- extra/twig-extra-bundle/LICENSE | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/LICENSE b/LICENSE index 8711927f6d9..a2236f90d80 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2009-2022 by the Twig Team. +Copyright (c) 2009-2023 by the Twig Team. All rights reserved. diff --git a/extra/cssinliner-extra/LICENSE b/extra/cssinliner-extra/LICENSE index 9c907a46a62..5c7ba0551cb 100644 --- a/extra/cssinliner-extra/LICENSE +++ b/extra/cssinliner-extra/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2019-2022 Fabien Potencier +Copyright (c) 2019-2023 Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/extra/html-extra/LICENSE b/extra/html-extra/LICENSE index 9c907a46a62..5c7ba0551cb 100644 --- a/extra/html-extra/LICENSE +++ b/extra/html-extra/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2019-2022 Fabien Potencier +Copyright (c) 2019-2023 Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/extra/inky-extra/LICENSE b/extra/inky-extra/LICENSE index 9c907a46a62..5c7ba0551cb 100644 --- a/extra/inky-extra/LICENSE +++ b/extra/inky-extra/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2019-2022 Fabien Potencier +Copyright (c) 2019-2023 Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/extra/intl-extra/LICENSE b/extra/intl-extra/LICENSE index 9c907a46a62..5c7ba0551cb 100644 --- a/extra/intl-extra/LICENSE +++ b/extra/intl-extra/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2019-2022 Fabien Potencier +Copyright (c) 2019-2023 Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/extra/markdown-extra/LICENSE b/extra/markdown-extra/LICENSE index 9c907a46a62..5c7ba0551cb 100644 --- a/extra/markdown-extra/LICENSE +++ b/extra/markdown-extra/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2019-2022 Fabien Potencier +Copyright (c) 2019-2023 Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/extra/string-extra/LICENSE b/extra/string-extra/LICENSE index 9c907a46a62..5c7ba0551cb 100644 --- a/extra/string-extra/LICENSE +++ b/extra/string-extra/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2019-2022 Fabien Potencier +Copyright (c) 2019-2023 Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/extra/twig-extra-bundle/LICENSE b/extra/twig-extra-bundle/LICENSE index 9c907a46a62..5c7ba0551cb 100644 --- a/extra/twig-extra-bundle/LICENSE +++ b/extra/twig-extra-bundle/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2019-2022 Fabien Potencier +Copyright (c) 2019-2023 Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal From 44c46712b8b65317b4657a698ee94e8abc3ea602 Mon Sep 17 00:00:00 2001 From: Christophe Coevoet Date: Tue, 3 Jan 2023 18:06:19 +0100 Subject: [PATCH 038/812] Fix error messages in sandboxed mode for has some and has every --- src/Extension/CoreExtension.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index 65caab31fe1..ca3d28bf305 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -1716,7 +1716,7 @@ function twig_array_reduce(Environment $env, $array, $arrow, $initial = null) function twig_array_some(Environment $env, $array, $arrow) { - twig_check_arrow_in_sandbox($env, $arrow, 'some', 'filter'); + twig_check_arrow_in_sandbox($env, $arrow, 'has some', 'operator'); foreach ($array as $k => $v) { if ($arrow($v, $k)) { @@ -1729,7 +1729,7 @@ function twig_array_some(Environment $env, $array, $arrow) function twig_array_every(Environment $env, $array, $arrow) { - twig_check_arrow_in_sandbox($env, $arrow, 'every', 'filter'); + twig_check_arrow_in_sandbox($env, $arrow, 'has every', 'operator'); foreach ($array as $k => $v) { if (!$arrow($v, $k)) { From 9527f93e759c4e5a23d116378044ab89b28dc1a5 Mon Sep 17 00:00:00 2001 From: Javier Spagnoletti Date: Sat, 21 Jan 2023 14:32:09 -0300 Subject: [PATCH 039/812] Update docs for filters that use the `calendar` option --- doc/filters/format_date.rst | 2 +- doc/filters/format_datetime.rst | 2 +- doc/filters/format_time.rst | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/filters/format_date.rst b/doc/filters/format_date.rst index c4a900a4360..cd6beba9f5b 100644 --- a/doc/filters/format_date.rst +++ b/doc/filters/format_date.rst @@ -33,4 +33,4 @@ Arguments * ``dateFormat``: The date format * ``pattern``: A date time pattern * ``timezone``: The date timezone -* ``calendar``: The calendar (Gregorian by default) +* ``calendar``: The calendar ("gregorian" by default) diff --git a/doc/filters/format_datetime.rst b/doc/filters/format_datetime.rst index 9fed54f8ec9..e5c07228adb 100644 --- a/doc/filters/format_datetime.rst +++ b/doc/filters/format_datetime.rst @@ -97,6 +97,6 @@ Arguments * ``timeFormat``: The time format * ``pattern``: A date time pattern * ``timezone``: The date timezone name -* ``calendar``: The calendar (Gregorian by default) +* ``calendar``: The calendar ("gregorian" by default) .. _ICU user guide: https://unicode-org.github.io/icu/userguide/format_parse/datetime/#datetime-format-syntax diff --git a/doc/filters/format_time.rst b/doc/filters/format_time.rst index 417b8a9c62b..1e213e6163b 100644 --- a/doc/filters/format_time.rst +++ b/doc/filters/format_time.rst @@ -33,4 +33,4 @@ Arguments * ``timeFormat``: The time format * ``pattern``: A date time pattern * ``timezone``: The date timezone -* ``calendar``: The calendar (Gregorian by default) +* ``calendar``: The calendar ("gregorian" by default) From 4f2f3f814e6d5dfab62a4fd5197ae9dfb021ef36 Mon Sep 17 00:00:00 2001 From: ju1ius Date: Thu, 19 Jan 2023 11:01:38 +0100 Subject: [PATCH 040/812] pass the current key to reduce filter's callback --- CHANGELOG | 1 + doc/filters/reduce.rst | 10 +++++----- src/Extension/CoreExtension.php | 13 +++++++------ tests/Fixtures/filters/reduce_key.test | 14 ++++++++++++++ 4 files changed, 27 insertions(+), 11 deletions(-) create mode 100644 tests/Fixtures/filters/reduce_key.test diff --git a/CHANGELOG b/CHANGELOG index 131bdadb148..28d5c7fae3d 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -11,6 +11,7 @@ * Fix optimizing closures callbacks * Add a better exception when getting an undefined constant via `constant` * Fix `if` nodes when outside of a block and with an empty body + * Arrow functions passed to the "reduce" filter now accept the current key as a third argument # 3.4.3 (2022-09-28) diff --git a/doc/filters/reduce.rst b/doc/filters/reduce.rst index 7df4646c745..72c68d0deb9 100644 --- a/doc/filters/reduce.rst +++ b/doc/filters/reduce.rst @@ -4,21 +4,21 @@ The ``reduce`` filter iteratively reduces a sequence or a mapping to a single value using an arrow function, so as to reduce it to a single value. The arrow function receives the return value of the previous iteration and the current -value of the sequence or mapping: +value and key of the sequence or mapping: .. code-block:: twig {% set numbers = [1, 2, 3] %} - {{ numbers|reduce((carry, v) => carry + v) }} - {# output 6 #} + {{ numbers|reduce((carry, v, k) => carry + v * k) }} + {# output 8 #} The ``reduce`` filter takes an ``initial`` value as a second argument: .. code-block:: twig - {{ numbers|reduce((carry, v) => carry + v, 10) }} - {# output 16 #} + {{ numbers|reduce((carry, v, k) => carry + v * k, 10) }} + {# output 18 #} Note that the arrow function has access to the current context. diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index ca3d28bf305..ce846e91f74 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -1703,15 +1703,16 @@ function twig_array_reduce(Environment $env, $array, $arrow, $initial = null) { twig_check_arrow_in_sandbox($env, $arrow, 'reduce', 'filter'); - if (!\is_array($array)) { - if (!$array instanceof \Traversable) { - throw new RuntimeError(sprintf('The "reduce" filter only works with arrays or "Traversable", got "%s" as first argument.', \gettype($array))); - } + if (!\is_array($array) && !$array instanceof \Traversable) { + throw new RuntimeError(sprintf('The "reduce" filter only works with arrays or "Traversable", got "%s" as first argument.', \gettype($array))); + } - $array = iterator_to_array($array); + $accumulator = $initial; + foreach ($array as $key => $value) { + $accumulator = $arrow($accumulator, $value, $key); } - return array_reduce($array, $arrow, $initial); + return $accumulator; } function twig_array_some(Environment $env, $array, $arrow) diff --git a/tests/Fixtures/filters/reduce_key.test b/tests/Fixtures/filters/reduce_key.test new file mode 100644 index 00000000000..fe1fb0a7ac5 --- /dev/null +++ b/tests/Fixtures/filters/reduce_key.test @@ -0,0 +1,14 @@ +--TEST-- +"reduce" filter passes iterable key to callback +--TEMPLATE-- +{% set status_classes = { + 'success': 200, + 'warning': 400, + 'error': 500, +} %} + +{{ status_classes|reduce((carry, v, k) => status_code >= v ? k : carry, '') }} +--DATA-- +return ['status_code' => 404] +--EXPECT-- +warning From f52b45d226ff6409b45a07fe2bb3b8ca045bd2fe Mon Sep 17 00:00:00 2001 From: Thomas Landauer Date: Tue, 31 Jan 2023 21:48:47 +0100 Subject: [PATCH 041/812] Minor: Fixing language --- doc/filters/striptags.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/filters/striptags.rst b/doc/filters/striptags.rst index d5f542b3d8b..64a9c8156fe 100644 --- a/doc/filters/striptags.rst +++ b/doc/filters/striptags.rst @@ -1,7 +1,7 @@ ``striptags`` ============= -The ``striptags`` filter strips SGML/XML tags and replace adjacent whitespace +The ``striptags`` filter strips SGML/XML tags and replaces adjacent whitespace characters by one space: .. code-block:: twig From f136668933b075a7bbb2d0197355ae65613a386a Mon Sep 17 00:00:00 2001 From: Jacob Richardson Date: Tue, 7 Feb 2023 17:06:12 +0000 Subject: [PATCH 042/812] Restores the leniency of the `matches` twig comparison, allowing null subject to result in a non-match. Resolves BC break introduced in PR https://github.com/twigphp/Twig/pull/3687 As per pattern in https://github.com/twigphp/Twig/pull/3617 --- src/Extension/CoreExtension.php | 6 +++--- tests/Fixtures/expressions/matches.test | 2 ++ 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index ce846e91f74..f99adda451b 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -1021,19 +1021,19 @@ function twig_compare($a, $b) /** * @param string $pattern - * @param string $subject + * @param string|null $subject * * @return int * * @throws RuntimeError When an invalid pattern is used */ -function twig_matches(string $regexp, string $str) +function twig_matches(string $regexp, ?string $str) { set_error_handler(function ($t, $m) use ($regexp) { throw new RuntimeError(sprintf('Regexp "%s" passed to "matches" is not valid', $regexp).substr($m, 12)); }); try { - return preg_match($regexp, $str); + return preg_match($regexp, $str ?? ''); } finally { restore_error_handler(); } diff --git a/tests/Fixtures/expressions/matches.test b/tests/Fixtures/expressions/matches.test index 95459c3b0f2..8f5e3669e61 100644 --- a/tests/Fixtures/expressions/matches.test +++ b/tests/Fixtures/expressions/matches.test @@ -4,9 +4,11 @@ Twig supports the "matches" operator {{ 'foo' matches '/o/' ? 'OK' : 'KO' }} {{ 'foo' matches '/^fo/' ? 'OK' : 'KO' }} {{ 'foo' matches '/O/i' ? 'OK' : 'KO' }} +{{ null matches '/o/' }} --DATA-- return [] --EXPECT-- OK OK OK +0 From 872646a70ff83b3628d50c9bafa117af9f1da59e Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Wed, 8 Feb 2023 08:44:48 +0100 Subject: [PATCH 043/812] Fix LICENSE year --- LICENSE | 2 +- extra/cssinliner-extra/LICENSE | 2 +- extra/html-extra/LICENSE | 2 +- extra/inky-extra/LICENSE | 2 +- extra/intl-extra/LICENSE | 2 +- extra/markdown-extra/LICENSE | 2 +- extra/string-extra/LICENSE | 2 +- extra/twig-extra-bundle/LICENSE | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/LICENSE b/LICENSE index a2236f90d80..fd8234e511b 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2009-2023 by the Twig Team. +Copyright (c) 2009-present by the Twig Team. All rights reserved. diff --git a/extra/cssinliner-extra/LICENSE b/extra/cssinliner-extra/LICENSE index 5c7ba0551cb..f37c76b591d 100644 --- a/extra/cssinliner-extra/LICENSE +++ b/extra/cssinliner-extra/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2019-2023 Fabien Potencier +Copyright (c) 2019-present Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/extra/html-extra/LICENSE b/extra/html-extra/LICENSE index 5c7ba0551cb..f37c76b591d 100644 --- a/extra/html-extra/LICENSE +++ b/extra/html-extra/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2019-2023 Fabien Potencier +Copyright (c) 2019-present Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/extra/inky-extra/LICENSE b/extra/inky-extra/LICENSE index 5c7ba0551cb..f37c76b591d 100644 --- a/extra/inky-extra/LICENSE +++ b/extra/inky-extra/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2019-2023 Fabien Potencier +Copyright (c) 2019-present Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/extra/intl-extra/LICENSE b/extra/intl-extra/LICENSE index 5c7ba0551cb..f37c76b591d 100644 --- a/extra/intl-extra/LICENSE +++ b/extra/intl-extra/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2019-2023 Fabien Potencier +Copyright (c) 2019-present Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/extra/markdown-extra/LICENSE b/extra/markdown-extra/LICENSE index 5c7ba0551cb..f37c76b591d 100644 --- a/extra/markdown-extra/LICENSE +++ b/extra/markdown-extra/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2019-2023 Fabien Potencier +Copyright (c) 2019-present Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/extra/string-extra/LICENSE b/extra/string-extra/LICENSE index 5c7ba0551cb..f37c76b591d 100644 --- a/extra/string-extra/LICENSE +++ b/extra/string-extra/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2019-2023 Fabien Potencier +Copyright (c) 2019-present Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/extra/twig-extra-bundle/LICENSE b/extra/twig-extra-bundle/LICENSE index 5c7ba0551cb..f37c76b591d 100644 --- a/extra/twig-extra-bundle/LICENSE +++ b/extra/twig-extra-bundle/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2019-2023 Fabien Potencier +Copyright (c) 2019-present Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal From 0752948cdf564f6f7932234037449f5567585251 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Wed, 8 Feb 2023 08:48:35 +0100 Subject: [PATCH 044/812] Update CHANGELOG --- CHANGELOG | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 28d5c7fae3d..408fcd259dd 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,9 @@ # 3.5.1 (2023-XX-XX) - * n/a + * Arrow functions passed to the "reduce" filter now accept the current key as a third argument + * Restores the leniency of the matches twig comparison + * Fix error messages in sandboxed mode for "has some" and "has every" + # 3.5.0 (2022-12-27) * Make Twig\ExpressionParser non-internal @@ -11,7 +14,6 @@ * Fix optimizing closures callbacks * Add a better exception when getting an undefined constant via `constant` * Fix `if` nodes when outside of a block and with an empty body - * Arrow functions passed to the "reduce" filter now accept the current key as a third argument # 3.4.3 (2022-09-28) From a6e0510cc793912b451fd40ab983a1d28f611c15 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Wed, 8 Feb 2023 08:49:20 +0100 Subject: [PATCH 045/812] Prepare the 3.5.1 release --- CHANGELOG | 2 +- src/Environment.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 408fcd259dd..fd2c9c62e6f 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,4 +1,4 @@ -# 3.5.1 (2023-XX-XX) +# 3.5.1 (2023-02-08) * Arrow functions passed to the "reduce" filter now accept the current key as a third argument * Restores the leniency of the matches twig comparison diff --git a/src/Environment.php b/src/Environment.php index 1d999a33511..dd721b41236 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -40,12 +40,12 @@ */ class Environment { - public const VERSION = '3.5.1-DEV'; + public const VERSION = '3.5.1'; public const VERSION_ID = 30501; public const MAJOR_VERSION = 3; public const MINOR_VERSION = 5; public const RELEASE_VERSION = 1; - public const EXTRA_VERSION = 'DEV'; + public const EXTRA_VERSION = ''; private $charset; private $loader; From 2bd9c96c74d6ae8523e61affee544ded2c795e27 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Wed, 8 Feb 2023 08:50:32 +0100 Subject: [PATCH 046/812] Fix LICENSE year --- extra/cache-extra/LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extra/cache-extra/LICENSE b/extra/cache-extra/LICENSE index efb17f98e7d..99c6bdf356e 100644 --- a/extra/cache-extra/LICENSE +++ b/extra/cache-extra/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2021 Fabien Potencier +Copyright (c) 2021-present Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal From e172f3c6f415c3fcefcf72b32379d1a9a66f5e5a Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Wed, 8 Feb 2023 08:51:03 +0100 Subject: [PATCH 047/812] Bump version --- CHANGELOG | 4 ++++ src/Environment.php | 8 ++++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index fd2c9c62e6f..568087f4623 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +# 3.5.2 (2023-XX-XX) + + * n/a + # 3.5.1 (2023-02-08) * Arrow functions passed to the "reduce" filter now accept the current key as a third argument diff --git a/src/Environment.php b/src/Environment.php index dd721b41236..5f91d6322cc 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -40,12 +40,12 @@ */ class Environment { - public const VERSION = '3.5.1'; - public const VERSION_ID = 30501; + public const VERSION = '3.5.2-DEV'; + public const VERSION_ID = 30502; public const MAJOR_VERSION = 3; public const MINOR_VERSION = 5; - public const RELEASE_VERSION = 1; - public const EXTRA_VERSION = ''; + public const RELEASE_VERSION = 2; + public const EXTRA_VERSION = 'DEV'; private $charset; private $loader; From 5776f76068dd2c56d302cde5bf4e726a1bb58e22 Mon Sep 17 00:00:00 2001 From: Quentin Schuler Date: Thu, 2 Feb 2023 09:24:56 +0100 Subject: [PATCH 048/812] Add the new PHP 8.0 IntlDateFormatter::RELATIVE_* constants for date formatting. --- doc/filters/format_datetime.rst | 6 +++ extra/intl-extra/IntlExtension.php | 45 ++++++++++++++++--- .../Tests/Fixtures/format_date_php8.test | 12 +++++ 3 files changed, 56 insertions(+), 7 deletions(-) create mode 100644 extra/intl-extra/Tests/Fixtures/format_date_php8.test diff --git a/doc/filters/format_datetime.rst b/doc/filters/format_datetime.rst index e5c07228adb..8f3b46d479a 100644 --- a/doc/filters/format_datetime.rst +++ b/doc/filters/format_datetime.rst @@ -26,6 +26,12 @@ You can tweak the output for the date part and the time part: Supported values are: ``none``, ``short``, ``medium``, ``long``, and ``full``. +.. versionadded:: 3.6 + + ``relative_short``, ``relative_medium``, ``relative_long``, and ``relative_full`` are also supported when running on + PHP 8.0 and superior or when using a polyfill that define the ``IntlDateFormatter::RELATIVE_*`` constants and + associated behavior. + For greater flexibility, you can even define your own pattern (see the `ICU user guide`_ for supported patterns). diff --git a/extra/intl-extra/IntlExtension.php b/extra/intl-extra/IntlExtension.php index 76c2b271e71..955d6ec9233 100644 --- a/extra/intl-extra/IntlExtension.php +++ b/extra/intl-extra/IntlExtension.php @@ -26,7 +26,36 @@ final class IntlExtension extends AbstractExtension { - private const DATE_FORMATS = [ + private static function availableDateFormats(): array + { + static $formats = null; + + if (null !== $formats) { + return $formats; + } + + $formats = [ + 'none' => \IntlDateFormatter::NONE, + 'short' => \IntlDateFormatter::SHORT, + 'medium' => \IntlDateFormatter::MEDIUM, + 'long' => \IntlDateFormatter::LONG, + 'full' => \IntlDateFormatter::FULL, + ]; + + // Assuming that each `RELATIVE_*` constant are defined when one of them is. + if (\defined('IntlDateFormatter::RELATIVE_FULL')) { + $formats = array_merge($formats, [ + 'relative_short' => \IntlDateFormatter::RELATIVE_SHORT, + 'relative_medium' => \IntlDateFormatter::RELATIVE_MEDIUM, + 'relative_long' => \IntlDateFormatter::RELATIVE_LONG, + 'relative_full' => \IntlDateFormatter::RELATIVE_FULL, + ]); + } + + return $formats; + } + + private const TIME_FORMATS = [ 'none' => \IntlDateFormatter::NONE, 'short' => \IntlDateFormatter::SHORT, 'medium' => \IntlDateFormatter::MEDIUM, @@ -370,12 +399,14 @@ public function formatTime(Environment $env, $date, ?string $timeFormat = 'mediu private function createDateFormatter(?string $locale, ?string $dateFormat, ?string $timeFormat, string $pattern, \DateTimeZone $timezone, string $calendar): \IntlDateFormatter { - if (null !== $dateFormat && !isset(self::DATE_FORMATS[$dateFormat])) { - throw new RuntimeError(sprintf('The date format "%s" does not exist, known formats are: "%s".', $dateFormat, implode('", "', array_keys(self::DATE_FORMATS)))); + $dateFormats = self::availableDateFormats(); + + if (null !== $dateFormat && !isset($dateFormats[$dateFormat])) { + throw new RuntimeError(sprintf('The date format "%s" does not exist, known formats are: "%s".', $dateFormat, implode('", "', array_keys($dateFormats)))); } - if (null !== $timeFormat && !isset(self::DATE_FORMATS[$timeFormat])) { - throw new RuntimeError(sprintf('The time format "%s" does not exist, known formats are: "%s".', $timeFormat, implode('", "', array_keys(self::DATE_FORMATS)))); + if (null !== $timeFormat && !isset(self::TIME_FORMATS[$timeFormat])) { + throw new RuntimeError(sprintf('The time format "%s" does not exist, known formats are: "%s".', $timeFormat, implode('", "', array_keys(self::TIME_FORMATS)))); } if (null === $locale) { @@ -384,8 +415,8 @@ private function createDateFormatter(?string $locale, ?string $dateFormat, ?stri $calendar = 'gregorian' === $calendar ? \IntlDateFormatter::GREGORIAN : \IntlDateFormatter::TRADITIONAL; - $dateFormatValue = self::DATE_FORMATS[$dateFormat] ?? null; - $timeFormatValue = self::DATE_FORMATS[$timeFormat] ?? null; + $dateFormatValue = $dateFormats[$dateFormat] ?? null; + $timeFormatValue = self::TIME_FORMATS[$timeFormat] ?? null; if ($this->dateFormatterPrototype) { $dateFormatValue = $dateFormatValue ?: $this->dateFormatterPrototype->getDateType(); diff --git a/extra/intl-extra/Tests/Fixtures/format_date_php8.test b/extra/intl-extra/Tests/Fixtures/format_date_php8.test new file mode 100644 index 00000000000..67e0e6f4dbe --- /dev/null +++ b/extra/intl-extra/Tests/Fixtures/format_date_php8.test @@ -0,0 +1,12 @@ +--TEST-- +"format_date" filter +--CONDITION-- +PHP_VERSION_ID >= 80000 +--TEMPLATE-- +{{ 'today 23:39:12'|format_datetime('relative_short', 'none', locale='fr') }} +{{ 'today 23:39:12'|format_datetime('relative_full', 'full', locale='fr') }} +--DATA-- +return []; +--EXPECT-- +aujourd’hui +aujourd’hui à 23:39:12 temps universel coordonné From 08d2037f264e45113896fee52f31d688fe4fd57c Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Thu, 9 Feb 2023 07:45:16 +0100 Subject: [PATCH 049/812] Bump version to 3.6 --- CHANGELOG | 2 +- composer.json | 5 ----- extra/cache-extra/composer.json | 5 ----- extra/cssinliner-extra/composer.json | 5 ----- extra/html-extra/composer.json | 5 ----- extra/inky-extra/composer.json | 5 ----- extra/intl-extra/composer.json | 5 ----- extra/markdown-extra/composer.json | 5 ----- extra/string-extra/composer.json | 5 ----- extra/twig-extra-bundle/composer.json | 5 ----- src/Environment.php | 8 ++++---- 11 files changed, 5 insertions(+), 50 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 568087f4623..2b71ee32c97 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,4 +1,4 @@ -# 3.5.2 (2023-XX-XX) +# 3.6.0 (2023-XX-XX) * n/a diff --git a/composer.json b/composer.json index 18d3135bb4b..5e9999ae30f 100644 --- a/composer.json +++ b/composer.json @@ -41,10 +41,5 @@ "psr-4" : { "Twig\\Tests\\" : "tests/" } - }, - "extra": { - "branch-alias": { - "dev-master": "3.5-dev" - } } } diff --git a/extra/cache-extra/composer.json b/extra/cache-extra/composer.json index 05d75afd5f4..ec116d7d4ab 100644 --- a/extra/cache-extra/composer.json +++ b/extra/cache-extra/composer.json @@ -27,10 +27,5 @@ "exclude-from-classmap": [ "/Tests/" ] - }, - "extra": { - "branch-alias": { - "dev-master": "3.5-dev" - } } } diff --git a/extra/cssinliner-extra/composer.json b/extra/cssinliner-extra/composer.json index 1866f417ef9..a6799ed0dce 100644 --- a/extra/cssinliner-extra/composer.json +++ b/extra/cssinliner-extra/composer.json @@ -27,10 +27,5 @@ "exclude-from-classmap": [ "/Tests/" ] - }, - "extra": { - "branch-alias": { - "dev-master": "3.5-dev" - } } } diff --git a/extra/html-extra/composer.json b/extra/html-extra/composer.json index e5a545cf086..44f5a9faa46 100644 --- a/extra/html-extra/composer.json +++ b/extra/html-extra/composer.json @@ -27,10 +27,5 @@ "exclude-from-classmap": [ "/Tests/" ] - }, - "extra": { - "branch-alias": { - "dev-master": "3.5-dev" - } } } diff --git a/extra/inky-extra/composer.json b/extra/inky-extra/composer.json index 0a813c7bf2a..17e1be92c96 100644 --- a/extra/inky-extra/composer.json +++ b/extra/inky-extra/composer.json @@ -27,10 +27,5 @@ "exclude-from-classmap": [ "/Tests/" ] - }, - "extra": { - "branch-alias": { - "dev-master": "3.5-dev" - } } } diff --git a/extra/intl-extra/composer.json b/extra/intl-extra/composer.json index 24f3f0577e3..d357ad7254e 100644 --- a/extra/intl-extra/composer.json +++ b/extra/intl-extra/composer.json @@ -27,10 +27,5 @@ "exclude-from-classmap": [ "/Tests/" ] - }, - "extra": { - "branch-alias": { - "dev-master": "3.5-dev" - } } } diff --git a/extra/markdown-extra/composer.json b/extra/markdown-extra/composer.json index f26143525a8..20389ff9b24 100644 --- a/extra/markdown-extra/composer.json +++ b/extra/markdown-extra/composer.json @@ -30,10 +30,5 @@ "exclude-from-classmap": [ "/Tests/" ] - }, - "extra": { - "branch-alias": { - "dev-master": "3.5-dev" - } } } diff --git a/extra/string-extra/composer.json b/extra/string-extra/composer.json index 2b4a6950e6e..ed5d3ffa2f1 100644 --- a/extra/string-extra/composer.json +++ b/extra/string-extra/composer.json @@ -28,10 +28,5 @@ "exclude-from-classmap": [ "/Tests/" ] - }, - "extra": { - "branch-alias": { - "dev-master": "3.5-dev" - } } } diff --git a/extra/twig-extra-bundle/composer.json b/extra/twig-extra-bundle/composer.json index 3e183704493..b7f739bdf1f 100644 --- a/extra/twig-extra-bundle/composer.json +++ b/extra/twig-extra-bundle/composer.json @@ -36,10 +36,5 @@ "exclude-from-classmap": [ "/Tests/" ] - }, - "extra": { - "branch-alias": { - "dev-master": "3.5-dev" - } } } diff --git a/src/Environment.php b/src/Environment.php index 5f91d6322cc..39b7cec2f65 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -40,11 +40,11 @@ */ class Environment { - public const VERSION = '3.5.2-DEV'; - public const VERSION_ID = 30502; + public const VERSION = '3.6.0-DEV'; + public const VERSION_ID = 30600; public const MAJOR_VERSION = 3; - public const MINOR_VERSION = 5; - public const RELEASE_VERSION = 2; + public const MINOR_VERSION = 3; + public const RELEASE_VERSION = 0; public const EXTRA_VERSION = 'DEV'; private $charset; From d140542c2ef7a3ca7f31acf4d02425a3eadd9c81 Mon Sep 17 00:00:00 2001 From: Tobias Meindl <17339632+tobilektri@users.noreply.github.com> Date: Wed, 15 Feb 2023 10:01:39 +0100 Subject: [PATCH 050/812] Allow psr/container 2.0.2 Should allow psr/container 2.0.2 - not only 1.1.2 Changes: https://github.com/php-fig/container/compare/2.0.2...1.1.2 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 5e9999ae30f..aeea64053ff 100644 --- a/composer.json +++ b/composer.json @@ -30,7 +30,7 @@ }, "require-dev": { "symfony/phpunit-bridge": "^4.4.9|^5.0.9|^6.0", - "psr/container": "^1.0" + "psr/container": "^1.0|^2.0" }, "autoload": { "psr-4" : { From 19be50ff01b5acd85d810df1aaefd7300de92ce9 Mon Sep 17 00:00:00 2001 From: Thomas Landauer Date: Tue, 11 Apr 2023 12:35:17 +0200 Subject: [PATCH 051/812] Adding mb_strlen --- doc/filters/length.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/doc/filters/length.rst b/doc/filters/length.rst index d36712233cd..a9dfae423ca 100644 --- a/doc/filters/length.rst +++ b/doc/filters/length.rst @@ -12,8 +12,12 @@ it will return the length of the string provided by that method. For objects that implement the ``Traversable`` interface, ``length`` will use the return value of the ``iterator_count()`` method. +For strings, `mb_strlen()`_ is used. + .. code-block:: twig {% if users|length > 10 %} ... {% endif %} + +.. _mb_strlen(): https://www.php.net/manual/function.mb-strlen.php From b7a8ebbe6f1811844d561f5dcbf468cdf5457e4c Mon Sep 17 00:00:00 2001 From: "Phil E. Taylor" Date: Fri, 14 Apr 2023 12:03:02 +0100 Subject: [PATCH 052/812] return annotation to suppress deprecation warning --- extra/twig-extra-bundle/TwigExtraBundle.php | 1 + 1 file changed, 1 insertion(+) diff --git a/extra/twig-extra-bundle/TwigExtraBundle.php b/extra/twig-extra-bundle/TwigExtraBundle.php index 5ed32a8ccca..a9c8f734bf8 100644 --- a/extra/twig-extra-bundle/TwigExtraBundle.php +++ b/extra/twig-extra-bundle/TwigExtraBundle.php @@ -17,6 +17,7 @@ class TwigExtraBundle extends Bundle { + /** @return void */ public function build(ContainerBuilder $container) { parent::build($container); From 9bb05acae41a0f575eb3914c1e80a99c6334ca33 Mon Sep 17 00:00:00 2001 From: Terence Eden Date: Mon, 24 Apr 2023 22:26:49 +0100 Subject: [PATCH 053/812] Put example all on one line Fixes #3833 --- doc/tags/if.rst | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/doc/tags/if.rst b/doc/tags/if.rst index 2d7475227c1..0523cb1ac19 100644 --- a/doc/tags/if.rst +++ b/doc/tags/if.rst @@ -26,8 +26,7 @@ You can also test if an array is not empty: .. note:: - If you want to test if the variable is defined, use ``if users is - defined`` instead. + If you want to test if the variable is defined, use ``if users is defined`` instead. You can also use ``not`` to check for values that evaluate to ``false``: From 73f5cad8fb09614a0ed1caa301315f9f75c2f2d7 Mon Sep 17 00:00:00 2001 From: Christoph Wieseke Date: Fri, 28 Apr 2023 13:40:11 +0200 Subject: [PATCH 054/812] Remove duplicate sentence in macro scoping --- doc/tags/macro.rst | 4 ---- 1 file changed, 4 deletions(-) diff --git a/doc/tags/macro.rst b/doc/tags/macro.rst index 42fc460cae8..effa6b6bdc9 100644 --- a/doc/tags/macro.rst +++ b/doc/tags/macro.rst @@ -104,10 +104,6 @@ When calling ``import`` or ``from`` from a ``block`` tag, the imported macros are only defined in the current block and they override macros defined at the template level with the same names. -When calling ``import`` or ``from`` from a ``macro`` tag, the imported macros -are only defined in the current macro and they override macros defined at the -template level with the same names. - Checking if a Macro is defined ------------------------------ From 2f7e868017acc698228446041a79ee786abf77bd Mon Sep 17 00:00:00 2001 From: Ryan Weaver Date: Wed, 3 May 2023 06:40:00 -0400 Subject: [PATCH 055/812] Making the Lexer initialize itself lazily, to avoid loading the extension set early --- src/Lexer.php | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/Lexer.php b/src/Lexer.php index edde9a7a0cb..78931d7119d 100644 --- a/src/Lexer.php +++ b/src/Lexer.php @@ -21,6 +21,8 @@ */ class Lexer { + private $isInitialized = false; + private $tokens; private $code; private $cursor; @@ -63,6 +65,15 @@ public function __construct(Environment $env, array $options = []) 'whitespace_line_chars' => ' \t\0\x0B', 'interpolation' => ['#{', '}'], ], $options); + } + + private function initialize() + { + if ($this->isInitialized) { + return; + } + + $this->isInitialized = true; // when PHP 7.3 is the min version, we will be able to remove the '#' part in preg_quote as it's part of the default $this->regexes = [ @@ -155,6 +166,8 @@ public function __construct(Environment $env, array $options = []) public function tokenize(Source $source) { + $this->initialize(); + $this->source = $source; $this->code = str_replace(["\r\n", "\r"], "\n", $source->getCode()); $this->cursor = 0; From 17bf0637d95afe89e198a487a6e7289d0e6970b1 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Wed, 3 May 2023 19:49:13 +0200 Subject: [PATCH 056/812] Update CHANGELOG --- CHANGELOG | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 0b5344858b5..9cc1d9a7191 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,6 @@ # 2.15.5 (2023-XX-XX) - * n/a + * Make the Lexer initialize itself lazily # 2.15.4 (2022-12-27) From fc02a6af3eeb97c4bf5650debc76c2eda85ac22e Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Wed, 3 May 2023 19:49:41 +0200 Subject: [PATCH 057/812] Prepare the 2.15.5 release --- CHANGELOG | 2 +- src/Environment.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 9cc1d9a7191..a48393ad05a 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,4 +1,4 @@ -# 2.15.5 (2023-XX-XX) +# 2.15.5 (2023-05-03) * Make the Lexer initialize itself lazily diff --git a/src/Environment.php b/src/Environment.php index c5ab53f4656..9cebf35d56f 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -38,12 +38,12 @@ */ class Environment { - public const VERSION = '2.15.5-DEV'; + public const VERSION = '2.15.5'; public const VERSION_ID = 21505; public const MAJOR_VERSION = 2; public const MINOR_VERSION = 15; public const RELEASE_VERSION = 5; - public const EXTRA_VERSION = 'DEV'; + public const EXTRA_VERSION = ''; private $charset; private $loader; From 0911e406751e6be83bad6debd25f6d3daf994325 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Wed, 3 May 2023 21:06:32 +0200 Subject: [PATCH 058/812] Update CHANGELOG --- CHANGELOG | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index d28ae6a8444..cbe1a076c79 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,7 @@ # 3.6.0 (2023-XX-XX) + * Allow psr/container 2.0 + * Add the new PHP 8.0 IntlDateFormatter::RELATIVE_* constants for date formatting * Make the Lexer initialize itself lazily # 3.5.1 (2023-02-08) From 106c170d08e8415d78be2d16c3d057d0d108262b Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Wed, 3 May 2023 21:06:57 +0200 Subject: [PATCH 059/812] Prepare the 3.6.0 release --- CHANGELOG | 2 +- src/Environment.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index cbe1a076c79..46134b232d5 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,4 +1,4 @@ -# 3.6.0 (2023-XX-XX) +# 3.6.0 (2023-05-03) * Allow psr/container 2.0 * Add the new PHP 8.0 IntlDateFormatter::RELATIVE_* constants for date formatting diff --git a/src/Environment.php b/src/Environment.php index 39b7cec2f65..4b7d989bbef 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -40,12 +40,12 @@ */ class Environment { - public const VERSION = '3.6.0-DEV'; + public const VERSION = '3.6.0'; public const VERSION_ID = 30600; public const MAJOR_VERSION = 3; public const MINOR_VERSION = 3; public const RELEASE_VERSION = 0; - public const EXTRA_VERSION = 'DEV'; + public const EXTRA_VERSION = ''; private $charset; private $loader; From 3fd3645601085084f808577b870e5e7e3569c1c6 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Wed, 3 May 2023 21:08:22 +0200 Subject: [PATCH 060/812] Bump version --- CHANGELOG | 4 ++++ src/Environment.php | 10 +++++----- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 46134b232d5..24a5c1c2efb 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +# 3.6.1 (2023-XX-XX) + + * n/a + # 3.6.0 (2023-05-03) * Allow psr/container 2.0 diff --git a/src/Environment.php b/src/Environment.php index 4b7d989bbef..a2d84798c67 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -40,12 +40,12 @@ */ class Environment { - public const VERSION = '3.6.0'; - public const VERSION_ID = 30600; + public const VERSION = '3.6.1-DEV'; + public const VERSION_ID = 30601; public const MAJOR_VERSION = 3; - public const MINOR_VERSION = 3; - public const RELEASE_VERSION = 0; - public const EXTRA_VERSION = ''; + public const MINOR_VERSION = 6; + public const RELEASE_VERSION = 1; + public const EXTRA_VERSION = 'DEV'; private $charset; private $loader; From 51ccbdc9831f41ba4c80a231864e42ef8d8f6910 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Wed, 3 May 2023 21:08:52 +0200 Subject: [PATCH 061/812] Bump version --- CHANGELOG | 4 ++++ src/Environment.php | 8 ++++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index a48393ad05a..43e6604b6c1 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +# 2.15.6 (2023-XX-XX) + + * n/a + # 2.15.5 (2023-05-03) * Make the Lexer initialize itself lazily diff --git a/src/Environment.php b/src/Environment.php index 9cebf35d56f..e6c27a4bc0a 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -38,12 +38,12 @@ */ class Environment { - public const VERSION = '2.15.5'; - public const VERSION_ID = 21505; + public const VERSION = '2.15.6-DEV'; + public const VERSION_ID = 21506; public const MAJOR_VERSION = 2; public const MINOR_VERSION = 15; - public const RELEASE_VERSION = 5; - public const EXTRA_VERSION = ''; + public const RELEASE_VERSION = 6; + public const EXTRA_VERSION = 'DEV'; private $charset; private $loader; From 72d6f20556ca4ac693c731a4b93835b0d70ca150 Mon Sep 17 00:00:00 2001 From: "Phil E. Taylor" Date: Sat, 6 May 2023 12:11:46 +0100 Subject: [PATCH 062/812] suppress native return type deprecation msg --- .../Compiler/MissingExtensionSuggestorPass.php | 1 + .../twig-extra-bundle/DependencyInjection/TwigExtraExtension.php | 1 + 2 files changed, 2 insertions(+) diff --git a/extra/twig-extra-bundle/DependencyInjection/Compiler/MissingExtensionSuggestorPass.php b/extra/twig-extra-bundle/DependencyInjection/Compiler/MissingExtensionSuggestorPass.php index 3f3cca62a35..245e5bfd1da 100644 --- a/extra/twig-extra-bundle/DependencyInjection/Compiler/MissingExtensionSuggestorPass.php +++ b/extra/twig-extra-bundle/DependencyInjection/Compiler/MissingExtensionSuggestorPass.php @@ -18,6 +18,7 @@ class MissingExtensionSuggestorPass implements CompilerPassInterface { + /** @return void */ public function process(ContainerBuilder $container) { if ($container->getParameter('kernel.debug')) { diff --git a/extra/twig-extra-bundle/DependencyInjection/TwigExtraExtension.php b/extra/twig-extra-bundle/DependencyInjection/TwigExtraExtension.php index 0f57d71f36a..a1d5decd56f 100644 --- a/extra/twig-extra-bundle/DependencyInjection/TwigExtraExtension.php +++ b/extra/twig-extra-bundle/DependencyInjection/TwigExtraExtension.php @@ -23,6 +23,7 @@ */ class TwigExtraExtension extends Extension { + /** @return void */ public function load(array $configs, ContainerBuilder $container) { $loader = new PhpFileLoader($container, new FileLocator(\dirname(__DIR__).'/Resources/config')); From 69d58e57054e4872f4a07694871cbfb0137da848 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Thu, 8 Jun 2023 14:51:43 +0200 Subject: [PATCH 063/812] Update CHANGELOG --- CHANGELOG | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 24a5c1c2efb..1751d9908fa 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,6 @@ # 3.6.1 (2023-XX-XX) - * n/a + * Suppress some native return type deprecation messages # 3.6.0 (2023-05-03) From 7e7d5839d4bec168dfeef0ac66d5c5a2edbabffd Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Thu, 8 Jun 2023 14:52:13 +0200 Subject: [PATCH 064/812] Prepare the 3.6.1 release --- CHANGELOG | 2 +- src/Environment.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 1751d9908fa..c3d65518e1f 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,4 +1,4 @@ -# 3.6.1 (2023-XX-XX) +# 3.6.1 (2023-06-08) * Suppress some native return type deprecation messages diff --git a/src/Environment.php b/src/Environment.php index a2d84798c67..70405b0d7e7 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -40,12 +40,12 @@ */ class Environment { - public const VERSION = '3.6.1-DEV'; + public const VERSION = '3.6.1'; public const VERSION_ID = 30601; public const MAJOR_VERSION = 3; public const MINOR_VERSION = 6; public const RELEASE_VERSION = 1; - public const EXTRA_VERSION = 'DEV'; + public const EXTRA_VERSION = ''; private $charset; private $loader; From ef9d785518cf5bb4363c622400afe5935fccff9a Mon Sep 17 00:00:00 2001 From: Matthias Pigulla Date: Fri, 9 Jun 2023 07:44:15 +0200 Subject: [PATCH 065/812] Mention where named arguments are supported --- doc/templates.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/templates.rst b/doc/templates.rst index 83f6b7d6c98..3b18e9db074 100644 --- a/doc/templates.rst +++ b/doc/templates.rst @@ -205,6 +205,8 @@ built-in functions. Named Arguments --------------- +Named arguments are supported in functions, filters and tests. + .. code-block:: twig {% for i in range(low=1, high=10, step=2) %} From df75bfef370907f3842c321280b64c42f8507ecb Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Fri, 9 Jun 2023 16:03:53 +0200 Subject: [PATCH 066/812] Bump version to 3.6.2 --- CHANGELOG | 4 ++++ src/Environment.php | 8 ++++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index c3d65518e1f..68737579b43 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +# 3.6.2 (2023-XX-XX) + + * n/a + # 3.6.1 (2023-06-08) * Suppress some native return type deprecation messages diff --git a/src/Environment.php b/src/Environment.php index 70405b0d7e7..995dc147e3b 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -40,12 +40,12 @@ */ class Environment { - public const VERSION = '3.6.1'; - public const VERSION_ID = 30601; + public const VERSION = '3.6.2-DEV'; + public const VERSION_ID = 30602; public const MAJOR_VERSION = 3; public const MINOR_VERSION = 6; - public const RELEASE_VERSION = 1; - public const EXTRA_VERSION = ''; + public const RELEASE_VERSION = 2; + public const EXTRA_VERSION = 'DEV'; private $charset; private $loader; From 0170dc3cd783ad49cfb8dc290fd2d904b36397dd Mon Sep 17 00:00:00 2001 From: Robin Martijn Date: Wed, 28 Jun 2023 14:29:30 +0200 Subject: [PATCH 067/812] add extra example to slice filter --- doc/filters/slice.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/doc/filters/slice.rst b/doc/filters/slice.rst index ae83b57a063..eb43d99461a 100644 --- a/doc/filters/slice.rst +++ b/doc/filters/slice.rst @@ -37,6 +37,9 @@ As syntactic sugar, you can also use the ``[]`` notation: {# you can omit the last argument -- which will select everything till the end #} {{ '12345'[2:] }} {# will display "345" #} + {# you can use a negative value -- for example to remove characters at the end #} + {{ '12345'[:-2] }} {# will display "123" #} + The ``slice`` filter works as the `array_slice`_ PHP function for arrays and `mb_substr`_ for strings with a fallback to `substr`_. From b0cabc093c6a8551f73bc7d948d3ba2465765c13 Mon Sep 17 00:00:00 2001 From: Richard Henkenjohann Date: Sat, 1 Jul 2023 16:42:59 +0200 Subject: [PATCH 068/812] Update signature to acknowledge a TemplateWrapper --- src/Extension/CoreExtension.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index f99adda451b..ba13a07770b 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -1303,12 +1303,12 @@ function twig_test_iterable($value) /** * Renders a template. * - * @param array $context - * @param string|array $template The template to render or an array of templates to try consecutively - * @param array $variables The variables to pass to the template - * @param bool $withContext - * @param bool $ignoreMissing Whether to ignore missing templates or not - * @param bool $sandboxed Whether to sandbox the template or not + * @param array $context + * @param string|array|TemplateWrapper $template The template to render or an array of templates to try consecutively + * @param array $variables The variables to pass to the template + * @param bool $withContext + * @param bool $ignoreMissing Whether to ignore missing templates or not + * @param bool $sandboxed Whether to sandbox the template or not * * @return string The rendered template */ From cd68de5e061146141ce36e023b5acc08e8df85a0 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Wed, 5 Jul 2023 11:30:59 +0200 Subject: [PATCH 069/812] Fix callable phpdoc for twig elements --- src/TwigFilter.php | 4 ++-- src/TwigFunction.php | 4 ++-- src/TwigTest.php | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/TwigFilter.php b/src/TwigFilter.php index 94e5f9b012b..e59919dd310 100644 --- a/src/TwigFilter.php +++ b/src/TwigFilter.php @@ -29,7 +29,7 @@ final class TwigFilter private $arguments = []; /** - * @param callable|null $callable A callable implementing the filter. If null, you need to overwrite the "node_class" option to customize compilation. + * @param callable|array|null $callable A callable implementing the filter. If null, you need to overwrite the "node_class" option to customize compilation. */ public function __construct(string $name, $callable = null, array $options = []) { @@ -57,7 +57,7 @@ public function getName(): string /** * Returns the callable to execute for this filter. * - * @return callable|null + * @return callable|array|null */ public function getCallable() { diff --git a/src/TwigFunction.php b/src/TwigFunction.php index 494d45b08c5..c10813224d2 100644 --- a/src/TwigFunction.php +++ b/src/TwigFunction.php @@ -29,7 +29,7 @@ final class TwigFunction private $arguments = []; /** - * @param callable|null $callable A callable implementing the function. If null, you need to overwrite the "node_class" option to customize compilation. + * @param callable|array|null $callable A callable implementing the function. If null, you need to overwrite the "node_class" option to customize compilation. */ public function __construct(string $name, $callable = null, array $options = []) { @@ -55,7 +55,7 @@ public function getName(): string /** * Returns the callable to execute for this function. * - * @return callable|null + * @return callable|array|null */ public function getCallable() { diff --git a/src/TwigTest.php b/src/TwigTest.php index 4c18632f559..7b81d9978e1 100644 --- a/src/TwigTest.php +++ b/src/TwigTest.php @@ -28,7 +28,7 @@ final class TwigTest private $arguments = []; /** - * @param callable|null $callable A callable implementing the test. If null, you need to overwrite the "node_class" option to customize compilation. + * @param callable|array|null $callable A callable implementing the test. If null, you need to overwrite the "node_class" option to customize compilation. */ public function __construct(string $name, $callable = null, array $options = []) { @@ -51,7 +51,7 @@ public function getName(): string /** * Returns the callable to execute for this test. * - * @return callable|null + * @return callable|array|null */ public function getCallable() { From ba4fe3ba34981324f6ec5cdc55d767d6534f3cdc Mon Sep 17 00:00:00 2001 From: Ryan Weaver Date: Fri, 5 May 2023 06:54:25 -0400 Subject: [PATCH 070/812] Adding support for the ...spread operator on arrays and hashes --- doc/templates.rst | 8 ++ src/ExpressionParser.php | 17 ++- src/Extension/CoreExtension.php | 26 ++-- src/Lexer.php | 7 +- src/Node/Expression/ArrayExpression.php | 59 ++++++++- src/Token.php | 6 + tests/ExpressionParserTest.php | 123 ++++++++++++------ .../expressions/spread_array_operator.test | 14 ++ .../expressions/spread_hash_operator.test | 37 ++++++ tests/LexerTest.php | 10 ++ tests/Node/Expression/FilterTest.php | 2 +- tests/Node/Expression/FunctionTest.php | 2 +- tests/Node/Expression/GetAttrTest.php | 2 +- tests/Node/Expression/TestTest.php | 2 +- 14 files changed, 253 insertions(+), 62 deletions(-) create mode 100644 tests/Fixtures/expressions/spread_array_operator.test create mode 100644 tests/Fixtures/expressions/spread_hash_operator.test diff --git a/doc/templates.rst b/doc/templates.rst index 5e52326e442..fd65ba1d7df 100644 --- a/doc/templates.rst +++ b/doc/templates.rst @@ -779,6 +779,14 @@ The following operators don't fit into any of the other categories: {# returns the value of foo if it is defined and not null, 'no' otherwise #} {{ foo ?? 'no' }} +* ``...``: The spread operator can be used to expand arrays or hashes (it cannot + be used to expand the arguments of a function call): + + .. code-block:: twig + + {% set numbers = [1, 2, ...moreNumbers] %} + {% set ratings = { 'foo': 10, 'bar': 5, ...moreRatings } %} + .. _templates-string-interpolation: String Interpolation diff --git a/src/ExpressionParser.php b/src/ExpressionParser.php index 2048c3c5486..38347cb391d 100644 --- a/src/ExpressionParser.php +++ b/src/ExpressionParser.php @@ -334,7 +334,14 @@ public function parseArrayExpression() } $first = false; - $node->addElement($this->parseExpression()); + if ($stream->test(/* Token::SPREAD_TYPE */ 13)) { + $stream->next(); + $expr = $this->parseExpression(); + $expr->setAttribute('spread', true); + $node->addElement($expr); + } else { + $node->addElement($this->parseExpression()); + } } $stream->expect(/* Token::PUNCTUATION_TYPE */ 9, ']', 'An opened array is not properly closed'); @@ -359,6 +366,14 @@ public function parseHashExpression() } $first = false; + if ($stream->test(/* Token::SPREAD_TYPE */ 13)) { + $stream->next(); + $value = $this->parseExpression(); + $value->setAttribute('spread', true); + $node->addElement($value); + continue; + } + // a hash key can be: // // * a number -- 12 diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index f99adda451b..0f1a10216f3 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -609,32 +609,34 @@ function twig_urlencode_filter($url) } /** - * Merges an array with another one. + * Merges any number of arrays or Traversable objects. * * {% set items = { 'apple': 'fruit', 'orange': 'fruit' } %} * - * {% set items = items|merge({ 'peugeot': 'car' }) %} + * {% set items = items|merge({ 'peugeot': 'car' }, { 'banana': 'fruit' }) %} * - * {# items now contains { 'apple': 'fruit', 'orange': 'fruit', 'peugeot': 'car' } #} + * {# items now contains { 'apple': 'fruit', 'orange': 'fruit', 'peugeot': 'car', 'banana': 'fruit' } #} * - * @param array|\Traversable $arr1 An array - * @param array|\Traversable $arr2 An array + * @param array|\Traversable ...$arrays Any number of arrays or Traversable objects to merge * * @return array The merged array */ -function twig_array_merge($arr1, $arr2) +function twig_array_merge(...$arrays) { - if (!twig_test_iterable($arr1)) { - throw new RuntimeError(sprintf('The merge filter only works with arrays or "Traversable", got "%s" as first argument.', \gettype($arr1))); - } + $result = []; + + foreach ($arrays as $argNumber => $array) { + if (!twig_test_iterable($array)) { + throw new RuntimeError(sprintf('The merge filter only works with arrays or "Traversable", got "%s" for argument %d.', \gettype($array), $argNumber + 1)); + } - if (!twig_test_iterable($arr2)) { - throw new RuntimeError(sprintf('The merge filter only works with arrays or "Traversable", got "%s" as second argument.', \gettype($arr2))); + $result = array_merge($result, twig_to_array($array)); } - return array_merge(twig_to_array($arr1), twig_to_array($arr2)); + return $result; } + /** * Slices a variable. * diff --git a/src/Lexer.php b/src/Lexer.php index 975b0b924ff..6c45efbc9f6 100644 --- a/src/Lexer.php +++ b/src/Lexer.php @@ -315,8 +315,13 @@ private function lexExpression(): void } } + // 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('...'); + } // arrow function - if ('=' === $this->code[$this->cursor] && '>' === $this->code[$this->cursor + 1]) { + elseif ('=' === $this->code[$this->cursor] && '>' === $this->code[$this->cursor + 1]) { $this->pushToken(Token::ARROW_TYPE, '=>'); $this->moveCursor('=>'); } diff --git a/src/Node/Expression/ArrayExpression.php b/src/Node/Expression/ArrayExpression.php index 0e25fe46ad8..1b29dd19e03 100644 --- a/src/Node/Expression/ArrayExpression.php +++ b/src/Node/Expression/ArrayExpression.php @@ -59,6 +59,9 @@ public function addElement(AbstractExpression $value, AbstractExpression $key = { if (null === $key) { $key = new ConstantExpression(++$this->index, $value->getTemplateLine()); + $key->setAttribute('index_specified', false); + } else { + $key->setAttribute('index_specified', true); } array_push($this->nodes, $key, $value); @@ -66,20 +69,62 @@ public function addElement(AbstractExpression $value, AbstractExpression $key = public function compile(Compiler $compiler): void { + $keyValuePairs = $this->getKeyValuePairs(); + $hasSpreadItem = $this->hasSpreadItem($keyValuePairs); + $needsArrayMergeSpread = \PHP_VERSION_ID < 80100 && $hasSpreadItem; + + if ($needsArrayMergeSpread) { + $compiler->raw('twig_array_merge('); + } $compiler->raw('['); $first = true; - foreach ($this->getKeyValuePairs() as $pair) { + $reopenAfterMergeSpread = false; + 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; + } if (!$first) { $compiler->raw(', '); } $first = false; - $compiler - ->subcompile($pair['key']) - ->raw(' => ') - ->subcompile($pair['value']) - ; + if ($pair['value']->hasAttribute('spread') && !$needsArrayMergeSpread) { + $compiler->raw('...')->subcompile($pair['value']); + } else { + $indexSpecified = false === $pair['key']->hasAttribute('index_specified') || true === $pair['key']->getAttribute('index_specified'); + if ($indexSpecified) { + $compiler + ->subcompile($pair['key']) + ->raw(' => ') + ; + } + $compiler->subcompile($pair['value']); + } + } + if (!$reopenAfterMergeSpread) { + $compiler->raw(']'); + } + if ($needsArrayMergeSpread) { + $compiler->raw(')'); } - $compiler->raw(']'); + } + + private function hasSpreadItem(array $pairs) + { + foreach ($pairs as $pair) { + if ($pair['value']->hasAttribute('spread')) { + return true; + } + } + + return false; } } diff --git a/src/Token.php b/src/Token.php index 53a6cafc350..fd1a89d2adc 100644 --- a/src/Token.php +++ b/src/Token.php @@ -35,6 +35,7 @@ final class Token public const INTERPOLATION_START_TYPE = 10; public const INTERPOLATION_END_TYPE = 11; public const ARROW_TYPE = 12; + public const SPREAD_TYPE = 13; public function __construct(int $type, $value, int $lineno) { @@ -133,6 +134,9 @@ public static function typeToString(int $type, bool $short = false): string case self::ARROW_TYPE: $name = 'ARROW_TYPE'; break; + case self::SPREAD_TYPE: + $name = 'SPREAD_TYPE'; + break; default: throw new \LogicException(sprintf('Token of type "%s" does not exist.', $type)); } @@ -171,6 +175,8 @@ public static function typeToEnglish(int $type): string return 'end of string interpolation'; case self::ARROW_TYPE: return 'arrow function'; + case self::SPREAD_TYPE: + return 'spread operator'; default: throw new \LogicException(sprintf('Token of type "%s" does not exist.', $type)); } diff --git a/tests/ExpressionParserTest.php b/tests/ExpressionParserTest.php index 1b6f385dac9..c6f34db0e9f 100644 --- a/tests/ExpressionParserTest.php +++ b/tests/ExpressionParserTest.php @@ -93,80 +93,108 @@ public function getTestsForArray() return [ // simple array ['{{ [1, 2] }}', new ArrayExpression([ - new ConstantExpression(0, 1), - new ConstantExpression(1, 1), + $this->createConstantExpression(0, false), + $this->createConstantExpression(1), - new ConstantExpression(1, 1), - new ConstantExpression(2, 1), + $this->createConstantExpression(1, false), + $this->createConstantExpression(2), ], 1), ], // array with trailing , ['{{ [1, 2, ] }}', new ArrayExpression([ - new ConstantExpression(0, 1), - new ConstantExpression(1, 1), + $this->createConstantExpression(0, false), + $this->createConstantExpression(1), - new ConstantExpression(1, 1), - new ConstantExpression(2, 1), + $this->createConstantExpression(1, false), + $this->createConstantExpression(2), ], 1), ], // simple hash ['{{ {"a": "b", "b": "c"} }}', new ArrayExpression([ - new ConstantExpression('a', 1), - new ConstantExpression('b', 1), + $this->createConstantExpression('a', true), + $this->createConstantExpression('b'), - new ConstantExpression('b', 1), - new ConstantExpression('c', 1), + $this->createConstantExpression('b', true), + $this->createConstantExpression('c'), ], 1), ], // hash with trailing , ['{{ {"a": "b", "b": "c", } }}', new ArrayExpression([ - new ConstantExpression('a', 1), - new ConstantExpression('b', 1), + $this->createConstantExpression('a', true), + $this->createConstantExpression('b'), - new ConstantExpression('b', 1), - new ConstantExpression('c', 1), + $this->createConstantExpression('b', true), + $this->createConstantExpression('c'), ], 1), ], // hash in an array ['{{ [1, {"a": "b", "b": "c"}] }}', new ArrayExpression([ - new ConstantExpression(0, 1), - new ConstantExpression(1, 1), + $this->createConstantExpression(0, false), + $this->createConstantExpression(1), - new ConstantExpression(1, 1), - new ArrayExpression([ - new ConstantExpression('a', 1), - new ConstantExpression('b', 1), + $this->createConstantExpression(1, false), + new ArrayExpression([ + $this->createConstantExpression('a', true), + $this->createConstantExpression('b'), - new ConstantExpression('b', 1), - new ConstantExpression('c', 1), - ], 1), + $this->createConstantExpression('b', true), + $this->createConstantExpression('c'), + ], 1), ], 1), ], // array in a hash ['{{ {"a": [1, 2], "b": "c"} }}', new ArrayExpression([ - new ConstantExpression('a', 1), - new ArrayExpression([ - new ConstantExpression(0, 1), - new ConstantExpression(1, 1), - - new ConstantExpression(1, 1), - new ConstantExpression(2, 1), - ], 1), - new ConstantExpression('b', 1), - new ConstantExpression('c', 1), + $this->createConstantExpression('a', true), + new ArrayExpression([ + $this->createConstantExpression(0, false), + $this->createConstantExpression(1), + + $this->createConstantExpression(1, false), + $this->createConstantExpression(2), + ], 1), + + $this->createConstantExpression('b', true), + $this->createConstantExpression('c'), ], 1), ], ['{{ {a, b} }}', new ArrayExpression([ - new ConstantExpression('a', 1), + $this->createConstantExpression('a', true), new NameExpression('a', 1), - new ConstantExpression('b', 1), + + $this->createConstantExpression('b', true), new NameExpression('b', 1), ], 1)], + + // array with spread operator + ['{{ [1, 2, ...foo] }}', + new ArrayExpression([ + $this->createConstantExpression(0, false), + $this->createConstantExpression(1), + + $this->createConstantExpression(1, false), + $this->createConstantExpression(2), + + $this->createConstantExpression(2, false), + $this->createNameExpression('foo', ['spread' => true]), + ], 1)], + + // hash with spread operator + ['{{ {"a": "b", "b": "c", ...otherLetters} }}', + new ArrayExpression([ + $this->createConstantExpression('a', true), + $this->createConstantExpression('b'), + + $this->createConstantExpression('b', true), + $this->createConstantExpression('c'), + + $this->createConstantExpression(0, false), + $this->createNameExpression('otherLetters', ['spread' => true]), + ], 1)], ]; } @@ -387,4 +415,25 @@ public function testUnknownTestWithoutSuggestions() $parser->parse($env->tokenize(new Source('{{ 1 is foobar }}', 'index'))); } + + private function createNameExpression(string $name, array $attributes) + { + $expression = new NameExpression($name, 1); + foreach ($attributes as $key => $value) { + $expression->setAttribute($key, $value); + } + + return $expression; + } + + private function createConstantExpression($value, ?bool $indexSpecified = null) + { + $constant = new ConstantExpression($value, 1); + + if (null !== $indexSpecified) { + $constant->setAttribute('index_specified', $indexSpecified); + } + + return $constant; + } } diff --git a/tests/Fixtures/expressions/spread_array_operator.test b/tests/Fixtures/expressions/spread_array_operator.test new file mode 100644 index 00000000000..292488eda9e --- /dev/null +++ b/tests/Fixtures/expressions/spread_array_operator.test @@ -0,0 +1,14 @@ +--TEST-- +Twig supports the spread operator on arrays +--TEMPLATE-- +{{ [1, 2, ...[3, 4]]|join(',') }} +{{ [1, 2, ...moreNumbers]|join(',') }} +{{ [1, 2, ...iterableNumbers]|join(',') }} +{{ [1, 2, ...iterableNumbers, 0, ...moreNumbers]|join(',') }} +--DATA-- +return ['moreNumbers' => [5, 6, 7, 8], 'iterableNumbers' => new \ArrayObject([6, 7, 8, 9])] +--EXPECT-- +1,2,3,4 +1,2,5,6,7,8 +1,2,6,7,8,9 +1,2,6,7,8,9,0,5,6,7,8 diff --git a/tests/Fixtures/expressions/spread_hash_operator.test b/tests/Fixtures/expressions/spread_hash_operator.test new file mode 100644 index 00000000000..c2429f00e97 --- /dev/null +++ b/tests/Fixtures/expressions/spread_hash_operator.test @@ -0,0 +1,37 @@ +--TEST-- +Twig supports the spread operator on hashes +--TEMPLATE-- +{% for key, value in { firstName: 'Ryan', lastName: 'Weaver', favoriteFood: 'popcorn', ...{favoriteFood: 'pizza', sport: 'running'} } %} + {{ key }}: {{ value }} +{% endfor %} + +{% for key, value in { firstName: 'Ryan', ...morePersonalDetails} %} + {{ key }}: {{ value }} +{% endfor %} + +{% for key, value in { firstName: 'Ryan', ...iterablePersonalDetails} %} + {{ key }}: {{ value }} +{% endfor %} + +{# multiple spreads #} +{% for key, value in { firstName: 'Ryan', ...iterablePersonalDetails, lastName: 'Weaver', ...morePersonalDetails} %} + {{ key }}: {{ value }} +{% endfor %} +--DATA-- +return ['morePersonalDetails' => ['favoriteColor' => 'orange'], 'iterablePersonalDetails' => new \ArrayObject(['favoriteShoes' => 'barefoot'])]; +--EXPECT-- + firstName: Ryan + lastName: Weaver + favoriteFood: pizza + sport: running + + firstName: Ryan + favoriteColor: orange + + firstName: Ryan + favoriteShoes: barefoot + + firstName: Ryan + favoriteShoes: barefoot + lastName: Weaver + favoriteColor: orange diff --git a/tests/LexerTest.php b/tests/LexerTest.php index fdb58c2d5b6..ad62c22acfb 100644 --- a/tests/LexerTest.php +++ b/tests/LexerTest.php @@ -51,6 +51,16 @@ public function testBracketsNesting() $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($this->createMock(LoaderInterface::class))); diff --git a/tests/Node/Expression/FilterTest.php b/tests/Node/Expression/FilterTest.php index 4b30c9cae7e..b8cc48fab16 100644 --- a/tests/Node/Expression/FilterTest.php +++ b/tests/Node/Expression/FilterTest.php @@ -127,7 +127,7 @@ protected function foobar() new ConstantExpression('3', 1), 'foo' => new ConstantExpression('bar', 1), ]); - $tests[] = [$node, 'Twig\Tests\Node\Expression\twig_tests_filter_barbar($context, "abc", "1", "2", [0 => "3", "foo" => "bar"])', $environment]; + $tests[] = [$node, 'Twig\Tests\Node\Expression\twig_tests_filter_barbar($context, "abc", "1", "2", ["3", "foo" => "bar"])', $environment]; // from extension $node = $this->createFilter($string, 'foo'); diff --git a/tests/Node/Expression/FunctionTest.php b/tests/Node/Expression/FunctionTest.php index 8c9beb370e9..05c07c02923 100644 --- a/tests/Node/Expression/FunctionTest.php +++ b/tests/Node/Expression/FunctionTest.php @@ -94,7 +94,7 @@ public function getTests() new ConstantExpression('3', 1), 'foo' => new ConstantExpression('bar', 1), ]); - $tests[] = [$node, 'Twig\Tests\Node\Expression\twig_tests_function_barbar("1", "2", [0 => "3", "foo" => "bar"])', $environment]; + $tests[] = [$node, 'Twig\Tests\Node\Expression\twig_tests_function_barbar("1", "2", ["3", "foo" => "bar"])', $environment]; // function as an anonymous function $node = $this->createFunction('anonymous', [new ConstantExpression('foo', 1)]); diff --git a/tests/Node/Expression/GetAttrTest.php b/tests/Node/Expression/GetAttrTest.php index 16c76c609c5..c76fb3992d5 100644 --- a/tests/Node/Expression/GetAttrTest.php +++ b/tests/Node/Expression/GetAttrTest.php @@ -53,7 +53,7 @@ public function getTests() $args->addElement(new NameExpression('foo', 1)); $args->addElement(new ConstantExpression('bar', 1)); $node = new GetAttrExpression($expr, $attr, $args, Template::METHOD_CALL, 1); - $tests[] = [$node, sprintf('%s%s, "bar", [0 => %s, 1 => "bar"], "method", false, false, false, 1)', $this->getAttributeGetter(), $this->getVariableGetter('foo', 1), $this->getVariableGetter('foo'))]; + $tests[] = [$node, sprintf('%s%s, "bar", [%s, "bar"], "method", false, false, false, 1)', $this->getAttributeGetter(), $this->getVariableGetter('foo', 1), $this->getVariableGetter('foo'))]; return $tests; } diff --git a/tests/Node/Expression/TestTest.php b/tests/Node/Expression/TestTest.php index 97955cb626f..df7c7202b0c 100644 --- a/tests/Node/Expression/TestTest.php +++ b/tests/Node/Expression/TestTest.php @@ -67,7 +67,7 @@ public function getTests() new ConstantExpression('3', 1), 'foo' => new ConstantExpression('bar', 1), ]); - $tests[] = [$node, 'Twig\Tests\Node\Expression\twig_tests_test_barbar("abc", "1", "2", [0 => "3", "foo" => "bar"])', $environment]; + $tests[] = [$node, 'Twig\Tests\Node\Expression\twig_tests_test_barbar("abc", "1", "2", ["3", "foo" => "bar"])', $environment]; return $tests; } From 75efa5e1ba3ec676cab888a5984bafd3ca963461 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Thu, 20 Jul 2023 17:20:05 +0200 Subject: [PATCH 071/812] Fix spread operator implementation --- src/Node/Expression/ArrayExpression.php | 21 +++-- tests/ExpressionParserTest.php | 101 +++++++++++------------- 2 files changed, 57 insertions(+), 65 deletions(-) diff --git a/src/Node/Expression/ArrayExpression.php b/src/Node/Expression/ArrayExpression.php index 1b29dd19e03..44428380239 100644 --- a/src/Node/Expression/ArrayExpression.php +++ b/src/Node/Expression/ArrayExpression.php @@ -59,9 +59,6 @@ public function addElement(AbstractExpression $value, AbstractExpression $key = { if (null === $key) { $key = new ConstantExpression(++$this->index, $value->getTemplateLine()); - $key->setAttribute('index_specified', false); - } else { - $key->setAttribute('index_specified', true); } array_push($this->nodes, $key, $value); @@ -70,8 +67,7 @@ public function addElement(AbstractExpression $value, AbstractExpression $key = public function compile(Compiler $compiler): void { $keyValuePairs = $this->getKeyValuePairs(); - $hasSpreadItem = $this->hasSpreadItem($keyValuePairs); - $needsArrayMergeSpread = \PHP_VERSION_ID < 80100 && $hasSpreadItem; + $needsArrayMergeSpread = \PHP_VERSION_ID < 80100 && $this->hasSpreadItem($keyValuePairs); if ($needsArrayMergeSpread) { $compiler->raw('twig_array_merge('); @@ -79,6 +75,7 @@ public function compile(Compiler $compiler): void $compiler->raw('['); $first = true; $reopenAfterMergeSpread = false; + $nextIndex = 0; foreach ($keyValuePairs as $pair) { if ($reopenAfterMergeSpread) { $compiler->raw(', ['); @@ -98,14 +95,22 @@ public function compile(Compiler $compiler): void if ($pair['value']->hasAttribute('spread') && !$needsArrayMergeSpread) { $compiler->raw('...')->subcompile($pair['value']); + ++$nextIndex; } else { - $indexSpecified = false === $pair['key']->hasAttribute('index_specified') || true === $pair['key']->getAttribute('index_specified'); - if ($indexSpecified) { + $key = $pair['key'] instanceof ConstantExpression ? $pair['key']->getAttribute('value') : null; + + if ($nextIndex !== $key) { + if (\is_int($key)) { + $nextIndex = $key + 1; + } $compiler ->subcompile($pair['key']) ->raw(' => ') ; + } else { + ++$nextIndex; } + $compiler->subcompile($pair['value']); } } @@ -117,7 +122,7 @@ public function compile(Compiler $compiler): void } } - private function hasSpreadItem(array $pairs) + private function hasSpreadItem(array $pairs): bool { foreach ($pairs as $pair) { if ($pair['value']->hasAttribute('spread')) { diff --git a/tests/ExpressionParserTest.php b/tests/ExpressionParserTest.php index c6f34db0e9f..ab02296b641 100644 --- a/tests/ExpressionParserTest.php +++ b/tests/ExpressionParserTest.php @@ -93,106 +93,104 @@ public function getTestsForArray() return [ // simple array ['{{ [1, 2] }}', new ArrayExpression([ - $this->createConstantExpression(0, false), - $this->createConstantExpression(1), + new ConstantExpression(0, 1), + new ConstantExpression(1, 1), - $this->createConstantExpression(1, false), - $this->createConstantExpression(2), + new ConstantExpression(1, 1), + new ConstantExpression(2, 1), ], 1), ], // array with trailing , ['{{ [1, 2, ] }}', new ArrayExpression([ - $this->createConstantExpression(0, false), - $this->createConstantExpression(1), + new ConstantExpression(0, 1), + new ConstantExpression(1, 1), - $this->createConstantExpression(1, false), - $this->createConstantExpression(2), + new ConstantExpression(1, 1), + new ConstantExpression(2, 1), ], 1), ], // simple hash ['{{ {"a": "b", "b": "c"} }}', new ArrayExpression([ - $this->createConstantExpression('a', true), - $this->createConstantExpression('b'), + new ConstantExpression('a', 1), + new ConstantExpression('b', 1), - $this->createConstantExpression('b', true), - $this->createConstantExpression('c'), + new ConstantExpression('b', 1), + new ConstantExpression('c', 1), ], 1), ], // hash with trailing , ['{{ {"a": "b", "b": "c", } }}', new ArrayExpression([ - $this->createConstantExpression('a', true), - $this->createConstantExpression('b'), + new ConstantExpression('a', 1), + new ConstantExpression('b', 1), - $this->createConstantExpression('b', true), - $this->createConstantExpression('c'), + new ConstantExpression('b', 1), + new ConstantExpression('c', 1), ], 1), ], // hash in an array ['{{ [1, {"a": "b", "b": "c"}] }}', new ArrayExpression([ - $this->createConstantExpression(0, false), - $this->createConstantExpression(1), + new ConstantExpression(0, 1), + new ConstantExpression(1, 1), - $this->createConstantExpression(1, false), + new ConstantExpression(1, 1), new ArrayExpression([ - $this->createConstantExpression('a', true), - $this->createConstantExpression('b'), + new ConstantExpression('a', 1), + new ConstantExpression('b', 1), - $this->createConstantExpression('b', true), - $this->createConstantExpression('c'), + new ConstantExpression('b', 1), + new ConstantExpression('c', 1), ], 1), ], 1), ], // array in a hash ['{{ {"a": [1, 2], "b": "c"} }}', new ArrayExpression([ - $this->createConstantExpression('a', true), - new ArrayExpression([ - $this->createConstantExpression(0, false), - $this->createConstantExpression(1), - - $this->createConstantExpression(1, false), - $this->createConstantExpression(2), - ], 1), + new ConstantExpression('a', 1), + new ArrayExpression([ + new ConstantExpression(0, 1), + new ConstantExpression(1, 1), - $this->createConstantExpression('b', true), - $this->createConstantExpression('c'), + new ConstantExpression(1, 1), + new ConstantExpression(2, 1), + ], 1), + new ConstantExpression('b', 1), + new ConstantExpression('c', 1), ], 1), ], ['{{ {a, b} }}', new ArrayExpression([ - $this->createConstantExpression('a', true), + new ConstantExpression('a', 1), new NameExpression('a', 1), - - $this->createConstantExpression('b', true), + new ConstantExpression('b', 1), new NameExpression('b', 1), ], 1)], // array with spread operator ['{{ [1, 2, ...foo] }}', new ArrayExpression([ - $this->createConstantExpression(0, false), - $this->createConstantExpression(1), + new ConstantExpression(0, 1), + new ConstantExpression(1, 1), - $this->createConstantExpression(1, false), - $this->createConstantExpression(2), + new ConstantExpression(1, 1), + new ConstantExpression(2, 1), - $this->createConstantExpression(2, false), + new ConstantExpression(2, 1), $this->createNameExpression('foo', ['spread' => true]), ], 1)], // hash with spread operator ['{{ {"a": "b", "b": "c", ...otherLetters} }}', new ArrayExpression([ - $this->createConstantExpression('a', true), - $this->createConstantExpression('b'), + new ConstantExpression('a', 1), + new ConstantExpression('b', 1), - $this->createConstantExpression('b', true), - $this->createConstantExpression('c'), + new ConstantExpression('b', 1), + new ConstantExpression('c', 1), - $this->createConstantExpression(0, false), + new ConstantExpression(0, 1), $this->createNameExpression('otherLetters', ['spread' => true]), ], 1)], ]; @@ -425,15 +423,4 @@ private function createNameExpression(string $name, array $attributes) return $expression; } - - private function createConstantExpression($value, ?bool $indexSpecified = null) - { - $constant = new ConstantExpression($value, 1); - - if (null !== $indexSpecified) { - $constant->setAttribute('index_specified', $indexSpecified); - } - - return $constant; - } } From 07c4b11399cc14ea4b98262c5469d9dd050111a5 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Wed, 26 Jul 2023 09:15:15 +0200 Subject: [PATCH 072/812] Update CHANGELOG --- CHANGELOG | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 68737579b43..b6551155c2e 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,6 @@ -# 3.6.2 (2023-XX-XX) +# 3.7.0 (2023-07-26) - * n/a + * Add support for the ...spread operator on arrays and hashes # 3.6.1 (2023-06-08) From 5cf942bbab3df42afa918caeba947f1b690af64b Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Wed, 26 Jul 2023 09:16:09 +0200 Subject: [PATCH 073/812] Prepare the 3.7.0 release --- src/Environment.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Environment.php b/src/Environment.php index 995dc147e3b..3c630c1b30c 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -40,12 +40,12 @@ */ class Environment { - public const VERSION = '3.6.2-DEV'; - public const VERSION_ID = 30602; + public const VERSION = '3.7.0'; + public const VERSION_ID = 30700; public const MAJOR_VERSION = 3; - public const MINOR_VERSION = 6; - public const RELEASE_VERSION = 2; - public const EXTRA_VERSION = 'DEV'; + public const MINOR_VERSION = 7; + public const RELEASE_VERSION = 0; + public const EXTRA_VERSION = ''; private $charset; private $loader; From 9a9580fff965e67fcd3d0ac448ad784cf13dc362 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Wed, 26 Jul 2023 09:17:41 +0200 Subject: [PATCH 074/812] Bump version --- CHANGELOG | 4 ++++ src/Environment.php | 8 ++++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index b6551155c2e..d07217e017b 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +# 3.7.1 (2023-XX-XX) + + * n/a + # 3.7.0 (2023-07-26) * Add support for the ...spread operator on arrays and hashes diff --git a/src/Environment.php b/src/Environment.php index 3c630c1b30c..3a05712e501 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -40,12 +40,12 @@ */ class Environment { - public const VERSION = '3.7.0'; - public const VERSION_ID = 30700; + public const VERSION = '3.7.1-DEV'; + public const VERSION_ID = 30701; public const MAJOR_VERSION = 3; public const MINOR_VERSION = 7; - public const RELEASE_VERSION = 0; - public const EXTRA_VERSION = ''; + public const RELEASE_VERSION = 1; + public const EXTRA_VERSION = 'DEV'; private $charset; private $loader; From 1b58589d563de454c86c9cee65052e776e036f33 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Thu, 27 Jul 2023 10:17:36 +0200 Subject: [PATCH 075/812] Fix callable phpdoc --- src/TwigFilter.php | 4 ++-- src/TwigFunction.php | 4 ++-- src/TwigTest.php | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/TwigFilter.php b/src/TwigFilter.php index e59919dd310..8993026c8cb 100644 --- a/src/TwigFilter.php +++ b/src/TwigFilter.php @@ -29,7 +29,7 @@ final class TwigFilter private $arguments = []; /** - * @param callable|array|null $callable A callable implementing the filter. If null, you need to overwrite the "node_class" option to customize compilation. + * @param callable|array{class-string, string}|null $callable A callable implementing the filter. If null, you need to overwrite the "node_class" option to customize compilation. */ public function __construct(string $name, $callable = null, array $options = []) { @@ -57,7 +57,7 @@ public function getName(): string /** * Returns the callable to execute for this filter. * - * @return callable|array|null + * @return callable|array{class-string, string}|null */ public function getCallable() { diff --git a/src/TwigFunction.php b/src/TwigFunction.php index c10813224d2..d910d1fd531 100644 --- a/src/TwigFunction.php +++ b/src/TwigFunction.php @@ -29,7 +29,7 @@ final class TwigFunction private $arguments = []; /** - * @param callable|array|null $callable A callable implementing the function. If null, you need to overwrite the "node_class" option to customize compilation. + * @param callable|array{class-string, string}|null $callable A callable implementing the function. If null, you need to overwrite the "node_class" option to customize compilation. */ public function __construct(string $name, $callable = null, array $options = []) { @@ -55,7 +55,7 @@ public function getName(): string /** * Returns the callable to execute for this function. * - * @return callable|array|null + * @return callable|array{class-string, string}|null */ public function getCallable() { diff --git a/src/TwigTest.php b/src/TwigTest.php index 7b81d9978e1..3769ec162b6 100644 --- a/src/TwigTest.php +++ b/src/TwigTest.php @@ -28,7 +28,7 @@ final class TwigTest private $arguments = []; /** - * @param callable|array|null $callable A callable implementing the test. If null, you need to overwrite the "node_class" option to customize compilation. + * @param callable|array{class-string, string}|null $callable A callable implementing the test. If null, you need to overwrite the "node_class" option to customize compilation. */ public function __construct(string $name, $callable = null, array $options = []) { @@ -51,7 +51,7 @@ public function getName(): string /** * Returns the callable to execute for this test. * - * @return callable|array|null + * @return callable|array{class-string, string}|null */ public function getCallable() { From fac08ec7d3ccb6e7743c6fe9efa80e4a17cb466b Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Thu, 27 Jul 2023 11:30:49 +0200 Subject: [PATCH 076/812] add return type for Symfony 7 compatibility --- .../twig-extra-bundle/DependencyInjection/Configuration.php | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/extra/twig-extra-bundle/DependencyInjection/Configuration.php b/extra/twig-extra-bundle/DependencyInjection/Configuration.php index 447e6ac76fa..24c5fd4d390 100644 --- a/extra/twig-extra-bundle/DependencyInjection/Configuration.php +++ b/extra/twig-extra-bundle/DependencyInjection/Configuration.php @@ -17,10 +17,7 @@ class Configuration implements ConfigurationInterface { - /** - * @return TreeBuilder - */ - public function getConfigTreeBuilder() + public function getConfigTreeBuilder(): TreeBuilder { $treeBuilder = new TreeBuilder('twig_extra'); $rootNode = $treeBuilder->getRootNode(); From 4c4f73ca70c361f3a82254f5c0e1e39651cd8f36 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Fri, 28 Jul 2023 11:59:50 +0200 Subject: [PATCH 077/812] Bump PHP version used by Drupal tests --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3f49259ca9a..0a95b580cd8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -129,7 +129,7 @@ jobs: strategy: matrix: php-version: - - '7.3' + - '8.2' steps: - name: "Checkout code" From 64543c3c490d6185f3c23ea8e9cbef52d6aa2e34 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Fri, 28 Jul 2023 12:18:03 +0200 Subject: [PATCH 078/812] Fix code --- .../DependencyInjection/TwigExtraExtension.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extra/twig-extra-bundle/DependencyInjection/TwigExtraExtension.php b/extra/twig-extra-bundle/DependencyInjection/TwigExtraExtension.php index b77b2e41239..fc9e8cc478a 100644 --- a/extra/twig-extra-bundle/DependencyInjection/TwigExtraExtension.php +++ b/extra/twig-extra-bundle/DependencyInjection/TwigExtraExtension.php @@ -22,7 +22,7 @@ */ class TwigExtraExtension extends Extension { - public function load(array $configs, ContainerBuilder $container) + public function load(array $configs, ContainerBuilder $container): void { $loader = new PhpFileLoader($container, new FileLocator(\dirname(__DIR__).'/Resources/config')); $configuration = $this->getConfiguration($configs, $container); From da38e858e3b2d8c0d37f47f76c694a6837ccbc5c Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Fri, 28 Jul 2023 12:21:15 +0200 Subject: [PATCH 079/812] Update Drupal version in tests --- tests/drupal_test.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/drupal_test.sh b/tests/drupal_test.sh index 7141f3181fd..e7fbf5dc7e9 100644 --- a/tests/drupal_test.sh +++ b/tests/drupal_test.sh @@ -6,7 +6,7 @@ set -e REPO=`pwd` cd /tmp rm -rf drupal-twig-test -composer create-project --no-interaction drupal/recommended-project:9.1.x-dev drupal-twig-test +composer create-project --no-interaction drupal/recommended-project:10.1.x-dev drupal-twig-test cd drupal-twig-test (cd vendor/twig && rm -rf twig && ln -sf $REPO twig) php ./web/core/scripts/drupal install --no-interaction demo_umami > output From 244d5de4548787055ce614a407122879c485b296 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sat, 29 Jul 2023 13:21:11 +0200 Subject: [PATCH 080/812] Fix BC break --- .../DependencyInjection/TwigExtraExtension.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/extra/twig-extra-bundle/DependencyInjection/TwigExtraExtension.php b/extra/twig-extra-bundle/DependencyInjection/TwigExtraExtension.php index fc9e8cc478a..d88eef91101 100644 --- a/extra/twig-extra-bundle/DependencyInjection/TwigExtraExtension.php +++ b/extra/twig-extra-bundle/DependencyInjection/TwigExtraExtension.php @@ -22,7 +22,8 @@ */ class TwigExtraExtension extends Extension { - public function load(array $configs, ContainerBuilder $container): void + /** @return void */ + public function load(array $configs, ContainerBuilder $container) { $loader = new PhpFileLoader($container, new FileLocator(\dirname(__DIR__).'/Resources/config')); $configuration = $this->getConfiguration($configs, $container); From c12990f9e764f6c0aa66c66aaff774d6da535a7e Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sat, 29 Jul 2023 13:31:08 +0200 Subject: [PATCH 081/812] Bump min deps --- composer.json | 2 +- extra/cssinliner-extra/composer.json | 2 +- extra/html-extra/composer.json | 4 ++-- extra/inky-extra/composer.json | 2 +- extra/intl-extra/composer.json | 4 ++-- extra/markdown-extra/composer.json | 2 +- extra/string-extra/composer.json | 4 ++-- extra/twig-extra-bundle/composer.json | 6 +++--- 8 files changed, 13 insertions(+), 13 deletions(-) diff --git a/composer.json b/composer.json index 4bc2421a9da..719f5d95020 100644 --- a/composer.json +++ b/composer.json @@ -30,7 +30,7 @@ "symfony/polyfill-php72": "^1.8" }, "require-dev": { - "symfony/phpunit-bridge": "^4.4.9|^5.0.9|^6.0", + "symfony/phpunit-bridge": "^5.4.9|^6.3", "psr/container": "^1.0" }, "autoload": { diff --git a/extra/cssinliner-extra/composer.json b/extra/cssinliner-extra/composer.json index ff0e7c00661..bb36e4ef158 100644 --- a/extra/cssinliner-extra/composer.json +++ b/extra/cssinliner-extra/composer.json @@ -20,7 +20,7 @@ "twig/twig": "^2.7|^3.0" }, "require-dev": { - "symfony/phpunit-bridge": "^4.4.9|^5.0.9|^6.0" + "symfony/phpunit-bridge": "^5.4|^6.3" }, "autoload": { "psr-4" : { "Twig\\Extra\\CssInliner\\" : "" }, diff --git a/extra/html-extra/composer.json b/extra/html-extra/composer.json index ca2b89a01a7..af7f9b2c5c3 100644 --- a/extra/html-extra/composer.json +++ b/extra/html-extra/composer.json @@ -16,11 +16,11 @@ ], "require": { "php": ">=7.1.3", - "symfony/mime": "^4.4|^5.0|^6.0", + "symfony/mime": "^5.4|^6.0", "twig/twig": "^2.7|^3.0" }, "require-dev": { - "symfony/phpunit-bridge": "^4.4.9|^5.0.9|^6.0" + "symfony/phpunit-bridge": "^5.4|^6.3" }, "autoload": { "psr-4" : { "Twig\\Extra\\Html\\" : "" }, diff --git a/extra/inky-extra/composer.json b/extra/inky-extra/composer.json index 93b5a818c5a..e662e60877c 100644 --- a/extra/inky-extra/composer.json +++ b/extra/inky-extra/composer.json @@ -20,7 +20,7 @@ "twig/twig": "^2.7|^3.0" }, "require-dev": { - "symfony/phpunit-bridge": "^4.4.9|^5.0.9|^6.0" + "symfony/phpunit-bridge": "^5.4|^6.3" }, "autoload": { "psr-4" : { "Twig\\Extra\\Inky\\" : "" }, diff --git a/extra/intl-extra/composer.json b/extra/intl-extra/composer.json index e8be1330621..24e69f9a7cb 100644 --- a/extra/intl-extra/composer.json +++ b/extra/intl-extra/composer.json @@ -17,10 +17,10 @@ "require": { "php": ">=7.1.3", "twig/twig": "^2.7|^3.0", - "symfony/intl": "^4.4|^5.0|^6.0" + "symfony/intl": "^5.4|^6.0" }, "require-dev": { - "symfony/phpunit-bridge": "^4.4.9|^5.0.9|^6.0" + "symfony/phpunit-bridge": "^5.4|^6.3" }, "autoload": { "psr-4" : { "Twig\\Extra\\Intl\\" : "" }, diff --git a/extra/markdown-extra/composer.json b/extra/markdown-extra/composer.json index 3cdc6f0744b..73513a43d19 100644 --- a/extra/markdown-extra/composer.json +++ b/extra/markdown-extra/composer.json @@ -19,7 +19,7 @@ "twig/twig": "^2.7|^3.0" }, "require-dev": { - "symfony/phpunit-bridge": "^4.4.9|^5.0.9|^6.0", + "symfony/phpunit-bridge": "^5.4|^6.3", "erusev/parsedown": "^1.7", "league/commonmark": "^1.0", "league/html-to-markdown": "^4.8|^5.0", diff --git a/extra/string-extra/composer.json b/extra/string-extra/composer.json index 58eee5bd9f9..cb33c1f5ef9 100644 --- a/extra/string-extra/composer.json +++ b/extra/string-extra/composer.json @@ -16,12 +16,12 @@ ], "require": { "php": ">=7.2.5", - "symfony/string": "^5.0|^6.0", + "symfony/string": "^5.4|^6.0", "symfony/translation-contracts": "^1.1|^2|^3", "twig/twig": "^2.7|^3.0" }, "require-dev": { - "symfony/phpunit-bridge": "^4.4.9|^5.0.9|^6.0" + "symfony/phpunit-bridge": "^5.4|^6.3" }, "autoload": { "psr-4" : { "Twig\\Extra\\String\\" : "" }, diff --git a/extra/twig-extra-bundle/composer.json b/extra/twig-extra-bundle/composer.json index 29c068abb87..72ed96203ed 100644 --- a/extra/twig-extra-bundle/composer.json +++ b/extra/twig-extra-bundle/composer.json @@ -16,12 +16,12 @@ ], "require": { "php": ">=7.2.5", - "symfony/framework-bundle": "^4.4|^5.0|^6.0", - "symfony/twig-bundle": "^4.4|^5.0|^6.0", + "symfony/framework-bundle": "^5.4|^6.0", + "symfony/twig-bundle": "^5.4|^6.0", "twig/twig": "^2.7|^3.0" }, "require-dev": { - "symfony/phpunit-bridge": "^4.4.9|^5.0.9|^6.0", + "symfony/phpunit-bridge": "^5.4|^6.3", "twig/cssinliner-extra": "^2.12|^3.0", "twig/html-extra": "^2.12|^3.0", "twig/inky-extra": "^2.12|^3.0", From a949600c4b852d21b177b254fcef0adf91006b3f Mon Sep 17 00:00:00 2001 From: Stanislav Romanov Date: Wed, 2 Aug 2023 09:39:10 +0300 Subject: [PATCH 082/812] Add `Twig Language Server` and `Modern Twig` extension to docs --- doc/templates.rst | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/doc/templates.rst b/doc/templates.rst index 3b18e9db074..7297256acad 100644 --- a/doc/templates.rst +++ b/doc/templates.rst @@ -59,7 +59,9 @@ Many IDEs support syntax highlighting and auto-completion for Twig: * *Notepad++* via the `Notepad++ Twig Highlighter`_ * *Emacs* via `web-mode.el`_ * *Atom* via the `PHP-twig for atom`_ -* *Visual Studio Code* via the `Twig pack`_ +* *Visual Studio Code* via the `Twig pack`_ or the `Modern Twig`_ + +There is the `Twig Language Server`_ that provides some language features like syntax highlighting, diagnostics, auto complete, etc. Also, `TwigFiddle`_ is an online service that allows you to execute Twig templates from a browser; it supports all versions of Twig. @@ -873,3 +875,5 @@ Twig can be extended. If you want to create your own extensions, read the .. _`PHP-twig for atom`: https://github.com/reesef/php-twig .. _`TwigFiddle`: https://twigfiddle.com/ .. _`Twig pack`: https://marketplace.visualstudio.com/items?itemName=bajdzis.vscode-twig-pack +.. _`Modern Twig`: https://marketplace.visualstudio.com/items?itemName=Stanislav.vscode-twig +.. _`Twig Language Server`: https://github.com/kaermorchen/twig-language-server/tree/master/packages/language-server From ef655c4d6ecbba2014892512d0c39065e5f15526 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Wed, 2 Aug 2023 10:55:33 +0200 Subject: [PATCH 083/812] Reorganize some information --- doc/templates.rst | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/doc/templates.rst b/doc/templates.rst index 7297256acad..816d04e9106 100644 --- a/doc/templates.rst +++ b/doc/templates.rst @@ -59,12 +59,15 @@ Many IDEs support syntax highlighting and auto-completion for Twig: * *Notepad++* via the `Notepad++ Twig Highlighter`_ * *Emacs* via `web-mode.el`_ * *Atom* via the `PHP-twig for atom`_ -* *Visual Studio Code* via the `Twig pack`_ or the `Modern Twig`_ +* *Visual Studio Code* via the `Twig pack`_ or `Modern Twig`_ -There is the `Twig Language Server`_ that provides some language features like syntax highlighting, diagnostics, auto complete, etc. +You might also be interested in: -Also, `TwigFiddle`_ is an online service that allows you to execute Twig templates -from a browser; it supports all versions of Twig. +* `TwigFiddle`_: an online service that allows you to execute Twig templates + from a browser; it supports all versions of Twig + +* `Twig Language Server`_: provides some language features like syntax + highlighting, diagnostics, auto complete, ... Variables --------- From 9503959e64c44840e4fbe6a64697f668ba0a63de Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Mon, 28 Aug 2023 13:08:35 +0200 Subject: [PATCH 084/812] Update CHANGELOG --- CHANGELOG | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index d07217e017b..e89501a5497 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,6 @@ # 3.7.1 (2023-XX-XX) - * n/a + * Fix some phpdocs # 3.7.0 (2023-07-26) From a0ce373a0ca3bf6c64b9e3e2124aca502ba39554 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Mon, 28 Aug 2023 13:09:02 +0200 Subject: [PATCH 085/812] Prepare the 3.7.1 release --- CHANGELOG | 2 +- src/Environment.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index e89501a5497..5ee518866bc 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,4 +1,4 @@ -# 3.7.1 (2023-XX-XX) +# 3.7.1 (2023-08-28) * Fix some phpdocs diff --git a/src/Environment.php b/src/Environment.php index 3a05712e501..a01c366c76a 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -40,12 +40,12 @@ */ class Environment { - public const VERSION = '3.7.1-DEV'; + public const VERSION = '3.7.1'; public const VERSION_ID = 30701; public const MAJOR_VERSION = 3; public const MINOR_VERSION = 7; public const RELEASE_VERSION = 1; - public const EXTRA_VERSION = 'DEV'; + public const EXTRA_VERSION = ''; private $charset; private $loader; From fd8f61b626cbddbe377553ef764436ffd1c2a021 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Mon, 28 Aug 2023 13:10:15 +0200 Subject: [PATCH 086/812] Bump version --- CHANGELOG | 4 ++++ src/Environment.php | 8 ++++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 5ee518866bc..a0b9de2b713 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +# 3.7.2 (2023-XX-XX) + + * n/a + # 3.7.1 (2023-08-28) * Fix some phpdocs diff --git a/src/Environment.php b/src/Environment.php index a01c366c76a..1477f687f7d 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -40,12 +40,12 @@ */ class Environment { - public const VERSION = '3.7.1'; - public const VERSION_ID = 30701; + public const VERSION = '3.7.2-DEV'; + public const VERSION_ID = 30702; public const MAJOR_VERSION = 3; public const MINOR_VERSION = 7; - public const RELEASE_VERSION = 1; - public const EXTRA_VERSION = ''; + public const RELEASE_VERSION = 2; + public const EXTRA_VERSION = 'DEV'; private $charset; private $loader; From 4d800d76320c5de1168ef388be0c7389d4f18f8b Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Mon, 11 Sep 2023 13:19:11 +0200 Subject: [PATCH 087/812] Bump CI actions --- .github/workflows/ci.yml | 6 +++--- .github/workflows/documentation.yml | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0a95b580cd8..97114e5f781 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,7 +34,7 @@ jobs: steps: - name: "Checkout code" - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: "Install PHP with extensions" uses: shivammathur/setup-php@v2 @@ -88,7 +88,7 @@ jobs: steps: - name: "Checkout code" - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: "Install PHP with extensions" uses: shivammathur/setup-php@v2 @@ -133,7 +133,7 @@ jobs: steps: - name: "Checkout code" - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: "Install PHP with extensions" uses: shivammathur/setup-php@v2 diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml index f2f46fc6d6e..9519f5d121d 100644 --- a/.github/workflows/documentation.yml +++ b/.github/workflows/documentation.yml @@ -18,7 +18,7 @@ jobs: steps: - name: "Checkout code" - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: "Set-up PHP" uses: shivammathur/setup-php@v2 @@ -54,7 +54,7 @@ jobs: steps: - name: "Checkout code" - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: "Run DOCtor-RST" uses: docker://oskarstark/doctor-rst From b8b0c39663d52e55f166c36bd3aa38ff6f25615f Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Mon, 11 Sep 2023 12:34:37 +0200 Subject: [PATCH 088/812] allow Symfony 7 packages to be installed --- .github/workflows/ci.yml | 2 +- composer.json | 2 +- extra/cache-extra/composer.json | 4 ++-- extra/cssinliner-extra/composer.json | 2 +- extra/html-extra/composer.json | 4 ++-- extra/inky-extra/composer.json | 2 +- extra/intl-extra/composer.json | 4 ++-- extra/markdown-extra/composer.json | 2 +- extra/string-extra/composer.json | 4 ++-- extra/twig-extra-bundle/composer.json | 6 +++--- 10 files changed, 16 insertions(+), 16 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d8af05234b4..b5a69f3c520 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -139,7 +139,7 @@ jobs: uses: shivammathur/setup-php@v2 with: coverage: "none" - extensions: "gd, pdo_sqlite" + extensions: "gd, pdo_sqlite, uuid" php-version: ${{ matrix.php-version }} ini-values: memory_limit=-1 tools: composer:v2 diff --git a/composer.json b/composer.json index ad83aeb133a..04dc93ffccf 100644 --- a/composer.json +++ b/composer.json @@ -29,7 +29,7 @@ "symfony/polyfill-ctype": "^1.8" }, "require-dev": { - "symfony/phpunit-bridge": "^5.4.9|^6.3", + "symfony/phpunit-bridge": "^5.4.9|^6.3|^7.0", "psr/container": "^1.0|^2.0" }, "autoload": { diff --git a/extra/cache-extra/composer.json b/extra/cache-extra/composer.json index ec116d7d4ab..b34e3142e26 100644 --- a/extra/cache-extra/composer.json +++ b/extra/cache-extra/composer.json @@ -16,11 +16,11 @@ ], "require": { "php": ">=7.2.5", - "symfony/cache": "^5.0|^6.0", + "symfony/cache": "^5.0|^6.0|^7.0", "twig/twig": "^2.4|^3.0" }, "require-dev": { - "symfony/phpunit-bridge": "^4.4.9|^5.0.9|^6.0" + "symfony/phpunit-bridge": "^4.4.9|^5.0.9|^6.0|^7.0" }, "autoload": { "psr-4" : { "Twig\\Extra\\Cache\\" : "" }, diff --git a/extra/cssinliner-extra/composer.json b/extra/cssinliner-extra/composer.json index cde7d3617cf..a1a6af7a97b 100644 --- a/extra/cssinliner-extra/composer.json +++ b/extra/cssinliner-extra/composer.json @@ -20,7 +20,7 @@ "twig/twig": "^2.7|^3.0" }, "require-dev": { - "symfony/phpunit-bridge": "^5.4|^6.3" + "symfony/phpunit-bridge": "^5.4|^6.3|^7.0" }, "autoload": { "psr-4" : { "Twig\\Extra\\CssInliner\\" : "" }, diff --git a/extra/html-extra/composer.json b/extra/html-extra/composer.json index b6a56665f05..98670abfacc 100644 --- a/extra/html-extra/composer.json +++ b/extra/html-extra/composer.json @@ -16,11 +16,11 @@ ], "require": { "php": ">=7.1.3", - "symfony/mime": "^5.4|^6.0", + "symfony/mime": "^5.4|^6.0|^7.0", "twig/twig": "^2.7|^3.0" }, "require-dev": { - "symfony/phpunit-bridge": "^5.4|^6.3" + "symfony/phpunit-bridge": "^5.4|^6.3|^7.0" }, "autoload": { "psr-4" : { "Twig\\Extra\\Html\\" : "" }, diff --git a/extra/inky-extra/composer.json b/extra/inky-extra/composer.json index bc27b906641..c4e42ea7fcd 100644 --- a/extra/inky-extra/composer.json +++ b/extra/inky-extra/composer.json @@ -20,7 +20,7 @@ "twig/twig": "^2.7|^3.0" }, "require-dev": { - "symfony/phpunit-bridge": "^5.4|^6.3" + "symfony/phpunit-bridge": "^5.4|^6.3|^7.0" }, "autoload": { "psr-4" : { "Twig\\Extra\\Inky\\" : "" }, diff --git a/extra/intl-extra/composer.json b/extra/intl-extra/composer.json index 2c0165ef626..e670bea3e3e 100644 --- a/extra/intl-extra/composer.json +++ b/extra/intl-extra/composer.json @@ -17,10 +17,10 @@ "require": { "php": ">=7.1.3", "twig/twig": "^2.7|^3.0", - "symfony/intl": "^5.4|^6.0" + "symfony/intl": "^5.4|^6.0|^7.0" }, "require-dev": { - "symfony/phpunit-bridge": "^5.4|^6.3" + "symfony/phpunit-bridge": "^5.4|^6.3|^7.0" }, "autoload": { "psr-4" : { "Twig\\Extra\\Intl\\" : "" }, diff --git a/extra/markdown-extra/composer.json b/extra/markdown-extra/composer.json index 60154f41a99..e278b955faa 100644 --- a/extra/markdown-extra/composer.json +++ b/extra/markdown-extra/composer.json @@ -19,7 +19,7 @@ "twig/twig": "^2.7|^3.0" }, "require-dev": { - "symfony/phpunit-bridge": "^5.4|^6.3", + "symfony/phpunit-bridge": "^5.4|^6.3|^7.0", "erusev/parsedown": "^1.7", "league/commonmark": "^1.0|^2.0", "league/html-to-markdown": "^4.8|^5.0", diff --git a/extra/string-extra/composer.json b/extra/string-extra/composer.json index 11ac690a7a0..ddd16c66f72 100644 --- a/extra/string-extra/composer.json +++ b/extra/string-extra/composer.json @@ -16,12 +16,12 @@ ], "require": { "php": ">=7.2.5", - "symfony/string": "^5.4|^6.0", + "symfony/string": "^5.4|^6.0|^7.0", "symfony/translation-contracts": "^1.1|^2|^3", "twig/twig": "^2.7|^3.0" }, "require-dev": { - "symfony/phpunit-bridge": "^5.4|^6.3" + "symfony/phpunit-bridge": "^5.4|^6.3|^7.0" }, "autoload": { "psr-4" : { "Twig\\Extra\\String\\" : "" }, diff --git a/extra/twig-extra-bundle/composer.json b/extra/twig-extra-bundle/composer.json index 031f222ad1e..942f6ae4c52 100644 --- a/extra/twig-extra-bundle/composer.json +++ b/extra/twig-extra-bundle/composer.json @@ -16,13 +16,13 @@ ], "require": { "php": ">=7.2.5", - "symfony/framework-bundle": "^5.4|^6.0", - "symfony/twig-bundle": "^5.4|^6.0", + "symfony/framework-bundle": "^5.4|^6.0|^7.0", + "symfony/twig-bundle": "^5.4|^6.0|^7.0", "twig/twig": "^2.7|^3.0" }, "require-dev": { "league/commonmark": "^1.0|^2.0", - "symfony/phpunit-bridge": "^5.4|^6.3", + "symfony/phpunit-bridge": "^5.4|^6.3|^7.0", "twig/cache-extra": "^3.0", "twig/cssinliner-extra": "^2.12|^3.0", "twig/html-extra": "^2.12|^3.0", From 5f7c48ca7d016d45c686ee701b8b1c727c995927 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Mon, 11 Sep 2023 17:25:06 +0200 Subject: [PATCH 089/812] Fix tests --- tests/drupal_test.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/drupal_test.sh b/tests/drupal_test.sh index e7fbf5dc7e9..29c71f21670 100644 --- a/tests/drupal_test.sh +++ b/tests/drupal_test.sh @@ -18,7 +18,7 @@ wget https://get.symfony.com/cli/installer -O - | bash export PATH="$HOME/.symfony5/bin:$PATH" symfony server:start -d --no-tls -curl -OLsS https://get.blackfire.io/blackfire-player.phar +curl -LsS -o blackfire-player.phar https://get.blackfire.io/blackfire-player-v1.31.0.phar chmod +x blackfire-player.phar cat > drupal-tests.bkf < Date: Mon, 11 Sep 2023 17:40:51 +0200 Subject: [PATCH 090/812] Remove Drupal tests --- .github/workflows/ci.yml | 3 --- tests/drupal_test.sh | 50 ---------------------------------------- 2 files changed, 53 deletions(-) delete mode 100644 tests/drupal_test.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 97114e5f781..405f0af1237 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -143,6 +143,3 @@ jobs: php-version: ${{ matrix.php-version }} ini-values: memory_limit=-1 tools: composer:v2 - - - run: bash ./tests/drupal_test.sh - shell: "bash" diff --git a/tests/drupal_test.sh b/tests/drupal_test.sh deleted file mode 100644 index e7fbf5dc7e9..00000000000 --- a/tests/drupal_test.sh +++ /dev/null @@ -1,50 +0,0 @@ -#!/bin/bash - -set -x -set -e - -REPO=`pwd` -cd /tmp -rm -rf drupal-twig-test -composer create-project --no-interaction drupal/recommended-project:10.1.x-dev drupal-twig-test -cd drupal-twig-test -(cd vendor/twig && rm -rf twig && ln -sf $REPO twig) -php ./web/core/scripts/drupal install --no-interaction demo_umami > output -perl -p -i -e 's/^([A-Za-z]+)\: (.+)$/export DRUPAL_\1=\2/' output -source output -#echo '$config["system.logging"]["error_level"] = "verbose";' >> web/sites/default/settings.php - -wget https://get.symfony.com/cli/installer -O - | bash -export PATH="$HOME/.symfony5/bin:$PATH" -symfony server:start -d --no-tls - -curl -OLsS https://get.blackfire.io/blackfire-player.phar -chmod +x blackfire-player.phar -cat > drupal-tests.bkf < Date: Fri, 15 Sep 2023 07:23:19 +0200 Subject: [PATCH 091/812] Set Twig 2 end of maintenance to December 2023 --- CHANGELOG | 2 ++ README.rst | 3 +++ 2 files changed, 5 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index 43e6604b6c1..b809182505b 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,5 @@ +**End of maintainance of version 2 is scheduled for end of December 2023** + # 2.15.6 (2023-XX-XX) * n/a diff --git a/README.rst b/README.rst index fbe7e9a9f83..f741105e350 100644 --- a/README.rst +++ b/README.rst @@ -6,6 +6,9 @@ Twig is a template language for PHP. Twig uses a syntax similar to the Django and Jinja template languages which inspired the Twig runtime environment. +**Twig version 2 end of maintainance is scheduled for end of December 2023.** +Please, upgrade at your earliest convenience. + Sponsors -------- From 5193653ecb34a541d98d0947604af06d64134dab Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sun, 8 Oct 2023 09:05:22 +0200 Subject: [PATCH 092/812] Fix CS --- extra/markdown-extra/MarkdownExtension.php | 2 +- extra/markdown-extra/Tests/FunctionalTest.php | 2 +- .../TwigExtraExtension.php | 2 +- .../Tests/Fixture/Kernel.php | 2 +- src/Cache/FilesystemCache.php | 2 +- src/Extension/CoreExtension.php | 11 +++---- src/Extension/DebugExtension.php | 4 +-- src/Extension/EscaperExtension.php | 4 +-- src/Lexer.php | 2 +- src/Markup.php | 3 -- .../MacroAutoImportNodeVisitor.php | 8 ++--- src/NodeVisitor/OptimizerNodeVisitor.php | 16 +++++----- src/Parser.php | 3 +- src/Test/IntegrationTestCase.php | 3 +- src/Token.php | 6 ++-- src/TokenParser/FromTokenParser.php | 4 +-- src/TokenParser/UseTokenParser.php | 4 +-- src/Util/TemplateDirIterator.php | 6 ---- tests/Extension/CoreTest.php | 6 ---- tests/Extension/EscaperTest.php | 24 +++++++-------- tests/IntegrationTest.php | 12 -------- tests/Node/IncludeTest.php | 10 +++---- tests/Node/ModuleTest.php | 10 +++---- tests/ParserTest.php | 2 +- tests/Profiler/Dumper/AbstractTest.php | 1 - tests/Profiler/Dumper/BlackfireTest.php | 2 +- tests/Profiler/Dumper/HtmlTest.php | 2 +- tests/Profiler/Dumper/TextTest.php | 2 +- tests/TemplateTest.php | 29 +++++++------------ 29 files changed, 72 insertions(+), 112 deletions(-) diff --git a/extra/markdown-extra/MarkdownExtension.php b/extra/markdown-extra/MarkdownExtension.php index 8f249ce6d86..6c4296bbf1b 100644 --- a/extra/markdown-extra/MarkdownExtension.php +++ b/extra/markdown-extra/MarkdownExtension.php @@ -34,7 +34,7 @@ function twig_html_to_markdown(string $body, array $options = []): string throw new \LogicException('You cannot use the "html_to_markdown" filter as league/html-to-markdown is not installed; try running "composer require league/html-to-markdown".'); } - $options = $options + [ + $options += [ 'hard_break' => true, 'strip_tags' => true, 'remove_nodes' => 'head style', diff --git a/extra/markdown-extra/Tests/FunctionalTest.php b/extra/markdown-extra/Tests/FunctionalTest.php index 368a0467b3b..42f4ff65a97 100644 --- a/extra/markdown-extra/Tests/FunctionalTest.php +++ b/extra/markdown-extra/Tests/FunctionalTest.php @@ -29,7 +29,7 @@ class FunctionalTest extends TestCase */ public function testMarkdown(string $template, string $expected): void { - foreach ([LeagueMarkdown::class, ErusevMarkdown::class, /*MichelfMarkdown::class,*/ DefaultMarkdown::class] as $class) { + foreach ([LeagueMarkdown::class, ErusevMarkdown::class, /* MichelfMarkdown::class, */ DefaultMarkdown::class] as $class) { $twig = new Environment(new ArrayLoader([ 'index' => $template, 'html' => <<isConfigEnabled($container, $config[$extension])) { $loader->load($extension.'.php'); - if ('markdown' === $extension && \class_exists(CommonMarkConverter::class)) { + if ('markdown' === $extension && class_exists(CommonMarkConverter::class)) { $loader->load('markdown_league.php'); } } diff --git a/extra/twig-extra-bundle/Tests/Fixture/Kernel.php b/extra/twig-extra-bundle/Tests/Fixture/Kernel.php index cbc9c4bd850..3df6357fe1f 100644 --- a/extra/twig-extra-bundle/Tests/Fixture/Kernel.php +++ b/extra/twig-extra-bundle/Tests/Fixture/Kernel.php @@ -4,11 +4,11 @@ use League\CommonMark\Extension\Strikethrough\StrikethroughExtension; use Symfony\Bundle\FrameworkBundle\FrameworkBundle; +use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait; use Symfony\Bundle\TwigBundle\TwigBundle; use Symfony\Component\Config\Loader\LoaderInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\HttpKernel\Kernel as BaseKernel; -use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait; use Twig\Extra\TwigExtraBundle\TwigExtraBundle; class Kernel extends BaseKernel diff --git a/src/Cache/FilesystemCache.php b/src/Cache/FilesystemCache.php index e075563aef6..4024adbd70d 100644 --- a/src/Cache/FilesystemCache.php +++ b/src/Cache/FilesystemCache.php @@ -63,7 +63,7 @@ public function write(string $key, string $content): void if (self::FORCE_BYTECODE_INVALIDATION == ($this->options & self::FORCE_BYTECODE_INVALIDATION)) { // Compile cached file into bytecode cache - if (\function_exists('opcache_invalidate') && filter_var(ini_get('opcache.enable'), \FILTER_VALIDATE_BOOLEAN)) { + if (\function_exists('opcache_invalidate') && filter_var(\ini_get('opcache.enable'), \FILTER_VALIDATE_BOOLEAN)) { @opcache_invalidate($key, true); } elseif (\function_exists('apc_compile_file')) { apc_compile_file($key); diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index 0f1a10216f3..6c16ee2b7d9 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -343,9 +343,9 @@ function twig_cycle($values, $position) * @param \Traversable|array|int|float|string $values The values to pick a random item from * @param int|null $max Maximum value used when $values is an int * - * @throws RuntimeError when $values is an empty array (does not apply to an empty string which is returned as is) - * * @return mixed A random value from the given sequence + * + * @throws RuntimeError when $values is an empty array (does not apply to an empty string which is returned as is) */ function twig_random(Environment $env, $values = null, $max = null) { @@ -768,7 +768,7 @@ function twig_split_filter(Environment $env, $value, $delimiter, $limit = null) { $value = $value ?? ''; - if (\strlen($delimiter) > 0) { + if ('' !== $delimiter) { return null === $limit ? explode($delimiter, $value) : explode($delimiter, $value, $limit); } @@ -1022,9 +1022,6 @@ function twig_compare($a, $b) } /** - * @param string $pattern - * @param string|null $subject - * * @return int * * @throws RuntimeError When an invalid pattern is used @@ -1123,7 +1120,7 @@ function twig_length_filter(Environment $env, $thing) return 0; } - if (is_scalar($thing)) { + if (\is_scalar($thing)) { return mb_strlen($thing, $env->getCharset()); } diff --git a/src/Extension/DebugExtension.php b/src/Extension/DebugExtension.php index bfb23d7bd4f..c0f10d5a303 100644 --- a/src/Extension/DebugExtension.php +++ b/src/Extension/DebugExtension.php @@ -19,10 +19,10 @@ public function getFunctions(): array // dump is safe if var_dump is overridden by xdebug $isDumpOutputHtmlSafe = \extension_loaded('xdebug') // false means that it was not set (and the default is on) or it explicitly enabled - && (false === ini_get('xdebug.overload_var_dump') || ini_get('xdebug.overload_var_dump')) + && (false === \ini_get('xdebug.overload_var_dump') || \ini_get('xdebug.overload_var_dump')) // false means that it was not set (and the default is on) or it explicitly enabled // xdebug.overload_var_dump produces HTML only when html_errors is also enabled - && (false === ini_get('html_errors') || ini_get('html_errors')) + && (false === \ini_get('html_errors') || \ini_get('html_errors')) || 'cli' === \PHP_SAPI ; diff --git a/src/Extension/EscaperExtension.php b/src/Extension/EscaperExtension.php index 9d2251dc6e1..ef8879dbdc6 100644 --- a/src/Extension/EscaperExtension.php +++ b/src/Extension/EscaperExtension.php @@ -341,7 +341,7 @@ function twig_escape_filter(Environment $env, $string, $strategy = 'html', $char * The following replaces characters undefined in HTML with the * hex entity for the Unicode replacement character. */ - if (($ord <= 0x1f && "\t" != $chr && "\n" != $chr && "\r" != $chr) || ($ord >= 0x7f && $ord <= 0x9f)) { + if (($ord <= 0x1F && "\t" != $chr && "\n" != $chr && "\r" != $chr) || ($ord >= 0x7F && $ord <= 0x9F)) { return '�'; } @@ -388,7 +388,7 @@ function twig_escape_filter(Environment $env, $string, $strategy = 'html', $char default: $escapers = $env->getExtension(EscaperExtension::class)->getEscapers(); - if (array_key_exists($strategy, $escapers)) { + if (\array_key_exists($strategy, $escapers)) { return $escapers[$strategy]($env, $string, $charset); } diff --git a/src/Lexer.php b/src/Lexer.php index 6c45efbc9f6..5a6258c2c96 100644 --- a/src/Lexer.php +++ b/src/Lexer.php @@ -422,7 +422,7 @@ private function lexString(): void $this->pushToken(/* Token::INTERPOLATION_START_TYPE */ 10); $this->moveCursor($match[0]); $this->pushState(self::STATE_INTERPOLATION); - } elseif (preg_match(self::REGEX_DQ_STRING_PART, $this->code, $match, 0, $this->cursor) && \strlen($match[0]) > 0) { + } elseif (preg_match(self::REGEX_DQ_STRING_PART, $this->code, $match, 0, $this->cursor) && '' !== $match[0]) { $this->pushToken(/* Token::STRING_TYPE */ 7, stripcslashes($match[0])); $this->moveCursor($match[0]); } elseif (preg_match(self::REGEX_DQ_STRING_DELIM, $this->code, $match, 0, $this->cursor)) { diff --git a/src/Markup.php b/src/Markup.php index 1788acc4f73..231a29b4121 100644 --- a/src/Markup.php +++ b/src/Markup.php @@ -41,9 +41,6 @@ public function count() return mb_strlen($this->content, $this->charset); } - /** - * @return mixed - */ #[\ReturnTypeWillChange] public function jsonSerialize() { diff --git a/src/NodeVisitor/MacroAutoImportNodeVisitor.php b/src/NodeVisitor/MacroAutoImportNodeVisitor.php index af477e65356..d6a7781ba27 100644 --- a/src/NodeVisitor/MacroAutoImportNodeVisitor.php +++ b/src/NodeVisitor/MacroAutoImportNodeVisitor.php @@ -50,10 +50,10 @@ public function leaveNode(Node $node, Environment $env): Node } } elseif ($this->inAModule) { if ( - $node instanceof GetAttrExpression && - $node->getNode('node') instanceof NameExpression && - '_self' === $node->getNode('node')->getAttribute('name') && - $node->getNode('attribute') instanceof ConstantExpression + $node instanceof GetAttrExpression + && $node->getNode('node') instanceof NameExpression + && '_self' === $node->getNode('node')->getAttribute('name') + && $node->getNode('attribute') instanceof ConstantExpression ) { $this->hasMacroCalls = true; diff --git a/src/NodeVisitor/OptimizerNodeVisitor.php b/src/NodeVisitor/OptimizerNodeVisitor.php index 7ac75e41ad3..d9c23ff0ec0 100644 --- a/src/NodeVisitor/OptimizerNodeVisitor.php +++ b/src/NodeVisitor/OptimizerNodeVisitor.php @@ -99,8 +99,8 @@ private function optimizePrintNode(Node $node, Environment $env): Node $exprNode = $node->getNode('expr'); if ( - $exprNode instanceof BlockReferenceExpression || - $exprNode instanceof ParentExpression + $exprNode instanceof BlockReferenceExpression + || $exprNode instanceof ParentExpression ) { $exprNode->setAttribute('output', true); @@ -166,7 +166,7 @@ private function enterOptimizeFor(Node $node, Environment $env): void && 'include' === $node->getAttribute('name') && (!$node->getNode('arguments')->hasNode('with_context') || false !== $node->getNode('arguments')->getNode('with_context')->getAttribute('value') - ) + ) ) { $this->addLoopToAll(); } @@ -175,12 +175,12 @@ private function enterOptimizeFor(Node $node, Environment $env): void elseif ($node instanceof GetAttrExpression && (!$node->getNode('attribute') instanceof ConstantExpression || 'parent' === $node->getNode('attribute')->getAttribute('value') - ) + ) && (true === $this->loops[0]->getAttribute('with_loop') - || ($node->getNode('node') instanceof NameExpression - && 'loop' === $node->getNode('node')->getAttribute('name') - ) - ) + || ($node->getNode('node') instanceof NameExpression + && 'loop' === $node->getNode('node')->getAttribute('name') + ) + ) ) { $this->addLoopToAll(); } diff --git a/src/Parser.php b/src/Parser.php index 4428208fed3..5590086f120 100644 --- a/src/Parser.php +++ b/src/Parser.php @@ -303,8 +303,7 @@ private function filterBodyNodes(Node $node, bool $nested = false): ?Node // check that the body does not contain non-empty output nodes if ( ($node instanceof TextNode && !ctype_space($node->getAttribute('data'))) - || - (!$node instanceof TextNode && !$node instanceof BlockReferenceNode && $node instanceof NodeOutputInterface) + || (!$node instanceof TextNode && !$node instanceof BlockReferenceNode && $node instanceof NodeOutputInterface) ) { if (false !== strpos((string) $node, \chr(0xEF).\chr(0xBB).\chr(0xBF))) { $t = substr($node->getAttribute('data'), 3); diff --git a/src/Test/IntegrationTestCase.php b/src/Test/IntegrationTestCase.php index 307302bb624..0aa5c3a5658 100644 --- a/src/Test/IntegrationTestCase.php +++ b/src/Test/IntegrationTestCase.php @@ -84,6 +84,7 @@ public function testIntegration($file, $message, $condition, $templates, $except /** * @dataProvider getLegacyTests + * * @group legacy */ public function testLegacyIntegration($file, $message, $condition, $templates, $exception, $outputs, $deprecation = '') @@ -257,7 +258,7 @@ protected static function parseTemplates($test) $templates = []; preg_match_all('/--TEMPLATE(?:\((.*?)\))?--(.*?)(?=\-\-TEMPLATE|$)/s', $test, $matches, \PREG_SET_ORDER); foreach ($matches as $match) { - $templates[($match[1] ?: 'index.twig')] = $match[2]; + $templates[$match[1] ?: 'index.twig'] = $match[2]; } return $templates; diff --git a/src/Token.php b/src/Token.php index fd1a89d2adc..59279b8fe7c 100644 --- a/src/Token.php +++ b/src/Token.php @@ -68,9 +68,9 @@ public function test($type, $values = null): bool } return ($this->type === $type) && ( - null === $values || - (\is_array($values) && \in_array($this->value, $values)) || - $this->value == $values + null === $values + || (\is_array($values) && \in_array($this->value, $values)) + || $this->value == $values ); } diff --git a/src/TokenParser/FromTokenParser.php b/src/TokenParser/FromTokenParser.php index 35098c267b1..31b6cde4148 100644 --- a/src/TokenParser/FromTokenParser.php +++ b/src/TokenParser/FromTokenParser.php @@ -32,7 +32,7 @@ public function parse(Token $token): Node $stream->expect(/* Token::NAME_TYPE */ 5, 'import'); $targets = []; - do { + while (true) { $name = $stream->expect(/* Token::NAME_TYPE */ 5)->getValue(); $alias = $name; @@ -45,7 +45,7 @@ public function parse(Token $token): Node if (!$stream->nextIf(/* Token::PUNCTUATION_TYPE */ 9, ',')) { break; } - } while (true); + } $stream->expect(/* Token::BLOCK_END_TYPE */ 3); diff --git a/src/TokenParser/UseTokenParser.php b/src/TokenParser/UseTokenParser.php index d0a2de41a2e..3cdbb98ad01 100644 --- a/src/TokenParser/UseTokenParser.php +++ b/src/TokenParser/UseTokenParser.php @@ -43,7 +43,7 @@ public function parse(Token $token): Node $targets = []; if ($stream->nextIf('with')) { - do { + while (true) { $name = $stream->expect(/* Token::NAME_TYPE */ 5)->getValue(); $alias = $name; @@ -56,7 +56,7 @@ public function parse(Token $token): Node if (!$stream->nextIf(/* Token::PUNCTUATION_TYPE */ 9, ',')) { break; } - } while (true); + } } $stream->expect(/* Token::BLOCK_END_TYPE */ 3); diff --git a/src/Util/TemplateDirIterator.php b/src/Util/TemplateDirIterator.php index 3bef14beec3..8125341bd81 100644 --- a/src/Util/TemplateDirIterator.php +++ b/src/Util/TemplateDirIterator.php @@ -16,18 +16,12 @@ */ class TemplateDirIterator extends \IteratorIterator { - /** - * @return mixed - */ #[\ReturnTypeWillChange] public function current() { return file_get_contents(parent::current()); } - /** - * @return mixed - */ #[\ReturnTypeWillChange] public function key() { diff --git a/tests/Extension/CoreTest.php b/tests/Extension/CoreTest.php index 29a799b8103..431517f6b21 100644 --- a/tests/Extension/CoreTest.php +++ b/tests/Extension/CoreTest.php @@ -380,9 +380,6 @@ public function rewind(): void $this->position = 0; } - /** - * @return mixed - */ #[\ReturnTypeWillChange] public function current() { @@ -393,9 +390,6 @@ public function current() throw new \LogicException('Code should only use the keys, not the values provided by iterator.'); } - /** - * @return mixed - */ #[\ReturnTypeWillChange] public function key() { diff --git a/tests/Extension/EscaperTest.php b/tests/Extension/EscaperTest.php index 9804feaa5c7..7c558c3ac1d 100644 --- a/tests/Extension/EscaperTest.php +++ b/tests/Extension/EscaperTest.php @@ -189,10 +189,10 @@ public function testJavascriptEscapingConvertsSpecialCharsWithInternalEncoding() try { mb_internal_encoding('ISO-8859-1'); foreach ($this->jsSpecialChars as $key => $value) { - $this->assertEquals($value, twig_escape_filter($twig, $key, 'js'), 'Failed to escape: ' . $key); + $this->assertEquals($value, twig_escape_filter($twig, $key, 'js'), 'Failed to escape: '.$key); } } finally { - if ($previousInternalEncoding !== false) { + if (false !== $previousInternalEncoding) { mb_internal_encoding($previousInternalEncoding); } } @@ -249,7 +249,7 @@ public function testUrlEscapingConvertsSpecialChars() public function testUnicodeCodepointConversionToUtf8() { $expected = ' ~ޙ'; - $codepoints = [0x20, 0x7e, 0x799]; + $codepoints = [0x20, 0x7E, 0x799]; $result = ''; foreach ($codepoints as $value) { $result .= $this->codepointToUtf8($value); @@ -270,19 +270,19 @@ protected function codepointToUtf8($codepoint) return \chr($codepoint); } if ($codepoint < 0x800) { - return \chr($codepoint >> 6 & 0x3f | 0xc0) - .\chr($codepoint & 0x3f | 0x80); + return \chr($codepoint >> 6 & 0x3F | 0xC0) + .\chr($codepoint & 0x3F | 0x80); } if ($codepoint < 0x10000) { - return \chr($codepoint >> 12 & 0x0f | 0xe0) - .\chr($codepoint >> 6 & 0x3f | 0x80) - .\chr($codepoint & 0x3f | 0x80); + return \chr($codepoint >> 12 & 0x0F | 0xE0) + .\chr($codepoint >> 6 & 0x3F | 0x80) + .\chr($codepoint & 0x3F | 0x80); } if ($codepoint < 0x110000) { - return \chr($codepoint >> 18 & 0x07 | 0xf0) - .\chr($codepoint >> 12 & 0x3f | 0x80) - .\chr($codepoint >> 6 & 0x3f | 0x80) - .\chr($codepoint & 0x3f | 0x80); + return \chr($codepoint >> 18 & 0x07 | 0xF0) + .\chr($codepoint >> 12 & 0x3F | 0x80) + .\chr($codepoint >> 6 & 0x3F | 0x80) + .\chr($codepoint & 0x3F | 0x80); } throw new \Exception('Codepoint requested outside of Unicode range.'); } diff --git a/tests/IntegrationTest.php b/tests/IntegrationTest.php index 893bda345e8..aa24d5fbe8f 100644 --- a/tests/IntegrationTest.php +++ b/tests/IntegrationTest.php @@ -105,18 +105,12 @@ public function rewind(): void $this->position = 0; } - /** - * @return mixed - */ #[\ReturnTypeWillChange] public function current() { return $this->array[$this->position]; } - /** - * @return mixed - */ #[\ReturnTypeWillChange] public function key() { @@ -371,9 +365,6 @@ class SimpleIteratorForTesting implements \Iterator private $data = [1, 2, 3, 4, 5, 6, 7]; private $key = 0; - /** - * @return mixed - */ #[\ReturnTypeWillChange] public function current() { @@ -385,9 +376,6 @@ public function next(): void ++$this->key; } - /** - * @return mixed - */ #[\ReturnTypeWillChange] public function key() { diff --git a/tests/Node/IncludeTest.php b/tests/Node/IncludeTest.php index ab1fdf0bfe0..92681662da7 100644 --- a/tests/Node/IncludeTest.php +++ b/tests/Node/IncludeTest.php @@ -47,11 +47,11 @@ public function getTests() ]; $expr = new ConditionalExpression( - new ConstantExpression(true, 1), - new ConstantExpression('foo', 1), - new ConstantExpression('foo', 1), - 0 - ); + new ConstantExpression(true, 1), + new ConstantExpression('foo', 1), + new ConstantExpression('foo', 1), + 0 + ); $node = new IncludeNode($expr, null, false, false, 1); $tests[] = [$node, <<createMock(LoaderInterface::class), ['debug' => true]); $node = new ModuleNode($body, $extends, $blocks, $macros, $traits, new Node([]), $source); diff --git a/tests/ParserTest.php b/tests/ParserTest.php index 65956cf7e74..cdd8e875743 100644 --- a/tests/ParserTest.php +++ b/tests/ParserTest.php @@ -168,7 +168,7 @@ public function testGetVarName() {{ foo }} {% endmacro %} EOF - , 'index'))); + , 'index'))); // The getVarName() must not depend on the template loaders, // If this test does not throw any exception, that's good. diff --git a/tests/Profiler/Dumper/AbstractTest.php b/tests/Profiler/Dumper/AbstractTest.php index 29e40d26cbc..1891c27507a 100644 --- a/tests/Profiler/Dumper/AbstractTest.php +++ b/tests/Profiler/Dumper/AbstractTest.php @@ -73,7 +73,6 @@ private function getMacroProfile(array $subProfiles = []) /** * @param string $name * @param float $duration - * @param bool $isTemplate * @param string $type * @param string $templateName * diff --git a/tests/Profiler/Dumper/BlackfireTest.php b/tests/Profiler/Dumper/BlackfireTest.php index b1c2cd7d1f0..3a33d94031b 100644 --- a/tests/Profiler/Dumper/BlackfireTest.php +++ b/tests/Profiler/Dumper/BlackfireTest.php @@ -31,6 +31,6 @@ public function testDump() embedded.twig==>included.twig//2 %d %d %d index.twig==>index.twig::macro(foo)//1 %d %d %d EOF - , $dumper->dump($this->getProfile())); + , $dumper->dump($this->getProfile())); } } diff --git a/tests/Profiler/Dumper/HtmlTest.php b/tests/Profiler/Dumper/HtmlTest.php index 20a1ab439c5..2dcbb9aec57 100644 --- a/tests/Profiler/Dumper/HtmlTest.php +++ b/tests/Profiler/Dumper/HtmlTest.php @@ -29,6 +29,6 @@ public function testDump() └ included.twig EOF - , $dumper->dump($this->getProfile())); + , $dumper->dump($this->getProfile())); } } diff --git a/tests/Profiler/Dumper/TextTest.php b/tests/Profiler/Dumper/TextTest.php index 8240e356bd2..ba19c2c90dc 100644 --- a/tests/Profiler/Dumper/TextTest.php +++ b/tests/Profiler/Dumper/TextTest.php @@ -29,6 +29,6 @@ public function testDump() └ included.twig EOF - , $dumper->dump($this->getProfile())); + , $dumper->dump($this->getProfile())); } } diff --git a/tests/TemplateTest.php b/tests/TemplateTest.php index bd26e9d95b7..9c2364c1d21 100644 --- a/tests/TemplateTest.php +++ b/tests/TemplateTest.php @@ -89,7 +89,7 @@ public function getAttributeExceptions() public function testGetAttributeWithSandbox($object, $item, $allowed) { $twig = new Environment($this->createMock(LoaderInterface::class)); - $policy = new SecurityPolicy([], [], [/*method*/], [/*prop*/], []); + $policy = new SecurityPolicy([], [], [/* method */], [/* prop */], []); $twig->addExtension(new SandboxExtension($policy, !$allowed)); $template = new TemplateForTest($twig); @@ -477,25 +477,22 @@ class TemplateArrayAccessObject implements \ArrayAccess '+4' => '+4', ]; - public function offsetExists($name) : bool + public function offsetExists($name): bool { return \array_key_exists($name, $this->attributes); } - /** - * @return mixed - */ #[\ReturnTypeWillChange] public function offsetGet($name) { return \array_key_exists($name, $this->attributes) ? $this->attributes[$name] : null; } - public function offsetSet($name, $value) : void + public function offsetSet($name, $value): void { } - public function offsetUnset($name) : void + public function offsetUnset($name): void { } } @@ -570,25 +567,22 @@ class TemplatePropertyObjectAndArrayAccess extends TemplatePropertyObject implem 'baf' => 'baf', ]; - public function offsetExists($offset) : bool + public function offsetExists($offset): bool { return \array_key_exists($offset, $this->data); } - /** - * @return mixed - */ #[\ReturnTypeWillChange] public function offsetGet($offset) { return $this->offsetExists($offset) ? $this->data[$offset] : 'n/a'; } - public function offsetSet($offset, $value) : void + public function offsetSet($offset, $value): void { } - public function offsetUnset($offset) : void + public function offsetUnset($offset): void { } } @@ -722,26 +716,23 @@ class TemplateArrayAccess implements \ArrayAccess ]; private $children = []; - public function offsetExists($offset) : bool + public function offsetExists($offset): bool { return \array_key_exists($offset, $this->children); } - /** - * @return mixed - */ #[\ReturnTypeWillChange] public function offsetGet($offset) { return $this->children[$offset]; } - public function offsetSet($offset, $value) : void + public function offsetSet($offset, $value): void { $this->children[$offset] = $value; } - public function offsetUnset($offset) : void + public function offsetUnset($offset): void { unset($this->children[$offset]); } From 1d5c09285f8a78673fa2a65dab8d5d2468c4373c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Sun, 8 Oct 2023 16:53:31 +0200 Subject: [PATCH 093/812] Use PHP 8.0 functions with polyfill --- composer.json | 1 + src/Error/Error.php | 6 +++--- src/Error/SyntaxError.php | 2 +- src/Extension/CoreExtension.php | 8 ++++---- src/FileExtensionEscapingStrategy.php | 2 +- src/Lexer.php | 6 +++--- src/Loader/FilesystemLoader.php | 2 +- src/Node/Expression/CallExpression.php | 6 +++--- src/Parser.php | 2 +- src/Profiler/Profile.php | 2 +- src/Test/IntegrationTestCase.php | 2 +- 11 files changed, 20 insertions(+), 19 deletions(-) diff --git a/composer.json b/composer.json index 04dc93ffccf..1b1726fe882 100644 --- a/composer.json +++ b/composer.json @@ -25,6 +25,7 @@ ], "require": { "php": ">=7.2.5", + "symfony/polyfill-php80": "^1.22", "symfony/polyfill-mbstring": "^1.3", "symfony/polyfill-ctype": "^1.8" }, diff --git a/src/Error/Error.php b/src/Error/Error.php index a68be65f203..d2d1f97a9dc 100644 --- a/src/Error/Error.php +++ b/src/Error/Error.php @@ -130,13 +130,13 @@ private function updateRepr(): void } $dot = false; - if ('.' === substr($this->message, -1)) { + if (str_ends_with($this->message, '.')) { $this->message = substr($this->message, 0, -1); $dot = true; } $questionMark = false; - if ('?' === substr($this->message, -1)) { + if (str_ends_with($this->message, '?')) { $this->message = substr($this->message, 0, -1); $questionMark = true; } @@ -172,7 +172,7 @@ private function guessTemplateInfo(): void foreach ($backtrace as $trace) { if (isset($trace['object']) && $trace['object'] instanceof Template) { $currentClass = \get_class($trace['object']); - $isEmbedContainer = null === $templateClass ? false : 0 === strpos($templateClass, $currentClass); + $isEmbedContainer = null === $templateClass ? false : str_starts_with($templateClass, $currentClass); if (null === $this->name || ($this->name == $trace['object']->getTemplateName() && !$isEmbedContainer)) { $template = $trace['object']; $templateClass = \get_class($trace['object']); diff --git a/src/Error/SyntaxError.php b/src/Error/SyntaxError.php index 726b3309e5b..77c437c6882 100644 --- a/src/Error/SyntaxError.php +++ b/src/Error/SyntaxError.php @@ -30,7 +30,7 @@ public function addSuggestions(string $name, array $items): void $alternatives = []; foreach ($items as $item) { $lev = levenshtein($name, $item); - if ($lev <= \strlen($name) / 3 || false !== strpos($item, $name)) { + if ($lev <= \strlen($name) / 3 || str_contains($item, $name)) { $alternatives[$item] = $lev; } } diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index 6c16ee2b7d9..430102f8681 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -926,7 +926,7 @@ function twig_in_filter($value, $compare) if (\is_string($compare)) { if (\is_string($value) || \is_int($value) || \is_float($value)) { - return '' === $value || false !== strpos($compare, (string) $value); + return '' === $value || str_contains($compare, (string) $value); } return false; @@ -1570,13 +1570,13 @@ function twig_get_attribute(Environment $env, Source $source, $object, $item, ar $classCache[$method] = $method; $classCache[$lcName = $lcMethods[$i]] = $method; - if ('g' === $lcName[0] && 0 === strpos($lcName, 'get')) { + if ('g' === $lcName[0] && str_starts_with($lcName, 'get')) { $name = substr($method, 3); $lcName = substr($lcName, 3); - } elseif ('i' === $lcName[0] && 0 === strpos($lcName, 'is')) { + } elseif ('i' === $lcName[0] && str_starts_with($lcName, 'is')) { $name = substr($method, 2); $lcName = substr($lcName, 2); - } elseif ('h' === $lcName[0] && 0 === strpos($lcName, 'has')) { + } elseif ('h' === $lcName[0] && str_starts_with($lcName, 'has')) { $name = substr($method, 3); $lcName = substr($lcName, 3); if (\in_array('is'.$lcName, $lcMethods)) { diff --git a/src/FileExtensionEscapingStrategy.php b/src/FileExtensionEscapingStrategy.php index 65198bbb649..812071bf971 100644 --- a/src/FileExtensionEscapingStrategy.php +++ b/src/FileExtensionEscapingStrategy.php @@ -37,7 +37,7 @@ public static function guess(string $name) return 'html'; // return html for directories } - if ('.twig' === substr($name, -5)) { + if (str_ends_with($name, '.twig')) { $name = substr($name, 0, -5); } diff --git a/src/Lexer.php b/src/Lexer.php index 5a6258c2c96..b23080f58e0 100644 --- a/src/Lexer.php +++ b/src/Lexer.php @@ -345,13 +345,13 @@ private function lexExpression(): void $this->moveCursor($match[0]); } // punctuation - elseif (false !== strpos(self::PUNCTUATION, $this->code[$this->cursor])) { + elseif (str_contains(self::PUNCTUATION, $this->code[$this->cursor])) { // opening bracket - if (false !== strpos('([{', $this->code[$this->cursor])) { + if (str_contains('([{', $this->code[$this->cursor])) { $this->brackets[] = [$this->code[$this->cursor], $this->lineno]; } // closing bracket - elseif (false !== strpos(')]}', $this->code[$this->cursor])) { + elseif (str_contains(')]}', $this->code[$this->cursor])) { if (empty($this->brackets)) { throw new SyntaxError(sprintf('Unexpected "%s".', $this->code[$this->cursor]), $this->lineno, $this->source); } diff --git a/src/Loader/FilesystemLoader.php b/src/Loader/FilesystemLoader.php index 62267a11c89..35a3299a490 100644 --- a/src/Loader/FilesystemLoader.php +++ b/src/Loader/FilesystemLoader.php @@ -250,7 +250,7 @@ private function parseName(string $name, string $default = self::MAIN_NAMESPACE) private function validateName(string $name): void { - if (false !== strpos($name, "\0")) { + if (str_contains($name, "\0")) { throw new LoaderError('A template name cannot contain NUL bytes.'); } diff --git a/src/Node/Expression/CallExpression.php b/src/Node/Expression/CallExpression.php index 11a6b1abcae..3a2d7a4fca4 100644 --- a/src/Node/Expression/CallExpression.php +++ b/src/Node/Expression/CallExpression.php @@ -24,7 +24,7 @@ protected function compileCallable(Compiler $compiler) { $callable = $this->getAttribute('callable'); - if (\is_string($callable) && false === strpos($callable, '::')) { + if (\is_string($callable) && !str_contains($callable, '::')) { $compiler->raw($callable); } else { [$r, $callable] = $this->reflectCallable($callable); @@ -297,13 +297,13 @@ private function reflectCallable($callable) } $r = new \ReflectionFunction($closure); - if (false !== strpos($r->name, '{closure}')) { + if (str_contains($r->name, '{closure}')) { return $this->reflector = [$r, $callable, 'Closure']; } if ($object = $r->getClosureThis()) { $callable = [$object, $r->name]; - $callableName = (\function_exists('get_debug_type') ? get_debug_type($object) : \get_class($object)).'::'.$r->name; + $callableName = get_debug_type($object).'::'.$r->name; } elseif (\PHP_VERSION_ID >= 80111 && $class = $r->getClosureCalledClass()) { $callableName = $class->name.'::'.$r->name; } elseif (\PHP_VERSION_ID < 80111 && $class = $r->getClosureScopeClass()) { diff --git a/src/Parser.php b/src/Parser.php index 5590086f120..4016a5f39ab 100644 --- a/src/Parser.php +++ b/src/Parser.php @@ -305,7 +305,7 @@ private function filterBodyNodes(Node $node, bool $nested = false): ?Node ($node instanceof TextNode && !ctype_space($node->getAttribute('data'))) || (!$node instanceof TextNode && !$node instanceof BlockReferenceNode && $node instanceof NodeOutputInterface) ) { - if (false !== strpos((string) $node, \chr(0xEF).\chr(0xBB).\chr(0xBF))) { + if (str_contains((string) $node, \chr(0xEF).\chr(0xBB).\chr(0xBF))) { $t = substr($node->getAttribute('data'), 3); if ('' === $t || ctype_space($t)) { // bypass empty nodes starting with a BOM diff --git a/src/Profiler/Profile.php b/src/Profiler/Profile.php index 252ca9b0cf4..7979a23c67a 100644 --- a/src/Profiler/Profile.php +++ b/src/Profiler/Profile.php @@ -32,7 +32,7 @@ public function __construct(string $template = 'main', string $type = self::ROOT { $this->template = $template; $this->type = $type; - $this->name = 0 === strpos($name, '__internal_') ? 'INTERNAL' : $name; + $this->name = str_starts_with($name, '__internal_') ? 'INTERNAL' : $name; $this->enter(); } diff --git a/src/Test/IntegrationTestCase.php b/src/Test/IntegrationTestCase.php index 0aa5c3a5658..e97ad417062 100644 --- a/src/Test/IntegrationTestCase.php +++ b/src/Test/IntegrationTestCase.php @@ -102,7 +102,7 @@ public function getTests($name, $legacyTests = false) continue; } - if ($legacyTests xor false !== strpos($file->getRealpath(), '.legacy.test')) { + if ($legacyTests xor str_contains($file->getRealpath(), '.legacy.test')) { continue; } From c56b87b4d7ee4f00b51aa234df8ee6a615610a8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Sun, 8 Oct 2023 19:29:03 +0200 Subject: [PATCH 094/812] Remove TemplateWrapper::render 2nd parameter not used --- src/TemplateWrapper.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/TemplateWrapper.php b/src/TemplateWrapper.php index c9c6b07c669..1ecd82251f3 100644 --- a/src/TemplateWrapper.php +++ b/src/TemplateWrapper.php @@ -35,9 +35,7 @@ public function __construct(Environment $env, Template $template) public function render(array $context = []): string { - // using func_get_args() allows to not expose the blocks argument - // as it should only be used by internal code - return $this->template->render($context, \func_get_args()[1] ?? []); + return $this->template->render($context); } public function display(array $context = []) From 41f1b0370a4bddcb4fa03f7a7556683a1050aaeb Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Sun, 8 Oct 2023 20:59:56 +0200 Subject: [PATCH 095/812] restore return type annotations --- src/Markup.php | 3 +++ src/Util/TemplateDirIterator.php | 6 ++++++ 2 files changed, 9 insertions(+) diff --git a/src/Markup.php b/src/Markup.php index 231a29b4121..1788acc4f73 100644 --- a/src/Markup.php +++ b/src/Markup.php @@ -41,6 +41,9 @@ public function count() return mb_strlen($this->content, $this->charset); } + /** + * @return mixed + */ #[\ReturnTypeWillChange] public function jsonSerialize() { diff --git a/src/Util/TemplateDirIterator.php b/src/Util/TemplateDirIterator.php index 8125341bd81..3bef14beec3 100644 --- a/src/Util/TemplateDirIterator.php +++ b/src/Util/TemplateDirIterator.php @@ -16,12 +16,18 @@ */ class TemplateDirIterator extends \IteratorIterator { + /** + * @return mixed + */ #[\ReturnTypeWillChange] public function current() { return file_get_contents(parent::current()); } + /** + * @return mixed + */ #[\ReturnTypeWillChange] public function key() { From 6d715e2002921f5854cbcc7b6e74667e83e38ced Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Sun, 8 Oct 2023 21:18:06 +0200 Subject: [PATCH 096/812] Remove unused variables and unreachable code --- src/Environment.php | 1 - src/ExpressionParser.php | 4 ---- src/Extension/CoreExtension.php | 3 +-- src/Node/Node.php | 1 - src/NodeVisitor/EscaperNodeVisitor.php | 9 ++++----- src/NodeVisitor/OptimizerNodeVisitor.php | 16 ++++++++-------- 6 files changed, 13 insertions(+), 21 deletions(-) diff --git a/src/Environment.php b/src/Environment.php index 1477f687f7d..1b37e35edd6 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -345,7 +345,6 @@ public function loadTemplate(string $cls, string $name, int $index = null): Temp $this->cache->load($key); } - $source = null; if (!class_exists($cls, false)) { $source = $this->getLoader()->getSourceContext($name); $content = $this->compileSource($source); diff --git a/src/ExpressionParser.php b/src/ExpressionParser.php index 38347cb391d..eb33868918f 100644 --- a/src/ExpressionParser.php +++ b/src/ExpressionParser.php @@ -506,10 +506,6 @@ public function parseSubscriptExpression($node) } if ($node instanceof NameExpression && null !== $this->parser->getImportedSymbol('template', $node->getAttribute('name'))) { - if (!$arg instanceof ConstantExpression) { - throw new SyntaxError(sprintf('Dynamic macro names are not supported (called on "%s").', $node->getAttribute('name')), $token->getLine(), $stream->getSourceContext()); - } - $name = $arg->getAttribute('value'); $node = new MethodCallExpression($node, 'macro_'.$name, $arguments, $lineno); diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index 6c16ee2b7d9..2302f907355 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -364,7 +364,6 @@ function twig_random(Environment $env, $values = null, $max = null) } } else { $min = $values; - $max = $max; } return mt_rand((int) $min, (int) $max); @@ -669,7 +668,7 @@ function twig_slice(Environment $env, $item, $start, $length = null, $preserveKe return \array_slice($item, $start, $length, $preserveKeys); } - return (string) mb_substr((string) $item, $start, $length, $env->getCharset()); + return mb_substr((string) $item, $start, $length, $env->getCharset()); } /** diff --git a/src/Node/Node.php b/src/Node/Node.php index c0558b9afdc..30659ae0fd1 100644 --- a/src/Node/Node.php +++ b/src/Node/Node.php @@ -27,7 +27,6 @@ class Node implements \Countable, \IteratorAggregate protected $lineno; protected $tag; - private $name; private $sourceContext; /** diff --git a/src/NodeVisitor/EscaperNodeVisitor.php b/src/NodeVisitor/EscaperNodeVisitor.php index fe56ea30741..57dce7e361c 100644 --- a/src/NodeVisitor/EscaperNodeVisitor.php +++ b/src/NodeVisitor/EscaperNodeVisitor.php @@ -57,7 +57,7 @@ public function enterNode(Node $node, Environment $env): Node } elseif ($node instanceof AutoEscapeNode) { $this->statusStack[] = $node->getAttribute('value'); } elseif ($node instanceof BlockNode) { - $this->statusStack[] = isset($this->blocks[$node->getAttribute('name')]) ? $this->blocks[$node->getAttribute('name')] : $this->needEscaping($env); + $this->statusStack[] = $this->blocks[$node->getAttribute('name')] ?? $this->needEscaping(); } elseif ($node instanceof ImportNode) { $this->safeVars[] = $node->getNode('var')->getAttribute('name'); } @@ -73,7 +73,7 @@ public function leaveNode(Node $node, Environment $env): ?Node $this->blocks = []; } elseif ($node instanceof FilterExpression) { return $this->preEscapeFilterNode($node, $env); - } elseif ($node instanceof PrintNode && false !== $type = $this->needEscaping($env)) { + } elseif ($node instanceof PrintNode && false !== $type = $this->needEscaping()) { $expression = $node->getNode('expr'); if ($expression instanceof ConditionalExpression && $this->shouldUnwrapConditional($expression, $env, $type)) { return new DoNode($this->unwrapConditional($expression, $env, $type), $expression->getTemplateLine()); @@ -85,7 +85,7 @@ public function leaveNode(Node $node, Environment $env): ?Node if ($node instanceof AutoEscapeNode || $node instanceof BlockNode) { array_pop($this->statusStack); } elseif ($node instanceof BlockReferenceNode) { - $this->blocks[$node->getAttribute('name')] = $this->needEscaping($env); + $this->blocks[$node->getAttribute('name')] = $this->needEscaping(); } return $node; @@ -183,12 +183,11 @@ private function isSafeFor(string $type, Node $expression, Environment $env): bo return \in_array($type, $safe) || \in_array('all', $safe); } - private function needEscaping(Environment $env) + private function needEscaping() { if (\count($this->statusStack)) { return $this->statusStack[\count($this->statusStack) - 1]; } - return $this->defaultStrategy ? $this->defaultStrategy : false; } diff --git a/src/NodeVisitor/OptimizerNodeVisitor.php b/src/NodeVisitor/OptimizerNodeVisitor.php index d9c23ff0ec0..6b39f00947c 100644 --- a/src/NodeVisitor/OptimizerNodeVisitor.php +++ b/src/NodeVisitor/OptimizerNodeVisitor.php @@ -63,7 +63,7 @@ public function __construct(int $optimizers = -1) public function enterNode(Node $node, Environment $env): Node { if (self::OPTIMIZE_FOR === (self::OPTIMIZE_FOR & $this->optimizers)) { - $this->enterOptimizeFor($node, $env); + $this->enterOptimizeFor($node); } return $node; @@ -72,14 +72,14 @@ public function enterNode(Node $node, Environment $env): Node public function leaveNode(Node $node, Environment $env): ?Node { if (self::OPTIMIZE_FOR === (self::OPTIMIZE_FOR & $this->optimizers)) { - $this->leaveOptimizeFor($node, $env); + $this->leaveOptimizeFor($node); } if (self::OPTIMIZE_RAW_FILTER === (self::OPTIMIZE_RAW_FILTER & $this->optimizers)) { - $node = $this->optimizeRawFilter($node, $env); + $node = $this->optimizeRawFilter($node); } - $node = $this->optimizePrintNode($node, $env); + $node = $this->optimizePrintNode($node); return $node; } @@ -91,7 +91,7 @@ public function leaveNode(Node $node, Environment $env): ?Node * * * "echo $this->render(Parent)Block()" with "$this->display(Parent)Block()" */ - private function optimizePrintNode(Node $node, Environment $env): Node + private function optimizePrintNode(Node $node): Node { if (!$node instanceof PrintNode) { return $node; @@ -113,7 +113,7 @@ private function optimizePrintNode(Node $node, Environment $env): Node /** * Removes "raw" filters. */ - private function optimizeRawFilter(Node $node, Environment $env): Node + private function optimizeRawFilter(Node $node): Node { if ($node instanceof FilterExpression && 'raw' == $node->getNode('filter')->getAttribute('value')) { return $node->getNode('node'); @@ -125,7 +125,7 @@ private function optimizeRawFilter(Node $node, Environment $env): Node /** * Optimizes "for" tag by removing the "loop" variable creation whenever possible. */ - private function enterOptimizeFor(Node $node, Environment $env): void + private function enterOptimizeFor(Node $node): void { if ($node instanceof ForNode) { // disable the loop variable by default @@ -189,7 +189,7 @@ private function enterOptimizeFor(Node $node, Environment $env): void /** * Optimizes "for" tag by removing the "loop" variable creation whenever possible. */ - private function leaveOptimizeFor(Node $node, Environment $env): void + private function leaveOptimizeFor(Node $node): void { if ($node instanceof ForNode) { array_shift($this->loops); From 81799a7461279c0e2ac9ca6805e1f7731290615e Mon Sep 17 00:00:00 2001 From: Mikhail Gunin Date: Wed, 11 Oct 2023 16:09:47 +0300 Subject: [PATCH 097/812] Add `Twiggy` extension for VS Code to docs. --- doc/templates.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/doc/templates.rst b/doc/templates.rst index c145895da0a..d70fdbbe458 100644 --- a/doc/templates.rst +++ b/doc/templates.rst @@ -59,7 +59,7 @@ Many IDEs support syntax highlighting and auto-completion for Twig: * *Notepad++* via the `Notepad++ Twig Highlighter`_ * *Emacs* via `web-mode.el`_ * *Atom* via the `PHP-twig for atom`_ -* *Visual Studio Code* via the `Twig pack`_ or `Modern Twig`_ +* *Visual Studio Code* via the `Twig pack`_, `Modern Twig`_ or `Twiggy`_ You might also be interested in: @@ -891,3 +891,4 @@ Twig can be extended. If you want to create your own extensions, read the .. _`Twig pack`: https://marketplace.visualstudio.com/items?itemName=bajdzis.vscode-twig-pack .. _`Modern Twig`: https://marketplace.visualstudio.com/items?itemName=Stanislav.vscode-twig .. _`Twig Language Server`: https://github.com/kaermorchen/twig-language-server/tree/master/packages/language-server +.. _`Twiggy`: https://marketplace.visualstudio.com/items?itemName=moetelo.twiggy From 534965078174773cdbc13e515737b9992a1e38c0 Mon Sep 17 00:00:00 2001 From: devojifr <44780880+devojifr@users.noreply.github.com> Date: Sat, 14 Oct 2023 11:22:02 +0200 Subject: [PATCH 098/812] range example leads to Array to string conversion exception In templates documentation, a range example is displayed and leads to an Array to string conversion exception without a join filter. --- doc/templates.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/templates.rst b/doc/templates.rst index d70fdbbe458..4ccfb8bb06f 100644 --- a/doc/templates.rst +++ b/doc/templates.rst @@ -752,11 +752,11 @@ The following operators don't fit into any of the other categories: (this is syntactic sugar for the :doc:`range` function): .. code-block:: twig - - {{ 1..5 }} + {# join filter is applied to avoid Array to string conversion exception #} + {{ (1..5)|join(', ') }} {# equivalent to #} - {{ range(1, 5) }} + {{ range(1, 5)|join(', ') }} Note that you must use parentheses when combining it with the filter operator due to the :ref:`operator precedence rules `: From 223ba6ecdef1558a586e75e557d1d2b01a64a46b Mon Sep 17 00:00:00 2001 From: Michael Bolli Date: Fri, 20 Oct 2023 16:28:22 +0200 Subject: [PATCH 099/812] fix `NumberFormatter::TYPE_CURRENCY` being deprecated in PHP 8.3 --- extra/intl-extra/IntlExtension.php | 1 - 1 file changed, 1 deletion(-) diff --git a/extra/intl-extra/IntlExtension.php b/extra/intl-extra/IntlExtension.php index 955d6ec9233..eedf136e010 100644 --- a/extra/intl-extra/IntlExtension.php +++ b/extra/intl-extra/IntlExtension.php @@ -67,7 +67,6 @@ private static function availableDateFormats(): array 'int32' => \NumberFormatter::TYPE_INT32, 'int64' => \NumberFormatter::TYPE_INT64, 'double' => \NumberFormatter::TYPE_DOUBLE, - 'currency' => \NumberFormatter::TYPE_CURRENCY, ]; private const NUMBER_STYLES = [ 'decimal' => \NumberFormatter::DECIMAL, From fd3f179718993dde6163626901adbde2389f0e19 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Fri, 20 Oct 2023 17:35:29 +0200 Subject: [PATCH 100/812] Rewrite an example --- doc/templates.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/doc/templates.rst b/doc/templates.rst index 4ccfb8bb06f..ccfea484cf9 100644 --- a/doc/templates.rst +++ b/doc/templates.rst @@ -752,11 +752,11 @@ The following operators don't fit into any of the other categories: (this is syntactic sugar for the :doc:`range` function): .. code-block:: twig - {# join filter is applied to avoid Array to string conversion exception #} - {{ (1..5)|join(', ') }} - {# equivalent to #} - {{ range(1, 5)|join(', ') }} + {% for i in 1..5 %}{{ i }}{% endfor %} + + {# is equivalent to #} + {% for i in range(1, 5) %}{{ i }}{% endfor %} Note that you must use parentheses when combining it with the filter operator due to the :ref:`operator precedence rules `: From eaf22ba98d6fc0f1e649ebbf601199fb3ea8681c Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Fri, 20 Oct 2023 17:39:50 +0200 Subject: [PATCH 101/812] Fix CS --- src/NodeVisitor/EscaperNodeVisitor.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/NodeVisitor/EscaperNodeVisitor.php b/src/NodeVisitor/EscaperNodeVisitor.php index 57dce7e361c..3d92ac9f258 100644 --- a/src/NodeVisitor/EscaperNodeVisitor.php +++ b/src/NodeVisitor/EscaperNodeVisitor.php @@ -188,6 +188,7 @@ private function needEscaping() if (\count($this->statusStack)) { return $this->statusStack[\count($this->statusStack) - 1]; } + return $this->defaultStrategy ? $this->defaultStrategy : false; } From cb8a8247844d129f6381844aa8164ab823a49424 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Sun, 8 Oct 2023 18:21:11 +0200 Subject: [PATCH 102/812] Use is_iterable when possible --- src/Extension/CoreExtension.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index 2302f907355..f5b0f828314 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -1229,7 +1229,7 @@ function twig_call_macro(Template $template, string $method, array $args, int $l */ function twig_ensure_traversable($seq) { - if ($seq instanceof \Traversable || \is_array($seq)) { + if (is_iterable($seq)) { return $seq; } @@ -1295,7 +1295,7 @@ function twig_test_empty($value) */ function twig_test_iterable($value) { - return $value instanceof \Traversable || \is_array($value); + return is_iterable($value); } /** From 6ca0d773c5c5f633abe3d5cce639594009762579 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Sun, 8 Oct 2023 18:31:09 +0200 Subject: [PATCH 103/812] Convert Ternary to Elvis or Null Coalescing --- src/Extension/CoreExtension.php | 2 +- src/Loader/FilesystemLoader.php | 2 +- src/NodeVisitor/EscaperNodeVisitor.php | 2 +- src/Profiler/Dumper/HtmlDumper.php | 2 +- src/Test/NodeTestCase.php | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index f5b0f828314..63267ba6776 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -655,7 +655,7 @@ function twig_slice(Environment $env, $item, $start, $length = null, $preserveKe if ($start >= 0 && $length >= 0 && $item instanceof \Iterator) { try { - return iterator_to_array(new \LimitIterator($item, $start, null === $length ? -1 : $length), $preserveKeys); + return iterator_to_array(new \LimitIterator($item, $start, $length ?? -1), $preserveKeys); } catch (\OutOfBoundsException $e) { return []; } diff --git a/src/Loader/FilesystemLoader.php b/src/Loader/FilesystemLoader.php index 62267a11c89..e10c8f2b092 100644 --- a/src/Loader/FilesystemLoader.php +++ b/src/Loader/FilesystemLoader.php @@ -36,7 +36,7 @@ class FilesystemLoader implements LoaderInterface */ public function __construct($paths = [], string $rootPath = null) { - $this->rootPath = (null === $rootPath ? getcwd() : $rootPath).\DIRECTORY_SEPARATOR; + $this->rootPath = ($rootPath ?? getcwd()).\DIRECTORY_SEPARATOR; if (null !== $rootPath && false !== ($realPath = realpath($rootPath))) { $this->rootPath = $realPath.\DIRECTORY_SEPARATOR; } diff --git a/src/NodeVisitor/EscaperNodeVisitor.php b/src/NodeVisitor/EscaperNodeVisitor.php index 3d92ac9f258..c390d7cc71b 100644 --- a/src/NodeVisitor/EscaperNodeVisitor.php +++ b/src/NodeVisitor/EscaperNodeVisitor.php @@ -189,7 +189,7 @@ private function needEscaping() return $this->statusStack[\count($this->statusStack) - 1]; } - return $this->defaultStrategy ? $this->defaultStrategy : false; + return $this->defaultStrategy ?: false; } private function getEscaperFilter(string $type, Node $node): FilterExpression diff --git a/src/Profiler/Dumper/HtmlDumper.php b/src/Profiler/Dumper/HtmlDumper.php index 1f2433b4d36..3c0daf1c8d3 100644 --- a/src/Profiler/Dumper/HtmlDumper.php +++ b/src/Profiler/Dumper/HtmlDumper.php @@ -37,7 +37,7 @@ protected function formatTemplate(Profile $profile, $prefix): string protected function formatNonTemplate(Profile $profile, $prefix): string { - return sprintf('%s└ %s::%s(%s)', $prefix, $profile->getTemplate(), $profile->getType(), isset(self::$colors[$profile->getType()]) ? self::$colors[$profile->getType()] : 'auto', $profile->getName()); + return sprintf('%s└ %s::%s(%s)', $prefix, $profile->getTemplate(), $profile->getType(), self::$colors[$profile->getType()] ?? 'auto', $profile->getName()); } protected function formatTime(Profile $profile, $percent): string diff --git a/src/Test/NodeTestCase.php b/src/Test/NodeTestCase.php index 3b8b2c86c67..8b1bef776d3 100644 --- a/src/Test/NodeTestCase.php +++ b/src/Test/NodeTestCase.php @@ -43,7 +43,7 @@ public function assertNodeCompilation($source, Node $node, Environment $environm protected function getCompiler(Environment $environment = null) { - return new Compiler(null === $environment ? $this->getEnvironment() : $environment); + return new Compiler($environment ?? $this->getEnvironment()); } protected function getEnvironment() From 991f518d320171cd17cec6fc57d387dcd0d01160 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Sun, 8 Oct 2023 22:21:41 +0200 Subject: [PATCH 104/812] Replace calls to twig_test_iterable to is_iterable --- CHANGELOG | 4 ++-- src/Extension/CoreExtension.php | 16 +++++++++------- src/Node/WithNode.php | 2 +- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index a0b9de2b713..faa5f4a20c6 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,6 @@ -# 3.7.2 (2023-XX-XX) +# 3.8.0 (2023-XX-XX) - * n/a + * Deprecate `twig_test_iterable` function. Use the native `is_iterable` instead. # 3.7.1 (2023-08-28) diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index 63267ba6776..d049d6f4e24 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -251,7 +251,7 @@ public function getTests(): array new TwigTest('divisible by', null, ['node_class' => DivisiblebyTest::class, 'one_mandatory_argument' => true]), new TwigTest('constant', null, ['node_class' => ConstantTest::class]), new TwigTest('empty', 'twig_test_empty'), - new TwigTest('iterable', 'twig_test_iterable'), + new TwigTest('iterable', 'is_iterable'), ]; } @@ -391,7 +391,7 @@ function twig_random(Environment $env, $values = null, $max = null) } } - if (!twig_test_iterable($values)) { + if (!is_iterable($values)) { return $values; } @@ -528,7 +528,7 @@ function twig_date_converter(Environment $env, $date = null, $timezone = null) */ function twig_replace_filter($str, $from) { - if (!twig_test_iterable($from)) { + if (!is_iterable($from)) { throw new RuntimeError(sprintf('The "replace" filter expects an array or "Traversable" as replace values, got "%s".', \is_object($from) ? \get_class($from) : \gettype($from))); } @@ -625,7 +625,7 @@ function twig_array_merge(...$arrays) $result = []; foreach ($arrays as $argNumber => $array) { - if (!twig_test_iterable($array)) { + if (!is_iterable($array)) { throw new RuntimeError(sprintf('The merge filter only works with arrays or "Traversable", got "%s" for argument %d.', \gettype($array), $argNumber + 1)); } @@ -721,7 +721,7 @@ function twig_last(Environment $env, $item) */ function twig_join_filter($value, $glue = '', $and = null) { - if (!twig_test_iterable($value)) { + if (!is_iterable($value)) { $value = (array) $value; } @@ -1292,6 +1292,8 @@ function twig_test_empty($value) * @param mixed $value A variable * * @return bool true if the value is traversable + * + * @deprecated since Twig 3.8, to be removed in 4.0 (use the native "is_iterable" function instead) */ function twig_test_iterable($value) { @@ -1427,7 +1429,7 @@ function twig_constant_is_defined($constant, $object = null) */ function twig_array_batch($items, $size, $fill = null, $preserveKeys = true) { - if (!twig_test_iterable($items)) { + if (!is_iterable($items)) { throw new RuntimeError(sprintf('The "batch" filter expects an array or "Traversable", got "%s".', \is_object($items) ? \get_class($items) : \gettype($items))); } @@ -1671,7 +1673,7 @@ function twig_array_column($array, $name, $index = null): array function twig_array_filter(Environment $env, $array, $arrow) { - if (!twig_test_iterable($array)) { + if (!is_iterable($array)) { throw new RuntimeError(sprintf('The "filter" filter expects an array or "Traversable", got "%s".', \is_object($array) ? \get_class($array) : \gettype($array))); } diff --git a/src/Node/WithNode.php b/src/Node/WithNode.php index 56a334496e9..2ac9123d0d1 100644 --- a/src/Node/WithNode.php +++ b/src/Node/WithNode.php @@ -45,7 +45,7 @@ public function compile(Compiler $compiler): void ->write(sprintf('$%s = ', $varsName)) ->subcompile($node) ->raw(";\n") - ->write(sprintf("if (!twig_test_iterable(\$%s)) {\n", $varsName)) + ->write(sprintf("if (!is_iterable(\$%s)) {\n", $varsName)) ->indent() ->write("throw new RuntimeError('Variables passed to the \"with\" tag must be a hash.', ") ->repr($node->getTemplateLine()) From 6f62291c14f7acaa32d8abf0e238a11ab6c7b788 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Fri, 20 Oct 2023 17:48:30 +0200 Subject: [PATCH 105/812] Add missing docs --- doc/deprecated.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/doc/deprecated.rst b/doc/deprecated.rst index ac22338e184..b624b569628 100644 --- a/doc/deprecated.rst +++ b/doc/deprecated.rst @@ -4,3 +4,6 @@ Deprecated Features This document lists deprecated features in Twig 3.x. Deprecated features are kept for backward compatibility and removed in the next major release (a feature that was deprecated in Twig 3.x is removed in Twig 4.0). + +The `twig_test_iterable` function is deprecated; use the native `is_iterable` +instead. From fb0d7495a8e5b77ce4928b798a2e49884375821a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Sat, 21 Oct 2023 19:08:22 +0200 Subject: [PATCH 106/812] Compile Elvis operator with Elvis operator --- src/ExpressionParser.php | 3 +++ src/Node/Expression/ConditionalExpression.php | 27 ++++++++++++------- 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/src/ExpressionParser.php b/src/ExpressionParser.php index eb33868918f..13e0f0876ed 100644 --- a/src/ExpressionParser.php +++ b/src/ExpressionParser.php @@ -183,11 +183,14 @@ private function parseConditionalExpression($expr): AbstractExpression if (!$this->parser->getStream()->nextIf(/* Token::PUNCTUATION_TYPE */ 9, ':')) { $expr2 = $this->parseExpression(); if ($this->parser->getStream()->nextIf(/* Token::PUNCTUATION_TYPE */ 9, ':')) { + // Ternary operator (expr ? expr2 : expr3) $expr3 = $this->parseExpression(); } else { + // Ternary without else (expr ? expr2) $expr3 = new ConstantExpression('', $this->parser->getCurrentToken()->getLine()); } } else { + // Ternary without then (expr ?: expr3) $expr2 = $expr; $expr3 = $this->parseExpression(); } diff --git a/src/Node/Expression/ConditionalExpression.php b/src/Node/Expression/ConditionalExpression.php index 2c7bd0a276c..d7db993579c 100644 --- a/src/Node/Expression/ConditionalExpression.php +++ b/src/Node/Expression/ConditionalExpression.php @@ -23,14 +23,23 @@ public function __construct(AbstractExpression $expr1, AbstractExpression $expr2 public function compile(Compiler $compiler): void { - $compiler - ->raw('((') - ->subcompile($this->getNode('expr1')) - ->raw(') ? (') - ->subcompile($this->getNode('expr2')) - ->raw(') : (') - ->subcompile($this->getNode('expr3')) - ->raw('))') - ; + // Ternary with no then uses Elvis operator + if ($this->getNode('expr1') === $this->getNode('expr2')) { + $compiler + ->raw('((') + ->subcompile($this->getNode('expr1')) + ->raw(') ?: (') + ->subcompile($this->getNode('expr3')) + ->raw('))'); + } else { + $compiler + ->raw('((') + ->subcompile($this->getNode('expr1')) + ->raw(') ? (') + ->subcompile($this->getNode('expr2')) + ->raw(') : (') + ->subcompile($this->getNode('expr3')) + ->raw('))'); + } } } From 30b5a566250a74cecd232697d0b2b32131de34ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Sat, 21 Oct 2023 19:44:29 +0200 Subject: [PATCH 107/812] Compile starts/ends with using PHP8 functions str_starts/ends_with --- src/Node/Expression/Binary/EndsWithBinary.php | 2 +- src/Node/Expression/Binary/StartsWithBinary.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Node/Expression/Binary/EndsWithBinary.php b/src/Node/Expression/Binary/EndsWithBinary.php index c3516b853fc..73fa20b1f66 100644 --- a/src/Node/Expression/Binary/EndsWithBinary.php +++ b/src/Node/Expression/Binary/EndsWithBinary.php @@ -24,7 +24,7 @@ public function compile(Compiler $compiler): void ->subcompile($this->getNode('left')) ->raw(sprintf(') && is_string($%s = ', $right)) ->subcompile($this->getNode('right')) - ->raw(sprintf(') && (\'\' === $%2$s || $%2$s === substr($%1$s, -strlen($%2$s))))', $left, $right)) + ->raw(sprintf(') && str_ends_with($%1$s, $%2$s))', $left, $right)) ; } diff --git a/src/Node/Expression/Binary/StartsWithBinary.php b/src/Node/Expression/Binary/StartsWithBinary.php index d0df1c4b639..22eff92a794 100644 --- a/src/Node/Expression/Binary/StartsWithBinary.php +++ b/src/Node/Expression/Binary/StartsWithBinary.php @@ -24,7 +24,7 @@ public function compile(Compiler $compiler): void ->subcompile($this->getNode('left')) ->raw(sprintf(') && is_string($%s = ', $right)) ->subcompile($this->getNode('right')) - ->raw(sprintf(') && (\'\' === $%2$s || 0 === strpos($%1$s, $%2$s)))', $left, $right)) + ->raw(sprintf(') && str_starts_with($%1$s, $%2$s))', $left, $right)) ; } From 5e1838dbcace4cb23c32641f7809d22c96eb3fce Mon Sep 17 00:00:00 2001 From: Yaakov Saxon Date: Thu, 14 Sep 2023 11:57:30 -0400 Subject: [PATCH 108/812] Fix premature loop exit in Security Policy lookup of allowed methods/properties --- src/Sandbox/SecurityPolicy.php | 10 +++---- tests/Extension/SandboxTest.php | 52 +++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 6 deletions(-) diff --git a/src/Sandbox/SecurityPolicy.php b/src/Sandbox/SecurityPolicy.php index 1406e8061a9..3b79a870f83 100644 --- a/src/Sandbox/SecurityPolicy.php +++ b/src/Sandbox/SecurityPolicy.php @@ -94,9 +94,8 @@ public function checkMethodAllowed($obj, $method) $allowed = false; $method = strtr($method, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'); foreach ($this->allowedMethods as $class => $methods) { - if ($obj instanceof $class) { - $allowed = \in_array($method, $methods); - + if ($obj instanceof $class && \in_array($method, $methods)) { + $allowed = true; break; } } @@ -111,9 +110,8 @@ public function checkPropertyAllowed($obj, $property) { $allowed = false; foreach ($this->allowedProperties as $class => $properties) { - if ($obj instanceof $class) { - $allowed = \in_array($property, \is_array($properties) ? $properties : [$properties]); - + if ($obj instanceof $class && \in_array($property, \is_array($properties) ? $properties : [$properties])) { + $allowed = true; break; } } diff --git a/tests/Extension/SandboxTest.php b/tests/Extension/SandboxTest.php index e365da63280..dff2ddc8dd4 100644 --- a/tests/Extension/SandboxTest.php +++ b/tests/Extension/SandboxTest.php @@ -36,6 +36,7 @@ protected function setUp(): void 'name' => 'Fabien', 'obj' => new FooObject(), 'arr' => ['obj' => new FooObject()], + 'child_obj' => new ChildClass(), ]; self::$templates = [ @@ -56,6 +57,8 @@ protected function setUp(): void '1_range_operator' => '{{ (1..2)[0] }}', '1_syntax_error_wrapper' => '{% sandbox %}{% include "1_syntax_error" %}{% endsandbox %}', '1_syntax_error' => '{% syntax error }}', + '1_childobj_parentmethod' => '{{ child_obj.ParentMethod() }}', + '1_childobj_childmethod' => '{{ child_obj.ChildMethod() }}', ]; } @@ -410,6 +413,42 @@ public function testSandboxWithClosureFilter() $this->assertSame('foo, bar', $twig->load('index')->render([])); } + public function testMultipleClassMatchesViaInheritanceInAllowedMethods() + { + $twig_child_first = $this->getEnvironment(true, [], self::$templates, [], [], [ + 'Twig\Tests\Extension\ChildClass' => ['ChildMethod'], + 'Twig\Tests\Extension\ParentClass' => ['ParentMethod'], + ]); + $twig_parent_first = $this->getEnvironment(true, [], self::$templates, [], [], [ + 'Twig\Tests\Extension\ParentClass' => ['ParentMethod'], + 'Twig\Tests\Extension\ChildClass' => ['ChildMethod'], + ]); + + try { + $twig_child_first->load('1_childobj_childmethod')->render(self::$params); + } catch (SecurityError $e) { + $this->fail('This test case is malfunctioning as even the child class method which comes first is not being allowed.'); + } + + try { + $twig_parent_first->load('1_childobj_parentmethod')->render(self::$params); + } catch (SecurityError $e) { + $this->fail('This test case is malfunctioning as even the parent class method which comes first is not being allowed.'); + } + + try { + $twig_parent_first->load('1_childobj_childmethod')->render(self::$params); + } catch (SecurityError $e) { + $this->fail('checkMethodAllowed is exiting prematurely after matching a parent class and not seeing a method allowed on a child class later in the list'); + } + + try { + $twig_child_first->load('1_childobj_parentmethod')->render(self::$params); + } catch (SecurityError $e) { + $this->fail('checkMethodAllowed is exiting prematurely after matching a child class and not seeing a method allowed on its parent class later in the list'); + } + } + protected function getEnvironment($sandboxed, $options, $templates, $tags = [], $filters = [], $methods = [], $properties = [], $functions = []) { $loader = new ArrayLoader($templates); @@ -421,6 +460,19 @@ protected function getEnvironment($sandboxed, $options, $templates, $tags = [], } } +class ParentClass +{ + public function ParentMethod() + { + } +} +class ChildClass extends ParentClass +{ + public function ChildMethod() + { + } +} + class FooObject { public static $called = ['__toString' => 0, 'foo' => 0, 'getFooBar' => 0]; From c75762c354153dc32b0daba0bca771e1ce495264 Mon Sep 17 00:00:00 2001 From: Jeroen Versteeg Date: Mon, 5 Jun 2023 13:26:39 +0200 Subject: [PATCH 109/812] Fix IntlExtension::formatDateTime use of date formatter prototype --- extra/intl-extra/IntlExtension.php | 22 ++++++++--- extra/intl-extra/Tests/IntlExtensionTest.php | 41 +++++++++++++++++++- 2 files changed, 57 insertions(+), 6 deletions(-) diff --git a/extra/intl-extra/IntlExtension.php b/extra/intl-extra/IntlExtension.php index 955d6ec9233..f8eaa60a91e 100644 --- a/extra/intl-extra/IntlExtension.php +++ b/extra/intl-extra/IntlExtension.php @@ -370,7 +370,14 @@ public function formatNumberStyle(string $style, $number, array $attrs = [], str public function formatDateTime(Environment $env, $date, ?string $dateFormat = 'medium', ?string $timeFormat = 'medium', string $pattern = '', $timezone = null, string $calendar = 'gregorian', string $locale = null): string { $date = twig_date_converter($env, $date, $timezone); - $formatter = $this->createDateFormatter($locale, $dateFormat, $timeFormat, $pattern, $date->getTimezone(), $calendar); + + $formatterTimezone = $timezone; + if (false === $formatterTimezone) { + $formatterTimezone = $date->getTimezone(); + } elseif (\is_string($formatterTimezone)) { + $formatterTimezone = new \DateTimeZone($timezone); + } + $formatter = $this->createDateFormatter($locale, $dateFormat, $timeFormat, $pattern, $formatterTimezone, $calendar); if (false === $ret = $formatter->format($date)) { throw new RuntimeError('Unable to format the given date.'); @@ -397,7 +404,7 @@ public function formatTime(Environment $env, $date, ?string $timeFormat = 'mediu return $this->formatDateTime($env, $date, 'none', $timeFormat, $pattern, $timezone, $calendar, $locale); } - private function createDateFormatter(?string $locale, ?string $dateFormat, ?string $timeFormat, string $pattern, \DateTimeZone $timezone, string $calendar): \IntlDateFormatter + private function createDateFormatter(?string $locale, ?string $dateFormat, ?string $timeFormat, string $pattern, ?\DateTimeZone $timezone, string $calendar): \IntlDateFormatter { $dateFormats = self::availableDateFormats(); @@ -410,7 +417,10 @@ private function createDateFormatter(?string $locale, ?string $dateFormat, ?stri } if (null === $locale) { - $locale = \Locale::getDefault(); + if ($this->dateFormatterPrototype) { + $locale = $this->dateFormatterPrototype->getLocale(); + } + $locale = $locale ?: \Locale::getDefault(); } $calendar = 'gregorian' === $calendar ? \IntlDateFormatter::GREGORIAN : \IntlDateFormatter::TRADITIONAL; @@ -421,12 +431,14 @@ private function createDateFormatter(?string $locale, ?string $dateFormat, ?stri if ($this->dateFormatterPrototype) { $dateFormatValue = $dateFormatValue ?: $this->dateFormatterPrototype->getDateType(); $timeFormatValue = $timeFormatValue ?: $this->dateFormatterPrototype->getTimeType(); - $timezone = $timezone ?: $this->dateFormatterPrototype->getTimeType(); + $timezone = $timezone ?: $this->dateFormatterPrototype->getTimeZone()->toDateTimeZone(); $calendar = $calendar ?: $this->dateFormatterPrototype->getCalendar(); $pattern = $pattern ?: $this->dateFormatterPrototype->getPattern(); } - $hash = $locale.'|'.$dateFormatValue.'|'.$timeFormatValue.'|'.$timezone->getName().'|'.$calendar.'|'.$pattern; + $timezoneName = $timezone ? $timezone->getName() : '(none)'; + + $hash = $locale.'|'.$dateFormatValue.'|'.$timeFormatValue.'|'.$timezoneName.'|'.$calendar.'|'.$pattern; if (!isset($this->dateFormatters[$hash])) { $this->dateFormatters[$hash] = new \IntlDateFormatter($locale, $dateFormatValue, $timeFormatValue, $timezone, $calendar, $pattern); diff --git a/extra/intl-extra/Tests/IntlExtensionTest.php b/extra/intl-extra/Tests/IntlExtensionTest.php index f2637620693..a05f796f914 100644 --- a/extra/intl-extra/Tests/IntlExtensionTest.php +++ b/extra/intl-extra/Tests/IntlExtensionTest.php @@ -12,17 +12,56 @@ namespace Twig\Extra\Intl\Tests; use PHPUnit\Framework\TestCase; +use Twig\Environment; use Twig\Extra\Intl\IntlExtension; +use Twig\Loader\ArrayLoader; class IntlExtensionTest extends TestCase { + public function testFormatterWithoutProto() + { + $ext = new IntlExtension(); + $env = new Environment(new ArrayLoader()); + + $this->assertSame('12.346', $ext->formatNumber('12.3456')); + $this->assertSame( + 'Feb 20, 2020, 1:37:00 PM', + $ext->formatDateTime($env, new \DateTime('2020-02-20T13:37:00+00:00')) + ); + } + public function testFormatterProto() { - $dateFormatterProto = new \IntlDateFormatter('fr', \IntlDateFormatter::FULL, \IntlDateFormatter::FULL); + $dateFormatterProto = new \IntlDateFormatter('fr', \IntlDateFormatter::FULL, \IntlDateFormatter::FULL, new \DateTimeZone('Europe/Paris')); $numberFormatterProto = new \NumberFormatter('fr', \NumberFormatter::DECIMAL); $numberFormatterProto->setTextAttribute(\NumberFormatter::POSITIVE_PREFIX, '++'); $numberFormatterProto->setAttribute(\NumberFormatter::FRACTION_DIGITS, 1); $ext = new IntlExtension($dateFormatterProto, $numberFormatterProto); + $env = new Environment(new ArrayLoader()); + $this->assertSame('++12,3', $ext->formatNumber('12.3456')); + $this->assertSame( + 'jeudi 20 février 2020 à 14:37:00 heure normale d’Europe centrale', + $ext->formatDateTime($env, new \DateTime('2020-02-20T13:37:00+00:00')) + ); + } + + public function testFormatterOverridenProto() + { + $dateFormatterProto = new \IntlDateFormatter('fr', \IntlDateFormatter::FULL, \IntlDateFormatter::FULL, new \DateTimeZone('Europe/Paris')); + $numberFormatterProto = new \NumberFormatter('fr', \NumberFormatter::DECIMAL); + $numberFormatterProto->setTextAttribute(\NumberFormatter::POSITIVE_PREFIX, '++'); + $numberFormatterProto->setAttribute(\NumberFormatter::FRACTION_DIGITS, 1); + $ext = new IntlExtension($dateFormatterProto, $numberFormatterProto); + $env = new Environment(new ArrayLoader()); + + $this->assertSame( + 'twelve point three', + $ext->formatNumber('12.3456', [], 'spellout', 'default', 'en_US') + ); + $this->assertSame( + '2020-02-20 13:37:00', + $ext->formatDateTime($env, new \DateTime('2020-02-20T13:37:00+00:00'), 'short', 'short', 'yyyy-MM-dd HH:mm:ss', 'UTC', 'gregorian', 'en_US') + ); } } From 85bf01b4abd4b4ee6f6d1aca19af74189c939d69 Mon Sep 17 00:00:00 2001 From: Drew Richards Date: Fri, 7 Apr 2023 13:39:09 -0400 Subject: [PATCH 110/812] Catch errors thrown during template rendering --- src/Error/Error.php | 2 +- src/Template.php | 4 ++-- tests/ErrorTest.php | 22 ++++++++++++++++++++++ 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/src/Error/Error.php b/src/Error/Error.php index a68be65f203..9a3f9df2d20 100644 --- a/src/Error/Error.php +++ b/src/Error/Error.php @@ -53,7 +53,7 @@ class Error extends \Exception * @param int $lineno The template line where the error occurred * @param Source|null $source The source context where the error occurred */ - public function __construct(string $message, int $lineno = -1, Source $source = null, \Exception $previous = null) + public function __construct(string $message, int $lineno = -1, Source $source = null, \Throwable $previous = null) { parent::__construct('', 0, $previous); diff --git a/src/Template.php b/src/Template.php index e04bd04a63e..ffbaae1ea1c 100644 --- a/src/Template.php +++ b/src/Template.php @@ -181,7 +181,7 @@ public function displayBlock($name, array $context, array $blocks = [], $useBloc } throw $e; - } catch (\Exception $e) { + } catch (\Throwable $e) { $e = new RuntimeError(sprintf('An exception has been thrown during the rendering of a template ("%s").', $e->getMessage()), -1, $template->getSourceContext(), $e); $e->guess(); @@ -404,7 +404,7 @@ protected function displayWithErrorHandling(array $context, array $blocks = []) } throw $e; - } catch (\Exception $e) { + } catch (\Throwable $e) { $e = new RuntimeError(sprintf('An exception has been thrown during the rendering of a template ("%s").', $e->getMessage()), -1, $this->getSourceContext(), $e); $e->guess(); diff --git a/tests/ErrorTest.php b/tests/ErrorTest.php index 7892be9d07b..db6418ed685 100644 --- a/tests/ErrorTest.php +++ b/tests/ErrorTest.php @@ -234,6 +234,28 @@ public function testTwigArrayReduceThrowsRuntimeExceptions() } } + public function testTwigArgumentCountErrorThrowsRuntimeExceptions() + { + $loader = new ArrayLoader([ + 'argument-error.html' => << true, 'cache' => false]); + + $template = $twig->load('argument-error.html'); + try { + $template->render(); + + $this->fail(); + } catch (RuntimeError $e) { + $this->assertEquals(2, $e->getTemplateLine()); + $this->assertEquals('argument-error.html', $e->getSourceContext()->getName()); + } + } + public function getErroredTemplates() { return [ From 62732646c87fc5aa10ffb3e58228d7a466c47474 Mon Sep 17 00:00:00 2001 From: Yaakov Saxon Date: Fri, 27 Oct 2023 12:15:56 -0400 Subject: [PATCH 111/812] Minor rename of SandboxTest functions->methods --- tests/Extension/SandboxTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Extension/SandboxTest.php b/tests/Extension/SandboxTest.php index dff2ddc8dd4..2e40a825da3 100644 --- a/tests/Extension/SandboxTest.php +++ b/tests/Extension/SandboxTest.php @@ -323,7 +323,7 @@ public function testSandboxAllowRangeOperator() $this->assertEquals('1', $twig->load('1_range_operator')->render(self::$params), 'Sandbox allow the range operator'); } - public function testSandboxAllowFunctionsCaseInsensitive() + public function testSandboxAllowMethodsCaseInsensitive() { foreach (['getfoobar', 'getFoobar', 'getFooBar'] as $name) { $twig = $this->getEnvironment(true, [], self::$templates, [], [], ['Twig\Tests\Extension\FooObject' => $name]); From 144c4dac7f3a56bf6ecddfb13f4a19f9a2482c10 Mon Sep 17 00:00:00 2001 From: Jonas Elfering Date: Fri, 3 Nov 2023 15:14:20 +0100 Subject: [PATCH 112/812] Fix timezone fallback to CoreExtension in IntlExtension This is probably a regression from #3844 --- extra/intl-extra/IntlExtension.php | 2 +- extra/intl-extra/Tests/IntlExtensionTest.php | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/extra/intl-extra/IntlExtension.php b/extra/intl-extra/IntlExtension.php index 3f3c600c487..142d25e10c5 100644 --- a/extra/intl-extra/IntlExtension.php +++ b/extra/intl-extra/IntlExtension.php @@ -371,7 +371,7 @@ public function formatDateTime(Environment $env, $date, ?string $dateFormat = 'm $date = twig_date_converter($env, $date, $timezone); $formatterTimezone = $timezone; - if (false === $formatterTimezone) { + if (null === $formatterTimezone) { $formatterTimezone = $date->getTimezone(); } elseif (\is_string($formatterTimezone)) { $formatterTimezone = new \DateTimeZone($timezone); diff --git a/extra/intl-extra/Tests/IntlExtensionTest.php b/extra/intl-extra/Tests/IntlExtensionTest.php index a05f796f914..6afa7036b75 100644 --- a/extra/intl-extra/Tests/IntlExtensionTest.php +++ b/extra/intl-extra/Tests/IntlExtensionTest.php @@ -13,6 +13,7 @@ use PHPUnit\Framework\TestCase; use Twig\Environment; +use Twig\Extension\CoreExtension; use Twig\Extra\Intl\IntlExtension; use Twig\Loader\ArrayLoader; @@ -30,6 +31,20 @@ public function testFormatterWithoutProto() ); } + public function testFormatterWithoutProtoFallsBackToCoreExtensionTimezone() + { + $ext = new IntlExtension(); + $env = new Environment(new ArrayLoader()); + // EET is always +2 without changes for daylight saving time + // so it has a fixed difference to UTC + $env->getExtension(CoreExtension::class)->setTimezone('EET'); + + $this->assertSame( + 'Feb 20, 2020, 3:37:00 PM', + $ext->formatDateTime($env, new \DateTime('2020-02-20T13:37:00+00:00', new \DateTimeZone('UTC'))) + ); + } + public function testFormatterProto() { $dateFormatterProto = new \IntlDateFormatter('fr', \IntlDateFormatter::FULL, \IntlDateFormatter::FULL, new \DateTimeZone('Europe/Paris')); From 4be326aeeb73b82005eaf3fa3e475645a4cd9935 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Tue, 21 Nov 2023 14:57:52 +0100 Subject: [PATCH 113/812] Fix tests and CS --- src/Node/ModuleNode.php | 18 +++++++++--------- tests/Node/ModuleTest.php | 24 ++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 9 deletions(-) diff --git a/src/Node/ModuleNode.php b/src/Node/ModuleNode.php index 0f4df35410f..9b485eeaf03 100644 --- a/src/Node/ModuleNode.php +++ b/src/Node/ModuleNode.php @@ -355,9 +355,9 @@ protected function compileMacros(Compiler $compiler) protected function compileGetTemplateName(Compiler $compiler) { $compiler - ->write("/**") - ->write(" * @codeCoverageIgnore") - ->write(" */") + ->write("/**\n") + ->write(" * @codeCoverageIgnore\n") + ->write(" */\n") ->write("public function getTemplateName()\n", "{\n") ->indent() ->write('return ') @@ -412,9 +412,9 @@ protected function compileIsTraitable(Compiler $compiler) } $compiler - ->write("/**") - ->write(" * @codeCoverageIgnore") - ->write(" */") + ->write("/**\n") + ->write(" * @codeCoverageIgnore\n") + ->write(" */\n") ->write("public function isTraitable()\n", "{\n") ->indent() ->write(sprintf("return %s;\n", $traitable ? 'true' : 'false')) @@ -426,9 +426,9 @@ protected function compileIsTraitable(Compiler $compiler) protected function compileDebugInfo(Compiler $compiler) { $compiler - ->write("/**") - ->write(" * @codeCoverageIgnore") - ->write(" */") + ->write("/**\n") + ->write(" * @codeCoverageIgnore\n") + ->write(" */\n") ->write("public function getDebugInfo()\n", "{\n") ->indent() ->write(sprintf("return %s;\n", str_replace("\n", '', var_export(array_reverse($compiler->getDebugInfo(), true), true)))) diff --git a/tests/Node/ModuleTest.php b/tests/Node/ModuleTest.php index c036e3a71c1..938fa2ea7a2 100644 --- a/tests/Node/ModuleTest.php +++ b/tests/Node/ModuleTest.php @@ -97,11 +97,17 @@ protected function doDisplay(array \$context, array \$blocks = []) echo "foo"; } + /** + * @codeCoverageIgnore + */ public function getTemplateName() { return "foo.twig"; } + /** + * @codeCoverageIgnore + */ public function getDebugInfo() { return array ( 37 => 1,); @@ -168,16 +174,25 @@ protected function doDisplay(array \$context, array \$blocks = []) \$this->parent->display(\$context, array_merge(\$this->blocks, \$blocks)); } + /** + * @codeCoverageIgnore + */ public function getTemplateName() { return "foo.twig"; } + /** + * @codeCoverageIgnore + */ public function isTraitable() { return false; } + /** + * @codeCoverageIgnore + */ public function getDebugInfo() { return array ( 43 => 1, 41 => 2, 34 => 1,); @@ -248,16 +263,25 @@ protected function doDisplay(array \$context, array \$blocks = []) \$this->getParent(\$context)->display(\$context, array_merge(\$this->blocks, \$blocks)); } + /** + * @codeCoverageIgnore + */ public function getTemplateName() { return "foo.twig"; } + /** + * @codeCoverageIgnore + */ public function isTraitable() { return false; } + /** + * @codeCoverageIgnore + */ public function getDebugInfo() { return array ( 43 => 2, 41 => 4, 34 => 2,); From fdb9d9eddfbdd818c589d38da06e54ba33fdf0ab Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Tue, 21 Nov 2023 10:09:29 +0100 Subject: [PATCH 114/812] Bump dependencies --- extra/cache-extra/composer.json | 4 ++-- extra/cssinliner-extra/composer.json | 6 +++--- extra/html-extra/composer.json | 6 +++--- extra/inky-extra/composer.json | 6 +++--- extra/intl-extra/composer.json | 6 +++--- extra/markdown-extra/composer.json | 6 +++--- extra/string-extra/composer.json | 4 ++-- extra/twig-extra-bundle/composer.json | 4 ++-- 8 files changed, 21 insertions(+), 21 deletions(-) diff --git a/extra/cache-extra/composer.json b/extra/cache-extra/composer.json index b34e3142e26..04177a419de 100644 --- a/extra/cache-extra/composer.json +++ b/extra/cache-extra/composer.json @@ -17,10 +17,10 @@ "require": { "php": ">=7.2.5", "symfony/cache": "^5.0|^6.0|^7.0", - "twig/twig": "^2.4|^3.0" + "twig/twig": "^3.0" }, "require-dev": { - "symfony/phpunit-bridge": "^4.4.9|^5.0.9|^6.0|^7.0" + "symfony/phpunit-bridge": "^6.4|^7.0" }, "autoload": { "psr-4" : { "Twig\\Extra\\Cache\\" : "" }, diff --git a/extra/cssinliner-extra/composer.json b/extra/cssinliner-extra/composer.json index a1a6af7a97b..32be7b44de4 100644 --- a/extra/cssinliner-extra/composer.json +++ b/extra/cssinliner-extra/composer.json @@ -15,12 +15,12 @@ } ], "require": { - "php": ">=7.1.3", + "php": ">=7.2.5", "tijsverkoyen/css-to-inline-styles": "^2.0", - "twig/twig": "^2.7|^3.0" + "twig/twig": "^3.0" }, "require-dev": { - "symfony/phpunit-bridge": "^5.4|^6.3|^7.0" + "symfony/phpunit-bridge": "^6.4|^7.0" }, "autoload": { "psr-4" : { "Twig\\Extra\\CssInliner\\" : "" }, diff --git a/extra/html-extra/composer.json b/extra/html-extra/composer.json index 98670abfacc..e67e53e5a82 100644 --- a/extra/html-extra/composer.json +++ b/extra/html-extra/composer.json @@ -15,12 +15,12 @@ } ], "require": { - "php": ">=7.1.3", + "php": ">=7.2.5", "symfony/mime": "^5.4|^6.0|^7.0", - "twig/twig": "^2.7|^3.0" + "twig/twig": "^3.0" }, "require-dev": { - "symfony/phpunit-bridge": "^5.4|^6.3|^7.0" + "symfony/phpunit-bridge": "^6.4|^7.0" }, "autoload": { "psr-4" : { "Twig\\Extra\\Html\\" : "" }, diff --git a/extra/inky-extra/composer.json b/extra/inky-extra/composer.json index c4e42ea7fcd..2cb5a5ad308 100644 --- a/extra/inky-extra/composer.json +++ b/extra/inky-extra/composer.json @@ -15,12 +15,12 @@ } ], "require": { - "php": ">=7.1.3", + "php": ">=7.2.5", "lorenzo/pinky": "^1.0.5", - "twig/twig": "^2.7|^3.0" + "twig/twig": "^3.0" }, "require-dev": { - "symfony/phpunit-bridge": "^5.4|^6.3|^7.0" + "symfony/phpunit-bridge": "^6.4|^7.0" }, "autoload": { "psr-4" : { "Twig\\Extra\\Inky\\" : "" }, diff --git a/extra/intl-extra/composer.json b/extra/intl-extra/composer.json index e670bea3e3e..208347077a2 100644 --- a/extra/intl-extra/composer.json +++ b/extra/intl-extra/composer.json @@ -15,12 +15,12 @@ } ], "require": { - "php": ">=7.1.3", - "twig/twig": "^2.7|^3.0", + "php": ">=7.2.5", + "twig/twig": "^3.0", "symfony/intl": "^5.4|^6.0|^7.0" }, "require-dev": { - "symfony/phpunit-bridge": "^5.4|^6.3|^7.0" + "symfony/phpunit-bridge": "^6.4|^7.0" }, "autoload": { "psr-4" : { "Twig\\Extra\\Intl\\" : "" }, diff --git a/extra/markdown-extra/composer.json b/extra/markdown-extra/composer.json index e278b955faa..52dc07d9ec2 100644 --- a/extra/markdown-extra/composer.json +++ b/extra/markdown-extra/composer.json @@ -15,11 +15,11 @@ } ], "require": { - "php": ">=7.1.3", - "twig/twig": "^2.7|^3.0" + "php": ">=7.2.5", + "twig/twig": "^3.0" }, "require-dev": { - "symfony/phpunit-bridge": "^5.4|^6.3|^7.0", + "symfony/phpunit-bridge": "^6.4|^7.0", "erusev/parsedown": "^1.7", "league/commonmark": "^1.0|^2.0", "league/html-to-markdown": "^4.8|^5.0", diff --git a/extra/string-extra/composer.json b/extra/string-extra/composer.json index ddd16c66f72..77050a11785 100644 --- a/extra/string-extra/composer.json +++ b/extra/string-extra/composer.json @@ -18,10 +18,10 @@ "php": ">=7.2.5", "symfony/string": "^5.4|^6.0|^7.0", "symfony/translation-contracts": "^1.1|^2|^3", - "twig/twig": "^2.7|^3.0" + "twig/twig": "^3.0" }, "require-dev": { - "symfony/phpunit-bridge": "^5.4|^6.3|^7.0" + "symfony/phpunit-bridge": "^6.4|^7.0" }, "autoload": { "psr-4" : { "Twig\\Extra\\String\\" : "" }, diff --git a/extra/twig-extra-bundle/composer.json b/extra/twig-extra-bundle/composer.json index 942f6ae4c52..cbdbd4b8ab0 100644 --- a/extra/twig-extra-bundle/composer.json +++ b/extra/twig-extra-bundle/composer.json @@ -18,11 +18,11 @@ "php": ">=7.2.5", "symfony/framework-bundle": "^5.4|^6.0|^7.0", "symfony/twig-bundle": "^5.4|^6.0|^7.0", - "twig/twig": "^2.7|^3.0" + "twig/twig": "^3.0" }, "require-dev": { "league/commonmark": "^1.0|^2.0", - "symfony/phpunit-bridge": "^5.4|^6.3|^7.0", + "symfony/phpunit-bridge": "^6.4|^7.0", "twig/cache-extra": "^3.0", "twig/cssinliner-extra": "^2.12|^3.0", "twig/html-extra": "^2.12|^3.0", From a04cc88ad745017cb9e1deeec07eef180b4091b0 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Tue, 21 Nov 2023 16:47:02 +0100 Subject: [PATCH 115/812] Add PHP 8.3 to the CI --- .github/workflows/ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9286b499b5b..909e97dd62b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,6 +29,7 @@ jobs: - '8.0' - '8.1' - '8.2' + - '8.3' experimental: [false] steps: @@ -75,6 +76,7 @@ jobs: - '8.0' - '8.1' - '8.2' + - '8.3' extension: - 'extra/cache-extra' - 'extra/cssinliner-extra' From a41a0e7fa53d6cbf86bd7eccb16307f5bc62aa7c Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Tue, 21 Nov 2023 18:34:15 +0100 Subject: [PATCH 116/812] Update CHANGELOG --- CHANGELOG | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index b809182505b..0d3548a86cb 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,8 +1,9 @@ **End of maintainance of version 2 is scheduled for end of December 2023** -# 2.15.6 (2023-XX-XX) +# 2.15.6 (2023-21-11) - * n/a + * Add return type for Symfony 7 compatibility + * Fix premature loop exit in Security Policy lookup of allowed methods/properties # 2.15.5 (2023-05-03) From ad637405a828601a56f32ccab9a85541c4b66c9d Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Tue, 21 Nov 2023 18:34:48 +0100 Subject: [PATCH 117/812] Prepare the 2.15.6 release --- src/Environment.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Environment.php b/src/Environment.php index e6c27a4bc0a..1b1f49e9d25 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -38,12 +38,12 @@ */ class Environment { - public const VERSION = '2.15.6-DEV'; + public const VERSION = '2.15.6'; public const VERSION_ID = 21506; public const MAJOR_VERSION = 2; public const MINOR_VERSION = 15; public const RELEASE_VERSION = 6; - public const EXTRA_VERSION = 'DEV'; + public const EXTRA_VERSION = ''; private $charset; private $loader; From b65ccdfb7d91723b08ced1bb3809c84864f83e03 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Tue, 21 Nov 2023 19:53:58 +0100 Subject: [PATCH 118/812] Update CHANGELOG --- CHANGELOG | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index faa5f4a20c6..535c1a521eb 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,11 @@ # 3.8.0 (2023-XX-XX) + * Catch errors thrown during template rendering + * Fix IntlExtension::formatDateTime use of date formatter prototype + * Fix premature loop exit in Security Policy lookup of allowed methods/properties + * Remove NumberFormatter::TYPE_CURRENCY (deprecated in PHP 8.3) + * Restore return type annotations + * Allow Symfony 7 packages to be installed * Deprecate `twig_test_iterable` function. Use the native `is_iterable` instead. # 3.7.1 (2023-08-28) From 9d15f0ac07f44dc4217883ec6ae02fd555c6f71d Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Tue, 21 Nov 2023 19:54:41 +0100 Subject: [PATCH 119/812] Prepare the 3.8.0 release --- CHANGELOG | 2 +- src/Environment.php | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 535c1a521eb..2b8341fd846 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,4 +1,4 @@ -# 3.8.0 (2023-XX-XX) +# 3.8.0 (2023-11-21) * Catch errors thrown during template rendering * Fix IntlExtension::formatDateTime use of date formatter prototype diff --git a/src/Environment.php b/src/Environment.php index 1b37e35edd6..d7d51cdb1c5 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -40,12 +40,12 @@ */ class Environment { - public const VERSION = '3.7.2-DEV'; - public const VERSION_ID = 30702; + public const VERSION = '3.8.0'; + public const VERSION_ID = 30800; public const MAJOR_VERSION = 3; - public const MINOR_VERSION = 7; - public const RELEASE_VERSION = 2; - public const EXTRA_VERSION = 'DEV'; + public const MINOR_VERSION = 8; + public const RELEASE_VERSION = 0; + public const EXTRA_VERSION = ''; private $charset; private $loader; From aeeec9a5e907a79e50a6bb78979154599401726e Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Tue, 21 Nov 2023 19:55:42 +0100 Subject: [PATCH 120/812] Bump version --- CHANGELOG | 4 ++++ src/Environment.php | 8 ++++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 2b8341fd846..087fd408090 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +# 3.8.1 (2023-XX-XX) + + * n/a + # 3.8.0 (2023-11-21) * Catch errors thrown during template rendering diff --git a/src/Environment.php b/src/Environment.php index d7d51cdb1c5..5329b143ee2 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -40,12 +40,12 @@ */ class Environment { - public const VERSION = '3.8.0'; - public const VERSION_ID = 30800; + public const VERSION = '3.8.1-DEV'; + public const VERSION_ID = 30801; public const MAJOR_VERSION = 3; public const MINOR_VERSION = 8; - public const RELEASE_VERSION = 0; - public const EXTRA_VERSION = ''; + public const RELEASE_VERSION = 1; + public const EXTRA_VERSION = 'DEV'; private $charset; private $loader; From ed2cfbd6f09dcba718cc7f235c240b15fd76042c Mon Sep 17 00:00:00 2001 From: Ca-Jou Date: Wed, 6 Dec 2023 14:54:04 +0100 Subject: [PATCH 121/812] update Blackfire documentation URL --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index f741105e350..7b3db2d237b 100644 --- a/README.rst +++ b/README.rst @@ -14,7 +14,7 @@ Sponsors .. raw:: html - + Blackfire.io From d602a5560369e3f7d1fe953e519dc3af5586b6a6 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sat, 9 Dec 2023 17:48:07 +0100 Subject: [PATCH 122/812] Tweak deprecated docs --- doc/deprecated.rst | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/doc/deprecated.rst b/doc/deprecated.rst index b624b569628..a5771ef4ec1 100644 --- a/doc/deprecated.rst +++ b/doc/deprecated.rst @@ -5,5 +5,8 @@ This document lists deprecated features in Twig 3.x. Deprecated features are kept for backward compatibility and removed in the next major release (a feature that was deprecated in Twig 3.x is removed in Twig 4.0). -The `twig_test_iterable` function is deprecated; use the native `is_iterable` -instead. +Functions +--------- + + * The `twig_test_iterable` function is deprecated; use the native + `is_iterable` instead. From 16abb69d12fa9ff5389b484db8134a9f3ddd7fa3 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sun, 8 Oct 2023 09:41:25 +0200 Subject: [PATCH 123/812] Deprecate internal extension functions in favor of methods on the extension classes --- CHANGELOG | 4 +++- composer.json | 1 + doc/deprecated.rst | 7 +++++++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 14ba7576b17..17caf366e03 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,7 +1,9 @@ -# 3.8.1 (2023-XX-XX) +# 3.9.0 (2023-XX-XX) * Add return type for Symfony 7 compatibility * Fix premature loop exit in Security Policy lookup of allowed methods/properties + * Deprecate all internal extension functions in favor of methods on the extension classes + * Mark all extension functions as @internal # 3.8.0 (2023-11-21) diff --git a/composer.json b/composer.json index 1b1726fe882..31a248c2bbf 100644 --- a/composer.json +++ b/composer.json @@ -26,6 +26,7 @@ "require": { "php": ">=7.2.5", "symfony/polyfill-php80": "^1.22", + "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-mbstring": "^1.3", "symfony/polyfill-ctype": "^1.8" }, diff --git a/doc/deprecated.rst b/doc/deprecated.rst index a5771ef4ec1..948dfb3ef43 100644 --- a/doc/deprecated.rst +++ b/doc/deprecated.rst @@ -10,3 +10,10 @@ Functions * The `twig_test_iterable` function is deprecated; use the native `is_iterable` instead. + +Extensions +---------- + +* All functions defined in Twig extensions are marked as internal as of Twig + 3.9.0, and will be removed in Twig 4.0. They have been replaced by internal + methods on their respective extension classes. From 6a18cda5aaad6fff27ebe32c6f753358837a4faa Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sat, 9 Dec 2023 18:04:05 +0100 Subject: [PATCH 124/812] Move functions for HtmlExtension --- extra/html-extra/HtmlExtension.php | 54 ++++++++++--------- extra/html-extra/Resources/functions.php | 23 ++++++++ .../html-extra/Tests/LegacyFunctionsTest.php | 26 +++++++++ extra/html-extra/composer.json | 2 + 4 files changed, 79 insertions(+), 26 deletions(-) create mode 100644 extra/html-extra/Resources/functions.php create mode 100644 extra/html-extra/Tests/LegacyFunctionsTest.php diff --git a/extra/html-extra/HtmlExtension.php b/extra/html-extra/HtmlExtension.php index ed740b47187..d5842bf500c 100644 --- a/extra/html-extra/HtmlExtension.php +++ b/extra/html-extra/HtmlExtension.php @@ -9,8 +9,10 @@ * file that was distributed with this source code. */ -namespace Twig\Extra\Html { +namespace Twig\Extra\Html; + use Symfony\Component\Mime\MimeTypes; +use Twig\Error\RuntimeError; use Twig\Extension\AbstractExtension; use Twig\TwigFilter; use Twig\TwigFunction; @@ -34,7 +36,7 @@ public function getFilters(): array public function getFunctions(): array { return [ - new TwigFunction('html_classes', 'twig_html_classes'), + new TwigFunction('html_classes', [self::class, 'htmlClasses']), ]; } @@ -45,6 +47,8 @@ public function getFunctions(): array * be done before calling this filter. * * @return string The generated data URI + * + * @internal */ public function dataUri(string $data, string $mime = null, array $parameters = []): string { @@ -79,33 +83,31 @@ public function dataUri(string $data, string $mime = null, array $parameters = [ return $repr; } -} -} - -namespace { -use Twig\Error\RuntimeError; -function twig_html_classes(...$args): string -{ - $classes = []; - foreach ($args as $i => $arg) { - if (\is_string($arg)) { - $classes[] = $arg; - } elseif (\is_array($arg)) { - foreach ($arg as $class => $condition) { - if (!\is_string($class)) { - throw new RuntimeError(sprintf('The html_classes function argument %d (key %d) should be a string, got "%s".', $i, $class, \gettype($class))); - } - if (!$condition) { - continue; + /** + * @internal + */ + public static function htmlClasses(...$args): string + { + $classes = []; + foreach ($args as $i => $arg) { + if (\is_string($arg)) { + $classes[] = $arg; + } elseif (\is_array($arg)) { + foreach ($arg as $class => $condition) { + if (!\is_string($class)) { + throw new RuntimeError(sprintf('The html_classes function argument %d (key %d) should be a string, got "%s".', $i, $class, \gettype($class))); + } + if (!$condition) { + continue; + } + $classes[] = $class; } - $classes[] = $class; + } else { + throw new RuntimeError(sprintf('The html_classes function argument %d should be either a string or an array, got "%s".', $i, \gettype($arg))); } - } else { - throw new RuntimeError(sprintf('The html_classes function argument %d should be either a string or an array, got "%s".', $i, \gettype($arg))); } - } - return implode(' ', array_unique($classes)); -} + return implode(' ', array_unique($classes)); + } } diff --git a/extra/html-extra/Resources/functions.php b/extra/html-extra/Resources/functions.php new file mode 100644 index 00000000000..4458f3bf8b7 --- /dev/null +++ b/extra/html-extra/Resources/functions.php @@ -0,0 +1,23 @@ +assertSame(HtmlExtension::htmlClasses(['charset' => 'utf-8']), \twig_html_classes(['charset' => 'utf-8'])); + } +} diff --git a/extra/html-extra/composer.json b/extra/html-extra/composer.json index e67e53e5a82..ca7b2f62cf4 100644 --- a/extra/html-extra/composer.json +++ b/extra/html-extra/composer.json @@ -16,6 +16,7 @@ ], "require": { "php": ">=7.2.5", + "symfony/deprecation-contracts": "^2.5|^3", "symfony/mime": "^5.4|^6.0|^7.0", "twig/twig": "^3.0" }, @@ -23,6 +24,7 @@ "symfony/phpunit-bridge": "^6.4|^7.0" }, "autoload": { + "files": [ "Resources/functions.php" ], "psr-4" : { "Twig\\Extra\\Html\\" : "" }, "exclude-from-classmap": [ "/Tests/" From bff189f19adcca9329084ae7fb482456170ed785 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sat, 9 Dec 2023 17:59:49 +0100 Subject: [PATCH 125/812] Move functions for CssInlinerExtension --- .../cssinliner-extra/CssInlinerExtension.php | 21 ++++++++------- .../cssinliner-extra/Resources/functions.php | 23 ++++++++++++++++ .../Tests/LegacyFunctionsTest.php | 27 +++++++++++++++++++ extra/cssinliner-extra/composer.json | 2 ++ 4 files changed, 64 insertions(+), 9 deletions(-) create mode 100644 extra/cssinliner-extra/Resources/functions.php create mode 100644 extra/cssinliner-extra/Tests/LegacyFunctionsTest.php diff --git a/extra/cssinliner-extra/CssInlinerExtension.php b/extra/cssinliner-extra/CssInlinerExtension.php index 4d8b75a5623..679e9165716 100644 --- a/extra/cssinliner-extra/CssInlinerExtension.php +++ b/extra/cssinliner-extra/CssInlinerExtension.php @@ -20,17 +20,20 @@ class CssInlinerExtension extends AbstractExtension public function getFilters() { return [ - new TwigFilter('inline_css', 'Twig\\Extra\\CssInliner\\twig_inline_css', ['is_safe' => ['all']]), + new TwigFilter('inline_css', [self::class, 'inlineCss'], ['is_safe' => ['all']]), ]; } -} -function twig_inline_css(string $body, string ...$css): string -{ - static $inliner; - if (null === $inliner) { - $inliner = new CssToInlineStyles(); + /** + * @internal + */ + public static function inlineCss(string $body, string ...$css): string + { + static $inliner; + if (null === $inliner) { + $inliner = new CssToInlineStyles(); + } + + return $inliner->convert($body, implode("\n", $css)); } - - return $inliner->convert($body, implode("\n", $css)); } diff --git a/extra/cssinliner-extra/Resources/functions.php b/extra/cssinliner-extra/Resources/functions.php new file mode 100644 index 00000000000..c4b32db2d95 --- /dev/null +++ b/extra/cssinliner-extra/Resources/functions.php @@ -0,0 +1,23 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Twig\Extra\CssInliner; + +/** + * @internal + * @deprecated since Twig 3.9.0 + */ +function twig_inline_css(string $body, string ...$css): string +{ + trigger_deprecation('twig/cssinliner-extra', '3.9.0', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + return CssInlinerExtension::inlineCss($body, ...$css); +} diff --git a/extra/cssinliner-extra/Tests/LegacyFunctionsTest.php b/extra/cssinliner-extra/Tests/LegacyFunctionsTest.php new file mode 100644 index 00000000000..20feef172c9 --- /dev/null +++ b/extra/cssinliner-extra/Tests/LegacyFunctionsTest.php @@ -0,0 +1,27 @@ +assertSame(CssInlinerExtension::inlineCss('

body

', 'p { color: red }'), twig_inline_css('

body

', 'p { color: red }')); + } +} diff --git a/extra/cssinliner-extra/composer.json b/extra/cssinliner-extra/composer.json index 32be7b44de4..0a279c61323 100644 --- a/extra/cssinliner-extra/composer.json +++ b/extra/cssinliner-extra/composer.json @@ -16,6 +16,7 @@ ], "require": { "php": ">=7.2.5", + "symfony/deprecation-contracts": "^2.5|^3", "tijsverkoyen/css-to-inline-styles": "^2.0", "twig/twig": "^3.0" }, @@ -23,6 +24,7 @@ "symfony/phpunit-bridge": "^6.4|^7.0" }, "autoload": { + "files": [ "Resources/functions.php" ], "psr-4" : { "Twig\\Extra\\CssInliner\\" : "" }, "exclude-from-classmap": [ "/Tests/" From e07e9b5be3cbcf924b3553d9e5976e26a6f39389 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sat, 9 Dec 2023 18:06:59 +0100 Subject: [PATCH 126/812] Move functions for InkyExtension --- extra/inky-extra/InkyExtension.php | 13 +++++---- extra/inky-extra/Resources/functions.php | 23 ++++++++++++++++ .../inky-extra/Tests/LegacyFunctionsTest.php | 27 +++++++++++++++++++ extra/inky-extra/composer.json | 2 ++ 4 files changed, 60 insertions(+), 5 deletions(-) create mode 100644 extra/inky-extra/Resources/functions.php create mode 100644 extra/inky-extra/Tests/LegacyFunctionsTest.php diff --git a/extra/inky-extra/InkyExtension.php b/extra/inky-extra/InkyExtension.php index 1ee2b515660..374cb7efbc5 100644 --- a/extra/inky-extra/InkyExtension.php +++ b/extra/inky-extra/InkyExtension.php @@ -20,12 +20,15 @@ class InkyExtension extends AbstractExtension public function getFilters() { return [ - new TwigFilter('inky_to_html', 'Twig\\Extra\\Inky\\twig_inky', ['is_safe' => ['html']]), + new TwigFilter('inky_to_html', [self::class, 'inky'], ['is_safe' => ['html']]), ]; } -} -function twig_inky(string $body): string -{ - return false === ($html = Pinky\transformString($body)->saveHTML()) ? '' : $html; + /** + * @internal + */ + public static function inky(string $body): string + { + return false === ($html = Pinky\transformString($body)->saveHTML()) ? '' : $html; + } } diff --git a/extra/inky-extra/Resources/functions.php b/extra/inky-extra/Resources/functions.php new file mode 100644 index 00000000000..feef69eabe8 --- /dev/null +++ b/extra/inky-extra/Resources/functions.php @@ -0,0 +1,23 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Twig\Extra\Inky; + +/** + * @internal + * @deprecated since Twig 3.9.0 + */ +function twig_inky(string $body): string +{ + trigger_deprecation('twig/inky-extra', '3.9.0', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + return InkyExtension::inky($body); +} diff --git a/extra/inky-extra/Tests/LegacyFunctionsTest.php b/extra/inky-extra/Tests/LegacyFunctionsTest.php new file mode 100644 index 00000000000..26ed1e76fe1 --- /dev/null +++ b/extra/inky-extra/Tests/LegacyFunctionsTest.php @@ -0,0 +1,27 @@ +assertSame(InkyExtension::inky('

Foo

'), twig_inky('

Foo

')); + } +} diff --git a/extra/inky-extra/composer.json b/extra/inky-extra/composer.json index 2cb5a5ad308..9eacfc55b35 100644 --- a/extra/inky-extra/composer.json +++ b/extra/inky-extra/composer.json @@ -16,6 +16,7 @@ ], "require": { "php": ">=7.2.5", + "symfony/deprecation-contracts": "^2.5|^3", "lorenzo/pinky": "^1.0.5", "twig/twig": "^3.0" }, @@ -23,6 +24,7 @@ "symfony/phpunit-bridge": "^6.4|^7.0" }, "autoload": { + "files": [ "Resources/functions.php" ], "psr-4" : { "Twig\\Extra\\Inky\\" : "" }, "exclude-from-classmap": [ "/Tests/" From 69a89e0e77b101ac414022833d12a509f137304a Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sat, 9 Dec 2023 18:24:26 +0100 Subject: [PATCH 127/812] Move functions for MarkdownExtension --- extra/markdown-extra/MarkdownExtension.php | 43 ++++++++++--------- extra/markdown-extra/Resources/functions.php | 23 ++++++++++ .../Tests/LegacyFunctionsTest.php | 27 ++++++++++++ extra/markdown-extra/composer.json | 2 + 4 files changed, 75 insertions(+), 20 deletions(-) create mode 100644 extra/markdown-extra/Resources/functions.php create mode 100644 extra/markdown-extra/Tests/LegacyFunctionsTest.php diff --git a/extra/markdown-extra/MarkdownExtension.php b/extra/markdown-extra/MarkdownExtension.php index 6c4296bbf1b..1647b66ff70 100644 --- a/extra/markdown-extra/MarkdownExtension.php +++ b/extra/markdown-extra/MarkdownExtension.php @@ -21,28 +21,31 @@ public function getFilters() { return [ new TwigFilter('markdown_to_html', ['Twig\\Extra\\Markdown\\MarkdownRuntime', 'convert'], ['is_safe' => ['all']]), - new TwigFilter('html_to_markdown', 'Twig\\Extra\\Markdown\\twig_html_to_markdown', ['is_safe' => ['all']]), + new TwigFilter('html_to_markdown', [self::class, 'htmlToMarkdown'], ['is_safe' => ['all']]), ]; } -} - -function twig_html_to_markdown(string $body, array $options = []): string -{ - static $converters; - - if (!class_exists(HtmlConverter::class)) { - throw new \LogicException('You cannot use the "html_to_markdown" filter as league/html-to-markdown is not installed; try running "composer require league/html-to-markdown".'); - } - $options += [ - 'hard_break' => true, - 'strip_tags' => true, - 'remove_nodes' => 'head style', - ]; - - if (!isset($converters[$key = serialize($options)])) { - $converters[$key] = new HtmlConverter($options); + /** + * @internal + */ + public static function htmlToMarkdown(string $body, array $options = []): string + { + static $converters; + + if (!class_exists(HtmlConverter::class)) { + throw new \LogicException('You cannot use the "html_to_markdown" filter as league/html-to-markdown is not installed; try running "composer require league/html-to-markdown".'); + } + + $options += [ + 'hard_break' => true, + 'strip_tags' => true, + 'remove_nodes' => 'head style', + ]; + + if (!isset($converters[$key = serialize($options)])) { + $converters[$key] = new HtmlConverter($options); + } + + return $converters[$key]->convert($body); } - - return $converters[$key]->convert($body); } diff --git a/extra/markdown-extra/Resources/functions.php b/extra/markdown-extra/Resources/functions.php new file mode 100644 index 00000000000..ad8da5a53b5 --- /dev/null +++ b/extra/markdown-extra/Resources/functions.php @@ -0,0 +1,23 @@ +assertSame(MarkdownExtension::htmlToMarkdown('

foo

'), html_to_markdown('

foo

')); + } +} diff --git a/extra/markdown-extra/composer.json b/extra/markdown-extra/composer.json index 52dc07d9ec2..745ee502209 100644 --- a/extra/markdown-extra/composer.json +++ b/extra/markdown-extra/composer.json @@ -16,6 +16,7 @@ ], "require": { "php": ">=7.2.5", + "symfony/deprecation-contracts": "^2.5|^3", "twig/twig": "^3.0" }, "require-dev": { @@ -26,6 +27,7 @@ "michelf/php-markdown": "^1.8|^2.0" }, "autoload": { + "files": [ "Resources/functions.php" ], "psr-4" : { "Twig\\Extra\\Markdown\\" : "" }, "exclude-from-classmap": [ "/Tests/" From d4d016034c6599b1727e97fee50e0a614aaba723 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sun, 10 Dec 2023 11:29:00 +0100 Subject: [PATCH 128/812] Move functions for DebugExtension --- composer.json | 3 + src/Extension/DebugExtension.php | 58 ++++++++++---------- src/Resources/debug.php | 24 ++++++++ tests/Extension/LegacyDebugFunctionsTest.php | 30 ++++++++++ 4 files changed, 86 insertions(+), 29 deletions(-) create mode 100644 src/Resources/debug.php create mode 100644 tests/Extension/LegacyDebugFunctionsTest.php diff --git a/composer.json b/composer.json index 31a248c2bbf..2802a450de7 100644 --- a/composer.json +++ b/composer.json @@ -35,6 +35,9 @@ "psr/container": "^1.0|^2.0" }, "autoload": { + "files": [ + "src/Resources/debug.php" + ], "psr-4" : { "Twig\\" : "src/" } diff --git a/src/Extension/DebugExtension.php b/src/Extension/DebugExtension.php index c0f10d5a303..e023c7087b3 100644 --- a/src/Extension/DebugExtension.php +++ b/src/Extension/DebugExtension.php @@ -9,8 +9,12 @@ * file that was distributed with this source code. */ -namespace Twig\Extension { +namespace Twig\Extension; + +use Twig\Environment; use Twig\TwigFunction; +use Twig\Template; +use Twig\TemplateWrapper; final class DebugExtension extends AbstractExtension { @@ -27,38 +31,34 @@ public function getFunctions(): array ; return [ - new TwigFunction('dump', 'twig_var_dump', ['is_safe' => $isDumpOutputHtmlSafe ? ['html'] : [], 'needs_context' => true, 'needs_environment' => true, 'is_variadic' => true]), + new TwigFunction('dump', [self::class, 'dump'], ['is_safe' => $isDumpOutputHtmlSafe ? ['html'] : [], 'needs_context' => true, 'needs_environment' => true, 'is_variadic' => true]), ]; } -} -} - -namespace { -use Twig\Environment; -use Twig\Template; -use Twig\TemplateWrapper; -function twig_var_dump(Environment $env, $context, ...$vars) -{ - if (!$env->isDebug()) { - return; - } - - ob_start(); - - if (!$vars) { - $vars = []; - foreach ($context as $key => $value) { - if (!$value instanceof Template && !$value instanceof TemplateWrapper) { - $vars[$key] = $value; + /** + * @internal + */ + public static function dump(Environment $env, $context, ...$vars) + { + if (!$env->isDebug()) { + return; + } + + ob_start(); + + if (!$vars) { + $vars = []; + foreach ($context as $key => $value) { + if (!$value instanceof Template && !$value instanceof TemplateWrapper) { + $vars[$key] = $value; + } } + + var_dump($vars); + } else { + var_dump(...$vars); } - - var_dump($vars); - } else { - var_dump(...$vars); + + return ob_get_clean(); } - - return ob_get_clean(); -} } diff --git a/src/Resources/debug.php b/src/Resources/debug.php new file mode 100644 index 00000000000..0041b4b79ac --- /dev/null +++ b/src/Resources/debug.php @@ -0,0 +1,24 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Twig\Environment; +use Twig\Extension\DebugExtension; + +/** + * @internal + * @deprecated since Twig 3.9.0 + */ +function twig_var_dump(Environment $env, $context, ...$vars) +{ + trigger_deprecation('twig/twig', '3.9.0', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + DebugExtension::dump($env, $context, ...$vars); +} diff --git a/tests/Extension/LegacyDebugFunctionsTest.php b/tests/Extension/LegacyDebugFunctionsTest.php new file mode 100644 index 00000000000..4cab762006b --- /dev/null +++ b/tests/Extension/LegacyDebugFunctionsTest.php @@ -0,0 +1,30 @@ +assertSame(DebugExtension::dump($env, 'Foo'), twig_var_dump($env, 'Foo')); + } +} From 196e91dd722867dab329a427ec3f60eb59df8eb5 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sun, 10 Dec 2023 11:38:36 +0100 Subject: [PATCH 129/812] Move functions for StringLoaderExtension --- composer.json | 3 +- src/Extension/StringLoaderExtension.php | 39 +++++++++---------- src/Resources/string_loader.php | 25 ++++++++++++ .../LegacyStringLoaderFunctionsTest.php | 30 ++++++++++++++ tests/Extension/StringLoaderExtensionTest.php | 2 +- 5 files changed, 77 insertions(+), 22 deletions(-) create mode 100644 src/Resources/string_loader.php create mode 100644 tests/Extension/LegacyStringLoaderFunctionsTest.php diff --git a/composer.json b/composer.json index 2802a450de7..f337562389c 100644 --- a/composer.json +++ b/composer.json @@ -36,7 +36,8 @@ }, "autoload": { "files": [ - "src/Resources/debug.php" + "src/Resources/debug.php", + "src/Resources/string_loader.php" ], "psr-4" : { "Twig\\" : "src/" diff --git a/src/Extension/StringLoaderExtension.php b/src/Extension/StringLoaderExtension.php index 7b451471007..fde395dd810 100644 --- a/src/Extension/StringLoaderExtension.php +++ b/src/Extension/StringLoaderExtension.php @@ -9,7 +9,10 @@ * file that was distributed with this source code. */ -namespace Twig\Extension { +namespace Twig\Extension; + +use Twig\Environment; +use Twig\TemplateWrapper; use Twig\TwigFunction; final class StringLoaderExtension extends AbstractExtension @@ -17,26 +20,22 @@ final class StringLoaderExtension extends AbstractExtension public function getFunctions(): array { return [ - new TwigFunction('template_from_string', 'twig_template_from_string', ['needs_environment' => true]), + new TwigFunction('template_from_string', [self::class, 'templateFromString'], ['needs_environment' => true]), ]; } -} -} - -namespace { -use Twig\Environment; -use Twig\TemplateWrapper; -/** - * Loads a template from a string. - * - * {{ include(template_from_string("Hello {{ name }}")) }} - * - * @param string $template A template as a string or object implementing __toString() - * @param string $name An optional name of the template to be used in error messages - */ -function twig_template_from_string(Environment $env, $template, string $name = null): TemplateWrapper -{ - return $env->createTemplate((string) $template, $name); -} + /** + * Loads a template from a string. + * + * {{ include(template_from_string("Hello {{ name }}")) }} + * + * @param string $template A template as a string or object implementing __toString() + * @param string $name An optional name of the template to be used in error messages + * + * @internal + */ + public static function templateFromString(Environment $env, $template, string $name = null): TemplateWrapper + { + return $env->createTemplate((string) $template, $name); + } } diff --git a/src/Resources/string_loader.php b/src/Resources/string_loader.php new file mode 100644 index 00000000000..b074495ca5b --- /dev/null +++ b/src/Resources/string_loader.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Twig\Environment; +use Twig\Extension\StringLoaderExtension; +use Twig\TemplateWrapper; + +/** + * @internal + * @deprecated since Twig 3.9.0 + */ +function twig_template_from_string(Environment $env, $template, string $name = null): TemplateWrapper +{ + trigger_deprecation('twig/twig', '3.9.0', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + return StringLoaderExtension::templateFromString($env, $template, $name); +} diff --git a/tests/Extension/LegacyStringLoaderFunctionsTest.php b/tests/Extension/LegacyStringLoaderFunctionsTest.php new file mode 100644 index 00000000000..a6cb31df0c4 --- /dev/null +++ b/tests/Extension/LegacyStringLoaderFunctionsTest.php @@ -0,0 +1,30 @@ +assertSame(StringLoaderExtension::templateFromString($env, 'Foo')->render(), twig_template_from_string($env, 'Foo')->render()); + } +} diff --git a/tests/Extension/StringLoaderExtensionTest.php b/tests/Extension/StringLoaderExtensionTest.php index 363a0825ecb..e4fa2e3ba8c 100644 --- a/tests/Extension/StringLoaderExtensionTest.php +++ b/tests/Extension/StringLoaderExtensionTest.php @@ -21,6 +21,6 @@ public function testIncludeWithTemplateStringAndNoSandbox() { $twig = new Environment($this->createMock('\Twig\Loader\LoaderInterface')); $twig->addExtension(new StringLoaderExtension()); - $this->assertSame('something', twig_include($twig, [], twig_template_from_string($twig, 'something'))); + $this->assertSame('something', twig_include($twig, [], StringLoaderExtension::templateFromString($twig, 'something'))); } } From 72071e8ece526c55c88142b6ace83f767a7908a0 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sun, 10 Dec 2023 11:49:57 +0100 Subject: [PATCH 130/812] Move functions for EscaperExtension --- composer.json | 1 + src/Extension/EscaperExtension.php | 467 +++++++++++++++-------------- src/Resources/escaper.php | 35 +++ tests/Extension/EscaperTest.php | 48 +-- tests/IntegrationTest.php | 3 +- 5 files changed, 296 insertions(+), 258 deletions(-) create mode 100644 src/Resources/escaper.php diff --git a/composer.json b/composer.json index f337562389c..bceeedd328d 100644 --- a/composer.json +++ b/composer.json @@ -37,6 +37,7 @@ "autoload": { "files": [ "src/Resources/debug.php", + "src/Resources/escaper.php", "src/Resources/string_loader.php" ], "psr-4" : { diff --git a/src/Extension/EscaperExtension.php b/src/Extension/EscaperExtension.php index ef8879dbdc6..88faa60a090 100644 --- a/src/Extension/EscaperExtension.php +++ b/src/Extension/EscaperExtension.php @@ -9,12 +9,19 @@ * file that was distributed with this source code. */ -namespace Twig\Extension { +namespace Twig\Extension; + +use Twig\Environment; +use Twig\Error\RuntimeError; use Twig\FileExtensionEscapingStrategy; +use Twig\Markup; +use Twig\Node\Expression\ConstantExpression; +use Twig\Node\Node; use Twig\NodeVisitor\EscaperNodeVisitor; use Twig\TokenParser\AutoEscapeTokenParser; use Twig\TwigFilter; + final class EscaperExtension extends AbstractExtension { private $defaultStrategy; @@ -49,9 +56,9 @@ public function getNodeVisitors(): array public function getFilters(): array { return [ - new TwigFilter('escape', 'twig_escape_filter', ['needs_environment' => true, 'is_safe_callback' => 'twig_escape_filter_is_safe']), - new TwigFilter('e', 'twig_escape_filter', ['needs_environment' => true, 'is_safe_callback' => 'twig_escape_filter_is_safe']), - new TwigFilter('raw', 'twig_raw_filter', ['is_safe' => ['all']]), + new TwigFilter('escape', [self::class, 'escape'], ['needs_environment' => true, 'is_safe_callback' => [self::class, 'escapeFilterIsSafe']]), + new TwigFilter('e', [self::class, 'escape'], ['needs_environment' => true, 'is_safe_callback' => [self::class, 'escapeFilterIsSafe']]), + new TwigFilter('raw', [self::class, 'raw'], ['is_safe' => ['all']]), ]; } @@ -132,285 +139,279 @@ public function addSafeClass(string $class, array $strategies) $this->safeLookup[$strategy][$class] = true; } } -} -} -namespace { -use Twig\Environment; -use Twig\Error\RuntimeError; -use Twig\Extension\EscaperExtension; -use Twig\Markup; -use Twig\Node\Expression\ConstantExpression; -use Twig\Node\Node; + /** + * Marks a variable as being safe. + * + * @param string $string A PHP variable + * + * @internal + */ + public static function raw($string) + { + return $string; + } -/** - * Marks a variable as being safe. - * - * @param string $string A PHP variable - */ -function twig_raw_filter($string) -{ - return $string; -} + /** + * @internal + */ + public static function escapeFilterIsSafe(Node $filterArgs) + { + foreach ($filterArgs as $arg) { + if ($arg instanceof ConstantExpression) { + return [$arg->getAttribute('value')]; + } -/** - * Escapes a string. - * - * @param mixed $string The value to be escaped - * @param string $strategy The escaping strategy - * @param string $charset The charset - * @param bool $autoescape Whether the function is called by the auto-escaping feature (true) or by the developer (false) - * - * @return string - */ -function twig_escape_filter(Environment $env, $string, $strategy = 'html', $charset = null, $autoescape = false) -{ - if ($autoescape && $string instanceof Markup) { - return $string; + return []; + } + + return ['html']; } - if (!\is_string($string)) { - if (\is_object($string) && method_exists($string, '__toString')) { - if ($autoescape) { - $c = \get_class($string); - $ext = $env->getExtension(EscaperExtension::class); - if (!isset($ext->safeClasses[$c])) { - $ext->safeClasses[$c] = []; - foreach (class_parents($string) + class_implements($string) as $class) { - if (isset($ext->safeClasses[$class])) { - $ext->safeClasses[$c] = array_unique(array_merge($ext->safeClasses[$c], $ext->safeClasses[$class])); - foreach ($ext->safeClasses[$class] as $s) { - $ext->safeLookup[$s][$c] = true; + /** + * Escapes a string. + * + * @param mixed $string The value to be escaped + * @param string $strategy The escaping strategy + * @param string $charset The charset + * @param bool $autoescape Whether the function is called by the auto-escaping feature (true) or by the developer (false) + * + * @return string + * + * @internal + */ + public static function escape(Environment $env, $string, $strategy = 'html', $charset = null, $autoescape = false) + { + if ($autoescape && $string instanceof Markup) { + return $string; + } + + if (!\is_string($string)) { + if (\is_object($string) && method_exists($string, '__toString')) { + if ($autoescape) { + $c = \get_class($string); + $ext = $env->getExtension(EscaperExtension::class); + if (!isset($ext->safeClasses[$c])) { + $ext->safeClasses[$c] = []; + foreach (class_parents($string) + class_implements($string) as $class) { + if (isset($ext->safeClasses[$class])) { + $ext->safeClasses[$c] = array_unique(array_merge($ext->safeClasses[$c], $ext->safeClasses[$class])); + foreach ($ext->safeClasses[$class] as $s) { + $ext->safeLookup[$s][$c] = true; + } } } } + if (isset($ext->safeLookup[$strategy][$c]) || isset($ext->safeLookup['all'][$c])) { + return (string) $string; + } } - if (isset($ext->safeLookup[$strategy][$c]) || isset($ext->safeLookup['all'][$c])) { - return (string) $string; - } - } - $string = (string) $string; - } elseif (\in_array($strategy, ['html', 'js', 'css', 'html_attr', 'url'])) { - return $string; + $string = (string) $string; + } elseif (\in_array($strategy, ['html', 'js', 'css', 'html_attr', 'url'])) { + return $string; + } } - } - - if ('' === $string) { - return ''; - } - if (null === $charset) { - $charset = $env->getCharset(); - } + if ('' === $string) { + return ''; + } - switch ($strategy) { - case 'html': - // see https://www.php.net/htmlspecialchars - - // Using a static variable to avoid initializing the array - // each time the function is called. Moving the declaration on the - // top of the function slow downs other escaping strategies. - static $htmlspecialcharsCharsets = [ - 'ISO-8859-1' => true, 'ISO8859-1' => true, - 'ISO-8859-15' => true, 'ISO8859-15' => true, - 'utf-8' => true, 'UTF-8' => true, - 'CP866' => true, 'IBM866' => true, '866' => true, - 'CP1251' => true, 'WINDOWS-1251' => true, 'WIN-1251' => true, - '1251' => true, - 'CP1252' => true, 'WINDOWS-1252' => true, '1252' => true, - 'KOI8-R' => true, 'KOI8-RU' => true, 'KOI8R' => true, - 'BIG5' => true, '950' => true, - 'GB2312' => true, '936' => true, - 'BIG5-HKSCS' => true, - 'SHIFT_JIS' => true, 'SJIS' => true, '932' => true, - 'EUC-JP' => true, 'EUCJP' => true, - 'ISO8859-5' => true, 'ISO-8859-5' => true, 'MACROMAN' => true, - ]; - - if (isset($htmlspecialcharsCharsets[$charset])) { - return htmlspecialchars($string, \ENT_QUOTES | \ENT_SUBSTITUTE, $charset); - } + if (null === $charset) { + $charset = $env->getCharset(); + } - if (isset($htmlspecialcharsCharsets[strtoupper($charset)])) { - // cache the lowercase variant for future iterations - $htmlspecialcharsCharsets[$charset] = true; + switch ($strategy) { + case 'html': + // see https://www.php.net/htmlspecialchars + + // Using a static variable to avoid initializing the array + // each time the function is called. Moving the declaration on the + // top of the function slow downs other escaping strategies. + static $htmlspecialcharsCharsets = [ + 'ISO-8859-1' => true, 'ISO8859-1' => true, + 'ISO-8859-15' => true, 'ISO8859-15' => true, + 'utf-8' => true, 'UTF-8' => true, + 'CP866' => true, 'IBM866' => true, '866' => true, + 'CP1251' => true, 'WINDOWS-1251' => true, 'WIN-1251' => true, + '1251' => true, + 'CP1252' => true, 'WINDOWS-1252' => true, '1252' => true, + 'KOI8-R' => true, 'KOI8-RU' => true, 'KOI8R' => true, + 'BIG5' => true, '950' => true, + 'GB2312' => true, '936' => true, + 'BIG5-HKSCS' => true, + 'SHIFT_JIS' => true, 'SJIS' => true, '932' => true, + 'EUC-JP' => true, 'EUCJP' => true, + 'ISO8859-5' => true, 'ISO-8859-5' => true, 'MACROMAN' => true, + ]; - return htmlspecialchars($string, \ENT_QUOTES | \ENT_SUBSTITUTE, $charset); - } + if (isset($htmlspecialcharsCharsets[$charset])) { + return htmlspecialchars($string, \ENT_QUOTES | \ENT_SUBSTITUTE, $charset); + } - $string = twig_convert_encoding($string, 'UTF-8', $charset); - $string = htmlspecialchars($string, \ENT_QUOTES | \ENT_SUBSTITUTE, 'UTF-8'); + if (isset($htmlspecialcharsCharsets[strtoupper($charset)])) { + // cache the lowercase variant for future iterations + $htmlspecialcharsCharsets[$charset] = true; - return iconv('UTF-8', $charset, $string); + return htmlspecialchars($string, \ENT_QUOTES | \ENT_SUBSTITUTE, $charset); + } - case 'js': - // escape all non-alphanumeric characters - // into their \x or \uHHHH representations - if ('UTF-8' !== $charset) { $string = twig_convert_encoding($string, 'UTF-8', $charset); - } + $string = htmlspecialchars($string, \ENT_QUOTES | \ENT_SUBSTITUTE, 'UTF-8'); - if (!preg_match('//u', $string)) { - throw new RuntimeError('The string to escape is not a valid UTF-8 string.'); - } - - $string = preg_replace_callback('#[^a-zA-Z0-9,\._]#Su', function ($matches) { - $char = $matches[0]; - - /* - * A few characters have short escape sequences in JSON and JavaScript. - * Escape sequences supported only by JavaScript, not JSON, are omitted. - * \" is also supported but omitted, because the resulting string is not HTML safe. - */ - static $shortMap = [ - '\\' => '\\\\', - '/' => '\\/', - "\x08" => '\b', - "\x0C" => '\f', - "\x0A" => '\n', - "\x0D" => '\r', - "\x09" => '\t', - ]; + return iconv('UTF-8', $charset, $string); - if (isset($shortMap[$char])) { - return $shortMap[$char]; + case 'js': + // escape all non-alphanumeric characters + // into their \x or \uHHHH representations + if ('UTF-8' !== $charset) { + $string = twig_convert_encoding($string, 'UTF-8', $charset); } - $codepoint = mb_ord($char, 'UTF-8'); - if (0x10000 > $codepoint) { - return sprintf('\u%04X', $codepoint); + if (!preg_match('//u', $string)) { + throw new RuntimeError('The string to escape is not a valid UTF-8 string.'); } - // Split characters outside the BMP into surrogate pairs - // https://tools.ietf.org/html/rfc2781.html#section-2.1 - $u = $codepoint - 0x10000; - $high = 0xD800 | ($u >> 10); - $low = 0xDC00 | ($u & 0x3FF); + $string = preg_replace_callback('#[^a-zA-Z0-9,\._]#Su', function ($matches) { + $char = $matches[0]; - return sprintf('\u%04X\u%04X', $high, $low); - }, $string); + /* + * A few characters have short escape sequences in JSON and JavaScript. + * Escape sequences supported only by JavaScript, not JSON, are omitted. + * \" is also supported but omitted, because the resulting string is not HTML safe. + */ + static $shortMap = [ + '\\' => '\\\\', + '/' => '\\/', + "\x08" => '\b', + "\x0C" => '\f', + "\x0A" => '\n', + "\x0D" => '\r', + "\x09" => '\t', + ]; - if ('UTF-8' !== $charset) { - $string = iconv('UTF-8', $charset, $string); - } + if (isset($shortMap[$char])) { + return $shortMap[$char]; + } - return $string; + $codepoint = mb_ord($char, 'UTF-8'); + if (0x10000 > $codepoint) { + return sprintf('\u%04X', $codepoint); + } - case 'css': - if ('UTF-8' !== $charset) { - $string = twig_convert_encoding($string, 'UTF-8', $charset); - } + // Split characters outside the BMP into surrogate pairs + // https://tools.ietf.org/html/rfc2781.html#section-2.1 + $u = $codepoint - 0x10000; + $high = 0xD800 | ($u >> 10); + $low = 0xDC00 | ($u & 0x3FF); - if (!preg_match('//u', $string)) { - throw new RuntimeError('The string to escape is not a valid UTF-8 string.'); - } + return sprintf('\u%04X\u%04X', $high, $low); + }, $string); - $string = preg_replace_callback('#[^a-zA-Z0-9]#Su', function ($matches) { - $char = $matches[0]; + if ('UTF-8' !== $charset) { + $string = iconv('UTF-8', $charset, $string); + } - return sprintf('\\%X ', 1 === \strlen($char) ? \ord($char) : mb_ord($char, 'UTF-8')); - }, $string); + return $string; - if ('UTF-8' !== $charset) { - $string = iconv('UTF-8', $charset, $string); - } + case 'css': + if ('UTF-8' !== $charset) { + $string = twig_convert_encoding($string, 'UTF-8', $charset); + } - return $string; + if (!preg_match('//u', $string)) { + throw new RuntimeError('The string to escape is not a valid UTF-8 string.'); + } - case 'html_attr': - if ('UTF-8' !== $charset) { - $string = twig_convert_encoding($string, 'UTF-8', $charset); - } + $string = preg_replace_callback('#[^a-zA-Z0-9]#Su', function ($matches) { + $char = $matches[0]; - if (!preg_match('//u', $string)) { - throw new RuntimeError('The string to escape is not a valid UTF-8 string.'); - } + return sprintf('\\%X ', 1 === \strlen($char) ? \ord($char) : mb_ord($char, 'UTF-8')); + }, $string); - $string = preg_replace_callback('#[^a-zA-Z0-9,\.\-_]#Su', function ($matches) { - /** - * This function is adapted from code coming from Zend Framework. - * - * @copyright Copyright (c) 2005-2012 Zend Technologies USA Inc. (https://www.zend.com) - * @license https://framework.zend.com/license/new-bsd New BSD License - */ - $chr = $matches[0]; - $ord = \ord($chr); - - /* - * The following replaces characters undefined in HTML with the - * hex entity for the Unicode replacement character. - */ - if (($ord <= 0x1F && "\t" != $chr && "\n" != $chr && "\r" != $chr) || ($ord >= 0x7F && $ord <= 0x9F)) { - return '�'; + if ('UTF-8' !== $charset) { + $string = iconv('UTF-8', $charset, $string); } - /* - * Check if the current character to escape has a name entity we should - * replace it with while grabbing the hex value of the character. - */ - if (1 === \strlen($chr)) { - /* - * While HTML supports far more named entities, the lowest common denominator - * has become HTML5's XML Serialisation which is restricted to the those named - * entities that XML supports. Using HTML entities would result in this error: - * XML Parsing Error: undefined entity - */ - static $entityMap = [ - 34 => '"', /* quotation mark */ - 38 => '&', /* ampersand */ - 60 => '<', /* less-than sign */ - 62 => '>', /* greater-than sign */ - ]; + return $string; - if (isset($entityMap[$ord])) { - return $entityMap[$ord]; - } + case 'html_attr': + if ('UTF-8' !== $charset) { + $string = twig_convert_encoding($string, 'UTF-8', $charset); + } - return sprintf('&#x%02X;', $ord); + if (!preg_match('//u', $string)) { + throw new RuntimeError('The string to escape is not a valid UTF-8 string.'); } - /* - * Per OWASP recommendations, we'll use hex entities for any other - * characters where a named entity does not exist. - */ - return sprintf('&#x%04X;', mb_ord($chr, 'UTF-8')); - }, $string); + $string = preg_replace_callback('#[^a-zA-Z0-9,\.\-_]#Su', function ($matches) { + /** + * This function is adapted from code coming from Zend Framework. + * + * @copyright Copyright (c) 2005-2012 Zend Technologies USA Inc. (https://www.zend.com) + * @license https://framework.zend.com/license/new-bsd New BSD License + */ + $chr = $matches[0]; + $ord = \ord($chr); - if ('UTF-8' !== $charset) { - $string = iconv('UTF-8', $charset, $string); - } + /* + * The following replaces characters undefined in HTML with the + * hex entity for the Unicode replacement character. + */ + if (($ord <= 0x1F && "\t" != $chr && "\n" != $chr && "\r" != $chr) || ($ord >= 0x7F && $ord <= 0x9F)) { + return '�'; + } - return $string; + /* + * Check if the current character to escape has a name entity we should + * replace it with while grabbing the hex value of the character. + */ + if (1 === \strlen($chr)) { + /* + * While HTML supports far more named entities, the lowest common denominator + * has become HTML5's XML Serialisation which is restricted to the those named + * entities that XML supports. Using HTML entities would result in this error: + * XML Parsing Error: undefined entity + */ + static $entityMap = [ + 34 => '"', /* quotation mark */ + 38 => '&', /* ampersand */ + 60 => '<', /* less-than sign */ + 62 => '>', /* greater-than sign */ + ]; + + if (isset($entityMap[$ord])) { + return $entityMap[$ord]; + } + + return sprintf('&#x%02X;', $ord); + } - case 'url': - return rawurlencode($string); + /* + * Per OWASP recommendations, we'll use hex entities for any other + * characters where a named entity does not exist. + */ + return sprintf('&#x%04X;', mb_ord($chr, 'UTF-8')); + }, $string); + + if ('UTF-8' !== $charset) { + $string = iconv('UTF-8', $charset, $string); + } - default: - $escapers = $env->getExtension(EscaperExtension::class)->getEscapers(); - if (\array_key_exists($strategy, $escapers)) { - return $escapers[$strategy]($env, $string, $charset); - } + return $string; - $validStrategies = implode(', ', array_merge(['html', 'js', 'url', 'css', 'html_attr'], array_keys($escapers))); + case 'url': + return rawurlencode($string); - throw new RuntimeError(sprintf('Invalid escaping strategy "%s" (valid ones: %s).', $strategy, $validStrategies)); - } -} + default: + $escapers = $env->getExtension(EscaperExtension::class)->getEscapers(); + if (\array_key_exists($strategy, $escapers)) { + return $escapers[$strategy]($env, $string, $charset); + } -/** - * @internal - */ -function twig_escape_filter_is_safe(Node $filterArgs) -{ - foreach ($filterArgs as $arg) { - if ($arg instanceof ConstantExpression) { - return [$arg->getAttribute('value')]; - } + $validStrategies = implode(', ', array_merge(['html', 'js', 'url', 'css', 'html_attr'], array_keys($escapers))); - return []; + throw new RuntimeError(sprintf('Invalid escaping strategy "%s" (valid ones: %s).', $strategy, $validStrategies)); + } } - - return ['html']; -} } diff --git a/src/Resources/escaper.php b/src/Resources/escaper.php new file mode 100644 index 00000000000..cf038645f8e --- /dev/null +++ b/src/Resources/escaper.php @@ -0,0 +1,35 @@ +createMock(LoaderInterface::class)); foreach ($this->htmlSpecialChars as $key => $value) { - $this->assertEquals($value, twig_escape_filter($twig, $key, 'html'), 'Failed to escape: '.$key); + $this->assertEquals($value, EscaperExtension::escape($twig, $key, 'html'), 'Failed to escape: '.$key); } } @@ -170,7 +170,7 @@ public function testHtmlAttributeEscapingConvertsSpecialChars() { $twig = new Environment($this->createMock(LoaderInterface::class)); foreach ($this->htmlAttrSpecialChars as $key => $value) { - $this->assertEquals($value, twig_escape_filter($twig, $key, 'html_attr'), 'Failed to escape: '.$key); + $this->assertEquals($value, EscaperExtension::escape($twig, $key, 'html_attr'), 'Failed to escape: '.$key); } } @@ -178,7 +178,7 @@ public function testJavascriptEscapingConvertsSpecialChars() { $twig = new Environment($this->createMock(LoaderInterface::class)); foreach ($this->jsSpecialChars as $key => $value) { - $this->assertEquals($value, twig_escape_filter($twig, $key, 'js'), 'Failed to escape: '.$key); + $this->assertEquals($value, EscaperExtension::escape($twig, $key, 'js'), 'Failed to escape: '.$key); } } @@ -189,7 +189,7 @@ public function testJavascriptEscapingConvertsSpecialCharsWithInternalEncoding() try { mb_internal_encoding('ISO-8859-1'); foreach ($this->jsSpecialChars as $key => $value) { - $this->assertEquals($value, twig_escape_filter($twig, $key, 'js'), 'Failed to escape: '.$key); + $this->assertEquals($value, EscaperExtension::escape($twig, $key, 'js'), 'Failed to escape: '.$key); } } finally { if (false !== $previousInternalEncoding) { @@ -201,40 +201,40 @@ public function testJavascriptEscapingConvertsSpecialCharsWithInternalEncoding() public function testJavascriptEscapingReturnsStringIfZeroLength() { $twig = new Environment($this->createMock(LoaderInterface::class)); - $this->assertEquals('', twig_escape_filter($twig, '', 'js')); + $this->assertEquals('', EscaperExtension::escape($twig, '', 'js')); } public function testJavascriptEscapingReturnsStringIfContainsOnlyDigits() { $twig = new Environment($this->createMock(LoaderInterface::class)); - $this->assertEquals('123', twig_escape_filter($twig, '123', 'js')); + $this->assertEquals('123', EscaperExtension::escape($twig, '123', 'js')); } public function testCssEscapingConvertsSpecialChars() { $twig = new Environment($this->createMock(LoaderInterface::class)); foreach ($this->cssSpecialChars as $key => $value) { - $this->assertEquals($value, twig_escape_filter($twig, $key, 'css'), 'Failed to escape: '.$key); + $this->assertEquals($value, EscaperExtension::escape($twig, $key, 'css'), 'Failed to escape: '.$key); } } public function testCssEscapingReturnsStringIfZeroLength() { $twig = new Environment($this->createMock(LoaderInterface::class)); - $this->assertEquals('', twig_escape_filter($twig, '', 'css')); + $this->assertEquals('', EscaperExtension::escape($twig, '', 'css')); } public function testCssEscapingReturnsStringIfContainsOnlyDigits() { $twig = new Environment($this->createMock(LoaderInterface::class)); - $this->assertEquals('123', twig_escape_filter($twig, '123', 'css')); + $this->assertEquals('123', EscaperExtension::escape($twig, '123', 'css')); } public function testUrlEscapingConvertsSpecialChars() { $twig = new Environment($this->createMock(LoaderInterface::class)); foreach ($this->urlSpecialChars as $key => $value) { - $this->assertEquals($value, twig_escape_filter($twig, $key, 'url'), 'Failed to escape: '.$key); + $this->assertEquals($value, EscaperExtension::escape($twig, $key, 'url'), 'Failed to escape: '.$key); } } @@ -296,15 +296,15 @@ public function testJavascriptEscapingEscapesOwaspRecommendedRanges() || $chr >= 0x41 && $chr <= 0x5A || $chr >= 0x61 && $chr <= 0x7A) { $literal = $this->codepointToUtf8($chr); - $this->assertEquals($literal, twig_escape_filter($twig, $literal, 'js')); + $this->assertEquals($literal, EscaperExtension::escape($twig, $literal, 'js')); } else { $literal = $this->codepointToUtf8($chr); if (\in_array($literal, $immune)) { - $this->assertEquals($literal, twig_escape_filter($twig, $literal, 'js')); + $this->assertEquals($literal, EscaperExtension::escape($twig, $literal, 'js')); } else { $this->assertNotEquals( $literal, - twig_escape_filter($twig, $literal, 'js'), + EscaperExtension::escape($twig, $literal, 'js'), "$literal should be escaped!"); } } @@ -320,15 +320,15 @@ public function testHtmlAttributeEscapingEscapesOwaspRecommendedRanges() || $chr >= 0x41 && $chr <= 0x5A || $chr >= 0x61 && $chr <= 0x7A) { $literal = $this->codepointToUtf8($chr); - $this->assertEquals($literal, twig_escape_filter($twig, $literal, 'html_attr')); + $this->assertEquals($literal, EscaperExtension::escape($twig, $literal, 'html_attr')); } else { $literal = $this->codepointToUtf8($chr); if (\in_array($literal, $immune)) { - $this->assertEquals($literal, twig_escape_filter($twig, $literal, 'html_attr')); + $this->assertEquals($literal, EscaperExtension::escape($twig, $literal, 'html_attr')); } else { $this->assertNotEquals( $literal, - twig_escape_filter($twig, $literal, 'html_attr'), + EscaperExtension::escape($twig, $literal, 'html_attr'), "$literal should be escaped!"); } } @@ -344,12 +344,12 @@ public function testCssEscapingEscapesOwaspRecommendedRanges() || $chr >= 0x41 && $chr <= 0x5A || $chr >= 0x61 && $chr <= 0x7A) { $literal = $this->codepointToUtf8($chr); - $this->assertEquals($literal, twig_escape_filter($twig, $literal, 'css')); + $this->assertEquals($literal, EscaperExtension::escape($twig, $literal, 'css')); } else { $literal = $this->codepointToUtf8($chr); $this->assertNotEquals( $literal, - twig_escape_filter($twig, $literal, 'css'), + EscaperExtension::escape($twig, $literal, 'css'), "$literal should be escaped!"); } } @@ -359,7 +359,7 @@ public function testUnknownCustomEscaper() { $this->expectException(RuntimeError::class); - twig_escape_filter(new Environment($this->createMock(LoaderInterface::class)), 'foo', 'bar'); + EscaperExtension::escape(new Environment($this->createMock(LoaderInterface::class)), 'foo', 'bar'); } /** @@ -370,7 +370,7 @@ public function testCustomEscaper($expected, $string, $strategy) $twig = new Environment($this->createMock(LoaderInterface::class)); $twig->getExtension(EscaperExtension::class)->setEscaper('foo', 'Twig\Tests\foo_escaper_for_test'); - $this->assertSame($expected, twig_escape_filter($twig, $string, $strategy)); + $this->assertSame($expected, EscaperExtension::escape($twig, $string, $strategy)); } public function provideCustomEscaperCases() @@ -389,8 +389,8 @@ public function testCustomEscapersOnMultipleEnvs() $env2 = new Environment($this->createMock(LoaderInterface::class)); $env2->getExtension(EscaperExtension::class)->setEscaper('foo', 'Twig\Tests\foo_escaper_for_test1'); - $this->assertSame('fooUTF-8', twig_escape_filter($env1, 'foo', 'foo')); - $this->assertSame('fooUTF-81', twig_escape_filter($env2, 'foo', 'foo')); + $this->assertSame('fooUTF-8', EscaperExtension::escape($env1, 'foo', 'foo')); + $this->assertSame('fooUTF-81', EscaperExtension::escape($env2, 'foo', 'foo')); } /** @@ -401,8 +401,8 @@ public function testObjectEscaping(string $escapedHtml, string $escapedJs, array $obj = new Extension_TestClass(); $twig = new Environment($this->createMock(LoaderInterface::class)); $twig->getExtension('\Twig\Extension\EscaperExtension')->setSafeClasses($safeClasses); - $this->assertSame($escapedHtml, twig_escape_filter($twig, $obj, 'html', null, true)); - $this->assertSame($escapedJs, twig_escape_filter($twig, $obj, 'js', null, true)); + $this->assertSame($escapedHtml, EscaperExtension::escape($twig, $obj, 'html', null, true)); + $this->assertSame($escapedJs, EscaperExtension::escape($twig, $obj, 'js', null, true)); } public function provideObjectsForEscaping() diff --git a/tests/IntegrationTest.php b/tests/IntegrationTest.php index aa24d5fbe8f..8dd5e3bf7f8 100644 --- a/tests/IntegrationTest.php +++ b/tests/IntegrationTest.php @@ -13,6 +13,7 @@ use Twig\Extension\AbstractExtension; use Twig\Extension\DebugExtension; +use Twig\Extension\EscaperExtension; use Twig\Extension\SandboxExtension; use Twig\Extension\StringLoaderExtension; use Twig\Node\Expression\ConstantExpression; @@ -215,7 +216,7 @@ public function §Function($value) */ public function escape_and_nl2br($env, $value, $sep = '
') { - return $this->nl2br(twig_escape_filter($env, $value, 'html'), $sep); + return $this->nl2br(EscaperExtension::escape($env, $value, 'html'), $sep); } /** From 54d34b969b256ecd1c7aa3416205206d87686ef7 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sun, 10 Dec 2023 20:01:42 +0100 Subject: [PATCH 131/812] Move functions for CoreExtension --- composer.json | 1 + extra/intl-extra/IntlExtension.php | 3 +- src/Extension/CoreExtension.php | 2478 +++++++++-------- src/Node/Expression/ArrayExpression.php | 2 +- src/Node/Expression/Binary/EqualBinary.php | 2 +- src/Node/Expression/Binary/GreaterBinary.php | 2 +- .../Expression/Binary/GreaterEqualBinary.php | 2 +- src/Node/Expression/Binary/HasEveryBinary.php | 2 +- src/Node/Expression/Binary/HasSomeBinary.php | 2 +- src/Node/Expression/Binary/InBinary.php | 2 +- src/Node/Expression/Binary/LessBinary.php | 2 +- .../Expression/Binary/LessEqualBinary.php | 2 +- src/Node/Expression/Binary/MatchesBinary.php | 2 +- src/Node/Expression/Binary/NotEqualBinary.php | 2 +- src/Node/Expression/Binary/NotInBinary.php | 2 +- src/Node/Expression/FunctionExpression.php | 3 +- src/Node/Expression/GetAttrExpression.php | 2 +- src/Node/Expression/MethodCallExpression.php | 2 +- src/Node/ForNode.php | 2 +- src/Node/IncludeNode.php | 4 +- src/Node/ModuleNode.php | 1 + src/Node/WithNode.php | 2 +- src/Resources/core.php | 497 ++++ src/Test/NodeTestCase.php | 2 +- tests/Extension/CoreTest.php | 39 +- tests/Extension/StringLoaderExtensionTest.php | 3 +- tests/Node/Expression/FilterTest.php | 10 +- tests/Node/Expression/FunctionTest.php | 2 +- tests/Node/ForTest.php | 8 +- tests/Node/IncludeTest.php | 6 +- tests/Node/ModuleTest.php | 9 +- tests/TemplateTest.php | 35 +- 32 files changed, 1865 insertions(+), 1268 deletions(-) create mode 100644 src/Resources/core.php diff --git a/composer.json b/composer.json index bceeedd328d..1e422dbbafe 100644 --- a/composer.json +++ b/composer.json @@ -36,6 +36,7 @@ }, "autoload": { "files": [ + "src/Resources/core.php", "src/Resources/debug.php", "src/Resources/escaper.php", "src/Resources/string_loader.php" diff --git a/extra/intl-extra/IntlExtension.php b/extra/intl-extra/IntlExtension.php index 142d25e10c5..13d4a4e4778 100644 --- a/extra/intl-extra/IntlExtension.php +++ b/extra/intl-extra/IntlExtension.php @@ -21,6 +21,7 @@ use Twig\Environment; use Twig\Error\RuntimeError; use Twig\Extension\AbstractExtension; +use Twig\Extension\CoreExtension; use Twig\TwigFilter; use Twig\TwigFunction; @@ -368,7 +369,7 @@ public function formatNumberStyle(string $style, $number, array $attrs = [], str */ public function formatDateTime(Environment $env, $date, ?string $dateFormat = 'medium', ?string $timeFormat = 'medium', string $pattern = '', $timezone = null, string $calendar = 'gregorian', string $locale = null): string { - $date = twig_date_converter($env, $date, $timezone); + $date = CoreExtension::dateConverter($env, $date, $timezone); $formatterTimezone = $timezone; if (null === $formatterTimezone) { diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index 36aa8f10a7f..8c9d0d4feff 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -9,8 +9,14 @@ * file that was distributed with this source code. */ -namespace Twig\Extension { +namespace Twig\Extension; + +use Twig\Environment; +use Twig\Error\LoaderError; +use Twig\Error\RuntimeError; use Twig\ExpressionParser; +use Twig\Extension\SandboxExtension; +use Twig\Markup; use Twig\Node\Expression\Binary\AddBinary; use Twig\Node\Expression\Binary\AndBinary; use Twig\Node\Expression\Binary\BitwiseAndBinary; @@ -52,6 +58,9 @@ use Twig\Node\Expression\Unary\NotUnary; use Twig\Node\Expression\Unary\PosUnary; use Twig\NodeVisitor\MacroAutoImportNodeVisitor; +use Twig\Source; +use Twig\Template; +use Twig\TemplateWrapper; use Twig\TokenParser\ApplyTokenParser; use Twig\TokenParser\BlockTokenParser; use Twig\TokenParser\DeprecatedTokenParser; @@ -177,50 +186,50 @@ public function getFilters(): array { return [ // formatting filters - new TwigFilter('date', 'twig_date_format_filter', ['needs_environment' => true]), - new TwigFilter('date_modify', 'twig_date_modify_filter', ['needs_environment' => true]), - new TwigFilter('format', 'twig_sprintf'), - new TwigFilter('replace', 'twig_replace_filter'), - new TwigFilter('number_format', 'twig_number_format_filter', ['needs_environment' => true]), + new TwigFilter('date', [self::class, 'dateFormatFilter'], ['needs_environment' => true]), + new TwigFilter('date_modify', [self::class, 'dateModifyFilter'], ['needs_environment' => true]), + new TwigFilter('format', [self::class, 'sprintf']), + new TwigFilter('replace', [self::class, 'replaceFilter']), + new TwigFilter('number_format', [self::class, 'numberFormatFilter'], ['needs_environment' => true]), new TwigFilter('abs', 'abs'), - new TwigFilter('round', 'twig_round'), + new TwigFilter('round', [self::class, 'round']), // encoding - new TwigFilter('url_encode', 'twig_urlencode_filter'), + new TwigFilter('url_encode', [self::class, 'urlencodeFilter']), new TwigFilter('json_encode', 'json_encode'), - new TwigFilter('convert_encoding', 'twig_convert_encoding'), + new TwigFilter('convert_encoding', [self::class, 'convertEncoding']), // string filters - new TwigFilter('title', 'twig_title_string_filter', ['needs_environment' => true]), - new TwigFilter('capitalize', 'twig_capitalize_string_filter', ['needs_environment' => true]), - new TwigFilter('upper', 'twig_upper_filter', ['needs_environment' => true]), - new TwigFilter('lower', 'twig_lower_filter', ['needs_environment' => true]), - new TwigFilter('striptags', 'twig_striptags'), - new TwigFilter('trim', 'twig_trim_filter'), - new TwigFilter('nl2br', 'twig_nl2br', ['pre_escape' => 'html', 'is_safe' => ['html']]), - new TwigFilter('spaceless', 'twig_spaceless', ['is_safe' => ['html']]), + new TwigFilter('title', [self::class, 'titleStringFilter'], ['needs_environment' => true]), + new TwigFilter('capitalize', [self::class, 'capitalizeStringFilter'], ['needs_environment' => true]), + new TwigFilter('upper', [self::class, 'upperFilter'], ['needs_environment' => true]), + new TwigFilter('lower', [self::class, 'lowerFilter'], ['needs_environment' => true]), + new TwigFilter('striptags', [self::class, 'striptags']), + new TwigFilter('trim', [self::class, 'trimFilter']), + new TwigFilter('nl2br', [self::class, 'nl2br'], ['pre_escape' => 'html', 'is_safe' => ['html']]), + new TwigFilter('spaceless', [self::class, 'spaceless'], ['is_safe' => ['html']]), // array helpers - new TwigFilter('join', 'twig_join_filter'), - new TwigFilter('split', 'twig_split_filter', ['needs_environment' => true]), - new TwigFilter('sort', 'twig_sort_filter', ['needs_environment' => true]), - new TwigFilter('merge', 'twig_array_merge'), - new TwigFilter('batch', 'twig_array_batch'), - new TwigFilter('column', 'twig_array_column'), - new TwigFilter('filter', 'twig_array_filter', ['needs_environment' => true]), - new TwigFilter('map', 'twig_array_map', ['needs_environment' => true]), - new TwigFilter('reduce', 'twig_array_reduce', ['needs_environment' => true]), + new TwigFilter('join', [self::class, 'joinFilter']), + new TwigFilter('split', [self::class, 'splitFilter'], ['needs_environment' => true]), + new TwigFilter('sort', [self::class, 'sortFilter'], ['needs_environment' => true]), + new TwigFilter('merge', [self::class, 'arrayMerge']), + new TwigFilter('batch', [self::class, 'arrayBatch']), + new TwigFilter('column', [self::class, 'arrayColumn']), + new TwigFilter('filter', [self::class, 'arrayFilter'], ['needs_environment' => true]), + new TwigFilter('map', [self::class, 'arrayMap'], ['needs_environment' => true]), + new TwigFilter('reduce', [self::class, 'arrayReduce'], ['needs_environment' => true]), // string/array filters - new TwigFilter('reverse', 'twig_reverse_filter', ['needs_environment' => true]), - new TwigFilter('length', 'twig_length_filter', ['needs_environment' => true]), - new TwigFilter('slice', 'twig_slice', ['needs_environment' => true]), - new TwigFilter('first', 'twig_first', ['needs_environment' => true]), - new TwigFilter('last', 'twig_last', ['needs_environment' => true]), + new TwigFilter('reverse', [self::class, 'reverseFilter'], ['needs_environment' => true]), + new TwigFilter('length', [self::class, 'lengthFilter'], ['needs_environment' => true]), + new TwigFilter('slice', [self::class, 'slice'], ['needs_environment' => true]), + new TwigFilter('first', [self::class, 'first'], ['needs_environment' => true]), + new TwigFilter('last', [self::class, 'last'], ['needs_environment' => true]), // iteration and runtime - new TwigFilter('default', '_twig_default_filter', ['node_class' => DefaultFilter::class]), - new TwigFilter('keys', 'twig_get_array_keys_filter'), + new TwigFilter('default', [self::class, 'defaultFilter'], ['node_class' => DefaultFilter::class]), + new TwigFilter('keys', [self::class, 'getArrayKeysFilter']), ]; } @@ -230,12 +239,12 @@ public function getFunctions(): array new TwigFunction('max', 'max'), new TwigFunction('min', 'min'), new TwigFunction('range', 'range'), - new TwigFunction('constant', 'twig_constant'), - new TwigFunction('cycle', 'twig_cycle'), - new TwigFunction('random', 'twig_random', ['needs_environment' => true]), - new TwigFunction('date', 'twig_date_converter', ['needs_environment' => true]), - new TwigFunction('include', 'twig_include', ['needs_environment' => true, 'needs_context' => true, 'is_safe' => ['all']]), - new TwigFunction('source', 'twig_source', ['needs_environment' => true, 'is_safe' => ['all']]), + new TwigFunction('constant', [self::class, 'constant']), + new TwigFunction('cycle', [self::class, 'cycle']), + new TwigFunction('random', [self::class, 'random'], ['needs_environment' => true]), + new TwigFunction('date', [self::class, 'dateConverter'], ['needs_environment' => true]), + new TwigFunction('include', [self::class, 'include'], ['needs_environment' => true, 'needs_context' => true, 'is_safe' => ['all']]), + new TwigFunction('source', [self::class, 'source'], ['needs_environment' => true, 'is_safe' => ['all']]), ]; } @@ -250,7 +259,7 @@ public function getTests(): array new TwigTest('null', null, ['node_class' => NullTest::class]), new TwigTest('divisible by', null, ['node_class' => DivisiblebyTest::class, 'one_mandatory_argument' => true]), new TwigTest('constant', null, ['node_class' => ConstantTest::class]), - new TwigTest('empty', 'twig_test_empty'), + new TwigTest('empty', [self::class, 'testEmpty']), new TwigTest('iterable', 'is_iterable'), ]; } @@ -303,192 +312,213 @@ public function getOperators(): array ], ]; } -} -} - -namespace { - use Twig\Environment; - use Twig\Error\LoaderError; - use Twig\Error\RuntimeError; - use Twig\Extension\CoreExtension; - use Twig\Extension\SandboxExtension; - use Twig\Markup; - use Twig\Source; - use Twig\Template; - use Twig\TemplateWrapper; - -/** - * Cycles over a value. - * - * @param \ArrayAccess|array $values - * @param int $position The cycle position - * - * @return string The next value in the cycle - */ -function twig_cycle($values, $position) -{ - if (!\is_array($values) && !$values instanceof \ArrayAccess) { - return $values; - } - return $values[$position % \count($values)]; -} + /** + * Cycles over a value. + * + * @param \ArrayAccess|array $values + * @param int $position The cycle position + * + * @return string The next value in the cycle + * + * @internal + */ + public static function cycle($values, $position) + { + if (!\is_array($values) && !$values instanceof \ArrayAccess) { + return $values; + } -/** - * Returns a random value depending on the supplied parameter type: - * - a random item from a \Traversable or array - * - a random character from a string - * - a random integer between 0 and the integer parameter. - * - * @param \Traversable|array|int|float|string $values The values to pick a random item from - * @param int|null $max Maximum value used when $values is an int - * - * @return mixed A random value from the given sequence - * - * @throws RuntimeError when $values is an empty array (does not apply to an empty string which is returned as is) - */ -function twig_random(Environment $env, $values = null, $max = null) -{ - if (null === $values) { - return null === $max ? mt_rand() : mt_rand(0, (int) $max); + return $values[$position % \count($values)]; } - if (\is_int($values) || \is_float($values)) { - if (null === $max) { - if ($values < 0) { - $max = 0; - $min = $values; + /** + * Returns a random value depending on the supplied parameter type: + * - a random item from a \Traversable or array + * - a random character from a string + * - a random integer between 0 and the integer parameter. + * + * @param \Traversable|array|int|float|string $values The values to pick a random item from + * @param int|null $max Maximum value used when $values is an int + * + * @return mixed A random value from the given sequence + * + * @throws RuntimeError when $values is an empty array (does not apply to an empty string which is returned as is) + * + * @internal + */ + public static function random(Environment $env, $values = null, $max = null) + { + if (null === $values) { + return null === $max ? mt_rand() : mt_rand(0, (int) $max); + } + + if (\is_int($values) || \is_float($values)) { + if (null === $max) { + if ($values < 0) { + $max = 0; + $min = $values; + } else { + $max = $values; + $min = 0; + } } else { - $max = $values; - $min = 0; + $min = $values; } - } else { - $min = $values; + + return mt_rand((int) $min, (int) $max); } - return mt_rand((int) $min, (int) $max); - } + if (\is_string($values)) { + if ('' === $values) { + return ''; + } - if (\is_string($values)) { - if ('' === $values) { - return ''; - } + $charset = $env->getCharset(); - $charset = $env->getCharset(); + if ('UTF-8' !== $charset) { + $values = self::convertEncoding($values, 'UTF-8', $charset); + } - if ('UTF-8' !== $charset) { - $values = twig_convert_encoding($values, 'UTF-8', $charset); + // unicode version of str_split() + // split at all positions, but not after the start and not before the end + $values = preg_split('/(? $value) { + $values[$i] = self::convertEncoding($value, $charset, 'UTF-8'); + } + } + } + + if (!is_iterable($values)) { + return $values; } - // unicode version of str_split() - // split at all positions, but not after the start and not before the end - $values = preg_split('/(? $value) { - $values[$i] = twig_convert_encoding($value, $charset, 'UTF-8'); - } + if (0 === \count($values)) { + throw new RuntimeError('The random function cannot pick from an empty array.'); } - } - if (!is_iterable($values)) { - return $values; + return $values[array_rand($values, 1)]; } - $values = twig_to_array($values); + /** + * Converts a date to the given format. + * + * {{ post.published_at|date("m/d/Y") }} + * + * @param \DateTimeInterface|\DateInterval|string $date A date + * @param string|null $format The target format, null to use the default + * @param \DateTimeZone|string|false|null $timezone The target timezone, null to use the default, false to leave unchanged + * + * @return string The formatted date + * + * @internal + */ + public static function dateFormatFilter(Environment $env, $date, $format = null, $timezone = null) + { + if (null === $format) { + $formats = $env->getExtension(self::class)->getDateFormat(); + $format = $date instanceof \DateInterval ? $formats[1] : $formats[0]; + } + + if ($date instanceof \DateInterval) { + return $date->format($format); + } - if (0 === \count($values)) { - throw new RuntimeError('The random function cannot pick from an empty array.'); + return self::dateConverter($env, $date, $timezone)->format($format); } - return $values[array_rand($values, 1)]; -} + /** + * Returns a new date object modified. + * + * {{ post.published_at|date_modify("-1day")|date("m/d/Y") }} + * + * @param \DateTimeInterface|string $date A date + * @param string $modifier A modifier string + * + * @return \DateTimeInterface + * + * @internal + */ + public static function dateModifyFilter(Environment $env, $date, $modifier) + { + $date = self::dateConverter($env, $date, false); -/** - * Converts a date to the given format. - * - * {{ post.published_at|date("m/d/Y") }} - * - * @param \DateTimeInterface|\DateInterval|string $date A date - * @param string|null $format The target format, null to use the default - * @param \DateTimeZone|string|false|null $timezone The target timezone, null to use the default, false to leave unchanged - * - * @return string The formatted date - */ -function twig_date_format_filter(Environment $env, $date, $format = null, $timezone = null) -{ - if (null === $format) { - $formats = $env->getExtension(CoreExtension::class)->getDateFormat(); - $format = $date instanceof \DateInterval ? $formats[1] : $formats[0]; + return $date->modify($modifier); } - if ($date instanceof \DateInterval) { - return $date->format($format); + /** + * Returns a formatted string. + * + * @param string|null $format + * @param ...$values + * + * @return string + * + * @internal + */ + public static function sprintf($format, ...$values) + { + return sprintf($format ?? '', ...$values); } - return twig_date_converter($env, $date, $timezone)->format($format); -} + /** + * Converts an input to a \DateTime instance. + * + * {% if date(user.created_at) < date('+2days') %} + * {# do something #} + * {% endif %} + * + * @param \DateTimeInterface|string|null $date A date or null to use the current time + * @param \DateTimeZone|string|false|null $timezone The target timezone, null to use the default, false to leave unchanged + * + * @return \DateTimeInterface + * + * @internal + */ + public static function dateConverter(Environment $env, $date = null, $timezone = null) + { + // determine the timezone + if (false !== $timezone) { + if (null === $timezone) { + $timezone = $env->getExtension(self::class)->getTimezone(); + } elseif (!$timezone instanceof \DateTimeZone) { + $timezone = new \DateTimeZone($timezone); + } + } -/** - * Returns a new date object modified. - * - * {{ post.published_at|date_modify("-1day")|date("m/d/Y") }} - * - * @param \DateTimeInterface|string $date A date - * @param string $modifier A modifier string - * - * @return \DateTimeInterface - */ -function twig_date_modify_filter(Environment $env, $date, $modifier) -{ - $date = twig_date_converter($env, $date, false); + // immutable dates + if ($date instanceof \DateTimeImmutable) { + return false !== $timezone ? $date->setTimezone($timezone) : $date; + } - return $date->modify($modifier); -} + if ($date instanceof \DateTimeInterface) { + $date = clone $date; + if (false !== $timezone) { + $date->setTimezone($timezone); + } -/** - * Returns a formatted string. - * - * @param string|null $format - * @param ...$values - * - * @return string - */ -function twig_sprintf($format, ...$values) -{ - return sprintf($format ?? '', ...$values); -} + return $date; + } -/** - * Converts an input to a \DateTime instance. - * - * {% if date(user.created_at) < date('+2days') %} - * {# do something #} - * {% endif %} - * - * @param \DateTimeInterface|string|null $date A date or null to use the current time - * @param \DateTimeZone|string|false|null $timezone The target timezone, null to use the default, false to leave unchanged - * - * @return \DateTimeInterface - */ -function twig_date_converter(Environment $env, $date = null, $timezone = null) -{ - // determine the timezone - if (false !== $timezone) { - if (null === $timezone) { - $timezone = $env->getExtension(CoreExtension::class)->getTimezone(); - } elseif (!$timezone instanceof \DateTimeZone) { - $timezone = new \DateTimeZone($timezone); + if (null === $date || 'now' === $date) { + if (null === $date) { + $date = 'now'; + } + + return new \DateTime($date, false !== $timezone ? $timezone : $env->getExtension(self::class)->getTimezone()); } - } - // immutable dates - if ($date instanceof \DateTimeImmutable) { - return false !== $timezone ? $date->setTimezone($timezone) : $date; - } + $asString = (string) $date; + if (ctype_digit($asString) || (!empty($asString) && '-' === $asString[0] && ctype_digit(substr($asString, 1)))) { + $date = new \DateTime('@'.$date); + } else { + $date = new \DateTime($date, $env->getExtension(self::class)->getTimezone()); + } - if ($date instanceof \DateTimeInterface) { - $date = clone $date; if (false !== $timezone) { $date->setTimezone($timezone); } @@ -496,993 +526,1068 @@ function twig_date_converter(Environment $env, $date = null, $timezone = null) return $date; } - if (null === $date || 'now' === $date) { - if (null === $date) { - $date = 'now'; + /** + * Replaces strings within a string. + * + * @param string|null $str String to replace in + * @param array|\Traversable $from Replace values + * + * @return string + * + * @internal + */ + public static function replaceFilter($str, $from) + { + if (!is_iterable($from)) { + throw new RuntimeError(sprintf('The "replace" filter expects an array or "Traversable" as replace values, got "%s".', \is_object($from) ? \get_class($from) : \gettype($from))); } - return new \DateTime($date, false !== $timezone ? $timezone : $env->getExtension(CoreExtension::class)->getTimezone()); - } - - $asString = (string) $date; - if (ctype_digit($asString) || (!empty($asString) && '-' === $asString[0] && ctype_digit(substr($asString, 1)))) { - $date = new \DateTime('@'.$date); - } else { - $date = new \DateTime($date, $env->getExtension(CoreExtension::class)->getTimezone()); - } - - if (false !== $timezone) { - $date->setTimezone($timezone); - } - - return $date; -} - -/** - * Replaces strings within a string. - * - * @param string|null $str String to replace in - * @param array|\Traversable $from Replace values - * - * @return string - */ -function twig_replace_filter($str, $from) -{ - if (!is_iterable($from)) { - throw new RuntimeError(sprintf('The "replace" filter expects an array or "Traversable" as replace values, got "%s".', \is_object($from) ? \get_class($from) : \gettype($from))); + return strtr($str ?? '', self::toArray($from)); } - return strtr($str ?? '', twig_to_array($from)); -} + /** + * Rounds a number. + * + * @param int|float|string|null $value The value to round + * @param int|float $precision The rounding precision + * @param string $method The method to use for rounding + * + * @return int|float The rounded number + * + * @internal + */ + public static function round($value, $precision = 0, $method = 'common') + { + $value = (float) $value; -/** - * Rounds a number. - * - * @param int|float|string|null $value The value to round - * @param int|float $precision The rounding precision - * @param string $method The method to use for rounding - * - * @return int|float The rounded number - */ -function twig_round($value, $precision = 0, $method = 'common') -{ - $value = (float) $value; + if ('common' === $method) { + return round($value, $precision); + } - if ('common' === $method) { - return round($value, $precision); - } + if ('ceil' !== $method && 'floor' !== $method) { + throw new RuntimeError('The round filter only supports the "common", "ceil", and "floor" methods.'); + } - if ('ceil' !== $method && 'floor' !== $method) { - throw new RuntimeError('The round filter only supports the "common", "ceil", and "floor" methods.'); + return $method($value * 10 ** $precision) / 10 ** $precision; } - return $method($value * 10 ** $precision) / 10 ** $precision; -} + /** + * Number format filter. + * + * All of the formatting options can be left null, in that case the defaults will + * be used. Supplying any of the parameters will override the defaults set in the + * environment object. + * + * @param mixed $number A float/int/string of the number to format + * @param int $decimal the number of decimal points to display + * @param string $decimalPoint the character(s) to use for the decimal point + * @param string $thousandSep the character(s) to use for the thousands separator + * + * @return string The formatted number + * + * @internal + */ + public static function numberFormatFilter(Environment $env, $number, $decimal = null, $decimalPoint = null, $thousandSep = null) + { + $defaults = $env->getExtension(self::class)->getNumberFormat(); + if (null === $decimal) { + $decimal = $defaults[0]; + } -/** - * Number format filter. - * - * All of the formatting options can be left null, in that case the defaults will - * be used. Supplying any of the parameters will override the defaults set in the - * environment object. - * - * @param mixed $number A float/int/string of the number to format - * @param int $decimal the number of decimal points to display - * @param string $decimalPoint the character(s) to use for the decimal point - * @param string $thousandSep the character(s) to use for the thousands separator - * - * @return string The formatted number - */ -function twig_number_format_filter(Environment $env, $number, $decimal = null, $decimalPoint = null, $thousandSep = null) -{ - $defaults = $env->getExtension(CoreExtension::class)->getNumberFormat(); - if (null === $decimal) { - $decimal = $defaults[0]; - } + if (null === $decimalPoint) { + $decimalPoint = $defaults[1]; + } - if (null === $decimalPoint) { - $decimalPoint = $defaults[1]; - } + if (null === $thousandSep) { + $thousandSep = $defaults[2]; + } - if (null === $thousandSep) { - $thousandSep = $defaults[2]; + return number_format((float) $number, $decimal, $decimalPoint, $thousandSep); } - return number_format((float) $number, $decimal, $decimalPoint, $thousandSep); -} + /** + * URL encodes (RFC 3986) a string as a path segment or an array as a query string. + * + * @param string|array|null $url A URL or an array of query parameters + * + * @return string The URL encoded value + * + * @internal + */ + public static function urlencodeFilter($url) + { + if (\is_array($url)) { + return http_build_query($url, '', '&', \PHP_QUERY_RFC3986); + } -/** - * URL encodes (RFC 3986) a string as a path segment or an array as a query string. - * - * @param string|array|null $url A URL or an array of query parameters - * - * @return string The URL encoded value - */ -function twig_urlencode_filter($url) -{ - if (\is_array($url)) { - return http_build_query($url, '', '&', \PHP_QUERY_RFC3986); + return rawurlencode($url ?? ''); } - return rawurlencode($url ?? ''); -} + /** + * Merges any number of arrays or Traversable objects. + * + * {% set items = { 'apple': 'fruit', 'orange': 'fruit' } %} + * + * {% set items = items|merge({ 'peugeot': 'car' }, { 'banana': 'fruit' }) %} + * + * {# items now contains { 'apple': 'fruit', 'orange': 'fruit', 'peugeot': 'car', 'banana': 'fruit' } #} + * + * @param array|\Traversable ...$arrays Any number of arrays or Traversable objects to merge + * + * @return array The merged array + * + * @internal + */ + public static function arrayMerge(...$arrays) + { + $result = []; -/** - * Merges any number of arrays or Traversable objects. - * - * {% set items = { 'apple': 'fruit', 'orange': 'fruit' } %} - * - * {% set items = items|merge({ 'peugeot': 'car' }, { 'banana': 'fruit' }) %} - * - * {# items now contains { 'apple': 'fruit', 'orange': 'fruit', 'peugeot': 'car', 'banana': 'fruit' } #} - * - * @param array|\Traversable ...$arrays Any number of arrays or Traversable objects to merge - * - * @return array The merged array - */ -function twig_array_merge(...$arrays) -{ - $result = []; + foreach ($arrays as $argNumber => $array) { + if (!is_iterable($array)) { + throw new RuntimeError(sprintf('The merge filter only works with arrays or "Traversable", got "%s" for argument %d.', \gettype($array), $argNumber + 1)); + } - foreach ($arrays as $argNumber => $array) { - if (!is_iterable($array)) { - throw new RuntimeError(sprintf('The merge filter only works with arrays or "Traversable", got "%s" for argument %d.', \gettype($array), $argNumber + 1)); + $result = array_merge($result, self::toArray($array)); } - $result = array_merge($result, twig_to_array($array)); + return $result; } - return $result; -} + /** + * Slices a variable. + * + * @param mixed $item A variable + * @param int $start Start of the slice + * @param int $length Size of the slice + * @param bool $preserveKeys Whether to preserve key or not (when the input is an array) + * + * @return mixed The sliced variable + * + * @internal + */ + public static function slice(Environment $env, $item, $start, $length = null, $preserveKeys = false) + { + if ($item instanceof \Traversable) { + while ($item instanceof \IteratorAggregate) { + $item = $item->getIterator(); + } + + if ($start >= 0 && $length >= 0 && $item instanceof \Iterator) { + try { + return iterator_to_array(new \LimitIterator($item, $start, $length ?? -1), $preserveKeys); + } catch (\OutOfBoundsException $e) { + return []; + } + } -/** - * Slices a variable. - * - * @param mixed $item A variable - * @param int $start Start of the slice - * @param int $length Size of the slice - * @param bool $preserveKeys Whether to preserve key or not (when the input is an array) - * - * @return mixed The sliced variable - */ -function twig_slice(Environment $env, $item, $start, $length = null, $preserveKeys = false) -{ - if ($item instanceof \Traversable) { - while ($item instanceof \IteratorAggregate) { - $item = $item->getIterator(); + $item = iterator_to_array($item, $preserveKeys); } - if ($start >= 0 && $length >= 0 && $item instanceof \Iterator) { - try { - return iterator_to_array(new \LimitIterator($item, $start, $length ?? -1), $preserveKeys); - } catch (\OutOfBoundsException $e) { - return []; - } + if (\is_array($item)) { + return \array_slice($item, $start, $length, $preserveKeys); } - $item = iterator_to_array($item, $preserveKeys); + return mb_substr((string) $item, $start, $length, $env->getCharset()); } - if (\is_array($item)) { - return \array_slice($item, $start, $length, $preserveKeys); + /** + * Returns the first element of the item. + * + * @param mixed $item A variable + * + * @return mixed The first element of the item + * + * @internal + */ + public static function first(Environment $env, $item) + { + $elements = self::slice($env, $item, 0, 1, false); + + return \is_string($elements) ? $elements : current($elements); } - return mb_substr((string) $item, $start, $length, $env->getCharset()); -} + /** + * Returns the last element of the item. + * + * @param mixed $item A variable + * + * @return mixed The last element of the item + * + * @internal + */ + public static function last(Environment $env, $item) + { + $elements = self::slice($env, $item, -1, 1, false); -/** - * Returns the first element of the item. - * - * @param mixed $item A variable - * - * @return mixed The first element of the item - */ -function twig_first(Environment $env, $item) -{ - $elements = twig_slice($env, $item, 0, 1, false); + return \is_string($elements) ? $elements : current($elements); + } - return \is_string($elements) ? $elements : current($elements); -} + /** + * Joins the values to a string. + * + * The separators between elements are empty strings per default, you can define them with the optional parameters. + * + * {{ [1, 2, 3]|join(', ', ' and ') }} + * {# returns 1, 2 and 3 #} + * + * {{ [1, 2, 3]|join('|') }} + * {# returns 1|2|3 #} + * + * {{ [1, 2, 3]|join }} + * {# returns 123 #} + * + * @param array $value An array + * @param string $glue The separator + * @param string|null $and The separator for the last pair + * + * @return string The concatenated string + * + * @internal + */ + public static function joinFilter($value, $glue = '', $and = null) + { + if (!is_iterable($value)) { + $value = (array) $value; + } -/** - * Returns the last element of the item. - * - * @param mixed $item A variable - * - * @return mixed The last element of the item - */ -function twig_last(Environment $env, $item) -{ - $elements = twig_slice($env, $item, -1, 1, false); + $value = self::toArray($value, false); - return \is_string($elements) ? $elements : current($elements); -} + if (0 === \count($value)) { + return ''; + } -/** - * Joins the values to a string. - * - * The separators between elements are empty strings per default, you can define them with the optional parameters. - * - * {{ [1, 2, 3]|join(', ', ' and ') }} - * {# returns 1, 2 and 3 #} - * - * {{ [1, 2, 3]|join('|') }} - * {# returns 1|2|3 #} - * - * {{ [1, 2, 3]|join }} - * {# returns 123 #} - * - * @param array $value An array - * @param string $glue The separator - * @param string|null $and The separator for the last pair - * - * @return string The concatenated string - */ -function twig_join_filter($value, $glue = '', $and = null) -{ - if (!is_iterable($value)) { - $value = (array) $value; - } + if (null === $and || $and === $glue) { + return implode($glue, $value); + } - $value = twig_to_array($value, false); + if (1 === \count($value)) { + return $value[0]; + } - if (0 === \count($value)) { - return ''; + return implode($glue, \array_slice($value, 0, -1)).$and.$value[\count($value) - 1]; } - if (null === $and || $and === $glue) { - return implode($glue, $value); - } + /** + * Splits the string into an array. + * + * {{ "one,two,three"|split(',') }} + * {# returns [one, two, three] #} + * + * {{ "one,two,three,four,five"|split(',', 3) }} + * {# returns [one, two, "three,four,five"] #} + * + * {{ "123"|split('') }} + * {# returns [1, 2, 3] #} + * + * {{ "aabbcc"|split('', 2) }} + * {# returns [aa, bb, cc] #} + * + * @param string|null $value A string + * @param string $delimiter The delimiter + * @param int $limit The limit + * + * @return array The split string as an array + * + * @internal + */ + public static function splitFilter(Environment $env, $value, $delimiter, $limit = null) + { + $value = $value ?? ''; - if (1 === \count($value)) { - return $value[0]; - } + if ('' !== $delimiter) { + return null === $limit ? explode($delimiter, $value) : explode($delimiter, $value, $limit); + } - return implode($glue, \array_slice($value, 0, -1)).$and.$value[\count($value) - 1]; -} + if ($limit <= 1) { + return preg_split('/(?getCharset()); + if ($length < $limit) { + return [$value]; + } - if ('' !== $delimiter) { - return null === $limit ? explode($delimiter, $value) : explode($delimiter, $value, $limit); - } + $r = []; + for ($i = 0; $i < $length; $i += $limit) { + $r[] = mb_substr($value, $i, $limit, $env->getCharset()); + } - if ($limit <= 1) { - return preg_split('/(?getCharset()); - if ($length < $limit) { - return [$value]; - } + // The '_default' filter is used internally to avoid using the ternary operator + // which costs a lot for big contexts (before PHP 5.4). So, on average, + // a function call is cheaper. + /** + * @internal + */ + public static function defaultFilter($value, $default = '') + { + if (self::testEmpty($value)) { + return $default; + } - $r = []; - for ($i = 0; $i < $length; $i += $limit) { - $r[] = mb_substr($value, $i, $limit, $env->getCharset()); + return $value; } - return $r; -} - -// The '_default' filter is used internally to avoid using the ternary operator -// which costs a lot for big contexts (before PHP 5.4). So, on average, -// a function call is cheaper. -/** - * @internal - */ -function _twig_default_filter($value, $default = '') -{ - if (twig_test_empty($value)) { - return $default; - } + /** + * Returns the keys for the given array. + * + * It is useful when you want to iterate over the keys of an array: + * + * {% for key in array|keys %} + * {# ... #} + * {% endfor %} + * + * @param array $array An array + * + * @return array The keys + * + * @internal + */ + public static function getArrayKeysFilter($array) + { + if ($array instanceof \Traversable) { + while ($array instanceof \IteratorAggregate) { + $array = $array->getIterator(); + } - return $value; -} + $keys = []; + if ($array instanceof \Iterator) { + $array->rewind(); + while ($array->valid()) { + $keys[] = $array->key(); + $array->next(); + } -/** - * Returns the keys for the given array. - * - * It is useful when you want to iterate over the keys of an array: - * - * {% for key in array|keys %} - * {# ... #} - * {% endfor %} - * - * @param array $array An array - * - * @return array The keys - */ -function twig_get_array_keys_filter($array) -{ - if ($array instanceof \Traversable) { - while ($array instanceof \IteratorAggregate) { - $array = $array->getIterator(); - } + return $keys; + } - $keys = []; - if ($array instanceof \Iterator) { - $array->rewind(); - while ($array->valid()) { - $keys[] = $array->key(); - $array->next(); + foreach ($array as $key => $item) { + $keys[] = $key; } return $keys; } - foreach ($array as $key => $item) { - $keys[] = $key; + if (!\is_array($array)) { + return []; } - return $keys; - } - - if (!\is_array($array)) { - return []; + return array_keys($array); } - return array_keys($array); -} + /** + * Reverses a variable. + * + * @param array|\Traversable|string|null $item An array, a \Traversable instance, or a string + * @param bool $preserveKeys Whether to preserve key or not + * + * @return mixed The reversed input + * + * @internal + */ + public static function reverseFilter(Environment $env, $item, $preserveKeys = false) + { + if ($item instanceof \Traversable) { + return array_reverse(iterator_to_array($item), $preserveKeys); + } -/** - * Reverses a variable. - * - * @param array|\Traversable|string|null $item An array, a \Traversable instance, or a string - * @param bool $preserveKeys Whether to preserve key or not - * - * @return mixed The reversed input - */ -function twig_reverse_filter(Environment $env, $item, $preserveKeys = false) -{ - if ($item instanceof \Traversable) { - return array_reverse(iterator_to_array($item), $preserveKeys); - } + if (\is_array($item)) { + return array_reverse($item, $preserveKeys); + } - if (\is_array($item)) { - return array_reverse($item, $preserveKeys); - } + $string = (string) $item; - $string = (string) $item; + $charset = $env->getCharset(); - $charset = $env->getCharset(); + if ('UTF-8' !== $charset) { + $string = self::convertEncoding($string, 'UTF-8', $charset); + } - if ('UTF-8' !== $charset) { - $string = twig_convert_encoding($string, 'UTF-8', $charset); - } + preg_match_all('/./us', $string, $matches); - preg_match_all('/./us', $string, $matches); + $string = implode('', array_reverse($matches[0])); - $string = implode('', array_reverse($matches[0])); + if ('UTF-8' !== $charset) { + $string = self::convertEncoding($string, $charset, 'UTF-8'); + } - if ('UTF-8' !== $charset) { - $string = twig_convert_encoding($string, $charset, 'UTF-8'); + return $string; } - return $string; -} + /** + * Sorts an array. + * + * @param array|\Traversable $array + * + * @return array + * + * @internal + */ + public static function sortFilter(Environment $env, $array, $arrow = null) + { + if ($array instanceof \Traversable) { + $array = iterator_to_array($array); + } elseif (!\is_array($array)) { + throw new RuntimeError(sprintf('The sort filter only works with arrays or "Traversable", got "%s".', \gettype($array))); + } -/** - * Sorts an array. - * - * @param array|\Traversable $array - * - * @return array - */ -function twig_sort_filter(Environment $env, $array, $arrow = null) -{ - if ($array instanceof \Traversable) { - $array = iterator_to_array($array); - } elseif (!\is_array($array)) { - throw new RuntimeError(sprintf('The sort filter only works with arrays or "Traversable", got "%s".', \gettype($array))); - } + if (null !== $arrow) { + self::checkArrowInSandbox($env, $arrow, 'sort', 'filter'); - if (null !== $arrow) { - twig_check_arrow_in_sandbox($env, $arrow, 'sort', 'filter'); + uasort($array, $arrow); + } else { + asort($array); + } - uasort($array, $arrow); - } else { - asort($array); + return $array; } - return $array; -} + /** + * @internal + */ + public static function inFilter($value, $compare) + { + if ($value instanceof Markup) { + $value = (string) $value; + } + if ($compare instanceof Markup) { + $compare = (string) $compare; + } -/** - * @internal - */ -function twig_in_filter($value, $compare) -{ - if ($value instanceof Markup) { - $value = (string) $value; - } - if ($compare instanceof Markup) { - $compare = (string) $compare; - } + if (\is_string($compare)) { + if (\is_string($value) || \is_int($value) || \is_float($value)) { + return '' === $value || str_contains($compare, (string) $value); + } - if (\is_string($compare)) { - if (\is_string($value) || \is_int($value) || \is_float($value)) { - return '' === $value || str_contains($compare, (string) $value); + return false; } - return false; - } - - if (!is_iterable($compare)) { - return false; - } + if (!is_iterable($compare)) { + return false; + } - if (\is_object($value) || \is_resource($value)) { - if (!\is_array($compare)) { - foreach ($compare as $item) { - if ($item === $value) { - return true; + if (\is_object($value) || \is_resource($value)) { + if (!\is_array($compare)) { + foreach ($compare as $item) { + if ($item === $value) { + return true; + } } + + return false; } - return false; + return \in_array($value, $compare, true); } - return \in_array($value, $compare, true); - } - - foreach ($compare as $item) { - if (0 === twig_compare($value, $item)) { - return true; + foreach ($compare as $item) { + if (0 === self::compare($value, $item)) { + return true; + } } - } - - return false; -} -/** - * Compares two values using a more strict version of the PHP non-strict comparison operator. - * - * @see https://wiki.php.net/rfc/string_to_number_comparison - * @see https://wiki.php.net/rfc/trailing_whitespace_numerics - * - * @internal - */ -function twig_compare($a, $b) -{ - // int <=> string - if (\is_int($a) && \is_string($b)) { - $bTrim = trim($b, " \t\n\r\v\f"); - if (!is_numeric($bTrim)) { - return (string) $a <=> $b; - } - if ((int) $bTrim == $bTrim) { - return $a <=> (int) $bTrim; - } else { - return (float) $a <=> (float) $bTrim; - } + return false; } - if (\is_string($a) && \is_int($b)) { - $aTrim = trim($a, " \t\n\r\v\f"); - if (!is_numeric($aTrim)) { - return $a <=> (string) $b; + + /** + * Compares two values using a more strict version of the PHP non-strict comparison operator. + * + * @see https://wiki.php.net/rfc/string_to_number_comparison + * @see https://wiki.php.net/rfc/trailing_whitespace_numerics + * + * @internal + */ + public static function compare($a, $b) + { + // int <=> string + if (\is_int($a) && \is_string($b)) { + $bTrim = trim($b, " \t\n\r\v\f"); + if (!is_numeric($bTrim)) { + return (string) $a <=> $b; + } + if ((int) $bTrim == $bTrim) { + return $a <=> (int) $bTrim; + } else { + return (float) $a <=> (float) $bTrim; + } } - if ((int) $aTrim == $aTrim) { - return (int) $aTrim <=> $b; - } else { - return (float) $aTrim <=> (float) $b; + if (\is_string($a) && \is_int($b)) { + $aTrim = trim($a, " \t\n\r\v\f"); + if (!is_numeric($aTrim)) { + return $a <=> (string) $b; + } + if ((int) $aTrim == $aTrim) { + return (int) $aTrim <=> $b; + } else { + return (float) $aTrim <=> (float) $b; + } } - } - // float <=> string - if (\is_float($a) && \is_string($b)) { - if (is_nan($a)) { - return 1; + // float <=> string + if (\is_float($a) && \is_string($b)) { + if (is_nan($a)) { + return 1; + } + $bTrim = trim($b, " \t\n\r\v\f"); + if (!is_numeric($bTrim)) { + return (string) $a <=> $b; + } + + return $a <=> (float) $bTrim; } - $bTrim = trim($b, " \t\n\r\v\f"); - if (!is_numeric($bTrim)) { - return (string) $a <=> $b; + if (\is_string($a) && \is_float($b)) { + if (is_nan($b)) { + return 1; + } + $aTrim = trim($a, " \t\n\r\v\f"); + if (!is_numeric($aTrim)) { + return $a <=> (string) $b; + } + + return (float) $aTrim <=> $b; } - return $a <=> (float) $bTrim; + // fallback to <=> + return $a <=> $b; } - if (\is_string($a) && \is_float($b)) { - if (is_nan($b)) { - return 1; + + /** + * @return int + * + * @throws RuntimeError When an invalid pattern is used + * + * @internal + */ + public static function matches(string $regexp, ?string $str) + { + set_error_handler(function ($t, $m) use ($regexp) { + throw new RuntimeError(sprintf('Regexp "%s" passed to "matches" is not valid', $regexp).substr($m, 12)); + }); + try { + return preg_match($regexp, $str ?? ''); + } finally { + restore_error_handler(); } - $aTrim = trim($a, " \t\n\r\v\f"); - if (!is_numeric($aTrim)) { - return $a <=> (string) $b; + } + + /** + * Returns a trimmed string. + * + * @param string|null $string + * @param string|null $characterMask + * @param string $side + * + * @return string + * + * @throws RuntimeError When an invalid trimming side is used (not a string or not 'left', 'right', or 'both') + * + * @internal + */ + public static function trimFilter($string, $characterMask = null, $side = 'both') + { + if (null === $characterMask) { + $characterMask = " \t\n\r\0\x0B"; } - return (float) $aTrim <=> $b; + switch ($side) { + case 'both': + return trim($string ?? '', $characterMask); + case 'left': + return ltrim($string ?? '', $characterMask); + case 'right': + return rtrim($string ?? '', $characterMask); + default: + throw new RuntimeError('Trimming side must be "left", "right" or "both".'); + } } - // fallback to <=> - return $a <=> $b; -} - -/** - * @return int - * - * @throws RuntimeError When an invalid pattern is used - */ -function twig_matches(string $regexp, ?string $str) -{ - set_error_handler(function ($t, $m) use ($regexp) { - throw new RuntimeError(sprintf('Regexp "%s" passed to "matches" is not valid', $regexp).substr($m, 12)); - }); - try { - return preg_match($regexp, $str ?? ''); - } finally { - restore_error_handler(); + /** + * Inserts HTML line breaks before all newlines in a string. + * + * @param string|null $string + * + * @return string + * + * @internal + */ + public static function nl2br($string) + { + return nl2br($string ?? ''); } -} -/** - * Returns a trimmed string. - * - * @param string|null $string - * @param string|null $characterMask - * @param string $side - * - * @return string - * - * @throws RuntimeError When an invalid trimming side is used (not a string or not 'left', 'right', or 'both') - */ -function twig_trim_filter($string, $characterMask = null, $side = 'both') -{ - if (null === $characterMask) { - $characterMask = " \t\n\r\0\x0B"; + /** + * Removes whitespaces between HTML tags. + * + * @param string|null $string + * + * @return string + * + * @internal + */ + public static function spaceless($content) + { + return trim(preg_replace('/>\s+<', $content ?? '')); } - switch ($side) { - case 'both': - return trim($string ?? '', $characterMask); - case 'left': - return ltrim($string ?? '', $characterMask); - case 'right': - return rtrim($string ?? '', $characterMask); - default: - throw new RuntimeError('Trimming side must be "left", "right" or "both".'); + /** + * @param string|null $string + * @param string $to + * @param string $from + * + * @return string + * + * @internal + */ + public static function convertEncoding($string, $to, $from) + { + if (!\function_exists('iconv')) { + throw new RuntimeError('Unable to convert encoding: required function iconv() does not exist. You should install ext-iconv or symfony/polyfill-iconv.'); + } + + return iconv($from, $to, $string ?? ''); } -} -/** - * Inserts HTML line breaks before all newlines in a string. - * - * @param string|null $string - * - * @return string - */ -function twig_nl2br($string) -{ - return nl2br($string ?? ''); -} + /** + * Returns the length of a variable. + * + * @param mixed $thing A variable + * + * @return int The length of the value + * + * @internal + */ + public static function lengthFilter(Environment $env, $thing) + { + if (null === $thing) { + return 0; + } -/** - * Removes whitespaces between HTML tags. - * - * @param string|null $string - * - * @return string - */ -function twig_spaceless($content) -{ - return trim(preg_replace('/>\s+<', $content ?? '')); -} + if (\is_scalar($thing)) { + return mb_strlen($thing, $env->getCharset()); + } -/** - * @param string|null $string - * @param string $to - * @param string $from - * - * @return string - */ -function twig_convert_encoding($string, $to, $from) -{ - if (!\function_exists('iconv')) { - throw new RuntimeError('Unable to convert encoding: required function iconv() does not exist. You should install ext-iconv or symfony/polyfill-iconv.'); - } + if ($thing instanceof \Countable || \is_array($thing) || $thing instanceof \SimpleXMLElement) { + return \count($thing); + } - return iconv($from, $to, $string ?? ''); -} + if ($thing instanceof \Traversable) { + return iterator_count($thing); + } -/** - * Returns the length of a variable. - * - * @param mixed $thing A variable - * - * @return int The length of the value - */ -function twig_length_filter(Environment $env, $thing) -{ - if (null === $thing) { - return 0; - } + if (method_exists($thing, '__toString') && !$thing instanceof \Countable) { + return mb_strlen((string) $thing, $env->getCharset()); + } - if (\is_scalar($thing)) { - return mb_strlen($thing, $env->getCharset()); + return 1; } - if ($thing instanceof \Countable || \is_array($thing) || $thing instanceof \SimpleXMLElement) { - return \count($thing); + /** + * Converts a string to uppercase. + * + * @param string|null $string A string + * + * @return string The uppercased string + * + * @internal + */ + public static function upperFilter(Environment $env, $string) + { + return mb_strtoupper($string ?? '', $env->getCharset()); } - if ($thing instanceof \Traversable) { - return iterator_count($thing); + /** + * Converts a string to lowercase. + * + * @param string|null $string A string + * + * @return string The lowercased string + * + * @internal + */ + public static function lowerFilter(Environment $env, $string) + { + return mb_strtolower($string ?? '', $env->getCharset()); } - if (method_exists($thing, '__toString') && !$thing instanceof \Countable) { - return mb_strlen((string) $thing, $env->getCharset()); + /** + * Strips HTML and PHP tags from a string. + * + * @param string|null $string + * @param string[]|string|null $string + * + * @return string + * + * @internal + */ + public static function striptags($string, $allowable_tags = null) + { + return strip_tags($string ?? '', $allowable_tags); } - return 1; -} - -/** - * Converts a string to uppercase. - * - * @param string|null $string A string - * - * @return string The uppercased string - */ -function twig_upper_filter(Environment $env, $string) -{ - return mb_strtoupper($string ?? '', $env->getCharset()); -} - -/** - * Converts a string to lowercase. - * - * @param string|null $string A string - * - * @return string The lowercased string - */ -function twig_lower_filter(Environment $env, $string) -{ - return mb_strtolower($string ?? '', $env->getCharset()); -} - -/** - * Strips HTML and PHP tags from a string. - * - * @param string|null $string - * @param string[]|string|null $string - * - * @return string - */ -function twig_striptags($string, $allowable_tags = null) -{ - return strip_tags($string ?? '', $allowable_tags); -} + /** + * Returns a titlecased string. + * + * @param string|null $string A string + * + * @return string The titlecased string + * + * @internal + */ + public static function titleStringFilter(Environment $env, $string) + { + if (null !== $charset = $env->getCharset()) { + return mb_convert_case($string ?? '', \MB_CASE_TITLE, $charset); + } -/** - * Returns a titlecased string. - * - * @param string|null $string A string - * - * @return string The titlecased string - */ -function twig_title_string_filter(Environment $env, $string) -{ - if (null !== $charset = $env->getCharset()) { - return mb_convert_case($string ?? '', \MB_CASE_TITLE, $charset); + return ucwords(strtolower($string ?? '')); } - return ucwords(strtolower($string ?? '')); -} - -/** - * Returns a capitalized string. - * - * @param string|null $string A string - * - * @return string The capitalized string - */ -function twig_capitalize_string_filter(Environment $env, $string) -{ - $charset = $env->getCharset(); + /** + * Returns a capitalized string. + * + * @param string|null $string A string + * + * @return string The capitalized string + * + * @internal + */ + public static function capitalizeStringFilter(Environment $env, $string) + { + $charset = $env->getCharset(); - return mb_strtoupper(mb_substr($string ?? '', 0, 1, $charset), $charset).mb_strtolower(mb_substr($string ?? '', 1, null, $charset), $charset); -} + return mb_strtoupper(mb_substr($string ?? '', 0, 1, $charset), $charset).mb_strtolower(mb_substr($string ?? '', 1, null, $charset), $charset); + } -/** - * @internal - */ -function twig_call_macro(Template $template, string $method, array $args, int $lineno, array $context, Source $source) -{ - if (!method_exists($template, $method)) { - $parent = $template; - while ($parent = $parent->getParent($context)) { - if (method_exists($parent, $method)) { - return $parent->$method(...$args); + /** + * @internal + */ + public static function callMacro(Template $template, string $method, array $args, int $lineno, array $context, Source $source) + { + if (!method_exists($template, $method)) { + $parent = $template; + while ($parent = $parent->getParent($context)) { + if (method_exists($parent, $method)) { + return $parent->$method(...$args); + } } + + throw new RuntimeError(sprintf('Macro "%s" is not defined in template "%s".', substr($method, \strlen('macro_')), $template->getTemplateName()), $lineno, $source); } - throw new RuntimeError(sprintf('Macro "%s" is not defined in template "%s".', substr($method, \strlen('macro_')), $template->getTemplateName()), $lineno, $source); + return $template->$method(...$args); } - return $template->$method(...$args); -} + /** + * @internal + */ + public static function ensureTraversable($seq) + { + if (is_iterable($seq)) { + return $seq; + } -/** - * @internal - */ -function twig_ensure_traversable($seq) -{ - if (is_iterable($seq)) { - return $seq; + return []; } - return []; -} + /** + * @internal + */ + public static function toArray($seq, $preserveKeys = true) + { + if ($seq instanceof \Traversable) { + return iterator_to_array($seq, $preserveKeys); + } -/** - * @internal - */ -function twig_to_array($seq, $preserveKeys = true) -{ - if ($seq instanceof \Traversable) { - return iterator_to_array($seq, $preserveKeys); - } + if (!\is_array($seq)) { + return $seq; + } - if (!\is_array($seq)) { - return $seq; + return $preserveKeys ? $seq : array_values($seq); } - return $preserveKeys ? $seq : array_values($seq); -} + /** + * Checks if a variable is empty. + * + * {# evaluates to true if the foo variable is null, false, or the empty string #} + * {% if foo is empty %} + * {# ... #} + * {% endif %} + * + * @param mixed $value A variable + * + * @return bool true if the value is empty, false otherwise + * + * @internal + */ + public static function testEmpty($value) + { + if ($value instanceof \Countable) { + return 0 === \count($value); + } + + if ($value instanceof \Traversable) { + return !iterator_count($value); + } -/** - * Checks if a variable is empty. - * - * {# evaluates to true if the foo variable is null, false, or the empty string #} - * {% if foo is empty %} - * {# ... #} - * {% endif %} - * - * @param mixed $value A variable - * - * @return bool true if the value is empty, false otherwise - */ -function twig_test_empty($value) -{ - if ($value instanceof \Countable) { - return 0 === \count($value); - } + if (\is_object($value) && method_exists($value, '__toString')) { + return '' === (string) $value; + } - if ($value instanceof \Traversable) { - return !iterator_count($value); + return '' === $value || false === $value || null === $value || [] === $value; } - if (\is_object($value) && method_exists($value, '__toString')) { - return '' === (string) $value; + /** + * Checks if a variable is traversable. + * + * {# evaluates to true if the foo variable is an array or a traversable object #} + * {% if foo is iterable %} + * {# ... #} + * {% endif %} + * + * @param mixed $value A variable + * + * @return bool true if the value is traversable + * + * @deprecated since Twig 3.8, to be removed in 4.0 (use the native "is_iterable" function instead) + * + * @internal + */ + public static function testIterable($value) + { + return is_iterable($value); } - return '' === $value || false === $value || null === $value || [] === $value; -} - -/** - * Checks if a variable is traversable. - * - * {# evaluates to true if the foo variable is an array or a traversable object #} - * {% if foo is iterable %} - * {# ... #} - * {% endif %} - * - * @param mixed $value A variable - * - * @return bool true if the value is traversable - * - * @deprecated since Twig 3.8, to be removed in 4.0 (use the native "is_iterable" function instead) - */ -function twig_test_iterable($value) -{ - return is_iterable($value); -} + /** + * Renders a template. + * + * @param array $context + * @param string|array|TemplateWrapper $template The template to render or an array of templates to try consecutively + * @param array $variables The variables to pass to the template + * @param bool $withContext + * @param bool $ignoreMissing Whether to ignore missing templates or not + * @param bool $sandboxed Whether to sandbox the template or not + * + * @return string The rendered template + * + * @internal + */ + public static function include(Environment $env, $context, $template, $variables = [], $withContext = true, $ignoreMissing = false, $sandboxed = false) + { + $alreadySandboxed = false; + $sandbox = null; + if ($withContext) { + $variables = array_merge($context, $variables); + } -/** - * Renders a template. - * - * @param array $context - * @param string|array|TemplateWrapper $template The template to render or an array of templates to try consecutively - * @param array $variables The variables to pass to the template - * @param bool $withContext - * @param bool $ignoreMissing Whether to ignore missing templates or not - * @param bool $sandboxed Whether to sandbox the template or not - * - * @return string The rendered template - */ -function twig_include(Environment $env, $context, $template, $variables = [], $withContext = true, $ignoreMissing = false, $sandboxed = false) -{ - $alreadySandboxed = false; - $sandbox = null; - if ($withContext) { - $variables = array_merge($context, $variables); - } + if ($isSandboxed = $sandboxed && $env->hasExtension(SandboxExtension::class)) { + $sandbox = $env->getExtension(SandboxExtension::class); + if (!$alreadySandboxed = $sandbox->isSandboxed()) { + $sandbox->enableSandbox(); + } - if ($isSandboxed = $sandboxed && $env->hasExtension(SandboxExtension::class)) { - $sandbox = $env->getExtension(SandboxExtension::class); - if (!$alreadySandboxed = $sandbox->isSandboxed()) { - $sandbox->enableSandbox(); + foreach ((\is_array($template) ? $template : [$template]) as $name) { + // if a Template instance is passed, it might have been instantiated outside of a sandbox, check security + if ($name instanceof TemplateWrapper || $name instanceof Template) { + $name->unwrap()->checkSecurity(); + } + } } - foreach ((\is_array($template) ? $template : [$template]) as $name) { - // if a Template instance is passed, it might have been instantiated outside of a sandbox, check security - if ($name instanceof TemplateWrapper || $name instanceof Template) { - $name->unwrap()->checkSecurity(); + try { + $loaded = null; + try { + $loaded = $env->resolveTemplate($template); + } catch (LoaderError $e) { + if (!$ignoreMissing) { + throw $e; + } + } + + return $loaded ? $loaded->render($variables) : ''; + } finally { + if ($isSandboxed && !$alreadySandboxed) { + $sandbox->disableSandbox(); } } } - try { - $loaded = null; + /** + * Returns a template content without rendering it. + * + * @param string $name The template name + * @param bool $ignoreMissing Whether to ignore missing templates or not + * + * @return string The template source + * + * @internal + */ + public static function source(Environment $env, $name, $ignoreMissing = false) + { + $loader = $env->getLoader(); try { - $loaded = $env->resolveTemplate($template); + return $loader->getSourceContext($name)->getCode(); } catch (LoaderError $e) { if (!$ignoreMissing) { throw $e; } } - - return $loaded ? $loaded->render($variables) : ''; - } finally { - if ($isSandboxed && !$alreadySandboxed) { - $sandbox->disableSandbox(); - } } -} -/** - * Returns a template content without rendering it. - * - * @param string $name The template name - * @param bool $ignoreMissing Whether to ignore missing templates or not - * - * @return string The template source - */ -function twig_source(Environment $env, $name, $ignoreMissing = false) -{ - $loader = $env->getLoader(); - try { - return $loader->getSourceContext($name)->getCode(); - } catch (LoaderError $e) { - if (!$ignoreMissing) { - throw $e; - } - } -} + /** + * Provides the ability to get constants from instances as well as class/global constants. + * + * @param string $constant The name of the constant + * @param object|null $object The object to get the constant from + * + * @return string + * + * @internal + */ + public static function constant($constant, $object = null) + { + if (null !== $object) { + if ('class' === $constant) { + return \get_class($object); + } -/** - * Provides the ability to get constants from instances as well as class/global constants. - * - * @param string $constant The name of the constant - * @param object|null $object The object to get the constant from - * - * @return string - */ -function twig_constant($constant, $object = null) -{ - if (null !== $object) { - if ('class' === $constant) { - return \get_class($object); + $constant = \get_class($object).'::'.$constant; } - $constant = \get_class($object).'::'.$constant; - } + if (!\defined($constant)) { + throw new RuntimeError(sprintf('Constant "%s" is undefined.', $constant)); + } - if (!\defined($constant)) { - throw new RuntimeError(sprintf('Constant "%s" is undefined.', $constant)); + return \constant($constant); } - return \constant($constant); -} + /** + * Checks if a constant exists. + * + * @param string $constant The name of the constant + * @param object|null $object The object to get the constant from + * + * @return bool + * + * @internal + */ + public static function constantIsDefined($constant, $object = null) + { + if (null !== $object) { + if ('class' === $constant) { + return true; + } -/** - * Checks if a constant exists. - * - * @param string $constant The name of the constant - * @param object|null $object The object to get the constant from - * - * @return bool - */ -function twig_constant_is_defined($constant, $object = null) -{ - if (null !== $object) { - if ('class' === $constant) { - return true; + $constant = \get_class($object).'::'.$constant; } - $constant = \get_class($object).'::'.$constant; + return \defined($constant); } - return \defined($constant); -} - -/** - * Batches item. - * - * @param array $items An array of items - * @param int $size The size of the batch - * @param mixed $fill A value used to fill missing items - * - * @return array - */ -function twig_array_batch($items, $size, $fill = null, $preserveKeys = true) -{ - if (!is_iterable($items)) { - throw new RuntimeError(sprintf('The "batch" filter expects an array or "Traversable", got "%s".', \is_object($items) ? \get_class($items) : \gettype($items))); - } + /** + * Batches item. + * + * @param array $items An array of items + * @param int $size The size of the batch + * @param mixed $fill A value used to fill missing items + * + * @return array + * + * @internal + */ + public static function arrayBatch($items, $size, $fill = null, $preserveKeys = true) + { + if (!is_iterable($items)) { + throw new RuntimeError(sprintf('The "batch" filter expects an array or "Traversable", got "%s".', \is_object($items) ? \get_class($items) : \gettype($items))); + } - $size = ceil($size); + $size = ceil($size); - $result = array_chunk(twig_to_array($items, $preserveKeys), $size, $preserveKeys); + $result = array_chunk(self::toArray($items, $preserveKeys), $size, $preserveKeys); - if (null !== $fill && $result) { - $last = \count($result) - 1; - if ($fillCount = $size - \count($result[$last])) { - for ($i = 0; $i < $fillCount; ++$i) { - $result[$last][] = $fill; + if (null !== $fill && $result) { + $last = \count($result) - 1; + if ($fillCount = $size - \count($result[$last])) { + for ($i = 0; $i < $fillCount; ++$i) { + $result[$last][] = $fill; + } } } - } - return $result; -} + return $result; + } -/** - * Returns the attribute value for a given array/object. - * - * @param mixed $object The object or array from where to get the item - * @param mixed $item The item to get from the array or object - * @param array $arguments An array of arguments to pass if the item is an object method - * @param string $type The type of attribute (@see \Twig\Template constants) - * @param bool $isDefinedTest Whether this is only a defined check - * @param bool $ignoreStrictCheck Whether to ignore the strict attribute check or not - * @param int $lineno The template line where the attribute was called - * - * @return mixed The attribute value, or a Boolean when $isDefinedTest is true, or null when the attribute is not set and $ignoreStrictCheck is true - * - * @throws RuntimeError if the attribute does not exist and Twig is running in strict mode and $isDefinedTest is false - * - * @internal - */ -function twig_get_attribute(Environment $env, Source $source, $object, $item, array $arguments = [], $type = /* Template::ANY_CALL */ 'any', $isDefinedTest = false, $ignoreStrictCheck = false, $sandboxed = false, int $lineno = -1) -{ - // array - if (/* Template::METHOD_CALL */ 'method' !== $type) { - $arrayItem = \is_bool($item) || \is_float($item) ? (int) $item : $item; + /** + * Returns the attribute value for a given array/object. + * + * @param mixed $object The object or array from where to get the item + * @param mixed $item The item to get from the array or object + * @param array $arguments An array of arguments to pass if the item is an object method + * @param string $type The type of attribute (@see \Twig\Template constants) + * @param bool $isDefinedTest Whether this is only a defined check + * @param bool $ignoreStrictCheck Whether to ignore the strict attribute check or not + * @param int $lineno The template line where the attribute was called + * + * @return mixed The attribute value, or a Boolean when $isDefinedTest is true, or null when the attribute is not set and $ignoreStrictCheck is true + * + * @throws RuntimeError if the attribute does not exist and Twig is running in strict mode and $isDefinedTest is false + * + * @internal + */ + public static function getAttribute(Environment $env, Source $source, $object, $item, array $arguments = [], $type = /* Template::ANY_CALL */ 'any', $isDefinedTest = false, $ignoreStrictCheck = false, $sandboxed = false, int $lineno = -1) + { + // array + if (/* Template::METHOD_CALL */ 'method' !== $type) { + $arrayItem = \is_bool($item) || \is_float($item) ? (int) $item : $item; + + if (((\is_array($object) || $object instanceof \ArrayObject) && (isset($object[$arrayItem]) || \array_key_exists($arrayItem, (array) $object))) + || ($object instanceof \ArrayAccess && isset($object[$arrayItem])) + ) { + if ($isDefinedTest) { + return true; + } - if (((\is_array($object) || $object instanceof \ArrayObject) && (isset($object[$arrayItem]) || \array_key_exists($arrayItem, (array) $object))) - || ($object instanceof ArrayAccess && isset($object[$arrayItem])) - ) { - if ($isDefinedTest) { - return true; + return $object[$arrayItem]; } - return $object[$arrayItem]; + if (/* Template::ARRAY_CALL */ 'array' === $type || !\is_object($object)) { + if ($isDefinedTest) { + return false; + } + + if ($ignoreStrictCheck || !$env->isStrictVariables()) { + return; + } + + if ($object instanceof \ArrayAccess) { + $message = sprintf('Key "%s" in object with ArrayAccess of class "%s" does not exist.', $arrayItem, \get_class($object)); + } elseif (\is_object($object)) { + $message = sprintf('Impossible to access a key "%s" on an object of class "%s" that does not implement ArrayAccess interface.', $item, \get_class($object)); + } elseif (\is_array($object)) { + if (empty($object)) { + $message = sprintf('Key "%s" does not exist as the array is empty.', $arrayItem); + } else { + $message = sprintf('Key "%s" for array with keys "%s" does not exist.', $arrayItem, implode(', ', array_keys($object))); + } + } elseif (/* Template::ARRAY_CALL */ 'array' === $type) { + if (null === $object) { + $message = sprintf('Impossible to access a key ("%s") on a null variable.', $item); + } else { + $message = sprintf('Impossible to access a key ("%s") on a %s variable ("%s").', $item, \gettype($object), $object); + } + } elseif (null === $object) { + $message = sprintf('Impossible to access an attribute ("%s") on a null variable.', $item); + } else { + $message = sprintf('Impossible to access an attribute ("%s") on a %s variable ("%s").', $item, \gettype($object), $object); + } + + throw new RuntimeError($message, $lineno, $source); + } } - if (/* Template::ARRAY_CALL */ 'array' === $type || !\is_object($object)) { + if (!\is_object($object)) { if ($isDefinedTest) { return false; } @@ -1491,260 +1596,245 @@ function twig_get_attribute(Environment $env, Source $source, $object, $item, ar return; } - if ($object instanceof ArrayAccess) { - $message = sprintf('Key "%s" in object with ArrayAccess of class "%s" does not exist.', $arrayItem, \get_class($object)); - } elseif (\is_object($object)) { - $message = sprintf('Impossible to access a key "%s" on an object of class "%s" that does not implement ArrayAccess interface.', $item, \get_class($object)); + if (null === $object) { + $message = sprintf('Impossible to invoke a method ("%s") on a null variable.', $item); } elseif (\is_array($object)) { - if (empty($object)) { - $message = sprintf('Key "%s" does not exist as the array is empty.', $arrayItem); - } else { - $message = sprintf('Key "%s" for array with keys "%s" does not exist.', $arrayItem, implode(', ', array_keys($object))); - } - } elseif (/* Template::ARRAY_CALL */ 'array' === $type) { - if (null === $object) { - $message = sprintf('Impossible to access a key ("%s") on a null variable.', $item); - } else { - $message = sprintf('Impossible to access a key ("%s") on a %s variable ("%s").', $item, \gettype($object), $object); - } - } elseif (null === $object) { - $message = sprintf('Impossible to access an attribute ("%s") on a null variable.', $item); + $message = sprintf('Impossible to invoke a method ("%s") on an array.', $item); } else { - $message = sprintf('Impossible to access an attribute ("%s") on a %s variable ("%s").', $item, \gettype($object), $object); + $message = sprintf('Impossible to invoke a method ("%s") on a %s variable ("%s").', $item, \gettype($object), $object); } throw new RuntimeError($message, $lineno, $source); } - } - - if (!\is_object($object)) { - if ($isDefinedTest) { - return false; - } - - if ($ignoreStrictCheck || !$env->isStrictVariables()) { - return; - } - if (null === $object) { - $message = sprintf('Impossible to invoke a method ("%s") on a null variable.', $item); - } elseif (\is_array($object)) { - $message = sprintf('Impossible to invoke a method ("%s") on an array.', $item); - } else { - $message = sprintf('Impossible to invoke a method ("%s") on a %s variable ("%s").', $item, \gettype($object), $object); + if ($object instanceof Template) { + throw new RuntimeError('Accessing \Twig\Template attributes is forbidden.', $lineno, $source); } - throw new RuntimeError($message, $lineno, $source); - } - - if ($object instanceof Template) { - throw new RuntimeError('Accessing \Twig\Template attributes is forbidden.', $lineno, $source); - } + // object property + if (/* Template::METHOD_CALL */ 'method' !== $type) { + if (isset($object->$item) || \array_key_exists((string) $item, (array) $object)) { + if ($isDefinedTest) { + return true; + } - // object property - if (/* Template::METHOD_CALL */ 'method' !== $type) { - if (isset($object->$item) || \array_key_exists((string) $item, (array) $object)) { - if ($isDefinedTest) { - return true; - } + if ($sandboxed) { + $env->getExtension(SandboxExtension::class)->checkPropertyAllowed($object, $item, $lineno, $source); + } - if ($sandboxed) { - $env->getExtension(SandboxExtension::class)->checkPropertyAllowed($object, $item, $lineno, $source); + return $object->$item; } - - return $object->$item; } - } - static $cache = []; - - $class = \get_class($object); - - // object method - // precedence: getXxx() > isXxx() > hasXxx() - if (!isset($cache[$class])) { - $methods = get_class_methods($object); - sort($methods); - $lcMethods = array_map(function ($value) { return strtr($value, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'); }, $methods); - $classCache = []; - foreach ($methods as $i => $method) { - $classCache[$method] = $method; - $classCache[$lcName = $lcMethods[$i]] = $method; - - if ('g' === $lcName[0] && str_starts_with($lcName, 'get')) { - $name = substr($method, 3); - $lcName = substr($lcName, 3); - } elseif ('i' === $lcName[0] && str_starts_with($lcName, 'is')) { - $name = substr($method, 2); - $lcName = substr($lcName, 2); - } elseif ('h' === $lcName[0] && str_starts_with($lcName, 'has')) { - $name = substr($method, 3); - $lcName = substr($lcName, 3); - if (\in_array('is'.$lcName, $lcMethods)) { + static $cache = []; + + $class = \get_class($object); + + // object method + // precedence: getXxx() > isXxx() > hasXxx() + if (!isset($cache[$class])) { + $methods = get_class_methods($object); + sort($methods); + $lcMethods = array_map(function ($value) { return strtr($value, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'); }, $methods); + $classCache = []; + foreach ($methods as $i => $method) { + $classCache[$method] = $method; + $classCache[$lcName = $lcMethods[$i]] = $method; + + if ('g' === $lcName[0] && str_starts_with($lcName, 'get')) { + $name = substr($method, 3); + $lcName = substr($lcName, 3); + } elseif ('i' === $lcName[0] && str_starts_with($lcName, 'is')) { + $name = substr($method, 2); + $lcName = substr($lcName, 2); + } elseif ('h' === $lcName[0] && str_starts_with($lcName, 'has')) { + $name = substr($method, 3); + $lcName = substr($lcName, 3); + if (\in_array('is'.$lcName, $lcMethods)) { + continue; + } + } else { continue; } - } else { - continue; - } - // skip get() and is() methods (in which case, $name is empty) - if ($name) { - if (!isset($classCache[$name])) { - $classCache[$name] = $method; - } + // skip get() and is() methods (in which case, $name is empty) + if ($name) { + if (!isset($classCache[$name])) { + $classCache[$name] = $method; + } - if (!isset($classCache[$lcName])) { - $classCache[$lcName] = $method; + if (!isset($classCache[$lcName])) { + $classCache[$lcName] = $method; + } } } + $cache[$class] = $classCache; } - $cache[$class] = $classCache; - } - $call = false; - if (isset($cache[$class][$item])) { - $method = $cache[$class][$item]; - } elseif (isset($cache[$class][$lcItem = strtr($item, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz')])) { - $method = $cache[$class][$lcItem]; - } elseif (isset($cache[$class]['__call'])) { - $method = $item; - $call = true; - } else { - if ($isDefinedTest) { - return false; + $call = false; + if (isset($cache[$class][$item])) { + $method = $cache[$class][$item]; + } elseif (isset($cache[$class][$lcItem = strtr($item, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz')])) { + $method = $cache[$class][$lcItem]; + } elseif (isset($cache[$class]['__call'])) { + $method = $item; + $call = true; + } else { + if ($isDefinedTest) { + return false; + } + + if ($ignoreStrictCheck || !$env->isStrictVariables()) { + return; + } + + throw new RuntimeError(sprintf('Neither the property "%1$s" nor one of the methods "%1$s()", "get%1$s()"/"is%1$s()"/"has%1$s()" or "__call()" exist and have public access in class "%2$s".', $item, $class), $lineno, $source); } - if ($ignoreStrictCheck || !$env->isStrictVariables()) { - return; + if ($isDefinedTest) { + return true; } - throw new RuntimeError(sprintf('Neither the property "%1$s" nor one of the methods "%1$s()", "get%1$s()"/"is%1$s()"/"has%1$s()" or "__call()" exist and have public access in class "%2$s".', $item, $class), $lineno, $source); - } + if ($sandboxed) { + $env->getExtension(SandboxExtension::class)->checkMethodAllowed($object, $method, $lineno, $source); + } - if ($isDefinedTest) { - return true; - } + // Some objects throw exceptions when they have __call, and the method we try + // to call is not supported. If ignoreStrictCheck is true, we should return null. + try { + $ret = $object->$method(...$arguments); + } catch (\BadMethodCallException $e) { + if ($call && ($ignoreStrictCheck || !$env->isStrictVariables())) { + return; + } + throw $e; + } - if ($sandboxed) { - $env->getExtension(SandboxExtension::class)->checkMethodAllowed($object, $method, $lineno, $source); + return $ret; } - // Some objects throw exceptions when they have __call, and the method we try - // to call is not supported. If ignoreStrictCheck is true, we should return null. - try { - $ret = $object->$method(...$arguments); - } catch (\BadMethodCallException $e) { - if ($call && ($ignoreStrictCheck || !$env->isStrictVariables())) { - return; + /** + * Returns the values from a single column in the input array. + * + *
+     *  {% set items = [{ 'fruit' : 'apple'}, {'fruit' : 'orange' }] %}
+     *
+     *  {% set fruits = items|column('fruit') %}
+     *
+     *  {# fruits now contains ['apple', 'orange'] #}
+     * 
+ * + * @param array|Traversable $array An array + * @param mixed $name The column name + * @param mixed $index The column to use as the index/keys for the returned array + * + * @return array The array of values + * + * @internal + */ + public static function arrayColumn($array, $name, $index = null): array + { + if ($array instanceof \Traversable) { + $array = iterator_to_array($array); + } elseif (!\is_array($array)) { + throw new RuntimeError(sprintf('The column filter only works with arrays or "Traversable", got "%s" as first argument.', \gettype($array))); } - throw $e; - } - - return $ret; -} -/** - * Returns the values from a single column in the input array. - * - *
- *  {% set items = [{ 'fruit' : 'apple'}, {'fruit' : 'orange' }] %}
- *
- *  {% set fruits = items|column('fruit') %}
- *
- *  {# fruits now contains ['apple', 'orange'] #}
- * 
- * - * @param array|Traversable $array An array - * @param mixed $name The column name - * @param mixed $index The column to use as the index/keys for the returned array - * - * @return array The array of values - */ -function twig_array_column($array, $name, $index = null): array -{ - if ($array instanceof Traversable) { - $array = iterator_to_array($array); - } elseif (!\is_array($array)) { - throw new RuntimeError(sprintf('The column filter only works with arrays or "Traversable", got "%s" as first argument.', \gettype($array))); + return array_column($array, $name, $index); } - return array_column($array, $name, $index); -} + /** + * @internal + */ + public static function arrayFilter(Environment $env, $array, $arrow) + { + if (!is_iterable($array)) { + throw new RuntimeError(sprintf('The "filter" filter expects an array or "Traversable", got "%s".', \is_object($array) ? \get_class($array) : \gettype($array))); + } -function twig_array_filter(Environment $env, $array, $arrow) -{ - if (!is_iterable($array)) { - throw new RuntimeError(sprintf('The "filter" filter expects an array or "Traversable", got "%s".', \is_object($array) ? \get_class($array) : \gettype($array))); - } + self::checkArrowInSandbox($env, $arrow, 'filter', 'filter'); - twig_check_arrow_in_sandbox($env, $arrow, 'filter', 'filter'); + if (\is_array($array)) { + return array_filter($array, $arrow, \ARRAY_FILTER_USE_BOTH); + } - if (\is_array($array)) { - return array_filter($array, $arrow, \ARRAY_FILTER_USE_BOTH); + // the IteratorIterator wrapping is needed as some internal PHP classes are \Traversable but do not implement \Iterator + return new \CallbackFilterIterator(new \IteratorIterator($array), $arrow); } - // the IteratorIterator wrapping is needed as some internal PHP classes are \Traversable but do not implement \Iterator - return new \CallbackFilterIterator(new \IteratorIterator($array), $arrow); -} + /** + * @internal + */ + public static function arrayMap(Environment $env, $array, $arrow) + { + self::checkArrowInSandbox($env, $arrow, 'map', 'filter'); -function twig_array_map(Environment $env, $array, $arrow) -{ - twig_check_arrow_in_sandbox($env, $arrow, 'map', 'filter'); + $r = []; + foreach ($array as $k => $v) { + $r[$k] = $arrow($v, $k); + } - $r = []; - foreach ($array as $k => $v) { - $r[$k] = $arrow($v, $k); + return $r; } - return $r; -} + /** + * @internal + */ + public static function arrayReduce(Environment $env, $array, $arrow, $initial = null) + { + self::checkArrowInSandbox($env, $arrow, 'reduce', 'filter'); -function twig_array_reduce(Environment $env, $array, $arrow, $initial = null) -{ - twig_check_arrow_in_sandbox($env, $arrow, 'reduce', 'filter'); + if (!\is_array($array) && !$array instanceof \Traversable) { + throw new RuntimeError(sprintf('The "reduce" filter only works with arrays or "Traversable", got "%s" as first argument.', \gettype($array))); + } - if (!\is_array($array) && !$array instanceof \Traversable) { - throw new RuntimeError(sprintf('The "reduce" filter only works with arrays or "Traversable", got "%s" as first argument.', \gettype($array))); - } + $accumulator = $initial; + foreach ($array as $key => $value) { + $accumulator = $arrow($accumulator, $value, $key); + } - $accumulator = $initial; - foreach ($array as $key => $value) { - $accumulator = $arrow($accumulator, $value, $key); + return $accumulator; } - return $accumulator; -} - -function twig_array_some(Environment $env, $array, $arrow) -{ - twig_check_arrow_in_sandbox($env, $arrow, 'has some', 'operator'); + /** + * @internal + */ + public static function arraySome(Environment $env, $array, $arrow) + { + self::checkArrowInSandbox($env, $arrow, 'has some', 'operator'); - foreach ($array as $k => $v) { - if ($arrow($v, $k)) { - return true; + foreach ($array as $k => $v) { + if ($arrow($v, $k)) { + return true; + } } - } - return false; -} + return false; + } -function twig_array_every(Environment $env, $array, $arrow) -{ - twig_check_arrow_in_sandbox($env, $arrow, 'has every', 'operator'); + /** + * @internal + */ + public static function arrayEvery(Environment $env, $array, $arrow) + { + self::checkArrowInSandbox($env, $arrow, 'has every', 'operator'); - foreach ($array as $k => $v) { - if (!$arrow($v, $k)) { - return false; + foreach ($array as $k => $v) { + if (!$arrow($v, $k)) { + return false; + } } - } - return true; -} + return true; + } -function twig_check_arrow_in_sandbox(Environment $env, $arrow, $thing, $type) -{ - if (!$arrow instanceof Closure && $env->hasExtension('\Twig\Extension\SandboxExtension') && $env->getExtension('\Twig\Extension\SandboxExtension')->isSandboxed()) { - throw new RuntimeError(sprintf('The callable passed to the "%s" %s must be a Closure in sandbox mode.', $thing, $type)); + /** + * @internal + */ + public static function checkArrowInSandbox(Environment $env, $arrow, $thing, $type) + { + if (!$arrow instanceof \Closure && $env->hasExtension('\Twig\Extension\SandboxExtension') && $env->getExtension('\Twig\Extension\SandboxExtension')->isSandboxed()) { + throw new RuntimeError(sprintf('The callable passed to the "%s" %s must be a Closure in sandbox mode.', $thing, $type)); + } } } -} diff --git a/src/Node/Expression/ArrayExpression.php b/src/Node/Expression/ArrayExpression.php index 44428380239..075c13590ee 100644 --- a/src/Node/Expression/ArrayExpression.php +++ b/src/Node/Expression/ArrayExpression.php @@ -70,7 +70,7 @@ public function compile(Compiler $compiler): void $needsArrayMergeSpread = \PHP_VERSION_ID < 80100 && $this->hasSpreadItem($keyValuePairs); if ($needsArrayMergeSpread) { - $compiler->raw('twig_array_merge('); + $compiler->raw('CoreExtension::arrayMerge('); } $compiler->raw('['); $first = true; diff --git a/src/Node/Expression/Binary/EqualBinary.php b/src/Node/Expression/Binary/EqualBinary.php index 6b48549ef26..5f423196fee 100644 --- a/src/Node/Expression/Binary/EqualBinary.php +++ b/src/Node/Expression/Binary/EqualBinary.php @@ -24,7 +24,7 @@ public function compile(Compiler $compiler): void } $compiler - ->raw('(0 === twig_compare(') + ->raw('(0 === CoreExtension::compare(') ->subcompile($this->getNode('left')) ->raw(', ') ->subcompile($this->getNode('right')) diff --git a/src/Node/Expression/Binary/GreaterBinary.php b/src/Node/Expression/Binary/GreaterBinary.php index e1dd06780b7..f42de3f8645 100644 --- a/src/Node/Expression/Binary/GreaterBinary.php +++ b/src/Node/Expression/Binary/GreaterBinary.php @@ -24,7 +24,7 @@ public function compile(Compiler $compiler): void } $compiler - ->raw('(1 === twig_compare(') + ->raw('(1 === CoreExtension::compare(') ->subcompile($this->getNode('left')) ->raw(', ') ->subcompile($this->getNode('right')) diff --git a/src/Node/Expression/Binary/GreaterEqualBinary.php b/src/Node/Expression/Binary/GreaterEqualBinary.php index df9bfcfbf9d..0c4f43fd94a 100644 --- a/src/Node/Expression/Binary/GreaterEqualBinary.php +++ b/src/Node/Expression/Binary/GreaterEqualBinary.php @@ -24,7 +24,7 @@ public function compile(Compiler $compiler): void } $compiler - ->raw('(0 <= twig_compare(') + ->raw('(0 <= CoreExtension::compare(') ->subcompile($this->getNode('left')) ->raw(', ') ->subcompile($this->getNode('right')) diff --git a/src/Node/Expression/Binary/HasEveryBinary.php b/src/Node/Expression/Binary/HasEveryBinary.php index adfabd44c7f..c57bb20e916 100644 --- a/src/Node/Expression/Binary/HasEveryBinary.php +++ b/src/Node/Expression/Binary/HasEveryBinary.php @@ -18,7 +18,7 @@ class HasEveryBinary extends AbstractBinary public function compile(Compiler $compiler): void { $compiler - ->raw('twig_array_every($this->env, ') + ->raw('CoreExtension::arrayEvery($this->env, ') ->subcompile($this->getNode('left')) ->raw(', ') ->subcompile($this->getNode('right')) diff --git a/src/Node/Expression/Binary/HasSomeBinary.php b/src/Node/Expression/Binary/HasSomeBinary.php index 270da369275..12293f84cb4 100644 --- a/src/Node/Expression/Binary/HasSomeBinary.php +++ b/src/Node/Expression/Binary/HasSomeBinary.php @@ -18,7 +18,7 @@ class HasSomeBinary extends AbstractBinary public function compile(Compiler $compiler): void { $compiler - ->raw('twig_array_some($this->env, ') + ->raw('CoreExtension::arraySome($this->env, ') ->subcompile($this->getNode('left')) ->raw(', ') ->subcompile($this->getNode('right')) diff --git a/src/Node/Expression/Binary/InBinary.php b/src/Node/Expression/Binary/InBinary.php index 6dbfa97f05c..68a98fe15b3 100644 --- a/src/Node/Expression/Binary/InBinary.php +++ b/src/Node/Expression/Binary/InBinary.php @@ -18,7 +18,7 @@ class InBinary extends AbstractBinary public function compile(Compiler $compiler): void { $compiler - ->raw('twig_in_filter(') + ->raw('CoreExtension::inFilter(') ->subcompile($this->getNode('left')) ->raw(', ') ->subcompile($this->getNode('right')) diff --git a/src/Node/Expression/Binary/LessBinary.php b/src/Node/Expression/Binary/LessBinary.php index 598e629134b..fb3264a2d47 100644 --- a/src/Node/Expression/Binary/LessBinary.php +++ b/src/Node/Expression/Binary/LessBinary.php @@ -24,7 +24,7 @@ public function compile(Compiler $compiler): void } $compiler - ->raw('(-1 === twig_compare(') + ->raw('(-1 === CoreExtension::compare(') ->subcompile($this->getNode('left')) ->raw(', ') ->subcompile($this->getNode('right')) diff --git a/src/Node/Expression/Binary/LessEqualBinary.php b/src/Node/Expression/Binary/LessEqualBinary.php index e3c4af58d4c..8f3653892d6 100644 --- a/src/Node/Expression/Binary/LessEqualBinary.php +++ b/src/Node/Expression/Binary/LessEqualBinary.php @@ -24,7 +24,7 @@ public function compile(Compiler $compiler): void } $compiler - ->raw('(0 >= twig_compare(') + ->raw('(0 >= CoreExtension::compare(') ->subcompile($this->getNode('left')) ->raw(', ') ->subcompile($this->getNode('right')) diff --git a/src/Node/Expression/Binary/MatchesBinary.php b/src/Node/Expression/Binary/MatchesBinary.php index a8bce6f4e75..4669044e01a 100644 --- a/src/Node/Expression/Binary/MatchesBinary.php +++ b/src/Node/Expression/Binary/MatchesBinary.php @@ -18,7 +18,7 @@ class MatchesBinary extends AbstractBinary public function compile(Compiler $compiler): void { $compiler - ->raw('twig_matches(') + ->raw('CoreExtension::matches(') ->subcompile($this->getNode('right')) ->raw(', ') ->subcompile($this->getNode('left')) diff --git a/src/Node/Expression/Binary/NotEqualBinary.php b/src/Node/Expression/Binary/NotEqualBinary.php index db47a289050..d137ef62738 100644 --- a/src/Node/Expression/Binary/NotEqualBinary.php +++ b/src/Node/Expression/Binary/NotEqualBinary.php @@ -24,7 +24,7 @@ public function compile(Compiler $compiler): void } $compiler - ->raw('(0 !== twig_compare(') + ->raw('(0 !== CoreExtension::compare(') ->subcompile($this->getNode('left')) ->raw(', ') ->subcompile($this->getNode('right')) diff --git a/src/Node/Expression/Binary/NotInBinary.php b/src/Node/Expression/Binary/NotInBinary.php index fcba6cca1cb..80c8755d8aa 100644 --- a/src/Node/Expression/Binary/NotInBinary.php +++ b/src/Node/Expression/Binary/NotInBinary.php @@ -18,7 +18,7 @@ class NotInBinary extends AbstractBinary public function compile(Compiler $compiler): void { $compiler - ->raw('!twig_in_filter(') + ->raw('!CoreExtension::inFilter(') ->subcompile($this->getNode('left')) ->raw(', ') ->subcompile($this->getNode('right')) diff --git a/src/Node/Expression/FunctionExpression.php b/src/Node/Expression/FunctionExpression.php index 71269775c3d..e89b3897872 100644 --- a/src/Node/Expression/FunctionExpression.php +++ b/src/Node/Expression/FunctionExpression.php @@ -12,6 +12,7 @@ namespace Twig\Node\Expression; use Twig\Compiler; +use Twig\Extension\CoreExtension; use Twig\Node\Node; class FunctionExpression extends CallExpression @@ -33,7 +34,7 @@ public function compile(Compiler $compiler) $this->setAttribute('arguments', $function->getArguments()); $callable = $function->getCallable(); if ('constant' === $name && $this->getAttribute('is_defined_test')) { - $callable = 'twig_constant_is_defined'; + $callable = [CoreExtension::class, 'constantIsDefined']; } $this->setAttribute('callable', $callable); $this->setAttribute('is_variadic', $function->isVariadic()); diff --git a/src/Node/Expression/GetAttrExpression.php b/src/Node/Expression/GetAttrExpression.php index e6a75ce9404..29a446b881b 100644 --- a/src/Node/Expression/GetAttrExpression.php +++ b/src/Node/Expression/GetAttrExpression.php @@ -57,7 +57,7 @@ public function compile(Compiler $compiler): void return; } - $compiler->raw('twig_get_attribute($this->env, $this->source, '); + $compiler->raw('CoreExtension::getAttribute($this->env, $this->source, '); if ($this->getAttribute('ignore_strict_check')) { $this->getNode('node')->setAttribute('ignore_strict_check', true); diff --git a/src/Node/Expression/MethodCallExpression.php b/src/Node/Expression/MethodCallExpression.php index d5ec0b6efcb..6fa1c3f9e08 100644 --- a/src/Node/Expression/MethodCallExpression.php +++ b/src/Node/Expression/MethodCallExpression.php @@ -39,7 +39,7 @@ public function compile(Compiler $compiler): void } $compiler - ->raw('twig_call_macro($macros[') + ->raw('CoreExtension::callMacro($macros[') ->repr($this->getNode('node')->getAttribute('name')) ->raw('], ') ->repr($this->getAttribute('method')) diff --git a/src/Node/ForNode.php b/src/Node/ForNode.php index 04addfbfe58..78b361d8a4e 100644 --- a/src/Node/ForNode.php +++ b/src/Node/ForNode.php @@ -42,7 +42,7 @@ public function compile(Compiler $compiler): void $compiler ->addDebugInfo($this) ->write("\$context['_parent'] = \$context;\n") - ->write("\$context['_seq'] = twig_ensure_traversable(") + ->write("\$context['_seq'] = CoreExtension::ensureTraversable(") ->subcompile($this->getNode('seq')) ->raw(");\n") ; diff --git a/src/Node/IncludeNode.php b/src/Node/IncludeNode.php index d540d6b23bf..be36b26574f 100644 --- a/src/Node/IncludeNode.php +++ b/src/Node/IncludeNode.php @@ -93,12 +93,12 @@ protected function addTemplateArguments(Compiler $compiler) $compiler->raw(false === $this->getAttribute('only') ? '$context' : '[]'); } elseif (false === $this->getAttribute('only')) { $compiler - ->raw('twig_array_merge($context, ') + ->raw('CoreExtension::arrayMerge($context, ') ->subcompile($this->getNode('variables')) ->raw(')') ; } else { - $compiler->raw('twig_to_array('); + $compiler->raw('CoreExtension::toArray('); $compiler->subcompile($this->getNode('variables')); $compiler->raw(')'); } diff --git a/src/Node/ModuleNode.php b/src/Node/ModuleNode.php index 9b485eeaf03..dce335c63f5 100644 --- a/src/Node/ModuleNode.php +++ b/src/Node/ModuleNode.php @@ -143,6 +143,7 @@ protected function compileClassHeader(Compiler $compiler) ->write("use Twig\Environment;\n") ->write("use Twig\Error\LoaderError;\n") ->write("use Twig\Error\RuntimeError;\n") + ->write("use Twig\Extension\CoreExtension;\n") ->write("use Twig\Extension\SandboxExtension;\n") ->write("use Twig\Markup;\n") ->write("use Twig\Sandbox\SecurityError;\n") diff --git a/src/Node/WithNode.php b/src/Node/WithNode.php index 2ac9123d0d1..302b40389ba 100644 --- a/src/Node/WithNode.php +++ b/src/Node/WithNode.php @@ -52,7 +52,7 @@ public function compile(Compiler $compiler): void ->raw(", \$this->getSourceContext());\n") ->outdent() ->write("}\n") - ->write(sprintf("\$%s = twig_to_array(\$%s);\n", $varsName, $varsName)) + ->write(sprintf("\$%s = CoreExtension::toArray(\$%s);\n", $varsName, $varsName)) ; if ($this->getAttribute('only')) { diff --git a/src/Resources/core.php b/src/Resources/core.php new file mode 100644 index 00000000000..f58a1fdf261 --- /dev/null +++ b/src/Resources/core.php @@ -0,0 +1,497 @@ +env, $this->source, '; + return 'CoreExtension::getAttribute($this->env, $this->source, '; } } diff --git a/tests/Extension/CoreTest.php b/tests/Extension/CoreTest.php index 431517f6b21..e1971aeea93 100644 --- a/tests/Extension/CoreTest.php +++ b/tests/Extension/CoreTest.php @@ -14,6 +14,7 @@ use PHPUnit\Framework\TestCase; use Twig\Environment; use Twig\Error\RuntimeError; +use Twig\Extension\CoreExtension; use Twig\Loader\LoaderInterface; class CoreTest extends TestCase @@ -26,7 +27,7 @@ public function testRandomFunction(array $expectedInArray, $value1, $value2 = nu $env = new Environment($this->createMock(LoaderInterface::class)); for ($i = 0; $i < 100; ++$i) { - $this->assertTrue(\in_array(twig_random($env, $value1, $value2), $expectedInArray, true)); // assertContains() would not consider the type + $this->assertTrue(\in_array(CoreExtension::random($env, $value1, $value2), $expectedInArray, true)); // assertContains() would not consider the type } } @@ -84,24 +85,24 @@ public function testRandomFunctionWithoutParameter() $max = mt_getrandmax(); for ($i = 0; $i < 100; ++$i) { - $val = twig_random(new Environment($this->createMock(LoaderInterface::class))); + $val = CoreExtension::random(new Environment($this->createMock(LoaderInterface::class))); $this->assertTrue(\is_int($val) && $val >= 0 && $val <= $max); } } public function testRandomFunctionReturnsAsIs() { - $this->assertSame('', twig_random(new Environment($this->createMock(LoaderInterface::class)), '')); - $this->assertSame('', twig_random(new Environment($this->createMock(LoaderInterface::class), ['charset' => null]), '')); + $this->assertSame('', CoreExtension::random(new Environment($this->createMock(LoaderInterface::class)), '')); + $this->assertSame('', CoreExtension::random(new Environment($this->createMock(LoaderInterface::class), ['charset' => null]), '')); $instance = new \stdClass(); - $this->assertSame($instance, twig_random(new Environment($this->createMock(LoaderInterface::class)), $instance)); + $this->assertSame($instance, CoreExtension::random(new Environment($this->createMock(LoaderInterface::class)), $instance)); } public function testRandomFunctionOfEmptyArrayThrowsException() { $this->expectException(RuntimeError::class); - twig_random(new Environment($this->createMock(LoaderInterface::class)), []); + CoreExtension::random(new Environment($this->createMock(LoaderInterface::class)), []); } public function testRandomFunctionOnNonUTF8String() @@ -111,7 +112,7 @@ public function testRandomFunctionOnNonUTF8String() $text = iconv('UTF-8', 'ISO-8859-1', 'Äé'); for ($i = 0; $i < 30; ++$i) { - $rand = twig_random($twig, $text); + $rand = CoreExtension::random($twig, $text); $this->assertTrue(\in_array(iconv('ISO-8859-1', 'UTF-8', $rand), ['Ä', 'é'], true)); } } @@ -122,7 +123,7 @@ public function testReverseFilterOnNonUTF8String() $twig->setCharset('ISO-8859-1'); $input = iconv('UTF-8', 'ISO-8859-1', 'Äé'); - $output = iconv('ISO-8859-1', 'UTF-8', twig_reverse_filter($twig, $input)); + $output = iconv('ISO-8859-1', 'UTF-8', CoreExtension::reverseFilter($twig, $input)); $this->assertEquals($output, 'éÄ'); } @@ -133,7 +134,7 @@ public function testReverseFilterOnNonUTF8String() public function testTwigFirst($expected, $input) { $twig = new Environment($this->createMock(LoaderInterface::class)); - $this->assertSame($expected, twig_first($twig, $input)); + $this->assertSame($expected, CoreExtension::first($twig, $input)); } public function provideTwigFirstCases() @@ -155,7 +156,7 @@ public function provideTwigFirstCases() public function testTwigLast($expected, $input) { $twig = new Environment($this->createMock(LoaderInterface::class)); - $this->assertSame($expected, twig_last($twig, $input)); + $this->assertSame($expected, CoreExtension::last($twig, $input)); } public function provideTwigLastCases() @@ -176,7 +177,7 @@ public function provideTwigLastCases() */ public function testArrayKeysFilter(array $expected, $input) { - $this->assertSame($expected, twig_get_array_keys_filter($input)); + $this->assertSame($expected, CoreExtension::getArrayKeysFilter($input)); } public function provideArrayKeyCases() @@ -199,7 +200,7 @@ public function provideArrayKeyCases() */ public function testInFilter($expected, $value, $compare) { - $this->assertSame($expected, twig_in_filter($value, $compare)); + $this->assertSame($expected, CoreExtension::inFilter($value, $compare)); } public function provideInFilterCases() @@ -228,7 +229,7 @@ public function provideInFilterCases() public function testSliceFilter($expected, $input, $start, $length = null, $preserveKeys = false) { $twig = new Environment($this->createMock(LoaderInterface::class)); - $this->assertSame($expected, twig_slice($twig, $input, $start, $length, $preserveKeys)); + $this->assertSame($expected, CoreExtension::slice($twig, $input, $start, $length, $preserveKeys)); } public function provideSliceFilterCases() @@ -257,16 +258,16 @@ public function provideSliceFilterCases() */ public function testCompare($expected, $a, $b) { - $this->assertSame($expected, twig_compare($a, $b)); - $this->assertSame($expected, -twig_compare($b, $a)); + $this->assertSame($expected, CoreExtension::compare($a, $b)); + $this->assertSame($expected, -CoreExtension::compare($b, $a)); } public function testCompareNAN() { - $this->assertSame(1, twig_compare(\NAN, 'NAN')); - $this->assertSame(1, twig_compare('NAN', \NAN)); - $this->assertSame(1, twig_compare(\NAN, 'foo')); - $this->assertSame(1, twig_compare('foo', \NAN)); + $this->assertSame(1, CoreExtension::compare(\NAN, 'NAN')); + $this->assertSame(1, CoreExtension::compare('NAN', \NAN)); + $this->assertSame(1, CoreExtension::compare(\NAN, 'foo')); + $this->assertSame(1, CoreExtension::compare('foo', \NAN)); } public function provideCompareCases() diff --git a/tests/Extension/StringLoaderExtensionTest.php b/tests/Extension/StringLoaderExtensionTest.php index e4fa2e3ba8c..4c67e12c719 100644 --- a/tests/Extension/StringLoaderExtensionTest.php +++ b/tests/Extension/StringLoaderExtensionTest.php @@ -13,6 +13,7 @@ use PHPUnit\Framework\TestCase; use Twig\Environment; +use Twig\Extension\CoreExtension; use Twig\Extension\StringLoaderExtension; class StringLoaderExtensionTest extends TestCase @@ -21,6 +22,6 @@ public function testIncludeWithTemplateStringAndNoSandbox() { $twig = new Environment($this->createMock('\Twig\Loader\LoaderInterface')); $twig->addExtension(new StringLoaderExtension()); - $this->assertSame('something', twig_include($twig, [], StringLoaderExtension::templateFromString($twig, 'something'))); + $this->assertSame('something', CoreExtension::include($twig, [], StringLoaderExtension::templateFromString($twig, 'something'))); } } diff --git a/tests/Node/Expression/FilterTest.php b/tests/Node/Expression/FilterTest.php index b8cc48fab16..18820cac8d1 100644 --- a/tests/Node/Expression/FilterTest.php +++ b/tests/Node/Expression/FilterTest.php @@ -69,7 +69,7 @@ protected function foobar() $node = $this->createFilter($expr, 'upper'); $node = $this->createFilter($node, 'number_format', [new ConstantExpression(2, 1), new ConstantExpression('.', 1), new ConstantExpression(',', 1)]); - $tests[] = [$node, 'twig_number_format_filter($this->env, twig_upper_filter($this->env, "foo"), 2, ".", ",")']; + $tests[] = [$node, 'Twig\Extension\CoreExtension::numberFormatFilter($this->env, Twig\Extension\CoreExtension::upperFilter($this->env, "foo"), 2, ".", ",")']; // named arguments $date = new ConstantExpression(0, 1); @@ -77,25 +77,25 @@ protected function foobar() 'timezone' => new ConstantExpression('America/Chicago', 1), 'format' => new ConstantExpression('d/m/Y H:i:s P', 1), ]); - $tests[] = [$node, 'twig_date_format_filter($this->env, 0, "d/m/Y H:i:s P", "America/Chicago")']; + $tests[] = [$node, 'Twig\Extension\CoreExtension::dateFormatFilter($this->env, 0, "d/m/Y H:i:s P", "America/Chicago")']; // skip an optional argument $date = new ConstantExpression(0, 1); $node = $this->createFilter($date, 'date', [ 'timezone' => new ConstantExpression('America/Chicago', 1), ]); - $tests[] = [$node, 'twig_date_format_filter($this->env, 0, null, "America/Chicago")']; + $tests[] = [$node, 'Twig\Extension\CoreExtension::dateFormatFilter($this->env, 0, null, "America/Chicago")']; // underscores vs camelCase for named arguments $string = new ConstantExpression('abc', 1); $node = $this->createFilter($string, 'reverse', [ 'preserve_keys' => new ConstantExpression(true, 1), ]); - $tests[] = [$node, 'twig_reverse_filter($this->env, "abc", true)']; + $tests[] = [$node, 'Twig\Extension\CoreExtension::reverseFilter($this->env, "abc", true)']; $node = $this->createFilter($string, 'reverse', [ 'preserveKeys' => new ConstantExpression(true, 1), ]); - $tests[] = [$node, 'twig_reverse_filter($this->env, "abc", true)']; + $tests[] = [$node, 'Twig\Extension\CoreExtension::reverseFilter($this->env, "abc", true)']; // filter as an anonymous function $node = $this->createFilter(new ConstantExpression('foo', 1), 'anonymous'); diff --git a/tests/Node/Expression/FunctionTest.php b/tests/Node/Expression/FunctionTest.php index 05c07c02923..7fde216497c 100644 --- a/tests/Node/Expression/FunctionTest.php +++ b/tests/Node/Expression/FunctionTest.php @@ -76,7 +76,7 @@ public function getTests() 'timezone' => new ConstantExpression('America/Chicago', 1), 'date' => new ConstantExpression(0, 1), ]); - $tests[] = [$node, 'twig_date_converter($this->env, 0, "America/Chicago")']; + $tests[] = [$node, 'Twig\Extension\CoreExtension::dateConverter($this->env, 0, "America/Chicago")']; // arbitrary named arguments $node = $this->createFunction('barbar'); diff --git a/tests/Node/ForTest.php b/tests/Node/ForTest.php index ce746481771..dbfac3269b2 100644 --- a/tests/Node/ForTest.php +++ b/tests/Node/ForTest.php @@ -57,7 +57,7 @@ public function getTests() $tests[] = [$node, <<getVariableGetter('items')}); +\$context['_seq'] = CoreExtension::ensureTraversable({$this->getVariableGetter('items')}); foreach (\$context['_seq'] as \$context["key"] => \$context["item"]) { echo {$this->getVariableGetter('foo')}; } @@ -78,7 +78,7 @@ public function getTests() $tests[] = [$node, <<getVariableGetter('values')}); +\$context['_seq'] = CoreExtension::ensureTraversable({$this->getVariableGetter('values')}); \$context['loop'] = [ 'parent' => \$context['_parent'], 'index0' => 0, @@ -120,7 +120,7 @@ public function getTests() $tests[] = [$node, <<getVariableGetter('values')}); +\$context['_seq'] = CoreExtension::ensureTraversable({$this->getVariableGetter('values')}); \$context['loop'] = [ 'parent' => \$context['_parent'], 'index0' => 0, @@ -162,7 +162,7 @@ public function getTests() $tests[] = [$node, <<getVariableGetter('values')}); +\$context['_seq'] = CoreExtension::ensureTraversable({$this->getVariableGetter('values')}); \$context['_iterated'] = false; \$context['loop'] = [ 'parent' => \$context['_parent'], diff --git a/tests/Node/IncludeTest.php b/tests/Node/IncludeTest.php index 92681662da7..6d96373bfd3 100644 --- a/tests/Node/IncludeTest.php +++ b/tests/Node/IncludeTest.php @@ -64,14 +64,14 @@ public function getTests() $node = new IncludeNode($expr, $vars, false, false, 1); $tests[] = [$node, <<loadTemplate("foo.twig", null, 1)->display(twig_array_merge(\$context, ["foo" => true])); +\$this->loadTemplate("foo.twig", null, 1)->display(CoreExtension::arrayMerge(\$context, ["foo" => true])); EOF ]; $node = new IncludeNode($expr, $vars, true, false, 1); $tests[] = [$node, <<loadTemplate("foo.twig", null, 1)->display(twig_to_array(["foo" => true])); +\$this->loadTemplate("foo.twig", null, 1)->display(CoreExtension::toArray(["foo" => true])); EOF ]; @@ -85,7 +85,7 @@ public function getTests() // ignore missing template } if (\$__internal_%s) { - \$__internal_%s->display(twig_to_array(["foo" => true])); + \$__internal_%s->display(CoreExtension::toArray(["foo" => true])); } EOF , null, true]; diff --git a/tests/Node/ModuleTest.php b/tests/Node/ModuleTest.php index 938fa2ea7a2..d6b378ad56d 100644 --- a/tests/Node/ModuleTest.php +++ b/tests/Node/ModuleTest.php @@ -63,6 +63,7 @@ public function getTests() use Twig\Environment; use Twig\Error\LoaderError; use Twig\Error\RuntimeError; +use Twig\Extension\CoreExtension; use Twig\Extension\SandboxExtension; use Twig\Markup; use Twig\Sandbox\SecurityError; @@ -110,7 +111,7 @@ public function getTemplateName() */ public function getDebugInfo() { - return array ( 37 => 1,); + return array ( 38 => 1,); } public function getSourceContext() @@ -133,6 +134,7 @@ public function getSourceContext() use Twig\Environment; use Twig\Error\LoaderError; use Twig\Error\RuntimeError; +use Twig\Extension\CoreExtension; use Twig\Extension\SandboxExtension; use Twig\Markup; use Twig\Sandbox\SecurityError; @@ -195,7 +197,7 @@ public function isTraitable() */ public function getDebugInfo() { - return array ( 43 => 1, 41 => 2, 34 => 1,); + return array ( 44 => 1, 42 => 2, 35 => 1,); } public function getSourceContext() @@ -223,6 +225,7 @@ public function getSourceContext() use Twig\Environment; use Twig\Error\LoaderError; use Twig\Error\RuntimeError; +use Twig\Extension\CoreExtension; use Twig\Extension\SandboxExtension; use Twig\Markup; use Twig\Sandbox\SecurityError; @@ -284,7 +287,7 @@ public function isTraitable() */ public function getDebugInfo() { - return array ( 43 => 2, 41 => 4, 34 => 2,); + return array ( 44 => 2, 42 => 4, 35 => 2,); } public function getSourceContext() diff --git a/tests/TemplateTest.php b/tests/TemplateTest.php index 9c2364c1d21..b14a0384a8f 100644 --- a/tests/TemplateTest.php +++ b/tests/TemplateTest.php @@ -14,6 +14,7 @@ use PHPUnit\Framework\TestCase; use Twig\Environment; use Twig\Error\RuntimeError; +use Twig\Extension\CoreExtension; use Twig\Extension\SandboxExtension; use Twig\Loader\ArrayLoader; use Twig\Loader\LoaderInterface; @@ -94,7 +95,7 @@ public function testGetAttributeWithSandbox($object, $item, $allowed) $template = new TemplateForTest($twig); try { - twig_get_attribute($twig, $template->getSourceContext(), $object, $item, [], 'any', false, false, true); + CoreExtension::getAttribute($twig, $template->getSourceContext(), $object, $item, [], 'any', false, false, true); if (!$allowed) { $this->fail(); @@ -180,14 +181,14 @@ public function testGetAttributeOnArrayWithConfusableKey() $this->assertSame('IntegerButStringWithLeadingZeros', $array['01']); $this->assertSame('EmptyString', $array[null]); - $this->assertSame('Zero', twig_get_attribute($twig, $template->getSourceContext(), $array, false), 'false is treated as 0 when accessing an array (equals PHP behavior)'); - $this->assertSame('One', twig_get_attribute($twig, $template->getSourceContext(), $array, true), 'true is treated as 1 when accessing an array (equals PHP behavior)'); - $this->assertSame('One', twig_get_attribute($twig, $template->getSourceContext(), $array, 1.5), 'float is casted to int when accessing an array (equals PHP behavior)'); - $this->assertSame('One', twig_get_attribute($twig, $template->getSourceContext(), $array, '1'), '"1" is treated as integer 1 when accessing an array (equals PHP behavior)'); - $this->assertSame('MinusOne', twig_get_attribute($twig, $template->getSourceContext(), $array, -1.5), 'negative float is casted to int when accessing an array (equals PHP behavior)'); - $this->assertSame('FloatButString', twig_get_attribute($twig, $template->getSourceContext(), $array, '1.5'), '"1.5" is treated as-is when accessing an array (equals PHP behavior)'); - $this->assertSame('IntegerButStringWithLeadingZeros', twig_get_attribute($twig, $template->getSourceContext(), $array, '01'), '"01" is treated as-is when accessing an array (equals PHP behavior)'); - $this->assertSame('EmptyString', twig_get_attribute($twig, $template->getSourceContext(), $array, null), 'null is treated as "" when accessing an array (equals PHP behavior)'); + $this->assertSame('Zero', CoreExtension::getAttribute($twig, $template->getSourceContext(), $array, false), 'false is treated as 0 when accessing an array (equals PHP behavior)'); + $this->assertSame('One', CoreExtension::getAttribute($twig, $template->getSourceContext(), $array, true), 'true is treated as 1 when accessing an array (equals PHP behavior)'); + $this->assertSame('One', CoreExtension::getAttribute($twig, $template->getSourceContext(), $array, 1.5), 'float is casted to int when accessing an array (equals PHP behavior)'); + $this->assertSame('One', CoreExtension::getAttribute($twig, $template->getSourceContext(), $array, '1'), '"1" is treated as integer 1 when accessing an array (equals PHP behavior)'); + $this->assertSame('MinusOne', CoreExtension::getAttribute($twig, $template->getSourceContext(), $array, -1.5), 'negative float is casted to int when accessing an array (equals PHP behavior)'); + $this->assertSame('FloatButString', CoreExtension::getAttribute($twig, $template->getSourceContext(), $array, '1.5'), '"1.5" is treated as-is when accessing an array (equals PHP behavior)'); + $this->assertSame('IntegerButStringWithLeadingZeros', CoreExtension::getAttribute($twig, $template->getSourceContext(), $array, '01'), '"01" is treated as-is when accessing an array (equals PHP behavior)'); + $this->assertSame('EmptyString', CoreExtension::getAttribute($twig, $template->getSourceContext(), $array, null), 'null is treated as "" when accessing an array (equals PHP behavior)'); } /** @@ -198,7 +199,7 @@ public function testGetAttribute($defined, $value, $object, $item, $arguments, $ $twig = new Environment($this->createMock(LoaderInterface::class)); $template = new TemplateForTest($twig); - $this->assertEquals($value, twig_get_attribute($twig, $template->getSourceContext(), $object, $item, $arguments, $type)); + $this->assertEquals($value, CoreExtension::getAttribute($twig, $template->getSourceContext(), $object, $item, $arguments, $type)); } /** @@ -210,13 +211,13 @@ public function testGetAttributeStrict($defined, $value, $object, $item, $argume $template = new TemplateForTest($twig); if ($defined) { - $this->assertEquals($value, twig_get_attribute($twig, $template->getSourceContext(), $object, $item, $arguments, $type)); + $this->assertEquals($value, CoreExtension::getAttribute($twig, $template->getSourceContext(), $object, $item, $arguments, $type)); } else { $this->expectException(RuntimeError::class); if (null !== $exceptionMessage) { $this->expectExceptionMessage($exceptionMessage); } - $this->assertEquals($value, twig_get_attribute($twig, $template->getSourceContext(), $object, $item, $arguments, $type)); + $this->assertEquals($value, CoreExtension::getAttribute($twig, $template->getSourceContext(), $object, $item, $arguments, $type)); } } @@ -228,7 +229,7 @@ public function testGetAttributeDefined($defined, $value, $object, $item, $argum $twig = new Environment($this->createMock(LoaderInterface::class)); $template = new TemplateForTest($twig); - $this->assertEquals($defined, twig_get_attribute($twig, $template->getSourceContext(), $object, $item, $arguments, $type, true)); + $this->assertEquals($defined, CoreExtension::getAttribute($twig, $template->getSourceContext(), $object, $item, $arguments, $type, true)); } /** @@ -239,7 +240,7 @@ public function testGetAttributeDefinedStrict($defined, $value, $object, $item, $twig = new Environment($this->createMock(LoaderInterface::class), ['strict_variables' => true]); $template = new TemplateForTest($twig); - $this->assertEquals($defined, twig_get_attribute($twig, $template->getSourceContext(), $object, $item, $arguments, $type, true)); + $this->assertEquals($defined, CoreExtension::getAttribute($twig, $template->getSourceContext(), $object, $item, $arguments, $type, true)); } public function testGetAttributeCallExceptions() @@ -249,7 +250,7 @@ public function testGetAttributeCallExceptions() $object = new TemplateMagicMethodExceptionObject(); - $this->assertNull(twig_get_attribute($twig, $template->getSourceContext(), $object, 'foo')); + $this->assertNull(CoreExtension::getAttribute($twig, $template->getSourceContext(), $object, 'foo')); } public function getGetAttributeTests() @@ -395,9 +396,9 @@ public function testGetIsMethods() $getIsObject = new TemplateGetIsMethods(); $template = new TemplateForTest($twig, 'index.twig'); // first time should not create a cache for "get" - $this->assertNull(twig_get_attribute($twig, $template->getSourceContext(), $getIsObject, 'get')); + $this->assertNull(CoreExtension::getAttribute($twig, $template->getSourceContext(), $getIsObject, 'get')); // 0 should be in the method cache now, so this should fail - $this->assertNull(twig_get_attribute($twig, $template->getSourceContext(), $getIsObject, 0)); + $this->assertNull(CoreExtension::getAttribute($twig, $template->getSourceContext(), $getIsObject, 0)); } } From aa7c454e2dccabb01343ff35cfadd5536b4b12d6 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sun, 10 Dec 2023 20:24:52 +0100 Subject: [PATCH 132/812] Fix CS --- .../cssinliner-extra/CssInlinerExtension.php | 2 +- .../cssinliner-extra/Resources/functions.php | 1 + .../Tests/LegacyFunctionsTest.php | 1 + extra/html-extra/Resources/functions.php | 1 + extra/inky-extra/Resources/functions.php | 1 + .../inky-extra/Tests/LegacyFunctionsTest.php | 1 + extra/markdown-extra/MarkdownExtension.php | 8 ++++---- extra/markdown-extra/Resources/functions.php | 1 + .../Tests/LegacyFunctionsTest.php | 4 +++- src/Extension/CoreExtension.php | 3 +-- src/Extension/DebugExtension.php | 10 +++++----- src/Extension/EscaperExtension.php | 19 +++++++++---------- src/Extension/StringLoaderExtension.php | 2 +- 13 files changed, 30 insertions(+), 24 deletions(-) diff --git a/extra/cssinliner-extra/CssInlinerExtension.php b/extra/cssinliner-extra/CssInlinerExtension.php index 679e9165716..2ceb2e08598 100644 --- a/extra/cssinliner-extra/CssInlinerExtension.php +++ b/extra/cssinliner-extra/CssInlinerExtension.php @@ -33,7 +33,7 @@ public static function inlineCss(string $body, string ...$css): string if (null === $inliner) { $inliner = new CssToInlineStyles(); } - + return $inliner->convert($body, implode("\n", $css)); } } diff --git a/extra/cssinliner-extra/Resources/functions.php b/extra/cssinliner-extra/Resources/functions.php index c4b32db2d95..60305e231c5 100644 --- a/extra/cssinliner-extra/Resources/functions.php +++ b/extra/cssinliner-extra/Resources/functions.php @@ -13,6 +13,7 @@ /** * @internal + * * @deprecated since Twig 3.9.0 */ function twig_inline_css(string $body, string ...$css): string diff --git a/extra/cssinliner-extra/Tests/LegacyFunctionsTest.php b/extra/cssinliner-extra/Tests/LegacyFunctionsTest.php index 20feef172c9..d62e267fdff 100644 --- a/extra/cssinliner-extra/Tests/LegacyFunctionsTest.php +++ b/extra/cssinliner-extra/Tests/LegacyFunctionsTest.php @@ -13,6 +13,7 @@ use PHPUnit\Framework\TestCase; use Twig\Extra\CssInliner\CssInlinerExtension; + use function Twig\Extra\CssInliner\twig_inline_css; /** diff --git a/extra/html-extra/Resources/functions.php b/extra/html-extra/Resources/functions.php index 4458f3bf8b7..ca18af1d344 100644 --- a/extra/html-extra/Resources/functions.php +++ b/extra/html-extra/Resources/functions.php @@ -13,6 +13,7 @@ /** * @internal + * * @deprecated since Twig 3.9.0 */ function twig_html_classes(...$args): string diff --git a/extra/inky-extra/Resources/functions.php b/extra/inky-extra/Resources/functions.php index feef69eabe8..0fa4111debb 100644 --- a/extra/inky-extra/Resources/functions.php +++ b/extra/inky-extra/Resources/functions.php @@ -13,6 +13,7 @@ /** * @internal + * * @deprecated since Twig 3.9.0 */ function twig_inky(string $body): string diff --git a/extra/inky-extra/Tests/LegacyFunctionsTest.php b/extra/inky-extra/Tests/LegacyFunctionsTest.php index 26ed1e76fe1..4810235b53f 100644 --- a/extra/inky-extra/Tests/LegacyFunctionsTest.php +++ b/extra/inky-extra/Tests/LegacyFunctionsTest.php @@ -13,6 +13,7 @@ use PHPUnit\Framework\TestCase; use Twig\Extra\Inky\InkyExtension; + use function Twig\Extra\Inky\twig_inky; /** diff --git a/extra/markdown-extra/MarkdownExtension.php b/extra/markdown-extra/MarkdownExtension.php index 1647b66ff70..6a245009556 100644 --- a/extra/markdown-extra/MarkdownExtension.php +++ b/extra/markdown-extra/MarkdownExtension.php @@ -31,21 +31,21 @@ public function getFilters() public static function htmlToMarkdown(string $body, array $options = []): string { static $converters; - + if (!class_exists(HtmlConverter::class)) { throw new \LogicException('You cannot use the "html_to_markdown" filter as league/html-to-markdown is not installed; try running "composer require league/html-to-markdown".'); } - + $options += [ 'hard_break' => true, 'strip_tags' => true, 'remove_nodes' => 'head style', ]; - + if (!isset($converters[$key = serialize($options)])) { $converters[$key] = new HtmlConverter($options); } - + return $converters[$key]->convert($body); } } diff --git a/extra/markdown-extra/Resources/functions.php b/extra/markdown-extra/Resources/functions.php index ad8da5a53b5..cf498364df0 100644 --- a/extra/markdown-extra/Resources/functions.php +++ b/extra/markdown-extra/Resources/functions.php @@ -13,6 +13,7 @@ /** * @internal + * * @deprecated since Twig 3.9.0 */ function html_to_markdown(string $body, array $options = []): string diff --git a/extra/markdown-extra/Tests/LegacyFunctionsTest.php b/extra/markdown-extra/Tests/LegacyFunctionsTest.php index fe0ecdaa800..19a861de3ef 100644 --- a/extra/markdown-extra/Tests/LegacyFunctionsTest.php +++ b/extra/markdown-extra/Tests/LegacyFunctionsTest.php @@ -12,9 +12,11 @@ namespace Twig\Extra\Markdown\Tests; use PHPUnit\Framework\TestCase; -use Twig\Extra\Markdown\MarkdownExtension; + use function Twig\Extra\Markdown\html_to_markdown; +use Twig\Extra\Markdown\MarkdownExtension; + /** * @group legacy */ diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index 8c9d0d4feff..ec90edbeab7 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -653,7 +653,6 @@ public static function arrayMerge(...$arrays) return $result; } - /** * Slices a variable. * @@ -1215,7 +1214,7 @@ public static function lowerFilter(Environment $env, $string) /** * Strips HTML and PHP tags from a string. * - * @param string|null $string + * @param string|null $string * @param string[]|string|null $string * * @return string diff --git a/src/Extension/DebugExtension.php b/src/Extension/DebugExtension.php index e023c7087b3..cefb44c5b8d 100644 --- a/src/Extension/DebugExtension.php +++ b/src/Extension/DebugExtension.php @@ -12,9 +12,9 @@ namespace Twig\Extension; use Twig\Environment; -use Twig\TwigFunction; use Twig\Template; use Twig\TemplateWrapper; +use Twig\TwigFunction; final class DebugExtension extends AbstractExtension { @@ -43,9 +43,9 @@ public static function dump(Environment $env, $context, ...$vars) if (!$env->isDebug()) { return; } - + ob_start(); - + if (!$vars) { $vars = []; foreach ($context as $key => $value) { @@ -53,12 +53,12 @@ public static function dump(Environment $env, $context, ...$vars) $vars[$key] = $value; } } - + var_dump($vars); } else { var_dump(...$vars); } - + return ob_get_clean(); } } diff --git a/src/Extension/EscaperExtension.php b/src/Extension/EscaperExtension.php index 88faa60a090..9a8c66c9047 100644 --- a/src/Extension/EscaperExtension.php +++ b/src/Extension/EscaperExtension.php @@ -21,7 +21,6 @@ use Twig\TokenParser\AutoEscapeTokenParser; use Twig\TwigFilter; - final class EscaperExtension extends AbstractExtension { private $defaultStrategy; @@ -177,7 +176,7 @@ public static function escapeFilterIsSafe(Node $filterArgs) * @param bool $autoescape Whether the function is called by the auto-escaping feature (true) or by the developer (false) * * @return string - * + * * @internal */ public static function escape(Environment $env, $string, $strategy = 'html', $charset = null, $autoescape = false) @@ -190,7 +189,7 @@ public static function escape(Environment $env, $string, $strategy = 'html', $ch if (\is_object($string) && method_exists($string, '__toString')) { if ($autoescape) { $c = \get_class($string); - $ext = $env->getExtension(EscaperExtension::class); + $ext = $env->getExtension(self::class); if (!isset($ext->safeClasses[$c])) { $ext->safeClasses[$c] = []; foreach (class_parents($string) + class_implements($string) as $class) { @@ -256,7 +255,7 @@ public static function escape(Environment $env, $string, $strategy = 'html', $ch return htmlspecialchars($string, \ENT_QUOTES | \ENT_SUBSTITUTE, $charset); } - $string = twig_convert_encoding($string, 'UTF-8', $charset); + $string = CoreExtension::convertEncoding($string, 'UTF-8', $charset); $string = htmlspecialchars($string, \ENT_QUOTES | \ENT_SUBSTITUTE, 'UTF-8'); return iconv('UTF-8', $charset, $string); @@ -265,7 +264,7 @@ public static function escape(Environment $env, $string, $strategy = 'html', $ch // escape all non-alphanumeric characters // into their \x or \uHHHH representations if ('UTF-8' !== $charset) { - $string = twig_convert_encoding($string, 'UTF-8', $charset); + $string = CoreExtension::convertEncoding($string, 'UTF-8', $charset); } if (!preg_match('//u', $string)) { @@ -316,7 +315,7 @@ public static function escape(Environment $env, $string, $strategy = 'html', $ch case 'css': if ('UTF-8' !== $charset) { - $string = twig_convert_encoding($string, 'UTF-8', $charset); + $string = CoreExtension::convertEncoding($string, 'UTF-8', $charset); } if (!preg_match('//u', $string)) { @@ -337,7 +336,7 @@ public static function escape(Environment $env, $string, $strategy = 'html', $ch case 'html_attr': if ('UTF-8' !== $charset) { - $string = twig_convert_encoding($string, 'UTF-8', $charset); + $string = CoreExtension::convertEncoding($string, 'UTF-8', $charset); } if (!preg_match('//u', $string)) { @@ -404,14 +403,14 @@ public static function escape(Environment $env, $string, $strategy = 'html', $ch return rawurlencode($string); default: - $escapers = $env->getExtension(EscaperExtension::class)->getEscapers(); + $escapers = $env->getExtension(self::class)->getEscapers(); if (\array_key_exists($strategy, $escapers)) { return $escapers[$strategy]($env, $string, $charset); } - $validStrategies = implode(', ', array_merge(['html', 'js', 'url', 'css', 'html_attr'], array_keys($escapers))); + $validStrategies = implode('", "', array_merge(['html', 'js', 'url', 'css', 'html_attr'], array_keys($escapers))); - throw new RuntimeError(sprintf('Invalid escaping strategy "%s" (valid ones: %s).', $strategy, $validStrategies)); + throw new RuntimeError(sprintf('Invalid escaping strategy "%s" (valid ones: "%s").', $strategy, $validStrategies)); } } } diff --git a/src/Extension/StringLoaderExtension.php b/src/Extension/StringLoaderExtension.php index fde395dd810..9b25d9a554c 100644 --- a/src/Extension/StringLoaderExtension.php +++ b/src/Extension/StringLoaderExtension.php @@ -31,7 +31,7 @@ public function getFunctions(): array * * @param string $template A template as a string or object implementing __toString() * @param string $name An optional name of the template to be used in error messages - * + * * @internal */ public static function templateFromString(Environment $env, $template, string $name = null): TemplateWrapper From dd24d5bfd262e27588b81ed47d4ff2fd3ac692a4 Mon Sep 17 00:00:00 2001 From: Louis-Arnaud Date: Tue, 12 Dec 2023 10:52:25 +0100 Subject: [PATCH 133/812] add docs & links --- doc/filters/batch.rst | 51 ++++++++++++++++++++++++++++++++++-------- doc/filters/date.rst | 5 +++-- doc/filters/keys.rst | 6 +++++ doc/filters/lower.rst | 6 +++++ doc/filters/reduce.rst | 6 +++++ doc/filters/slice.rst | 24 +++++++++++++++++++- doc/filters/upper.rst | 6 +++++ 7 files changed, 92 insertions(+), 12 deletions(-) diff --git a/doc/filters/batch.rst b/doc/filters/batch.rst index 18a227feb39..adb2948c6a3 100644 --- a/doc/filters/batch.rst +++ b/doc/filters/batch.rst @@ -12,8 +12,8 @@ missing items: {% for row in items|batch(3, 'No item') %} - {% for column in row %} - + {% for index, column in row %} + {% endfor %} {% endfor %} @@ -25,14 +25,47 @@ The above example will be rendered as:
{{ column }}{{ index }} - {{ column }}
- - - + + + - - - + + + + +
abc0 - a1 - b2 - c
dNo itemNo item3 - d4 - No item5 - No item
+ +If you choose to set the third parameter ``preserve_keys`` to ``false``, the keys will be reset in each loop. + +.. code-block:: html+twig + + {% set items = ['a', 'b', 'c', 'd'] %} + + + {% for row in items|batch(3, 'No item', false) %} + + {% for index, column in row %} + + {% endfor %} + + {% endfor %} +
{{ index }} - {{ column }}
+ +The above example will be rendered as: + +.. code-block:: html+twig + + + + + + + + + + +
0 - a1 - b2 - c
0 - d1 - No item2 - No item
@@ -41,4 +74,4 @@ Arguments * ``size``: The size of the batch; fractional numbers will be rounded up * ``fill``: Used to fill in missing items -* ``preserve_keys``: Whether to preserve keys or not +* ``preserve_keys``: Whether to preserve keys or not (defaults to ``true``) diff --git a/doc/filters/date.rst b/doc/filters/date.rst index 70470f0f4d2..7ac9b8750a3 100644 --- a/doc/filters/date.rst +++ b/doc/filters/date.rst @@ -47,7 +47,7 @@ Timezone By default, the date is displayed by applying the default timezone (the one specified in php.ini or declared in Twig -- see below), but you can override -it by explicitly specifying a timezone: +it by explicitly specifying a supported `timezone`_: .. code-block:: twig @@ -68,7 +68,7 @@ The default timezone can also be set globally by calling ``setTimezone()``:: Arguments --------- -* ``format``: The date format +* ``format``: The date format (default format is ``F j, Y H:i``, which will render as ``January 11, 2024 15:17``) * ``timezone``: The date timezone .. _`strtotime`: https://www.php.net/strtotime @@ -76,3 +76,4 @@ Arguments .. _`DateInterval`: https://www.php.net/DateInterval .. _`date`: https://www.php.net/date .. _`DateInterval::format`: https://www.php.net/DateInterval.format +.. _`timezone`: https://www.php.net/manual/en/timezones.php diff --git a/doc/filters/keys.rst b/doc/filters/keys.rst index 58609471720..6bed8291e15 100644 --- a/doc/filters/keys.rst +++ b/doc/filters/keys.rst @@ -9,3 +9,9 @@ iterate over the keys of an array: {% for key in array|keys %} ... {% endfor %} + +.. note:: + + Internally, Twig uses the PHP `array_keys`_ function. + +.. _`array_keys`: https://www.php.net/array_keys diff --git a/doc/filters/lower.rst b/doc/filters/lower.rst index c0a0e0cddf4..1dff6039e10 100644 --- a/doc/filters/lower.rst +++ b/doc/filters/lower.rst @@ -8,3 +8,9 @@ The ``lower`` filter converts a value to lowercase: {{ 'WELCOME'|lower }} {# outputs 'welcome' #} + +.. note:: + + Internally, Twig uses the PHP `mb_strtolower`_ function. + +.. _`mb_strtolower`: https://www.php.net/manual/fr/function.mb-strtolower.php diff --git a/doc/filters/reduce.rst b/doc/filters/reduce.rst index 72c68d0deb9..ff529cdc83c 100644 --- a/doc/filters/reduce.rst +++ b/doc/filters/reduce.rst @@ -27,3 +27,9 @@ Arguments * ``arrow``: The arrow function * ``initial``: The initial value + +.. note:: + + Internally, Twig uses the PHP `array_reduce`_ function. + +.. _`array_reduce`: https://www.php.net/array_reduce diff --git a/doc/filters/slice.rst b/doc/filters/slice.rst index eb43d99461a..3642b85d830 100644 --- a/doc/filters/slice.rst +++ b/doc/filters/slice.rst @@ -54,6 +54,28 @@ negative then the sequence will stop that many elements from the end of the variable. If it is omitted, then the sequence will have everything from offset up until the end of the variable. +The argument ``preserve_keys`` is used to reset the index during the loop. + +.. code-block:: twig + + {% for key, value in [1, 2, 3, 4, 5]|slice(1, 2, true) %} + {{ key }} - {{ value }} + {% endfor %} + + {# output + 1 - 2 + 2 - 3 + #} + + {% for key, value in [1, 2, 3, 4, 5]|slice(1, 2) %} + {{ key }} - {{ value }} + {% endfor %} + + {# output + 0 - 2 + 1 - 3 + #} + .. note:: It also works with objects implementing the `Traversable`_ interface. @@ -63,7 +85,7 @@ Arguments * ``start``: The start of the slice * ``length``: The size of the slice -* ``preserve_keys``: Whether to preserve key or not (when the input is an array) +* ``preserve_keys``: Whether to preserve key or not (when the input is an array), by default the value is ``false``. .. _`Traversable`: https://www.php.net/manual/en/class.traversable.php .. _`array_slice`: https://www.php.net/array_slice diff --git a/doc/filters/upper.rst b/doc/filters/upper.rst index 01c9fbb0b53..2ca7cbeb55d 100644 --- a/doc/filters/upper.rst +++ b/doc/filters/upper.rst @@ -8,3 +8,9 @@ The ``upper`` filter converts a value to uppercase: {{ 'welcome'|upper }} {# outputs 'WELCOME' #} + +.. note:: + + Internally, Twig uses the PHP `mb_strtoupper`_ function. + +.. _`mb_strtoupper`: https://www.php.net/mb_strtoupper From a18da1614adb4ee0156208d6ea144f0716f5e1fa Mon Sep 17 00:00:00 2001 From: Yaakov Saxon Date: Thu, 14 Sep 2023 12:11:37 -0400 Subject: [PATCH 134/812] Add SourcePolicyInterface to selectively enable the Sandbox based on a template's Source --- src/Extension/SandboxExtension.php | 28 +++++++++++++----- src/Node/CheckSecurityNode.php | 3 +- src/Sandbox/SourcePolicyInterface.php | 24 +++++++++++++++ tests/Extension/SandboxTest.php | 42 +++++++++++++++++++++++++-- 4 files changed, 85 insertions(+), 12 deletions(-) create mode 100644 src/Sandbox/SourcePolicyInterface.php diff --git a/src/Extension/SandboxExtension.php b/src/Extension/SandboxExtension.php index dca3262a432..2230810359f 100644 --- a/src/Extension/SandboxExtension.php +++ b/src/Extension/SandboxExtension.php @@ -15,6 +15,7 @@ use Twig\Sandbox\SecurityNotAllowedMethodError; use Twig\Sandbox\SecurityNotAllowedPropertyError; use Twig\Sandbox\SecurityPolicyInterface; +use Twig\Sandbox\SourcePolicyInterface; use Twig\Source; use Twig\TokenParser\SandboxTokenParser; @@ -23,11 +24,13 @@ final class SandboxExtension extends AbstractExtension private $sandboxedGlobally; private $sandboxed; private $policy; + private $sourcePolicy; - public function __construct(SecurityPolicyInterface $policy, $sandboxed = false) + public function __construct(SecurityPolicyInterface $policy, $sandboxed = false, SourcePolicyInterface $sourcePolicy = null) { $this->policy = $policy; $this->sandboxedGlobally = $sandboxed; + $this->sourcePolicy = $sourcePolicy; } public function getTokenParsers() @@ -50,9 +53,9 @@ public function disableSandbox() $this->sandboxed = false; } - public function isSandboxed() + public function isSandboxed(Source $source = null) { - return $this->sandboxedGlobally || $this->sandboxed; + return $this->sandboxedGlobally || $this->sandboxed || $this->isSourceSandboxed($source); } public function isSandboxedGlobally() @@ -60,6 +63,15 @@ public function isSandboxedGlobally() return $this->sandboxedGlobally; } + private function isSourceSandboxed(?Source $source): bool + { + if (null === $source || null === $this->sourcePolicy) { + return false; + } + + return $this->sourcePolicy->enableSandbox($source); + } + public function setSecurityPolicy(SecurityPolicyInterface $policy) { $this->policy = $policy; @@ -70,16 +82,16 @@ public function getSecurityPolicy() return $this->policy; } - public function checkSecurity($tags, $filters, $functions) + public function checkSecurity($tags, $filters, $functions, Source $source = null) { - if ($this->isSandboxed()) { + if ($this->isSandboxed($source)) { $this->policy->checkSecurity($tags, $filters, $functions); } } public function checkMethodAllowed($obj, $method, int $lineno = -1, Source $source = null) { - if ($this->isSandboxed()) { + if ($this->isSandboxed($source)) { try { $this->policy->checkMethodAllowed($obj, $method); } catch (SecurityNotAllowedMethodError $e) { @@ -93,7 +105,7 @@ public function checkMethodAllowed($obj, $method, int $lineno = -1, Source $sour public function checkPropertyAllowed($obj, $property, int $lineno = -1, Source $source = null) { - if ($this->isSandboxed()) { + if ($this->isSandboxed($source)) { try { $this->policy->checkPropertyAllowed($obj, $property); } catch (SecurityNotAllowedPropertyError $e) { @@ -107,7 +119,7 @@ public function checkPropertyAllowed($obj, $property, int $lineno = -1, Source $ public function ensureToStringAllowed($obj, int $lineno = -1, Source $source = null) { - if ($this->isSandboxed() && \is_object($obj) && method_exists($obj, '__toString')) { + if ($this->isSandboxed($source) && \is_object($obj) && method_exists($obj, '__toString')) { try { $this->policy->checkMethodAllowed($obj, '__toString'); } catch (SecurityNotAllowedMethodError $e) { diff --git a/src/Node/CheckSecurityNode.php b/src/Node/CheckSecurityNode.php index 489a3652ddf..7b2981bbe53 100644 --- a/src/Node/CheckSecurityNode.php +++ b/src/Node/CheckSecurityNode.php @@ -58,7 +58,8 @@ public function compile(Compiler $compiler) ->indent() ->write(!$tags ? "[],\n" : "['".implode("', '", array_keys($tags))."'],\n") ->write(!$filters ? "[],\n" : "['".implode("', '", array_keys($filters))."'],\n") - ->write(!$functions ? "[]\n" : "['".implode("', '", array_keys($functions))."']\n") + ->write(!$functions ? "[],\n" : "['".implode("', '", array_keys($functions))."'],\n") + ->write("\$this->source\n") ->outdent() ->write(");\n") ->outdent() diff --git a/src/Sandbox/SourcePolicyInterface.php b/src/Sandbox/SourcePolicyInterface.php new file mode 100644 index 00000000000..b952f1ea6fa --- /dev/null +++ b/src/Sandbox/SourcePolicyInterface.php @@ -0,0 +1,24 @@ +load('1_childobj_childmethod')->render(self::$params); } catch (SecurityError $e) { $this->fail('checkMethodAllowed is exiting prematurely after matching a parent class and not seeing a method allowed on a child class later in the list'); - } + } try { $twig_child_first->load('1_childobj_parentmethod')->render(self::$params); @@ -449,15 +450,50 @@ public function testMultipleClassMatchesViaInheritanceInAllowedMethods() } } - protected function getEnvironment($sandboxed, $options, $templates, $tags = [], $filters = [], $methods = [], $properties = [], $functions = []) + protected function getEnvironment($sandboxed, $options, $templates, $tags = [], $filters = [], $methods = [], $properties = [], $functions = [], $sourcePolicy = null) { $loader = new ArrayLoader($templates); $twig = new Environment($loader, array_merge(['debug' => true, 'cache' => false, 'autoescape' => false], $options)); $policy = new SecurityPolicy($tags, $filters, $methods, $properties, $functions); - $twig->addExtension(new SandboxExtension($policy, $sandboxed)); + $twig->addExtension(new SandboxExtension($policy, $sandboxed, $sourcePolicy)); return $twig; } + + public function testSandboxSourcePolicyEnableReturningFalse() + { + $twig = $this->getEnvironment(false, [], self::$templates, [], [], [], [], [], new class() implements \Twig\Sandbox\SourcePolicyInterface { + public function enableSandbox(Source $source): bool + { + return '1_basic' != $source->getName(); + } + }); + $this->assertEquals('FOO', $twig->load('1_basic')->render(self::$params)); + } + + public function testSandboxSourcePolicyEnableReturningTrue() + { + $twig = $this->getEnvironment(false, [], self::$templates, [], [], [], [], [], new class() implements \Twig\Sandbox\SourcePolicyInterface { + public function enableSandbox(Source $source): bool + { + return '1_basic' === $source->getName(); + } + }); + $this->expectException(SecurityError::class); + $twig->load('1_basic')->render([]); + } + + public function testSandboxSourcePolicyFalseDoesntOverrideOtherEnables() + { + $twig = $this->getEnvironment(true, [], self::$templates, [], [], [], [], [], new class() implements \Twig\Sandbox\SourcePolicyInterface { + public function enableSandbox(Source $source): bool + { + return false; + } + }); + $this->expectException(SecurityError::class); + $twig->load('1_basic')->render([]); + } } class ParentClass From fca89954f15912b17d16402808ea6115db0e59dd Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Wed, 20 Dec 2023 20:26:16 +0100 Subject: [PATCH 135/812] Simplify tests --- tests/Extension/SandboxTest.php | 33 +++++++++++---------------------- 1 file changed, 11 insertions(+), 22 deletions(-) diff --git a/tests/Extension/SandboxTest.php b/tests/Extension/SandboxTest.php index b0eba837976..2b7711ab156 100644 --- a/tests/Extension/SandboxTest.php +++ b/tests/Extension/SandboxTest.php @@ -84,8 +84,7 @@ public function testSandboxUnallowedMethodAccessor() try { $twig->load('1_basic1')->render(self::$params); $this->fail('Sandbox throws a SecurityError exception if an unallowed method is called'); - } catch (SecurityError $e) { - $this->assertInstanceOf(SecurityNotAllowedMethodError::class, $e, 'Exception should be an instance of Twig_Sandbox_SecurityNotAllowedMethodError'); + } catch (SecurityNotAllowedMethodError $e) { $this->assertEquals('Twig\Tests\Extension\FooObject', $e->getClassName(), 'Exception should be raised on the "Twig\Tests\Extension\FooObject" class'); $this->assertEquals('foo', $e->getMethodName(), 'Exception should be raised on the "foo" method'); } @@ -110,8 +109,7 @@ public function testSandboxGloballyFalseUnallowedFilterWithIncludeTemplateFromSt try { $twig->load('1_basic2_include_template_from_string_sandboxed')->render(self::$params); $this->fail('Sandbox throws a SecurityError exception if an unallowed filter is called'); - } catch (SecurityError $e) { - $this->assertInstanceOf('\Twig\Sandbox\SecurityNotAllowedFilterError', $e, 'Exception should be an instance of Twig_Sandbox_SecurityNotAllowedFilterError'); + } catch (SecurityNotAllowedFilterError $e) { $this->assertEquals('upper', $e->getFilterName(), 'Exception should be raised on the "upper" filter'); } } @@ -123,8 +121,7 @@ public function testSandboxGloballyTrueUnallowedFilterWithIncludeTemplateFromStr try { $twig->load('1_basic2_include_template_from_string_sandboxed')->render(self::$params); $this->fail('Sandbox throws a SecurityError exception if an unallowed filter is called'); - } catch (SecurityError $e) { - $this->assertInstanceOf('\Twig\Sandbox\SecurityNotAllowedFilterError', $e, 'Exception should be an instance of Twig_Sandbox_SecurityNotAllowedFilterError'); + } catch (SecurityNotAllowedFilterError $e) { $this->assertEquals('upper', $e->getFilterName(), 'Exception should be raised on the "upper" filter'); } } @@ -143,8 +140,7 @@ public function testSandboxGloballyTrueUnallowedFilterWithIncludeTemplateFromStr try { $twig->load('1_basic2_include_template_from_string')->render(self::$params); $this->fail('Sandbox throws a SecurityError exception if an unallowed filter is called'); - } catch (SecurityError $e) { - $this->assertInstanceOf('\Twig\Sandbox\SecurityNotAllowedFilterError', $e, 'Exception should be an instance of Twig_Sandbox_SecurityNotAllowedFilterError'); + } catch (SecurityNotAllowedFilterError $e) { $this->assertEquals('upper', $e->getFilterName(), 'Exception should be raised on the "upper" filter'); } } @@ -155,8 +151,7 @@ public function testSandboxUnallowedFilter() try { $twig->load('1_basic2')->render(self::$params); $this->fail('Sandbox throws a SecurityError exception if an unallowed filter is called'); - } catch (SecurityError $e) { - $this->assertInstanceOf(SecurityNotAllowedFilterError::class, $e, 'Exception should be an instance of Twig_Sandbox_SecurityNotAllowedFilterError'); + } catch (SecurityNotAllowedFilterError $e) { $this->assertEquals('upper', $e->getFilterName(), 'Exception should be raised on the "upper" filter'); } } @@ -167,8 +162,7 @@ public function testSandboxUnallowedTag() try { $twig->load('1_basic3')->render(self::$params); $this->fail('Sandbox throws a SecurityError exception if an unallowed tag is used in the template'); - } catch (SecurityError $e) { - $this->assertInstanceOf(SecurityNotAllowedTagError::class, $e, 'Exception should be an instance of Twig_Sandbox_SecurityNotAllowedTagError'); + } catch (SecurityNotAllowedTagError $e) { $this->assertEquals('if', $e->getTagName(), 'Exception should be raised on the "if" tag'); } } @@ -179,8 +173,7 @@ public function testSandboxUnallowedProperty() try { $twig->load('1_basic4')->render(self::$params); $this->fail('Sandbox throws a SecurityError exception if an unallowed property is called in the template'); - } catch (SecurityError $e) { - $this->assertInstanceOf(SecurityNotAllowedPropertyError::class, $e, 'Exception should be an instance of Twig_Sandbox_SecurityNotAllowedPropertyError'); + } catch (SecurityNotAllowedPropertyError $e) { $this->assertEquals('Twig\Tests\Extension\FooObject', $e->getClassName(), 'Exception should be raised on the "Twig\Tests\Extension\FooObject" class'); $this->assertEquals('bar', $e->getPropertyName(), 'Exception should be raised on the "bar" property'); } @@ -195,8 +188,7 @@ public function testSandboxUnallowedToString($template) try { $twig->load('index')->render(self::$params); $this->fail('Sandbox throws a SecurityError exception if an unallowed method (__toString()) is called in the template'); - } catch (SecurityError $e) { - $this->assertInstanceOf(SecurityNotAllowedMethodError::class, $e, 'Exception should be an instance of Twig_Sandbox_SecurityNotAllowedMethodError'); + } catch (SecurityNotAllowedMethodError $e) { $this->assertEquals('Twig\Tests\Extension\FooObject', $e->getClassName(), 'Exception should be raised on the "Twig\Tests\Extension\FooObject" class'); $this->assertEquals('__tostring', $e->getMethodName(), 'Exception should be raised on the "__toString" method'); } @@ -268,8 +260,7 @@ public function testSandboxUnallowedFunction() try { $twig->load('1_basic7')->render(self::$params); $this->fail('Sandbox throws a SecurityError exception if an unallowed function is called in the template'); - } catch (SecurityError $e) { - $this->assertInstanceOf(SecurityNotAllowedFunctionError::class, $e, 'Exception should be an instance of Twig_Sandbox_SecurityNotAllowedFunctionError'); + } catch (SecurityNotAllowedFunctionError $e) { $this->assertEquals('cycle', $e->getFunctionName(), 'Exception should be raised on the "cycle" function'); } } @@ -280,8 +271,7 @@ public function testSandboxUnallowedRangeOperator() try { $twig->load('1_range_operator')->render(self::$params); $this->fail('Sandbox throws a SecurityError exception if the unallowed range operator is called'); - } catch (SecurityError $e) { - $this->assertInstanceOf(SecurityNotAllowedFunctionError::class, $e, 'Exception should be an instance of Twig_Sandbox_SecurityNotAllowedFunctionError'); + } catch (SecurityNotAllowedFunctionError $e) { $this->assertEquals('range', $e->getFunctionName(), 'Exception should be raised on the "range" function'); } } @@ -355,8 +345,7 @@ public function testSandboxLocallySetForAnInclude() try { $twig->load('3_basic')->render(self::$params); $this->fail('Sandbox throws a SecurityError exception when the included file is sandboxed'); - } catch (SecurityError $e) { - $this->assertInstanceOf(SecurityNotAllowedTagError::class, $e, 'Exception should be an instance of Twig_Sandbox_SecurityNotAllowedTagError'); + } catch (SecurityNotAllowedTagError $e) { $this->assertEquals('sandbox', $e->getTagName()); } } From e6a98a000f7b6f737d2f8161e09c984a11b9ad94 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Wed, 20 Dec 2023 20:28:40 +0100 Subject: [PATCH 136/812] Fix test without any assertions --- tests/Extension/SandboxTest.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/Extension/SandboxTest.php b/tests/Extension/SandboxTest.php index 2b7711ab156..cbe6175787b 100644 --- a/tests/Extension/SandboxTest.php +++ b/tests/Extension/SandboxTest.php @@ -437,6 +437,8 @@ public function testMultipleClassMatchesViaInheritanceInAllowedMethods() } catch (SecurityError $e) { $this->fail('checkMethodAllowed is exiting prematurely after matching a child class and not seeing a method allowed on its parent class later in the list'); } + + $this->expectNotToPerformAssertions(); } protected function getEnvironment($sandboxed, $options, $templates, $tags = [], $filters = [], $methods = [], $properties = [], $functions = [], $sourcePolicy = null) From bc22e38ce600d8bc5ecb85c2b16320a7a45d84ea Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Wed, 20 Dec 2023 20:34:40 +0100 Subject: [PATCH 137/812] Fix bad merge --- src/Extension/SandboxExtension.php | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/Extension/SandboxExtension.php b/src/Extension/SandboxExtension.php index 4a877c0beb8..c942682d18a 100644 --- a/src/Extension/SandboxExtension.php +++ b/src/Extension/SandboxExtension.php @@ -82,11 +82,7 @@ public function getSecurityPolicy(): SecurityPolicyInterface return $this->policy; } -<<<<<<< HEAD - public function checkSecurity($tags, $filters, $functions): void -======= - public function checkSecurity($tags, $filters, $functions, Source $source = null) ->>>>>>> 2.x + public function checkSecurity($tags, $filters, $functions, Source $source = null): void { if ($this->isSandboxed($source)) { $this->policy->checkSecurity($tags, $filters, $functions); From 0b9e30e3d2ed2c65df1937152d9c84da6fe52aa8 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Wed, 20 Dec 2023 20:23:01 +0100 Subject: [PATCH 138/812] Simplify code --- src/Template.php | 11 ++++++++++- src/TemplateWrapper.php | 19 +------------------ tests/TemplateTest.php | 8 +------- 3 files changed, 12 insertions(+), 26 deletions(-) diff --git a/src/Template.php b/src/Template.php index ffbaae1ea1c..a45bf6e1e06 100644 --- a/src/Template.php +++ b/src/Template.php @@ -235,12 +235,21 @@ public function renderParentBlock($name, array $context, array $blocks = []) */ public function renderBlock($name, array $context, array $blocks = [], $useBlocks = true) { + $level = ob_get_level(); if ($this->env->isDebug()) { ob_start(); } else { ob_start(function () { return ''; }); } - $this->displayBlock($name, $context, $blocks, $useBlocks); + try { + $this->displayBlock($name, $context, $blocks, $useBlocks); + } catch (\Throwable $e) { + while (ob_get_level() > $level) { + ob_end_clean(); + } + + throw $e; + } return ob_get_clean(); } diff --git a/src/TemplateWrapper.php b/src/TemplateWrapper.php index 1ecd82251f3..e94e983ce65 100644 --- a/src/TemplateWrapper.php +++ b/src/TemplateWrapper.php @@ -60,24 +60,7 @@ public function getBlockNames(array $context = []): array public function renderBlock(string $name, array $context = []): string { - $context = $this->env->mergeGlobals($context); - $level = ob_get_level(); - if ($this->env->isDebug()) { - ob_start(); - } else { - ob_start(function () { return ''; }); - } - try { - $this->template->displayBlock($name, $context); - } catch (\Throwable $e) { - while (ob_get_level() > $level) { - ob_end_clean(); - } - - throw $e; - } - - return ob_get_clean(); + return $this->template->renderBlock($name, $this->env->mergeGlobals($context)); } public function displayBlock(string $name, array $context = []) diff --git a/tests/TemplateTest.php b/tests/TemplateTest.php index b14a0384a8f..154f0e8c07a 100644 --- a/tests/TemplateTest.php +++ b/tests/TemplateTest.php @@ -130,13 +130,7 @@ public function testRenderBlockWithUndefinedBlock() $twig = new Environment($this->createMock(LoaderInterface::class)); $template = new TemplateForTest($twig, 'index.twig'); - try { - $template->renderBlock('unknown', []); - } catch (\Exception $e) { - ob_end_clean(); - - throw $e; - } + $template->renderBlock('unknown', []); } public function testDisplayBlockWithUndefinedBlock() From d77586da99b5df2fd993f5bf0bd24b6d7f43e933 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Fri, 22 Dec 2023 08:34:46 +0100 Subject: [PATCH 139/812] Update CHANGELOG --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index 17caf366e03..55a307c39b3 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -4,6 +4,7 @@ * Fix premature loop exit in Security Policy lookup of allowed methods/properties * Deprecate all internal extension functions in favor of methods on the extension classes * Mark all extension functions as @internal + * Add SourcePolicyInterface to selectively enable the Sandbox based on a template's Source # 3.8.0 (2023-11-21) From 4c621adb490f5be4649d804da46fed6d554dca9e Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Fri, 22 Dec 2023 08:35:50 +0100 Subject: [PATCH 140/812] Bump version --- src/Environment.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Environment.php b/src/Environment.php index 5329b143ee2..77588720b72 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -40,11 +40,11 @@ */ class Environment { - public const VERSION = '3.8.1-DEV'; - public const VERSION_ID = 30801; + public const VERSION = '3.9.0-DEV'; + public const VERSION_ID = 30900; public const MAJOR_VERSION = 3; - public const MINOR_VERSION = 8; - public const RELEASE_VERSION = 1; + public const MINOR_VERSION = 9; + public const RELEASE_VERSION = 0; public const EXTRA_VERSION = 'DEV'; private $charset; From 3963851a41c357c0b237cf53737105e018c6e757 Mon Sep 17 00:00:00 2001 From: Malte Wunsch Date: Mon, 19 Jun 2023 19:53:08 +0200 Subject: [PATCH 141/812] Improve error message It is not obvious that you cannot use the Twig function `constant` to get the special `::class` constants of classes like so: `{{ constant('Twig\\Extension\\CoreExtension::class') }}`, due to the underlying PHP function `\constant()`. The previous error message in this case: 'Constant "Twig\Extension\CoreExtension::class" is undefined.' was not very helpful. --- src/Extension/CoreExtension.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index ec90edbeab7..2a5cf028932 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -1456,6 +1456,10 @@ public static function constant($constant, $object = null) } if (!\defined($constant)) { + if ('::class' === strtolower(substr($constant, -7))) { + throw new RuntimeError(sprintf('You cannot use the Twig function "constant()" to access "%s". You could provide an object and call constant("class", $object) or use the class name directly as a string.', $constant)); + } + throw new RuntimeError(sprintf('Constant "%s" is undefined.', $constant)); } From 44b3bac55204bec711be3817aa498cc34f0662b7 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sun, 24 Dec 2023 09:32:26 +0100 Subject: [PATCH 142/812] Fix intl tests --- .../Tests/Fixtures/language_names.test | 8 ++++---- .../intl-extra/Tests/Fixtures/locale_names.test | 8 ++++---- .../Tests/Fixtures/timezone_names.test | 8 ++++---- extra/intl-extra/Tests/IntlExtensionTest.php | 17 ++++++++++------- 4 files changed, 22 insertions(+), 19 deletions(-) diff --git a/extra/intl-extra/Tests/Fixtures/language_names.test b/extra/intl-extra/Tests/Fixtures/language_names.test index bd30607f8a4..ce5a95f49a3 100644 --- a/extra/intl-extra/Tests/Fixtures/language_names.test +++ b/extra/intl-extra/Tests/Fixtures/language_names.test @@ -2,15 +2,15 @@ "language_names" function --TEMPLATE-- {{ language_names('UNKNOWN')|length }} -{{ language_names()|length }} -{{ language_names('fr')|length }} +{{ language_names()|length > 600 ? 'ok' : 'ko' }} +{{ language_names('fr')|length > 600 ? 'ok' : 'ko' }} {{ language_names()['fr'] }} {{ language_names('fr')['fr'] }} --DATA-- return []; --EXPECT-- 0 -634 -634 +ok +ok French français diff --git a/extra/intl-extra/Tests/Fixtures/locale_names.test b/extra/intl-extra/Tests/Fixtures/locale_names.test index f7e830f6693..202e43aae45 100644 --- a/extra/intl-extra/Tests/Fixtures/locale_names.test +++ b/extra/intl-extra/Tests/Fixtures/locale_names.test @@ -2,15 +2,15 @@ "locale_names" function --TEMPLATE-- {{ locale_names('UNKNOWN')|length }} -{{ locale_names()|length }} -{{ locale_names('fr')|length }} +{{ locale_names()|length > 600 ? 'ok' : 'ko' }} +{{ locale_names('fr')|length > 600 ? 'ok' : 'ko' }} {{ locale_names()['fr'] }} {{ locale_names('fr')['fr'] }} --DATA-- return []; --EXPECT-- 0 -637 -637 +ok +ok French français diff --git a/extra/intl-extra/Tests/Fixtures/timezone_names.test b/extra/intl-extra/Tests/Fixtures/timezone_names.test index 51b50704c38..c8ee51c6b00 100644 --- a/extra/intl-extra/Tests/Fixtures/timezone_names.test +++ b/extra/intl-extra/Tests/Fixtures/timezone_names.test @@ -2,15 +2,15 @@ "timezone_names" function --TEMPLATE-- {{ timezone_names('UNKNOWN')|length }} -{{ timezone_names()|length }} -{{ timezone_names('fr')|length }} +{{ timezone_names()|length > 400 ? 'ok' : 'ko' }} +{{ timezone_names('fr')|length > 400 ? 'ok' : 'ko' }} {{ timezone_names()['Europe/Paris'] }} {{ timezone_names('fr')['Europe/Paris'] }} --DATA-- return []; --EXPECT-- 0 -434 -434 +ok +ok Central European Time (Paris) heure d’Europe centrale (Paris) diff --git a/extra/intl-extra/Tests/IntlExtensionTest.php b/extra/intl-extra/Tests/IntlExtensionTest.php index 6afa7036b75..688a415f4d0 100644 --- a/extra/intl-extra/Tests/IntlExtensionTest.php +++ b/extra/intl-extra/Tests/IntlExtensionTest.php @@ -25,8 +25,8 @@ public function testFormatterWithoutProto() $env = new Environment(new ArrayLoader()); $this->assertSame('12.346', $ext->formatNumber('12.3456')); - $this->assertSame( - 'Feb 20, 2020, 1:37:00 PM', + $this->assertStringStartsWith( + 'Feb 20, 2020, 1:37:00', $ext->formatDateTime($env, new \DateTime('2020-02-20T13:37:00+00:00')) ); } @@ -39,8 +39,8 @@ public function testFormatterWithoutProtoFallsBackToCoreExtensionTimezone() // so it has a fixed difference to UTC $env->getExtension(CoreExtension::class)->setTimezone('EET'); - $this->assertSame( - 'Feb 20, 2020, 3:37:00 PM', + $this->assertStringStartsWith( + 'Feb 20, 2020, 3:37:00', $ext->formatDateTime($env, new \DateTime('2020-02-20T13:37:00+00:00', new \DateTimeZone('UTC'))) ); } @@ -55,9 +55,12 @@ public function testFormatterProto() $env = new Environment(new ArrayLoader()); $this->assertSame('++12,3', $ext->formatNumber('12.3456')); - $this->assertSame( - 'jeudi 20 février 2020 à 14:37:00 heure normale d’Europe centrale', - $ext->formatDateTime($env, new \DateTime('2020-02-20T13:37:00+00:00')) + $this->assertContains( + $ext->formatDateTime($env, new \DateTime('2020-02-20T13:37:00+00:00', new \DateTimeZone('Europe/Paris'))), + [ + 'jeudi 20 février 2020 à 13:37:00 heure normale d’Europe centrale', + 'jeudi 20 février 2020 à 13:37:00 temps universel coordonné', + ] ); } From 6e2f2655a88a84ccd03cd030641666c0c9caa19b Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sun, 24 Dec 2023 09:54:44 +0100 Subject: [PATCH 143/812] Add missing dots at the end of exception messages --- tests/IntegrationTest.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/IntegrationTest.php b/tests/IntegrationTest.php index 8dd5e3bf7f8..e2b211a01de 100644 --- a/tests/IntegrationTest.php +++ b/tests/IntegrationTest.php @@ -272,7 +272,7 @@ public function is_multi_word($value) public function __call($method, $arguments) { if ('magicCall' !== $method) { - throw new \BadMethodCallException('Unexpected call to __call'); + throw new \BadMethodCallException('Unexpected call to __call.'); } return 'magic_'.$arguments[0]; @@ -281,7 +281,7 @@ public function __call($method, $arguments) public static function __callStatic($method, $arguments) { if ('magicStaticCall' !== $method) { - throw new \BadMethodCallException('Unexpected call to __callStatic'); + throw new \BadMethodCallException('Unexpected call to __callStatic.'); } return 'static_magic_'.$arguments[0]; @@ -296,7 +296,7 @@ class MagicCallStub { public function __call($name, $args) { - throw new \Exception('__call shall not be called'); + throw new \Exception('__call shall not be called.'); } } @@ -339,7 +339,7 @@ public function count(): int public function __toString() { - throw new \Exception('__toString shall not be called on \Countables'); + throw new \Exception('__toString shall not be called on \Countables.'); } } From 6ebbc3059325e0f30a5408c60afc1516af69123a Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Wed, 27 Dec 2023 16:11:54 +0100 Subject: [PATCH 144/812] Abstract node capture in its own Node --- extra/cache-extra/Node/CacheNode.php | 16 ++--- .../TokenParser/CacheTokenParser.php | 8 ++- extra/cache-extra/composer.json | 2 +- src/Node/CaptureNode.php | 63 +++++++++++++++++++ src/Node/MacroNode.php | 24 ++----- src/Node/SetNode.php | 40 +++++------- tests/Node/MacroTest.php | 18 +++--- tests/Node/SetTest.php | 13 +++- 8 files changed, 118 insertions(+), 66 deletions(-) create mode 100644 src/Node/CaptureNode.php diff --git a/extra/cache-extra/Node/CacheNode.php b/extra/cache-extra/Node/CacheNode.php index f79873aa9a5..de6d964fd3c 100644 --- a/extra/cache-extra/Node/CacheNode.php +++ b/extra/cache-extra/Node/CacheNode.php @@ -12,13 +12,17 @@ namespace Twig\Extra\Cache\Node; use Twig\Compiler; +use Twig\Node\CaptureNode; use Twig\Node\Expression\AbstractExpression; use Twig\Node\Node; -class CacheNode extends Node +class CacheNode extends AbstractExpression { public function __construct(AbstractExpression $key, ?AbstractExpression $ttl, ?AbstractExpression $tags, Node $body, int $lineno, string $tag) { + $body = new CaptureNode($body, $lineno); + $body->setAttribute('raw', true); + $nodes = ['key' => $key, 'body' => $body]; if (null !== $ttl) { $nodes['ttl'] = $ttl; @@ -34,7 +38,7 @@ public function compile(Compiler $compiler): void { $compiler ->addDebugInfo($this) - ->write('$cached = $this->env->getRuntime(\'Twig\Extra\Cache\CacheRuntime\')->getCache()->get(') + ->raw('$this->env->getRuntime(\'Twig\Extra\Cache\CacheRuntime\')->getCache()->get(') ->subcompile($this->getNode('key')) ->raw(", function (\Symfony\Contracts\Cache\ItemInterface \$item) use (\$context, \$macros) {\n") ->indent() @@ -57,13 +61,11 @@ public function compile(Compiler $compiler): void } $compiler - ->write("ob_start(function () { return ''; });\n") + ->write('return ') ->subcompile($this->getNode('body')) - ->write("\n") - ->write("return ob_get_clean();\n") + ->raw(";\n") ->outdent() - ->write("});\n") - ->write("echo '' === \$cached ? '' : new Markup(\$cached, \$this->env->getCharset());\n") + ->write("})\n") ; } } diff --git a/extra/cache-extra/TokenParser/CacheTokenParser.php b/extra/cache-extra/TokenParser/CacheTokenParser.php index cb50b72d369..5590ab27441 100644 --- a/extra/cache-extra/TokenParser/CacheTokenParser.php +++ b/extra/cache-extra/TokenParser/CacheTokenParser.php @@ -13,7 +13,10 @@ use Twig\Error\SyntaxError; use Twig\Extra\Cache\Node\CacheNode; +use Twig\Node\Expression\ConstantExpression; +use Twig\Node\Expression\FilterExpression; use Twig\Node\Node; +use Twig\Node\PrintNode; use Twig\Token; use Twig\TokenParser\AbstractTokenParser; @@ -54,7 +57,10 @@ public function parse(Token $token): Node $body = $this->parser->subparse([$this, 'decideCacheEnd'], true); $stream->expect(Token::BLOCK_END_TYPE); - return new CacheNode($key, $ttl, $tags, $body, $token->getLine(), $this->getTag()); + $body = new CacheNode($key, $ttl, $tags, $body, $token->getLine(), $this->getTag()); + $body = new FilterExpression($body, new ConstantExpression('raw', $token->getLine()), new Node(), $token->getLine()); + + return new PrintNode($body, $token->getLine(), $this->getTag()); } public function decideCacheEnd(Token $token): bool diff --git a/extra/cache-extra/composer.json b/extra/cache-extra/composer.json index 04177a419de..ec2b4896632 100644 --- a/extra/cache-extra/composer.json +++ b/extra/cache-extra/composer.json @@ -17,7 +17,7 @@ "require": { "php": ">=7.2.5", "symfony/cache": "^5.0|^6.0|^7.0", - "twig/twig": "^3.0" + "twig/twig": "^3.9" }, "require-dev": { "symfony/phpunit-bridge": "^6.4|^7.0" diff --git a/src/Node/CaptureNode.php b/src/Node/CaptureNode.php new file mode 100644 index 00000000000..dae441c8d23 --- /dev/null +++ b/src/Node/CaptureNode.php @@ -0,0 +1,63 @@ + + */ +class CaptureNode extends Node +{ + public function __construct(Node $body, int $lineno, string $tag = null) + { + parent::__construct(['body' => $body], ['raw' => false, 'with_blocks' => false], $lineno, $tag); + } + + public function compile(Compiler $compiler): void + { + if ($this->getAttribute('with_blocks')) { + $compiler->raw("(function () use (&\$context, \$macros, \$blocks) {\n"); + } else { + $compiler->raw("(function () use (&\$context, \$macros) {\n"); + } + $compiler->indent(); + if ($compiler->getEnvironment()->isDebug()) { + $compiler->write("ob_start();\n"); + } else { + $compiler->write("ob_start(function () { return ''; });\n"); + } + $compiler + ->write("try {\n") + ->indent() + ->subcompile($this->getNode('body')) + ->raw("\n") + ; + if ($this->getAttribute('raw')) { + $compiler->write("return ob_get_contents();\n"); + } else { + $compiler->write("return ('' === \$tmp = ob_get_contents()) ? '' : new Markup(\$tmp, \$this->env->getCharset());\n"); + } + $compiler + ->outdent() + ->write("} finally {\n") + ->indent() + ->write("ob_end_clean();\n") + ->outdent() + ->write("}\n") + ->outdent() + ->write('})()') + ; + } +} diff --git a/src/Node/MacroNode.php b/src/Node/MacroNode.php index 7f1b24d5372..4f645eb2bb0 100644 --- a/src/Node/MacroNode.php +++ b/src/Node/MacroNode.php @@ -31,6 +31,8 @@ public function __construct(string $name, Node $body, Node $arguments, int $line } } + $body = new CaptureNode($body, $lineno, $tag); + parent::__construct(['body' => $body, 'arguments' => $arguments], ['name' => $name], $lineno, $tag); } @@ -81,31 +83,13 @@ public function compile(Compiler $compiler): void ->write('') ->string(self::VARARGS_NAME) ->raw(' => ') - ; - - $compiler ->raw("\$__varargs__,\n") ->outdent() ->write("]);\n\n") ->write("\$blocks = [];\n\n") - ; - if ($compiler->getEnvironment()->isDebug()) { - $compiler->write("ob_start();\n"); - } else { - $compiler->write("ob_start(function () { return ''; });\n"); - } - $compiler - ->write("try {\n") - ->indent() + ->write('return ') ->subcompile($this->getNode('body')) - ->raw("\n") - ->write("return ('' === \$tmp = ob_get_contents()) ? '' : new Markup(\$tmp, \$this->env->getCharset());\n") - ->outdent() - ->write("} finally {\n") - ->indent() - ->write("ob_end_clean();\n") - ->outdent() - ->write("}\n") + ->raw(";\n") ->outdent() ->write("}\n\n") ; diff --git a/src/Node/SetNode.php b/src/Node/SetNode.php index 96b6bd8bf58..9559193bd55 100644 --- a/src/Node/SetNode.php +++ b/src/Node/SetNode.php @@ -23,22 +23,24 @@ class SetNode extends Node implements NodeCaptureInterface { public function __construct(bool $capture, Node $names, Node $values, int $lineno, string $tag = null) { - parent::__construct(['names' => $names, 'values' => $values], ['capture' => $capture, 'safe' => false], $lineno, $tag); - /* * Optimizes the node when capture is used for a large block of text. * * {% set foo %}foo{% endset %} is compiled to $context['foo'] = new Twig\Markup("foo"); */ - if ($this->getAttribute('capture')) { - $this->setAttribute('safe', true); - - $values = $this->getNode('values'); + $safe = false; + if ($capture) { + $safe = true; if ($values instanceof TextNode) { - $this->setNode('values', new ConstantExpression($values->getAttribute('data'), $values->getTemplateLine())); - $this->setAttribute('capture', false); + $values = new ConstantExpression($values->getAttribute('data'), $values->getTemplateLine()); + $capture = false; + } else { + $values = new CaptureNode($values, $values->getTemplateLine()); + $values->setAttribute('with_blocks', true); } } + + parent::__construct(['names' => $names, 'values' => $values], ['capture' => $capture, 'safe' => $safe], $lineno, $tag); } public function compile(Compiler $compiler): void @@ -56,27 +58,13 @@ public function compile(Compiler $compiler): void } $compiler->raw(')'); } else { - if ($this->getAttribute('capture')) { - if ($compiler->getEnvironment()->isDebug()) { - $compiler->write("ob_start();\n"); - } else { - $compiler->write("ob_start(function () { return ''; });\n"); - } - $compiler - ->subcompile($this->getNode('values')) - ; - } - $compiler->subcompile($this->getNode('names'), false); - - if ($this->getAttribute('capture')) { - $compiler->raw(" = ('' === \$tmp = ob_get_clean()) ? '' : new Markup(\$tmp, \$this->env->getCharset())"); - } } + $compiler->raw(' = '); - if (!$this->getAttribute('capture')) { - $compiler->raw(' = '); - + if ($this->getAttribute('capture')) { + $compiler->subcompile($this->getNode('values')); + } else { if (\count($this->getNode('names')) > 1) { $compiler->write('['); foreach ($this->getNode('values') as $idx => $value) { diff --git a/tests/Node/MacroTest.php b/tests/Node/MacroTest.php index 88800b6e5c3..f11bf167c19 100644 --- a/tests/Node/MacroTest.php +++ b/tests/Node/MacroTest.php @@ -26,7 +26,7 @@ public function testConstructor() $arguments = new Node([new NameExpression('foo', 1)], [], 1); $node = new MacroNode('foo', $body, $arguments, 1); - $this->assertEquals($body, $node->getNode('body')); + $this->assertEquals($body, $node->getNode('body')->getNode('body')); $this->assertEquals($arguments, $node->getNode('arguments')); $this->assertEquals('foo', $node->getAttribute('name')); } @@ -54,14 +54,16 @@ public function macro_foo(\$__foo__ = null, \$__bar__ = "Foo", ...\$__varargs__) \$blocks = []; - ob_start(function () { return ''; }); - try { - echo "foo"; + return (function () use (&\$context, \$macros) { + ob_start(function () { return ''; }); + try { + echo "foo"; - return ('' === \$tmp = ob_get_contents()) ? '' : new Markup(\$tmp, \$this->env->getCharset()); - } finally { - ob_end_clean(); - } + return ('' === \$tmp = ob_get_contents()) ? '' : new Markup(\$tmp, \$this->env->getCharset()); + } finally { + ob_end_clean(); + } + })(); } EOF ], diff --git a/tests/Node/SetTest.php b/tests/Node/SetTest.php index 370af95f2cd..d3754cc2938 100644 --- a/tests/Node/SetTest.php +++ b/tests/Node/SetTest.php @@ -51,9 +51,16 @@ public function getTests() $node = new SetNode(true, $names, $values, 1); $tests[] = [$node, <<env->getCharset()); +\$context["foo"] = (function () use (&\$context, \$macros, \$blocks) { + ob_start(function () { return ''; }); + try { + echo "foo"; + + return ('' === \$tmp = ob_get_contents()) ? '' : new Markup(\$tmp, \$this->env->getCharset()); + } finally { + ob_end_clean(); + } +})(); EOF ]; From 665dce899f1d4c579fc54ee7378739d9fd7a9a10 Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Sun, 31 Dec 2023 10:32:09 +0100 Subject: [PATCH 145/812] rework the CaptureNode implementation --- src/Node/CaptureNode.php | 2 +- src/Node/MacroNode.php | 6 ++---- src/Node/SetNode.php | 4 +++- tests/Node/MacroTest.php | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Node/CaptureNode.php b/src/Node/CaptureNode.php index dae441c8d23..418f0863de5 100644 --- a/src/Node/CaptureNode.php +++ b/src/Node/CaptureNode.php @@ -57,7 +57,7 @@ public function compile(Compiler $compiler): void ->outdent() ->write("}\n") ->outdent() - ->write('})()') + ->write('})();') ; } } diff --git a/src/Node/MacroNode.php b/src/Node/MacroNode.php index 4f645eb2bb0..63fb8dea56a 100644 --- a/src/Node/MacroNode.php +++ b/src/Node/MacroNode.php @@ -31,8 +31,6 @@ public function __construct(string $name, Node $body, Node $arguments, int $line } } - $body = new CaptureNode($body, $lineno, $tag); - parent::__construct(['body' => $body, 'arguments' => $arguments], ['name' => $name], $lineno, $tag); } @@ -88,8 +86,8 @@ public function compile(Compiler $compiler): void ->write("]);\n\n") ->write("\$blocks = [];\n\n") ->write('return ') - ->subcompile($this->getNode('body')) - ->raw(";\n") + ->subcompile(new CaptureNode($this->getNode('body'), $this->getNode('body')->lineno, $this->getNode('body')->tag)) + ->raw("\n") ->outdent() ->write("}\n\n") ; diff --git a/src/Node/SetNode.php b/src/Node/SetNode.php index 9559193bd55..1d33cc775b0 100644 --- a/src/Node/SetNode.php +++ b/src/Node/SetNode.php @@ -86,8 +86,10 @@ public function compile(Compiler $compiler): void $compiler->subcompile($this->getNode('values')); } } + + $compiler->raw(';'); } - $compiler->raw(";\n"); + $compiler->raw("\n"); } } diff --git a/tests/Node/MacroTest.php b/tests/Node/MacroTest.php index f11bf167c19..bd7140b8859 100644 --- a/tests/Node/MacroTest.php +++ b/tests/Node/MacroTest.php @@ -26,7 +26,7 @@ public function testConstructor() $arguments = new Node([new NameExpression('foo', 1)], [], 1); $node = new MacroNode('foo', $body, $arguments, 1); - $this->assertEquals($body, $node->getNode('body')->getNode('body')); + $this->assertEquals($body, $node->getNode('body')); $this->assertEquals($arguments, $node->getNode('arguments')); $this->assertEquals('foo', $node->getAttribute('name')); } From 9c0897fa2ce1f67afa714e2ec1d9b78ae8949fff Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Fri, 5 Jan 2024 09:48:24 +0100 Subject: [PATCH 146/812] Remove usage of list() in favor of [] --- src/Node/SetNode.php | 4 ++-- tests/Node/SetTest.php | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Node/SetNode.php b/src/Node/SetNode.php index 1d33cc775b0..4057c6a75ba 100644 --- a/src/Node/SetNode.php +++ b/src/Node/SetNode.php @@ -48,7 +48,7 @@ public function compile(Compiler $compiler): void $compiler->addDebugInfo($this); if (\count($this->getNode('names')) > 1) { - $compiler->write('list('); + $compiler->write('['); foreach ($this->getNode('names') as $idx => $node) { if ($idx) { $compiler->raw(', '); @@ -56,7 +56,7 @@ public function compile(Compiler $compiler): void $compiler->subcompile($node); } - $compiler->raw(')'); + $compiler->raw(']'); } else { $compiler->subcompile($this->getNode('names'), false); } diff --git a/tests/Node/SetTest.php b/tests/Node/SetTest.php index d3754cc2938..c639be6a494 100644 --- a/tests/Node/SetTest.php +++ b/tests/Node/SetTest.php @@ -78,7 +78,7 @@ public function getTests() $node = new SetNode(false, $names, $values, 1); $tests[] = [$node, <<getVariableGetter('bar')}]; +[\$context["foo"], \$context["bar"]] = ["foo", {$this->getVariableGetter('bar')}]; EOF ]; From af8ec178b6d53ef1455a20ccc8f0cd349856c44d Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Fri, 5 Jan 2024 12:46:24 +0100 Subject: [PATCH 147/812] Remove list() usage in code --- src/ExpressionParser.php | 2 +- src/Lexer.php | 6 +++--- src/Loader/FilesystemLoader.php | 2 +- src/Node/Expression/CallExpression.php | 2 +- src/Profiler/Profile.php | 2 +- src/Test/IntegrationTestCase.php | 2 +- src/TokenParser/EmbedTokenParser.php | 2 +- src/TokenParser/IncludeTokenParser.php | 2 +- 8 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/ExpressionParser.php b/src/ExpressionParser.php index 13e0f0876ed..912d0a930a5 100644 --- a/src/ExpressionParser.php +++ b/src/ExpressionParser.php @@ -704,7 +704,7 @@ private function parseNotTestExpression(Node $node): NotUnary private function parseTestExpression(Node $node): TestExpression { $stream = $this->parser->getStream(); - list($name, $test) = $this->getTest($node->getTemplateLine()); + [$name, $test] = $this->getTest($node->getTemplateLine()); $class = $this->getTestNodeClass($test); $arguments = null; diff --git a/src/Lexer.php b/src/Lexer.php index b23080f58e0..9e4d6119eb7 100644 --- a/src/Lexer.php +++ b/src/Lexer.php @@ -210,7 +210,7 @@ public function tokenize(Source $source): TokenStream $this->pushToken(/* Token::EOF_TYPE */ -1); if (!empty($this->brackets)) { - list($expect, $lineno) = array_pop($this->brackets); + [$expect, $lineno] = array_pop($this->brackets); throw new SyntaxError(sprintf('Unclosed "%s".', $expect), $lineno, $this->source); } @@ -356,7 +356,7 @@ private function lexExpression(): void throw new SyntaxError(sprintf('Unexpected "%s".', $this->code[$this->cursor]), $this->lineno, $this->source); } - list($expect, $lineno) = array_pop($this->brackets); + [$expect, $lineno] = array_pop($this->brackets); if ($this->code[$this->cursor] != strtr($expect, '([{', ')]}')) { throw new SyntaxError(sprintf('Unclosed "%s".', $expect), $lineno, $this->source); } @@ -426,7 +426,7 @@ private function lexString(): void $this->pushToken(/* Token::STRING_TYPE */ 7, stripcslashes($match[0])); $this->moveCursor($match[0]); } elseif (preg_match(self::REGEX_DQ_STRING_DELIM, $this->code, $match, 0, $this->cursor)) { - list($expect, $lineno) = array_pop($this->brackets); + [$expect, $lineno] = array_pop($this->brackets); if ('"' != $this->code[$this->cursor]) { throw new SyntaxError(sprintf('Unclosed "%s".', $expect), $lineno, $this->source); } diff --git a/src/Loader/FilesystemLoader.php b/src/Loader/FilesystemLoader.php index 1073a406a06..1b277fe2fc4 100644 --- a/src/Loader/FilesystemLoader.php +++ b/src/Loader/FilesystemLoader.php @@ -183,7 +183,7 @@ protected function findTemplate(string $name, bool $throw = true) } try { - list($namespace, $shortname) = $this->parseName($name); + [$namespace, $shortname] = $this->parseName($name); $this->validateName($shortname); } catch (LoaderError $e) { diff --git a/src/Node/Expression/CallExpression.php b/src/Node/Expression/CallExpression.php index 3a2d7a4fca4..5c7326bf411 100644 --- a/src/Node/Expression/CallExpression.php +++ b/src/Node/Expression/CallExpression.php @@ -140,7 +140,7 @@ protected function getArguments($callable, $arguments) throw new \LogicException($message); } - list($callableParameters, $isPhpVariadic) = $this->getCallableParameters($callable, $isVariadic); + [$callableParameters, $isPhpVariadic] = $this->getCallableParameters($callable, $isVariadic); $arguments = []; $names = []; $missingArguments = []; diff --git a/src/Profiler/Profile.php b/src/Profiler/Profile.php index 7979a23c67a..72506b7c8da 100644 --- a/src/Profiler/Profile.php +++ b/src/Profiler/Profile.php @@ -176,6 +176,6 @@ public function __serialize(): array */ public function __unserialize(array $data): void { - list($this->template, $this->name, $this->type, $this->starts, $this->ends, $this->profiles) = $data; + [$this->template, $this->name, $this->type, $this->starts, $this->ends, $this->profiles] = $data; } } diff --git a/src/Test/IntegrationTestCase.php b/src/Test/IntegrationTestCase.php index e97ad417062..90ff39aadaf 100644 --- a/src/Test/IntegrationTestCase.php +++ b/src/Test/IntegrationTestCase.php @@ -234,7 +234,7 @@ protected function doIntegrationTest($file, $message, $condition, $templates, $e } if (false !== $exception) { - list($class) = explode(':', $exception); + [$class] = explode(':', $exception); $constraintClass = class_exists('PHPUnit\Framework\Constraint\Exception') ? 'PHPUnit\Framework\Constraint\Exception' : 'PHPUnit_Framework_Constraint_Exception'; $this->assertThat(null, new $constraintClass($class)); } diff --git a/src/TokenParser/EmbedTokenParser.php b/src/TokenParser/EmbedTokenParser.php index 64b4f296f27..adf683cc19e 100644 --- a/src/TokenParser/EmbedTokenParser.php +++ b/src/TokenParser/EmbedTokenParser.php @@ -30,7 +30,7 @@ public function parse(Token $token): Node $parent = $this->parser->getExpressionParser()->parseExpression(); - list($variables, $only, $ignoreMissing) = $this->parseArguments(); + [$variables, $only, $ignoreMissing] = $this->parseArguments(); $parentToken = $fakeParentToken = new Token(/* Token::STRING_TYPE */ 7, '__parent__', $token->getLine()); if ($parent instanceof ConstantExpression) { diff --git a/src/TokenParser/IncludeTokenParser.php b/src/TokenParser/IncludeTokenParser.php index 28beb8ae477..fda5bfd8c0a 100644 --- a/src/TokenParser/IncludeTokenParser.php +++ b/src/TokenParser/IncludeTokenParser.php @@ -31,7 +31,7 @@ public function parse(Token $token): Node { $expr = $this->parser->getExpressionParser()->parseExpression(); - list($variables, $only, $ignoreMissing) = $this->parseArguments(); + [$variables, $only, $ignoreMissing] = $this->parseArguments(); return new IncludeNode($expr, $variables, $only, $ignoreMissing, $token->getLine(), $this->getTag()); } From c44bd1ff627d98dfd2ee1ed944af004b656d534e Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Wed, 10 Jan 2024 20:22:00 +0100 Subject: [PATCH 148/812] Optimize TextNodes --- src/NodeVisitor/OptimizerNodeVisitor.php | 43 ++++++++++++++++++++++++ tests/ExpressionParserTest.php | 3 -- 2 files changed, 43 insertions(+), 3 deletions(-) diff --git a/src/NodeVisitor/OptimizerNodeVisitor.php b/src/NodeVisitor/OptimizerNodeVisitor.php index 6b39f00947c..9f9b81cbfd5 100644 --- a/src/NodeVisitor/OptimizerNodeVisitor.php +++ b/src/NodeVisitor/OptimizerNodeVisitor.php @@ -24,6 +24,7 @@ use Twig\Node\IncludeNode; use Twig\Node\Node; use Twig\Node\PrintNode; +use Twig\Node\TextNode; /** * Tries to optimize the AST. @@ -43,6 +44,7 @@ final class OptimizerNodeVisitor implements NodeVisitorInterface public const OPTIMIZE_NONE = 0; public const OPTIMIZE_FOR = 2; public const OPTIMIZE_RAW_FILTER = 4; + public const OPTIMIZE_TEXT_NODES = 8; private $loops = []; private $loopsTargets = []; @@ -81,6 +83,42 @@ public function leaveNode(Node $node, Environment $env): ?Node $node = $this->optimizePrintNode($node); + if (self::OPTIMIZE_TEXT_NODES === (self::OPTIMIZE_TEXT_NODES & $this->optimizers)) { + $node = $this->mergeTextNodeCalls($node); + } + + return $node; + } + + private function mergeTextNodeCalls(Node $node): Node + { + $text = ''; + $names = []; + foreach ($node as $k => $n) { + if (!$n instanceof TextNode) { + return $node; + } + + $text .= $n->getAttribute('data'); + $names[] = $k; + } + + if (!$text) { + return $node; + } + + if (Node::class === get_class($node)) { + return new TextNode($text, $node->getTemplateLine()); + } + + foreach ($names as $i => $name) { + if (0 === $i) { + $node->setNode($name, new TextNode($text, $node->getTemplateLine())); + } else { + $node->removeNode($name); + } + } + return $node; } @@ -98,6 +136,11 @@ private function optimizePrintNode(Node $node): Node } $exprNode = $node->getNode('expr'); + + if ($exprNode instanceof ConstantExpression && is_string($exprNode->getAttribute('value'))) { + return new TextNode($exprNode->getAttribute('value'), $exprNode->getTemplateLine()); + } + if ( $exprNode instanceof BlockReferenceExpression || $exprNode instanceof ParentExpression diff --git a/tests/ExpressionParserTest.php b/tests/ExpressionParserTest.php index ab02296b641..afd1884d8ee 100644 --- a/tests/ExpressionParserTest.php +++ b/tests/ExpressionParserTest.php @@ -223,9 +223,6 @@ public function testStringExpression($template, $expected) public function getTestsForString() { return [ - [ - '{{ "foo" }}', new ConstantExpression('foo', 1), - ], [ '{{ "foo #{bar}" }}', new ConcatBinary( new ConstantExpression('foo ', 1), From d53509d4579e12b3603b9147b104d2169c3281b2 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Thu, 11 Jan 2024 08:19:14 +0100 Subject: [PATCH 149/812] Add tests when a template doesn't have output nodes --- tests/TemplateTest.php | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/TemplateTest.php b/tests/TemplateTest.php index 154f0e8c07a..32d3babede2 100644 --- a/tests/TemplateTest.php +++ b/tests/TemplateTest.php @@ -123,6 +123,24 @@ public function getGetAttributeWithSandbox() ]; } + /** + * @dataProvider getRenderTemplateWithoutOutputData + */ + public function testRenderTemplateWithoutOutput(string $template) + { + $twig = new Environment(new ArrayLoader(['index' => $template])); + $this->assertSame('', $twig->render('index')); + } + + public function getRenderTemplateWithoutOutputData() + { + return [ + [''], + ['{% for var in [] %}{% endfor %}'], + ['{% if false %}{% endif %}'], + ]; + } + public function testRenderBlockWithUndefinedBlock() { $this->expectException(RuntimeError::class); From d21666f728749a91be3a020ead1bc781bc637dd2 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Thu, 11 Jan 2024 21:45:07 +0100 Subject: [PATCH 150/812] Throws proper Twig exception when using cycle on an empty array --- src/Extension/CoreExtension.php | 4 ++++ tests/Fixtures/functions/cycle_empty.test | 8 ++++++++ 2 files changed, 12 insertions(+) create mode 100644 tests/Fixtures/functions/cycle_empty.test diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index 2a5cf028932..c4e0bdab228 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -329,6 +329,10 @@ public static function cycle($values, $position) return $values; } + if (!\count($values)) { + throw new RuntimeError('The "cycle" function does not work on empty arrays'); + } + return $values[$position % \count($values)]; } diff --git a/tests/Fixtures/functions/cycle_empty.test b/tests/Fixtures/functions/cycle_empty.test new file mode 100644 index 00000000000..1eaffc8e5f6 --- /dev/null +++ b/tests/Fixtures/functions/cycle_empty.test @@ -0,0 +1,8 @@ +--TEST-- +"cycle" function returns an error on empty arrays +--TEMPLATE-- +{{ cycle([], 0) }} +--DATA-- +return [] +--EXCEPTION-- +Twig\Error\RuntimeError: The "cycle" function does not work on empty arrays in "index.twig" at line 2 From 9ef031efdcb4c6329cbbd910bdc9872f7facc6ae Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Fri, 12 Jan 2024 08:20:25 +0100 Subject: [PATCH 151/812] Remove an obsolete comment --- src/Environment.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Environment.php b/src/Environment.php index 77588720b72..f9e0086c60a 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -249,7 +249,6 @@ public function setCache($cache) * * * The cache key for the given template; * * The currently enabled extensions; - * * Whether the Twig C extension is available or not; * * PHP version; * * Twig version; * * Options with what environment was created. From 284a5106510296f60471c4009bf2600e2b482562 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Mon, 22 Jan 2024 18:49:50 +0100 Subject: [PATCH 152/812] Bump actions/cache for CI --- .github/workflows/documentation.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml index 9519f5d121d..8fe7c868c29 100644 --- a/.github/workflows/documentation.yml +++ b/.github/workflows/documentation.yml @@ -33,7 +33,7 @@ jobs: run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT - name: Cache dependencies - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ${{ steps.composercache.outputs.dir }} key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} From 1ed424d565631869381e451f5868829b8f0f3e1f Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Mon, 22 Jan 2024 18:56:14 +0100 Subject: [PATCH 153/812] Clarify the behavior when a property/method does not exist --- doc/api.rst | 2 ++ doc/templates.rst | 17 +++++++++++------ 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/doc/api.rst b/doc/api.rst index 3186e293d26..73452e53c3c 100644 --- a/doc/api.rst +++ b/doc/api.rst @@ -99,6 +99,8 @@ The following options are available: the ``auto_reload`` option, it will be determined automatically based on the ``debug`` value. +.. _environment_options_strict_variables: + * ``strict_variables`` *boolean* If set to ``false``, Twig will silently ignore invalid diff --git a/doc/templates.rst b/doc/templates.rst index ccfea484cf9..0486c52e3c7 100644 --- a/doc/templates.rst +++ b/doc/templates.rst @@ -90,6 +90,13 @@ PHP object, or items of a PHP array): variable but the print statement. When accessing variables inside tags, don't put the braces around them. +If a variable or attribute does not exist, the behavior depends on the +``strict_variables`` option value (see :ref:`environment options +`): + +* When ``false``, it returns ``null``; +* When ``true``, it throws an exception. + .. sidebar:: Implementation For convenience's sake ``foo.bar`` does the following things on the PHP @@ -102,17 +109,15 @@ PHP object, or items of a PHP array): * if not, and if ``foo`` is an object, check that ``getBar`` is a valid method; * if not, and if ``foo`` is an object, check that ``isBar`` is a valid method; * if not, and if ``foo`` is an object, check that ``hasBar`` is a valid method; - * if not, return a ``null`` value. + * if not, and if ``strict_variables`` is ``false``, return ``null``; + * if not, throw an exception. Twig also supports a specific syntax for accessing items on PHP arrays, ``foo['bar']``: * check if ``foo`` is an array and ``bar`` a valid element; - * if not, return a ``null`` value. - -If a variable or attribute does not exist, you will receive a ``null`` value -when the ``strict_variables`` option is set to ``false``; alternatively, if ``strict_variables`` -is set, Twig will throw an error (see :ref:`environment options`). + * if not, and if ``strict_variables`` is ``false``, return ``null``; + * if not, throw an exception. .. note:: From b1a8feaa55358b2b0f741a0abf5ae3e5b01aa902 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Thu, 25 Jan 2024 08:19:28 +0100 Subject: [PATCH 154/812] Remove support for Twig 2.x and unmaintained Symfony versions --- composer.json | 2 +- extra/cache-extra/composer.json | 2 +- extra/html-extra/composer.json | 2 +- extra/intl-extra/composer.json | 2 +- extra/string-extra/composer.json | 2 +- extra/twig-extra-bundle/composer.json | 16 ++++++++-------- 6 files changed, 13 insertions(+), 13 deletions(-) diff --git a/composer.json b/composer.json index 1e422dbbafe..57ba92964f7 100644 --- a/composer.json +++ b/composer.json @@ -31,7 +31,7 @@ "symfony/polyfill-ctype": "^1.8" }, "require-dev": { - "symfony/phpunit-bridge": "^5.4.9|^6.3|^7.0", + "symfony/phpunit-bridge": "^5.4.9|^6.4|^7.0", "psr/container": "^1.0|^2.0" }, "autoload": { diff --git a/extra/cache-extra/composer.json b/extra/cache-extra/composer.json index ec2b4896632..97fce8b81c9 100644 --- a/extra/cache-extra/composer.json +++ b/extra/cache-extra/composer.json @@ -16,7 +16,7 @@ ], "require": { "php": ">=7.2.5", - "symfony/cache": "^5.0|^6.0|^7.0", + "symfony/cache": "^5.4|^6.4|^7.0", "twig/twig": "^3.9" }, "require-dev": { diff --git a/extra/html-extra/composer.json b/extra/html-extra/composer.json index ca7b2f62cf4..21592fb9ccc 100644 --- a/extra/html-extra/composer.json +++ b/extra/html-extra/composer.json @@ -17,7 +17,7 @@ "require": { "php": ">=7.2.5", "symfony/deprecation-contracts": "^2.5|^3", - "symfony/mime": "^5.4|^6.0|^7.0", + "symfony/mime": "^5.4|^6.4|^7.0", "twig/twig": "^3.0" }, "require-dev": { diff --git a/extra/intl-extra/composer.json b/extra/intl-extra/composer.json index 208347077a2..c39239164bc 100644 --- a/extra/intl-extra/composer.json +++ b/extra/intl-extra/composer.json @@ -17,7 +17,7 @@ "require": { "php": ">=7.2.5", "twig/twig": "^3.0", - "symfony/intl": "^5.4|^6.0|^7.0" + "symfony/intl": "^5.4|^6.4|^7.0" }, "require-dev": { "symfony/phpunit-bridge": "^6.4|^7.0" diff --git a/extra/string-extra/composer.json b/extra/string-extra/composer.json index 77050a11785..4539faf0c2d 100644 --- a/extra/string-extra/composer.json +++ b/extra/string-extra/composer.json @@ -16,7 +16,7 @@ ], "require": { "php": ">=7.2.5", - "symfony/string": "^5.4|^6.0|^7.0", + "symfony/string": "^5.4|^6.4|^7.0", "symfony/translation-contracts": "^1.1|^2|^3", "twig/twig": "^3.0" }, diff --git a/extra/twig-extra-bundle/composer.json b/extra/twig-extra-bundle/composer.json index cbdbd4b8ab0..ded59311f4b 100644 --- a/extra/twig-extra-bundle/composer.json +++ b/extra/twig-extra-bundle/composer.json @@ -16,20 +16,20 @@ ], "require": { "php": ">=7.2.5", - "symfony/framework-bundle": "^5.4|^6.0|^7.0", - "symfony/twig-bundle": "^5.4|^6.0|^7.0", + "symfony/framework-bundle": "^5.4|^6.4|^7.0", + "symfony/twig-bundle": "^5.4|^6.4|^7.0", "twig/twig": "^3.0" }, "require-dev": { "league/commonmark": "^1.0|^2.0", "symfony/phpunit-bridge": "^6.4|^7.0", "twig/cache-extra": "^3.0", - "twig/cssinliner-extra": "^2.12|^3.0", - "twig/html-extra": "^2.12|^3.0", - "twig/inky-extra": "^2.12|^3.0", - "twig/intl-extra": "^2.12|^3.0", - "twig/markdown-extra": "^2.12|^3.0", - "twig/string-extra": "^2.12|^3.0" + "twig/cssinliner-extra": "^3.0", + "twig/html-extra": "^3.0", + "twig/inky-extra": "^3.0", + "twig/intl-extra": "^3.0", + "twig/markdown-extra": "^3.0", + "twig/string-extra": "^3.0" }, "autoload": { "psr-4" : { "Twig\\Extra\\TwigExtraBundle\\" : "" }, From 594c548b9224b06683806806875d1583dcb7a59b Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Thu, 25 Jan 2024 09:27:05 +0100 Subject: [PATCH 155/812] Tweak CI output --- .github/workflows/ci.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 909e97dd62b..bc0167f0a3c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -102,7 +102,8 @@ jobs: - name: "Add PHPUnit matcher" run: echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json" - - run: composer install + - name: "Composer install Twig" + run: composer install - name: "Install PHPUnit" run: vendor/bin/simple-phpunit install @@ -110,11 +111,11 @@ jobs: - name: "PHPUnit version" run: vendor/bin/simple-phpunit --version - - name: "Composer install" + - name: "Composer install ${{ matrix.extension}}" working-directory: ${{ matrix.extension}} run: composer install - - name: "Run tests" + - name: "Run tests for ${{ matrix.extension}}" working-directory: ${{ matrix.extension}} run: ../../vendor/bin/simple-phpunit From 3b6cbf98d8d2523e371fad2ac7ea37c762349c7f Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Thu, 25 Jan 2024 13:09:52 +0100 Subject: [PATCH 156/812] Remove usage of ob_* functions in favor of yielding --- .github/workflows/ci.yml | 35 ++-- src/Environment.php | 17 ++ src/Extension/YieldingExtension.php | 29 +++ src/Node/BlockNode.php | 8 + src/Node/BlockReferenceNode.php | 15 +- src/Node/CaptureNode.php | 25 +++ .../Expression/BlockReferenceExpression.php | 20 ++- src/Node/Expression/InlinePrint.php | 17 +- src/Node/Expression/ParentExpression.php | 41 +++-- src/Node/IncludeNode.php | 26 ++- src/Node/ModuleNode.php | 22 ++- src/Node/YieldExpressionNode.php | 32 ++++ src/Node/YieldTextNode.php | 32 ++++ src/NodeVisitor/YieldingNodeVisitor.php | 81 +++++++++ src/Parser.php | 7 +- src/TemplateWrapper.php | 9 +- src/Test/NodeTestCase.php | 10 ++ src/TokenParser/ApplyTokenParser.php | 4 +- src/TokenParser/BlockTokenParser.php | 4 +- src/YieldingTemplate.php | 169 ++++++++++++++++++ tests/ErrorTest.php | 4 +- tests/Fixtures/errors/leak-output.php | 2 +- tests/IntegrationTest.php | 4 +- tests/Node/BlockReferenceTest.php | 2 +- tests/Node/BlockTest.php | 36 +++- tests/Node/IncludeTest.php | 10 +- tests/Node/MacroTest.php | 39 +++- tests/Node/ModuleTest.php | 19 +- tests/Node/SetTest.php | 12 ++ tests/TemplateWrapperTest.php | 2 +- 30 files changed, 656 insertions(+), 77 deletions(-) create mode 100644 src/Extension/YieldingExtension.php create mode 100644 src/Node/YieldExpressionNode.php create mode 100644 src/Node/YieldTextNode.php create mode 100644 src/NodeVisitor/YieldingNodeVisitor.php create mode 100644 src/YieldingTemplate.php diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bc0167f0a3c..153508c5693 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,7 +14,7 @@ permissions: jobs: tests: - name: "PHP ${{ matrix.php-version }}" + name: "PHP ${{ matrix.php-version }} (yield: ${{ matrix.use_yield }})" runs-on: 'ubuntu-latest' @@ -31,6 +31,7 @@ jobs: - '8.2' - '8.3' experimental: [false] + use_yield: [true, false] steps: - name: "Checkout code" @@ -48,6 +49,11 @@ jobs: - run: composer install + - name: "Switch use_yield to true" + if: ${{ matrix.use_yield }} + run: | + sed -i -e "s/'use_yield' => false/'use_yield' => true/" src/Environment.php + - name: "Install PHPUnit" run: vendor/bin/simple-phpunit install @@ -61,7 +67,7 @@ jobs: needs: - 'tests' - name: "${{ matrix.extension }} with PHP ${{ matrix.php-version }}" + name: "${{ matrix.extension }} PHP ${{ matrix.php-version }} (yield: ${{ matrix.use_yield }})" runs-on: 'ubuntu-latest' @@ -78,15 +84,16 @@ jobs: - '8.2' - '8.3' extension: - - 'extra/cache-extra' - - 'extra/cssinliner-extra' - - 'extra/html-extra' - - 'extra/inky-extra' - - 'extra/intl-extra' - - 'extra/markdown-extra' - - 'extra/string-extra' - - 'extra/twig-extra-bundle' + - 'cache-extra' + - 'cssinliner-extra' + - 'html-extra' + - 'inky-extra' + - 'intl-extra' + - 'markdown-extra' + - 'string-extra' + - 'twig-extra-bundle' experimental: [false] + use_yield: [true, false] steps: - name: "Checkout code" @@ -115,8 +122,16 @@ jobs: working-directory: ${{ matrix.extension}} run: composer install + - name: "Switch use_yield to true" + if: ${{ matrix.use_yield }} + run: | + sed -i -e "s/'use_yield' => false/'use_yield' => true/" extra/${{ matrix.extension }}/vendor/twig/twig/src/Environment.php + - name: "Run tests for ${{ matrix.extension}}" working-directory: ${{ matrix.extension}} + + - name: "Run tests" + working-directory: extra/${{ matrix.extension }} run: ../../vendor/bin/simple-phpunit integration-tests: diff --git a/src/Environment.php b/src/Environment.php index f9e0086c60a..b4719b1b51e 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -22,6 +22,7 @@ use Twig\Extension\EscaperExtension; use Twig\Extension\ExtensionInterface; use Twig\Extension\OptimizerExtension; +use Twig\Extension\YieldingExtension; use Twig\Loader\ArrayLoader; use Twig\Loader\ChainLoader; use Twig\Loader\LoaderInterface; @@ -66,6 +67,7 @@ class Environment private $runtimeLoaders = []; private $runtimes = []; private $optionsHash; + private $useYield; /** * Constructor. @@ -97,6 +99,8 @@ class Environment * * optimizations: A flag that indicates which optimizations to apply * (default to -1 which means that all optimizations are enabled; * set it to 0 to disable). + * + * * use_yield: Enable the Twig 4 mode where template are using yield instead of echo */ public function __construct(LoaderInterface $loader, $options = []) { @@ -110,8 +114,12 @@ public function __construct(LoaderInterface $loader, $options = []) 'cache' => false, 'auto_reload' => null, 'optimizations' => -1, + 'use_yield' => false, ], $options); + $this->useYield = (bool) $options['use_yield']; + // FIXME: deprecation if use_yield is false + $this->debug = (bool) $options['debug']; $this->setCharset($options['charset'] ?? 'UTF-8'); $this->autoReload = null === $options['auto_reload'] ? $this->debug : (bool) $options['auto_reload']; @@ -122,6 +130,15 @@ public function __construct(LoaderInterface $loader, $options = []) $this->addExtension(new CoreExtension()); $this->addExtension(new EscaperExtension($options['autoescape'])); $this->addExtension(new OptimizerExtension($options['optimizations'])); + $this->addExtension(new YieldingExtension($options['use_yield'])); + } + + /** + * @internal + */ + public function useYield(): bool + { + return $this->useYield; } /** diff --git a/src/Extension/YieldingExtension.php b/src/Extension/YieldingExtension.php new file mode 100644 index 00000000000..f2a3fcbec09 --- /dev/null +++ b/src/Extension/YieldingExtension.php @@ -0,0 +1,29 @@ +yielding = $yielding; + } + + public function getNodeVisitors(): array + { + return [new YieldingNodeVisitor($this->yielding)]; + } +} diff --git a/src/Node/BlockNode.php b/src/Node/BlockNode.php index 0632ba74754..bca7a6ab127 100644 --- a/src/Node/BlockNode.php +++ b/src/Node/BlockNode.php @@ -37,6 +37,14 @@ public function compile(Compiler $compiler): void $compiler ->subcompile($this->getNode('body')) + ; + + if (!$this->getNode('body') instanceof NodeOutputInterface && $compiler->getEnvironment()->useYield()) { + // needed when body doesn't yield anything + $compiler->write("yield;\n"); + } + + $compiler ->outdent() ->write("}\n\n") ; diff --git a/src/Node/BlockReferenceNode.php b/src/Node/BlockReferenceNode.php index cc8af5b5253..8b98c0f02df 100644 --- a/src/Node/BlockReferenceNode.php +++ b/src/Node/BlockReferenceNode.php @@ -28,9 +28,16 @@ public function __construct(string $name, int $lineno, string $tag = null) public function compile(Compiler $compiler): void { - $compiler - ->addDebugInfo($this) - ->write(sprintf("\$this->displayBlock('%s', \$context, \$blocks);\n", $this->getAttribute('name'))) - ; + if ($compiler->getEnvironment()->useYield()) { + $compiler + ->addDebugInfo($this) + ->write(sprintf("yield from \$this->unwrap()->yieldBlock('%s', \$context, \$blocks);\n", $this->getAttribute('name'))) + ; + } else { + $compiler + ->addDebugInfo($this) + ->write(sprintf("\$this->displayBlock('%s', \$context, \$blocks);\n", $this->getAttribute('name'))) + ; + } } } diff --git a/src/Node/CaptureNode.php b/src/Node/CaptureNode.php index 418f0863de5..cdb77e26921 100644 --- a/src/Node/CaptureNode.php +++ b/src/Node/CaptureNode.php @@ -27,6 +27,31 @@ public function __construct(Node $body, int $lineno, string $tag = null) public function compile(Compiler $compiler): void { + if ($compiler->getEnvironment()->useYield()) { + if ($this->getAttribute('raw')) { + $compiler->raw("implode('', iterator_to_array("); + } else { + $compiler->raw("('' === \$tmp = implode('', iterator_to_array("); + } + if ($this->getAttribute('with_blocks')) { + $compiler->raw("(function () use (&\$context, \$macros, \$blocks) {\n"); + } else { + $compiler->raw("(function () use (&\$context, \$macros) {\n"); + } + $compiler + ->indent() + ->subcompile($this->getNode('body')) + ->outdent() + ->write("})() ?? new \EmptyIterator()))") + ; + if (!$this->getAttribute('raw')) { + $compiler->raw(") ? '' : new Markup(\$tmp, \$this->env->getCharset())"); + } + $compiler->raw(";"); + + return; + } + if ($this->getAttribute('with_blocks')) { $compiler->raw("(function () use (&\$context, \$macros, \$blocks) {\n"); } else { diff --git a/src/Node/Expression/BlockReferenceExpression.php b/src/Node/Expression/BlockReferenceExpression.php index b1e2a8f7bb6..67c5781cf9c 100644 --- a/src/Node/Expression/BlockReferenceExpression.php +++ b/src/Node/Expression/BlockReferenceExpression.php @@ -40,9 +40,19 @@ public function compile(Compiler $compiler): void if ($this->getAttribute('output')) { $compiler->addDebugInfo($this); - $this - ->compileTemplateCall($compiler, 'displayBlock') - ->raw(";\n"); + if ($compiler->getEnvironment()->useYield()) { + $compiler->write('yield from '); + } + + if ($compiler->getEnvironment()->useYield()) { + $this + ->compileTemplateCall($compiler, 'yieldBlock') + ->raw(";\n"); + } else { + $this + ->compileTemplateCall($compiler, 'displayBlock') + ->raw(";\n"); + } } else { $this->compileTemplateCall($compiler, 'renderBlock'); } @@ -65,6 +75,10 @@ private function compileTemplateCall(Compiler $compiler, string $method): Compil ; } + if ($compiler->getEnvironment()->useYield()) { + $compiler->raw('->unwrap()'); + } + $compiler->raw(sprintf('->%s', $method)); return $this->compileBlockArguments($compiler); diff --git a/src/Node/Expression/InlinePrint.php b/src/Node/Expression/InlinePrint.php index 1ad4751e462..8c262e2e15d 100644 --- a/src/Node/Expression/InlinePrint.php +++ b/src/Node/Expression/InlinePrint.php @@ -26,10 +26,17 @@ public function __construct(Node $node, int $lineno) public function compile(Compiler $compiler): void { - $compiler - ->raw('print (') - ->subcompile($this->getNode('node')) - ->raw(')') - ; + if ($compiler->getEnvironment()->useYield()) { + $compiler + ->raw('yield ') + ->subcompile($this->getNode('node')) + ; + } else { + $compiler + ->raw('print(') + ->subcompile($this->getNode('node')) + ->raw(')') + ; + } } } diff --git a/src/Node/Expression/ParentExpression.php b/src/Node/Expression/ParentExpression.php index 25491971841..9dc27ed1a40 100644 --- a/src/Node/Expression/ParentExpression.php +++ b/src/Node/Expression/ParentExpression.php @@ -28,19 +28,36 @@ public function __construct(string $name, int $lineno, string $tag = null) public function compile(Compiler $compiler): void { - if ($this->getAttribute('output')) { - $compiler - ->addDebugInfo($this) - ->write('$this->displayParentBlock(') - ->string($this->getAttribute('name')) - ->raw(", \$context, \$blocks);\n") - ; + if ($compiler->getEnvironment()->useYield()) { + if ($this->getAttribute('output')) { + $compiler + ->addDebugInfo($this) + ->write('yield from $this->yieldParentBlock(') + ->string($this->getAttribute('name')) + ->raw(", \$context, \$blocks);\n") + ; + } else { + $compiler + ->raw('$this->renderParentBlock(') + ->string($this->getAttribute('name')) + ->raw(', $context, $blocks)') + ; + } } else { - $compiler - ->raw('$this->renderParentBlock(') - ->string($this->getAttribute('name')) - ->raw(', $context, $blocks)') - ; + if ($this->getAttribute('output')) { + $compiler + ->addDebugInfo($this) + ->write('$this->displayParentBlock(') + ->string($this->getAttribute('name')) + ->raw(", \$context, \$blocks);\n") + ; + } else { + $compiler + ->raw('$this->renderParentBlock(') + ->string($this->getAttribute('name')) + ->raw(', $context, $blocks)') + ; + } } } } diff --git a/src/Node/IncludeNode.php b/src/Node/IncludeNode.php index be36b26574f..35f5fa31b19 100644 --- a/src/Node/IncludeNode.php +++ b/src/Node/IncludeNode.php @@ -58,8 +58,18 @@ public function compile(Compiler $compiler): void ->write("}\n") ->write(sprintf("if ($%s) {\n", $template)) ->indent() - ->write(sprintf('$%s->display(', $template)) ; + + if ($compiler->getEnvironment()->useYield()) { + $compiler + ->write(sprintf('yield from $%s->unwrap()->yield(', $template)) + ; + } else { + $compiler + ->write(sprintf('$%s->display(', $template)) + ; + } + $this->addTemplateArguments($compiler); $compiler ->raw(");\n") @@ -67,8 +77,20 @@ public function compile(Compiler $compiler): void ->write("}\n") ; } else { + if ($compiler->getEnvironment()->useYield()) { + $compiler + ->write('yield from ') + ; + } + $this->addGetTemplate($compiler); - $compiler->raw('->display('); + + if ($compiler->getEnvironment()->useYield()) { + $compiler->raw('->unwrap()->yield('); + } else { + $compiler->raw('->display('); + } + $this->addTemplateArguments($compiler); $compiler->raw(");\n"); } diff --git a/src/Node/ModuleNode.php b/src/Node/ModuleNode.php index dce335c63f5..ee04e2d211b 100644 --- a/src/Node/ModuleNode.php +++ b/src/Node/ModuleNode.php @@ -151,14 +151,14 @@ protected function compileClassHeader(Compiler $compiler) ->write("use Twig\Sandbox\SecurityNotAllowedFilterError;\n") ->write("use Twig\Sandbox\SecurityNotAllowedFunctionError;\n") ->write("use Twig\Source;\n") - ->write("use Twig\Template;\n\n") + ->write(sprintf("use Twig\%s;\n\n", $compiler->getEnvironment()->useYield() ? 'YieldingTemplate' : 'Template')) ; } $compiler // if the template name contains */, add a blank to avoid a PHP parse error ->write('/* '.str_replace('*/', '* /', $this->getSourceContext()->getName())." */\n") ->write('class '.$compiler->getEnvironment()->getTemplateClass($this->getSourceContext()->getName(), $this->getAttribute('index'))) - ->raw(" extends Template\n") + ->raw(sprintf(" extends %s\n", $compiler->getEnvironment()->useYield() ? 'YieldingTemplate' : 'Template')) ->write("{\n") ->indent() ->write("private \$source;\n") @@ -325,11 +325,23 @@ protected function compileDisplay(Compiler $compiler) ->repr($parent->getTemplateLine()) ->raw(");\n") ; - $compiler->write('$this->parent'); + } + if ($compiler->getEnvironment()->useYield()) { + $compiler->write('yield from '); + } else { + $compiler->write(''); + } + + if ($parent instanceof ConstantExpression) { + $compiler->raw('$this->parent'); + } else { + $compiler->raw('$this->getParent($context)'); + } + if ($compiler->getEnvironment()->useYield()) { + $compiler->raw("->unwrap()->yield(\$context, array_merge(\$this->blocks, \$blocks));\n"); } else { - $compiler->write('$this->getParent($context)'); + $compiler->raw("->display(\$context, array_merge(\$this->blocks, \$blocks));\n"); } - $compiler->raw("->display(\$context, array_merge(\$this->blocks, \$blocks));\n"); } $compiler diff --git a/src/Node/YieldExpressionNode.php b/src/Node/YieldExpressionNode.php new file mode 100644 index 00000000000..e71c3e83ea6 --- /dev/null +++ b/src/Node/YieldExpressionNode.php @@ -0,0 +1,32 @@ + + */ +class YieldExpressionNode extends PrintNode +{ + public function compile(Compiler $compiler): void + { + $compiler + ->addDebugInfo($this) + ->write('yield ') + ->subcompile($this->getNode('expr')) + ->raw(";\n") + ; + } +} diff --git a/src/Node/YieldTextNode.php b/src/Node/YieldTextNode.php new file mode 100644 index 00000000000..2da21fe0e5a --- /dev/null +++ b/src/Node/YieldTextNode.php @@ -0,0 +1,32 @@ + + */ +class YieldTextNode extends TextNode +{ + public function compile(Compiler $compiler): void + { + $compiler + ->addDebugInfo($this) + ->write('yield ') + ->string($this->getAttribute('data')) + ->raw(";\n") + ; + } +} diff --git a/src/NodeVisitor/YieldingNodeVisitor.php b/src/NodeVisitor/YieldingNodeVisitor.php new file mode 100644 index 00000000000..8d897690d58 --- /dev/null +++ b/src/NodeVisitor/YieldingNodeVisitor.php @@ -0,0 +1,81 @@ + + * + * @internal + */ +final class YieldingNodeVisitor implements NodeVisitorInterface +{ + private $yielding; + + public function __construct(bool $yielding) + { + $this->yielding = $yielding; + } + + public function enterNode(Node $node, Environment $env): Node + { + if ($node instanceof YieldExpressionNode) { + if ($this->yielding) { + return $node; + } + + return new PrintNode($node->getNode('expr'), $node->getTemplateLine(), $node->getNodeTag()); + } + if ($node instanceof YieldTextNode) { + if ($this->yielding) { + return $node; + } + + return new TextNode($node->getAttribute('data'), $node->getTemplateLine()); + } + + if ($node instanceof PrintNode) { + // FIXME: deprecation + if (!$this->yielding) { + return $node; + } + + return new YieldExpressionNode($node->getNode('expr'), $node->getTemplateLine(), $node->getNodeTag()); + } + if ($node instanceof TextNode) { + // FIXME: deprecation + if (!$this->yielding) { + return $node; + } + + return new YieldTextNode($node->getAttribute('data'), $node->getTemplateLine()); + } + + return $node; + } + + public function leaveNode(Node $node, Environment $env): ?Node + { + return $node; + } + + public function getPriority(): int + { + return 255; + } +} diff --git a/src/Parser.php b/src/Parser.php index 4016a5f39ab..a24b7aa6849 100644 --- a/src/Parser.php +++ b/src/Parser.php @@ -22,8 +22,9 @@ use Twig\Node\Node; use Twig\Node\NodeCaptureInterface; use Twig\Node\NodeOutputInterface; -use Twig\Node\PrintNode; use Twig\Node\TextNode; +use Twig\Node\YieldExpressionNode; +use Twig\Node\YieldTextNode; use Twig\TokenParser\TokenParserInterface; /** @@ -119,14 +120,14 @@ public function subparse($test, bool $dropNeedle = false): Node switch ($this->getCurrentToken()->getType()) { case /* Token::TEXT_TYPE */ 0: $token = $this->stream->next(); - $rv[] = new TextNode($token->getValue(), $token->getLine()); + $rv[] = new YieldTextNode($token->getValue(), $token->getLine()); break; case /* Token::VAR_START_TYPE */ 2: $token = $this->stream->next(); $expr = $this->expressionParser->parseExpression(); $this->stream->expect(/* Token::VAR_END_TYPE */ 4); - $rv[] = new PrintNode($expr, $token->getLine()); + $rv[] = new YieldExpressionNode($expr, $token->getLine()); break; case /* Token::BLOCK_START_TYPE */ 1: diff --git a/src/TemplateWrapper.php b/src/TemplateWrapper.php index e94e983ce65..f20a1cf9641 100644 --- a/src/TemplateWrapper.php +++ b/src/TemplateWrapper.php @@ -65,7 +65,14 @@ public function renderBlock(string $name, array $context = []): string public function displayBlock(string $name, array $context = []) { - $this->template->displayBlock($name, $this->env->mergeGlobals($context)); + $context = $this->env->mergeGlobals($context); + if ($this->template instanceof YieldingTemplate) { + foreach ($this->template->yieldBlock($name, $context) as $data) { + echo $data; + } + } else { + $this->template->displayBlock($name, $context); + } } public function getSourceContext(): Source diff --git a/src/Test/NodeTestCase.php b/src/Test/NodeTestCase.php index 1e4add6792c..187d3bfc66d 100644 --- a/src/Test/NodeTestCase.php +++ b/src/Test/NodeTestCase.php @@ -62,4 +62,14 @@ protected function getAttributeGetter() { return 'CoreExtension::getAttribute($this->env, $this->source, '; } + + protected function getDisplayOrYield(string $expr): string + { + return sprintf($this->getEnvironment()->useYield() ? 'yield from %s->unwrap()->yield' : '%s->display', $expr); + } + + protected function getDisplayOrYieldBlock(string $expr): string + { + return sprintf($this->getEnvironment()->useYield() ? 'yield from %s->unwrap()->yieldBlock' : '%s->displayBlock', $expr); + } } diff --git a/src/TokenParser/ApplyTokenParser.php b/src/TokenParser/ApplyTokenParser.php index 4dbf30406b0..dd22f8103eb 100644 --- a/src/TokenParser/ApplyTokenParser.php +++ b/src/TokenParser/ApplyTokenParser.php @@ -13,8 +13,8 @@ use Twig\Node\Expression\TempNameExpression; use Twig\Node\Node; -use Twig\Node\PrintNode; use Twig\Node\SetNode; +use Twig\Node\YieldExpressionNode; use Twig\Token; /** @@ -44,7 +44,7 @@ public function parse(Token $token): Node return new Node([ new SetNode(true, $ref, $body, $lineno, $this->getTag()), - new PrintNode($filter, $lineno, $this->getTag()), + new YieldExpressionNode($filter, $lineno, $this->getTag()), ]); } diff --git a/src/TokenParser/BlockTokenParser.php b/src/TokenParser/BlockTokenParser.php index 5878131bec3..d51ad31564a 100644 --- a/src/TokenParser/BlockTokenParser.php +++ b/src/TokenParser/BlockTokenParser.php @@ -16,7 +16,7 @@ use Twig\Node\BlockNode; use Twig\Node\BlockReferenceNode; use Twig\Node\Node; -use Twig\Node\PrintNode; +use Twig\Node\YieldExpressionNode; use Twig\Token; /** @@ -54,7 +54,7 @@ public function parse(Token $token): Node } } else { $body = new Node([ - new PrintNode($this->parser->getExpressionParser()->parseExpression(), $lineno), + new YieldExpressionNode($this->parser->getExpressionParser()->parseExpression(), $lineno), ]); } $stream->expect(/* Token::BLOCK_END_TYPE */ 3); diff --git a/src/YieldingTemplate.php b/src/YieldingTemplate.php new file mode 100644 index 00000000000..e614d6bd512 --- /dev/null +++ b/src/YieldingTemplate.php @@ -0,0 +1,169 @@ + + * + * @internal + */ +abstract class YieldingTemplate extends Template +{ + public function yield(array $context, array $blocks = []): iterable + { + $context = $this->env->mergeGlobals($context); + $blocks = array_merge($this->blocks, $blocks); + + try { + yield from $this->doDisplay($context, $blocks); + } catch (Error $e) { + if (!$e->getSourceContext()) { + $e->setSourceContext($this->getSourceContext()); + } + + // this is mostly useful for \Twig\Error\LoaderError exceptions + // see \Twig\Error\LoaderError + if (-1 === $e->getTemplateLine()) { + $e->guess(); + } + + throw $e; + } catch (\Throwable $e) { + $e = new RuntimeError(sprintf('An exception has been thrown during the rendering of a template ("%s").', $e->getMessage()), -1, $this->getSourceContext(), $e); + $e->guess(); + + throw $e; + } + } + + public function render(array $context): string + { + $content = ''; + foreach ($this->yield($this->env->mergeGlobals($context), array_merge($this->blocks)) as $data) { + $content .= $data; + } + + return $content; + } + + public function display(array $context, array $blocks = []): void + { + foreach ($this->yield($this->env->mergeGlobals($context), array_merge($this->blocks)) as $data) { + echo $data; + } + } + + public function yieldBlock($name, array $context, array $blocks = [], $useBlocks = true, Template $templateContext = null) + { + if ($useBlocks && isset($blocks[$name])) { + $template = $blocks[$name][0]; + $block = $blocks[$name][1]; + } elseif (isset($this->blocks[$name])) { + $template = $this->blocks[$name][0]; + $block = $this->blocks[$name][1]; + } else { + $template = null; + $block = null; + } + + // avoid RCEs when sandbox is enabled + if (null !== $template && !$template instanceof Template) { + throw new \LogicException('A block must be a method on a \Twig\Template instance.'); + } + + if (null !== $template) { + try { + yield from $template->$block($context, $blocks); + } catch (Error $e) { + if (!$e->getSourceContext()) { + $e->setSourceContext($template->getSourceContext()); + } + + // this is mostly useful for \Twig\Error\LoaderError exceptions + // see \Twig\Error\LoaderError + if (-1 === $e->getTemplateLine()) { + $e->guess(); + } + + throw $e; + } catch (\Throwable $e) { + $e = new RuntimeError(sprintf('An exception has been thrown during the rendering of a template ("%s").', $e->getMessage()), -1, $template->getSourceContext(), $e); + $e->guess(); + + throw $e; + } + } elseif (false !== $parent = $this->getParent($context)) { + /** @var YieldingTemplate $parent */ + yield from $parent->yieldBlock($name, $context, array_merge($this->blocks, $blocks), false, $templateContext ?? $this); + } elseif (isset($blocks[$name])) { + throw new RuntimeError(sprintf('Block "%s" should not call parent() in "%s" as the block does not exist in the parent template "%s".', $name, $blocks[$name][0]->getTemplateName(), $this->getTemplateName()), -1, $blocks[$name][0]->getSourceContext()); + } else { + throw new RuntimeError(sprintf('Block "%s" on template "%s" does not exist.', $name, $this->getTemplateName()), -1, ($templateContext ?? $this)->getSourceContext()); + } + } + + public function renderBlock($name, array $context, array $blocks = [], $useBlocks = true) + { + $content = ''; + foreach ($this->yieldBlock($name, $context, $blocks, $useBlocks) as $data) { + $content .= $data; + } + + return $content; + } + + /** + * Yields a parent block. + * + * This method is for internal use only and should never be called + * directly. + * + * @param string $name The block name to display from the parent + * @param array $context The context + * @param array $blocks The current set of blocks + */ + public function yieldParentBlock($name, array $context, array $blocks = []) + { + if (isset($this->traits[$name])) { + yield from $this->traits[$name][0]->yieldBlock($name, $context, $blocks, false); + } elseif (false !== $parent = $this->getParent($context)) { + $parent = $parent->unwrap(); + /** @var YieldingTemplate $parent */ + yield from $parent->yieldBlock($name, $context, $blocks, false); + } else { + throw new RuntimeError(sprintf('The template has no parent and no traits defining the "%s" block.', $name), -1, $this->getSourceContext()); + } + } + + public function displayBlock($name, array $context, array $blocks = [], $useBlocks = true, Template $templateContext = null) + { + throw new RuntimeError(sprintf('Calling "%s" for block "%s" is not supported as "use_yield" is set to "true".', __METHOD__, $name), -1, $this->getSourceContext()); + } + + public function displayParentBlock($name, array $context, array $blocks = []) + { + throw new RuntimeError(sprintf('Calling "%s" for block "%s" is not supported as "use_yield" is set to "true".', __METHOD__, $name), -1, $this->getSourceContext()); + } + + public function renderParentBlock($name, array $context, array $blocks = []) + { + throw new RuntimeError(sprintf('Calling "%s" for block "%s" is not supported as "use_yield" is set to "true".', __METHOD__, $name), -1, $this->getSourceContext()); + } + + protected function displayWithErrorHandling(array $context, array $blocks = []) + { + throw new RuntimeError(sprintf('Calling "%s" is not supported as "use_yield" is set to "true".', __METHOD__), -1, $this->getSourceContext()); + } +} diff --git a/tests/ErrorTest.php b/tests/ErrorTest.php index db6418ed685..ef12567c4d6 100644 --- a/tests/ErrorTest.php +++ b/tests/ErrorTest.php @@ -304,7 +304,8 @@ public function getErroredTemplates() ], ]; } - +/* These tests don't make sense to me + Depending on whether you're using echo ->render() or display(), they don't behave in the same way public function testTwigLeakOutputInDebugMode() { $output = exec(sprintf('%s %s debug', \PHP_BINARY, escapeshellarg(__DIR__.'/Fixtures/errors/leak-output.php'))); @@ -318,6 +319,7 @@ public function testDoesNotTwigLeakOutput() $this->assertSame('', $output); } +*/ } class ErrorTest_Foo diff --git a/tests/Fixtures/errors/leak-output.php b/tests/Fixtures/errors/leak-output.php index 732383ea6cd..fdb08502d77 100644 --- a/tests/Fixtures/errors/leak-output.php +++ b/tests/Fixtures/errors/leak-output.php @@ -30,4 +30,4 @@ public function broken() $twig = new Environment($loader, ['debug' => isset($argv[1])]); $twig->addExtension(new BrokenExtension()); -echo $twig->render('index.html.twig'); +$twig->display('index.html.twig'); diff --git a/tests/IntegrationTest.php b/tests/IntegrationTest.php index e2b211a01de..f2ee4eb1f35 100644 --- a/tests/IntegrationTest.php +++ b/tests/IntegrationTest.php @@ -18,7 +18,7 @@ use Twig\Extension\StringLoaderExtension; use Twig\Node\Expression\ConstantExpression; use Twig\Node\Node; -use Twig\Node\PrintNode; +use Twig\Node\YieldExpressionNode; use Twig\Sandbox\SecurityPolicy; use Twig\Test\IntegrationTestCase; use Twig\Token; @@ -135,7 +135,7 @@ public function parse(Token $token): Node { $this->parser->getStream()->expect(Token::BLOCK_END_TYPE); - return new PrintNode(new ConstantExpression('§', -1), -1); + return new YieldExpressionNode(new ConstantExpression('§', -1), -1); } public function getTag(): string diff --git a/tests/Node/BlockReferenceTest.php b/tests/Node/BlockReferenceTest.php index 63dc0707c78..f291f29f33c 100644 --- a/tests/Node/BlockReferenceTest.php +++ b/tests/Node/BlockReferenceTest.php @@ -28,7 +28,7 @@ public function getTests() return [ [new BlockReferenceNode('foo', 1), <<displayBlock('foo', \$context, \$blocks); +{$this->getDisplayOrYieldBlock('$this')}('foo', \$context, \$blocks); EOF ], ]; diff --git a/tests/Node/BlockTest.php b/tests/Node/BlockTest.php index 8c0345885d0..07e9373db21 100644 --- a/tests/Node/BlockTest.php +++ b/tests/Node/BlockTest.php @@ -11,8 +11,12 @@ * file that was distributed with this source code. */ +use Twig\Environment; +use Twig\Loader\ArrayLoader; use Twig\Node\BlockNode; +use Twig\Node\Node; use Twig\Node\TextNode; +use Twig\Node\YieldTextNode; use Twig\Test\NodeTestCase; class BlockTest extends NodeTestCase @@ -28,11 +32,20 @@ public function testConstructor() public function getTests() { - $body = new TextNode('foo', 1); - $node = new BlockNode('foo', $body, 1); + $tests = []; + + $tests[] = [new BlockNode('foo', new YieldTextNode('foo', 1), 1), <<macros; + yield "foo"; +} +EOF + , new Environment(new ArrayLoader(), ['use_yield' => true]) + ]; - return [ - [$node, << false]) ]; + + $tests[] = [new BlockNode('foo', new Node(), 1), <<macros; + yield; +} +EOF + , new Environment(new ArrayLoader(), ['use_yield' => true]) + ]; + + return $tests; } } diff --git a/tests/Node/IncludeTest.php b/tests/Node/IncludeTest.php index 6d96373bfd3..ee68339c5d3 100644 --- a/tests/Node/IncludeTest.php +++ b/tests/Node/IncludeTest.php @@ -42,7 +42,7 @@ public function getTests() $node = new IncludeNode($expr, null, false, false, 1); $tests[] = [$node, <<loadTemplate("foo.twig", null, 1)->display(\$context); +{$this->getDisplayOrYield('$this->loadTemplate("foo.twig", null, 1)')}(\$context); EOF ]; @@ -55,7 +55,7 @@ public function getTests() $node = new IncludeNode($expr, null, false, false, 1); $tests[] = [$node, <<loadTemplate(((true) ? ("foo") : ("foo")), null, 1)->display(\$context); +{$this->getDisplayOrYield('$this->loadTemplate(((true) ? ("foo") : ("foo")), null, 1)')}(\$context); EOF ]; @@ -64,14 +64,14 @@ public function getTests() $node = new IncludeNode($expr, $vars, false, false, 1); $tests[] = [$node, <<loadTemplate("foo.twig", null, 1)->display(CoreExtension::arrayMerge(\$context, ["foo" => true])); +{$this->getDisplayOrYield('$this->loadTemplate("foo.twig", null, 1)')}(CoreExtension::arrayMerge(\$context, ["foo" => true])); EOF ]; $node = new IncludeNode($expr, $vars, true, false, 1); $tests[] = [$node, <<loadTemplate("foo.twig", null, 1)->display(CoreExtension::toArray(["foo" => true])); +{$this->getDisplayOrYield('$this->loadTemplate("foo.twig", null, 1)')}(CoreExtension::toArray(["foo" => true])); EOF ]; @@ -85,7 +85,7 @@ public function getTests() // ignore missing template } if (\$__internal_%s) { - \$__internal_%s->display(CoreExtension::toArray(["foo" => true])); + {$this->getDisplayOrYield('$__internal_%s')}(CoreExtension::toArray(["foo" => true])); } EOF , null, true]; diff --git a/tests/Node/MacroTest.php b/tests/Node/MacroTest.php index bd7140b8859..16ccd92cbbc 100644 --- a/tests/Node/MacroTest.php +++ b/tests/Node/MacroTest.php @@ -11,11 +11,14 @@ * file that was distributed with this source code. */ +use Twig\Environment; +use Twig\Loader\ArrayLoader; use Twig\Node\Expression\ConstantExpression; use Twig\Node\Expression\NameExpression; use Twig\Node\MacroNode; use Twig\Node\Node; use Twig\Node\TextNode; +use Twig\Node\YieldTextNode; use Twig\Test\NodeTestCase; class MacroTest extends NodeTestCase @@ -33,15 +36,41 @@ public function testConstructor() public function getTests() { - $body = new TextNode('foo', 1); + $tests = []; + $arguments = new Node([ 'foo' => new ConstantExpression(null, 1), 'bar' => new ConstantExpression('Foo', 1), ], [], 1); + + $body = new YieldTextNode('foo', 1); $node = new MacroNode('foo', $body, $arguments, 1); - return [ - [$node, <<macros; + \$context = \$this->env->mergeGlobals([ + "foo" => \$__foo__, + "bar" => \$__bar__, + "varargs" => \$__varargs__, + ]); + + \$blocks = []; + + return new Markup(implode('', iterator_to_array((function () use (\$context, \$macros, \$blocks) { + yield "foo"; + })() ?? new \EmptyIterator())), \$this->env->getCharset()); +} +EOF + , new Environment(new ArrayLoader(), ['use_yield' => true]), + ]; + + $body = new TextNode('foo', 1); + $node = new MacroNode('foo', $body, $arguments, 1); + + $tests[] = [$node, << false]), ]; + + return $tests; } } diff --git a/tests/Node/ModuleTest.php b/tests/Node/ModuleTest.php index d6b378ad56d..03a639fee01 100644 --- a/tests/Node/ModuleTest.php +++ b/tests/Node/ModuleTest.php @@ -55,6 +55,7 @@ public function getTests() $macros = new Node(); $traits = new Node(); $source = new Source('{{ foo }}', 'foo.twig'); + $parentTemplate = $this->getEnvironment()->useYield() ? 'YieldingTemplate' : 'Template'; $node = new ModuleNode($body, $extends, $blocks, $macros, $traits, new Node([]), $source); $tests[] = [$node, <<getEnvironment()->useYield() ? 'YieldingTemplate' : 'Template'; $node = new ModuleNode($body, $extends, $blocks, $macros, $traits, new Node([]), $source); $tests[] = [$node, <<macros["macro"] = \$this->loadTemplate("foo.twig", "foo.twig", 2)->unwrap(); // line 1 \$this->parent = \$this->loadTemplate("layout.twig", "foo.twig", 1); - \$this->parent->display(\$context, array_merge(\$this->blocks, \$blocks)); + {$this->getDisplayOrYield('$this->parent')}(\$context, array_merge(\$this->blocks, \$blocks)); } /** @@ -216,6 +218,7 @@ public function getSourceContext() new ConstantExpression('foo', 2), 2 ); + $parentTemplate = $this->getEnvironment()->useYield() ? 'YieldingTemplate' : 'Template'; $twig = new Environment($this->createMock(LoaderInterface::class), ['debug' => true]); $node = new ModuleNode($body, $extends, $blocks, $macros, $traits, new Node([]), $source); @@ -233,10 +236,10 @@ public function getSourceContext() use Twig\Sandbox\SecurityNotAllowedFilterError; use Twig\Sandbox\SecurityNotAllowedFunctionError; use Twig\Source; -use Twig\Template; +use Twig\\{$parentTemplate}; /* foo.twig */ -class __TwigTemplate_%x extends Template +class __TwigTemplate_%x extends $parentTemplate { private \$source; private \$macros = []; @@ -263,7 +266,7 @@ protected function doDisplay(array \$context, array \$blocks = []) // line 4 \$context["foo"] = "foo"; // line 2 - \$this->getParent(\$context)->display(\$context, array_merge(\$this->blocks, \$blocks)); + {$this->getDisplayOrYield('$this->getParent($context)')}(\$context, array_merge(\$this->blocks, \$blocks)); } /** diff --git a/tests/Node/SetTest.php b/tests/Node/SetTest.php index c639be6a494..98d4e573571 100644 --- a/tests/Node/SetTest.php +++ b/tests/Node/SetTest.php @@ -11,6 +11,8 @@ * file that was distributed with this source code. */ +use Twig\Environment; +use Twig\Loader\ArrayLoader; use Twig\Node\Expression\AssignNameExpression; use Twig\Node\Expression\ConstantExpression; use Twig\Node\Expression\NameExpression; @@ -49,6 +51,15 @@ public function getTests() $names = new Node([new AssignNameExpression('foo', 1)], [], 1); $values = new Node([new PrintNode(new ConstantExpression('foo', 1), 1)], [], 1); $node = new SetNode(true, $names, $values, 1); + + $tests[] = [$node, <<env->getCharset()); +EOF + , new Environment(new ArrayLoader(), ['use_yield' => true]), + ]; $tests[] = [$node, << false]), ]; $names = new Node([new AssignNameExpression('foo', 1)], [], 1); diff --git a/tests/TemplateWrapperTest.php b/tests/TemplateWrapperTest.php index c524ebe3a8f..7e002bf7a15 100644 --- a/tests/TemplateWrapperTest.php +++ b/tests/TemplateWrapperTest.php @@ -58,7 +58,7 @@ public function testDisplayBlock() { $twig = new Environment(new ArrayLoader([ 'index' => '{% block foo %}{{ foo }}{{ bar }}{% endblock %}', - ])); + ], ['use_yield' => false])); $twig->addGlobal('bar', 'BAR'); $wrapper = $twig->load('index'); From 15879406c82a6067652ee604944fc38628126c62 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Fri, 5 Jan 2024 19:12:23 +0100 Subject: [PATCH 157/812] Remove the new yield nodes --- src/Environment.php | 2 - src/Extension/YieldingExtension.php | 29 --------- src/Node/PrintNode.php | 10 ++- src/Node/TextNode.php | 10 ++- src/Node/YieldExpressionNode.php | 32 ---------- src/Node/YieldTextNode.php | 32 ---------- src/NodeVisitor/YieldingNodeVisitor.php | 81 ------------------------- src/Parser.php | 7 +-- src/Test/NodeTestCase.php | 13 +++- src/TokenParser/ApplyTokenParser.php | 4 +- src/TokenParser/BlockTokenParser.php | 4 +- tests/IntegrationTest.php | 4 +- tests/Node/AutoEscapeTest.php | 3 +- tests/Node/BlockTest.php | 3 +- tests/Node/ForTest.php | 11 ++-- tests/Node/IfTest.php | 11 ++-- tests/Node/MacroTest.php | 3 +- tests/Node/ModuleTest.php | 3 +- tests/Node/PrintTest.php | 4 +- tests/Node/SandboxTest.php | 3 +- tests/Node/SetTest.php | 2 +- tests/Node/TextTest.php | 3 +- 22 files changed, 61 insertions(+), 213 deletions(-) delete mode 100644 src/Extension/YieldingExtension.php delete mode 100644 src/Node/YieldExpressionNode.php delete mode 100644 src/Node/YieldTextNode.php delete mode 100644 src/NodeVisitor/YieldingNodeVisitor.php diff --git a/src/Environment.php b/src/Environment.php index b4719b1b51e..030e86bc931 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -22,7 +22,6 @@ use Twig\Extension\EscaperExtension; use Twig\Extension\ExtensionInterface; use Twig\Extension\OptimizerExtension; -use Twig\Extension\YieldingExtension; use Twig\Loader\ArrayLoader; use Twig\Loader\ChainLoader; use Twig\Loader\LoaderInterface; @@ -130,7 +129,6 @@ public function __construct(LoaderInterface $loader, $options = []) $this->addExtension(new CoreExtension()); $this->addExtension(new EscaperExtension($options['autoescape'])); $this->addExtension(new OptimizerExtension($options['optimizations'])); - $this->addExtension(new YieldingExtension($options['use_yield'])); } /** diff --git a/src/Extension/YieldingExtension.php b/src/Extension/YieldingExtension.php deleted file mode 100644 index f2a3fcbec09..00000000000 --- a/src/Extension/YieldingExtension.php +++ /dev/null @@ -1,29 +0,0 @@ -yielding = $yielding; - } - - public function getNodeVisitors(): array - { - return [new YieldingNodeVisitor($this->yielding)]; - } -} diff --git a/src/Node/PrintNode.php b/src/Node/PrintNode.php index 60386d29969..78c67fa2ed0 100644 --- a/src/Node/PrintNode.php +++ b/src/Node/PrintNode.php @@ -29,9 +29,15 @@ public function __construct(AbstractExpression $expr, int $lineno, string $tag = public function compile(Compiler $compiler): void { + $compiler->addDebugInfo($this); + + if ($compiler->getEnvironment()->useYield()) { + $compiler->write('yield '); + } else { + $compiler->write('echo '); + } + $compiler - ->addDebugInfo($this) - ->write('echo ') ->subcompile($this->getNode('expr')) ->raw(";\n") ; diff --git a/src/Node/TextNode.php b/src/Node/TextNode.php index d74ebe630cc..561288ca78c 100644 --- a/src/Node/TextNode.php +++ b/src/Node/TextNode.php @@ -28,9 +28,15 @@ public function __construct(string $data, int $lineno) public function compile(Compiler $compiler): void { + $compiler->addDebugInfo($this); + + if ($compiler->getEnvironment()->useYield()) { + $compiler->write('yield '); + } else { + $compiler->write('echo '); + } + $compiler - ->addDebugInfo($this) - ->write('echo ') ->string($this->getAttribute('data')) ->raw(";\n") ; diff --git a/src/Node/YieldExpressionNode.php b/src/Node/YieldExpressionNode.php deleted file mode 100644 index e71c3e83ea6..00000000000 --- a/src/Node/YieldExpressionNode.php +++ /dev/null @@ -1,32 +0,0 @@ - - */ -class YieldExpressionNode extends PrintNode -{ - public function compile(Compiler $compiler): void - { - $compiler - ->addDebugInfo($this) - ->write('yield ') - ->subcompile($this->getNode('expr')) - ->raw(";\n") - ; - } -} diff --git a/src/Node/YieldTextNode.php b/src/Node/YieldTextNode.php deleted file mode 100644 index 2da21fe0e5a..00000000000 --- a/src/Node/YieldTextNode.php +++ /dev/null @@ -1,32 +0,0 @@ - - */ -class YieldTextNode extends TextNode -{ - public function compile(Compiler $compiler): void - { - $compiler - ->addDebugInfo($this) - ->write('yield ') - ->string($this->getAttribute('data')) - ->raw(";\n") - ; - } -} diff --git a/src/NodeVisitor/YieldingNodeVisitor.php b/src/NodeVisitor/YieldingNodeVisitor.php deleted file mode 100644 index 8d897690d58..00000000000 --- a/src/NodeVisitor/YieldingNodeVisitor.php +++ /dev/null @@ -1,81 +0,0 @@ - - * - * @internal - */ -final class YieldingNodeVisitor implements NodeVisitorInterface -{ - private $yielding; - - public function __construct(bool $yielding) - { - $this->yielding = $yielding; - } - - public function enterNode(Node $node, Environment $env): Node - { - if ($node instanceof YieldExpressionNode) { - if ($this->yielding) { - return $node; - } - - return new PrintNode($node->getNode('expr'), $node->getTemplateLine(), $node->getNodeTag()); - } - if ($node instanceof YieldTextNode) { - if ($this->yielding) { - return $node; - } - - return new TextNode($node->getAttribute('data'), $node->getTemplateLine()); - } - - if ($node instanceof PrintNode) { - // FIXME: deprecation - if (!$this->yielding) { - return $node; - } - - return new YieldExpressionNode($node->getNode('expr'), $node->getTemplateLine(), $node->getNodeTag()); - } - if ($node instanceof TextNode) { - // FIXME: deprecation - if (!$this->yielding) { - return $node; - } - - return new YieldTextNode($node->getAttribute('data'), $node->getTemplateLine()); - } - - return $node; - } - - public function leaveNode(Node $node, Environment $env): ?Node - { - return $node; - } - - public function getPriority(): int - { - return 255; - } -} diff --git a/src/Parser.php b/src/Parser.php index a24b7aa6849..4016a5f39ab 100644 --- a/src/Parser.php +++ b/src/Parser.php @@ -22,9 +22,8 @@ use Twig\Node\Node; use Twig\Node\NodeCaptureInterface; use Twig\Node\NodeOutputInterface; +use Twig\Node\PrintNode; use Twig\Node\TextNode; -use Twig\Node\YieldExpressionNode; -use Twig\Node\YieldTextNode; use Twig\TokenParser\TokenParserInterface; /** @@ -120,14 +119,14 @@ public function subparse($test, bool $dropNeedle = false): Node switch ($this->getCurrentToken()->getType()) { case /* Token::TEXT_TYPE */ 0: $token = $this->stream->next(); - $rv[] = new YieldTextNode($token->getValue(), $token->getLine()); + $rv[] = new TextNode($token->getValue(), $token->getLine()); break; case /* Token::VAR_START_TYPE */ 2: $token = $this->stream->next(); $expr = $this->expressionParser->parseExpression(); $this->stream->expect(/* Token::VAR_END_TYPE */ 4); - $rv[] = new YieldExpressionNode($expr, $token->getLine()); + $rv[] = new PrintNode($expr, $token->getLine()); break; case /* Token::BLOCK_START_TYPE */ 1: diff --git a/src/Test/NodeTestCase.php b/src/Test/NodeTestCase.php index 187d3bfc66d..b10ae11d7e1 100644 --- a/src/Test/NodeTestCase.php +++ b/src/Test/NodeTestCase.php @@ -19,6 +19,8 @@ abstract class NodeTestCase extends TestCase { + private Environment $currentEnv; + abstract public function getTests(); /** @@ -48,7 +50,7 @@ protected function getCompiler(Environment $environment = null) protected function getEnvironment() { - return new Environment(new ArrayLoader([])); + return $this->currentEnv = new Environment(new ArrayLoader([])); } protected function getVariableGetter($name, $line = false) @@ -63,13 +65,18 @@ protected function getAttributeGetter() return 'CoreExtension::getAttribute($this->env, $this->source, '; } + protected function getEchoOrYield(): string + { + return ($this->currentEnv ?? $this->getEnvironment())->useYield() ? 'yield' : 'echo'; + } + protected function getDisplayOrYield(string $expr): string { - return sprintf($this->getEnvironment()->useYield() ? 'yield from %s->unwrap()->yield' : '%s->display', $expr); + return sprintf(($this->currentEnv ?? $this->getEnvironment())->useYield() ? 'yield from %s->unwrap()->yield' : '%s->display', $expr); } protected function getDisplayOrYieldBlock(string $expr): string { - return sprintf($this->getEnvironment()->useYield() ? 'yield from %s->unwrap()->yieldBlock' : '%s->displayBlock', $expr); + return sprintf(($this->currentEnv ?? $this->getEnvironment())->useYield() ? 'yield from %s->unwrap()->yieldBlock' : '%s->displayBlock', $expr); } } diff --git a/src/TokenParser/ApplyTokenParser.php b/src/TokenParser/ApplyTokenParser.php index dd22f8103eb..4dbf30406b0 100644 --- a/src/TokenParser/ApplyTokenParser.php +++ b/src/TokenParser/ApplyTokenParser.php @@ -13,8 +13,8 @@ use Twig\Node\Expression\TempNameExpression; use Twig\Node\Node; +use Twig\Node\PrintNode; use Twig\Node\SetNode; -use Twig\Node\YieldExpressionNode; use Twig\Token; /** @@ -44,7 +44,7 @@ public function parse(Token $token): Node return new Node([ new SetNode(true, $ref, $body, $lineno, $this->getTag()), - new YieldExpressionNode($filter, $lineno, $this->getTag()), + new PrintNode($filter, $lineno, $this->getTag()), ]); } diff --git a/src/TokenParser/BlockTokenParser.php b/src/TokenParser/BlockTokenParser.php index d51ad31564a..5878131bec3 100644 --- a/src/TokenParser/BlockTokenParser.php +++ b/src/TokenParser/BlockTokenParser.php @@ -16,7 +16,7 @@ use Twig\Node\BlockNode; use Twig\Node\BlockReferenceNode; use Twig\Node\Node; -use Twig\Node\YieldExpressionNode; +use Twig\Node\PrintNode; use Twig\Token; /** @@ -54,7 +54,7 @@ public function parse(Token $token): Node } } else { $body = new Node([ - new YieldExpressionNode($this->parser->getExpressionParser()->parseExpression(), $lineno), + new PrintNode($this->parser->getExpressionParser()->parseExpression(), $lineno), ]); } $stream->expect(/* Token::BLOCK_END_TYPE */ 3); diff --git a/tests/IntegrationTest.php b/tests/IntegrationTest.php index f2ee4eb1f35..e2b211a01de 100644 --- a/tests/IntegrationTest.php +++ b/tests/IntegrationTest.php @@ -18,7 +18,7 @@ use Twig\Extension\StringLoaderExtension; use Twig\Node\Expression\ConstantExpression; use Twig\Node\Node; -use Twig\Node\YieldExpressionNode; +use Twig\Node\PrintNode; use Twig\Sandbox\SecurityPolicy; use Twig\Test\IntegrationTestCase; use Twig\Token; @@ -135,7 +135,7 @@ public function parse(Token $token): Node { $this->parser->getStream()->expect(Token::BLOCK_END_TYPE); - return new YieldExpressionNode(new ConstantExpression('§', -1), -1); + return new PrintNode(new ConstantExpression('§', -1), -1); } public function getTag(): string diff --git a/tests/Node/AutoEscapeTest.php b/tests/Node/AutoEscapeTest.php index d0f641c083c..b2df9b1605c 100644 --- a/tests/Node/AutoEscapeTest.php +++ b/tests/Node/AutoEscapeTest.php @@ -31,9 +31,10 @@ public function getTests() { $body = new Node([new TextNode('foo', 1)]); $node = new AutoEscapeNode(true, $body, 1); + $displayStmt = $this->getEchoOrYield(); return [ - [$node, "// line 1\necho \"foo\";"], + [$node, "// line 1\n$displayStmt \"foo\";"], ]; } } diff --git a/tests/Node/BlockTest.php b/tests/Node/BlockTest.php index 07e9373db21..29b2e0f9d35 100644 --- a/tests/Node/BlockTest.php +++ b/tests/Node/BlockTest.php @@ -16,7 +16,6 @@ use Twig\Node\BlockNode; use Twig\Node\Node; use Twig\Node\TextNode; -use Twig\Node\YieldTextNode; use Twig\Test\NodeTestCase; class BlockTest extends NodeTestCase @@ -34,7 +33,7 @@ public function getTests() { $tests = []; - $tests[] = [new BlockNode('foo', new YieldTextNode('foo', 1), 1), <<setAttribute('with_loop', false); + $displayStmt = $this->getEchoOrYield(); $tests[] = [$node, <<getVariableGetter('items')}); foreach (\$context['_seq'] as \$context["key"] => \$context["item"]) { - echo {$this->getVariableGetter('foo')}; + $displayStmt {$this->getVariableGetter('foo')}; } \$_parent = \$context['_parent']; unset(\$context['_seq'], \$context['_iterated'], \$context['key'], \$context['item'], \$context['_parent'], \$context['loop']); @@ -93,7 +94,7 @@ public function getTests() \$context['loop']['last'] = 1 === \$length; } foreach (\$context['_seq'] as \$context["k"] => \$context["v"]) { - echo {$this->getVariableGetter('foo')}; + $displayStmt {$this->getVariableGetter('foo')}; ++\$context['loop']['index0']; ++\$context['loop']['index']; \$context['loop']['first'] = false; @@ -135,7 +136,7 @@ public function getTests() \$context['loop']['last'] = 1 === \$length; } foreach (\$context['_seq'] as \$context["k"] => \$context["v"]) { - echo {$this->getVariableGetter('foo')}; + $displayStmt {$this->getVariableGetter('foo')}; ++\$context['loop']['index0']; ++\$context['loop']['index']; \$context['loop']['first'] = false; @@ -178,7 +179,7 @@ public function getTests() \$context['loop']['last'] = 1 === \$length; } foreach (\$context['_seq'] as \$context["k"] => \$context["v"]) { - echo {$this->getVariableGetter('foo')}; + $displayStmt {$this->getVariableGetter('foo')}; \$context['_iterated'] = true; ++\$context['loop']['index0']; ++\$context['loop']['index']; @@ -190,7 +191,7 @@ public function getTests() } } if (!\$context['_iterated']) { - echo {$this->getVariableGetter('foo')}; + $displayStmt {$this->getVariableGetter('foo')}; } \$_parent = \$context['_parent']; unset(\$context['_seq'], \$context['_iterated'], \$context['k'], \$context['v'], \$context['_parent'], \$context['loop']); diff --git a/tests/Node/IfTest.php b/tests/Node/IfTest.php index d5a6eac8ab4..5dda061d0cb 100644 --- a/tests/Node/IfTest.php +++ b/tests/Node/IfTest.php @@ -47,11 +47,12 @@ public function getTests() ], [], 1); $else = null; $node = new IfNode($t, $else, 1); + $displayStmt = $this->getEchoOrYield(); $tests[] = [$node, <<getVariableGetter('foo')}; + $displayStmt {$this->getVariableGetter('foo')}; } EOF ]; @@ -68,9 +69,9 @@ public function getTests() $tests[] = [$node, <<getVariableGetter('foo')}; + $displayStmt {$this->getVariableGetter('foo')}; } elseif (false) { - echo {$this->getVariableGetter('bar')}; + $displayStmt {$this->getVariableGetter('bar')}; } EOF ]; @@ -85,9 +86,9 @@ public function getTests() $tests[] = [$node, <<getVariableGetter('foo')}; + $displayStmt {$this->getVariableGetter('foo')}; } else { - echo {$this->getVariableGetter('bar')}; + $displayStmt {$this->getVariableGetter('bar')}; } EOF ]; diff --git a/tests/Node/MacroTest.php b/tests/Node/MacroTest.php index 16ccd92cbbc..7efea3cefc6 100644 --- a/tests/Node/MacroTest.php +++ b/tests/Node/MacroTest.php @@ -18,7 +18,6 @@ use Twig\Node\MacroNode; use Twig\Node\Node; use Twig\Node\TextNode; -use Twig\Node\YieldTextNode; use Twig\Test\NodeTestCase; class MacroTest extends NodeTestCase @@ -43,7 +42,7 @@ public function getTests() 'bar' => new ConstantExpression('Foo', 1), ], [], 1); - $body = new YieldTextNode('foo', 1); + $body = new TextNode('foo', 1); $node = new MacroNode('foo', $body, $arguments, 1); $text[] = [$node, <<getEnvironment()->useYield() ? 'YieldingTemplate' : 'Template'; + $displayStmt = $this->getEchoOrYield(); $node = new ModuleNode($body, $extends, $blocks, $macros, $traits, new Node([]), $source); $tests[] = [$node, <<macros; // line 1 - echo "foo"; + $displayStmt "foo"; } /** diff --git a/tests/Node/PrintTest.php b/tests/Node/PrintTest.php index 49f8eb49840..f951c2e3695 100644 --- a/tests/Node/PrintTest.php +++ b/tests/Node/PrintTest.php @@ -28,7 +28,9 @@ public function testConstructor() public function getTests() { $tests = []; - $tests[] = [new PrintNode(new ConstantExpression('foo', 1), 1), "// line 1\necho \"foo\";"]; + $displayStmt = $this->getEchoOrYield(); + + $tests[] = [new PrintNode(new ConstantExpression('foo', 1), 1), "// line 1\n$displayStmt \"foo\";"]; return $tests; } diff --git a/tests/Node/SandboxTest.php b/tests/Node/SandboxTest.php index 7cbddd75fb9..bf16f1f03c0 100644 --- a/tests/Node/SandboxTest.php +++ b/tests/Node/SandboxTest.php @@ -31,6 +31,7 @@ public function getTests() $body = new TextNode('foo', 1); $node = new SandboxNode($body, 1); + $displayStmt = $this->getEchoOrYield(); $tests[] = [$node, <<sandbox->enableSandbox(); } try { - echo "foo"; + $displayStmt "foo"; } finally { if (!\$alreadySandboxed) { \$this->sandbox->disableSandbox(); diff --git a/tests/Node/SetTest.php b/tests/Node/SetTest.php index 98d4e573571..70b3530bd78 100644 --- a/tests/Node/SetTest.php +++ b/tests/Node/SetTest.php @@ -55,7 +55,7 @@ public function getTests() $tests[] = [$node, <<env->getCharset()); EOF , new Environment(new ArrayLoader(), ['use_yield' => true]), diff --git a/tests/Node/TextTest.php b/tests/Node/TextTest.php index ace191213d8..31639cc2deb 100644 --- a/tests/Node/TextTest.php +++ b/tests/Node/TextTest.php @@ -26,7 +26,8 @@ public function testConstructor() public function getTests() { $tests = []; - $tests[] = [new TextNode('foo', 1), "// line 1\necho \"foo\";"]; + $displayStmt = $this->getEchoOrYield(); + $tests[] = [new TextNode('foo', 1), "// line 1\n$displayStmt \"foo\";"]; return $tests; } From a9c7307b5c0ab97735ba96d5c33caf8334b95548 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Fri, 5 Jan 2024 19:16:08 +0100 Subject: [PATCH 158/812] Tweak code --- src/Environment.php | 10 ++++++++-- src/Test/NodeTestCase.php | 5 ++++- src/YieldingTemplate.php | 8 ++++++++ tests/Node/BlockTest.php | 16 +++++++++------- tests/Node/MacroTest.php | 20 +++++++++++--------- tests/Node/SetTest.php | 16 ++++++++++------ 6 files changed, 50 insertions(+), 25 deletions(-) diff --git a/src/Environment.php b/src/Environment.php index 030e86bc931..dbe382d4403 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -66,6 +66,7 @@ class Environment private $runtimeLoaders = []; private $runtimes = []; private $optionsHash; + /** @var bool */ private $useYield; /** @@ -99,7 +100,9 @@ class Environment * (default to -1 which means that all optimizations are enabled; * set it to 0 to disable). * - * * use_yield: Enable the Twig 4 mode where template are using yield instead of echo + * * use_yield: Enable a new mode where template are using "yield" instead of "echo" + * (default to "false", but switch it to "true" when possible + * as this will be the only supported mode in Twig 4.0) */ public function __construct(LoaderInterface $loader, $options = []) { @@ -117,7 +120,9 @@ public function __construct(LoaderInterface $loader, $options = []) ], $options); $this->useYield = (bool) $options['use_yield']; - // FIXME: deprecation if use_yield is false + if (!$this->useYield) { + trigger_deprecation('twig/twig', '3.9.0', 'Not setting "use_yield" to "true" is deprecated.'); + } $this->debug = (bool) $options['debug']; $this->setCharset($options['charset'] ?? 'UTF-8'); @@ -849,6 +854,7 @@ private function updateOptionsHash(): void self::VERSION, (int) $this->debug, (int) $this->strictVariables, + $this->useYield ? '1' : '0', ]); } } diff --git a/src/Test/NodeTestCase.php b/src/Test/NodeTestCase.php index b10ae11d7e1..8df0e9fee3a 100644 --- a/src/Test/NodeTestCase.php +++ b/src/Test/NodeTestCase.php @@ -19,7 +19,10 @@ abstract class NodeTestCase extends TestCase { - private Environment $currentEnv; + /** + * @var Environment + */ + private $currentEnv; abstract public function getTests(); diff --git a/src/YieldingTemplate.php b/src/YieldingTemplate.php index e614d6bd512..9f8cb418b8b 100644 --- a/src/YieldingTemplate.php +++ b/src/YieldingTemplate.php @@ -21,6 +21,9 @@ */ abstract class YieldingTemplate extends Template { + /** + * @return iterable + */ public function yield(array $context, array $blocks = []): iterable { $context = $this->env->mergeGlobals($context); @@ -65,6 +68,9 @@ public function display(array $context, array $blocks = []): void } } + /** + * @return iterable + */ public function yieldBlock($name, array $context, array $blocks = [], $useBlocks = true, Template $templateContext = null) { if ($useBlocks && isset($blocks[$name])) { @@ -133,6 +139,8 @@ public function renderBlock($name, array $context, array $blocks = [], $useBlock * @param string $name The block name to display from the parent * @param array $context The context * @param array $blocks The current set of blocks + * + * @return iterable */ public function yieldParentBlock($name, array $context, array $blocks = []) { diff --git a/tests/Node/BlockTest.php b/tests/Node/BlockTest.php index 29b2e0f9d35..0edc0fc033d 100644 --- a/tests/Node/BlockTest.php +++ b/tests/Node/BlockTest.php @@ -44,7 +44,8 @@ public function block_foo(\$context, array \$blocks = []) , new Environment(new ArrayLoader(), ['use_yield' => true]) ]; - $tests[] = [new BlockNode('foo', new TextNode('foo', 1), 1), <<getEnvironment()->useYield()) { + $tests[] = [new BlockNode('foo', new TextNode('foo', 1), 1), << false]) - ]; - - $tests[] = [new BlockNode('foo', new Node(), 1), << true]) - ]; + , new Environment(new ArrayLoader()) + ]; + } return $tests; } diff --git a/tests/Node/MacroTest.php b/tests/Node/MacroTest.php index 7efea3cefc6..948949af66d 100644 --- a/tests/Node/MacroTest.php +++ b/tests/Node/MacroTest.php @@ -45,7 +45,8 @@ public function getTests() $body = new TextNode('foo', 1); $node = new MacroNode('foo', $body, $arguments, 1); - $text[] = [$node, <<getEnvironment()->useYield()) { + $text[] = [$node, <<env->getCharset()); } EOF - , new Environment(new ArrayLoader(), ['use_yield' => true]), - ]; + , new Environment(new ArrayLoader()), + ]; + } else { + $body = new TextNode('foo', 1); + $node = new MacroNode('foo', $body, $arguments, 1); - $body = new TextNode('foo', 1); - $node = new MacroNode('foo', $body, $arguments, 1); - - $tests[] = [$node, << false]), - ]; + , new Environment(new ArrayLoader()), + ]; + } return $tests; } diff --git a/tests/Node/SetTest.php b/tests/Node/SetTest.php index 70b3530bd78..84d8a77c087 100644 --- a/tests/Node/SetTest.php +++ b/tests/Node/SetTest.php @@ -48,19 +48,22 @@ public function getTests() EOF ]; + $names = new Node([new AssignNameExpression('foo', 1)], [], 1); $values = new Node([new PrintNode(new ConstantExpression('foo', 1), 1)], [], 1); $node = new SetNode(true, $names, $values, 1); - $tests[] = [$node, <<getEnvironment()->useYield()) { + $tests[] = [$node, <<env->getCharset()); EOF - , new Environment(new ArrayLoader(), ['use_yield' => true]), - ]; - $tests[] = [$node, << false]), - ]; + , new Environment(new ArrayLoader()), + ]; + } $names = new Node([new AssignNameExpression('foo', 1)], [], 1); $values = new TextNode('foo', 1); From 4fd62abb872c976deeb99f4a3241a059d6812c91 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sat, 6 Jan 2024 16:51:59 +0100 Subject: [PATCH 159/812] - --- .github/workflows/ci.yml | 5 ++++- CHANGELOG | 2 ++ tests/ErrorTest.php | 4 +--- tests/Fixtures/errors/leak-output.php | 4 ++-- tests/ignore-use-yield-deprecations | 1 + 5 files changed, 10 insertions(+), 6 deletions(-) create mode 100644 tests/ignore-use-yield-deprecations diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 153508c5693..f78843e8edb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,6 +20,9 @@ jobs: continue-on-error: ${{ matrix.experimental }} + env: + SYMFONY_DEPRECATIONS_HELPER: ignoreFile=./tests/ignore-use-yield-deprecations + strategy: matrix: php-version: @@ -132,7 +135,7 @@ jobs: - name: "Run tests" working-directory: extra/${{ matrix.extension }} - run: ../../vendor/bin/simple-phpunit + run: SYMFONY_DEPRECATIONS_HELPER=ignoreFile=../../tests/ignore-use-yield-deprecations ../../vendor/bin/simple-phpunit integration-tests: needs: diff --git a/CHANGELOG b/CHANGELOG index 55a307c39b3..b14430bcd06 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,7 @@ # 3.9.0 (2023-XX-XX) + * Add a new "yield" mode for output generation + The "use_yield" Environment option controls the strategy: use "false" for "echo", "true" for "yield" * Add return type for Symfony 7 compatibility * Fix premature loop exit in Security Policy lookup of allowed methods/properties * Deprecate all internal extension functions in favor of methods on the extension classes diff --git a/tests/ErrorTest.php b/tests/ErrorTest.php index ef12567c4d6..db6418ed685 100644 --- a/tests/ErrorTest.php +++ b/tests/ErrorTest.php @@ -304,8 +304,7 @@ public function getErroredTemplates() ], ]; } -/* These tests don't make sense to me - Depending on whether you're using echo ->render() or display(), they don't behave in the same way + public function testTwigLeakOutputInDebugMode() { $output = exec(sprintf('%s %s debug', \PHP_BINARY, escapeshellarg(__DIR__.'/Fixtures/errors/leak-output.php'))); @@ -319,7 +318,6 @@ public function testDoesNotTwigLeakOutput() $this->assertSame('', $output); } -*/ } class ErrorTest_Foo diff --git a/tests/Fixtures/errors/leak-output.php b/tests/Fixtures/errors/leak-output.php index fdb08502d77..6ad2295089c 100644 --- a/tests/Fixtures/errors/leak-output.php +++ b/tests/Fixtures/errors/leak-output.php @@ -27,7 +27,7 @@ public function broken() $loader = new ArrayLoader([ 'index.html.twig' => 'Hello {{ "world"|broken }}', ]); -$twig = new Environment($loader, ['debug' => isset($argv[1])]); +$twig = new Environment($loader, ['debug' => isset($argv[1]), 'use_yield' => false]); $twig->addExtension(new BrokenExtension()); -$twig->display('index.html.twig'); +echo $twig->render('index.html.twig'); diff --git a/tests/ignore-use-yield-deprecations b/tests/ignore-use-yield-deprecations new file mode 100644 index 00000000000..0f844211547 --- /dev/null +++ b/tests/ignore-use-yield-deprecations @@ -0,0 +1 @@ +%Since twig/twig 3.9.0: Not setting "use_yield" to "true" is deprecated.% From 71e90fab7b3d627af3300d76aa0b3ac857f43d40 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sun, 7 Jan 2024 21:44:50 +0100 Subject: [PATCH 160/812] Remove some tests --- .github/workflows/ci.yml | 5 +--- tests/ErrorTest.php | 14 ------------ tests/Fixtures/errors/leak-output.php | 33 --------------------------- 3 files changed, 1 insertion(+), 51 deletions(-) delete mode 100644 tests/Fixtures/errors/leak-output.php diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f78843e8edb..527ac2c12d6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,9 +20,6 @@ jobs: continue-on-error: ${{ matrix.experimental }} - env: - SYMFONY_DEPRECATIONS_HELPER: ignoreFile=./tests/ignore-use-yield-deprecations - strategy: matrix: php-version: @@ -64,7 +61,7 @@ jobs: run: vendor/bin/simple-phpunit --version - name: "Run tests" - run: vendor/bin/simple-phpunit + run: SYMFONY_DEPRECATIONS_HELPER=ignoreFile=./tests/ignore-use-yield-deprecations vendor/bin/simple-phpunit extension-tests: needs: diff --git a/tests/ErrorTest.php b/tests/ErrorTest.php index db6418ed685..ee6a86c49aa 100644 --- a/tests/ErrorTest.php +++ b/tests/ErrorTest.php @@ -304,20 +304,6 @@ public function getErroredTemplates() ], ]; } - - public function testTwigLeakOutputInDebugMode() - { - $output = exec(sprintf('%s %s debug', \PHP_BINARY, escapeshellarg(__DIR__.'/Fixtures/errors/leak-output.php'))); - - $this->assertSame('Hello OOPS', $output); - } - - public function testDoesNotTwigLeakOutput() - { - $output = exec(sprintf('%s %s', \PHP_BINARY, escapeshellarg(__DIR__.'/Fixtures/errors/leak-output.php'))); - - $this->assertSame('', $output); - } } class ErrorTest_Foo diff --git a/tests/Fixtures/errors/leak-output.php b/tests/Fixtures/errors/leak-output.php deleted file mode 100644 index 6ad2295089c..00000000000 --- a/tests/Fixtures/errors/leak-output.php +++ /dev/null @@ -1,33 +0,0 @@ - 'Hello {{ "world"|broken }}', -]); -$twig = new Environment($loader, ['debug' => isset($argv[1]), 'use_yield' => false]); -$twig->addExtension(new BrokenExtension()); - -echo $twig->render('index.html.twig'); From 72922a361216b346d6a535fd87ea53ad0aa8951a Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Thu, 11 Jan 2024 07:58:42 +0100 Subject: [PATCH 161/812] - --- src/Node/Expression/BlockReferenceExpression.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/Node/Expression/BlockReferenceExpression.php b/src/Node/Expression/BlockReferenceExpression.php index 67c5781cf9c..e63c5b2149d 100644 --- a/src/Node/Expression/BlockReferenceExpression.php +++ b/src/Node/Expression/BlockReferenceExpression.php @@ -42,9 +42,6 @@ public function compile(Compiler $compiler): void if ($compiler->getEnvironment()->useYield()) { $compiler->write('yield from '); - } - - if ($compiler->getEnvironment()->useYield()) { $this ->compileTemplateCall($compiler, 'yieldBlock') ->raw(";\n"); From 1469e6a6bcb30b12acf267a71aa3a16ea05e83b9 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Thu, 11 Jan 2024 08:22:24 +0100 Subject: [PATCH 162/812] Add support for templates that do not have output Nodes --- src/Node/ModuleNode.php | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/Node/ModuleNode.php b/src/Node/ModuleNode.php index ee04e2d211b..a9d494e6924 100644 --- a/src/Node/ModuleNode.php +++ b/src/Node/ModuleNode.php @@ -342,6 +342,9 @@ protected function compileDisplay(Compiler $compiler) } else { $compiler->raw("->display(\$context, array_merge(\$this->blocks, \$blocks));\n"); } + } elseif ($compiler->getEnvironment()->useYield() && !$this->hasNodeOutputNodes($this->getNode('body'))) { + // ensure at least one yield call even for templates with no output + $compiler->write("yield;\n"); } $compiler @@ -483,4 +486,19 @@ protected function compileLoadTemplate(Compiler $compiler, $node, $var) throw new \LogicException('Trait templates can only be constant nodes.'); } } + + private function hasNodeOutputNodes(Node $node): bool + { + if ($node instanceof NodeOutputInterface) { + return true; + } + + foreach ($node as $child) { + if ($this->hasNodeOutputNodes($child)) { + return true; + } + } + + return false; + } } From 1591aa5c7d2b66e3fc307f5b1c182942d80c4093 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Thu, 11 Jan 2024 08:36:03 +0100 Subject: [PATCH 163/812] Optimize code --- src/YieldingTemplate.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/YieldingTemplate.php b/src/YieldingTemplate.php index 9f8cb418b8b..53027b93211 100644 --- a/src/YieldingTemplate.php +++ b/src/YieldingTemplate.php @@ -54,7 +54,7 @@ public function yield(array $context, array $blocks = []): iterable public function render(array $context): string { $content = ''; - foreach ($this->yield($this->env->mergeGlobals($context), array_merge($this->blocks)) as $data) { + foreach ($this->yield($context) as $data) { $content .= $data; } @@ -63,7 +63,7 @@ public function render(array $context): string public function display(array $context, array $blocks = []): void { - foreach ($this->yield($this->env->mergeGlobals($context), array_merge($this->blocks)) as $data) { + foreach ($this->yield($context, $blocks) as $data) { echo $data; } } From 210e2d2b9eb4f73c48dc9adc7bd1d93d2db6f3f3 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Fri, 12 Jan 2024 12:54:58 +0100 Subject: [PATCH 164/812] Fix tests --- extra/twig-extra-bundle/Tests/Fixture/Kernel.php | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/extra/twig-extra-bundle/Tests/Fixture/Kernel.php b/extra/twig-extra-bundle/Tests/Fixture/Kernel.php index 3df6357fe1f..3283af20252 100644 --- a/extra/twig-extra-bundle/Tests/Fixture/Kernel.php +++ b/extra/twig-extra-bundle/Tests/Fixture/Kernel.php @@ -24,11 +24,17 @@ public function registerBundles(): iterable protected function configureContainer(ContainerBuilder $c, LoaderInterface $loader): void { - $c->loadFromExtension('framework', [ + $config = [ 'secret' => 'S3CRET', 'test' => true, - ]); - + 'router' => ['utf8' => true], + 'http_method_override' => false, + ]; + if (Kernel::MAJOR_VERSION >= 6 && Kernel::MINOR_VERSION >= 2) { + $config['handle_all_throwables'] = true; + $config['php_errors']['log'] = true; + } + $c->loadFromExtension('framework', $config); $c->loadFromExtension('twig', [ 'default_path' => __DIR__.'/views', ]); From 14d38036477b93976b56b1ee7046e86a390645dd Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sun, 14 Jan 2024 09:25:32 +0100 Subject: [PATCH 165/812] Add a deprecation when a Node uses echo/print --- CHANGELOG | 5 ++-- .../Tests/Fixture/Kernel.php | 2 +- src/Compiler.php | 28 +++++++++++++++++++ src/Node/Expression/InlinePrint.php | 2 ++ src/Node/PrintNode.php | 6 +++- src/Node/TextNode.php | 6 +++- 6 files changed, 44 insertions(+), 5 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index b14430bcd06..d0bc0cae33c 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,7 +1,8 @@ -# 3.9.0 (2023-XX-XX) +# 3.9.0 (2024-XX-XX) * Add a new "yield" mode for output generation - The "use_yield" Environment option controls the strategy: use "false" for "echo", "true" for "yield" + The "use_yield" Environment option controls the output strategy: use "false" for "echo", "true" for "yield" + "yield" will be the only strategy supported in the next major version * Add return type for Symfony 7 compatibility * Fix premature loop exit in Security Policy lookup of allowed methods/properties * Deprecate all internal extension functions in favor of methods on the extension classes diff --git a/extra/twig-extra-bundle/Tests/Fixture/Kernel.php b/extra/twig-extra-bundle/Tests/Fixture/Kernel.php index 3283af20252..857ed95d41a 100644 --- a/extra/twig-extra-bundle/Tests/Fixture/Kernel.php +++ b/extra/twig-extra-bundle/Tests/Fixture/Kernel.php @@ -30,7 +30,7 @@ protected function configureContainer(ContainerBuilder $c, LoaderInterface $load 'router' => ['utf8' => true], 'http_method_override' => false, ]; - if (Kernel::MAJOR_VERSION >= 6 && Kernel::MINOR_VERSION >= 2) { + if (6 === Kernel::MAJOR_VERSION) { $config['handle_all_throwables'] = true; $config['php_errors']['log'] = true; } diff --git a/src/Compiler.php b/src/Compiler.php index eb652c61a4e..543b4e1046d 100644 --- a/src/Compiler.php +++ b/src/Compiler.php @@ -27,10 +27,12 @@ class Compiler private $sourceOffset; private $sourceLine; private $varNameSalt = 0; + private $checkForOutput; public function __construct(Environment $env) { $this->env = $env; + $this->checkForOutput = $env->isDebug(); } public function getEnvironment(): Environment @@ -85,6 +87,16 @@ public function subcompile(Node $node, bool $raw = true) return $this; } + /** + * @return $this + */ + public function checkForOutput(bool $checkForOutput) + { + $this->checkForOutput = $checkForOutput ? $this->env->isDebug() : false; + + return $this; + } + /** * Adds a raw string to the compiled code. * @@ -92,6 +104,9 @@ public function subcompile(Node $node, bool $raw = true) */ public function raw(string $string) { + if ($this->checkForOutput) { + $this->checkStringForOutput(trim($string)); + } $this->source .= $string; return $this; @@ -105,6 +120,10 @@ public function raw(string $string) public function write(...$strings) { foreach ($strings as $string) { + if ($this->checkForOutput) { + $this->checkStringForOutput(trim($string)); + } + $this->source .= str_repeat(' ', $this->indentation * 4).$string; } @@ -220,4 +239,13 @@ public function getVarName(): string { return sprintf('__internal_compile_%d', $this->varNameSalt++); } + + private function checkStringForOutput(string $string): void + { + if (str_starts_with($string, 'echo')) { + trigger_deprecation('twig/twig', '3.9.0', 'Using "echo" in a "Node::compile()" method is deprecated; use a "TextNode" or "PrintNode" instead or use "yield" when "use_yield" is "true" on the environment (triggered by "%s").', $string); + } elseif (str_starts_with($string, 'print')) { + trigger_deprecation('twig/twig', '3.9.0', 'Using "print" in a "Node::compile()" method is deprecated; use a "TextNode" or "PrintNode" instead or use "yield" when "use_yield" is "true" on the environment (triggered by "%s").', $string); + } + } } diff --git a/src/Node/Expression/InlinePrint.php b/src/Node/Expression/InlinePrint.php index 8c262e2e15d..725536a869d 100644 --- a/src/Node/Expression/InlinePrint.php +++ b/src/Node/Expression/InlinePrint.php @@ -33,7 +33,9 @@ public function compile(Compiler $compiler): void ; } else { $compiler + ->checkForOutput(false) ->raw('print(') + ->checkForOutput(true) ->subcompile($this->getNode('node')) ->raw(')') ; diff --git a/src/Node/PrintNode.php b/src/Node/PrintNode.php index 78c67fa2ed0..36bcaee6410 100644 --- a/src/Node/PrintNode.php +++ b/src/Node/PrintNode.php @@ -34,7 +34,11 @@ public function compile(Compiler $compiler): void if ($compiler->getEnvironment()->useYield()) { $compiler->write('yield '); } else { - $compiler->write('echo '); + $compiler + ->checkForOutput(false) + ->write('echo ') + ->checkForOutput(true) + ; } $compiler diff --git a/src/Node/TextNode.php b/src/Node/TextNode.php index 561288ca78c..3e417dad60d 100644 --- a/src/Node/TextNode.php +++ b/src/Node/TextNode.php @@ -33,7 +33,11 @@ public function compile(Compiler $compiler): void if ($compiler->getEnvironment()->useYield()) { $compiler->write('yield '); } else { - $compiler->write('echo '); + $compiler + ->checkForOutput(false) + ->write('echo ') + ->checkForOutput(true) + ; } $compiler From 0d3a28390702a531ba65baa089c50058a2c2c872 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Thu, 25 Jan 2024 07:42:57 +0100 Subject: [PATCH 166/812] Tweak code --- src/Node/BlockNode.php | 2 +- src/Node/ModuleNode.php | 2 +- tests/Node/BlockTest.php | 18 +++++++++--------- tests/TemplateWrapperTest.php | 17 +++++++++++------ 4 files changed, 22 insertions(+), 17 deletions(-) diff --git a/src/Node/BlockNode.php b/src/Node/BlockNode.php index bca7a6ab127..241dff0b2ec 100644 --- a/src/Node/BlockNode.php +++ b/src/Node/BlockNode.php @@ -41,7 +41,7 @@ public function compile(Compiler $compiler): void if (!$this->getNode('body') instanceof NodeOutputInterface && $compiler->getEnvironment()->useYield()) { // needed when body doesn't yield anything - $compiler->write("yield;\n"); + $compiler->write("yield '';\n"); } $compiler diff --git a/src/Node/ModuleNode.php b/src/Node/ModuleNode.php index a9d494e6924..4bc35344ffb 100644 --- a/src/Node/ModuleNode.php +++ b/src/Node/ModuleNode.php @@ -344,7 +344,7 @@ protected function compileDisplay(Compiler $compiler) } } elseif ($compiler->getEnvironment()->useYield() && !$this->hasNodeOutputNodes($this->getNode('body'))) { // ensure at least one yield call even for templates with no output - $compiler->write("yield;\n"); + $compiler->write("yield '';\n"); } $compiler diff --git a/tests/Node/BlockTest.php b/tests/Node/BlockTest.php index 0edc0fc033d..cdc2c861cdb 100644 --- a/tests/Node/BlockTest.php +++ b/tests/Node/BlockTest.php @@ -33,35 +33,35 @@ public function getTests() { $tests = []; - $tests[] = [new BlockNode('foo', new TextNode('foo', 1), 1), <<getEnvironment()->useYield()) { + $tests[] = [new BlockNode('foo', new TextNode('foo', 1), 1), <<macros; - yield "foo"; + echo "foo"; } EOF - , new Environment(new ArrayLoader(), ['use_yield' => true]) - ]; - - if (!$this->getEnvironment()->useYield()) { + , new Environment(new ArrayLoader()) + ]; + } else { $tests[] = [new BlockNode('foo', new TextNode('foo', 1), 1), <<macros; - echo "foo"; + yield "foo"; } EOF , new Environment(new ArrayLoader()) ]; - } else { + $tests[] = [new BlockNode('foo', new Node(), 1), <<macros; - yield; + yield ''; } EOF , new Environment(new ArrayLoader()) diff --git a/tests/TemplateWrapperTest.php b/tests/TemplateWrapperTest.php index 7e002bf7a15..776ac3fa806 100644 --- a/tests/TemplateWrapperTest.php +++ b/tests/TemplateWrapperTest.php @@ -58,14 +58,19 @@ public function testDisplayBlock() { $twig = new Environment(new ArrayLoader([ 'index' => '{% block foo %}{{ foo }}{{ bar }}{% endblock %}', - ], ['use_yield' => false])); - $twig->addGlobal('bar', 'BAR'); + ])); - $wrapper = $twig->load('index'); + if (!$twig->useYield()) { + $twig->addGlobal('bar', 'BAR'); + + $wrapper = $twig->load('index'); - ob_start(); - $wrapper->displayBlock('foo', ['foo' => 'FOO']); + ob_start(); + $wrapper->displayBlock('foo', ['foo' => 'FOO']); - $this->assertEquals('FOOBAR', ob_get_clean()); + $this->assertEquals('FOOBAR', ob_get_clean()); + } else { + $this->markTestSkipped('yield not used.'); + } } } From a93723eae25ae5dcfbed876a1804107dbf37536c Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Thu, 25 Jan 2024 08:09:49 +0100 Subject: [PATCH 167/812] Fix bug --- src/YieldingTemplate.php | 13 +++++++++---- tests/Fixtures/functions/parent_in_condition.test | 11 +++++++++++ 2 files changed, 20 insertions(+), 4 deletions(-) create mode 100644 tests/Fixtures/functions/parent_in_condition.test diff --git a/src/YieldingTemplate.php b/src/YieldingTemplate.php index 53027b93211..93ee894d2b8 100644 --- a/src/YieldingTemplate.php +++ b/src/YieldingTemplate.php @@ -155,17 +155,22 @@ public function yieldParentBlock($name, array $context, array $blocks = []) } } - public function displayBlock($name, array $context, array $blocks = [], $useBlocks = true, Template $templateContext = null) + public function renderParentBlock($name, array $context, array $blocks = []) { - throw new RuntimeError(sprintf('Calling "%s" for block "%s" is not supported as "use_yield" is set to "true".', __METHOD__, $name), -1, $this->getSourceContext()); + $content = ''; + foreach ($this->yieldParentBlock($name, $context, $blocks) as $data) { + $content .= $data; + } + + return $content; } - public function displayParentBlock($name, array $context, array $blocks = []) + public function displayBlock($name, array $context, array $blocks = [], $useBlocks = true, Template $templateContext = null) { throw new RuntimeError(sprintf('Calling "%s" for block "%s" is not supported as "use_yield" is set to "true".', __METHOD__, $name), -1, $this->getSourceContext()); } - public function renderParentBlock($name, array $context, array $blocks = []) + public function displayParentBlock($name, array $context, array $blocks = []) { throw new RuntimeError(sprintf('Calling "%s" for block "%s" is not supported as "use_yield" is set to "true".', __METHOD__, $name), -1, $this->getSourceContext()); } diff --git a/tests/Fixtures/functions/parent_in_condition.test b/tests/Fixtures/functions/parent_in_condition.test new file mode 100644 index 00000000000..f3d51d2dd41 --- /dev/null +++ b/tests/Fixtures/functions/parent_in_condition.test @@ -0,0 +1,11 @@ +--TEST-- +"block" calling parent() in a conditional expression +--TEMPLATE-- +{% extends "parent.twig" %} +{% block label %}{{ parent() ?: 'foo' }}{% endblock %} +--TEMPLATE(parent.twig)-- +{% block label %}PARENT_LABEL{% endblock %} +--DATA-- +return [] +--EXPECT-- +PARENT_LABEL From d3b59226209b9007a6ab812f190c94678d1090a7 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Mon, 5 Feb 2024 16:08:29 +0100 Subject: [PATCH 168/812] Fix tests config --- .github/workflows/ci.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 527ac2c12d6..3d24406006d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -128,9 +128,6 @@ jobs: sed -i -e "s/'use_yield' => false/'use_yield' => true/" extra/${{ matrix.extension }}/vendor/twig/twig/src/Environment.php - name: "Run tests for ${{ matrix.extension}}" - working-directory: ${{ matrix.extension}} - - - name: "Run tests" working-directory: extra/${{ matrix.extension }} run: SYMFONY_DEPRECATIONS_HELPER=ignoreFile=../../tests/ignore-use-yield-deprecations ../../vendor/bin/simple-phpunit From 84bb846172e67e2f44ac6602215a909ef832c24e Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Mon, 5 Feb 2024 16:10:51 +0100 Subject: [PATCH 169/812] Fix tests config --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3d24406006d..6274bd1fe29 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -119,7 +119,7 @@ jobs: run: vendor/bin/simple-phpunit --version - name: "Composer install ${{ matrix.extension}}" - working-directory: ${{ matrix.extension}} + working-directory: extra/${{ matrix.extension}} run: composer install - name: "Switch use_yield to true" From 2b2ac80d345f943728828188c48147b405401f78 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Mon, 5 Feb 2024 16:23:59 +0100 Subject: [PATCH 170/812] Fxi some errors reported by phpstan --- .../cache-extra/TokenParser/CacheTokenParser.php | 4 ++-- src/Environment.php | 4 ++-- src/ExpressionParser.php | 4 ++-- src/Extension/CoreExtension.php | 16 +++++++--------- src/Extension/EscaperExtension.php | 2 +- src/Node/Expression/Filter/DefaultFilter.php | 2 +- src/Node/Expression/Test/ConstantTest.php | 6 +++--- src/Node/Expression/Test/DivisiblebyTest.php | 2 +- src/Node/Expression/Test/SameasTest.php | 2 +- src/Node/IfNode.php | 6 +++--- src/Node/ModuleNode.php | 4 ++-- src/NodeVisitor/EscaperNodeVisitor.php | 4 ---- src/Parser.php | 3 +++ src/Test/IntegrationTestCase.php | 1 + src/TokenParser/ForTokenParser.php | 6 +++--- src/TokenStream.php | 4 +--- tests/Cache/FilesystemTest.php | 2 +- tests/CompilerTest.php | 4 ++-- tests/ExpressionParserTest.php | 4 ++-- tests/Node/Expression/ArrayTest.php | 2 +- tests/Node/ForTest.php | 2 +- tests/NodeVisitor/OptimizerTest.php | 4 ++-- 22 files changed, 42 insertions(+), 46 deletions(-) diff --git a/extra/cache-extra/TokenParser/CacheTokenParser.php b/extra/cache-extra/TokenParser/CacheTokenParser.php index 5590ab27441..323096dd1ae 100644 --- a/extra/cache-extra/TokenParser/CacheTokenParser.php +++ b/extra/cache-extra/TokenParser/CacheTokenParser.php @@ -40,13 +40,13 @@ public function parse(Token $token): Node if (1 !== \count($args)) { throw new SyntaxError(sprintf('The "ttl" modifier takes exactly one argument (%d given).', \count($args)), $stream->getCurrent()->getLine(), $stream->getSourceContext()); } - $ttl = $args->getNode(0); + $ttl = $args->getNode('0'); break; case 'tags': if (1 !== \count($args)) { throw new SyntaxError(sprintf('The "tags" modifier takes exactly one argument (%d given).', \count($args)), $stream->getCurrent()->getLine(), $stream->getSourceContext()); } - $tags = $args->getNode(0); + $tags = $args->getNode('0'); break; default: throw new SyntaxError(sprintf('Unknown "%s" configuration.', $k), $stream->getCurrent()->getLine(), $stream->getSourceContext()); diff --git a/src/Environment.php b/src/Environment.php index f9e0086c60a..1a575f6c68a 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -437,7 +437,7 @@ public function resolveTemplate($names): TemplateWrapper $count = \count($names); foreach ($names as $name) { if ($name instanceof Template) { - return $name; + return new TemplateWrapper($this, $name); } if ($name instanceof TemplateWrapper) { return $name; @@ -535,7 +535,7 @@ public function getLoader(): LoaderInterface public function setCharset(string $charset) { - if ('UTF8' === $charset = null === $charset ? null : strtoupper($charset)) { + if ('UTF8' === $charset = strtoupper($charset ?: '')) { // iconv on Windows requires "UTF-8" instead of "UTF8" $charset = 'UTF-8'; } diff --git a/src/ExpressionParser.php b/src/ExpressionParser.php index 912d0a930a5..6839bc93204 100644 --- a/src/ExpressionParser.php +++ b/src/ExpressionParser.php @@ -452,14 +452,14 @@ public function getFunctionNode($name, $line) throw new SyntaxError('The "block" function takes one argument (the block name).', $line, $this->parser->getStream()->getSourceContext()); } - return new BlockReferenceExpression($args->getNode(0), \count($args) > 1 ? $args->getNode(1) : null, $line); + return new BlockReferenceExpression($args->getNode('0'), \count($args) > 1 ? $args->getNode('1') : null, $line); case 'attribute': $args = $this->parseArguments(); if (\count($args) < 2) { throw new SyntaxError('The "attribute" function takes at least two arguments (the variable and the attributes).', $line, $this->parser->getStream()->getSourceContext()); } - return new GetAttrExpression($args->getNode(0), $args->getNode(1), \count($args) > 2 ? $args->getNode(2) : null, Template::ANY_CALL, $line); + return new GetAttrExpression($args->getNode('0'), $args->getNode('1'), \count($args) > 2 ? $args->getNode('2') : null, Template::ANY_CALL, $line); default: if (null !== $alias = $this->parser->getImportedSymbol('function', $name)) { $arguments = new ArrayExpression([], $line); diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index c4e0bdab228..598caca4536 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -1124,7 +1124,7 @@ public static function nl2br($string) /** * Removes whitespaces between HTML tags. * - * @param string|null $string + * @param string|null $content * * @return string * @@ -1241,11 +1241,7 @@ public static function striptags($string, $allowable_tags = null) */ public static function titleStringFilter(Environment $env, $string) { - if (null !== $charset = $env->getCharset()) { - return mb_convert_case($string ?? '', \MB_CASE_TITLE, $charset); - } - - return ucwords(strtolower($string ?? '')); + return mb_convert_case($string ?? '', \MB_CASE_TITLE, $env->getCharset()); } /** @@ -1436,6 +1432,8 @@ public static function source(Environment $env, $name, $ignoreMissing = false) if (!$ignoreMissing) { throw $e; } + + return ''; } } @@ -1731,9 +1729,9 @@ public static function getAttribute(Environment $env, Source $source, $object, $ * {# fruits now contains ['apple', 'orange'] #} * * - * @param array|Traversable $array An array - * @param mixed $name The column name - * @param mixed $index The column to use as the index/keys for the returned array + * @param array|\Traversable $array An array + * @param mixed $name The column name + * @param mixed $index The column to use as the index/keys for the returned array * * @return array The array of values * diff --git a/src/Extension/EscaperExtension.php b/src/Extension/EscaperExtension.php index 9a8c66c9047..dee0f79fe8c 100644 --- a/src/Extension/EscaperExtension.php +++ b/src/Extension/EscaperExtension.php @@ -175,7 +175,7 @@ public static function escapeFilterIsSafe(Node $filterArgs) * @param string $charset The charset * @param bool $autoescape Whether the function is called by the auto-escaping feature (true) or by the developer (false) * - * @return string + * @return string|Markup * * @internal */ diff --git a/src/Node/Expression/Filter/DefaultFilter.php b/src/Node/Expression/Filter/DefaultFilter.php index 6a572d48848..e8eae20ecd3 100644 --- a/src/Node/Expression/Filter/DefaultFilter.php +++ b/src/Node/Expression/Filter/DefaultFilter.php @@ -35,7 +35,7 @@ public function __construct(Node $node, ConstantExpression $filterName, Node $ar if ('default' === $filterName->getAttribute('value') && ($node instanceof NameExpression || $node instanceof GetAttrExpression)) { $test = new DefinedTest(clone $node, 'defined', new Node(), $node->getTemplateLine()); - $false = \count($arguments) ? $arguments->getNode(0) : new ConstantExpression('', $node->getTemplateLine()); + $false = \count($arguments) ? $arguments->getNode('0') : new ConstantExpression('', $node->getTemplateLine()); $node = new ConditionalExpression($test, $default, $false, $node->getTemplateLine()); } else { diff --git a/src/Node/Expression/Test/ConstantTest.php b/src/Node/Expression/Test/ConstantTest.php index 57e9319d574..867fd09517c 100644 --- a/src/Node/Expression/Test/ConstantTest.php +++ b/src/Node/Expression/Test/ConstantTest.php @@ -33,16 +33,16 @@ public function compile(Compiler $compiler): void ->raw(' === constant(') ; - if ($this->getNode('arguments')->hasNode(1)) { + if ($this->getNode('arguments')->hasNode('1')) { $compiler ->raw('get_class(') - ->subcompile($this->getNode('arguments')->getNode(1)) + ->subcompile($this->getNode('arguments')->getNode('1')) ->raw(')."::".') ; } $compiler - ->subcompile($this->getNode('arguments')->getNode(0)) + ->subcompile($this->getNode('arguments')->getNode('0')) ->raw('))') ; } diff --git a/src/Node/Expression/Test/DivisiblebyTest.php b/src/Node/Expression/Test/DivisiblebyTest.php index 4cb3ee09692..90d58a49a16 100644 --- a/src/Node/Expression/Test/DivisiblebyTest.php +++ b/src/Node/Expression/Test/DivisiblebyTest.php @@ -29,7 +29,7 @@ public function compile(Compiler $compiler): void ->raw('(0 == ') ->subcompile($this->getNode('node')) ->raw(' % ') - ->subcompile($this->getNode('arguments')->getNode(0)) + ->subcompile($this->getNode('arguments')->getNode('0')) ->raw(')') ; } diff --git a/src/Node/Expression/Test/SameasTest.php b/src/Node/Expression/Test/SameasTest.php index c96d2bc01a3..f1e24db6f7e 100644 --- a/src/Node/Expression/Test/SameasTest.php +++ b/src/Node/Expression/Test/SameasTest.php @@ -27,7 +27,7 @@ public function compile(Compiler $compiler): void ->raw('(') ->subcompile($this->getNode('node')) ->raw(' === ') - ->subcompile($this->getNode('arguments')->getNode(0)) + ->subcompile($this->getNode('arguments')->getNode('0')) ->raw(')') ; } diff --git a/src/Node/IfNode.php b/src/Node/IfNode.php index 569ab7950e0..b41ee828d43 100644 --- a/src/Node/IfNode.php +++ b/src/Node/IfNode.php @@ -47,13 +47,13 @@ public function compile(Compiler $compiler): void } $compiler - ->subcompile($this->getNode('tests')->getNode($i)) + ->subcompile($this->getNode('tests')->getNode((string) $i)) ->raw(") {\n") ->indent() ; // The node might not exists if the content is empty - if ($this->getNode('tests')->hasNode($i + 1)) { - $compiler->subcompile($this->getNode('tests')->getNode($i + 1)); + if ($this->getNode('tests')->hasNode((string) ($i + 1))) { + $compiler->subcompile($this->getNode('tests')->getNode((string) ($i + 1))); } } diff --git a/src/Node/ModuleNode.php b/src/Node/ModuleNode.php index dce335c63f5..44186dc5d00 100644 --- a/src/Node/ModuleNode.php +++ b/src/Node/ModuleNode.php @@ -381,7 +381,7 @@ protected function compileIsTraitable(Compiler $compiler) $traitable = !$this->hasNode('parent') && 0 === \count($this->getNode('macros')); if ($traitable) { if ($this->getNode('body') instanceof BodyNode) { - $nodes = $this->getNode('body')->getNode(0); + $nodes = $this->getNode('body')->getNode('0'); } else { $nodes = $this->getNode('body'); } @@ -418,7 +418,7 @@ protected function compileIsTraitable(Compiler $compiler) ->write(" */\n") ->write("public function isTraitable()\n", "{\n") ->indent() - ->write(sprintf("return %s;\n", $traitable ? 'true' : 'false')) + ->write("return false;\n") ->outdent() ->write("}\n\n") ; diff --git a/src/NodeVisitor/EscaperNodeVisitor.php b/src/NodeVisitor/EscaperNodeVisitor.php index c390d7cc71b..91e2ea89392 100644 --- a/src/NodeVisitor/EscaperNodeVisitor.php +++ b/src/NodeVisitor/EscaperNodeVisitor.php @@ -131,10 +131,6 @@ private function escapeInlinePrintNode(InlinePrint $node, Environment $env, stri private function escapePrintNode(PrintNode $node, Environment $env, string $type): Node { - if (false === $type) { - return $node; - } - $expression = $node->getNode('expr'); if ($this->isSafeFor($type, $expression, $env)) { diff --git a/src/Parser.php b/src/Parser.php index 4016a5f39ab..0c7629b5572 100644 --- a/src/Parser.php +++ b/src/Parser.php @@ -101,6 +101,9 @@ public function parse(TokenStream $stream, $test = null, bool $dropNeedle = fals $traverser = new NodeTraverser($this->env, $this->visitors); + /** + * @var ModuleNode $node + */ $node = $traverser->traverse($node); // restore previous stack so previous parse() call can resume working diff --git a/src/Test/IntegrationTestCase.php b/src/Test/IntegrationTestCase.php index 90ff39aadaf..570c378bbe0 100644 --- a/src/Test/IntegrationTestCase.php +++ b/src/Test/IntegrationTestCase.php @@ -149,6 +149,7 @@ protected function doIntegrationTest($file, $message, $condition, $templates, $e } if ($condition) { + $ret = ''; eval('$ret = '.$condition.';'); if (!$ret) { $this->markTestSkipped($condition); diff --git a/src/TokenParser/ForTokenParser.php b/src/TokenParser/ForTokenParser.php index bac8ba2dae8..1af6da8fde5 100644 --- a/src/TokenParser/ForTokenParser.php +++ b/src/TokenParser/ForTokenParser.php @@ -49,12 +49,12 @@ public function parse(Token $token): Node $stream->expect(/* Token::BLOCK_END_TYPE */ 3); if (\count($targets) > 1) { - $keyTarget = $targets->getNode(0); + $keyTarget = $targets->getNode('0'); $keyTarget = new AssignNameExpression($keyTarget->getAttribute('name'), $keyTarget->getTemplateLine()); - $valueTarget = $targets->getNode(1); + $valueTarget = $targets->getNode('1'); } else { $keyTarget = new AssignNameExpression('_key', $lineno); - $valueTarget = $targets->getNode(0); + $valueTarget = $targets->getNode('0'); } $valueTarget = new AssignNameExpression($valueTarget->getAttribute('name'), $valueTarget->getTemplateLine()); diff --git a/src/TokenStream.php b/src/TokenStream.php index 1eac11a02d6..cb578e4fcb9 100644 --- a/src/TokenStream.php +++ b/src/TokenStream.php @@ -60,9 +60,7 @@ public function next(): Token */ public function nextIf($primary, $secondary = null) { - if ($this->tokens[$this->current]->test($primary, $secondary)) { - return $this->next(); - } + return $this->tokens[$this->current]->test($primary, $secondary) ? $this->next() : null; } /** diff --git a/tests/Cache/FilesystemTest.php b/tests/Cache/FilesystemTest.php index 349869c26e3..63b98a900b0 100644 --- a/tests/Cache/FilesystemTest.php +++ b/tests/Cache/FilesystemTest.php @@ -23,7 +23,7 @@ class FilesystemTest extends TestCase protected function setUp(): void { - $nonce = hash(\PHP_VERSION_ID < 80100 ? 'sha256' : 'xxh128', uniqid(mt_rand(), true)); + $nonce = hash(\PHP_VERSION_ID < 80100 ? 'sha256' : 'xxh128', uniqid((string) mt_rand(), true)); $this->classname = '__Twig_Tests_Cache_FilesystemTest_Template_'.$nonce; $this->directory = sys_get_temp_dir().'/twig-test'; $this->cache = new FilesystemCache($this->directory); diff --git a/tests/CompilerTest.php b/tests/CompilerTest.php index 35ffad90980..a71ee093af5 100644 --- a/tests/CompilerTest.php +++ b/tests/CompilerTest.php @@ -22,7 +22,7 @@ public function testReprNumericValueWithLocale() { $compiler = new Compiler(new Environment($this->createMock(LoaderInterface::class))); - $locale = setlocale(\LC_NUMERIC, 0); + $locale = setlocale(\LC_NUMERIC, '0'); if (false === $locale) { $this->markTestSkipped('Your platform does not support locales.'); } @@ -33,7 +33,7 @@ public function testReprNumericValueWithLocale() } $this->assertEquals('1.2', $compiler->repr(1.2)->getSource()); - $this->assertStringContainsString('fr', strtolower(setlocale(\LC_NUMERIC, 0))); + $this->assertStringContainsString('fr', strtolower(setlocale(\LC_NUMERIC, '0'))); setlocale(\LC_NUMERIC, $locale); } diff --git a/tests/ExpressionParserTest.php b/tests/ExpressionParserTest.php index afd1884d8ee..bb000225ae5 100644 --- a/tests/ExpressionParserTest.php +++ b/tests/ExpressionParserTest.php @@ -64,7 +64,7 @@ public function testArrayExpression($template, $expected) $parser = new Parser($env); $expected->setSourceContext($source); - $this->assertEquals($expected, $parser->parse($stream)->getNode('body')->getNode(0)->getNode('expr')); + $this->assertEquals($expected, $parser->parse($stream)->getNode('body')->getNode('0')->getNode('expr')); } /** @@ -217,7 +217,7 @@ public function testStringExpression($template, $expected) $parser = new Parser($env); $expected->setSourceContext($source); - $this->assertEquals($expected, $parser->parse($stream)->getNode('body')->getNode(0)->getNode('expr')); + $this->assertEquals($expected, $parser->parse($stream)->getNode('body')->getNode('0')->getNode('expr')); } public function getTestsForString() diff --git a/tests/Node/Expression/ArrayTest.php b/tests/Node/Expression/ArrayTest.php index cfd9c67f3cb..f72eeab757b 100644 --- a/tests/Node/Expression/ArrayTest.php +++ b/tests/Node/Expression/ArrayTest.php @@ -22,7 +22,7 @@ public function testConstructor() $elements = [new ConstantExpression('foo', 1), $foo = new ConstantExpression('bar', 1)]; $node = new ArrayExpression($elements, 1); - $this->assertEquals($foo, $node->getNode(1)); + $this->assertEquals($foo, $node->getNode('1')); } public function getTests() diff --git a/tests/Node/ForTest.php b/tests/Node/ForTest.php index dbfac3269b2..42c34f8031c 100644 --- a/tests/Node/ForTest.php +++ b/tests/Node/ForTest.php @@ -33,7 +33,7 @@ public function testConstructor() $this->assertEquals($keyTarget, $node->getNode('key_target')); $this->assertEquals($valueTarget, $node->getNode('value_target')); $this->assertEquals($seq, $node->getNode('seq')); - $this->assertEquals($body, $node->getNode('body')->getNode(0)); + $this->assertEquals($body, $node->getNode('body')->getNode('0')); $this->assertFalse($node->hasNode('else')); $else = new PrintNode(new NameExpression('foo', 1), 1); diff --git a/tests/NodeVisitor/OptimizerTest.php b/tests/NodeVisitor/OptimizerTest.php index 8d02f6c7e2a..b685f32248a 100644 --- a/tests/NodeVisitor/OptimizerTest.php +++ b/tests/NodeVisitor/OptimizerTest.php @@ -28,7 +28,7 @@ public function testRenderBlockOptimizer() $stream = $env->parse($env->tokenize(new Source('{{ block("foo") }}', 'index'))); - $node = $stream->getNode('body')->getNode(0); + $node = $stream->getNode('body')->getNode('0'); $this->assertInstanceOf(BlockReferenceExpression::class, $node); $this->assertTrue($node->getAttribute('output')); @@ -40,7 +40,7 @@ public function testRenderParentBlockOptimizer() $stream = $env->parse($env->tokenize(new Source('{% extends "foo" %}{% block content %}{{ parent() }}{% endblock %}', 'index'))); - $node = $stream->getNode('blocks')->getNode('content')->getNode(0)->getNode('body'); + $node = $stream->getNode('blocks')->getNode('content')->getNode('0')->getNode('body'); $this->assertInstanceOf(ParentExpression::class, $node); $this->assertTrue($node->getAttribute('output')); From d9d7e136c13d4bcd67eb8a6edb4dc32bb4814fee Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Wed, 7 Feb 2024 08:31:24 +0100 Subject: [PATCH 171/812] fix TwigExtraBundle tests with Symfony 6.0/6.1 --- extra/twig-extra-bundle/Tests/Fixture/Kernel.php | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/extra/twig-extra-bundle/Tests/Fixture/Kernel.php b/extra/twig-extra-bundle/Tests/Fixture/Kernel.php index 857ed95d41a..faad85c187a 100644 --- a/extra/twig-extra-bundle/Tests/Fixture/Kernel.php +++ b/extra/twig-extra-bundle/Tests/Fixture/Kernel.php @@ -5,6 +5,7 @@ use League\CommonMark\Extension\Strikethrough\StrikethroughExtension; use Symfony\Bundle\FrameworkBundle\FrameworkBundle; use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait; +use Symfony\Bundle\FrameworkBundle\Test\NotificationAssertionsTrait; use Symfony\Bundle\TwigBundle\TwigBundle; use Symfony\Component\Config\Loader\LoaderInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; @@ -29,11 +30,16 @@ protected function configureContainer(ContainerBuilder $c, LoaderInterface $load 'test' => true, 'router' => ['utf8' => true], 'http_method_override' => false, + 'php_errors' => [ + 'log' => true, + ], ]; - if (6 === Kernel::MAJOR_VERSION) { + + // the "handle_all_throwables" option was introduced in FrameworkBundle 6.2 (and so was the NotificationAssertionsTrait) + if (trait_exists(NotificationAssertionsTrait::class)) { $config['handle_all_throwables'] = true; - $config['php_errors']['log'] = true; } + $c->loadFromExtension('framework', $config); $c->loadFromExtension('twig', [ 'default_path' => __DIR__.'/views', From 675f62d60c546a6a2590a837c8d6580093ff0a5a Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Wed, 7 Feb 2024 12:11:29 +0100 Subject: [PATCH 172/812] Update PHPUnit schema --- phpunit.xml.dist | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 9af92f4639d..24d5bd9ea45 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,5 +1,6 @@ - + + ./tests/ From 2836af3d3f14740b2fed7ea4de8e9a5513a1d728 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Andr=C3=A9?= Date: Sat, 10 Feb 2024 02:32:40 +0100 Subject: [PATCH 173/812] Remove redundant "$thing instanceof \Countable" check The check L1183 is redundant as "$thing instanceof \Countable" is already checked L1175. ```php if ($thing instanceof \Countable || \is_array($thing) || $thing instanceof \SimpleXMLElement) { return \count($thing); } if ($thing instanceof \Traversable) { return iterator_count($thing); } if (method_exists($thing, '__toString') && !$thing instanceof \Countable) { return mb_strlen((string) $thing, $env->getCharset()); } ``` --- src/Extension/CoreExtension.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index 598caca4536..f90a9ce44cd 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -1180,7 +1180,7 @@ public static function lengthFilter(Environment $env, $thing) return iterator_count($thing); } - if (method_exists($thing, '__toString') && !$thing instanceof \Countable) { + if (method_exists($thing, '__toString')) { return mb_strlen((string) $thing, $env->getCharset()); } From f7f11ea410c38fd38b7d9bad3b45be903159c45c Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sat, 10 Feb 2024 09:49:29 +0100 Subject: [PATCH 174/812] Fix CS --- extra/cache-extra/Tests/FunctionalTest.php | 2 +- extra/html-extra/HtmlExtension.php | 4 +- .../html-extra/Tests/LegacyFunctionsTest.php | 2 +- extra/intl-extra/IntlExtension.php | 38 +++++++++---------- extra/markdown-extra/ErusevMarkdown.php | 2 +- extra/markdown-extra/LeagueMarkdown.php | 2 +- extra/markdown-extra/MichelfMarkdown.php | 2 +- extra/string-extra/StringExtension.php | 2 +- src/Environment.php | 6 +-- src/Error/Error.php | 4 +- src/Extension/CoreExtension.php | 1 - src/Extension/SandboxExtension.php | 12 +++--- src/Extension/StringLoaderExtension.php | 2 +- src/Loader/FilesystemLoader.php | 2 +- src/Node/BlockNode.php | 2 +- src/Node/BlockReferenceNode.php | 2 +- src/Node/CaptureNode.php | 4 +- src/Node/DeprecatedNode.php | 2 +- src/Node/DoNode.php | 2 +- src/Node/EmbedNode.php | 2 +- src/Node/Expression/ArrayExpression.php | 2 +- .../Expression/BlockReferenceExpression.php | 2 +- src/Node/Expression/Filter/DefaultFilter.php | 2 +- src/Node/Expression/FilterExpression.php | 2 +- src/Node/Expression/ParentExpression.php | 2 +- src/Node/ForLoopNode.php | 2 +- src/Node/ForNode.php | 2 +- src/Node/IfNode.php | 2 +- src/Node/ImportNode.php | 2 +- src/Node/IncludeNode.php | 2 +- src/Node/MacroNode.php | 2 +- src/Node/Node.php | 2 +- src/Node/PrintNode.php | 2 +- src/Node/SandboxNode.php | 2 +- src/Node/SetNode.php | 2 +- src/Node/WithNode.php | 2 +- src/NodeVisitor/OptimizerNodeVisitor.php | 2 +- src/NodeVisitor/SafeAnalysisNodeVisitor.php | 2 +- src/Parser.php | 2 +- src/Resources/string_loader.php | 3 +- src/Template.php | 2 +- src/Test/NodeTestCase.php | 4 +- src/TokenStream.php | 4 +- src/YieldingTemplate.php | 4 +- tests/Node/BlockTest.php | 6 +-- tests/Node/SetTest.php | 1 - 46 files changed, 78 insertions(+), 79 deletions(-) diff --git a/extra/cache-extra/Tests/FunctionalTest.php b/extra/cache-extra/Tests/FunctionalTest.php index 111026a89d9..0ae24436ec5 100644 --- a/extra/cache-extra/Tests/FunctionalTest.php +++ b/extra/cache-extra/Tests/FunctionalTest.php @@ -65,7 +65,7 @@ public function testTagsTooManyArgs() $twig->render('index'); } - private function createEnvironment(array $templates, ArrayAdapter $cache = null): Environment + private function createEnvironment(array $templates, ?ArrayAdapter $cache = null): Environment { $twig = new Environment(new ArrayLoader($templates)); $cache = $cache ?? new ArrayAdapter(); diff --git a/extra/html-extra/HtmlExtension.php b/extra/html-extra/HtmlExtension.php index d5842bf500c..e4cf9d3bd55 100644 --- a/extra/html-extra/HtmlExtension.php +++ b/extra/html-extra/HtmlExtension.php @@ -21,7 +21,7 @@ final class HtmlExtension extends AbstractExtension { private $mimeTypes; - public function __construct(MimeTypes $mimeTypes = null) + public function __construct(?MimeTypes $mimeTypes = null) { $this->mimeTypes = $mimeTypes; } @@ -50,7 +50,7 @@ public function getFunctions(): array * * @internal */ - public function dataUri(string $data, string $mime = null, array $parameters = []): string + public function dataUri(string $data, ?string $mime = null, array $parameters = []): string { $repr = 'data:'; diff --git a/extra/html-extra/Tests/LegacyFunctionsTest.php b/extra/html-extra/Tests/LegacyFunctionsTest.php index 4e290fb8cc5..accf8afb39c 100644 --- a/extra/html-extra/Tests/LegacyFunctionsTest.php +++ b/extra/html-extra/Tests/LegacyFunctionsTest.php @@ -21,6 +21,6 @@ class LegacyFunctionsTest extends TestCase { public function testHtmlToMarkdown() { - $this->assertSame(HtmlExtension::htmlClasses(['charset' => 'utf-8']), \twig_html_classes(['charset' => 'utf-8'])); + $this->assertSame(HtmlExtension::htmlClasses(['charset' => 'utf-8']), twig_html_classes(['charset' => 'utf-8'])); } } diff --git a/extra/intl-extra/IntlExtension.php b/extra/intl-extra/IntlExtension.php index 13d4a4e4778..0b33331f6a8 100644 --- a/extra/intl-extra/IntlExtension.php +++ b/extra/intl-extra/IntlExtension.php @@ -150,7 +150,7 @@ private static function availableDateFormats(): array private $dateFormatterPrototype; private $numberFormatterPrototype; - public function __construct(\IntlDateFormatter $dateFormatterPrototype = null, \NumberFormatter $numberFormatterPrototype = null) + public function __construct(?\IntlDateFormatter $dateFormatterPrototype = null, ?\NumberFormatter $numberFormatterPrototype = null) { $this->dateFormatterPrototype = $dateFormatterPrototype; $this->numberFormatterPrototype = $numberFormatterPrototype; @@ -191,7 +191,7 @@ public function getFunctions() ]; } - public function getCountryName(?string $country, string $locale = null): string + public function getCountryName(?string $country, ?string $locale = null): string { if (null === $country) { return ''; @@ -204,7 +204,7 @@ public function getCountryName(?string $country, string $locale = null): string } } - public function getCurrencyName(?string $currency, string $locale = null): string + public function getCurrencyName(?string $currency, ?string $locale = null): string { if (null === $currency) { return ''; @@ -217,7 +217,7 @@ public function getCurrencyName(?string $currency, string $locale = null): strin } } - public function getCurrencySymbol(?string $currency, string $locale = null): string + public function getCurrencySymbol(?string $currency, ?string $locale = null): string { if (null === $currency) { return ''; @@ -230,7 +230,7 @@ public function getCurrencySymbol(?string $currency, string $locale = null): str } } - public function getLanguageName(?string $language, string $locale = null): string + public function getLanguageName(?string $language, ?string $locale = null): string { if (null === $language) { return ''; @@ -243,7 +243,7 @@ public function getLanguageName(?string $language, string $locale = null): strin } } - public function getLocaleName(?string $data, string $locale = null): string + public function getLocaleName(?string $data, ?string $locale = null): string { if (null === $data) { return ''; @@ -256,7 +256,7 @@ public function getLocaleName(?string $data, string $locale = null): string } } - public function getTimezoneName(?string $timezone, string $locale = null): string + public function getTimezoneName(?string $timezone, ?string $locale = null): string { if (null === $timezone) { return ''; @@ -278,7 +278,7 @@ public function getCountryTimezones(string $country): array } } - public function getLanguageNames(string $locale = null): array + public function getLanguageNames(?string $locale = null): array { try { return Languages::getNames($locale); @@ -287,7 +287,7 @@ public function getLanguageNames(string $locale = null): array } } - public function getScriptNames(string $locale = null): array + public function getScriptNames(?string $locale = null): array { try { return Scripts::getNames($locale); @@ -296,7 +296,7 @@ public function getScriptNames(string $locale = null): array } } - public function getCountryNames(string $locale = null): array + public function getCountryNames(?string $locale = null): array { try { return Countries::getNames($locale); @@ -305,7 +305,7 @@ public function getCountryNames(string $locale = null): array } } - public function getLocaleNames(string $locale = null): array + public function getLocaleNames(?string $locale = null): array { try { return Locales::getNames($locale); @@ -314,7 +314,7 @@ public function getLocaleNames(string $locale = null): array } } - public function getCurrencyNames(string $locale = null): array + public function getCurrencyNames(?string $locale = null): array { try { return Currencies::getNames($locale); @@ -323,7 +323,7 @@ public function getCurrencyNames(string $locale = null): array } } - public function getTimezoneNames(string $locale = null): array + public function getTimezoneNames(?string $locale = null): array { try { return Timezones::getNames($locale); @@ -332,7 +332,7 @@ public function getTimezoneNames(string $locale = null): array } } - public function formatCurrency($amount, string $currency, array $attrs = [], string $locale = null): string + public function formatCurrency($amount, string $currency, array $attrs = [], ?string $locale = null): string { $formatter = $this->createNumberFormatter($locale, 'currency', $attrs); @@ -343,7 +343,7 @@ public function formatCurrency($amount, string $currency, array $attrs = [], str return $ret; } - public function formatNumber($number, array $attrs = [], string $style = 'decimal', string $type = 'default', string $locale = null): string + public function formatNumber($number, array $attrs = [], string $style = 'decimal', string $type = 'default', ?string $locale = null): string { if (!isset(self::NUMBER_TYPES[$type])) { throw new RuntimeError(sprintf('The type "%s" does not exist, known types are: "%s".', $type, implode('", "', array_keys(self::NUMBER_TYPES)))); @@ -358,7 +358,7 @@ public function formatNumber($number, array $attrs = [], string $style = 'decima return $ret; } - public function formatNumberStyle(string $style, $number, array $attrs = [], string $type = 'default', string $locale = null): string + public function formatNumberStyle(string $style, $number, array $attrs = [], string $type = 'default', ?string $locale = null): string { return $this->formatNumber($number, $attrs, $style, $type, $locale); } @@ -367,7 +367,7 @@ public function formatNumberStyle(string $style, $number, array $attrs = [], str * @param \DateTimeInterface|string|null $date A date or null to use the current time * @param \DateTimeZone|string|false|null $timezone The target timezone, null to use the default, false to leave unchanged */ - public function formatDateTime(Environment $env, $date, ?string $dateFormat = 'medium', ?string $timeFormat = 'medium', string $pattern = '', $timezone = null, string $calendar = 'gregorian', string $locale = null): string + public function formatDateTime(Environment $env, $date, ?string $dateFormat = 'medium', ?string $timeFormat = 'medium', string $pattern = '', $timezone = null, string $calendar = 'gregorian', ?string $locale = null): string { $date = CoreExtension::dateConverter($env, $date, $timezone); @@ -390,7 +390,7 @@ public function formatDateTime(Environment $env, $date, ?string $dateFormat = 'm * @param \DateTimeInterface|string|null $date A date or null to use the current time * @param \DateTimeZone|string|false|null $timezone The target timezone, null to use the default, false to leave unchanged */ - public function formatDate(Environment $env, $date, ?string $dateFormat = 'medium', string $pattern = '', $timezone = null, string $calendar = 'gregorian', string $locale = null): string + public function formatDate(Environment $env, $date, ?string $dateFormat = 'medium', string $pattern = '', $timezone = null, string $calendar = 'gregorian', ?string $locale = null): string { return $this->formatDateTime($env, $date, $dateFormat, 'none', $pattern, $timezone, $calendar, $locale); } @@ -399,7 +399,7 @@ public function formatDate(Environment $env, $date, ?string $dateFormat = 'mediu * @param \DateTimeInterface|string|null $date A date or null to use the current time * @param \DateTimeZone|string|false|null $timezone The target timezone, null to use the default, false to leave unchanged */ - public function formatTime(Environment $env, $date, ?string $timeFormat = 'medium', string $pattern = '', $timezone = null, string $calendar = 'gregorian', string $locale = null): string + public function formatTime(Environment $env, $date, ?string $timeFormat = 'medium', string $pattern = '', $timezone = null, string $calendar = 'gregorian', ?string $locale = null): string { return $this->formatDateTime($env, $date, 'none', $timeFormat, $pattern, $timezone, $calendar, $locale); } diff --git a/extra/markdown-extra/ErusevMarkdown.php b/extra/markdown-extra/ErusevMarkdown.php index f4f7e1c48fb..47b030893e5 100644 --- a/extra/markdown-extra/ErusevMarkdown.php +++ b/extra/markdown-extra/ErusevMarkdown.php @@ -17,7 +17,7 @@ class ErusevMarkdown implements MarkdownInterface { private $converter; - public function __construct(Parsedown $converter = null) + public function __construct(?Parsedown $converter = null) { $this->converter = $converter ?: new Parsedown(); } diff --git a/extra/markdown-extra/LeagueMarkdown.php b/extra/markdown-extra/LeagueMarkdown.php index 2390901c01e..be266770240 100644 --- a/extra/markdown-extra/LeagueMarkdown.php +++ b/extra/markdown-extra/LeagueMarkdown.php @@ -18,7 +18,7 @@ class LeagueMarkdown implements MarkdownInterface private $converter; private $legacySupport; - public function __construct(CommonMarkConverter $converter = null) + public function __construct(?CommonMarkConverter $converter = null) { $this->converter = $converter ?: new CommonMarkConverter(); $this->legacySupport = !method_exists($this->converter, 'convert'); diff --git a/extra/markdown-extra/MichelfMarkdown.php b/extra/markdown-extra/MichelfMarkdown.php index 2660a7f0440..0acc3a3a41d 100644 --- a/extra/markdown-extra/MichelfMarkdown.php +++ b/extra/markdown-extra/MichelfMarkdown.php @@ -17,7 +17,7 @@ class MichelfMarkdown implements MarkdownInterface { private $converter; - public function __construct(MarkdownExtra $converter = null) + public function __construct(?MarkdownExtra $converter = null) { if (null === $converter) { $converter = new MarkdownExtra(); diff --git a/extra/string-extra/StringExtension.php b/extra/string-extra/StringExtension.php index 7b5d0049204..2e827af0567 100644 --- a/extra/string-extra/StringExtension.php +++ b/extra/string-extra/StringExtension.php @@ -22,7 +22,7 @@ final class StringExtension extends AbstractExtension { private $slugger; - public function __construct(SluggerInterface $slugger = null) + public function __construct(?SluggerInterface $slugger = null) { $this->slugger = $slugger ?: new AsciiSlugger(); } diff --git a/src/Environment.php b/src/Environment.php index 5ec708bc76a..f05de27743d 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -278,7 +278,7 @@ public function setCache($cache) * * @internal */ - public function getTemplateClass(string $name, int $index = null): string + public function getTemplateClass(string $name, ?int $index = null): string { $key = $this->getLoader()->getCacheKey($name).$this->optionsHash; @@ -346,7 +346,7 @@ public function load($name): TemplateWrapper * * @internal */ - public function loadTemplate(string $cls, string $name, int $index = null): Template + public function loadTemplate(string $cls, string $name, ?int $index = null): Template { $mainCls = $cls; if (null !== $index) { @@ -401,7 +401,7 @@ public function loadTemplate(string $cls, string $name, int $index = null): Temp * @throws LoaderError When the template cannot be found * @throws SyntaxError When an error occurred during compilation */ - public function createTemplate(string $template, string $name = null): TemplateWrapper + public function createTemplate(string $template, ?string $name = null): TemplateWrapper { $hash = hash(\PHP_VERSION_ID < 80100 ? 'sha256' : 'xxh128', $template, false); if (null !== $name) { diff --git a/src/Error/Error.php b/src/Error/Error.php index bca1fa64c5b..0df213598cf 100644 --- a/src/Error/Error.php +++ b/src/Error/Error.php @@ -53,7 +53,7 @@ class Error extends \Exception * @param int $lineno The template line where the error occurred * @param Source|null $source The source context where the error occurred */ - public function __construct(string $message, int $lineno = -1, Source $source = null, \Throwable $previous = null) + public function __construct(string $message, int $lineno = -1, ?Source $source = null, ?\Throwable $previous = null) { parent::__construct('', 0, $previous); @@ -93,7 +93,7 @@ public function getSourceContext(): ?Source return $this->name ? new Source($this->sourceCode, $this->name, $this->sourcePath) : null; } - public function setSourceContext(Source $source = null): void + public function setSourceContext(?Source $source = null): void { if (null === $source) { $this->sourceCode = $this->name = $this->sourcePath = null; diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index 598caca4536..0f28e233d65 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -15,7 +15,6 @@ use Twig\Error\LoaderError; use Twig\Error\RuntimeError; use Twig\ExpressionParser; -use Twig\Extension\SandboxExtension; use Twig\Markup; use Twig\Node\Expression\Binary\AddBinary; use Twig\Node\Expression\Binary\AndBinary; diff --git a/src/Extension/SandboxExtension.php b/src/Extension/SandboxExtension.php index c942682d18a..921df287a44 100644 --- a/src/Extension/SandboxExtension.php +++ b/src/Extension/SandboxExtension.php @@ -26,7 +26,7 @@ final class SandboxExtension extends AbstractExtension private $policy; private $sourcePolicy; - public function __construct(SecurityPolicyInterface $policy, $sandboxed = false, SourcePolicyInterface $sourcePolicy = null) + public function __construct(SecurityPolicyInterface $policy, $sandboxed = false, ?SourcePolicyInterface $sourcePolicy = null) { $this->policy = $policy; $this->sandboxedGlobally = $sandboxed; @@ -53,7 +53,7 @@ public function disableSandbox(): void $this->sandboxed = false; } - public function isSandboxed(Source $source = null): bool + public function isSandboxed(?Source $source = null): bool { return $this->sandboxedGlobally || $this->sandboxed || $this->isSourceSandboxed($source); } @@ -82,14 +82,14 @@ public function getSecurityPolicy(): SecurityPolicyInterface return $this->policy; } - public function checkSecurity($tags, $filters, $functions, Source $source = null): void + public function checkSecurity($tags, $filters, $functions, ?Source $source = null): void { if ($this->isSandboxed($source)) { $this->policy->checkSecurity($tags, $filters, $functions); } } - public function checkMethodAllowed($obj, $method, int $lineno = -1, Source $source = null): void + public function checkMethodAllowed($obj, $method, int $lineno = -1, ?Source $source = null): void { if ($this->isSandboxed($source)) { try { @@ -103,7 +103,7 @@ public function checkMethodAllowed($obj, $method, int $lineno = -1, Source $sour } } - public function checkPropertyAllowed($obj, $property, int $lineno = -1, Source $source = null): void + public function checkPropertyAllowed($obj, $property, int $lineno = -1, ?Source $source = null): void { if ($this->isSandboxed($source)) { try { @@ -117,7 +117,7 @@ public function checkPropertyAllowed($obj, $property, int $lineno = -1, Source $ } } - public function ensureToStringAllowed($obj, int $lineno = -1, Source $source = null) + public function ensureToStringAllowed($obj, int $lineno = -1, ?Source $source = null) { if ($this->isSandboxed($source) && \is_object($obj) && method_exists($obj, '__toString')) { try { diff --git a/src/Extension/StringLoaderExtension.php b/src/Extension/StringLoaderExtension.php index 9b25d9a554c..0945678a832 100644 --- a/src/Extension/StringLoaderExtension.php +++ b/src/Extension/StringLoaderExtension.php @@ -34,7 +34,7 @@ public function getFunctions(): array * * @internal */ - public static function templateFromString(Environment $env, $template, string $name = null): TemplateWrapper + public static function templateFromString(Environment $env, $template, ?string $name = null): TemplateWrapper { return $env->createTemplate((string) $template, $name); } diff --git a/src/Loader/FilesystemLoader.php b/src/Loader/FilesystemLoader.php index 1b277fe2fc4..8472796f7b0 100644 --- a/src/Loader/FilesystemLoader.php +++ b/src/Loader/FilesystemLoader.php @@ -34,7 +34,7 @@ class FilesystemLoader implements LoaderInterface * @param string|array $paths A path or an array of paths where to look for templates * @param string|null $rootPath The root path common to all relative paths (null for getcwd()) */ - public function __construct($paths = [], string $rootPath = null) + public function __construct($paths = [], ?string $rootPath = null) { $this->rootPath = ($rootPath ?? getcwd()).\DIRECTORY_SEPARATOR; if (null !== $rootPath && false !== ($realPath = realpath($rootPath))) { diff --git a/src/Node/BlockNode.php b/src/Node/BlockNode.php index 241dff0b2ec..e20eae252bc 100644 --- a/src/Node/BlockNode.php +++ b/src/Node/BlockNode.php @@ -21,7 +21,7 @@ */ class BlockNode extends Node { - public function __construct(string $name, Node $body, int $lineno, string $tag = null) + public function __construct(string $name, Node $body, int $lineno, ?string $tag = null) { parent::__construct(['body' => $body], ['name' => $name], $lineno, $tag); } diff --git a/src/Node/BlockReferenceNode.php b/src/Node/BlockReferenceNode.php index 8b98c0f02df..8abb6f954f2 100644 --- a/src/Node/BlockReferenceNode.php +++ b/src/Node/BlockReferenceNode.php @@ -21,7 +21,7 @@ */ class BlockReferenceNode extends Node implements NodeOutputInterface { - public function __construct(string $name, int $lineno, string $tag = null) + public function __construct(string $name, int $lineno, ?string $tag = null) { parent::__construct([], ['name' => $name], $lineno, $tag); } diff --git a/src/Node/CaptureNode.php b/src/Node/CaptureNode.php index cdb77e26921..53ccce7f8e5 100644 --- a/src/Node/CaptureNode.php +++ b/src/Node/CaptureNode.php @@ -20,7 +20,7 @@ */ class CaptureNode extends Node { - public function __construct(Node $body, int $lineno, string $tag = null) + public function __construct(Node $body, int $lineno, ?string $tag = null) { parent::__construct(['body' => $body], ['raw' => false, 'with_blocks' => false], $lineno, $tag); } @@ -47,7 +47,7 @@ public function compile(Compiler $compiler): void if (!$this->getAttribute('raw')) { $compiler->raw(") ? '' : new Markup(\$tmp, \$this->env->getCharset())"); } - $compiler->raw(";"); + $compiler->raw(';'); return; } diff --git a/src/Node/DeprecatedNode.php b/src/Node/DeprecatedNode.php index 5ff44307fc5..ff9fcb4d630 100644 --- a/src/Node/DeprecatedNode.php +++ b/src/Node/DeprecatedNode.php @@ -22,7 +22,7 @@ */ class DeprecatedNode extends Node { - public function __construct(AbstractExpression $expr, int $lineno, string $tag = null) + public function __construct(AbstractExpression $expr, int $lineno, ?string $tag = null) { parent::__construct(['expr' => $expr], [], $lineno, $tag); } diff --git a/src/Node/DoNode.php b/src/Node/DoNode.php index f7783d19f40..bf979dae77b 100644 --- a/src/Node/DoNode.php +++ b/src/Node/DoNode.php @@ -21,7 +21,7 @@ */ class DoNode extends Node { - public function __construct(AbstractExpression $expr, int $lineno, string $tag = null) + public function __construct(AbstractExpression $expr, int $lineno, ?string $tag = null) { parent::__construct(['expr' => $expr], [], $lineno, $tag); } diff --git a/src/Node/EmbedNode.php b/src/Node/EmbedNode.php index 903c3f6c7ac..ce95f3a3990 100644 --- a/src/Node/EmbedNode.php +++ b/src/Node/EmbedNode.php @@ -23,7 +23,7 @@ class EmbedNode extends IncludeNode { // we don't inject the module to avoid node visitors to traverse it twice (as it will be already visited in the main module) - public function __construct(string $name, int $index, ?AbstractExpression $variables, bool $only, bool $ignoreMissing, int $lineno, string $tag = null) + public function __construct(string $name, int $index, ?AbstractExpression $variables, bool $only, bool $ignoreMissing, int $lineno, ?string $tag = null) { parent::__construct(new ConstantExpression('not_used', $lineno), $variables, $only, $ignoreMissing, $lineno, $tag); diff --git a/src/Node/Expression/ArrayExpression.php b/src/Node/Expression/ArrayExpression.php index 075c13590ee..39b02f54e9d 100644 --- a/src/Node/Expression/ArrayExpression.php +++ b/src/Node/Expression/ArrayExpression.php @@ -55,7 +55,7 @@ public function hasElement(AbstractExpression $key): bool return false; } - public function addElement(AbstractExpression $value, AbstractExpression $key = null): void + public function addElement(AbstractExpression $value, ?AbstractExpression $key = null): void { if (null === $key) { $key = new ConstantExpression(++$this->index, $value->getTemplateLine()); diff --git a/src/Node/Expression/BlockReferenceExpression.php b/src/Node/Expression/BlockReferenceExpression.php index e63c5b2149d..9b187b9245e 100644 --- a/src/Node/Expression/BlockReferenceExpression.php +++ b/src/Node/Expression/BlockReferenceExpression.php @@ -22,7 +22,7 @@ */ class BlockReferenceExpression extends AbstractExpression { - public function __construct(Node $name, ?Node $template, int $lineno, string $tag = null) + public function __construct(Node $name, ?Node $template, int $lineno, ?string $tag = null) { $nodes = ['name' => $name]; if (null !== $template) { diff --git a/src/Node/Expression/Filter/DefaultFilter.php b/src/Node/Expression/Filter/DefaultFilter.php index e8eae20ecd3..7eb0ea7704a 100644 --- a/src/Node/Expression/Filter/DefaultFilter.php +++ b/src/Node/Expression/Filter/DefaultFilter.php @@ -29,7 +29,7 @@ */ class DefaultFilter extends FilterExpression { - public function __construct(Node $node, ConstantExpression $filterName, Node $arguments, int $lineno, string $tag = null) + public function __construct(Node $node, ConstantExpression $filterName, Node $arguments, int $lineno, ?string $tag = null) { $default = new FilterExpression($node, new ConstantExpression('default', $node->getTemplateLine()), $arguments, $node->getTemplateLine()); diff --git a/src/Node/Expression/FilterExpression.php b/src/Node/Expression/FilterExpression.php index 0fc1588696b..fec652a431a 100644 --- a/src/Node/Expression/FilterExpression.php +++ b/src/Node/Expression/FilterExpression.php @@ -17,7 +17,7 @@ class FilterExpression extends CallExpression { - public function __construct(Node $node, ConstantExpression $filterName, Node $arguments, int $lineno, string $tag = null) + public function __construct(Node $node, ConstantExpression $filterName, Node $arguments, int $lineno, ?string $tag = null) { parent::__construct(['node' => $node, 'filter' => $filterName, 'arguments' => $arguments], [], $lineno, $tag); } diff --git a/src/Node/Expression/ParentExpression.php b/src/Node/Expression/ParentExpression.php index 9dc27ed1a40..22457cc3b3f 100644 --- a/src/Node/Expression/ParentExpression.php +++ b/src/Node/Expression/ParentExpression.php @@ -21,7 +21,7 @@ */ class ParentExpression extends AbstractExpression { - public function __construct(string $name, int $lineno, string $tag = null) + public function __construct(string $name, int $lineno, ?string $tag = null) { parent::__construct([], ['output' => false, 'name' => $name], $lineno, $tag); } diff --git a/src/Node/ForLoopNode.php b/src/Node/ForLoopNode.php index d5ce845a791..9120b962fe9 100644 --- a/src/Node/ForLoopNode.php +++ b/src/Node/ForLoopNode.php @@ -20,7 +20,7 @@ */ class ForLoopNode extends Node { - public function __construct(int $lineno, string $tag = null) + public function __construct(int $lineno, ?string $tag = null) { parent::__construct([], ['with_loop' => false, 'ifexpr' => false, 'else' => false], $lineno, $tag); } diff --git a/src/Node/ForNode.php b/src/Node/ForNode.php index 78b361d8a4e..f4df0c77de5 100644 --- a/src/Node/ForNode.php +++ b/src/Node/ForNode.php @@ -25,7 +25,7 @@ class ForNode extends Node { private $loop; - public function __construct(AssignNameExpression $keyTarget, AssignNameExpression $valueTarget, AbstractExpression $seq, ?Node $ifexpr, Node $body, ?Node $else, int $lineno, string $tag = null) + public function __construct(AssignNameExpression $keyTarget, AssignNameExpression $valueTarget, AbstractExpression $seq, ?Node $ifexpr, Node $body, ?Node $else, int $lineno, ?string $tag = null) { $body = new Node([$body, $this->loop = new ForLoopNode($lineno, $tag)]); diff --git a/src/Node/IfNode.php b/src/Node/IfNode.php index b41ee828d43..940e5deab9d 100644 --- a/src/Node/IfNode.php +++ b/src/Node/IfNode.php @@ -21,7 +21,7 @@ */ class IfNode extends Node { - public function __construct(Node $tests, ?Node $else, int $lineno, string $tag = null) + public function __construct(Node $tests, ?Node $else, int $lineno, ?string $tag = null) { $nodes = ['tests' => $tests]; if (null !== $else) { diff --git a/src/Node/ImportNode.php b/src/Node/ImportNode.php index 5378d799e28..1a3494c9179 100644 --- a/src/Node/ImportNode.php +++ b/src/Node/ImportNode.php @@ -22,7 +22,7 @@ */ class ImportNode extends Node { - public function __construct(AbstractExpression $expr, AbstractExpression $var, int $lineno, string $tag = null, bool $global = true) + public function __construct(AbstractExpression $expr, AbstractExpression $var, int $lineno, ?string $tag = null, bool $global = true) { parent::__construct(['expr' => $expr, 'var' => $var], ['global' => $global], $lineno, $tag); } diff --git a/src/Node/IncludeNode.php b/src/Node/IncludeNode.php index 35f5fa31b19..09c6622ec62 100644 --- a/src/Node/IncludeNode.php +++ b/src/Node/IncludeNode.php @@ -22,7 +22,7 @@ */ class IncludeNode extends Node implements NodeOutputInterface { - public function __construct(AbstractExpression $expr, ?AbstractExpression $variables, bool $only, bool $ignoreMissing, int $lineno, string $tag = null) + public function __construct(AbstractExpression $expr, ?AbstractExpression $variables, bool $only, bool $ignoreMissing, int $lineno, ?string $tag = null) { $nodes = ['expr' => $expr]; if (null !== $variables) { diff --git a/src/Node/MacroNode.php b/src/Node/MacroNode.php index 63fb8dea56a..ae62a22b5af 100644 --- a/src/Node/MacroNode.php +++ b/src/Node/MacroNode.php @@ -23,7 +23,7 @@ class MacroNode extends Node { public const VARARGS_NAME = 'varargs'; - public function __construct(string $name, Node $body, Node $arguments, int $lineno, string $tag = null) + public function __construct(string $name, Node $body, Node $arguments, int $lineno, ?string $tag = null) { foreach ($arguments as $argumentName => $argument) { if (self::VARARGS_NAME === $argumentName) { diff --git a/src/Node/Node.php b/src/Node/Node.php index 30659ae0fd1..7b90b6092a3 100644 --- a/src/Node/Node.php +++ b/src/Node/Node.php @@ -35,7 +35,7 @@ class Node implements \Countable, \IteratorAggregate * @param int $lineno The line number * @param string $tag The tag name associated with the Node */ - public function __construct(array $nodes = [], array $attributes = [], int $lineno = 0, string $tag = null) + public function __construct(array $nodes = [], array $attributes = [], int $lineno = 0, ?string $tag = null) { foreach ($nodes as $name => $node) { if (!$node instanceof self) { diff --git a/src/Node/PrintNode.php b/src/Node/PrintNode.php index 36bcaee6410..369995f5c86 100644 --- a/src/Node/PrintNode.php +++ b/src/Node/PrintNode.php @@ -22,7 +22,7 @@ */ class PrintNode extends Node implements NodeOutputInterface { - public function __construct(AbstractExpression $expr, int $lineno, string $tag = null) + public function __construct(AbstractExpression $expr, int $lineno, ?string $tag = null) { parent::__construct(['expr' => $expr], [], $lineno, $tag); } diff --git a/src/Node/SandboxNode.php b/src/Node/SandboxNode.php index 4d5666bff13..0ffef6dbb94 100644 --- a/src/Node/SandboxNode.php +++ b/src/Node/SandboxNode.php @@ -20,7 +20,7 @@ */ class SandboxNode extends Node { - public function __construct(Node $body, int $lineno, string $tag = null) + public function __construct(Node $body, int $lineno, ?string $tag = null) { parent::__construct(['body' => $body], [], $lineno, $tag); } diff --git a/src/Node/SetNode.php b/src/Node/SetNode.php index 4057c6a75ba..7dea5002310 100644 --- a/src/Node/SetNode.php +++ b/src/Node/SetNode.php @@ -21,7 +21,7 @@ */ class SetNode extends Node implements NodeCaptureInterface { - public function __construct(bool $capture, Node $names, Node $values, int $lineno, string $tag = null) + public function __construct(bool $capture, Node $names, Node $values, int $lineno, ?string $tag = null) { /* * Optimizes the node when capture is used for a large block of text. diff --git a/src/Node/WithNode.php b/src/Node/WithNode.php index 302b40389ba..3dd2b07ec8a 100644 --- a/src/Node/WithNode.php +++ b/src/Node/WithNode.php @@ -20,7 +20,7 @@ */ class WithNode extends Node { - public function __construct(Node $body, ?Node $variables, bool $only, int $lineno, string $tag = null) + public function __construct(Node $body, ?Node $variables, bool $only, int $lineno, ?string $tag = null) { $nodes = ['body' => $body]; if (null !== $variables) { diff --git a/src/NodeVisitor/OptimizerNodeVisitor.php b/src/NodeVisitor/OptimizerNodeVisitor.php index 9f9b81cbfd5..6af056ac414 100644 --- a/src/NodeVisitor/OptimizerNodeVisitor.php +++ b/src/NodeVisitor/OptimizerNodeVisitor.php @@ -137,7 +137,7 @@ private function optimizePrintNode(Node $node): Node $exprNode = $node->getNode('expr'); - if ($exprNode instanceof ConstantExpression && is_string($exprNode->getAttribute('value'))) { + if ($exprNode instanceof ConstantExpression && \is_string($exprNode->getAttribute('value'))) { return new TextNode($exprNode->getAttribute('value'), $exprNode->getTemplateLine()); } diff --git a/src/NodeVisitor/SafeAnalysisNodeVisitor.php b/src/NodeVisitor/SafeAnalysisNodeVisitor.php index 90d6f2e0fd0..6df046e1c7b 100644 --- a/src/NodeVisitor/SafeAnalysisNodeVisitor.php +++ b/src/NodeVisitor/SafeAnalysisNodeVisitor.php @@ -136,7 +136,7 @@ public function leaveNode(Node $node, Environment $env): ?Node return $node; } - private function intersectSafe(array $a = null, array $b = null): array + private function intersectSafe(?array $a = null, ?array $b = null): array { if (null === $a || null === $b) { return []; diff --git a/src/Parser.php b/src/Parser.php index 0c7629b5572..adcaee31633 100644 --- a/src/Parser.php +++ b/src/Parser.php @@ -250,7 +250,7 @@ public function embedTemplate(ModuleNode $template) $this->embeddedTemplates[] = $template; } - public function addImportedSymbol(string $type, string $alias, string $name = null, AbstractExpression $node = null): void + public function addImportedSymbol(string $type, string $alias, ?string $name = null, ?AbstractExpression $node = null): void { $this->importedSymbols[0][$type][$alias] = ['name' => $name, 'node' => $node]; } diff --git a/src/Resources/string_loader.php b/src/Resources/string_loader.php index b074495ca5b..af5e152b2d7 100644 --- a/src/Resources/string_loader.php +++ b/src/Resources/string_loader.php @@ -15,9 +15,10 @@ /** * @internal + * * @deprecated since Twig 3.9.0 */ -function twig_template_from_string(Environment $env, $template, string $name = null): TemplateWrapper +function twig_template_from_string(Environment $env, $template, ?string $name = null): TemplateWrapper { trigger_deprecation('twig/twig', '3.9.0', 'Using the internal "%s" function is deprecated.', __FUNCTION__); diff --git a/src/Template.php b/src/Template.php index a45bf6e1e06..0c06ac9187d 100644 --- a/src/Template.php +++ b/src/Template.php @@ -148,7 +148,7 @@ public function displayParentBlock($name, array $context, array $blocks = []) * @param array $blocks The current set of blocks * @param bool $useBlocks Whether to use the current set of blocks */ - public function displayBlock($name, array $context, array $blocks = [], $useBlocks = true, self $templateContext = null) + public function displayBlock($name, array $context, array $blocks = [], $useBlocks = true, ?self $templateContext = null) { if ($useBlocks && isset($blocks[$name])) { $template = $blocks[$name][0]; diff --git a/src/Test/NodeTestCase.php b/src/Test/NodeTestCase.php index 8df0e9fee3a..b4ddafe7598 100644 --- a/src/Test/NodeTestCase.php +++ b/src/Test/NodeTestCase.php @@ -34,7 +34,7 @@ public function testCompile($node, $source, $environment = null, $isPattern = fa $this->assertNodeCompilation($source, $node, $environment, $isPattern); } - public function assertNodeCompilation($source, Node $node, Environment $environment = null, $isPattern = false) + public function assertNodeCompilation($source, Node $node, ?Environment $environment = null, $isPattern = false) { $compiler = $this->getCompiler($environment); $compiler->compile($node); @@ -46,7 +46,7 @@ public function assertNodeCompilation($source, Node $node, Environment $environm } } - protected function getCompiler(Environment $environment = null) + protected function getCompiler(?Environment $environment = null) { return new Compiler($environment ?? $this->getEnvironment()); } diff --git a/src/TokenStream.php b/src/TokenStream.php index cb578e4fcb9..89e7e0f3f80 100644 --- a/src/TokenStream.php +++ b/src/TokenStream.php @@ -25,7 +25,7 @@ final class TokenStream private $current = 0; private $source; - public function __construct(array $tokens, Source $source = null) + public function __construct(array $tokens, ?Source $source = null) { $this->tokens = $tokens; $this->source = $source ?: new Source('', ''); @@ -66,7 +66,7 @@ public function nextIf($primary, $secondary = null) /** * Tests a token and returns it or throws a syntax error. */ - public function expect($type, $value = null, string $message = null): Token + public function expect($type, $value = null, ?string $message = null): Token { $token = $this->tokens[$this->current]; if (!$token->test($type, $value)) { diff --git a/src/YieldingTemplate.php b/src/YieldingTemplate.php index 93ee894d2b8..c9d7a578b44 100644 --- a/src/YieldingTemplate.php +++ b/src/YieldingTemplate.php @@ -71,7 +71,7 @@ public function display(array $context, array $blocks = []): void /** * @return iterable */ - public function yieldBlock($name, array $context, array $blocks = [], $useBlocks = true, Template $templateContext = null) + public function yieldBlock($name, array $context, array $blocks = [], $useBlocks = true, ?Template $templateContext = null) { if ($useBlocks && isset($blocks[$name])) { $template = $blocks[$name][0]; @@ -165,7 +165,7 @@ public function renderParentBlock($name, array $context, array $blocks = []) return $content; } - public function displayBlock($name, array $context, array $blocks = [], $useBlocks = true, Template $templateContext = null) + public function displayBlock($name, array $context, array $blocks = [], $useBlocks = true, ?Template $templateContext = null) { throw new RuntimeError(sprintf('Calling "%s" for block "%s" is not supported as "use_yield" is set to "true".', __METHOD__, $name), -1, $this->getSourceContext()); } diff --git a/tests/Node/BlockTest.php b/tests/Node/BlockTest.php index cdc2c861cdb..a021145667d 100644 --- a/tests/Node/BlockTest.php +++ b/tests/Node/BlockTest.php @@ -42,7 +42,7 @@ public function block_foo(\$context, array \$blocks = []) echo "foo"; } EOF - , new Environment(new ArrayLoader()) + , new Environment(new ArrayLoader()), ]; } else { $tests[] = [new BlockNode('foo', new TextNode('foo', 1), 1), << Date: Tue, 6 Feb 2024 11:31:19 +0100 Subject: [PATCH 175/812] Add `#[YieldReady]` to allow extensions to tell when they're ready for yielding --- .github/workflows/ci.yml | 26 +- CHANGELOG | 6 +- src/Attribute/YieldReady.php | 20 ++ src/Compiler.php | 61 ++-- src/Environment.php | 10 +- src/Extension/CoreExtension.php | 31 +++ src/Extension/YieldNotReadyExtension.php | 32 +++ src/Node/AutoEscapeNode.php | 2 + src/Node/BlockNode.php | 11 +- src/Node/BlockReferenceNode.php | 17 +- src/Node/BodyNode.php | 3 + src/Node/CaptureNode.php | 59 +--- src/Node/CheckSecurityCallNode.php | 2 + src/Node/CheckSecurityNode.php | 2 + src/Node/CheckToStringNode.php | 2 + src/Node/DeprecatedNode.php | 2 + src/Node/DoNode.php | 2 + src/Node/EmbedNode.php | 2 + .../Expression/BlockReferenceExpression.php | 20 +- src/Node/Expression/InlinePrint.php | 18 +- src/Node/Expression/ParentExpression.php | 41 +-- src/Node/FlushNode.php | 2 + src/Node/ForLoopNode.php | 2 + src/Node/ForNode.php | 2 + src/Node/IfNode.php | 2 + src/Node/ImportNode.php | 2 + src/Node/IncludeNode.php | 28 +- src/Node/MacroNode.php | 2 + src/Node/ModuleNode.php | 23 +- src/Node/Node.php | 4 +- src/Node/PrintNode.php | 13 +- src/Node/SandboxNode.php | 2 + src/Node/SetNode.php | 2 + src/Node/TextNode.php | 13 +- src/Node/WithNode.php | 2 + src/NodeVisitor/YieldNotReadyNodeVisitor.php | 60 ++++ src/Resources/core.php | 176 ++++++------ src/Resources/debug.php | 4 +- src/Resources/escaper.php | 8 +- src/Resources/string_loader.php | 4 +- src/Template.php | 260 +++++++++++------- src/TemplateWrapper.php | 8 +- src/Test/NodeTestCase.php | 15 - src/YieldingTemplate.php | 182 ------------ tests/EnvironmentTest.php | 44 +++ tests/Node/AutoEscapeTest.php | 3 +- tests/Node/BlockReferenceTest.php | 4 +- tests/Node/BlockTest.php | 32 +-- tests/Node/ForTest.php | 11 +- tests/Node/IfTest.php | 11 +- tests/Node/IncludeTest.php | 18 +- tests/Node/MacroTest.php | 39 +-- tests/Node/ModuleTest.php | 23 +- tests/Node/PrintTest.php | 4 +- tests/Node/SandboxTest.php | 3 +- tests/Node/SetTest.php | 19 +- tests/Node/TextTest.php | 3 +- tests/TemplateWrapperTest.php | 14 +- tests/ignore-use-yield-deprecations | 1 - 59 files changed, 654 insertions(+), 760 deletions(-) create mode 100644 src/Attribute/YieldReady.php create mode 100644 src/Extension/YieldNotReadyExtension.php create mode 100644 src/NodeVisitor/YieldNotReadyNodeVisitor.php delete mode 100644 src/YieldingTemplate.php delete mode 100644 tests/ignore-use-yield-deprecations diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6274bd1fe29..b22266b7ab1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,12 +14,10 @@ permissions: jobs: tests: - name: "PHP ${{ matrix.php-version }} (yield: ${{ matrix.use_yield }})" + name: "PHP ${{ matrix.php-version }}" runs-on: 'ubuntu-latest' - continue-on-error: ${{ matrix.experimental }} - strategy: matrix: php-version: @@ -30,8 +28,6 @@ jobs: - '8.1' - '8.2' - '8.3' - experimental: [false] - use_yield: [true, false] steps: - name: "Checkout code" @@ -49,8 +45,8 @@ jobs: - run: composer install - - name: "Switch use_yield to true" - if: ${{ matrix.use_yield }} + - name: "Switch use_yield to true on PHP ${{ matrix.php-version }}" + if: "matrix.php-version == '8.2'" run: | sed -i -e "s/'use_yield' => false/'use_yield' => true/" src/Environment.php @@ -61,13 +57,13 @@ jobs: run: vendor/bin/simple-phpunit --version - name: "Run tests" - run: SYMFONY_DEPRECATIONS_HELPER=ignoreFile=./tests/ignore-use-yield-deprecations vendor/bin/simple-phpunit + run: vendor/bin/simple-phpunit extension-tests: needs: - 'tests' - name: "${{ matrix.extension }} PHP ${{ matrix.php-version }} (yield: ${{ matrix.use_yield }})" + name: "${{ matrix.extension }} PHP ${{ matrix.php-version }}" runs-on: 'ubuntu-latest' @@ -92,8 +88,6 @@ jobs: - 'markdown-extra' - 'string-extra' - 'twig-extra-bundle' - experimental: [false] - use_yield: [true, false] steps: - name: "Checkout code" @@ -118,18 +112,18 @@ jobs: - name: "PHPUnit version" run: vendor/bin/simple-phpunit --version - - name: "Composer install ${{ matrix.extension}}" - working-directory: extra/${{ matrix.extension}} + - name: "Composer install ${{ matrix.extension }}" + working-directory: extra/${{ matrix.extension }} run: composer install - name: "Switch use_yield to true" - if: ${{ matrix.use_yield }} + if: "matrix.php == '8.2'" run: | sed -i -e "s/'use_yield' => false/'use_yield' => true/" extra/${{ matrix.extension }}/vendor/twig/twig/src/Environment.php - - name: "Run tests for ${{ matrix.extension}}" + - name: "Run tests for ${{ matrix.extension }}" working-directory: extra/${{ matrix.extension }} - run: SYMFONY_DEPRECATIONS_HELPER=ignoreFile=../../tests/ignore-use-yield-deprecations ../../vendor/bin/simple-phpunit + run: ../../vendor/bin/simple-phpunit integration-tests: needs: diff --git a/CHANGELOG b/CHANGELOG index d0bc0cae33c..a6e7a9365ee 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,7 +1,9 @@ # 3.9.0 (2024-XX-XX) - * Add a new "yield" mode for output generation - The "use_yield" Environment option controls the output strategy: use "false" for "echo", "true" for "yield" + * Add a new "yield" mode for output generation; + Node implementations that use "echo" or "print" should use "yield" instead; + all Node implementations should be flagged with `#[YieldReady]` once they've been made ready for "yield"; + the "use_yield" Environment option can be turned on when all nodes have been made `#[YieldReady]`; "yield" will be the only strategy supported in the next major version * Add return type for Symfony 7 compatibility * Fix premature loop exit in Security Policy lookup of allowed methods/properties diff --git a/src/Attribute/YieldReady.php b/src/Attribute/YieldReady.php new file mode 100644 index 00000000000..335c4351e58 --- /dev/null +++ b/src/Attribute/YieldReady.php @@ -0,0 +1,20 @@ +env = $env; - $this->checkForOutput = $env->isDebug(); } public function getEnvironment(): Environment @@ -68,9 +68,20 @@ public function reset(int $indentation = 0) public function compile(Node $node, int $indentation = 0) { $this->reset($indentation); - $node->compile($this); + $this->didUseEchoStack[] = $this->didUseEcho; - return $this; + try { + $this->didUseEcho = false; + $node->compile($this); + + if ($this->didUseEcho) { + trigger_deprecation('twig/twig', '3.9', 'Using "%s" is deprecated, use "yield" instead in "%s", then flag the class with #[YieldReady].', $this->didUseEcho, \get_class($node)); + } + + return $this; + } finally { + $this->didUseEcho = array_pop($this->didUseEchoStack); + } } /** @@ -78,23 +89,24 @@ public function compile(Node $node, int $indentation = 0) */ public function subcompile(Node $node, bool $raw = true) { - if (false === $raw) { + if (!$raw) { $this->source .= str_repeat(' ', $this->indentation * 4); } - $node->compile($this); + $this->didUseEchoStack[] = $this->didUseEcho; - return $this; - } + try { + $this->didUseEcho = false; + $node->compile($this); - /** - * @return $this - */ - public function checkForOutput(bool $checkForOutput) - { - $this->checkForOutput = $checkForOutput ? $this->env->isDebug() : false; + if ($this->didUseEcho) { + trigger_deprecation('twig/twig', '3.9', 'Using "%s" is deprecated, use "yield" instead in "%s", then flag the class with #[YieldReady].', $this->didUseEcho, \get_class($node)); + } - return $this; + return $this; + } finally { + $this->didUseEcho = array_pop($this->didUseEchoStack); + } } /** @@ -104,9 +116,7 @@ public function checkForOutput(bool $checkForOutput) */ public function raw(string $string) { - if ($this->checkForOutput) { - $this->checkStringForOutput(trim($string)); - } + $this->checkForEcho($string); $this->source .= $string; return $this; @@ -120,10 +130,7 @@ public function raw(string $string) public function write(...$strings) { foreach ($strings as $string) { - if ($this->checkForOutput) { - $this->checkStringForOutput(trim($string)); - } - + $this->checkForEcho($string); $this->source .= str_repeat(' ', $this->indentation * 4).$string; } @@ -240,12 +247,12 @@ public function getVarName(): string return sprintf('__internal_compile_%d', $this->varNameSalt++); } - private function checkStringForOutput(string $string): void + private function checkForEcho(string $string): void { - if (str_starts_with($string, 'echo')) { - trigger_deprecation('twig/twig', '3.9.0', 'Using "echo" in a "Node::compile()" method is deprecated; use a "TextNode" or "PrintNode" instead or use "yield" when "use_yield" is "true" on the environment (triggered by "%s").', $string); - } elseif (str_starts_with($string, 'print')) { - trigger_deprecation('twig/twig', '3.9.0', 'Using "print" in a "Node::compile()" method is deprecated; use a "TextNode" or "PrintNode" instead or use "yield" when "use_yield" is "true" on the environment (triggered by "%s").', $string); + if ($this->didUseEcho) { + return; } + + $this->didUseEcho = preg_match('/^\s*+(echo|print)\b/', $string, $m) ? $m[1] : false; } } diff --git a/src/Environment.php b/src/Environment.php index f05de27743d..c6ba8b2c0fe 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -22,6 +22,7 @@ use Twig\Extension\EscaperExtension; use Twig\Extension\ExtensionInterface; use Twig\Extension\OptimizerExtension; +use Twig\Extension\YieldNotReadyExtension; use Twig\Loader\ArrayLoader; use Twig\Loader\ChainLoader; use Twig\Loader\LoaderInterface; @@ -100,7 +101,7 @@ class Environment * (default to -1 which means that all optimizations are enabled; * set it to 0 to disable). * - * * use_yield: Enable a new mode where template are using "yield" instead of "echo" + * * use_yield: Enable templates to exclusively use "yield" instead of "echo" * (default to "false", but switch it to "true" when possible * as this will be the only supported mode in Twig 4.0) */ @@ -120,10 +121,6 @@ public function __construct(LoaderInterface $loader, $options = []) ], $options); $this->useYield = (bool) $options['use_yield']; - if (!$this->useYield) { - trigger_deprecation('twig/twig', '3.9.0', 'Not setting "use_yield" to "true" is deprecated.'); - } - $this->debug = (bool) $options['debug']; $this->setCharset($options['charset'] ?? 'UTF-8'); $this->autoReload = null === $options['auto_reload'] ? $this->debug : (bool) $options['auto_reload']; @@ -133,6 +130,9 @@ public function __construct(LoaderInterface $loader, $options = []) $this->addExtension(new CoreExtension()); $this->addExtension(new EscaperExtension($options['autoescape'])); + if (\PHP_VERSION_ID >= 80000) { + $this->addExtension(new YieldNotReadyExtension($this->useYield)); + } $this->addExtension(new OptimizerExtension($options['optimizations'])); } diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index 3a3caa9fe0c..b11e2e5e237 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -1841,4 +1841,35 @@ public static function checkArrowInSandbox(Environment $env, $arrow, $thing, $ty throw new RuntimeError(sprintf('The callable passed to the "%s" %s must be a Closure in sandbox mode.', $thing, $type)); } } + + /** + * @internal to be removed in Twig 4 + */ + public static function captureOutput(iterable $body): string + { + $output = ''; + $level = ob_get_level(); + ob_start(); + + try { + foreach ($body as $data) { + if (ob_get_length()) { + $output .= ob_get_clean(); + ob_start(); + } + + $output .= $data; + } + + if (ob_get_length()) { + $output .= ob_get_clean(); + } + } finally { + while (ob_get_level() > $level) { + ob_end_clean(); + } + } + + return $output; + } } diff --git a/src/Extension/YieldNotReadyExtension.php b/src/Extension/YieldNotReadyExtension.php new file mode 100644 index 00000000000..2503c8d8140 --- /dev/null +++ b/src/Extension/YieldNotReadyExtension.php @@ -0,0 +1,32 @@ +useYield = $useYield; + } + + public function getNodeVisitors(): array + { + return [new YieldNotReadyNodeVisitor($this->useYield)]; + } +} diff --git a/src/Node/AutoEscapeNode.php b/src/Node/AutoEscapeNode.php index cd970411b8d..f9bc17e078f 100644 --- a/src/Node/AutoEscapeNode.php +++ b/src/Node/AutoEscapeNode.php @@ -11,6 +11,7 @@ namespace Twig\Node; +use Twig\Attribute\YieldReady; use Twig\Compiler; /** @@ -24,6 +25,7 @@ * * @author Fabien Potencier */ +#[YieldReady] class AutoEscapeNode extends Node { public function __construct($value, Node $body, int $lineno, string $tag = 'autoescape') diff --git a/src/Node/BlockNode.php b/src/Node/BlockNode.php index e20eae252bc..65174c02c6f 100644 --- a/src/Node/BlockNode.php +++ b/src/Node/BlockNode.php @@ -12,6 +12,7 @@ namespace Twig\Node; +use Twig\Attribute\YieldReady; use Twig\Compiler; /** @@ -19,6 +20,7 @@ * * @author Fabien Potencier */ +#[YieldReady] class BlockNode extends Node { public function __construct(string $name, Node $body, int $lineno, ?string $tag = null) @@ -37,14 +39,7 @@ public function compile(Compiler $compiler): void $compiler ->subcompile($this->getNode('body')) - ; - - if (!$this->getNode('body') instanceof NodeOutputInterface && $compiler->getEnvironment()->useYield()) { - // needed when body doesn't yield anything - $compiler->write("yield '';\n"); - } - - $compiler + ->write("return; yield '';\n") // needed when body doesn't yield anything ->outdent() ->write("}\n\n") ; diff --git a/src/Node/BlockReferenceNode.php b/src/Node/BlockReferenceNode.php index 8abb6f954f2..f48082be36d 100644 --- a/src/Node/BlockReferenceNode.php +++ b/src/Node/BlockReferenceNode.php @@ -12,6 +12,7 @@ namespace Twig\Node; +use Twig\Attribute\YieldReady; use Twig\Compiler; /** @@ -19,6 +20,7 @@ * * @author Fabien Potencier */ +#[YieldReady] class BlockReferenceNode extends Node implements NodeOutputInterface { public function __construct(string $name, int $lineno, ?string $tag = null) @@ -28,16 +30,9 @@ public function __construct(string $name, int $lineno, ?string $tag = null) public function compile(Compiler $compiler): void { - if ($compiler->getEnvironment()->useYield()) { - $compiler - ->addDebugInfo($this) - ->write(sprintf("yield from \$this->unwrap()->yieldBlock('%s', \$context, \$blocks);\n", $this->getAttribute('name'))) - ; - } else { - $compiler - ->addDebugInfo($this) - ->write(sprintf("\$this->displayBlock('%s', \$context, \$blocks);\n", $this->getAttribute('name'))) - ; - } + $compiler + ->addDebugInfo($this) + ->write(sprintf("yield from \$this->unwrap()->yieldBlock('%s', \$context, \$blocks);\n", $this->getAttribute('name'))) + ; } } diff --git a/src/Node/BodyNode.php b/src/Node/BodyNode.php index 041cbf685b1..08115b3bd00 100644 --- a/src/Node/BodyNode.php +++ b/src/Node/BodyNode.php @@ -11,11 +11,14 @@ namespace Twig\Node; +use Twig\Attribute\YieldReady; + /** * Represents a body node. * * @author Fabien Potencier */ +#[YieldReady] class BodyNode extends Node { } diff --git a/src/Node/CaptureNode.php b/src/Node/CaptureNode.php index 53ccce7f8e5..27ae4c60537 100644 --- a/src/Node/CaptureNode.php +++ b/src/Node/CaptureNode.php @@ -11,6 +11,7 @@ namespace Twig\Node; +use Twig\Attribute\YieldReady; use Twig\Compiler; /** @@ -18,6 +19,7 @@ * * @author Fabien Potencier */ +#[YieldReady] class CaptureNode extends Node { public function __construct(Node $body, int $lineno, ?string $tag = null) @@ -27,62 +29,29 @@ public function __construct(Node $body, int $lineno, ?string $tag = null) public function compile(Compiler $compiler): void { - if ($compiler->getEnvironment()->useYield()) { - if ($this->getAttribute('raw')) { - $compiler->raw("implode('', iterator_to_array("); - } else { - $compiler->raw("('' === \$tmp = implode('', iterator_to_array("); - } - if ($this->getAttribute('with_blocks')) { - $compiler->raw("(function () use (&\$context, \$macros, \$blocks) {\n"); - } else { - $compiler->raw("(function () use (&\$context, \$macros) {\n"); - } - $compiler - ->indent() - ->subcompile($this->getNode('body')) - ->outdent() - ->write("})() ?? new \EmptyIterator()))") - ; - if (!$this->getAttribute('raw')) { - $compiler->raw(") ? '' : new Markup(\$tmp, \$this->env->getCharset())"); - } - $compiler->raw(';'); + $useYield = $compiler->getEnvironment()->useYield(); - return; + if (!$this->getAttribute('raw')) { + $compiler->raw("('' === \$tmp = "); } - + $compiler->raw($useYield ? "implode('', iterator_to_array(" : "\\Twig\\Extension\\CoreExtension::captureOutput("); if ($this->getAttribute('with_blocks')) { $compiler->raw("(function () use (&\$context, \$macros, \$blocks) {\n"); } else { $compiler->raw("(function () use (&\$context, \$macros) {\n"); } - $compiler->indent(); - if ($compiler->getEnvironment()->isDebug()) { - $compiler->write("ob_start();\n"); - } else { - $compiler->write("ob_start(function () { return ''; });\n"); - } $compiler - ->write("try {\n") ->indent() ->subcompile($this->getNode('body')) - ->raw("\n") - ; - if ($this->getAttribute('raw')) { - $compiler->write("return ob_get_contents();\n"); - } else { - $compiler->write("return ('' === \$tmp = ob_get_contents()) ? '' : new Markup(\$tmp, \$this->env->getCharset());\n"); - } - $compiler - ->outdent() - ->write("} finally {\n") - ->indent() - ->write("ob_end_clean();\n") ->outdent() - ->write("}\n") - ->outdent() - ->write('})();') + ->write("})() ?? new \EmptyIterator())") ; + if ($useYield) { + $compiler->raw(')'); + } + if (!$this->getAttribute('raw')) { + $compiler->raw(") ? '' : new Markup(\$tmp, \$this->env->getCharset())"); + } + $compiler->raw(';'); } } diff --git a/src/Node/CheckSecurityCallNode.php b/src/Node/CheckSecurityCallNode.php index a78a38d80bb..d5f45761895 100644 --- a/src/Node/CheckSecurityCallNode.php +++ b/src/Node/CheckSecurityCallNode.php @@ -11,11 +11,13 @@ namespace Twig\Node; +use Twig\Attribute\YieldReady; use Twig\Compiler; /** * @author Fabien Potencier */ +#[YieldReady] class CheckSecurityCallNode extends Node { public function compile(Compiler $compiler) diff --git a/src/Node/CheckSecurityNode.php b/src/Node/CheckSecurityNode.php index c531c4dd415..991a428a42d 100644 --- a/src/Node/CheckSecurityNode.php +++ b/src/Node/CheckSecurityNode.php @@ -11,11 +11,13 @@ namespace Twig\Node; +use Twig\Attribute\YieldReady; use Twig\Compiler; /** * @author Fabien Potencier */ +#[YieldReady] class CheckSecurityNode extends Node { private $usedFilters; diff --git a/src/Node/CheckToStringNode.php b/src/Node/CheckToStringNode.php index c7a9d6984e7..81fb92404f0 100644 --- a/src/Node/CheckToStringNode.php +++ b/src/Node/CheckToStringNode.php @@ -11,6 +11,7 @@ namespace Twig\Node; +use Twig\Attribute\YieldReady; use Twig\Compiler; use Twig\Node\Expression\AbstractExpression; @@ -24,6 +25,7 @@ * * @author Fabien Potencier */ +#[YieldReady] class CheckToStringNode extends AbstractExpression { public function __construct(AbstractExpression $expr) diff --git a/src/Node/DeprecatedNode.php b/src/Node/DeprecatedNode.php index ff9fcb4d630..2dc425dd301 100644 --- a/src/Node/DeprecatedNode.php +++ b/src/Node/DeprecatedNode.php @@ -11,6 +11,7 @@ namespace Twig\Node; +use Twig\Attribute\YieldReady; use Twig\Compiler; use Twig\Node\Expression\AbstractExpression; use Twig\Node\Expression\ConstantExpression; @@ -20,6 +21,7 @@ * * @author Yonel Ceruto */ +#[YieldReady] class DeprecatedNode extends Node { public function __construct(AbstractExpression $expr, int $lineno, ?string $tag = null) diff --git a/src/Node/DoNode.php b/src/Node/DoNode.php index bf979dae77b..445016ab285 100644 --- a/src/Node/DoNode.php +++ b/src/Node/DoNode.php @@ -11,6 +11,7 @@ namespace Twig\Node; +use Twig\Attribute\YieldReady; use Twig\Compiler; use Twig\Node\Expression\AbstractExpression; @@ -19,6 +20,7 @@ * * @author Fabien Potencier */ +#[YieldReady] class DoNode extends Node { public function __construct(AbstractExpression $expr, int $lineno, ?string $tag = null) diff --git a/src/Node/EmbedNode.php b/src/Node/EmbedNode.php index ce95f3a3990..54550946215 100644 --- a/src/Node/EmbedNode.php +++ b/src/Node/EmbedNode.php @@ -11,6 +11,7 @@ namespace Twig\Node; +use Twig\Attribute\YieldReady; use Twig\Compiler; use Twig\Node\Expression\AbstractExpression; use Twig\Node\Expression\ConstantExpression; @@ -20,6 +21,7 @@ * * @author Fabien Potencier */ +#[YieldReady] class EmbedNode extends IncludeNode { // we don't inject the module to avoid node visitors to traverse it twice (as it will be already visited in the main module) diff --git a/src/Node/Expression/BlockReferenceExpression.php b/src/Node/Expression/BlockReferenceExpression.php index 9b187b9245e..13e72df17ce 100644 --- a/src/Node/Expression/BlockReferenceExpression.php +++ b/src/Node/Expression/BlockReferenceExpression.php @@ -40,16 +40,10 @@ public function compile(Compiler $compiler): void if ($this->getAttribute('output')) { $compiler->addDebugInfo($this); - if ($compiler->getEnvironment()->useYield()) { - $compiler->write('yield from '); - $this - ->compileTemplateCall($compiler, 'yieldBlock') - ->raw(";\n"); - } else { - $this - ->compileTemplateCall($compiler, 'displayBlock') - ->raw(";\n"); - } + $compiler->write('yield from '); + $this + ->compileTemplateCall($compiler, 'yieldBlock') + ->raw(";\n"); } else { $this->compileTemplateCall($compiler, 'renderBlock'); } @@ -72,11 +66,7 @@ private function compileTemplateCall(Compiler $compiler, string $method): Compil ; } - if ($compiler->getEnvironment()->useYield()) { - $compiler->raw('->unwrap()'); - } - - $compiler->raw(sprintf('->%s', $method)); + $compiler->raw(sprintf('->unwrap()->%s', $method)); return $this->compileBlockArguments($compiler); } diff --git a/src/Node/Expression/InlinePrint.php b/src/Node/Expression/InlinePrint.php index 725536a869d..0a3c2e4f9ec 100644 --- a/src/Node/Expression/InlinePrint.php +++ b/src/Node/Expression/InlinePrint.php @@ -26,19 +26,9 @@ public function __construct(Node $node, int $lineno) public function compile(Compiler $compiler): void { - if ($compiler->getEnvironment()->useYield()) { - $compiler - ->raw('yield ') - ->subcompile($this->getNode('node')) - ; - } else { - $compiler - ->checkForOutput(false) - ->raw('print(') - ->checkForOutput(true) - ->subcompile($this->getNode('node')) - ->raw(')') - ; - } + $compiler + ->raw('yield ') + ->subcompile($this->getNode('node')) + ; } } diff --git a/src/Node/Expression/ParentExpression.php b/src/Node/Expression/ParentExpression.php index 22457cc3b3f..59d833ac99d 100644 --- a/src/Node/Expression/ParentExpression.php +++ b/src/Node/Expression/ParentExpression.php @@ -28,36 +28,19 @@ public function __construct(string $name, int $lineno, ?string $tag = null) public function compile(Compiler $compiler): void { - if ($compiler->getEnvironment()->useYield()) { - if ($this->getAttribute('output')) { - $compiler - ->addDebugInfo($this) - ->write('yield from $this->yieldParentBlock(') - ->string($this->getAttribute('name')) - ->raw(", \$context, \$blocks);\n") - ; - } else { - $compiler - ->raw('$this->renderParentBlock(') - ->string($this->getAttribute('name')) - ->raw(', $context, $blocks)') - ; - } + if ($this->getAttribute('output')) { + $compiler + ->addDebugInfo($this) + ->write('yield from $this->yieldParentBlock(') + ->string($this->getAttribute('name')) + ->raw(", \$context, \$blocks);\n") + ; } else { - if ($this->getAttribute('output')) { - $compiler - ->addDebugInfo($this) - ->write('$this->displayParentBlock(') - ->string($this->getAttribute('name')) - ->raw(", \$context, \$blocks);\n") - ; - } else { - $compiler - ->raw('$this->renderParentBlock(') - ->string($this->getAttribute('name')) - ->raw(', $context, $blocks)') - ; - } + $compiler + ->raw('$this->renderParentBlock(') + ->string($this->getAttribute('name')) + ->raw(', $context, $blocks)') + ; } } } diff --git a/src/Node/FlushNode.php b/src/Node/FlushNode.php index fa50a88ee56..8a3dde6fcc2 100644 --- a/src/Node/FlushNode.php +++ b/src/Node/FlushNode.php @@ -11,6 +11,7 @@ namespace Twig\Node; +use Twig\Attribute\YieldReady; use Twig\Compiler; /** @@ -18,6 +19,7 @@ * * @author Fabien Potencier */ +#[YieldReady] class FlushNode extends Node { public function __construct(int $lineno, string $tag) diff --git a/src/Node/ForLoopNode.php b/src/Node/ForLoopNode.php index 9120b962fe9..503687c2b2e 100644 --- a/src/Node/ForLoopNode.php +++ b/src/Node/ForLoopNode.php @@ -11,6 +11,7 @@ namespace Twig\Node; +use Twig\Attribute\YieldReady; use Twig\Compiler; /** @@ -18,6 +19,7 @@ * * @author Fabien Potencier */ +#[YieldReady] class ForLoopNode extends Node { public function __construct(int $lineno, ?string $tag = null) diff --git a/src/Node/ForNode.php b/src/Node/ForNode.php index f4df0c77de5..5222cf9bf9f 100644 --- a/src/Node/ForNode.php +++ b/src/Node/ForNode.php @@ -12,6 +12,7 @@ namespace Twig\Node; +use Twig\Attribute\YieldReady; use Twig\Compiler; use Twig\Node\Expression\AbstractExpression; use Twig\Node\Expression\AssignNameExpression; @@ -21,6 +22,7 @@ * * @author Fabien Potencier */ +#[YieldReady] class ForNode extends Node { private $loop; diff --git a/src/Node/IfNode.php b/src/Node/IfNode.php index 940e5deab9d..1b883305ad4 100644 --- a/src/Node/IfNode.php +++ b/src/Node/IfNode.php @@ -12,6 +12,7 @@ namespace Twig\Node; +use Twig\Attribute\YieldReady; use Twig\Compiler; /** @@ -19,6 +20,7 @@ * * @author Fabien Potencier */ +#[YieldReady] class IfNode extends Node { public function __construct(Node $tests, ?Node $else, int $lineno, ?string $tag = null) diff --git a/src/Node/ImportNode.php b/src/Node/ImportNode.php index 1a3494c9179..db47bfe61c9 100644 --- a/src/Node/ImportNode.php +++ b/src/Node/ImportNode.php @@ -11,6 +11,7 @@ namespace Twig\Node; +use Twig\Attribute\YieldReady; use Twig\Compiler; use Twig\Node\Expression\AbstractExpression; use Twig\Node\Expression\NameExpression; @@ -20,6 +21,7 @@ * * @author Fabien Potencier */ +#[YieldReady] class ImportNode extends Node { public function __construct(AbstractExpression $expr, AbstractExpression $var, int $lineno, ?string $tag = null, bool $global = true) diff --git a/src/Node/IncludeNode.php b/src/Node/IncludeNode.php index 09c6622ec62..abc0f35460e 100644 --- a/src/Node/IncludeNode.php +++ b/src/Node/IncludeNode.php @@ -12,6 +12,7 @@ namespace Twig\Node; +use Twig\Attribute\YieldReady; use Twig\Compiler; use Twig\Node\Expression\AbstractExpression; @@ -20,6 +21,7 @@ * * @author Fabien Potencier */ +#[YieldReady] class IncludeNode extends Node implements NodeOutputInterface { public function __construct(AbstractExpression $expr, ?AbstractExpression $variables, bool $only, bool $ignoreMissing, int $lineno, ?string $tag = null) @@ -58,18 +60,9 @@ public function compile(Compiler $compiler): void ->write("}\n") ->write(sprintf("if ($%s) {\n", $template)) ->indent() + ->write(sprintf('yield from $%s->unwrap()->yield(', $template)) ; - if ($compiler->getEnvironment()->useYield()) { - $compiler - ->write(sprintf('yield from $%s->unwrap()->yield(', $template)) - ; - } else { - $compiler - ->write(sprintf('$%s->display(', $template)) - ; - } - $this->addTemplateArguments($compiler); $compiler ->raw(");\n") @@ -77,20 +70,9 @@ public function compile(Compiler $compiler): void ->write("}\n") ; } else { - if ($compiler->getEnvironment()->useYield()) { - $compiler - ->write('yield from ') - ; - } - + $compiler->write('yield from '); $this->addGetTemplate($compiler); - - if ($compiler->getEnvironment()->useYield()) { - $compiler->raw('->unwrap()->yield('); - } else { - $compiler->raw('->display('); - } - + $compiler->raw('->unwrap()->yield('); $this->addTemplateArguments($compiler); $compiler->raw(");\n"); } diff --git a/src/Node/MacroNode.php b/src/Node/MacroNode.php index ae62a22b5af..e44150f526c 100644 --- a/src/Node/MacroNode.php +++ b/src/Node/MacroNode.php @@ -11,6 +11,7 @@ namespace Twig\Node; +use Twig\Attribute\YieldReady; use Twig\Compiler; use Twig\Error\SyntaxError; @@ -19,6 +20,7 @@ * * @author Fabien Potencier */ +#[YieldReady] class MacroNode extends Node { public const VARARGS_NAME = 'varargs'; diff --git a/src/Node/ModuleNode.php b/src/Node/ModuleNode.php index 04a5326a15c..6a76ed1ea5d 100644 --- a/src/Node/ModuleNode.php +++ b/src/Node/ModuleNode.php @@ -12,6 +12,7 @@ namespace Twig\Node; +use Twig\Attribute\YieldReady; use Twig\Compiler; use Twig\Node\Expression\AbstractExpression; use Twig\Node\Expression\ConstantExpression; @@ -26,6 +27,7 @@ * * @author Fabien Potencier */ +#[YieldReady] final class ModuleNode extends Node { public function __construct(Node $body, ?AbstractExpression $parent, Node $blocks, Node $macros, Node $traits, $embeddedTemplates, Source $source) @@ -151,14 +153,14 @@ protected function compileClassHeader(Compiler $compiler) ->write("use Twig\Sandbox\SecurityNotAllowedFilterError;\n") ->write("use Twig\Sandbox\SecurityNotAllowedFunctionError;\n") ->write("use Twig\Source;\n") - ->write(sprintf("use Twig\%s;\n\n", $compiler->getEnvironment()->useYield() ? 'YieldingTemplate' : 'Template')) + ->write(sprintf("use Twig\%s;\n\n", 'Template')) ; } $compiler // if the template name contains */, add a blank to avoid a PHP parse error ->write('/* '.str_replace('*/', '* /', $this->getSourceContext()->getName())." */\n") ->write('class '.$compiler->getEnvironment()->getTemplateClass($this->getSourceContext()->getName(), $this->getAttribute('index'))) - ->raw(sprintf(" extends %s\n", $compiler->getEnvironment()->useYield() ? 'YieldingTemplate' : 'Template')) + ->raw(sprintf(" extends %s\n", 'Template')) ->write("{\n") ->indent() ->write("private \$source;\n") @@ -326,25 +328,16 @@ protected function compileDisplay(Compiler $compiler) ->raw(");\n") ; } - if ($compiler->getEnvironment()->useYield()) { - $compiler->write('yield from '); - } else { - $compiler->write(''); - } + $compiler->write('yield from '); if ($parent instanceof ConstantExpression) { $compiler->raw('$this->parent'); } else { $compiler->raw('$this->getParent($context)'); } - if ($compiler->getEnvironment()->useYield()) { - $compiler->raw("->unwrap()->yield(\$context, array_merge(\$this->blocks, \$blocks));\n"); - } else { - $compiler->raw("->display(\$context, array_merge(\$this->blocks, \$blocks));\n"); - } - } elseif ($compiler->getEnvironment()->useYield() && !$this->hasNodeOutputNodes($this->getNode('body'))) { - // ensure at least one yield call even for templates with no output - $compiler->write("yield '';\n"); + $compiler->raw("->unwrap()->yield(\$context, array_merge(\$this->blocks, \$blocks));\n"); + } else { + $compiler->write("return; yield '';\n"); // ensure at least one yield call even for templates with no output } $compiler diff --git a/src/Node/Node.php b/src/Node/Node.php index 7b90b6092a3..16591fcf5e5 100644 --- a/src/Node/Node.php +++ b/src/Node/Node.php @@ -12,6 +12,7 @@ namespace Twig\Node; +use Twig\Attribute\YieldReady; use Twig\Compiler; use Twig\Source; @@ -20,6 +21,7 @@ * * @author Fabien Potencier */ +#[YieldReady] class Node implements \Countable, \IteratorAggregate { protected $nodes; @@ -82,7 +84,7 @@ public function __toString() public function compile(Compiler $compiler) { foreach ($this->nodes as $node) { - $node->compile($compiler); + $compiler->subcompile($node); } } diff --git a/src/Node/PrintNode.php b/src/Node/PrintNode.php index 369995f5c86..a6a89bd74c6 100644 --- a/src/Node/PrintNode.php +++ b/src/Node/PrintNode.php @@ -12,6 +12,7 @@ namespace Twig\Node; +use Twig\Attribute\YieldReady; use Twig\Compiler; use Twig\Node\Expression\AbstractExpression; @@ -20,6 +21,7 @@ * * @author Fabien Potencier */ +#[YieldReady] class PrintNode extends Node implements NodeOutputInterface { public function __construct(AbstractExpression $expr, int $lineno, ?string $tag = null) @@ -31,17 +33,8 @@ public function compile(Compiler $compiler): void { $compiler->addDebugInfo($this); - if ($compiler->getEnvironment()->useYield()) { - $compiler->write('yield '); - } else { - $compiler - ->checkForOutput(false) - ->write('echo ') - ->checkForOutput(true) - ; - } - $compiler + ->write('yield ') ->subcompile($this->getNode('expr')) ->raw(";\n") ; diff --git a/src/Node/SandboxNode.php b/src/Node/SandboxNode.php index 0ffef6dbb94..80aecbdba36 100644 --- a/src/Node/SandboxNode.php +++ b/src/Node/SandboxNode.php @@ -11,6 +11,7 @@ namespace Twig\Node; +use Twig\Attribute\YieldReady; use Twig\Compiler; /** @@ -18,6 +19,7 @@ * * @author Fabien Potencier */ +#[YieldReady] class SandboxNode extends Node { public function __construct(Node $body, int $lineno, ?string $tag = null) diff --git a/src/Node/SetNode.php b/src/Node/SetNode.php index 7dea5002310..6b4c873e1ab 100644 --- a/src/Node/SetNode.php +++ b/src/Node/SetNode.php @@ -11,6 +11,7 @@ namespace Twig\Node; +use Twig\Attribute\YieldReady; use Twig\Compiler; use Twig\Node\Expression\ConstantExpression; @@ -19,6 +20,7 @@ * * @author Fabien Potencier */ +#[YieldReady] class SetNode extends Node implements NodeCaptureInterface { public function __construct(bool $capture, Node $names, Node $values, int $lineno, ?string $tag = null) diff --git a/src/Node/TextNode.php b/src/Node/TextNode.php index 3e417dad60d..fae65fb2cb4 100644 --- a/src/Node/TextNode.php +++ b/src/Node/TextNode.php @@ -12,6 +12,7 @@ namespace Twig\Node; +use Twig\Attribute\YieldReady; use Twig\Compiler; /** @@ -19,6 +20,7 @@ * * @author Fabien Potencier */ +#[YieldReady] class TextNode extends Node implements NodeOutputInterface { public function __construct(string $data, int $lineno) @@ -30,17 +32,8 @@ public function compile(Compiler $compiler): void { $compiler->addDebugInfo($this); - if ($compiler->getEnvironment()->useYield()) { - $compiler->write('yield '); - } else { - $compiler - ->checkForOutput(false) - ->write('echo ') - ->checkForOutput(true) - ; - } - $compiler + ->write('yield ') ->string($this->getAttribute('data')) ->raw(";\n") ; diff --git a/src/Node/WithNode.php b/src/Node/WithNode.php index 3dd2b07ec8a..9b8c5788466 100644 --- a/src/Node/WithNode.php +++ b/src/Node/WithNode.php @@ -11,6 +11,7 @@ namespace Twig\Node; +use Twig\Attribute\YieldReady; use Twig\Compiler; /** @@ -18,6 +19,7 @@ * * @author Fabien Potencier */ +#[YieldReady] class WithNode extends Node { public function __construct(Node $body, ?Node $variables, bool $only, int $lineno, ?string $tag = null) diff --git a/src/NodeVisitor/YieldNotReadyNodeVisitor.php b/src/NodeVisitor/YieldNotReadyNodeVisitor.php new file mode 100644 index 00000000000..34c3ac18ba6 --- /dev/null +++ b/src/NodeVisitor/YieldNotReadyNodeVisitor.php @@ -0,0 +1,60 @@ +useYield = $useYield; + } + + public function enterNode(Node $node, Environment $env): Node + { + $class = \get_class($node); + + if ($node instanceof AbstractExpression || isset($this->yieldReadyNodes[$class])) { + return $node; + } + + if (!$this->yieldReadyNodes[$class] = (bool) (new \ReflectionClass($class))->getAttributes(YieldReady::class)) { + if ($this->useYield) { + throw new \LogicException(sprintf('You cannot enable the "use_yield" option of Twig as node "%s" is not marked as ready for it; please make it ready and then flag it with the #[YieldReady] attribute.', $class)); + } + + trigger_deprecation('twig/twig', '3.9', 'Twig node "%s" is not marked as ready for using "yield" instead of "echo"; please make it ready and then flag it with the #[YieldReady] attribute.', $class); + } + + return $node; + } + + public function leaveNode(Node $node, Environment $env): ?Node + { + return $node; + } + + public function getPriority(): int + { + return 255; + } +} diff --git a/src/Resources/core.php b/src/Resources/core.php index f58a1fdf261..eb08cafcd3a 100644 --- a/src/Resources/core.php +++ b/src/Resources/core.php @@ -14,484 +14,484 @@ /** * @internal - * @deprecated since Twig 3.9.0 + * @deprecated since Twig 3.9 */ function twig_cycle($values, $position) { - trigger_deprecation('twig/twig', '3.9.0', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); return CoreExtension::cycle($values, $position); } /** * @internal - * @deprecated since Twig 3.9.0 + * @deprecated since Twig 3.9 */ function twig_random(Environment $env, $values = null, $max = null) { - trigger_deprecation('twig/twig', '3.9.0', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); return CoreExtension::random($env, $values, $max); } /** * @internal - * @deprecated since Twig 3.9.0 + * @deprecated since Twig 3.9 */ function twig_date_format_filter(Environment $env, $date, $format = null, $timezone = null) { - trigger_deprecation('twig/twig', '3.9.0', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); return CoreExtension::dateFormatFilter($env, $date, $format, $timezone); } /** * @internal - * @deprecated since Twig 3.9.0 + * @deprecated since Twig 3.9 */ function twig_date_modify_filter(Environment $env, $date, $modifier) { - trigger_deprecation('twig/twig', '3.9.0', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); return CoreExtension::dateModifyFilter($env, $date, $modifier); } /** * @internal - * @deprecated since Twig 3.9.0 + * @deprecated since Twig 3.9 */ function twig_sprintf($format, ...$values) { - trigger_deprecation('twig/twig', '3.9.0', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); return CoreExtension::sprintf($format, ...$values); } /** * @internal - * @deprecated since Twig 3.9.0 + * @deprecated since Twig 3.9 */ function twig_date_converter(Environment $env, $date = null, $timezone = null) { - trigger_deprecation('twig/twig', '3.9.0', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); return CoreExtension::dateConverter($env, $date, $timezone); } /** * @internal - * @deprecated since Twig 3.9.0 + * @deprecated since Twig 3.9 */ function twig_replace_filter($str, $from) { - trigger_deprecation('twig/twig', '3.9.0', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); return CoreExtension::replaceFilter($str, $from); } /** * @internal - * @deprecated since Twig 3.9.0 + * @deprecated since Twig 3.9 */ function twig_round($value, $precision = 0, $method = 'common') { - trigger_deprecation('twig/twig', '3.9.0', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); return CoreExtension::round($value, $precision, $method); } /** * @internal - * @deprecated since Twig 3.9.0 + * @deprecated since Twig 3.9 */ function twig_number_format_filter(Environment $env, $number, $decimal = null, $decimalPoint = null, $thousandSep = null) { - trigger_deprecation('twig/twig', '3.9.0', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); return CoreExtension::numberFormatFilter($env, $number, $decimal, $decimalPoint, $thousandSep); } /** * @internal - * @deprecated since Twig 3.9.0 + * @deprecated since Twig 3.9 */ function twig_urlencode_filter($url) { - trigger_deprecation('twig/twig', '3.9.0', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); return CoreExtension::urlencodeFilter($url); } /** * @internal - * @deprecated since Twig 3.9.0 + * @deprecated since Twig 3.9 */ function twig_array_merge(...$arrays) { - trigger_deprecation('twig/twig', '3.9.0', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); return CoreExtension::arrayMerge(...$arrays); } /** * @internal - * @deprecated since Twig 3.9.0 + * @deprecated since Twig 3.9 */ function twig_slice(Environment $env, $item, $start, $length = null, $preserveKeys = false) { - trigger_deprecation('twig/twig', '3.9.0', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); return CoreExtension::slice($env, $item, $start, $length, $preserveKeys); } /** * @internal - * @deprecated since Twig 3.9.0 + * @deprecated since Twig 3.9 */ function twig_first(Environment $env, $item) { - trigger_deprecation('twig/twig', '3.9.0', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); return CoreExtension::first($env, $item); } /** * @internal - * @deprecated since Twig 3.9.0 + * @deprecated since Twig 3.9 */ function twig_last(Environment $env, $item) { - trigger_deprecation('twig/twig', '3.9.0', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); return CoreExtension::last($env, $item); } /** * @internal - * @deprecated since Twig 3.9.0 + * @deprecated since Twig 3.9 */ function twig_join_filter($value, $glue = '', $and = null) { - trigger_deprecation('twig/twig', '3.9.0', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); return CoreExtension::joinFilter($value, $glue, $and); } /** * @internal - * @deprecated since Twig 3.9.0 + * @deprecated since Twig 3.9 */ function twig_split_filter(Environment $env, $value, $delimiter, $limit = null) { - trigger_deprecation('twig/twig', '3.9.0', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); return CoreExtension::splitFilter($env, $value, $delimiter, $limit); } /** * @internal - * @deprecated since Twig 3.9.0 + * @deprecated since Twig 3.9 */ function twig_get_array_keys_filter($array) { - trigger_deprecation('twig/twig', '3.9.0', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); return CoreExtension::getArrayKeysFilter($array); } /** * @internal - * @deprecated since Twig 3.9.0 + * @deprecated since Twig 3.9 */ function twig_reverse_filter(Environment $env, $item, $preserveKeys = false) { - trigger_deprecation('twig/twig', '3.9.0', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); return CoreExtension::reverseFilter($env, $item, $preserveKeys); } /** * @internal - * @deprecated since Twig 3.9.0 + * @deprecated since Twig 3.9 */ function twig_sort_filter(Environment $env, $array, $arrow = null) { - trigger_deprecation('twig/twig', '3.9.0', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); return CoreExtension::sortFilter($env, $array, $arrow); } /** * @internal - * @deprecated since Twig 3.9.0 + * @deprecated since Twig 3.9 */ function twig_matches(string $regexp, ?string $str) { - trigger_deprecation('twig/twig', '3.9.0', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); return CoreExtension::matches($regexp, $str); } /** * @internal - * @deprecated since Twig 3.9.0 + * @deprecated since Twig 3.9 */ function twig_trim_filter($string, $characterMask = null, $side = 'both') { - trigger_deprecation('twig/twig', '3.9.0', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); return CoreExtension::trimFilter($string, $characterMask, $side); } /** * @internal - * @deprecated since Twig 3.9.0 + * @deprecated since Twig 3.9 */ function twig_nl2br($string) { - trigger_deprecation('twig/twig', '3.9.0', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); return CoreExtension::nl2br($string); } /** * @internal - * @deprecated since Twig 3.9.0 + * @deprecated since Twig 3.9 */ function twig_spaceless($content) { - trigger_deprecation('twig/twig', '3.9.0', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); return CoreExtension::spaceless($content); } /** * @internal - * @deprecated since Twig 3.9.0 + * @deprecated since Twig 3.9 */ function twig_convert_encoding($string, $to, $from) { - trigger_deprecation('twig/twig', '3.9.0', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); return CoreExtension::convertEncoding($string, $to, $from); } /** * @internal - * @deprecated since Twig 3.9.0 + * @deprecated since Twig 3.9 */ function twig_length_filter(Environment $env, $thing) { - trigger_deprecation('twig/twig', '3.9.0', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); return CoreExtension::lengthFilter($env, $thing); } /** * @internal - * @deprecated since Twig 3.9.0 + * @deprecated since Twig 3.9 */ function twig_upper_filter(Environment $env, $string) { - trigger_deprecation('twig/twig', '3.9.0', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); return CoreExtension::upperFilter($env, $string); } /** * @internal - * @deprecated since Twig 3.9.0 + * @deprecated since Twig 3.9 */ function twig_lower_filter(Environment $env, $string) { - trigger_deprecation('twig/twig', '3.9.0', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); return CoreExtension::lowerFilter($env, $string); } /** * @internal - * @deprecated since Twig 3.9.0 + * @deprecated since Twig 3.9 */ function twig_striptags($string, $allowable_tags = null) { - trigger_deprecation('twig/twig', '3.9.0', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); return CoreExtension::striptags($string, $allowable_tags); } /** * @internal - * @deprecated since Twig 3.9.0 + * @deprecated since Twig 3.9 */ function twig_title_string_filter(Environment $env, $string) { - trigger_deprecation('twig/twig', '3.9.0', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); return CoreExtension::titleStringFilter($env, $string); } /** * @internal - * @deprecated since Twig 3.9.0 + * @deprecated since Twig 3.9 */ function twig_capitalize_string_filter(Environment $env, $string) { - trigger_deprecation('twig/twig', '3.9.0', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); return CoreExtension::capitalizeStringFilter($env, $string); } /** * @internal - * @deprecated since Twig 3.9.0 + * @deprecated since Twig 3.9 */ function twig_test_empty($value) { - trigger_deprecation('twig/twig', '3.9.0', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); return CoreExtension::testEmpty($value); } /** * @internal - * @deprecated since Twig 3.9.0 + * @deprecated since Twig 3.9 */ function twig_test_iterable($value) { - trigger_deprecation('twig/twig', '3.9.0', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); return is_iterable($value); } /** * @internal - * @deprecated since Twig 3.9.0 + * @deprecated since Twig 3.9 */ function twig_include(Environment $env, $context, $template, $variables = [], $withContext = true, $ignoreMissing = false, $sandboxed = false) { - trigger_deprecation('twig/twig', '3.9.0', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); return CoreExtension::include($env, $context, $template, $variables, $withContext, $ignoreMissing, $sandboxed); } /** * @internal - * @deprecated since Twig 3.9.0 + * @deprecated since Twig 3.9 */ function twig_source(Environment $env, $name, $ignoreMissing = false) { - trigger_deprecation('twig/twig', '3.9.0', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); return CoreExtension::source($env, $name, $ignoreMissing); } /** * @internal - * @deprecated since Twig 3.9.0 + * @deprecated since Twig 3.9 */ function twig_constant($constant, $object = null) { - trigger_deprecation('twig/twig', '3.9.0', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); return CoreExtension::constant($constant, $object); } /** * @internal - * @deprecated since Twig 3.9.0 + * @deprecated since Twig 3.9 */ function twig_constant_is_defined($constant, $object = null) { - trigger_deprecation('twig/twig', '3.9.0', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); return CoreExtension::constantIsDefined($constant, $object); } /** * @internal - * @deprecated since Twig 3.9.0 + * @deprecated since Twig 3.9 */ function twig_array_batch($items, $size, $fill = null, $preserveKeys = true) { - trigger_deprecation('twig/twig', '3.9.0', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); return CoreExtension::arrayBatch($items, $size, $fill, $preserveKeys); } /** * @internal - * @deprecated since Twig 3.9.0 + * @deprecated since Twig 3.9 */ function twig_array_column($array, $name, $index = null): array { - trigger_deprecation('twig/twig', '3.9.0', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); return CoreExtension::arrayColumn($array, $name, $index); } /** * @internal - * @deprecated since Twig 3.9.0 + * @deprecated since Twig 3.9 */ function twig_array_filter(Environment $env, $array, $arrow) { - trigger_deprecation('twig/twig', '3.9.0', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); return CoreExtension::arrayFilter($env, $array, $arrow); } /** * @internal - * @deprecated since Twig 3.9.0 + * @deprecated since Twig 3.9 */ function twig_array_map(Environment $env, $array, $arrow) { - trigger_deprecation('twig/twig', '3.9.0', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); return CoreExtension::arrayMap($env, $array, $arrow); } /** * @internal - * @deprecated since Twig 3.9.0 + * @deprecated since Twig 3.9 */ function twig_array_reduce(Environment $env, $array, $arrow, $initial = null) { - trigger_deprecation('twig/twig', '3.9.0', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); return CoreExtension::arrayReduce($env, $array, $arrow, $initial); } /** * @internal - * @deprecated since Twig 3.9.0 + * @deprecated since Twig 3.9 */ function twig_array_some(Environment $env, $array, $arrow) { - trigger_deprecation('twig/twig', '3.9.0', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); return CoreExtension::arraySome($env, $array, $arrow); } /** * @internal - * @deprecated since Twig 3.9.0 + * @deprecated since Twig 3.9 */ function twig_array_every(Environment $env, $array, $arrow) { - trigger_deprecation('twig/twig', '3.9.0', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); return CoreExtension::arrayEvery($env, $array, $arrow); } /** * @internal - * @deprecated since Twig 3.9.0 + * @deprecated since Twig 3.9 */ function twig_check_arrow_in_sandbox(Environment $env, $arrow, $thing, $type) { - trigger_deprecation('twig/twig', '3.9.0', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); return CoreExtension::checkArrowInSandbox($env, $arrow, $thing, $type); } diff --git a/src/Resources/debug.php b/src/Resources/debug.php index 0041b4b79ac..6f59cf6c13d 100644 --- a/src/Resources/debug.php +++ b/src/Resources/debug.php @@ -14,11 +14,11 @@ /** * @internal - * @deprecated since Twig 3.9.0 + * @deprecated since Twig 3.9 */ function twig_var_dump(Environment $env, $context, ...$vars) { - trigger_deprecation('twig/twig', '3.9.0', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); DebugExtension::dump($env, $context, ...$vars); } diff --git a/src/Resources/escaper.php b/src/Resources/escaper.php index cf038645f8e..ea377803504 100644 --- a/src/Resources/escaper.php +++ b/src/Resources/escaper.php @@ -14,22 +14,22 @@ /** * @internal - * @deprecated since Twig 3.9.0 + * @deprecated since Twig 3.9 */ function twig_raw_filter($string) { - trigger_deprecation('twig/twig', '3.9.0', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); return $string; } /** * @internal - * @deprecated since Twig 3.9.0 + * @deprecated since Twig 3.9 */ function twig_escape_filter(Environment $env, $string, $strategy = 'html', $charset = null, $autoescape = false) { - trigger_deprecation('twig/twig', '3.9.0', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); return EscaperExtension::escape($env, $string, $strategy, $charset, $autoescape); } diff --git a/src/Resources/string_loader.php b/src/Resources/string_loader.php index af5e152b2d7..8f0e6492aab 100644 --- a/src/Resources/string_loader.php +++ b/src/Resources/string_loader.php @@ -16,11 +16,11 @@ /** * @internal * - * @deprecated since Twig 3.9.0 + * @deprecated since Twig 3.9 */ function twig_template_from_string(Environment $env, $template, ?string $name = null): TemplateWrapper { - trigger_deprecation('twig/twig', '3.9.0', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); return StringLoaderExtension::templateFromString($env, $template, $name); } diff --git a/src/Template.php b/src/Template.php index 0c06ac9187d..bb8146a2c5b 100644 --- a/src/Template.php +++ b/src/Template.php @@ -41,9 +41,12 @@ abstract class Template protected $extensions = []; protected $sandbox; + private $useYield; + public function __construct(Environment $env) { $this->env = $env; + $this->useYield = $env->useYield(); $this->extensions = $env->getExtensions(); } @@ -74,7 +77,7 @@ abstract public function getSourceContext(); * This method is for internal use only and should never be called * directly. * - * @return Template|TemplateWrapper|false The parent template or false if there is no parent + * @return self|TemplateWrapper|false The parent template or false if there is no parent */ public function getParent(array $context) { @@ -83,9 +86,7 @@ public function getParent(array $context) } try { - $parent = $this->doGetParent($context); - - if (false === $parent) { + if (!$parent = $this->doGetParent($context)) { return false; } @@ -128,12 +129,8 @@ public function isTraitable() */ public function displayParentBlock($name, array $context, array $blocks = []) { - if (isset($this->traits[$name])) { - $this->traits[$name][0]->displayBlock($name, $context, $blocks, false); - } elseif (false !== $parent = $this->getParent($context)) { - $parent->displayBlock($name, $context, $blocks, false); - } else { - throw new RuntimeError(sprintf('The template has no parent and no traits defining the "%s" block.', $name), -1, $this->getSourceContext()); + foreach ($this->yieldParentBlock($name, $context, $blocks) as $data) { + echo $data; } } @@ -150,49 +147,8 @@ public function displayParentBlock($name, array $context, array $blocks = []) */ public function displayBlock($name, array $context, array $blocks = [], $useBlocks = true, ?self $templateContext = null) { - if ($useBlocks && isset($blocks[$name])) { - $template = $blocks[$name][0]; - $block = $blocks[$name][1]; - } elseif (isset($this->blocks[$name])) { - $template = $this->blocks[$name][0]; - $block = $this->blocks[$name][1]; - } else { - $template = null; - $block = null; - } - - // avoid RCEs when sandbox is enabled - if (null !== $template && !$template instanceof self) { - throw new \LogicException('A block must be a method on a \Twig\Template instance.'); - } - - if (null !== $template) { - try { - $template->$block($context, $blocks); - } catch (Error $e) { - if (!$e->getSourceContext()) { - $e->setSourceContext($template->getSourceContext()); - } - - // this is mostly useful for \Twig\Error\LoaderError exceptions - // see \Twig\Error\LoaderError - if (-1 === $e->getTemplateLine()) { - $e->guess(); - } - - throw $e; - } catch (\Throwable $e) { - $e = new RuntimeError(sprintf('An exception has been thrown during the rendering of a template ("%s").', $e->getMessage()), -1, $template->getSourceContext(), $e); - $e->guess(); - - throw $e; - } - } elseif (false !== $parent = $this->getParent($context)) { - $parent->displayBlock($name, $context, array_merge($this->blocks, $blocks), false, $templateContext ?? $this); - } elseif (isset($blocks[$name])) { - throw new RuntimeError(sprintf('Block "%s" should not call parent() in "%s" as the block does not exist in the parent template "%s".', $name, $blocks[$name][0]->getTemplateName(), $this->getTemplateName()), -1, $blocks[$name][0]->getSourceContext()); - } else { - throw new RuntimeError(sprintf('Block "%s" on template "%s" does not exist.', $name, $this->getTemplateName()), -1, ($templateContext ?? $this)->getSourceContext()); + foreach ($this->yieldBlock($name, $context, $blocks, $useBlocks, $templateContext) as $data) { + echo $data; } } @@ -210,14 +166,12 @@ public function displayBlock($name, array $context, array $blocks = [], $useBloc */ public function renderParentBlock($name, array $context, array $blocks = []) { - if ($this->env->isDebug()) { - ob_start(); - } else { - ob_start(function () { return ''; }); + $content = ''; + foreach ($this->yieldParentBlock($name, $context, $blocks) as $data) { + $content .= $data; } - $this->displayParentBlock($name, $context, $blocks); - return ob_get_clean(); + return $content; } /** @@ -235,23 +189,12 @@ public function renderParentBlock($name, array $context, array $blocks = []) */ public function renderBlock($name, array $context, array $blocks = [], $useBlocks = true) { - $level = ob_get_level(); - if ($this->env->isDebug()) { - ob_start(); - } else { - ob_start(function () { return ''; }); + $content = ''; + foreach ($this->yieldBlock($name, $context, $blocks, $useBlocks) as $data) { + $content .= $data; } - try { - $this->displayBlock($name, $context, $blocks, $useBlocks); - } catch (\Throwable $e) { - while (ob_get_level() > $level) { - ob_end_clean(); - } - throw $e; - } - - return ob_get_clean(); + return $content; } /** @@ -276,7 +219,7 @@ public function hasBlock($name, array $context, array $blocks = []) return true; } - if (false !== $parent = $this->getParent($context)) { + if ($parent = $this->getParent($context)) { return $parent->hasBlock($name, $context); } @@ -298,7 +241,7 @@ public function getBlockNames(array $context, array $blocks = []) { $names = array_merge(array_keys($blocks), array_keys($this->blocks)); - if (false !== $parent = $this->getParent($context)) { + if ($parent = $this->getParent($context)) { $names = array_merge($names, $parent->getBlockNames($context)); } @@ -306,7 +249,7 @@ public function getBlockNames(array $context, array $blocks = []) } /** - * @return Template|TemplateWrapper + * @return self|TemplateWrapper */ protected function loadTemplate($template, $templateName = null, $line = null, $index = null) { @@ -351,7 +294,7 @@ protected function loadTemplate($template, $templateName = null, $line = null, $ /** * @internal * - * @return Template + * @return self */ public function unwrap() { @@ -371,36 +314,52 @@ public function getBlocks() return $this->blocks; } - public function display(array $context, array $blocks = []) + public function display(array $context, array $blocks = []): void { - $this->displayWithErrorHandling($this->env->mergeGlobals($context), array_merge($this->blocks, $blocks)); + foreach ($this->yield($context, $blocks) as $data) { + echo $data; + } } - public function render(array $context) + public function render(array $context): string { - $level = ob_get_level(); - if ($this->env->isDebug()) { - ob_start(); - } else { - ob_start(function () { return ''; }); + $content = ''; + foreach ($this->yield($context) as $data) { + $content .= $data; } - try { - $this->display($context); - } catch (\Throwable $e) { - while (ob_get_level() > $level) { - ob_end_clean(); - } - throw $e; - } - - return ob_get_clean(); + return $content; } - protected function displayWithErrorHandling(array $context, array $blocks = []) + /** + * @return iterable + */ + public function yield(array $context, array $blocks = []): iterable { + $context = $this->env->mergeGlobals($context); + $blocks = array_merge($this->blocks, $blocks); + try { - $this->doDisplay($context, $blocks); + if ($this->useYield) { + yield from $this->doDisplay($context, $blocks); + return; + } + + $level = ob_get_level(); + ob_start(); + + foreach ($this->doDisplay($context, $blocks) as $data) { + if (ob_get_length()) { + $data = ob_get_clean().$data; + ob_start(); + } + + yield $data; + } + + if (ob_get_length()) { + yield ob_get_clean(); + } } catch (Error $e) { if (!$e->getSourceContext()) { $e->setSourceContext($this->getSourceContext()); @@ -418,6 +377,111 @@ protected function displayWithErrorHandling(array $context, array $blocks = []) $e->guess(); throw $e; + } finally { + if (!$this->useYield) { + while (ob_get_level() > $level) { + ob_end_clean(); + } + } + } + } + + /** + * @return iterable + */ + public function yieldBlock($name, array $context, array $blocks = [], $useBlocks = true, ?self $templateContext = null) + { + if ($useBlocks && isset($blocks[$name])) { + $template = $blocks[$name][0]; + $block = $blocks[$name][1]; + } elseif (isset($this->blocks[$name])) { + $template = $this->blocks[$name][0]; + $block = $this->blocks[$name][1]; + } else { + $template = null; + $block = null; + } + + // avoid RCEs when sandbox is enabled + if (null !== $template && !$template instanceof self) { + throw new \LogicException('A block must be a method on a \Twig\Template instance.'); + } + + if (null !== $template) { + try { + if ($this->useYield) { + yield from $template->$block($context, $blocks); + return; + } + + $level = ob_get_level(); + ob_start(); + + foreach ($template->$block($context, $blocks) as $data) { + if (ob_get_length()) { + $data = ob_get_clean().$data; + ob_start(); + } + + yield $data; + } + + if (ob_get_length()) { + yield ob_get_clean(); + } + } catch (Error $e) { + if (!$e->getSourceContext()) { + $e->setSourceContext($template->getSourceContext()); + } + + // this is mostly useful for \Twig\Error\LoaderError exceptions + // see \Twig\Error\LoaderError + if (-1 === $e->getTemplateLine()) { + $e->guess(); + } + + throw $e; + } catch (\Throwable $e) { + $e = new RuntimeError(sprintf('An exception has been thrown during the rendering of a template ("%s").', $e->getMessage()), -1, $template->getSourceContext(), $e); + $e->guess(); + + throw $e; + } finally { + if (!$this->useYield) { + while (ob_get_level() > $level) { + ob_end_clean(); + } + } + } + } elseif ($parent = $this->getParent($context)) { + yield from $parent->yieldBlock($name, $context, array_merge($this->blocks, $blocks), false, $templateContext ?? $this); + } elseif (isset($blocks[$name])) { + throw new RuntimeError(sprintf('Block "%s" should not call parent() in "%s" as the block does not exist in the parent template "%s".', $name, $blocks[$name][0]->getTemplateName(), $this->getTemplateName()), -1, $blocks[$name][0]->getSourceContext()); + } else { + throw new RuntimeError(sprintf('Block "%s" on template "%s" does not exist.', $name, $this->getTemplateName()), -1, ($templateContext ?? $this)->getSourceContext()); + } + } + + /** + * Yields a parent block. + * + * This method is for internal use only and should never be called + * directly. + * + * @param string $name The block name to display from the parent + * @param array $context The context + * @param array $blocks The current set of blocks + * + * @return iterable + */ + public function yieldParentBlock($name, array $context, array $blocks = []) + { + if (isset($this->traits[$name])) { + yield from $this->traits[$name][0]->yieldBlock($name, $context, $blocks, false); + } elseif ($parent = $this->getParent($context)) { + yield from $parent->unwrap()->yieldBlock($name, $context, $blocks, false); + } else { + throw new RuntimeError(sprintf('The template has no parent and no traits defining the "%s" block.', $name), -1, $this->getSourceContext()); } } diff --git a/src/TemplateWrapper.php b/src/TemplateWrapper.php index f20a1cf9641..fcfb070c799 100644 --- a/src/TemplateWrapper.php +++ b/src/TemplateWrapper.php @@ -66,12 +66,8 @@ public function renderBlock(string $name, array $context = []): string public function displayBlock(string $name, array $context = []) { $context = $this->env->mergeGlobals($context); - if ($this->template instanceof YieldingTemplate) { - foreach ($this->template->yieldBlock($name, $context) as $data) { - echo $data; - } - } else { - $this->template->displayBlock($name, $context); + foreach ($this->template->yieldBlock($name, $context) as $data) { + echo $data; } } diff --git a/src/Test/NodeTestCase.php b/src/Test/NodeTestCase.php index b4ddafe7598..30d6810f8af 100644 --- a/src/Test/NodeTestCase.php +++ b/src/Test/NodeTestCase.php @@ -67,19 +67,4 @@ protected function getAttributeGetter() { return 'CoreExtension::getAttribute($this->env, $this->source, '; } - - protected function getEchoOrYield(): string - { - return ($this->currentEnv ?? $this->getEnvironment())->useYield() ? 'yield' : 'echo'; - } - - protected function getDisplayOrYield(string $expr): string - { - return sprintf(($this->currentEnv ?? $this->getEnvironment())->useYield() ? 'yield from %s->unwrap()->yield' : '%s->display', $expr); - } - - protected function getDisplayOrYieldBlock(string $expr): string - { - return sprintf(($this->currentEnv ?? $this->getEnvironment())->useYield() ? 'yield from %s->unwrap()->yieldBlock' : '%s->displayBlock', $expr); - } } diff --git a/src/YieldingTemplate.php b/src/YieldingTemplate.php deleted file mode 100644 index c9d7a578b44..00000000000 --- a/src/YieldingTemplate.php +++ /dev/null @@ -1,182 +0,0 @@ - - * - * @internal - */ -abstract class YieldingTemplate extends Template -{ - /** - * @return iterable - */ - public function yield(array $context, array $blocks = []): iterable - { - $context = $this->env->mergeGlobals($context); - $blocks = array_merge($this->blocks, $blocks); - - try { - yield from $this->doDisplay($context, $blocks); - } catch (Error $e) { - if (!$e->getSourceContext()) { - $e->setSourceContext($this->getSourceContext()); - } - - // this is mostly useful for \Twig\Error\LoaderError exceptions - // see \Twig\Error\LoaderError - if (-1 === $e->getTemplateLine()) { - $e->guess(); - } - - throw $e; - } catch (\Throwable $e) { - $e = new RuntimeError(sprintf('An exception has been thrown during the rendering of a template ("%s").', $e->getMessage()), -1, $this->getSourceContext(), $e); - $e->guess(); - - throw $e; - } - } - - public function render(array $context): string - { - $content = ''; - foreach ($this->yield($context) as $data) { - $content .= $data; - } - - return $content; - } - - public function display(array $context, array $blocks = []): void - { - foreach ($this->yield($context, $blocks) as $data) { - echo $data; - } - } - - /** - * @return iterable - */ - public function yieldBlock($name, array $context, array $blocks = [], $useBlocks = true, ?Template $templateContext = null) - { - if ($useBlocks && isset($blocks[$name])) { - $template = $blocks[$name][0]; - $block = $blocks[$name][1]; - } elseif (isset($this->blocks[$name])) { - $template = $this->blocks[$name][0]; - $block = $this->blocks[$name][1]; - } else { - $template = null; - $block = null; - } - - // avoid RCEs when sandbox is enabled - if (null !== $template && !$template instanceof Template) { - throw new \LogicException('A block must be a method on a \Twig\Template instance.'); - } - - if (null !== $template) { - try { - yield from $template->$block($context, $blocks); - } catch (Error $e) { - if (!$e->getSourceContext()) { - $e->setSourceContext($template->getSourceContext()); - } - - // this is mostly useful for \Twig\Error\LoaderError exceptions - // see \Twig\Error\LoaderError - if (-1 === $e->getTemplateLine()) { - $e->guess(); - } - - throw $e; - } catch (\Throwable $e) { - $e = new RuntimeError(sprintf('An exception has been thrown during the rendering of a template ("%s").', $e->getMessage()), -1, $template->getSourceContext(), $e); - $e->guess(); - - throw $e; - } - } elseif (false !== $parent = $this->getParent($context)) { - /** @var YieldingTemplate $parent */ - yield from $parent->yieldBlock($name, $context, array_merge($this->blocks, $blocks), false, $templateContext ?? $this); - } elseif (isset($blocks[$name])) { - throw new RuntimeError(sprintf('Block "%s" should not call parent() in "%s" as the block does not exist in the parent template "%s".', $name, $blocks[$name][0]->getTemplateName(), $this->getTemplateName()), -1, $blocks[$name][0]->getSourceContext()); - } else { - throw new RuntimeError(sprintf('Block "%s" on template "%s" does not exist.', $name, $this->getTemplateName()), -1, ($templateContext ?? $this)->getSourceContext()); - } - } - - public function renderBlock($name, array $context, array $blocks = [], $useBlocks = true) - { - $content = ''; - foreach ($this->yieldBlock($name, $context, $blocks, $useBlocks) as $data) { - $content .= $data; - } - - return $content; - } - - /** - * Yields a parent block. - * - * This method is for internal use only and should never be called - * directly. - * - * @param string $name The block name to display from the parent - * @param array $context The context - * @param array $blocks The current set of blocks - * - * @return iterable - */ - public function yieldParentBlock($name, array $context, array $blocks = []) - { - if (isset($this->traits[$name])) { - yield from $this->traits[$name][0]->yieldBlock($name, $context, $blocks, false); - } elseif (false !== $parent = $this->getParent($context)) { - $parent = $parent->unwrap(); - /** @var YieldingTemplate $parent */ - yield from $parent->yieldBlock($name, $context, $blocks, false); - } else { - throw new RuntimeError(sprintf('The template has no parent and no traits defining the "%s" block.', $name), -1, $this->getSourceContext()); - } - } - - public function renderParentBlock($name, array $context, array $blocks = []) - { - $content = ''; - foreach ($this->yieldParentBlock($name, $context, $blocks) as $data) { - $content .= $data; - } - - return $content; - } - - public function displayBlock($name, array $context, array $blocks = [], $useBlocks = true, ?Template $templateContext = null) - { - throw new RuntimeError(sprintf('Calling "%s" for block "%s" is not supported as "use_yield" is set to "true".', __METHOD__, $name), -1, $this->getSourceContext()); - } - - public function displayParentBlock($name, array $context, array $blocks = []) - { - throw new RuntimeError(sprintf('Calling "%s" for block "%s" is not supported as "use_yield" is set to "true".', __METHOD__, $name), -1, $this->getSourceContext()); - } - - protected function displayWithErrorHandling(array $context, array $blocks = []) - { - throw new RuntimeError(sprintf('Calling "%s" is not supported as "use_yield" is set to "true".', __METHOD__), -1, $this->getSourceContext()); - } -} diff --git a/tests/EnvironmentTest.php b/tests/EnvironmentTest.php index b22fdf42450..688024b8e02 100644 --- a/tests/EnvironmentTest.php +++ b/tests/EnvironmentTest.php @@ -12,10 +12,12 @@ */ use PHPUnit\Framework\TestCase; +use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; use Twig\Cache\CacheInterface; use Twig\Cache\FilesystemCache; use Twig\Environment; use Twig\Error\RuntimeError; +use Twig\Error\SyntaxError; use Twig\Extension\AbstractExtension; use Twig\Extension\ExtensionInterface; use Twig\Extension\GlobalsInterface; @@ -34,6 +36,8 @@ class EnvironmentTest extends TestCase { + use ExpectDeprecationTrait; + public function testAutoescapeOption() { $loader = new ArrayLoader([ @@ -403,6 +407,32 @@ public function testUndefinedTokenParserCallback() $this->assertSame('dynamic', $parser->getTag()); } + /** + * @group legacy + * + * @requires PHP 8 + */ + public function testLegacyEchoingNode() + { + $loader = new ArrayLoader(['echo_bar' => 'A{% set v %}B{% test %}C{% endset %}D{% test %}E{{ v }}F']); + + $twig = new Environment($loader); + $twig->addExtension(new EnvironmentTest_Extension()); + + if ($twig->useYield()) { + $this->expectException(SyntaxError::class); + $this->expectExceptionMessage('An exception has been thrown during the compilation of a template ("You cannot enable the "use_yield" option of Twig as node "Twig\Tests\EnvironmentTest_LegacyEchoingNode" is not marked as ready for it; please make it ready and then flag it with the #[YieldReady] attribute.") in "echo_bar".'); + } else { + $this->expectDeprecation(<<<'EOF' +Since twig/twig 3.9: Twig node "Twig\Tests\EnvironmentTest_LegacyEchoingNode" is not marked as ready for using "yield" instead of "echo"; please make it ready and then flag it with the #[YieldReady] attribute. + Since twig/twig 3.9: Using "echo" is deprecated, use "yield" instead in "Twig\Tests\EnvironmentTest_LegacyEchoingNode", then flag the class with #[YieldReady]. +EOF + ); + } + + $this->assertSame('ADbarEBbarCF', $twig->render('echo_bar')); + } + protected function getMockLoader($templateName, $templateContent) { $loader = $this->createMock(LoaderInterface::class); @@ -486,6 +516,9 @@ class EnvironmentTest_TokenParser extends AbstractTokenParser { public function parse(Token $token): Node { + $this->parser->getStream()->expect(Token::BLOCK_END_TYPE); + + return new EnvironmentTest_LegacyEchoingNode(); } public function getTag(): string @@ -530,3 +563,14 @@ public function fromRuntime($name = 'bar') return $name; } } + +class EnvironmentTest_LegacyEchoingNode extends Node +{ + public function compile($compiler) + { + $compiler + ->addDebugInfo($this) + ->write('echo "bar";') + ; + } +} diff --git a/tests/Node/AutoEscapeTest.php b/tests/Node/AutoEscapeTest.php index b2df9b1605c..9cf18742bb7 100644 --- a/tests/Node/AutoEscapeTest.php +++ b/tests/Node/AutoEscapeTest.php @@ -31,10 +31,9 @@ public function getTests() { $body = new Node([new TextNode('foo', 1)]); $node = new AutoEscapeNode(true, $body, 1); - $displayStmt = $this->getEchoOrYield(); return [ - [$node, "// line 1\n$displayStmt \"foo\";"], + [$node, "// line 1\nyield \"foo\";"], ]; } } diff --git a/tests/Node/BlockReferenceTest.php b/tests/Node/BlockReferenceTest.php index f291f29f33c..1211ee17b83 100644 --- a/tests/Node/BlockReferenceTest.php +++ b/tests/Node/BlockReferenceTest.php @@ -26,9 +26,9 @@ public function testConstructor() public function getTests() { return [ - [new BlockReferenceNode('foo', 1), <<getDisplayOrYieldBlock('$this')}('foo', \$context, \$blocks); +yield from $this->unwrap()->yieldBlock('foo', $context, $blocks); EOF ], ]; diff --git a/tests/Node/BlockTest.php b/tests/Node/BlockTest.php index a021145667d..280a95cba4a 100644 --- a/tests/Node/BlockTest.php +++ b/tests/Node/BlockTest.php @@ -32,41 +32,17 @@ public function testConstructor() public function getTests() { $tests = []; - - if (!$this->getEnvironment()->useYield()) { - $tests[] = [new BlockNode('foo', new TextNode('foo', 1), 1), <<macros; - echo "foo"; -} -EOF - , new Environment(new ArrayLoader()), - ]; - } else { - $tests[] = [new BlockNode('foo', new TextNode('foo', 1), 1), <<macros; yield "foo"; + return; yield ''; } EOF - , new Environment(new ArrayLoader()), - ]; - - $tests[] = [new BlockNode('foo', new Node(), 1), <<macros; - yield ''; -} -EOF - , new Environment(new ArrayLoader()), - ]; - } + , new Environment(new ArrayLoader()), + ]; return $tests; } diff --git a/tests/Node/ForTest.php b/tests/Node/ForTest.php index 5b6a3ee4f51..3ca4b22a304 100644 --- a/tests/Node/ForTest.php +++ b/tests/Node/ForTest.php @@ -53,14 +53,13 @@ public function getTests() $else = null; $node = new ForNode($keyTarget, $valueTarget, $seq, null, $body, $else, 1); $node->setAttribute('with_loop', false); - $displayStmt = $this->getEchoOrYield(); $tests[] = [$node, <<getVariableGetter('items')}); foreach (\$context['_seq'] as \$context["key"] => \$context["item"]) { - $displayStmt {$this->getVariableGetter('foo')}; + yield {$this->getVariableGetter('foo')}; } \$_parent = \$context['_parent']; unset(\$context['_seq'], \$context['_iterated'], \$context['key'], \$context['item'], \$context['_parent'], \$context['loop']); @@ -94,7 +93,7 @@ public function getTests() \$context['loop']['last'] = 1 === \$length; } foreach (\$context['_seq'] as \$context["k"] => \$context["v"]) { - $displayStmt {$this->getVariableGetter('foo')}; + yield {$this->getVariableGetter('foo')}; ++\$context['loop']['index0']; ++\$context['loop']['index']; \$context['loop']['first'] = false; @@ -136,7 +135,7 @@ public function getTests() \$context['loop']['last'] = 1 === \$length; } foreach (\$context['_seq'] as \$context["k"] => \$context["v"]) { - $displayStmt {$this->getVariableGetter('foo')}; + yield {$this->getVariableGetter('foo')}; ++\$context['loop']['index0']; ++\$context['loop']['index']; \$context['loop']['first'] = false; @@ -179,7 +178,7 @@ public function getTests() \$context['loop']['last'] = 1 === \$length; } foreach (\$context['_seq'] as \$context["k"] => \$context["v"]) { - $displayStmt {$this->getVariableGetter('foo')}; + yield {$this->getVariableGetter('foo')}; \$context['_iterated'] = true; ++\$context['loop']['index0']; ++\$context['loop']['index']; @@ -191,7 +190,7 @@ public function getTests() } } if (!\$context['_iterated']) { - $displayStmt {$this->getVariableGetter('foo')}; + yield {$this->getVariableGetter('foo')}; } \$_parent = \$context['_parent']; unset(\$context['_seq'], \$context['_iterated'], \$context['k'], \$context['v'], \$context['_parent'], \$context['loop']); diff --git a/tests/Node/IfTest.php b/tests/Node/IfTest.php index 5dda061d0cb..26821a39b35 100644 --- a/tests/Node/IfTest.php +++ b/tests/Node/IfTest.php @@ -47,12 +47,11 @@ public function getTests() ], [], 1); $else = null; $node = new IfNode($t, $else, 1); - $displayStmt = $this->getEchoOrYield(); $tests[] = [$node, <<getVariableGetter('foo')}; + yield {$this->getVariableGetter('foo')}; } EOF ]; @@ -69,9 +68,9 @@ public function getTests() $tests[] = [$node, <<getVariableGetter('foo')}; + yield {$this->getVariableGetter('foo')}; } elseif (false) { - $displayStmt {$this->getVariableGetter('bar')}; + yield {$this->getVariableGetter('bar')}; } EOF ]; @@ -86,9 +85,9 @@ public function getTests() $tests[] = [$node, <<getVariableGetter('foo')}; + yield {$this->getVariableGetter('foo')}; } else { - $displayStmt {$this->getVariableGetter('bar')}; + yield {$this->getVariableGetter('bar')}; } EOF ]; diff --git a/tests/Node/IncludeTest.php b/tests/Node/IncludeTest.php index ee68339c5d3..cda9d7bf27f 100644 --- a/tests/Node/IncludeTest.php +++ b/tests/Node/IncludeTest.php @@ -40,9 +40,9 @@ public function getTests() $expr = new ConstantExpression('foo.twig', 1); $node = new IncludeNode($expr, null, false, false, 1); - $tests[] = [$node, <<getDisplayOrYield('$this->loadTemplate("foo.twig", null, 1)')}(\$context); +yield from $this->loadTemplate("foo.twig", null, 1)->unwrap()->yield($context); EOF ]; @@ -53,25 +53,25 @@ public function getTests() 0 ); $node = new IncludeNode($expr, null, false, false, 1); - $tests[] = [$node, <<getDisplayOrYield('$this->loadTemplate(((true) ? ("foo") : ("foo")), null, 1)')}(\$context); +yield from $this->loadTemplate(((true) ? ("foo") : ("foo")), null, 1)->unwrap()->yield($context); EOF ]; $expr = new ConstantExpression('foo.twig', 1); $vars = new ArrayExpression([new ConstantExpression('foo', 1), new ConstantExpression(true, 1)], 1); $node = new IncludeNode($expr, $vars, false, false, 1); - $tests[] = [$node, <<getDisplayOrYield('$this->loadTemplate("foo.twig", null, 1)')}(CoreExtension::arrayMerge(\$context, ["foo" => true])); +yield from $this->loadTemplate("foo.twig", null, 1)->unwrap()->yield(CoreExtension::arrayMerge($context, ["foo" => true])); EOF ]; $node = new IncludeNode($expr, $vars, true, false, 1); - $tests[] = [$node, <<getDisplayOrYield('$this->loadTemplate("foo.twig", null, 1)')}(CoreExtension::toArray(["foo" => true])); +yield from $this->loadTemplate("foo.twig", null, 1)->unwrap()->yield(CoreExtension::toArray(["foo" => true])); EOF ]; @@ -85,7 +85,7 @@ public function getTests() // ignore missing template } if (\$__internal_%s) { - {$this->getDisplayOrYield('$__internal_%s')}(CoreExtension::toArray(["foo" => true])); + yield from \$__internal_%s->unwrap()->yield(CoreExtension::toArray(["foo" => true])); } EOF , null, true]; diff --git a/tests/Node/MacroTest.php b/tests/Node/MacroTest.php index 948949af66d..09d7ee6ca7a 100644 --- a/tests/Node/MacroTest.php +++ b/tests/Node/MacroTest.php @@ -45,8 +45,7 @@ public function getTests() $body = new TextNode('foo', 1); $node = new MacroNode('foo', $body, $arguments, 1); - if ($this->getEnvironment()->useYield()) { - $text[] = [$node, <<env->getCharset()); } EOF - , new Environment(new ArrayLoader()), - ]; - } else { - $body = new TextNode('foo', 1); - $node = new MacroNode('foo', $body, $arguments, 1); - - $tests[] = [$node, <<macros; - \$context = \$this->env->mergeGlobals([ - "foo" => \$__foo__, - "bar" => \$__bar__, - "varargs" => \$__varargs__, - ]); - - \$blocks = []; - - return (function () use (&\$context, \$macros) { - ob_start(function () { return ''; }); - try { - echo "foo"; - - return ('' === \$tmp = ob_get_contents()) ? '' : new Markup(\$tmp, \$this->env->getCharset()); - } finally { - ob_end_clean(); - } - })(); -} -EOF - , new Environment(new ArrayLoader()), - ]; - } + , new Environment(new ArrayLoader()), + ]; return $tests; } diff --git a/tests/Node/ModuleTest.php b/tests/Node/ModuleTest.php index 240b69e83ac..2b5e79c2c8e 100644 --- a/tests/Node/ModuleTest.php +++ b/tests/Node/ModuleTest.php @@ -55,8 +55,6 @@ public function getTests() $macros = new Node(); $traits = new Node(); $source = new Source('{{ foo }}', 'foo.twig'); - $parentTemplate = $this->getEnvironment()->useYield() ? 'YieldingTemplate' : 'Template'; - $displayStmt = $this->getEchoOrYield(); $node = new ModuleNode($body, $extends, $blocks, $macros, $traits, new Node([]), $source); $tests[] = [$node, <<macros; // line 1 - $displayStmt "foo"; + yield "foo"; + return; yield ''; } /** @@ -128,7 +127,6 @@ public function getSourceContext() $body = new Node([$import]); $extends = new ConstantExpression('layout.twig', 1); - $parentTemplate = $this->getEnvironment()->useYield() ? 'YieldingTemplate' : 'Template'; $node = new ModuleNode($body, $extends, $blocks, $macros, $traits, new Node([]), $source); $tests[] = [$node, <<macros["macro"] = \$this->loadTemplate("foo.twig", "foo.twig", 2)->unwrap(); // line 1 \$this->parent = \$this->loadTemplate("layout.twig", "foo.twig", 1); - {$this->getDisplayOrYield('$this->parent')}(\$context, array_merge(\$this->blocks, \$blocks)); + yield from \$this->parent->unwrap()->yield(\$context, array_merge(\$this->blocks, \$blocks)); } /** @@ -219,7 +217,6 @@ public function getSourceContext() new ConstantExpression('foo', 2), 2 ); - $parentTemplate = $this->getEnvironment()->useYield() ? 'YieldingTemplate' : 'Template'; $twig = new Environment($this->createMock(LoaderInterface::class), ['debug' => true]); $node = new ModuleNode($body, $extends, $blocks, $macros, $traits, new Node([]), $source); @@ -237,10 +234,10 @@ public function getSourceContext() use Twig\Sandbox\SecurityNotAllowedFilterError; use Twig\Sandbox\SecurityNotAllowedFunctionError; use Twig\Source; -use Twig\\{$parentTemplate}; +use Twig\Template; /* foo.twig */ -class __TwigTemplate_%x extends $parentTemplate +class __TwigTemplate_%x extends Template { private \$source; private \$macros = []; @@ -267,7 +264,7 @@ protected function doDisplay(array \$context, array \$blocks = []) // line 4 \$context["foo"] = "foo"; // line 2 - {$this->getDisplayOrYield('$this->getParent($context)')}(\$context, array_merge(\$this->blocks, \$blocks)); + yield from \$this->getParent(\$context)->unwrap()->yield(\$context, array_merge(\$this->blocks, \$blocks)); } /** diff --git a/tests/Node/PrintTest.php b/tests/Node/PrintTest.php index f951c2e3695..2df440c2808 100644 --- a/tests/Node/PrintTest.php +++ b/tests/Node/PrintTest.php @@ -28,9 +28,7 @@ public function testConstructor() public function getTests() { $tests = []; - $displayStmt = $this->getEchoOrYield(); - - $tests[] = [new PrintNode(new ConstantExpression('foo', 1), 1), "// line 1\n$displayStmt \"foo\";"]; + $tests[] = [new PrintNode(new ConstantExpression('foo', 1), 1), "// line 1\nyield \"foo\";"]; return $tests; } diff --git a/tests/Node/SandboxTest.php b/tests/Node/SandboxTest.php index bf16f1f03c0..c74feba42f3 100644 --- a/tests/Node/SandboxTest.php +++ b/tests/Node/SandboxTest.php @@ -31,7 +31,6 @@ public function getTests() $body = new TextNode('foo', 1); $node = new SandboxNode($body, 1); - $displayStmt = $this->getEchoOrYield(); $tests[] = [$node, <<sandbox->enableSandbox(); } try { - $displayStmt "foo"; + yield "foo"; } finally { if (!\$alreadySandboxed) { \$this->sandbox->disableSandbox(); diff --git a/tests/Node/SetTest.php b/tests/Node/SetTest.php index 81ba3ea4629..57f66cb9d61 100644 --- a/tests/Node/SetTest.php +++ b/tests/Node/SetTest.php @@ -62,18 +62,11 @@ public function getTests() , new Environment(new ArrayLoader()), ]; } else { - $tests[] = [$node, <<env->getCharset()); - } finally { - ob_end_clean(); - } -})(); +$context["foo"] = ('' === $tmp = \Twig\Extension\CoreExtension::captureOutput((function () use (&$context, $macros, $blocks) { + yield "foo"; +})() ?? new \EmptyIterator())) ? '' : new Markup($tmp, $this->env->getCharset()); EOF , new Environment(new ArrayLoader()), ]; @@ -91,9 +84,9 @@ public function getTests() $names = new Node([new AssignNameExpression('foo', 1), new AssignNameExpression('bar', 1)], [], 1); $values = new Node([new ConstantExpression('foo', 1), new NameExpression('bar', 1)], [], 1); $node = new SetNode(false, $names, $values, 1); - $tests[] = [$node, <<getVariableGetter('bar')}]; +[$context["foo"], $context["bar"]] = ["foo", ($context["bar"] ?? null)]; EOF ]; diff --git a/tests/Node/TextTest.php b/tests/Node/TextTest.php index 31639cc2deb..357362c3c7e 100644 --- a/tests/Node/TextTest.php +++ b/tests/Node/TextTest.php @@ -26,8 +26,7 @@ public function testConstructor() public function getTests() { $tests = []; - $displayStmt = $this->getEchoOrYield(); - $tests[] = [new TextNode('foo', 1), "// line 1\n$displayStmt \"foo\";"]; + $tests[] = [new TextNode('foo', 1), "// line 1\nyield \"foo\";"]; return $tests; } diff --git a/tests/TemplateWrapperTest.php b/tests/TemplateWrapperTest.php index 776ac3fa806..a302aba059d 100644 --- a/tests/TemplateWrapperTest.php +++ b/tests/TemplateWrapperTest.php @@ -60,17 +60,13 @@ public function testDisplayBlock() 'index' => '{% block foo %}{{ foo }}{{ bar }}{% endblock %}', ])); - if (!$twig->useYield()) { - $twig->addGlobal('bar', 'BAR'); + $twig->addGlobal('bar', 'BAR'); - $wrapper = $twig->load('index'); + $wrapper = $twig->load('index'); - ob_start(); - $wrapper->displayBlock('foo', ['foo' => 'FOO']); + ob_start(); + $wrapper->displayBlock('foo', ['foo' => 'FOO']); - $this->assertEquals('FOOBAR', ob_get_clean()); - } else { - $this->markTestSkipped('yield not used.'); - } + $this->assertEquals('FOOBAR', ob_get_clean()); } } diff --git a/tests/ignore-use-yield-deprecations b/tests/ignore-use-yield-deprecations deleted file mode 100644 index 0f844211547..00000000000 --- a/tests/ignore-use-yield-deprecations +++ /dev/null @@ -1 +0,0 @@ -%Since twig/twig 3.9.0: Not setting "use_yield" to "true" is deprecated.% From b46e93c7257fb01b7c77768210997b1e00643b91 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Thu, 15 Feb 2024 12:09:06 +0100 Subject: [PATCH 176/812] Minor fixes --- src/Node/ModuleNode.php | 4 ++-- src/Profiler/Node/EnterProfileNode.php | 2 ++ src/Profiler/Node/LeaveProfileNode.php | 2 ++ 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Node/ModuleNode.php b/src/Node/ModuleNode.php index 6a76ed1ea5d..10e94f68198 100644 --- a/src/Node/ModuleNode.php +++ b/src/Node/ModuleNode.php @@ -153,14 +153,14 @@ protected function compileClassHeader(Compiler $compiler) ->write("use Twig\Sandbox\SecurityNotAllowedFilterError;\n") ->write("use Twig\Sandbox\SecurityNotAllowedFunctionError;\n") ->write("use Twig\Source;\n") - ->write(sprintf("use Twig\%s;\n\n", 'Template')) + ->write("use Twig\Template;\n\n") ; } $compiler // if the template name contains */, add a blank to avoid a PHP parse error ->write('/* '.str_replace('*/', '* /', $this->getSourceContext()->getName())." */\n") ->write('class '.$compiler->getEnvironment()->getTemplateClass($this->getSourceContext()->getName(), $this->getAttribute('index'))) - ->raw(sprintf(" extends %s\n", 'Template')) + ->raw(" extends Template\n") ->write("{\n") ->indent() ->write("private \$source;\n") diff --git a/src/Profiler/Node/EnterProfileNode.php b/src/Profiler/Node/EnterProfileNode.php index 1494baf44a3..7b71f8b30df 100644 --- a/src/Profiler/Node/EnterProfileNode.php +++ b/src/Profiler/Node/EnterProfileNode.php @@ -11,6 +11,7 @@ namespace Twig\Profiler\Node; +use Twig\Attribute\YieldReady; use Twig\Compiler; use Twig\Node\Node; @@ -19,6 +20,7 @@ * * @author Fabien Potencier */ +#[YieldReady] class EnterProfileNode extends Node { public function __construct(string $extensionName, string $type, string $name, string $varName) diff --git a/src/Profiler/Node/LeaveProfileNode.php b/src/Profiler/Node/LeaveProfileNode.php index 94cebbaa832..7e9ef9b64ee 100644 --- a/src/Profiler/Node/LeaveProfileNode.php +++ b/src/Profiler/Node/LeaveProfileNode.php @@ -11,6 +11,7 @@ namespace Twig\Profiler\Node; +use Twig\Attribute\YieldReady; use Twig\Compiler; use Twig\Node\Node; @@ -19,6 +20,7 @@ * * @author Fabien Potencier */ +#[YieldReady] class LeaveProfileNode extends Node { public function __construct(string $varName) From 2d262bc35f7d4393b7df05b9a4ae84cb7ad6ad99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Andr=C3=A9?= Date: Wed, 3 Apr 2024 08:23:11 +0200 Subject: [PATCH 177/812] Fix param name in docblock (minor) --- src/Environment.php | 8 ++++---- src/Extension/CoreExtension.php | 20 ++++++++++---------- src/Extension/StringLoaderExtension.php | 4 ++-- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/Environment.php b/src/Environment.php index c6ba8b2c0fe..ec9c39da5ac 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -337,8 +337,8 @@ public function load($name): TemplateWrapper * This method is for internal use only and should never be called * directly. * - * @param string $name The template name - * @param int $index The index if it is an embedded template + * @param string $name The template name + * @param int|null $index The index if it is an embedded template * * @throws LoaderError When the template cannot be found * @throws RuntimeError When a previously generated cache is corrupted @@ -395,8 +395,8 @@ public function loadTemplate(string $cls, string $name, ?int $index = null): Tem * * This method should not be used as a generic way to load templates. * - * @param string $template The template source - * @param string $name An optional name of the template to be used in error messages + * @param string $template The template source + * @param string|null $name An optional name of the template to be used in error messages * * @throws LoaderError When the template cannot be found * @throws SyntaxError When an error occurred during compilation diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index b11e2e5e237..dbc5f61440d 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -89,8 +89,8 @@ final class CoreExtension extends AbstractExtension /** * Sets the default format to be used by the date filter. * - * @param string $format The default date format string - * @param string $dateIntervalFormat The default date interval format string + * @param string|null $format The default date format string + * @param string|null $dateIntervalFormat The default date interval format string */ public function setDateFormat($format = null, $dateIntervalFormat = null) { @@ -581,10 +581,10 @@ public static function round($value, $precision = 0, $method = 'common') * be used. Supplying any of the parameters will override the defaults set in the * environment object. * - * @param mixed $number A float/int/string of the number to format - * @param int $decimal the number of decimal points to display - * @param string $decimalPoint the character(s) to use for the decimal point - * @param string $thousandSep the character(s) to use for the thousands separator + * @param mixed $number A float/int/string of the number to format + * @param int|null $decimal the number of decimal points to display + * @param string|null $decimalPoint the character(s) to use for the decimal point + * @param string|null $thousandSep the character(s) to use for the thousands separator * * @return string The formatted number * @@ -787,7 +787,7 @@ public static function joinFilter($value, $glue = '', $and = null) * * @param string|null $value A string * @param string $delimiter The delimiter - * @param int $limit The limit + * @param int|null $limit The limit * * @return array The split string as an array * @@ -1218,7 +1218,7 @@ public static function lowerFilter(Environment $env, $string) * Strips HTML and PHP tags from a string. * * @param string|null $string - * @param string[]|string|null $string + * @param string[]|string|null $allowable_tags * * @return string * @@ -1729,8 +1729,8 @@ public static function getAttribute(Environment $env, Source $source, $object, $ * * * @param array|\Traversable $array An array - * @param mixed $name The column name - * @param mixed $index The column to use as the index/keys for the returned array + * @param int|string $name The column name + * @param int|string|null $index The column to use as the index/keys for the returned array * * @return array The array of values * diff --git a/src/Extension/StringLoaderExtension.php b/src/Extension/StringLoaderExtension.php index 0945678a832..12f5c30aa91 100644 --- a/src/Extension/StringLoaderExtension.php +++ b/src/Extension/StringLoaderExtension.php @@ -29,8 +29,8 @@ public function getFunctions(): array * * {{ include(template_from_string("Hello {{ name }}")) }} * - * @param string $template A template as a string or object implementing __toString() - * @param string $name An optional name of the template to be used in error messages + * @param string $template A template as a string or object implementing __toString() + * @param string|null $name An optional name of the template to be used in error messages * * @internal */ From 7e8f5eb1a555833768b6ec96a6ab1133478ff3b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Alfaiate?= Date: Mon, 1 Apr 2024 14:39:28 +0700 Subject: [PATCH 178/812] Fix exception when timezone is false --- extra/intl-extra/IntlExtension.php | 2 +- extra/intl-extra/Tests/IntlExtensionTest.php | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/extra/intl-extra/IntlExtension.php b/extra/intl-extra/IntlExtension.php index 0b33331f6a8..d932e7f7fc5 100644 --- a/extra/intl-extra/IntlExtension.php +++ b/extra/intl-extra/IntlExtension.php @@ -372,7 +372,7 @@ public function formatDateTime(Environment $env, $date, ?string $dateFormat = 'm $date = CoreExtension::dateConverter($env, $date, $timezone); $formatterTimezone = $timezone; - if (null === $formatterTimezone) { + if (null === $formatterTimezone || false === $formatterTimezone) { $formatterTimezone = $date->getTimezone(); } elseif (\is_string($formatterTimezone)) { $formatterTimezone = new \DateTimeZone($timezone); diff --git a/extra/intl-extra/Tests/IntlExtensionTest.php b/extra/intl-extra/Tests/IntlExtensionTest.php index 688a415f4d0..91aa9e84f01 100644 --- a/extra/intl-extra/Tests/IntlExtensionTest.php +++ b/extra/intl-extra/Tests/IntlExtensionTest.php @@ -45,6 +45,20 @@ public function testFormatterWithoutProtoFallsBackToCoreExtensionTimezone() ); } + public function testFormatterWithoutProtoSkipTimezoneConverter() + { + $ext = new IntlExtension(); + $env = new Environment(new ArrayLoader()); + // EET is always +2 without changes for daylight saving time + // so it has a fixed difference to UTC + $env->getExtension(CoreExtension::class)->setTimezone('EET'); + + $this->assertStringStartsWith( + 'Feb 20, 2020, 1:37:00', + $ext->formatDateTime($env, new \DateTime('2020-02-20T13:37:00+00:00', new \DateTimeZone('UTC')), 'medium', 'medium', '', false) + ); + } + public function testFormatterProto() { $dateFormatterProto = new \IntlDateFormatter('fr', \IntlDateFormatter::FULL, \IntlDateFormatter::FULL, new \DateTimeZone('Europe/Paris')); From b6ae45c3d9126e715282f6d5ec1a16fa4c5ffddd Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sat, 13 Apr 2024 09:36:08 +0200 Subject: [PATCH 179/812] Add a reference to the Twig CS fixer tool --- doc/coding_standards.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/doc/coding_standards.rst b/doc/coding_standards.rst index 721b0f13aaf..5cbd46cf46d 100644 --- a/doc/coding_standards.rst +++ b/doc/coding_standards.rst @@ -1,6 +1,12 @@ Coding Standards ================ +.. note:: + + The `Twig CS fixer tool `_ + uses the coding standards described in this document to automatically fix + your templates. + When writing Twig templates, we recommend you to follow these official coding standards: From 97df38b39d06adff8bcb2c2e47900742ca3a574b Mon Sep 17 00:00:00 2001 From: Thomas Landauer Date: Sat, 13 Apr 2024 10:44:08 +0200 Subject: [PATCH 180/812] Update coding_standards.rst: "one (and only one)" => "exactly one" Page: https://twig.symfony.com/doc/3.x/coding_standards.html --- doc/coding_standards.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/coding_standards.rst b/doc/coding_standards.rst index 5cbd46cf46d..310cc8c4338 100644 --- a/doc/coding_standards.rst +++ b/doc/coding_standards.rst @@ -10,7 +10,7 @@ Coding Standards When writing Twig templates, we recommend you to follow these official coding standards: -* Put one (and only one) space after the start of a delimiter (``{{``, ``{%``, +* Put exactly one space after the start of a delimiter (``{{``, ``{%``, and ``{#``) and before the end of a delimiter (``}}``, ``%}``, and ``#}``): .. code-block:: twig @@ -28,7 +28,7 @@ standards: {#- comment -#} {%- if foo -%}{%- endif -%} -* Put one (and only one) space before and after the following operators: +* Put exactly one space before and after the following operators: comparison operators (``==``, ``!=``, ``<``, ``>``, ``>=``, ``<=``), math operators (``+``, ``-``, ``/``, ``*``, ``%``, ``//``, ``**``), logic operators (``not``, ``and``, ``or``), ``~``, ``is``, ``in``, and the ternary @@ -40,7 +40,7 @@ standards: {{ foo ~ bar }} {{ true ? true : false }} -* Put one (and only one) space after the ``:`` sign in hashes and ``,`` in +* Put exactly one space after the ``:`` sign in hashes and ``,`` in arrays and hashes: .. code-block:: twig From 30b5658f895db60ff456fcba1b54392d2c3e4e9b Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sun, 14 Apr 2024 10:22:11 +0200 Subject: [PATCH 181/812] Fix CS --- CHANGELOG | 2 +- src/Node/CaptureNode.php | 2 +- src/Template.php | 2 ++ tests/Node/BlockTest.php | 1 - 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index a6e7a9365ee..e604e7397c4 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -199,7 +199,7 @@ * removed Parser::isReservedMacroName() * removed SanboxedPrintNode * removed Node::setTemplateName() - * made classes maked as "@final" final + * made classes marked as "@final" final * removed InitRuntimeInterface, ExistsLoaderInterface, and SourceContextLoaderInterface * removed the "spaceless" tag * removed Twig\Environment::getBaseTemplateClass() and Twig\Environment::setBaseTemplateClass() diff --git a/src/Node/CaptureNode.php b/src/Node/CaptureNode.php index 27ae4c60537..561e1ea53c8 100644 --- a/src/Node/CaptureNode.php +++ b/src/Node/CaptureNode.php @@ -34,7 +34,7 @@ public function compile(Compiler $compiler): void if (!$this->getAttribute('raw')) { $compiler->raw("('' === \$tmp = "); } - $compiler->raw($useYield ? "implode('', iterator_to_array(" : "\\Twig\\Extension\\CoreExtension::captureOutput("); + $compiler->raw($useYield ? "implode('', iterator_to_array(" : '\\Twig\\Extension\\CoreExtension::captureOutput('); if ($this->getAttribute('with_blocks')) { $compiler->raw("(function () use (&\$context, \$macros, \$blocks) {\n"); } else { diff --git a/src/Template.php b/src/Template.php index bb8146a2c5b..f3133324034 100644 --- a/src/Template.php +++ b/src/Template.php @@ -342,6 +342,7 @@ public function yield(array $context, array $blocks = []): iterable try { if ($this->useYield) { yield from $this->doDisplay($context, $blocks); + return; } @@ -411,6 +412,7 @@ public function yieldBlock($name, array $context, array $blocks = [], $useBlocks try { if ($this->useYield) { yield from $template->$block($context, $blocks); + return; } diff --git a/tests/Node/BlockTest.php b/tests/Node/BlockTest.php index 280a95cba4a..0938e74a180 100644 --- a/tests/Node/BlockTest.php +++ b/tests/Node/BlockTest.php @@ -14,7 +14,6 @@ use Twig\Environment; use Twig\Loader\ArrayLoader; use Twig\Node\BlockNode; -use Twig\Node\Node; use Twig\Node\TextNode; use Twig\Test\NodeTestCase; From fe2bdd2b665ddddcf4aa4cb1e7b90373006b295a Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sun, 14 Apr 2024 10:44:37 +0200 Subject: [PATCH 182/812] Fix doc markup --- doc/deprecated.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/deprecated.rst b/doc/deprecated.rst index 948dfb3ef43..2c697c69de0 100644 --- a/doc/deprecated.rst +++ b/doc/deprecated.rst @@ -8,8 +8,8 @@ feature that was deprecated in Twig 3.x is removed in Twig 4.0). Functions --------- - * The `twig_test_iterable` function is deprecated; use the native - `is_iterable` instead. + * The ``twig_test_iterable`` function is deprecated; use the native PHP + ``is_iterable`` function instead. Extensions ---------- From 41d702d6bea299a6b1b4970d40dbdd6f124aefb0 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sun, 14 Apr 2024 10:33:37 +0200 Subject: [PATCH 183/812] Add a deprecation notice when using AbstractNodeVisitor (deprecated since 2.9) --- CHANGELOG | 1 + doc/deprecated.rst | 6 ++++++ src/NodeVisitor/AbstractNodeVisitor.php | 4 ++-- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index e604e7397c4..f8f073c8328 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,6 @@ # 3.9.0 (2024-XX-XX) + * Deprecate AbstractNodeVisitor * Add a new "yield" mode for output generation; Node implementations that use "echo" or "print" should use "yield" instead; all Node implementations should be flagged with `#[YieldReady]` once they've been made ready for "yield"; diff --git a/doc/deprecated.rst b/doc/deprecated.rst index 2c697c69de0..b4cdf774d7f 100644 --- a/doc/deprecated.rst +++ b/doc/deprecated.rst @@ -17,3 +17,9 @@ Extensions * All functions defined in Twig extensions are marked as internal as of Twig 3.9.0, and will be removed in Twig 4.0. They have been replaced by internal methods on their respective extension classes. + +Node Visitors +------------- + +* The ``Twig\NodeVisitor\AbstractNodeVisitor`` class is deprecated, implement the + ``Twig\NodeVisitor\NodeVisitorInterface`` interface instead. diff --git a/src/NodeVisitor/AbstractNodeVisitor.php b/src/NodeVisitor/AbstractNodeVisitor.php index d7036ae5511..5de35fd096a 100644 --- a/src/NodeVisitor/AbstractNodeVisitor.php +++ b/src/NodeVisitor/AbstractNodeVisitor.php @@ -17,9 +17,9 @@ /** * Used to make node visitors compatible with Twig 1.x and 2.x. * - * To be removed in Twig 3.1. - * * @author Fabien Potencier + * + * @deprecated since 3.9 (to be removed in 4.0) */ abstract class AbstractNodeVisitor implements NodeVisitorInterface { From 61031d6cfa29c10740dd755a4471d99631fa8a19 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Wed, 13 Dec 2023 13:40:03 +0100 Subject: [PATCH 184/812] Deprecate passing a Template instance in Environment::resolveTemplate() and Template::loadTemplate() --- CHANGELOG | 1 + src/Environment.php | 6 ++++-- src/Template.php | 12 ++++++++++-- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index f8f073c8328..7c6e293b34e 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,7 @@ # 3.9.0 (2024-XX-XX) * Deprecate AbstractNodeVisitor + * Deprecate passing Template to Environment::resolveTemplate() and Template::loadTemplate() * Add a new "yield" mode for output generation; Node implementations that use "echo" or "print" should use "yield" instead; all Node implementations should be flagged with `#[YieldReady]` once they've been made ready for "yield"; diff --git a/src/Environment.php b/src/Environment.php index ec9c39da5ac..bcc587f1e0b 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -443,12 +443,12 @@ public function isTemplateFresh(string $name, int $time): bool * Similar to load() but it also accepts instances of \Twig\Template and * \Twig\TemplateWrapper, and an array of templates where each is tried to be loaded. * - * @param string|TemplateWrapper|array $names A template or an array of templates to try consecutively + * @param string|TemplateWrapper|array $names A template or an array of templates to try consecutively * * @throws LoaderError When none of the templates can be found * @throws SyntaxError When an error occurred during compilation */ - public function resolveTemplate($names): TemplateWrapper + public function resolveTemplate(string|TemplateWrapper|Template|array $names): TemplateWrapper { if (!\is_array($names)) { return $this->load($names); @@ -457,6 +457,8 @@ public function resolveTemplate($names): TemplateWrapper $count = \count($names); foreach ($names as $name) { if ($name instanceof Template) { + trigger_deprecation('twig/twig', '3.9', 'Passing a "%s" instance to "%s" is deprecated.', Template::class, __METHOD__); + return new TemplateWrapper($this, $name); } if ($name instanceof TemplateWrapper) { diff --git a/src/Template.php b/src/Template.php index f3133324034..54527c25021 100644 --- a/src/Template.php +++ b/src/Template.php @@ -249,16 +249,24 @@ public function getBlockNames(array $context, array $blocks = []) } /** + * @param string|TemplateWrapper|array $template + * * @return self|TemplateWrapper */ - protected function loadTemplate($template, $templateName = null, $line = null, $index = null) + protected function loadTemplate(string|TemplateWrapper|self|array $template, $templateName = null, $line = null, $index = null) { try { if (\is_array($template)) { return $this->env->resolveTemplate($template); } - if ($template instanceof self || $template instanceof TemplateWrapper) { + if ($template instanceof TemplateWrapper) { + return $template; + } + + if ($template instanceof self) { + trigger_deprecation('twig/twig', '3.9', 'Passing a "%s" instance to "%s" is deprecated.', self::class, __METHOD__); + return $template; } From a62581ae5e91351680b43d26b87925def55b5364 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sun, 14 Apr 2024 10:10:13 +0200 Subject: [PATCH 185/812] Fix typo --- src/Environment.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Environment.php b/src/Environment.php index bcc587f1e0b..1e13a3c491e 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -440,8 +440,8 @@ public function isTemplateFresh(string $name, int $time): bool /** * Tries to load a template consecutively from an array. * - * Similar to load() but it also accepts instances of \Twig\Template and - * \Twig\TemplateWrapper, and an array of templates where each is tried to be loaded. + * Similar to load() but it also accepts instances of \Twig\TemplateWrapper + * and an array of templates where each is tried to be loaded. * * @param string|TemplateWrapper|array $names A template or an array of templates to try consecutively * From dbe9456f770217e669bfd41eadc65a760efb3b2f Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sun, 14 Apr 2024 10:15:06 +0200 Subject: [PATCH 186/812] Fix incompatibility with old PHP versions --- CHANGELOG | 2 +- doc/deprecated.rst | 8 ++++++++ src/Environment.php | 7 ++++++- src/Template.php | 2 +- 4 files changed, 16 insertions(+), 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 7c6e293b34e..21bf1278bd8 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,7 +1,7 @@ # 3.9.0 (2024-XX-XX) * Deprecate AbstractNodeVisitor - * Deprecate passing Template to Environment::resolveTemplate() and Template::loadTemplate() + * Deprecate passing Template to Environment::resolveTemplate(), Environment::load(), and Template::loadTemplate() * Add a new "yield" mode for output generation; Node implementations that use "echo" or "print" should use "yield" instead; all Node implementations should be flagged with `#[YieldReady]` once they've been made ready for "yield"; diff --git a/doc/deprecated.rst b/doc/deprecated.rst index b4cdf774d7f..4121d190592 100644 --- a/doc/deprecated.rst +++ b/doc/deprecated.rst @@ -23,3 +23,11 @@ Node Visitors * The ``Twig\NodeVisitor\AbstractNodeVisitor`` class is deprecated, implement the ``Twig\NodeVisitor\NodeVisitorInterface`` interface instead. + +Templates +--------- + +* Passing ``Twig\\Template`` instances to Twig public API is deprecated (like + in ``Environment::resolveTemplate()``, ``Environment::load()``, and + ``Template::loadTemplate()``); pass instances of ``Twig\\TemplateWrapper`` + instead. diff --git a/src/Environment.php b/src/Environment.php index 1e13a3c491e..3422ca49f74 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -327,6 +327,11 @@ public function load($name): TemplateWrapper if ($name instanceof TemplateWrapper) { return $name; } + if ($name instanceof Template) { + trigger_deprecation('twig/twig', '3.9', 'Passing a "%s" instance to "%s" is deprecated.', self::class, __METHOD__); + + return $name; + } return new TemplateWrapper($this, $this->loadTemplate($this->getTemplateClass($name), $name)); } @@ -448,7 +453,7 @@ public function isTemplateFresh(string $name, int $time): bool * @throws LoaderError When none of the templates can be found * @throws SyntaxError When an error occurred during compilation */ - public function resolveTemplate(string|TemplateWrapper|Template|array $names): TemplateWrapper + public function resolveTemplate($names): TemplateWrapper { if (!\is_array($names)) { return $this->load($names); diff --git a/src/Template.php b/src/Template.php index 54527c25021..f200a610fd1 100644 --- a/src/Template.php +++ b/src/Template.php @@ -253,7 +253,7 @@ public function getBlockNames(array $context, array $blocks = []) * * @return self|TemplateWrapper */ - protected function loadTemplate(string|TemplateWrapper|self|array $template, $templateName = null, $line = null, $index = null) + protected function loadTemplate($template, $templateName = null, $line = null, $index = null) { try { if (\is_array($template)) { From 6f3d173f64347e55362c81d5178ae1c0d126f180 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sun, 14 Apr 2024 12:32:42 +0200 Subject: [PATCH 187/812] Fix markup --- doc/deprecated.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/deprecated.rst b/doc/deprecated.rst index 4121d190592..06a532bd010 100644 --- a/doc/deprecated.rst +++ b/doc/deprecated.rst @@ -27,7 +27,7 @@ Node Visitors Templates --------- -* Passing ``Twig\\Template`` instances to Twig public API is deprecated (like +* Passing ``Twig\Template`` instances to Twig public API is deprecated (like in ``Environment::resolveTemplate()``, ``Environment::load()``, and - ``Template::loadTemplate()``); pass instances of ``Twig\\TemplateWrapper`` + ``Template::loadTemplate()``); pass instances of ``Twig\TemplateWrapper`` instead. From 6091e7d60cf6097f8994b2a4bd0c3aecc82efa4a Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sun, 14 Apr 2024 12:38:19 +0200 Subject: [PATCH 188/812] Remove usage of array() in fixtures --- tests/Fixtures/extensions/anonymous_functions.test | 2 +- tests/Fixtures/filters/column.test | 4 ++-- tests/Fixtures/functions/block_without_parent.test | 2 +- tests/Fixtures/tags/block/conditional_block.test | 2 +- tests/Fixtures/tags/inheritance/capturing_block.test | 2 +- tests/Fixtures/tags/inheritance/conditional_block.test | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/Fixtures/extensions/anonymous_functions.test b/tests/Fixtures/extensions/anonymous_functions.test index 842ecf7a180..a850eeef170 100644 --- a/tests/Fixtures/extensions/anonymous_functions.test +++ b/tests/Fixtures/extensions/anonymous_functions.test @@ -4,7 +4,7 @@ use an anonymous function as a function {{ anon_foo('bar') }} {{ 'bar'|anon_foo }} --DATA-- -return array() +return [] --EXPECT-- *bar* *bar* diff --git a/tests/Fixtures/filters/column.test b/tests/Fixtures/filters/column.test index a2a7f2eb30d..d47c44e15dd 100644 --- a/tests/Fixtures/filters/column.test +++ b/tests/Fixtures/filters/column.test @@ -4,8 +4,8 @@ {{ array|column('foo')|join }} {{ traversable|column('foo')|join }} --DATA-- -$items = array(array('bar' => 'foo', 'foo' => 'bar'), array('foo' => 'foo', 'bar' => 'bar')); -return array('array' => $items, 'traversable' => new ArrayIterator($items)); +$items = [['bar' => 'foo', 'foo' => 'bar'], ['foo' => 'foo', 'bar' => 'bar']]; +return ['array' => $items, 'traversable' => new ArrayIterator($items)]; --EXPECT-- barfoo barfoo diff --git a/tests/Fixtures/functions/block_without_parent.test b/tests/Fixtures/functions/block_without_parent.test index 7fb7ef63246..0f68cb9d0f7 100644 --- a/tests/Fixtures/functions/block_without_parent.test +++ b/tests/Fixtures/functions/block_without_parent.test @@ -6,6 +6,6 @@ --TEMPLATE(parent.twig)-- {{ block('label') }} --DATA-- -return array() +return [] --EXCEPTION-- Twig\Error\RuntimeError: Block "label" should not call parent() in "index.twig" as the block does not exist in the parent template "parent.twig" in "index.twig" at line 3. diff --git a/tests/Fixtures/tags/block/conditional_block.test b/tests/Fixtures/tags/block/conditional_block.test index d4e2ae009fb..04bf601e7d4 100644 --- a/tests/Fixtures/tags/block/conditional_block.test +++ b/tests/Fixtures/tags/block/conditional_block.test @@ -4,6 +4,6 @@ conditional "block" tag {% if false %}{% block foo %}FOO{% endblock %}{% endif %} {% if true %}{% block bar %}BAR{% endblock %}{% endif %} --DATA-- -return array() +return [] --EXPECT-- BAR diff --git a/tests/Fixtures/tags/inheritance/capturing_block.test b/tests/Fixtures/tags/inheritance/capturing_block.test index 703e33fd0cc..91db2c22f59 100644 --- a/tests/Fixtures/tags/inheritance/capturing_block.test +++ b/tests/Fixtures/tags/inheritance/capturing_block.test @@ -12,6 +12,6 @@ capturing "block" tag with "extends" tag {% block content %}{% endblock %} {% block content1 %}{% endblock %} --DATA-- -return array() +return [] --EXPECT-- FOOBARFOO diff --git a/tests/Fixtures/tags/inheritance/conditional_block.test b/tests/Fixtures/tags/inheritance/conditional_block.test index 2c6e2e37f3b..0b42212dd1a 100644 --- a/tests/Fixtures/tags/inheritance/conditional_block.test +++ b/tests/Fixtures/tags/inheritance/conditional_block.test @@ -9,6 +9,6 @@ conditional "block" tag with "extends" tag --TEMPLATE(layout.twig)-- {% block content %}{% endblock %} --DATA-- -return array() +return [] --EXCEPTION-- Twig\Error\SyntaxError: A block definition cannot be nested under non-capturing nodes in "index.twig" at line 5. From 11511e181d2ee97105e4b621f60b30f2d60883d8 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Tue, 19 Mar 2024 20:01:22 +0100 Subject: [PATCH 189/812] Add PHP 8.4 to CI --- .github/workflows/ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b22266b7ab1..6f9588cb311 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,6 +28,7 @@ jobs: - '8.1' - '8.2' - '8.3' + - '8.4' steps: - name: "Checkout code" @@ -79,6 +80,7 @@ jobs: - '8.1' - '8.2' - '8.3' + - '8.4' extension: - 'cache-extra' - 'cssinliner-extra' From fdfc5dddb6f894dffe63d080328223a466bebc67 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Mon, 15 Apr 2024 10:22:35 +0200 Subject: [PATCH 190/812] Fix compat with PHP 8.4 --- CHANGELOG | 1 + src/Node/Expression/CallExpression.php | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 21bf1278bd8..38421d5f245 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,6 @@ # 3.9.0 (2024-XX-XX) + * Add support for PHP 8.4 * Deprecate AbstractNodeVisitor * Deprecate passing Template to Environment::resolveTemplate(), Environment::load(), and Template::loadTemplate() * Add a new "yield" mode for output generation; diff --git a/src/Node/Expression/CallExpression.php b/src/Node/Expression/CallExpression.php index 5c7326bf411..71a9c739a2c 100644 --- a/src/Node/Expression/CallExpression.php +++ b/src/Node/Expression/CallExpression.php @@ -297,7 +297,7 @@ private function reflectCallable($callable) } $r = new \ReflectionFunction($closure); - if (str_contains($r->name, '{closure}')) { + if (str_contains($r->name, '{closure')) { return $this->reflector = [$r, $callable, 'Closure']; } From e58b2feda24b78f7e76503fbd0a6ca0640a51d9b Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Tue, 16 Apr 2024 12:04:02 -0400 Subject: [PATCH 191/812] Update CHANGELOG --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index 38421d5f245..34642cadee0 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -13,6 +13,7 @@ * Deprecate all internal extension functions in favor of methods on the extension classes * Mark all extension functions as @internal * Add SourcePolicyInterface to selectively enable the Sandbox based on a template's Source + * Throw a proper Twig exception when using cycle on an empty array # 3.8.0 (2023-11-21) From 47857eebb197745f66369b76c044a2359e3cc5b9 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Tue, 16 Apr 2024 12:04:21 -0400 Subject: [PATCH 192/812] Prepare the 3.9.0 release --- CHANGELOG | 2 +- src/Environment.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 34642cadee0..9ff2ead3dbe 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,4 +1,4 @@ -# 3.9.0 (2024-XX-XX) +# 3.9.0 (2024-04-16) * Add support for PHP 8.4 * Deprecate AbstractNodeVisitor diff --git a/src/Environment.php b/src/Environment.php index 3422ca49f74..f18edf2fc2c 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -41,12 +41,12 @@ */ class Environment { - public const VERSION = '3.9.0-DEV'; + public const VERSION = '3.9.0'; public const VERSION_ID = 30900; public const MAJOR_VERSION = 3; public const MINOR_VERSION = 9; public const RELEASE_VERSION = 0; - public const EXTRA_VERSION = 'DEV'; + public const EXTRA_VERSION = ''; private $charset; private $loader; From 2105d659574211ce6da200da5867180a1e00c1ef Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Tue, 16 Apr 2024 12:05:55 -0400 Subject: [PATCH 193/812] Bump version --- CHANGELOG | 4 ++++ src/Environment.php | 8 ++++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 9ff2ead3dbe..ef5b57fc236 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +# 3.9.1 (2024-XX-XX) + + * n/a + # 3.9.0 (2024-04-16) * Add support for PHP 8.4 diff --git a/src/Environment.php b/src/Environment.php index f18edf2fc2c..8b9c024c9fa 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -41,12 +41,12 @@ */ class Environment { - public const VERSION = '3.9.0'; - public const VERSION_ID = 30900; + public const VERSION = '3.9.1-DEV'; + public const VERSION_ID = 30901; public const MAJOR_VERSION = 3; public const MINOR_VERSION = 9; - public const RELEASE_VERSION = 0; - public const EXTRA_VERSION = ''; + public const RELEASE_VERSION = 1; + public const EXTRA_VERSION = 'DEV'; private $charset; private $loader; From 64cebe0e0b102e81b14d7008fe05e4ad8d06058c Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Tue, 16 Apr 2024 17:30:40 -0400 Subject: [PATCH 194/812] Fix CaptureNode for some use cases --- CHANGELOG | 2 +- src/Node/MacroNode.php | 5 ++++- tests/Fixtures/macros/macro_with_capture.test | 14 ++++++++++++++ 3 files changed, 19 insertions(+), 2 deletions(-) create mode 100644 tests/Fixtures/macros/macro_with_capture.test diff --git a/CHANGELOG b/CHANGELOG index ef5b57fc236..07490c531a2 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,6 @@ # 3.9.1 (2024-XX-XX) - * n/a + * Fix missing `$blocks` variable in `CaptureNode` # 3.9.0 (2024-04-16) diff --git a/src/Node/MacroNode.php b/src/Node/MacroNode.php index e44150f526c..761ef55d2fb 100644 --- a/src/Node/MacroNode.php +++ b/src/Node/MacroNode.php @@ -79,6 +79,9 @@ public function compile(Compiler $compiler): void ; } + $node = new CaptureNode($this->getNode('body'), $this->getNode('body')->lineno, $this->getNode('body')->tag); + $node->setAttribute('with_blocks', true); + $compiler ->write('') ->string(self::VARARGS_NAME) @@ -88,7 +91,7 @@ public function compile(Compiler $compiler): void ->write("]);\n\n") ->write("\$blocks = [];\n\n") ->write('return ') - ->subcompile(new CaptureNode($this->getNode('body'), $this->getNode('body')->lineno, $this->getNode('body')->tag)) + ->subcompile($node) ->raw("\n") ->outdent() ->write("}\n\n") diff --git a/tests/Fixtures/macros/macro_with_capture.test b/tests/Fixtures/macros/macro_with_capture.test new file mode 100644 index 00000000000..f67a7fcdb9b --- /dev/null +++ b/tests/Fixtures/macros/macro_with_capture.test @@ -0,0 +1,14 @@ +--TEST-- +macro +--TEMPLATE-- +{{ _self.some_macro() }} + +{% macro some_macro() %} + {% apply spaceless %} + {% if true %}foo{% endif %} + {% endapply %} +{% endmacro %} +--DATA-- +return [] +--EXPECT-- +foo From 664647f46bf68a98cf5af27b42de8017a3e2d878 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Wed, 17 Apr 2024 05:00:00 -0400 Subject: [PATCH 195/812] Prepare the 3.9.1 release --- CHANGELOG | 2 +- src/Environment.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 07490c531a2..4c4de1e5297 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,4 +1,4 @@ -# 3.9.1 (2024-XX-XX) +# 3.9.1 (2024-04-17) * Fix missing `$blocks` variable in `CaptureNode` diff --git a/src/Environment.php b/src/Environment.php index 8b9c024c9fa..b7d251be93e 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -41,12 +41,12 @@ */ class Environment { - public const VERSION = '3.9.1-DEV'; + public const VERSION = '3.9.1'; public const VERSION_ID = 30901; public const MAJOR_VERSION = 3; public const MINOR_VERSION = 9; public const RELEASE_VERSION = 1; - public const EXTRA_VERSION = 'DEV'; + public const EXTRA_VERSION = ''; private $charset; private $loader; From 0b4269e6c13e0e2802d531dfb618ad00a4b72d47 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Wed, 17 Apr 2024 05:00:49 -0400 Subject: [PATCH 196/812] Bump version --- CHANGELOG | 4 ++++ src/Environment.php | 8 ++++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 4c4de1e5297..a0e0694c140 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +# 3.9.2 (2024-XX-XX) + + * n/a + # 3.9.1 (2024-04-17) * Fix missing `$blocks` variable in `CaptureNode` diff --git a/src/Environment.php b/src/Environment.php index b7d251be93e..09cd0a6d692 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -41,12 +41,12 @@ */ class Environment { - public const VERSION = '3.9.1'; - public const VERSION_ID = 30901; + public const VERSION = '3.9.2-DEV'; + public const VERSION_ID = 30902; public const MAJOR_VERSION = 3; public const MINOR_VERSION = 9; - public const RELEASE_VERSION = 1; - public const EXTRA_VERSION = ''; + public const RELEASE_VERSION = 2; + public const EXTRA_VERSION = 'DEV'; private $charset; private $loader; From 5d903b63258761b2af99134d3c9e1018c85f155f Mon Sep 17 00:00:00 2001 From: benedikt brunner <122370755+Benedikt-Brunner@users.noreply.github.com> Date: Wed, 17 Apr 2024 12:41:53 +0000 Subject: [PATCH 197/812] Require correct twig core version --- extra/intl-extra/composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extra/intl-extra/composer.json b/extra/intl-extra/composer.json index c39239164bc..a46eada315c 100644 --- a/extra/intl-extra/composer.json +++ b/extra/intl-extra/composer.json @@ -16,7 +16,7 @@ ], "require": { "php": ">=7.2.5", - "twig/twig": "^3.0", + "twig/twig": "^3.9", "symfony/intl": "^5.4|^6.4|^7.0" }, "require-dev": { From 3ca9685f58dce37fba0f850c8c2e5b41a99b0776 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Wed, 17 Apr 2024 08:50:46 -0400 Subject: [PATCH 198/812] Fix usage of display_end hook --- CHANGELOG | 2 +- src/Node/ModuleNode.php | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index a0e0694c140..26966d28200 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,6 @@ # 3.9.2 (2024-XX-XX) - * n/a + * Fix usage of display_end hook # 3.9.1 (2024-04-17) diff --git a/src/Node/ModuleNode.php b/src/Node/ModuleNode.php index 10e94f68198..df407caf0a9 100644 --- a/src/Node/ModuleNode.php +++ b/src/Node/ModuleNode.php @@ -336,12 +336,15 @@ protected function compileDisplay(Compiler $compiler) $compiler->raw('$this->getParent($context)'); } $compiler->raw("->unwrap()->yield(\$context, array_merge(\$this->blocks, \$blocks));\n"); - } else { + } + + $compiler->subcompile($this->getNode('display_end')); + + if (!$this->hasNode('parent')) { $compiler->write("return; yield '';\n"); // ensure at least one yield call even for templates with no output } $compiler - ->subcompile($this->getNode('display_end')) ->outdent() ->write("}\n\n") ; From 856cb5a6cfd6f3e4dc1f6c9a8f54e259503f7cf3 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Wed, 17 Apr 2024 10:16:01 -0400 Subject: [PATCH 199/812] Prepare the 3.9.2 release --- CHANGELOG | 2 +- src/Environment.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 26966d28200..1c88f2ed0c9 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,4 +1,4 @@ -# 3.9.2 (2024-XX-XX) +# 3.9.2 (2024-04-17) * Fix usage of display_end hook diff --git a/src/Environment.php b/src/Environment.php index 09cd0a6d692..540ee02d624 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -41,12 +41,12 @@ */ class Environment { - public const VERSION = '3.9.2-DEV'; + public const VERSION = '3.9.2'; public const VERSION_ID = 30902; public const MAJOR_VERSION = 3; public const MINOR_VERSION = 9; public const RELEASE_VERSION = 2; - public const EXTRA_VERSION = 'DEV'; + public const EXTRA_VERSION = ''; private $charset; private $loader; From c145871023dcc6c2117f8af3aa691f75d0924e01 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Wed, 17 Apr 2024 10:17:23 -0400 Subject: [PATCH 200/812] Bump version --- CHANGELOG | 4 ++++ src/Environment.php | 8 ++++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 1c88f2ed0c9..95034696869 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +# 3.9.3 (2024-XX-XX) + + * n/a + # 3.9.2 (2024-04-17) * Fix usage of display_end hook diff --git a/src/Environment.php b/src/Environment.php index 540ee02d624..c761bdb416c 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -41,12 +41,12 @@ */ class Environment { - public const VERSION = '3.9.2'; - public const VERSION_ID = 30902; + public const VERSION = '3.9.3-DEV'; + public const VERSION_ID = 30903; public const MAJOR_VERSION = 3; public const MINOR_VERSION = 9; - public const RELEASE_VERSION = 2; - public const EXTRA_VERSION = ''; + public const RELEASE_VERSION = 3; + public const EXTRA_VERSION = 'DEV'; private $charset; private $loader; From 1170144f66d5c11123668d52f64ae46a2a287d8e Mon Sep 17 00:00:00 2001 From: Francesco Sardara Date: Wed, 17 Apr 2024 18:07:44 +0200 Subject: [PATCH 201/812] Add twig_escape_filter_is_safe() as deprecated. --- src/Resources/escaper.php | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/Resources/escaper.php b/src/Resources/escaper.php index ea377803504..bc93331a630 100644 --- a/src/Resources/escaper.php +++ b/src/Resources/escaper.php @@ -11,9 +11,11 @@ use Twig\Environment; use Twig\Extension\EscaperExtension; +use Twig\Node\Node; /** * @internal + * * @deprecated since Twig 3.9 */ function twig_raw_filter($string) @@ -25,6 +27,7 @@ function twig_raw_filter($string) /** * @internal + * * @deprecated since Twig 3.9 */ function twig_escape_filter(Environment $env, $string, $strategy = 'html', $charset = null, $autoescape = false) @@ -33,3 +36,15 @@ function twig_escape_filter(Environment $env, $string, $strategy = 'html', $char return EscaperExtension::escape($env, $string, $strategy, $charset, $autoescape); } + +/** + * @internal + * + * @deprecated since Twig 3.9 + */ +function twig_escape_filter_is_safe(Node $filterArgs) +{ + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + return EscaperExtension::escapeFilterIsSafe($filterArgs); +} From f7121a23bcce8d1b9aec1e5304373dcecbe3ff7e Mon Sep 17 00:00:00 2001 From: Gildas de Cadoudal Date: Thu, 18 Apr 2024 09:45:09 +0200 Subject: [PATCH 202/812] fix: #4029 when use_yield is true CaptureNode use iterator_to_array preserveKeys argument to false --- src/Node/CaptureNode.php | 4 +++- .../regression/4029-iterator_to_array.test | 14 ++++++++++++++ tests/Node/MacroTest.php | 2 +- tests/Node/SetTest.php | 2 +- 4 files changed, 19 insertions(+), 3 deletions(-) create mode 100644 tests/Fixtures/regression/4029-iterator_to_array.test diff --git a/src/Node/CaptureNode.php b/src/Node/CaptureNode.php index 561e1ea53c8..7c187727ade 100644 --- a/src/Node/CaptureNode.php +++ b/src/Node/CaptureNode.php @@ -44,9 +44,11 @@ public function compile(Compiler $compiler): void ->indent() ->subcompile($this->getNode('body')) ->outdent() - ->write("})() ?? new \EmptyIterator())") + ->write("})() ?? new \EmptyIterator()") ; if ($useYield) { + $compiler->raw(', false))'); + } else { $compiler->raw(')'); } if (!$this->getAttribute('raw')) { diff --git a/tests/Fixtures/regression/4029-iterator_to_array.test b/tests/Fixtures/regression/4029-iterator_to_array.test new file mode 100644 index 00000000000..99afd892f50 --- /dev/null +++ b/tests/Fixtures/regression/4029-iterator_to_array.test @@ -0,0 +1,14 @@ +--TEST-- +#4029 When use_yield is true, CaptureNode fall in iterator_to_array pitfall regarding index overwrite +--TEMPLATE-- +{%- set tmp -%} + {%- block foo 'foo' -%} + {%- block bar 'bar' -%} +{%- endset -%} +{{ tmp }} +--DATA-- +return [] +--CONFIG-- +return ['use_yield' => true] +--EXPECT-- +foobar \ No newline at end of file diff --git a/tests/Node/MacroTest.php b/tests/Node/MacroTest.php index 09d7ee6ca7a..88ce9b299e2 100644 --- a/tests/Node/MacroTest.php +++ b/tests/Node/MacroTest.php @@ -60,7 +60,7 @@ public function macro_foo(\$__foo__ = null, \$__bar__ = "Foo", ...\$__varargs__) return new Markup(implode('', iterator_to_array((function () use (\$context, \$macros, \$blocks) { yield "foo"; - })() ?? new \EmptyIterator())), \$this->env->getCharset()); + })() ?? new \EmptyIterator(), false)), \$this->env->getCharset()); } EOF , new Environment(new ArrayLoader()), diff --git a/tests/Node/SetTest.php b/tests/Node/SetTest.php index 57f66cb9d61..ed6ee9ebe19 100644 --- a/tests/Node/SetTest.php +++ b/tests/Node/SetTest.php @@ -57,7 +57,7 @@ public function getTests() // line 1 \$context["foo"] = ('' === \$tmp = implode('', iterator_to_array((function () use (&\$context, \$macros, \$blocks) { yield "foo"; -})() ?? new \EmptyIterator()))) ? '' : new Markup(\$tmp, \$this->env->getCharset()); +})() ?? new \EmptyIterator(), false))) ? '' : new Markup(\$tmp, \$this->env->getCharset()); EOF , new Environment(new ArrayLoader()), ]; From b61a4224933449786d999f3e3aa6921102bcf456 Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Thu, 18 Apr 2024 11:24:21 +0200 Subject: [PATCH 203/812] change extended DI extension class --- .../DependencyInjection/TwigExtraExtension.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extra/twig-extra-bundle/DependencyInjection/TwigExtraExtension.php b/extra/twig-extra-bundle/DependencyInjection/TwigExtraExtension.php index f8ab840e450..501927e21ae 100644 --- a/extra/twig-extra-bundle/DependencyInjection/TwigExtraExtension.php +++ b/extra/twig-extra-bundle/DependencyInjection/TwigExtraExtension.php @@ -14,8 +14,8 @@ use League\CommonMark\CommonMarkConverter; use Symfony\Component\Config\FileLocator; use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Extension\Extension; use Symfony\Component\DependencyInjection\Loader\PhpFileLoader; -use Symfony\Component\HttpKernel\DependencyInjection\Extension; use Twig\Extra\TwigExtraBundle\Extensions; /** From 6c6315263e9f7041f78ce0c169ed33b0859d6b04 Mon Sep 17 00:00:00 2001 From: Gildas de Cadoudal Date: Thu, 18 Apr 2024 11:47:39 +0200 Subject: [PATCH 204/812] fix: #4033 add missing unwrap call when a TemplateWrapper instance can be present --- src/Node/ModuleNode.php | 4 ++-- src/Template.php | 2 +- .../regression/4033-missing-unwrap.test | 19 +++++++++++++++++++ 3 files changed, 22 insertions(+), 3 deletions(-) create mode 100644 tests/Fixtures/regression/4033-missing-unwrap.test diff --git a/src/Node/ModuleNode.php b/src/Node/ModuleNode.php index df407caf0a9..df5d78d0305 100644 --- a/src/Node/ModuleNode.php +++ b/src/Node/ModuleNode.php @@ -198,7 +198,7 @@ protected function compileConstructor(Compiler $compiler) ->raw(', ') ->repr($node->getTemplateLine()) ->raw(");\n") - ->write(sprintf("if (!\$_trait_%s->isTraitable()) {\n", $i)) + ->write(sprintf("if (!\$_trait_%s->unwrap()->isTraitable()) {\n", $i)) ->indent() ->write("throw new RuntimeError('Template \"'.") ->subcompile($trait->getNode('template')) @@ -207,7 +207,7 @@ protected function compileConstructor(Compiler $compiler) ->raw(", \$this->source);\n") ->outdent() ->write("}\n") - ->write(sprintf("\$_trait_%s_blocks = \$_trait_%s->getBlocks();\n\n", $i, $i)) + ->write(sprintf("\$_trait_%s_blocks = \$_trait_%s->unwrap()->getBlocks();\n\n", $i, $i)) ; foreach ($trait->getNode('targets') as $key => $value) { diff --git a/src/Template.php b/src/Template.php index f200a610fd1..e08837737da 100644 --- a/src/Template.php +++ b/src/Template.php @@ -464,7 +464,7 @@ public function yieldBlock($name, array $context, array $blocks = [], $useBlocks } } } elseif ($parent = $this->getParent($context)) { - yield from $parent->yieldBlock($name, $context, array_merge($this->blocks, $blocks), false, $templateContext ?? $this); + yield from $parent->unwrap()->yieldBlock($name, $context, array_merge($this->blocks, $blocks), false, $templateContext ?? $this); } elseif (isset($blocks[$name])) { throw new RuntimeError(sprintf('Block "%s" should not call parent() in "%s" as the block does not exist in the parent template "%s".', $name, $blocks[$name][0]->getTemplateName(), $this->getTemplateName()), -1, $blocks[$name][0]->getSourceContext()); } else { diff --git a/tests/Fixtures/regression/4033-missing-unwrap.test b/tests/Fixtures/regression/4033-missing-unwrap.test new file mode 100644 index 00000000000..37778731c66 --- /dev/null +++ b/tests/Fixtures/regression/4033-missing-unwrap.test @@ -0,0 +1,19 @@ +--TEST-- +Call to undefined method Twig\\TemplateWrapper::yieldBlock() +--TEMPLATE-- +{% extends 'parent' %} +{%- block content -%} + {{ parent() }} + child +{%- endblock -%} +--TEMPLATE(parent)-- +{% extends ['unknowngrandparent', 'grandparent'] %} +--TEMPLATE(grandparent)-- +{%- block content -%} + grandparent +{%- endblock -%} +--DATA-- +return [] +--EXPECT-- + grandparent + child \ No newline at end of file From 5a79652e7c572fce25be2aa926a587fb7281d83b Mon Sep 17 00:00:00 2001 From: Daniel Gorgan Date: Thu, 18 Apr 2024 13:17:11 +0300 Subject: [PATCH 205/812] Ensure Lexer:: is always initialized --- src/Lexer.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Lexer.php b/src/Lexer.php index 9e4d6119eb7..4d7cffdd12b 100644 --- a/src/Lexer.php +++ b/src/Lexer.php @@ -71,8 +71,6 @@ private function initialize() return; } - $this->isInitialized = true; - // when PHP 7.3 is the min version, we will be able to remove the '#' part in preg_quote as it's part of the default $this->regexes = [ // }} @@ -160,6 +158,8 @@ private function initialize() 'interpolation_start' => '{'.preg_quote($this->options['interpolation'][0], '#').'\s*}A', 'interpolation_end' => '{\s*'.preg_quote($this->options['interpolation'][1], '#').'}A', ]; + + $this->isInitialized = true; } public function tokenize(Source $source): TokenStream From cb307d7feae950e87c2f351fbe65435bca82175b Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Thu, 18 Apr 2024 07:59:01 -0400 Subject: [PATCH 206/812] Update CHANGELOG --- CHANGELOG | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 95034696869..24cbcd0223d 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,9 @@ # 3.9.3 (2024-XX-XX) - * n/a + * Add missing `twig_escape_filter_is_safe` deprecated function + * Fix yield usage with CaptureNode + * Add missing unwrap call when using a TemplateWrapper instance internally + * Ensure Lexer is initialized early on # 3.9.2 (2024-04-17) From a842d75fed59cdbcbd3a3ad7fb9eb768fc350d58 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Thu, 18 Apr 2024 07:59:33 -0400 Subject: [PATCH 207/812] Prepare the 3.9.3 release --- CHANGELOG | 2 +- src/Environment.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 24cbcd0223d..76d6969a931 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,4 +1,4 @@ -# 3.9.3 (2024-XX-XX) +# 3.9.3 (2024-04-18) * Add missing `twig_escape_filter_is_safe` deprecated function * Fix yield usage with CaptureNode diff --git a/src/Environment.php b/src/Environment.php index c761bdb416c..9fbfe5a2d9e 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -41,12 +41,12 @@ */ class Environment { - public const VERSION = '3.9.3-DEV'; + public const VERSION = '3.9.3'; public const VERSION_ID = 30903; public const MAJOR_VERSION = 3; public const MINOR_VERSION = 9; public const RELEASE_VERSION = 3; - public const EXTRA_VERSION = 'DEV'; + public const EXTRA_VERSION = ''; private $charset; private $loader; From ad934312cd08b6b466932fdfdd5b97ae680bc28a Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Thu, 18 Apr 2024 08:00:10 -0400 Subject: [PATCH 208/812] Bump version --- CHANGELOG | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index 76d6969a931..185c8d214ec 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +# 3.9.4 (2024-XX-XX) + + * n/a + # 3.9.3 (2024-04-18) * Add missing `twig_escape_filter_is_safe` deprecated function From b212f1bb2f037db9f96bfd03a2580eddb46540db Mon Sep 17 00:00:00 2001 From: Kevin Bond Date: Thu, 18 Apr 2024 16:54:09 -0400 Subject: [PATCH 209/812] Auto-close PRs on subtree-splits --- .github/sync-packages.php | 71 +++++++++++++++++++ .github/workflows/package-tests.yml | 26 +++++++ extra/cache-extra/.gitattributes | 1 + .../.github/PULL_REQUEST_TEMPLATE.md | 8 +++ .../.github/workflows/check-subtree-split.yml | 33 +++++++++ extra/cssinliner-extra/.gitattributes | 1 + .../.github/PULL_REQUEST_TEMPLATE.md | 8 +++ .../.github/workflows/check-subtree-split.yml | 33 +++++++++ extra/html-extra/.gitattributes | 1 + .../.github/PULL_REQUEST_TEMPLATE.md | 8 +++ .../.github/workflows/check-subtree-split.yml | 33 +++++++++ extra/inky-extra/.gitattributes | 1 + .../.github/PULL_REQUEST_TEMPLATE.md | 8 +++ .../.github/workflows/check-subtree-split.yml | 33 +++++++++ extra/intl-extra/.gitattributes | 1 + .../.github/PULL_REQUEST_TEMPLATE.md | 8 +++ .../.github/workflows/check-subtree-split.yml | 33 +++++++++ extra/markdown-extra/.gitattributes | 1 + .../.github/PULL_REQUEST_TEMPLATE.md | 8 +++ .../.github/workflows/check-subtree-split.yml | 33 +++++++++ extra/string-extra/.gitattributes | 1 + .../.github/PULL_REQUEST_TEMPLATE.md | 8 +++ .../.github/workflows/check-subtree-split.yml | 33 +++++++++ extra/twig-extra-bundle/.gitattributes | 1 + .../.github/PULL_REQUEST_TEMPLATE.md | 8 +++ .../.github/workflows/check-subtree-split.yml | 33 +++++++++ 26 files changed, 433 insertions(+) create mode 100644 .github/sync-packages.php create mode 100644 .github/workflows/package-tests.yml create mode 100644 extra/cache-extra/.github/PULL_REQUEST_TEMPLATE.md create mode 100644 extra/cache-extra/.github/workflows/check-subtree-split.yml create mode 100644 extra/cssinliner-extra/.github/PULL_REQUEST_TEMPLATE.md create mode 100644 extra/cssinliner-extra/.github/workflows/check-subtree-split.yml create mode 100644 extra/html-extra/.github/PULL_REQUEST_TEMPLATE.md create mode 100644 extra/html-extra/.github/workflows/check-subtree-split.yml create mode 100644 extra/inky-extra/.github/PULL_REQUEST_TEMPLATE.md create mode 100644 extra/inky-extra/.github/workflows/check-subtree-split.yml create mode 100644 extra/intl-extra/.github/PULL_REQUEST_TEMPLATE.md create mode 100644 extra/intl-extra/.github/workflows/check-subtree-split.yml create mode 100644 extra/markdown-extra/.github/PULL_REQUEST_TEMPLATE.md create mode 100644 extra/markdown-extra/.github/workflows/check-subtree-split.yml create mode 100644 extra/string-extra/.github/PULL_REQUEST_TEMPLATE.md create mode 100644 extra/string-extra/.github/workflows/check-subtree-split.yml create mode 100644 extra/twig-extra-bundle/.github/PULL_REQUEST_TEMPLATE.md create mode 100644 extra/twig-extra-bundle/.github/workflows/check-subtree-split.yml diff --git a/.github/sync-packages.php b/.github/sync-packages.php new file mode 100644 index 00000000000..4c47843aa4e --- /dev/null +++ b/.github/sync-packages.php @@ -0,0 +1,71 @@ + Date: Fri, 19 Apr 2024 10:26:00 -0400 Subject: [PATCH 210/812] Use ::class everywhere --- src/Extension/CoreExtension.php | 2 +- tests/EnvironmentTest.php | 4 ++-- tests/Extension/EscaperTest.php | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index dbc5f61440d..c7c6d14fc50 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -1837,7 +1837,7 @@ public static function arrayEvery(Environment $env, $array, $arrow) */ public static function checkArrowInSandbox(Environment $env, $arrow, $thing, $type) { - if (!$arrow instanceof \Closure && $env->hasExtension('\Twig\Extension\SandboxExtension') && $env->getExtension('\Twig\Extension\SandboxExtension')->isSandboxed()) { + if (!$arrow instanceof \Closure && $env->hasExtension(SandboxExtension::class) && $env->getExtension(SandboxExtension::class)->isSandboxed()) { throw new RuntimeError(sprintf('The callable passed to the "%s" %s must be a Closure in sandbox mode.', $thing, $type)); } } diff --git a/tests/EnvironmentTest.php b/tests/EnvironmentTest.php index 688024b8e02..e7eb26e4c21 100644 --- a/tests/EnvironmentTest.php +++ b/tests/EnvironmentTest.php @@ -271,8 +271,8 @@ public function testHasGetExtensionByClassName() { $twig = new Environment($this->createMock(LoaderInterface::class)); $twig->addExtension($ext = new EnvironmentTest_Extension()); - $this->assertSame($ext, $twig->getExtension('Twig\Tests\EnvironmentTest_Extension')); - $this->assertSame($ext, $twig->getExtension('\Twig\Tests\EnvironmentTest_Extension')); + $this->assertSame($ext, $twig->getExtension(EnvironmentTest_Extension::class)); + $this->assertSame($ext, $twig->getExtension(EnvironmentTest_Extension::class)); } public function testAddExtension() diff --git a/tests/Extension/EscaperTest.php b/tests/Extension/EscaperTest.php index 554a7c8fb7c..c4657891532 100644 --- a/tests/Extension/EscaperTest.php +++ b/tests/Extension/EscaperTest.php @@ -400,7 +400,7 @@ public function testObjectEscaping(string $escapedHtml, string $escapedJs, array { $obj = new Extension_TestClass(); $twig = new Environment($this->createMock(LoaderInterface::class)); - $twig->getExtension('\Twig\Extension\EscaperExtension')->setSafeClasses($safeClasses); + $twig->getExtension(EscaperExtension::class)->setSafeClasses($safeClasses); $this->assertSame($escapedHtml, EscaperExtension::escape($twig, $obj, 'html', null, true)); $this->assertSame($escapedJs, EscaperExtension::escape($twig, $obj, 'js', null, true)); } From ccc20cbd1b12228a1c1eca3d7d0e6eacab75c560 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Wed, 24 Apr 2024 08:00:09 +0200 Subject: [PATCH 211/812] Fix a warning --- src/Lexer.php | 2 +- tests/LexerTest.php | 23 +++++++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/src/Lexer.php b/src/Lexer.php index 4d7cffdd12b..e15e896f5cf 100644 --- a/src/Lexer.php +++ b/src/Lexer.php @@ -321,7 +321,7 @@ private function lexExpression(): void $this->moveCursor('...'); } // arrow function - elseif ('=' === $this->code[$this->cursor] && '>' === $this->code[$this->cursor + 1]) { + elseif ('=' === $this->code[$this->cursor] && ($this->cursor + 1 < $this->end) && '>' === $this->code[$this->cursor + 1]) { $this->pushToken(Token::ARROW_TYPE, '=>'); $this->moveCursor('=>'); } diff --git a/tests/LexerTest.php b/tests/LexerTest.php index ad62c22acfb..2aad47ac9b3 100644 --- a/tests/LexerTest.php +++ b/tests/LexerTest.php @@ -378,4 +378,27 @@ public function testOverridingSyntax() // can be executed without throwing any exceptions $this->addToAssertionCount(1); } + + /** + * @dataProvider getTemplateForErrorsAtTheEndOfTheStream + */ + public function testErrorsAtTheEndOfTheStream(string $template) + { + $lexer = new Lexer(new Environment($this->createMock(LoaderInterface::class))); + set_error_handler(function () { + $this->fail('Lexer should not emit warnings.'); + }); + try { + $lexer->tokenize(new Source($template, 'index')); + $this->addToAssertionCount(1); + } finally { + restore_error_handler(); + } + } + + public function getTemplateForErrorsAtTheEndOfTheStream() + { + yield ['{{ =']; + yield ['{{ ..']; + } } From 4a7de4ab5603ad8f199222cae3951fc271a8c45d Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Wed, 24 Apr 2024 08:21:14 +0200 Subject: [PATCH 212/812] Bump version to 3.9.4-DEV --- src/Environment.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Environment.php b/src/Environment.php index 9fbfe5a2d9e..37e4ec845d2 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -41,12 +41,12 @@ */ class Environment { - public const VERSION = '3.9.3'; - public const VERSION_ID = 30903; + public const VERSION = '3.9.4-DEV'; + public const VERSION_ID = 30904; public const MAJOR_VERSION = 3; public const MINOR_VERSION = 9; - public const RELEASE_VERSION = 3; - public const EXTRA_VERSION = ''; + public const RELEASE_VERSION = 4; + public const EXTRA_VERSION = 'DEV'; private $charset; private $loader; From 350670a32109e21e05d4e773546edf1c5626491d Mon Sep 17 00:00:00 2001 From: Daniel Penning Date: Wed, 24 Apr 2024 17:30:04 +0200 Subject: [PATCH 213/812] synchronize sourceContext --- src/Node/Node.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Node/Node.php b/src/Node/Node.php index 16591fcf5e5..4ac94f1bc9b 100644 --- a/src/Node/Node.php +++ b/src/Node/Node.php @@ -138,6 +138,9 @@ public function getNode(string $name): self public function setNode(string $name, self $node): void { + if (null !== $this->sourceContext) { + $node->setSourceContext($this->sourceContext); + } $this->nodes[$name] = $node; } From 609620e83ed8cab0c2138aef54d8e0c50442c832 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Sat, 27 Apr 2024 21:31:11 +0200 Subject: [PATCH 214/812] Respect coding standard in documentation --- doc/filters/first.rst | 2 +- doc/filters/last.rst | 2 +- doc/filters/merge.rst | 12 ++++++------ doc/filters/sort.rst | 6 +++--- doc/tags/with.rst | 6 +++--- doc/templates.rst | 14 +++++++------- 6 files changed, 21 insertions(+), 21 deletions(-) diff --git a/doc/filters/first.rst b/doc/filters/first.rst index e0cc7cb1c9e..0d9ba9c4957 100644 --- a/doc/filters/first.rst +++ b/doc/filters/first.rst @@ -9,7 +9,7 @@ a string: {{ [1, 2, 3, 4]|first }} {# outputs 1 #} - {{ { a: 1, b: 2, c: 3, d: 4 }|first }} + {{ {a: 1, b: 2, c: 3, d: 4}|first }} {# outputs 1 #} {{ '1234'|first }} diff --git a/doc/filters/last.rst b/doc/filters/last.rst index d7ac6a533be..865e57cc117 100644 --- a/doc/filters/last.rst +++ b/doc/filters/last.rst @@ -9,7 +9,7 @@ a string: {{ [1, 2, 3, 4]|last }} {# outputs 4 #} - {{ { a: 1, b: 2, c: 3, d: 4 }|last }} + {{ {a: 1, b: 2, c: 3, d: 4}|last }} {# outputs 4 #} {{ '1234'|last }} diff --git a/doc/filters/merge.rst b/doc/filters/merge.rst index 40146b5d7e5..b1d75c40b62 100644 --- a/doc/filters/merge.rst +++ b/doc/filters/merge.rst @@ -17,11 +17,11 @@ The ``merge`` filter also works on hashes: .. code-block:: twig - {% set items = { 'apple': 'fruit', 'orange': 'fruit', 'peugeot': 'unknown' } %} + {% set items = {'apple': 'fruit', 'orange': 'fruit', 'peugeot': 'unknown'} %} {% set items = items|merge({ 'peugeot': 'car', 'renault': 'car' }) %} - {# items now contains { 'apple': 'fruit', 'orange': 'fruit', 'peugeot': 'car', 'renault': 'car' } #} + {# items now contains {'apple': 'fruit', 'orange': 'fruit', 'peugeot': 'car', 'renault': 'car'} #} For hashes, the merging process occurs on the keys: if the key does not already exist, it is added but if the key already exists, its value is @@ -34,12 +34,12 @@ overridden. .. code-block:: twig - {% set items = { 'apple': 'fruit', 'orange': 'fruit' } %} + {% set items = {'apple': 'fruit', 'orange': 'fruit'} %} - {% set items = { 'apple': 'unknown' }|merge(items) %} + {% set items = {'apple': 'unknown'}|merge(items) %} + + {# items now contains {'apple': 'fruit', 'orange': 'fruit'} #} - {# items now contains { 'apple': 'fruit', 'orange': 'fruit' } #} - .. note:: Internally, Twig uses the PHP `array_merge`_ function. It supports diff --git a/doc/filters/sort.rst b/doc/filters/sort.rst index 1816c35e6fc..98b7afe376d 100644 --- a/doc/filters/sort.rst +++ b/doc/filters/sort.rst @@ -20,9 +20,9 @@ You can pass an arrow function to sort the array: .. code-block:: html+twig {% set fruits = [ - { name: 'Apples', quantity: 5 }, - { name: 'Oranges', quantity: 2 }, - { name: 'Grapes', quantity: 4 }, + {name: 'Apples', quantity: 5}, + {name: 'Oranges', quantity: 2}, + {name: 'Grapes', quantity: 4}, ] %} {% for fruit in fruits|sort((a, b) => a.quantity <=> b.quantity)|column('name') %} diff --git a/doc/tags/with.rst b/doc/tags/with.rst index 107432f6fc8..420c82ac6c1 100644 --- a/doc/tags/with.rst +++ b/doc/tags/with.rst @@ -18,13 +18,13 @@ is equivalent to the following one: .. code-block:: twig - {% with { foo: 42 } %} + {% with {foo: 42} %} {{ foo }} {# foo is 42 here #} {% endwith %} foo is not visible here any longer {# it works with any expression that resolves to a hash #} - {% set vars = { foo: 42 } %} + {% set vars = {foo: 42} %} {% with vars %} ... {% endwith %} @@ -35,7 +35,7 @@ disable this behavior by appending the ``only`` keyword: .. code-block:: twig {% set bar = 'bar' %} - {% with { foo: 42 } only %} + {% with {foo: 42} only %} {# only foo is defined #} {# bar is not defined #} {% endwith %} diff --git a/doc/templates.rst b/doc/templates.rst index 0486c52e3c7..530b94ef3c2 100644 --- a/doc/templates.rst +++ b/doc/templates.rst @@ -560,22 +560,22 @@ exist: .. code-block:: twig {# keys as string #} - { 'foo': 'foo', 'bar': 'bar' } + {'foo': 'foo', 'bar': 'bar'} {# keys as names (equivalent to the previous hash) #} - { foo: 'foo', bar: 'bar' } + {foo: 'foo', bar: 'bar'} {# keys as integer #} - { 2: 'foo', 4: 'bar' } + {2: 'foo', 4: 'bar'} {# keys can be omitted if it is the same as the variable name #} - { foo } + {foo} {# is equivalent to the following #} - { 'foo': foo } + {'foo': foo} {# keys as expressions (the expression must be enclosed into parentheses) #} {% set foo = 'foo' %} - { (foo): 'foo', (1 + 1): 'bar', (foo ~ 'b'): 'baz' } + {(foo): 'foo', (1 + 1): 'bar', (foo ~ 'b'): 'baz'} * ``true`` / ``false``: ``true`` represents the true value, ``false`` represents the false value. @@ -797,7 +797,7 @@ The following operators don't fit into any of the other categories: .. code-block:: twig {% set numbers = [1, 2, ...moreNumbers] %} - {% set ratings = { 'foo': 10, 'bar': 5, ...moreRatings } %} + {% set ratings = {'foo': 10, 'bar': 5, ...moreRatings} %} .. _templates-string-interpolation: From 01e0b7ce4281529078adf5e20a78a48b8ae55831 Mon Sep 17 00:00:00 2001 From: lacpandore Date: Thu, 29 Feb 2024 18:08:57 +0100 Subject: [PATCH 215/812] [Doc] Correct Order & Explanation on precedence --- doc/templates.rst | 39 +++++++++++++++++++++++++++++++++------ 1 file changed, 33 insertions(+), 6 deletions(-) diff --git a/doc/templates.rst b/doc/templates.rst index 0486c52e3c7..c80285e90fc 100644 --- a/doc/templates.rst +++ b/doc/templates.rst @@ -514,13 +514,40 @@ Twig allows expressions everywhere. .. note:: + Twig uses operators to perform various operations within templates. Understanding + the precedence of these operators is crucial for writing correct and efficient Twig templates. + The operator precedence is as follows, with the lowest-precedence operators - listed first: ``?:`` (ternary operator), ``b-and``, ``b-xor``, ``b-or``, - ``or``, ``and``, ``==``, ``!=``, ``<=>``, ``<``, ``>``, ``>=``, ``<=``, - ``in``, ``matches``, ``starts with``, ``ends with``, ``has every``, ``has - some``, ``..``, ``+``, ``-``, - ``~``, ``*``, ``/``, ``//``, ``%``, ``is`` (tests), ``**``, ``??``, ``|`` - (filters), ``[]``, and ``.``: + listed first: + + ============================= =================================== =================================================== + Operator Score of precedence Description + ============================= =================================== =================================================== + ``?:`` Perfoms a ternary, conditional statement. + ``or`` 10 Performs a logical OR operation between two + boolean expressions. + ``and`` 15 Performs a logical AND operation between two + boolean expressions. + ``b-or`` 16 Performs a bitwise OR operation on integers. + ``b-xor`` 17 Performs a bitwise XOR operation on integers. + ``b-and`` 18 Performs a bitwise AND operation on integers + ``==``, ``!=``, ``<=>``, 20 Comparison Operators: Compare values and check + ``<``, ``>``, ``>=``, for containment, pattern matching, etc. + ``<=``, ``not in``, ``in``, + ``matches``, ``starts with``, + ``ends with``, ``has some``, + ``has every`` + ``..`` 25 Creates a range of values, commonly used in loops. + ``+``, ``-`` 30 Performs operations on numbers. + ``~`` 40 Concatenates strings together. + ``*``, ``/``, ``//``, ``%`` 60 Handles arithmetic operations on numbers. + ``is``, ``is not`` 100 Tests + ``**`` 200 Raises a number to the power of another. + ``??`` 300 Handles cases where a variable might be null. + ``|``,``[]``,``.`` Filters are evaluated first + ============================= =================================== =================================================== + + This means that ``{{ 6 b-and 2 or 6 b-and 16 }}`` results in ``(6 & 2) || (6 & 16)``. .. code-block:: twig From e8df2b721737b5a87fe23bda7da872fb61803ba2 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sun, 28 Apr 2024 10:47:44 +0200 Subject: [PATCH 216/812] Tweak docs about operators --- doc/templates.rst | 122 +++++++++++++++++++++++++--------------------- 1 file changed, 67 insertions(+), 55 deletions(-) diff --git a/doc/templates.rst b/doc/templates.rst index 8b36ff87ec5..5ec7eb7f786 100644 --- a/doc/templates.rst +++ b/doc/templates.rst @@ -512,53 +512,6 @@ Expressions Twig allows expressions everywhere. -.. note:: - - Twig uses operators to perform various operations within templates. Understanding - the precedence of these operators is crucial for writing correct and efficient Twig templates. - - The operator precedence is as follows, with the lowest-precedence operators - listed first: - - ============================= =================================== =================================================== - Operator Score of precedence Description - ============================= =================================== =================================================== - ``?:`` Perfoms a ternary, conditional statement. - ``or`` 10 Performs a logical OR operation between two - boolean expressions. - ``and`` 15 Performs a logical AND operation between two - boolean expressions. - ``b-or`` 16 Performs a bitwise OR operation on integers. - ``b-xor`` 17 Performs a bitwise XOR operation on integers. - ``b-and`` 18 Performs a bitwise AND operation on integers - ``==``, ``!=``, ``<=>``, 20 Comparison Operators: Compare values and check - ``<``, ``>``, ``>=``, for containment, pattern matching, etc. - ``<=``, ``not in``, ``in``, - ``matches``, ``starts with``, - ``ends with``, ``has some``, - ``has every`` - ``..`` 25 Creates a range of values, commonly used in loops. - ``+``, ``-`` 30 Performs operations on numbers. - ``~`` 40 Concatenates strings together. - ``*``, ``/``, ``//``, ``%`` 60 Handles arithmetic operations on numbers. - ``is``, ``is not`` 100 Tests - ``**`` 200 Raises a number to the power of another. - ``??`` 300 Handles cases where a variable might be null. - ``|``,``[]``,``.`` Filters are evaluated first - ============================= =================================== =================================================== - - This means that ``{{ 6 b-and 2 or 6 b-and 16 }}`` results in ``(6 & 2) || (6 & 16)``. - - .. code-block:: twig - - {% set greeting = 'Hello ' %} - {% set name = 'Fabien' %} - - {{ greeting ~ name|lower }} {# Hello fabien #} - - {# use parenthesis to change precedence #} - {{ (greeting ~ name)|lower }} {# hello fabien #} - Literals ~~~~~~~~ @@ -622,6 +575,20 @@ Arrays and hashes can be nested: but :ref:`string interpolation ` is only supported in double-quoted strings. +.. _templates-string-interpolation: + +String Interpolation +~~~~~~~~~~~~~~~~~~~~ + +String interpolation (``#{expression}``) allows any valid expression to appear +within a *double-quoted string*. The result of evaluating that expression is +inserted into the string: + +.. code-block:: twig + + {{ "foo #{bar} baz" }} + {{ "foo #{1 + 2} baz" }} + Math ~~~~ @@ -826,19 +793,64 @@ The following operators don't fit into any of the other categories: {% set numbers = [1, 2, ...moreNumbers] %} {% set ratings = {'foo': 10, 'bar': 5, ...moreRatings} %} -.. _templates-string-interpolation: -String Interpolation -~~~~~~~~~~~~~~~~~~~~ +Operators +~~~~~~~~~ + +Twig uses operators to perform various operations within templates. +Understanding the precedence of these operators is crucial for writing correct +and efficient Twig templates. + +The operator precedence rules are as follows, with the lowest-precedence +operators listed first: + +============================= =================================== ===================================================== +Operator Score of precedence Description +============================= =================================== ===================================================== +``?:`` 0 Ternary operator, conditional statement +``or`` 10 Logical OR operation between two boolean expressions +``and`` 15 Logical AND operation between two boolean expressions +``b-or`` 16 Bitwise OR operation on integers +``b-xor`` 17 Bitwise XOR operation on integers +``b-and`` 18 Bitwise AND operation on integers +``==``, ``!=``, ``<=>``, 20 Comparison operators +``<``, ``>``, ``>=``, +``<=``, ``not in``, ``in``, +``matches``, ``starts with``, +``ends with``, ``has some``, +``has every`` +``..`` 25 Range of values +``+``, ``-`` 30 Addition and substraction on numbers +``~`` 40 String concatenation +``not`` 50 Negates a statement +``*``, ``/``, ``//``, ``%`` 60 Arithmetic operations on numbers +``is``, ``is not`` 100 Tests +``**`` 200 Raises a number to the power of another +``??`` 300 Default value when a variable is null +``+``, ``-`` 500 Unary operations on numbers +``|``,``[]``,``.`` - Filters, array, hash, and attribute access +============================= =================================== ===================================================== + +Without using any parentheses, the operator precedence rules are used to +determine how to convert the code to PHP: -String interpolation (``#{expression}``) allows any valid expression to appear -within a *double-quoted string*. The result of evaluating that expression is -inserted into the string: +.. code-block:: twig + + {{ 6 b-and 2 or 6 b-and 16 }} + + {# it is converted to the following PHP code: (6 & 2) || (6 & 16) #} + +Change the default precedence by explicitely grouping expressions with parentheses: .. code-block:: twig - {{ "foo #{bar} baz" }} - {{ "foo #{1 + 2} baz" }} + {% set greeting = 'Hello ' %} + {% set name = 'Fabien' %} + + {{ greeting ~ name|lower }} {# Hello fabien #} + + {# use parenthesis to change precedence #} + {{ (greeting ~ name)|lower }} {# hello fabien #} .. _templates-whitespace-control: From 5148d10515f5361e8f0aa8a178567b13146c40ae Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sun, 28 Apr 2024 13:12:05 +0200 Subject: [PATCH 217/812] Fix some inconsistencies --- tests/Extension/EscaperTest.php | 2 +- tests/Fixtures/functions/include/template_instance.test | 2 +- .../tags/inheritance/parent_as_template_wrapper.test | 2 +- tests/Fixtures/tags/inheritance/template_instance.test | 2 +- tests/Node/Expression/CallTest.php | 6 +++--- tests/Util/DeprecationCollectorTest.php | 4 ++-- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/tests/Extension/EscaperTest.php b/tests/Extension/EscaperTest.php index c4657891532..d8fedc64e29 100644 --- a/tests/Extension/EscaperTest.php +++ b/tests/Extension/EscaperTest.php @@ -17,7 +17,7 @@ use Twig\Extension\EscaperExtension; use Twig\Loader\LoaderInterface; -class Twig_Tests_Extension_EscaperTest extends TestCase +class EscaperTest extends TestCase { /** * All character encodings supported by htmlspecialchars(). diff --git a/tests/Fixtures/functions/include/template_instance.test b/tests/Fixtures/functions/include/template_instance.test index 4c8b450835c..be18d244ac0 100644 --- a/tests/Fixtures/functions/include/template_instance.test +++ b/tests/Fixtures/functions/include/template_instance.test @@ -1,5 +1,5 @@ --TEST-- -"include" function accepts Twig_Template instance +"include" function accepts Twig\Template instance --TEMPLATE-- {{ include(foo) }} FOO --TEMPLATE(foo.twig)-- diff --git a/tests/Fixtures/tags/inheritance/parent_as_template_wrapper.test b/tests/Fixtures/tags/inheritance/parent_as_template_wrapper.test index 1aaed556c57..cf257f25dc7 100644 --- a/tests/Fixtures/tags/inheritance/parent_as_template_wrapper.test +++ b/tests/Fixtures/tags/inheritance/parent_as_template_wrapper.test @@ -1,5 +1,5 @@ --TEST-- -"extends" tag with a parent as a Twig_TemplateWrapper instance +"extends" tag with a parent as a Twig\TemplateWrapper instance --TEMPLATE-- {% extends foo %} diff --git a/tests/Fixtures/tags/inheritance/template_instance.test b/tests/Fixtures/tags/inheritance/template_instance.test index a5a223886dc..b9009e5df53 100644 --- a/tests/Fixtures/tags/inheritance/template_instance.test +++ b/tests/Fixtures/tags/inheritance/template_instance.test @@ -1,5 +1,5 @@ --TEST-- -"extends" tag accepts Twig_Template instance +"extends" tag accepts Twig\Template instance --TEMPLATE-- {% extends foo %} diff --git a/tests/Node/Expression/CallTest.php b/tests/Node/Expression/CallTest.php index 10fa00bea6e..47051207644 100644 --- a/tests/Node/Expression/CallTest.php +++ b/tests/Node/Expression/CallTest.php @@ -125,10 +125,10 @@ public function customFunctionWithArbitraryArguments() public function testResolveArgumentsWithMissingParameterForArbitraryArgumentsOnFunction() { $this->expectException(\LogicException::class); - $this->expectExceptionMessageMatches('#^The last parameter of "Twig\\\\Tests\\\\Node\\\\Expression\\\\custom_Twig_Tests_Node_Expression_CallTest_function" for function "foo" must be an array with default value, eg\\. "array \\$arg \\= \\[\\]"\\.$#'); + $this->expectExceptionMessageMatches('#^The last parameter of "Twig\\\\Tests\\\\Node\\\\Expression\\\\custom_call_test_function" for function "foo" must be an array with default value, eg\\. "array \\$arg \\= \\[\\]"\\.$#'); $node = new Node_Expression_Call([], ['type' => 'function', 'name' => 'foo', 'is_variadic' => true]); - $node->getArguments('Twig\Tests\Node\Expression\custom_Twig_Tests_Node_Expression_CallTest_function', []); + $node->getArguments('Twig\Tests\Node\Expression\custom_call_test_function', []); } public function testResolveArgumentsWithMissingParameterForArbitraryArgumentsOnObject() @@ -156,6 +156,6 @@ public function __invoke($required) } } -function custom_Twig_Tests_Node_Expression_CallTest_function($required) +function custom_call_test_function($required) { } diff --git a/tests/Util/DeprecationCollectorTest.php b/tests/Util/DeprecationCollectorTest.php index 7b5794d83c4..18f889f6a42 100644 --- a/tests/Util/DeprecationCollectorTest.php +++ b/tests/Util/DeprecationCollectorTest.php @@ -28,7 +28,7 @@ public function testCollect() $twig->addFunction(new TwigFunction('deprec', [$this, 'deprec'], ['deprecated' => '1.1'])); $collector = new DeprecationCollector($twig); - $deprecations = $collector->collect(new Twig_Tests_Util_Iterator()); + $deprecations = $collector->collect(new Iterator()); $this->assertEquals(['Twig Function "deprec" is deprecated since version 1.1 in deprec.twig at line 1.'], $deprecations); } @@ -38,7 +38,7 @@ public function deprec() } } -class Twig_Tests_Util_Iterator implements \IteratorAggregate +class Iterator implements \IteratorAggregate { public function getIterator(): \Traversable { From 84bff0643c0b75b0493165e7cbc3d7b05b282bba Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Fri, 19 Apr 2024 17:40:57 -0400 Subject: [PATCH 218/812] Extract the escaping logic to a runtime --- CHANGELOG | 4 +- src/Environment.php | 20 +- src/Extension/EscaperExtension.php | 334 +++----------------- src/NodeVisitor/EscaperNodeVisitor.php | 3 +- src/Resources/escaper.php | 3 +- src/Runtime/EscaperRuntime.php | 370 ++++++++++++++++++++++ tests/Extension/EscaperTest.php | 403 ++---------------------- tests/IntegrationTest.php | 4 +- tests/Runtime/EscaperRuntimeTest.php | 411 +++++++++++++++++++++++++ 9 files changed, 863 insertions(+), 689 deletions(-) create mode 100644 src/Runtime/EscaperRuntime.php create mode 100644 tests/Runtime/EscaperRuntimeTest.php diff --git a/CHANGELOG b/CHANGELOG index 185c8d214ec..b3921e2dece 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,6 @@ -# 3.9.4 (2024-XX-XX) +# 3.10.0 (2024-XX-XX) - * n/a + * Extract the escaping logic to a runtime # 3.9.3 (2024-04-18) diff --git a/src/Environment.php b/src/Environment.php index 37e4ec845d2..5e0e0c2d255 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -31,6 +31,8 @@ use Twig\Node\ModuleNode; use Twig\Node\Node; use Twig\NodeVisitor\NodeVisitorInterface; +use Twig\Runtime\EscaperRuntime; +use Twig\RuntimeLoader\FactoryRuntimeLoader; use Twig\RuntimeLoader\RuntimeLoaderInterface; use Twig\TokenParser\TokenParserInterface; @@ -41,11 +43,11 @@ */ class Environment { - public const VERSION = '3.9.4-DEV'; - public const VERSION_ID = 30904; + public const VERSION = '3.10.0-DEV'; + public const VERSION_ID = 301000; public const MAJOR_VERSION = 3; - public const MINOR_VERSION = 9; - public const RELEASE_VERSION = 4; + public const MINOR_VERSION = 10; + public const RELEASE_VERSION = 0; public const EXTRA_VERSION = 'DEV'; private $charset; @@ -69,6 +71,7 @@ class Environment private $optionsHash; /** @var bool */ private $useYield; + private $defaultRuntimeLoader; /** * Constructor. @@ -127,9 +130,12 @@ public function __construct(LoaderInterface $loader, $options = []) $this->strictVariables = (bool) $options['strict_variables']; $this->setCache($options['cache']); $this->extensionSet = new ExtensionSet(); + $this->defaultRuntimeLoader = new FactoryRuntimeLoader([ + EscaperRuntime::class => fn () => new EscaperRuntime($options['autoescape'], $this->charset), + ]); $this->addExtension(new CoreExtension()); - $this->addExtension(new EscaperExtension($options['autoescape'])); + $this->addExtension(new EscaperExtension($this->getRuntime(EscaperRuntime::class))); if (\PHP_VERSION_ID >= 80000) { $this->addExtension(new YieldNotReadyExtension($this->useYield)); } @@ -620,6 +626,10 @@ public function getRuntime(string $class) } } + if (null !== $runtime = $this->defaultRuntimeLoader->load($class)) { + return $this->runtimes[$class] = $runtime; + } + throw new RuntimeError(sprintf('Unable to load the "%s" runtime.', $class)); } diff --git a/src/Extension/EscaperExtension.php b/src/Extension/EscaperExtension.php index dee0f79fe8c..7da6854d178 100644 --- a/src/Extension/EscaperExtension.php +++ b/src/Extension/EscaperExtension.php @@ -12,34 +12,21 @@ namespace Twig\Extension; use Twig\Environment; -use Twig\Error\RuntimeError; -use Twig\FileExtensionEscapingStrategy; -use Twig\Markup; use Twig\Node\Expression\ConstantExpression; use Twig\Node\Node; use Twig\NodeVisitor\EscaperNodeVisitor; +use Twig\Runtime\EscaperRuntime; use Twig\TokenParser\AutoEscapeTokenParser; use Twig\TwigFilter; final class EscaperExtension extends AbstractExtension { - private $defaultStrategy; - private $escapers = []; + private ?Environment $environment = null; + private array $escapers = []; - /** @internal */ - public $safeClasses = []; - - /** @internal */ - public $safeLookup = []; - - /** - * @param string|false|callable $defaultStrategy An escaping strategy - * - * @see setDefaultStrategy() - */ - public function __construct($defaultStrategy = 'html') - { - $this->setDefaultStrategy($defaultStrategy); + public function __construct( + private EscaperRuntime $escaper, + ) { } public function getTokenParsers(): array @@ -55,12 +42,19 @@ public function getNodeVisitors(): array public function getFilters(): array { return [ - new TwigFilter('escape', [self::class, 'escape'], ['needs_environment' => true, 'is_safe_callback' => [self::class, 'escapeFilterIsSafe']]), - new TwigFilter('e', [self::class, 'escape'], ['needs_environment' => true, 'is_safe_callback' => [self::class, 'escapeFilterIsSafe']]), + new TwigFilter('escape', [EscaperRuntime::class, 'escape'], ['is_safe_callback' => [self::class, 'escapeFilterIsSafe']]), + new TwigFilter('e', [EscaperRuntime::class, 'escape'], ['is_safe_callback' => [self::class, 'escapeFilterIsSafe']]), new TwigFilter('raw', [self::class, 'raw'], ['is_safe' => ['all']]), ]; } + public function setEnvironment(Environment $environment): void + { + trigger_deprecation('twig/twig', '3.10', 'The "%s()" method is deprecated and not needed if you are using methods from "Twig\Runtime\EscaperRuntime".', __METHOD__); + + $this->environment = $environment; + } + /** * Sets the default strategy to use when not defined by the user. * @@ -71,11 +65,9 @@ public function getFilters(): array */ public function setDefaultStrategy($defaultStrategy): void { - if ('name' === $defaultStrategy) { - $defaultStrategy = [FileExtensionEscapingStrategy::class, 'guess']; - } + trigger_deprecation('twig/twig', '3.10', 'The "%s()" method is deprecated, use the "Twig\Runtime\EscaperRuntime::setDefaultStrategy()" method instead.', __METHOD__); - $this->defaultStrategy = $defaultStrategy; + $this->escaper->setDefaultStrategy($defaultStrategy); } /** @@ -87,56 +79,57 @@ public function setDefaultStrategy($defaultStrategy): void */ public function getDefaultStrategy(string $name) { - // disable string callables to avoid calling a function named html or js, - // or any other upcoming escaping strategy - if (!\is_string($this->defaultStrategy) && false !== $this->defaultStrategy) { - return \call_user_func($this->defaultStrategy, $name); - } + trigger_deprecation('twig/twig', '3.10', 'The "%s()" method is deprecated, use the "Twig\Runtime\EscaperRuntime::getDefaultStrategy()" method instead.', __METHOD__); - return $this->defaultStrategy; + return $this->escaper->getDefaultStrategy($name); } /** * Defines a new escaper to be used via the escape filter. * - * @param string $strategy The strategy name that should be used as a strategy in the escape call - * @param callable $callable A valid PHP callable + * @param string $strategy The strategy name that should be used as a strategy in the escape call + * @param callable(Environment, string) $callable A valid PHP callable */ public function setEscaper($strategy, callable $callable) { + trigger_deprecation('twig/twig', '3.10', 'The "%s()" method is deprecated, use the "Twig\Runtime\EscaperRuntime::setEscaper()" method instead (be warned that Environment is not passed anymore to the callable).', __METHOD__); + + if (!$this->environment) { + throw new \LogicException('You must call setEnvironment() before calling setEscaper().'); + } + $this->escapers[$strategy] = $callable; + $callable = function ($string, $charset) use ($callable) { + return $callable($this->environment, $string, $charset); + }; + + $this->escaper->setEscaper($strategy, $callable); } /** * Gets all defined escapers. * - * @return callable[] An array of escapers + * @return array An array of escapers */ public function getEscapers() { + trigger_deprecation('twig/twig', '3.10', 'The "%s()" method is deprecated, use the "Twig\Runtime\EscaperRuntime::getEscaper()" method instead.', __METHOD__); + return $this->escapers; } public function setSafeClasses(array $safeClasses = []) { - $this->safeClasses = []; - $this->safeLookup = []; - foreach ($safeClasses as $class => $strategies) { - $this->addSafeClass($class, $strategies); - } + trigger_deprecation('twig/twig', '3.10', 'The "%s()" method is deprecated, use the "Twig\Runtime\EscaperRuntime::setSafeClasses()" method instead.', __METHOD__); + + $this->escaper->setSafeClasses($safeClasses); } public function addSafeClass(string $class, array $strategies) { - $class = ltrim($class, '\\'); - if (!isset($this->safeClasses[$class])) { - $this->safeClasses[$class] = []; - } - $this->safeClasses[$class] = array_merge($this->safeClasses[$class], $strategies); + trigger_deprecation('twig/twig', '3.10', 'The "%s()" method is deprecated, use the "Twig\Runtime\EscaperRuntime::addSafeClass()" method instead.', __METHOD__); - foreach ($strategies as $strategy) { - $this->safeLookup[$strategy][$class] = true; - } + $this->escaper->addSafeClass($class, $strategies); } /** @@ -166,251 +159,4 @@ public static function escapeFilterIsSafe(Node $filterArgs) return ['html']; } - - /** - * Escapes a string. - * - * @param mixed $string The value to be escaped - * @param string $strategy The escaping strategy - * @param string $charset The charset - * @param bool $autoescape Whether the function is called by the auto-escaping feature (true) or by the developer (false) - * - * @return string|Markup - * - * @internal - */ - public static function escape(Environment $env, $string, $strategy = 'html', $charset = null, $autoescape = false) - { - if ($autoescape && $string instanceof Markup) { - return $string; - } - - if (!\is_string($string)) { - if (\is_object($string) && method_exists($string, '__toString')) { - if ($autoescape) { - $c = \get_class($string); - $ext = $env->getExtension(self::class); - if (!isset($ext->safeClasses[$c])) { - $ext->safeClasses[$c] = []; - foreach (class_parents($string) + class_implements($string) as $class) { - if (isset($ext->safeClasses[$class])) { - $ext->safeClasses[$c] = array_unique(array_merge($ext->safeClasses[$c], $ext->safeClasses[$class])); - foreach ($ext->safeClasses[$class] as $s) { - $ext->safeLookup[$s][$c] = true; - } - } - } - } - if (isset($ext->safeLookup[$strategy][$c]) || isset($ext->safeLookup['all'][$c])) { - return (string) $string; - } - } - - $string = (string) $string; - } elseif (\in_array($strategy, ['html', 'js', 'css', 'html_attr', 'url'])) { - return $string; - } - } - - if ('' === $string) { - return ''; - } - - if (null === $charset) { - $charset = $env->getCharset(); - } - - switch ($strategy) { - case 'html': - // see https://www.php.net/htmlspecialchars - - // Using a static variable to avoid initializing the array - // each time the function is called. Moving the declaration on the - // top of the function slow downs other escaping strategies. - static $htmlspecialcharsCharsets = [ - 'ISO-8859-1' => true, 'ISO8859-1' => true, - 'ISO-8859-15' => true, 'ISO8859-15' => true, - 'utf-8' => true, 'UTF-8' => true, - 'CP866' => true, 'IBM866' => true, '866' => true, - 'CP1251' => true, 'WINDOWS-1251' => true, 'WIN-1251' => true, - '1251' => true, - 'CP1252' => true, 'WINDOWS-1252' => true, '1252' => true, - 'KOI8-R' => true, 'KOI8-RU' => true, 'KOI8R' => true, - 'BIG5' => true, '950' => true, - 'GB2312' => true, '936' => true, - 'BIG5-HKSCS' => true, - 'SHIFT_JIS' => true, 'SJIS' => true, '932' => true, - 'EUC-JP' => true, 'EUCJP' => true, - 'ISO8859-5' => true, 'ISO-8859-5' => true, 'MACROMAN' => true, - ]; - - if (isset($htmlspecialcharsCharsets[$charset])) { - return htmlspecialchars($string, \ENT_QUOTES | \ENT_SUBSTITUTE, $charset); - } - - if (isset($htmlspecialcharsCharsets[strtoupper($charset)])) { - // cache the lowercase variant for future iterations - $htmlspecialcharsCharsets[$charset] = true; - - return htmlspecialchars($string, \ENT_QUOTES | \ENT_SUBSTITUTE, $charset); - } - - $string = CoreExtension::convertEncoding($string, 'UTF-8', $charset); - $string = htmlspecialchars($string, \ENT_QUOTES | \ENT_SUBSTITUTE, 'UTF-8'); - - return iconv('UTF-8', $charset, $string); - - case 'js': - // escape all non-alphanumeric characters - // into their \x or \uHHHH representations - if ('UTF-8' !== $charset) { - $string = CoreExtension::convertEncoding($string, 'UTF-8', $charset); - } - - if (!preg_match('//u', $string)) { - throw new RuntimeError('The string to escape is not a valid UTF-8 string.'); - } - - $string = preg_replace_callback('#[^a-zA-Z0-9,\._]#Su', function ($matches) { - $char = $matches[0]; - - /* - * A few characters have short escape sequences in JSON and JavaScript. - * Escape sequences supported only by JavaScript, not JSON, are omitted. - * \" is also supported but omitted, because the resulting string is not HTML safe. - */ - static $shortMap = [ - '\\' => '\\\\', - '/' => '\\/', - "\x08" => '\b', - "\x0C" => '\f', - "\x0A" => '\n', - "\x0D" => '\r', - "\x09" => '\t', - ]; - - if (isset($shortMap[$char])) { - return $shortMap[$char]; - } - - $codepoint = mb_ord($char, 'UTF-8'); - if (0x10000 > $codepoint) { - return sprintf('\u%04X', $codepoint); - } - - // Split characters outside the BMP into surrogate pairs - // https://tools.ietf.org/html/rfc2781.html#section-2.1 - $u = $codepoint - 0x10000; - $high = 0xD800 | ($u >> 10); - $low = 0xDC00 | ($u & 0x3FF); - - return sprintf('\u%04X\u%04X', $high, $low); - }, $string); - - if ('UTF-8' !== $charset) { - $string = iconv('UTF-8', $charset, $string); - } - - return $string; - - case 'css': - if ('UTF-8' !== $charset) { - $string = CoreExtension::convertEncoding($string, 'UTF-8', $charset); - } - - if (!preg_match('//u', $string)) { - throw new RuntimeError('The string to escape is not a valid UTF-8 string.'); - } - - $string = preg_replace_callback('#[^a-zA-Z0-9]#Su', function ($matches) { - $char = $matches[0]; - - return sprintf('\\%X ', 1 === \strlen($char) ? \ord($char) : mb_ord($char, 'UTF-8')); - }, $string); - - if ('UTF-8' !== $charset) { - $string = iconv('UTF-8', $charset, $string); - } - - return $string; - - case 'html_attr': - if ('UTF-8' !== $charset) { - $string = CoreExtension::convertEncoding($string, 'UTF-8', $charset); - } - - if (!preg_match('//u', $string)) { - throw new RuntimeError('The string to escape is not a valid UTF-8 string.'); - } - - $string = preg_replace_callback('#[^a-zA-Z0-9,\.\-_]#Su', function ($matches) { - /** - * This function is adapted from code coming from Zend Framework. - * - * @copyright Copyright (c) 2005-2012 Zend Technologies USA Inc. (https://www.zend.com) - * @license https://framework.zend.com/license/new-bsd New BSD License - */ - $chr = $matches[0]; - $ord = \ord($chr); - - /* - * The following replaces characters undefined in HTML with the - * hex entity for the Unicode replacement character. - */ - if (($ord <= 0x1F && "\t" != $chr && "\n" != $chr && "\r" != $chr) || ($ord >= 0x7F && $ord <= 0x9F)) { - return '�'; - } - - /* - * Check if the current character to escape has a name entity we should - * replace it with while grabbing the hex value of the character. - */ - if (1 === \strlen($chr)) { - /* - * While HTML supports far more named entities, the lowest common denominator - * has become HTML5's XML Serialisation which is restricted to the those named - * entities that XML supports. Using HTML entities would result in this error: - * XML Parsing Error: undefined entity - */ - static $entityMap = [ - 34 => '"', /* quotation mark */ - 38 => '&', /* ampersand */ - 60 => '<', /* less-than sign */ - 62 => '>', /* greater-than sign */ - ]; - - if (isset($entityMap[$ord])) { - return $entityMap[$ord]; - } - - return sprintf('&#x%02X;', $ord); - } - - /* - * Per OWASP recommendations, we'll use hex entities for any other - * characters where a named entity does not exist. - */ - return sprintf('&#x%04X;', mb_ord($chr, 'UTF-8')); - }, $string); - - if ('UTF-8' !== $charset) { - $string = iconv('UTF-8', $charset, $string); - } - - return $string; - - case 'url': - return rawurlencode($string); - - default: - $escapers = $env->getExtension(self::class)->getEscapers(); - if (\array_key_exists($strategy, $escapers)) { - return $escapers[$strategy]($env, $string, $charset); - } - - $validStrategies = implode('", "', array_merge(['html', 'js', 'url', 'css', 'html_attr'], array_keys($escapers))); - - throw new RuntimeError(sprintf('Invalid escaping strategy "%s" (valid ones: "%s").', $strategy, $validStrategies)); - } - } } diff --git a/src/NodeVisitor/EscaperNodeVisitor.php b/src/NodeVisitor/EscaperNodeVisitor.php index 91e2ea89392..fe9b3472ecd 100644 --- a/src/NodeVisitor/EscaperNodeVisitor.php +++ b/src/NodeVisitor/EscaperNodeVisitor.php @@ -26,6 +26,7 @@ use Twig\Node\Node; use Twig\Node\PrintNode; use Twig\NodeTraverser; +use Twig\Runtime\EscaperRuntime; /** * @author Fabien Potencier @@ -49,7 +50,7 @@ public function __construct() public function enterNode(Node $node, Environment $env): Node { if ($node instanceof ModuleNode) { - if ($env->hasExtension(EscaperExtension::class) && $defaultStrategy = $env->getExtension(EscaperExtension::class)->getDefaultStrategy($node->getTemplateName())) { + if ($env->hasExtension(EscaperExtension::class) && $defaultStrategy = $env->getRuntime(EscaperRuntime::class)->getDefaultStrategy($node->getTemplateName())) { $this->defaultStrategy = $defaultStrategy; } $this->safeVars = []; diff --git a/src/Resources/escaper.php b/src/Resources/escaper.php index bc93331a630..a2ee8e7aaa0 100644 --- a/src/Resources/escaper.php +++ b/src/Resources/escaper.php @@ -12,6 +12,7 @@ use Twig\Environment; use Twig\Extension\EscaperExtension; use Twig\Node\Node; +use Twig\Runtime\EscaperRuntime; /** * @internal @@ -34,7 +35,7 @@ function twig_escape_filter(Environment $env, $string, $strategy = 'html', $char { trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); - return EscaperExtension::escape($env, $string, $strategy, $charset, $autoescape); + return $env->getRuntime(EscaperRuntime::class)->escape($string, $strategy, $charset, $autoescape); } /** diff --git a/src/Runtime/EscaperRuntime.php b/src/Runtime/EscaperRuntime.php new file mode 100644 index 00000000000..6b55c7fb346 --- /dev/null +++ b/src/Runtime/EscaperRuntime.php @@ -0,0 +1,370 @@ +setDefaultStrategy($defaultStrategy); + } + + /** + * Sets the default strategy to use when not defined by the user. + * + * The strategy can be a valid PHP callback that takes the template + * name as an argument and returns the strategy to use. + * + * @param string|false|callable $defaultStrategy An escaping strategy + */ + public function setDefaultStrategy($defaultStrategy): void + { + if ('name' === $defaultStrategy) { + $defaultStrategy = [FileExtensionEscapingStrategy::class, 'guess']; + } + + $this->defaultStrategy = $defaultStrategy; + } + + /** + * Gets the default strategy to use when not defined by the user. + * + * @param string $name The template name + * + * @return string|false The default strategy to use for the template + */ + public function getDefaultStrategy(string $name) + { + // disable string callables to avoid calling a function named html or js, + // or any other upcoming escaping strategy + if (!\is_string($this->defaultStrategy) && false !== $this->defaultStrategy) { + return \call_user_func($this->defaultStrategy, $name); + } + + return $this->defaultStrategy; + } + + /** + * Defines a new escaper to be used via the escape filter. + * + * @param string $strategy The strategy name that should be used as a strategy in the escape call + * @param callable(string $string, string $charset) $callable A valid PHP callable + */ + public function setEscaper($strategy, callable $callable) + { + $this->escapers[$strategy] = $callable; + } + + /** + * Gets all defined escapers. + * + * @return array An array of escapers + */ + public function getEscapers() + { + return $this->escapers; + } + + public function setSafeClasses(array $safeClasses = []) + { + $this->safeClasses = []; + $this->safeLookup = []; + foreach ($safeClasses as $class => $strategies) { + $this->addSafeClass($class, $strategies); + } + } + + public function addSafeClass(string $class, array $strategies) + { + $class = ltrim($class, '\\'); + if (!isset($this->safeClasses[$class])) { + $this->safeClasses[$class] = []; + } + $this->safeClasses[$class] = array_merge($this->safeClasses[$class], $strategies); + + foreach ($strategies as $strategy) { + $this->safeLookup[$strategy][$class] = true; + } + } + + /** + * Escapes a string. + * + * @param mixed $string The value to be escaped + * @param string $strategy The escaping strategy + * @param string|null $charset The charset + * @param bool $autoescape Whether the function is called by the auto-escaping feature (true) or by the developer (false) + * + * @throws RuntimeException + */ + public function escape($string, string $strategy = 'html', ?string $charset = null, bool $autoescape = false): mixed + { + if ($autoescape && $string instanceof Markup) { + return $string; + } + + if (!\is_string($string)) { + if (\is_object($string) && method_exists($string, '__toString')) { + if ($autoescape) { + $c = \get_class($string); + if (!isset($this->safeClasses[$c])) { + $this->safeClasses[$c] = []; + foreach (class_parents($string) + class_implements($string) as $class) { + if (isset($this->safeClasses[$class])) { + $this->safeClasses[$c] = array_unique(array_merge($this->safeClasses[$c], $this->safeClasses[$class])); + foreach ($this->safeClasses[$class] as $s) { + $this->safeLookup[$s][$c] = true; + } + } + } + } + if (isset($this->safeLookup[$strategy][$c]) || isset($this->safeLookup['all'][$c])) { + return (string) $string; + } + } + + $string = (string) $string; + } elseif (\in_array($strategy, ['html', 'js', 'css', 'html_attr', 'url'])) { + // we return the input as is (which can be of any type) + return $string; + } + } + + if ('' === $string) { + return ''; + } + + $charset = $charset ?: $this->charset; + + switch ($strategy) { + case 'html': + // see https://www.php.net/htmlspecialchars + + // Using a static variable to avoid initializing the array + // each time the function is called. Moving the declaration on the + // top of the function slow downs other escaping strategies. + static $htmlspecialcharsCharsets = [ + 'ISO-8859-1' => true, 'ISO8859-1' => true, + 'ISO-8859-15' => true, 'ISO8859-15' => true, + 'utf-8' => true, 'UTF-8' => true, + 'CP866' => true, 'IBM866' => true, '866' => true, + 'CP1251' => true, 'WINDOWS-1251' => true, 'WIN-1251' => true, + '1251' => true, + 'CP1252' => true, 'WINDOWS-1252' => true, '1252' => true, + 'KOI8-R' => true, 'KOI8-RU' => true, 'KOI8R' => true, + 'BIG5' => true, '950' => true, + 'GB2312' => true, '936' => true, + 'BIG5-HKSCS' => true, + 'SHIFT_JIS' => true, 'SJIS' => true, '932' => true, + 'EUC-JP' => true, 'EUCJP' => true, + 'ISO8859-5' => true, 'ISO-8859-5' => true, 'MACROMAN' => true, + ]; + + if (isset($htmlspecialcharsCharsets[$charset])) { + return htmlspecialchars($string, \ENT_QUOTES | \ENT_SUBSTITUTE, $charset); + } + + if (isset($htmlspecialcharsCharsets[strtoupper($charset)])) { + // cache the lowercase variant for future iterations + $htmlspecialcharsCharsets[$charset] = true; + + return htmlspecialchars($string, \ENT_QUOTES | \ENT_SUBSTITUTE, $charset); + } + + $string = $this->convertEncoding($string, 'UTF-8', $charset); + $string = htmlspecialchars($string, \ENT_QUOTES | \ENT_SUBSTITUTE, 'UTF-8'); + + return iconv('UTF-8', $charset, $string); + + case 'js': + // escape all non-alphanumeric characters + // into their \x or \uHHHH representations + if ('UTF-8' !== $charset) { + $string = $this->convertEncoding($string, 'UTF-8', $charset); + } + + if (!preg_match('//u', $string)) { + throw new RuntimeError('The string to escape is not a valid UTF-8 string.'); + } + + $string = preg_replace_callback('#[^a-zA-Z0-9,\._]#Su', function ($matches) { + $char = $matches[0]; + + /* + * A few characters have short escape sequences in JSON and JavaScript. + * Escape sequences supported only by JavaScript, not JSON, are omitted. + * \" is also supported but omitted, because the resulting string is not HTML safe. + */ + static $shortMap = [ + '\\' => '\\\\', + '/' => '\\/', + "\x08" => '\b', + "\x0C" => '\f', + "\x0A" => '\n', + "\x0D" => '\r', + "\x09" => '\t', + ]; + + if (isset($shortMap[$char])) { + return $shortMap[$char]; + } + + $codepoint = mb_ord($char, 'UTF-8'); + if (0x10000 > $codepoint) { + return sprintf('\u%04X', $codepoint); + } + + // Split characters outside the BMP into surrogate pairs + // https://tools.ietf.org/html/rfc2781.html#section-2.1 + $u = $codepoint - 0x10000; + $high = 0xD800 | ($u >> 10); + $low = 0xDC00 | ($u & 0x3FF); + + return sprintf('\u%04X\u%04X', $high, $low); + }, $string); + + if ('UTF-8' !== $charset) { + $string = iconv('UTF-8', $charset, $string); + } + + return $string; + + case 'css': + if ('UTF-8' !== $charset) { + $string = $this->convertEncoding($string, 'UTF-8', $charset); + } + + if (!preg_match('//u', $string)) { + throw new RuntimeError('The string to escape is not a valid UTF-8 string.'); + } + + $string = preg_replace_callback('#[^a-zA-Z0-9]#Su', function ($matches) { + $char = $matches[0]; + + return sprintf('\\%X ', 1 === \strlen($char) ? \ord($char) : mb_ord($char, 'UTF-8')); + }, $string); + + if ('UTF-8' !== $charset) { + $string = iconv('UTF-8', $charset, $string); + } + + return $string; + + case 'html_attr': + if ('UTF-8' !== $charset) { + $string = $this->convertEncoding($string, 'UTF-8', $charset); + } + + if (!preg_match('//u', $string)) { + throw new RuntimeError('The string to escape is not a valid UTF-8 string.'); + } + + $string = preg_replace_callback('#[^a-zA-Z0-9,\.\-_]#Su', function ($matches) { + /** + * This function is adapted from code coming from Zend Framework. + * + * @copyright Copyright (c) 2005-2012 Zend Technologies USA Inc. (https://www.zend.com) + * @license https://framework.zend.com/license/new-bsd New BSD License + */ + $chr = $matches[0]; + $ord = \ord($chr); + + /* + * The following replaces characters undefined in HTML with the + * hex entity for the Unicode replacement character. + */ + if (($ord <= 0x1F && "\t" != $chr && "\n" != $chr && "\r" != $chr) || ($ord >= 0x7F && $ord <= 0x9F)) { + return '�'; + } + + /* + * Check if the current character to escape has a name entity we should + * replace it with while grabbing the hex value of the character. + */ + if (1 === \strlen($chr)) { + /* + * While HTML supports far more named entities, the lowest common denominator + * has become HTML5's XML Serialisation which is restricted to the those named + * entities that XML supports. Using HTML entities would result in this error: + * XML Parsing Error: undefined entity + */ + static $entityMap = [ + 34 => '"', /* quotation mark */ + 38 => '&', /* ampersand */ + 60 => '<', /* less-than sign */ + 62 => '>', /* greater-than sign */ + ]; + + if (isset($entityMap[$ord])) { + return $entityMap[$ord]; + } + + return sprintf('&#x%02X;', $ord); + } + + /* + * Per OWASP recommendations, we'll use hex entities for any other + * characters where a named entity does not exist. + */ + return sprintf('&#x%04X;', mb_ord($chr, 'UTF-8')); + }, $string); + + if ('UTF-8' !== $charset) { + $string = iconv('UTF-8', $charset, $string); + } + + return $string; + + case 'url': + return rawurlencode($string); + + default: + if (\array_key_exists($strategy, $this->escapers)) { + return $this->escapers[$strategy]($string, $charset); + } + + $validStrategies = implode('", "', array_merge(['html', 'js', 'url', 'css', 'html_attr'], array_keys($this->escapers))); + + throw new RuntimeError(sprintf('Invalid escaping strategy "%s" (valid ones: "%s").', $strategy, $validStrategies)); + } + } + + private function convertEncoding(string $string, string $to, string $from) + { + if (!\function_exists('iconv')) { + throw new RuntimeError('Unable to convert encoding: required function iconv() does not exist. You should install ext-iconv or symfony/polyfill-iconv.'); + } + + return iconv($from, $to, $string); + } +} diff --git a/tests/Extension/EscaperTest.php b/tests/Extension/EscaperTest.php index d8fedc64e29..d53f3b52970 100644 --- a/tests/Extension/EscaperTest.php +++ b/tests/Extension/EscaperTest.php @@ -13,364 +13,24 @@ use PHPUnit\Framework\TestCase; use Twig\Environment; -use Twig\Error\RuntimeError; use Twig\Extension\EscaperExtension; use Twig\Loader\LoaderInterface; +use Twig\Runtime\EscaperRuntime; class EscaperTest extends TestCase { - /** - * All character encodings supported by htmlspecialchars(). - */ - protected $htmlSpecialChars = [ - '\'' => ''', - '"' => '"', - '<' => '<', - '>' => '>', - '&' => '&', - ]; - - protected $htmlAttrSpecialChars = [ - '\'' => ''', - /* Characters beyond ASCII value 255 to unicode escape */ - 'Ā' => 'Ā', - '😀' => '😀', - /* Immune chars excluded */ - ',' => ',', - '.' => '.', - '-' => '-', - '_' => '_', - /* Basic alnums excluded */ - 'a' => 'a', - 'A' => 'A', - 'z' => 'z', - 'Z' => 'Z', - '0' => '0', - '9' => '9', - /* Basic control characters and null */ - "\r" => ' ', - "\n" => ' ', - "\t" => ' ', - "\0" => '�', // should use Unicode replacement char - /* Encode chars as named entities where possible */ - '<' => '<', - '>' => '>', - '&' => '&', - '"' => '"', - /* Encode spaces for quoteless attribute protection */ - ' ' => ' ', - ]; - - protected $jsSpecialChars = [ - /* HTML special chars - escape without exception to hex */ - '<' => '\\u003C', - '>' => '\\u003E', - '\'' => '\\u0027', - '"' => '\\u0022', - '&' => '\\u0026', - '/' => '\\/', - /* Characters beyond ASCII value 255 to unicode escape */ - 'Ā' => '\\u0100', - '😀' => '\\uD83D\\uDE00', - /* Immune chars excluded */ - ',' => ',', - '.' => '.', - '_' => '_', - /* Basic alnums excluded */ - 'a' => 'a', - 'A' => 'A', - 'z' => 'z', - 'Z' => 'Z', - '0' => '0', - '9' => '9', - /* Basic control characters and null */ - "\r" => '\r', - "\n" => '\n', - "\x08" => '\b', - "\t" => '\t', - "\x0C" => '\f', - "\0" => '\\u0000', - /* Encode spaces for quoteless attribute protection */ - ' ' => '\\u0020', - ]; - - protected $urlSpecialChars = [ - /* HTML special chars - escape without exception to percent encoding */ - '<' => '%3C', - '>' => '%3E', - '\'' => '%27', - '"' => '%22', - '&' => '%26', - /* Characters beyond ASCII value 255 to hex sequence */ - 'Ā' => '%C4%80', - /* Punctuation and unreserved check */ - ',' => '%2C', - '.' => '.', - '_' => '_', - '-' => '-', - ':' => '%3A', - ';' => '%3B', - '!' => '%21', - /* Basic alnums excluded */ - 'a' => 'a', - 'A' => 'A', - 'z' => 'z', - 'Z' => 'Z', - '0' => '0', - '9' => '9', - /* Basic control characters and null */ - "\r" => '%0D', - "\n" => '%0A', - "\t" => '%09', - "\0" => '%00', - /* PHP quirks from the past */ - ' ' => '%20', - '~' => '~', - '+' => '%2B', - ]; - - protected $cssSpecialChars = [ - /* HTML special chars - escape without exception to hex */ - '<' => '\\3C ', - '>' => '\\3E ', - '\'' => '\\27 ', - '"' => '\\22 ', - '&' => '\\26 ', - /* Characters beyond ASCII value 255 to unicode escape */ - 'Ā' => '\\100 ', - /* Immune chars excluded */ - ',' => '\\2C ', - '.' => '\\2E ', - '_' => '\\5F ', - /* Basic alnums excluded */ - 'a' => 'a', - 'A' => 'A', - 'z' => 'z', - 'Z' => 'Z', - '0' => '0', - '9' => '9', - /* Basic control characters and null */ - "\r" => '\\D ', - "\n" => '\\A ', - "\t" => '\\9 ', - "\0" => '\\0 ', - /* Encode spaces for quoteless attribute protection */ - ' ' => '\\20 ', - ]; - - public function testHtmlEscapingConvertsSpecialChars() - { - $twig = new Environment($this->createMock(LoaderInterface::class)); - foreach ($this->htmlSpecialChars as $key => $value) { - $this->assertEquals($value, EscaperExtension::escape($twig, $key, 'html'), 'Failed to escape: '.$key); - } - } - - public function testHtmlAttributeEscapingConvertsSpecialChars() - { - $twig = new Environment($this->createMock(LoaderInterface::class)); - foreach ($this->htmlAttrSpecialChars as $key => $value) { - $this->assertEquals($value, EscaperExtension::escape($twig, $key, 'html_attr'), 'Failed to escape: '.$key); - } - } - - public function testJavascriptEscapingConvertsSpecialChars() - { - $twig = new Environment($this->createMock(LoaderInterface::class)); - foreach ($this->jsSpecialChars as $key => $value) { - $this->assertEquals($value, EscaperExtension::escape($twig, $key, 'js'), 'Failed to escape: '.$key); - } - } - - public function testJavascriptEscapingConvertsSpecialCharsWithInternalEncoding() - { - $twig = new Environment($this->createMock(LoaderInterface::class)); - $previousInternalEncoding = mb_internal_encoding(); - try { - mb_internal_encoding('ISO-8859-1'); - foreach ($this->jsSpecialChars as $key => $value) { - $this->assertEquals($value, EscaperExtension::escape($twig, $key, 'js'), 'Failed to escape: '.$key); - } - } finally { - if (false !== $previousInternalEncoding) { - mb_internal_encoding($previousInternalEncoding); - } - } - } - - public function testJavascriptEscapingReturnsStringIfZeroLength() - { - $twig = new Environment($this->createMock(LoaderInterface::class)); - $this->assertEquals('', EscaperExtension::escape($twig, '', 'js')); - } - - public function testJavascriptEscapingReturnsStringIfContainsOnlyDigits() - { - $twig = new Environment($this->createMock(LoaderInterface::class)); - $this->assertEquals('123', EscaperExtension::escape($twig, '123', 'js')); - } - - public function testCssEscapingConvertsSpecialChars() - { - $twig = new Environment($this->createMock(LoaderInterface::class)); - foreach ($this->cssSpecialChars as $key => $value) { - $this->assertEquals($value, EscaperExtension::escape($twig, $key, 'css'), 'Failed to escape: '.$key); - } - } - - public function testCssEscapingReturnsStringIfZeroLength() - { - $twig = new Environment($this->createMock(LoaderInterface::class)); - $this->assertEquals('', EscaperExtension::escape($twig, '', 'css')); - } - - public function testCssEscapingReturnsStringIfContainsOnlyDigits() - { - $twig = new Environment($this->createMock(LoaderInterface::class)); - $this->assertEquals('123', EscaperExtension::escape($twig, '123', 'css')); - } - - public function testUrlEscapingConvertsSpecialChars() - { - $twig = new Environment($this->createMock(LoaderInterface::class)); - foreach ($this->urlSpecialChars as $key => $value) { - $this->assertEquals($value, EscaperExtension::escape($twig, $key, 'url'), 'Failed to escape: '.$key); - } - } - - /** - * Range tests to confirm escaped range of characters is within OWASP recommendation. - */ - - /** - * Only testing the first few 2 ranges on this prot. function as that's all these - * other range tests require. - */ - public function testUnicodeCodepointConversionToUtf8() - { - $expected = ' ~ޙ'; - $codepoints = [0x20, 0x7E, 0x799]; - $result = ''; - foreach ($codepoints as $value) { - $result .= $this->codepointToUtf8($value); - } - $this->assertEquals($expected, $result); - } - - /** - * Convert a Unicode Codepoint to a literal UTF-8 character. - * - * @param int $codepoint Unicode codepoint in hex notation - * - * @return string UTF-8 literal string - */ - protected function codepointToUtf8($codepoint) - { - if ($codepoint < 0x80) { - return \chr($codepoint); - } - if ($codepoint < 0x800) { - return \chr($codepoint >> 6 & 0x3F | 0xC0) - .\chr($codepoint & 0x3F | 0x80); - } - if ($codepoint < 0x10000) { - return \chr($codepoint >> 12 & 0x0F | 0xE0) - .\chr($codepoint >> 6 & 0x3F | 0x80) - .\chr($codepoint & 0x3F | 0x80); - } - if ($codepoint < 0x110000) { - return \chr($codepoint >> 18 & 0x07 | 0xF0) - .\chr($codepoint >> 12 & 0x3F | 0x80) - .\chr($codepoint >> 6 & 0x3F | 0x80) - .\chr($codepoint & 0x3F | 0x80); - } - throw new \Exception('Codepoint requested outside of Unicode range.'); - } - - public function testJavascriptEscapingEscapesOwaspRecommendedRanges() - { - $twig = new Environment($this->createMock(LoaderInterface::class)); - $immune = [',', '.', '_']; // Exceptions to escaping ranges - for ($chr = 0; $chr < 0xFF; ++$chr) { - if ($chr >= 0x30 && $chr <= 0x39 - || $chr >= 0x41 && $chr <= 0x5A - || $chr >= 0x61 && $chr <= 0x7A) { - $literal = $this->codepointToUtf8($chr); - $this->assertEquals($literal, EscaperExtension::escape($twig, $literal, 'js')); - } else { - $literal = $this->codepointToUtf8($chr); - if (\in_array($literal, $immune)) { - $this->assertEquals($literal, EscaperExtension::escape($twig, $literal, 'js')); - } else { - $this->assertNotEquals( - $literal, - EscaperExtension::escape($twig, $literal, 'js'), - "$literal should be escaped!"); - } - } - } - } - - public function testHtmlAttributeEscapingEscapesOwaspRecommendedRanges() - { - $twig = new Environment($this->createMock(LoaderInterface::class)); - $immune = [',', '.', '-', '_']; // Exceptions to escaping ranges - for ($chr = 0; $chr < 0xFF; ++$chr) { - if ($chr >= 0x30 && $chr <= 0x39 - || $chr >= 0x41 && $chr <= 0x5A - || $chr >= 0x61 && $chr <= 0x7A) { - $literal = $this->codepointToUtf8($chr); - $this->assertEquals($literal, EscaperExtension::escape($twig, $literal, 'html_attr')); - } else { - $literal = $this->codepointToUtf8($chr); - if (\in_array($literal, $immune)) { - $this->assertEquals($literal, EscaperExtension::escape($twig, $literal, 'html_attr')); - } else { - $this->assertNotEquals( - $literal, - EscaperExtension::escape($twig, $literal, 'html_attr'), - "$literal should be escaped!"); - } - } - } - } - - public function testCssEscapingEscapesOwaspRecommendedRanges() - { - $twig = new Environment($this->createMock(LoaderInterface::class)); - // CSS has no exceptions to escaping ranges - for ($chr = 0; $chr < 0xFF; ++$chr) { - if ($chr >= 0x30 && $chr <= 0x39 - || $chr >= 0x41 && $chr <= 0x5A - || $chr >= 0x61 && $chr <= 0x7A) { - $literal = $this->codepointToUtf8($chr); - $this->assertEquals($literal, EscaperExtension::escape($twig, $literal, 'css')); - } else { - $literal = $this->codepointToUtf8($chr); - $this->assertNotEquals( - $literal, - EscaperExtension::escape($twig, $literal, 'css'), - "$literal should be escaped!"); - } - } - } - - public function testUnknownCustomEscaper() - { - $this->expectException(RuntimeError::class); - - EscaperExtension::escape(new Environment($this->createMock(LoaderInterface::class)), 'foo', 'bar'); - } - /** * @dataProvider provideCustomEscaperCases + * + * @group legacy */ public function testCustomEscaper($expected, $string, $strategy) { $twig = new Environment($this->createMock(LoaderInterface::class)); - $twig->getExtension(EscaperExtension::class)->setEscaper('foo', 'Twig\Tests\foo_escaper_for_test'); - - $this->assertSame($expected, EscaperExtension::escape($twig, $string, $strategy)); + $escaperExt = $twig->getExtension(EscaperExtension::class); + $escaperExt->setEnvironment($twig); + $escaperExt->setEscaper('foo', 'Twig\Tests\foo_escaper_for_test'); + $this->assertSame($expected, $twig->getRuntime(EscaperRuntime::class)->escape($string, $strategy)); } public function provideCustomEscaperCases() @@ -382,37 +42,23 @@ public function provideCustomEscaperCases() ]; } + /** + * @group legacy + */ public function testCustomEscapersOnMultipleEnvs() { $env1 = new Environment($this->createMock(LoaderInterface::class)); - $env1->getExtension(EscaperExtension::class)->setEscaper('foo', 'Twig\Tests\foo_escaper_for_test'); - $env2 = new Environment($this->createMock(LoaderInterface::class)); - $env2->getExtension(EscaperExtension::class)->setEscaper('foo', 'Twig\Tests\foo_escaper_for_test1'); + $escaperExt1 = $env1->getExtension(EscaperExtension::class); + $escaperExt1->setEnvironment($env1); + $escaperExt1->setEscaper('foo', 'Twig\Tests\foo_escaper_for_test'); - $this->assertSame('fooUTF-8', EscaperExtension::escape($env1, 'foo', 'foo')); - $this->assertSame('fooUTF-81', EscaperExtension::escape($env2, 'foo', 'foo')); - } - - /** - * @dataProvider provideObjectsForEscaping - */ - public function testObjectEscaping(string $escapedHtml, string $escapedJs, array $safeClasses) - { - $obj = new Extension_TestClass(); - $twig = new Environment($this->createMock(LoaderInterface::class)); - $twig->getExtension(EscaperExtension::class)->setSafeClasses($safeClasses); - $this->assertSame($escapedHtml, EscaperExtension::escape($twig, $obj, 'html', null, true)); - $this->assertSame($escapedJs, EscaperExtension::escape($twig, $obj, 'js', null, true)); - } + $env2 = new Environment($this->createMock(LoaderInterface::class)); + $escaperExt2 = $env2->getExtension(EscaperExtension::class); + $escaperExt2->setEnvironment($env2); + $escaperExt2->setEscaper('foo', 'Twig\Tests\foo_escaper_for_test1'); - public function provideObjectsForEscaping() - { - return [ - ['<br />', '
', ['\Twig\Tests\Extension_TestClass' => ['js']]], - ['
', '\u003Cbr\u0020\/\u003E', ['\Twig\Tests\Extension_TestClass' => ['html']]], - ['<br />', '
', ['\Twig\Tests\Extension_SafeHtmlInterface' => ['js']]], - ['
', '
', ['\Twig\Tests\Extension_SafeHtmlInterface' => ['all']]], - ]; + $this->assertSame('fooUTF-8', $env1->getRuntime(EscaperRuntime::class)->escape('foo', 'foo')); + $this->assertSame('fooUTF-81', $env2->getRuntime(EscaperRuntime::class)->escape('foo', 'foo')); } } @@ -425,14 +71,3 @@ function foo_escaper_for_test1(Environment $twig, $string, $charset) { return $string.$charset.'1'; } - -interface Extension_SafeHtmlInterface -{ -} -class Extension_TestClass implements Extension_SafeHtmlInterface -{ - public function __toString() - { - return '
'; - } -} diff --git a/tests/IntegrationTest.php b/tests/IntegrationTest.php index e2b211a01de..76dc98de6e0 100644 --- a/tests/IntegrationTest.php +++ b/tests/IntegrationTest.php @@ -13,12 +13,12 @@ use Twig\Extension\AbstractExtension; use Twig\Extension\DebugExtension; -use Twig\Extension\EscaperExtension; use Twig\Extension\SandboxExtension; use Twig\Extension\StringLoaderExtension; use Twig\Node\Expression\ConstantExpression; use Twig\Node\Node; use Twig\Node\PrintNode; +use Twig\Runtime\EscaperRuntime; use Twig\Sandbox\SecurityPolicy; use Twig\Test\IntegrationTestCase; use Twig\Token; @@ -216,7 +216,7 @@ public function §Function($value) */ public function escape_and_nl2br($env, $value, $sep = '
') { - return $this->nl2br(EscaperExtension::escape($env, $value, 'html'), $sep); + return $this->nl2br($env->getRuntime(EscaperRuntime::class)->escape($value, 'html'), $sep); } /** diff --git a/tests/Runtime/EscaperRuntimeTest.php b/tests/Runtime/EscaperRuntimeTest.php new file mode 100644 index 00000000000..2dc4bb95e56 --- /dev/null +++ b/tests/Runtime/EscaperRuntimeTest.php @@ -0,0 +1,411 @@ + ''', + '"' => '"', + '<' => '<', + '>' => '>', + '&' => '&', + ]; + + protected $htmlAttrSpecialChars = [ + '\'' => ''', + /* Characters beyond ASCII value 255 to unicode escape */ + 'Ā' => 'Ā', + '😀' => '😀', + /* Immune chars excluded */ + ',' => ',', + '.' => '.', + '-' => '-', + '_' => '_', + /* Basic alnums excluded */ + 'a' => 'a', + 'A' => 'A', + 'z' => 'z', + 'Z' => 'Z', + '0' => '0', + '9' => '9', + /* Basic control characters and null */ + "\r" => ' ', + "\n" => ' ', + "\t" => ' ', + "\0" => '�', // should use Unicode replacement char + /* Encode chars as named entities where possible */ + '<' => '<', + '>' => '>', + '&' => '&', + '"' => '"', + /* Encode spaces for quoteless attribute protection */ + ' ' => ' ', + ]; + + protected $jsSpecialChars = [ + /* HTML special chars - escape without exception to hex */ + '<' => '\\u003C', + '>' => '\\u003E', + '\'' => '\\u0027', + '"' => '\\u0022', + '&' => '\\u0026', + '/' => '\\/', + /* Characters beyond ASCII value 255 to unicode escape */ + 'Ā' => '\\u0100', + '😀' => '\\uD83D\\uDE00', + /* Immune chars excluded */ + ',' => ',', + '.' => '.', + '_' => '_', + /* Basic alnums excluded */ + 'a' => 'a', + 'A' => 'A', + 'z' => 'z', + 'Z' => 'Z', + '0' => '0', + '9' => '9', + /* Basic control characters and null */ + "\r" => '\r', + "\n" => '\n', + "\x08" => '\b', + "\t" => '\t', + "\x0C" => '\f', + "\0" => '\\u0000', + /* Encode spaces for quoteless attribute protection */ + ' ' => '\\u0020', + ]; + + protected $urlSpecialChars = [ + /* HTML special chars - escape without exception to percent encoding */ + '<' => '%3C', + '>' => '%3E', + '\'' => '%27', + '"' => '%22', + '&' => '%26', + /* Characters beyond ASCII value 255 to hex sequence */ + 'Ā' => '%C4%80', + /* Punctuation and unreserved check */ + ',' => '%2C', + '.' => '.', + '_' => '_', + '-' => '-', + ':' => '%3A', + ';' => '%3B', + '!' => '%21', + /* Basic alnums excluded */ + 'a' => 'a', + 'A' => 'A', + 'z' => 'z', + 'Z' => 'Z', + '0' => '0', + '9' => '9', + /* Basic control characters and null */ + "\r" => '%0D', + "\n" => '%0A', + "\t" => '%09', + "\0" => '%00', + /* PHP quirks from the past */ + ' ' => '%20', + '~' => '~', + '+' => '%2B', + ]; + + protected $cssSpecialChars = [ + /* HTML special chars - escape without exception to hex */ + '<' => '\\3C ', + '>' => '\\3E ', + '\'' => '\\27 ', + '"' => '\\22 ', + '&' => '\\26 ', + /* Characters beyond ASCII value 255 to unicode escape */ + 'Ā' => '\\100 ', + /* Immune chars excluded */ + ',' => '\\2C ', + '.' => '\\2E ', + '_' => '\\5F ', + /* Basic alnums excluded */ + 'a' => 'a', + 'A' => 'A', + 'z' => 'z', + 'Z' => 'Z', + '0' => '0', + '9' => '9', + /* Basic control characters and null */ + "\r" => '\\D ', + "\n" => '\\A ', + "\t" => '\\9 ', + "\0" => '\\0 ', + /* Encode spaces for quoteless attribute protection */ + ' ' => '\\20 ', + ]; + + public function testHtmlEscapingConvertsSpecialChars() + { + foreach ($this->htmlSpecialChars as $key => $value) { + $this->assertEquals($value, (new EscaperRuntime())->escape($key, 'html'), 'Failed to escape: '.$key); + } + } + + public function testHtmlAttributeEscapingConvertsSpecialChars() + { + foreach ($this->htmlAttrSpecialChars as $key => $value) { + $this->assertEquals($value, (new EscaperRuntime())->escape($key, 'html_attr'), 'Failed to escape: '.$key); + } + } + + public function testJavascriptEscapingConvertsSpecialChars() + { + foreach ($this->jsSpecialChars as $key => $value) { + $this->assertEquals($value, (new EscaperRuntime())->escape($key, 'js'), 'Failed to escape: '.$key); + } + } + + public function testJavascriptEscapingConvertsSpecialCharsWithInternalEncoding() + { + $previousInternalEncoding = mb_internal_encoding(); + try { + mb_internal_encoding('ISO-8859-1'); + foreach ($this->jsSpecialChars as $key => $value) { + $this->assertEquals($value, (new EscaperRuntime())->escape($key, 'js'), 'Failed to escape: '.$key); + } + } finally { + if (false !== $previousInternalEncoding) { + mb_internal_encoding($previousInternalEncoding); + } + } + } + + public function testJavascriptEscapingReturnsStringIfZeroLength() + { + $this->assertEquals('', (new EscaperRuntime())->escape('', 'js')); + } + + public function testJavascriptEscapingReturnsStringIfContainsOnlyDigits() + { + $this->assertEquals('123', (new EscaperRuntime())->escape('123', 'js')); + } + + public function testCssEscapingConvertsSpecialChars() + { + foreach ($this->cssSpecialChars as $key => $value) { + $this->assertEquals($value, (new EscaperRuntime())->escape($key, 'css'), 'Failed to escape: '.$key); + } + } + + public function testCssEscapingReturnsStringIfZeroLength() + { + $this->assertEquals('', (new EscaperRuntime())->escape('', 'css')); + } + + public function testCssEscapingReturnsStringIfContainsOnlyDigits() + { + $this->assertEquals('123', (new EscaperRuntime())->escape('123', 'css')); + } + + public function testUrlEscapingConvertsSpecialChars() + { + foreach ($this->urlSpecialChars as $key => $value) { + $this->assertEquals($value, (new EscaperRuntime())->escape($key, 'url'), 'Failed to escape: '.$key); + } + } + + /** + * Range tests to confirm escaped range of characters is within OWASP recommendation. + */ + + /** + * Only testing the first few 2 ranges on this prot. function as that's all these + * other range tests require. + */ + public function testUnicodeCodepointConversionToUtf8() + { + $expected = ' ~ޙ'; + $codepoints = [0x20, 0x7E, 0x799]; + $result = ''; + foreach ($codepoints as $value) { + $result .= $this->codepointToUtf8($value); + } + $this->assertEquals($expected, $result); + } + + /** + * Convert a Unicode Codepoint to a literal UTF-8 character. + * + * @param int $codepoint Unicode codepoint in hex notation + * + * @return string UTF-8 literal string + */ + protected function codepointToUtf8($codepoint) + { + if ($codepoint < 0x80) { + return \chr($codepoint); + } + if ($codepoint < 0x800) { + return \chr($codepoint >> 6 & 0x3F | 0xC0) + .\chr($codepoint & 0x3F | 0x80); + } + if ($codepoint < 0x10000) { + return \chr($codepoint >> 12 & 0x0F | 0xE0) + .\chr($codepoint >> 6 & 0x3F | 0x80) + .\chr($codepoint & 0x3F | 0x80); + } + if ($codepoint < 0x110000) { + return \chr($codepoint >> 18 & 0x07 | 0xF0) + .\chr($codepoint >> 12 & 0x3F | 0x80) + .\chr($codepoint >> 6 & 0x3F | 0x80) + .\chr($codepoint & 0x3F | 0x80); + } + throw new \Exception('Codepoint requested outside of Unicode range.'); + } + + public function testJavascriptEscapingEscapesOwaspRecommendedRanges() + { + $immune = [',', '.', '_']; // Exceptions to escaping ranges + for ($chr = 0; $chr < 0xFF; ++$chr) { + if ($chr >= 0x30 && $chr <= 0x39 + || $chr >= 0x41 && $chr <= 0x5A + || $chr >= 0x61 && $chr <= 0x7A) { + $literal = $this->codepointToUtf8($chr); + $this->assertEquals($literal, (new EscaperRuntime())->escape($literal, 'js')); + } else { + $literal = $this->codepointToUtf8($chr); + if (\in_array($literal, $immune)) { + $this->assertEquals($literal, (new EscaperRuntime())->escape($literal, 'js')); + } else { + $this->assertNotEquals( + $literal, + (new EscaperRuntime())->escape($literal, 'js'), + "$literal should be escaped!"); + } + } + } + } + + public function testHtmlAttributeEscapingEscapesOwaspRecommendedRanges() + { + $immune = [',', '.', '-', '_']; // Exceptions to escaping ranges + for ($chr = 0; $chr < 0xFF; ++$chr) { + if ($chr >= 0x30 && $chr <= 0x39 + || $chr >= 0x41 && $chr <= 0x5A + || $chr >= 0x61 && $chr <= 0x7A) { + $literal = $this->codepointToUtf8($chr); + $this->assertEquals($literal, (new EscaperRuntime())->escape($literal, 'html_attr')); + } else { + $literal = $this->codepointToUtf8($chr); + if (\in_array($literal, $immune)) { + $this->assertEquals($literal, (new EscaperRuntime())->escape($literal, 'html_attr')); + } else { + $this->assertNotEquals( + $literal, + (new EscaperRuntime())->escape($literal, 'html_attr'), + "$literal should be escaped!"); + } + } + } + } + + public function testCssEscapingEscapesOwaspRecommendedRanges() + { + // CSS has no exceptions to escaping ranges + for ($chr = 0; $chr < 0xFF; ++$chr) { + if ($chr >= 0x30 && $chr <= 0x39 + || $chr >= 0x41 && $chr <= 0x5A + || $chr >= 0x61 && $chr <= 0x7A) { + $literal = $this->codepointToUtf8($chr); + $this->assertEquals($literal, (new EscaperRuntime())->escape($literal, 'css')); + } else { + $literal = $this->codepointToUtf8($chr); + $this->assertNotEquals( + $literal, + (new EscaperRuntime())->escape($literal, 'css'), + "$literal should be escaped!"); + } + } + } + + public function testUnknownCustomEscaper() + { + $this->expectException(RuntimeError::class); + + (new EscaperRuntime())->escape('foo', 'bar'); + } + + /** + * @dataProvider provideCustomEscaperCases + */ + public function testCustomEscaper($expected, $string, $strategy) + { + $escaper = new EscaperRuntime(); + $escaper->setEscaper('foo', 'Twig\Tests\foo_escaper_for_test_runtime'); + $this->assertSame($expected, $escaper->escape($string, $strategy)); + } + + public function provideCustomEscaperCases() + { + return [ + ['fooUTF-8', 'foo', 'foo'], + ['UTF-8', null, 'foo'], + ['42UTF-8', 42, 'foo'], + ]; + } + + /** + * @dataProvider provideObjectsForEscaping + */ + public function testObjectEscaping(string $escapedHtml, string $escapedJs, array $safeClasses) + { + $obj = new Extension_TestClass(); + $escaper = new EscaperRuntime(); + $escaper->setSafeClasses($safeClasses); + $this->assertSame($escapedHtml, $escaper->escape($obj, 'html', null, true)); + $this->assertSame($escapedJs, $escaper->escape($obj, 'js', null, true)); + } + + public function provideObjectsForEscaping() + { + return [ + ['<br />', '
', ['\Twig\Tests\Extension_TestClass' => ['js']]], + ['
', '\u003Cbr\u0020\/\u003E', ['\Twig\Tests\Extension_TestClass' => ['html']]], + ['<br />', '
', ['\Twig\Tests\Extension_SafeHtmlInterface' => ['js']]], + ['
', '
', ['\Twig\Tests\Extension_SafeHtmlInterface' => ['all']]], + ]; + } +} + +function foo_escaper_for_test_runtime($string, $charset) +{ + return $string.$charset; +} + +function foo_escaper_for_test1_runtime($string, $charset) +{ + return $string.$charset.'1'; +} + +interface Extension_SafeHtmlInterface +{ +} +class Extension_TestClass implements Extension_SafeHtmlInterface +{ + public function __toString() + { + return '
'; + } +} From 70b2e3ca5d26fd286b71360259aff18ed0f8aad6 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sun, 28 Apr 2024 20:55:06 +0200 Subject: [PATCH 219/812] Fix compat with older versions of PHP --- src/Environment.php | 2 +- src/Extension/EscaperExtension.php | 13 +++++++------ src/Runtime/EscaperRuntime.php | 11 ++++++----- 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/src/Environment.php b/src/Environment.php index 5e0e0c2d255..bb2a257d122 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -131,7 +131,7 @@ public function __construct(LoaderInterface $loader, $options = []) $this->setCache($options['cache']); $this->extensionSet = new ExtensionSet(); $this->defaultRuntimeLoader = new FactoryRuntimeLoader([ - EscaperRuntime::class => fn () => new EscaperRuntime($options['autoescape'], $this->charset), + EscaperRuntime::class => function () use ($options) { return new EscaperRuntime($options['autoescape'], $this->charset); }, ]); $this->addExtension(new CoreExtension()); diff --git a/src/Extension/EscaperExtension.php b/src/Extension/EscaperExtension.php index 7da6854d178..8167f92bb35 100644 --- a/src/Extension/EscaperExtension.php +++ b/src/Extension/EscaperExtension.php @@ -21,12 +21,13 @@ final class EscaperExtension extends AbstractExtension { - private ?Environment $environment = null; - private array $escapers = []; + private $environment; + private $escapers = []; + private $escaper; - public function __construct( - private EscaperRuntime $escaper, - ) { + public function __construct(EscaperRuntime $escaper) + { + $this->escaper = $escaper; } public function getTokenParsers(): array @@ -94,7 +95,7 @@ public function setEscaper($strategy, callable $callable) { trigger_deprecation('twig/twig', '3.10', 'The "%s()" method is deprecated, use the "Twig\Runtime\EscaperRuntime::setEscaper()" method instead (be warned that Environment is not passed anymore to the callable).', __METHOD__); - if (!$this->environment) { + if (!isset($this->environment)) { throw new \LogicException('You must call setEnvironment() before calling setEscaper().'); } diff --git a/src/Runtime/EscaperRuntime.php b/src/Runtime/EscaperRuntime.php index 6b55c7fb346..0124bfe247e 100644 --- a/src/Runtime/EscaperRuntime.php +++ b/src/Runtime/EscaperRuntime.php @@ -27,16 +27,17 @@ final class EscaperRuntime implements RuntimeExtensionInterface /** @internal */ public $safeLookup = []; + private $charset; + /** * @param string|false|callable $defaultStrategy An escaping strategy * * @see setDefaultStrategy() */ - public function __construct( - $defaultStrategy = 'html', - private string $charset = 'UTF-8', - ) { + public function __construct($defaultStrategy = 'html', $charset = 'UTF-8') + { $this->setDefaultStrategy($defaultStrategy); + $this->charset = $charset; } /** @@ -127,7 +128,7 @@ public function addSafeClass(string $class, array $strategies) * * @throws RuntimeException */ - public function escape($string, string $strategy = 'html', ?string $charset = null, bool $autoescape = false): mixed + public function escape($string, string $strategy = 'html', ?string $charset = null, bool $autoescape = false) { if ($autoescape && $string instanceof Markup) { return $string; From d728d1b8a6fa8cdd603a3940be069fad902d648d Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Mon, 29 Apr 2024 08:09:07 +0200 Subject: [PATCH 220/812] Add more information about migrating away from twig_escape_filter() --- CHANGELOG | 7 ++++++- doc/deprecated.rst | 14 ++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index b3921e2dece..98aca75708a 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,11 @@ # 3.10.0 (2024-XX-XX) - * Extract the escaping logic to a runtime + * Extract the escaping logic from the `EscapingExtension` class to a new `EscapingRuntime` class. + + The following methods from ``Twig\\Extension\\EscaperExtension`` are + deprecated: ``setEscaper()``, ``getEscapers()``, ``setDefaultStrategy()``, + ``getDefaultStrategy()``, ``setSafeClasses``, ``addSafeClasses()``. Use the + same methods on the ``Twig\\Runtime\\EscaperRuntime`` class instead. # 3.9.3 (2024-04-18) diff --git a/doc/deprecated.rst b/doc/deprecated.rst index 06a532bd010..275a334d6c8 100644 --- a/doc/deprecated.rst +++ b/doc/deprecated.rst @@ -18,6 +18,20 @@ Extensions 3.9.0, and will be removed in Twig 4.0. They have been replaced by internal methods on their respective extension classes. + If you were using the ``twig_escape_filter()`` function is your code, use + ``$env->getRuntime(EscaperRuntime::class)->escape()`` instead. + +* The following methods from ``Twig\Extension\EscaperExtension`` are + deprecated: ``setEscaper()``, ``getEscapers()``, ``setDefaultStrategy()``, + ``getDefaultStrategy()``, ``setSafeClasses``, ``addSafeClasses()``. Use the + same methods on the ``Twig\Runtime\EscaperRuntime`` class instead. + + Before: + $twig->getExtension(EscaperExtension::class)->METHOD() + + After: + $twig->getRuntime(EscaperRuntime::class)->METHOD(); + Node Visitors ------------- From e4a78534ffd391f47a6fbca202545f74c6428635 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Mon, 29 Apr 2024 14:25:26 +0200 Subject: [PATCH 221/812] Add missing @deprecated tags --- src/Extension/EscaperExtension.php | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/Extension/EscaperExtension.php b/src/Extension/EscaperExtension.php index 8167f92bb35..cbac1f6a011 100644 --- a/src/Extension/EscaperExtension.php +++ b/src/Extension/EscaperExtension.php @@ -49,6 +49,9 @@ public function getFilters(): array ]; } + /** + * @deprecated since Twig 3.10 + */ public function setEnvironment(Environment $environment): void { trigger_deprecation('twig/twig', '3.10', 'The "%s()" method is deprecated and not needed if you are using methods from "Twig\Runtime\EscaperRuntime".', __METHOD__); @@ -63,6 +66,8 @@ public function setEnvironment(Environment $environment): void * name as an argument and returns the strategy to use. * * @param string|false|callable $defaultStrategy An escaping strategy + * + * @deprecated since Twig 3.10 */ public function setDefaultStrategy($defaultStrategy): void { @@ -77,6 +82,8 @@ public function setDefaultStrategy($defaultStrategy): void * @param string $name The template name * * @return string|false The default strategy to use for the template + * + * @deprecated since Twig 3.10 */ public function getDefaultStrategy(string $name) { @@ -90,6 +97,8 @@ public function getDefaultStrategy(string $name) * * @param string $strategy The strategy name that should be used as a strategy in the escape call * @param callable(Environment, string) $callable A valid PHP callable + * + * @deprecated since Twig 3.10 */ public function setEscaper($strategy, callable $callable) { @@ -111,6 +120,8 @@ public function setEscaper($strategy, callable $callable) * Gets all defined escapers. * * @return array An array of escapers + * + * @deprecated since Twig 3.10 */ public function getEscapers() { @@ -119,6 +130,9 @@ public function getEscapers() return $this->escapers; } + /** + * @deprecated since Twig 3.10 + */ public function setSafeClasses(array $safeClasses = []) { trigger_deprecation('twig/twig', '3.10', 'The "%s()" method is deprecated, use the "Twig\Runtime\EscaperRuntime::setSafeClasses()" method instead.', __METHOD__); @@ -126,6 +140,9 @@ public function setSafeClasses(array $safeClasses = []) $this->escaper->setSafeClasses($safeClasses); } + /** + * @deprecated since Twig 3.10 + */ public function addSafeClass(string $class, array $strategies) { trigger_deprecation('twig/twig', '3.10', 'The "%s()" method is deprecated, use the "Twig\Runtime\EscaperRuntime::addSafeClass()" method instead.', __METHOD__); From a517c14f199f86b333ad09f0b142bdbf6467e002 Mon Sep 17 00:00:00 2001 From: Kevin Bond Date: Tue, 23 Jan 2024 09:42:53 -0500 Subject: [PATCH 222/812] [html-extra] filter classes --- extra/html-extra/HtmlExtension.php | 2 +- extra/html-extra/Tests/Fixtures/html_classes.test | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/extra/html-extra/HtmlExtension.php b/extra/html-extra/HtmlExtension.php index d5842bf500c..28873b30f08 100644 --- a/extra/html-extra/HtmlExtension.php +++ b/extra/html-extra/HtmlExtension.php @@ -108,6 +108,6 @@ public static function htmlClasses(...$args): string } } - return implode(' ', array_unique($classes)); + return implode(' ', array_unique(array_filter($classes, static function($v) { return '' !== $v; }))); } } diff --git a/extra/html-extra/Tests/Fixtures/html_classes.test b/extra/html-extra/Tests/Fixtures/html_classes.test index 8a5304cf63d..65ecaba6ada 100644 --- a/extra/html-extra/Tests/Fixtures/html_classes.test +++ b/extra/html-extra/Tests/Fixtures/html_classes.test @@ -1,12 +1,12 @@ --TEST-- "html_classes" function --TEMPLATE-- -{{ html_classes('a', {'b': true, 'c': false}, 'd') }} +{{ html_classes('a', {'b': true, 'c': false}, 'd', false ? 'e', true ? 'f', '0') }} {% set class_a = 'a' %} {% set class_b = 'b' %} {{ html_classes(class_a, {(class_b): true})}} --DATA-- return [] --EXPECT-- -a b d +a b d f 0 a b From 83fe22ab159346d83490e0ba0f74952c1554c722 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Tue, 30 Apr 2024 12:36:06 +0200 Subject: [PATCH 223/812] Get rid of weird code --- src/Node/CheckSecurityNode.php | 39 +++++++++++++++++----------------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/src/Node/CheckSecurityNode.php b/src/Node/CheckSecurityNode.php index 991a428a42d..3525c9f9586 100644 --- a/src/Node/CheckSecurityNode.php +++ b/src/Node/CheckSecurityNode.php @@ -26,41 +26,30 @@ class CheckSecurityNode extends Node public function __construct(array $usedFilters, array $usedTags, array $usedFunctions) { - $this->usedFilters = $usedFilters; - $this->usedTags = $usedTags; - $this->usedFunctions = $usedFunctions; + $this->usedFilters = $this->collect($usedFilters); + $this->usedTags = $this->collect($usedTags); + $this->usedFunctions = $this->collect($usedFunctions); parent::__construct(); } public function compile(Compiler $compiler): void { - $tags = $filters = $functions = []; - foreach (['tags', 'filters', 'functions'] as $type) { - foreach ($this->{'used'.ucfirst($type)} as $name => $node) { - if ($node instanceof Node) { - ${$type}[$name] = $node->getTemplateLine(); - } else { - ${$type}[$node] = null; - } - } - } - $compiler ->write("\n") ->write("public function checkSecurity()\n") ->write("{\n") ->indent() - ->write('static $tags = ')->repr(array_filter($tags))->raw(";\n") - ->write('static $filters = ')->repr(array_filter($filters))->raw(";\n") - ->write('static $functions = ')->repr(array_filter($functions))->raw(";\n\n") + ->write('static $tags = ')->repr(array_filter($this->usedTags))->raw(";\n") + ->write('static $filters = ')->repr(array_filter($this->usedFilters))->raw(";\n") + ->write('static $functions = ')->repr(array_filter($this->usedFunctions))->raw(";\n\n") ->write("try {\n") ->indent() ->write("\$this->sandbox->checkSecurity(\n") ->indent() - ->write(!$tags ? "[],\n" : "['".implode("', '", array_keys($tags))."'],\n") - ->write(!$filters ? "[],\n" : "['".implode("', '", array_keys($filters))."'],\n") - ->write(!$functions ? "[],\n" : "['".implode("', '", array_keys($functions))."'],\n") + ->write(!$this->usedTags ? "[],\n" : "['".implode("', '", array_keys($this->usedTags))."'],\n") + ->write(!$this->usedFilters ? "[],\n" : "['".implode("', '", array_keys($this->usedFilters))."'],\n") + ->write(!$this->usedFunctions ? "[],\n" : "['".implode("', '", array_keys($this->usedFunctions))."'],\n") ->write("\$this->source\n") ->outdent() ->write(");\n") @@ -88,4 +77,14 @@ public function compile(Compiler $compiler): void ->write("}\n") ; } + + private function collect(array $used) + { + $collected = []; + foreach ($used as $name => $node) { + $collected[$name] = $node instanceof Node ? $node->getTemplateLine() : null; + } + + return $collected; + } } From 469d52d1b80d22ac6b462fcf3933cef11184e195 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Tue, 30 Apr 2024 13:40:56 +0200 Subject: [PATCH 224/812] Simplify sandbox code --- src/Node/CheckSecurityNode.php | 21 ++++++++------------- src/NodeVisitor/SandboxNodeVisitor.php | 11 +++++++---- 2 files changed, 15 insertions(+), 17 deletions(-) diff --git a/src/Node/CheckSecurityNode.php b/src/Node/CheckSecurityNode.php index 3525c9f9586..6e591aad40a 100644 --- a/src/Node/CheckSecurityNode.php +++ b/src/Node/CheckSecurityNode.php @@ -24,11 +24,16 @@ class CheckSecurityNode extends Node private $usedTags; private $usedFunctions; + /** + * @param array $usedFilters + * @param array $usedTags + * @param array $usedFunctions + */ public function __construct(array $usedFilters, array $usedTags, array $usedFunctions) { - $this->usedFilters = $this->collect($usedFilters); - $this->usedTags = $this->collect($usedTags); - $this->usedFunctions = $this->collect($usedFunctions); + $this->usedFilters = $usedFilters; + $this->usedTags = $usedTags; + $this->usedFunctions = $usedFunctions; parent::__construct(); } @@ -77,14 +82,4 @@ public function compile(Compiler $compiler): void ->write("}\n") ; } - - private function collect(array $used) - { - $collected = []; - foreach ($used as $name => $node) { - $collected[$name] = $node instanceof Node ? $node->getTemplateLine() : null; - } - - return $collected; - } } diff --git a/src/NodeVisitor/SandboxNodeVisitor.php b/src/NodeVisitor/SandboxNodeVisitor.php index 1446cee6b98..d1108394fdf 100644 --- a/src/NodeVisitor/SandboxNodeVisitor.php +++ b/src/NodeVisitor/SandboxNodeVisitor.php @@ -34,8 +34,11 @@ final class SandboxNodeVisitor implements NodeVisitorInterface { private $inAModule = false; + /** @var array */ private $tags; + /** @var array */ private $filters; + /** @var array */ private $functions; private $needsToStringWrap = false; @@ -51,22 +54,22 @@ public function enterNode(Node $node, Environment $env): Node } elseif ($this->inAModule) { // look for tags if ($node->getNodeTag() && !isset($this->tags[$node->getNodeTag()])) { - $this->tags[$node->getNodeTag()] = $node; + $this->tags[$node->getNodeTag()] = $node->getTemplateLine(); } // look for filters if ($node instanceof FilterExpression && !isset($this->filters[$node->getNode('filter')->getAttribute('value')])) { - $this->filters[$node->getNode('filter')->getAttribute('value')] = $node; + $this->filters[$node->getNode('filter')->getAttribute('value')] = $node->getTemplateLine(); } // look for functions if ($node instanceof FunctionExpression && !isset($this->functions[$node->getAttribute('name')])) { - $this->functions[$node->getAttribute('name')] = $node; + $this->functions[$node->getAttribute('name')] = $node->getTemplateLine(); } // the .. operator is equivalent to the range() function if ($node instanceof RangeBinary && !isset($this->functions['range'])) { - $this->functions['range'] = $node; + $this->functions['range'] = $node->getTemplateLine(); } if ($node instanceof PrintNode) { From e142e3bf78c5cf9a8d63a5e269627cebd18a4a54 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Tue, 30 Apr 2024 13:43:26 +0200 Subject: [PATCH 225/812] Fix type --- src/Test/IntegrationTestCase.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Test/IntegrationTestCase.php b/src/Test/IntegrationTestCase.php index 570c378bbe0..d0731ce19d5 100644 --- a/src/Test/IntegrationTestCase.php +++ b/src/Test/IntegrationTestCase.php @@ -188,7 +188,7 @@ protected function doIntegrationTest($file, $message, $condition, $templates, $e // avoid using the same PHP class name for different cases $p = new \ReflectionProperty($twig, 'templateClassPrefix'); $p->setAccessible(true); - $p->setValue($twig, '__TwigTemplate_'.hash(\PHP_VERSION_ID < 80100 ? 'sha256' : 'xxh128', uniqid(mt_rand(), true), false).'_'); + $p->setValue($twig, '__TwigTemplate_'.hash(\PHP_VERSION_ID < 80100 ? 'sha256' : 'xxh128', uniqid((string) mt_rand(), true), false).'_'); $deprecations = []; try { From c2fc437f6a5da5123e0a8ad8eb6cc6dbe7f06bdd Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Tue, 30 Apr 2024 15:23:35 +0200 Subject: [PATCH 226/812] Fix a test --- src/Extension/CoreExtension.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index c7c6d14fc50..017cec79fc1 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -498,7 +498,7 @@ public static function dateConverter(Environment $env, $date = null, $timezone = return false !== $timezone ? $date->setTimezone($timezone) : $date; } - if ($date instanceof \DateTimeInterface) { + if ($date instanceof \DateTime) { $date = clone $date; if (false !== $timezone) { $date->setTimezone($timezone); From fa714ce6bd0d2d8932df4c694e8966d50d35aeaf Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Tue, 30 Apr 2024 15:28:33 +0200 Subject: [PATCH 227/812] Fix CS --- src/Extension/CoreExtension.php | 2 +- tests/Fixtures/functions/cycle_empty.test | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index 017cec79fc1..85aa9078d4d 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -329,7 +329,7 @@ public static function cycle($values, $position) } if (!\count($values)) { - throw new RuntimeError('The "cycle" function does not work on empty arrays'); + throw new RuntimeError('The "cycle" function does not work on empty arrays.'); } return $values[$position % \count($values)]; diff --git a/tests/Fixtures/functions/cycle_empty.test b/tests/Fixtures/functions/cycle_empty.test index 1eaffc8e5f6..bb338452a6b 100644 --- a/tests/Fixtures/functions/cycle_empty.test +++ b/tests/Fixtures/functions/cycle_empty.test @@ -5,4 +5,4 @@ --DATA-- return [] --EXCEPTION-- -Twig\Error\RuntimeError: The "cycle" function does not work on empty arrays in "index.twig" at line 2 +Twig\Error\RuntimeError: The "cycle" function does not work on empty arrays in "index.twig" at line 2. From 82f8864e7b634bc7c382198398b4f73e79848ccb Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Tue, 30 Apr 2024 15:38:55 +0200 Subject: [PATCH 228/812] Fix a @return type hint --- src/Extension/CoreExtension.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index 85aa9078d4d..e6a72560c58 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -478,7 +478,7 @@ public static function sprintf($format, ...$values) * @param \DateTimeInterface|string|null $date A date or null to use the current time * @param \DateTimeZone|string|false|null $timezone The target timezone, null to use the default, false to leave unchanged * - * @return \DateTimeInterface + * @return \DateTime|\DateTimeImmutable * * @internal */ From 8c43456ac66eb6e11729f50602f39a25cc3e95cf Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Tue, 30 Apr 2024 15:57:58 +0200 Subject: [PATCH 229/812] Fix capturing output from extensions that still use echo --- src/Node/CaptureNode.php | 3 ++- tests/EnvironmentTest.php | 4 ++-- tests/Node/MacroTest.php | 3 ++- tests/Node/SetTest.php | 6 ++++-- 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/Node/CaptureNode.php b/src/Node/CaptureNode.php index 7c187727ade..534f1d01f17 100644 --- a/src/Node/CaptureNode.php +++ b/src/Node/CaptureNode.php @@ -43,8 +43,9 @@ public function compile(Compiler $compiler): void $compiler ->indent() ->subcompile($this->getNode('body')) + ->write("return; yield '';\n") ->outdent() - ->write("})() ?? new \EmptyIterator()") + ->write('})()') ; if ($useYield) { $compiler->raw(', false))'); diff --git a/tests/EnvironmentTest.php b/tests/EnvironmentTest.php index e7eb26e4c21..57a3bf34d85 100644 --- a/tests/EnvironmentTest.php +++ b/tests/EnvironmentTest.php @@ -414,7 +414,7 @@ public function testUndefinedTokenParserCallback() */ public function testLegacyEchoingNode() { - $loader = new ArrayLoader(['echo_bar' => 'A{% set v %}B{% test %}C{% endset %}D{% test %}E{{ v }}F']); + $loader = new ArrayLoader(['echo_bar' => 'A{% set v %}B{% test %}C{% endset %}D{% test %}E{{ v }}F{% set w %}{% test %}{% endset %}G{{ w }}H']); $twig = new Environment($loader); $twig->addExtension(new EnvironmentTest_Extension()); @@ -430,7 +430,7 @@ public function testLegacyEchoingNode() ); } - $this->assertSame('ADbarEBbarCF', $twig->render('echo_bar')); + $this->assertSame('ADbarEBbarCFGbarH', $twig->render('echo_bar')); } protected function getMockLoader($templateName, $templateContent) diff --git a/tests/Node/MacroTest.php b/tests/Node/MacroTest.php index 88ce9b299e2..ce51187be8c 100644 --- a/tests/Node/MacroTest.php +++ b/tests/Node/MacroTest.php @@ -60,7 +60,8 @@ public function macro_foo(\$__foo__ = null, \$__bar__ = "Foo", ...\$__varargs__) return new Markup(implode('', iterator_to_array((function () use (\$context, \$macros, \$blocks) { yield "foo"; - })() ?? new \EmptyIterator(), false)), \$this->env->getCharset()); + return; yield ''; + })(), false)), \$this->env->getCharset()); } EOF , new Environment(new ArrayLoader()), diff --git a/tests/Node/SetTest.php b/tests/Node/SetTest.php index ed6ee9ebe19..f250b80eed3 100644 --- a/tests/Node/SetTest.php +++ b/tests/Node/SetTest.php @@ -57,7 +57,8 @@ public function getTests() // line 1 \$context["foo"] = ('' === \$tmp = implode('', iterator_to_array((function () use (&\$context, \$macros, \$blocks) { yield "foo"; -})() ?? new \EmptyIterator(), false))) ? '' : new Markup(\$tmp, \$this->env->getCharset()); + return; yield ''; +})(), false))) ? '' : new Markup(\$tmp, \$this->env->getCharset()); EOF , new Environment(new ArrayLoader()), ]; @@ -66,7 +67,8 @@ public function getTests() // line 1 $context["foo"] = ('' === $tmp = \Twig\Extension\CoreExtension::captureOutput((function () use (&$context, $macros, $blocks) { yield "foo"; -})() ?? new \EmptyIterator())) ? '' : new Markup($tmp, $this->env->getCharset()); + return; yield ''; +})())) ? '' : new Markup($tmp, $this->env->getCharset()); EOF , new Environment(new ArrayLoader()), ]; From ead768a4957f488bb9bef9480d33920e20af61b5 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Wed, 1 May 2024 09:11:02 +0200 Subject: [PATCH 230/812] - --- CHANGELOG | 3 ++- doc/deprecated.rst | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 98aca75708a..b48cc10c6ca 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,7 @@ # 3.10.0 (2024-XX-XX) - * Extract the escaping logic from the `EscapingExtension` class to a new `EscapingRuntime` class. + * Extract the escaping logic from the `EscapingExtension` class to a new + `EscapingRuntime` class. The following methods from ``Twig\\Extension\\EscaperExtension`` are deprecated: ``setEscaper()``, ``getEscapers()``, ``setDefaultStrategy()``, diff --git a/doc/deprecated.rst b/doc/deprecated.rst index 275a334d6c8..e770f3ff607 100644 --- a/doc/deprecated.rst +++ b/doc/deprecated.rst @@ -18,7 +18,7 @@ Extensions 3.9.0, and will be removed in Twig 4.0. They have been replaced by internal methods on their respective extension classes. - If you were using the ``twig_escape_filter()`` function is your code, use + If you were using the ``twig_escape_filter()`` function in your code, use ``$env->getRuntime(EscaperRuntime::class)->escape()`` instead. * The following methods from ``Twig\Extension\EscaperExtension`` are From 16acdf69fe4487c03557b29cd31a7da3006d4ceb Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Wed, 1 May 2024 10:00:34 +0200 Subject: [PATCH 231/812] Rename some internal methods --- src/Extension/CoreExtension.php | 207 ++++++++---------------- src/Node/Expression/ArrayExpression.php | 2 +- src/Node/IncludeNode.php | 2 +- src/Resources/core.php | 46 +++--- tests/Extension/CoreTest.php | 4 +- tests/Node/Expression/FilterTest.php | 10 +- tests/Node/Expression/FunctionTest.php | 2 +- tests/Node/IncludeTest.php | 2 +- 8 files changed, 98 insertions(+), 177 deletions(-) diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index e6a72560c58..8f45bfc387f 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -185,50 +185,50 @@ public function getFilters(): array { return [ // formatting filters - new TwigFilter('date', [self::class, 'dateFormatFilter'], ['needs_environment' => true]), - new TwigFilter('date_modify', [self::class, 'dateModifyFilter'], ['needs_environment' => true]), + new TwigFilter('date', [self::class, 'formatDate'], ['needs_environment' => true]), + new TwigFilter('date_modify', [self::class, 'modifyDate'], ['needs_environment' => true]), new TwigFilter('format', [self::class, 'sprintf']), - new TwigFilter('replace', [self::class, 'replaceFilter']), - new TwigFilter('number_format', [self::class, 'numberFormatFilter'], ['needs_environment' => true]), + new TwigFilter('replace', [self::class, 'replace']), + new TwigFilter('number_format', [self::class, 'formatNumber'], ['needs_environment' => true]), new TwigFilter('abs', 'abs'), new TwigFilter('round', [self::class, 'round']), // encoding - new TwigFilter('url_encode', [self::class, 'urlencodeFilter']), + new TwigFilter('url_encode', [self::class, 'urlencode']), new TwigFilter('json_encode', 'json_encode'), new TwigFilter('convert_encoding', [self::class, 'convertEncoding']), // string filters - new TwigFilter('title', [self::class, 'titleStringFilter'], ['needs_environment' => true]), - new TwigFilter('capitalize', [self::class, 'capitalizeStringFilter'], ['needs_environment' => true]), - new TwigFilter('upper', [self::class, 'upperFilter'], ['needs_environment' => true]), - new TwigFilter('lower', [self::class, 'lowerFilter'], ['needs_environment' => true]), + new TwigFilter('title', [self::class, 'titleCase'], ['needs_environment' => true]), + new TwigFilter('capitalize', [self::class, 'capitalize'], ['needs_environment' => true]), + new TwigFilter('upper', [self::class, 'upper'], ['needs_environment' => true]), + new TwigFilter('lower', [self::class, 'lower'], ['needs_environment' => true]), new TwigFilter('striptags', [self::class, 'striptags']), - new TwigFilter('trim', [self::class, 'trimFilter']), + new TwigFilter('trim', [self::class, 'trim']), new TwigFilter('nl2br', [self::class, 'nl2br'], ['pre_escape' => 'html', 'is_safe' => ['html']]), new TwigFilter('spaceless', [self::class, 'spaceless'], ['is_safe' => ['html']]), // array helpers - new TwigFilter('join', [self::class, 'joinFilter']), - new TwigFilter('split', [self::class, 'splitFilter'], ['needs_environment' => true]), - new TwigFilter('sort', [self::class, 'sortFilter'], ['needs_environment' => true]), - new TwigFilter('merge', [self::class, 'arrayMerge']), - new TwigFilter('batch', [self::class, 'arrayBatch']), - new TwigFilter('column', [self::class, 'arrayColumn']), - new TwigFilter('filter', [self::class, 'arrayFilter'], ['needs_environment' => true]), - new TwigFilter('map', [self::class, 'arrayMap'], ['needs_environment' => true]), - new TwigFilter('reduce', [self::class, 'arrayReduce'], ['needs_environment' => true]), + new TwigFilter('join', [self::class, 'join']), + new TwigFilter('split', [self::class, 'split'], ['needs_environment' => true]), + new TwigFilter('sort', [self::class, 'sort'], ['needs_environment' => true]), + new TwigFilter('merge', [self::class, 'merge']), + new TwigFilter('batch', [self::class, 'batch']), + new TwigFilter('column', [self::class, 'column']), + new TwigFilter('filter', [self::class, 'filter'], ['needs_environment' => true]), + new TwigFilter('map', [self::class, 'map'], ['needs_environment' => true]), + new TwigFilter('reduce', [self::class, 'reduce'], ['needs_environment' => true]), // string/array filters - new TwigFilter('reverse', [self::class, 'reverseFilter'], ['needs_environment' => true]), - new TwigFilter('length', [self::class, 'lengthFilter'], ['needs_environment' => true]), + new TwigFilter('reverse', [self::class, 'reverse'], ['needs_environment' => true]), + new TwigFilter('length', [self::class, 'length'], ['needs_environment' => true]), new TwigFilter('slice', [self::class, 'slice'], ['needs_environment' => true]), new TwigFilter('first', [self::class, 'first'], ['needs_environment' => true]), new TwigFilter('last', [self::class, 'last'], ['needs_environment' => true]), // iteration and runtime - new TwigFilter('default', [self::class, 'defaultFilter'], ['node_class' => DefaultFilter::class]), - new TwigFilter('keys', [self::class, 'getArrayKeysFilter']), + new TwigFilter('default', [self::class, 'default'], ['node_class' => DefaultFilter::class]), + new TwigFilter('keys', [self::class, 'keys']), ]; } @@ -241,7 +241,7 @@ public function getFunctions(): array new TwigFunction('constant', [self::class, 'constant']), new TwigFunction('cycle', [self::class, 'cycle']), new TwigFunction('random', [self::class, 'random'], ['needs_environment' => true]), - new TwigFunction('date', [self::class, 'dateConverter'], ['needs_environment' => true]), + new TwigFunction('date', [self::class, 'convertDate'], ['needs_environment' => true]), new TwigFunction('include', [self::class, 'include'], ['needs_environment' => true, 'needs_context' => true, 'is_safe' => ['all']]), new TwigFunction('source', [self::class, 'source'], ['needs_environment' => true, 'is_safe' => ['all']]), ]; @@ -322,7 +322,7 @@ public function getOperators(): array * * @internal */ - public static function cycle($values, $position) + public static function cycle($values, $position): string { if (!\is_array($values) && !$values instanceof \ArrayAccess) { return $values; @@ -408,7 +408,7 @@ public static function random(Environment $env, $values = null, $max = null) } /** - * Converts a date to the given format. + * Formats a date. * * {{ post.published_at|date("m/d/Y") }} * @@ -416,11 +416,9 @@ public static function random(Environment $env, $values = null, $max = null) * @param string|null $format The target format, null to use the default * @param \DateTimeZone|string|false|null $timezone The target timezone, null to use the default, false to leave unchanged * - * @return string The formatted date - * * @internal */ - public static function dateFormatFilter(Environment $env, $date, $format = null, $timezone = null) + public static function formatDate(Environment $env, $date, $format = null, $timezone = null): string { if (null === $format) { $formats = $env->getExtension(self::class)->getDateFormat(); @@ -431,7 +429,7 @@ public static function dateFormatFilter(Environment $env, $date, $format = null, return $date->format($format); } - return self::dateConverter($env, $date, $timezone)->format($format); + return self::convertDate($env, $date, $timezone)->format($format); } /** @@ -442,15 +440,13 @@ public static function dateFormatFilter(Environment $env, $date, $format = null, * @param \DateTimeInterface|string $date A date * @param string $modifier A modifier string * - * @return \DateTimeInterface + * @return \DateTime|\DateTimeImmutable * * @internal */ - public static function dateModifyFilter(Environment $env, $date, $modifier) + public static function modifyDate(Environment $env, $date, $modifier) { - $date = self::dateConverter($env, $date, false); - - return $date->modify($modifier); + return self::convertDate($env, $date, false)->modify($modifier); } /** @@ -459,11 +455,9 @@ public static function dateModifyFilter(Environment $env, $date, $modifier) * @param string|null $format * @param ...$values * - * @return string - * * @internal */ - public static function sprintf($format, ...$values) + public static function sprintf($format, ...$values): string { return sprintf($format ?? '', ...$values); } @@ -482,7 +476,7 @@ public static function sprintf($format, ...$values) * * @internal */ - public static function dateConverter(Environment $env, $date = null, $timezone = null) + public static function convertDate(Environment $env, $date = null, $timezone = null) { // determine the timezone if (false !== $timezone) { @@ -535,11 +529,9 @@ public static function dateConverter(Environment $env, $date = null, $timezone = * @param string|null $str String to replace in * @param array|\Traversable $from Replace values * - * @return string - * * @internal */ - public static function replaceFilter($str, $from) + public static function replace($str, $from): string { if (!is_iterable($from)) { throw new RuntimeError(sprintf('The "replace" filter expects an array or "Traversable" as replace values, got "%s".', \is_object($from) ? \get_class($from) : \gettype($from))); @@ -586,11 +578,9 @@ public static function round($value, $precision = 0, $method = 'common') * @param string|null $decimalPoint the character(s) to use for the decimal point * @param string|null $thousandSep the character(s) to use for the thousands separator * - * @return string The formatted number - * * @internal */ - public static function numberFormatFilter(Environment $env, $number, $decimal = null, $decimalPoint = null, $thousandSep = null) + public static function formatNumber(Environment $env, $number, $decimal = null, $decimalPoint = null, $thousandSep = null): string { $defaults = $env->getExtension(self::class)->getNumberFormat(); if (null === $decimal) { @@ -613,11 +603,9 @@ public static function numberFormatFilter(Environment $env, $number, $decimal = * * @param string|array|null $url A URL or an array of query parameters * - * @return string The URL encoded value - * * @internal */ - public static function urlencodeFilter($url) + public static function urlencode($url): string { if (\is_array($url)) { return http_build_query($url, '', '&', \PHP_QUERY_RFC3986); @@ -637,11 +625,9 @@ public static function urlencodeFilter($url) * * @param array|\Traversable ...$arrays Any number of arrays or Traversable objects to merge * - * @return array The merged array - * * @internal */ - public static function arrayMerge(...$arrays) + public static function merge(...$arrays): array { $result = []; @@ -743,11 +729,9 @@ public static function last(Environment $env, $item) * @param string $glue The separator * @param string|null $and The separator for the last pair * - * @return string The concatenated string - * * @internal */ - public static function joinFilter($value, $glue = '', $and = null) + public static function join($value, $glue = '', $and = null): string { if (!is_iterable($value)) { $value = (array) $value; @@ -789,11 +773,9 @@ public static function joinFilter($value, $glue = '', $and = null) * @param string $delimiter The delimiter * @param int|null $limit The limit * - * @return array The split string as an array - * * @internal */ - public static function splitFilter(Environment $env, $value, $delimiter, $limit = null) + public static function split(Environment $env, $value, $delimiter, $limit = null): array { $value = $value ?? ''; @@ -824,7 +806,7 @@ public static function splitFilter(Environment $env, $value, $delimiter, $limit /** * @internal */ - public static function defaultFilter($value, $default = '') + public static function default($value, $default = '') { if (self::testEmpty($value)) { return $default; @@ -842,13 +824,9 @@ public static function defaultFilter($value, $default = '') * {# ... #} * {% endfor %} * - * @param array $array An array - * - * @return array The keys - * * @internal */ - public static function getArrayKeysFilter($array) + public static function keys($array): array { if ($array instanceof \Traversable) { while ($array instanceof \IteratorAggregate) { @@ -890,7 +868,7 @@ public static function getArrayKeysFilter($array) * * @internal */ - public static function reverseFilter(Environment $env, $item, $preserveKeys = false) + public static function reverse(Environment $env, $item, $preserveKeys = false) { if ($item instanceof \Traversable) { return array_reverse(iterator_to_array($item), $preserveKeys); @@ -924,11 +902,9 @@ public static function reverseFilter(Environment $env, $item, $preserveKeys = fa * * @param array|\Traversable $array * - * @return array - * * @internal */ - public static function sortFilter(Environment $env, $array, $arrow = null) + public static function sort(Environment $env, $array, $arrow = null): array { if ($array instanceof \Traversable) { $array = iterator_to_array($array); @@ -1057,13 +1033,11 @@ public static function compare($a, $b) } /** - * @return int - * * @throws RuntimeError When an invalid pattern is used * * @internal */ - public static function matches(string $regexp, ?string $str) + public static function matches(string $regexp, ?string $str): int { set_error_handler(function ($t, $m) use ($regexp) { throw new RuntimeError(sprintf('Regexp "%s" passed to "matches" is not valid', $regexp).substr($m, 12)); @@ -1082,13 +1056,11 @@ public static function matches(string $regexp, ?string $str) * @param string|null $characterMask * @param string $side * - * @return string - * * @throws RuntimeError When an invalid trimming side is used (not a string or not 'left', 'right', or 'both') * * @internal */ - public static function trimFilter($string, $characterMask = null, $side = 'both') + public static function trim($string, $characterMask = null, $side = 'both'): string { if (null === $characterMask) { $characterMask = " \t\n\r\0\x0B"; @@ -1111,11 +1083,9 @@ public static function trimFilter($string, $characterMask = null, $side = 'both' * * @param string|null $string * - * @return string - * * @internal */ - public static function nl2br($string) + public static function nl2br($string): string { return nl2br($string ?? ''); } @@ -1125,11 +1095,9 @@ public static function nl2br($string) * * @param string|null $content * - * @return string - * * @internal */ - public static function spaceless($content) + public static function spaceless($content): string { return trim(preg_replace('/>\s+<', $content ?? '')); } @@ -1139,11 +1107,9 @@ public static function spaceless($content) * @param string $to * @param string $from * - * @return string - * * @internal */ - public static function convertEncoding($string, $to, $from) + public static function convertEncoding($string, $to, $from): string { if (!\function_exists('iconv')) { throw new RuntimeError('Unable to convert encoding: required function iconv() does not exist. You should install ext-iconv or symfony/polyfill-iconv.'); @@ -1157,11 +1123,9 @@ public static function convertEncoding($string, $to, $from) * * @param mixed $thing A variable * - * @return int The length of the value - * * @internal */ - public static function lengthFilter(Environment $env, $thing) + public static function length(Environment $env, $thing): int { if (null === $thing) { return 0; @@ -1191,11 +1155,9 @@ public static function lengthFilter(Environment $env, $thing) * * @param string|null $string A string * - * @return string The uppercased string - * * @internal */ - public static function upperFilter(Environment $env, $string) + public static function upper(Environment $env, $string): string { return mb_strtoupper($string ?? '', $env->getCharset()); } @@ -1205,11 +1167,9 @@ public static function upperFilter(Environment $env, $string) * * @param string|null $string A string * - * @return string The lowercased string - * * @internal */ - public static function lowerFilter(Environment $env, $string) + public static function lower(Environment $env, $string): string { return mb_strtolower($string ?? '', $env->getCharset()); } @@ -1220,11 +1180,9 @@ public static function lowerFilter(Environment $env, $string) * @param string|null $string * @param string[]|string|null $allowable_tags * - * @return string - * * @internal */ - public static function striptags($string, $allowable_tags = null) + public static function striptags($string, $allowable_tags = null): string { return strip_tags($string ?? '', $allowable_tags); } @@ -1234,11 +1192,9 @@ public static function striptags($string, $allowable_tags = null) * * @param string|null $string A string * - * @return string The titlecased string - * * @internal */ - public static function titleStringFilter(Environment $env, $string) + public static function titleCase(Environment $env, $string): string { return mb_convert_case($string ?? '', \MB_CASE_TITLE, $env->getCharset()); } @@ -1248,11 +1204,9 @@ public static function titleStringFilter(Environment $env, $string) * * @param string|null $string A string * - * @return string The capitalized string - * * @internal */ - public static function capitalizeStringFilter(Environment $env, $string) + public static function capitalize(Environment $env, $string): string { $charset = $env->getCharset(); @@ -1316,11 +1270,9 @@ public static function toArray($seq, $preserveKeys = true) * * @param mixed $value A variable * - * @return bool true if the value is empty, false otherwise - * * @internal */ - public static function testEmpty($value) + public static function testEmpty($value): bool { if ($value instanceof \Countable) { return 0 === \count($value); @@ -1337,27 +1289,6 @@ public static function testEmpty($value) return '' === $value || false === $value || null === $value || [] === $value; } - /** - * Checks if a variable is traversable. - * - * {# evaluates to true if the foo variable is an array or a traversable object #} - * {% if foo is iterable %} - * {# ... #} - * {% endif %} - * - * @param mixed $value A variable - * - * @return bool true if the value is traversable - * - * @deprecated since Twig 3.8, to be removed in 4.0 (use the native "is_iterable" function instead) - * - * @internal - */ - public static function testIterable($value) - { - return is_iterable($value); - } - /** * Renders a template. * @@ -1368,11 +1299,9 @@ public static function testIterable($value) * @param bool $ignoreMissing Whether to ignore missing templates or not * @param bool $sandboxed Whether to sandbox the template or not * - * @return string The rendered template - * * @internal */ - public static function include(Environment $env, $context, $template, $variables = [], $withContext = true, $ignoreMissing = false, $sandboxed = false) + public static function include(Environment $env, $context, $template, $variables = [], $withContext = true, $ignoreMissing = false, $sandboxed = false): string { $alreadySandboxed = false; $sandbox = null; @@ -1418,11 +1347,9 @@ public static function include(Environment $env, $context, $template, $variables * @param string $name The template name * @param bool $ignoreMissing Whether to ignore missing templates or not * - * @return string The template source - * * @internal */ - public static function source(Environment $env, $name, $ignoreMissing = false) + public static function source(Environment $env, $name, $ignoreMissing = false): string { $loader = $env->getLoader(); try { @@ -1442,11 +1369,9 @@ public static function source(Environment $env, $name, $ignoreMissing = false) * @param string $constant The name of the constant * @param object|null $object The object to get the constant from * - * @return string - * * @internal */ - public static function constant($constant, $object = null) + public static function constant($constant, $object = null): string { if (null !== $object) { if ('class' === $constant) { @@ -1473,11 +1398,9 @@ public static function constant($constant, $object = null) * @param string $constant The name of the constant * @param object|null $object The object to get the constant from * - * @return bool - * * @internal */ - public static function constantIsDefined($constant, $object = null) + public static function constantIsDefined($constant, $object = null): bool { if (null !== $object) { if ('class' === $constant) { @@ -1497,11 +1420,9 @@ public static function constantIsDefined($constant, $object = null) * @param int $size The size of the batch * @param mixed $fill A value used to fill missing items * - * @return array - * * @internal */ - public static function arrayBatch($items, $size, $fill = null, $preserveKeys = true) + public static function batch($items, $size, $fill = null, $preserveKeys = true): array { if (!is_iterable($items)) { throw new RuntimeError(sprintf('The "batch" filter expects an array or "Traversable", got "%s".', \is_object($items) ? \get_class($items) : \gettype($items))); @@ -1736,7 +1657,7 @@ public static function getAttribute(Environment $env, Source $source, $object, $ * * @internal */ - public static function arrayColumn($array, $name, $index = null): array + public static function column($array, $name, $index = null): array { if ($array instanceof \Traversable) { $array = iterator_to_array($array); @@ -1750,7 +1671,7 @@ public static function arrayColumn($array, $name, $index = null): array /** * @internal */ - public static function arrayFilter(Environment $env, $array, $arrow) + public static function filter(Environment $env, $array, $arrow) { if (!is_iterable($array)) { throw new RuntimeError(sprintf('The "filter" filter expects an array or "Traversable", got "%s".', \is_object($array) ? \get_class($array) : \gettype($array))); @@ -1769,7 +1690,7 @@ public static function arrayFilter(Environment $env, $array, $arrow) /** * @internal */ - public static function arrayMap(Environment $env, $array, $arrow) + public static function map(Environment $env, $array, $arrow) { self::checkArrowInSandbox($env, $arrow, 'map', 'filter'); @@ -1784,7 +1705,7 @@ public static function arrayMap(Environment $env, $array, $arrow) /** * @internal */ - public static function arrayReduce(Environment $env, $array, $arrow, $initial = null) + public static function reduce(Environment $env, $array, $arrow, $initial = null) { self::checkArrowInSandbox($env, $arrow, 'reduce', 'filter'); diff --git a/src/Node/Expression/ArrayExpression.php b/src/Node/Expression/ArrayExpression.php index 39b02f54e9d..5f8b0f63f37 100644 --- a/src/Node/Expression/ArrayExpression.php +++ b/src/Node/Expression/ArrayExpression.php @@ -70,7 +70,7 @@ public function compile(Compiler $compiler): void $needsArrayMergeSpread = \PHP_VERSION_ID < 80100 && $this->hasSpreadItem($keyValuePairs); if ($needsArrayMergeSpread) { - $compiler->raw('CoreExtension::arrayMerge('); + $compiler->raw('CoreExtension::merge('); } $compiler->raw('['); $first = true; diff --git a/src/Node/IncludeNode.php b/src/Node/IncludeNode.php index abc0f35460e..f10779a801c 100644 --- a/src/Node/IncludeNode.php +++ b/src/Node/IncludeNode.php @@ -97,7 +97,7 @@ protected function addTemplateArguments(Compiler $compiler) $compiler->raw(false === $this->getAttribute('only') ? '$context' : '[]'); } elseif (false === $this->getAttribute('only')) { $compiler - ->raw('CoreExtension::arrayMerge($context, ') + ->raw('CoreExtension::merge($context, ') ->subcompile($this->getNode('variables')) ->raw(')') ; diff --git a/src/Resources/core.php b/src/Resources/core.php index eb08cafcd3a..fc73d325b43 100644 --- a/src/Resources/core.php +++ b/src/Resources/core.php @@ -42,7 +42,7 @@ function twig_date_format_filter(Environment $env, $date, $format = null, $timez { trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); - return CoreExtension::dateFormatFilter($env, $date, $format, $timezone); + return CoreExtension::formatDate($env, $date, $format, $timezone); } /** @@ -53,7 +53,7 @@ function twig_date_modify_filter(Environment $env, $date, $modifier) { trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); - return CoreExtension::dateModifyFilter($env, $date, $modifier); + return CoreExtension::modifyDate($env, $date, $modifier); } /** @@ -75,7 +75,7 @@ function twig_date_converter(Environment $env, $date = null, $timezone = null) { trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); - return CoreExtension::dateConverter($env, $date, $timezone); + return CoreExtension::convertDate($env, $date, $timezone); } /** @@ -86,7 +86,7 @@ function twig_replace_filter($str, $from) { trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); - return CoreExtension::replaceFilter($str, $from); + return CoreExtension::replace($str, $from); } /** @@ -108,7 +108,7 @@ function twig_number_format_filter(Environment $env, $number, $decimal = null, $ { trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); - return CoreExtension::numberFormatFilter($env, $number, $decimal, $decimalPoint, $thousandSep); + return CoreExtension::formatNumber($env, $number, $decimal, $decimalPoint, $thousandSep); } /** @@ -119,7 +119,7 @@ function twig_urlencode_filter($url) { trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); - return CoreExtension::urlencodeFilter($url); + return CoreExtension::urlencode($url); } /** @@ -130,7 +130,7 @@ function twig_array_merge(...$arrays) { trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); - return CoreExtension::arrayMerge(...$arrays); + return CoreExtension::merge(...$arrays); } /** @@ -174,7 +174,7 @@ function twig_join_filter($value, $glue = '', $and = null) { trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); - return CoreExtension::joinFilter($value, $glue, $and); + return CoreExtension::join($value, $glue, $and); } /** @@ -185,7 +185,7 @@ function twig_split_filter(Environment $env, $value, $delimiter, $limit = null) { trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); - return CoreExtension::splitFilter($env, $value, $delimiter, $limit); + return CoreExtension::split($env, $value, $delimiter, $limit); } /** @@ -196,7 +196,7 @@ function twig_get_array_keys_filter($array) { trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); - return CoreExtension::getArrayKeysFilter($array); + return CoreExtension::keys($array); } /** @@ -207,7 +207,7 @@ function twig_reverse_filter(Environment $env, $item, $preserveKeys = false) { trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); - return CoreExtension::reverseFilter($env, $item, $preserveKeys); + return CoreExtension::reverse($env, $item, $preserveKeys); } /** @@ -218,7 +218,7 @@ function twig_sort_filter(Environment $env, $array, $arrow = null) { trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); - return CoreExtension::sortFilter($env, $array, $arrow); + return CoreExtension::sort($env, $array, $arrow); } /** @@ -240,7 +240,7 @@ function twig_trim_filter($string, $characterMask = null, $side = 'both') { trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); - return CoreExtension::trimFilter($string, $characterMask, $side); + return CoreExtension::trim($string, $characterMask, $side); } /** @@ -284,7 +284,7 @@ function twig_length_filter(Environment $env, $thing) { trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); - return CoreExtension::lengthFilter($env, $thing); + return CoreExtension::length($env, $thing); } /** @@ -295,7 +295,7 @@ function twig_upper_filter(Environment $env, $string) { trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); - return CoreExtension::upperFilter($env, $string); + return CoreExtension::upper($env, $string); } /** @@ -306,7 +306,7 @@ function twig_lower_filter(Environment $env, $string) { trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); - return CoreExtension::lowerFilter($env, $string); + return CoreExtension::lower($env, $string); } /** @@ -328,7 +328,7 @@ function twig_title_string_filter(Environment $env, $string) { trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); - return CoreExtension::titleStringFilter($env, $string); + return CoreExtension::titleCase($env, $string); } /** @@ -339,7 +339,7 @@ function twig_capitalize_string_filter(Environment $env, $string) { trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); - return CoreExtension::capitalizeStringFilter($env, $string); + return CoreExtension::capitalize($env, $string); } /** @@ -416,7 +416,7 @@ function twig_array_batch($items, $size, $fill = null, $preserveKeys = true) { trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); - return CoreExtension::arrayBatch($items, $size, $fill, $preserveKeys); + return CoreExtension::batch($items, $size, $fill, $preserveKeys); } /** @@ -427,7 +427,7 @@ function twig_array_column($array, $name, $index = null): array { trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); - return CoreExtension::arrayColumn($array, $name, $index); + return CoreExtension::column($array, $name, $index); } /** @@ -438,7 +438,7 @@ function twig_array_filter(Environment $env, $array, $arrow) { trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); - return CoreExtension::arrayFilter($env, $array, $arrow); + return CoreExtension::filter($env, $array, $arrow); } /** @@ -449,7 +449,7 @@ function twig_array_map(Environment $env, $array, $arrow) { trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); - return CoreExtension::arrayMap($env, $array, $arrow); + return CoreExtension::map($env, $array, $arrow); } /** @@ -460,7 +460,7 @@ function twig_array_reduce(Environment $env, $array, $arrow, $initial = null) { trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); - return CoreExtension::arrayReduce($env, $array, $arrow, $initial); + return CoreExtension::reduce($env, $array, $arrow, $initial); } /** diff --git a/tests/Extension/CoreTest.php b/tests/Extension/CoreTest.php index e1971aeea93..9fda22eac5f 100644 --- a/tests/Extension/CoreTest.php +++ b/tests/Extension/CoreTest.php @@ -123,7 +123,7 @@ public function testReverseFilterOnNonUTF8String() $twig->setCharset('ISO-8859-1'); $input = iconv('UTF-8', 'ISO-8859-1', 'Äé'); - $output = iconv('ISO-8859-1', 'UTF-8', CoreExtension::reverseFilter($twig, $input)); + $output = iconv('ISO-8859-1', 'UTF-8', CoreExtension::reverse($twig, $input)); $this->assertEquals($output, 'éÄ'); } @@ -177,7 +177,7 @@ public function provideTwigLastCases() */ public function testArrayKeysFilter(array $expected, $input) { - $this->assertSame($expected, CoreExtension::getArrayKeysFilter($input)); + $this->assertSame($expected, CoreExtension::keys($input)); } public function provideArrayKeyCases() diff --git a/tests/Node/Expression/FilterTest.php b/tests/Node/Expression/FilterTest.php index 18820cac8d1..352c9c94e11 100644 --- a/tests/Node/Expression/FilterTest.php +++ b/tests/Node/Expression/FilterTest.php @@ -69,7 +69,7 @@ protected function foobar() $node = $this->createFilter($expr, 'upper'); $node = $this->createFilter($node, 'number_format', [new ConstantExpression(2, 1), new ConstantExpression('.', 1), new ConstantExpression(',', 1)]); - $tests[] = [$node, 'Twig\Extension\CoreExtension::numberFormatFilter($this->env, Twig\Extension\CoreExtension::upperFilter($this->env, "foo"), 2, ".", ",")']; + $tests[] = [$node, 'Twig\Extension\CoreExtension::formatNumber($this->env, Twig\Extension\CoreExtension::upper($this->env, "foo"), 2, ".", ",")']; // named arguments $date = new ConstantExpression(0, 1); @@ -77,25 +77,25 @@ protected function foobar() 'timezone' => new ConstantExpression('America/Chicago', 1), 'format' => new ConstantExpression('d/m/Y H:i:s P', 1), ]); - $tests[] = [$node, 'Twig\Extension\CoreExtension::dateFormatFilter($this->env, 0, "d/m/Y H:i:s P", "America/Chicago")']; + $tests[] = [$node, 'Twig\Extension\CoreExtension::formatDate($this->env, 0, "d/m/Y H:i:s P", "America/Chicago")']; // skip an optional argument $date = new ConstantExpression(0, 1); $node = $this->createFilter($date, 'date', [ 'timezone' => new ConstantExpression('America/Chicago', 1), ]); - $tests[] = [$node, 'Twig\Extension\CoreExtension::dateFormatFilter($this->env, 0, null, "America/Chicago")']; + $tests[] = [$node, 'Twig\Extension\CoreExtension::formatDate($this->env, 0, null, "America/Chicago")']; // underscores vs camelCase for named arguments $string = new ConstantExpression('abc', 1); $node = $this->createFilter($string, 'reverse', [ 'preserve_keys' => new ConstantExpression(true, 1), ]); - $tests[] = [$node, 'Twig\Extension\CoreExtension::reverseFilter($this->env, "abc", true)']; + $tests[] = [$node, 'Twig\Extension\CoreExtension::reverse($this->env, "abc", true)']; $node = $this->createFilter($string, 'reverse', [ 'preserveKeys' => new ConstantExpression(true, 1), ]); - $tests[] = [$node, 'Twig\Extension\CoreExtension::reverseFilter($this->env, "abc", true)']; + $tests[] = [$node, 'Twig\Extension\CoreExtension::reverse($this->env, "abc", true)']; // filter as an anonymous function $node = $this->createFilter(new ConstantExpression('foo', 1), 'anonymous'); diff --git a/tests/Node/Expression/FunctionTest.php b/tests/Node/Expression/FunctionTest.php index 7fde216497c..aeabfc3c679 100644 --- a/tests/Node/Expression/FunctionTest.php +++ b/tests/Node/Expression/FunctionTest.php @@ -76,7 +76,7 @@ public function getTests() 'timezone' => new ConstantExpression('America/Chicago', 1), 'date' => new ConstantExpression(0, 1), ]); - $tests[] = [$node, 'Twig\Extension\CoreExtension::dateConverter($this->env, 0, "America/Chicago")']; + $tests[] = [$node, 'Twig\Extension\CoreExtension::convertDate($this->env, 0, "America/Chicago")']; // arbitrary named arguments $node = $this->createFunction('barbar'); diff --git a/tests/Node/IncludeTest.php b/tests/Node/IncludeTest.php index cda9d7bf27f..55454f8d410 100644 --- a/tests/Node/IncludeTest.php +++ b/tests/Node/IncludeTest.php @@ -64,7 +64,7 @@ public function getTests() $node = new IncludeNode($expr, $vars, false, false, 1); $tests[] = [$node, <<<'EOF' // line 1 -yield from $this->loadTemplate("foo.twig", null, 1)->unwrap()->yield(CoreExtension::arrayMerge($context, ["foo" => true])); +yield from $this->loadTemplate("foo.twig", null, 1)->unwrap()->yield(CoreExtension::merge($context, ["foo" => true])); EOF ]; From a19ec5b2000498361eb063758a161cf984329229 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Wed, 1 May 2024 11:02:32 +0200 Subject: [PATCH 232/812] Fix method call --- extra/intl-extra/IntlExtension.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extra/intl-extra/IntlExtension.php b/extra/intl-extra/IntlExtension.php index d932e7f7fc5..1e632a3a96a 100644 --- a/extra/intl-extra/IntlExtension.php +++ b/extra/intl-extra/IntlExtension.php @@ -369,7 +369,7 @@ public function formatNumberStyle(string $style, $number, array $attrs = [], str */ public function formatDateTime(Environment $env, $date, ?string $dateFormat = 'medium', ?string $timeFormat = 'medium', string $pattern = '', $timezone = null, string $calendar = 'gregorian', ?string $locale = null): string { - $date = CoreExtension::dateConverter($env, $date, $timezone); + $date = CoreExtension::convertDate($env, $date, $timezone); $formatterTimezone = $timezone; if (null === $formatterTimezone || false === $formatterTimezone) { From c63f695e8ae1ef76cba944cf29b875dc035611be Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Wed, 1 May 2024 12:58:33 +0200 Subject: [PATCH 233/812] Add needs_charset option for filters and functions --- CHANGELOG | 1 + doc/advanced.rst | 11 ++++ src/Extension/CoreExtension.php | 70 ++++++++++------------ src/Node/Expression/CallExpression.php | 11 ++++ src/Node/Expression/FilterExpression.php | 1 + src/Node/Expression/FunctionExpression.php | 1 + src/Resources/core.php | 22 +++---- src/TwigFilter.php | 6 ++ src/TwigFunction.php | 6 ++ tests/Extension/CoreTest.php | 34 ++++------- tests/Node/Expression/FilterTest.php | 6 +- 11 files changed, 93 insertions(+), 76 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index b48cc10c6ca..fa91aeb9c0b 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,6 @@ # 3.10.0 (2024-XX-XX) + * Add `needs_charset` option for filters and functions * Extract the escaping logic from the `EscapingExtension` class to a new `EscapingRuntime` class. diff --git a/doc/advanced.rst b/doc/advanced.rst index 307650f6a46..cf1b9870afc 100644 --- a/doc/advanced.rst +++ b/doc/advanced.rst @@ -175,6 +175,17 @@ The ``\Twig\TwigFilter`` class takes an array of options as its last argument:: $filter = new \Twig\TwigFilter('rot13', 'str_rot13', $options); +Charset-aware Filters +~~~~~~~~~~~~~~~~~~~~~ + +If you want to access the default charset in your filter, set the +``needs_charset`` option to ``true``; Twig will pass the default charset as the +first argument to the filter call:: + + $filter = new \Twig\TwigFilter('rot13', function (string $charset, $string) { + return str_rot13($string); + }, ['needs_charset' => true]); + Environment-aware Filters ~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index 8f45bfc387f..c193a7f0ed0 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -199,10 +199,10 @@ public function getFilters(): array new TwigFilter('convert_encoding', [self::class, 'convertEncoding']), // string filters - new TwigFilter('title', [self::class, 'titleCase'], ['needs_environment' => true]), - new TwigFilter('capitalize', [self::class, 'capitalize'], ['needs_environment' => true]), - new TwigFilter('upper', [self::class, 'upper'], ['needs_environment' => true]), - new TwigFilter('lower', [self::class, 'lower'], ['needs_environment' => true]), + new TwigFilter('title', [self::class, 'titleCase'], ['needs_charset' => true]), + new TwigFilter('capitalize', [self::class, 'capitalize'], ['needs_charset' => true]), + new TwigFilter('upper', [self::class, 'upper'], ['needs_charset' => true]), + new TwigFilter('lower', [self::class, 'lower'], ['needs_charset' => true]), new TwigFilter('striptags', [self::class, 'striptags']), new TwigFilter('trim', [self::class, 'trim']), new TwigFilter('nl2br', [self::class, 'nl2br'], ['pre_escape' => 'html', 'is_safe' => ['html']]), @@ -210,7 +210,7 @@ public function getFilters(): array // array helpers new TwigFilter('join', [self::class, 'join']), - new TwigFilter('split', [self::class, 'split'], ['needs_environment' => true]), + new TwigFilter('split', [self::class, 'split'], ['needs_charset' => true]), new TwigFilter('sort', [self::class, 'sort'], ['needs_environment' => true]), new TwigFilter('merge', [self::class, 'merge']), new TwigFilter('batch', [self::class, 'batch']), @@ -220,11 +220,11 @@ public function getFilters(): array new TwigFilter('reduce', [self::class, 'reduce'], ['needs_environment' => true]), // string/array filters - new TwigFilter('reverse', [self::class, 'reverse'], ['needs_environment' => true]), - new TwigFilter('length', [self::class, 'length'], ['needs_environment' => true]), - new TwigFilter('slice', [self::class, 'slice'], ['needs_environment' => true]), - new TwigFilter('first', [self::class, 'first'], ['needs_environment' => true]), - new TwigFilter('last', [self::class, 'last'], ['needs_environment' => true]), + new TwigFilter('reverse', [self::class, 'reverse'], ['needs_charset' => true]), + new TwigFilter('length', [self::class, 'length'], ['needs_charset' => true]), + new TwigFilter('slice', [self::class, 'slice'], ['needs_charset' => true]), + new TwigFilter('first', [self::class, 'first'], ['needs_charset' => true]), + new TwigFilter('last', [self::class, 'last'], ['needs_charset' => true]), // iteration and runtime new TwigFilter('default', [self::class, 'default'], ['node_class' => DefaultFilter::class]), @@ -240,7 +240,7 @@ public function getFunctions(): array new TwigFunction('range', 'range'), new TwigFunction('constant', [self::class, 'constant']), new TwigFunction('cycle', [self::class, 'cycle']), - new TwigFunction('random', [self::class, 'random'], ['needs_environment' => true]), + new TwigFunction('random', [self::class, 'random'], ['needs_charset' => true]), new TwigFunction('date', [self::class, 'convertDate'], ['needs_environment' => true]), new TwigFunction('include', [self::class, 'include'], ['needs_environment' => true, 'needs_context' => true, 'is_safe' => ['all']]), new TwigFunction('source', [self::class, 'source'], ['needs_environment' => true, 'is_safe' => ['all']]), @@ -350,7 +350,7 @@ public static function cycle($values, $position): string * * @internal */ - public static function random(Environment $env, $values = null, $max = null) + public static function random(string $charset, $values = null, $max = null) { if (null === $values) { return null === $max ? mt_rand() : mt_rand(0, (int) $max); @@ -377,8 +377,6 @@ public static function random(Environment $env, $values = null, $max = null) return ''; } - $charset = $env->getCharset(); - if ('UTF-8' !== $charset) { $values = self::convertEncoding($values, 'UTF-8', $charset); } @@ -654,7 +652,7 @@ public static function merge(...$arrays): array * * @internal */ - public static function slice(Environment $env, $item, $start, $length = null, $preserveKeys = false) + public static function slice(string $charset, $item, $start, $length = null, $preserveKeys = false) { if ($item instanceof \Traversable) { while ($item instanceof \IteratorAggregate) { @@ -676,7 +674,7 @@ public static function slice(Environment $env, $item, $start, $length = null, $p return \array_slice($item, $start, $length, $preserveKeys); } - return mb_substr((string) $item, $start, $length, $env->getCharset()); + return mb_substr((string) $item, $start, $length, $charset); } /** @@ -688,9 +686,9 @@ public static function slice(Environment $env, $item, $start, $length = null, $p * * @internal */ - public static function first(Environment $env, $item) + public static function first(string $charset, $item) { - $elements = self::slice($env, $item, 0, 1, false); + $elements = self::slice($charset, $item, 0, 1, false); return \is_string($elements) ? $elements : current($elements); } @@ -704,9 +702,9 @@ public static function first(Environment $env, $item) * * @internal */ - public static function last(Environment $env, $item) + public static function last(string $charset, $item) { - $elements = self::slice($env, $item, -1, 1, false); + $elements = self::slice($charset, $item, -1, 1, false); return \is_string($elements) ? $elements : current($elements); } @@ -775,7 +773,7 @@ public static function join($value, $glue = '', $and = null): string * * @internal */ - public static function split(Environment $env, $value, $delimiter, $limit = null): array + public static function split(string $charset, $value, $delimiter, $limit = null): array { $value = $value ?? ''; @@ -787,14 +785,14 @@ public static function split(Environment $env, $value, $delimiter, $limit = null return preg_split('/(?getCharset()); + $length = mb_strlen($value, $charset); if ($length < $limit) { return [$value]; } $r = []; for ($i = 0; $i < $length; $i += $limit) { - $r[] = mb_substr($value, $i, $limit, $env->getCharset()); + $r[] = mb_substr($value, $i, $limit, $charset); } return $r; @@ -868,7 +866,7 @@ public static function keys($array): array * * @internal */ - public static function reverse(Environment $env, $item, $preserveKeys = false) + public static function reverse(string $charset, $item, $preserveKeys = false) { if ($item instanceof \Traversable) { return array_reverse(iterator_to_array($item), $preserveKeys); @@ -880,8 +878,6 @@ public static function reverse(Environment $env, $item, $preserveKeys = false) $string = (string) $item; - $charset = $env->getCharset(); - if ('UTF-8' !== $charset) { $string = self::convertEncoding($string, 'UTF-8', $charset); } @@ -1125,14 +1121,14 @@ public static function convertEncoding($string, $to, $from): string * * @internal */ - public static function length(Environment $env, $thing): int + public static function length(string $charset, $thing): int { if (null === $thing) { return 0; } if (\is_scalar($thing)) { - return mb_strlen($thing, $env->getCharset()); + return mb_strlen($thing, $charset); } if ($thing instanceof \Countable || \is_array($thing) || $thing instanceof \SimpleXMLElement) { @@ -1144,7 +1140,7 @@ public static function length(Environment $env, $thing): int } if (method_exists($thing, '__toString')) { - return mb_strlen((string) $thing, $env->getCharset()); + return mb_strlen((string) $thing, $charset); } return 1; @@ -1157,9 +1153,9 @@ public static function length(Environment $env, $thing): int * * @internal */ - public static function upper(Environment $env, $string): string + public static function upper(string $charset, $string): string { - return mb_strtoupper($string ?? '', $env->getCharset()); + return mb_strtoupper($string ?? '', $charset); } /** @@ -1169,9 +1165,9 @@ public static function upper(Environment $env, $string): string * * @internal */ - public static function lower(Environment $env, $string): string + public static function lower(string $charset, $string): string { - return mb_strtolower($string ?? '', $env->getCharset()); + return mb_strtolower($string ?? '', $charset); } /** @@ -1194,9 +1190,9 @@ public static function striptags($string, $allowable_tags = null): string * * @internal */ - public static function titleCase(Environment $env, $string): string + public static function titleCase(string $charset, $string): string { - return mb_convert_case($string ?? '', \MB_CASE_TITLE, $env->getCharset()); + return mb_convert_case($string ?? '', \MB_CASE_TITLE, $charset); } /** @@ -1206,10 +1202,8 @@ public static function titleCase(Environment $env, $string): string * * @internal */ - public static function capitalize(Environment $env, $string): string + public static function capitalize(string $charset, $string): string { - $charset = $env->getCharset(); - return mb_strtoupper(mb_substr($string ?? '', 0, 1, $charset), $charset).mb_strtolower(mb_substr($string ?? '', 1, null, $charset), $charset); } diff --git a/src/Node/Expression/CallExpression.php b/src/Node/Expression/CallExpression.php index 71a9c739a2c..d6ac5fc460c 100644 --- a/src/Node/Expression/CallExpression.php +++ b/src/Node/Expression/CallExpression.php @@ -61,7 +61,15 @@ protected function compileArguments(Compiler $compiler, $isArray = false): void $first = true; + if ($this->hasAttribute('needs_charset') && $this->getAttribute('needs_charset')) { + $compiler->raw('$this->env->getCharset()'); + $first = false; + } + if ($this->hasAttribute('needs_environment') && $this->getAttribute('needs_environment')) { + if (!$first) { + $compiler->raw(', '); + } $compiler->raw('$this->env'); $first = false; } @@ -245,6 +253,9 @@ private function getCallableParameters($callable, bool $isVariadic): array if ($this->hasNode('node')) { array_shift($parameters); } + if ($this->hasAttribute('needs_charset') && $this->getAttribute('needs_charset')) { + array_shift($parameters); + } if ($this->hasAttribute('needs_environment') && $this->getAttribute('needs_environment')) { array_shift($parameters); } diff --git a/src/Node/Expression/FilterExpression.php b/src/Node/Expression/FilterExpression.php index fec652a431a..c803d5708cb 100644 --- a/src/Node/Expression/FilterExpression.php +++ b/src/Node/Expression/FilterExpression.php @@ -29,6 +29,7 @@ public function compile(Compiler $compiler): void $this->setAttribute('name', $name); $this->setAttribute('type', 'filter'); + $this->setAttribute('needs_charset', $filter->needsCharset()); $this->setAttribute('needs_environment', $filter->needsEnvironment()); $this->setAttribute('needs_context', $filter->needsContext()); $this->setAttribute('arguments', $filter->getArguments()); diff --git a/src/Node/Expression/FunctionExpression.php b/src/Node/Expression/FunctionExpression.php index e89b3897872..d903a9e8f13 100644 --- a/src/Node/Expression/FunctionExpression.php +++ b/src/Node/Expression/FunctionExpression.php @@ -29,6 +29,7 @@ public function compile(Compiler $compiler) $this->setAttribute('name', $name); $this->setAttribute('type', 'function'); + $this->setAttribute('needs_charset', $function->needsCharset()); $this->setAttribute('needs_environment', $function->needsEnvironment()); $this->setAttribute('needs_context', $function->needsContext()); $this->setAttribute('arguments', $function->getArguments()); diff --git a/src/Resources/core.php b/src/Resources/core.php index fc73d325b43..0d4cfc91953 100644 --- a/src/Resources/core.php +++ b/src/Resources/core.php @@ -31,7 +31,7 @@ function twig_random(Environment $env, $values = null, $max = null) { trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); - return CoreExtension::random($env, $values, $max); + return CoreExtension::random($env->getCharset(), $values, $max); } /** @@ -141,7 +141,7 @@ function twig_slice(Environment $env, $item, $start, $length = null, $preserveKe { trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); - return CoreExtension::slice($env, $item, $start, $length, $preserveKeys); + return CoreExtension::slice($env->getCharset(), $item, $start, $length, $preserveKeys); } /** @@ -152,7 +152,7 @@ function twig_first(Environment $env, $item) { trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); - return CoreExtension::first($env, $item); + return CoreExtension::first($env->getCharset(), $item); } /** @@ -163,7 +163,7 @@ function twig_last(Environment $env, $item) { trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); - return CoreExtension::last($env, $item); + return CoreExtension::last($env->getCharset(), $item); } /** @@ -185,7 +185,7 @@ function twig_split_filter(Environment $env, $value, $delimiter, $limit = null) { trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); - return CoreExtension::split($env, $value, $delimiter, $limit); + return CoreExtension::split($env->getCharset(), $value, $delimiter, $limit); } /** @@ -207,7 +207,7 @@ function twig_reverse_filter(Environment $env, $item, $preserveKeys = false) { trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); - return CoreExtension::reverse($env, $item, $preserveKeys); + return CoreExtension::reverse($env->getCharset(), $item, $preserveKeys); } /** @@ -284,7 +284,7 @@ function twig_length_filter(Environment $env, $thing) { trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); - return CoreExtension::length($env, $thing); + return CoreExtension::length($env->getCharset(), $thing); } /** @@ -295,7 +295,7 @@ function twig_upper_filter(Environment $env, $string) { trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); - return CoreExtension::upper($env, $string); + return CoreExtension::upper($env->getCharset(), $string); } /** @@ -306,7 +306,7 @@ function twig_lower_filter(Environment $env, $string) { trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); - return CoreExtension::lower($env, $string); + return CoreExtension::lower($env->getCharset(), $string); } /** @@ -328,7 +328,7 @@ function twig_title_string_filter(Environment $env, $string) { trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); - return CoreExtension::titleCase($env, $string); + return CoreExtension::titleCase($env->getCharset(), $string); } /** @@ -339,7 +339,7 @@ function twig_capitalize_string_filter(Environment $env, $string) { trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); - return CoreExtension::capitalize($env, $string); + return CoreExtension::capitalize($env->getCharset(), $string); } /** diff --git a/src/TwigFilter.php b/src/TwigFilter.php index 8993026c8cb..c02469b916f 100644 --- a/src/TwigFilter.php +++ b/src/TwigFilter.php @@ -38,6 +38,7 @@ public function __construct(string $name, $callable = null, array $options = []) $this->options = array_merge([ 'needs_environment' => false, 'needs_context' => false, + 'needs_charset' => false, 'is_variadic' => false, 'is_safe' => null, 'is_safe_callback' => null, @@ -79,6 +80,11 @@ public function getArguments(): array return $this->arguments; } + public function needsCharset(): bool + { + return $this->options['needs_charset']; + } + public function needsEnvironment(): bool { return $this->options['needs_environment']; diff --git a/src/TwigFunction.php b/src/TwigFunction.php index d910d1fd531..c15def63303 100644 --- a/src/TwigFunction.php +++ b/src/TwigFunction.php @@ -38,6 +38,7 @@ public function __construct(string $name, $callable = null, array $options = []) $this->options = array_merge([ 'needs_environment' => false, 'needs_context' => false, + 'needs_charset' => false, 'is_variadic' => false, 'is_safe' => null, 'is_safe_callback' => null, @@ -77,6 +78,11 @@ public function getArguments(): array return $this->arguments; } + public function needsCharset(): bool + { + return $this->options['needs_charset']; + } + public function needsEnvironment(): bool { return $this->options['needs_environment']; diff --git a/tests/Extension/CoreTest.php b/tests/Extension/CoreTest.php index 9fda22eac5f..2d3c0c27ab6 100644 --- a/tests/Extension/CoreTest.php +++ b/tests/Extension/CoreTest.php @@ -12,10 +12,8 @@ */ use PHPUnit\Framework\TestCase; -use Twig\Environment; use Twig\Error\RuntimeError; use Twig\Extension\CoreExtension; -use Twig\Loader\LoaderInterface; class CoreTest extends TestCase { @@ -24,10 +22,8 @@ class CoreTest extends TestCase */ public function testRandomFunction(array $expectedInArray, $value1, $value2 = null) { - $env = new Environment($this->createMock(LoaderInterface::class)); - for ($i = 0; $i < 100; ++$i) { - $this->assertTrue(\in_array(CoreExtension::random($env, $value1, $value2), $expectedInArray, true)); // assertContains() would not consider the type + $this->assertTrue(\in_array(CoreExtension::random('UTF-8', $value1, $value2), $expectedInArray, true)); // assertContains() would not consider the type } } @@ -85,45 +81,38 @@ public function testRandomFunctionWithoutParameter() $max = mt_getrandmax(); for ($i = 0; $i < 100; ++$i) { - $val = CoreExtension::random(new Environment($this->createMock(LoaderInterface::class))); + $val = CoreExtension::random('UTF-8'); $this->assertTrue(\is_int($val) && $val >= 0 && $val <= $max); } } public function testRandomFunctionReturnsAsIs() { - $this->assertSame('', CoreExtension::random(new Environment($this->createMock(LoaderInterface::class)), '')); - $this->assertSame('', CoreExtension::random(new Environment($this->createMock(LoaderInterface::class), ['charset' => null]), '')); + $this->assertSame('', CoreExtension::random('UTF-8', '')); $instance = new \stdClass(); - $this->assertSame($instance, CoreExtension::random(new Environment($this->createMock(LoaderInterface::class)), $instance)); + $this->assertSame($instance, CoreExtension::random('UTF-8', $instance)); } public function testRandomFunctionOfEmptyArrayThrowsException() { $this->expectException(RuntimeError::class); - CoreExtension::random(new Environment($this->createMock(LoaderInterface::class)), []); + CoreExtension::random('UTF-8', []); } public function testRandomFunctionOnNonUTF8String() { - $twig = new Environment($this->createMock(LoaderInterface::class)); - $twig->setCharset('ISO-8859-1'); - $text = iconv('UTF-8', 'ISO-8859-1', 'Äé'); for ($i = 0; $i < 30; ++$i) { - $rand = CoreExtension::random($twig, $text); + $rand = CoreExtension::random('ISO-8859-1', $text); $this->assertTrue(\in_array(iconv('ISO-8859-1', 'UTF-8', $rand), ['Ä', 'é'], true)); } } public function testReverseFilterOnNonUTF8String() { - $twig = new Environment($this->createMock(LoaderInterface::class)); - $twig->setCharset('ISO-8859-1'); - $input = iconv('UTF-8', 'ISO-8859-1', 'Äé'); - $output = iconv('ISO-8859-1', 'UTF-8', CoreExtension::reverse($twig, $input)); + $output = iconv('ISO-8859-1', 'UTF-8', CoreExtension::reverse('ISO-8859-1', $input)); $this->assertEquals($output, 'éÄ'); } @@ -133,8 +122,7 @@ public function testReverseFilterOnNonUTF8String() */ public function testTwigFirst($expected, $input) { - $twig = new Environment($this->createMock(LoaderInterface::class)); - $this->assertSame($expected, CoreExtension::first($twig, $input)); + $this->assertSame($expected, CoreExtension::first('UTF-8', $input)); } public function provideTwigFirstCases() @@ -155,8 +143,7 @@ public function provideTwigFirstCases() */ public function testTwigLast($expected, $input) { - $twig = new Environment($this->createMock(LoaderInterface::class)); - $this->assertSame($expected, CoreExtension::last($twig, $input)); + $this->assertSame($expected, CoreExtension::last('UTF-8', $input)); } public function provideTwigLastCases() @@ -228,8 +215,7 @@ public function provideInFilterCases() */ public function testSliceFilter($expected, $input, $start, $length = null, $preserveKeys = false) { - $twig = new Environment($this->createMock(LoaderInterface::class)); - $this->assertSame($expected, CoreExtension::slice($twig, $input, $start, $length, $preserveKeys)); + $this->assertSame($expected, CoreExtension::slice('UTF-8', $input, $start, $length, $preserveKeys)); } public function provideSliceFilterCases() diff --git a/tests/Node/Expression/FilterTest.php b/tests/Node/Expression/FilterTest.php index 352c9c94e11..e28dd73d8c6 100644 --- a/tests/Node/Expression/FilterTest.php +++ b/tests/Node/Expression/FilterTest.php @@ -69,7 +69,7 @@ protected function foobar() $node = $this->createFilter($expr, 'upper'); $node = $this->createFilter($node, 'number_format', [new ConstantExpression(2, 1), new ConstantExpression('.', 1), new ConstantExpression(',', 1)]); - $tests[] = [$node, 'Twig\Extension\CoreExtension::formatNumber($this->env, Twig\Extension\CoreExtension::upper($this->env, "foo"), 2, ".", ",")']; + $tests[] = [$node, 'Twig\Extension\CoreExtension::formatNumber($this->env, Twig\Extension\CoreExtension::upper($this->env->getCharset(), "foo"), 2, ".", ",")']; // named arguments $date = new ConstantExpression(0, 1); @@ -91,11 +91,11 @@ protected function foobar() $node = $this->createFilter($string, 'reverse', [ 'preserve_keys' => new ConstantExpression(true, 1), ]); - $tests[] = [$node, 'Twig\Extension\CoreExtension::reverse($this->env, "abc", true)']; + $tests[] = [$node, 'Twig\Extension\CoreExtension::reverse($this->env->getCharset(), "abc", true)']; $node = $this->createFilter($string, 'reverse', [ 'preserveKeys' => new ConstantExpression(true, 1), ]); - $tests[] = [$node, 'Twig\Extension\CoreExtension::reverse($this->env, "abc", true)']; + $tests[] = [$node, 'Twig\Extension\CoreExtension::reverse($this->env->getCharset(), "abc", true)']; // filter as an anonymous function $node = $this->createFilter(new ConstantExpression('foo', 1), 'anonymous'); From 283475b1029561da7198bd7792d81f463a1028b7 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Fri, 19 Apr 2024 10:44:20 -0400 Subject: [PATCH 234/812] Move some static extension methods to non-static --- extra/intl-extra/IntlExtension.php | 2 +- extra/intl-extra/composer.json | 2 +- src/Extension/CoreExtension.php | 40 +++++++++++++++----------- src/Resources/core.php | 8 +++--- tests/Node/Expression/FilterTest.php | 6 ++-- tests/Node/Expression/FunctionTest.php | 2 +- 6 files changed, 34 insertions(+), 26 deletions(-) diff --git a/extra/intl-extra/IntlExtension.php b/extra/intl-extra/IntlExtension.php index 1e632a3a96a..e87fd086efa 100644 --- a/extra/intl-extra/IntlExtension.php +++ b/extra/intl-extra/IntlExtension.php @@ -369,7 +369,7 @@ public function formatNumberStyle(string $style, $number, array $attrs = [], str */ public function formatDateTime(Environment $env, $date, ?string $dateFormat = 'medium', ?string $timeFormat = 'medium', string $pattern = '', $timezone = null, string $calendar = 'gregorian', ?string $locale = null): string { - $date = CoreExtension::convertDate($env, $date, $timezone); + $date = $env->getExtension(CoreExtension::class)->convertDate($date, $timezone); $formatterTimezone = $timezone; if (null === $formatterTimezone || false === $formatterTimezone) { diff --git a/extra/intl-extra/composer.json b/extra/intl-extra/composer.json index a46eada315c..b3c3ceff366 100644 --- a/extra/intl-extra/composer.json +++ b/extra/intl-extra/composer.json @@ -16,7 +16,7 @@ ], "require": { "php": ">=7.2.5", - "twig/twig": "^3.9", + "twig/twig": "^3.10", "symfony/intl": "^5.4|^6.4|^7.0" }, "require-dev": { diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index c193a7f0ed0..79e6cd7638b 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -185,11 +185,11 @@ public function getFilters(): array { return [ // formatting filters - new TwigFilter('date', [self::class, 'formatDate'], ['needs_environment' => true]), - new TwigFilter('date_modify', [self::class, 'modifyDate'], ['needs_environment' => true]), + new TwigFilter('date', [$this, 'formatDate']), + new TwigFilter('date_modify', [$this, 'modifyDate']), new TwigFilter('format', [self::class, 'sprintf']), new TwigFilter('replace', [self::class, 'replace']), - new TwigFilter('number_format', [self::class, 'formatNumber'], ['needs_environment' => true]), + new TwigFilter('number_format', [$this, 'formatNumber']), new TwigFilter('abs', 'abs'), new TwigFilter('round', [self::class, 'round']), @@ -241,7 +241,7 @@ public function getFunctions(): array new TwigFunction('constant', [self::class, 'constant']), new TwigFunction('cycle', [self::class, 'cycle']), new TwigFunction('random', [self::class, 'random'], ['needs_charset' => true]), - new TwigFunction('date', [self::class, 'convertDate'], ['needs_environment' => true]), + new TwigFunction('date', [$this, 'convertDate']), new TwigFunction('include', [self::class, 'include'], ['needs_environment' => true, 'needs_context' => true, 'is_safe' => ['all']]), new TwigFunction('source', [self::class, 'source'], ['needs_environment' => true, 'is_safe' => ['all']]), ]; @@ -416,10 +416,10 @@ public static function random(string $charset, $values = null, $max = null) * * @internal */ - public static function formatDate(Environment $env, $date, $format = null, $timezone = null): string + public function formatDate($date, $format = null, $timezone = null): string { if (null === $format) { - $formats = $env->getExtension(self::class)->getDateFormat(); + $formats = $this->getDateFormat(); $format = $date instanceof \DateInterval ? $formats[1] : $formats[0]; } @@ -427,7 +427,7 @@ public static function formatDate(Environment $env, $date, $format = null, $time return $date->format($format); } - return self::convertDate($env, $date, $timezone)->format($format); + return $this->convertDate($date, $timezone)->format($format); } /** @@ -442,9 +442,9 @@ public static function formatDate(Environment $env, $date, $format = null, $time * * @internal */ - public static function modifyDate(Environment $env, $date, $modifier) + public function modifyDate($date, $modifier) { - return self::convertDate($env, $date, false)->modify($modifier); + return $this->convertDate($date, false)->modify($modifier); } /** @@ -460,6 +460,14 @@ public static function sprintf($format, ...$values): string return sprintf($format ?? '', ...$values); } + /** + * @internal + */ + public static function dateConverter(Environment $env, $date, $format = null, $timezone = null): string + { + return $env->getExtension(CoreExtension::class)->formatDate($date, $format, $timezone); + } + /** * Converts an input to a \DateTime instance. * @@ -474,12 +482,12 @@ public static function sprintf($format, ...$values): string * * @internal */ - public static function convertDate(Environment $env, $date = null, $timezone = null) + public function convertDate($date = null, $timezone = null) { // determine the timezone if (false !== $timezone) { if (null === $timezone) { - $timezone = $env->getExtension(self::class)->getTimezone(); + $timezone = $this->getTimezone(); } elseif (!$timezone instanceof \DateTimeZone) { $timezone = new \DateTimeZone($timezone); } @@ -504,14 +512,14 @@ public static function convertDate(Environment $env, $date = null, $timezone = n $date = 'now'; } - return new \DateTime($date, false !== $timezone ? $timezone : $env->getExtension(self::class)->getTimezone()); + return new \DateTime($date, false !== $timezone ? $timezone : $this->getTimezone()); } $asString = (string) $date; if (ctype_digit($asString) || (!empty($asString) && '-' === $asString[0] && ctype_digit(substr($asString, 1)))) { $date = new \DateTime('@'.$date); } else { - $date = new \DateTime($date, $env->getExtension(self::class)->getTimezone()); + $date = new \DateTime($date, $this->getTimezone()); } if (false !== $timezone) { @@ -565,7 +573,7 @@ public static function round($value, $precision = 0, $method = 'common') } /** - * Number format filter. + * Formats a number. * * All of the formatting options can be left null, in that case the defaults will * be used. Supplying any of the parameters will override the defaults set in the @@ -578,9 +586,9 @@ public static function round($value, $precision = 0, $method = 'common') * * @internal */ - public static function formatNumber(Environment $env, $number, $decimal = null, $decimalPoint = null, $thousandSep = null): string + public function formatNumber($number, $decimal = null, $decimalPoint = null, $thousandSep = null): string { - $defaults = $env->getExtension(self::class)->getNumberFormat(); + $defaults = $this->getNumberFormat(); if (null === $decimal) { $decimal = $defaults[0]; } diff --git a/src/Resources/core.php b/src/Resources/core.php index 0d4cfc91953..18a441faebb 100644 --- a/src/Resources/core.php +++ b/src/Resources/core.php @@ -42,7 +42,7 @@ function twig_date_format_filter(Environment $env, $date, $format = null, $timez { trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); - return CoreExtension::formatDate($env, $date, $format, $timezone); + return $env->getExtension(CoreExtension::class)->formatDate($date, $format, $timezone); } /** @@ -53,7 +53,7 @@ function twig_date_modify_filter(Environment $env, $date, $modifier) { trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); - return CoreExtension::modifyDate($env, $date, $modifier); + return $env->getExtension(CoreExtension::class)->modifyDate($date, $modifier); } /** @@ -75,7 +75,7 @@ function twig_date_converter(Environment $env, $date = null, $timezone = null) { trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); - return CoreExtension::convertDate($env, $date, $timezone); + return $env->getExtension(CoreExtension::class)->convertDate($date, $timezone); } /** @@ -108,7 +108,7 @@ function twig_number_format_filter(Environment $env, $number, $decimal = null, $ { trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); - return CoreExtension::formatNumber($env, $number, $decimal, $decimalPoint, $thousandSep); + return $env->getExtension(CoreExtension::class)->formatNumber($number, $decimal, $decimalPoint, $thousandSep); } /** diff --git a/tests/Node/Expression/FilterTest.php b/tests/Node/Expression/FilterTest.php index e28dd73d8c6..f99d6242f2e 100644 --- a/tests/Node/Expression/FilterTest.php +++ b/tests/Node/Expression/FilterTest.php @@ -69,7 +69,7 @@ protected function foobar() $node = $this->createFilter($expr, 'upper'); $node = $this->createFilter($node, 'number_format', [new ConstantExpression(2, 1), new ConstantExpression('.', 1), new ConstantExpression(',', 1)]); - $tests[] = [$node, 'Twig\Extension\CoreExtension::formatNumber($this->env, Twig\Extension\CoreExtension::upper($this->env->getCharset(), "foo"), 2, ".", ",")']; + $tests[] = [$node, '$this->extensions[\'Twig\Extension\CoreExtension\']->formatNumber(Twig\Extension\CoreExtension::upper($this->env->getCharset(), "foo"), 2, ".", ",")']; // named arguments $date = new ConstantExpression(0, 1); @@ -77,14 +77,14 @@ protected function foobar() 'timezone' => new ConstantExpression('America/Chicago', 1), 'format' => new ConstantExpression('d/m/Y H:i:s P', 1), ]); - $tests[] = [$node, 'Twig\Extension\CoreExtension::formatDate($this->env, 0, "d/m/Y H:i:s P", "America/Chicago")']; + $tests[] = [$node, '$this->extensions[\'Twig\Extension\CoreExtension\']->formatDate(0, "d/m/Y H:i:s P", "America/Chicago")']; // skip an optional argument $date = new ConstantExpression(0, 1); $node = $this->createFilter($date, 'date', [ 'timezone' => new ConstantExpression('America/Chicago', 1), ]); - $tests[] = [$node, 'Twig\Extension\CoreExtension::formatDate($this->env, 0, null, "America/Chicago")']; + $tests[] = [$node, '$this->extensions[\'Twig\Extension\CoreExtension\']->formatDate(0, null, "America/Chicago")']; // underscores vs camelCase for named arguments $string = new ConstantExpression('abc', 1); diff --git a/tests/Node/Expression/FunctionTest.php b/tests/Node/Expression/FunctionTest.php index aeabfc3c679..aa40e355553 100644 --- a/tests/Node/Expression/FunctionTest.php +++ b/tests/Node/Expression/FunctionTest.php @@ -76,7 +76,7 @@ public function getTests() 'timezone' => new ConstantExpression('America/Chicago', 1), 'date' => new ConstantExpression(0, 1), ]); - $tests[] = [$node, 'Twig\Extension\CoreExtension::convertDate($this->env, 0, "America/Chicago")']; + $tests[] = [$node, '$this->extensions[\'Twig\Extension\CoreExtension\']->convertDate(0, "America/Chicago")']; // arbitrary named arguments $node = $this->createFunction('barbar'); From 0a275945aff9ee6eaefb53231a78d482cefdc28f Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Wed, 1 May 2024 14:09:28 +0200 Subject: [PATCH 235/812] Make some CoreExtension methods part of the public API --- CHANGELOG | 2 ++ src/Extension/CoreExtension.php | 6 ------ 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index fa91aeb9c0b..c70d8ec2b5e 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,7 @@ # 3.10.0 (2024-XX-XX) + * Make `CoreExtension::formatDate`, `CoreExtension::convertDate`, and + `CoreExtension::formatNumber` part of the public API * Add `needs_charset` option for filters and functions * Extract the escaping logic from the `EscapingExtension` class to a new `EscapingRuntime` class. diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index 79e6cd7638b..98febb64bad 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -413,8 +413,6 @@ public static function random(string $charset, $values = null, $max = null) * @param \DateTimeInterface|\DateInterval|string $date A date * @param string|null $format The target format, null to use the default * @param \DateTimeZone|string|false|null $timezone The target timezone, null to use the default, false to leave unchanged - * - * @internal */ public function formatDate($date, $format = null, $timezone = null): string { @@ -479,8 +477,6 @@ public static function dateConverter(Environment $env, $date, $format = null, $t * @param \DateTimeZone|string|false|null $timezone The target timezone, null to use the default, false to leave unchanged * * @return \DateTime|\DateTimeImmutable - * - * @internal */ public function convertDate($date = null, $timezone = null) { @@ -583,8 +579,6 @@ public static function round($value, $precision = 0, $method = 'common') * @param int|null $decimal the number of decimal points to display * @param string|null $decimalPoint the character(s) to use for the decimal point * @param string|null $thousandSep the character(s) to use for the thousands separator - * - * @internal */ public function formatNumber($number, $decimal = null, $decimalPoint = null, $thousandSep = null): string { From b222be9974652ead419a5f7c91348c0529f5337e Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Wed, 1 May 2024 16:06:53 +0200 Subject: [PATCH 236/812] Fix CS --- extra/html-extra/HtmlExtension.php | 2 +- src/Extension/CoreExtension.php | 2 +- src/Resources/core.php | 44 ++++++++++++++++++++++++++++++ src/Resources/debug.php | 1 + 4 files changed, 47 insertions(+), 2 deletions(-) diff --git a/extra/html-extra/HtmlExtension.php b/extra/html-extra/HtmlExtension.php index 266cbdef4f3..debb20bbc1a 100644 --- a/extra/html-extra/HtmlExtension.php +++ b/extra/html-extra/HtmlExtension.php @@ -108,6 +108,6 @@ public static function htmlClasses(...$args): string } } - return implode(' ', array_unique(array_filter($classes, static function($v) { return '' !== $v; }))); + return implode(' ', array_unique(array_filter($classes, static function ($v) { return '' !== $v; }))); } } diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index 98febb64bad..bd9552f10ba 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -463,7 +463,7 @@ public static function sprintf($format, ...$values): string */ public static function dateConverter(Environment $env, $date, $format = null, $timezone = null): string { - return $env->getExtension(CoreExtension::class)->formatDate($date, $format, $timezone); + return $env->getExtension(self::class)->formatDate($date, $format, $timezone); } /** diff --git a/src/Resources/core.php b/src/Resources/core.php index 18a441faebb..e5372cda492 100644 --- a/src/Resources/core.php +++ b/src/Resources/core.php @@ -14,6 +14,7 @@ /** * @internal + * * @deprecated since Twig 3.9 */ function twig_cycle($values, $position) @@ -25,6 +26,7 @@ function twig_cycle($values, $position) /** * @internal + * * @deprecated since Twig 3.9 */ function twig_random(Environment $env, $values = null, $max = null) @@ -36,6 +38,7 @@ function twig_random(Environment $env, $values = null, $max = null) /** * @internal + * * @deprecated since Twig 3.9 */ function twig_date_format_filter(Environment $env, $date, $format = null, $timezone = null) @@ -47,6 +50,7 @@ function twig_date_format_filter(Environment $env, $date, $format = null, $timez /** * @internal + * * @deprecated since Twig 3.9 */ function twig_date_modify_filter(Environment $env, $date, $modifier) @@ -58,6 +62,7 @@ function twig_date_modify_filter(Environment $env, $date, $modifier) /** * @internal + * * @deprecated since Twig 3.9 */ function twig_sprintf($format, ...$values) @@ -69,6 +74,7 @@ function twig_sprintf($format, ...$values) /** * @internal + * * @deprecated since Twig 3.9 */ function twig_date_converter(Environment $env, $date = null, $timezone = null) @@ -80,6 +86,7 @@ function twig_date_converter(Environment $env, $date = null, $timezone = null) /** * @internal + * * @deprecated since Twig 3.9 */ function twig_replace_filter($str, $from) @@ -91,6 +98,7 @@ function twig_replace_filter($str, $from) /** * @internal + * * @deprecated since Twig 3.9 */ function twig_round($value, $precision = 0, $method = 'common') @@ -102,6 +110,7 @@ function twig_round($value, $precision = 0, $method = 'common') /** * @internal + * * @deprecated since Twig 3.9 */ function twig_number_format_filter(Environment $env, $number, $decimal = null, $decimalPoint = null, $thousandSep = null) @@ -113,6 +122,7 @@ function twig_number_format_filter(Environment $env, $number, $decimal = null, $ /** * @internal + * * @deprecated since Twig 3.9 */ function twig_urlencode_filter($url) @@ -124,6 +134,7 @@ function twig_urlencode_filter($url) /** * @internal + * * @deprecated since Twig 3.9 */ function twig_array_merge(...$arrays) @@ -135,6 +146,7 @@ function twig_array_merge(...$arrays) /** * @internal + * * @deprecated since Twig 3.9 */ function twig_slice(Environment $env, $item, $start, $length = null, $preserveKeys = false) @@ -146,6 +158,7 @@ function twig_slice(Environment $env, $item, $start, $length = null, $preserveKe /** * @internal + * * @deprecated since Twig 3.9 */ function twig_first(Environment $env, $item) @@ -157,6 +170,7 @@ function twig_first(Environment $env, $item) /** * @internal + * * @deprecated since Twig 3.9 */ function twig_last(Environment $env, $item) @@ -168,6 +182,7 @@ function twig_last(Environment $env, $item) /** * @internal + * * @deprecated since Twig 3.9 */ function twig_join_filter($value, $glue = '', $and = null) @@ -179,6 +194,7 @@ function twig_join_filter($value, $glue = '', $and = null) /** * @internal + * * @deprecated since Twig 3.9 */ function twig_split_filter(Environment $env, $value, $delimiter, $limit = null) @@ -190,6 +206,7 @@ function twig_split_filter(Environment $env, $value, $delimiter, $limit = null) /** * @internal + * * @deprecated since Twig 3.9 */ function twig_get_array_keys_filter($array) @@ -201,6 +218,7 @@ function twig_get_array_keys_filter($array) /** * @internal + * * @deprecated since Twig 3.9 */ function twig_reverse_filter(Environment $env, $item, $preserveKeys = false) @@ -212,6 +230,7 @@ function twig_reverse_filter(Environment $env, $item, $preserveKeys = false) /** * @internal + * * @deprecated since Twig 3.9 */ function twig_sort_filter(Environment $env, $array, $arrow = null) @@ -223,6 +242,7 @@ function twig_sort_filter(Environment $env, $array, $arrow = null) /** * @internal + * * @deprecated since Twig 3.9 */ function twig_matches(string $regexp, ?string $str) @@ -234,6 +254,7 @@ function twig_matches(string $regexp, ?string $str) /** * @internal + * * @deprecated since Twig 3.9 */ function twig_trim_filter($string, $characterMask = null, $side = 'both') @@ -245,6 +266,7 @@ function twig_trim_filter($string, $characterMask = null, $side = 'both') /** * @internal + * * @deprecated since Twig 3.9 */ function twig_nl2br($string) @@ -256,6 +278,7 @@ function twig_nl2br($string) /** * @internal + * * @deprecated since Twig 3.9 */ function twig_spaceless($content) @@ -267,6 +290,7 @@ function twig_spaceless($content) /** * @internal + * * @deprecated since Twig 3.9 */ function twig_convert_encoding($string, $to, $from) @@ -278,6 +302,7 @@ function twig_convert_encoding($string, $to, $from) /** * @internal + * * @deprecated since Twig 3.9 */ function twig_length_filter(Environment $env, $thing) @@ -289,6 +314,7 @@ function twig_length_filter(Environment $env, $thing) /** * @internal + * * @deprecated since Twig 3.9 */ function twig_upper_filter(Environment $env, $string) @@ -300,6 +326,7 @@ function twig_upper_filter(Environment $env, $string) /** * @internal + * * @deprecated since Twig 3.9 */ function twig_lower_filter(Environment $env, $string) @@ -311,6 +338,7 @@ function twig_lower_filter(Environment $env, $string) /** * @internal + * * @deprecated since Twig 3.9 */ function twig_striptags($string, $allowable_tags = null) @@ -322,6 +350,7 @@ function twig_striptags($string, $allowable_tags = null) /** * @internal + * * @deprecated since Twig 3.9 */ function twig_title_string_filter(Environment $env, $string) @@ -333,6 +362,7 @@ function twig_title_string_filter(Environment $env, $string) /** * @internal + * * @deprecated since Twig 3.9 */ function twig_capitalize_string_filter(Environment $env, $string) @@ -344,6 +374,7 @@ function twig_capitalize_string_filter(Environment $env, $string) /** * @internal + * * @deprecated since Twig 3.9 */ function twig_test_empty($value) @@ -355,6 +386,7 @@ function twig_test_empty($value) /** * @internal + * * @deprecated since Twig 3.9 */ function twig_test_iterable($value) @@ -366,6 +398,7 @@ function twig_test_iterable($value) /** * @internal + * * @deprecated since Twig 3.9 */ function twig_include(Environment $env, $context, $template, $variables = [], $withContext = true, $ignoreMissing = false, $sandboxed = false) @@ -377,6 +410,7 @@ function twig_include(Environment $env, $context, $template, $variables = [], $w /** * @internal + * * @deprecated since Twig 3.9 */ function twig_source(Environment $env, $name, $ignoreMissing = false) @@ -388,6 +422,7 @@ function twig_source(Environment $env, $name, $ignoreMissing = false) /** * @internal + * * @deprecated since Twig 3.9 */ function twig_constant($constant, $object = null) @@ -399,6 +434,7 @@ function twig_constant($constant, $object = null) /** * @internal + * * @deprecated since Twig 3.9 */ function twig_constant_is_defined($constant, $object = null) @@ -410,6 +446,7 @@ function twig_constant_is_defined($constant, $object = null) /** * @internal + * * @deprecated since Twig 3.9 */ function twig_array_batch($items, $size, $fill = null, $preserveKeys = true) @@ -421,6 +458,7 @@ function twig_array_batch($items, $size, $fill = null, $preserveKeys = true) /** * @internal + * * @deprecated since Twig 3.9 */ function twig_array_column($array, $name, $index = null): array @@ -432,6 +470,7 @@ function twig_array_column($array, $name, $index = null): array /** * @internal + * * @deprecated since Twig 3.9 */ function twig_array_filter(Environment $env, $array, $arrow) @@ -443,6 +482,7 @@ function twig_array_filter(Environment $env, $array, $arrow) /** * @internal + * * @deprecated since Twig 3.9 */ function twig_array_map(Environment $env, $array, $arrow) @@ -454,6 +494,7 @@ function twig_array_map(Environment $env, $array, $arrow) /** * @internal + * * @deprecated since Twig 3.9 */ function twig_array_reduce(Environment $env, $array, $arrow, $initial = null) @@ -465,6 +506,7 @@ function twig_array_reduce(Environment $env, $array, $arrow, $initial = null) /** * @internal + * * @deprecated since Twig 3.9 */ function twig_array_some(Environment $env, $array, $arrow) @@ -476,6 +518,7 @@ function twig_array_some(Environment $env, $array, $arrow) /** * @internal + * * @deprecated since Twig 3.9 */ function twig_array_every(Environment $env, $array, $arrow) @@ -487,6 +530,7 @@ function twig_array_every(Environment $env, $array, $arrow) /** * @internal + * * @deprecated since Twig 3.9 */ function twig_check_arrow_in_sandbox(Environment $env, $arrow, $thing, $type) diff --git a/src/Resources/debug.php b/src/Resources/debug.php index 6f59cf6c13d..104b4f4e0d2 100644 --- a/src/Resources/debug.php +++ b/src/Resources/debug.php @@ -14,6 +14,7 @@ /** * @internal + * * @deprecated since Twig 3.9 */ function twig_var_dump(Environment $env, $context, ...$vars) From 0d1a937f61aa36aab95d5315d187501bb6da1ae0 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Wed, 1 May 2024 18:23:14 +0200 Subject: [PATCH 237/812] Fix CS --- src/Node/CheckSecurityCallNode.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Node/CheckSecurityCallNode.php b/src/Node/CheckSecurityCallNode.php index d5f45761895..66aaeb52c29 100644 --- a/src/Node/CheckSecurityCallNode.php +++ b/src/Node/CheckSecurityCallNode.php @@ -23,7 +23,7 @@ class CheckSecurityCallNode extends Node public function compile(Compiler $compiler) { $compiler - ->write("\$this->sandbox = \$this->env->getExtension('\Twig\Extension\SandboxExtension');\n") + ->write("\$this->sandbox = \$this->env->getExtension(SandboxExtension::class);\n") ->write("\$this->checkSecurity();\n") ; } From bee4263cd6b41e846a3f3ad4644d7113f612fde2 Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Thu, 2 May 2024 09:47:24 +0200 Subject: [PATCH 238/812] fix class names --- CHANGELOG | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index c70d8ec2b5e..065b8baa039 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -3,8 +3,8 @@ * Make `CoreExtension::formatDate`, `CoreExtension::convertDate`, and `CoreExtension::formatNumber` part of the public API * Add `needs_charset` option for filters and functions - * Extract the escaping logic from the `EscapingExtension` class to a new - `EscapingRuntime` class. + * Extract the escaping logic from the `EscaperExtension` class to a new + `EscaperRuntime` class. The following methods from ``Twig\\Extension\\EscaperExtension`` are deprecated: ``setEscaper()``, ``getEscapers()``, ``setDefaultStrategy()``, From 59542c3cafb30beea2e256688f230e37a29fb1ca Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sat, 4 May 2024 08:32:54 +0200 Subject: [PATCH 239/812] Fix wrong tests --- src/Extension/EscaperExtension.php | 2 +- tests/Extension/EscaperTest.php | 14 +++++++------- tests/Runtime/EscaperRuntimeTest.php | 9 ++------- 3 files changed, 10 insertions(+), 15 deletions(-) diff --git a/src/Extension/EscaperExtension.php b/src/Extension/EscaperExtension.php index cbac1f6a011..82cab284102 100644 --- a/src/Extension/EscaperExtension.php +++ b/src/Extension/EscaperExtension.php @@ -110,7 +110,7 @@ public function setEscaper($strategy, callable $callable) $this->escapers[$strategy] = $callable; $callable = function ($string, $charset) use ($callable) { - return $callable($this->environment, $string, $charset); + return $callable($this->environment, $string); }; $this->escaper->setEscaper($strategy, $callable); diff --git a/tests/Extension/EscaperTest.php b/tests/Extension/EscaperTest.php index d53f3b52970..e51f25e1eaa 100644 --- a/tests/Extension/EscaperTest.php +++ b/tests/Extension/EscaperTest.php @@ -29,7 +29,7 @@ public function testCustomEscaper($expected, $string, $strategy) $twig = new Environment($this->createMock(LoaderInterface::class)); $escaperExt = $twig->getExtension(EscaperExtension::class); $escaperExt->setEnvironment($twig); - $escaperExt->setEscaper('foo', 'Twig\Tests\foo_escaper_for_test'); + $escaperExt->setEscaper('foo', 'Twig\Tests\legacy_escaper'); $this->assertSame($expected, $twig->getRuntime(EscaperRuntime::class)->escape($string, $strategy)); } @@ -50,24 +50,24 @@ public function testCustomEscapersOnMultipleEnvs() $env1 = new Environment($this->createMock(LoaderInterface::class)); $escaperExt1 = $env1->getExtension(EscaperExtension::class); $escaperExt1->setEnvironment($env1); - $escaperExt1->setEscaper('foo', 'Twig\Tests\foo_escaper_for_test'); + $escaperExt1->setEscaper('foo', 'Twig\Tests\legacy_escaper'); $env2 = new Environment($this->createMock(LoaderInterface::class)); $escaperExt2 = $env2->getExtension(EscaperExtension::class); $escaperExt2->setEnvironment($env2); - $escaperExt2->setEscaper('foo', 'Twig\Tests\foo_escaper_for_test1'); + $escaperExt2->setEscaper('foo', 'Twig\Tests\legacy_escaper_again'); $this->assertSame('fooUTF-8', $env1->getRuntime(EscaperRuntime::class)->escape('foo', 'foo')); $this->assertSame('fooUTF-81', $env2->getRuntime(EscaperRuntime::class)->escape('foo', 'foo')); } } -function foo_escaper_for_test(Environment $twig, $string, $charset) +function legacy_escaper(Environment $twig, $string) { - return $string.$charset; + return $string.$twig->getCharset(); } -function foo_escaper_for_test1(Environment $twig, $string, $charset) +function legacy_escaper_again(Environment $twig, $string) { - return $string.$charset.'1'; + return $string.$twig->getCharset().'1'; } diff --git a/tests/Runtime/EscaperRuntimeTest.php b/tests/Runtime/EscaperRuntimeTest.php index 2dc4bb95e56..042473408d3 100644 --- a/tests/Runtime/EscaperRuntimeTest.php +++ b/tests/Runtime/EscaperRuntimeTest.php @@ -353,7 +353,7 @@ public function testUnknownCustomEscaper() public function testCustomEscaper($expected, $string, $strategy) { $escaper = new EscaperRuntime(); - $escaper->setEscaper('foo', 'Twig\Tests\foo_escaper_for_test_runtime'); + $escaper->setEscaper('foo', 'Twig\Tests\escaper'); $this->assertSame($expected, $escaper->escape($string, $strategy)); } @@ -389,16 +389,11 @@ public function provideObjectsForEscaping() } } -function foo_escaper_for_test_runtime($string, $charset) +function escaper($string, $charset) { return $string.$charset; } -function foo_escaper_for_test1_runtime($string, $charset) -{ - return $string.$charset.'1'; -} - interface Extension_SafeHtmlInterface { } From 22810841ba22224b99cb231d1bf582d51f539cfc Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sat, 4 May 2024 08:48:54 +0200 Subject: [PATCH 240/812] Move back default strategy to the extension --- CHANGELOG | 6 ++-- doc/deprecated.rst | 6 ++-- src/Environment.php | 4 +-- src/Extension/EscaperExtension.php | 30 +++++++++++------ src/NodeVisitor/EscaperNodeVisitor.php | 3 +- src/Runtime/EscaperRuntime.php | 45 +------------------------- 6 files changed, 30 insertions(+), 64 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 065b8baa039..5098440bdb2 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -7,9 +7,9 @@ `EscaperRuntime` class. The following methods from ``Twig\\Extension\\EscaperExtension`` are - deprecated: ``setEscaper()``, ``getEscapers()``, ``setDefaultStrategy()``, - ``getDefaultStrategy()``, ``setSafeClasses``, ``addSafeClasses()``. Use the - same methods on the ``Twig\\Runtime\\EscaperRuntime`` class instead. + deprecated: ``setEscaper()``, ``getEscapers()``, ``setSafeClasses``, + ``addSafeClasses()``. Use the same methods on the + ``Twig\\Runtime\\EscaperRuntime`` class instead. # 3.9.3 (2024-04-18) diff --git a/doc/deprecated.rst b/doc/deprecated.rst index e770f3ff607..27ec642b200 100644 --- a/doc/deprecated.rst +++ b/doc/deprecated.rst @@ -22,9 +22,9 @@ Extensions ``$env->getRuntime(EscaperRuntime::class)->escape()`` instead. * The following methods from ``Twig\Extension\EscaperExtension`` are - deprecated: ``setEscaper()``, ``getEscapers()``, ``setDefaultStrategy()``, - ``getDefaultStrategy()``, ``setSafeClasses``, ``addSafeClasses()``. Use the - same methods on the ``Twig\Runtime\EscaperRuntime`` class instead. + deprecated: ``setEscaper()``, ``getEscapers()``, ``setSafeClasses``, + ``addSafeClasses()``. Use the same methods on the + ``Twig\Runtime\EscaperRuntime`` class instead: Before: $twig->getExtension(EscaperExtension::class)->METHOD() diff --git a/src/Environment.php b/src/Environment.php index bb2a257d122..2405e41fa41 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -131,11 +131,11 @@ public function __construct(LoaderInterface $loader, $options = []) $this->setCache($options['cache']); $this->extensionSet = new ExtensionSet(); $this->defaultRuntimeLoader = new FactoryRuntimeLoader([ - EscaperRuntime::class => function () use ($options) { return new EscaperRuntime($options['autoescape'], $this->charset); }, + EscaperRuntime::class => function () { return new EscaperRuntime($this->charset); }, ]); $this->addExtension(new CoreExtension()); - $this->addExtension(new EscaperExtension($this->getRuntime(EscaperRuntime::class))); + $this->addExtension(new EscaperExtension($this->getRuntime(EscaperRuntime::class), $options['autoescape'])); if (\PHP_VERSION_ID >= 80000) { $this->addExtension(new YieldNotReadyExtension($this->useYield)); } diff --git a/src/Extension/EscaperExtension.php b/src/Extension/EscaperExtension.php index cbac1f6a011..a44e84a2a09 100644 --- a/src/Extension/EscaperExtension.php +++ b/src/Extension/EscaperExtension.php @@ -12,6 +12,7 @@ namespace Twig\Extension; use Twig\Environment; +use Twig\FileExtensionEscapingStrategy; use Twig\Node\Expression\ConstantExpression; use Twig\Node\Node; use Twig\NodeVisitor\EscaperNodeVisitor; @@ -24,9 +25,16 @@ final class EscaperExtension extends AbstractExtension private $environment; private $escapers = []; private $escaper; + private $defaultStrategy; - public function __construct(EscaperRuntime $escaper) + /** + * @param string|false|callable $defaultStrategy An escaping strategy + * + * @see setDefaultStrategy() + */ + public function __construct(EscaperRuntime $escaper, $defaultStrategy = 'html') { + $this->setDefaultStrategy($defaultStrategy); $this->escaper = $escaper; } @@ -65,15 +73,15 @@ public function setEnvironment(Environment $environment): void * The strategy can be a valid PHP callback that takes the template * name as an argument and returns the strategy to use. * - * @param string|false|callable $defaultStrategy An escaping strategy - * - * @deprecated since Twig 3.10 + * @param string|false|callable(string $templateName): string $defaultStrategy An escaping strategy */ public function setDefaultStrategy($defaultStrategy): void { - trigger_deprecation('twig/twig', '3.10', 'The "%s()" method is deprecated, use the "Twig\Runtime\EscaperRuntime::setDefaultStrategy()" method instead.', __METHOD__); + if ('name' === $defaultStrategy) { + $defaultStrategy = [FileExtensionEscapingStrategy::class, 'guess']; + } - $this->escaper->setDefaultStrategy($defaultStrategy); + $this->defaultStrategy = $defaultStrategy; } /** @@ -82,14 +90,16 @@ public function setDefaultStrategy($defaultStrategy): void * @param string $name The template name * * @return string|false The default strategy to use for the template - * - * @deprecated since Twig 3.10 */ public function getDefaultStrategy(string $name) { - trigger_deprecation('twig/twig', '3.10', 'The "%s()" method is deprecated, use the "Twig\Runtime\EscaperRuntime::getDefaultStrategy()" method instead.', __METHOD__); + // disable string callables to avoid calling a function named html or js, + // or any other upcoming escaping strategy + if (!\is_string($this->defaultStrategy) && false !== $this->defaultStrategy) { + return \call_user_func($this->defaultStrategy, $name); + } - return $this->escaper->getDefaultStrategy($name); + return $this->defaultStrategy; } /** diff --git a/src/NodeVisitor/EscaperNodeVisitor.php b/src/NodeVisitor/EscaperNodeVisitor.php index fe9b3472ecd..91e2ea89392 100644 --- a/src/NodeVisitor/EscaperNodeVisitor.php +++ b/src/NodeVisitor/EscaperNodeVisitor.php @@ -26,7 +26,6 @@ use Twig\Node\Node; use Twig\Node\PrintNode; use Twig\NodeTraverser; -use Twig\Runtime\EscaperRuntime; /** * @author Fabien Potencier @@ -50,7 +49,7 @@ public function __construct() public function enterNode(Node $node, Environment $env): Node { if ($node instanceof ModuleNode) { - if ($env->hasExtension(EscaperExtension::class) && $defaultStrategy = $env->getRuntime(EscaperRuntime::class)->getDefaultStrategy($node->getTemplateName())) { + if ($env->hasExtension(EscaperExtension::class) && $defaultStrategy = $env->getExtension(EscaperExtension::class)->getDefaultStrategy($node->getTemplateName())) { $this->defaultStrategy = $defaultStrategy; } $this->safeVars = []; diff --git a/src/Runtime/EscaperRuntime.php b/src/Runtime/EscaperRuntime.php index 0124bfe247e..0e38ae195d0 100644 --- a/src/Runtime/EscaperRuntime.php +++ b/src/Runtime/EscaperRuntime.php @@ -13,12 +13,10 @@ use Twig\Error\RuntimeError; use Twig\Extension\RuntimeExtensionInterface; -use Twig\FileExtensionEscapingStrategy; use Twig\Markup; final class EscaperRuntime implements RuntimeExtensionInterface { - private $defaultStrategy; private $escapers = []; /** @internal */ @@ -29,52 +27,11 @@ final class EscaperRuntime implements RuntimeExtensionInterface private $charset; - /** - * @param string|false|callable $defaultStrategy An escaping strategy - * - * @see setDefaultStrategy() - */ - public function __construct($defaultStrategy = 'html', $charset = 'UTF-8') + public function __construct($charset = 'UTF-8') { - $this->setDefaultStrategy($defaultStrategy); $this->charset = $charset; } - /** - * Sets the default strategy to use when not defined by the user. - * - * The strategy can be a valid PHP callback that takes the template - * name as an argument and returns the strategy to use. - * - * @param string|false|callable $defaultStrategy An escaping strategy - */ - public function setDefaultStrategy($defaultStrategy): void - { - if ('name' === $defaultStrategy) { - $defaultStrategy = [FileExtensionEscapingStrategy::class, 'guess']; - } - - $this->defaultStrategy = $defaultStrategy; - } - - /** - * Gets the default strategy to use when not defined by the user. - * - * @param string $name The template name - * - * @return string|false The default strategy to use for the template - */ - public function getDefaultStrategy(string $name) - { - // disable string callables to avoid calling a function named html or js, - // or any other upcoming escaping strategy - if (!\is_string($this->defaultStrategy) && false !== $this->defaultStrategy) { - return \call_user_func($this->defaultStrategy, $name); - } - - return $this->defaultStrategy; - } - /** * Defines a new escaper to be used via the escape filter. * From f2c9795b26a0ec68ad78446ee3c9005991a08c2a Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sat, 4 May 2024 16:56:38 +0200 Subject: [PATCH 241/812] Fix EscaperExtension constructor to not change from previous releases (will be the same in 4.x as well) --- src/Environment.php | 2 +- src/Extension/EscaperExtension.php | 26 +++++++++++++++++++++++--- tests/EnvironmentTest.php | 2 +- tests/Extension/EscaperTest.php | 3 +++ 4 files changed, 28 insertions(+), 5 deletions(-) diff --git a/src/Environment.php b/src/Environment.php index 2405e41fa41..ea1d10ec557 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -135,7 +135,7 @@ public function __construct(LoaderInterface $loader, $options = []) ]); $this->addExtension(new CoreExtension()); - $this->addExtension(new EscaperExtension($this->getRuntime(EscaperRuntime::class), $options['autoescape'])); + $this->addExtension(new EscaperExtension($options['autoescape'])); if (\PHP_VERSION_ID >= 80000) { $this->addExtension(new YieldNotReadyExtension($this->useYield)); } diff --git a/src/Extension/EscaperExtension.php b/src/Extension/EscaperExtension.php index 8343bfd399c..a0654cd8c18 100644 --- a/src/Extension/EscaperExtension.php +++ b/src/Extension/EscaperExtension.php @@ -32,10 +32,9 @@ final class EscaperExtension extends AbstractExtension * * @see setDefaultStrategy() */ - public function __construct(EscaperRuntime $escaper, $defaultStrategy = 'html') + public function __construct($defaultStrategy = 'html') { $this->setDefaultStrategy($defaultStrategy); - $this->escaper = $escaper; } public function getTokenParsers(): array @@ -67,6 +66,16 @@ public function setEnvironment(Environment $environment): void $this->environment = $environment; } + /** + * @deprecated since Twig 3.10 + */ + public function setEscaperRuntime(EscaperRuntime $escaper) + { + trigger_deprecation('twig/twig', '3.10', 'The "%s()" method is deprecated and not needed if you are using methods from "Twig\Runtime\EscaperRuntime".', __METHOD__); + + $this->escaper = $escaper; + } + /** * Sets the default strategy to use when not defined by the user. * @@ -115,7 +124,10 @@ public function setEscaper($strategy, callable $callable) trigger_deprecation('twig/twig', '3.10', 'The "%s()" method is deprecated, use the "Twig\Runtime\EscaperRuntime::setEscaper()" method instead (be warned that Environment is not passed anymore to the callable).', __METHOD__); if (!isset($this->environment)) { - throw new \LogicException('You must call setEnvironment() before calling setEscaper().'); + throw new \LogicException(sprintf('You must call "setEnvironment()" before calling "%s()".', __METHOD__)); + } + if (!isset($this->escaper)) { + throw new \LogicException(sprintf('You must call "setEscaperRuntime()" before calling "%s()".', __METHOD__)); } $this->escapers[$strategy] = $callable; @@ -147,6 +159,10 @@ public function setSafeClasses(array $safeClasses = []) { trigger_deprecation('twig/twig', '3.10', 'The "%s()" method is deprecated, use the "Twig\Runtime\EscaperRuntime::setSafeClasses()" method instead.', __METHOD__); + if (!isset($this->escaper)) { + throw new \LogicException(sprintf('You must call "setEscaperRuntime()" before calling %s().', __METHOD__)); + } + $this->escaper->setSafeClasses($safeClasses); } @@ -157,6 +173,10 @@ public function addSafeClass(string $class, array $strategies) { trigger_deprecation('twig/twig', '3.10', 'The "%s()" method is deprecated, use the "Twig\Runtime\EscaperRuntime::addSafeClass()" method instead.', __METHOD__); + if (!isset($this->escaper)) { + throw new \LogicException(sprintf('You must call setEscaperRuntime() before calling %s().', __METHOD__)); + } + $this->escaper->addSafeClass($class, $strategies); } diff --git a/tests/EnvironmentTest.php b/tests/EnvironmentTest.php index 57a3bf34d85..4eb22036e6b 100644 --- a/tests/EnvironmentTest.php +++ b/tests/EnvironmentTest.php @@ -334,7 +334,7 @@ public function testAddRuntimeLoader() 'func_string_named_args' => '{{ from_runtime_string(name="foo") }}', ]); - $twig = new Environment($loader); + $twig = new Environment($loader, ['autoescape' => false]); $twig->addExtension(new EnvironmentTest_ExtensionWithoutRuntime()); $twig->addRuntimeLoader($runtimeLoader); diff --git a/tests/Extension/EscaperTest.php b/tests/Extension/EscaperTest.php index e51f25e1eaa..b23930b5a8e 100644 --- a/tests/Extension/EscaperTest.php +++ b/tests/Extension/EscaperTest.php @@ -29,6 +29,7 @@ public function testCustomEscaper($expected, $string, $strategy) $twig = new Environment($this->createMock(LoaderInterface::class)); $escaperExt = $twig->getExtension(EscaperExtension::class); $escaperExt->setEnvironment($twig); + $escaperExt->setEscaperRuntime($twig->getRuntime(EscaperRuntime::class)); $escaperExt->setEscaper('foo', 'Twig\Tests\legacy_escaper'); $this->assertSame($expected, $twig->getRuntime(EscaperRuntime::class)->escape($string, $strategy)); } @@ -50,10 +51,12 @@ public function testCustomEscapersOnMultipleEnvs() $env1 = new Environment($this->createMock(LoaderInterface::class)); $escaperExt1 = $env1->getExtension(EscaperExtension::class); $escaperExt1->setEnvironment($env1); + $escaperExt1->setEscaperRuntime($env1->getRuntime(EscaperRuntime::class)); $escaperExt1->setEscaper('foo', 'Twig\Tests\legacy_escaper'); $env2 = new Environment($this->createMock(LoaderInterface::class)); $escaperExt2 = $env2->getExtension(EscaperExtension::class); + $escaperExt2->setEscaperRuntime($env2->getRuntime(EscaperRuntime::class)); $escaperExt2->setEnvironment($env2); $escaperExt2->setEscaper('foo', 'Twig\Tests\legacy_escaper_again'); From 00c43a9c3890dc3c74cac7f92600bd91ef8c35e1 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sat, 4 May 2024 19:05:47 +0200 Subject: [PATCH 242/812] Fix wrong @throws annotation --- src/Runtime/EscaperRuntime.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Runtime/EscaperRuntime.php b/src/Runtime/EscaperRuntime.php index 0e38ae195d0..eb5dfd06cf6 100644 --- a/src/Runtime/EscaperRuntime.php +++ b/src/Runtime/EscaperRuntime.php @@ -83,7 +83,7 @@ public function addSafeClass(string $class, array $strategies) * @param string|null $charset The charset * @param bool $autoescape Whether the function is called by the auto-escaping feature (true) or by the developer (false) * - * @throws RuntimeException + * @throws RuntimeError */ public function escape($string, string $strategy = 'html', ?string $charset = null, bool $autoescape = false) { From 799355dc823c80467f82670dfca8b57e9cd09622 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Wed, 8 May 2024 11:17:29 +0200 Subject: [PATCH 243/812] Remove double ; in generated code --- src/Node/CaptureNode.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Node/CaptureNode.php b/src/Node/CaptureNode.php index 534f1d01f17..068dcfaafe5 100644 --- a/src/Node/CaptureNode.php +++ b/src/Node/CaptureNode.php @@ -53,8 +53,7 @@ public function compile(Compiler $compiler): void $compiler->raw(')'); } if (!$this->getAttribute('raw')) { - $compiler->raw(") ? '' : new Markup(\$tmp, \$this->env->getCharset())"); + $compiler->raw(") ? '' : new Markup(\$tmp, \$this->env->getCharset());"); } - $compiler->raw(';'); } } From d30b72c36474ca4bea543794f879d0b4673fb254 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sat, 11 May 2024 08:44:27 +0200 Subject: [PATCH 244/812] Fix blocks not available under some circumstancies --- extra/cache-extra/Node/CacheNode.php | 2 +- src/Node/CaptureNode.php | 10 +++------- src/Node/MacroNode.php | 1 - src/Node/SetNode.php | 1 - 4 files changed, 4 insertions(+), 10 deletions(-) diff --git a/extra/cache-extra/Node/CacheNode.php b/extra/cache-extra/Node/CacheNode.php index de6d964fd3c..5cb73c592c1 100644 --- a/extra/cache-extra/Node/CacheNode.php +++ b/extra/cache-extra/Node/CacheNode.php @@ -40,7 +40,7 @@ public function compile(Compiler $compiler): void ->addDebugInfo($this) ->raw('$this->env->getRuntime(\'Twig\Extra\Cache\CacheRuntime\')->getCache()->get(') ->subcompile($this->getNode('key')) - ->raw(", function (\Symfony\Contracts\Cache\ItemInterface \$item) use (\$context, \$macros) {\n") + ->raw(", function (\Symfony\Contracts\Cache\ItemInterface \$item) use (\$context, \$macros, \$blocks) {\n") ->indent() ; diff --git a/src/Node/CaptureNode.php b/src/Node/CaptureNode.php index 534f1d01f17..d7db218c3c1 100644 --- a/src/Node/CaptureNode.php +++ b/src/Node/CaptureNode.php @@ -24,7 +24,7 @@ class CaptureNode extends Node { public function __construct(Node $body, int $lineno, ?string $tag = null) { - parent::__construct(['body' => $body], ['raw' => false, 'with_blocks' => false], $lineno, $tag); + parent::__construct(['body' => $body], ['raw' => false], $lineno, $tag); } public function compile(Compiler $compiler): void @@ -34,13 +34,9 @@ public function compile(Compiler $compiler): void if (!$this->getAttribute('raw')) { $compiler->raw("('' === \$tmp = "); } - $compiler->raw($useYield ? "implode('', iterator_to_array(" : '\\Twig\\Extension\\CoreExtension::captureOutput('); - if ($this->getAttribute('with_blocks')) { - $compiler->raw("(function () use (&\$context, \$macros, \$blocks) {\n"); - } else { - $compiler->raw("(function () use (&\$context, \$macros) {\n"); - } $compiler + ->raw($useYield ? "implode('', iterator_to_array(" : '\\Twig\\Extension\\CoreExtension::captureOutput(') + ->raw("(function () use (&\$context, \$macros, \$blocks) {\n") ->indent() ->subcompile($this->getNode('body')) ->write("return; yield '';\n") diff --git a/src/Node/MacroNode.php b/src/Node/MacroNode.php index 761ef55d2fb..78e1b6f76dd 100644 --- a/src/Node/MacroNode.php +++ b/src/Node/MacroNode.php @@ -80,7 +80,6 @@ public function compile(Compiler $compiler): void } $node = new CaptureNode($this->getNode('body'), $this->getNode('body')->lineno, $this->getNode('body')->tag); - $node->setAttribute('with_blocks', true); $compiler ->write('') diff --git a/src/Node/SetNode.php b/src/Node/SetNode.php index 6b4c873e1ab..0900f1542a9 100644 --- a/src/Node/SetNode.php +++ b/src/Node/SetNode.php @@ -38,7 +38,6 @@ public function __construct(bool $capture, Node $names, Node $values, int $linen $capture = false; } else { $values = new CaptureNode($values, $values->getTemplateLine()); - $values->setAttribute('with_blocks', true); } } From 55ae21493b5770634c95a23434f7c97dd6065d6e Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sat, 11 May 2024 08:59:04 +0200 Subject: [PATCH 245/812] Add more tests --- .../cache-extra/Tests/Fixtures/cache_complex.test | 15 +++++++++++++++ .../Tests/Fixtures/cache_with_blocks.test | 15 +++++++++++++++ 2 files changed, 30 insertions(+) create mode 100644 extra/cache-extra/Tests/Fixtures/cache_complex.test create mode 100644 extra/cache-extra/Tests/Fixtures/cache_with_blocks.test diff --git a/extra/cache-extra/Tests/Fixtures/cache_complex.test b/extra/cache-extra/Tests/Fixtures/cache_complex.test new file mode 100644 index 00000000000..5a2e28f4709 --- /dev/null +++ b/extra/cache-extra/Tests/Fixtures/cache_complex.test @@ -0,0 +1,15 @@ +--TEST-- +"cache" tag +--TEMPLATE-- +{% cache 'test_%s_%s'|format(10, 10000) ttl(36000) %} + {% set content %} + OK + {% endset %} + {% apply spaceless %} + {{ content }} + {% endapply %} +{% endcache %} +--DATA-- +return [] +--EXPECT-- +OK diff --git a/extra/cache-extra/Tests/Fixtures/cache_with_blocks.test b/extra/cache-extra/Tests/Fixtures/cache_with_blocks.test new file mode 100644 index 00000000000..79721d7fca8 --- /dev/null +++ b/extra/cache-extra/Tests/Fixtures/cache_with_blocks.test @@ -0,0 +1,15 @@ +--TEST-- +"cache" tag +--TEMPLATE-- +{% extends "layout.twig" %} +{% block bar %} + {% cache "foo" %} + {%- block content %}FOO{% endblock %} + {% endcache %} +{% endblock %} +--TEMPLATE(layout.twig)-- +{% block content %}{% endblock %} +--DATA-- +return [] +--EXPECT-- +FOO From 318f7b8bb637985bbe9e0b1f79dc8c11fa21a662 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sat, 11 May 2024 09:09:43 +0200 Subject: [PATCH 246/812] Fix typo --- extra/cache-extra/Tests/Fixtures/cache_complex.test | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extra/cache-extra/Tests/Fixtures/cache_complex.test b/extra/cache-extra/Tests/Fixtures/cache_complex.test index 5a2e28f4709..30aa729a26b 100644 --- a/extra/cache-extra/Tests/Fixtures/cache_complex.test +++ b/extra/cache-extra/Tests/Fixtures/cache_complex.test @@ -6,7 +6,7 @@ OK {% endset %} {% apply spaceless %} - {{ content }} + {{ content }} {% endapply %} {% endcache %} --DATA-- From 761aa56a6f5aa98d5054b6d4a536f0f78a9271f3 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sat, 11 May 2024 09:35:57 +0200 Subject: [PATCH 247/812] Revert "minor #4042 Auto-close PRs on subtree-splits (kbond)" This reverts commit 12625c0c050db5a71d94ab8d599bf711673a41c3, reversing changes made to ad934312cd08b6b466932fdfdd5b97ae680bc28a. --- .github/sync-packages.php | 71 ------------------- .github/workflows/package-tests.yml | 26 ------- extra/cache-extra/.gitattributes | 1 - .../.github/PULL_REQUEST_TEMPLATE.md | 8 --- .../.github/workflows/check-subtree-split.yml | 33 --------- extra/cssinliner-extra/.gitattributes | 1 - .../.github/PULL_REQUEST_TEMPLATE.md | 8 --- .../.github/workflows/check-subtree-split.yml | 33 --------- extra/html-extra/.gitattributes | 1 - .../.github/PULL_REQUEST_TEMPLATE.md | 8 --- .../.github/workflows/check-subtree-split.yml | 33 --------- extra/inky-extra/.gitattributes | 1 - .../.github/PULL_REQUEST_TEMPLATE.md | 8 --- .../.github/workflows/check-subtree-split.yml | 33 --------- extra/intl-extra/.gitattributes | 1 - .../.github/PULL_REQUEST_TEMPLATE.md | 8 --- .../.github/workflows/check-subtree-split.yml | 33 --------- extra/markdown-extra/.gitattributes | 1 - .../.github/PULL_REQUEST_TEMPLATE.md | 8 --- .../.github/workflows/check-subtree-split.yml | 33 --------- extra/string-extra/.gitattributes | 1 - .../.github/PULL_REQUEST_TEMPLATE.md | 8 --- .../.github/workflows/check-subtree-split.yml | 33 --------- extra/twig-extra-bundle/.gitattributes | 1 - .../.github/PULL_REQUEST_TEMPLATE.md | 8 --- .../.github/workflows/check-subtree-split.yml | 33 --------- 26 files changed, 433 deletions(-) delete mode 100644 .github/sync-packages.php delete mode 100644 .github/workflows/package-tests.yml delete mode 100644 extra/cache-extra/.github/PULL_REQUEST_TEMPLATE.md delete mode 100644 extra/cache-extra/.github/workflows/check-subtree-split.yml delete mode 100644 extra/cssinliner-extra/.github/PULL_REQUEST_TEMPLATE.md delete mode 100644 extra/cssinliner-extra/.github/workflows/check-subtree-split.yml delete mode 100644 extra/html-extra/.github/PULL_REQUEST_TEMPLATE.md delete mode 100644 extra/html-extra/.github/workflows/check-subtree-split.yml delete mode 100644 extra/inky-extra/.github/PULL_REQUEST_TEMPLATE.md delete mode 100644 extra/inky-extra/.github/workflows/check-subtree-split.yml delete mode 100644 extra/intl-extra/.github/PULL_REQUEST_TEMPLATE.md delete mode 100644 extra/intl-extra/.github/workflows/check-subtree-split.yml delete mode 100644 extra/markdown-extra/.github/PULL_REQUEST_TEMPLATE.md delete mode 100644 extra/markdown-extra/.github/workflows/check-subtree-split.yml delete mode 100644 extra/string-extra/.github/PULL_REQUEST_TEMPLATE.md delete mode 100644 extra/string-extra/.github/workflows/check-subtree-split.yml delete mode 100644 extra/twig-extra-bundle/.github/PULL_REQUEST_TEMPLATE.md delete mode 100644 extra/twig-extra-bundle/.github/workflows/check-subtree-split.yml diff --git a/.github/sync-packages.php b/.github/sync-packages.php deleted file mode 100644 index 4c47843aa4e..00000000000 --- a/.github/sync-packages.php +++ /dev/null @@ -1,71 +0,0 @@ - Date: Sat, 11 May 2024 09:37:03 +0200 Subject: [PATCH 248/812] Fix markup --- doc/templates.rst | 1 - 1 file changed, 1 deletion(-) diff --git a/doc/templates.rst b/doc/templates.rst index 5ec7eb7f786..1026dd8eac6 100644 --- a/doc/templates.rst +++ b/doc/templates.rst @@ -793,7 +793,6 @@ The following operators don't fit into any of the other categories: {% set numbers = [1, 2, ...moreNumbers] %} {% set ratings = {'foo': 10, 'bar': 5, ...moreRatings} %} - Operators ~~~~~~~~~ From 87adff0c14d56f8ccc44493e3219416790a80e48 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sat, 11 May 2024 09:43:08 +0200 Subject: [PATCH 249/812] Update CHANGELOG --- CHANGELOG | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index 5098440bdb2..cdf3c80e952 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -10,6 +10,10 @@ deprecated: ``setEscaper()``, ``getEscapers()``, ``setSafeClasses``, ``addSafeClasses()``. Use the same methods on the ``Twig\\Runtime\\EscaperRuntime`` class instead. + * Fix capturing output from extensions that still use echo + * Fix a PHP warning in the Lexer on malformed templates + * Fix blocks not available under some circumstances + * Synchronize source context in templates when setting a Node on a Node # 3.9.3 (2024-04-18) From 34422f90043aba75e419dd77ee0a86e1688c28ee Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sat, 11 May 2024 09:44:16 +0200 Subject: [PATCH 250/812] Prepare the 3.10.0 release --- CHANGELOG | 2 +- src/Environment.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index cdf3c80e952..ce4f11e729f 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,4 +1,4 @@ -# 3.10.0 (2024-XX-XX) +# 3.10.0 (2024-05-11) * Make `CoreExtension::formatDate`, `CoreExtension::convertDate`, and `CoreExtension::formatNumber` part of the public API diff --git a/src/Environment.php b/src/Environment.php index ea1d10ec557..7a2bd01d969 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -43,12 +43,12 @@ */ class Environment { - public const VERSION = '3.10.0-DEV'; + public const VERSION = '3.10.0'; public const VERSION_ID = 301000; public const MAJOR_VERSION = 3; public const MINOR_VERSION = 10; public const RELEASE_VERSION = 0; - public const EXTRA_VERSION = 'DEV'; + public const EXTRA_VERSION = ''; private $charset; private $loader; From bfc3b7b19be1d3411d5b9ada4091ec7eecd3dd8a Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sat, 11 May 2024 09:45:17 +0200 Subject: [PATCH 251/812] Bump version --- CHANGELOG | 4 ++++ src/Environment.php | 8 ++++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index ce4f11e729f..77360fede51 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +# 3.10.1 (2024-XX-XX) + + * n/a + # 3.10.0 (2024-05-11) * Make `CoreExtension::formatDate`, `CoreExtension::convertDate`, and diff --git a/src/Environment.php b/src/Environment.php index 7a2bd01d969..10d2cdd9bc9 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -43,12 +43,12 @@ */ class Environment { - public const VERSION = '3.10.0'; - public const VERSION_ID = 301000; + public const VERSION = '3.10.1-DEV'; + public const VERSION_ID = 301001; public const MAJOR_VERSION = 3; public const MINOR_VERSION = 10; - public const RELEASE_VERSION = 0; - public const EXTRA_VERSION = ''; + public const RELEASE_VERSION = 1; + public const EXTRA_VERSION = 'DEV'; private $charset; private $loader; From f6ce88df2f2045564a2da9fbcb0454a33760a9a8 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sat, 11 May 2024 18:56:54 +0200 Subject: [PATCH 252/812] Fix constant return type --- CHANGELOG | 2 +- src/Extension/CoreExtension.php | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 77360fede51..a3958294991 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,6 @@ # 3.10.1 (2024-XX-XX) - * n/a + * Fix constant return type # 3.10.0 (2024-05-11) diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index bd9552f10ba..d7915ef9bcd 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -1365,9 +1365,12 @@ public static function source(Environment $env, $name, $ignoreMissing = false): * @param string $constant The name of the constant * @param object|null $object The object to get the constant from * + * @return mixed Class constants can return many types like scalars, arrays, and + * objects depending on the PHP version (\BackedEnum, \UnitEnum, etc.) + * * @internal */ - public static function constant($constant, $object = null): string + public static function constant($constant, $object = null) { if (null !== $object) { if ('class' === $constant) { From 1953f77844f76e3a880eddc9af68ba548737b814 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sat, 11 May 2024 16:33:56 +0200 Subject: [PATCH 253/812] Fix BC break on escaper extension --- CHANGELOG | 1 + src/Environment.php | 4 +++- src/Extension/EscaperExtension.php | 14 +++++++------- tests/Extension/EscaperTest.php | 19 +++++++++++++------ 4 files changed, 24 insertions(+), 14 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index a3958294991..e8564b9040b 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,6 @@ # 3.10.1 (2024-XX-XX) + * Fix BC break on escaper extension * Fix constant return type # 3.10.0 (2024-05-11) diff --git a/src/Environment.php b/src/Environment.php index 10d2cdd9bc9..cfdfb44f022 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -135,7 +135,9 @@ public function __construct(LoaderInterface $loader, $options = []) ]); $this->addExtension(new CoreExtension()); - $this->addExtension(new EscaperExtension($options['autoescape'])); + $escaperExt = new EscaperExtension($options['autoescape']); + $escaperExt->setEnvironment($this, false); + $this->addExtension($escaperExt); if (\PHP_VERSION_ID >= 80000) { $this->addExtension(new YieldNotReadyExtension($this->useYield)); } diff --git a/src/Extension/EscaperExtension.php b/src/Extension/EscaperExtension.php index a0654cd8c18..cf821c6d5d4 100644 --- a/src/Extension/EscaperExtension.php +++ b/src/Extension/EscaperExtension.php @@ -59,11 +59,14 @@ public function getFilters(): array /** * @deprecated since Twig 3.10 */ - public function setEnvironment(Environment $environment): void + public function setEnvironment(Environment $environment, bool $triggerDeprecation = true): void { - trigger_deprecation('twig/twig', '3.10', 'The "%s()" method is deprecated and not needed if you are using methods from "Twig\Runtime\EscaperRuntime".', __METHOD__); + if ($triggerDeprecation) { + trigger_deprecation('twig/twig', '3.10', 'The "%s()" method is deprecated and not needed if you are using methods from "Twig\Runtime\EscaperRuntime".', __METHOD__); + } $this->environment = $environment; + $this->escaper = $environment->getRuntime(EscaperRuntime::class); } /** @@ -126,9 +129,6 @@ public function setEscaper($strategy, callable $callable) if (!isset($this->environment)) { throw new \LogicException(sprintf('You must call "setEnvironment()" before calling "%s()".', __METHOD__)); } - if (!isset($this->escaper)) { - throw new \LogicException(sprintf('You must call "setEscaperRuntime()" before calling "%s()".', __METHOD__)); - } $this->escapers[$strategy] = $callable; $callable = function ($string, $charset) use ($callable) { @@ -160,7 +160,7 @@ public function setSafeClasses(array $safeClasses = []) trigger_deprecation('twig/twig', '3.10', 'The "%s()" method is deprecated, use the "Twig\Runtime\EscaperRuntime::setSafeClasses()" method instead.', __METHOD__); if (!isset($this->escaper)) { - throw new \LogicException(sprintf('You must call "setEscaperRuntime()" before calling %s().', __METHOD__)); + throw new \LogicException(sprintf('You must call "setEnvironment()" before calling "%s()".', __METHOD__)); } $this->escaper->setSafeClasses($safeClasses); @@ -174,7 +174,7 @@ public function addSafeClass(string $class, array $strategies) trigger_deprecation('twig/twig', '3.10', 'The "%s()" method is deprecated, use the "Twig\Runtime\EscaperRuntime::addSafeClass()" method instead.', __METHOD__); if (!isset($this->escaper)) { - throw new \LogicException(sprintf('You must call setEscaperRuntime() before calling %s().', __METHOD__)); + throw new \LogicException(sprintf('You must call "setEnvironment()" before calling "%s()".', __METHOD__)); } $this->escaper->addSafeClass($class, $strategies); diff --git a/tests/Extension/EscaperTest.php b/tests/Extension/EscaperTest.php index b23930b5a8e..950bd1b47b6 100644 --- a/tests/Extension/EscaperTest.php +++ b/tests/Extension/EscaperTest.php @@ -28,8 +28,6 @@ public function testCustomEscaper($expected, $string, $strategy) { $twig = new Environment($this->createMock(LoaderInterface::class)); $escaperExt = $twig->getExtension(EscaperExtension::class); - $escaperExt->setEnvironment($twig); - $escaperExt->setEscaperRuntime($twig->getRuntime(EscaperRuntime::class)); $escaperExt->setEscaper('foo', 'Twig\Tests\legacy_escaper'); $this->assertSame($expected, $twig->getRuntime(EscaperRuntime::class)->escape($string, $strategy)); } @@ -43,6 +41,19 @@ public function provideCustomEscaperCases() ]; } + /** + * @dataProvider provideCustomEscaperCases + * + * @group legacy + */ + public function testCustomEscaperWithoutCallingSetEscaperRuntime($expected, $string, $strategy) + { + $twig = new Environment($this->createMock(LoaderInterface::class)); + $escaperExt = $twig->getExtension(EscaperExtension::class); + $escaperExt->setEscaper('foo', 'Twig\Tests\legacy_escaper'); + $this->assertSame($expected, $twig->getRuntime(EscaperRuntime::class)->escape($string, $strategy)); + } + /** * @group legacy */ @@ -50,14 +61,10 @@ public function testCustomEscapersOnMultipleEnvs() { $env1 = new Environment($this->createMock(LoaderInterface::class)); $escaperExt1 = $env1->getExtension(EscaperExtension::class); - $escaperExt1->setEnvironment($env1); - $escaperExt1->setEscaperRuntime($env1->getRuntime(EscaperRuntime::class)); $escaperExt1->setEscaper('foo', 'Twig\Tests\legacy_escaper'); $env2 = new Environment($this->createMock(LoaderInterface::class)); $escaperExt2 = $env2->getExtension(EscaperExtension::class); - $escaperExt2->setEscaperRuntime($env2->getRuntime(EscaperRuntime::class)); - $escaperExt2->setEnvironment($env2); $escaperExt2->setEscaper('foo', 'Twig\Tests\legacy_escaper_again'); $this->assertSame('fooUTF-8', $env1->getRuntime(EscaperRuntime::class)->escape('foo', 'foo')); From 3af5ab2e52279e5e23dc192b1a26db3b8cffa4e7 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sun, 12 May 2024 08:16:18 +0200 Subject: [PATCH 254/812] Prepare the 3.10.1 release --- CHANGELOG | 2 +- src/Environment.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index e8564b9040b..9fbdb798718 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,4 +1,4 @@ -# 3.10.1 (2024-XX-XX) +# 3.10.1 (2024-05-12) * Fix BC break on escaper extension * Fix constant return type diff --git a/src/Environment.php b/src/Environment.php index cfdfb44f022..b7cb5399b98 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -43,12 +43,12 @@ */ class Environment { - public const VERSION = '3.10.1-DEV'; + public const VERSION = '3.10.1'; public const VERSION_ID = 301001; public const MAJOR_VERSION = 3; public const MINOR_VERSION = 10; public const RELEASE_VERSION = 1; - public const EXTRA_VERSION = 'DEV'; + public const EXTRA_VERSION = ''; private $charset; private $loader; From 0bdc73a5997fe0ab16ec44dc46351bc66edf7496 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sun, 12 May 2024 08:17:25 +0200 Subject: [PATCH 255/812] Bump version --- CHANGELOG | 4 ++++ src/Environment.php | 8 ++++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 9fbdb798718..eed466a2655 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +# 3.10.2 (2024-XX-XX) + + * n/a + # 3.10.1 (2024-05-12) * Fix BC break on escaper extension diff --git a/src/Environment.php b/src/Environment.php index b7cb5399b98..71d83c4edfd 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -43,12 +43,12 @@ */ class Environment { - public const VERSION = '3.10.1'; - public const VERSION_ID = 301001; + public const VERSION = '3.10.2-DEV'; + public const VERSION_ID = 301002; public const MAJOR_VERSION = 3; public const MINOR_VERSION = 10; - public const RELEASE_VERSION = 1; - public const EXTRA_VERSION = ''; + public const RELEASE_VERSION = 2; + public const EXTRA_VERSION = 'DEV'; private $charset; private $loader; From 04b1ad3cedfb81f48b6a7ec8d77a2116c604c7d6 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Mon, 13 May 2024 18:01:26 +0200 Subject: [PATCH 256/812] Fix old escaper signature + add docs --- CHANGELOG | 2 +- doc/filters/escape.rst | 29 +++++++++++++++++++++++----- doc/internals.rst | 7 +++++-- src/Extension/EscaperExtension.php | 8 ++++---- tests/Extension/EscaperTest.php | 22 ++++++++++----------- tests/Runtime/EscaperRuntimeTest.php | 12 ++++++------ 6 files changed, 51 insertions(+), 29 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index eed466a2655..80234c86d33 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,6 @@ # 3.10.2 (2024-XX-XX) - * n/a + * Fix support for the deprecated escaper signature # 3.10.1 (2024-05-12) diff --git a/doc/filters/escape.rst b/doc/filters/escape.rst index e8b735db46b..70d18eb5722 100644 --- a/doc/filters/escape.rst +++ b/doc/filters/escape.rst @@ -93,15 +93,34 @@ to learn more about this topic. Custom Escapers --------------- +.. versionadded:: 3.10 + + The ``EscaperRuntime`` class has been added in 3.10. On previous versions, + you can define custom escapers by calling the ``setEscaper()`` method on + the escaper extension instance. The first argument is the escaper strategy + (to be used in the ``escape`` call) and the second one must be a valid PHP + callable:: + + use Twig\Extension\EscaperExtension; + + $twig = new \Twig\Environment($loader); + $twig->getExtension(EscaperExtension::class)->setEscaper('csv', 'csv_escaper'); + + When called by Twig, the callable receives the Twig environment instance, + the string to escape, and the charset. + You can define custom escapers by calling the ``setEscaper()`` method on the -escaper extension instance. The first argument is the escaper name (to be -used in the ``escape`` call) and the second one must be a valid PHP callable:: +escaper runtime instance. It accepts two arguments: the strategy name and a PHP +callable that accepts a string to escape and the charset:: + + use Twig\Runtime\EscaperRuntime; $twig = new \Twig\Environment($loader); - $twig->getExtension(\Twig\Extension\EscaperExtension::class)->setEscaper('csv', 'csv_escaper'); + $escaper = fn ($string, $charset) => $string; + $twig->getRuntime(EscaperRuntime::class)->setEscaper('identity', $escaper); -When called by Twig, the callable receives the Twig environment instance, the -string to escape, and the charset. + # Usage in a template: + # {{ 'foo'|escape('identity') }} .. note:: diff --git a/doc/internals.rst b/doc/internals.rst index ccbb202f372..2aeb12f3fc4 100644 --- a/doc/internals.rst +++ b/doc/internals.rst @@ -124,9 +124,12 @@ using):: { protected function doDisplay(array $context, array $blocks = []) { + $macros = $this->macros; // line 1 - echo "Hello "; - echo twig_escape_filter($this->env, (isset($context["name"]) ? $context["name"] : null), "html", null, true); + yield "Hello "; + // line 2 + yield $this->env->getRuntime('Twig\Runtime\EscaperRuntime')->escape((isset($context["name"]) || array_key_exists("name", $context) ? $context["name"] : (function () { throw new RuntimeError('Variable "name" does not exist.', 2, $this->source); })()), "html", null, true); + return; yield ''; } // some more code diff --git a/src/Extension/EscaperExtension.php b/src/Extension/EscaperExtension.php index cf821c6d5d4..3360ef6710f 100644 --- a/src/Extension/EscaperExtension.php +++ b/src/Extension/EscaperExtension.php @@ -117,8 +117,8 @@ public function getDefaultStrategy(string $name) /** * Defines a new escaper to be used via the escape filter. * - * @param string $strategy The strategy name that should be used as a strategy in the escape call - * @param callable(Environment, string) $callable A valid PHP callable + * @param string $strategy The strategy name that should be used as a strategy in the escape call + * @param callable(Environment, string, string) $callable A valid PHP callable * * @deprecated since Twig 3.10 */ @@ -132,7 +132,7 @@ public function setEscaper($strategy, callable $callable) $this->escapers[$strategy] = $callable; $callable = function ($string, $charset) use ($callable) { - return $callable($this->environment, $string); + return $callable($this->environment, $string, $charset); }; $this->escaper->setEscaper($strategy, $callable); @@ -141,7 +141,7 @@ public function setEscaper($strategy, callable $callable) /** * Gets all defined escapers. * - * @return array An array of escapers + * @return array An array of escapers * * @deprecated since Twig 3.10 */ diff --git a/tests/Extension/EscaperTest.php b/tests/Extension/EscaperTest.php index 950bd1b47b6..e211677bfa8 100644 --- a/tests/Extension/EscaperTest.php +++ b/tests/Extension/EscaperTest.php @@ -29,15 +29,15 @@ public function testCustomEscaper($expected, $string, $strategy) $twig = new Environment($this->createMock(LoaderInterface::class)); $escaperExt = $twig->getExtension(EscaperExtension::class); $escaperExt->setEscaper('foo', 'Twig\Tests\legacy_escaper'); - $this->assertSame($expected, $twig->getRuntime(EscaperRuntime::class)->escape($string, $strategy)); + $this->assertSame($expected, $twig->getRuntime(EscaperRuntime::class)->escape($string, $strategy, 'ISO-8859-1')); } public function provideCustomEscaperCases() { return [ - ['fooUTF-8', 'foo', 'foo'], - ['UTF-8', null, 'foo'], - ['42UTF-8', 42, 'foo'], + ['foo**ISO-8859-1**UTF-8', 'foo', 'foo'], + ['**ISO-8859-1**UTF-8', null, 'foo'], + ['42**ISO-8859-1**UTF-8', 42, 'foo'], ]; } @@ -51,7 +51,7 @@ public function testCustomEscaperWithoutCallingSetEscaperRuntime($expected, $str $twig = new Environment($this->createMock(LoaderInterface::class)); $escaperExt = $twig->getExtension(EscaperExtension::class); $escaperExt->setEscaper('foo', 'Twig\Tests\legacy_escaper'); - $this->assertSame($expected, $twig->getRuntime(EscaperRuntime::class)->escape($string, $strategy)); + $this->assertSame($expected, $twig->getRuntime(EscaperRuntime::class)->escape($string, $strategy, 'ISO-8859-1')); } /** @@ -67,17 +67,17 @@ public function testCustomEscapersOnMultipleEnvs() $escaperExt2 = $env2->getExtension(EscaperExtension::class); $escaperExt2->setEscaper('foo', 'Twig\Tests\legacy_escaper_again'); - $this->assertSame('fooUTF-8', $env1->getRuntime(EscaperRuntime::class)->escape('foo', 'foo')); - $this->assertSame('fooUTF-81', $env2->getRuntime(EscaperRuntime::class)->escape('foo', 'foo')); + $this->assertSame('foo**ISO-8859-1**UTF-8', $env1->getRuntime(EscaperRuntime::class)->escape('foo', 'foo', 'ISO-8859-1')); + $this->assertSame('foo**ISO-8859-1**UTF-8**again', $env2->getRuntime(EscaperRuntime::class)->escape('foo', 'foo', 'ISO-8859-1')); } } -function legacy_escaper(Environment $twig, $string) +function legacy_escaper(Environment $twig, $string, $charset) { - return $string.$twig->getCharset(); + return $string.'**'.$charset.'**'.$twig->getCharset(); } -function legacy_escaper_again(Environment $twig, $string) +function legacy_escaper_again(Environment $twig, $string, $charset) { - return $string.$twig->getCharset().'1'; + return $string.'**'.$charset.'**'.$twig->getCharset().'**again'; } diff --git a/tests/Runtime/EscaperRuntimeTest.php b/tests/Runtime/EscaperRuntimeTest.php index 042473408d3..11764f4384f 100644 --- a/tests/Runtime/EscaperRuntimeTest.php +++ b/tests/Runtime/EscaperRuntimeTest.php @@ -350,19 +350,19 @@ public function testUnknownCustomEscaper() /** * @dataProvider provideCustomEscaperCases */ - public function testCustomEscaper($expected, $string, $strategy) + public function testCustomEscaper($expected, $string, $strategy, $charset) { $escaper = new EscaperRuntime(); $escaper->setEscaper('foo', 'Twig\Tests\escaper'); - $this->assertSame($expected, $escaper->escape($string, $strategy)); + $this->assertSame($expected, $escaper->escape($string, $strategy, $charset)); } public function provideCustomEscaperCases() { return [ - ['fooUTF-8', 'foo', 'foo'], - ['UTF-8', null, 'foo'], - ['42UTF-8', 42, 'foo'], + ['foo**ISO-8859-1', 'foo', 'foo', 'ISO-8859-1'], + ['**ISO-8859-1', null, 'foo', 'ISO-8859-1'], + ['42**UTF-8', 42, 'foo', null], ]; } @@ -391,7 +391,7 @@ public function provideObjectsForEscaping() function escaper($string, $charset) { - return $string.$charset; + return $string.'**'.$charset; } interface Extension_SafeHtmlInterface From 7aaed0b8311a557cc8c4047a71fd03153a00e755 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Tue, 14 May 2024 07:04:16 +0100 Subject: [PATCH 257/812] Prepare the 3.10.2 release --- CHANGELOG | 2 +- src/Environment.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 80234c86d33..1d68f7c80e0 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,4 +1,4 @@ -# 3.10.2 (2024-XX-XX) +# 3.10.2 (2024-05-14) * Fix support for the deprecated escaper signature diff --git a/src/Environment.php b/src/Environment.php index 71d83c4edfd..d3f7763e2a0 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -43,12 +43,12 @@ */ class Environment { - public const VERSION = '3.10.2-DEV'; + public const VERSION = '3.10.2'; public const VERSION_ID = 301002; public const MAJOR_VERSION = 3; public const MINOR_VERSION = 10; public const RELEASE_VERSION = 2; - public const EXTRA_VERSION = 'DEV'; + public const EXTRA_VERSION = ''; private $charset; private $loader; From 7069d82f237dd4d412c79dea997cc3b7b79b5f51 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Tue, 14 May 2024 07:05:11 +0100 Subject: [PATCH 258/812] Bump version --- CHANGELOG | 4 ++++ src/Environment.php | 8 ++++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 1d68f7c80e0..b8d1db7c172 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +# 3.10.3 (2024-XX-XX) + + * n/a + # 3.10.2 (2024-05-14) * Fix support for the deprecated escaper signature diff --git a/src/Environment.php b/src/Environment.php index d3f7763e2a0..6ba594b4f8b 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -43,12 +43,12 @@ */ class Environment { - public const VERSION = '3.10.2'; - public const VERSION_ID = 301002; + public const VERSION = '3.10.3-DEV'; + public const VERSION_ID = 301003; public const MAJOR_VERSION = 3; public const MINOR_VERSION = 10; - public const RELEASE_VERSION = 2; - public const EXTRA_VERSION = ''; + public const RELEASE_VERSION = 3; + public const EXTRA_VERSION = 'DEV'; private $charset; private $loader; From 504252835c0a819bedda7e205bf87faa1c1f3b0b Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Thu, 16 May 2024 11:46:35 +0200 Subject: [PATCH 259/812] Fix parse error in generated code when using CaptureNode --- src/Node/CaptureNode.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Node/CaptureNode.php b/src/Node/CaptureNode.php index 2b2bc09700f..b1cb357f569 100644 --- a/src/Node/CaptureNode.php +++ b/src/Node/CaptureNode.php @@ -50,6 +50,8 @@ public function compile(Compiler $compiler): void } if (!$this->getAttribute('raw')) { $compiler->raw(") ? '' : new Markup(\$tmp, \$this->env->getCharset());"); + } else { + $compiler->raw(';'); } } } From 67f29781ffafa520b0bbfbd8384674b42db04572 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Thu, 16 May 2024 11:04:27 +0100 Subject: [PATCH 260/812] Prepare the 3.10.3 release --- CHANGELOG | 4 ++-- src/Environment.php | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index b8d1db7c172..aecaab23953 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,6 @@ -# 3.10.3 (2024-XX-XX) +# 3.10.3 (2024-05-16) - * n/a + * Fix missing ; in generated code # 3.10.2 (2024-05-14) diff --git a/src/Environment.php b/src/Environment.php index 6ba594b4f8b..b035249b0fe 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -43,12 +43,12 @@ */ class Environment { - public const VERSION = '3.10.3-DEV'; + public const VERSION = '3.10.3'; public const VERSION_ID = 301003; public const MAJOR_VERSION = 3; public const MINOR_VERSION = 10; public const RELEASE_VERSION = 3; - public const EXTRA_VERSION = 'DEV'; + public const EXTRA_VERSION = ''; private $charset; private $loader; From ab071efe3b2d5caf2c662c9d018d9eacc76058d9 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Fri, 31 May 2024 17:05:59 +0200 Subject: [PATCH 261/812] Bump version --- CHANGELOG | 4 ++++ src/Environment.php | 10 +++++----- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index aecaab23953..241b826d20f 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +# 3.10.4 (2024-XX-XX) + + * n/a + # 3.10.3 (2024-05-16) * Fix missing ; in generated code diff --git a/src/Environment.php b/src/Environment.php index b035249b0fe..ad5aabffd95 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -43,12 +43,12 @@ */ class Environment { - public const VERSION = '3.10.3'; - public const VERSION_ID = 301003; - public const MAJOR_VERSION = 3; + public const VERSION = '3.10.4-DEV'; + public const VERSION_ID = 301004; + public const MAJOR_VERSION = 4; public const MINOR_VERSION = 10; - public const RELEASE_VERSION = 3; - public const EXTRA_VERSION = ''; + public const RELEASE_VERSION = 4; + public const EXTRA_VERSION = 'DEV'; private $charset; private $loader; From 71d6c82de6c09190f952f669d8242ea7532e4115 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Mon, 17 Jun 2024 11:22:10 +0200 Subject: [PATCH 262/812] Deprecate CallExpression::compileArguments --- CHANGELOG | 4 ++-- doc/deprecated.rst | 7 +++++++ src/Node/Expression/CallExpression.php | 4 ++++ 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 241b826d20f..73bf3ba61a2 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,6 @@ -# 3.10.4 (2024-XX-XX) +# 3.11.0 (2024-XX-XX) - * n/a + * Deprecate the second argument of `Twig\Node\Expression\CallExpression::compileArguments()` # 3.10.3 (2024-05-16) diff --git a/doc/deprecated.rst b/doc/deprecated.rst index 27ec642b200..4b4493af574 100644 --- a/doc/deprecated.rst +++ b/doc/deprecated.rst @@ -32,6 +32,13 @@ Extensions After: $twig->getRuntime(EscaperRuntime::class)->METHOD(); +Nodes +----- + +* The second argument of the + ``Twig\Node\Expression\CallExpression::compileArguments()`` method is + deprecated. + Node Visitors ------------- diff --git a/src/Node/Expression/CallExpression.php b/src/Node/Expression/CallExpression.php index d6ac5fc460c..3ac10ee6544 100644 --- a/src/Node/Expression/CallExpression.php +++ b/src/Node/Expression/CallExpression.php @@ -57,6 +57,10 @@ protected function compileCallable(Compiler $compiler) protected function compileArguments(Compiler $compiler, $isArray = false): void { + if (func_num_args() >= 2) { + trigger_deprecation('twig/twig', '3.11', 'Passing a second argument to "%s()" is deprecated.', __METHOD__); + } + $compiler->raw($isArray ? '[' : '('); $first = true; From b63bde30632902414d8f7b8ccd23a7d6f63fbb4b Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Fri, 21 Jun 2024 08:16:55 +0200 Subject: [PATCH 263/812] Optimize sprintf() calls for PHP 8.4 --- .php-cs-fixer.dist.php | 5 +- doc/_build/build.php | 4 +- .../TokenParser/CacheTokenParser.php | 6 +-- extra/html-extra/HtmlExtension.php | 4 +- extra/intl-extra/IntlExtension.php | 14 +++--- .../MissingExtensionSuggestor.php | 6 +-- src/Cache/FilesystemCache.php | 6 +-- src/Compiler.php | 6 +-- src/Environment.php | 14 +++--- src/Error/Error.php | 6 +-- src/Error/SyntaxError.php | 2 +- src/ExpressionParser.php | 44 ++++++++-------- src/Extension/CoreExtension.php | 50 +++++++++---------- src/Extension/EscaperExtension.php | 6 +-- src/Extension/StagingExtension.php | 8 +-- src/ExtensionSet.php | 18 +++---- src/Lexer.php | 14 +++--- src/Loader/ArrayLoader.php | 6 +-- src/Loader/ChainLoader.php | 6 +-- src/Loader/FilesystemLoader.php | 12 ++--- src/Node/BlockNode.php | 2 +- src/Node/BlockReferenceNode.php | 2 +- src/Node/DeprecatedNode.php | 6 +-- src/Node/Expression/Binary/EndsWithBinary.php | 6 +-- .../Expression/Binary/StartsWithBinary.php | 6 +-- .../Expression/BlockReferenceExpression.php | 2 +- src/Node/Expression/CallExpression.php | 32 ++++++------ src/Node/IncludeNode.php | 8 +-- src/Node/MacroNode.php | 4 +- src/Node/ModuleNode.php | 22 ++++---- src/Node/Node.php | 10 ++-- src/Node/WithNode.php | 12 ++--- src/NodeVisitor/OptimizerNodeVisitor.php | 4 +- src/NodeVisitor/YieldNotReadyNodeVisitor.php | 2 +- src/Parser.php | 8 +-- src/Profiler/Dumper/BaseDumper.php | 2 +- src/Profiler/Dumper/BlackfireDumper.php | 6 +-- src/Profiler/Dumper/HtmlDumper.php | 6 +-- src/Profiler/Dumper/TextDumper.php | 6 +-- src/Profiler/Node/EnterProfileNode.php | 4 +- src/Profiler/Node/LeaveProfileNode.php | 2 +- .../NodeVisitor/ProfilerNodeVisitor.php | 2 +- src/Runtime/EscaperRuntime.php | 12 ++--- src/Sandbox/SecurityPolicy.php | 10 ++-- src/Template.php | 10 ++-- src/Test/IntegrationTestCase.php | 12 ++--- src/Test/NodeTestCase.php | 2 +- src/Token.php | 6 +-- src/TokenParser/BlockTokenParser.php | 4 +- src/TokenParser/IfTokenParser.php | 2 +- src/TokenParser/MacroTokenParser.php | 2 +- src/TokenStream.php | 6 +-- tests/ErrorTest.php | 4 +- tests/Extension/CoreTest.php | 2 +- tests/Node/Expression/FilterTest.php | 2 +- tests/Node/Expression/GetAttrTest.php | 4 +- tests/NodeVisitor/OptimizerTest.php | 2 +- tests/Profiler/ProfileTest.php | 2 +- tests/TemplateTest.php | 4 +- 59 files changed, 239 insertions(+), 238 deletions(-) diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php index b07ac7fcabd..5c3a731a16e 100644 --- a/.php-cs-fixer.dist.php +++ b/.php-cs-fixer.dist.php @@ -13,8 +13,9 @@ 'heredoc_to_nowdoc' => false, 'ordered_imports' => true, 'phpdoc_types_order' => ['null_adjustment' => 'always_last', 'sort_algorithm' => 'none'], - 'native_function_invocation' => ['include' => ['@compiler_optimized'], 'scope' => 'all'], - ]) + // TODO: Remove once the "compiler_optimized" set includes "sprintf" + 'native_function_invocation' => ['include' => ['@compiler_optimized', 'sprintf'], 'scope' => 'all'], + ]) ->setRiskyAllowed(true) ->setFinder((new PhpCsFixer\Finder())->in(__DIR__)) ; diff --git a/doc/_build/build.php b/doc/_build/build.php index b93ef3bc8b6..2b183b1867b 100755 --- a/doc/_build/build.php +++ b/doc/_build/build.php @@ -40,9 +40,9 @@ file_put_contents($htmlFilePath, str_replace('href="assets/', 'href="/assets/', $htmlContents)); } - $io->success(sprintf('The Twig docs were successfully built at %s', realpath($outputDir))); + $io->success(\sprintf('The Twig docs were successfully built at %s', realpath($outputDir))); } else { - $io->error(sprintf("There were some errors while building the docs:\n\n%s\n", $result->getErrorTrace())); + $io->error(\sprintf("There were some errors while building the docs:\n\n%s\n", $result->getErrorTrace())); $io->newLine(); $io->comment('Tip: you can add the -v, -vv or -vvv flags to this command to get debug information.'); diff --git a/extra/cache-extra/TokenParser/CacheTokenParser.php b/extra/cache-extra/TokenParser/CacheTokenParser.php index 323096dd1ae..4cf8d482d05 100644 --- a/extra/cache-extra/TokenParser/CacheTokenParser.php +++ b/extra/cache-extra/TokenParser/CacheTokenParser.php @@ -38,18 +38,18 @@ public function parse(Token $token): Node switch ($k) { case 'ttl': if (1 !== \count($args)) { - throw new SyntaxError(sprintf('The "ttl" modifier takes exactly one argument (%d given).', \count($args)), $stream->getCurrent()->getLine(), $stream->getSourceContext()); + throw new SyntaxError(\sprintf('The "ttl" modifier takes exactly one argument (%d given).', \count($args)), $stream->getCurrent()->getLine(), $stream->getSourceContext()); } $ttl = $args->getNode('0'); break; case 'tags': if (1 !== \count($args)) { - throw new SyntaxError(sprintf('The "tags" modifier takes exactly one argument (%d given).', \count($args)), $stream->getCurrent()->getLine(), $stream->getSourceContext()); + throw new SyntaxError(\sprintf('The "tags" modifier takes exactly one argument (%d given).', \count($args)), $stream->getCurrent()->getLine(), $stream->getSourceContext()); } $tags = $args->getNode('0'); break; default: - throw new SyntaxError(sprintf('Unknown "%s" configuration.', $k), $stream->getCurrent()->getLine(), $stream->getSourceContext()); + throw new SyntaxError(\sprintf('Unknown "%s" configuration.', $k), $stream->getCurrent()->getLine(), $stream->getSourceContext()); } } diff --git a/extra/html-extra/HtmlExtension.php b/extra/html-extra/HtmlExtension.php index debb20bbc1a..83fcc58b195 100644 --- a/extra/html-extra/HtmlExtension.php +++ b/extra/html-extra/HtmlExtension.php @@ -96,7 +96,7 @@ public static function htmlClasses(...$args): string } elseif (\is_array($arg)) { foreach ($arg as $class => $condition) { if (!\is_string($class)) { - throw new RuntimeError(sprintf('The html_classes function argument %d (key %d) should be a string, got "%s".', $i, $class, \gettype($class))); + throw new RuntimeError(\sprintf('The html_classes function argument %d (key %d) should be a string, got "%s".', $i, $class, \gettype($class))); } if (!$condition) { continue; @@ -104,7 +104,7 @@ public static function htmlClasses(...$args): string $classes[] = $class; } } else { - throw new RuntimeError(sprintf('The html_classes function argument %d should be either a string or an array, got "%s".', $i, \gettype($arg))); + throw new RuntimeError(\sprintf('The html_classes function argument %d should be either a string or an array, got "%s".', $i, \gettype($arg))); } } diff --git a/extra/intl-extra/IntlExtension.php b/extra/intl-extra/IntlExtension.php index e87fd086efa..7278db21400 100644 --- a/extra/intl-extra/IntlExtension.php +++ b/extra/intl-extra/IntlExtension.php @@ -346,7 +346,7 @@ public function formatCurrency($amount, string $currency, array $attrs = [], ?st public function formatNumber($number, array $attrs = [], string $style = 'decimal', string $type = 'default', ?string $locale = null): string { if (!isset(self::NUMBER_TYPES[$type])) { - throw new RuntimeError(sprintf('The type "%s" does not exist, known types are: "%s".', $type, implode('", "', array_keys(self::NUMBER_TYPES)))); + throw new RuntimeError(\sprintf('The type "%s" does not exist, known types are: "%s".', $type, implode('", "', array_keys(self::NUMBER_TYPES)))); } $formatter = $this->createNumberFormatter($locale, $style, $attrs); @@ -409,11 +409,11 @@ private function createDateFormatter(?string $locale, ?string $dateFormat, ?stri $dateFormats = self::availableDateFormats(); if (null !== $dateFormat && !isset($dateFormats[$dateFormat])) { - throw new RuntimeError(sprintf('The date format "%s" does not exist, known formats are: "%s".', $dateFormat, implode('", "', array_keys($dateFormats)))); + throw new RuntimeError(\sprintf('The date format "%s" does not exist, known formats are: "%s".', $dateFormat, implode('", "', array_keys($dateFormats)))); } if (null !== $timeFormat && !isset(self::TIME_FORMATS[$timeFormat])) { - throw new RuntimeError(sprintf('The time format "%s" does not exist, known formats are: "%s".', $timeFormat, implode('", "', array_keys(self::TIME_FORMATS)))); + throw new RuntimeError(\sprintf('The time format "%s" does not exist, known formats are: "%s".', $timeFormat, implode('", "', array_keys(self::TIME_FORMATS)))); } if (null === $locale) { @@ -450,7 +450,7 @@ private function createDateFormatter(?string $locale, ?string $dateFormat, ?stri private function createNumberFormatter(?string $locale, string $style, array $attrs = []): \NumberFormatter { if (!isset(self::NUMBER_STYLES[$style])) { - throw new RuntimeError(sprintf('The style "%s" does not exist, known styles are: "%s".', $style, implode('", "', array_keys(self::NUMBER_STYLES)))); + throw new RuntimeError(\sprintf('The style "%s" does not exist, known styles are: "%s".', $style, implode('", "', array_keys(self::NUMBER_STYLES)))); } if (null === $locale) { @@ -492,18 +492,18 @@ private function createNumberFormatter(?string $locale, string $style, array $at foreach ($attrs as $name => $value) { if (!isset(self::NUMBER_ATTRIBUTES[$name])) { - throw new RuntimeError(sprintf('The number formatter attribute "%s" does not exist, known attributes are: "%s".', $name, implode('", "', array_keys(self::NUMBER_ATTRIBUTES)))); + throw new RuntimeError(\sprintf('The number formatter attribute "%s" does not exist, known attributes are: "%s".', $name, implode('", "', array_keys(self::NUMBER_ATTRIBUTES)))); } if ('rounding_mode' === $name) { if (!isset(self::NUMBER_ROUNDING_ATTRIBUTES[$value])) { - throw new RuntimeError(sprintf('The number formatter rounding mode "%s" does not exist, known modes are: "%s".', $value, implode('", "', array_keys(self::NUMBER_ROUNDING_ATTRIBUTES)))); + throw new RuntimeError(\sprintf('The number formatter rounding mode "%s" does not exist, known modes are: "%s".', $value, implode('", "', array_keys(self::NUMBER_ROUNDING_ATTRIBUTES)))); } $value = self::NUMBER_ROUNDING_ATTRIBUTES[$value]; } elseif ('padding_position' === $name) { if (!isset(self::NUMBER_PADDING_ATTRIBUTES[$value])) { - throw new RuntimeError(sprintf('The number formatter padding position "%s" does not exist, known positions are: "%s".', $value, implode('", "', array_keys(self::NUMBER_PADDING_ATTRIBUTES)))); + throw new RuntimeError(\sprintf('The number formatter padding position "%s" does not exist, known positions are: "%s".', $value, implode('", "', array_keys(self::NUMBER_PADDING_ATTRIBUTES)))); } $value = self::NUMBER_PADDING_ATTRIBUTES[$value]; diff --git a/extra/twig-extra-bundle/MissingExtensionSuggestor.php b/extra/twig-extra-bundle/MissingExtensionSuggestor.php index 683d3d6c0f3..0f8e1d5fda0 100644 --- a/extra/twig-extra-bundle/MissingExtensionSuggestor.php +++ b/extra/twig-extra-bundle/MissingExtensionSuggestor.php @@ -18,7 +18,7 @@ final class MissingExtensionSuggestor public function suggestFilter(string $name): bool { if ($filter = Extensions::getFilter($name)) { - throw new SyntaxError(sprintf('The "%s" filter is part of the %s, which is not installed/enabled; try running "composer require %s".', $name, $filter[0], $filter[1])); + throw new SyntaxError(\sprintf('The "%s" filter is part of the %s, which is not installed/enabled; try running "composer require %s".', $name, $filter[0], $filter[1])); } return false; @@ -27,7 +27,7 @@ public function suggestFilter(string $name): bool public function suggestFunction(string $name): bool { if ($function = Extensions::getFunction($name)) { - throw new SyntaxError(sprintf('The "%s" function is part of the %s, which is not installed/enabled; try running "composer require %s".', $name, $function[0], $function[1])); + throw new SyntaxError(\sprintf('The "%s" function is part of the %s, which is not installed/enabled; try running "composer require %s".', $name, $function[0], $function[1])); } return false; @@ -36,7 +36,7 @@ public function suggestFunction(string $name): bool public function suggestTag(string $name): bool { if ($function = Extensions::getTag($name)) { - throw new SyntaxError(sprintf('The "%s" tag is part of the %s, which is not installed/enabled; try running "composer require %s".', $name, $function[0], $function[1])); + throw new SyntaxError(\sprintf('The "%s" tag is part of the %s, which is not installed/enabled; try running "composer require %s".', $name, $function[0], $function[1])); } return false; diff --git a/src/Cache/FilesystemCache.php b/src/Cache/FilesystemCache.php index 4024adbd70d..2e79fac0508 100644 --- a/src/Cache/FilesystemCache.php +++ b/src/Cache/FilesystemCache.php @@ -50,11 +50,11 @@ public function write(string $key, string $content): void if (false === @mkdir($dir, 0777, true)) { clearstatcache(true, $dir); if (!is_dir($dir)) { - throw new \RuntimeException(sprintf('Unable to create the cache directory (%s).', $dir)); + throw new \RuntimeException(\sprintf('Unable to create the cache directory (%s).', $dir)); } } } elseif (!is_writable($dir)) { - throw new \RuntimeException(sprintf('Unable to write in the cache directory (%s).', $dir)); + throw new \RuntimeException(\sprintf('Unable to write in the cache directory (%s).', $dir)); } $tmpFile = tempnam($dir, basename($key)); @@ -73,7 +73,7 @@ public function write(string $key, string $content): void return; } - throw new \RuntimeException(sprintf('Failed to write cache file "%s".', $key)); + throw new \RuntimeException(\sprintf('Failed to write cache file "%s".', $key)); } public function getTimestamp(string $key): int diff --git a/src/Compiler.php b/src/Compiler.php index a2379d13811..1e7ed04c61d 100644 --- a/src/Compiler.php +++ b/src/Compiler.php @@ -144,7 +144,7 @@ public function write(...$strings) */ public function string(string $value) { - $this->source .= sprintf('"%s"', addcslashes($value, "\0\t\"\$\\")); + $this->source .= \sprintf('"%s"', addcslashes($value, "\0\t\"\$\\")); return $this; } @@ -196,7 +196,7 @@ public function repr($value) public function addDebugInfo(Node $node) { if ($node->getTemplateLine() != $this->lastLine) { - $this->write(sprintf("// line %d\n", $node->getTemplateLine())); + $this->write(\sprintf("// line %d\n", $node->getTemplateLine())); $this->sourceLine += substr_count($this->source, "\n", $this->sourceOffset); $this->sourceOffset = \strlen($this->source); @@ -244,7 +244,7 @@ public function outdent(int $step = 1) public function getVarName(): string { - return sprintf('__internal_compile_%d', $this->varNameSalt++); + return \sprintf('__internal_compile_%d', $this->varNameSalt++); } private function checkForEcho(string $string): void diff --git a/src/Environment.php b/src/Environment.php index ad5aabffd95..ead76e3707f 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -393,7 +393,7 @@ public function loadTemplate(string $cls, string $name, ?int $index = null): Tem } if (!class_exists($cls, false)) { - throw new RuntimeError(sprintf('Failed to load Twig template "%s", index "%s": cache might be corrupted.', $name, $index), -1, $source); + throw new RuntimeError(\sprintf('Failed to load Twig template "%s", index "%s": cache might be corrupted.', $name, $index), -1, $source); } } } @@ -418,9 +418,9 @@ public function createTemplate(string $template, ?string $name = null): Template { $hash = hash(\PHP_VERSION_ID < 80100 ? 'sha256' : 'xxh128', $template, false); if (null !== $name) { - $name = sprintf('%s (string template %s)', $name, $hash); + $name = \sprintf('%s (string template %s)', $name, $hash); } else { - $name = sprintf('__string_template__%s', $hash); + $name = \sprintf('__string_template__%s', $hash); } $loader = new ChainLoader([ @@ -485,7 +485,7 @@ public function resolveTemplate($names): TemplateWrapper return $this->load($name); } - throw new LoaderError(sprintf('Unable to find one of the following templates: "%s".', implode('", "', $names))); + throw new LoaderError(\sprintf('Unable to find one of the following templates: "%s".', implode('", "', $names))); } public function setLexer(Lexer $lexer) @@ -554,7 +554,7 @@ public function compileSource(Source $source): string $e->setSourceContext($source); throw $e; } catch (\Exception $e) { - throw new SyntaxError(sprintf('An exception has been thrown during the compilation of a template ("%s").', $e->getMessage()), -1, $source, $e); + throw new SyntaxError(\sprintf('An exception has been thrown during the compilation of a template ("%s").', $e->getMessage()), -1, $source, $e); } } @@ -632,7 +632,7 @@ public function getRuntime(string $class) return $this->runtimes[$class] = $runtime; } - throw new RuntimeError(sprintf('Unable to load the "%s" runtime.', $class)); + throw new RuntimeError(\sprintf('Unable to load the "%s" runtime.', $class)); } public function addExtension(ExtensionInterface $extension) @@ -803,7 +803,7 @@ public function getFunctions(): array public function addGlobal(string $name, $value) { if ($this->extensionSet->isInitialized() && !\array_key_exists($name, $this->getGlobals())) { - throw new \LogicException(sprintf('Unable to add global "%s" as the runtime or the extensions have already been initialized.', $name)); + throw new \LogicException(\sprintf('Unable to add global "%s" as the runtime or the extensions have already been initialized.', $name)); } if (null !== $this->resolvedGlobals) { diff --git a/src/Error/Error.php b/src/Error/Error.php index 0df213598cf..4efd9cafba9 100644 --- a/src/Error/Error.php +++ b/src/Error/Error.php @@ -143,15 +143,15 @@ private function updateRepr(): void if ($this->name) { if (\is_string($this->name) || (\is_object($this->name) && method_exists($this->name, '__toString'))) { - $name = sprintf('"%s"', $this->name); + $name = \sprintf('"%s"', $this->name); } else { $name = json_encode($this->name); } - $this->message .= sprintf(' in %s', $name); + $this->message .= \sprintf(' in %s', $name); } if ($this->lineno && $this->lineno >= 0) { - $this->message .= sprintf(' at line %d', $this->lineno); + $this->message .= \sprintf(' at line %d', $this->lineno); } if ($dot) { diff --git a/src/Error/SyntaxError.php b/src/Error/SyntaxError.php index 77c437c6882..841b653f552 100644 --- a/src/Error/SyntaxError.php +++ b/src/Error/SyntaxError.php @@ -41,6 +41,6 @@ public function addSuggestions(string $name, array $items): void asort($alternatives); - $this->appendMessage(sprintf(' Did you mean "%s"?', implode('", "', array_keys($alternatives)))); + $this->appendMessage(\sprintf(' Did you mean "%s"?', implode('", "', array_keys($alternatives)))); } } diff --git a/src/ExpressionParser.php b/src/ExpressionParser.php index 6839bc93204..4be64934171 100644 --- a/src/ExpressionParser.php +++ b/src/ExpressionParser.php @@ -265,7 +265,7 @@ public function parsePrimaryExpression() if (isset($this->unaryOperators[$token->getValue()])) { $class = $this->unaryOperators[$token->getValue()]['class']; if (!\in_array($class, [NegUnary::class, PosUnary::class])) { - throw new SyntaxError(sprintf('Unexpected unary operator "%s".', $token->getValue()), $token->getLine(), $this->parser->getStream()->getSourceContext()); + throw new SyntaxError(\sprintf('Unexpected unary operator "%s".', $token->getValue()), $token->getLine(), $this->parser->getStream()->getSourceContext()); } $this->parser->getStream()->next(); @@ -282,9 +282,9 @@ public function parsePrimaryExpression() } elseif ($token->test(/* Token::PUNCTUATION_TYPE */ 9, '{')) { $node = $this->parseHashExpression(); } elseif ($token->test(/* Token::OPERATOR_TYPE */ 8, '=') && ('==' === $this->parser->getStream()->look(-1)->getValue() || '!=' === $this->parser->getStream()->look(-1)->getValue())) { - throw new SyntaxError(sprintf('Unexpected operator of value "%s". Did you try to use "===" or "!==" for strict comparison? Use "is same as(value)" instead.', $token->getValue()), $token->getLine(), $this->parser->getStream()->getSourceContext()); + throw new SyntaxError(\sprintf('Unexpected operator of value "%s". Did you try to use "===" or "!==" for strict comparison? Use "is same as(value)" instead.', $token->getValue()), $token->getLine(), $this->parser->getStream()->getSourceContext()); } else { - throw new SyntaxError(sprintf('Unexpected token "%s" of value "%s".', Token::typeToEnglish($token->getType()), $token->getValue()), $token->getLine(), $this->parser->getStream()->getSourceContext()); + throw new SyntaxError(\sprintf('Unexpected token "%s" of value "%s".', Token::typeToEnglish($token->getType()), $token->getValue()), $token->getLine(), $this->parser->getStream()->getSourceContext()); } } @@ -399,7 +399,7 @@ public function parseHashExpression() } else { $current = $stream->getCurrent(); - throw new SyntaxError(sprintf('A hash key must be a quoted string, a number, a name, or an expression enclosed in parentheses (unexpected token "%s" of value "%s".', Token::typeToEnglish($current->getType()), $current->getValue()), $current->getLine(), $stream->getSourceContext()); + throw new SyntaxError(\sprintf('A hash key must be a quoted string, a number, a name, or an expression enclosed in parentheses (unexpected token "%s" of value "%s".', Token::typeToEnglish($current->getType()), $current->getValue()), $current->getLine(), $stream->getSourceContext()); } $stream->expect(/* Token::PUNCTUATION_TYPE */ 9, ':', 'A hash key must be followed by a colon (:)'); @@ -505,7 +505,7 @@ public function parseSubscriptExpression($node) } } } else { - throw new SyntaxError(sprintf('Expected name or number, got value "%s" of type %s.', $token->getValue(), Token::typeToEnglish($token->getType())), $lineno, $stream->getSourceContext()); + throw new SyntaxError(\sprintf('Expected name or number, got value "%s" of type %s.', $token->getValue(), Token::typeToEnglish($token->getType())), $lineno, $stream->getSourceContext()); } if ($node instanceof NameExpression && null !== $this->parser->getImportedSymbol('template', $node->getAttribute('name'))) { @@ -623,7 +623,7 @@ public function parseArguments($namedArguments = false, $definition = false, $al $name = null; if ($namedArguments && $token = $stream->nextIf(/* Token::OPERATOR_TYPE */ 8, '=')) { if (!$value instanceof NameExpression) { - throw new SyntaxError(sprintf('A parameter name must be a string, "%s" given.', \get_class($value)), $token->getLine(), $stream->getSourceContext()); + throw new SyntaxError(\sprintf('A parameter name must be a string, "%s" given.', \get_class($value)), $token->getLine(), $stream->getSourceContext()); } $name = $value->getAttribute('name'); @@ -671,7 +671,7 @@ public function parseAssignmentExpression() } $value = $token->getValue(); if (\in_array(strtr($value, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), ['true', 'false', 'none', 'null'])) { - throw new SyntaxError(sprintf('You cannot assign a value to "%s".', $value), $token->getLine(), $stream->getSourceContext()); + throw new SyntaxError(\sprintf('You cannot assign a value to "%s".', $value), $token->getLine(), $stream->getSourceContext()); } $targets[] = new AssignNameExpression($value, $token->getLine()); @@ -742,7 +742,7 @@ private function getTest(int $line): array } } - $e = new SyntaxError(sprintf('Unknown "%s" test.', $name), $line, $stream->getSourceContext()); + $e = new SyntaxError(\sprintf('Unknown "%s" test.', $name), $line, $stream->getSourceContext()); $e->addSuggestions($name, array_keys($this->env->getTests())); throw $e; @@ -752,16 +752,16 @@ private function getTestNodeClass(TwigTest $test): string { if ($test->isDeprecated()) { $stream = $this->parser->getStream(); - $message = sprintf('Twig Test "%s" is deprecated', $test->getName()); + $message = \sprintf('Twig Test "%s" is deprecated', $test->getName()); if ($test->getDeprecatedVersion()) { - $message .= sprintf(' since version %s', $test->getDeprecatedVersion()); + $message .= \sprintf(' since version %s', $test->getDeprecatedVersion()); } if ($test->getAlternative()) { - $message .= sprintf('. Use "%s" instead', $test->getAlternative()); + $message .= \sprintf('. Use "%s" instead', $test->getAlternative()); } $src = $stream->getSourceContext(); - $message .= sprintf(' in %s at line %d.', $src->getPath() ?: $src->getName(), $stream->getCurrent()->getLine()); + $message .= \sprintf(' in %s at line %d.', $src->getPath() ?: $src->getName(), $stream->getCurrent()->getLine()); @trigger_error($message, \E_USER_DEPRECATED); } @@ -772,22 +772,22 @@ private function getTestNodeClass(TwigTest $test): string private function getFunctionNodeClass(string $name, int $line): string { if (!$function = $this->env->getFunction($name)) { - $e = new SyntaxError(sprintf('Unknown "%s" function.', $name), $line, $this->parser->getStream()->getSourceContext()); + $e = new SyntaxError(\sprintf('Unknown "%s" function.', $name), $line, $this->parser->getStream()->getSourceContext()); $e->addSuggestions($name, array_keys($this->env->getFunctions())); throw $e; } if ($function->isDeprecated()) { - $message = sprintf('Twig Function "%s" is deprecated', $function->getName()); + $message = \sprintf('Twig Function "%s" is deprecated', $function->getName()); if ($function->getDeprecatedVersion()) { - $message .= sprintf(' since version %s', $function->getDeprecatedVersion()); + $message .= \sprintf(' since version %s', $function->getDeprecatedVersion()); } if ($function->getAlternative()) { - $message .= sprintf('. Use "%s" instead', $function->getAlternative()); + $message .= \sprintf('. Use "%s" instead', $function->getAlternative()); } $src = $this->parser->getStream()->getSourceContext(); - $message .= sprintf(' in %s at line %d.', $src->getPath() ?: $src->getName(), $line); + $message .= \sprintf(' in %s at line %d.', $src->getPath() ?: $src->getName(), $line); @trigger_error($message, \E_USER_DEPRECATED); } @@ -798,22 +798,22 @@ private function getFunctionNodeClass(string $name, int $line): string private function getFilterNodeClass(string $name, int $line): string { if (!$filter = $this->env->getFilter($name)) { - $e = new SyntaxError(sprintf('Unknown "%s" filter.', $name), $line, $this->parser->getStream()->getSourceContext()); + $e = new SyntaxError(\sprintf('Unknown "%s" filter.', $name), $line, $this->parser->getStream()->getSourceContext()); $e->addSuggestions($name, array_keys($this->env->getFilters())); throw $e; } if ($filter->isDeprecated()) { - $message = sprintf('Twig Filter "%s" is deprecated', $filter->getName()); + $message = \sprintf('Twig Filter "%s" is deprecated', $filter->getName()); if ($filter->getDeprecatedVersion()) { - $message .= sprintf(' since version %s', $filter->getDeprecatedVersion()); + $message .= \sprintf(' since version %s', $filter->getDeprecatedVersion()); } if ($filter->getAlternative()) { - $message .= sprintf('. Use "%s" instead', $filter->getAlternative()); + $message .= \sprintf('. Use "%s" instead', $filter->getAlternative()); } $src = $this->parser->getStream()->getSourceContext(); - $message .= sprintf(' in %s at line %d.', $src->getPath() ?: $src->getName(), $line); + $message .= \sprintf(' in %s at line %d.', $src->getPath() ?: $src->getName(), $line); @trigger_error($message, \E_USER_DEPRECATED); } diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index d7915ef9bcd..405a35353f2 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -455,7 +455,7 @@ public function modifyDate($date, $modifier) */ public static function sprintf($format, ...$values): string { - return sprintf($format ?? '', ...$values); + return \sprintf($format ?? '', ...$values); } /** @@ -536,7 +536,7 @@ public function convertDate($date = null, $timezone = null) public static function replace($str, $from): string { if (!is_iterable($from)) { - throw new RuntimeError(sprintf('The "replace" filter expects an array or "Traversable" as replace values, got "%s".', \is_object($from) ? \get_class($from) : \gettype($from))); + throw new RuntimeError(\sprintf('The "replace" filter expects an array or "Traversable" as replace values, got "%s".', \is_object($from) ? \get_class($from) : \gettype($from))); } return strtr($str ?? '', self::toArray($from)); @@ -633,7 +633,7 @@ public static function merge(...$arrays): array foreach ($arrays as $argNumber => $array) { if (!is_iterable($array)) { - throw new RuntimeError(sprintf('The merge filter only works with arrays or "Traversable", got "%s" for argument %d.', \gettype($array), $argNumber + 1)); + throw new RuntimeError(\sprintf('The merge filter only works with arrays or "Traversable", got "%s" for argument %d.', \gettype($array), $argNumber + 1)); } $result = array_merge($result, self::toArray($array)); @@ -907,7 +907,7 @@ public static function sort(Environment $env, $array, $arrow = null): array if ($array instanceof \Traversable) { $array = iterator_to_array($array); } elseif (!\is_array($array)) { - throw new RuntimeError(sprintf('The sort filter only works with arrays or "Traversable", got "%s".', \gettype($array))); + throw new RuntimeError(\sprintf('The sort filter only works with arrays or "Traversable", got "%s".', \gettype($array))); } if (null !== $arrow) { @@ -1038,7 +1038,7 @@ public static function compare($a, $b) public static function matches(string $regexp, ?string $str): int { set_error_handler(function ($t, $m) use ($regexp) { - throw new RuntimeError(sprintf('Regexp "%s" passed to "matches" is not valid', $regexp).substr($m, 12)); + throw new RuntimeError(\sprintf('Regexp "%s" passed to "matches" is not valid', $regexp).substr($m, 12)); }); try { return preg_match($regexp, $str ?? ''); @@ -1222,7 +1222,7 @@ public static function callMacro(Template $template, string $method, array $args } } - throw new RuntimeError(sprintf('Macro "%s" is not defined in template "%s".', substr($method, \strlen('macro_')), $template->getTemplateName()), $lineno, $source); + throw new RuntimeError(\sprintf('Macro "%s" is not defined in template "%s".', substr($method, \strlen('macro_')), $template->getTemplateName()), $lineno, $source); } return $template->$method(...$args); @@ -1382,10 +1382,10 @@ public static function constant($constant, $object = null) if (!\defined($constant)) { if ('::class' === strtolower(substr($constant, -7))) { - throw new RuntimeError(sprintf('You cannot use the Twig function "constant()" to access "%s". You could provide an object and call constant("class", $object) or use the class name directly as a string.', $constant)); + throw new RuntimeError(\sprintf('You cannot use the Twig function "constant()" to access "%s". You could provide an object and call constant("class", $object) or use the class name directly as a string.', $constant)); } - throw new RuntimeError(sprintf('Constant "%s" is undefined.', $constant)); + throw new RuntimeError(\sprintf('Constant "%s" is undefined.', $constant)); } return \constant($constant); @@ -1424,7 +1424,7 @@ public static function constantIsDefined($constant, $object = null): bool public static function batch($items, $size, $fill = null, $preserveKeys = true): array { if (!is_iterable($items)) { - throw new RuntimeError(sprintf('The "batch" filter expects an array or "Traversable", got "%s".', \is_object($items) ? \get_class($items) : \gettype($items))); + throw new RuntimeError(\sprintf('The "batch" filter expects an array or "Traversable", got "%s".', \is_object($items) ? \get_class($items) : \gettype($items))); } $size = ceil($size); @@ -1486,25 +1486,25 @@ public static function getAttribute(Environment $env, Source $source, $object, $ } if ($object instanceof \ArrayAccess) { - $message = sprintf('Key "%s" in object with ArrayAccess of class "%s" does not exist.', $arrayItem, \get_class($object)); + $message = \sprintf('Key "%s" in object with ArrayAccess of class "%s" does not exist.', $arrayItem, \get_class($object)); } elseif (\is_object($object)) { - $message = sprintf('Impossible to access a key "%s" on an object of class "%s" that does not implement ArrayAccess interface.', $item, \get_class($object)); + $message = \sprintf('Impossible to access a key "%s" on an object of class "%s" that does not implement ArrayAccess interface.', $item, \get_class($object)); } elseif (\is_array($object)) { if (empty($object)) { - $message = sprintf('Key "%s" does not exist as the array is empty.', $arrayItem); + $message = \sprintf('Key "%s" does not exist as the array is empty.', $arrayItem); } else { - $message = sprintf('Key "%s" for array with keys "%s" does not exist.', $arrayItem, implode(', ', array_keys($object))); + $message = \sprintf('Key "%s" for array with keys "%s" does not exist.', $arrayItem, implode(', ', array_keys($object))); } } elseif (/* Template::ARRAY_CALL */ 'array' === $type) { if (null === $object) { - $message = sprintf('Impossible to access a key ("%s") on a null variable.', $item); + $message = \sprintf('Impossible to access a key ("%s") on a null variable.', $item); } else { - $message = sprintf('Impossible to access a key ("%s") on a %s variable ("%s").', $item, \gettype($object), $object); + $message = \sprintf('Impossible to access a key ("%s") on a %s variable ("%s").', $item, \gettype($object), $object); } } elseif (null === $object) { - $message = sprintf('Impossible to access an attribute ("%s") on a null variable.', $item); + $message = \sprintf('Impossible to access an attribute ("%s") on a null variable.', $item); } else { - $message = sprintf('Impossible to access an attribute ("%s") on a %s variable ("%s").', $item, \gettype($object), $object); + $message = \sprintf('Impossible to access an attribute ("%s") on a %s variable ("%s").', $item, \gettype($object), $object); } throw new RuntimeError($message, $lineno, $source); @@ -1521,11 +1521,11 @@ public static function getAttribute(Environment $env, Source $source, $object, $ } if (null === $object) { - $message = sprintf('Impossible to invoke a method ("%s") on a null variable.', $item); + $message = \sprintf('Impossible to invoke a method ("%s") on a null variable.', $item); } elseif (\is_array($object)) { - $message = sprintf('Impossible to invoke a method ("%s") on an array.', $item); + $message = \sprintf('Impossible to invoke a method ("%s") on an array.', $item); } else { - $message = sprintf('Impossible to invoke a method ("%s") on a %s variable ("%s").', $item, \gettype($object), $object); + $message = \sprintf('Impossible to invoke a method ("%s") on a %s variable ("%s").', $item, \gettype($object), $object); } throw new RuntimeError($message, $lineno, $source); @@ -1612,7 +1612,7 @@ public static function getAttribute(Environment $env, Source $source, $object, $ return; } - throw new RuntimeError(sprintf('Neither the property "%1$s" nor one of the methods "%1$s()", "get%1$s()"/"is%1$s()"/"has%1$s()" or "__call()" exist and have public access in class "%2$s".', $item, $class), $lineno, $source); + throw new RuntimeError(\sprintf('Neither the property "%1$s" nor one of the methods "%1$s()", "get%1$s()"/"is%1$s()"/"has%1$s()" or "__call()" exist and have public access in class "%2$s".', $item, $class), $lineno, $source); } if ($isDefinedTest) { @@ -1661,7 +1661,7 @@ public static function column($array, $name, $index = null): array if ($array instanceof \Traversable) { $array = iterator_to_array($array); } elseif (!\is_array($array)) { - throw new RuntimeError(sprintf('The column filter only works with arrays or "Traversable", got "%s" as first argument.', \gettype($array))); + throw new RuntimeError(\sprintf('The column filter only works with arrays or "Traversable", got "%s" as first argument.', \gettype($array))); } return array_column($array, $name, $index); @@ -1673,7 +1673,7 @@ public static function column($array, $name, $index = null): array public static function filter(Environment $env, $array, $arrow) { if (!is_iterable($array)) { - throw new RuntimeError(sprintf('The "filter" filter expects an array or "Traversable", got "%s".', \is_object($array) ? \get_class($array) : \gettype($array))); + throw new RuntimeError(\sprintf('The "filter" filter expects an array or "Traversable", got "%s".', \is_object($array) ? \get_class($array) : \gettype($array))); } self::checkArrowInSandbox($env, $arrow, 'filter', 'filter'); @@ -1709,7 +1709,7 @@ public static function reduce(Environment $env, $array, $arrow, $initial = null) self::checkArrowInSandbox($env, $arrow, 'reduce', 'filter'); if (!\is_array($array) && !$array instanceof \Traversable) { - throw new RuntimeError(sprintf('The "reduce" filter only works with arrays or "Traversable", got "%s" as first argument.', \gettype($array))); + throw new RuntimeError(\sprintf('The "reduce" filter only works with arrays or "Traversable", got "%s" as first argument.', \gettype($array))); } $accumulator = $initial; @@ -1758,7 +1758,7 @@ public static function arrayEvery(Environment $env, $array, $arrow) public static function checkArrowInSandbox(Environment $env, $arrow, $thing, $type) { if (!$arrow instanceof \Closure && $env->hasExtension(SandboxExtension::class) && $env->getExtension(SandboxExtension::class)->isSandboxed()) { - throw new RuntimeError(sprintf('The callable passed to the "%s" %s must be a Closure in sandbox mode.', $thing, $type)); + throw new RuntimeError(\sprintf('The callable passed to the "%s" %s must be a Closure in sandbox mode.', $thing, $type)); } } diff --git a/src/Extension/EscaperExtension.php b/src/Extension/EscaperExtension.php index 3360ef6710f..60ae2008e3e 100644 --- a/src/Extension/EscaperExtension.php +++ b/src/Extension/EscaperExtension.php @@ -127,7 +127,7 @@ public function setEscaper($strategy, callable $callable) trigger_deprecation('twig/twig', '3.10', 'The "%s()" method is deprecated, use the "Twig\Runtime\EscaperRuntime::setEscaper()" method instead (be warned that Environment is not passed anymore to the callable).', __METHOD__); if (!isset($this->environment)) { - throw new \LogicException(sprintf('You must call "setEnvironment()" before calling "%s()".', __METHOD__)); + throw new \LogicException(\sprintf('You must call "setEnvironment()" before calling "%s()".', __METHOD__)); } $this->escapers[$strategy] = $callable; @@ -160,7 +160,7 @@ public function setSafeClasses(array $safeClasses = []) trigger_deprecation('twig/twig', '3.10', 'The "%s()" method is deprecated, use the "Twig\Runtime\EscaperRuntime::setSafeClasses()" method instead.', __METHOD__); if (!isset($this->escaper)) { - throw new \LogicException(sprintf('You must call "setEnvironment()" before calling "%s()".', __METHOD__)); + throw new \LogicException(\sprintf('You must call "setEnvironment()" before calling "%s()".', __METHOD__)); } $this->escaper->setSafeClasses($safeClasses); @@ -174,7 +174,7 @@ public function addSafeClass(string $class, array $strategies) trigger_deprecation('twig/twig', '3.10', 'The "%s()" method is deprecated, use the "Twig\Runtime\EscaperRuntime::addSafeClass()" method instead.', __METHOD__); if (!isset($this->escaper)) { - throw new \LogicException(sprintf('You must call "setEnvironment()" before calling "%s()".', __METHOD__)); + throw new \LogicException(\sprintf('You must call "setEnvironment()" before calling "%s()".', __METHOD__)); } $this->escaper->addSafeClass($class, $strategies); diff --git a/src/Extension/StagingExtension.php b/src/Extension/StagingExtension.php index 0ea47f90c5c..59db2ca7d4a 100644 --- a/src/Extension/StagingExtension.php +++ b/src/Extension/StagingExtension.php @@ -35,7 +35,7 @@ final class StagingExtension extends AbstractExtension public function addFunction(TwigFunction $function): void { if (isset($this->functions[$function->getName()])) { - throw new \LogicException(sprintf('Function "%s" is already registered.', $function->getName())); + throw new \LogicException(\sprintf('Function "%s" is already registered.', $function->getName())); } $this->functions[$function->getName()] = $function; @@ -49,7 +49,7 @@ public function getFunctions(): array public function addFilter(TwigFilter $filter): void { if (isset($this->filters[$filter->getName()])) { - throw new \LogicException(sprintf('Filter "%s" is already registered.', $filter->getName())); + throw new \LogicException(\sprintf('Filter "%s" is already registered.', $filter->getName())); } $this->filters[$filter->getName()] = $filter; @@ -73,7 +73,7 @@ public function getNodeVisitors(): array public function addTokenParser(TokenParserInterface $parser): void { if (isset($this->tokenParsers[$parser->getTag()])) { - throw new \LogicException(sprintf('Tag "%s" is already registered.', $parser->getTag())); + throw new \LogicException(\sprintf('Tag "%s" is already registered.', $parser->getTag())); } $this->tokenParsers[$parser->getTag()] = $parser; @@ -87,7 +87,7 @@ public function getTokenParsers(): array public function addTest(TwigTest $test): void { if (isset($this->tests[$test->getName()])) { - throw new \LogicException(sprintf('Test "%s" is already registered.', $test->getName())); + throw new \LogicException(\sprintf('Test "%s" is already registered.', $test->getName())); } $this->tests[$test->getName()] = $test; diff --git a/src/ExtensionSet.php b/src/ExtensionSet.php index d32200ceb51..34b600063a2 100644 --- a/src/ExtensionSet.php +++ b/src/ExtensionSet.php @@ -70,7 +70,7 @@ public function getExtension(string $class): ExtensionInterface $class = ltrim($class, '\\'); if (!isset($this->extensions[$class])) { - throw new RuntimeError(sprintf('The "%s" extension is not enabled.', $class)); + throw new RuntimeError(\sprintf('The "%s" extension is not enabled.', $class)); } return $this->extensions[$class]; @@ -125,11 +125,11 @@ public function addExtension(ExtensionInterface $extension): void $class = \get_class($extension); if ($this->initialized) { - throw new \LogicException(sprintf('Unable to register extension "%s" as extensions have already been initialized.', $class)); + throw new \LogicException(\sprintf('Unable to register extension "%s" as extensions have already been initialized.', $class)); } if (isset($this->extensions[$class])) { - throw new \LogicException(sprintf('Unable to register extension "%s" as it is already registered.', $class)); + throw new \LogicException(\sprintf('Unable to register extension "%s" as it is already registered.', $class)); } $this->extensions[$class] = $extension; @@ -138,7 +138,7 @@ public function addExtension(ExtensionInterface $extension): void public function addFunction(TwigFunction $function): void { if ($this->initialized) { - throw new \LogicException(sprintf('Unable to add function "%s" as extensions have already been initialized.', $function->getName())); + throw new \LogicException(\sprintf('Unable to add function "%s" as extensions have already been initialized.', $function->getName())); } $this->staging->addFunction($function); @@ -194,7 +194,7 @@ public function registerUndefinedFunctionCallback(callable $callable): void public function addFilter(TwigFilter $filter): void { if ($this->initialized) { - throw new \LogicException(sprintf('Unable to add filter "%s" as extensions have already been initialized.', $filter->getName())); + throw new \LogicException(\sprintf('Unable to add filter "%s" as extensions have already been initialized.', $filter->getName())); } $this->staging->addFilter($filter); @@ -330,7 +330,7 @@ public function getGlobals(): array $extGlobals = $extension->getGlobals(); if (!\is_array($extGlobals)) { - throw new \UnexpectedValueException(sprintf('"%s::getGlobals()" must return an array of globals.', \get_class($extension))); + throw new \UnexpectedValueException(\sprintf('"%s::getGlobals()" must return an array of globals.', \get_class($extension))); } $globals = array_merge($globals, $extGlobals); @@ -346,7 +346,7 @@ public function getGlobals(): array public function addTest(TwigTest $test): void { if ($this->initialized) { - throw new \LogicException(sprintf('Unable to add test "%s" as extensions have already been initialized.', $test->getName())); + throw new \LogicException(\sprintf('Unable to add test "%s" as extensions have already been initialized.', $test->getName())); } $this->staging->addTest($test); @@ -466,11 +466,11 @@ private function initExtension(ExtensionInterface $extension): void // operators if ($operators = $extension->getOperators()) { if (!\is_array($operators)) { - throw new \InvalidArgumentException(sprintf('"%s::getOperators()" must return an array with operators, got "%s".', \get_class($extension), \is_object($operators) ? \get_class($operators) : \gettype($operators).(\is_resource($operators) ? '' : '#'.$operators))); + throw new \InvalidArgumentException(\sprintf('"%s::getOperators()" must return an array with operators, got "%s".', \get_class($extension), \is_object($operators) ? \get_class($operators) : \gettype($operators).(\is_resource($operators) ? '' : '#'.$operators))); } if (2 !== \count($operators)) { - throw new \InvalidArgumentException(sprintf('"%s::getOperators()" must return an array of 2 elements, got %d.', \get_class($extension), \count($operators))); + throw new \InvalidArgumentException(\sprintf('"%s::getOperators()" must return an array of 2 elements, got %d.', \get_class($extension), \count($operators))); } $this->unaryOperators = array_merge($this->unaryOperators, $operators[0]); diff --git a/src/Lexer.php b/src/Lexer.php index e15e896f5cf..8973fbbc8da 100644 --- a/src/Lexer.php +++ b/src/Lexer.php @@ -211,7 +211,7 @@ public function tokenize(Source $source): TokenStream if (!empty($this->brackets)) { [$expect, $lineno] = array_pop($this->brackets); - throw new SyntaxError(sprintf('Unclosed "%s".', $expect), $lineno, $this->source); + throw new SyntaxError(\sprintf('Unclosed "%s".', $expect), $lineno, $this->source); } return new TokenStream($this->tokens, $this->source); @@ -311,7 +311,7 @@ private function lexExpression(): void $this->moveCursor($match[0]); if ($this->cursor >= $this->end) { - throw new SyntaxError(sprintf('Unclosed "%s".', self::STATE_BLOCK === $this->state ? 'block' : 'variable'), $this->currentVarBlockLine, $this->source); + throw new SyntaxError(\sprintf('Unclosed "%s".', self::STATE_BLOCK === $this->state ? 'block' : 'variable'), $this->currentVarBlockLine, $this->source); } } @@ -353,12 +353,12 @@ private function lexExpression(): void // closing bracket elseif (str_contains(')]}', $this->code[$this->cursor])) { if (empty($this->brackets)) { - throw new SyntaxError(sprintf('Unexpected "%s".', $this->code[$this->cursor]), $this->lineno, $this->source); + throw new SyntaxError(\sprintf('Unexpected "%s".', $this->code[$this->cursor]), $this->lineno, $this->source); } [$expect, $lineno] = array_pop($this->brackets); if ($this->code[$this->cursor] != strtr($expect, '([{', ')]}')) { - throw new SyntaxError(sprintf('Unclosed "%s".', $expect), $lineno, $this->source); + throw new SyntaxError(\sprintf('Unclosed "%s".', $expect), $lineno, $this->source); } } @@ -378,7 +378,7 @@ private function lexExpression(): void } // unlexable else { - throw new SyntaxError(sprintf('Unexpected character "%s".', $this->code[$this->cursor]), $this->lineno, $this->source); + throw new SyntaxError(\sprintf('Unexpected character "%s".', $this->code[$this->cursor]), $this->lineno, $this->source); } } @@ -428,14 +428,14 @@ private function lexString(): void } elseif (preg_match(self::REGEX_DQ_STRING_DELIM, $this->code, $match, 0, $this->cursor)) { [$expect, $lineno] = array_pop($this->brackets); if ('"' != $this->code[$this->cursor]) { - throw new SyntaxError(sprintf('Unclosed "%s".', $expect), $lineno, $this->source); + throw new SyntaxError(\sprintf('Unclosed "%s".', $expect), $lineno, $this->source); } $this->popState(); ++$this->cursor; } else { // unlexable - throw new SyntaxError(sprintf('Unexpected character "%s".', $this->code[$this->cursor]), $this->lineno, $this->source); + throw new SyntaxError(\sprintf('Unexpected character "%s".', $this->code[$this->cursor]), $this->lineno, $this->source); } } diff --git a/src/Loader/ArrayLoader.php b/src/Loader/ArrayLoader.php index 5d726c35ac7..ce613c9cc1e 100644 --- a/src/Loader/ArrayLoader.php +++ b/src/Loader/ArrayLoader.php @@ -46,7 +46,7 @@ public function setTemplate(string $name, string $template): void public function getSourceContext(string $name): Source { if (!isset($this->templates[$name])) { - throw new LoaderError(sprintf('Template "%s" is not defined.', $name)); + throw new LoaderError(\sprintf('Template "%s" is not defined.', $name)); } return new Source($this->templates[$name], $name); @@ -60,7 +60,7 @@ public function exists(string $name): bool public function getCacheKey(string $name): string { if (!isset($this->templates[$name])) { - throw new LoaderError(sprintf('Template "%s" is not defined.', $name)); + throw new LoaderError(\sprintf('Template "%s" is not defined.', $name)); } return $name.':'.$this->templates[$name]; @@ -69,7 +69,7 @@ public function getCacheKey(string $name): string public function isFresh(string $name, int $time): bool { if (!isset($this->templates[$name])) { - throw new LoaderError(sprintf('Template "%s" is not defined.', $name)); + throw new LoaderError(\sprintf('Template "%s" is not defined.', $name)); } return true; diff --git a/src/Loader/ChainLoader.php b/src/Loader/ChainLoader.php index fbf4f3a0654..163c029f848 100644 --- a/src/Loader/ChainLoader.php +++ b/src/Loader/ChainLoader.php @@ -63,7 +63,7 @@ public function getSourceContext(string $name): Source } } - throw new LoaderError(sprintf('Template "%s" is not defined%s.', $name, $exceptions ? ' ('.implode(', ', $exceptions).')' : '')); + throw new LoaderError(\sprintf('Template "%s" is not defined%s.', $name, $exceptions ? ' ('.implode(', ', $exceptions).')' : '')); } public function exists(string $name): bool @@ -96,7 +96,7 @@ public function getCacheKey(string $name): string } } - throw new LoaderError(sprintf('Template "%s" is not defined%s.', $name, $exceptions ? ' ('.implode(', ', $exceptions).')' : '')); + throw new LoaderError(\sprintf('Template "%s" is not defined%s.', $name, $exceptions ? ' ('.implode(', ', $exceptions).')' : '')); } public function isFresh(string $name, int $time): bool @@ -114,6 +114,6 @@ public function isFresh(string $name, int $time): bool } } - throw new LoaderError(sprintf('Template "%s" is not defined%s.', $name, $exceptions ? ' ('.implode(', ', $exceptions).')' : '')); + throw new LoaderError(\sprintf('Template "%s" is not defined%s.', $name, $exceptions ? ' ('.implode(', ', $exceptions).')' : '')); } } diff --git a/src/Loader/FilesystemLoader.php b/src/Loader/FilesystemLoader.php index 8472796f7b0..c60964f5fc8 100644 --- a/src/Loader/FilesystemLoader.php +++ b/src/Loader/FilesystemLoader.php @@ -89,7 +89,7 @@ public function addPath(string $path, string $namespace = self::MAIN_NAMESPACE): $checkPath = $this->isAbsolutePath($path) ? $path : $this->rootPath.$path; if (!is_dir($checkPath)) { - throw new LoaderError(sprintf('The "%s" directory does not exist ("%s").', $path, $checkPath)); + throw new LoaderError(\sprintf('The "%s" directory does not exist ("%s").', $path, $checkPath)); } $this->paths[$namespace][] = rtrim($path, '/\\'); @@ -105,7 +105,7 @@ public function prependPath(string $path, string $namespace = self::MAIN_NAMESPA $checkPath = $this->isAbsolutePath($path) ? $path : $this->rootPath.$path; if (!is_dir($checkPath)) { - throw new LoaderError(sprintf('The "%s" directory does not exist ("%s").', $path, $checkPath)); + throw new LoaderError(\sprintf('The "%s" directory does not exist ("%s").', $path, $checkPath)); } $path = rtrim($path, '/\\'); @@ -195,7 +195,7 @@ protected function findTemplate(string $name, bool $throw = true) } if (!isset($this->paths[$namespace])) { - $this->errorCache[$name] = sprintf('There are no registered paths for namespace "%s".', $namespace); + $this->errorCache[$name] = \sprintf('There are no registered paths for namespace "%s".', $namespace); if (!$throw) { return null; @@ -218,7 +218,7 @@ protected function findTemplate(string $name, bool $throw = true) } } - $this->errorCache[$name] = sprintf('Unable to find template "%s" (looked into: %s).', $name, implode(', ', $this->paths[$namespace])); + $this->errorCache[$name] = \sprintf('Unable to find template "%s" (looked into: %s).', $name, implode(', ', $this->paths[$namespace])); if (!$throw) { return null; @@ -236,7 +236,7 @@ private function parseName(string $name, string $default = self::MAIN_NAMESPACE) { if (isset($name[0]) && '@' == $name[0]) { if (false === $pos = strpos($name, '/')) { - throw new LoaderError(sprintf('Malformed namespaced template name "%s" (expecting "@namespace/template_name").', $name)); + throw new LoaderError(\sprintf('Malformed namespaced template name "%s" (expecting "@namespace/template_name").', $name)); } $namespace = substr($name, 1, $pos - 1); @@ -265,7 +265,7 @@ private function validateName(string $name): void } if ($level < 0) { - throw new LoaderError(sprintf('Looks like you try to load a template outside configured directories (%s).', $name)); + throw new LoaderError(\sprintf('Looks like you try to load a template outside configured directories (%s).', $name)); } } } diff --git a/src/Node/BlockNode.php b/src/Node/BlockNode.php index 65174c02c6f..15973a343fb 100644 --- a/src/Node/BlockNode.php +++ b/src/Node/BlockNode.php @@ -32,7 +32,7 @@ public function compile(Compiler $compiler): void { $compiler ->addDebugInfo($this) - ->write(sprintf("public function block_%s(\$context, array \$blocks = [])\n", $this->getAttribute('name')), "{\n") + ->write(\sprintf("public function block_%s(\$context, array \$blocks = [])\n", $this->getAttribute('name')), "{\n") ->indent() ->write("\$macros = \$this->macros;\n") ; diff --git a/src/Node/BlockReferenceNode.php b/src/Node/BlockReferenceNode.php index f48082be36d..23c73eabee9 100644 --- a/src/Node/BlockReferenceNode.php +++ b/src/Node/BlockReferenceNode.php @@ -32,7 +32,7 @@ public function compile(Compiler $compiler): void { $compiler ->addDebugInfo($this) - ->write(sprintf("yield from \$this->unwrap()->yieldBlock('%s', \$context, \$blocks);\n", $this->getAttribute('name'))) + ->write(\sprintf("yield from \$this->unwrap()->yieldBlock('%s', \$context, \$blocks);\n", $this->getAttribute('name'))) ; } } diff --git a/src/Node/DeprecatedNode.php b/src/Node/DeprecatedNode.php index 2dc425dd301..1a07ab81afb 100644 --- a/src/Node/DeprecatedNode.php +++ b/src/Node/DeprecatedNode.php @@ -40,15 +40,15 @@ public function compile(Compiler $compiler): void ->subcompile($expr); } else { $varName = $compiler->getVarName(); - $compiler->write(sprintf('$%s = ', $varName)) + $compiler->write(\sprintf('$%s = ', $varName)) ->subcompile($expr) ->raw(";\n") - ->write(sprintf('@trigger_error($%s', $varName)); + ->write(\sprintf('@trigger_error($%s', $varName)); } $compiler ->raw('.') - ->string(sprintf(' ("%s" at line %d).', $this->getTemplateName(), $this->getTemplateLine())) + ->string(\sprintf(' ("%s" at line %d).', $this->getTemplateName(), $this->getTemplateLine())) ->raw(", E_USER_DEPRECATED);\n") ; } diff --git a/src/Node/Expression/Binary/EndsWithBinary.php b/src/Node/Expression/Binary/EndsWithBinary.php index 73fa20b1f66..a73a5608dd9 100644 --- a/src/Node/Expression/Binary/EndsWithBinary.php +++ b/src/Node/Expression/Binary/EndsWithBinary.php @@ -20,11 +20,11 @@ public function compile(Compiler $compiler): void $left = $compiler->getVarName(); $right = $compiler->getVarName(); $compiler - ->raw(sprintf('(is_string($%s = ', $left)) + ->raw(\sprintf('(is_string($%s = ', $left)) ->subcompile($this->getNode('left')) - ->raw(sprintf(') && is_string($%s = ', $right)) + ->raw(\sprintf(') && is_string($%s = ', $right)) ->subcompile($this->getNode('right')) - ->raw(sprintf(') && str_ends_with($%1$s, $%2$s))', $left, $right)) + ->raw(\sprintf(') && str_ends_with($%1$s, $%2$s))', $left, $right)) ; } diff --git a/src/Node/Expression/Binary/StartsWithBinary.php b/src/Node/Expression/Binary/StartsWithBinary.php index 22eff92a794..4519f30d9fb 100644 --- a/src/Node/Expression/Binary/StartsWithBinary.php +++ b/src/Node/Expression/Binary/StartsWithBinary.php @@ -20,11 +20,11 @@ public function compile(Compiler $compiler): void $left = $compiler->getVarName(); $right = $compiler->getVarName(); $compiler - ->raw(sprintf('(is_string($%s = ', $left)) + ->raw(\sprintf('(is_string($%s = ', $left)) ->subcompile($this->getNode('left')) - ->raw(sprintf(') && is_string($%s = ', $right)) + ->raw(\sprintf(') && is_string($%s = ', $right)) ->subcompile($this->getNode('right')) - ->raw(sprintf(') && str_starts_with($%1$s, $%2$s))', $left, $right)) + ->raw(\sprintf(') && str_starts_with($%1$s, $%2$s))', $left, $right)) ; } diff --git a/src/Node/Expression/BlockReferenceExpression.php b/src/Node/Expression/BlockReferenceExpression.php index 13e72df17ce..62938223371 100644 --- a/src/Node/Expression/BlockReferenceExpression.php +++ b/src/Node/Expression/BlockReferenceExpression.php @@ -66,7 +66,7 @@ private function compileTemplateCall(Compiler $compiler, string $method): Compil ; } - $compiler->raw(sprintf('->unwrap()->%s', $method)); + $compiler->raw(\sprintf('->unwrap()->%s', $method)); return $this->compileBlockArguments($compiler); } diff --git a/src/Node/Expression/CallExpression.php b/src/Node/Expression/CallExpression.php index 3ac10ee6544..549e8c43a1a 100644 --- a/src/Node/Expression/CallExpression.php +++ b/src/Node/Expression/CallExpression.php @@ -33,22 +33,22 @@ protected function compileCallable(Compiler $compiler) $compiler->raw($callable); } elseif (\is_array($callable) && \is_string($callable[0])) { if (!$r instanceof \ReflectionMethod || $r->isStatic()) { - $compiler->raw(sprintf('%s::%s', $callable[0], $callable[1])); + $compiler->raw(\sprintf('%s::%s', $callable[0], $callable[1])); } else { - $compiler->raw(sprintf('$this->env->getRuntime(\'%s\')->%s', $callable[0], $callable[1])); + $compiler->raw(\sprintf('$this->env->getRuntime(\'%s\')->%s', $callable[0], $callable[1])); } } elseif (\is_array($callable) && $callable[0] instanceof ExtensionInterface) { $class = \get_class($callable[0]); if (!$compiler->getEnvironment()->hasExtension($class)) { // Compile a non-optimized call to trigger a \Twig\Error\RuntimeError, which cannot be a compile-time error - $compiler->raw(sprintf('$this->env->getExtension(\'%s\')', $class)); + $compiler->raw(\sprintf('$this->env->getExtension(\'%s\')', $class)); } else { - $compiler->raw(sprintf('$this->extensions[\'%s\']', ltrim($class, '\\'))); + $compiler->raw(\sprintf('$this->extensions[\'%s\']', ltrim($class, '\\'))); } - $compiler->raw(sprintf('->%s', $callable[1])); + $compiler->raw(\sprintf('->%s', $callable[1])); } else { - $compiler->raw(sprintf('$this->env->get%s(\'%s\')->getCallable()', ucfirst($this->getAttribute('type')), $this->getAttribute('name'))); + $compiler->raw(\sprintf('$this->env->get%s(\'%s\')->getCallable()', ucfirst($this->getAttribute('type')), $this->getAttribute('name'))); } } @@ -57,7 +57,7 @@ protected function compileCallable(Compiler $compiler) protected function compileArguments(Compiler $compiler, $isArray = false): void { - if (func_num_args() >= 2) { + if (\func_num_args() >= 2) { trigger_deprecation('twig/twig', '3.11', 'Passing a second argument to "%s()" is deprecated.', __METHOD__); } @@ -131,7 +131,7 @@ protected function getArguments($callable, $arguments) $named = true; $name = $this->normalizeName($name); } elseif ($named) { - throw new SyntaxError(sprintf('Positional arguments cannot be used after named arguments for %s "%s".', $callType, $callName), $this->getTemplateLine(), $this->getSourceContext()); + throw new SyntaxError(\sprintf('Positional arguments cannot be used after named arguments for %s "%s".', $callType, $callName), $this->getTemplateLine(), $this->getSourceContext()); } $parameters[$name] = $node; @@ -144,9 +144,9 @@ protected function getArguments($callable, $arguments) if (!$callable) { if ($named) { - $message = sprintf('Named arguments are not supported for %s "%s".', $callType, $callName); + $message = \sprintf('Named arguments are not supported for %s "%s".', $callType, $callName); } else { - $message = sprintf('Arbitrary positional arguments are not supported for %s "%s".', $callType, $callName); + $message = \sprintf('Arbitrary positional arguments are not supported for %s "%s".', $callType, $callName); } throw new \LogicException($message); @@ -172,11 +172,11 @@ protected function getArguments($callable, $arguments) if (\array_key_exists($name, $parameters)) { if (\array_key_exists($pos, $parameters)) { - throw new SyntaxError(sprintf('Argument "%s" is defined twice for %s "%s".', $name, $callType, $callName), $this->getTemplateLine(), $this->getSourceContext()); + throw new SyntaxError(\sprintf('Argument "%s" is defined twice for %s "%s".', $name, $callType, $callName), $this->getTemplateLine(), $this->getSourceContext()); } if (\count($missingArguments)) { - throw new SyntaxError(sprintf( + throw new SyntaxError(\sprintf( 'Argument "%s" could not be assigned for %s "%s(%s)" because it is mapped to an internal PHP function which cannot determine default value for optional argument%s "%s".', $name, $callType, $callName, implode(', ', $names), \count($missingArguments) > 1 ? 's' : '', implode('", "', $missingArguments) ), $this->getTemplateLine(), $this->getSourceContext()); @@ -201,7 +201,7 @@ protected function getArguments($callable, $arguments) $missingArguments[] = $name; } } else { - throw new SyntaxError(sprintf('Value for argument "%s" is required for %s "%s".', $name, $callType, $callName), $this->getTemplateLine(), $this->getSourceContext()); + throw new SyntaxError(\sprintf('Value for argument "%s" is required for %s "%s".', $name, $callType, $callName), $this->getTemplateLine(), $this->getSourceContext()); } } @@ -232,7 +232,7 @@ protected function getArguments($callable, $arguments) } throw new SyntaxError( - sprintf( + \sprintf( 'Unknown argument%s "%s" for %s "%s(%s)".', \count($parameters) > 1 ? 's' : '', implode('", "', array_keys($parameters)), $callType, $callName, implode(', ', $names) ), @@ -281,7 +281,7 @@ private function getCallableParameters($callable, bool $isVariadic): array array_pop($parameters); $isPhpVariadic = true; } else { - throw new \LogicException(sprintf('The last parameter of "%s" for %s "%s" must be an array with default value, eg. "array $arg = []".', $callableName, $this->getAttribute('type'), $this->getAttribute('name'))); + throw new \LogicException(\sprintf('The last parameter of "%s" for %s "%s" must be an array with default value, eg. "array $arg = []".', $callableName, $this->getAttribute('type'), $this->getAttribute('name'))); } } @@ -308,7 +308,7 @@ private function reflectCallable($callable) try { $closure = \Closure::fromCallable($callable); } catch (\TypeError $e) { - throw new \LogicException(sprintf('Callback for %s "%s" is not callable in the current scope.', $this->getAttribute('type'), $this->getAttribute('name')), 0, $e); + throw new \LogicException(\sprintf('Callback for %s "%s" is not callable in the current scope.', $this->getAttribute('type'), $this->getAttribute('name')), 0, $e); } $r = new \ReflectionFunction($closure); diff --git a/src/Node/IncludeNode.php b/src/Node/IncludeNode.php index f10779a801c..7073fa4ac38 100644 --- a/src/Node/IncludeNode.php +++ b/src/Node/IncludeNode.php @@ -42,10 +42,10 @@ public function compile(Compiler $compiler): void $template = $compiler->getVarName(); $compiler - ->write(sprintf("$%s = null;\n", $template)) + ->write(\sprintf("$%s = null;\n", $template)) ->write("try {\n") ->indent() - ->write(sprintf('$%s = ', $template)) + ->write(\sprintf('$%s = ', $template)) ; $this->addGetTemplate($compiler); @@ -58,9 +58,9 @@ public function compile(Compiler $compiler): void ->write("// ignore missing template\n") ->outdent() ->write("}\n") - ->write(sprintf("if ($%s) {\n", $template)) + ->write(\sprintf("if ($%s) {\n", $template)) ->indent() - ->write(sprintf('yield from $%s->unwrap()->yield(', $template)) + ->write(\sprintf('yield from $%s->unwrap()->yield(', $template)) ; $this->addTemplateArguments($compiler); diff --git a/src/Node/MacroNode.php b/src/Node/MacroNode.php index 78e1b6f76dd..a6048de9bd8 100644 --- a/src/Node/MacroNode.php +++ b/src/Node/MacroNode.php @@ -29,7 +29,7 @@ public function __construct(string $name, Node $body, Node $arguments, int $line { foreach ($arguments as $argumentName => $argument) { if (self::VARARGS_NAME === $argumentName) { - throw new SyntaxError(sprintf('The argument "%s" in macro "%s" cannot be defined because the variable "%s" is reserved for arbitrary arguments.', self::VARARGS_NAME, $name, self::VARARGS_NAME), $argument->getTemplateLine(), $argument->getSourceContext()); + throw new SyntaxError(\sprintf('The argument "%s" in macro "%s" cannot be defined because the variable "%s" is reserved for arbitrary arguments.', self::VARARGS_NAME, $name, self::VARARGS_NAME), $argument->getTemplateLine(), $argument->getSourceContext()); } } @@ -40,7 +40,7 @@ public function compile(Compiler $compiler): void { $compiler ->addDebugInfo($this) - ->write(sprintf('public function macro_%s(', $this->getAttribute('name'))) + ->write(\sprintf('public function macro_%s(', $this->getAttribute('name'))) ; $count = \count($this->getNode('arguments')); diff --git a/src/Node/ModuleNode.php b/src/Node/ModuleNode.php index df5d78d0305..fb85cd89546 100644 --- a/src/Node/ModuleNode.php +++ b/src/Node/ModuleNode.php @@ -191,14 +191,14 @@ protected function compileConstructor(Compiler $compiler) $compiler ->addDebugInfo($node) - ->write(sprintf('$_trait_%s = $this->loadTemplate(', $i)) + ->write(\sprintf('$_trait_%s = $this->loadTemplate(', $i)) ->subcompile($node) ->raw(', ') ->repr($node->getTemplateName()) ->raw(', ') ->repr($node->getTemplateLine()) ->raw(");\n") - ->write(sprintf("if (!\$_trait_%s->unwrap()->isTraitable()) {\n", $i)) + ->write(\sprintf("if (!\$_trait_%s->unwrap()->isTraitable()) {\n", $i)) ->indent() ->write("throw new RuntimeError('Template \"'.") ->subcompile($trait->getNode('template')) @@ -207,12 +207,12 @@ protected function compileConstructor(Compiler $compiler) ->raw(", \$this->source);\n") ->outdent() ->write("}\n") - ->write(sprintf("\$_trait_%s_blocks = \$_trait_%s->unwrap()->getBlocks();\n\n", $i, $i)) + ->write(\sprintf("\$_trait_%s_blocks = \$_trait_%s->unwrap()->getBlocks();\n\n", $i, $i)) ; foreach ($trait->getNode('targets') as $key => $value) { $compiler - ->write(sprintf('if (!isset($_trait_%s_blocks[', $i)) + ->write(\sprintf('if (!isset($_trait_%s_blocks[', $i)) ->string($key) ->raw("])) {\n") ->indent() @@ -226,11 +226,11 @@ protected function compileConstructor(Compiler $compiler) ->outdent() ->write("}\n\n") - ->write(sprintf('$_trait_%s_blocks[', $i)) + ->write(\sprintf('$_trait_%s_blocks[', $i)) ->subcompile($value) - ->raw(sprintf('] = $_trait_%s_blocks[', $i)) + ->raw(\sprintf('] = $_trait_%s_blocks[', $i)) ->string($key) - ->raw(sprintf(']; unset($_trait_%s_blocks[', $i)) + ->raw(\sprintf(']; unset($_trait_%s_blocks[', $i)) ->string($key) ->raw("]);\n\n") ; @@ -245,7 +245,7 @@ protected function compileConstructor(Compiler $compiler) for ($i = 0; $i < $countTraits; ++$i) { $compiler - ->write(sprintf('$_trait_%s_blocks'.($i == $countTraits - 1 ? '' : ',')."\n", $i)) + ->write(\sprintf('$_trait_%s_blocks'.($i == $countTraits - 1 ? '' : ',')."\n", $i)) ; } @@ -278,7 +278,7 @@ protected function compileConstructor(Compiler $compiler) foreach ($this->getNode('blocks') as $name => $node) { $compiler - ->write(sprintf("'%s' => [\$this, 'block_%s'],\n", $name, $name)) + ->write(\sprintf("'%s' => [\$this, 'block_%s'],\n", $name, $name)) ; } @@ -443,7 +443,7 @@ protected function compileDebugInfo(Compiler $compiler) ->write(" */\n") ->write("public function getDebugInfo()\n", "{\n") ->indent() - ->write(sprintf("return %s;\n", str_replace("\n", '', var_export(array_reverse($compiler->getDebugInfo(), true), true)))) + ->write(\sprintf("return %s;\n", str_replace("\n", '', var_export(array_reverse($compiler->getDebugInfo(), true), true)))) ->outdent() ->write("}\n\n") ; @@ -470,7 +470,7 @@ protected function compileLoadTemplate(Compiler $compiler, $node, $var) { if ($node instanceof ConstantExpression) { $compiler - ->write(sprintf('%s = $this->loadTemplate(', $var)) + ->write(\sprintf('%s = $this->loadTemplate(', $var)) ->subcompile($node) ->raw(', ') ->repr($node->getTemplateName()) diff --git a/src/Node/Node.php b/src/Node/Node.php index 4ac94f1bc9b..2da6bd88dff 100644 --- a/src/Node/Node.php +++ b/src/Node/Node.php @@ -41,7 +41,7 @@ public function __construct(array $nodes = [], array $attributes = [], int $line { foreach ($nodes as $name => $node) { if (!$node instanceof self) { - throw new \InvalidArgumentException(sprintf('Using "%s" for the value of node "%s" of "%s" is not supported. You must pass a \Twig\Node\Node instance.', \is_object($node) ? \get_class($node) : (null === $node ? 'null' : \gettype($node)), $name, static::class)); + throw new \InvalidArgumentException(\sprintf('Using "%s" for the value of node "%s" of "%s" is not supported. You must pass a \Twig\Node\Node instance.', \is_object($node) ? \get_class($node) : (null === $node ? 'null' : \gettype($node)), $name, static::class)); } } $this->nodes = $nodes; @@ -54,7 +54,7 @@ public function __toString() { $attributes = []; foreach ($this->attributes as $name => $value) { - $attributes[] = sprintf('%s: %s', $name, str_replace("\n", '', var_export($value, true))); + $attributes[] = \sprintf('%s: %s', $name, str_replace("\n", '', var_export($value, true))); } $repr = [static::class.'('.implode(', ', $attributes)]; @@ -67,7 +67,7 @@ public function __toString() $noderepr[] = str_repeat(' ', $len).$line; } - $repr[] = sprintf(' %s: %s', $name, ltrim(implode("\n", $noderepr))); + $repr[] = \sprintf(' %s: %s', $name, ltrim(implode("\n", $noderepr))); } $repr[] = ')'; @@ -106,7 +106,7 @@ public function hasAttribute(string $name): bool public function getAttribute(string $name) { if (!\array_key_exists($name, $this->attributes)) { - throw new \LogicException(sprintf('Attribute "%s" does not exist for Node "%s".', $name, static::class)); + throw new \LogicException(\sprintf('Attribute "%s" does not exist for Node "%s".', $name, static::class)); } return $this->attributes[$name]; @@ -130,7 +130,7 @@ public function hasNode(string $name): bool public function getNode(string $name): self { if (!isset($this->nodes[$name])) { - throw new \LogicException(sprintf('Node "%s" does not exist for Node "%s".', $name, static::class)); + throw new \LogicException(\sprintf('Node "%s" does not exist for Node "%s".', $name, static::class)); } return $this->nodes[$name]; diff --git a/src/Node/WithNode.php b/src/Node/WithNode.php index 9b8c5788466..c78136c81a0 100644 --- a/src/Node/WithNode.php +++ b/src/Node/WithNode.php @@ -38,35 +38,35 @@ public function compile(Compiler $compiler): void $parentContextName = $compiler->getVarName(); - $compiler->write(sprintf("\$%s = \$context;\n", $parentContextName)); + $compiler->write(\sprintf("\$%s = \$context;\n", $parentContextName)); if ($this->hasNode('variables')) { $node = $this->getNode('variables'); $varsName = $compiler->getVarName(); $compiler - ->write(sprintf('$%s = ', $varsName)) + ->write(\sprintf('$%s = ', $varsName)) ->subcompile($node) ->raw(";\n") - ->write(sprintf("if (!is_iterable(\$%s)) {\n", $varsName)) + ->write(\sprintf("if (!is_iterable(\$%s)) {\n", $varsName)) ->indent() ->write("throw new RuntimeError('Variables passed to the \"with\" tag must be a hash.', ") ->repr($node->getTemplateLine()) ->raw(", \$this->getSourceContext());\n") ->outdent() ->write("}\n") - ->write(sprintf("\$%s = CoreExtension::toArray(\$%s);\n", $varsName, $varsName)) + ->write(\sprintf("\$%s = CoreExtension::toArray(\$%s);\n", $varsName, $varsName)) ; if ($this->getAttribute('only')) { $compiler->write("\$context = [];\n"); } - $compiler->write(sprintf("\$context = \$this->env->mergeGlobals(array_merge(\$context, \$%s));\n", $varsName)); + $compiler->write(\sprintf("\$context = \$this->env->mergeGlobals(array_merge(\$context, \$%s));\n", $varsName)); } $compiler ->subcompile($this->getNode('body')) - ->write(sprintf("\$context = \$%s;\n", $parentContextName)) + ->write(\sprintf("\$context = \$%s;\n", $parentContextName)) ; } } diff --git a/src/NodeVisitor/OptimizerNodeVisitor.php b/src/NodeVisitor/OptimizerNodeVisitor.php index 6af056ac414..a2540f16a96 100644 --- a/src/NodeVisitor/OptimizerNodeVisitor.php +++ b/src/NodeVisitor/OptimizerNodeVisitor.php @@ -56,7 +56,7 @@ final class OptimizerNodeVisitor implements NodeVisitorInterface public function __construct(int $optimizers = -1) { if ($optimizers > (self::OPTIMIZE_FOR | self::OPTIMIZE_RAW_FILTER)) { - throw new \InvalidArgumentException(sprintf('Optimizer mode "%s" is not valid.', $optimizers)); + throw new \InvalidArgumentException(\sprintf('Optimizer mode "%s" is not valid.', $optimizers)); } $this->optimizers = $optimizers; @@ -107,7 +107,7 @@ private function mergeTextNodeCalls(Node $node): Node return $node; } - if (Node::class === get_class($node)) { + if (Node::class === \get_class($node)) { return new TextNode($text, $node->getTemplateLine()); } diff --git a/src/NodeVisitor/YieldNotReadyNodeVisitor.php b/src/NodeVisitor/YieldNotReadyNodeVisitor.php index 34c3ac18ba6..6470bdabc86 100644 --- a/src/NodeVisitor/YieldNotReadyNodeVisitor.php +++ b/src/NodeVisitor/YieldNotReadyNodeVisitor.php @@ -39,7 +39,7 @@ public function enterNode(Node $node, Environment $env): Node if (!$this->yieldReadyNodes[$class] = (bool) (new \ReflectionClass($class))->getAttributes(YieldReady::class)) { if ($this->useYield) { - throw new \LogicException(sprintf('You cannot enable the "use_yield" option of Twig as node "%s" is not marked as ready for it; please make it ready and then flag it with the #[YieldReady] attribute.', $class)); + throw new \LogicException(\sprintf('You cannot enable the "use_yield" option of Twig as node "%s" is not marked as ready for it; please make it ready and then flag it with the #[YieldReady] attribute.', $class)); } trigger_deprecation('twig/twig', '3.9', 'Twig node "%s" is not marked as ready for using "yield" instead of "echo"; please make it ready and then flag it with the #[YieldReady] attribute.', $class); diff --git a/src/Parser.php b/src/Parser.php index adcaee31633..42447dd00b9 100644 --- a/src/Parser.php +++ b/src/Parser.php @@ -52,7 +52,7 @@ public function __construct(Environment $env) public function getVarName(): string { - return sprintf('__internal_parse_%d', $this->varNameSalt++); + return \sprintf('__internal_parse_%d', $this->varNameSalt++); } public function parse(TokenStream $stream, $test = null, bool $dropNeedle = false): ModuleNode @@ -154,13 +154,13 @@ public function subparse($test, bool $dropNeedle = false): Node if (!$subparser = $this->env->getTokenParser($token->getValue())) { if (null !== $test) { - $e = new SyntaxError(sprintf('Unexpected "%s" tag', $token->getValue()), $token->getLine(), $this->stream->getSourceContext()); + $e = new SyntaxError(\sprintf('Unexpected "%s" tag', $token->getValue()), $token->getLine(), $this->stream->getSourceContext()); if (\is_array($test) && isset($test[0]) && $test[0] instanceof TokenParserInterface) { - $e->appendMessage(sprintf(' (expecting closing tag for the "%s" tag defined near line %s).', $test[0]->getTag(), $lineno)); + $e->appendMessage(\sprintf(' (expecting closing tag for the "%s" tag defined near line %s).', $test[0]->getTag(), $lineno)); } } else { - $e = new SyntaxError(sprintf('Unknown "%s" tag.', $token->getValue()), $token->getLine(), $this->stream->getSourceContext()); + $e = new SyntaxError(\sprintf('Unknown "%s" tag.', $token->getValue()), $token->getLine(), $this->stream->getSourceContext()); $e->addSuggestions($token->getValue(), array_keys($this->env->getTokenParsers())); } diff --git a/src/Profiler/Dumper/BaseDumper.php b/src/Profiler/Dumper/BaseDumper.php index 4da43e475fb..267718c1f5f 100644 --- a/src/Profiler/Dumper/BaseDumper.php +++ b/src/Profiler/Dumper/BaseDumper.php @@ -50,7 +50,7 @@ private function dumpProfile(Profile $profile, $prefix = '', $sibling = false): if ($profile->getDuration() * 1000 < 1) { $str = $start."\n"; } else { - $str = sprintf("%s %s\n", $start, $this->formatTime($profile, $percent)); + $str = \sprintf("%s %s\n", $start, $this->formatTime($profile, $percent)); } $nCount = \count($profile->getProfiles()); diff --git a/src/Profiler/Dumper/BlackfireDumper.php b/src/Profiler/Dumper/BlackfireDumper.php index 03abe0fa071..bb3fbb52a2a 100644 --- a/src/Profiler/Dumper/BlackfireDumper.php +++ b/src/Profiler/Dumper/BlackfireDumper.php @@ -24,7 +24,7 @@ public function dump(Profile $profile): string $this->dumpProfile('main()', $profile, $data); $this->dumpChildren('main()', $profile, $data); - $start = sprintf('%f', microtime(true)); + $start = \sprintf('%f', microtime(true)); $str = <<isTemplate()) { $name = $p->getTemplate(); } else { - $name = sprintf('%s::%s(%s)', $p->getTemplate(), $p->getType(), $p->getName()); + $name = \sprintf('%s::%s(%s)', $p->getTemplate(), $p->getType(), $p->getName()); } - $this->dumpProfile(sprintf('%s==>%s', $parent, $name), $p, $data); + $this->dumpProfile(\sprintf('%s==>%s', $parent, $name), $p, $data); $this->dumpChildren($name, $p, $data); } } diff --git a/src/Profiler/Dumper/HtmlDumper.php b/src/Profiler/Dumper/HtmlDumper.php index 3c0daf1c8d3..cdab2de5953 100644 --- a/src/Profiler/Dumper/HtmlDumper.php +++ b/src/Profiler/Dumper/HtmlDumper.php @@ -32,16 +32,16 @@ public function dump(Profile $profile): string protected function formatTemplate(Profile $profile, $prefix): string { - return sprintf('%s└ %s', $prefix, self::$colors['template'], $profile->getTemplate()); + return \sprintf('%s└ %s', $prefix, self::$colors['template'], $profile->getTemplate()); } protected function formatNonTemplate(Profile $profile, $prefix): string { - return sprintf('%s└ %s::%s(%s)', $prefix, $profile->getTemplate(), $profile->getType(), self::$colors[$profile->getType()] ?? 'auto', $profile->getName()); + return \sprintf('%s└ %s::%s(%s)', $prefix, $profile->getTemplate(), $profile->getType(), self::$colors[$profile->getType()] ?? 'auto', $profile->getName()); } protected function formatTime(Profile $profile, $percent): string { - return sprintf('%.2fms/%.0f%%', $percent > 20 ? self::$colors['big'] : 'auto', $profile->getDuration() * 1000, $percent); + return \sprintf('%.2fms/%.0f%%', $percent > 20 ? self::$colors['big'] : 'auto', $profile->getDuration() * 1000, $percent); } } diff --git a/src/Profiler/Dumper/TextDumper.php b/src/Profiler/Dumper/TextDumper.php index 31561c466bb..1c1f77e949c 100644 --- a/src/Profiler/Dumper/TextDumper.php +++ b/src/Profiler/Dumper/TextDumper.php @@ -20,16 +20,16 @@ final class TextDumper extends BaseDumper { protected function formatTemplate(Profile $profile, $prefix): string { - return sprintf('%s└ %s', $prefix, $profile->getTemplate()); + return \sprintf('%s└ %s', $prefix, $profile->getTemplate()); } protected function formatNonTemplate(Profile $profile, $prefix): string { - return sprintf('%s└ %s::%s(%s)', $prefix, $profile->getTemplate(), $profile->getType(), $profile->getName()); + return \sprintf('%s└ %s::%s(%s)', $prefix, $profile->getTemplate(), $profile->getType(), $profile->getName()); } protected function formatTime(Profile $profile, $percent): string { - return sprintf('%.2fms/%.0f%%', $profile->getDuration() * 1000, $percent); + return \sprintf('%.2fms/%.0f%%', $profile->getDuration() * 1000, $percent); } } diff --git a/src/Profiler/Node/EnterProfileNode.php b/src/Profiler/Node/EnterProfileNode.php index 7b71f8b30df..4d8e504d1d7 100644 --- a/src/Profiler/Node/EnterProfileNode.php +++ b/src/Profiler/Node/EnterProfileNode.php @@ -31,10 +31,10 @@ public function __construct(string $extensionName, string $type, string $name, s public function compile(Compiler $compiler): void { $compiler - ->write(sprintf('$%s = $this->extensions[', $this->getAttribute('var_name'))) + ->write(\sprintf('$%s = $this->extensions[', $this->getAttribute('var_name'))) ->repr($this->getAttribute('extension_name')) ->raw("];\n") - ->write(sprintf('$%s->enter($%s = new \Twig\Profiler\Profile($this->getTemplateName(), ', $this->getAttribute('var_name'), $this->getAttribute('var_name').'_prof')) + ->write(\sprintf('$%s->enter($%s = new \Twig\Profiler\Profile($this->getTemplateName(), ', $this->getAttribute('var_name'), $this->getAttribute('var_name').'_prof')) ->repr($this->getAttribute('type')) ->raw(', ') ->repr($this->getAttribute('name')) diff --git a/src/Profiler/Node/LeaveProfileNode.php b/src/Profiler/Node/LeaveProfileNode.php index 7e9ef9b64ee..bd9227e5271 100644 --- a/src/Profiler/Node/LeaveProfileNode.php +++ b/src/Profiler/Node/LeaveProfileNode.php @@ -32,7 +32,7 @@ public function compile(Compiler $compiler): void { $compiler ->write("\n") - ->write(sprintf("\$%s->leave(\$%s);\n\n", $this->getAttribute('var_name'), $this->getAttribute('var_name').'_prof')) + ->write(\sprintf("\$%s->leave(\$%s);\n\n", $this->getAttribute('var_name'), $this->getAttribute('var_name').'_prof')) ; } } diff --git a/src/Profiler/NodeVisitor/ProfilerNodeVisitor.php b/src/Profiler/NodeVisitor/ProfilerNodeVisitor.php index 91abee807df..4d2a581054b 100644 --- a/src/Profiler/NodeVisitor/ProfilerNodeVisitor.php +++ b/src/Profiler/NodeVisitor/ProfilerNodeVisitor.php @@ -33,7 +33,7 @@ final class ProfilerNodeVisitor implements NodeVisitorInterface public function __construct(string $extensionName) { $this->extensionName = $extensionName; - $this->varName = sprintf('__internal_%s', hash(\PHP_VERSION_ID < 80100 ? 'sha256' : 'xxh128', $extensionName)); + $this->varName = \sprintf('__internal_%s', hash(\PHP_VERSION_ID < 80100 ? 'sha256' : 'xxh128', $extensionName)); } public function enterNode(Node $node, Environment $env): Node diff --git a/src/Runtime/EscaperRuntime.php b/src/Runtime/EscaperRuntime.php index eb5dfd06cf6..433a0250cca 100644 --- a/src/Runtime/EscaperRuntime.php +++ b/src/Runtime/EscaperRuntime.php @@ -199,7 +199,7 @@ public function escape($string, string $strategy = 'html', ?string $charset = nu $codepoint = mb_ord($char, 'UTF-8'); if (0x10000 > $codepoint) { - return sprintf('\u%04X', $codepoint); + return \sprintf('\u%04X', $codepoint); } // Split characters outside the BMP into surrogate pairs @@ -208,7 +208,7 @@ public function escape($string, string $strategy = 'html', ?string $charset = nu $high = 0xD800 | ($u >> 10); $low = 0xDC00 | ($u & 0x3FF); - return sprintf('\u%04X\u%04X', $high, $low); + return \sprintf('\u%04X\u%04X', $high, $low); }, $string); if ('UTF-8' !== $charset) { @@ -229,7 +229,7 @@ public function escape($string, string $strategy = 'html', ?string $charset = nu $string = preg_replace_callback('#[^a-zA-Z0-9]#Su', function ($matches) { $char = $matches[0]; - return sprintf('\\%X ', 1 === \strlen($char) ? \ord($char) : mb_ord($char, 'UTF-8')); + return \sprintf('\\%X ', 1 === \strlen($char) ? \ord($char) : mb_ord($char, 'UTF-8')); }, $string); if ('UTF-8' !== $charset) { @@ -287,14 +287,14 @@ public function escape($string, string $strategy = 'html', ?string $charset = nu return $entityMap[$ord]; } - return sprintf('&#x%02X;', $ord); + return \sprintf('&#x%02X;', $ord); } /* * Per OWASP recommendations, we'll use hex entities for any other * characters where a named entity does not exist. */ - return sprintf('&#x%04X;', mb_ord($chr, 'UTF-8')); + return \sprintf('&#x%04X;', mb_ord($chr, 'UTF-8')); }, $string); if ('UTF-8' !== $charset) { @@ -313,7 +313,7 @@ public function escape($string, string $strategy = 'html', ?string $charset = nu $validStrategies = implode('", "', array_merge(['html', 'js', 'url', 'css', 'html_attr'], array_keys($this->escapers))); - throw new RuntimeError(sprintf('Invalid escaping strategy "%s" (valid ones: "%s").', $strategy, $validStrategies)); + throw new RuntimeError(\sprintf('Invalid escaping strategy "%s" (valid ones: "%s").', $strategy, $validStrategies)); } } diff --git a/src/Sandbox/SecurityPolicy.php b/src/Sandbox/SecurityPolicy.php index a725aa4f104..417d38a8d4e 100644 --- a/src/Sandbox/SecurityPolicy.php +++ b/src/Sandbox/SecurityPolicy.php @@ -68,19 +68,19 @@ public function checkSecurity($tags, $filters, $functions): void { foreach ($tags as $tag) { if (!\in_array($tag, $this->allowedTags)) { - throw new SecurityNotAllowedTagError(sprintf('Tag "%s" is not allowed.', $tag), $tag); + throw new SecurityNotAllowedTagError(\sprintf('Tag "%s" is not allowed.', $tag), $tag); } } foreach ($filters as $filter) { if (!\in_array($filter, $this->allowedFilters)) { - throw new SecurityNotAllowedFilterError(sprintf('Filter "%s" is not allowed.', $filter), $filter); + throw new SecurityNotAllowedFilterError(\sprintf('Filter "%s" is not allowed.', $filter), $filter); } } foreach ($functions as $function) { if (!\in_array($function, $this->allowedFunctions)) { - throw new SecurityNotAllowedFunctionError(sprintf('Function "%s" is not allowed.', $function), $function); + throw new SecurityNotAllowedFunctionError(\sprintf('Function "%s" is not allowed.', $function), $function); } } } @@ -102,7 +102,7 @@ public function checkMethodAllowed($obj, $method): void if (!$allowed) { $class = \get_class($obj); - throw new SecurityNotAllowedMethodError(sprintf('Calling "%s" method on a "%s" object is not allowed.', $method, $class), $class, $method); + throw new SecurityNotAllowedMethodError(\sprintf('Calling "%s" method on a "%s" object is not allowed.', $method, $class), $class, $method); } } @@ -118,7 +118,7 @@ public function checkPropertyAllowed($obj, $property): void if (!$allowed) { $class = \get_class($obj); - throw new SecurityNotAllowedPropertyError(sprintf('Calling "%s" property on a "%s" object is not allowed.', $property, $class), $class, $property); + throw new SecurityNotAllowedPropertyError(\sprintf('Calling "%s" property on a "%s" object is not allowed.', $property, $class), $class, $property); } } } diff --git a/src/Template.php b/src/Template.php index e08837737da..04c530cc9c8 100644 --- a/src/Template.php +++ b/src/Template.php @@ -382,7 +382,7 @@ public function yield(array $context, array $blocks = []): iterable throw $e; } catch (\Throwable $e) { - $e = new RuntimeError(sprintf('An exception has been thrown during the rendering of a template ("%s").', $e->getMessage()), -1, $this->getSourceContext(), $e); + $e = new RuntimeError(\sprintf('An exception has been thrown during the rendering of a template ("%s").', $e->getMessage()), -1, $this->getSourceContext(), $e); $e->guess(); throw $e; @@ -452,7 +452,7 @@ public function yieldBlock($name, array $context, array $blocks = [], $useBlocks throw $e; } catch (\Throwable $e) { - $e = new RuntimeError(sprintf('An exception has been thrown during the rendering of a template ("%s").', $e->getMessage()), -1, $template->getSourceContext(), $e); + $e = new RuntimeError(\sprintf('An exception has been thrown during the rendering of a template ("%s").', $e->getMessage()), -1, $template->getSourceContext(), $e); $e->guess(); throw $e; @@ -466,9 +466,9 @@ public function yieldBlock($name, array $context, array $blocks = [], $useBlocks } elseif ($parent = $this->getParent($context)) { yield from $parent->unwrap()->yieldBlock($name, $context, array_merge($this->blocks, $blocks), false, $templateContext ?? $this); } elseif (isset($blocks[$name])) { - throw new RuntimeError(sprintf('Block "%s" should not call parent() in "%s" as the block does not exist in the parent template "%s".', $name, $blocks[$name][0]->getTemplateName(), $this->getTemplateName()), -1, $blocks[$name][0]->getSourceContext()); + throw new RuntimeError(\sprintf('Block "%s" should not call parent() in "%s" as the block does not exist in the parent template "%s".', $name, $blocks[$name][0]->getTemplateName(), $this->getTemplateName()), -1, $blocks[$name][0]->getSourceContext()); } else { - throw new RuntimeError(sprintf('Block "%s" on template "%s" does not exist.', $name, $this->getTemplateName()), -1, ($templateContext ?? $this)->getSourceContext()); + throw new RuntimeError(\sprintf('Block "%s" on template "%s" does not exist.', $name, $this->getTemplateName()), -1, ($templateContext ?? $this)->getSourceContext()); } } @@ -491,7 +491,7 @@ public function yieldParentBlock($name, array $context, array $blocks = []) } elseif ($parent = $this->getParent($context)) { yield from $parent->unwrap()->yieldBlock($name, $context, $blocks, false); } else { - throw new RuntimeError(sprintf('The template has no parent and no traits defining the "%s" block.', $name), -1, $this->getSourceContext()); + throw new RuntimeError(\sprintf('The template has no parent and no traits defining the "%s" block.', $name), -1, $this->getSourceContext()); } } diff --git a/src/Test/IntegrationTestCase.php b/src/Test/IntegrationTestCase.php index d0731ce19d5..44853063dfa 100644 --- a/src/Test/IntegrationTestCase.php +++ b/src/Test/IntegrationTestCase.php @@ -123,7 +123,7 @@ public function getTests($name, $legacyTests = false) $exception = false; preg_match_all('/--DATA--(.*?)(?:--CONFIG--(.*?))?--EXPECT--(.*?)(?=\-\-DATA\-\-|$)/s', $test, $outputs, \PREG_SET_ORDER); } else { - throw new \InvalidArgumentException(sprintf('Test "%s" is not valid.', str_replace($fixturesDir.'/', '', $file))); + throw new \InvalidArgumentException(\sprintf('Test "%s" is not valid.', str_replace($fixturesDir.'/', '', $file))); } $tests[] = [str_replace($fixturesDir.'/', '', $file), $message, $condition, $templates, $exception, $outputs, $deprecation]; @@ -206,14 +206,14 @@ protected function doIntegrationTest($file, $message, $condition, $templates, $e } catch (\Exception $e) { if (false !== $exception) { $message = $e->getMessage(); - $this->assertSame(trim($exception), trim(sprintf('%s: %s', \get_class($e), $message))); + $this->assertSame(trim($exception), trim(\sprintf('%s: %s', \get_class($e), $message))); $last = substr($message, \strlen($message) - 1); $this->assertTrue('.' === $last || '?' === $last, 'Exception message must end with a dot or a question mark.'); return; } - throw new Error(sprintf('%s: %s', \get_class($e), $e->getMessage()), -1, null, $e); + throw new Error(\sprintf('%s: %s', \get_class($e), $e->getMessage()), -1, null, $e); } finally { restore_error_handler(); } @@ -224,14 +224,14 @@ protected function doIntegrationTest($file, $message, $condition, $templates, $e $output = trim($template->render(eval($match[1].';')), "\n "); } catch (\Exception $e) { if (false !== $exception) { - $this->assertSame(trim($exception), trim(sprintf('%s: %s', \get_class($e), $e->getMessage()))); + $this->assertSame(trim($exception), trim(\sprintf('%s: %s', \get_class($e), $e->getMessage()))); return; } - $e = new Error(sprintf('%s: %s', \get_class($e), $e->getMessage()), -1, null, $e); + $e = new Error(\sprintf('%s: %s', \get_class($e), $e->getMessage()), -1, null, $e); - $output = trim(sprintf('%s: %s', \get_class($e), $e->getMessage())); + $output = trim(\sprintf('%s: %s', \get_class($e), $e->getMessage())); } if (false !== $exception) { diff --git a/src/Test/NodeTestCase.php b/src/Test/NodeTestCase.php index 30d6810f8af..61080bd8e13 100644 --- a/src/Test/NodeTestCase.php +++ b/src/Test/NodeTestCase.php @@ -60,7 +60,7 @@ protected function getVariableGetter($name, $line = false) { $line = $line > 0 ? "// line $line\n" : ''; - return sprintf('%s($context["%s"] ?? null)', $line, $name); + return \sprintf('%s($context["%s"] ?? null)', $line, $name); } protected function getAttributeGetter() diff --git a/src/Token.php b/src/Token.php index 59279b8fe7c..5be39bdc7ee 100644 --- a/src/Token.php +++ b/src/Token.php @@ -46,7 +46,7 @@ public function __construct(int $type, $value, int $lineno) public function __toString() { - return sprintf('%s(%s)', self::typeToString($this->type, true), $this->value); + return \sprintf('%s(%s)', self::typeToString($this->type, true), $this->value); } /** @@ -138,7 +138,7 @@ public static function typeToString(int $type, bool $short = false): string $name = 'SPREAD_TYPE'; break; default: - throw new \LogicException(sprintf('Token of type "%s" does not exist.', $type)); + throw new \LogicException(\sprintf('Token of type "%s" does not exist.', $type)); } return $short ? $name : 'Twig\Token::'.$name; @@ -178,7 +178,7 @@ public static function typeToEnglish(int $type): string case self::SPREAD_TYPE: return 'spread operator'; default: - throw new \LogicException(sprintf('Token of type "%s" does not exist.', $type)); + throw new \LogicException(\sprintf('Token of type "%s" does not exist.', $type)); } } } diff --git a/src/TokenParser/BlockTokenParser.php b/src/TokenParser/BlockTokenParser.php index 5878131bec3..c654d31f940 100644 --- a/src/TokenParser/BlockTokenParser.php +++ b/src/TokenParser/BlockTokenParser.php @@ -37,7 +37,7 @@ public function parse(Token $token): Node $stream = $this->parser->getStream(); $name = $stream->expect(/* Token::NAME_TYPE */ 5)->getValue(); if ($this->parser->hasBlock($name)) { - throw new SyntaxError(sprintf("The block '%s' has already been defined line %d.", $name, $this->parser->getBlock($name)->getTemplateLine()), $stream->getCurrent()->getLine(), $stream->getSourceContext()); + throw new SyntaxError(\sprintf("The block '%s' has already been defined line %d.", $name, $this->parser->getBlock($name)->getTemplateLine()), $stream->getCurrent()->getLine(), $stream->getSourceContext()); } $this->parser->setBlock($name, $block = new BlockNode($name, new Node([]), $lineno)); $this->parser->pushLocalScope(); @@ -49,7 +49,7 @@ public function parse(Token $token): Node $value = $token->getValue(); if ($value != $name) { - throw new SyntaxError(sprintf('Expected endblock for block "%s" (but "%s" given).', $name, $value), $stream->getCurrent()->getLine(), $stream->getSourceContext()); + throw new SyntaxError(\sprintf('Expected endblock for block "%s" (but "%s" given).', $name, $value), $stream->getCurrent()->getLine(), $stream->getSourceContext()); } } } else { diff --git a/src/TokenParser/IfTokenParser.php b/src/TokenParser/IfTokenParser.php index c0fe6df0d10..569ccfaf11a 100644 --- a/src/TokenParser/IfTokenParser.php +++ b/src/TokenParser/IfTokenParser.php @@ -63,7 +63,7 @@ public function parse(Token $token): Node break; default: - throw new SyntaxError(sprintf('Unexpected end of template. Twig was looking for the following tags "else", "elseif", or "endif" to close the "if" block started at line %d).', $lineno), $stream->getCurrent()->getLine(), $stream->getSourceContext()); + throw new SyntaxError(\sprintf('Unexpected end of template. Twig was looking for the following tags "else", "elseif", or "endif" to close the "if" block started at line %d).', $lineno), $stream->getCurrent()->getLine(), $stream->getSourceContext()); } } diff --git a/src/TokenParser/MacroTokenParser.php b/src/TokenParser/MacroTokenParser.php index f584927e908..1f0e3e97fd1 100644 --- a/src/TokenParser/MacroTokenParser.php +++ b/src/TokenParser/MacroTokenParser.php @@ -43,7 +43,7 @@ public function parse(Token $token): Node $value = $token->getValue(); if ($value != $name) { - throw new SyntaxError(sprintf('Expected endmacro for macro "%s" (but "%s" given).', $name, $value), $stream->getCurrent()->getLine(), $stream->getSourceContext()); + throw new SyntaxError(\sprintf('Expected endmacro for macro "%s" (but "%s" given).', $name, $value), $stream->getCurrent()->getLine(), $stream->getSourceContext()); } } $this->parser->popLocalScope(); diff --git a/src/TokenStream.php b/src/TokenStream.php index 89e7e0f3f80..9921f788d88 100644 --- a/src/TokenStream.php +++ b/src/TokenStream.php @@ -71,11 +71,11 @@ public function expect($type, $value = null, ?string $message = null): Token $token = $this->tokens[$this->current]; if (!$token->test($type, $value)) { $line = $token->getLine(); - throw new SyntaxError(sprintf('%sUnexpected token "%s"%s ("%s" expected%s).', + throw new SyntaxError(\sprintf('%sUnexpected token "%s"%s ("%s" expected%s).', $message ? $message.'. ' : '', Token::typeToEnglish($token->getType()), - $token->getValue() ? sprintf(' of value "%s"', $token->getValue()) : '', - Token::typeToEnglish($type), $value ? sprintf(' with value "%s"', $value) : ''), + $token->getValue() ? \sprintf(' of value "%s"', $token->getValue()) : '', + Token::typeToEnglish($type), $value ? \sprintf(' with value "%s"', $value) : ''), $line, $this->source ); diff --git a/tests/ErrorTest.php b/tests/ErrorTest.php index ee6a86c49aa..7db1259e4b9 100644 --- a/tests/ErrorTest.php +++ b/tests/ErrorTest.php @@ -139,7 +139,7 @@ public function testTwigExceptionAddsFileAndLine($templates, $name, $line) $this->fail(); } catch (RuntimeError $e) { - $this->assertEquals(sprintf('Variable "foo" does not exist in "%s" at line %d.', $name, $line), $e->getMessage()); + $this->assertEquals(\sprintf('Variable "foo" does not exist in "%s" at line %d.', $name, $line), $e->getMessage()); $this->assertEquals($line, $e->getTemplateLine()); $this->assertEquals($name, $e->getSourceContext()->getName()); } @@ -149,7 +149,7 @@ public function testTwigExceptionAddsFileAndLine($templates, $name, $line) $this->fail(); } catch (RuntimeError $e) { - $this->assertEquals(sprintf('An exception has been thrown during the rendering of a template ("Runtime error...") in "%s" at line %d.', $name, $line), $e->getMessage()); + $this->assertEquals(\sprintf('An exception has been thrown during the rendering of a template ("Runtime error...") in "%s" at line %d.', $name, $line), $e->getMessage()); $this->assertEquals($line, $e->getTemplateLine()); $this->assertEquals($name, $e->getSourceContext()->getName()); } diff --git a/tests/Extension/CoreTest.php b/tests/Extension/CoreTest.php index 2d3c0c27ab6..31458628115 100644 --- a/tests/Extension/CoreTest.php +++ b/tests/Extension/CoreTest.php @@ -387,7 +387,7 @@ public function next(): void { ++$this->position; if ($this->position === $this->maxPosition) { - throw new \LogicException(sprintf('Code should not iterate beyond %d.', $this->maxPosition)); + throw new \LogicException(\sprintf('Code should not iterate beyond %d.', $this->maxPosition)); } } diff --git a/tests/Node/Expression/FilterTest.php b/tests/Node/Expression/FilterTest.php index f99d6242f2e..7234ac31816 100644 --- a/tests/Node/Expression/FilterTest.php +++ b/tests/Node/Expression/FilterTest.php @@ -131,7 +131,7 @@ protected function foobar() // from extension $node = $this->createFilter($string, 'foo'); - $tests[] = [$node, sprintf('$this->extensions[\'%s\']->foo("abc")', \get_class($extension)), $environment]; + $tests[] = [$node, \sprintf('$this->extensions[\'%s\']->foo("abc")', \get_class($extension)), $environment]; $node = $this->createFilter($string, 'foobar'); $tests[] = [$node, '$this->env->getFilter(\'foobar\')->getCallable()("abc")', $environment]; diff --git a/tests/Node/Expression/GetAttrTest.php b/tests/Node/Expression/GetAttrTest.php index c76fb3992d5..38b70555c33 100644 --- a/tests/Node/Expression/GetAttrTest.php +++ b/tests/Node/Expression/GetAttrTest.php @@ -43,7 +43,7 @@ public function getTests() $attr = new ConstantExpression('bar', 1); $args = new ArrayExpression([], 1); $node = new GetAttrExpression($expr, $attr, $args, Template::ANY_CALL, 1); - $tests[] = [$node, sprintf('%s%s, "bar", [], "any", false, false, false, 1)', $this->getAttributeGetter(), $this->getVariableGetter('foo', 1))]; + $tests[] = [$node, \sprintf('%s%s, "bar", [], "any", false, false, false, 1)', $this->getAttributeGetter(), $this->getVariableGetter('foo', 1))]; $node = new GetAttrExpression($expr, $attr, $args, Template::ARRAY_CALL, 1); $tests[] = [$node, '(($__internal_%s = // line 1'."\n". @@ -53,7 +53,7 @@ public function getTests() $args->addElement(new NameExpression('foo', 1)); $args->addElement(new ConstantExpression('bar', 1)); $node = new GetAttrExpression($expr, $attr, $args, Template::METHOD_CALL, 1); - $tests[] = [$node, sprintf('%s%s, "bar", [%s, "bar"], "method", false, false, false, 1)', $this->getAttributeGetter(), $this->getVariableGetter('foo', 1), $this->getVariableGetter('foo'))]; + $tests[] = [$node, \sprintf('%s%s, "bar", [%s, "bar"], "method", false, false, false, 1)', $this->getAttributeGetter(), $this->getVariableGetter('foo', 1), $this->getVariableGetter('foo'))]; return $tests; } diff --git a/tests/NodeVisitor/OptimizerTest.php b/tests/NodeVisitor/OptimizerTest.php index b685f32248a..e6435ca0273 100644 --- a/tests/NodeVisitor/OptimizerTest.php +++ b/tests/NodeVisitor/OptimizerTest.php @@ -56,7 +56,7 @@ public function testForOptimizer($template, $expected) $stream = $env->parse($env->tokenize(new Source($template, 'index'))); foreach ($expected as $target => $withLoop) { - $this->assertTrue($this->checkForConfiguration($stream, $target, $withLoop), sprintf('variable %s is %soptimized', $target, $withLoop ? 'not ' : '')); + $this->assertTrue($this->checkForConfiguration($stream, $target, $withLoop), \sprintf('variable %s is %soptimized', $target, $withLoop ? 'not ' : '')); } } diff --git a/tests/Profiler/ProfileTest.php b/tests/Profiler/ProfileTest.php index a1f553cb756..5f41fc9c1df 100644 --- a/tests/Profiler/ProfileTest.php +++ b/tests/Profiler/ProfileTest.php @@ -77,7 +77,7 @@ public function testGetDuration() usleep(1); $profile->leave(); - $this->assertTrue($profile->getDuration() > 0, sprintf('Expected duration > 0, got: %f', $profile->getDuration())); + $this->assertTrue($profile->getDuration() > 0, \sprintf('Expected duration > 0, got: %f', $profile->getDuration())); } public function testSerialize() diff --git a/tests/TemplateTest.php b/tests/TemplateTest.php index 32d3babede2..9f9ca5a9ea7 100644 --- a/tests/TemplateTest.php +++ b/tests/TemplateTest.php @@ -57,7 +57,7 @@ public function testGetAttributeExceptions($template, $message) $template->render($context); $this->fail('Accessing an invalid attribute should throw an exception.'); } catch (RuntimeError $e) { - $this->assertSame(sprintf($message, 'index'), $e->getMessage()); + $this->assertSame(\sprintf($message, 'index'), $e->getMessage()); } } @@ -763,6 +763,6 @@ class TemplateMagicMethodExceptionObject { public function __call($method, $arguments) { - throw new \BadMethodCallException(sprintf('Unknown method "%s".', $method)); + throw new \BadMethodCallException(\sprintf('Unknown method "%s".', $method)); } } From b78dee86826bbfad176f507e8595ff7af34e5cb9 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Fri, 21 Jun 2024 08:22:31 +0200 Subject: [PATCH 264/812] Update PHPUnit config --- extra/cache-extra/phpunit.xml.dist | 46 +++++++++------------- extra/cssinliner-extra/phpunit.xml.dist | 46 +++++++++------------- extra/html-extra/phpunit.xml.dist | 46 +++++++++------------- extra/inky-extra/phpunit.xml.dist | 46 +++++++++------------- extra/intl-extra/phpunit.xml.dist | 48 +++++++++-------------- extra/markdown-extra/phpunit.xml.dist | 46 +++++++++------------- extra/string-extra/phpunit.xml.dist | 46 +++++++++------------- extra/twig-extra-bundle/phpunit.xml.dist | 50 ++++++++++-------------- 8 files changed, 147 insertions(+), 227 deletions(-) diff --git a/extra/cache-extra/phpunit.xml.dist b/extra/cache-extra/phpunit.xml.dist index fb5f43192e6..0cfdd72967c 100644 --- a/extra/cache-extra/phpunit.xml.dist +++ b/extra/cache-extra/phpunit.xml.dist @@ -1,30 +1,20 @@ - - - - - - - - - ./Tests/ - - - - - - ./ - - ./Tests - ./vendor - - - + + + + ./ + + + ./Tests + ./vendor + + + + + + + + ./Tests/ + + diff --git a/extra/cssinliner-extra/phpunit.xml.dist b/extra/cssinliner-extra/phpunit.xml.dist index 78ead58f5cd..de3fc99ea67 100644 --- a/extra/cssinliner-extra/phpunit.xml.dist +++ b/extra/cssinliner-extra/phpunit.xml.dist @@ -1,30 +1,20 @@ - - - - - - - - - ./Tests/ - - - - - - ./ - - ./Tests - ./vendor - - - + + + + ./ + + + ./Tests + ./vendor + + + + + + + + ./Tests/ + + diff --git a/extra/html-extra/phpunit.xml.dist b/extra/html-extra/phpunit.xml.dist index 83a13c728a4..1b088ff266e 100644 --- a/extra/html-extra/phpunit.xml.dist +++ b/extra/html-extra/phpunit.xml.dist @@ -1,30 +1,20 @@ - - - - - - - - - ./Tests/ - - - - - - ./ - - ./Tests - ./vendor - - - + + + + ./ + + + ./Tests + ./vendor + + + + + + + + ./Tests/ + + diff --git a/extra/inky-extra/phpunit.xml.dist b/extra/inky-extra/phpunit.xml.dist index 1a317bf68d0..7f759dd3d5c 100644 --- a/extra/inky-extra/phpunit.xml.dist +++ b/extra/inky-extra/phpunit.xml.dist @@ -1,30 +1,20 @@ - - - - - - - - - ./Tests/ - - - - - - ./ - - ./Tests - ./vendor - - - + + + + ./ + + + ./Tests + ./vendor + + + + + + + + ./Tests/ + + diff --git a/extra/intl-extra/phpunit.xml.dist b/extra/intl-extra/phpunit.xml.dist index be06f3797a5..d33987d1649 100644 --- a/extra/intl-extra/phpunit.xml.dist +++ b/extra/intl-extra/phpunit.xml.dist @@ -1,31 +1,21 @@ - - - - - - - - - - ./Tests/ - - - - - - ./ - - ./Tests - ./vendor - - - + + + + ./ + + + ./Tests + ./vendor + + + + + + + + + ./Tests/ + + diff --git a/extra/markdown-extra/phpunit.xml.dist b/extra/markdown-extra/phpunit.xml.dist index cc7b8577ddc..a40846ed435 100644 --- a/extra/markdown-extra/phpunit.xml.dist +++ b/extra/markdown-extra/phpunit.xml.dist @@ -1,30 +1,20 @@ - - - - - - - - - ./Tests/ - - - - - - ./ - - ./Tests - ./vendor - - - + + + + ./ + + + ./Tests + ./vendor + + + + + + + + ./Tests/ + + diff --git a/extra/string-extra/phpunit.xml.dist b/extra/string-extra/phpunit.xml.dist index 930cc0ab1f3..aa15cb642c7 100644 --- a/extra/string-extra/phpunit.xml.dist +++ b/extra/string-extra/phpunit.xml.dist @@ -1,30 +1,20 @@ - - - - - - - - - ./Tests/ - - - - - - ./ - - ./Tests - ./vendor - - - + + + + ./ + + + ./Tests + ./vendor + + + + + + + + ./Tests/ + + diff --git a/extra/twig-extra-bundle/phpunit.xml.dist b/extra/twig-extra-bundle/phpunit.xml.dist index 41534858a1f..c8d88d89c48 100644 --- a/extra/twig-extra-bundle/phpunit.xml.dist +++ b/extra/twig-extra-bundle/phpunit.xml.dist @@ -1,32 +1,22 @@ - - - - - - - - - - - ./Tests/ - - - - - - ./ - - ./Tests - ./vendor - - - + + + + ./ + + + ./Tests + ./vendor + + + + + + + + + + ./Tests/ + + From 5157402a779638b9bb804435ddd2faab08bb294a Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sun, 16 Jun 2024 17:59:12 +0200 Subject: [PATCH 265/812] Rename hash to mapping --- CHANGELOG | 2 ++ doc/coding_standards.rst | 6 ++--- doc/deprecated.rst | 6 +++++ doc/filters/merge.rst | 4 ++-- doc/filters/replace.rst | 2 +- doc/tags/with.rst | 6 ++--- doc/templates.rst | 12 +++++----- doc/tests/empty.rst | 2 +- src/ExpressionParser.php | 24 +++++++++++++------ src/Node/WithNode.php | 2 +- tests/ExpressionParserTest.php | 10 ++++---- ...ator.test => spread_mapping_operator.test} | 2 +- ...with_no_hash.test => with_no_mapping.test} | 6 ++--- 13 files changed, 51 insertions(+), 33 deletions(-) rename tests/Fixtures/expressions/{spread_hash_operator.test => spread_mapping_operator.test} (95%) rename tests/Fixtures/tags/with/{with_no_hash.test => with_no_mapping.test} (56%) diff --git a/CHANGELOG b/CHANGELOG index 73bf3ba61a2..54d3a847b33 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,8 @@ # 3.11.0 (2024-XX-XX) * Deprecate the second argument of `Twig\Node\Expression\CallExpression::compileArguments()` + * Deprecate `Twig\ExpressionParser\parseHashExpression()` in favor of + `Twig\ExpressionParser::parseMappingExpression()` # 3.10.3 (2024-05-16) diff --git a/doc/coding_standards.rst b/doc/coding_standards.rst index 310cc8c4338..f7bdb43c239 100644 --- a/doc/coding_standards.rst +++ b/doc/coding_standards.rst @@ -40,8 +40,8 @@ standards: {{ foo ~ bar }} {{ true ? true : false }} -* Put exactly one space after the ``:`` sign in hashes and ``,`` in - arrays and hashes: +* Put exactly one space after the ``:`` sign in mappings and ``,`` in arrays + and mappings: .. code-block:: twig @@ -81,7 +81,7 @@ standards: {{ range(1..10) }} * Do not put any spaces before and after the opening and the closing of arrays - and hashes: + and mappings: .. code-block:: twig diff --git a/doc/deprecated.rst b/doc/deprecated.rst index 4b4493af574..91b6d9329a7 100644 --- a/doc/deprecated.rst +++ b/doc/deprecated.rst @@ -45,6 +45,12 @@ Node Visitors * The ``Twig\NodeVisitor\AbstractNodeVisitor`` class is deprecated, implement the ``Twig\NodeVisitor\NodeVisitorInterface`` interface instead. +Parser +------ + +* The ``Twig\ExpressionParser::parseHashExpression()`` method is deprecated, use + ``Twig\ExpressionParser::parseMappingExpression()`` instead. + Templates --------- diff --git a/doc/filters/merge.rst b/doc/filters/merge.rst index b1d75c40b62..633dcf75fe8 100644 --- a/doc/filters/merge.rst +++ b/doc/filters/merge.rst @@ -13,7 +13,7 @@ The ``merge`` filter merges an array with another array: New values are added at the end of the existing ones. -The ``merge`` filter also works on hashes: +The ``merge`` filter also works on mappings: .. code-block:: twig @@ -23,7 +23,7 @@ The ``merge`` filter also works on hashes: {# items now contains {'apple': 'fruit', 'orange': 'fruit', 'peugeot': 'car', 'renault': 'car'} #} -For hashes, the merging process occurs on the keys: if the key does not +For mappings, the merging process occurs on the keys: if the key does not already exist, it is added but if the key already exists, its value is overridden. diff --git a/doc/filters/replace.rst b/doc/filters/replace.rst index 0c38b73bfe4..63e7f4800c5 100644 --- a/doc/filters/replace.rst +++ b/doc/filters/replace.rst @@ -17,7 +17,7 @@ format is free-form): Arguments --------- -* ``from``: The placeholder values as a hash +* ``from``: The placeholder values as a mapping .. seealso:: diff --git a/doc/tags/with.rst b/doc/tags/with.rst index 420c82ac6c1..268bb373dd4 100644 --- a/doc/tags/with.rst +++ b/doc/tags/with.rst @@ -13,8 +13,8 @@ scope are not visible outside of the scope: foo is not visible here any longer Instead of defining variables at the beginning of the scope, you can pass a -hash of variables you want to define in the ``with`` tag; the previous example -is equivalent to the following one: +mapping of variables you want to define in the ``with`` tag; the previous +example is equivalent to the following one: .. code-block:: twig @@ -23,7 +23,7 @@ is equivalent to the following one: {% endwith %} foo is not visible here any longer - {# it works with any expression that resolves to a hash #} + {# it works with any expression that resolves to a mapping #} {% set vars = {foo: 42} %} {% with vars %} ... diff --git a/doc/templates.rst b/doc/templates.rst index 1026dd8eac6..0acc1c8d993 100644 --- a/doc/templates.rst +++ b/doc/templates.rst @@ -534,7 +534,7 @@ exist: * ``["foo", "bar"]``: Arrays are defined by a sequence of expressions separated by a comma (``,``) and wrapped with squared brackets (``[]``). -* ``{"foo": "bar"}``: Hashes are defined by a list of keys and values +* ``{"foo": "bar"}``: Mappings are defined by a list of keys and values separated by a comma (``,``) and wrapped with curly braces (``{}``): .. code-block:: twig @@ -542,7 +542,7 @@ exist: {# keys as string #} {'foo': 'foo', 'bar': 'bar'} - {# keys as names (equivalent to the previous hash) #} + {# keys as names (equivalent to the previous mapping) #} {foo: 'foo', bar: 'bar'} {# keys as integer #} @@ -563,7 +563,7 @@ exist: * ``null``: ``null`` represents no specific value. This is the value returned when a variable does not exist. ``none`` is an alias for ``null``. -Arrays and hashes can be nested: +Arrays and mappings can be nested: .. code-block:: twig @@ -785,8 +785,8 @@ The following operators don't fit into any of the other categories: {# returns the value of foo if it is defined and not null, 'no' otherwise #} {{ foo ?? 'no' }} -* ``...``: The spread operator can be used to expand arrays or hashes (it cannot - be used to expand the arguments of a function call): +* ``...``: The spread operator can be used to expand arrays or mappings (it + cannot be used to expand the arguments of a function call): .. code-block:: twig @@ -827,7 +827,7 @@ Operator Score of precedence Description ``**`` 200 Raises a number to the power of another ``??`` 300 Default value when a variable is null ``+``, ``-`` 500 Unary operations on numbers -``|``,``[]``,``.`` - Filters, array, hash, and attribute access +``|``,``[]``,``.`` - Filters, array, mapping, and attribute access ============================= =================================== ===================================================== Without using any parentheses, the operator precedence rules are used to diff --git a/doc/tests/empty.rst b/doc/tests/empty.rst index 0233eca48f5..6348f92aea0 100644 --- a/doc/tests/empty.rst +++ b/doc/tests/empty.rst @@ -2,7 +2,7 @@ ========= ``empty`` checks if a variable is an empty string, an empty array, an empty -hash, exactly ``false``, or exactly ``null``. +mapping, exactly ``false``, or exactly ``null``. For objects that implement the ``Countable`` interface, ``empty`` will check the return value of the ``count()`` method. diff --git a/src/ExpressionParser.php b/src/ExpressionParser.php index 4be64934171..443c7daa195 100644 --- a/src/ExpressionParser.php +++ b/src/ExpressionParser.php @@ -280,7 +280,7 @@ public function parsePrimaryExpression() if ($token->test(/* Token::PUNCTUATION_TYPE */ 9, '[')) { $node = $this->parseArrayExpression(); } elseif ($token->test(/* Token::PUNCTUATION_TYPE */ 9, '{')) { - $node = $this->parseHashExpression(); + $node = $this->parseMappingExpression(); } elseif ($token->test(/* Token::OPERATOR_TYPE */ 8, '=') && ('==' === $this->parser->getStream()->look(-1)->getValue() || '!=' === $this->parser->getStream()->look(-1)->getValue())) { throw new SyntaxError(\sprintf('Unexpected operator of value "%s". Did you try to use "===" or "!==" for strict comparison? Use "is same as(value)" instead.', $token->getValue()), $token->getLine(), $this->parser->getStream()->getSourceContext()); } else { @@ -351,16 +351,26 @@ public function parseArrayExpression() return $node; } + /** + * @deprecated since 3.11, use parseMappingExpression() instead + */ public function parseHashExpression() + { + trigger_deprecation('twig/twig', '3.11', 'Calling "%s()" is deprecated, use "parseMappingExpression()" instead.', __METHOD__); + + return $this->parseMappingExpression(); + } + + public function parseMappingExpression() { $stream = $this->parser->getStream(); - $stream->expect(/* Token::PUNCTUATION_TYPE */ 9, '{', 'A hash element was expected'); + $stream->expect(/* Token::PUNCTUATION_TYPE */ 9, '{', 'A mapping element was expected'); $node = new ArrayExpression([], $stream->getCurrent()->getLine()); $first = true; while (!$stream->test(/* Token::PUNCTUATION_TYPE */ 9, '}')) { if (!$first) { - $stream->expect(/* Token::PUNCTUATION_TYPE */ 9, ',', 'A hash value must be followed by a comma'); + $stream->expect(/* Token::PUNCTUATION_TYPE */ 9, ',', 'A mapping value must be followed by a comma'); // trailing ,? if ($stream->test(/* Token::PUNCTUATION_TYPE */ 9, '}')) { @@ -377,7 +387,7 @@ public function parseHashExpression() continue; } - // a hash key can be: + // a mapping key can be: // // * a number -- 12 // * a string -- 'a' @@ -399,15 +409,15 @@ public function parseHashExpression() } else { $current = $stream->getCurrent(); - throw new SyntaxError(\sprintf('A hash key must be a quoted string, a number, a name, or an expression enclosed in parentheses (unexpected token "%s" of value "%s".', Token::typeToEnglish($current->getType()), $current->getValue()), $current->getLine(), $stream->getSourceContext()); + throw new SyntaxError(\sprintf('A mapping key must be a quoted string, a number, a name, or an expression enclosed in parentheses (unexpected token "%s" of value "%s".', Token::typeToEnglish($current->getType()), $current->getValue()), $current->getLine(), $stream->getSourceContext()); } - $stream->expect(/* Token::PUNCTUATION_TYPE */ 9, ':', 'A hash key must be followed by a colon (:)'); + $stream->expect(/* Token::PUNCTUATION_TYPE */ 9, ':', 'A mapping key must be followed by a colon (:)'); $value = $this->parseExpression(); $node->addElement($value, $key); } - $stream->expect(/* Token::PUNCTUATION_TYPE */ 9, '}', 'An opened hash is not properly closed'); + $stream->expect(/* Token::PUNCTUATION_TYPE */ 9, '}', 'An opened mapping is not properly closed'); return $node; } diff --git a/src/Node/WithNode.php b/src/Node/WithNode.php index c78136c81a0..a7b7e70d9dd 100644 --- a/src/Node/WithNode.php +++ b/src/Node/WithNode.php @@ -49,7 +49,7 @@ public function compile(Compiler $compiler): void ->raw(";\n") ->write(\sprintf("if (!is_iterable(\$%s)) {\n", $varsName)) ->indent() - ->write("throw new RuntimeError('Variables passed to the \"with\" tag must be a hash.', ") + ->write("throw new RuntimeError('Variables passed to the \"with\" tag must be a mapping.', ") ->repr($node->getTemplateLine()) ->raw(", \$this->getSourceContext());\n") ->outdent() diff --git a/tests/ExpressionParserTest.php b/tests/ExpressionParserTest.php index bb000225ae5..e00c2598dbb 100644 --- a/tests/ExpressionParserTest.php +++ b/tests/ExpressionParserTest.php @@ -111,7 +111,7 @@ public function getTestsForArray() ], 1), ], - // simple hash + // simple mapping ['{{ {"a": "b", "b": "c"} }}', new ArrayExpression([ new ConstantExpression('a', 1), new ConstantExpression('b', 1), @@ -121,7 +121,7 @@ public function getTestsForArray() ], 1), ], - // hash with trailing , + // mapping with trailing , ['{{ {"a": "b", "b": "c", } }}', new ArrayExpression([ new ConstantExpression('a', 1), new ConstantExpression('b', 1), @@ -131,7 +131,7 @@ public function getTestsForArray() ], 1), ], - // hash in an array + // mapping in an array ['{{ [1, {"a": "b", "b": "c"}] }}', new ArrayExpression([ new ConstantExpression(0, 1), new ConstantExpression(1, 1), @@ -147,7 +147,7 @@ public function getTestsForArray() ], 1), ], - // array in a hash + // array in a mapping ['{{ {"a": [1, 2], "b": "c"} }}', new ArrayExpression([ new ConstantExpression('a', 1), new ArrayExpression([ @@ -181,7 +181,7 @@ public function getTestsForArray() $this->createNameExpression('foo', ['spread' => true]), ], 1)], - // hash with spread operator + // mapping with spread operator ['{{ {"a": "b", "b": "c", ...otherLetters} }}', new ArrayExpression([ new ConstantExpression('a', 1), diff --git a/tests/Fixtures/expressions/spread_hash_operator.test b/tests/Fixtures/expressions/spread_mapping_operator.test similarity index 95% rename from tests/Fixtures/expressions/spread_hash_operator.test rename to tests/Fixtures/expressions/spread_mapping_operator.test index c2429f00e97..e944eee8ac7 100644 --- a/tests/Fixtures/expressions/spread_hash_operator.test +++ b/tests/Fixtures/expressions/spread_mapping_operator.test @@ -1,5 +1,5 @@ --TEST-- -Twig supports the spread operator on hashes +Twig supports the spread operator on mappings --TEMPLATE-- {% for key, value in { firstName: 'Ryan', lastName: 'Weaver', favoriteFood: 'popcorn', ...{favoriteFood: 'pizza', sport: 'running'} } %} {{ key }}: {{ value }} diff --git a/tests/Fixtures/tags/with/with_no_hash.test b/tests/Fixtures/tags/with/with_no_mapping.test similarity index 56% rename from tests/Fixtures/tags/with/with_no_hash.test rename to tests/Fixtures/tags/with/with_no_mapping.test index 7083050b42e..7b22c5171a9 100644 --- a/tests/Fixtures/tags/with/with_no_hash.test +++ b/tests/Fixtures/tags/with/with_no_mapping.test @@ -1,10 +1,10 @@ --TEST-- -"with" tag with an expression that is not a hash +"with" tag with an expression that is not a mapping --TEMPLATE-- {% with vars %} {{ foo }}{{ bar }} {% endwith %} --DATA-- -return ['vars' => 'no-hash'] +return ['vars' => 'no-mapping'] --EXCEPTION-- -Twig\Error\RuntimeError: Variables passed to the "with" tag must be a hash in "index.twig" at line 2. +Twig\Error\RuntimeError: Variables passed to the "with" tag must be a mapping in "index.twig" at line 2. From c44be99a848e3cccac4524a7fc754af177acd620 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sun, 16 Jun 2024 19:50:38 +0200 Subject: [PATCH 266/812] Rename array to sequence --- CHANGELOG | 3 ++ doc/coding_standards.rst | 6 ++-- doc/deprecated.rst | 3 ++ doc/filters/data_uri.rst | 2 +- doc/filters/keys.rst | 14 ++++++--- doc/filters/merge.rst | 6 ++-- doc/filters/sort.rst | 4 +-- doc/filters/split.rst | 2 +- doc/filters/url_encode.rst | 4 +-- doc/functions/cycle.rst | 2 +- doc/tags/for.rst | 8 ++--- doc/tags/if.rst | 8 +++-- doc/templates.rst | 16 +++++----- doc/tests/empty.rst | 2 +- src/ExpressionParser.php | 17 +++++++---- src/Extension/CoreExtension.php | 24 +++++++-------- tests/ExpressionParserTest.php | 24 +++++++-------- .../exception_in_extension_extends.test | 2 +- .../exception_in_extension_include.test | 2 +- .../Fixtures/filters/replace_invalid_arg.test | 2 +- tests/Fixtures/functions/cycle_empty.test | 8 ----- tests/TemplateTest.php | 30 +++++++++---------- 22 files changed, 102 insertions(+), 87 deletions(-) delete mode 100644 tests/Fixtures/functions/cycle_empty.test diff --git a/CHANGELOG b/CHANGELOG index 54d3a847b33..01e4acaba6b 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -4,6 +4,9 @@ * Deprecate `Twig\ExpressionParser\parseHashExpression()` in favor of `Twig\ExpressionParser::parseMappingExpression()` + * Deprecate `Twig\ExpressionParser\parseArrayExpression()`` in favor of + `Twig\ExpressionParser::parseSequenceExpression()` + # 3.10.3 (2024-05-16) * Fix missing ; in generated code diff --git a/doc/coding_standards.rst b/doc/coding_standards.rst index f7bdb43c239..d10906fa643 100644 --- a/doc/coding_standards.rst +++ b/doc/coding_standards.rst @@ -40,7 +40,7 @@ standards: {{ foo ~ bar }} {{ true ? true : false }} -* Put exactly one space after the ``:`` sign in mappings and ``,`` in arrays +* Put exactly one space after the ``:`` sign in mappings and ``,`` in sequences and mappings: .. code-block:: twig @@ -80,8 +80,8 @@ standards: {{ foo|default('foo') }} {{ range(1..10) }} -* Do not put any spaces before and after the opening and the closing of arrays - and mappings: +* Do not put any spaces before and after the opening and the closing of + sequences and mappings: .. code-block:: twig diff --git a/doc/deprecated.rst b/doc/deprecated.rst index 91b6d9329a7..c994dc7fa2f 100644 --- a/doc/deprecated.rst +++ b/doc/deprecated.rst @@ -51,6 +51,9 @@ Parser * The ``Twig\ExpressionParser::parseHashExpression()`` method is deprecated, use ``Twig\ExpressionParser::parseMappingExpression()`` instead. +* The ``Twig\ExpressionParser::parseArrayExpression()`` method is deprecated, use + ``Twig\ExpressionParser::parseSequenceExpression()`` instead. + Templates --------- diff --git a/doc/filters/data_uri.rst b/doc/filters/data_uri.rst index e008266b346..131a71bb8b9 100644 --- a/doc/filters/data_uri.rst +++ b/doc/filters/data_uri.rst @@ -50,6 +50,6 @@ Arguments --------- * ``mime``: The mime type -* ``parameters``: An array of parameters +* ``parameters``: A mapping of parameters .. _RFC 2397: https://tools.ietf.org/html/rfc2397 diff --git a/doc/filters/keys.rst b/doc/filters/keys.rst index 6bed8291e15..26c61bcd1e4 100644 --- a/doc/filters/keys.rst +++ b/doc/filters/keys.rst @@ -1,14 +1,20 @@ ``keys`` ======== -The ``keys`` filter returns the keys of an array. It is useful when you want to -iterate over the keys of an array: +The ``keys`` filter returns the keys of a sequence or a mapping. It is useful +when you want to iterate over the keys of a sequence or a mapping: .. code-block:: twig - {% for key in array|keys %} - ... + {% for key in [1, 2, 3, 4]|keys %} + {{ key }} {% endfor %} + {# outputs: 1 2 3 4 #} + + {% for key in {a: 'a_value', b: 'b_value'}|keys %} + {{ key }} + {% endfor %} + {# outputs: a b #} .. note:: diff --git a/doc/filters/merge.rst b/doc/filters/merge.rst index 633dcf75fe8..fc21884ba7a 100644 --- a/doc/filters/merge.rst +++ b/doc/filters/merge.rst @@ -1,7 +1,9 @@ ``merge`` ========= -The ``merge`` filter merges an array with another array: +The ``merge`` filter merges sequences and mappings. + +The ``merge`` filter also works on sequences: .. code-block:: twig @@ -29,7 +31,7 @@ overridden. .. tip:: - If you want to ensure that some values are defined in an array (by given + If you want to ensure that some values are defined in a mapping (by given default values), reverse the two elements in the call: .. code-block:: twig diff --git a/doc/filters/sort.rst b/doc/filters/sort.rst index 98b7afe376d..4afd60e7d8a 100644 --- a/doc/filters/sort.rst +++ b/doc/filters/sort.rst @@ -1,7 +1,7 @@ ``sort`` ======== -The ``sort`` filter sorts an array: +The ``sort`` filter sorts sequences and mappings: .. code-block:: twig @@ -15,7 +15,7 @@ The ``sort`` filter sorts an array: association. It supports Traversable objects by transforming those to arrays. -You can pass an arrow function to sort the array: +You can pass an arrow function to configure the sorting: .. code-block:: html+twig diff --git a/doc/filters/split.rst b/doc/filters/split.rst index 386ae3043d0..f6728ddad3b 100644 --- a/doc/filters/split.rst +++ b/doc/filters/split.rst @@ -11,7 +11,7 @@ of strings: You can also pass a ``limit`` argument: -* If ``limit`` is positive, the returned array will contain a maximum of +* If ``limit`` is positive, the returned sequence will contain a maximum of limit elements with the last element containing the rest of string; * If ``limit`` is negative, all components except the last -limit are diff --git a/doc/filters/url_encode.rst b/doc/filters/url_encode.rst index c5919be016b..f7898313497 100644 --- a/doc/filters/url_encode.rst +++ b/doc/filters/url_encode.rst @@ -1,8 +1,8 @@ ``url_encode`` ============== -The ``url_encode`` filter percent encodes a given string as URL segment -or an array as query string: +The ``url_encode`` filter percent encodes a given string as URL segment or a +mapping as query string: .. code-block:: twig diff --git a/doc/functions/cycle.rst b/doc/functions/cycle.rst index 46f8f21cab9..3b6db61c431 100644 --- a/doc/functions/cycle.rst +++ b/doc/functions/cycle.rst @@ -1,7 +1,7 @@ ``cycle`` ========= -The ``cycle`` function cycles on an array of values: +The ``cycle`` function cycles on a sequence or mapping: .. code-block:: twig diff --git a/doc/tags/for.rst b/doc/tags/for.rst index 4517f590724..3bd859bd05c 100644 --- a/doc/tags/for.rst +++ b/doc/tags/for.rst @@ -1,8 +1,8 @@ ``for`` ======= -Loop over each item in a sequence. For example, to display a list of users -provided in a variable called ``users``: +Loop over each item in a sequence or a mapping. For example, to display a list +of users provided in a variable called ``users``: .. code-block:: html+twig @@ -15,8 +15,8 @@ provided in a variable called ``users``: .. note:: - A sequence can be either an array or an object implementing the - ``Traversable`` interface. + A sequence or a mapping can be either an array or an object implementing + the ``Traversable`` interface. If you do need to iterate over a sequence of numbers, you can use the ``..`` operator: diff --git a/doc/tags/if.rst b/doc/tags/if.rst index 0523cb1ac19..8a29af5da11 100644 --- a/doc/tags/if.rst +++ b/doc/tags/if.rst @@ -12,7 +12,7 @@ In the simplest form you can use it to test if an expression evaluates to

Our website is in maintenance mode. Please, come back later.

{% endif %} -You can also test if an array is not empty: +You can also test if a sequence or a mapping is not empty: .. code-block:: html+twig @@ -71,8 +71,10 @@ use more complex ``expressions`` there too: INF (Infinity) true whitespace-only string true string "0" or '0' false - empty array false + empty sequence false + empty mapping false null false - non-empty array true + non-empty sequence true + non-empty mapping true object true ====================== ==================== diff --git a/doc/templates.rst b/doc/templates.rst index 0acc1c8d993..4c1b847b9a2 100644 --- a/doc/templates.rst +++ b/doc/templates.rst @@ -102,7 +102,7 @@ If a variable or attribute does not exist, the behavior depends on the For convenience's sake ``foo.bar`` does the following things on the PHP layer: - * check if ``foo`` is an array and ``bar`` a valid element; + * check if ``foo`` is a sequence or a mapping and ``bar`` a valid element; * if not, and if ``foo`` is an object, check that ``bar`` is a valid property; * if not, and if ``foo`` is an object, check that ``bar`` is a valid method (even if ``bar`` is the constructor - use ``__construct()`` instead); @@ -115,7 +115,7 @@ If a variable or attribute does not exist, the behavior depends on the Twig also supports a specific syntax for accessing items on PHP arrays, ``foo['bar']``: - * check if ``foo`` is an array and ``bar`` a valid element; + * check if ``foo`` is a sequence or a mapping and ``bar`` a valid element; * if not, and if ``strict_variables`` is ``false``, return ``null``; * if not, throw an exception. @@ -531,7 +531,7 @@ exist: writing the number down. If a dot is present the number is a float, otherwise an integer. -* ``["foo", "bar"]``: Arrays are defined by a sequence of expressions +* ``["foo", "bar"]``: Sequences are defined by a sequence of expressions separated by a comma (``,``) and wrapped with squared brackets (``[]``). * ``{"foo": "bar"}``: Mappings are defined by a list of keys and values @@ -563,7 +563,7 @@ exist: * ``null``: ``null`` represents no specific value. This is the value returned when a variable does not exist. ``none`` is an alias for ``null``. -Arrays and mappings can be nested: +Sequences and mappings can be nested: .. code-block:: twig @@ -698,8 +698,8 @@ operand is contained in the right: .. tip:: - You can use this filter to perform a containment test on strings, arrays, - or objects implementing the ``Traversable`` interface. + You can use this filter to perform a containment test on strings, + sequences, mappings, or objects implementing the ``Traversable`` interface. To perform a negative test, use the ``not in`` operator: @@ -785,7 +785,7 @@ The following operators don't fit into any of the other categories: {# returns the value of foo if it is defined and not null, 'no' otherwise #} {{ foo ?? 'no' }} -* ``...``: The spread operator can be used to expand arrays or mappings (it +* ``...``: The spread operator can be used to expand sequences or mappings (it cannot be used to expand the arguments of a function call): .. code-block:: twig @@ -827,7 +827,7 @@ Operator Score of precedence Description ``**`` 200 Raises a number to the power of another ``??`` 300 Default value when a variable is null ``+``, ``-`` 500 Unary operations on numbers -``|``,``[]``,``.`` - Filters, array, mapping, and attribute access +``|``,``[]``,``.`` - Filters, sequence, mapping, and attribute access ============================= =================================== ===================================================== Without using any parentheses, the operator precedence rules are used to diff --git a/doc/tests/empty.rst b/doc/tests/empty.rst index 6348f92aea0..3abdb8bbd86 100644 --- a/doc/tests/empty.rst +++ b/doc/tests/empty.rst @@ -1,7 +1,7 @@ ``empty`` ========= -``empty`` checks if a variable is an empty string, an empty array, an empty +``empty`` checks if a variable is an empty string, an empty sequence, an empty mapping, exactly ``false``, or exactly ``null``. For objects that implement the ``Countable`` interface, ``empty`` will check the diff --git a/src/ExpressionParser.php b/src/ExpressionParser.php index 443c7daa195..3a97370ed79 100644 --- a/src/ExpressionParser.php +++ b/src/ExpressionParser.php @@ -278,7 +278,7 @@ public function parsePrimaryExpression() // no break default: if ($token->test(/* Token::PUNCTUATION_TYPE */ 9, '[')) { - $node = $this->parseArrayExpression(); + $node = $this->parseSequenceExpression(); } elseif ($token->test(/* Token::PUNCTUATION_TYPE */ 9, '{')) { $node = $this->parseMappingExpression(); } elseif ($token->test(/* Token::OPERATOR_TYPE */ 8, '=') && ('==' === $this->parser->getStream()->look(-1)->getValue() || '!=' === $this->parser->getStream()->look(-1)->getValue())) { @@ -320,15 +320,22 @@ public function parseStringExpression() } public function parseArrayExpression() + { + trigger_deprecation('twig/twig', '3.11', 'Calling "%s()" is deprecated, use "parseSequenceExpression()" instead.', __METHOD__); + + return $this->parseSequenceExpression(); + } + + public function parseSequenceExpression() { $stream = $this->parser->getStream(); - $stream->expect(/* Token::PUNCTUATION_TYPE */ 9, '[', 'An array element was expected'); + $stream->expect(/* Token::PUNCTUATION_TYPE */ 9, '[', 'A sequence element was expected'); $node = new ArrayExpression([], $stream->getCurrent()->getLine()); $first = true; while (!$stream->test(/* Token::PUNCTUATION_TYPE */ 9, ']')) { if (!$first) { - $stream->expect(/* Token::PUNCTUATION_TYPE */ 9, ',', 'An array element must be followed by a comma'); + $stream->expect(/* Token::PUNCTUATION_TYPE */ 9, ',', 'A sequence element must be followed by a comma'); // trailing ,? if ($stream->test(/* Token::PUNCTUATION_TYPE */ 9, ']')) { @@ -346,7 +353,7 @@ public function parseArrayExpression() $node->addElement($this->parseExpression()); } } - $stream->expect(/* Token::PUNCTUATION_TYPE */ 9, ']', 'An opened array is not properly closed'); + $stream->expect(/* Token::PUNCTUATION_TYPE */ 9, ']', 'An opened sequence is not properly closed'); return $node; } @@ -641,7 +648,7 @@ public function parseArguments($namedArguments = false, $definition = false, $al $value = $this->parsePrimaryExpression(); if (!$this->checkConstantExpression($value)) { - throw new SyntaxError('A default value for an argument must be a constant (a boolean, a string, a number, or an array).', $token->getLine(), $stream->getSourceContext()); + throw new SyntaxError('A default value for an argument must be a constant (a boolean, a string, a number, a sequence, or a mapping).', $token->getLine(), $stream->getSourceContext()); } } else { $value = $this->parseExpression(0, $allowArrow); diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index 405a35353f2..a7bdc8204f1 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -329,7 +329,7 @@ public static function cycle($values, $position): string } if (!\count($values)) { - throw new RuntimeError('The "cycle" function does not work on empty arrays.'); + throw new RuntimeError('The "cycle" function does not work on empty sequences/mappings.'); } return $values[$position % \count($values)]; @@ -399,7 +399,7 @@ public static function random(string $charset, $values = null, $max = null) $values = self::toArray($values); if (0 === \count($values)) { - throw new RuntimeError('The random function cannot pick from an empty array.'); + throw new RuntimeError('The random function cannot pick from an empty sequence/mapping.'); } return $values[array_rand($values, 1)]; @@ -536,7 +536,7 @@ public function convertDate($date = null, $timezone = null) public static function replace($str, $from): string { if (!is_iterable($from)) { - throw new RuntimeError(\sprintf('The "replace" filter expects an array or "Traversable" as replace values, got "%s".', \is_object($from) ? \get_class($from) : \gettype($from))); + throw new RuntimeError(\sprintf('The "replace" filter expects a sequence/mapping or "Traversable" as replace values, got "%s".', \is_object($from) ? \get_class($from) : \gettype($from))); } return strtr($str ?? '', self::toArray($from)); @@ -633,7 +633,7 @@ public static function merge(...$arrays): array foreach ($arrays as $argNumber => $array) { if (!is_iterable($array)) { - throw new RuntimeError(\sprintf('The merge filter only works with arrays or "Traversable", got "%s" for argument %d.', \gettype($array), $argNumber + 1)); + throw new RuntimeError(\sprintf('The merge filter only works with sequences/mappings or "Traversable", got "%s" for argument %d.', \gettype($array), $argNumber + 1)); } $result = array_merge($result, self::toArray($array)); @@ -907,7 +907,7 @@ public static function sort(Environment $env, $array, $arrow = null): array if ($array instanceof \Traversable) { $array = iterator_to_array($array); } elseif (!\is_array($array)) { - throw new RuntimeError(\sprintf('The sort filter only works with arrays or "Traversable", got "%s".', \gettype($array))); + throw new RuntimeError(\sprintf('The sort filter only works with sequences/mappings or "Traversable", got "%s".', \gettype($array))); } if (null !== $arrow) { @@ -1424,7 +1424,7 @@ public static function constantIsDefined($constant, $object = null): bool public static function batch($items, $size, $fill = null, $preserveKeys = true): array { if (!is_iterable($items)) { - throw new RuntimeError(\sprintf('The "batch" filter expects an array or "Traversable", got "%s".', \is_object($items) ? \get_class($items) : \gettype($items))); + throw new RuntimeError(\sprintf('The "batch" filter expects a sequence/mapping or "Traversable", got "%s".', \is_object($items) ? \get_class($items) : \gettype($items))); } $size = ceil($size); @@ -1491,9 +1491,9 @@ public static function getAttribute(Environment $env, Source $source, $object, $ $message = \sprintf('Impossible to access a key "%s" on an object of class "%s" that does not implement ArrayAccess interface.', $item, \get_class($object)); } elseif (\is_array($object)) { if (empty($object)) { - $message = \sprintf('Key "%s" does not exist as the array is empty.', $arrayItem); + $message = \sprintf('Key "%s" does not exist as the sequence/mapping is empty.', $arrayItem); } else { - $message = \sprintf('Key "%s" for array with keys "%s" does not exist.', $arrayItem, implode(', ', array_keys($object))); + $message = \sprintf('Key "%s" for sequence/mapping with keys "%s" does not exist.', $arrayItem, implode(', ', array_keys($object))); } } elseif (/* Template::ARRAY_CALL */ 'array' === $type) { if (null === $object) { @@ -1523,7 +1523,7 @@ public static function getAttribute(Environment $env, Source $source, $object, $ if (null === $object) { $message = \sprintf('Impossible to invoke a method ("%s") on a null variable.', $item); } elseif (\is_array($object)) { - $message = \sprintf('Impossible to invoke a method ("%s") on an array.', $item); + $message = \sprintf('Impossible to invoke a method ("%s") on a sequence/mapping.', $item); } else { $message = \sprintf('Impossible to invoke a method ("%s") on a %s variable ("%s").', $item, \gettype($object), $object); } @@ -1661,7 +1661,7 @@ public static function column($array, $name, $index = null): array if ($array instanceof \Traversable) { $array = iterator_to_array($array); } elseif (!\is_array($array)) { - throw new RuntimeError(\sprintf('The column filter only works with arrays or "Traversable", got "%s" as first argument.', \gettype($array))); + throw new RuntimeError(\sprintf('The column filter only works with sequences/mappings or "Traversable", got "%s" as first argument.', \gettype($array))); } return array_column($array, $name, $index); @@ -1673,7 +1673,7 @@ public static function column($array, $name, $index = null): array public static function filter(Environment $env, $array, $arrow) { if (!is_iterable($array)) { - throw new RuntimeError(\sprintf('The "filter" filter expects an array or "Traversable", got "%s".', \is_object($array) ? \get_class($array) : \gettype($array))); + throw new RuntimeError(\sprintf('The "filter" filter expects a sequence/mapping or "Traversable", got "%s".', \is_object($array) ? \get_class($array) : \gettype($array))); } self::checkArrowInSandbox($env, $arrow, 'filter', 'filter'); @@ -1709,7 +1709,7 @@ public static function reduce(Environment $env, $array, $arrow, $initial = null) self::checkArrowInSandbox($env, $arrow, 'reduce', 'filter'); if (!\is_array($array) && !$array instanceof \Traversable) { - throw new RuntimeError(\sprintf('The "reduce" filter only works with arrays or "Traversable", got "%s" as first argument.', \gettype($array))); + throw new RuntimeError(\sprintf('The "reduce" filter only works with sequences/mappings or "Traversable", got "%s" as first argument.', \gettype($array))); } $accumulator = $initial; diff --git a/tests/ExpressionParserTest.php b/tests/ExpressionParserTest.php index e00c2598dbb..abf5ae3a872 100644 --- a/tests/ExpressionParserTest.php +++ b/tests/ExpressionParserTest.php @@ -55,9 +55,9 @@ public function getFailingTestsForAssignment() } /** - * @dataProvider getTestsForArray + * @dataProvider getTestsForSequence */ - public function testArrayExpression($template, $expected) + public function testSequenceExpression($template, $expected) { $env = new Environment($this->createMock(LoaderInterface::class), ['cache' => false, 'autoescape' => false]); $stream = $env->tokenize($source = new Source($template, '')); @@ -68,9 +68,9 @@ public function testArrayExpression($template, $expected) } /** - * @dataProvider getFailingTestsForArray + * @dataProvider getFailingTestsForSequence */ - public function testArraySyntaxError($template) + public function testSequenceSyntaxError($template) { $this->expectException(SyntaxError::class); @@ -79,7 +79,7 @@ public function testArraySyntaxError($template) $parser->parse($env->tokenize(new Source($template, 'index'))); } - public function getFailingTestsForArray() + public function getFailingTestsForSequence() { return [ ['{{ [1, "a": "b"] }}'], @@ -88,10 +88,10 @@ public function getFailingTestsForArray() ]; } - public function getTestsForArray() + public function getTestsForSequence() { return [ - // simple array + // simple sequence ['{{ [1, 2] }}', new ArrayExpression([ new ConstantExpression(0, 1), new ConstantExpression(1, 1), @@ -101,7 +101,7 @@ public function getTestsForArray() ], 1), ], - // array with trailing , + // sequence with trailing , ['{{ [1, 2, ] }}', new ArrayExpression([ new ConstantExpression(0, 1), new ConstantExpression(1, 1), @@ -131,7 +131,7 @@ public function getTestsForArray() ], 1), ], - // mapping in an array + // mapping in a sequence ['{{ [1, {"a": "b", "b": "c"}] }}', new ArrayExpression([ new ConstantExpression(0, 1), new ConstantExpression(1, 1), @@ -147,7 +147,7 @@ public function getTestsForArray() ], 1), ], - // array in a mapping + // sequence in a mapping ['{{ {"a": [1, 2], "b": "c"} }}', new ArrayExpression([ new ConstantExpression('a', 1), new ArrayExpression([ @@ -168,7 +168,7 @@ public function getTestsForArray() new NameExpression('b', 1), ], 1)], - // array with spread operator + // sequence with spread operator ['{{ [1, 2, ...foo] }}', new ArrayExpression([ new ConstantExpression(0, 1), @@ -301,7 +301,7 @@ public function testMacroDefinitionDoesNotSupportNonNameVariableName() public function testMacroDefinitionDoesNotSupportNonConstantDefaultValues($template) { $this->expectException(SyntaxError::class); - $this->expectExceptionMessage('A default value for an argument must be a constant (a boolean, a string, a number, or an array) in "index" at line 1'); + $this->expectExceptionMessage('A default value for an argument must be a constant (a boolean, a string, a number, a sequence, or a mapping) in "index" at line 1'); $env = new Environment($this->createMock(LoaderInterface::class), ['cache' => false, 'autoescape' => false]); $parser = new Parser($env); diff --git a/tests/Fixtures/exceptions/exception_in_extension_extends.test b/tests/Fixtures/exceptions/exception_in_extension_extends.test index 2ab298059d3..fee521878d1 100644 --- a/tests/Fixtures/exceptions/exception_in_extension_extends.test +++ b/tests/Fixtures/exceptions/exception_in_extension_extends.test @@ -9,4 +9,4 @@ Exception thrown from a child for an extension error --DATA-- return [] --EXCEPTION-- -Twig\Error\RuntimeError: The random function cannot pick from an empty array in "base.twig" at line 4. +Twig\Error\RuntimeError: The random function cannot pick from an empty sequence/mapping in "base.twig" at line 4. diff --git a/tests/Fixtures/exceptions/exception_in_extension_include.test b/tests/Fixtures/exceptions/exception_in_extension_include.test index e2281b2903b..ab09cabb759 100644 --- a/tests/Fixtures/exceptions/exception_in_extension_include.test +++ b/tests/Fixtures/exceptions/exception_in_extension_include.test @@ -9,4 +9,4 @@ Exception thrown from an include for an extension error --DATA-- return [] --EXCEPTION-- -Twig\Error\RuntimeError: The random function cannot pick from an empty array in "content.twig" at line 4. +Twig\Error\RuntimeError: The random function cannot pick from an empty sequence/mapping in "content.twig" at line 4. diff --git a/tests/Fixtures/filters/replace_invalid_arg.test b/tests/Fixtures/filters/replace_invalid_arg.test index ba6fea4125a..ea163250093 100644 --- a/tests/Fixtures/filters/replace_invalid_arg.test +++ b/tests/Fixtures/filters/replace_invalid_arg.test @@ -5,4 +5,4 @@ Exception for invalid argument type in replace call --DATA-- return ['stdClass' => new \stdClass()] --EXCEPTION-- -Twig\Error\RuntimeError: The "replace" filter expects an array or "Traversable" as replace values, got "stdClass" in "index.twig" at line 2. +Twig\Error\RuntimeError: The "replace" filter expects a sequence/mapping or "Traversable" as replace values, got "stdClass" in "index.twig" at line 2. diff --git a/tests/Fixtures/functions/cycle_empty.test b/tests/Fixtures/functions/cycle_empty.test deleted file mode 100644 index bb338452a6b..00000000000 --- a/tests/Fixtures/functions/cycle_empty.test +++ /dev/null @@ -1,8 +0,0 @@ ---TEST-- -"cycle" function returns an error on empty arrays ---TEMPLATE-- -{{ cycle([], 0) }} ---DATA-- -return [] ---EXCEPTION-- -Twig\Error\RuntimeError: The "cycle" function does not work on empty arrays in "index.twig" at line 2. diff --git a/tests/TemplateTest.php b/tests/TemplateTest.php index 9f9ca5a9ea7..1256b5df547 100644 --- a/tests/TemplateTest.php +++ b/tests/TemplateTest.php @@ -66,17 +66,17 @@ public function getAttributeExceptions() return [ ['{{ string["a"] }}', 'Impossible to access a key ("a") on a string variable ("foo") in "%s" at line 1.'], ['{{ null["a"] }}', 'Impossible to access a key ("a") on a null variable in "%s" at line 1.'], - ['{{ empty_array["a"] }}', 'Key "a" does not exist as the array is empty in "%s" at line 1.'], - ['{{ array["a"] }}', 'Key "a" for array with keys "foo" does not exist in "%s" at line 1.'], + ['{{ empty_array["a"] }}', 'Key "a" does not exist as the sequence/mapping is empty in "%s" at line 1.'], + ['{{ array["a"] }}', 'Key "a" for sequence/mapping with keys "foo" does not exist in "%s" at line 1.'], ['{{ array_access["a"] }}', 'Key "a" in object with ArrayAccess of class "Twig\Tests\TemplateArrayAccessObject" does not exist in "%s" at line 1.'], ['{{ string.a }}', 'Impossible to access an attribute ("a") on a string variable ("foo") in "%s" at line 1.'], ['{{ string.a() }}', 'Impossible to invoke a method ("a") on a string variable ("foo") in "%s" at line 1.'], ['{{ null.a }}', 'Impossible to access an attribute ("a") on a null variable in "%s" at line 1.'], ['{{ null.a() }}', 'Impossible to invoke a method ("a") on a null variable in "%s" at line 1.'], - ['{{ array.a() }}', 'Impossible to invoke a method ("a") on an array in "%s" at line 1.'], - ['{{ empty_array.a }}', 'Key "a" does not exist as the array is empty in "%s" at line 1.'], - ['{{ array.a }}', 'Key "a" for array with keys "foo" does not exist in "%s" at line 1.'], - ['{{ attribute(array, -10) }}', 'Key "-10" for array with keys "foo" does not exist in "%s" at line 1.'], + ['{{ array.a() }}', 'Impossible to invoke a method ("a") on a sequence/mapping in "%s" at line 1.'], + ['{{ empty_array.a }}', 'Key "a" does not exist as the sequence/mapping is empty in "%s" at line 1.'], + ['{{ array.a }}', 'Key "a" for sequence/mapping with keys "foo" does not exist in "%s" at line 1.'], + ['{{ attribute(array, -10) }}', 'Key "-10" for sequence/mapping with keys "foo" does not exist in "%s" at line 1.'], ['{{ array_access.a }}', 'Neither the property "a" nor one of the methods "a()", "geta()"/"isa()"/"hasa()" or "__call()" exist and have public access in class "Twig\Tests\TemplateArrayAccessObject" in "%s" at line 1.'], ['{% from _self import foo %}{% macro foo(obj) %}{{ obj.missing_method() }}{% endmacro %}{{ foo(array_access) }}', 'Neither the property "missing_method" nor one of the methods "missing_method()", "getmissing_method()"/"ismissing_method()"/"hasmissing_method()" or "__call()" exist and have public access in class "Twig\Tests\TemplateArrayAccessObject" in "%s" at line 1.'], ['{{ magic_exception.test }}', 'An exception has been thrown during the rendering of a template ("Hey! Don\'t try to isset me!") in "%s" at line 1.'], @@ -193,14 +193,14 @@ public function testGetAttributeOnArrayWithConfusableKey() $this->assertSame('IntegerButStringWithLeadingZeros', $array['01']); $this->assertSame('EmptyString', $array[null]); - $this->assertSame('Zero', CoreExtension::getAttribute($twig, $template->getSourceContext(), $array, false), 'false is treated as 0 when accessing an array (equals PHP behavior)'); - $this->assertSame('One', CoreExtension::getAttribute($twig, $template->getSourceContext(), $array, true), 'true is treated as 1 when accessing an array (equals PHP behavior)'); - $this->assertSame('One', CoreExtension::getAttribute($twig, $template->getSourceContext(), $array, 1.5), 'float is casted to int when accessing an array (equals PHP behavior)'); - $this->assertSame('One', CoreExtension::getAttribute($twig, $template->getSourceContext(), $array, '1'), '"1" is treated as integer 1 when accessing an array (equals PHP behavior)'); - $this->assertSame('MinusOne', CoreExtension::getAttribute($twig, $template->getSourceContext(), $array, -1.5), 'negative float is casted to int when accessing an array (equals PHP behavior)'); - $this->assertSame('FloatButString', CoreExtension::getAttribute($twig, $template->getSourceContext(), $array, '1.5'), '"1.5" is treated as-is when accessing an array (equals PHP behavior)'); - $this->assertSame('IntegerButStringWithLeadingZeros', CoreExtension::getAttribute($twig, $template->getSourceContext(), $array, '01'), '"01" is treated as-is when accessing an array (equals PHP behavior)'); - $this->assertSame('EmptyString', CoreExtension::getAttribute($twig, $template->getSourceContext(), $array, null), 'null is treated as "" when accessing an array (equals PHP behavior)'); + $this->assertSame('Zero', CoreExtension::getAttribute($twig, $template->getSourceContext(), $array, false), 'false is treated as 0 when accessing a sequence/mapping (equals PHP behavior)'); + $this->assertSame('One', CoreExtension::getAttribute($twig, $template->getSourceContext(), $array, true), 'true is treated as 1 when accessing a sequence/mapping (equals PHP behavior)'); + $this->assertSame('One', CoreExtension::getAttribute($twig, $template->getSourceContext(), $array, 1.5), 'float is casted to int when accessing a sequence/mapping (equals PHP behavior)'); + $this->assertSame('One', CoreExtension::getAttribute($twig, $template->getSourceContext(), $array, '1'), '"1" is treated as integer 1 when accessing a sequence/mapping (equals PHP behavior)'); + $this->assertSame('MinusOne', CoreExtension::getAttribute($twig, $template->getSourceContext(), $array, -1.5), 'negative float is casted to int when accessing a sequence/mapping (equals PHP behavior)'); + $this->assertSame('FloatButString', CoreExtension::getAttribute($twig, $template->getSourceContext(), $array, '1.5'), '"1.5" is treated as-is when accessing a sequence/mapping (equals PHP behavior)'); + $this->assertSame('IntegerButStringWithLeadingZeros', CoreExtension::getAttribute($twig, $template->getSourceContext(), $array, '01'), '"01" is treated as-is when accessing a sequence/mapping (equals PHP behavior)'); + $this->assertSame('EmptyString', CoreExtension::getAttribute($twig, $template->getSourceContext(), $array, null), 'null is treated as "" when accessing a sequence/mapping (equals PHP behavior)'); } /** @@ -395,7 +395,7 @@ public function getGetAttributeTests() $tests = array_merge($tests, [ [false, null, 42, 'a', [], $anyType, 'Impossible to access an attribute ("a") on a integer variable ("42") in "index.twig".'], [false, null, 'string', 'a', [], $anyType, 'Impossible to access an attribute ("a") on a string variable ("string") in "index.twig".'], - [false, null, [], 'a', [], $anyType, 'Key "a" does not exist as the array is empty in "index.twig".'], + [false, null, [], 'a', [], $anyType, 'Key "a" does not exist as the sequence/mapping is empty in "index.twig".'], ]); return $tests; From bc19f2f48e4a192717eabb5481a73dc1d4b4164a Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Tue, 18 Jun 2024 07:24:08 +0200 Subject: [PATCH 267/812] Tweak --- doc/filters/merge.rst | 14 +++++--------- tests/Fixtures/functions/cycle_empty_mapping.test | 8 ++++++++ tests/Fixtures/functions/cycle_empty_sequence.test | 8 ++++++++ .../Fixtures/regression/strings_like_numbers.test | 4 ++-- 4 files changed, 23 insertions(+), 11 deletions(-) create mode 100644 tests/Fixtures/functions/cycle_empty_mapping.test create mode 100644 tests/Fixtures/functions/cycle_empty_sequence.test diff --git a/doc/filters/merge.rst b/doc/filters/merge.rst index fc21884ba7a..d0b302c8f60 100644 --- a/doc/filters/merge.rst +++ b/doc/filters/merge.rst @@ -1,9 +1,9 @@ ``merge`` ========= -The ``merge`` filter merges sequences and mappings. +The ``merge`` filter merges sequences and mappings: -The ``merge`` filter also works on sequences: +For sequences, new values are added at the end of the existing ones: .. code-block:: twig @@ -13,9 +13,9 @@ The ``merge`` filter also works on sequences: {# values now contains [1, 2, 'apple', 'orange'] #} -New values are added at the end of the existing ones. - -The ``merge`` filter also works on mappings: +For mappings, the merging process occurs on the keys; if the key does not +already exist, it is added but if the key already exists, its value is +overridden: .. code-block:: twig @@ -25,10 +25,6 @@ The ``merge`` filter also works on mappings: {# items now contains {'apple': 'fruit', 'orange': 'fruit', 'peugeot': 'car', 'renault': 'car'} #} -For mappings, the merging process occurs on the keys: if the key does not -already exist, it is added but if the key already exists, its value is -overridden. - .. tip:: If you want to ensure that some values are defined in a mapping (by given diff --git a/tests/Fixtures/functions/cycle_empty_mapping.test b/tests/Fixtures/functions/cycle_empty_mapping.test new file mode 100644 index 00000000000..6296c2c39ff --- /dev/null +++ b/tests/Fixtures/functions/cycle_empty_mapping.test @@ -0,0 +1,8 @@ +--TEST-- +"cycle" function returns an error on empty mappings +--TEMPLATE-- +{{ cycle({}, 0) }} +--DATA-- +return [] +--EXCEPTION-- +Twig\Error\RuntimeError: The "cycle" function does not work on empty sequences/mappings in "index.twig" at line 2. diff --git a/tests/Fixtures/functions/cycle_empty_sequence.test b/tests/Fixtures/functions/cycle_empty_sequence.test new file mode 100644 index 00000000000..01d9fe127ac --- /dev/null +++ b/tests/Fixtures/functions/cycle_empty_sequence.test @@ -0,0 +1,8 @@ +--TEST-- +"cycle" function returns an error on empty sequences +--TEMPLATE-- +{{ cycle([], 0) }} +--DATA-- +return [] +--EXCEPTION-- +Twig\Error\RuntimeError: The "cycle" function does not work on empty sequences/mappings in "index.twig" at line 2. diff --git a/tests/Fixtures/regression/strings_like_numbers.test b/tests/Fixtures/regression/strings_like_numbers.test index 62fe8848587..3884226fa4b 100644 --- a/tests/Fixtures/regression/strings_like_numbers.test +++ b/tests/Fixtures/regression/strings_like_numbers.test @@ -1,8 +1,8 @@ --TEST-- Twig does not confuse strings with integers in getAttribute() --TEMPLATE-- -{{ hash['2e2'] }} +{{ mapping['2e2'] }} --DATA-- -return ['hash' => ['2e2' => 'works']] +return ['mapping' => ['2e2' => 'works']] --EXPECT-- works From f24929a4c93e6fc62768003770c90565d633ab5a Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sat, 22 Jun 2024 19:38:57 +0200 Subject: [PATCH 268/812] Fix typo --- doc/templates.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/templates.rst b/doc/templates.rst index 1026dd8eac6..0b09e7de096 100644 --- a/doc/templates.rst +++ b/doc/templates.rst @@ -819,7 +819,7 @@ Operator Score of precedence Description ``ends with``, ``has some``, ``has every`` ``..`` 25 Range of values -``+``, ``-`` 30 Addition and substraction on numbers +``+``, ``-`` 30 Addition and subtraction on numbers ``~`` 40 String concatenation ``not`` 50 Negates a statement ``*``, ``/``, ``//``, ``%`` 60 Arithmetic operations on numbers From c77354ebadcb6e92d90d025a2a75b353dd8478fa Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sat, 22 Jun 2024 19:44:04 +0200 Subject: [PATCH 269/812] Add missing comment --- src/ExpressionParser.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/ExpressionParser.php b/src/ExpressionParser.php index 3a97370ed79..006c1bdbba7 100644 --- a/src/ExpressionParser.php +++ b/src/ExpressionParser.php @@ -319,6 +319,9 @@ public function parseStringExpression() return $expr; } + /** + * @deprecated since 3.11, use parseSequenceExpression() instead + */ public function parseArrayExpression() { trigger_deprecation('twig/twig', '3.11', 'Calling "%s()" is deprecated, use "parseSequenceExpression()" instead.', __METHOD__); From c5c95a2d5c9d888dad3b7812ba5f09e866c04038 Mon Sep 17 00:00:00 2001 From: Pierre Date: Thu, 20 Jul 2023 10:31:29 +0200 Subject: [PATCH 270/812] Issue #3828: Add sequence and mapping tests --- CHANGELOG | 2 +- composer.json | 3 +- doc/tests/iterable.rst | 2 +- doc/tests/mapping.rst | 14 ++++++++ doc/tests/sequence.rst | 14 ++++++++ src/Extension/CoreExtension.php | 52 ++++++++++++++++++++++++++++++ tests/Fixtures/tests/mapping.test | 38 ++++++++++++++++++++++ tests/Fixtures/tests/sequence.test | 38 ++++++++++++++++++++++ 8 files changed, 160 insertions(+), 3 deletions(-) create mode 100644 doc/tests/mapping.rst create mode 100644 doc/tests/sequence.rst create mode 100644 tests/Fixtures/tests/mapping.test create mode 100644 tests/Fixtures/tests/sequence.test diff --git a/CHANGELOG b/CHANGELOG index 01e4acaba6b..f6ce15887b0 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -3,9 +3,9 @@ * Deprecate the second argument of `Twig\Node\Expression\CallExpression::compileArguments()` * Deprecate `Twig\ExpressionParser\parseHashExpression()` in favor of `Twig\ExpressionParser::parseMappingExpression()` - * Deprecate `Twig\ExpressionParser\parseArrayExpression()`` in favor of `Twig\ExpressionParser::parseSequenceExpression()` + * Add `sequence` and `mapping` tests. # 3.10.3 (2024-05-16) diff --git a/composer.json b/composer.json index 57ba92964f7..26cb4972ec5 100644 --- a/composer.json +++ b/composer.json @@ -28,7 +28,8 @@ "symfony/polyfill-php80": "^1.22", "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-mbstring": "^1.3", - "symfony/polyfill-ctype": "^1.8" + "symfony/polyfill-ctype": "^1.8", + "symfony/polyfill-php81": "^1.29" }, "require-dev": { "symfony/phpunit-bridge": "^5.4.9|^6.4|^7.0", diff --git a/doc/tests/iterable.rst b/doc/tests/iterable.rst index 4ebfe9d8a50..8c83efcd89d 100644 --- a/doc/tests/iterable.rst +++ b/doc/tests/iterable.rst @@ -5,7 +5,7 @@ .. code-block:: twig - {# evaluates to true if the foo variable is iterable #} + {# evaluates to true if the users variable is iterable #} {% if users is iterable %} {% for user in users %} Hello {{ user }}! diff --git a/doc/tests/mapping.rst b/doc/tests/mapping.rst new file mode 100644 index 00000000000..ab90cd6c16e --- /dev/null +++ b/doc/tests/mapping.rst @@ -0,0 +1,14 @@ +``mapping`` +=========== + +``mapping`` checks if a variable is a mapping: + +.. code-block:: twig + + {% set users = {alice: "Alice Dupond", bob: "Bob Smith"} %} + {# evaluates to true if the users variable is a mapping #} + {% if users is mapping %} + {% for key, user in users %} + {{ key }}: {{ user }}; + {% endfor %} + {% endif %} diff --git a/doc/tests/sequence.rst b/doc/tests/sequence.rst new file mode 100644 index 00000000000..0ae47a38705 --- /dev/null +++ b/doc/tests/sequence.rst @@ -0,0 +1,14 @@ +``sequence`` +============ + +``sequence`` checks if a variable is a sequence: + +.. code-block:: twig + + {% set users = ["Alice", "Bob"] %} + {# evaluates to true if the users variable is a sequence #} + {% if users is sequence %} + {% for user in users %} + Hello {{ user }}! + {% endfor %} + {% endif %} diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index a7bdc8204f1..d6bcfffb229 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -260,6 +260,8 @@ public function getTests(): array new TwigTest('constant', null, ['node_class' => ConstantTest::class]), new TwigTest('empty', [self::class, 'testEmpty']), new TwigTest('iterable', 'is_iterable'), + new TwigTest('sequence', [self::class, 'testSequence']), + new TwigTest('mapping', [self::class, 'testMapping']), ]; } @@ -1285,6 +1287,56 @@ public static function testEmpty($value): bool return '' === $value || false === $value || null === $value || [] === $value; } + /** + * Checks if a variable is a sequence. + * + * {# evaluates to true if the foo variable is a sequence #} + * {% if foo is sequence %} + * {# ... #} + * {% endif %} + * + * @param mixed $value + * + * @internal + */ + public static function testSequence($value): bool + { + if ($value instanceof \ArrayObject) { + $value = $value->getArrayCopy(); + } + + if ($value instanceof \Traversable) { + $value = iterator_to_array($value); + } + + return \is_array($value) && array_is_list($value); + } + + /** + * Checks if a variable is a mapping. + * + * {# evaluates to true if the foo variable is a mapping #} + * {% if foo is mapping %} + * {# ... #} + * {% endif %} + * + * @param mixed $value + * + * @internal + */ + public static function testMapping($value): bool + { + if ($value instanceof \ArrayObject) { + $value = $value->getArrayCopy(); + } + + if ($value instanceof \Traversable) { + $value = iterator_to_array($value); + } + + return (\is_array($value) && !array_is_list($value)) || \is_object($value); + } + /** * Renders a template. * diff --git a/tests/Fixtures/tests/mapping.test b/tests/Fixtures/tests/mapping.test new file mode 100644 index 00000000000..3e4fce048aa --- /dev/null +++ b/tests/Fixtures/tests/mapping.test @@ -0,0 +1,38 @@ +--TEST-- +"mapping" test +--TEMPLATE-- +{{ empty is mapping ? 'ok' : 'ko' }} +{{ sequence is mapping ? 'ok' : 'ko' }} +{{ empty_array_obj is mapping ? 'ok' : 'ko' }} +{{ sequence_array_obj is mapping ? 'ok' : 'ko' }} +{{ mapping_array_obj is mapping ? 'ok' : 'ko' }} +{{ obj is mapping ? 'ok' : 'ko' }} +{{ mapping is mapping ? 'ok' : 'ko' }} +{{ string is mapping ? 'ok' : 'ko' }} +--DATA-- +return [ + 'empty' => [], + 'sequence' => [ + 'foo', + 'bar', + 'baz' + ], + 'empty_array_obj' => new \ArrayObject(), + 'sequence_array_obj' => new \ArrayObject(['foo', 'bar']), + 'mapping_array_obj' => new \ArrayObject(['foo' => 'bar']), + 'obj' => new \stdClass(), + 'mapping' => [ + 'foo' => 'bar', + 'bar' => 'foo' + ], + 'string' => 'test', +] +--EXPECT-- +ko +ko +ko +ko +ok +ok +ok +ko diff --git a/tests/Fixtures/tests/sequence.test b/tests/Fixtures/tests/sequence.test new file mode 100644 index 00000000000..fb8a31212d3 --- /dev/null +++ b/tests/Fixtures/tests/sequence.test @@ -0,0 +1,38 @@ +--TEST-- +"sequence" test +--TEMPLATE-- +{{ empty is sequence ? 'ok' : 'ko' }} +{{ sequence is sequence ? 'ok' : 'ko' }} +{{ empty_array_obj is sequence ? 'ok' : 'ko' }} +{{ sequence_array_obj is sequence ? 'ok' : 'ko' }} +{{ mapping_array_obj is sequence ? 'ok' : 'ko' }} +{{ obj is sequence ? 'ok' : 'ko' }} +{{ mapping is sequence ? 'ok' : 'ko' }} +{{ string is sequence ? 'ok' : 'ko' }} +--DATA-- +return [ + 'empty' => [], + 'sequence' => [ + 'foo', + 'bar', + 'baz' + ], + 'empty_array_obj' => new \ArrayObject(), + 'sequence_array_obj' => new \ArrayObject(['foo', 'bar']), + 'mapping_array_obj' => new \ArrayObject(['foo' => 'bar']), + 'obj' => new \stdClass(), + 'mapping' => [ + 'foo' => 'bar', + 'bar' => 'foo' + ], + 'string' => 'test', +] +--EXPECT-- +ok +ok +ok +ok +ko +ko +ko +ko From 491a84175dad2f79363d344b3db0f9585346d04e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Andr=C3=A9?= Date: Sun, 30 Jun 2024 10:13:10 +0200 Subject: [PATCH 271/812] Document OPTIMIZE_TEXT_NODES (and remove obselete OPTIMIZE_VAR_ACCESS) --- doc/api.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/api.rst b/doc/api.rst index 73452e53c3c..84b9d558593 100644 --- a/doc/api.rst +++ b/doc/api.rst @@ -563,8 +563,8 @@ Twig supports the following optimizations: * ``\Twig\NodeVisitor\OptimizerNodeVisitor::OPTIMIZE_RAW_FILTER``, removes the ``raw`` filter whenever possible. -* ``\Twig\NodeVisitor\OptimizerNodeVisitor::OPTIMIZE_VAR_ACCESS``, simplifies the creation - and access of variables in the compiled templates whenever possible. +* ``\Twig\NodeVisitor\OptimizerNodeVisitor::OPTIMIZE_TEXT_NODES``, optimizes the text + nodes by merging consecutive text nodes into a single one. Exceptions ---------- From 4a8e8ee16b0fdfd3b7db7197039c4968df59df6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Andr=C3=A9?= Date: Sun, 30 Jun 2024 10:31:01 +0200 Subject: [PATCH 272/812] [Doc] Modernize Class name in examples From "Project_Twig_Extension" to "CustomExtension" Just a suggestion, feel free to close if you want to keep it like this :) I found those namings only in "advanced" --- doc/advanced.rst | 58 +++++++++++++++++++++++++----------------------- 1 file changed, 30 insertions(+), 28 deletions(-) diff --git a/doc/advanced.rst b/doc/advanced.rst index cf1b9870afc..e4f9401c6b8 100644 --- a/doc/advanced.rst +++ b/doc/advanced.rst @@ -338,11 +338,11 @@ compilation. This is useful if your test can be compiled into PHP primitives. This is used by many of the tests built into Twig:: namespace App; - + use Twig\Environment; use Twig\Node\Expression\TestExpression; use Twig\TwigTest; - + $twig = new Environment($loader); $test = new TwigTest( 'odd', @@ -456,14 +456,14 @@ Add a tag by calling the ``addTokenParser`` method on the ``\Twig\Environment`` instance:: $twig = new \Twig\Environment($loader); - $twig->addTokenParser(new Project_Set_TokenParser()); + $twig->addTokenParser(new CustomSetTokenParser()); Defining a Token Parser ~~~~~~~~~~~~~~~~~~~~~~~ Now, let's see the actual code of this class:: - class Project_Set_TokenParser extends \Twig\TokenParser\AbstractTokenParser + class CustomSetTokenParser extends \Twig\TokenParser\AbstractTokenParser { public function parse(\Twig\Token $token) { @@ -475,7 +475,7 @@ Now, let's see the actual code of this class:: $value = $parser->getExpressionParser()->parseExpression(); $stream->expect(\Twig\Token::BLOCK_END_TYPE); - return new Project_Set_Node($name, $value, $token->getLine(), $this->getTag()); + return new CustomSetNode($name, $value, $token->getLine(), $this->getTag()); } public function getTag() @@ -488,7 +488,7 @@ The ``getTag()`` method must return the tag we want to parse, here ``set``. The ``parse()`` method is invoked whenever the parser encounters a ``set`` tag. It should return a ``\Twig\Node\Node`` instance that represents the node (the -``Project_Set_Node`` calls creating is explained in the next section). +``CustomSetNode`` calls creating is explained in the next section). The parsing process is simplified thanks to a bunch of methods you can call from the token stream (``$this->parser->getStream()``): @@ -518,9 +518,9 @@ the ``set`` tag. Defining a Node ~~~~~~~~~~~~~~~ -The ``Project_Set_Node`` class itself is quite short:: +The ``CustomSetNode`` class itself is quite short:: - class Project_Set_Node extends \Twig\Node\Node + class CustomSetNode extends \Twig\Node\Node { public function __construct($name, \Twig\Node\Expression\AbstractExpression $value, $line, $tag = null) { @@ -631,7 +631,7 @@ To keep your extension class clean and lean, inherit from the built-in ``\Twig\Extension\AbstractExtension`` class instead of implementing the interface as it provides empty implementations for all methods:: - class Project_Twig_Extension extends \Twig\Extension\AbstractExtension + class CustomTwigExtension extends \Twig\Extension\AbstractExtension { } @@ -644,7 +644,7 @@ You can register an extension by using the ``addExtension()`` method on your main ``Environment`` object:: $twig = new \Twig\Environment($loader); - $twig->addExtension(new Project_Twig_Extension()); + $twig->addExtension(new CustomTwigExtension()); .. tip:: @@ -656,7 +656,7 @@ Globals Global variables can be registered in an extension via the ``getGlobals()`` method:: - class Project_Twig_Extension extends \Twig\Extension\AbstractExtension implements \Twig\Extension\GlobalsInterface + class CustomTwigExtension extends \Twig\Extension\AbstractExtension implements \Twig\Extension\GlobalsInterface { public function getGlobals(): array { @@ -674,7 +674,7 @@ Functions Functions can be registered in an extension via the ``getFunctions()`` method:: - class Project_Twig_Extension extends \Twig\Extension\AbstractExtension + class CustomTwigExtension extends \Twig\Extension\AbstractExtension { public function getFunctions() { @@ -693,7 +693,7 @@ To add a filter to an extension, you need to override the ``getFilters()`` method. This method must return an array of filters to add to the Twig environment:: - class Project_Twig_Extension extends \Twig\Extension\AbstractExtension + class CustomTwigExtension extends \Twig\Extension\AbstractExtension { public function getFilters() { @@ -712,18 +712,18 @@ Adding a tag in an extension can be done by overriding the ``getTokenParsers()`` method. This method must return an array of tags to add to the Twig environment:: - class Project_Twig_Extension extends \Twig\Extension\AbstractExtension + class CustomTwigExtension extends \Twig\Extension\AbstractExtension { public function getTokenParsers() { - return [new Project_Set_TokenParser()]; + return [new CustomSetTokenParser()]; } // ... } In the above code, we have added a single new tag, defined by the -``Project_Set_TokenParser`` class. The ``Project_Set_TokenParser`` class is +``CustomSetTokenParser`` class. The ``CustomSetTokenParser`` class is responsible for parsing the tag and compiling it to PHP. Operators @@ -732,7 +732,7 @@ Operators The ``getOperators()`` methods lets you add new operators. Here is how to add the ``!``, ``||``, and ``&&`` operators:: - class Project_Twig_Extension extends \Twig\Extension\AbstractExtension + class CustomTwigExtension extends \Twig\Extension\AbstractExtension { public function getOperators() { @@ -755,7 +755,7 @@ Tests The ``getTests()`` method lets you add new test functions:: - class Project_Twig_Extension extends \Twig\Extension\AbstractExtension + class CustomTwigExtension extends \Twig\Extension\AbstractExtension { public function getTests() { @@ -784,7 +784,7 @@ any valid PHP callable: The simplest way to use methods is to define them on the extension itself:: - class Project_Twig_Extension extends \Twig\Extension\AbstractExtension + class CustomTwigExtension extends \Twig\Extension\AbstractExtension { private $rot13Provider; @@ -822,7 +822,7 @@ must be autoload-able):: // implement the logic to create an instance of $class // and inject its dependencies // most of the time, it means using your dependency injection container - if ('Project_Twig_RuntimeExtension' === $class) { + if ('CustomRuntimeExtension' === $class) { return new $class(new Rot13Provider()); } else { // ... @@ -838,9 +838,9 @@ must be autoload-able):: (``\Twig\RuntimeLoader\ContainerRuntimeLoader``). It is now possible to move the runtime logic to a new -``Project_Twig_RuntimeExtension`` class and use it directly in the extension:: +``CustomRuntimeExtension`` class and use it directly in the extension:: - class Project_Twig_RuntimeExtension + class CustomRuntimeExtension { private $rot13Provider; @@ -855,14 +855,14 @@ It is now possible to move the runtime logic to a new } } - class Project_Twig_Extension extends \Twig\Extension\AbstractExtension + class CustomTwigExtension extends \Twig\Extension\AbstractExtension { public function getFunctions() { return [ - new \Twig\TwigFunction('rot13', ['Project_Twig_RuntimeExtension', 'rot13']), + new \Twig\TwigFunction('rot13', ['CustomRuntimeExtension', 'rot13']), // or - new \Twig\TwigFunction('rot13', 'Project_Twig_RuntimeExtension::rot13'), + new \Twig\TwigFunction('rot13', 'CustomRuntimeExtension::rot13'), ]; } } @@ -890,15 +890,17 @@ structure in your test directory:: The ``IntegrationTest.php`` file should look like this:: + namespace Project\Tests; + use Twig\Test\IntegrationTestCase; - class Project_Tests_IntegrationTest extends IntegrationTestCase + class IntegrationTest extends IntegrationTestCase { public function getExtensions() { return [ - new Project_Twig_Extension1(), - new Project_Twig_Extension2(), + new CustomTwigExtension1(), + new CustomTwigExtension2(), ]; } From 2f8219e6919640c7055f6808c493d60ad69c95b5 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Mon, 1 Jul 2024 18:40:39 +0200 Subject: [PATCH 273/812] Remove usage of uniqid --- src/Test/IntegrationTestCase.php | 2 +- tests/Cache/FilesystemTest.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Test/IntegrationTestCase.php b/src/Test/IntegrationTestCase.php index 44853063dfa..6462e413d66 100644 --- a/src/Test/IntegrationTestCase.php +++ b/src/Test/IntegrationTestCase.php @@ -188,7 +188,7 @@ protected function doIntegrationTest($file, $message, $condition, $templates, $e // avoid using the same PHP class name for different cases $p = new \ReflectionProperty($twig, 'templateClassPrefix'); $p->setAccessible(true); - $p->setValue($twig, '__TwigTemplate_'.hash(\PHP_VERSION_ID < 80100 ? 'sha256' : 'xxh128', uniqid((string) mt_rand(), true), false).'_'); + $p->setValue($twig, '__TwigTemplate_'.hash(\PHP_VERSION_ID < 80100 ? 'sha256' : 'xxh128', random_bytes(32), false).'_'); $deprecations = []; try { diff --git a/tests/Cache/FilesystemTest.php b/tests/Cache/FilesystemTest.php index 63b98a900b0..934ce016082 100644 --- a/tests/Cache/FilesystemTest.php +++ b/tests/Cache/FilesystemTest.php @@ -23,7 +23,7 @@ class FilesystemTest extends TestCase protected function setUp(): void { - $nonce = hash(\PHP_VERSION_ID < 80100 ? 'sha256' : 'xxh128', uniqid((string) mt_rand(), true)); + $nonce = hash(\PHP_VERSION_ID < 80100 ? 'sha256' : 'xxh128', random_bytes(32)); $this->classname = '__Twig_Tests_Cache_FilesystemTest_Template_'.$nonce; $this->directory = sys_get_temp_dir().'/twig-test'; $this->cache = new FilesystemCache($this->directory); From ae429bd51de7f9b81bc0fe3019237d6dc5eebd24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Andr=C3=A9?= Date: Sun, 30 Jun 2024 23:24:19 +0200 Subject: [PATCH 274/812] [Doc] Replace http links with https --- doc/tags/extends.rst | 2 +- doc/templates.rst | 12 +++++------- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/doc/tags/extends.rst b/doc/tags/extends.rst index 7f1c1e8c1b8..84d813e1742 100644 --- a/doc/tags/extends.rst +++ b/doc/tags/extends.rst @@ -26,7 +26,7 @@ skeleton document:
{% block content %}{% endblock %}
diff --git a/doc/templates.rst b/doc/templates.rst index c7e2e3130fb..e09a6a0185c 100644 --- a/doc/templates.rst +++ b/doc/templates.rst @@ -47,8 +47,8 @@ IDEs Integration Many IDEs support syntax highlighting and auto-completion for Twig: * *Textmate* via the `Twig bundle`_ -* *Vim* via the `Jinja syntax plugin`_ or the `vim-twig plugin`_ -* *Netbeans* via the `Twig syntax plugin`_ (until 7.1, native as of 7.2) +* *Vim* via the `vim-twig plugin`_ +* *Netbeans* (native as of 7.2) * *PhpStorm* (native as of 2.1) * *Eclipse* via the `Twig plugin`_ * *Sublime Text* via the `Twig bundle`_ @@ -360,7 +360,7 @@ document that might be used for a two-column page:
{% block content %}{% endblock %}
@@ -918,16 +918,14 @@ Extensions Twig can be extended. If you want to create your own extensions, read the :ref:`Creating an Extension ` chapter. -.. _`Twig bundle`: https://github.com/Anomareh/PHP-Twig.tmbundle -.. _`Jinja syntax plugin`: http://jinja.pocoo.org/docs/integration/#vim +.. _`Twig bundle`: https://github.com/uhnomoli/PHP-Twig.tmbundle .. _`vim-twig plugin`: https://github.com/lumiliet/vim-twig -.. _`Twig syntax plugin`: http://plugins.netbeans.org/plugin/37069/php-twig .. _`Twig plugin`: https://github.com/pulse00/Twig-Eclipse-Plugin .. _`Twig language definition`: https://github.com/gabrielcorpse/gedit-twig-template-language .. _`Twig syntax mode`: https://github.com/bobthecow/Twig-HTML.mode .. _`other Twig syntax mode`: https://github.com/muxx/Twig-HTML.mode .. _`Notepad++ Twig Highlighter`: https://github.com/Banane9/notepadplusplus-twig -.. _`web-mode.el`: http://web-mode.org/ +.. _`web-mode.el`: https://web-mode.org/ .. _`regular expressions`: https://www.php.net/manual/en/pcre.pattern.php .. _`PHP-twig for atom`: https://github.com/reesef/php-twig .. _`TwigFiddle`: https://twigfiddle.com/ From cb392489c33dd457098158c58c9bc9c99b990a21 Mon Sep 17 00:00:00 2001 From: Mathieu Santostefano Date: Tue, 18 Jun 2024 22:49:24 +0200 Subject: [PATCH 275/812] feat(string-extra): Add singularize and pluralize filter --- doc/filters/pluralize.rst | 45 +++++++++++++++++++ doc/filters/singularize.rst | 45 +++++++++++++++++++ extra/string-extra/README.md | 6 +++ extra/string-extra/StringExtension.php | 45 +++++++++++++++++++ .../Fixtures/pluralize-invalid-language.test | 10 +++++ .../Tests/Fixtures/pluralize.test | 17 +++++++ .../Tests/Fixtures/singularize.test | 19 ++++++++ .../singularizelize-invalid-language.test | 10 +++++ 8 files changed, 197 insertions(+) create mode 100644 doc/filters/pluralize.rst create mode 100644 doc/filters/singularize.rst create mode 100755 extra/string-extra/Tests/Fixtures/pluralize-invalid-language.test create mode 100755 extra/string-extra/Tests/Fixtures/pluralize.test create mode 100755 extra/string-extra/Tests/Fixtures/singularize.test create mode 100755 extra/string-extra/Tests/Fixtures/singularizelize-invalid-language.test diff --git a/doc/filters/pluralize.rst b/doc/filters/pluralize.rst new file mode 100644 index 00000000000..644d5284791 --- /dev/null +++ b/doc/filters/pluralize.rst @@ -0,0 +1,45 @@ +``pluralize`` +======== + +The ``pluralize`` filter transforms a given noun in its singular form into its plural version. + +Here is an example: + +.. code-block:: twig + + {{ 'partitions'|pluralize('en') }} + partition + +.. note:: + `lang` parameter is mandatory for this filter as only English and French are supported by the Inflector in Symfony. + +The ``pluralize`` filter uses the method by the same name in Symfony's +`Inflector `_. + +.. note:: + + The ``pluralize`` filter is part of the ``StringExtension`` which is not + installed by default. Install it first: + + .. code-block:: bash + + $ composer require twig/string-extra + + Then, on Symfony projects, install the ``twig/extra-bundle``: + + .. code-block:: bash + + $ composer require twig/extra-bundle + + Otherwise, add the extension explicitly on the Twig environment:: + + use Twig\Extra\String\StringExtension; + + $twig = new \Twig\Environment(...); + $twig->addExtension(new StringExtension()); + +Arguments +--------- + +* ``lang``: The lang of the original string. Only English (`en`) and French (`fr`) are supported. +* ``singleResult``: This argument is optional. If set to false, the filter will return an array of pluralized words. Default is true. diff --git a/doc/filters/singularize.rst b/doc/filters/singularize.rst new file mode 100644 index 00000000000..3488127c263 --- /dev/null +++ b/doc/filters/singularize.rst @@ -0,0 +1,45 @@ +``singularize`` +======== + +The ``singularize`` filter transforms a given noun in its plural form into its singular version. + +Here is an example: + +.. code-block:: twig + + {{ 'partitions'|singularize('en') }} + partition + +.. note:: + `lang` parameter is mandatory for this filter as only English and French are supported by the Inflector in Symfony. + +The ``singularize`` filter uses the method by the same name in Symfony's +`Inflector `_. + +.. note:: + + The ``singularize`` filter is part of the ``StringExtension`` which is not + installed by default. Install it first: + + .. code-block:: bash + + $ composer require twig/string-extra + + Then, on Symfony projects, install the ``twig/extra-bundle``: + + .. code-block:: bash + + $ composer require twig/extra-bundle + + Otherwise, add the extension explicitly on the Twig environment:: + + use Twig\Extra\String\StringExtension; + + $twig = new \Twig\Environment(...); + $twig->addExtension(new StringExtension()); + +Arguments +--------- + +* ``lang``: The lang of the original string. Only English (`en`) and French (`fr`) are supported. +* ``singleResult``: This argument is optional. If set to false, the filter will return an array of singularized nouns. Default is true. diff --git a/extra/string-extra/README.md b/extra/string-extra/README.md index 6687723bfae..e2569c6599d 100644 --- a/extra/string-extra/README.md +++ b/extra/string-extra/README.md @@ -10,7 +10,13 @@ object to give access to [methods of the class][2]. It also provides a [`slug`][3] filter which is simply a wrapper for the [`AsciiSlugger`][4]'s `slug` method. +In addition, two filters are provided to [`singularize`][5] and [`pluralize`][6] a noun. +Behind the scenes, it uses the [`Inflector`][6] class from the Symfony String component. + [1]: https://twig.symfony.com/u [2]: https://symfony.com/doc/current/components/string.html [3]: https://twig.symfony.com/slug [4]: https://symfony.com/doc/current/components/string.html#slugger +[5]: https://twig.symfony.com/singularize +[6]: https://twig.symfony.com/pluralize +[7]: https://symfony.com/doc/current/components/string.html#inflector diff --git a/extra/string-extra/StringExtension.php b/extra/string-extra/StringExtension.php index 2e827af0567..a1322d4dd7a 100644 --- a/extra/string-extra/StringExtension.php +++ b/extra/string-extra/StringExtension.php @@ -12,6 +12,9 @@ namespace Twig\Extra\String; use Symfony\Component\String\AbstractUnicodeString; +use Symfony\Component\String\Inflector\EnglishInflector; +use Symfony\Component\String\Inflector\FrenchInflector; +use Symfony\Component\String\Inflector\InflectorInterface; use Symfony\Component\String\Slugger\AsciiSlugger; use Symfony\Component\String\Slugger\SluggerInterface; use Symfony\Component\String\UnicodeString; @@ -21,6 +24,8 @@ final class StringExtension extends AbstractExtension { private $slugger; + private $frenchInflector; + private $englishInflector; public function __construct(?SluggerInterface $slugger = null) { @@ -32,6 +37,8 @@ public function getFilters() return [ new TwigFilter('u', [$this, 'createUnicodeString']), new TwigFilter('slug', [$this, 'createSlug']), + new TwigFilter('pluralize', [$this, 'pluralize']), + new TwigFilter('singularize', [$this, 'singularize']), ]; } @@ -44,4 +51,42 @@ public function createSlug(string $string, string $separator = '-', ?string $loc { return $this->slugger->slug($string, $separator, $locale); } + + /** + * @return array|string + */ + public function pluralize(string $value, string $lang, bool $singleResult = true) + { + switch (true) { + case $singleResult: + return $this->getInflector($lang)->pluralize($value)[0]; + default: + return $this->getInflector($lang)->pluralize($value); + } + } + + /** + * @return array|string + */ + public function singularize(string $value, string $lang, bool $singleResult = true) + { + switch (true) { + case $singleResult: + return $this->getInflector($lang)->singularize($value)[0]; + default: + return $this->getInflector($lang)->singularize($value); + } + } + + private function getInflector(string $lang): InflectorInterface + { + switch ($lang) { + case 'fr': + return $this->frenchInflector ?? $this->frenchInflector = new FrenchInflector(); + case 'en': + return $this->englishInflector ?? $this->englishInflector = new EnglishInflector(); + default: + throw new \InvalidArgumentException(sprintf('Language "%s" is not supported.', $lang)); + } + } } diff --git a/extra/string-extra/Tests/Fixtures/pluralize-invalid-language.test b/extra/string-extra/Tests/Fixtures/pluralize-invalid-language.test new file mode 100755 index 00000000000..766f8b895a2 --- /dev/null +++ b/extra/string-extra/Tests/Fixtures/pluralize-invalid-language.test @@ -0,0 +1,10 @@ +--TEST-- +"pluralize" filter +--TEMPLATE-- +{{ 'partition'|pluralize('it') }} + +--DATA-- +return [] + +--EXCEPTION-- +Twig\Error\RuntimeError: An exception has been thrown during the rendering of a template ("Language "it" is not supported.") in "index.twig" at line 2. \ No newline at end of file diff --git a/extra/string-extra/Tests/Fixtures/pluralize.test b/extra/string-extra/Tests/Fixtures/pluralize.test new file mode 100755 index 00000000000..bfea9747be7 --- /dev/null +++ b/extra/string-extra/Tests/Fixtures/pluralize.test @@ -0,0 +1,17 @@ +--TEST-- +"pluralize" filter +--TEMPLATE-- +{{ 'partition'|pluralize('fr') }} +{{ 'partition'|pluralize('fr', false)|first }} +{{ 'person'|pluralize('fr') }} +{{ 'person'|pluralize('en', false)|first }} +{{ 'person'|pluralize('en', false)|last }} + +--DATA-- +return [] +--EXPECT-- +partitions +partitions +persons +persons +people diff --git a/extra/string-extra/Tests/Fixtures/singularize.test b/extra/string-extra/Tests/Fixtures/singularize.test new file mode 100755 index 00000000000..307fadeaa7d --- /dev/null +++ b/extra/string-extra/Tests/Fixtures/singularize.test @@ -0,0 +1,19 @@ +--TEST-- +"singularize" filter +--TEMPLATE-- +{{ 'partitions'|singularize('fr') }} +{{ 'partitions'|singularize('fr', false)|first }} +{{ 'persons'|singularize('fr') }} +{{ 'persons'|singularize('en', false)|first }} +{{ 'people'|singularize('en') }} +{{ 'people'|singularize('en', false)|first }} + +--DATA-- +return [] +--EXPECT-- +partition +partition +person +person +person +person diff --git a/extra/string-extra/Tests/Fixtures/singularizelize-invalid-language.test b/extra/string-extra/Tests/Fixtures/singularizelize-invalid-language.test new file mode 100755 index 00000000000..10300a8c1a5 --- /dev/null +++ b/extra/string-extra/Tests/Fixtures/singularizelize-invalid-language.test @@ -0,0 +1,10 @@ +--TEST-- +"singularize" filter +--TEMPLATE-- +{{ 'partitions'|singularize('it') }} + +--DATA-- +return [] + +--EXCEPTION-- +Twig\Error\RuntimeError: An exception has been thrown during the rendering of a template ("Language "it" is not supported.") in "index.twig" at line 2. \ No newline at end of file From 8c969d403c0aa2ccf3fc7f8d8e7f4d0d3be754fd Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Thu, 4 Jul 2024 07:54:06 +0200 Subject: [PATCH 276/812] Tweak code and docs --- CHANGELOG | 1 + doc/filters/index.rst | 2 + doc/filters/plural.rst | 53 +++++++++++++++++++ doc/filters/pluralize.rst | 45 ---------------- doc/filters/singular.rst | 52 ++++++++++++++++++ doc/filters/singularize.rst | 45 ---------------- extra/string-extra/README.md | 17 +++--- extra/string-extra/StringExtension.php | 32 ++++++----- ...uage.test => plural-invalid-language.test} | 4 +- extra/string-extra/Tests/Fixtures/plural.test | 15 ++++++ .../Tests/Fixtures/pluralize.test | 17 ------ ...ge.test => singular-invalid-language.test} | 4 +- .../string-extra/Tests/Fixtures/singular.test | 19 +++++++ .../Tests/Fixtures/singularize.test | 19 ------- 14 files changed, 169 insertions(+), 156 deletions(-) create mode 100644 doc/filters/plural.rst delete mode 100644 doc/filters/pluralize.rst create mode 100644 doc/filters/singular.rst delete mode 100644 doc/filters/singularize.rst rename extra/string-extra/Tests/Fixtures/{pluralize-invalid-language.test => plural-invalid-language.test} (79%) create mode 100755 extra/string-extra/Tests/Fixtures/plural.test delete mode 100755 extra/string-extra/Tests/Fixtures/pluralize.test rename extra/string-extra/Tests/Fixtures/{singularizelize-invalid-language.test => singular-invalid-language.test} (78%) create mode 100755 extra/string-extra/Tests/Fixtures/singular.test delete mode 100755 extra/string-extra/Tests/Fixtures/singularize.test diff --git a/CHANGELOG b/CHANGELOG index 73bf3ba61a2..3e000cb2a36 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,6 @@ # 3.11.0 (2024-XX-XX) + * Add the `singular` and `plural` filters in `StringExtension` * Deprecate the second argument of `Twig\Node\Expression\CallExpression::compileArguments()` # 3.10.3 (2024-05-16) diff --git a/doc/filters/index.rst b/doc/filters/index.rst index eea2383e505..64b49aa9d3e 100644 --- a/doc/filters/index.rst +++ b/doc/filters/index.rst @@ -41,11 +41,13 @@ Filters merge nl2br number_format + plural raw reduce replace reverse round + singular slice slug sort diff --git a/doc/filters/plural.rst b/doc/filters/plural.rst new file mode 100644 index 00000000000..1c9db960f35 --- /dev/null +++ b/doc/filters/plural.rst @@ -0,0 +1,53 @@ +``plural`` +========== + +.. versionadded:: 3.11 + + The ``plural`` filter was added in Twig 3.11. + +The ``plural`` filter transforms a given noun in its singular form into its +plural version: + +.. code-block:: twig + + {# English (en) rules are used by default #} + {{ 'partition'|pluralize() }} + partitions + + {{ 'partition'|pluralize('fr') }} + partitions + +.. note:: + + The ``plural`` filter is part of the ``StringExtension`` which is not + installed by default. Install it first: + + .. code-block:: bash + + $ composer require twig/string-extra + + Then, on Symfony projects, install the ``twig/extra-bundle``: + + .. code-block:: bash + + $ composer require twig/extra-bundle + + Otherwise, add the extension explicitly on the Twig environment:: + + use Twig\Extra\String\StringExtension; + + $twig = new \Twig\Environment(...); + $twig->addExtension(new StringExtension()); + +Arguments +--------- + +* ``locale``: The locale of the original string (limited to languages supported by the from Symfony `inflector`_, part of the String component) +* ``all``: Whether to return all possible plurals as an array, default is ``false`` + +.. note:: + + Internally, Twig uses the `pluralize`_ method from the Symfony String component. + +.. _`inflector`: +.. _`pluralize`: diff --git a/doc/filters/pluralize.rst b/doc/filters/pluralize.rst deleted file mode 100644 index 644d5284791..00000000000 --- a/doc/filters/pluralize.rst +++ /dev/null @@ -1,45 +0,0 @@ -``pluralize`` -======== - -The ``pluralize`` filter transforms a given noun in its singular form into its plural version. - -Here is an example: - -.. code-block:: twig - - {{ 'partitions'|pluralize('en') }} - partition - -.. note:: - `lang` parameter is mandatory for this filter as only English and French are supported by the Inflector in Symfony. - -The ``pluralize`` filter uses the method by the same name in Symfony's -`Inflector `_. - -.. note:: - - The ``pluralize`` filter is part of the ``StringExtension`` which is not - installed by default. Install it first: - - .. code-block:: bash - - $ composer require twig/string-extra - - Then, on Symfony projects, install the ``twig/extra-bundle``: - - .. code-block:: bash - - $ composer require twig/extra-bundle - - Otherwise, add the extension explicitly on the Twig environment:: - - use Twig\Extra\String\StringExtension; - - $twig = new \Twig\Environment(...); - $twig->addExtension(new StringExtension()); - -Arguments ---------- - -* ``lang``: The lang of the original string. Only English (`en`) and French (`fr`) are supported. -* ``singleResult``: This argument is optional. If set to false, the filter will return an array of pluralized words. Default is true. diff --git a/doc/filters/singular.rst b/doc/filters/singular.rst new file mode 100644 index 00000000000..0a441da5b68 --- /dev/null +++ b/doc/filters/singular.rst @@ -0,0 +1,52 @@ +``singular`` +============ + +.. versionadded:: 3.11 + + The ``singular`` filter was added in Twig 3.11. + +The ``singular`` filter transforms a given noun in its plural form into its +singular version: + +.. code-block:: twig + + {# English (en) rules are used by default #} + {{ 'partitions'|singular() }} + partition + + {{ 'partitions'|singular('fr') }} + partition + +.. note:: + + The ``singular`` filter is part of the ``StringExtension`` which is not + installed by default. Install it first: + + .. code-block:: bash + + $ composer require twig/string-extra + + Then, on Symfony projects, install the ``twig/extra-bundle``: + + .. code-block:: bash + + $ composer require twig/extra-bundle + + Otherwise, add the extension explicitly on the Twig environment:: + + use Twig\Extra\String\StringExtension; + + $twig = new \Twig\Environment(...); + $twig->addExtension(new StringExtension()); + +Arguments +--------- + +* ``locale``: The locale of the original string (limited to languages supported by the from Symfony `inflector`_, part of the String component) +* ``all``: Whether to return all possible plurals as an array, default is ``false`` + +.. note:: + + Internally, Twig uses the `singularize`_ method from the Symfony String component. + +.. _`singularize`: diff --git a/doc/filters/singularize.rst b/doc/filters/singularize.rst deleted file mode 100644 index 3488127c263..00000000000 --- a/doc/filters/singularize.rst +++ /dev/null @@ -1,45 +0,0 @@ -``singularize`` -======== - -The ``singularize`` filter transforms a given noun in its plural form into its singular version. - -Here is an example: - -.. code-block:: twig - - {{ 'partitions'|singularize('en') }} - partition - -.. note:: - `lang` parameter is mandatory for this filter as only English and French are supported by the Inflector in Symfony. - -The ``singularize`` filter uses the method by the same name in Symfony's -`Inflector `_. - -.. note:: - - The ``singularize`` filter is part of the ``StringExtension`` which is not - installed by default. Install it first: - - .. code-block:: bash - - $ composer require twig/string-extra - - Then, on Symfony projects, install the ``twig/extra-bundle``: - - .. code-block:: bash - - $ composer require twig/extra-bundle - - Otherwise, add the extension explicitly on the Twig environment:: - - use Twig\Extra\String\StringExtension; - - $twig = new \Twig\Environment(...); - $twig->addExtension(new StringExtension()); - -Arguments ---------- - -* ``lang``: The lang of the original string. Only English (`en`) and French (`fr`) are supported. -* ``singleResult``: This argument is optional. If set to false, the filter will return an array of singularized nouns. Default is true. diff --git a/extra/string-extra/README.md b/extra/string-extra/README.md index e2569c6599d..c5a8ca4b9d9 100644 --- a/extra/string-extra/README.md +++ b/extra/string-extra/README.md @@ -2,21 +2,20 @@ String Extension ================ This package is a Twig extension that provides integration with the Symfony -String component. +String component. It provides the following filters: -It provides a [`u`][1] filter that wraps a text in a `UnicodeString` -object to give access to [methods of the class][2]. + * [`u`][1]: Wraps a text in a `UnicodeString` object to give access to +[methods of the class][2]. -It also provides a [`slug`][3] filter which is simply a wrapper for the -[`AsciiSlugger`][4]'s `slug` method. + * [`slug`][3]: Wraps the [`AsciiSlugger`][4]'s `slug` method. -In addition, two filters are provided to [`singularize`][5] and [`pluralize`][6] a noun. -Behind the scenes, it uses the [`Inflector`][6] class from the Symfony String component. + * [`singular`][5] and [`plural`][6]: Wraps the [`Inflector`][7] `singularize` + and `pluralize` methods. [1]: https://twig.symfony.com/u [2]: https://symfony.com/doc/current/components/string.html [3]: https://twig.symfony.com/slug [4]: https://symfony.com/doc/current/components/string.html#slugger -[5]: https://twig.symfony.com/singularize -[6]: https://twig.symfony.com/pluralize +[5]: https://twig.symfony.com/singular +[6]: https://twig.symfony.com/plural [7]: https://symfony.com/doc/current/components/string.html#inflector diff --git a/extra/string-extra/StringExtension.php b/extra/string-extra/StringExtension.php index a1322d4dd7a..e541e7c16d4 100644 --- a/extra/string-extra/StringExtension.php +++ b/extra/string-extra/StringExtension.php @@ -37,8 +37,8 @@ public function getFilters() return [ new TwigFilter('u', [$this, 'createUnicodeString']), new TwigFilter('slug', [$this, 'createSlug']), - new TwigFilter('pluralize', [$this, 'pluralize']), - new TwigFilter('singularize', [$this, 'singularize']), + new TwigFilter('plural', [$this, 'plural']), + new TwigFilter('singular', [$this, 'singular']), ]; } @@ -55,38 +55,36 @@ public function createSlug(string $string, string $separator = '-', ?string $loc /** * @return array|string */ - public function pluralize(string $value, string $lang, bool $singleResult = true) + public function plural(string $value, string $locale = 'en', bool $all = false) { - switch (true) { - case $singleResult: - return $this->getInflector($lang)->pluralize($value)[0]; - default: - return $this->getInflector($lang)->pluralize($value); + if ($all) { + return $this->getInflector($locale)->pluralize($value); } + + return $this->getInflector($locale)->pluralize($value)[0]; } /** * @return array|string */ - public function singularize(string $value, string $lang, bool $singleResult = true) + public function singular(string $value, string $locale = 'en', bool $all = false) { - switch (true) { - case $singleResult: - return $this->getInflector($lang)->singularize($value)[0]; - default: - return $this->getInflector($lang)->singularize($value); + if ($all) { + return $this->getInflector($locale)->singularize($value); } + + return $this->getInflector($locale)->singularize($value)[0]; } - private function getInflector(string $lang): InflectorInterface + private function getInflector(string $locale): InflectorInterface { - switch ($lang) { + switch ($locale) { case 'fr': return $this->frenchInflector ?? $this->frenchInflector = new FrenchInflector(); case 'en': return $this->englishInflector ?? $this->englishInflector = new EnglishInflector(); default: - throw new \InvalidArgumentException(sprintf('Language "%s" is not supported.', $lang)); + throw new \InvalidArgumentException(sprintf('Locale "%s" is not supported.', $locale)); } } } diff --git a/extra/string-extra/Tests/Fixtures/pluralize-invalid-language.test b/extra/string-extra/Tests/Fixtures/plural-invalid-language.test similarity index 79% rename from extra/string-extra/Tests/Fixtures/pluralize-invalid-language.test rename to extra/string-extra/Tests/Fixtures/plural-invalid-language.test index 766f8b895a2..cbbe0ee26a0 100755 --- a/extra/string-extra/Tests/Fixtures/pluralize-invalid-language.test +++ b/extra/string-extra/Tests/Fixtures/plural-invalid-language.test @@ -1,7 +1,7 @@ --TEST-- -"pluralize" filter +"plural" filter --TEMPLATE-- -{{ 'partition'|pluralize('it') }} +{{ 'partition'|plural('it') }} --DATA-- return [] diff --git a/extra/string-extra/Tests/Fixtures/plural.test b/extra/string-extra/Tests/Fixtures/plural.test new file mode 100755 index 00000000000..b561e2ad0d6 --- /dev/null +++ b/extra/string-extra/Tests/Fixtures/plural.test @@ -0,0 +1,15 @@ +--TEST-- +"plural" filter +--TEMPLATE-- +{{ 'partition'|plural('fr') }} +{{ 'partition'|plural('fr', all=true)|join(',') }} +{{ 'person'|plural('fr') }} +{{ 'person'|plural('en', all=true)|join(',') }} + +--DATA-- +return [] +--EXPECT-- +partitions +partitions +persons +persons,people diff --git a/extra/string-extra/Tests/Fixtures/pluralize.test b/extra/string-extra/Tests/Fixtures/pluralize.test deleted file mode 100755 index bfea9747be7..00000000000 --- a/extra/string-extra/Tests/Fixtures/pluralize.test +++ /dev/null @@ -1,17 +0,0 @@ ---TEST-- -"pluralize" filter ---TEMPLATE-- -{{ 'partition'|pluralize('fr') }} -{{ 'partition'|pluralize('fr', false)|first }} -{{ 'person'|pluralize('fr') }} -{{ 'person'|pluralize('en', false)|first }} -{{ 'person'|pluralize('en', false)|last }} - ---DATA-- -return [] ---EXPECT-- -partitions -partitions -persons -persons -people diff --git a/extra/string-extra/Tests/Fixtures/singularizelize-invalid-language.test b/extra/string-extra/Tests/Fixtures/singular-invalid-language.test similarity index 78% rename from extra/string-extra/Tests/Fixtures/singularizelize-invalid-language.test rename to extra/string-extra/Tests/Fixtures/singular-invalid-language.test index 10300a8c1a5..c56cdd3e7f8 100755 --- a/extra/string-extra/Tests/Fixtures/singularizelize-invalid-language.test +++ b/extra/string-extra/Tests/Fixtures/singular-invalid-language.test @@ -1,7 +1,7 @@ --TEST-- -"singularize" filter +"singular" filter --TEMPLATE-- -{{ 'partitions'|singularize('it') }} +{{ 'partitions'|singular('it') }} --DATA-- return [] diff --git a/extra/string-extra/Tests/Fixtures/singular.test b/extra/string-extra/Tests/Fixtures/singular.test new file mode 100755 index 00000000000..01e03db66a6 --- /dev/null +++ b/extra/string-extra/Tests/Fixtures/singular.test @@ -0,0 +1,19 @@ +--TEST-- +"singular" filter +--TEMPLATE-- +{{ 'partitions'|singular('fr') }} +{{ 'partitions'|singular('fr', all=true)|join(',') }} +{{ 'persons'|singular('fr') }} +{{ 'persons'|singular('en', all=true)|join(',') }} +{{ 'people'|singular('en') }} +{{ 'people'|singular('en', all=true)|join(',') }} + +--DATA-- +return [] +--EXPECT-- +partition +partition +person +person +person +person diff --git a/extra/string-extra/Tests/Fixtures/singularize.test b/extra/string-extra/Tests/Fixtures/singularize.test deleted file mode 100755 index 307fadeaa7d..00000000000 --- a/extra/string-extra/Tests/Fixtures/singularize.test +++ /dev/null @@ -1,19 +0,0 @@ ---TEST-- -"singularize" filter ---TEMPLATE-- -{{ 'partitions'|singularize('fr') }} -{{ 'partitions'|singularize('fr', false)|first }} -{{ 'persons'|singularize('fr') }} -{{ 'persons'|singularize('en', false)|first }} -{{ 'people'|singularize('en') }} -{{ 'people'|singularize('en', false)|first }} - ---DATA-- -return [] ---EXPECT-- -partition -partition -person -person -person -person From 82d31319bbe19f2c50ddc97c2a2a0a8b47db6b4f Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Fri, 5 Jul 2024 07:59:36 +0200 Subject: [PATCH 277/812] Fix tests --- extra/string-extra/Tests/Fixtures/plural-invalid-language.test | 2 +- .../string-extra/Tests/Fixtures/singular-invalid-language.test | 2 +- tests/drupal_test.sh | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/extra/string-extra/Tests/Fixtures/plural-invalid-language.test b/extra/string-extra/Tests/Fixtures/plural-invalid-language.test index cbbe0ee26a0..9e3851daac4 100755 --- a/extra/string-extra/Tests/Fixtures/plural-invalid-language.test +++ b/extra/string-extra/Tests/Fixtures/plural-invalid-language.test @@ -7,4 +7,4 @@ return [] --EXCEPTION-- -Twig\Error\RuntimeError: An exception has been thrown during the rendering of a template ("Language "it" is not supported.") in "index.twig" at line 2. \ No newline at end of file +Twig\Error\RuntimeError: An exception has been thrown during the rendering of a template ("Locale "it" is not supported.") in "index.twig" at line 2. \ No newline at end of file diff --git a/extra/string-extra/Tests/Fixtures/singular-invalid-language.test b/extra/string-extra/Tests/Fixtures/singular-invalid-language.test index c56cdd3e7f8..deaa6fbffb5 100755 --- a/extra/string-extra/Tests/Fixtures/singular-invalid-language.test +++ b/extra/string-extra/Tests/Fixtures/singular-invalid-language.test @@ -7,4 +7,4 @@ return [] --EXCEPTION-- -Twig\Error\RuntimeError: An exception has been thrown during the rendering of a template ("Language "it" is not supported.") in "index.twig" at line 2. \ No newline at end of file +Twig\Error\RuntimeError: An exception has been thrown during the rendering of a template ("Locale "it" is not supported.") in "index.twig" at line 2. \ No newline at end of file diff --git a/tests/drupal_test.sh b/tests/drupal_test.sh index 29c71f21670..eff75f2495e 100644 --- a/tests/drupal_test.sh +++ b/tests/drupal_test.sh @@ -9,6 +9,7 @@ rm -rf drupal-twig-test composer create-project --no-interaction drupal/recommended-project:10.1.x-dev drupal-twig-test cd drupal-twig-test (cd vendor/twig && rm -rf twig && ln -sf $REPO twig) +composer dump-autoload php ./web/core/scripts/drupal install --no-interaction demo_umami > output perl -p -i -e 's/^([A-Za-z]+)\: (.+)$/export DRUPAL_\1=\2/' output source output From b37417972ceda5a6d76ef21f16681e93a38ca3b7 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Fri, 5 Jul 2024 08:05:05 +0200 Subject: [PATCH 278/812] Add missing reference in docs --- doc/filters/singular.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/filters/singular.rst b/doc/filters/singular.rst index 0a441da5b68..9a9e03ff19b 100644 --- a/doc/filters/singular.rst +++ b/doc/filters/singular.rst @@ -49,4 +49,5 @@ Arguments Internally, Twig uses the `singularize`_ method from the Symfony String component. +.. _`inflector`: .. _`singularize`: From 17430ae5acc0931834c2f1e88f591730e5ef54f5 Mon Sep 17 00:00:00 2001 From: sarah-eit Date: Fri, 19 Jan 2024 11:33:56 +0100 Subject: [PATCH 279/812] Add shuffle filter --- doc/filters/shuffle.rst | 89 +++++++++++++++++++++++++++++ src/Extension/CoreExtension.php | 31 ++++++++++ tests/Fixtures/filters/shuffle.test | 16 ++++++ 3 files changed, 136 insertions(+) create mode 100644 doc/filters/shuffle.rst create mode 100644 tests/Fixtures/filters/shuffle.test diff --git a/doc/filters/shuffle.rst b/doc/filters/shuffle.rst new file mode 100644 index 00000000000..31020059e12 --- /dev/null +++ b/doc/filters/shuffle.rst @@ -0,0 +1,89 @@ +``shuffle`` +=========== + +The ``shuffle`` filter shuffles a sequence, a mapping, or a string: + +.. code-block:: twig + + {% for user in users|shuffle %} + ... + {% endfor %} + +.. caution:: + + The shuffled array does not preserve keys. So if the input had not sequential keys + but indexed keys (using the user id for instance), + it is not the case anymore after shuffling it. + +Example 1: + +.. code-block:: html+twig + + {% set items = [ + 'a', + 'b', + 'c', + ] %} + +
    + {% for item in items|shuffle %} +
  • {{ item }}
  • + {% endfor %} +
+ +The above example will be rendered as: + +.. code-block:: html + +
    +
  • a
  • +
  • c
  • +
  • b
  • +
+ +Note, results can also be : +"a, b, c" or "b, a, c" or "b, c, a" or "c, a, b" or "c, b, a". + +Example 2: + +.. code-block:: html+twig + + {% set items = { + 'a': 'd', + 'b': 'e', + 'c': 'f', + } %} + +
    + {% for index, item in items|shuffle %} +
  • {{ index }} - {{ item }}
  • + {% endfor %} +
+ +The above example will be rendered as: + +.. code-block:: html + +
    +
  • 0 - d
  • +
  • 1 - f
  • +
  • 2 - e
  • +
+ +Note, results can also be : +"d, e, f" or "e, d, f" or "e, f, d" or "f, d, e" or "f, e, d". + +.. code-block:: html+twig + + {% set string = 'ghi' %} + +

{{ string|shuffle }}

+ +The above example will be rendered as: + +.. code-block:: html + +

gih

+ +Note, results can also be : +"ghi" or "hgi" or "hig" or "igh" or "ihg". diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index d6bcfffb229..26a9a734490 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -221,6 +221,7 @@ public function getFilters(): array // string/array filters new TwigFilter('reverse', [self::class, 'reverse'], ['needs_charset' => true]), + new TwigFilter('shuffle', [self::class, 'shuffle'], ['needs_charset' => true]), new TwigFilter('length', [self::class, 'length'], ['needs_charset' => true]), new TwigFilter('slice', [self::class, 'slice'], ['needs_charset' => true]), new TwigFilter('first', [self::class, 'first'], ['needs_charset' => true]), @@ -897,6 +898,36 @@ public static function reverse(string $charset, $item, $preserveKeys = false) return $string; } + /** + * Shuffles an array, a \Traversable instance, or a string. + * The function does not preserve keys. + * + * @internal + */ + public static function shuffle(string $charset, array|\Traversable|string|null $item): mixed + { + if (\is_string($item)) { + if ('UTF-8' !== $charset) { + $item = self::convertEncoding($item, 'UTF-8', $charset); + } + + $item = preg_split('/(? new \ArrayObject([0 => 3, 1 => 2, 2 => 1])] +--EXPECT-- +3 +2 +6 +3 +3 From 2dacaadd98c117572bce5a2f2a66aff28bc994e4 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Fri, 5 Jul 2024 09:24:50 +0200 Subject: [PATCH 280/812] Tweak code, tests, and docs for shuffle --- CHANGELOG | 1 + doc/filters/index.rst | 1 + doc/filters/shuffle.rst | 21 ++++++++++++--------- src/Extension/CoreExtension.php | 8 +++++++- tests/Fixtures/filters/shuffle.test | 22 +++++++++++----------- 5 files changed, 32 insertions(+), 21 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 7f5003f34f0..c19c9572f68 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,6 @@ # 3.11.0 (2024-XX-XX) + * Add the `shuffle` filter * Add the `singular` and `plural` filters in `StringExtension` * Deprecate the second argument of `Twig\Node\Expression\CallExpression::compileArguments()` * Deprecate `Twig\ExpressionParser\parseHashExpression()` in favor of diff --git a/doc/filters/index.rst b/doc/filters/index.rst index 64b49aa9d3e..7d2bde1b1b0 100644 --- a/doc/filters/index.rst +++ b/doc/filters/index.rst @@ -47,6 +47,7 @@ Filters replace reverse round + shuffle singular slice slug diff --git a/doc/filters/shuffle.rst b/doc/filters/shuffle.rst index 31020059e12..9ade4f01130 100644 --- a/doc/filters/shuffle.rst +++ b/doc/filters/shuffle.rst @@ -1,6 +1,10 @@ ``shuffle`` =========== +.. versionadded:: 3.11 + + The ``shuffle`` filter was added in Twig 3.11. + The ``shuffle`` filter shuffles a sequence, a mapping, or a string: .. code-block:: twig @@ -11,9 +15,9 @@ The ``shuffle`` filter shuffles a sequence, a mapping, or a string: .. caution:: - The shuffled array does not preserve keys. So if the input had not sequential keys - but indexed keys (using the user id for instance), - it is not the case anymore after shuffling it. + The shuffled array does not preserve keys. So if the input had not + sequential keys but indexed keys (using the user id for instance), it is + not the case anymore after shuffling it. Example 1: @@ -41,8 +45,8 @@ The above example will be rendered as:
  • b
  • -Note, results can also be : -"a, b, c" or "b, a, c" or "b, c, a" or "c, a, b" or "c, b, a". +The result can also be: "a, b, c" or "b, a, c" or "b, c, a" or "c, a, b" or +"c, b, a". Example 2: @@ -70,8 +74,8 @@ The above example will be rendered as:
  • 2 - e
  • -Note, results can also be : -"d, e, f" or "e, d, f" or "e, f, d" or "f, d, e" or "f, e, d". +The result can also be: "d, e, f" or "e, d, f" or "e, f, d" or "f, d, e" or +"f, e, d". .. code-block:: html+twig @@ -85,5 +89,4 @@ The above example will be rendered as:

    gih

    -Note, results can also be : -"ghi" or "hgi" or "hig" or "igh" or "ihg". +The result can also be: "ghi" or "hgi" or "hig" or "igh" or "ihg". diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index 26a9a734490..ea61b7a5b17 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -902,9 +902,13 @@ public static function reverse(string $charset, $item, $preserveKeys = false) * Shuffles an array, a \Traversable instance, or a string. * The function does not preserve keys. * + * @param array|\Traversable|string|null $item + * + * @return mixed + * * @internal */ - public static function shuffle(string $charset, array|\Traversable|string|null $item): mixed + public static function shuffle(string $charset, $item) { if (\is_string($item)) { if ('UTF-8' !== $charset) { @@ -918,6 +922,8 @@ public static function shuffle(string $charset, array|\Traversable|string|null $ if ('UTF-8' !== $charset) { $item = self::convertEncoding($item, $charset, 'UTF-8'); } + + return $item; } if ($item instanceof \Traversable || \is_array($item)) { diff --git a/tests/Fixtures/filters/shuffle.test b/tests/Fixtures/filters/shuffle.test index 4d96ab0af8a..5a4029dccf0 100644 --- a/tests/Fixtures/filters/shuffle.test +++ b/tests/Fixtures/filters/shuffle.test @@ -1,16 +1,16 @@ --TEST-- "shuffle" filter --TEMPLATE-- -{{ 'bar'|shuffle|length }} -{{ [3, 1]|shuffle|join()|length }} -{{ ['foo', 'bar']|shuffle|join()|length }} -{{ {'a': 'd', 'b': 'e', 'c': 'f'}|shuffle|join()|length }} -{{ traversable|shuffle|join|length }} +{% set test = 'ok'|shuffle %}{{ 'ok' is same as test or 'ko' is same as test ? 'ok' : 'ko' }} +{% set test = [3, 1]|shuffle %}{{ [3, 1] is same as test or [1, 3] is same as test ? 'ok' : 'ko' }} +{% set test = ['foo', 'bar']|shuffle %}{{ ['foo', 'bar'] is same as test or ['bar', 'foo'] is same as test ? 'ok' : 'ko' }} +{% set test = {'a': 'd', 'b': 'e'}|shuffle %}{{ ['d', 'e'] is same as test or ['e', 'd'] is same as test ? 'ok' : 'ko' }} +{% set test = traversable|shuffle %}{{ [3, 1] is same as test or [1, 3] is same as test ? 'ok' : 'ko' }} --DATA-- -return ['traversable' => new \ArrayObject([0 => 3, 1 => 2, 2 => 1])] +return ['traversable' => new \ArrayObject([0 => 3, 1 => 1])] --EXPECT-- -3 -2 -6 -3 -3 +ok +ok +ok +ok +ok From f21cb5a6c22d4aaadca58702e71d4e71e9e62abe Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Fri, 5 Jul 2024 09:40:27 +0200 Subject: [PATCH 281/812] Fixed typo --- CHANGELOG | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 7f5003f34f0..ae6bfe90e63 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -6,7 +6,7 @@ `Twig\ExpressionParser::parseMappingExpression()` * Deprecate `Twig\ExpressionParser\parseArrayExpression()`` in favor of `Twig\ExpressionParser::parseSequenceExpression()` - * Add `sequence` and `mapping` tests. + * Add `sequence` and `mapping` tests # 3.10.3 (2024-05-16) From 8602b7f3dc96a26f3a69dd113f0d922006516867 Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Fri, 5 Jul 2024 23:09:31 +0200 Subject: [PATCH 282/812] simplify an iterable check --- src/Extension/CoreExtension.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index ea61b7a5b17..d0e1dccc7c7 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -926,7 +926,7 @@ public static function shuffle(string $charset, $item) return $item; } - if ($item instanceof \Traversable || \is_array($item)) { + if (is_iterable($item)) { $item = self::toArray($item, false); shuffle($item); } From c79c7e7a093be2d818a71f3252c5d77ce7fa7684 Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Fri, 5 Jul 2024 23:26:55 +0200 Subject: [PATCH 283/812] fix the set of installed packages for TwigExtraBundle on PHP 8.0 --- .github/workflows/ci.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6f9588cb311..bf2cafdb6dc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -114,6 +114,11 @@ jobs: - name: "PHPUnit version" run: vendor/bin/simple-phpunit --version + - name: "Prevent installing symfony/translation-contract 3.0" + if: "matrix.extension == 'twig-extra-bundle'" + working-directory: extra/${{ matrix.extension }} + run: "composer require --no-update 'symfony/translation-contracts:^1.1|^2.0'" + - name: "Composer install ${{ matrix.extension }}" working-directory: extra/${{ matrix.extension }} run: composer install From 07967dea802a8b0584daaaba0e4f2b574875f03f Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Fri, 5 Jul 2024 23:56:22 +0200 Subject: [PATCH 284/812] fix condition for running use_yield test with extra packages --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6f9588cb311..94c5a6abd2e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -119,7 +119,7 @@ jobs: run: composer install - name: "Switch use_yield to true" - if: "matrix.php == '8.2'" + if: "matrix.php-version == '8.2'" run: | sed -i -e "s/'use_yield' => false/'use_yield' => true/" extra/${{ matrix.extension }}/vendor/twig/twig/src/Environment.php From 17f7ee34df80a8c4492752fc914a26e0c4d608f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Andr=C3=A9?= Date: Sun, 7 Jul 2024 13:04:07 +0200 Subject: [PATCH 285/812] [Doc] Add missing Token types * Document Token::OPERATOR_TYPE * Token::SPREAD_TYPE --- doc/internals.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/doc/internals.rst b/doc/internals.rst index 2aeb12f3fc4..c9f23ce5c64 100644 --- a/doc/internals.rst +++ b/doc/internals.rst @@ -30,7 +30,7 @@ The Lexer The lexer tokenizes a template source code into a token stream (each token is an instance of ``\Twig\Token``, and the stream is an instance of -``\Twig\TokenStream``). The default lexer recognizes 13 different token types: +``\Twig\TokenStream``). The default lexer recognizes 15 different token types: * ``\Twig\Token::BLOCK_START_TYPE``, ``\Twig\Token::BLOCK_END_TYPE``: Delimiters for blocks (``{% %}``) * ``\Twig\Token::VAR_START_TYPE``, ``\Twig\Token::VAR_END_TYPE``: Delimiters for variables (``{{ }}``) @@ -39,6 +39,8 @@ an instance of ``\Twig\Token``, and the stream is an instance of * ``\Twig\Token::NUMBER_TYPE``: A number in an expression; * ``\Twig\Token::STRING_TYPE``: A string in an expression; * ``\Twig\Token::OPERATOR_TYPE``: An operator; +* ``\Twig\Token::ARROW_TYPE``: A arrow function operator (``=>``); +* ``\Twig\Token::SPREAD_TYPE``: A spread operator (``...``); * ``\Twig\Token::PUNCTUATION_TYPE``: A punctuation sign; * ``\Twig\Token::INTERPOLATION_START_TYPE``, ``\Twig\Token::INTERPOLATION_END_TYPE``: Delimiters for string interpolation; * ``\Twig\Token::EOF_TYPE``: Ends of template. From d54d8438a5cbefe034c5e9d9a884661b845cbe4b Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Sun, 7 Jul 2024 23:44:32 +0200 Subject: [PATCH 286/812] fix grammar for article --- doc/internals.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/internals.rst b/doc/internals.rst index c9f23ce5c64..97661411dd4 100644 --- a/doc/internals.rst +++ b/doc/internals.rst @@ -39,7 +39,7 @@ an instance of ``\Twig\Token``, and the stream is an instance of * ``\Twig\Token::NUMBER_TYPE``: A number in an expression; * ``\Twig\Token::STRING_TYPE``: A string in an expression; * ``\Twig\Token::OPERATOR_TYPE``: An operator; -* ``\Twig\Token::ARROW_TYPE``: A arrow function operator (``=>``); +* ``\Twig\Token::ARROW_TYPE``: An arrow function operator (``=>``); * ``\Twig\Token::SPREAD_TYPE``: A spread operator (``...``); * ``\Twig\Token::PUNCTUATION_TYPE``: A punctuation sign; * ``\Twig\Token::INTERPOLATION_START_TYPE``, ``\Twig\Token::INTERPOLATION_END_TYPE``: Delimiters for string interpolation; From 1c6f44e095341ef682e95d46c116a482dbf773f9 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Mon, 8 Jul 2024 14:32:51 +0200 Subject: [PATCH 287/812] Fix markup --- doc/api.rst | 2 +- doc/filters/format_number.rst | 4 ++-- doc/filters/trim.rst | 5 +++-- doc/tags/for.rst | 8 ++++---- 4 files changed, 10 insertions(+), 9 deletions(-) diff --git a/doc/api.rst b/doc/api.rst index 84b9d558593..7b780189362 100644 --- a/doc/api.rst +++ b/doc/api.rst @@ -431,7 +431,7 @@ The escaping rules are implemented as follows: {{ var|upper }} {# is equivalent to {{ var|upper|escape }} #} -* The `raw` filter should only be used at the end of the filter chain: +* The ``raw`` filter should only be used at the end of the filter chain: .. code-block:: twig diff --git a/doc/filters/format_number.rst b/doc/filters/format_number.rst index a1c2804ab4b..994404e07d2 100644 --- a/doc/filters/format_number.rst +++ b/doc/filters/format_number.rst @@ -62,8 +62,8 @@ The list of supported styles: * ``ordinal``; * ``duration``. -As a shortcut, you can use the ``format_*_number`` filters by replacing `*` with -a style: +As a shortcut, you can use the ``format_*_number`` filters by replacing ``*`` +with a style: .. code-block:: twig diff --git a/doc/filters/trim.rst b/doc/filters/trim.rst index a3d36ca0345..238928bc78a 100644 --- a/doc/filters/trim.rst +++ b/doc/filters/trim.rst @@ -31,8 +31,9 @@ Arguments * ``character_mask``: The characters to strip -* ``side``: The default is to strip from the left and the right (`both`) sides, but `left` - and `right` will strip from either the left side or right side only +* ``side``: The default is to strip from the left and the right (``both``) + sides, but ``left`` and ``right`` will strip from either the left side or + right side only .. _`trim`: https://www.php.net/trim .. _`ltrim`: https://www.php.net/ltrim diff --git a/doc/tags/for.rst b/doc/tags/for.rst index 3bd859bd05c..656d9c07b17 100644 --- a/doc/tags/for.rst +++ b/doc/tags/for.rst @@ -50,8 +50,8 @@ The ``..`` operator can take any expression at both sides: If you need a step different from 1, you can use the ``range`` function instead. -The `loop` variable -------------------- +The ``loop`` variable +--------------------- Inside of a ``for`` loop block you can access some special variables: @@ -80,8 +80,8 @@ Variable Description ``loop.last`` variables are only available for PHP arrays, or objects that implement the ``Countable`` interface. -The `else` Clause ------------------ +The ``else`` Clause +------------------- If no iteration took place because the sequence was empty, you can render a replacement block by using ``else``: From 133cbb77536bcb0e8c553e0808e795ecccb91826 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Tue, 9 Jul 2024 10:19:44 +0200 Subject: [PATCH 288/812] Add more tests --- tests/NodeVisitor/OptimizerTest.php | 36 ++++++++++++++++++++++++----- 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/tests/NodeVisitor/OptimizerTest.php b/tests/NodeVisitor/OptimizerTest.php index e6435ca0273..96d5ef5b11c 100644 --- a/tests/NodeVisitor/OptimizerTest.php +++ b/tests/NodeVisitor/OptimizerTest.php @@ -15,6 +15,7 @@ use Twig\Environment; use Twig\Loader\LoaderInterface; use Twig\Node\Expression\BlockReferenceExpression; +use Twig\Node\Expression\NameExpression; use Twig\Node\Expression\ParentExpression; use Twig\Node\ForNode; use Twig\Node\Node; @@ -46,21 +47,44 @@ public function testRenderParentBlockOptimizer() $this->assertTrue($node->getAttribute('output')); } + public function testForVarOptimizer() + { + $env = new Environment($this->createMock(LoaderInterface::class), ['cache' => false, 'autoescape' => false]); + + $template = '{% for i, j in foo %}{{ loop.index }}{{ i }}{{ j }}{% endfor %}'; + $stream = $env->parse($env->tokenize(new Source($template, 'index'))); + + foreach (['loop', 'i', 'j'] as $target) { + $this->checkForVarConfiguration($stream, $target); + } + } + + public function checkForVarConfiguration(Node $node, $target) + { + foreach ($node as $n) { + if (NameExpression::class === get_class($n) && $target === $n->getAttribute('name')) { + $this->assertTrue($n->getAttribute('always_defined')); + } else { + $this->checkForVarConfiguration($n, $target); + } + } + } + /** - * @dataProvider getTestsForForOptimizer + * @dataProvider getTestsForForLoopOptimizer */ - public function testForOptimizer($template, $expected) + public function testForLoopOptimizer($template, $expected) { $env = new Environment($this->createMock(LoaderInterface::class), ['cache' => false]); $stream = $env->parse($env->tokenize(new Source($template, 'index'))); foreach ($expected as $target => $withLoop) { - $this->assertTrue($this->checkForConfiguration($stream, $target, $withLoop), \sprintf('variable %s is %soptimized', $target, $withLoop ? 'not ' : '')); + $this->assertTrue($this->checkForLoopConfiguration($stream, $target, $withLoop), \sprintf('variable %s is %soptimized', $target, $withLoop ? 'not ' : '')); } } - public function getTestsForForOptimizer() + public function getTestsForForLoopOptimizer() { return [ ['{% for i in foo %}{% endfor %}', ['i' => false]], @@ -99,7 +123,7 @@ public function getTestsForForOptimizer() ]; } - public function checkForConfiguration(Node $node, $target, $withLoop) + public function checkForLoopConfiguration(Node $node, $target, $withLoop) { foreach ($node as $n) { if ($n instanceof ForNode) { @@ -108,7 +132,7 @@ public function checkForConfiguration(Node $node, $target, $withLoop) } } - $ret = $this->checkForConfiguration($n, $target, $withLoop); + $ret = $this->checkForLoopConfiguration($n, $target, $withLoop); if (null !== $ret) { return $ret; } From 10deb7328c7cd37391b5eeb777008ae235e01278 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Andr=C3=A9?= Date: Wed, 10 Jul 2024 00:23:00 +0200 Subject: [PATCH 289/812] [Doc] Fix `do` tag label in link --- doc/advanced.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/advanced.rst b/doc/advanced.rst index e4f9401c6b8..c1073242fd5 100644 --- a/doc/advanced.rst +++ b/doc/advanced.rst @@ -413,7 +413,7 @@ Most of the time though, a tag is not needed: * If your tag does not output anything, but only exists because of a side effect, create a **function** that returns nothing and call it via the - :doc:`filter ` tag. + :doc:`do ` tag. For instance, if you want to create a tag that logs text, create a ``log`` function instead and call it via the :doc:`do ` tag: From 6f12d008be3a8a9ba9517607c322bcdcbc53ff0c Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sun, 14 Jul 2024 11:04:52 +0200 Subject: [PATCH 290/812] Make a small optimization --- src/Node/CheckSecurityCallNode.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Node/CheckSecurityCallNode.php b/src/Node/CheckSecurityCallNode.php index 66aaeb52c29..9c162d129c6 100644 --- a/src/Node/CheckSecurityCallNode.php +++ b/src/Node/CheckSecurityCallNode.php @@ -23,7 +23,7 @@ class CheckSecurityCallNode extends Node public function compile(Compiler $compiler) { $compiler - ->write("\$this->sandbox = \$this->env->getExtension(SandboxExtension::class);\n") + ->write("\$this->sandbox = \$this->extensions[SandboxExtension::class];\n") ->write("\$this->checkSecurity();\n") ; } From 093f51a8b98796ea3db8bd43a0c23170447dd538 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sun, 14 Jul 2024 11:24:35 +0200 Subject: [PATCH 291/812] Extract ReflectionCallable to make it reusable --- src/Node/Expression/CallExpression.php | 56 +++-------------- src/Util/ReflectionCallable.php | 86 ++++++++++++++++++++++++++ 2 files changed, 96 insertions(+), 46 deletions(-) create mode 100644 src/Util/ReflectionCallable.php diff --git a/src/Node/Expression/CallExpression.php b/src/Node/Expression/CallExpression.php index 549e8c43a1a..19b7d1bad67 100644 --- a/src/Node/Expression/CallExpression.php +++ b/src/Node/Expression/CallExpression.php @@ -15,10 +15,11 @@ use Twig\Error\SyntaxError; use Twig\Extension\ExtensionInterface; use Twig\Node\Node; +use Twig\Util\ReflectionCallable; abstract class CallExpression extends AbstractExpression { - private $reflector; + private ?ReflectionCallable $reflector = null; protected function compileCallable(Compiler $compiler) { @@ -27,7 +28,9 @@ protected function compileCallable(Compiler $compiler) if (\is_string($callable) && !str_contains($callable, '::')) { $compiler->raw($callable); } else { - [$r, $callable] = $this->reflectCallable($callable); + $rc = $this->reflectCallable($callable); + $r = $rc->getReflector(); + $callable = $rc->getCallable(); if (\is_string($callable)) { $compiler->raw($callable); @@ -251,7 +254,9 @@ protected function normalizeName(string $name): string private function getCallableParameters($callable, bool $isVariadic): array { - [$r, , $callableName] = $this->reflectCallable($callable); + $rc = $this->reflectCallable($callable); + $r = $rc->getReflector(); + $callableName = $rc->getName(); $parameters = $r->getParameters(); if ($this->hasNode('node')) { @@ -288,49 +293,8 @@ private function getCallableParameters($callable, bool $isVariadic): array return [$parameters, $isPhpVariadic]; } - private function reflectCallable($callable) + private function reflectCallable($callable): ReflectionCallable { - if (null !== $this->reflector) { - return $this->reflector; - } - - if (\is_string($callable) && false !== $pos = strpos($callable, '::')) { - $callable = [substr($callable, 0, $pos), substr($callable, 2 + $pos)]; - } - - if (\is_array($callable) && method_exists($callable[0], $callable[1])) { - $r = new \ReflectionMethod($callable[0], $callable[1]); - - return $this->reflector = [$r, $callable, $r->class.'::'.$r->name]; - } - - $checkVisibility = $callable instanceof \Closure; - try { - $closure = \Closure::fromCallable($callable); - } catch (\TypeError $e) { - throw new \LogicException(\sprintf('Callback for %s "%s" is not callable in the current scope.', $this->getAttribute('type'), $this->getAttribute('name')), 0, $e); - } - $r = new \ReflectionFunction($closure); - - if (str_contains($r->name, '{closure')) { - return $this->reflector = [$r, $callable, 'Closure']; - } - - if ($object = $r->getClosureThis()) { - $callable = [$object, $r->name]; - $callableName = get_debug_type($object).'::'.$r->name; - } elseif (\PHP_VERSION_ID >= 80111 && $class = $r->getClosureCalledClass()) { - $callableName = $class->name.'::'.$r->name; - } elseif (\PHP_VERSION_ID < 80111 && $class = $r->getClosureScopeClass()) { - $callableName = (\is_array($callable) ? $callable[0] : $class->name).'::'.$r->name; - } else { - $callable = $callableName = $r->name; - } - - if ($checkVisibility && \is_array($callable) && method_exists(...$callable) && !(new \ReflectionMethod(...$callable))->isPublic()) { - $callable = $r->getClosure(); - } - - return $this->reflector = [$r, $callable, $callableName]; + return $this->reflector ??= new ReflectionCallable($callable, $this->getAttribute('type'), $this->getAttribute('name')); } } diff --git a/src/Util/ReflectionCallable.php b/src/Util/ReflectionCallable.php new file mode 100644 index 00000000000..5b593e58d48 --- /dev/null +++ b/src/Util/ReflectionCallable.php @@ -0,0 +1,86 @@ + + * + * @internal + */ +final class ReflectionCallable +{ + private \ReflectionFunctionAbstract $reflector; + private \Closure|string|array|null $callable = null; + private string $name; + + public function __construct($callable, string $debugType = 'unknown', string $debugName = 'unknown') + { + if (\is_string($callable) && false !== $pos = strpos($callable, '::')) { + $callable = [substr($callable, 0, $pos), substr($callable, 2 + $pos)]; + } + + if (\is_array($callable) && method_exists($callable[0], $callable[1])) { + $this->reflector = $r = new \ReflectionMethod($callable[0], $callable[1]); + $this->callable = $callable; + $this->name = $r->class.'::'.$r->name; + + return; + } + + $checkVisibility = $callable instanceof \Closure; + try { + $closure = \Closure::fromCallable($callable); + } catch (\TypeError $e) { + throw new \LogicException(\sprintf('Callback for %s "%s" is not callable in the current scope.', $debugType, $debugName), 0, $e); + } + $this->reflector = $r = new \ReflectionFunction($closure); + + if (str_contains($r->name, '{closure')) { + $this->callable = $callable; + $this->name = 'Closure'; + + return; + } + + if ($object = $r->getClosureThis()) { + $callable = [$object, $r->name]; + $this->name = get_debug_type($object).'::'.$r->name; + } elseif (\PHP_VERSION_ID >= 80111 && $class = $r->getClosureCalledClass()) { + $this->name = $class->name.'::'.$r->name; + } elseif (\PHP_VERSION_ID < 80111 && $class = $r->getClosureScopeClass()) { + $this->name = (\is_array($callable) ? $callable[0] : $class->name).'::'.$r->name; + } else { + $callable = $this->name = $r->name; + } + + if ($checkVisibility && \is_array($callable) && method_exists(...$callable) && !(new \ReflectionMethod(...$callable))->isPublic()) { + $callable = $r->getClosure(); + } + + $this->callable = $callable; + } + + public function getReflector(): \ReflectionFunctionAbstract + { + return $this->reflector; + } + + public function getCallable(): \Closure|string|array|null + { + return $this->callable; + } + + public function getName(): string + { + return $this->name; + } +} From bb967b3dd372fbd93629133758b30c4342442e58 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sun, 14 Jul 2024 12:08:28 +0200 Subject: [PATCH 292/812] Make compatible with PHP<8 --- src/Node/Expression/CallExpression.php | 8 ++++++-- src/Util/ReflectionCallable.php | 8 ++++---- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/Node/Expression/CallExpression.php b/src/Node/Expression/CallExpression.php index 19b7d1bad67..997e8dc9f55 100644 --- a/src/Node/Expression/CallExpression.php +++ b/src/Node/Expression/CallExpression.php @@ -19,7 +19,7 @@ abstract class CallExpression extends AbstractExpression { - private ?ReflectionCallable $reflector = null; + private $reflector = null; protected function compileCallable(Compiler $compiler) { @@ -295,6 +295,10 @@ private function getCallableParameters($callable, bool $isVariadic): array private function reflectCallable($callable): ReflectionCallable { - return $this->reflector ??= new ReflectionCallable($callable, $this->getAttribute('type'), $this->getAttribute('name')); + if (!$this->reflector) { + $this->reflector = new ReflectionCallable($callable, $this->getAttribute('type'), $this->getAttribute('name')); + } + + return $this->reflector; } } diff --git a/src/Util/ReflectionCallable.php b/src/Util/ReflectionCallable.php index 5b593e58d48..312a8c7af7d 100644 --- a/src/Util/ReflectionCallable.php +++ b/src/Util/ReflectionCallable.php @@ -18,9 +18,9 @@ */ final class ReflectionCallable { - private \ReflectionFunctionAbstract $reflector; - private \Closure|string|array|null $callable = null; - private string $name; + private $reflector; + private $callable = null; + private $name; public function __construct($callable, string $debugType = 'unknown', string $debugName = 'unknown') { @@ -74,7 +74,7 @@ public function getReflector(): \ReflectionFunctionAbstract return $this->reflector; } - public function getCallable(): \Closure|string|array|null + public function getCallable() { return $this->callable; } From d2eab12e7006b91bece20532e736702a73b8622e Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sun, 14 Jul 2024 10:34:34 +0200 Subject: [PATCH 293/812] Add support for first class callables --- src/Parser.php | 6 ++-- src/Util/ReflectionCallable.php | 2 ++ tests/Node/Expression/FilterTest.php | 11 ++++++ tests/Node/Expression/FilterTestExtension.php | 34 +++++++++++++++++++ 4 files changed, 51 insertions(+), 2 deletions(-) create mode 100644 tests/Node/Expression/FilterTestExtension.php diff --git a/src/Parser.php b/src/Parser.php index 42447dd00b9..7cfc2058c01 100644 --- a/src/Parser.php +++ b/src/Parser.php @@ -25,6 +25,7 @@ use Twig\Node\PrintNode; use Twig\Node\TextNode; use Twig\TokenParser\TokenParserInterface; +use Twig\Util\ReflectionCallable; /** * @author Fabien Potencier @@ -156,8 +157,9 @@ public function subparse($test, bool $dropNeedle = false): Node if (null !== $test) { $e = new SyntaxError(\sprintf('Unexpected "%s" tag', $token->getValue()), $token->getLine(), $this->stream->getSourceContext()); - if (\is_array($test) && isset($test[0]) && $test[0] instanceof TokenParserInterface) { - $e->appendMessage(\sprintf(' (expecting closing tag for the "%s" tag defined near line %s).', $test[0]->getTag(), $lineno)); + $callable = (new ReflectionCallable($test))->getCallable(); + if (\is_array($callable) && $callable[0] instanceof TokenParserInterface) { + $e->appendMessage(\sprintf(' (expecting closing tag for the "%s" tag defined near line %s).', $callable[0]->getTag(), $lineno)); } } else { $e = new SyntaxError(\sprintf('Unknown "%s" tag.', $token->getValue()), $token->getLine(), $this->stream->getSourceContext()); diff --git a/src/Util/ReflectionCallable.php b/src/Util/ReflectionCallable.php index 312a8c7af7d..54384e14bd8 100644 --- a/src/Util/ReflectionCallable.php +++ b/src/Util/ReflectionCallable.php @@ -55,8 +55,10 @@ public function __construct($callable, string $debugType = 'unknown', string $de $callable = [$object, $r->name]; $this->name = get_debug_type($object).'::'.$r->name; } elseif (\PHP_VERSION_ID >= 80111 && $class = $r->getClosureCalledClass()) { + $callable = [$class->name, $r->name]; $this->name = $class->name.'::'.$r->name; } elseif (\PHP_VERSION_ID < 80111 && $class = $r->getClosureScopeClass()) { + $callable = [\is_array($callable) ? $callable[0] : $class->name, $r->name]; $this->name = (\is_array($callable) ? $callable[0] : $class->name).'::'.$r->name; } else { $callable = $this->name = $r->name; diff --git a/tests/Node/Expression/FilterTest.php b/tests/Node/Expression/FilterTest.php index 7234ac31816..06f1baef4ec 100644 --- a/tests/Node/Expression/FilterTest.php +++ b/tests/Node/Expression/FilterTest.php @@ -43,6 +43,9 @@ public function getTests() $environment->addFilter(new TwigFilter('bar_closure', \Closure::fromCallable(twig_tests_filter_dummy::class), ['needs_environment' => true])); $environment->addFilter(new TwigFilter('barbar', 'Twig\Tests\Node\Expression\twig_tests_filter_barbar', ['needs_context' => true, 'is_variadic' => true])); $environment->addFilter(new TwigFilter('magic_static', __NAMESPACE__.'\ChildMagicCallStub::magicStaticCall')); + if (\PHP_VERSION_ID >= 80111) { + $environment->addExtension(new FilterTestExtension()); + } $extension = new class() extends AbstractExtension { public function getFilters(): array @@ -121,6 +124,14 @@ protected function foobar() $node = $this->createFilter($string, 'barbar', ['arg2' => new ConstantExpression('bar', 1)]); $tests[] = [$node, 'Twig\Tests\Node\Expression\twig_tests_filter_barbar($context, "abc", null, "bar")', $environment]; + if (\PHP_VERSION_ID >= 80111) { + $node = $this->createFilter($string, 'first_class_callable_static'); + $tests[] = [$node, 'Twig\Tests\Node\Expression\FilterTestExtension::staticMethod("abc")', $environment]; + + $node = $this->createFilter($string, 'first_class_callable_object'); + $tests[] = [$node, '$this->extensions[\'Twig\Tests\Node\Expression\FilterTestExtension\']->objectMethod("abc")', $environment]; + } + $node = $this->createFilter($string, 'barbar', [ new ConstantExpression('1', 1), new ConstantExpression('2', 1), diff --git a/tests/Node/Expression/FilterTestExtension.php b/tests/Node/Expression/FilterTestExtension.php new file mode 100644 index 00000000000..4a564ae3aa2 --- /dev/null +++ b/tests/Node/Expression/FilterTestExtension.php @@ -0,0 +1,34 @@ +objectMethod(...)), + ]; + } + + public static function staticMethod() + { + } + + public function objectMethod() + { + } +} From b0cd5dc5a810f045edcb269caccd85d4324d586c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Andr=C3=A9?= Date: Sun, 21 Jul 2024 21:05:07 +0200 Subject: [PATCH 294/812] [Doc] Fix javascript code-block Fix code-block syntax (double space) --- doc/recipes.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/recipes.rst b/doc/recipes.rst index 8c1caa8058a..b342457edf2 100644 --- a/doc/recipes.rst +++ b/doc/recipes.rst @@ -516,7 +516,7 @@ include in your templates: ``interpolateProvider`` service, for instance at the module initialization time: - .. code-block:: javascript + .. code-block:: javascript angular.module('myApp', []).config(function($interpolateProvider) { $interpolateProvider.startSymbol('{[').endSymbol(']}'); From 3531f959d3124ca548c3af893fe8a2b039e46a0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Andr=C3=A9?= Date: Mon, 22 Jul 2024 01:25:30 +0200 Subject: [PATCH 295/812] Deprecate NameExpression internal methods Deprecate two methods from NameExpression: * isSimple(): did not find any usage in Twig (or even Symfony) repositories * isSpecial(): inlined for better performance ($name is available in the method) --- CHANGELOG | 4 +++- doc/deprecated.rst | 4 ++++ src/Node/Expression/NameExpression.php | 14 ++++++++++++-- 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 9fc60cc9918..933fec8fb38 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -5,9 +5,11 @@ * Deprecate the second argument of `Twig\Node\Expression\CallExpression::compileArguments()` * Deprecate `Twig\ExpressionParser\parseHashExpression()` in favor of `Twig\ExpressionParser::parseMappingExpression()` - * Deprecate `Twig\ExpressionParser\parseArrayExpression()`` in favor of + * Deprecate `Twig\ExpressionParser\parseArrayExpression()` in favor of `Twig\ExpressionParser::parseSequenceExpression()` * Add `sequence` and `mapping` tests + * Deprecate `Twig\Node\Expression\NameExpression::isSimple()` and + `Twig\Node\Expression\NameExpression::isSpecial()` # 3.10.3 (2024-05-16) diff --git a/doc/deprecated.rst b/doc/deprecated.rst index c994dc7fa2f..67118ca360b 100644 --- a/doc/deprecated.rst +++ b/doc/deprecated.rst @@ -39,6 +39,10 @@ Nodes ``Twig\Node\Expression\CallExpression::compileArguments()`` method is deprecated. +* The ``Twig\Node\Expression\NameExpression::isSimple()`` and + ``Twig\Node\Expression\NameExpression::isSpecial()`` methods are deprecated as + of Twig 3.11 and will be removed in Twig 4.0. + Node Visitors ------------- diff --git a/src/Node/Expression/NameExpression.php b/src/Node/Expression/NameExpression.php index c3563f01238..286aa5ae20d 100644 --- a/src/Node/Expression/NameExpression.php +++ b/src/Node/Expression/NameExpression.php @@ -34,7 +34,7 @@ public function compile(Compiler $compiler): void $compiler->addDebugInfo($this); if ($this->getAttribute('is_defined_test')) { - if ($this->isSpecial()) { + if (isset($this->specialVars[$name])) { $compiler->repr(true); } elseif (\PHP_VERSION_ID >= 70400) { $compiler @@ -51,7 +51,7 @@ public function compile(Compiler $compiler): void ->raw(', $context))') ; } - } elseif ($this->isSpecial()) { + } elseif (isset($this->specialVars[$name])) { $compiler->raw($this->specialVars[$name]); } elseif ($this->getAttribute('always_defined')) { $compiler @@ -85,13 +85,23 @@ public function compile(Compiler $compiler): void } } + /** + * @deprecated since Twig 3.11 (to be removed in 4.0) + */ public function isSpecial() { + trigger_deprecation('twig/twig', '3.11', 'The "%s()" method is deprecated and will be removed in Twig 4.0.', __METHOD__); + return isset($this->specialVars[$this->getAttribute('name')]); } + /** + * @deprecated since Twig 3.11 (to be removed in 4.0) + */ public function isSimple() { + trigger_deprecation('twig/twig', '3.11', 'The "%s()" method is deprecated and will be removed in Twig 4.0.', __METHOD__); + return !$this->isSpecial() && !$this->getAttribute('is_defined_test'); } } From e4e95f5b861ff532d7e601f9816e951772645275 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Tue, 23 Jul 2024 23:20:37 +0200 Subject: [PATCH 296/812] Replace template loader mocks by ArrayLoader --- src/Test/NodeTestCase.php | 2 +- tests/CompilerTest.php | 4 +- tests/CustomExtensionTest.php | 4 +- tests/EnvironmentTest.php | 12 ++--- tests/ExpressionParserTest.php | 34 +++++++------- tests/Extension/EscaperTest.php | 10 ++-- tests/Extension/StringLoaderExtensionTest.php | 3 +- tests/LexerTest.php | 46 +++++++++---------- tests/Loader/ArrayTest.php | 8 ++-- tests/Node/DeprecatedTest.php | 4 +- tests/Node/Expression/FilterTest.php | 5 +- tests/Node/Expression/FunctionTest.php | 5 +- tests/Node/Expression/NameTest.php | 6 +-- tests/Node/Expression/TestTest.php | 5 +- tests/Node/ModuleTest.php | 6 +-- tests/NodeVisitor/OptimizerTest.php | 10 ++-- tests/ParserTest.php | 12 ++--- tests/TemplateTest.php | 24 +++++----- tests/Util/DeprecationCollectorTest.php | 4 +- 19 files changed, 101 insertions(+), 103 deletions(-) diff --git a/src/Test/NodeTestCase.php b/src/Test/NodeTestCase.php index 61080bd8e13..e6a95494888 100644 --- a/src/Test/NodeTestCase.php +++ b/src/Test/NodeTestCase.php @@ -53,7 +53,7 @@ protected function getCompiler(?Environment $environment = null) protected function getEnvironment() { - return $this->currentEnv = new Environment(new ArrayLoader([])); + return $this->currentEnv = new Environment(new ArrayLoader()); } protected function getVariableGetter($name, $line = false) diff --git a/tests/CompilerTest.php b/tests/CompilerTest.php index a71ee093af5..051edfeb2cd 100644 --- a/tests/CompilerTest.php +++ b/tests/CompilerTest.php @@ -14,13 +14,13 @@ use PHPUnit\Framework\TestCase; use Twig\Compiler; use Twig\Environment; -use Twig\Loader\LoaderInterface; +use Twig\Loader\ArrayLoader; class CompilerTest extends TestCase { public function testReprNumericValueWithLocale() { - $compiler = new Compiler(new Environment($this->createMock(LoaderInterface::class))); + $compiler = new Compiler(new Environment(new ArrayLoader())); $locale = setlocale(\LC_NUMERIC, '0'); if (false === $locale) { diff --git a/tests/CustomExtensionTest.php b/tests/CustomExtensionTest.php index 0a7ba12d270..a2ac0dbed5b 100644 --- a/tests/CustomExtensionTest.php +++ b/tests/CustomExtensionTest.php @@ -14,7 +14,7 @@ use PHPUnit\Framework\TestCase; use Twig\Environment; use Twig\Extension\ExtensionInterface; -use Twig\Loader\LoaderInterface; +use Twig\Loader\ArrayLoader; class CustomExtensionTest extends TestCase { @@ -26,7 +26,7 @@ public function testGetInvalidOperators(ExtensionInterface $extension, $expected $this->expectException(\InvalidArgumentException::class); $this->expectExceptionMessage($expectedExceptionMessage); - $env = new Environment($this->createMock(LoaderInterface::class)); + $env = new Environment(new ArrayLoader()); $env->addExtension($extension); $env->getUnaryOperators(); } diff --git a/tests/EnvironmentTest.php b/tests/EnvironmentTest.php index 4eb22036e6b..d2547e7b2fc 100644 --- a/tests/EnvironmentTest.php +++ b/tests/EnvironmentTest.php @@ -269,7 +269,7 @@ public function testAutoReloadOutdatedCacheHit() public function testHasGetExtensionByClassName() { - $twig = new Environment($this->createMock(LoaderInterface::class)); + $twig = new Environment(new ArrayLoader()); $twig->addExtension($ext = new EnvironmentTest_Extension()); $this->assertSame($ext, $twig->getExtension(EnvironmentTest_Extension::class)); $this->assertSame($ext, $twig->getExtension(EnvironmentTest_Extension::class)); @@ -277,7 +277,7 @@ public function testHasGetExtensionByClassName() public function testAddExtension() { - $twig = new Environment($this->createMock(LoaderInterface::class)); + $twig = new Environment(new ArrayLoader()); $twig->addExtension(new EnvironmentTest_Extension()); $this->assertArrayHasKey('test', $twig->getTokenParsers()); @@ -314,7 +314,7 @@ public function testOverrideExtension() $this->expectException(\LogicException::class); $this->expectExceptionMessage('Unable to register extension "Twig\Tests\EnvironmentTest_Extension" as it is already registered.'); - $twig = new Environment($this->createMock(LoaderInterface::class)); + $twig = new Environment(new ArrayLoader()); $twig->addExtension(new EnvironmentTest_Extension()); $twig->addExtension(new EnvironmentTest_Extension()); @@ -358,7 +358,7 @@ public function testFailLoadTemplate() public function testUndefinedFunctionCallback() { - $twig = new Environment($this->createMock(LoaderInterface::class)); + $twig = new Environment(new ArrayLoader()); $twig->registerUndefinedFunctionCallback(function (string $name) { if ('dynamic' === $name) { return new TwigFunction('dynamic', function () { return 'dynamic'; }); @@ -374,7 +374,7 @@ public function testUndefinedFunctionCallback() public function testUndefinedFilterCallback() { - $twig = new Environment($this->createMock(LoaderInterface::class)); + $twig = new Environment(new ArrayLoader()); $twig->registerUndefinedFilterCallback(function (string $name) { if ('dynamic' === $name) { return new TwigFilter('dynamic', function () { return 'dynamic'; }); @@ -390,7 +390,7 @@ public function testUndefinedFilterCallback() public function testUndefinedTokenParserCallback() { - $twig = new Environment($this->createMock(LoaderInterface::class)); + $twig = new Environment(new ArrayLoader()); $twig->registerUndefinedTokenParserCallback(function (string $name) { if ('dynamic' === $name) { $parser = $this->createMock(TokenParserInterface::class); diff --git a/tests/ExpressionParserTest.php b/tests/ExpressionParserTest.php index abf5ae3a872..8dd91ba670c 100644 --- a/tests/ExpressionParserTest.php +++ b/tests/ExpressionParserTest.php @@ -14,7 +14,7 @@ use PHPUnit\Framework\TestCase; use Twig\Environment; use Twig\Error\SyntaxError; -use Twig\Loader\LoaderInterface; +use Twig\Loader\ArrayLoader; use Twig\Node\Expression\ArrayExpression; use Twig\Node\Expression\Binary\ConcatBinary; use Twig\Node\Expression\ConstantExpression; @@ -31,7 +31,7 @@ public function testCanOnlyAssignToNames($template) { $this->expectException(SyntaxError::class); - $env = new Environment($this->createMock(LoaderInterface::class), ['cache' => false, 'autoescape' => false]); + $env = new Environment(new ArrayLoader(), ['cache' => false, 'autoescape' => false]); $parser = new Parser($env); $parser->parse($env->tokenize(new Source($template, 'index'))); } @@ -59,7 +59,7 @@ public function getFailingTestsForAssignment() */ public function testSequenceExpression($template, $expected) { - $env = new Environment($this->createMock(LoaderInterface::class), ['cache' => false, 'autoescape' => false]); + $env = new Environment(new ArrayLoader(), ['cache' => false, 'autoescape' => false]); $stream = $env->tokenize($source = new Source($template, '')); $parser = new Parser($env); $expected->setSourceContext($source); @@ -74,7 +74,7 @@ public function testSequenceSyntaxError($template) { $this->expectException(SyntaxError::class); - $env = new Environment($this->createMock(LoaderInterface::class), ['cache' => false, 'autoescape' => false]); + $env = new Environment(new ArrayLoader(), ['cache' => false, 'autoescape' => false]); $parser = new Parser($env); $parser->parse($env->tokenize(new Source($template, 'index'))); } @@ -200,7 +200,7 @@ public function testStringExpressionDoesNotConcatenateTwoConsecutiveStrings() { $this->expectException(SyntaxError::class); - $env = new Environment($this->createMock(LoaderInterface::class), ['cache' => false, 'autoescape' => false, 'optimizations' => 0]); + $env = new Environment(new ArrayLoader(), ['cache' => false, 'autoescape' => false, 'optimizations' => 0]); $stream = $env->tokenize(new Source('{{ "a" "b" }}', 'index')); $parser = new Parser($env); @@ -212,7 +212,7 @@ public function testStringExpressionDoesNotConcatenateTwoConsecutiveStrings() */ public function testStringExpression($template, $expected) { - $env = new Environment($this->createMock(LoaderInterface::class), ['cache' => false, 'autoescape' => false, 'optimizations' => 0]); + $env = new Environment(new ArrayLoader(), ['cache' => false, 'autoescape' => false, 'optimizations' => 0]); $stream = $env->tokenize($source = new Source($template, '')); $parser = new Parser($env); $expected->setSourceContext($source); @@ -268,7 +268,7 @@ public function testAttributeCallDoesNotSupportNamedArguments() { $this->expectException(SyntaxError::class); - $env = new Environment($this->createMock(LoaderInterface::class), ['cache' => false, 'autoescape' => false]); + $env = new Environment(new ArrayLoader(), ['cache' => false, 'autoescape' => false]); $parser = new Parser($env); $parser->parse($env->tokenize(new Source('{{ foo.bar(name="Foo") }}', 'index'))); @@ -278,7 +278,7 @@ public function testMacroCallDoesNotSupportNamedArguments() { $this->expectException(SyntaxError::class); - $env = new Environment($this->createMock(LoaderInterface::class), ['cache' => false, 'autoescape' => false]); + $env = new Environment(new ArrayLoader(), ['cache' => false, 'autoescape' => false]); $parser = new Parser($env); $parser->parse($env->tokenize(new Source('{% from _self import foo %}{% macro foo() %}{% endmacro %}{{ foo(name="Foo") }}', 'index'))); @@ -289,7 +289,7 @@ public function testMacroDefinitionDoesNotSupportNonNameVariableName() $this->expectException(SyntaxError::class); $this->expectExceptionMessage('An argument must be a name. Unexpected token "string" of value "a" ("name" expected) in "index" at line 1.'); - $env = new Environment($this->createMock(LoaderInterface::class), ['cache' => false, 'autoescape' => false]); + $env = new Environment(new ArrayLoader(), ['cache' => false, 'autoescape' => false]); $parser = new Parser($env); $parser->parse($env->tokenize(new Source('{% macro foo("a") %}{% endmacro %}', 'index'))); @@ -303,7 +303,7 @@ public function testMacroDefinitionDoesNotSupportNonConstantDefaultValues($templ $this->expectException(SyntaxError::class); $this->expectExceptionMessage('A default value for an argument must be a constant (a boolean, a string, a number, a sequence, or a mapping) in "index" at line 1'); - $env = new Environment($this->createMock(LoaderInterface::class), ['cache' => false, 'autoescape' => false]); + $env = new Environment(new ArrayLoader(), ['cache' => false, 'autoescape' => false]); $parser = new Parser($env); $parser->parse($env->tokenize(new Source($template, 'index'))); @@ -322,7 +322,7 @@ public function getMacroDefinitionDoesNotSupportNonConstantDefaultValues() */ public function testMacroDefinitionSupportsConstantDefaultValues($template) { - $env = new Environment($this->createMock(LoaderInterface::class), ['cache' => false, 'autoescape' => false]); + $env = new Environment(new ArrayLoader(), ['cache' => false, 'autoescape' => false]); $parser = new Parser($env); $parser->parse($env->tokenize(new Source($template, 'index'))); @@ -350,7 +350,7 @@ public function testUnknownFunction() $this->expectException(SyntaxError::class); $this->expectExceptionMessage('Unknown "cycl" function. Did you mean "cycle" in "index" at line 1?'); - $env = new Environment($this->createMock(LoaderInterface::class), ['cache' => false, 'autoescape' => false]); + $env = new Environment(new ArrayLoader(), ['cache' => false, 'autoescape' => false]); $parser = new Parser($env); $parser->parse($env->tokenize(new Source('{{ cycl() }}', 'index'))); @@ -361,7 +361,7 @@ public function testUnknownFunctionWithoutSuggestions() $this->expectException(SyntaxError::class); $this->expectExceptionMessage('Unknown "foobar" function in "index" at line 1.'); - $env = new Environment($this->createMock(LoaderInterface::class), ['cache' => false, 'autoescape' => false]); + $env = new Environment(new ArrayLoader(), ['cache' => false, 'autoescape' => false]); $parser = new Parser($env); $parser->parse($env->tokenize(new Source('{{ foobar() }}', 'index'))); @@ -372,7 +372,7 @@ public function testUnknownFilter() $this->expectException(SyntaxError::class); $this->expectExceptionMessage('Unknown "lowe" filter. Did you mean "lower" in "index" at line 1?'); - $env = new Environment($this->createMock(LoaderInterface::class), ['cache' => false, 'autoescape' => false]); + $env = new Environment(new ArrayLoader(), ['cache' => false, 'autoescape' => false]); $parser = new Parser($env); $parser->parse($env->tokenize(new Source('{{ 1|lowe }}', 'index'))); @@ -383,7 +383,7 @@ public function testUnknownFilterWithoutSuggestions() $this->expectException(SyntaxError::class); $this->expectExceptionMessage('Unknown "foobar" filter in "index" at line 1.'); - $env = new Environment($this->createMock(LoaderInterface::class), ['cache' => false, 'autoescape' => false]); + $env = new Environment(new ArrayLoader(), ['cache' => false, 'autoescape' => false]); $parser = new Parser($env); $parser->parse($env->tokenize(new Source('{{ 1|foobar }}', 'index'))); @@ -394,7 +394,7 @@ public function testUnknownTest() $this->expectException(SyntaxError::class); $this->expectExceptionMessage('Unknown "nul" test. Did you mean "null" in "index" at line 1'); - $env = new Environment($this->createMock(LoaderInterface::class), ['cache' => false, 'autoescape' => false]); + $env = new Environment(new ArrayLoader(), ['cache' => false, 'autoescape' => false]); $parser = new Parser($env); $stream = $env->tokenize(new Source('{{ 1 is nul }}', 'index')); $parser->parse($stream); @@ -405,7 +405,7 @@ public function testUnknownTestWithoutSuggestions() $this->expectException(SyntaxError::class); $this->expectExceptionMessage('Unknown "foobar" test in "index" at line 1.'); - $env = new Environment($this->createMock(LoaderInterface::class), ['cache' => false, 'autoescape' => false]); + $env = new Environment(new ArrayLoader(), ['cache' => false, 'autoescape' => false]); $parser = new Parser($env); $parser->parse($env->tokenize(new Source('{{ 1 is foobar }}', 'index'))); diff --git a/tests/Extension/EscaperTest.php b/tests/Extension/EscaperTest.php index e211677bfa8..4f707184085 100644 --- a/tests/Extension/EscaperTest.php +++ b/tests/Extension/EscaperTest.php @@ -14,7 +14,7 @@ use PHPUnit\Framework\TestCase; use Twig\Environment; use Twig\Extension\EscaperExtension; -use Twig\Loader\LoaderInterface; +use Twig\Loader\ArrayLoader; use Twig\Runtime\EscaperRuntime; class EscaperTest extends TestCase @@ -26,7 +26,7 @@ class EscaperTest extends TestCase */ public function testCustomEscaper($expected, $string, $strategy) { - $twig = new Environment($this->createMock(LoaderInterface::class)); + $twig = new Environment(new ArrayLoader()); $escaperExt = $twig->getExtension(EscaperExtension::class); $escaperExt->setEscaper('foo', 'Twig\Tests\legacy_escaper'); $this->assertSame($expected, $twig->getRuntime(EscaperRuntime::class)->escape($string, $strategy, 'ISO-8859-1')); @@ -48,7 +48,7 @@ public function provideCustomEscaperCases() */ public function testCustomEscaperWithoutCallingSetEscaperRuntime($expected, $string, $strategy) { - $twig = new Environment($this->createMock(LoaderInterface::class)); + $twig = new Environment(new ArrayLoader()); $escaperExt = $twig->getExtension(EscaperExtension::class); $escaperExt->setEscaper('foo', 'Twig\Tests\legacy_escaper'); $this->assertSame($expected, $twig->getRuntime(EscaperRuntime::class)->escape($string, $strategy, 'ISO-8859-1')); @@ -59,11 +59,11 @@ public function testCustomEscaperWithoutCallingSetEscaperRuntime($expected, $str */ public function testCustomEscapersOnMultipleEnvs() { - $env1 = new Environment($this->createMock(LoaderInterface::class)); + $env1 = new Environment(new ArrayLoader()); $escaperExt1 = $env1->getExtension(EscaperExtension::class); $escaperExt1->setEscaper('foo', 'Twig\Tests\legacy_escaper'); - $env2 = new Environment($this->createMock(LoaderInterface::class)); + $env2 = new Environment(new ArrayLoader()); $escaperExt2 = $env2->getExtension(EscaperExtension::class); $escaperExt2->setEscaper('foo', 'Twig\Tests\legacy_escaper_again'); diff --git a/tests/Extension/StringLoaderExtensionTest.php b/tests/Extension/StringLoaderExtensionTest.php index 4c67e12c719..d37b8f2634f 100644 --- a/tests/Extension/StringLoaderExtensionTest.php +++ b/tests/Extension/StringLoaderExtensionTest.php @@ -15,12 +15,13 @@ use Twig\Environment; use Twig\Extension\CoreExtension; use Twig\Extension\StringLoaderExtension; +use Twig\Loader\ArrayLoader; class StringLoaderExtensionTest extends TestCase { public function testIncludeWithTemplateStringAndNoSandbox() { - $twig = new Environment($this->createMock('\Twig\Loader\LoaderInterface')); + $twig = new Environment(new ArrayLoader()); $twig->addExtension(new StringLoaderExtension()); $this->assertSame('something', CoreExtension::include($twig, [], StringLoaderExtension::templateFromString($twig, 'something'))); } diff --git a/tests/LexerTest.php b/tests/LexerTest.php index 2aad47ac9b3..7926034ffa1 100644 --- a/tests/LexerTest.php +++ b/tests/LexerTest.php @@ -15,7 +15,7 @@ use Twig\Environment; use Twig\Error\SyntaxError; use Twig\Lexer; -use Twig\Loader\LoaderInterface; +use Twig\Loader\ArrayLoader; use Twig\Source; use Twig\Token; @@ -25,7 +25,7 @@ public function testNameLabelForTag() { $template = '{% § %}'; - $lexer = new Lexer(new Environment($this->createMock(LoaderInterface::class))); + $lexer = new Lexer(new Environment(new ArrayLoader())); $stream = $lexer->tokenize(new Source($template, 'index')); $stream->expect(Token::BLOCK_START_TYPE); @@ -36,7 +36,7 @@ public function testNameLabelForFunction() { $template = '{{ §() }}'; - $lexer = new Lexer(new Environment($this->createMock(LoaderInterface::class))); + $lexer = new Lexer(new Environment(new ArrayLoader())); $stream = $lexer->tokenize(new Source($template, 'index')); $stream->expect(Token::VAR_START_TYPE); @@ -63,7 +63,7 @@ public function testSpreadOperator() protected function countToken($template, $type, $value = null) { - $lexer = new Lexer(new Environment($this->createMock(LoaderInterface::class))); + $lexer = new Lexer(new Environment(new ArrayLoader())); $stream = $lexer->tokenize(new Source($template, 'index')); $count = 0; @@ -88,7 +88,7 @@ public function testLineDirective() ."baz\n" ."}}\n"; - $lexer = new Lexer(new Environment($this->createMock(LoaderInterface::class))); + $lexer = new Lexer(new Environment(new ArrayLoader())); $stream = $lexer->tokenize(new Source($template, 'index')); // foo\nbar\n @@ -108,7 +108,7 @@ public function testLineDirectiveInline() ."baz\n" ."}}\n"; - $lexer = new Lexer(new Environment($this->createMock(LoaderInterface::class))); + $lexer = new Lexer(new Environment(new ArrayLoader())); $stream = $lexer->tokenize(new Source($template, 'index')); // foo\nbar @@ -123,7 +123,7 @@ public function testLongComments() { $template = '{# '.str_repeat('*', 100000).' #}'; - $lexer = new Lexer(new Environment($this->createMock(LoaderInterface::class))); + $lexer = new Lexer(new Environment(new ArrayLoader())); $lexer->tokenize(new Source($template, 'index')); // add a dummy assertion here to satisfy PHPUnit, the only thing we want to test is that the code above @@ -135,7 +135,7 @@ public function testLongVerbatim() { $template = '{% verbatim %}'.str_repeat('*', 100000).'{% endverbatim %}'; - $lexer = new Lexer(new Environment($this->createMock(LoaderInterface::class))); + $lexer = new Lexer(new Environment(new ArrayLoader())); $lexer->tokenize(new Source($template, 'index')); // add a dummy assertion here to satisfy PHPUnit, the only thing we want to test is that the code above @@ -147,7 +147,7 @@ public function testLongVar() { $template = '{{ '.str_repeat('x', 100000).' }}'; - $lexer = new Lexer(new Environment($this->createMock(LoaderInterface::class))); + $lexer = new Lexer(new Environment(new ArrayLoader())); $lexer->tokenize(new Source($template, 'index')); // add a dummy assertion here to satisfy PHPUnit, the only thing we want to test is that the code above @@ -159,7 +159,7 @@ public function testLongBlock() { $template = '{% '.str_repeat('x', 100000).' %}'; - $lexer = new Lexer(new Environment($this->createMock(LoaderInterface::class))); + $lexer = new Lexer(new Environment(new ArrayLoader())); $lexer->tokenize(new Source($template, 'index')); // add a dummy assertion here to satisfy PHPUnit, the only thing we want to test is that the code above @@ -171,7 +171,7 @@ public function testBigNumbers() { $template = '{{ 922337203685477580700 }}'; - $lexer = new Lexer(new Environment($this->createMock(LoaderInterface::class))); + $lexer = new Lexer(new Environment(new ArrayLoader())); $stream = $lexer->tokenize(new Source($template, 'index')); $stream->next(); $node = $stream->next(); @@ -185,7 +185,7 @@ public function testStringWithEscapedDelimiter() '{{ "foo \" bar" }}' => 'foo " bar', ]; - $lexer = new Lexer(new Environment($this->createMock(LoaderInterface::class))); + $lexer = new Lexer(new Environment(new ArrayLoader())); foreach ($tests as $template => $expected) { $stream = $lexer->tokenize(new Source($template, 'index')); $stream->expect(Token::VAR_START_TYPE); @@ -201,7 +201,7 @@ public function testStringWithInterpolation() { $template = 'foo {{ "bar #{ baz + 1 }" }}'; - $lexer = new Lexer(new Environment($this->createMock(LoaderInterface::class))); + $lexer = new Lexer(new Environment(new ArrayLoader())); $stream = $lexer->tokenize(new Source($template, 'index')); $stream->expect(Token::TEXT_TYPE, 'foo '); $stream->expect(Token::VAR_START_TYPE); @@ -222,7 +222,7 @@ public function testStringWithEscapedInterpolation() { $template = '{{ "bar \#{baz+1}" }}'; - $lexer = new Lexer(new Environment($this->createMock(LoaderInterface::class))); + $lexer = new Lexer(new Environment(new ArrayLoader())); $stream = $lexer->tokenize(new Source($template, 'index')); $stream->expect(Token::VAR_START_TYPE); $stream->expect(Token::STRING_TYPE, 'bar #{baz+1}'); @@ -237,7 +237,7 @@ public function testStringWithHash() { $template = '{{ "bar # baz" }}'; - $lexer = new Lexer(new Environment($this->createMock(LoaderInterface::class))); + $lexer = new Lexer(new Environment(new ArrayLoader())); $stream = $lexer->tokenize(new Source($template, 'index')); $stream->expect(Token::VAR_START_TYPE); $stream->expect(Token::STRING_TYPE, 'bar # baz'); @@ -255,7 +255,7 @@ public function testStringWithUnterminatedInterpolation() $template = '{{ "bar #{x" }}'; - $lexer = new Lexer(new Environment($this->createMock(LoaderInterface::class))); + $lexer = new Lexer(new Environment(new ArrayLoader())); $lexer->tokenize(new Source($template, 'index')); } @@ -263,7 +263,7 @@ public function testStringWithNestedInterpolations() { $template = '{{ "bar #{ "foo#{bar}" }" }}'; - $lexer = new Lexer(new Environment($this->createMock(LoaderInterface::class))); + $lexer = new Lexer(new Environment(new ArrayLoader())); $stream = $lexer->tokenize(new Source($template, 'index')); $stream->expect(Token::VAR_START_TYPE); $stream->expect(Token::STRING_TYPE, 'bar '); @@ -284,7 +284,7 @@ public function testStringWithNestedInterpolationsInBlock() { $template = '{% foo "bar #{ "foo#{bar}" }" %}'; - $lexer = new Lexer(new Environment($this->createMock(LoaderInterface::class))); + $lexer = new Lexer(new Environment(new ArrayLoader())); $stream = $lexer->tokenize(new Source($template, 'index')); $stream->expect(Token::BLOCK_START_TYPE); $stream->expect(Token::NAME_TYPE, 'foo'); @@ -306,7 +306,7 @@ public function testOperatorEndingWithALetterAtTheEndOfALine() { $template = "{{ 1 and\n0}}"; - $lexer = new Lexer(new Environment($this->createMock(LoaderInterface::class))); + $lexer = new Lexer(new Environment(new ArrayLoader())); $stream = $lexer->tokenize(new Source($template, 'index')); $stream->expect(Token::VAR_START_TYPE); $stream->expect(Token::NUMBER_TYPE, 1); @@ -331,7 +331,7 @@ public function testUnterminatedVariable() '; - $lexer = new Lexer(new Environment($this->createMock(LoaderInterface::class))); + $lexer = new Lexer(new Environment(new ArrayLoader())); $lexer->tokenize(new Source($template, 'index')); } @@ -349,14 +349,14 @@ public function testUnterminatedBlock() '; - $lexer = new Lexer(new Environment($this->createMock(LoaderInterface::class))); + $lexer = new Lexer(new Environment(new ArrayLoader())); $lexer->tokenize(new Source($template, 'index')); } public function testOverridingSyntax() { $template = '[# comment #]{# variable #}/# if true #/true/# endif #/'; - $lexer = new Lexer(new Environment($this->createMock(LoaderInterface::class)), [ + $lexer = new Lexer(new Environment(new ArrayLoader()), [ 'tag_comment' => ['[#', '#]'], 'tag_block' => ['/#', '#/'], 'tag_variable' => ['{#', '#}'], @@ -384,7 +384,7 @@ public function testOverridingSyntax() */ public function testErrorsAtTheEndOfTheStream(string $template) { - $lexer = new Lexer(new Environment($this->createMock(LoaderInterface::class))); + $lexer = new Lexer(new Environment(new ArrayLoader())); set_error_handler(function () { $this->fail('Lexer should not emit warnings.'); }); diff --git a/tests/Loader/ArrayTest.php b/tests/Loader/ArrayTest.php index 76714bb0858..543fe9ff6a9 100644 --- a/tests/Loader/ArrayTest.php +++ b/tests/Loader/ArrayTest.php @@ -21,7 +21,7 @@ public function testGetSourceContextWhenTemplateDoesNotExist() { $this->expectException(LoaderError::class); - $loader = new ArrayLoader([]); + $loader = new ArrayLoader(); $loader->getSourceContext('foo'); } @@ -59,14 +59,14 @@ public function testGetCacheKeyWhenTemplateDoesNotExist() { $this->expectException(LoaderError::class); - $loader = new ArrayLoader([]); + $loader = new ArrayLoader(); $loader->getCacheKey('foo'); } public function testSetTemplate() { - $loader = new ArrayLoader([]); + $loader = new ArrayLoader(); $loader->setTemplate('foo', 'bar'); $this->assertEquals('bar', $loader->getSourceContext('foo')->getCode()); @@ -82,7 +82,7 @@ public function testIsFreshWhenTemplateDoesNotExist() { $this->expectException(LoaderError::class); - $loader = new ArrayLoader([]); + $loader = new ArrayLoader(); $loader->isFresh('foo', time()); } diff --git a/tests/Node/DeprecatedTest.php b/tests/Node/DeprecatedTest.php index 24185f4b573..63259eda3d3 100644 --- a/tests/Node/DeprecatedTest.php +++ b/tests/Node/DeprecatedTest.php @@ -12,7 +12,7 @@ */ use Twig\Environment; -use Twig\Loader\LoaderInterface; +use Twig\Loader\ArrayLoader; use Twig\Node\DeprecatedNode; use Twig\Node\Expression\ConstantExpression; use Twig\Node\Expression\FunctionExpression; @@ -62,7 +62,7 @@ public function getTests() EOF ]; - $environment = new Environment($this->createMock(LoaderInterface::class)); + $environment = new Environment(new ArrayLoader()); $environment->addFunction(new TwigFunction('foo', 'foo', [])); $expr = new FunctionExpression('foo', new Node(), 1); diff --git a/tests/Node/Expression/FilterTest.php b/tests/Node/Expression/FilterTest.php index 7234ac31816..9af49306947 100644 --- a/tests/Node/Expression/FilterTest.php +++ b/tests/Node/Expression/FilterTest.php @@ -15,7 +15,6 @@ use Twig\Error\SyntaxError; use Twig\Extension\AbstractExtension; use Twig\Loader\ArrayLoader; -use Twig\Loader\LoaderInterface; use Twig\Node\Expression\ConstantExpression; use Twig\Node\Expression\FilterExpression; use Twig\Node\Node; @@ -38,7 +37,7 @@ public function testConstructor() public function getTests() { - $environment = new Environment($this->createMock(LoaderInterface::class)); + $environment = new Environment(new ArrayLoader()); $environment->addFilter(new TwigFilter('bar', 'twig_tests_filter_dummy', ['needs_environment' => true])); $environment->addFilter(new TwigFilter('bar_closure', \Closure::fromCallable(twig_tests_filter_dummy::class), ['needs_environment' => true])); $environment->addFilter(new TwigFilter('barbar', 'Twig\Tests\Node\Expression\twig_tests_filter_barbar', ['needs_context' => true, 'is_variadic' => true])); @@ -180,7 +179,7 @@ protected function createFilter($node, $name, array $arguments = []) protected function getEnvironment() { - $env = new Environment(new ArrayLoader([])); + $env = new Environment(new ArrayLoader()); $env->addFilter(new TwigFilter('anonymous', function () {})); return $env; diff --git a/tests/Node/Expression/FunctionTest.php b/tests/Node/Expression/FunctionTest.php index aa40e355553..347e7c42906 100644 --- a/tests/Node/Expression/FunctionTest.php +++ b/tests/Node/Expression/FunctionTest.php @@ -13,7 +13,6 @@ use Twig\Environment; use Twig\Loader\ArrayLoader; -use Twig\Loader\LoaderInterface; use Twig\Node\Expression\ConstantExpression; use Twig\Node\Expression\FunctionExpression; use Twig\Node\Node; @@ -34,7 +33,7 @@ public function testConstructor() public function getTests() { - $environment = new Environment($this->createMock(LoaderInterface::class)); + $environment = new Environment(new ArrayLoader()); $environment->addFunction(new TwigFunction('foo', 'twig_tests_function_dummy', [])); $environment->addFunction(new TwigFunction('foo_closure', \Closure::fromCallable(twig_tests_function_dummy::class), [])); $environment->addFunction(new TwigFunction('bar', 'twig_tests_function_dummy', ['needs_environment' => true])); @@ -110,7 +109,7 @@ protected function createFunction($name, array $arguments = []) protected function getEnvironment() { - $env = new Environment(new ArrayLoader([])); + $env = new Environment(new ArrayLoader()); $env->addFunction(new TwigFunction('anonymous', function () {})); return $env; diff --git a/tests/Node/Expression/NameTest.php b/tests/Node/Expression/NameTest.php index 57ba02b7600..3e5437444ba 100644 --- a/tests/Node/Expression/NameTest.php +++ b/tests/Node/Expression/NameTest.php @@ -12,7 +12,7 @@ */ use Twig\Environment; -use Twig\Loader\LoaderInterface; +use Twig\Loader\ArrayLoader; use Twig\Node\Expression\NameExpression; use Twig\Test\NodeTestCase; @@ -31,8 +31,8 @@ public function getTests() $self = new NameExpression('_self', 1); $context = new NameExpression('_context', 1); - $env = new Environment($this->createMock(LoaderInterface::class), ['strict_variables' => true]); - $env1 = new Environment($this->createMock(LoaderInterface::class), ['strict_variables' => false]); + $env = new Environment(new ArrayLoader(), ['strict_variables' => true]); + $env1 = new Environment(new ArrayLoader(), ['strict_variables' => false]); $output = '(isset($context["foo"]) || array_key_exists("foo", $context) ? $context["foo"] : (function () { throw new RuntimeError(\'Variable "foo" does not exist.\', 1, $this->source); })())'; diff --git a/tests/Node/Expression/TestTest.php b/tests/Node/Expression/TestTest.php index df7c7202b0c..1a5461666ca 100644 --- a/tests/Node/Expression/TestTest.php +++ b/tests/Node/Expression/TestTest.php @@ -13,7 +13,6 @@ use Twig\Environment; use Twig\Loader\ArrayLoader; -use Twig\Loader\LoaderInterface; use Twig\Node\Expression\ConstantExpression; use Twig\Node\Expression\Test\NullTest; use Twig\Node\Expression\TestExpression; @@ -37,7 +36,7 @@ public function testConstructor() public function getTests() { - $environment = new Environment($this->createMock(LoaderInterface::class)); + $environment = new Environment(new ArrayLoader()); $environment->addTest(new TwigTest('barbar', 'Twig\Tests\Node\Expression\twig_tests_test_barbar', ['is_variadic' => true, 'need_context' => true])); $tests = []; @@ -79,7 +78,7 @@ protected function createTest($node, $name, array $arguments = []) protected function getEnvironment() { - $env = new Environment(new ArrayLoader([])); + $env = new Environment(new ArrayLoader()); $env->addTest(new TwigTest('anonymous', function () {})); return $env; diff --git a/tests/Node/ModuleTest.php b/tests/Node/ModuleTest.php index 2b5e79c2c8e..974bc22ce67 100644 --- a/tests/Node/ModuleTest.php +++ b/tests/Node/ModuleTest.php @@ -12,7 +12,7 @@ */ use Twig\Environment; -use Twig\Loader\LoaderInterface; +use Twig\Loader\ArrayLoader; use Twig\Node\Expression\AssignNameExpression; use Twig\Node\Expression\ConditionalExpression; use Twig\Node\Expression\ConstantExpression; @@ -45,7 +45,7 @@ public function testConstructor() public function getTests() { - $twig = new Environment($this->createMock(LoaderInterface::class)); + $twig = new Environment(new ArrayLoader(['foo.twig' => '{{ foo }}'])); $tests = []; @@ -218,7 +218,7 @@ public function getSourceContext() 2 ); - $twig = new Environment($this->createMock(LoaderInterface::class), ['debug' => true]); + $twig = new Environment(new ArrayLoader(['foo.twig' => '{{ foo }}']), ['debug' => true]); $node = new ModuleNode($body, $extends, $blocks, $macros, $traits, new Node([]), $source); $tests[] = [$node, <<createMock(LoaderInterface::class), ['cache' => false, 'autoescape' => false]); + $env = new Environment(new ArrayLoader(), ['cache' => false, 'autoescape' => false]); $stream = $env->parse($env->tokenize(new Source('{{ block("foo") }}', 'index'))); @@ -37,7 +37,7 @@ public function testRenderBlockOptimizer() public function testRenderParentBlockOptimizer() { - $env = new Environment($this->createMock(LoaderInterface::class), ['cache' => false, 'autoescape' => false]); + $env = new Environment(new ArrayLoader(), ['cache' => false, 'autoescape' => false]); $stream = $env->parse($env->tokenize(new Source('{% extends "foo" %}{% block content %}{{ parent() }}{% endblock %}', 'index'))); @@ -49,7 +49,7 @@ public function testRenderParentBlockOptimizer() public function testForVarOptimizer() { - $env = new Environment($this->createMock(LoaderInterface::class), ['cache' => false, 'autoescape' => false]); + $env = new Environment(new ArrayLoader(), ['cache' => false, 'autoescape' => false]); $template = '{% for i, j in foo %}{{ loop.index }}{{ i }}{{ j }}{% endfor %}'; $stream = $env->parse($env->tokenize(new Source($template, 'index'))); @@ -75,7 +75,7 @@ public function checkForVarConfiguration(Node $node, $target) */ public function testForLoopOptimizer($template, $expected) { - $env = new Environment($this->createMock(LoaderInterface::class), ['cache' => false]); + $env = new Environment(new ArrayLoader(), ['cache' => false]); $stream = $env->parse($env->tokenize(new Source($template, 'index'))); diff --git a/tests/ParserTest.php b/tests/ParserTest.php index cdd8e875743..73de41e6fdd 100644 --- a/tests/ParserTest.php +++ b/tests/ParserTest.php @@ -14,7 +14,7 @@ use PHPUnit\Framework\TestCase; use Twig\Environment; use Twig\Error\SyntaxError; -use Twig\Loader\LoaderInterface; +use Twig\Loader\ArrayLoader; use Twig\Node\Node; use Twig\Node\SetNode; use Twig\Node\TextNode; @@ -37,7 +37,7 @@ public function testUnknownTag() new Token(Token::BLOCK_END_TYPE, '', 1), new Token(Token::EOF_TYPE, '', 1), ]); - $parser = new Parser(new Environment($this->createMock(LoaderInterface::class))); + $parser = new Parser(new Environment(new ArrayLoader())); $parser->parse($stream); } @@ -52,7 +52,7 @@ public function testUnknownTagWithoutSuggestions() new Token(Token::BLOCK_END_TYPE, '', 1), new Token(Token::EOF_TYPE, '', 1), ]); - $parser = new Parser(new Environment($this->createMock(LoaderInterface::class))); + $parser = new Parser(new Environment(new ArrayLoader())); $parser->parse($stream); } @@ -133,7 +133,7 @@ public function getFilterBodyNodesWithBOMData() public function testParseIsReentrant() { - $twig = new Environment($this->createMock(LoaderInterface::class), [ + $twig = new Environment(new ArrayLoader(), [ 'autoescape' => false, 'optimizations' => 0, ]); @@ -156,7 +156,7 @@ public function testParseIsReentrant() public function testGetVarName() { - $twig = new Environment($this->createMock(LoaderInterface::class), [ + $twig = new Environment(new ArrayLoader(), [ 'autoescape' => false, 'optimizations' => 0, ]); @@ -177,7 +177,7 @@ public function testGetVarName() protected function getParser() { - $parser = new Parser(new Environment($this->createMock(LoaderInterface::class))); + $parser = new Parser(new Environment(new ArrayLoader())); $parser->setParent(new Node()); $p = new \ReflectionProperty($parser, 'stream'); diff --git a/tests/TemplateTest.php b/tests/TemplateTest.php index 1256b5df547..8ee46c19bd5 100644 --- a/tests/TemplateTest.php +++ b/tests/TemplateTest.php @@ -29,7 +29,7 @@ public function testDisplayBlocksAcceptTemplateOnlyAsBlocks() { $this->expectException(\LogicException::class); - $twig = new Environment($this->createMock(LoaderInterface::class)); + $twig = new Environment(new ArrayLoader()); $template = new TemplateForTest($twig); $template->displayBlock('foo', [], ['foo' => [new \stdClass(), 'foo']]); } @@ -89,7 +89,7 @@ public function getAttributeExceptions() */ public function testGetAttributeWithSandbox($object, $item, $allowed) { - $twig = new Environment($this->createMock(LoaderInterface::class)); + $twig = new Environment(new ArrayLoader()); $policy = new SecurityPolicy([], [], [/* method */], [/* prop */], []); $twig->addExtension(new SandboxExtension($policy, !$allowed)); $template = new TemplateForTest($twig); @@ -146,7 +146,7 @@ public function testRenderBlockWithUndefinedBlock() $this->expectException(RuntimeError::class); $this->expectExceptionMessage('Block "unknown" on template "index.twig" does not exist in "index.twig".'); - $twig = new Environment($this->createMock(LoaderInterface::class)); + $twig = new Environment(new ArrayLoader()); $template = new TemplateForTest($twig, 'index.twig'); $template->renderBlock('unknown', []); } @@ -156,7 +156,7 @@ public function testDisplayBlockWithUndefinedBlock() $this->expectException(RuntimeError::class); $this->expectExceptionMessage('Block "unknown" on template "index.twig" does not exist in "index.twig".'); - $twig = new Environment($this->createMock(LoaderInterface::class)); + $twig = new Environment(new ArrayLoader()); $template = new TemplateForTest($twig, 'index.twig'); $template->displayBlock('unknown', []); } @@ -166,14 +166,14 @@ public function testDisplayBlockWithUndefinedParentBlock() $this->expectException(RuntimeError::class); $this->expectExceptionMessage('Block "foo" should not call parent() in "index.twig" as the block does not exist in the parent template "parent.twig"'); - $twig = new Environment($this->createMock(LoaderInterface::class)); + $twig = new Environment(new ArrayLoader()); $template = new TemplateForTest($twig, 'parent.twig'); $template->displayBlock('foo', [], ['foo' => [new TemplateForTest($twig, 'index.twig'), 'block_foo']], false); } public function testGetAttributeOnArrayWithConfusableKey() { - $twig = new Environment($this->createMock(LoaderInterface::class)); + $twig = new Environment(new ArrayLoader()); $template = new TemplateForTest($twig); $array = ['Zero', 'One', -1 => 'MinusOne', '' => 'EmptyString', '1.5' => 'FloatButString', '01' => 'IntegerButStringWithLeadingZeros']; @@ -208,7 +208,7 @@ public function testGetAttributeOnArrayWithConfusableKey() */ public function testGetAttribute($defined, $value, $object, $item, $arguments, $type) { - $twig = new Environment($this->createMock(LoaderInterface::class)); + $twig = new Environment(new ArrayLoader()); $template = new TemplateForTest($twig); $this->assertEquals($value, CoreExtension::getAttribute($twig, $template->getSourceContext(), $object, $item, $arguments, $type)); @@ -219,7 +219,7 @@ public function testGetAttribute($defined, $value, $object, $item, $arguments, $ */ public function testGetAttributeStrict($defined, $value, $object, $item, $arguments, $type, $exceptionMessage = null) { - $twig = new Environment($this->createMock(LoaderInterface::class), ['strict_variables' => true]); + $twig = new Environment(new ArrayLoader(), ['strict_variables' => true]); $template = new TemplateForTest($twig); if ($defined) { @@ -238,7 +238,7 @@ public function testGetAttributeStrict($defined, $value, $object, $item, $argume */ public function testGetAttributeDefined($defined, $value, $object, $item, $arguments, $type) { - $twig = new Environment($this->createMock(LoaderInterface::class)); + $twig = new Environment(new ArrayLoader()); $template = new TemplateForTest($twig); $this->assertEquals($defined, CoreExtension::getAttribute($twig, $template->getSourceContext(), $object, $item, $arguments, $type, true)); @@ -249,7 +249,7 @@ public function testGetAttributeDefined($defined, $value, $object, $item, $argum */ public function testGetAttributeDefinedStrict($defined, $value, $object, $item, $arguments, $type) { - $twig = new Environment($this->createMock(LoaderInterface::class), ['strict_variables' => true]); + $twig = new Environment(new ArrayLoader(), ['strict_variables' => true]); $template = new TemplateForTest($twig); $this->assertEquals($defined, CoreExtension::getAttribute($twig, $template->getSourceContext(), $object, $item, $arguments, $type, true)); @@ -257,7 +257,7 @@ public function testGetAttributeDefinedStrict($defined, $value, $object, $item, public function testGetAttributeCallExceptions() { - $twig = new Environment($this->createMock(LoaderInterface::class)); + $twig = new Environment(new ArrayLoader()); $template = new TemplateForTest($twig); $object = new TemplateMagicMethodExceptionObject(); @@ -403,7 +403,7 @@ public function getGetAttributeTests() public function testGetIsMethods() { - $twig = new Environment($this->createMock(LoaderInterface::class)); + $twig = new Environment(new ArrayLoader()); $getIsObject = new TemplateGetIsMethods(); $template = new TemplateForTest($twig, 'index.twig'); diff --git a/tests/Util/DeprecationCollectorTest.php b/tests/Util/DeprecationCollectorTest.php index 18f889f6a42..547744e0b40 100644 --- a/tests/Util/DeprecationCollectorTest.php +++ b/tests/Util/DeprecationCollectorTest.php @@ -13,7 +13,7 @@ use PHPUnit\Framework\TestCase; use Twig\Environment; -use Twig\Loader\LoaderInterface; +use Twig\Loader\ArrayLoader; use Twig\TwigFunction; use Twig\Util\DeprecationCollector; @@ -24,7 +24,7 @@ class DeprecationCollectorTest extends TestCase */ public function testCollect() { - $twig = new Environment($this->createMock(LoaderInterface::class)); + $twig = new Environment(new ArrayLoader()); $twig->addFunction(new TwigFunction('deprec', [$this, 'deprec'], ['deprecated' => '1.1'])); $collector = new DeprecationCollector($twig); From ebcd5034ee9d78dbec7c34165afeec0a3e4b438e Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Tue, 23 Jul 2024 23:18:02 +0200 Subject: [PATCH 297/812] Refactor some tests --- tests/Node/Expression/FilterTest.php | 69 +++++++++++++++----------- tests/Node/Expression/FunctionTest.php | 14 +++--- tests/Node/Expression/TestTest.php | 4 +- 3 files changed, 50 insertions(+), 37 deletions(-) diff --git a/tests/Node/Expression/FilterTest.php b/tests/Node/Expression/FilterTest.php index e7740c962dc..9ce5e4e4c80 100644 --- a/tests/Node/Expression/FilterTest.php +++ b/tests/Node/Expression/FilterTest.php @@ -23,6 +23,8 @@ class FilterTest extends NodeTestCase { + private $extension = null; + public function testConstructor() { $expr = new ConstantExpression('foo', 1); @@ -35,35 +37,14 @@ public function testConstructor() $this->assertEquals($args, $node->getNode('arguments')); } - public function getTests() + protected function tearDown(): void { - $environment = new Environment(new ArrayLoader()); - $environment->addFilter(new TwigFilter('bar', 'twig_tests_filter_dummy', ['needs_environment' => true])); - $environment->addFilter(new TwigFilter('bar_closure', \Closure::fromCallable(twig_tests_filter_dummy::class), ['needs_environment' => true])); - $environment->addFilter(new TwigFilter('barbar', 'Twig\Tests\Node\Expression\twig_tests_filter_barbar', ['needs_context' => true, 'is_variadic' => true])); - $environment->addFilter(new TwigFilter('magic_static', __NAMESPACE__.'\ChildMagicCallStub::magicStaticCall')); - if (\PHP_VERSION_ID >= 80111) { - $environment->addExtension(new FilterTestExtension()); - } - - $extension = new class() extends AbstractExtension { - public function getFilters(): array - { - return [ - new TwigFilter('foo', \Closure::fromCallable([$this, 'foo'])), - new TwigFilter('foobar', \Closure::fromCallable([$this, 'foobar'])), - ]; - } - - public function foo() - { - } + $this->extension = null; + } - protected function foobar() - { - } - }; - $environment->addExtension($extension); + public function getTests() + { + $environment = $this->getEnvironment(); $tests = []; @@ -141,7 +122,7 @@ protected function foobar() // from extension $node = $this->createFilter($string, 'foo'); - $tests[] = [$node, \sprintf('$this->extensions[\'%s\']->foo("abc")', \get_class($extension)), $environment]; + $tests[] = [$node, \sprintf('$this->extensions[\'%s\']->foo("abc")', \get_class($this->getExtension())), $environment]; $node = $this->createFilter($string, 'foobar'); $tests[] = [$node, '$this->env->getFilter(\'foobar\')->getCallable()("abc")', $environment]; @@ -192,9 +173,41 @@ protected function getEnvironment() { $env = new Environment(new ArrayLoader()); $env->addFilter(new TwigFilter('anonymous', function () {})); + $env->addFilter(new TwigFilter('bar', 'twig_tests_filter_dummy', ['needs_environment' => true])); + $env->addFilter(new TwigFilter('bar_closure', \Closure::fromCallable(twig_tests_filter_dummy::class), ['needs_environment' => true])); + $env->addFilter(new TwigFilter('barbar', 'Twig\Tests\Node\Expression\twig_tests_filter_barbar', ['needs_context' => true, 'is_variadic' => true])); + $env->addFilter(new TwigFilter('magic_static', __NAMESPACE__.'\ChildMagicCallStub::magicStaticCall')); + if (\PHP_VERSION_ID >= 80111) { + $env->addExtension(new FilterTestExtension()); + } + $env->addExtension($this->getExtension()); return $env; } + + private function getExtension() + { + if ($this->extension) { + return $this->extension; + } + return $this->extension = new class() extends AbstractExtension { + public function getFilters(): array + { + return [ + new TwigFilter('foo', \Closure::fromCallable([$this, 'foo'])), + new TwigFilter('foobar', \Closure::fromCallable([$this, 'foobar'])), + ]; + } + + public function foo() + { + } + + protected function foobar() + { + } + }; + } } function twig_tests_filter_dummy() diff --git a/tests/Node/Expression/FunctionTest.php b/tests/Node/Expression/FunctionTest.php index 347e7c42906..d8df87696ba 100644 --- a/tests/Node/Expression/FunctionTest.php +++ b/tests/Node/Expression/FunctionTest.php @@ -33,13 +33,7 @@ public function testConstructor() public function getTests() { - $environment = new Environment(new ArrayLoader()); - $environment->addFunction(new TwigFunction('foo', 'twig_tests_function_dummy', [])); - $environment->addFunction(new TwigFunction('foo_closure', \Closure::fromCallable(twig_tests_function_dummy::class), [])); - $environment->addFunction(new TwigFunction('bar', 'twig_tests_function_dummy', ['needs_environment' => true])); - $environment->addFunction(new TwigFunction('foofoo', 'twig_tests_function_dummy', ['needs_context' => true])); - $environment->addFunction(new TwigFunction('foobar', 'twig_tests_function_dummy', ['needs_environment' => true, 'needs_context' => true])); - $environment->addFunction(new TwigFunction('barbar', 'Twig\Tests\Node\Expression\twig_tests_function_barbar', ['is_variadic' => true])); + $environment = $this->getEnvironment(); $tests = []; @@ -111,6 +105,12 @@ protected function getEnvironment() { $env = new Environment(new ArrayLoader()); $env->addFunction(new TwigFunction('anonymous', function () {})); + $env->addFunction(new TwigFunction('foo', 'twig_tests_function_dummy', [])); + $env->addFunction(new TwigFunction('foo_closure', \Closure::fromCallable(twig_tests_function_dummy::class), [])); + $env->addFunction(new TwigFunction('bar', 'twig_tests_function_dummy', ['needs_environment' => true])); + $env->addFunction(new TwigFunction('foofoo', 'twig_tests_function_dummy', ['needs_context' => true])); + $env->addFunction(new TwigFunction('foobar', 'twig_tests_function_dummy', ['needs_environment' => true, 'needs_context' => true])); + $env->addFunction(new TwigFunction('barbar', 'Twig\Tests\Node\Expression\twig_tests_function_barbar', ['is_variadic' => true])); return $env; } diff --git a/tests/Node/Expression/TestTest.php b/tests/Node/Expression/TestTest.php index 1a5461666ca..93c6cd53303 100644 --- a/tests/Node/Expression/TestTest.php +++ b/tests/Node/Expression/TestTest.php @@ -36,8 +36,7 @@ public function testConstructor() public function getTests() { - $environment = new Environment(new ArrayLoader()); - $environment->addTest(new TwigTest('barbar', 'Twig\Tests\Node\Expression\twig_tests_test_barbar', ['is_variadic' => true, 'need_context' => true])); + $environment = $this->getEnvironment(); $tests = []; @@ -80,6 +79,7 @@ protected function getEnvironment() { $env = new Environment(new ArrayLoader()); $env->addTest(new TwigTest('anonymous', function () {})); + $env->addTest(new TwigTest('barbar', 'Twig\Tests\Node\Expression\twig_tests_test_barbar', ['is_variadic' => true, 'need_context' => true])); return $env; } From 343fefae1004fe41db65ba4ad76b790c26ee1589 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Andr=C3=A9?= Date: Sat, 27 Jul 2024 14:21:54 +0200 Subject: [PATCH 298/812] [Doc] Fix code syntax in deprecated.rst Add missing backticks --- doc/deprecated.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/deprecated.rst b/doc/deprecated.rst index c994dc7fa2f..9b7003a6917 100644 --- a/doc/deprecated.rst +++ b/doc/deprecated.rst @@ -27,10 +27,10 @@ Extensions ``Twig\Runtime\EscaperRuntime`` class instead: Before: - $twig->getExtension(EscaperExtension::class)->METHOD() + ``$twig->getExtension(EscaperExtension::class)->METHOD();`` After: - $twig->getRuntime(EscaperRuntime::class)->METHOD(); + ``$twig->getRuntime(EscaperRuntime::class)->METHOD();`` Nodes ----- From 27f6171f2f98e7b9c0d9e175118b23177a02d414 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Mon, 22 Jul 2024 13:46:32 +0200 Subject: [PATCH 299/812] Add support for yielding from a generator in PrintNode --- CHANGELOG | 1 + src/Node/Expression/AbstractExpression.php | 4 +++ src/Node/PrintNode.php | 5 ++- src/NodeVisitor/SandboxNodeVisitor.php | 2 +- tests/Node/PrintTest.php | 9 +++++ tests/NodeVisitor/SandboxTest.php | 40 ++++++++++++++++++++++ 6 files changed, 57 insertions(+), 4 deletions(-) create mode 100644 tests/NodeVisitor/SandboxTest.php diff --git a/CHANGELOG b/CHANGELOG index 933fec8fb38..785a2694997 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,6 @@ # 3.11.0 (2024-XX-XX) + * Add the possibility to yield from a generator in `PrintNode` * Add the `shuffle` filter * Add the `singular` and `plural` filters in `StringExtension` * Deprecate the second argument of `Twig\Node\Expression\CallExpression::compileArguments()` diff --git a/src/Node/Expression/AbstractExpression.php b/src/Node/Expression/AbstractExpression.php index 42da0559d12..1692f5671ef 100644 --- a/src/Node/Expression/AbstractExpression.php +++ b/src/Node/Expression/AbstractExpression.php @@ -21,4 +21,8 @@ */ abstract class AbstractExpression extends Node { + public function isGenerator(): bool + { + return $this->hasAttribute('is_generator') && $this->getAttribute('is_generator'); + } } diff --git a/src/Node/PrintNode.php b/src/Node/PrintNode.php index a6a89bd74c6..bdc738301f1 100644 --- a/src/Node/PrintNode.php +++ b/src/Node/PrintNode.php @@ -31,10 +31,9 @@ public function __construct(AbstractExpression $expr, int $lineno, ?string $tag public function compile(Compiler $compiler): void { - $compiler->addDebugInfo($this); - $compiler - ->write('yield ') + ->addDebugInfo($this) + ->write($this->getNode('expr')->isGenerator() ? 'yield from ' : 'yield ') ->subcompile($this->getNode('expr')) ->raw(";\n") ; diff --git a/src/NodeVisitor/SandboxNodeVisitor.php b/src/NodeVisitor/SandboxNodeVisitor.php index d1108394fdf..68020885e40 100644 --- a/src/NodeVisitor/SandboxNodeVisitor.php +++ b/src/NodeVisitor/SandboxNodeVisitor.php @@ -119,7 +119,7 @@ public function leaveNode(Node $node, Environment $env): ?Node private function wrapNode(Node $node, string $name): void { $expr = $node->getNode($name); - if ($expr instanceof NameExpression || $expr instanceof GetAttrExpression) { + if (($expr instanceof NameExpression || $expr instanceof GetAttrExpression) && !$expr->isGenerator()) { $node->setNode($name, new CheckToStringNode($expr)); } } diff --git a/tests/Node/PrintTest.php b/tests/Node/PrintTest.php index 2df440c2808..09c2a19ab17 100644 --- a/tests/Node/PrintTest.php +++ b/tests/Node/PrintTest.php @@ -12,7 +12,10 @@ */ use Twig\Node\Expression\ConstantExpression; +use Twig\Node\Expression\GetAttrExpression; +use Twig\Node\Expression\NameExpression; use Twig\Node\PrintNode; +use Twig\Template; use Twig\Test\NodeTestCase; class PrintTest extends NodeTestCase @@ -30,6 +33,12 @@ public function getTests() $tests = []; $tests[] = [new PrintNode(new ConstantExpression('foo', 1), 1), "// line 1\nyield \"foo\";"]; + $expr = new NameExpression('foo', 1); + $attr = new ConstantExpression('bar', 1); + $node = new GetAttrExpression($expr, $attr, null, Template::METHOD_CALL, 1); + $node->setAttribute('is_generator', true); + $tests[] = [new PrintNode($node, 1), "// line 1\nyield from CoreExtension::getAttribute(\$this->env, \$this->source, (\$context[\"foo\"] ?? null), \"bar\", [], \"method\", false, false, false, 1);"]; + return $tests; } } diff --git a/tests/NodeVisitor/SandboxTest.php b/tests/NodeVisitor/SandboxTest.php new file mode 100644 index 00000000000..d465abc3593 --- /dev/null +++ b/tests/NodeVisitor/SandboxTest.php @@ -0,0 +1,40 @@ +setAttribute('is_generator', true); + $node = new ModuleNode(new PrintNode($expr, 1), null, new Node(), new Node(), new Node(), new Node([]), new Source('foo', 'foo')); + $traverser = new NodeTraverser($env, [new SandboxNodeVisitor($env)]); + $node = $traverser->traverse($node); + + $this->assertNotInstanceOf(CheckToStringNode::class, $node->getNode('body')->getNode('expr')); + $this->assertSame("// line 1\nyield from (\$context[\"foo\"] ?? null);\n", $env->compile($node->getNode('body'))); + } +} From 0e6d717f2dabe50caa21ae284f212ba4ecfcd575 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sat, 27 Jul 2024 18:03:53 +0200 Subject: [PATCH 300/812] Fix optimizer mode validation in OptimizerNodeVisitor --- CHANGELOG | 1 + src/NodeVisitor/OptimizerNodeVisitor.php | 2 +- tests/NodeVisitor/OptimizerTest.php | 11 +++++++++++ 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 785a2694997..39b04988901 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,6 @@ # 3.11.0 (2024-XX-XX) + * Fix optimizer mode validation in `OptimizerNodeVisitor` * Add the possibility to yield from a generator in `PrintNode` * Add the `shuffle` filter * Add the `singular` and `plural` filters in `StringExtension` diff --git a/src/NodeVisitor/OptimizerNodeVisitor.php b/src/NodeVisitor/OptimizerNodeVisitor.php index a2540f16a96..325edab18b6 100644 --- a/src/NodeVisitor/OptimizerNodeVisitor.php +++ b/src/NodeVisitor/OptimizerNodeVisitor.php @@ -55,7 +55,7 @@ final class OptimizerNodeVisitor implements NodeVisitorInterface */ public function __construct(int $optimizers = -1) { - if ($optimizers > (self::OPTIMIZE_FOR | self::OPTIMIZE_RAW_FILTER)) { + if ($optimizers > (self::OPTIMIZE_FOR | self::OPTIMIZE_RAW_FILTER | self::OPTIMIZE_TEXT_NODES)) { throw new \InvalidArgumentException(\sprintf('Optimizer mode "%s" is not valid.', $optimizers)); } diff --git a/tests/NodeVisitor/OptimizerTest.php b/tests/NodeVisitor/OptimizerTest.php index f0f61132b4b..b9eeea9325c 100644 --- a/tests/NodeVisitor/OptimizerTest.php +++ b/tests/NodeVisitor/OptimizerTest.php @@ -19,10 +19,21 @@ use Twig\Node\Expression\ParentExpression; use Twig\Node\ForNode; use Twig\Node\Node; +use Twig\NodeVisitor\OptimizerNodeVisitor; use Twig\Source; class OptimizerTest extends TestCase { + public function testConstructor() + { + $this->expectNotToPerformAssertions(); + new OptimizerNodeVisitor( + OptimizerNodeVisitor::OPTIMIZE_FOR + | OptimizerNodeVisitor::OPTIMIZE_RAW_FILTER + | OptimizerNodeVisitor::OPTIMIZE_TEXT_NODES + ); + } + public function testRenderBlockOptimizer() { $env = new Environment(new ArrayLoader(), ['cache' => false, 'autoescape' => false]); From 84b1b3e0b3a252547a5cba9086e1cebb91324025 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sat, 27 Jul 2024 17:51:08 +0200 Subject: [PATCH 301/812] Remove magic handling of the raw filter --- src/Extension/EscaperExtension.php | 15 ++------------ src/Node/Expression/Filter/RawFilter.php | 26 ++++++++++++++++++++++++ src/NodeVisitor/OptimizerNodeVisitor.php | 21 ++++--------------- tests/Fixtures/filters/raw.test | 8 ++++++++ tests/NodeVisitor/OptimizerTest.php | 1 - 5 files changed, 40 insertions(+), 31 deletions(-) create mode 100644 src/Node/Expression/Filter/RawFilter.php create mode 100644 tests/Fixtures/filters/raw.test diff --git a/src/Extension/EscaperExtension.php b/src/Extension/EscaperExtension.php index 60ae2008e3e..b3ebf65fe8b 100644 --- a/src/Extension/EscaperExtension.php +++ b/src/Extension/EscaperExtension.php @@ -14,6 +14,7 @@ use Twig\Environment; use Twig\FileExtensionEscapingStrategy; use Twig\Node\Expression\ConstantExpression; +use Twig\Node\Expression\Filter\RawFilter; use Twig\Node\Node; use Twig\NodeVisitor\EscaperNodeVisitor; use Twig\Runtime\EscaperRuntime; @@ -52,7 +53,7 @@ public function getFilters(): array return [ new TwigFilter('escape', [EscaperRuntime::class, 'escape'], ['is_safe_callback' => [self::class, 'escapeFilterIsSafe']]), new TwigFilter('e', [EscaperRuntime::class, 'escape'], ['is_safe_callback' => [self::class, 'escapeFilterIsSafe']]), - new TwigFilter('raw', [self::class, 'raw'], ['is_safe' => ['all']]), + new TwigFilter('raw', null, ['is_safe' => ['all'], 'node_class' => RawFilter::class]), ]; } @@ -180,18 +181,6 @@ public function addSafeClass(string $class, array $strategies) $this->escaper->addSafeClass($class, $strategies); } - /** - * Marks a variable as being safe. - * - * @param string $string A PHP variable - * - * @internal - */ - public static function raw($string) - { - return $string; - } - /** * @internal */ diff --git a/src/Node/Expression/Filter/RawFilter.php b/src/Node/Expression/Filter/RawFilter.php new file mode 100644 index 00000000000..c3847c99ce6 --- /dev/null +++ b/src/Node/Expression/Filter/RawFilter.php @@ -0,0 +1,26 @@ + + */ +class RawFilter extends FilterExpression +{ + public function compile(Compiler $compiler): void + { + $compiler->subcompile($this->getNode('node')); + } +} diff --git a/src/NodeVisitor/OptimizerNodeVisitor.php b/src/NodeVisitor/OptimizerNodeVisitor.php index 325edab18b6..55f5d6eb960 100644 --- a/src/NodeVisitor/OptimizerNodeVisitor.php +++ b/src/NodeVisitor/OptimizerNodeVisitor.php @@ -15,7 +15,6 @@ use Twig\Node\BlockReferenceNode; use Twig\Node\Expression\BlockReferenceExpression; use Twig\Node\Expression\ConstantExpression; -use Twig\Node\Expression\FilterExpression; use Twig\Node\Expression\FunctionExpression; use Twig\Node\Expression\GetAttrExpression; use Twig\Node\Expression\NameExpression; @@ -59,6 +58,10 @@ public function __construct(int $optimizers = -1) throw new \InvalidArgumentException(\sprintf('Optimizer mode "%s" is not valid.', $optimizers)); } + if (-1 !== $optimizers && self::OPTIMIZE_RAW_FILTER === (self::OPTIMIZE_RAW_FILTER & $optimizers)) { + trigger_deprecation('twig/twig', '3.11', 'The "Twig\NodeVisitor\OptimizerNodeVisitor::OPTIMIZE_RAW_FILTER" option is deprecated and does nothing.'); + } + $this->optimizers = $optimizers; } @@ -77,10 +80,6 @@ public function leaveNode(Node $node, Environment $env): ?Node $this->leaveOptimizeFor($node); } - if (self::OPTIMIZE_RAW_FILTER === (self::OPTIMIZE_RAW_FILTER & $this->optimizers)) { - $node = $this->optimizeRawFilter($node); - } - $node = $this->optimizePrintNode($node); if (self::OPTIMIZE_TEXT_NODES === (self::OPTIMIZE_TEXT_NODES & $this->optimizers)) { @@ -153,18 +152,6 @@ private function optimizePrintNode(Node $node): Node return $node; } - /** - * Removes "raw" filters. - */ - private function optimizeRawFilter(Node $node): Node - { - if ($node instanceof FilterExpression && 'raw' == $node->getNode('filter')->getAttribute('value')) { - return $node->getNode('node'); - } - - return $node; - } - /** * Optimizes "for" tag by removing the "loop" variable creation whenever possible. */ diff --git a/tests/Fixtures/filters/raw.test b/tests/Fixtures/filters/raw.test new file mode 100644 index 00000000000..b23513a608f --- /dev/null +++ b/tests/Fixtures/filters/raw.test @@ -0,0 +1,8 @@ +--TEST-- +"raw" filter excludes a variable from being escaped +--TEMPLATE-- +{{ br|raw }} +--DATA-- +return ['br' => '
    '] +--EXPECT-- +
    diff --git a/tests/NodeVisitor/OptimizerTest.php b/tests/NodeVisitor/OptimizerTest.php index b9eeea9325c..b670d89495b 100644 --- a/tests/NodeVisitor/OptimizerTest.php +++ b/tests/NodeVisitor/OptimizerTest.php @@ -29,7 +29,6 @@ public function testConstructor() $this->expectNotToPerformAssertions(); new OptimizerNodeVisitor( OptimizerNodeVisitor::OPTIMIZE_FOR - | OptimizerNodeVisitor::OPTIMIZE_RAW_FILTER | OptimizerNodeVisitor::OPTIMIZE_TEXT_NODES ); } From de7767814b0e34b3bfea486e35664c5928876f32 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sun, 28 Jul 2024 10:14:39 +0200 Subject: [PATCH 302/812] Remove deprecate feature in docs --- doc/api.rst | 3 --- 1 file changed, 3 deletions(-) diff --git a/doc/api.rst b/doc/api.rst index 7b780189362..219bdec3395 100644 --- a/doc/api.rst +++ b/doc/api.rst @@ -560,9 +560,6 @@ Twig supports the following optimizations: * ``\Twig\NodeVisitor\OptimizerNodeVisitor::OPTIMIZE_FOR``, optimizes the ``for`` tag by removing the ``loop`` variable creation whenever possible. -* ``\Twig\NodeVisitor\OptimizerNodeVisitor::OPTIMIZE_RAW_FILTER``, removes the ``raw`` - filter whenever possible. - * ``\Twig\NodeVisitor\OptimizerNodeVisitor::OPTIMIZE_TEXT_NODES``, optimizes the text nodes by merging consecutive text nodes into a single one. From a17891292c017d3d2907d09a8e560cfbfe5c5115 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sun, 28 Jul 2024 10:50:35 +0200 Subject: [PATCH 303/812] Fix raw filter compat --- src/Node/Expression/FilterExpression.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/Node/Expression/FilterExpression.php b/src/Node/Expression/FilterExpression.php index c803d5708cb..2241adee4c7 100644 --- a/src/Node/Expression/FilterExpression.php +++ b/src/Node/Expression/FilterExpression.php @@ -25,6 +25,13 @@ public function __construct(Node $node, ConstantExpression $filterName, Node $ar public function compile(Compiler $compiler): void { $name = $this->getNode('filter')->getAttribute('value'); + if ('raw' === $name) { + trigger_deprecation('twig/twig', '3.11', 'Creating the "raw" filter via "FilterExpression" is deprecated; use "RawFilter" instead.'); + + $compiler->subcompile($this->getNode('node')); + + return; + } $filter = $compiler->getEnvironment()->getFilter($name); $this->setAttribute('name', $name); From 6e55b5f9569a9ac0874f2759d144fb203d5c2a0c Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sun, 28 Jul 2024 11:03:29 +0200 Subject: [PATCH 304/812] Simplify usage of RawFilter --- src/Node/Expression/Filter/RawFilter.php | 14 +++++++++ tests/Node/Expression/Filter/RawTest.php | 38 ++++++++++++++++++++++++ 2 files changed, 52 insertions(+) create mode 100644 tests/Node/Expression/Filter/RawTest.php diff --git a/src/Node/Expression/Filter/RawFilter.php b/src/Node/Expression/Filter/RawFilter.php index c3847c99ce6..584942306f2 100644 --- a/src/Node/Expression/Filter/RawFilter.php +++ b/src/Node/Expression/Filter/RawFilter.php @@ -12,13 +12,27 @@ namespace Twig\Node\Expression\Filter; use Twig\Compiler; +use Twig\Node\Expression\ConstantExpression; use Twig\Node\Expression\FilterExpression; +use Twig\Node\Node; /** * @author Fabien Potencier */ class RawFilter extends FilterExpression { + public function __construct(Node $node, ?ConstantExpression $filterName = null, ?Node $arguments = null, int $lineno = 0, ?string $tag = null) + { + if (null === $filterName) { + $filterName = new ConstantExpression('raw', $node->getTemplateLine()); + } + if (null === $arguments) { + $arguments = new Node(); + } + + parent::__construct($node, $filterName, $arguments, $lineno ?: $node->getTemplateLine(), $tag ?: $node->getNodeTag()); + } + public function compile(Compiler $compiler): void { $compiler->subcompile($this->getNode('node')); diff --git a/tests/Node/Expression/Filter/RawTest.php b/tests/Node/Expression/Filter/RawTest.php new file mode 100644 index 00000000000..89e495cca54 --- /dev/null +++ b/tests/Node/Expression/Filter/RawTest.php @@ -0,0 +1,38 @@ +assertSame(12, $filter->getTemplateLine()); + $this->assertSame('raw', $filter->getNode('filter')->getAttribute('value')); + $this->assertSame($node, $filter->getNode('node')); + $this->assertSame(0, count($filter->getNode('arguments'))); + } + + public function getTests() + { + $node = new RawFilter(new ConstantExpression('foo', 12)); + + return [ + [$node, '"foo"'], + ]; + } +} From 8e72c84c6362a3a7937e985d2c2cfaf9cd2e8942 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sun, 28 Jul 2024 11:00:56 +0200 Subject: [PATCH 305/812] Use RawFilter in cache extra --- extra/cache-extra/TokenParser/CacheTokenParser.php | 4 ++-- extra/cache-extra/composer.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/extra/cache-extra/TokenParser/CacheTokenParser.php b/extra/cache-extra/TokenParser/CacheTokenParser.php index 4cf8d482d05..e6d6e1c84fa 100644 --- a/extra/cache-extra/TokenParser/CacheTokenParser.php +++ b/extra/cache-extra/TokenParser/CacheTokenParser.php @@ -14,6 +14,7 @@ use Twig\Error\SyntaxError; use Twig\Extra\Cache\Node\CacheNode; use Twig\Node\Expression\ConstantExpression; +use Twig\Node\Expression\Filter\RawFilter; use Twig\Node\Expression\FilterExpression; use Twig\Node\Node; use Twig\Node\PrintNode; @@ -58,9 +59,8 @@ public function parse(Token $token): Node $stream->expect(Token::BLOCK_END_TYPE); $body = new CacheNode($key, $ttl, $tags, $body, $token->getLine(), $this->getTag()); - $body = new FilterExpression($body, new ConstantExpression('raw', $token->getLine()), new Node(), $token->getLine()); - return new PrintNode($body, $token->getLine(), $this->getTag()); + return new PrintNode(new RawFilter($body), $token->getLine(), $this->getTag()); } public function decideCacheEnd(Token $token): bool diff --git a/extra/cache-extra/composer.json b/extra/cache-extra/composer.json index 97fce8b81c9..60d05b90f42 100644 --- a/extra/cache-extra/composer.json +++ b/extra/cache-extra/composer.json @@ -17,7 +17,7 @@ "require": { "php": ">=7.2.5", "symfony/cache": "^5.4|^6.4|^7.0", - "twig/twig": "^3.9" + "twig/twig": "^3.11" }, "require-dev": { "symfony/phpunit-bridge": "^6.4|^7.0" From 215f9880f1bbd10cf01317c4cb1b40a4ebb5590e Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sun, 28 Jul 2024 16:25:21 +0200 Subject: [PATCH 306/812] Remove the templateClassPrefix property on Environment --- src/Environment.php | 3 +-- src/Test/IntegrationTestCase.php | 5 ----- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/src/Environment.php b/src/Environment.php index ead76e3707f..9efe23d21ba 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -63,7 +63,6 @@ class Environment private $resolvedGlobals; private $loadedTemplates; private $strictVariables; - private $templateClassPrefix = '__TwigTemplate_'; private $originalCache; private $extensionSet; private $runtimeLoaders = []; @@ -290,7 +289,7 @@ public function getTemplateClass(string $name, ?int $index = null): string { $key = $this->getLoader()->getCacheKey($name).$this->optionsHash; - return $this->templateClassPrefix.hash(\PHP_VERSION_ID < 80100 ? 'sha256' : 'xxh128', $key).(null === $index ? '' : '___'.$index); + return '__TwigTemplate_'.hash(\PHP_VERSION_ID < 80100 ? 'sha256' : 'xxh128', $key).(null === $index ? '' : '___'.$index); } /** diff --git a/src/Test/IntegrationTestCase.php b/src/Test/IntegrationTestCase.php index 6462e413d66..5519dd0a99f 100644 --- a/src/Test/IntegrationTestCase.php +++ b/src/Test/IntegrationTestCase.php @@ -185,11 +185,6 @@ protected function doIntegrationTest($file, $message, $condition, $templates, $e $twig->addFunction($function); } - // avoid using the same PHP class name for different cases - $p = new \ReflectionProperty($twig, 'templateClassPrefix'); - $p->setAccessible(true); - $p->setValue($twig, '__TwigTemplate_'.hash(\PHP_VERSION_ID < 80100 ? 'sha256' : 'xxh128', random_bytes(32), false).'_'); - $deprecations = []; try { $prevHandler = set_error_handler(function ($type, $msg, $file, $line, $context = []) use (&$deprecations, &$prevHandler) { From df4e66f420b30e334f18de604ea7501227d18c1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Andr=C3=A9?= Date: Tue, 30 Jul 2024 00:36:02 +0200 Subject: [PATCH 307/812] Fix EscaperRuntime namespace From `Twig\Tests` to `Twig\Tests\Runtime` --- tests/Runtime/EscaperRuntimeTest.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/Runtime/EscaperRuntimeTest.php b/tests/Runtime/EscaperRuntimeTest.php index 11764f4384f..593563a3b2a 100644 --- a/tests/Runtime/EscaperRuntimeTest.php +++ b/tests/Runtime/EscaperRuntimeTest.php @@ -1,6 +1,6 @@ setEscaper('foo', 'Twig\Tests\escaper'); + $escaper->setEscaper('foo', 'Twig\Tests\Runtime\escaper'); $this->assertSame($expected, $escaper->escape($string, $strategy, $charset)); } @@ -381,10 +381,10 @@ public function testObjectEscaping(string $escapedHtml, string $escapedJs, array public function provideObjectsForEscaping() { return [ - ['<br />', '
    ', ['\Twig\Tests\Extension_TestClass' => ['js']]], - ['
    ', '\u003Cbr\u0020\/\u003E', ['\Twig\Tests\Extension_TestClass' => ['html']]], - ['<br />', '
    ', ['\Twig\Tests\Extension_SafeHtmlInterface' => ['js']]], - ['
    ', '
    ', ['\Twig\Tests\Extension_SafeHtmlInterface' => ['all']]], + ['<br />', '
    ', ['\Twig\Tests\Runtime\Extension_TestClass' => ['js']]], + ['
    ', '\u003Cbr\u0020\/\u003E', ['\Twig\Tests\Runtime\Extension_TestClass' => ['html']]], + ['<br />', '
    ', ['\Twig\Tests\Runtime\Extension_SafeHtmlInterface' => ['js']]], + ['
    ', '
    ', ['\Twig\Tests\Runtime\Extension_SafeHtmlInterface' => ['all']]], ]; } } From ae4f284043fddf3c82fdf3b6570c6cb91effaa24 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Tue, 30 Jul 2024 09:45:47 +0200 Subject: [PATCH 308/812] Fix various phpstan errors --- src/Extension/AbstractExtension.php | 2 +- src/Extension/CoreExtension.php | 2 +- src/Extension/EscaperExtension.php | 4 ++-- src/Extension/ExtensionInterface.php | 7 +++---- src/ExtensionSet.php | 9 +++++---- src/Node/Expression/CallExpression.php | 2 +- src/Node/Expression/MethodCallExpression.php | 4 +++- src/Node/PrintNode.php | 7 +++++-- src/Runtime/EscaperRuntime.php | 5 +++-- src/Test/NodeTestCase.php | 6 +++++- src/Util/DeprecationCollector.php | 2 ++ 11 files changed, 31 insertions(+), 19 deletions(-) diff --git a/src/Extension/AbstractExtension.php b/src/Extension/AbstractExtension.php index 422925f31b9..a1b083b6884 100644 --- a/src/Extension/AbstractExtension.php +++ b/src/Extension/AbstractExtension.php @@ -40,6 +40,6 @@ public function getFunctions() public function getOperators() { - return []; + return [[], []]; } } diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index d0e1dccc7c7..7383a68a48b 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -1516,7 +1516,7 @@ public static function batch($items, $size, $fill = null, $preserveKeys = true): throw new RuntimeError(\sprintf('The "batch" filter expects a sequence/mapping or "Traversable", got "%s".', \is_object($items) ? \get_class($items) : \gettype($items))); } - $size = ceil($size); + $size = (int) ceil($size); $result = array_chunk(self::toArray($items, $preserveKeys), $size, $preserveKeys); diff --git a/src/Extension/EscaperExtension.php b/src/Extension/EscaperExtension.php index b3ebf65fe8b..f453ada93b6 100644 --- a/src/Extension/EscaperExtension.php +++ b/src/Extension/EscaperExtension.php @@ -119,7 +119,7 @@ public function getDefaultStrategy(string $name) * Defines a new escaper to be used via the escape filter. * * @param string $strategy The strategy name that should be used as a strategy in the escape call - * @param callable(Environment, string, string) $callable A valid PHP callable + * @param callable(Environment, string, string): string $callable A valid PHP callable * * @deprecated since Twig 3.10 */ @@ -142,7 +142,7 @@ public function setEscaper($strategy, callable $callable) /** * Gets all defined escapers. * - * @return array An array of escapers + * @return array An array of escapers * * @deprecated since Twig 3.10 */ diff --git a/src/Extension/ExtensionInterface.php b/src/Extension/ExtensionInterface.php index ab9c2c37c19..10a42b6b161 100644 --- a/src/Extension/ExtensionInterface.php +++ b/src/Extension/ExtensionInterface.php @@ -12,8 +12,7 @@ namespace Twig\Extension; use Twig\ExpressionParser; -use Twig\Node\Expression\Binary\AbstractBinary; -use Twig\Node\Expression\Unary\AbstractUnary; +use Twig\Node\Expression\AbstractExpression; use Twig\NodeVisitor\NodeVisitorInterface; use Twig\TokenParser\TokenParserInterface; use Twig\TwigFilter; @@ -68,8 +67,8 @@ public function getFunctions(); * @return array First array of unary operators, second array of binary operators * * @psalm-return array{ - * array}>, - * array, associativity: ExpressionParser::OPERATOR_*}> + * array}>, + * array, associativity: ExpressionParser::OPERATOR_*}> * } */ public function getOperators(); diff --git a/src/ExtensionSet.php b/src/ExtensionSet.php index 34b600063a2..8b59a13e186 100644 --- a/src/ExtensionSet.php +++ b/src/ExtensionSet.php @@ -15,6 +15,7 @@ use Twig\Extension\ExtensionInterface; use Twig\Extension\GlobalsInterface; use Twig\Extension\StagingExtension; +use Twig\Node\Expression\AbstractExpression; use Twig\Node\Expression\Binary\AbstractBinary; use Twig\Node\Expression\Unary\AbstractUnary; use Twig\NodeVisitor\NodeVisitorInterface; @@ -39,9 +40,9 @@ final class ExtensionSet private $tests; /** @var array */ private $functions; - /** @var array}> */ + /** @var array}> */ private $unaryOperators; - /** @var array, associativity: ExpressionParser::OPERATOR_*}> */ + /** @var array, associativity: ExpressionParser::OPERATOR_*}> */ private $binaryOperators; /** @var array */ private $globals; @@ -391,7 +392,7 @@ public function getTest(string $name): ?TwigTest } /** - * @return array}> + * @return array}> */ public function getUnaryOperators(): array { @@ -403,7 +404,7 @@ public function getUnaryOperators(): array } /** - * @return array, associativity: ExpressionParser::OPERATOR_*}> + * @return array, associativity: ExpressionParser::OPERATOR_*}> */ public function getBinaryOperators(): array { diff --git a/src/Node/Expression/CallExpression.php b/src/Node/Expression/CallExpression.php index 997e8dc9f55..cd81df47a38 100644 --- a/src/Node/Expression/CallExpression.php +++ b/src/Node/Expression/CallExpression.php @@ -279,7 +279,7 @@ private function getCallableParameters($callable, bool $isVariadic): array $isPhpVariadic = false; if ($isVariadic) { $argument = end($parameters); - $isArray = $argument && $argument->hasType() && 'array' === $argument->getType()->getName(); + $isArray = $argument && $argument->hasType() && $argument->getType() instanceof \ReflectionNamedType && 'array' === $argument->getType()->getName(); if ($isArray && $argument->isDefaultValueAvailable() && [] === $argument->getDefaultValue()) { array_pop($parameters); } elseif ($argument && $argument->isVariadic()) { diff --git a/src/Node/Expression/MethodCallExpression.php b/src/Node/Expression/MethodCallExpression.php index 6fa1c3f9e08..01806f91d10 100644 --- a/src/Node/Expression/MethodCallExpression.php +++ b/src/Node/Expression/MethodCallExpression.php @@ -46,7 +46,9 @@ public function compile(Compiler $compiler): void ->raw(', [') ; $first = true; - foreach ($this->getNode('arguments')->getKeyValuePairs() as $pair) { + /** @var ArrayExpression */ + $args = $this->getNode('arguments'); + foreach ($args->getKeyValuePairs() as $pair) { if (!$first) { $compiler->raw(', '); } diff --git a/src/Node/PrintNode.php b/src/Node/PrintNode.php index bdc738301f1..da442d85207 100644 --- a/src/Node/PrintNode.php +++ b/src/Node/PrintNode.php @@ -31,10 +31,13 @@ public function __construct(AbstractExpression $expr, int $lineno, ?string $tag public function compile(Compiler $compiler): void { + /** @var AbstractExpression */ + $expr = $this->getNode('expr'); + $compiler ->addDebugInfo($this) - ->write($this->getNode('expr')->isGenerator() ? 'yield from ' : 'yield ') - ->subcompile($this->getNode('expr')) + ->write($expr->isGenerator() ? 'yield from ' : 'yield ') + ->subcompile($expr) ->raw(";\n") ; } diff --git a/src/Runtime/EscaperRuntime.php b/src/Runtime/EscaperRuntime.php index 433a0250cca..4df83430614 100644 --- a/src/Runtime/EscaperRuntime.php +++ b/src/Runtime/EscaperRuntime.php @@ -17,6 +17,7 @@ final class EscaperRuntime implements RuntimeExtensionInterface { + /** @var array */ private $escapers = []; /** @internal */ @@ -36,7 +37,7 @@ public function __construct($charset = 'UTF-8') * Defines a new escaper to be used via the escape filter. * * @param string $strategy The strategy name that should be used as a strategy in the escape call - * @param callable(string $string, string $charset) $callable A valid PHP callable + * @param callable(string $string, string $charset): string $callable A valid PHP callable */ public function setEscaper($strategy, callable $callable) { @@ -46,7 +47,7 @@ public function setEscaper($strategy, callable $callable) /** * Gets all defined escapers. * - * @return array An array of escapers + * @return array An array of escapers */ public function getEscapers() { diff --git a/src/Test/NodeTestCase.php b/src/Test/NodeTestCase.php index e6a95494888..4046f08cdc9 100644 --- a/src/Test/NodeTestCase.php +++ b/src/Test/NodeTestCase.php @@ -53,7 +53,11 @@ protected function getCompiler(?Environment $environment = null) protected function getEnvironment() { - return $this->currentEnv = new Environment(new ArrayLoader()); + if (!$this->currentEnv) { + $this->currentEnv = new Environment(new ArrayLoader()); + } + + return $this->currentEnv; } protected function getVariableGetter($name, $line = false) diff --git a/src/Util/DeprecationCollector.php b/src/Util/DeprecationCollector.php index 378b666bdb8..ad531061716 100644 --- a/src/Util/DeprecationCollector.php +++ b/src/Util/DeprecationCollector.php @@ -60,6 +60,8 @@ public function collect(\Traversable $iterator): array if (\E_USER_DEPRECATED === $type) { $deprecations[] = $msg; } + + return false; }); foreach ($iterator as $name => $contents) { From 4e262511930e408e4c7eda07b1c977f2ea98575c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Andr=C3=A9?= Date: Sun, 14 Jan 2024 00:40:23 +0100 Subject: [PATCH 309/812] Add new "find" filter --- CHANGELOG | 1 + doc/filters/find.rst | 57 ++++++++++++++++++++++++++++++++ src/Extension/CoreExtension.php | 17 ++++++++++ tests/Fixtures/filters/find.test | 46 ++++++++++++++++++++++++++ 4 files changed, 121 insertions(+) create mode 100644 doc/filters/find.rst create mode 100644 tests/Fixtures/filters/find.test diff --git a/CHANGELOG b/CHANGELOG index 39b04988901..40fc1b4a0ec 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,6 @@ # 3.11.0 (2024-XX-XX) + * Add the `find` filter * Fix optimizer mode validation in `OptimizerNodeVisitor` * Add the possibility to yield from a generator in `PrintNode` * Add the `shuffle` filter diff --git a/doc/filters/find.rst b/doc/filters/find.rst new file mode 100644 index 00000000000..f11b68e36c4 --- /dev/null +++ b/doc/filters/find.rst @@ -0,0 +1,57 @@ +``find`` +======== + +.. versionadded:: 3.11 + + The ``find`` filter was added in Twig 3.11. + +The ``find`` filter returns the first element of a sequence matching an arrow +function. The arrow function receives the value of the sequence: + +.. code-block:: twig + + {% set sizes = [34, 36, 38, 40, 42] %} + + {{ sizes|find(v => v > 38) }} + {# output 40 #} + +It also works with mappings: + +.. code-block:: twig + + {% set sizes = { + xxs: 32, + xs: 34, + s: 36, + m: 38, + l: 40, + xl: 42, + } %} + + {{ sizes|find(v => v > 38) }} + + {# output 40 #} + +The arrow function also receives the key as a second argument: + +.. code-block:: twig + + {{ sizes|find((v, k) => 's' not in k) }} + + {# output 38 #} + +Note that the arrow function has access to the current context: + +.. code-block:: twig + + {% set my_size = 39 %} + + {{ sizes|find(v => v >= my_size) }} + + {# output 40 #} + +Arguments +--------- + +* ``array``: The sequence or mapping +* ``arrow``: The arrow function diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index d0e1dccc7c7..d212e5ccda5 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -218,6 +218,7 @@ public function getFilters(): array new TwigFilter('filter', [self::class, 'filter'], ['needs_environment' => true]), new TwigFilter('map', [self::class, 'map'], ['needs_environment' => true]), new TwigFilter('reduce', [self::class, 'reduce'], ['needs_environment' => true]), + new TwigFilter('find', [self::class, 'find'], ['needs_environment' => true]), // string/array filters new TwigFilter('reverse', [self::class, 'reverse'], ['needs_charset' => true]), @@ -1775,6 +1776,22 @@ public static function filter(Environment $env, $array, $arrow) return new \CallbackFilterIterator(new \IteratorIterator($array), $arrow); } + /** + * @internal + */ + public static function find(Environment $env, $array, $arrow) + { + self::checkArrowInSandbox($env, $arrow, 'find', 'filter'); + + foreach ($array as $k => $v) { + if ($arrow($v, $k)) { + return $v; + } + } + + return null; + } + /** * @internal */ diff --git a/tests/Fixtures/filters/find.test b/tests/Fixtures/filters/find.test new file mode 100644 index 00000000000..3d1dbd422c1 --- /dev/null +++ b/tests/Fixtures/filters/find.test @@ -0,0 +1,46 @@ +--TEST-- +"filter" filter +--TEMPLATE-- + +{{ [1, 2]|find((v) => v > 3) }} + +{{ [1, 5, 3, 4, 5]|find((v) => v > 3) }} + +{{ [1, 5, 3, 4, 5]|find((v) => v > 3) }} + +{{ {a: 1, b: 2, c: 5, d: 8}|find(v => v > 3) }} + +{{ {a: 1, b: 2, c: 5, d: 8}|find((v, k) => (v > 3) and (k != "c")) }} + +{{ [1, 5, 3, 4, 5]|find(v => v > 3) }} + +{{ it|find((v) => v > 3) }} + +{{ ita|find(v => v > 3) }} + +{{ xml|find(x => true) }} + +--DATA-- +return [ + 'it' => new \ArrayIterator(['a' => 1, 'b' => 2, 'c' => 5, 'd' => 8]), + 'ita' => new Twig\Tests\IteratorAggregateStub(['a' => 1, 'b' => 2, 'c' => 5, 'd' => 8]), + 'xml' => new \SimpleXMLElement('foobarbaz'), +] +--EXPECT-- + + +5 + +5 + +5 + +8 + +5 + +5 + +5 + +foo From fc644721451e02285e51ed2343c43f2f18b5f8a3 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sun, 4 Aug 2024 16:28:18 +0200 Subject: [PATCH 310/812] Fix dumping callables on Node::__toString() --- src/Node/Node.php | 2 +- tests/Node/NodeTest.php | 26 ++++++++++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) create mode 100644 tests/Node/NodeTest.php diff --git a/src/Node/Node.php b/src/Node/Node.php index 2da6bd88dff..17b60dc354a 100644 --- a/src/Node/Node.php +++ b/src/Node/Node.php @@ -54,7 +54,7 @@ public function __toString() { $attributes = []; foreach ($this->attributes as $name => $value) { - $attributes[] = \sprintf('%s: %s', $name, str_replace("\n", '', var_export($value, true))); + $attributes[] = \sprintf('%s: %s', $name, is_callable($value) ? '\Closure' : str_replace("\n", '', var_export($value, true))); } $repr = [static::class.'('.implode(', ', $attributes)]; diff --git a/tests/Node/NodeTest.php b/tests/Node/NodeTest.php new file mode 100644 index 00000000000..8e82af5b9e9 --- /dev/null +++ b/tests/Node/NodeTest.php @@ -0,0 +1,26 @@ + function () { return '1'; }], 1); + + $this->assertEquals('Twig\Node\Node(value: \Closure)', (string) $node); + } +} From cea171433156fc3f0aa3e9739101b332055380f9 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Wed, 7 Aug 2024 11:12:53 +0200 Subject: [PATCH 311/812] Add a note about how macros can override existing functions --- doc/tags/macro.rst | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/doc/tags/macro.rst b/doc/tags/macro.rst index effa6b6bdc9..5ad2cf020d8 100644 --- a/doc/tags/macro.rst +++ b/doc/tags/macro.rst @@ -7,7 +7,7 @@ are useful to reuse template fragments to not repeat yourself. Macros are defined in regular templates. Imagine having a generic helper template that define how to render HTML forms -via macros (called ``forms.html``): +via macros (called ``forms.twig``): .. code-block:: html+twig @@ -49,9 +49,9 @@ tag: .. code-block:: twig - {% import "forms.html" as forms %} + {% import "forms.twig" as forms %} -The above ``import`` call imports the ``forms.html`` file (which can contain +The above ``import`` call imports the ``forms.twig`` file (which can contain only macros, or a template and some macros), and import the macros as items of the ``forms`` local variable. @@ -67,11 +67,23 @@ via the ``from`` tag: .. code-block:: html+twig - {% from 'forms.html' import input as input_field, textarea %} + {% from 'forms.twig' import input as input_field, textarea %}

    {{ input_field('password', '', 'password') }}

    {{ textarea('comment') }}

    +.. caution:: + + As macros imported via ``from`` are called like functions, be careful to + not override existing functions: + + .. code-block:: twig + + {% from 'forms.twig' import input as include %} + + {# include refers to the macro and not to the built-in "include" function #} + {{ include() }} + .. tip:: When macro usages and definitions are in the same template, you don't need to From 0dbe1d9f6d2437d5588d9eeab88e8cee1627e240 Mon Sep 17 00:00:00 2001 From: "Quentin D." <4972091+Okhoshi@users.noreply.github.com> Date: Sun, 4 Aug 2024 17:06:21 +0000 Subject: [PATCH 312/812] Introduce ChainCache and ReadOnlyFilesystemCache Signed-off-by: Quentin Devos <4972091+Okhoshi@users.noreply.github.com> --- src/Cache/ChainCache.php | 81 +++++++++ src/Cache/ReadOnlyFilesystemCache.php | 25 +++ tests/Cache/ChainTest.php | 231 +++++++++++++++++++++++++ tests/Cache/ReadOnlyFilesystemTest.php | 132 ++++++++++++++ 4 files changed, 469 insertions(+) create mode 100644 src/Cache/ChainCache.php create mode 100644 src/Cache/ReadOnlyFilesystemCache.php create mode 100644 tests/Cache/ChainTest.php create mode 100644 tests/Cache/ReadOnlyFilesystemTest.php diff --git a/src/Cache/ChainCache.php b/src/Cache/ChainCache.php new file mode 100644 index 00000000000..18c66f35f95 --- /dev/null +++ b/src/Cache/ChainCache.php @@ -0,0 +1,81 @@ + + */ +final class ChainCache implements CacheInterface +{ + private $caches; + + /** + * @param iterable $caches The ordered list of caches used to store and fetch cached items + */ + public function __construct(iterable $caches) + { + $this->caches = $caches; + } + + public function generateKey(string $name, string $className): string + { + return $className.'#'.$name; + } + + public function write(string $key, string $content): void + { + $splitKey = $this->splitKey($key); + + foreach ($this->caches as $cache) { + $cache->write($cache->generateKey(...$splitKey), $content); + } + } + + public function load(string $key): void + { + [$name, $className] = $this->splitKey($key); + + foreach ($this->caches as $cache) { + $cache->load($cache->generateKey($name, $className)); + + if (class_exists($className, false)) { + break; + } + } + } + + public function getTimestamp(string $key): int + { + $splitKey = $this->splitKey($key); + + foreach ($this->caches as $cache) { + if (0 < $timestamp = $cache->getTimestamp($cache->generateKey(...$splitKey))) { + return $timestamp; + } + } + + return 0; + } + + /** + * @return string[] + */ + private function splitKey(string $key): array + { + return array_reverse(explode('#', $key, 2)); + } +} diff --git a/src/Cache/ReadOnlyFilesystemCache.php b/src/Cache/ReadOnlyFilesystemCache.php new file mode 100644 index 00000000000..3ba6514c950 --- /dev/null +++ b/src/Cache/ReadOnlyFilesystemCache.php @@ -0,0 +1,25 @@ + + */ +class ReadOnlyFilesystemCache extends FilesystemCache +{ + public function write(string $key, string $content): void + { + // Do nothing with the content, it's a read-only filesystem. + } +} diff --git a/tests/Cache/ChainTest.php b/tests/Cache/ChainTest.php new file mode 100644 index 00000000000..f87e1629355 --- /dev/null +++ b/tests/Cache/ChainTest.php @@ -0,0 +1,231 @@ +classname = '__Twig_Tests_Cache_ChainTest_Template_'.$nonce; + $this->directory = sys_get_temp_dir().'/twig-test'; + $this->cache = new ChainCache([ + new FilesystemCache($this->directory.'/A'), + new FilesystemCache($this->directory.'/B'), + ]); + $this->key = $this->cache->generateKey('_test_', $this->classname); + } + + protected function tearDown(): void + { + if (file_exists($this->directory)) { + FilesystemHelper::removeDir($this->directory); + } + } + + public function testLoadInA() + { + $cache = new FilesystemCache($this->directory.'/A'); + $key = $cache->generateKey('_test_', $this->classname); + + $dir = \dirname($key); + @mkdir($dir, 0777, true); + $this->assertDirectoryExists($dir); + $this->assertFalse(class_exists($this->classname, false)); + + $content = $this->generateSource(); + file_put_contents($key, $content); + var_dump($key); + + $this->cache->load($this->key); + + $this->assertTrue(class_exists($this->classname, false)); + } + + public function testLoadInB() + { + $cache = new FilesystemCache($this->directory.'/B'); + $key = $cache->generateKey('_test_', $this->classname); + + $dir = \dirname($key); + @mkdir($dir, 0777, true); + $this->assertDirectoryExists($dir); + $this->assertFalse(class_exists($this->classname, false)); + + $content = $this->generateSource(); + file_put_contents($key, $content); + var_dump($key); + + $this->cache->load($this->key); + + $this->assertTrue(class_exists($this->classname, false)); + } + + public function testLoadInBoth() + { + $cache = new FilesystemCache($this->directory.'/A'); + $key = $cache->generateKey('_test_', $this->classname); + + $dir = \dirname($key); + @mkdir($dir, 0777, true); + $this->assertDirectoryExists($dir); + $this->assertFalse(class_exists($this->classname, false)); + + $content = $this->generateSource(); + file_put_contents($key, $content); + + $cache = new FilesystemCache($this->directory.'/B'); + $key = $cache->generateKey('_test_', $this->classname); + + $dir = \dirname($key); + @mkdir($dir, 0777, true); + $this->assertDirectoryExists($dir); + $this->assertFalse(class_exists($this->classname, false)); + + $content = $this->generateSource(); + file_put_contents($key, $content); + + $this->cache->load($this->key); + + $this->assertTrue(class_exists($this->classname, false)); + } + + public function testLoadMissing() + { + $this->assertFalse(class_exists($this->classname, false)); + + $this->cache->load($this->key); + + $this->assertFalse(class_exists($this->classname, false)); + } + + public function testWrite() + { + $content = $this->generateSource(); + + $cacheA = new FilesystemCache($this->directory.'/A'); + $keyA = $cacheA->generateKey('_test_', $this->classname); + + $this->assertFileDoesNotExist($keyA); + $this->assertFileDoesNotExist($this->directory.'/A'); + + $cacheB = new FilesystemCache($this->directory.'/B'); + $keyB = $cacheB->generateKey('_test_', $this->classname); + + $this->assertFileDoesNotExist($keyB); + $this->assertFileDoesNotExist($this->directory.'/B'); + + $this->cache->write($this->key, $content); + + $this->assertFileExists($this->directory.'/A'); + $this->assertFileExists($keyA); + $this->assertSame(file_get_contents($keyA), $content); + + $this->assertFileExists($this->directory.'/B'); + $this->assertFileExists($keyB); + $this->assertSame(file_get_contents($keyB), $content); + } + + public function testGetTimestampInA() + { + $cache = new FilesystemCache($this->directory.'/A'); + $key = $cache->generateKey('_test_', $this->classname); + + $dir = \dirname($key); + @mkdir($dir, 0777, true); + $this->assertDirectoryExists($dir); + + // Create the file with a specific modification time. + touch($key, 1234567890); + + $this->assertSame(1234567890, $this->cache->getTimestamp($this->key)); + } + + public function testGetTimestampInB() + { + $cache = new FilesystemCache($this->directory.'/B'); + $key = $cache->generateKey('_test_', $this->classname); + + $dir = \dirname($key); + @mkdir($dir, 0777, true); + $this->assertDirectoryExists($dir); + + // Create the file with a specific modification time. + touch($key, 1234567890); + + $this->assertSame(1234567890, $this->cache->getTimestamp($this->key)); + } + + public function testGetTimestampInBoth() + { + $cacheA = new FilesystemCache($this->directory.'/A'); + $keyA = $cacheA->generateKey('_test_', $this->classname); + + $dir = \dirname($keyA); + @mkdir($dir, 0777, true); + $this->assertDirectoryExists($dir); + + // Create the file with a specific modification time. + touch($keyA, 1234567890); + + $cacheB = new FilesystemCache($this->directory.'/B'); + $keyB = $cacheB->generateKey('_test_', $this->classname); + + $dir = \dirname($keyB); + @mkdir($dir, 0777, true); + $this->assertDirectoryExists($dir); + + // Create the file with a specific modification time. + touch($keyB, 1234567891); + + $this->assertSame(1234567890, $this->cache->getTimestamp($this->key)); + } + + public function testGetTimestampMissingFile() + { + $this->assertSame(0, $this->cache->getTimestamp($this->key)); + } + + /** + * @dataProvider provideInput + */ + public function testGenerateKey($expected, $input) + { + $cache = new ChainCache([]); + $this->assertSame($expected, $cache->generateKey($input, static::class)); + } + + public static function provideInput() + { + return [ + ['Twig\Tests\Cache\ChainTest#_test_', '_test_'], + ['Twig\Tests\Cache\ChainTest#_test#with#hashtag_', '_test#with#hashtag_'], + ]; + } + + private function generateSource() + { + return strtr(' $this->classname, + ]); + } +} diff --git a/tests/Cache/ReadOnlyFilesystemTest.php b/tests/Cache/ReadOnlyFilesystemTest.php new file mode 100644 index 00000000000..fe6dffcdba3 --- /dev/null +++ b/tests/Cache/ReadOnlyFilesystemTest.php @@ -0,0 +1,132 @@ +classname = '__Twig_Tests_Cache_ReadOnlyFilesystemTest_Template_'.$nonce; + $this->directory = sys_get_temp_dir().'/twig-test'; + $this->cache = new ReadOnlyFilesystemCache($this->directory); + } + + protected function tearDown(): void + { + if (file_exists($this->directory)) { + FilesystemHelper::removeDir($this->directory); + } + } + + public function testLoad() + { + $key = $this->directory.'/cache/ro-cachefile.php'; + + $dir = \dirname($key); + @mkdir($dir, 0777, true); + $this->assertDirectoryExists($dir); + $this->assertFalse(class_exists($this->classname, false)); + + $content = $this->generateSource(); + file_put_contents($key, $content); + + $this->cache->load($key); + + $this->assertTrue(class_exists($this->classname, false)); + } + + public function testLoadMissing() + { + $key = $this->directory.'/cache/cachefile.php'; + + $this->assertFalse(class_exists($this->classname, false)); + + $this->cache->load($key); + + $this->assertFalse(class_exists($this->classname, false)); + } + + public function testWrite() + { + $key = $this->directory.'/cache/cachefile.php'; + $content = $this->generateSource(); + + $this->assertFileDoesNotExist($key); + $this->assertFileDoesNotExist($this->directory); + + $this->cache->write($key, $content); + + $this->assertFileDoesNotExist($this->directory); + $this->assertFileDoesNotExist($key); + } + + public function testGetTimestamp() + { + $key = $this->directory.'/cache/cachefile.php'; + + $dir = \dirname($key); + @mkdir($dir, 0777, true); + $this->assertDirectoryExists($dir); + + // Create the file with a specific modification time. + touch($key, 1234567890); + + $this->assertSame(1234567890, $this->cache->getTimestamp($key)); + } + + public function testGetTimestampMissingFile() + { + $key = $this->directory.'/cache/cachefile.php'; + $this->assertSame(0, $this->cache->getTimestamp($key)); + } + + /** + * Test file cache is tolerant towards trailing (back)slashes on the configured cache directory. + * + * @dataProvider provideDirectories + */ + public function testGenerateKey($expected, $input) + { + $cache = new ReadOnlyFilesystemCache($input); + $this->assertMatchesRegularExpression($expected, $cache->generateKey('_test_', static::class)); + } + + public static function provideDirectories() + { + $pattern = '#a/b/[a-zA-Z0-9]+/[a-zA-Z0-9]+.php$#'; + + return [ + [$pattern, 'a/b'], + [$pattern, 'a/b/'], + [$pattern, 'a/b\\'], + [$pattern, 'a/b\\/'], + [$pattern, 'a/b\\//'], + ['#/'.substr($pattern, 1), '/a/b'], + ]; + } + + private function generateSource() + { + return strtr(' $this->classname, + ]); + } +} From 693693ba3a4a9e73fc3a0e5f4961bf3d23a7c46d Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Wed, 7 Aug 2024 18:58:14 +0200 Subject: [PATCH 313/812] Mark ConstantExpression as @final --- src/Node/Expression/ConstantExpression.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Node/Expression/ConstantExpression.php b/src/Node/Expression/ConstantExpression.php index 7ddbcc6fa99..2a8909d5469 100644 --- a/src/Node/Expression/ConstantExpression.php +++ b/src/Node/Expression/ConstantExpression.php @@ -14,6 +14,9 @@ use Twig\Compiler; +/** + * @final + */ class ConstantExpression extends AbstractExpression { public function __construct($value, int $lineno) From f5e10e10f1b3f7778e1e5099d0e3c7b42142be9c Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Wed, 7 Aug 2024 19:34:09 +0200 Subject: [PATCH 314/812] Fix CS --- .../TokenParser/CacheTokenParser.php | 2 - extra/markdown-extra/Tests/FunctionalTest.php | 4 +- extra/string-extra/StringExtension.php | 2 +- src/Extension/EscaperExtension.php | 2 +- src/Node/Node.php | 2 +- src/Runtime/EscaperRuntime.php | 2 +- tests/Cache/ChainTest.php | 4 +- tests/Cache/ReadOnlyFilesystemTest.php | 2 +- tests/ExpressionParserTest.php | 108 +++++++++--------- tests/Node/DeprecatedTest.php | 2 +- tests/Node/Expression/Filter/RawTest.php | 2 +- tests/Node/Expression/FilterTest.php | 1 + tests/Node/IncludeTest.php | 2 +- tests/Node/ModuleTest.php | 6 +- tests/TemplateTest.php | 1 - 15 files changed, 70 insertions(+), 72 deletions(-) diff --git a/extra/cache-extra/TokenParser/CacheTokenParser.php b/extra/cache-extra/TokenParser/CacheTokenParser.php index e6d6e1c84fa..61d5d2877c1 100644 --- a/extra/cache-extra/TokenParser/CacheTokenParser.php +++ b/extra/cache-extra/TokenParser/CacheTokenParser.php @@ -13,9 +13,7 @@ use Twig\Error\SyntaxError; use Twig\Extra\Cache\Node\CacheNode; -use Twig\Node\Expression\ConstantExpression; use Twig\Node\Expression\Filter\RawFilter; -use Twig\Node\Expression\FilterExpression; use Twig\Node\Node; use Twig\Node\PrintNode; use Twig\Token; diff --git a/extra/markdown-extra/Tests/FunctionalTest.php b/extra/markdown-extra/Tests/FunctionalTest.php index 42f4ff65a97..0d9b73a59d0 100644 --- a/extra/markdown-extra/Tests/FunctionalTest.php +++ b/extra/markdown-extra/Tests/FunctionalTest.php @@ -70,7 +70,7 @@ public function getMarkdownTests() Great! {% endapply %} EOF - , "

    Hello

    \n+

    Great!

    "], + , "

    Hello

    \n+

    Great!

    "], [<<Hello\n+

    Great!

    "], + , "

    Hello

    \n+

    Great!

    "], ["{{ include('html')|markdown_to_html }}", "

    Hello

    \n+

    Great!

    "], ]; } diff --git a/extra/string-extra/StringExtension.php b/extra/string-extra/StringExtension.php index e541e7c16d4..e0abb845f9f 100644 --- a/extra/string-extra/StringExtension.php +++ b/extra/string-extra/StringExtension.php @@ -84,7 +84,7 @@ private function getInflector(string $locale): InflectorInterface case 'en': return $this->englishInflector ?? $this->englishInflector = new EnglishInflector(); default: - throw new \InvalidArgumentException(sprintf('Locale "%s" is not supported.', $locale)); + throw new \InvalidArgumentException(\sprintf('Locale "%s" is not supported.', $locale)); } } } diff --git a/src/Extension/EscaperExtension.php b/src/Extension/EscaperExtension.php index f453ada93b6..d8e9b6e4814 100644 --- a/src/Extension/EscaperExtension.php +++ b/src/Extension/EscaperExtension.php @@ -118,7 +118,7 @@ public function getDefaultStrategy(string $name) /** * Defines a new escaper to be used via the escape filter. * - * @param string $strategy The strategy name that should be used as a strategy in the escape call + * @param string $strategy The strategy name that should be used as a strategy in the escape call * @param callable(Environment, string, string): string $callable A valid PHP callable * * @deprecated since Twig 3.10 diff --git a/src/Node/Node.php b/src/Node/Node.php index 17b60dc354a..e0e473e8cc4 100644 --- a/src/Node/Node.php +++ b/src/Node/Node.php @@ -54,7 +54,7 @@ public function __toString() { $attributes = []; foreach ($this->attributes as $name => $value) { - $attributes[] = \sprintf('%s: %s', $name, is_callable($value) ? '\Closure' : str_replace("\n", '', var_export($value, true))); + $attributes[] = \sprintf('%s: %s', $name, \is_callable($value) ? '\Closure' : str_replace("\n", '', var_export($value, true))); } $repr = [static::class.'('.implode(', ', $attributes)]; diff --git a/src/Runtime/EscaperRuntime.php b/src/Runtime/EscaperRuntime.php index 4df83430614..b1dac964022 100644 --- a/src/Runtime/EscaperRuntime.php +++ b/src/Runtime/EscaperRuntime.php @@ -36,7 +36,7 @@ public function __construct($charset = 'UTF-8') /** * Defines a new escaper to be used via the escape filter. * - * @param string $strategy The strategy name that should be used as a strategy in the escape call + * @param string $strategy The strategy name that should be used as a strategy in the escape call * @param callable(string $string, string $charset): string $callable A valid PHP callable */ public function setEscaper($strategy, callable $callable) diff --git a/tests/Cache/ChainTest.php b/tests/Cache/ChainTest.php index f87e1629355..bb6ed7cf30d 100644 --- a/tests/Cache/ChainTest.php +++ b/tests/Cache/ChainTest.php @@ -121,13 +121,13 @@ public function testLoadMissing() public function testWrite() { $content = $this->generateSource(); - + $cacheA = new FilesystemCache($this->directory.'/A'); $keyA = $cacheA->generateKey('_test_', $this->classname); $this->assertFileDoesNotExist($keyA); $this->assertFileDoesNotExist($this->directory.'/A'); - + $cacheB = new FilesystemCache($this->directory.'/B'); $keyB = $cacheB->generateKey('_test_', $this->classname); diff --git a/tests/Cache/ReadOnlyFilesystemTest.php b/tests/Cache/ReadOnlyFilesystemTest.php index fe6dffcdba3..424f605239d 100644 --- a/tests/Cache/ReadOnlyFilesystemTest.php +++ b/tests/Cache/ReadOnlyFilesystemTest.php @@ -12,8 +12,8 @@ */ use PHPUnit\Framework\TestCase; -use Twig\Tests\FilesystemHelper; use Twig\Cache\ReadOnlyFilesystemCache; +use Twig\Tests\FilesystemHelper; class ReadOnlyFilesystemTest extends TestCase { diff --git a/tests/ExpressionParserTest.php b/tests/ExpressionParserTest.php index 8dd91ba670c..b1e3d5017bd 100644 --- a/tests/ExpressionParserTest.php +++ b/tests/ExpressionParserTest.php @@ -93,73 +93,73 @@ public function getTestsForSequence() return [ // simple sequence ['{{ [1, 2] }}', new ArrayExpression([ - new ConstantExpression(0, 1), - new ConstantExpression(1, 1), + new ConstantExpression(0, 1), + new ConstantExpression(1, 1), - new ConstantExpression(1, 1), - new ConstantExpression(2, 1), - ], 1), + new ConstantExpression(1, 1), + new ConstantExpression(2, 1), + ], 1), ], // sequence with trailing , ['{{ [1, 2, ] }}', new ArrayExpression([ - new ConstantExpression(0, 1), - new ConstantExpression(1, 1), + new ConstantExpression(0, 1), + new ConstantExpression(1, 1), - new ConstantExpression(1, 1), - new ConstantExpression(2, 1), - ], 1), + new ConstantExpression(1, 1), + new ConstantExpression(2, 1), + ], 1), ], // simple mapping ['{{ {"a": "b", "b": "c"} }}', new ArrayExpression([ - new ConstantExpression('a', 1), - new ConstantExpression('b', 1), + new ConstantExpression('a', 1), + new ConstantExpression('b', 1), - new ConstantExpression('b', 1), - new ConstantExpression('c', 1), - ], 1), + new ConstantExpression('b', 1), + new ConstantExpression('c', 1), + ], 1), ], // mapping with trailing , ['{{ {"a": "b", "b": "c", } }}', new ArrayExpression([ - new ConstantExpression('a', 1), - new ConstantExpression('b', 1), + new ConstantExpression('a', 1), + new ConstantExpression('b', 1), - new ConstantExpression('b', 1), - new ConstantExpression('c', 1), - ], 1), + new ConstantExpression('b', 1), + new ConstantExpression('c', 1), + ], 1), ], // mapping in a sequence ['{{ [1, {"a": "b", "b": "c"}] }}', new ArrayExpression([ - new ConstantExpression(0, 1), - new ConstantExpression(1, 1), + new ConstantExpression(0, 1), + new ConstantExpression(1, 1), - new ConstantExpression(1, 1), - new ArrayExpression([ - new ConstantExpression('a', 1), - new ConstantExpression('b', 1), + new ConstantExpression(1, 1), + new ArrayExpression([ + new ConstantExpression('a', 1), + new ConstantExpression('b', 1), - new ConstantExpression('b', 1), - new ConstantExpression('c', 1), - ], 1), + new ConstantExpression('b', 1), + new ConstantExpression('c', 1), ], 1), + ], 1), ], // sequence in a mapping ['{{ {"a": [1, 2], "b": "c"} }}', new ArrayExpression([ - new ConstantExpression('a', 1), - new ArrayExpression([ - new ConstantExpression(0, 1), - new ConstantExpression(1, 1), + new ConstantExpression('a', 1), + new ArrayExpression([ + new ConstantExpression(0, 1), + new ConstantExpression(1, 1), - new ConstantExpression(1, 1), - new ConstantExpression(2, 1), - ], 1), - new ConstantExpression('b', 1), - new ConstantExpression('c', 1), + new ConstantExpression(1, 1), + new ConstantExpression(2, 1), ], 1), + new ConstantExpression('b', 1), + new ConstantExpression('c', 1), + ], 1), ], ['{{ {a, b} }}', new ArrayExpression([ new ConstantExpression('a', 1), @@ -170,29 +170,29 @@ public function getTestsForSequence() // sequence with spread operator ['{{ [1, 2, ...foo] }}', - new ArrayExpression([ - new ConstantExpression(0, 1), - new ConstantExpression(1, 1), + new ArrayExpression([ + new ConstantExpression(0, 1), + new ConstantExpression(1, 1), - new ConstantExpression(1, 1), - new ConstantExpression(2, 1), + new ConstantExpression(1, 1), + new ConstantExpression(2, 1), - new ConstantExpression(2, 1), - $this->createNameExpression('foo', ['spread' => true]), - ], 1)], + new ConstantExpression(2, 1), + $this->createNameExpression('foo', ['spread' => true]), + ], 1)], // mapping with spread operator ['{{ {"a": "b", "b": "c", ...otherLetters} }}', - new ArrayExpression([ - new ConstantExpression('a', 1), - new ConstantExpression('b', 1), + new ArrayExpression([ + new ConstantExpression('a', 1), + new ConstantExpression('b', 1), - new ConstantExpression('b', 1), - new ConstantExpression('c', 1), + new ConstantExpression('b', 1), + new ConstantExpression('c', 1), - new ConstantExpression(0, 1), - $this->createNameExpression('otherLetters', ['spread' => true]), - ], 1)], + new ConstantExpression(0, 1), + $this->createNameExpression('otherLetters', ['spread' => true]), + ], 1)], ]; } diff --git a/tests/Node/DeprecatedTest.php b/tests/Node/DeprecatedTest.php index 63259eda3d3..90e958cb4dc 100644 --- a/tests/Node/DeprecatedTest.php +++ b/tests/Node/DeprecatedTest.php @@ -77,7 +77,7 @@ public function getTests() \$$varName = foo(); @trigger_error(\$$varName." (\"foo.twig\" at line 1).", E_USER_DEPRECATED); EOF - , $environment]; + , $environment]; return $tests; } diff --git a/tests/Node/Expression/Filter/RawTest.php b/tests/Node/Expression/Filter/RawTest.php index 89e495cca54..72999fd4481 100644 --- a/tests/Node/Expression/Filter/RawTest.php +++ b/tests/Node/Expression/Filter/RawTest.php @@ -24,7 +24,7 @@ public function testConstructor() $this->assertSame(12, $filter->getTemplateLine()); $this->assertSame('raw', $filter->getNode('filter')->getAttribute('value')); $this->assertSame($node, $filter->getNode('node')); - $this->assertSame(0, count($filter->getNode('arguments'))); + $this->assertCount(0, $filter->getNode('arguments')); } public function getTests() diff --git a/tests/Node/Expression/FilterTest.php b/tests/Node/Expression/FilterTest.php index 9ce5e4e4c80..3e5feebe238 100644 --- a/tests/Node/Expression/FilterTest.php +++ b/tests/Node/Expression/FilterTest.php @@ -190,6 +190,7 @@ private function getExtension() if ($this->extension) { return $this->extension; } + return $this->extension = new class() extends AbstractExtension { public function getFilters(): array { diff --git a/tests/Node/IncludeTest.php b/tests/Node/IncludeTest.php index 55454f8d410..446fbd29395 100644 --- a/tests/Node/IncludeTest.php +++ b/tests/Node/IncludeTest.php @@ -88,7 +88,7 @@ public function getTests() yield from \$__internal_%s->unwrap()->yield(CoreExtension::toArray(["foo" => true])); } EOF - , null, true]; + , null, true]; return $tests; } diff --git a/tests/Node/ModuleTest.php b/tests/Node/ModuleTest.php index 974bc22ce67..1306401df6d 100644 --- a/tests/Node/ModuleTest.php +++ b/tests/Node/ModuleTest.php @@ -121,7 +121,7 @@ public function getSourceContext() } } EOF - , $twig, true]; + , $twig, true]; $import = new ImportNode(new ConstantExpression('foo.twig', 1), new AssignNameExpression('macro', 1), 2); @@ -207,7 +207,7 @@ public function getSourceContext() } } EOF - , $twig, true]; + , $twig, true]; $set = new SetNode(false, new Node([new AssignNameExpression('foo', 4)]), new Node([new ConstantExpression('foo', 4)]), 4); $body = new Node([$set]); @@ -297,7 +297,7 @@ public function getSourceContext() } } EOF - , $twig, true]; + , $twig, true]; return $tests; } diff --git a/tests/TemplateTest.php b/tests/TemplateTest.php index 8ee46c19bd5..37d6fe62a8c 100644 --- a/tests/TemplateTest.php +++ b/tests/TemplateTest.php @@ -17,7 +17,6 @@ use Twig\Extension\CoreExtension; use Twig\Extension\SandboxExtension; use Twig\Loader\ArrayLoader; -use Twig\Loader\LoaderInterface; use Twig\Sandbox\SecurityError; use Twig\Sandbox\SecurityPolicy; use Twig\Source; From 230e31d384b2661289ce1f3dcd7574c2fbea9274 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Wed, 7 Aug 2024 19:06:26 +0200 Subject: [PATCH 315/812] Add a recipe about how to mark a node as safe --- CHANGELOG | 1 + doc/recipes.rst | 11 +++++++++++ 2 files changed, 12 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index 40fc1b4a0ec..24fc6e7d364 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,6 @@ # 3.11.0 (2024-XX-XX) + * Mark `ConstantExpression` as being `@final` * Add the `find` filter * Fix optimizer mode validation in `OptimizerNodeVisitor` * Add the possibility to yield from a generator in `PrintNode` diff --git a/doc/recipes.rst b/doc/recipes.rst index b342457edf2..c091d34fc63 100644 --- a/doc/recipes.rst +++ b/doc/recipes.rst @@ -528,4 +528,15 @@ include in your templates: 'tag_variable' => ['{[', ']}'], ])); +Marking a Node as being safe +---------------------------- + +When using the escaper extension, you might want to mark some nodes as being +safe to avoid any escaping. You can do so by wrapping your expression with a +``RawFilter`` node:: + + use Twig\Node\Expression\Filter\RawFilter; + + $safeExpr = new RawFilter(new YourSafeNode()); + .. _callback: https://www.php.net/manual/en/function.is-callable.php From e8141bbd348582e44715a98201d7f6b86ed69aef Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Wed, 7 Aug 2024 21:09:18 +0200 Subject: [PATCH 316/812] Move static attributes for Function|Filter|TestExpression to the constructor --- src/Node/Expression/FilterExpression.php | 8 +++++--- src/Node/Expression/FunctionExpression.php | 4 +--- src/Node/Expression/TestExpression.php | 7 ++----- 3 files changed, 8 insertions(+), 11 deletions(-) diff --git a/src/Node/Expression/FilterExpression.php b/src/Node/Expression/FilterExpression.php index 2241adee4c7..251870ae552 100644 --- a/src/Node/Expression/FilterExpression.php +++ b/src/Node/Expression/FilterExpression.php @@ -19,12 +19,16 @@ class FilterExpression extends CallExpression { public function __construct(Node $node, ConstantExpression $filterName, Node $arguments, int $lineno, ?string $tag = null) { - parent::__construct(['node' => $node, 'filter' => $filterName, 'arguments' => $arguments], [], $lineno, $tag); + parent::__construct(['node' => $node, 'filter' => $filterName, 'arguments' => $arguments], ['name' => $filterName->getAttribute('value'), 'type' => 'filter'], $lineno, $tag); } public function compile(Compiler $compiler): void { $name = $this->getNode('filter')->getAttribute('value'); + if ($name !== $this->getAttribute('name')) { + trigger_deprecation('twig/twig', '3.11', 'Changing the value of a "filter" node in a NodeVisitor class is not supported anymore.'); + $this->setAttribute('name', $name); + } if ('raw' === $name) { trigger_deprecation('twig/twig', '3.11', 'Creating the "raw" filter via "FilterExpression" is deprecated; use "RawFilter" instead.'); @@ -34,8 +38,6 @@ public function compile(Compiler $compiler): void } $filter = $compiler->getEnvironment()->getFilter($name); - $this->setAttribute('name', $name); - $this->setAttribute('type', 'filter'); $this->setAttribute('needs_charset', $filter->needsCharset()); $this->setAttribute('needs_environment', $filter->needsEnvironment()); $this->setAttribute('needs_context', $filter->needsContext()); diff --git a/src/Node/Expression/FunctionExpression.php b/src/Node/Expression/FunctionExpression.php index d903a9e8f13..ef99c401a94 100644 --- a/src/Node/Expression/FunctionExpression.php +++ b/src/Node/Expression/FunctionExpression.php @@ -19,7 +19,7 @@ class FunctionExpression extends CallExpression { public function __construct(string $name, Node $arguments, int $lineno) { - parent::__construct(['arguments' => $arguments], ['name' => $name, 'is_defined_test' => false], $lineno); + parent::__construct(['arguments' => $arguments], ['name' => $name, 'type' => 'function', 'is_defined_test' => false], $lineno); } public function compile(Compiler $compiler) @@ -27,8 +27,6 @@ public function compile(Compiler $compiler) $name = $this->getAttribute('name'); $function = $compiler->getEnvironment()->getFunction($name); - $this->setAttribute('name', $name); - $this->setAttribute('type', 'function'); $this->setAttribute('needs_charset', $function->needsCharset()); $this->setAttribute('needs_environment', $function->needsEnvironment()); $this->setAttribute('needs_context', $function->needsContext()); diff --git a/src/Node/Expression/TestExpression.php b/src/Node/Expression/TestExpression.php index e518bd8f10b..29c5a522cd8 100644 --- a/src/Node/Expression/TestExpression.php +++ b/src/Node/Expression/TestExpression.php @@ -23,16 +23,13 @@ public function __construct(Node $node, string $name, ?Node $arguments, int $lin $nodes['arguments'] = $arguments; } - parent::__construct($nodes, ['name' => $name], $lineno); + parent::__construct($nodes, ['name' => $name, 'type' => 'test'], $lineno); } public function compile(Compiler $compiler): void { - $name = $this->getAttribute('name'); - $test = $compiler->getEnvironment()->getTest($name); + $test = $compiler->getEnvironment()->getTest($this->getAttribute('name')); - $this->setAttribute('name', $name); - $this->setAttribute('type', 'test'); $this->setAttribute('arguments', $test->getArguments()); $this->setAttribute('callable', $test->getCallable()); $this->setAttribute('is_variadic', $test->isVariadic()); From 390c5a0b05553bc3cc5ce4000ba7048856ced373 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Wed, 7 Aug 2024 22:59:17 +0200 Subject: [PATCH 317/812] Remove debug info --- tests/Cache/ChainTest.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/Cache/ChainTest.php b/tests/Cache/ChainTest.php index bb6ed7cf30d..338cc49de58 100644 --- a/tests/Cache/ChainTest.php +++ b/tests/Cache/ChainTest.php @@ -54,7 +54,6 @@ public function testLoadInA() $content = $this->generateSource(); file_put_contents($key, $content); - var_dump($key); $this->cache->load($this->key); @@ -73,7 +72,6 @@ public function testLoadInB() $content = $this->generateSource(); file_put_contents($key, $content); - var_dump($key); $this->cache->load($this->key); From e1a94481905cd85615631ed8e99bae01fcf4030b Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Thu, 8 Aug 2024 09:23:55 +0200 Subject: [PATCH 318/812] clarify that macros shadow other macros/functions --- doc/tags/macro.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/tags/macro.rst b/doc/tags/macro.rst index 5ad2cf020d8..874f35e5092 100644 --- a/doc/tags/macro.rst +++ b/doc/tags/macro.rst @@ -75,7 +75,7 @@ via the ``from`` tag: .. caution:: As macros imported via ``from`` are called like functions, be careful to - not override existing functions: + not shadow existing functions: .. code-block:: twig @@ -113,7 +113,7 @@ Imported macros are not available in the body of ``embed`` tags, you need to explicitly re-import macros inside the tag. When calling ``import`` or ``from`` from a ``block`` tag, the imported macros -are only defined in the current block and they override macros defined at the +are only defined in the current block and they shadow macros defined at the template level with the same names. Checking if a Macro is defined From 839da16d0420a142b83c8118e17996914d0a1fe3 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Thu, 8 Aug 2024 11:12:39 +0200 Subject: [PATCH 319/812] Tweak docs --- doc/tags/macro.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/tags/macro.rst b/doc/tags/macro.rst index 874f35e5092..d1d0641c090 100644 --- a/doc/tags/macro.rst +++ b/doc/tags/macro.rst @@ -74,8 +74,8 @@ via the ``from`` tag: .. caution:: - As macros imported via ``from`` are called like functions, be careful to - not shadow existing functions: + As macros imported via ``from`` are called like functions, be careful that + they shadow existing functions: .. code-block:: twig From 0ce79df5c4054d18e8e543a382bcfda35770dbd2 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Thu, 8 Aug 2024 11:56:21 +0200 Subject: [PATCH 320/812] Use trigger_deprecation() instead of trigger_error() for deprecations --- CHANGELOG | 2 ++ doc/advanced.rst | 12 +++++++ doc/tags/deprecated.rst | 11 ++++++ src/ExpressionParser.php | 15 ++------ src/Node/DeprecatedNode.php | 36 ++++++++++++++----- src/TokenParser/DeprecatedTokenParser.php | 29 +++++++++++++-- src/TwigFilter.php | 6 ++++ src/TwigFunction.php | 6 ++++ src/TwigTest.php | 6 ++++ .../tags/deprecated/with_package.legacy.test | 10 ++++++ .../with_package_version.legacy.test | 10 ++++++ tests/Node/DeprecatedTest.php | 14 +++++--- tests/Util/DeprecationCollectorTest.php | 4 +-- 13 files changed, 131 insertions(+), 30 deletions(-) create mode 100644 tests/Fixtures/tags/deprecated/with_package.legacy.test create mode 100644 tests/Fixtures/tags/deprecated/with_package_version.legacy.test diff --git a/CHANGELOG b/CHANGELOG index 24fc6e7d364..fd552fe3563 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,7 @@ # 3.11.0 (2024-XX-XX) + * Add the possibility to add a package and a version to the `deprecated` tag + * Add the possibility to add a package for filter/function/test deprecations * Mark `ConstantExpression` as being `@final` * Add the `find` filter * Fix optimizer mode validation in `OptimizerNodeVisitor` diff --git a/doc/advanced.rst b/doc/advanced.rst index c1073242fd5..1e89694e7e2 100644 --- a/doc/advanced.rst +++ b/doc/advanced.rst @@ -285,6 +285,18 @@ deprecated one when that makes sense:: // ... }, ['deprecated' => true, 'alternative' => 'new_one']); +.. versionadded:: 3.11 + + The ``deprecating_package`` option was added in Twig 3.11. + +You can also set the ``deprecating_package`` option to specify the package that +is deprecating the filter, and ``deprecated`` can be set to the package version +when the filter was deprecated:: + + $filter = new \Twig\TwigFilter('obsolete', function () { + // ... + }, ['deprecated' => '1.1', 'deprecating_package' => 'foo/bar']); + When a filter is deprecated, Twig emits a deprecation notice when compiling a template using it. See :ref:`deprecation-notices` for more information. diff --git a/doc/tags/deprecated.rst b/doc/tags/deprecated.rst index d2dbe7f2bf7..811e88e3ff1 100644 --- a/doc/tags/deprecated.rst +++ b/doc/tags/deprecated.rst @@ -23,6 +23,17 @@ You can also deprecate a macro in the following way: Note that by default, the deprecation notices are silenced and never displayed nor logged. See :ref:`deprecation-notices` to learn how to handle them. +.. versionadded:: 3.11 + + The ``package`` and ``version`` options were added in Twig 3.11. + +You can optionally add the package and the version that introduced the deprecation: + +.. code-block:: twig + + {% deprecated 'The "base.twig" template is deprecated, use "layout.twig" instead.' package='twig/twig' %} + {% deprecated 'The "base.twig" template is deprecated, use "layout.twig" instead.' package='twig/twig' version='3.11' %} + .. note:: Don't use the ``deprecated`` tag to deprecate a ``block`` as the diff --git a/src/ExpressionParser.php b/src/ExpressionParser.php index 006c1bdbba7..84484404864 100644 --- a/src/ExpressionParser.php +++ b/src/ExpressionParser.php @@ -774,16 +774,13 @@ private function getTestNodeClass(TwigTest $test): string $stream = $this->parser->getStream(); $message = \sprintf('Twig Test "%s" is deprecated', $test->getName()); - if ($test->getDeprecatedVersion()) { - $message .= \sprintf(' since version %s', $test->getDeprecatedVersion()); - } if ($test->getAlternative()) { $message .= \sprintf('. Use "%s" instead', $test->getAlternative()); } $src = $stream->getSourceContext(); $message .= \sprintf(' in %s at line %d.', $src->getPath() ?: $src->getName(), $stream->getCurrent()->getLine()); - @trigger_error($message, \E_USER_DEPRECATED); + trigger_deprecation($test->getDeprecatingPackage(), $test->getDeprecatedVersion(), $message); } return $test->getNodeClass(); @@ -800,16 +797,13 @@ private function getFunctionNodeClass(string $name, int $line): string if ($function->isDeprecated()) { $message = \sprintf('Twig Function "%s" is deprecated', $function->getName()); - if ($function->getDeprecatedVersion()) { - $message .= \sprintf(' since version %s', $function->getDeprecatedVersion()); - } if ($function->getAlternative()) { $message .= \sprintf('. Use "%s" instead', $function->getAlternative()); } $src = $this->parser->getStream()->getSourceContext(); $message .= \sprintf(' in %s at line %d.', $src->getPath() ?: $src->getName(), $line); - @trigger_error($message, \E_USER_DEPRECATED); + trigger_deprecation($function->getDeprecatingPackage(), $function->getDeprecatedVersion(), $message); } return $function->getNodeClass(); @@ -826,16 +820,13 @@ private function getFilterNodeClass(string $name, int $line): string if ($filter->isDeprecated()) { $message = \sprintf('Twig Filter "%s" is deprecated', $filter->getName()); - if ($filter->getDeprecatedVersion()) { - $message .= \sprintf(' since version %s', $filter->getDeprecatedVersion()); - } if ($filter->getAlternative()) { $message .= \sprintf('. Use "%s" instead', $filter->getAlternative()); } $src = $this->parser->getStream()->getSourceContext(); $message .= \sprintf(' in %s at line %d.', $src->getPath() ?: $src->getName(), $line); - @trigger_error($message, \E_USER_DEPRECATED); + trigger_deprecation($filter->getDeprecatingPackage(), $filter->getDeprecatedVersion(), $message); } return $filter->getNodeClass(); diff --git a/src/Node/DeprecatedNode.php b/src/Node/DeprecatedNode.php index 1a07ab81afb..afeb8332e29 100644 --- a/src/Node/DeprecatedNode.php +++ b/src/Node/DeprecatedNode.php @@ -35,21 +35,39 @@ public function compile(Compiler $compiler): void $expr = $this->getNode('expr'); - if ($expr instanceof ConstantExpression) { - $compiler->write('@trigger_error(') - ->subcompile($expr); - } else { + if (!$expr instanceof ConstantExpression) { $varName = $compiler->getVarName(); - $compiler->write(\sprintf('$%s = ', $varName)) + $compiler + ->write(\sprintf('$%s = ', $varName)) ->subcompile($expr) ->raw(";\n") - ->write(\sprintf('@trigger_error($%s', $varName)); + ; + } + + $compiler->write('trigger_deprecation('); + if ($this->hasNode('package')) { + $compiler->subcompile($this->getNode('package')); + } else { + $compiler->raw("''"); + } + $compiler->raw(', '); + if ($this->hasNode('version')) { + $compiler->subcompile($this->getNode('version')); + } else { + $compiler->raw("''"); + } + $compiler->raw(', '); + + if ($expr instanceof ConstantExpression) { + $compiler->subcompile($expr); + } else { + $compiler->write(\sprintf('$%s', $varName)); } $compiler - ->raw('.') - ->string(\sprintf(' ("%s" at line %d).', $this->getTemplateName(), $this->getTemplateLine())) - ->raw(", E_USER_DEPRECATED);\n") + ->raw(".") + ->string(\sprintf(' in "%s" at line %d.', $this->getTemplateName(), $this->getTemplateLine())) + ->raw(");\n") ; } } diff --git a/src/TokenParser/DeprecatedTokenParser.php b/src/TokenParser/DeprecatedTokenParser.php index 31416c79c15..c17c4aadc29 100644 --- a/src/TokenParser/DeprecatedTokenParser.php +++ b/src/TokenParser/DeprecatedTokenParser.php @@ -11,6 +11,7 @@ namespace Twig\TokenParser; +use Twig\Error\SyntaxError; use Twig\Node\DeprecatedNode; use Twig\Node\Node; use Twig\Token; @@ -21,6 +22,8 @@ * {% deprecated 'The "base.twig" template is deprecated, use "layout.twig" instead.' %} * {% extends 'layout.html.twig' %} * + * {% deprecated 'The "base.twig" template is deprecated, use "layout.twig" instead.' package="foo/bar" version="1.1" %} + * * @author Yonel Ceruto * * @internal @@ -29,11 +32,31 @@ final class DeprecatedTokenParser extends AbstractTokenParser { public function parse(Token $token): Node { - $expr = $this->parser->getExpressionParser()->parseExpression(); + $stream = $this->parser->getStream(); + $expressionParser = $this->parser->getExpressionParser(); + $expr = $expressionParser->parseExpression(); + $node = new DeprecatedNode($expr, $token->getLine(), $this->getTag()); + + while ($stream->test(Token::NAME_TYPE)) { + $k = $stream->getCurrent()->getValue(); + $stream->next(); + $stream->expect(Token::OPERATOR_TYPE, '='); + + switch ($k) { + case 'package': + $node->setNode('package', $expressionParser->parseExpression()); + break; + case 'version': + $node->setNode('version', $expressionParser->parseExpression()); + break; + default: + throw new SyntaxError(\sprintf('Unknown "%s" option.', $k), $stream->getCurrent()->getLine(), $stream->getSourceContext()); + } + } - $this->parser->getStream()->expect(Token::BLOCK_END_TYPE); + $stream->expect(Token::BLOCK_END_TYPE); - return new DeprecatedNode($expr, $token->getLine(), $this->getTag()); + return $node; } public function getTag(): string diff --git a/src/TwigFilter.php b/src/TwigFilter.php index c02469b916f..2b80df9399b 100644 --- a/src/TwigFilter.php +++ b/src/TwigFilter.php @@ -46,6 +46,7 @@ public function __construct(string $name, $callable = null, array $options = []) 'preserves_safety' => null, 'node_class' => FilterExpression::class, 'deprecated' => false, + 'deprecating_package' => '', 'alternative' => null, ], $options); } @@ -128,6 +129,11 @@ public function isDeprecated(): bool return (bool) $this->options['deprecated']; } + public function getDeprecatingPackage(): string + { + return $this->options['deprecating_package']; + } + public function getDeprecatedVersion(): string { return \is_bool($this->options['deprecated']) ? '' : $this->options['deprecated']; diff --git a/src/TwigFunction.php b/src/TwigFunction.php index c15def63303..bfee7eb87e0 100644 --- a/src/TwigFunction.php +++ b/src/TwigFunction.php @@ -44,6 +44,7 @@ public function __construct(string $name, $callable = null, array $options = []) 'is_safe_callback' => null, 'node_class' => FunctionExpression::class, 'deprecated' => false, + 'deprecating_package' => '', 'alternative' => null, ], $options); } @@ -116,6 +117,11 @@ public function isDeprecated(): bool return (bool) $this->options['deprecated']; } + public function getDeprecatingPackage(): string + { + return $this->options['deprecating_package']; + } + public function getDeprecatedVersion(): string { return \is_bool($this->options['deprecated']) ? '' : $this->options['deprecated']; diff --git a/src/TwigTest.php b/src/TwigTest.php index 3769ec162b6..0b43a284906 100644 --- a/src/TwigTest.php +++ b/src/TwigTest.php @@ -38,6 +38,7 @@ public function __construct(string $name, $callable = null, array $options = []) 'is_variadic' => false, 'node_class' => TestExpression::class, 'deprecated' => false, + 'deprecating_package' => '', 'alternative' => null, 'one_mandatory_argument' => false, ], $options); @@ -83,6 +84,11 @@ public function isDeprecated(): bool return (bool) $this->options['deprecated']; } + public function getDeprecatingPackage(): string + { + return $this->options['deprecating_package']; + } + public function getDeprecatedVersion(): string { return \is_bool($this->options['deprecated']) ? '' : $this->options['deprecated']; diff --git a/tests/Fixtures/tags/deprecated/with_package.legacy.test b/tests/Fixtures/tags/deprecated/with_package.legacy.test new file mode 100644 index 00000000000..877643f01f4 --- /dev/null +++ b/tests/Fixtures/tags/deprecated/with_package.legacy.test @@ -0,0 +1,10 @@ +--TEST-- +Deprecating a template with "deprecated" tag +--TEMPLATE-- +{% deprecated 'The "index.twig" template is deprecated, use "greeting.twig" instead.' package="foo/bar" %} + +Hello Fabien +--DATA-- +return [] +--EXPECT-- +Hello Fabien diff --git a/tests/Fixtures/tags/deprecated/with_package_version.legacy.test b/tests/Fixtures/tags/deprecated/with_package_version.legacy.test new file mode 100644 index 00000000000..68722994e04 --- /dev/null +++ b/tests/Fixtures/tags/deprecated/with_package_version.legacy.test @@ -0,0 +1,10 @@ +--TEST-- +Deprecating a template with "deprecated" tag +--TEMPLATE-- +{% deprecated 'The "index.twig" template is deprecated, use "greeting.twig" instead.' package="foo/bar" version=1.1 %} + +Hello Fabien +--DATA-- +return [] +--EXPECT-- +Hello Fabien diff --git a/tests/Node/DeprecatedTest.php b/tests/Node/DeprecatedTest.php index 90e958cb4dc..440bd520e52 100644 --- a/tests/Node/DeprecatedTest.php +++ b/tests/Node/DeprecatedTest.php @@ -39,25 +39,29 @@ public function getTests() $expr = new ConstantExpression('This section is deprecated', 1); $node = new DeprecatedNode($expr, 1, 'deprecated'); $node->setSourceContext(new Source('', 'foo.twig')); + $node->setNode('package', new ConstantExpression('twig/twig', 1)); + $node->setNode('version', new ConstantExpression('1.1', 1)); $tests[] = [$node, <<setSourceContext(new Source('', 'foo.twig')); + $dep->setNode('package', new ConstantExpression('twig/twig', 1)); + $dep->setNode('version', new ConstantExpression('1.1', 1)); $tests[] = [$node, <<setSourceContext(new Source('', 'foo.twig')); + $node->setNode('package', new ConstantExpression('twig/twig', 1)); + $node->setNode('version', new ConstantExpression('1.1', 1)); $compiler = $this->getCompiler($environment); $varName = $compiler->getVarName(); @@ -75,7 +81,7 @@ public function getTests() $tests[] = [$node, <<addFunction(new TwigFunction('deprec', [$this, 'deprec'], ['deprecated' => '1.1'])); + $twig->addFunction(new TwigFunction('deprec', [$this, 'deprec'], ['deprecated' => '1.1', 'deprecating_package' => 'foo/bar'])); $collector = new DeprecationCollector($twig); $deprecations = $collector->collect(new Iterator()); - $this->assertEquals(['Twig Function "deprec" is deprecated since version 1.1 in deprec.twig at line 1.'], $deprecations); + $this->assertEquals(['Since foo/bar 1.1: Twig Function "deprec" is deprecated in deprec.twig at line 1.'], $deprecations); } public function deprec() From 18894cf91ed3cfa3ecd4344112c4eb6a32f279ab Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Thu, 8 Aug 2024 11:07:26 +0200 Subject: [PATCH 321/812] Add the possibility to deprecate attributes and nodes on Node --- CHANGELOG | 1 + src/Node/NameDeprecation.php | 46 ++++++++++++++++++++++++ src/Node/Node.php | 54 ++++++++++++++++++++++++++++ tests/Node/NodeTest.php | 68 ++++++++++++++++++++++++++++++++++++ 4 files changed, 169 insertions(+) create mode 100644 src/Node/NameDeprecation.php diff --git a/CHANGELOG b/CHANGELOG index fd552fe3563..3922d3f140d 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,6 @@ # 3.11.0 (2024-XX-XX) + * Add the possibility to deprecate attributes and nodes on `Node` * Add the possibility to add a package and a version to the `deprecated` tag * Add the possibility to add a package for filter/function/test deprecations * Mark `ConstantExpression` as being `@final` diff --git a/src/Node/NameDeprecation.php b/src/Node/NameDeprecation.php new file mode 100644 index 00000000000..63ab285761a --- /dev/null +++ b/src/Node/NameDeprecation.php @@ -0,0 +1,46 @@ + + */ +class NameDeprecation +{ + private $package; + private $version; + private $newName; + + public function __construct(string $package = '', string $version = '', string $newName = '') + { + $this->package = $package; + $this->version = $version; + $this->newName = $newName; + } + + public function getPackage(): string + { + return $this->package; + } + + public function getVersion(): string + { + return $this->version; + } + + public function getNewName(): string + { + return $this->newName; + } +} diff --git a/src/Node/Node.php b/src/Node/Node.php index e0e473e8cc4..5ef661f5e37 100644 --- a/src/Node/Node.php +++ b/src/Node/Node.php @@ -30,6 +30,10 @@ class Node implements \Countable, \IteratorAggregate protected $tag; private $sourceContext; + /** @var array */ + private $nodeNameDeprecations = []; + /** @var array */ + private $attributeNameDeprecations = []; /** * @param array $nodes An array of named nodes @@ -109,14 +113,39 @@ public function getAttribute(string $name) throw new \LogicException(\sprintf('Attribute "%s" does not exist for Node "%s".', $name, static::class)); } + $triggerDeprecation = \func_num_args() > 1 ? func_get_arg(1) : true; + if ($triggerDeprecation && isset($this->attributeNameDeprecations[$name])) { + $dep = $this->attributeNameDeprecations[$name]; + if ($dep->getNewName()) { + trigger_deprecation($dep->getPackage(), $dep->getVersion(), 'Getting attribute "%s" on a "%s" class is deprecated, get the "%s" attribute instead.', $name, static::class, $dep->getNewName()); + } else { + trigger_deprecation($dep->getPackage(), $dep->getVersion(), 'Getting attribute "%s" on a "%s" class is deprecated.', $name, static::class); + } + } + return $this->attributes[$name]; } public function setAttribute(string $name, $value): void { + $triggerDeprecation = \func_num_args() > 2 ? func_get_arg(2) : true; + if ($triggerDeprecation && isset($this->attributeNameDeprecations[$name])) { + $dep = $this->attributeNameDeprecations[$name]; + if ($dep->getNewName()) { + trigger_deprecation($dep->getPackage(), $dep->getVersion(), 'Setting attribute "%s" on a "%s" class is deprecated, set the "%s" attribute instead.', $name, static::class, $dep->getNewName()); + } else { + trigger_deprecation($dep->getPackage(), $dep->getVersion(), 'Setting attribute "%s" on a "%s" class is deprecated.', $name, static::class); + } + } + $this->attributes[$name] = $value; } + public function deprecateAttribute(string $name, NameDeprecation $dep): void + { + $this->attributeNameDeprecations[$name] = $dep; + } + public function removeAttribute(string $name): void { unset($this->attributes[$name]); @@ -133,11 +162,31 @@ public function getNode(string $name): self throw new \LogicException(\sprintf('Node "%s" does not exist for Node "%s".', $name, static::class)); } + $triggerDeprecation = \func_num_args() > 1 ? func_get_arg(1) : true; + if ($triggerDeprecation && isset($this->nodeNameDeprecations[$name])) { + $dep = $this->nodeNameDeprecations[$name]; + if ($dep->getNewName()) { + trigger_deprecation($dep->getPackage(), $dep->getVersion(), 'Getting node "%s" on a "%s" class is deprecated, get the "%s" node instead.', $name, static::class, $dep->getNewName()); + } else { + trigger_deprecation($dep->getPackage(), $dep->getVersion(), 'Getting node "%s" on a "%s" class is deprecated.', $name, static::class); + } + } + return $this->nodes[$name]; } public function setNode(string $name, self $node): void { + $triggerDeprecation = \func_num_args() > 2 ? func_get_arg(2) : true; + if ($triggerDeprecation && isset($this->nodeNameDeprecations[$name])) { + $dep = $this->nodeNameDeprecations[$name]; + if ($dep->getNewName()) { + trigger_deprecation($dep->getPackage(), $dep->getVersion(), 'Setting node "%s" on a "%s" class is deprecated, set the "%s" node instead.', $name, static::class, $dep->getNewName()); + } else { + trigger_deprecation($dep->getPackage(), $dep->getVersion(), 'Setting node "%s" on a "%s" class is deprecated.', $name, static::class); + } + } + if (null !== $this->sourceContext) { $node->setSourceContext($this->sourceContext); } @@ -149,6 +198,11 @@ public function removeNode(string $name): void unset($this->nodes[$name]); } + public function deprecateNode(string $name, NameDeprecation $dep): void + { + $this->nodeNameDeprecations[$name] = $dep; + } + /** * @return int */ diff --git a/tests/Node/NodeTest.php b/tests/Node/NodeTest.php index 8e82af5b9e9..b4f8eee02aa 100644 --- a/tests/Node/NodeTest.php +++ b/tests/Node/NodeTest.php @@ -12,10 +12,14 @@ */ use PHPUnit\Framework\TestCase; +use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; +use Twig\Node\NameDeprecation; use Twig\Node\Node; class NodeTest extends TestCase { + use ExpectDeprecationTrait; + public function testToString() { // callable is not a supported type for a Node attribute, but Drupal uses some apparently @@ -23,4 +27,68 @@ public function testToString() $this->assertEquals('Twig\Node\Node(value: \Closure)', (string) $node); } + + public function testAttributeDeprecationIgnore() + { + $node = new Node([], ['foo' => false]); + $node->deprecateAttribute('foo', new NameDeprecation('foo/bar', '2.0', 'bar')); + + $this->assertSame(false, $node->getAttribute('foo', false)); + } + + /** + * @group legacy + */ + public function testAttributeDeprecationWithoutAlternative() + { + $node = new Node([], ['foo' => false]); + $node->deprecateAttribute('foo', new NameDeprecation('foo/bar', '2.0')); + + $this->expectDeprecation('Since foo/bar 2.0: Getting attribute "foo" on a "Twig\Node\Node" class is deprecated.'); + $this->assertSame(false, $node->getAttribute('foo')); + } + + /** + * @group legacy + */ + public function testAttributeDeprecationWithAlternative() + { + $node = new Node([], ['foo' => false]); + $node->deprecateAttribute('foo', new NameDeprecation('foo/bar', '2.0', 'bar')); + + $this->expectDeprecation('Since foo/bar 2.0: Getting attribute "foo" on a "Twig\Node\Node" class is deprecated, get the "bar" attribute instead.'); + $this->assertSame(false, $node->getAttribute('foo')); + } + + public function testNodeDeprecationIgnore() + { + $node = new Node(['foo' => $foo = new Node()], []); + $node->deprecateNode('foo', new NameDeprecation('foo/bar', '2.0')); + + $this->assertSame($foo, $node->getNode('foo', false)); + } + + /** + * @group legacy + */ + public function testNodeDeprecationWithoutAlternative() + { + $node = new Node(['foo' => $foo = new Node()], []); + $node->deprecateNode('foo', new NameDeprecation('foo/bar', '2.0')); + + $this->expectDeprecation('Since foo/bar 2.0: Getting node "foo" on a "Twig\Node\Node" class is deprecated.'); + $this->assertSame($foo, $node->getNode('foo')); + } + + /** + * @group legacy + */ + public function testNodeAttributeDeprecationWithAlternative() + { + $node = new Node(['foo' => $foo = new Node()], []); + $node->deprecateNode('foo', new NameDeprecation('foo/bar', '2.0', 'bar')); + + $this->expectDeprecation('Since foo/bar 2.0: Getting node "foo" on a "Twig\Node\Node" class is deprecated, get the "bar" node instead.'); + $this->assertSame($foo, $node->getNode('foo')); + } } From fe32121084d944715241e2cf401f53a70e341565 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Thu, 8 Aug 2024 18:03:04 +0200 Subject: [PATCH 322/812] Update CHANGELOG --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index 3922d3f140d..07382aecb5a 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,6 @@ # 3.11.0 (2024-XX-XX) + * Add `Twig\Cache\ChainCache` and `Twig\Cache\ReadOnlyFilesystemCache` * Add the possibility to deprecate attributes and nodes on `Node` * Add the possibility to add a package and a version to the `deprecated` tag * Add the possibility to add a package for filter/function/test deprecations From e80fb8ebba85c7341a97a9ebf825d7fd4b77708d Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Thu, 8 Aug 2024 18:15:16 +0200 Subject: [PATCH 323/812] Prepare the 3.11.0 release --- CHANGELOG | 2 +- src/Environment.php | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 07382aecb5a..dd776e083b8 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,4 +1,4 @@ -# 3.11.0 (2024-XX-XX) +# 3.11.0 (2024-08-08) * Add `Twig\Cache\ChainCache` and `Twig\Cache\ReadOnlyFilesystemCache` * Add the possibility to deprecate attributes and nodes on `Node` diff --git a/src/Environment.php b/src/Environment.php index 9efe23d21ba..32b13135c69 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -43,12 +43,12 @@ */ class Environment { - public const VERSION = '3.10.4-DEV'; - public const VERSION_ID = 301004; + public const VERSION = '3.11.0'; + public const VERSION_ID = 301100; public const MAJOR_VERSION = 4; - public const MINOR_VERSION = 10; - public const RELEASE_VERSION = 4; - public const EXTRA_VERSION = 'DEV'; + public const MINOR_VERSION = 11; + public const RELEASE_VERSION = 0; + public const EXTRA_VERSION = ''; private $charset; private $loader; From 3301b99276043b32bdcad285e2b553a94ce8fcdf Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Thu, 8 Aug 2024 18:16:11 +0200 Subject: [PATCH 324/812] Bump version --- CHANGELOG | 4 ++++ src/Environment.php | 8 ++++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index dd776e083b8..1eaccd6cafe 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +# 3.11.1 (2024-XX-XX) + + * n/a + # 3.11.0 (2024-08-08) * Add `Twig\Cache\ChainCache` and `Twig\Cache\ReadOnlyFilesystemCache` diff --git a/src/Environment.php b/src/Environment.php index 32b13135c69..95e90cb248c 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -43,12 +43,12 @@ */ class Environment { - public const VERSION = '3.11.0'; - public const VERSION_ID = 301100; + public const VERSION = '3.11.1-DEV'; + public const VERSION_ID = 301101; public const MAJOR_VERSION = 4; public const MINOR_VERSION = 11; - public const RELEASE_VERSION = 0; - public const EXTRA_VERSION = ''; + public const RELEASE_VERSION = 1; + public const EXTRA_VERSION = 'DEV'; private $charset; private $loader; From a5dc02bbc5b025c41296fdbcc8ed0525ba578676 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Fri, 9 Aug 2024 17:12:19 +0200 Subject: [PATCH 325/812] Fix integration tests when a test has more than on data/expect section and deprecations --- CHANGELOG | 2 +- src/Test/IntegrationTestCase.php | 7 +++++-- tests/Fixtures/functions/deprecated.test | 21 +++++++++++++++++++++ tests/IntegrationTest.php | 1 + 4 files changed, 28 insertions(+), 3 deletions(-) create mode 100644 tests/Fixtures/functions/deprecated.test diff --git a/CHANGELOG b/CHANGELOG index 1eaccd6cafe..ff4cb5bc52c 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,6 @@ # 3.11.1 (2024-XX-XX) - * n/a + * Fix integration tests when a test has more than on data/expect section and deprecations # 3.11.0 (2024-08-08) diff --git a/src/Test/IntegrationTestCase.php b/src/Test/IntegrationTestCase.php index 5519dd0a99f..88b3349c4ed 100644 --- a/src/Test/IntegrationTestCase.php +++ b/src/Test/IntegrationTestCase.php @@ -156,13 +156,16 @@ protected function doIntegrationTest($file, $message, $condition, $templates, $e } } - $loader = new ArrayLoader($templates); - foreach ($outputs as $i => $match) { $config = array_merge([ 'cache' => false, 'strict_variables' => true, ], $match[2] ? eval($match[2].';') : []); + // make sure that template are always compiled even if they are the same (useful when testing with more than one data/expect sections) + foreach ($templates as $j => $template) { + $templates[$j] = $template.str_repeat(' ', $i); + } + $loader = new ArrayLoader($templates); $twig = new Environment($loader, $config); $twig->addGlobal('global', 'global'); foreach ($this->getRuntimeLoaders() as $runtimeLoader) { diff --git a/tests/Fixtures/functions/deprecated.test b/tests/Fixtures/functions/deprecated.test new file mode 100644 index 00000000000..355e43303dd --- /dev/null +++ b/tests/Fixtures/functions/deprecated.test @@ -0,0 +1,21 @@ +--TEST-- +Functions can be deprecated_function +--DEPRECATION-- +Since foo/bar 1.1: Twig Function "deprecated_function" is deprecated. Use "not_deprecated_function" instead in index.twig at line 2. +Since foo/bar 1.1: Twig Function "deprecated_function" is deprecated. Use "not_deprecated_function" instead in index.twig at line 4. +--TEMPLATE-- +{{ deprecated_function() }} + +{{ deprecated_function() }} +--DATA-- +return [] +--EXPECT-- +foo + +foo +--DATA-- +return [] +--EXPECT-- +foo + +foo diff --git a/tests/IntegrationTest.php b/tests/IntegrationTest.php index 76dc98de6e0..f68ac15cf8c 100644 --- a/tests/IntegrationTest.php +++ b/tests/IntegrationTest.php @@ -185,6 +185,7 @@ public function getFunctions(): array new TwigFunction('*_path', [$this, 'dynamic_path']), new TwigFunction('*_foo_*_bar', [$this, 'dynamic_foo']), new TwigFunction('anon_foo', function ($name) { return '*'.$name.'*'; }), + new TwigFunction('deprecated_function', function () { return 'foo'; }, ['deprecated' => '1.1', 'deprecating_package' => 'foo/bar', 'alternative' => 'not_deprecated_function']), ]; } From 555014093102e8a2a6ec404241b9480dce991404 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Fri, 9 Aug 2024 18:02:41 +0200 Subject: [PATCH 326/812] Remove deprecation arg --- src/Extension/EscaperExtension.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Extension/EscaperExtension.php b/src/Extension/EscaperExtension.php index d8e9b6e4814..52531c436b5 100644 --- a/src/Extension/EscaperExtension.php +++ b/src/Extension/EscaperExtension.php @@ -60,8 +60,9 @@ public function getFilters(): array /** * @deprecated since Twig 3.10 */ - public function setEnvironment(Environment $environment, bool $triggerDeprecation = true): void + public function setEnvironment(Environment $environment): void { + $triggerDeprecation = \func_num_args() > 1 ? func_get_arg(1) : true; if ($triggerDeprecation) { trigger_deprecation('twig/twig', '3.10', 'The "%s()" method is deprecated and not needed if you are using methods from "Twig\Runtime\EscaperRuntime".', __METHOD__); } From ff4d01f940fb3fea0d3b695ae594306c1e2d2378 Mon Sep 17 00:00:00 2001 From: Christophe Coevoet Date: Wed, 7 Aug 2024 16:18:40 +0200 Subject: [PATCH 327/812] Implement the enum_cases function The implementation contains an optimized implementation of the function for the common case of using a string literal as the argument. It will validate the enum existence during compilation and compile the code to use `MyEnum::cases()` directly. --- CHANGELOG | 3 +- doc/functions/enum_cases.rst | 21 ++++++++++ doc/functions/index.rst | 1 + src/Extension/CoreExtension.php | 22 ++++++++++ .../FunctionNode/EnumCasesFunction.php | 41 +++++++++++++++++++ tests/DummyBackedEnum.php | 9 ++++ tests/DummyUnitEnum.php | 9 ++++ .../enum_cases/invalid_dynamic_enum.test | 13 ++++++ .../functions/enum_cases/invalid_enum.test | 10 +++++ .../enum_cases/invalid_enum_escaping.test | 10 +++++ .../enum_cases/invalid_literal_type.test | 10 +++++ .../Fixtures/functions/enum_cases/valid.test | 24 +++++++++++ 12 files changed, 172 insertions(+), 1 deletion(-) create mode 100644 doc/functions/enum_cases.rst create mode 100644 src/Node/Expression/FunctionNode/EnumCasesFunction.php create mode 100644 tests/DummyBackedEnum.php create mode 100644 tests/DummyUnitEnum.php create mode 100644 tests/Fixtures/functions/enum_cases/invalid_dynamic_enum.test create mode 100644 tests/Fixtures/functions/enum_cases/invalid_enum.test create mode 100644 tests/Fixtures/functions/enum_cases/invalid_enum_escaping.test create mode 100644 tests/Fixtures/functions/enum_cases/invalid_literal_type.test create mode 100644 tests/Fixtures/functions/enum_cases/valid.test diff --git a/CHANGELOG b/CHANGELOG index ff4cb5bc52c..31bbadcd907 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,7 @@ -# 3.11.1 (2024-XX-XX) +# 3.12.0 (2024-XX-XX) * Fix integration tests when a test has more than on data/expect section and deprecations + * Add the `enum_cases` function # 3.11.0 (2024-08-08) diff --git a/doc/functions/enum_cases.rst b/doc/functions/enum_cases.rst new file mode 100644 index 00000000000..2e6f883486e --- /dev/null +++ b/doc/functions/enum_cases.rst @@ -0,0 +1,21 @@ +``enum_cases`` +============== + +.. versionadded:: 3.12 + + The ``enum_cases`` function was added in Twig 3.12. + +``enum_cases`` returns the list of cases for a given enum: + +.. code-block:: twig + + {% for case in enum_cases('App\\MyEnum') %} + {{ case.value }} + {% endfor %} + +When using a string literal for the ``enum`` argument, it will be validated during compile time to be a valid enum name. + +Arguments +--------- + +* ``enum``: The FQCN of the enum diff --git a/doc/functions/index.rst b/doc/functions/index.rst index eebcd61c7ab..0809fd0be5f 100644 --- a/doc/functions/index.rst +++ b/doc/functions/index.rst @@ -10,6 +10,7 @@ Functions cycle date dump + enum_cases html_classes include max diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index 5ac80884a58..260735abbd6 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -45,6 +45,7 @@ use Twig\Node\Expression\Binary\StartsWithBinary; use Twig\Node\Expression\Binary\SubBinary; use Twig\Node\Expression\Filter\DefaultFilter; +use Twig\Node\Expression\FunctionNode\EnumCasesFunction; use Twig\Node\Expression\NullCoalesceExpression; use Twig\Node\Expression\Test\ConstantTest; use Twig\Node\Expression\Test\DefinedTest; @@ -246,6 +247,7 @@ public function getFunctions(): array new TwigFunction('date', [$this, 'convertDate']), new TwigFunction('include', [self::class, 'include'], ['needs_environment' => true, 'needs_context' => true, 'is_safe' => ['all']]), new TwigFunction('source', [self::class, 'source'], ['needs_environment' => true, 'is_safe' => ['all']]), + new TwigFunction('enum_cases', [self::class, 'enumCases'], ['node_class' => EnumCasesFunction::class]), ]; } @@ -1449,6 +1451,26 @@ public static function source(Environment $env, $name, $ignoreMissing = false): } } + /** + * Returns the list of cases of the enum. + * + * @template T of \UnitEnum + * + * @param class-string $enum + * + * @return list + * + * @internal + */ + public static function enumCases(string $enum): array + { + if (!enum_exists($enum)) { + throw new RuntimeError(\sprintf('Enum "%s" does not exist.', $enum)); + } + + return $enum::cases(); + } + /** * Provides the ability to get constants from instances as well as class/global constants. * diff --git a/src/Node/Expression/FunctionNode/EnumCasesFunction.php b/src/Node/Expression/FunctionNode/EnumCasesFunction.php new file mode 100644 index 00000000000..171f611de23 --- /dev/null +++ b/src/Node/Expression/FunctionNode/EnumCasesFunction.php @@ -0,0 +1,41 @@ +getNode('arguments'); + if ($arguments->hasNode('enum')) { + $firstArgument = $arguments->getNode('enum'); + } elseif ($arguments->hasNode('0')) { + $firstArgument = $arguments->getNode('0'); + } else { + $firstArgument = null; + } + + if (!$firstArgument instanceof ConstantExpression || \count($arguments) !== 1) { + parent::compile($compiler); + + return; + } + + $value = $firstArgument->getAttribute('value'); + + if (!\is_string($value)) { + throw new SyntaxError('The first argument of the "enum_cases" function must be a string.', $this->getTemplateLine(), $this->getSourceContext()); + } + + if (!enum_exists($value)) { + throw new SyntaxError(\sprintf('The first argument of the "enum_cases" function must be the name of an enum, "%s" given.', $value), $this->getTemplateLine(), $this->getSourceContext()); + } + + $compiler->raw(\sprintf('%s::cases()', $value)); + } +} diff --git a/tests/DummyBackedEnum.php b/tests/DummyBackedEnum.php new file mode 100644 index 00000000000..c9b07d2085d --- /dev/null +++ b/tests/DummyBackedEnum.php @@ -0,0 +1,9 @@ += 80100 +--TEMPLATE-- +{% set from_variable = 'Twig\\Tests\\NonExistentEnum' %} +{% for c in enum_cases(from_variable) %} + {{~ c.name }} +{% endfor %} +--DATA-- +return [] +--EXCEPTION-- +Twig\Error\RuntimeError: Enum "Twig\Tests\NonExistentEnum" does not exist in "index.twig" at line 3. diff --git a/tests/Fixtures/functions/enum_cases/invalid_enum.test b/tests/Fixtures/functions/enum_cases/invalid_enum.test new file mode 100644 index 00000000000..3084c37e34c --- /dev/null +++ b/tests/Fixtures/functions/enum_cases/invalid_enum.test @@ -0,0 +1,10 @@ +--TEST-- +"enum_cases" function with invalid enum class +--CONDITION-- +\PHP_VERSION_ID >= 80100 +--TEMPLATE-- +{% for c in enum_cases('Twig\\Tests\\NonExistentEnum') %} + {{~ c.name }} +{% endfor %} +--EXCEPTION-- +Twig\Error\SyntaxError: The first argument of the "enum_cases" function must be the name of an enum, "Twig\Tests\NonExistentEnum" given in "index.twig" at line 2. diff --git a/tests/Fixtures/functions/enum_cases/invalid_enum_escaping.test b/tests/Fixtures/functions/enum_cases/invalid_enum_escaping.test new file mode 100644 index 00000000000..1d5828fbca5 --- /dev/null +++ b/tests/Fixtures/functions/enum_cases/invalid_enum_escaping.test @@ -0,0 +1,10 @@ +--TEST-- +"enum_cases" function with missing \ escaping +--CONDITION-- +\PHP_VERSION_ID >= 80100 +--TEMPLATE-- +{% for c in enum_cases('Twig\Tests\DummyBackedEnum') %} + {{~ c.name }} +{% endfor %} +--EXCEPTION-- +Twig\Error\SyntaxError: The first argument of the "enum_cases" function must be the name of an enum, "TwigTestsDummyBackedEnum" given in "index.twig" at line 2. diff --git a/tests/Fixtures/functions/enum_cases/invalid_literal_type.test b/tests/Fixtures/functions/enum_cases/invalid_literal_type.test new file mode 100644 index 00000000000..6e7945d0003 --- /dev/null +++ b/tests/Fixtures/functions/enum_cases/invalid_literal_type.test @@ -0,0 +1,10 @@ +--TEST-- +"enum_cases" function with invalid literal type +--CONDITION-- +\PHP_VERSION_ID >= 80100 +--TEMPLATE-- +{% for c in enum_cases(13) %} + {{~ c.name }} +{% endfor %} +--EXCEPTION-- +Twig\Error\SyntaxError: The first argument of the "enum_cases" function must be a string in "index.twig" at line 2. diff --git a/tests/Fixtures/functions/enum_cases/valid.test b/tests/Fixtures/functions/enum_cases/valid.test new file mode 100644 index 00000000000..011d6478651 --- /dev/null +++ b/tests/Fixtures/functions/enum_cases/valid.test @@ -0,0 +1,24 @@ +--TEST-- +"enum_cases" function +--CONDITION-- +\PHP_VERSION_ID >= 80100 +--TEMPLATE-- +{% for c in enum_cases('Twig\\Tests\\DummyBackedEnum') %} + {{~ c.name }}: {{ c.value }} +{% endfor %} +{% for c in enum_cases('Twig\\Tests\\DummyUnitEnum') %} + {{~ c.name }} +{% endfor %} +{% set from_variable='Twig\\Tests\\DummyUnitEnum' %} +{% for c in enum_cases(from_variable) %} + {{~ c.name }} +{% endfor %} +--DATA-- +return [] +--EXPECT-- +FOO: foo +BAR: bar +BAR +BAZ +BAR +BAZ From 221e62cf634acc1ce4688847741148e197936743 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sat, 10 Aug 2024 11:01:45 +0200 Subject: [PATCH 328/812] Bump version to 3.12 --- src/Environment.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Environment.php b/src/Environment.php index 95e90cb248c..96bf8afbdb1 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -43,11 +43,11 @@ */ class Environment { - public const VERSION = '3.11.1-DEV'; - public const VERSION_ID = 301101; - public const MAJOR_VERSION = 4; - public const MINOR_VERSION = 11; - public const RELEASE_VERSION = 1; + public const VERSION = '3.12.0-DEV'; + public const VERSION_ID = 301200; + public const MAJOR_VERSION = 3; + public const MINOR_VERSION = 12; + public const RELEASE_VERSION = 0; public const EXTRA_VERSION = 'DEV'; private $charset; From 38e47a3e94bf28d3d939bd67e0597c84e8467595 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sat, 10 Aug 2024 11:32:28 +0200 Subject: [PATCH 329/812] Add a note about how to escape a string interpolation in a string --- doc/templates.rst | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/doc/templates.rst b/doc/templates.rst index e09a6a0185c..b0f5b9a1c29 100644 --- a/doc/templates.rst +++ b/doc/templates.rst @@ -589,6 +589,16 @@ inserted into the string: {{ "foo #{bar} baz" }} {{ "foo #{1 + 2} baz" }} +.. tip:: + + String interpolations can be ignored by escaping them with a backslash + (``\``): + + .. code-block:: twig + + {# outputs foo #{1 + 2} baz #} + {{ "foo \#{1 + 2} baz" }} + Math ~~~~ From 38250b91caf7c6882b709822ecae242e19c706b6 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sat, 10 Aug 2024 12:12:20 +0200 Subject: [PATCH 330/812] Add more tests to the Lexer --- tests/LexerTest.php | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/tests/LexerTest.php b/tests/LexerTest.php index 7926034ffa1..38ee2361569 100644 --- a/tests/LexerTest.php +++ b/tests/LexerTest.php @@ -401,4 +401,32 @@ public function getTemplateForErrorsAtTheEndOfTheStream() yield ['{{ =']; yield ['{{ ..']; } + + /** + * @dataProvider getTemplateForStrings + */ + public function testStrings(string $expected) + { + $template = '{{ "'.$expected.'" }}'; + $lexer = new Lexer(new Environment(new ArrayLoader())); + $stream = $lexer->tokenize(new Source($template, 'index')); + $stream->expect(Token::VAR_START_TYPE); + $stream->expect(Token::STRING_TYPE, $expected); + + $template = "{{ '".$expected."' }}"; + $lexer = new Lexer(new Environment(new ArrayLoader())); + $stream = $lexer->tokenize(new Source($template, 'index')); + $stream->expect(Token::VAR_START_TYPE); + $stream->expect(Token::STRING_TYPE, $expected); + + // add a dummy assertion here to satisfy PHPUnit, the only thing we want to test is that the code above + // can be executed without throwing any exceptions + $this->addToAssertionCount(1); + } + + public function getTemplateForStrings() + { + yield ["日本では、春になると桜の花が咲きます。多くの人々は、公園や川の近くに集まり、お花見を楽しみます。桜の花びらが風に舞い、まるで雪のように見える瞬間は、とても美しいです。"]; + yield ["في العالم العربي، يُعتبر الخط العربي أحد أجمل أشكال الفن. يُستخدم الخط في تزيين المساجد والكتب والمخطوطات القديمة. يتميز الخط العربي بجماله وتناسقه، ويُعتبر رمزًا للثقافة الإسلامية."]; + } } From c070cd719c6d8675cc6699b1df10a851f8815c35 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sat, 10 Aug 2024 12:30:45 +0200 Subject: [PATCH 331/812] Drop support for 8.0 --- .github/workflows/ci.yml | 6 ------ CHANGELOG | 1 + composer.json | 3 +-- doc/intro.rst | 2 +- extra/cache-extra/composer.json | 2 +- extra/cssinliner-extra/composer.json | 2 +- extra/html-extra/composer.json | 2 +- extra/inky-extra/composer.json | 2 +- extra/intl-extra/composer.json | 2 +- extra/markdown-extra/composer.json | 2 +- extra/string-extra/composer.json | 2 +- extra/twig-extra-bundle/composer.json | 2 +- 12 files changed, 11 insertions(+), 17 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b9735529697..3de8aa10cdd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,9 +21,6 @@ jobs: strategy: matrix: php-version: - - '7.2.5' - - '7.3' - - '7.4' - '8.0' - '8.1' - '8.2' @@ -73,9 +70,6 @@ jobs: strategy: matrix: php-version: - - '7.2.5' - - '7.3' - - '7.4' - '8.0' - '8.1' - '8.2' diff --git a/CHANGELOG b/CHANGELOG index 31bbadcd907..f88926cad14 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,6 @@ # 3.12.0 (2024-XX-XX) + * Bump minimum PHP version to 8.0 * Fix integration tests when a test has more than on data/expect section and deprecations * Add the `enum_cases` function diff --git a/composer.json b/composer.json index 26cb4972ec5..e0c3e6c6cc1 100644 --- a/composer.json +++ b/composer.json @@ -24,8 +24,7 @@ } ], "require": { - "php": ">=7.2.5", - "symfony/polyfill-php80": "^1.22", + "php": ">=8.0.2", "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-mbstring": "^1.3", "symfony/polyfill-ctype": "^1.8", diff --git a/doc/intro.rst b/doc/intro.rst index 8914507e4f0..5b0256f224c 100644 --- a/doc/intro.rst +++ b/doc/intro.rst @@ -30,7 +30,7 @@ Slim, Yii, Laravel, and Codeigniter — just to name a few. Prerequisites ------------- -Twig 3.x needs at least **PHP 7.2.5** to run. +Twig 3.x needs at least **PHP 8.0.2** to run. Installation ------------ diff --git a/extra/cache-extra/composer.json b/extra/cache-extra/composer.json index 60d05b90f42..09b67186e1d 100644 --- a/extra/cache-extra/composer.json +++ b/extra/cache-extra/composer.json @@ -15,7 +15,7 @@ } ], "require": { - "php": ">=7.2.5", + "php": ">=8.0.2", "symfony/cache": "^5.4|^6.4|^7.0", "twig/twig": "^3.11" }, diff --git a/extra/cssinliner-extra/composer.json b/extra/cssinliner-extra/composer.json index 0a279c61323..66b4ace78e5 100644 --- a/extra/cssinliner-extra/composer.json +++ b/extra/cssinliner-extra/composer.json @@ -15,7 +15,7 @@ } ], "require": { - "php": ">=7.2.5", + "php": ">=8.0.2", "symfony/deprecation-contracts": "^2.5|^3", "tijsverkoyen/css-to-inline-styles": "^2.0", "twig/twig": "^3.0" diff --git a/extra/html-extra/composer.json b/extra/html-extra/composer.json index 21592fb9ccc..a5b65d00bce 100644 --- a/extra/html-extra/composer.json +++ b/extra/html-extra/composer.json @@ -15,7 +15,7 @@ } ], "require": { - "php": ">=7.2.5", + "php": ">=8.0.2", "symfony/deprecation-contracts": "^2.5|^3", "symfony/mime": "^5.4|^6.4|^7.0", "twig/twig": "^3.0" diff --git a/extra/inky-extra/composer.json b/extra/inky-extra/composer.json index 9eacfc55b35..77d83e2ff92 100644 --- a/extra/inky-extra/composer.json +++ b/extra/inky-extra/composer.json @@ -15,7 +15,7 @@ } ], "require": { - "php": ">=7.2.5", + "php": ">=8.0.2", "symfony/deprecation-contracts": "^2.5|^3", "lorenzo/pinky": "^1.0.5", "twig/twig": "^3.0" diff --git a/extra/intl-extra/composer.json b/extra/intl-extra/composer.json index b3c3ceff366..54b3f8956d9 100644 --- a/extra/intl-extra/composer.json +++ b/extra/intl-extra/composer.json @@ -15,7 +15,7 @@ } ], "require": { - "php": ">=7.2.5", + "php": ">=8.0.2", "twig/twig": "^3.10", "symfony/intl": "^5.4|^6.4|^7.0" }, diff --git a/extra/markdown-extra/composer.json b/extra/markdown-extra/composer.json index 745ee502209..8dfe2fa844f 100644 --- a/extra/markdown-extra/composer.json +++ b/extra/markdown-extra/composer.json @@ -15,7 +15,7 @@ } ], "require": { - "php": ">=7.2.5", + "php": ">=8.0.2", "symfony/deprecation-contracts": "^2.5|^3", "twig/twig": "^3.0" }, diff --git a/extra/string-extra/composer.json b/extra/string-extra/composer.json index 4539faf0c2d..321394691e1 100644 --- a/extra/string-extra/composer.json +++ b/extra/string-extra/composer.json @@ -15,7 +15,7 @@ } ], "require": { - "php": ">=7.2.5", + "php": ">=8.0.2", "symfony/string": "^5.4|^6.4|^7.0", "symfony/translation-contracts": "^1.1|^2|^3", "twig/twig": "^3.0" diff --git a/extra/twig-extra-bundle/composer.json b/extra/twig-extra-bundle/composer.json index ded59311f4b..15e365a3878 100644 --- a/extra/twig-extra-bundle/composer.json +++ b/extra/twig-extra-bundle/composer.json @@ -15,7 +15,7 @@ } ], "require": { - "php": ">=7.2.5", + "php": ">=8.0.2", "symfony/framework-bundle": "^5.4|^6.4|^7.0", "symfony/twig-bundle": "^5.4|^6.4|^7.0", "twig/twig": "^3.0" From 84bc23af93761b263a803ae5566b5dc694029545 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sat, 10 Aug 2024 17:24:40 +0200 Subject: [PATCH 332/812] Add the notion of a Twig callable --- CHANGELOG | 1 + src/AbstractTwigCallable.php | 105 ++++++++++++++++++++++++++++++++++ src/TwigCallableInterface.php | 47 +++++++++++++++ src/TwigFilter.php | 90 ++--------------------------- src/TwigFunction.php | 90 ++--------------------------- src/TwigTest.php | 69 ++++------------------ 6 files changed, 171 insertions(+), 231 deletions(-) create mode 100644 src/AbstractTwigCallable.php create mode 100644 src/TwigCallableInterface.php diff --git a/CHANGELOG b/CHANGELOG index f88926cad14..b6ef802a151 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,6 @@ # 3.12.0 (2024-XX-XX) + * Add the notion of Twig callables (functions, filters, and tests) * Bump minimum PHP version to 8.0 * Fix integration tests when a test has more than on data/expect section and deprecations * Add the `enum_cases` function diff --git a/src/AbstractTwigCallable.php b/src/AbstractTwigCallable.php new file mode 100644 index 00000000000..b4ff5956512 --- /dev/null +++ b/src/AbstractTwigCallable.php @@ -0,0 +1,105 @@ + + */ +abstract class AbstractTwigCallable implements TwigCallableInterface +{ + protected $options; + + private $name; + private $callable; + private $arguments; + + public function __construct(string $name, $callable = null, array $options = []) + { + $this->name = $name; + $this->callable = $callable; + $this->arguments = []; + $this->options = array_merge([ + 'needs_environment' => false, + 'needs_context' => false, + 'needs_charset' => false, + 'is_variadic' => false, + 'deprecated' => false, + 'deprecating_package' => '', + 'alternative' => null, + ], $options); + } + + public function getName(): string + { + return $this->name; + } + + public function getCallable() + { + return $this->callable; + } + + public function getNodeClass(): string + { + return $this->options['node_class']; + } + + public function needsCharset(): bool + { + return $this->options['needs_charset']; + } + + public function needsEnvironment(): bool + { + return $this->options['needs_environment']; + } + + public function needsContext(): bool + { + return $this->options['needs_context']; + } + + public function setArguments(array $arguments): void + { + $this->arguments = $arguments; + } + + public function getArguments(): array + { + return $this->arguments; + } + + public function isVariadic(): bool + { + return $this->options['is_variadic']; + } + + public function isDeprecated(): bool + { + return (bool) $this->options['deprecated']; + } + + public function getDeprecatingPackage(): string + { + return $this->options['deprecating_package']; + } + + public function getDeprecatedVersion(): string + { + return \is_bool($this->options['deprecated']) ? '' : $this->options['deprecated']; + } + + public function getAlternative(): ?string + { + return $this->options['alternative']; + } +} diff --git a/src/TwigCallableInterface.php b/src/TwigCallableInterface.php new file mode 100644 index 00000000000..ddce193a23d --- /dev/null +++ b/src/TwigCallableInterface.php @@ -0,0 +1,47 @@ + + */ +interface TwigCallableInterface +{ + public function getName(): string; + + /** + * @return callable|array{class-string, string}|null + */ + public function getCallable(); + + public function getNodeClass(): string; + + public function needsCharset(): bool; + + public function needsEnvironment(): bool; + + public function needsContext(): bool; + + public function setArguments(array $arguments): void; + + public function getArguments(): array; + + public function isVariadic(): bool; + + public function isDeprecated(): bool; + + public function getDeprecatingPackage(): string; + + public function getDeprecatedVersion(): string; + + public function getAlternative(): ?string; +} diff --git a/src/TwigFilter.php b/src/TwigFilter.php index 2b80df9399b..36bb5d817db 100644 --- a/src/TwigFilter.php +++ b/src/TwigFilter.php @@ -21,79 +21,22 @@ * * @see https://twig.symfony.com/doc/templates.html#filters */ -final class TwigFilter +final class TwigFilter extends AbstractTwigCallable { - private $name; - private $callable; - private $options; - private $arguments = []; - /** * @param callable|array{class-string, string}|null $callable A callable implementing the filter. If null, you need to overwrite the "node_class" option to customize compilation. */ public function __construct(string $name, $callable = null, array $options = []) { - $this->name = $name; - $this->callable = $callable; + parent::__construct($name, $callable, $options); + $this->options = array_merge([ - 'needs_environment' => false, - 'needs_context' => false, - 'needs_charset' => false, - 'is_variadic' => false, 'is_safe' => null, 'is_safe_callback' => null, 'pre_escape' => null, 'preserves_safety' => null, 'node_class' => FilterExpression::class, - 'deprecated' => false, - 'deprecating_package' => '', - 'alternative' => null, - ], $options); - } - - public function getName(): string - { - return $this->name; - } - - /** - * Returns the callable to execute for this filter. - * - * @return callable|array{class-string, string}|null - */ - public function getCallable() - { - return $this->callable; - } - - public function getNodeClass(): string - { - return $this->options['node_class']; - } - - public function setArguments(array $arguments): void - { - $this->arguments = $arguments; - } - - public function getArguments(): array - { - return $this->arguments; - } - - public function needsCharset(): bool - { - return $this->options['needs_charset']; - } - - public function needsEnvironment(): bool - { - return $this->options['needs_environment']; - } - - public function needsContext(): bool - { - return $this->options['needs_context']; + ], $this->options); } public function getSafe(Node $filterArgs): ?array @@ -118,29 +61,4 @@ public function getPreEscape(): ?string { return $this->options['pre_escape']; } - - public function isVariadic(): bool - { - return $this->options['is_variadic']; - } - - public function isDeprecated(): bool - { - return (bool) $this->options['deprecated']; - } - - public function getDeprecatingPackage(): string - { - return $this->options['deprecating_package']; - } - - public function getDeprecatedVersion(): string - { - return \is_bool($this->options['deprecated']) ? '' : $this->options['deprecated']; - } - - public function getAlternative(): ?string - { - return $this->options['alternative']; - } } diff --git a/src/TwigFunction.php b/src/TwigFunction.php index bfee7eb87e0..4797320ebbd 100644 --- a/src/TwigFunction.php +++ b/src/TwigFunction.php @@ -21,77 +21,20 @@ * * @see https://twig.symfony.com/doc/templates.html#functions */ -final class TwigFunction +final class TwigFunction extends AbstractTwigCallable { - private $name; - private $callable; - private $options; - private $arguments = []; - /** * @param callable|array{class-string, string}|null $callable A callable implementing the function. If null, you need to overwrite the "node_class" option to customize compilation. */ public function __construct(string $name, $callable = null, array $options = []) { - $this->name = $name; - $this->callable = $callable; + parent::__construct($name, $callable, $options); + $this->options = array_merge([ - 'needs_environment' => false, - 'needs_context' => false, - 'needs_charset' => false, - 'is_variadic' => false, 'is_safe' => null, 'is_safe_callback' => null, 'node_class' => FunctionExpression::class, - 'deprecated' => false, - 'deprecating_package' => '', - 'alternative' => null, - ], $options); - } - - public function getName(): string - { - return $this->name; - } - - /** - * Returns the callable to execute for this function. - * - * @return callable|array{class-string, string}|null - */ - public function getCallable() - { - return $this->callable; - } - - public function getNodeClass(): string - { - return $this->options['node_class']; - } - - public function setArguments(array $arguments): void - { - $this->arguments = $arguments; - } - - public function getArguments(): array - { - return $this->arguments; - } - - public function needsCharset(): bool - { - return $this->options['needs_charset']; - } - - public function needsEnvironment(): bool - { - return $this->options['needs_environment']; - } - - public function needsContext(): bool - { - return $this->options['needs_context']; + ], $this->options); } public function getSafe(Node $functionArgs): ?array @@ -106,29 +49,4 @@ public function getSafe(Node $functionArgs): ?array return []; } - - public function isVariadic(): bool - { - return (bool) $this->options['is_variadic']; - } - - public function isDeprecated(): bool - { - return (bool) $this->options['deprecated']; - } - - public function getDeprecatingPackage(): string - { - return $this->options['deprecating_package']; - } - - public function getDeprecatedVersion(): string - { - return \is_bool($this->options['deprecated']) ? '' : $this->options['deprecated']; - } - - public function getAlternative(): ?string - { - return $this->options['alternative']; - } } diff --git a/src/TwigTest.php b/src/TwigTest.php index 0b43a284906..8690413336b 100644 --- a/src/TwigTest.php +++ b/src/TwigTest.php @@ -20,83 +20,34 @@ * * @see https://twig.symfony.com/doc/templates.html#test-operator */ -final class TwigTest +final class TwigTest extends AbstractTwigCallable { - private $name; - private $callable; - private $options; - private $arguments = []; - /** * @param callable|array{class-string, string}|null $callable A callable implementing the test. If null, you need to overwrite the "node_class" option to customize compilation. */ public function __construct(string $name, $callable = null, array $options = []) { - $this->name = $name; - $this->callable = $callable; + parent::__construct($name, $callable, $options); + $this->options = array_merge([ - 'is_variadic' => false, 'node_class' => TestExpression::class, - 'deprecated' => false, - 'deprecating_package' => '', - 'alternative' => null, 'one_mandatory_argument' => false, - ], $options); - } - - public function getName(): string - { - return $this->name; - } - - /** - * Returns the callable to execute for this test. - * - * @return callable|array{class-string, string}|null - */ - public function getCallable() - { - return $this->callable; - } - - public function getNodeClass(): string - { - return $this->options['node_class']; - } - - public function setArguments(array $arguments): void - { - $this->arguments = $arguments; - } - - public function getArguments(): array - { - return $this->arguments; - } - - public function isVariadic(): bool - { - return (bool) $this->options['is_variadic']; - } - - public function isDeprecated(): bool - { - return (bool) $this->options['deprecated']; + ], $this->options); } - public function getDeprecatingPackage(): string + public function needsCharset(): bool { - return $this->options['deprecating_package']; + return false; } - public function getDeprecatedVersion(): string + public function needsEnvironment(): bool { - return \is_bool($this->options['deprecated']) ? '' : $this->options['deprecated']; + return false; } - public function getAlternative(): ?string + public function needsContext(): bool { - return $this->options['alternative']; + return false; } public function hasOneMandatoryArgument(): bool From 8a8f22131c28af7bb107277ed7f4633ec2e366f0 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sat, 10 Aug 2024 18:25:26 +0200 Subject: [PATCH 333/812] Simplify tests --- tests/Node/Expression/CallTest.php | 42 ++++++++++++++---------------- 1 file changed, 19 insertions(+), 23 deletions(-) diff --git a/tests/Node/Expression/CallTest.php b/tests/Node/Expression/CallTest.php index 47051207644..7f02e68f5c1 100644 --- a/tests/Node/Expression/CallTest.php +++ b/tests/Node/Expression/CallTest.php @@ -102,51 +102,47 @@ public function testGetArgumentsWithInvalidCallable() $this->getArguments($node, ['', []]); } - public static function customStaticFunction($arg1, $arg2 = 'default', $arg3 = []) + public function testResolveArgumentsWithMissingParameterForArbitraryArgumentsOnFunction() { - } + $this->expectException(\LogicException::class); + $this->expectExceptionMessageMatches('#^The last parameter of "Twig\\\\Tests\\\\Node\\\\Expression\\\\custom_call_test_function" for function "foo" must be an array with default value, eg\\. "array \\$arg \\= \\[\\]"\\.$#'); - public function customFunction($arg1, $arg2 = 'default', $arg3 = []) - { + $node = new Node_Expression_Call([], ['type' => 'function', 'name' => 'foo', 'is_variadic' => true]); + $this->getArguments($node, ['Twig\Tests\Node\Expression\custom_call_test_function', []]); } - private function getArguments($call, $args) + public function testResolveArgumentsWithMissingParameterForArbitraryArgumentsOnObject() { - $m = new \ReflectionMethod($call, 'getArguments'); - $m->setAccessible(true); + $this->expectException(\LogicException::class); + $this->expectExceptionMessageMatches('#^The last parameter of "Twig\\\\Tests\\\\Node\\\\Expression\\\\CallableTestClass\\:\\:__invoke" for function "foo" must be an array with default value, eg\\. "array \\$arg \\= \\[\\]"\\.$#'); - return $m->invokeArgs($call, $args); + $node = new Node_Expression_Call([], ['type' => 'function', 'name' => 'foo', 'is_variadic' => true]); + $this->getArguments($node, [new CallableTestClass(), []]); } - public function customFunctionWithArbitraryArguments() + public static function customStaticFunction($arg1, $arg2 = 'default', $arg3 = []) { } - public function testResolveArgumentsWithMissingParameterForArbitraryArgumentsOnFunction() + public function customFunction($arg1, $arg2 = 'default', $arg3 = []) { - $this->expectException(\LogicException::class); - $this->expectExceptionMessageMatches('#^The last parameter of "Twig\\\\Tests\\\\Node\\\\Expression\\\\custom_call_test_function" for function "foo" must be an array with default value, eg\\. "array \\$arg \\= \\[\\]"\\.$#'); + } - $node = new Node_Expression_Call([], ['type' => 'function', 'name' => 'foo', 'is_variadic' => true]); - $node->getArguments('Twig\Tests\Node\Expression\custom_call_test_function', []); + public function customFunctionWithArbitraryArguments() + { } - public function testResolveArgumentsWithMissingParameterForArbitraryArgumentsOnObject() + private function getArguments($call, $args) { - $this->expectException(\LogicException::class); - $this->expectExceptionMessageMatches('#^The last parameter of "Twig\\\\Tests\\\\Node\\\\Expression\\\\CallableTestClass\\:\\:__invoke" for function "foo" must be an array with default value, eg\\. "array \\$arg \\= \\[\\]"\\.$#'); + $m = new \ReflectionMethod($call, 'getArguments'); + $m->setAccessible(true); - $node = new Node_Expression_Call([], ['type' => 'function', 'name' => 'foo', 'is_variadic' => true]); - $node->getArguments(new CallableTestClass(), []); + return $m->invokeArgs($call, $args); } } class Node_Expression_Call extends CallExpression { - public function getArguments($callable, $arguments) - { - return parent::getArguments($callable, $arguments); - } } class CallableTestClass From e1b099707ca5f4b839bbd4aa729d80b6bfc9857f Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sun, 11 Aug 2024 12:56:40 +0200 Subject: [PATCH 334/812] Micro-micro optimization --- src/NodeVisitor/SafeAnalysisNodeVisitor.php | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/NodeVisitor/SafeAnalysisNodeVisitor.php b/src/NodeVisitor/SafeAnalysisNodeVisitor.php index 6df046e1c7b..8a5135aae91 100644 --- a/src/NodeVisitor/SafeAnalysisNodeVisitor.php +++ b/src/NodeVisitor/SafeAnalysisNodeVisitor.php @@ -97,9 +97,8 @@ public function leaveNode(Node $node, Environment $env): ?Node } elseif ($node instanceof FilterExpression) { // filter expression is safe when the filter is safe $name = $node->getNode('filter')->getAttribute('value'); - $args = $node->getNode('arguments'); if ($filter = $env->getFilter($name)) { - $safe = $filter->getSafe($args); + $safe = $filter->getSafe($node->getNode('arguments')); if (null === $safe) { $safe = $this->intersectSafe($this->getSafe($node->getNode('node')), $filter->getPreservesSafety()); } @@ -110,9 +109,8 @@ public function leaveNode(Node $node, Environment $env): ?Node } elseif ($node instanceof FunctionExpression) { // function expression is safe when the function is safe $name = $node->getAttribute('name'); - $args = $node->getNode('arguments'); if ($function = $env->getFunction($name)) { - $this->setSafe($node, $function->getSafe($args)); + $this->setSafe($node, $function->getSafe($node->getNode('arguments'))); } else { $this->setSafe($node, []); } From c6656cf5d03fad363ec1e1c503197f165a9f57f1 Mon Sep 17 00:00:00 2001 From: Ruud Kamphuis Date: Wed, 7 Aug 2024 19:19:01 +0200 Subject: [PATCH 335/812] Deprecate unnecessary escape characters See #4123 #2712 This allows to change the implementation in v4 so that we no longer have to escape backslashes. --- doc/templates.rst | 23 +++++++++-- src/Lexer.php | 79 +++++++++++++++++++++++++++++++++++++- tests/LexerTest.php | 93 +++++++++++++++++++++++++++++++++++++++------ 3 files changed, 177 insertions(+), 18 deletions(-) diff --git a/doc/templates.rst b/doc/templates.rst index b0f5b9a1c29..2746d90f6a7 100644 --- a/doc/templates.rst +++ b/doc/templates.rst @@ -522,10 +522,25 @@ exist: * ``"Hello World"``: Everything between two double or single quotes is a string. They are useful whenever you need a string in the template (for example as arguments to function calls, filters or just to extend or include - a template). A string can contain a delimiter if it is preceded by a - backslash (``\``) -- like in ``'It\'s good'``. If the string contains a - backslash (e.g. ``'c:\Program Files'``) escape it by doubling it - (e.g. ``'c:\\Program Files'``). + a template). + + Note that certain characters require escaping: + * ``\f``: Form feed + * ``\n``: New line + * ``\r``: Carriage return + * ``\t``: Horizontal tab + * ``\v``: Vertical tab + * ``\x``: Hexadecimal escape sequence + * ``\0`` to ``\377``: Octal escape sequences representing characters + * ``\``: Backslash + + When using single-quoted strings, the single quote character (``'``) needs to be escaped with a backslash (``\'``). + When using double-quoted strings, the double quote character (``"``) needs to be escaped with a backslash (``\"``). + + For example, a single quoted string can contain a delimiter if it is preceded by a + backslash (``\``) -- like in ``'It\'s good'``. If the string contains a + backslash (e.g. ``'c:\Program Files'``) escape it by doubling it + (e.g. ``'c:\\Program Files'``). * ``42`` / ``42.23``: Integers and floating point numbers are created by writing the number down. If a dot is present the number is a float, diff --git a/src/Lexer.php b/src/Lexer.php index 8973fbbc8da..526eb750b61 100644 --- a/src/Lexer.php +++ b/src/Lexer.php @@ -367,7 +367,7 @@ private function lexExpression(): void } // strings elseif (preg_match(self::REGEX_STRING, $this->code, $match, 0, $this->cursor)) { - $this->pushToken(/* Token::STRING_TYPE */ 7, stripcslashes(substr($match[0], 1, -1))); + $this->pushToken(/* Token::STRING_TYPE */ 7, $this->stripcslashes(substr($match[0], 1, -1), substr($match[0], 0, 1))); $this->moveCursor($match[0]); } // opening double quoted string @@ -382,6 +382,81 @@ private function lexExpression(): void } } + private function stripcslashes(string $str, string $quoteType): string + { + if (!str_contains($str, '\\')) { + return $str; + } + + $result = ''; + $length = strlen($str); + $specialChars = [ + 'f' => "\f", + 'n' => "\n", + 'r' => "\r", + 't' => "\t", + 'v' => "\v", + ]; + + for ($i = 0; $i < $length; $i++) { + if ($str[$i] !== '\\' || $i + 1 >= $length) { + $result .= $str[$i]; + + continue; + } + + $nextChar = $str[$i + 1]; + if (isset($specialChars[$nextChar])) { + $result .= $specialChars[$nextChar]; + $i++; + } elseif ($nextChar === '\\') { + $result .= '\\'; + $i++; + } elseif ($nextChar === "'" || $nextChar === '"') { + if ($nextChar !== $quoteType) { + trigger_deprecation('twig/twig', '3.12', 'Character "%s" at position %d does not need to be escaped anymore.', $nextChar, $i + 2); + } + $result .= $nextChar; + $i++; + } elseif ($nextChar === '#' && $i + 2 < $length && $str[$i + 2] === '{') { + $result .= '#{'; + $i += 2; + } elseif ($nextChar === 'x' && $i + 2 < $length && ctype_xdigit($str[$i + 2])) { + $hex = $str[$i + 2]; + if ($i + 3 < $length && ctype_xdigit($str[$i + 3])) { + $hex .= $str[$i + 3]; + $i++; + } + $result .= chr(hexdec($hex)); + $i += 2; + } elseif (ctype_digit($nextChar) && $nextChar < '8') { + $octal = $nextChar; + for ($j = $i + 1; $j <= 3; $j++) { + $o = $j + 1; + if ($o >= $length) { + break; + } + if (!ctype_digit($str[$o])) { + break; + } + if ($str[$o] >= '8') { + break; + } + $octal .= $str[$o]; + $i++; + } + $result .= chr(octdec($octal)); + $i++; + } else { + trigger_deprecation('twig/twig', '3.12', 'Character "%s" at position %d does not need to be escaped anymore.', $nextChar, $i + 2); + $result .= $nextChar; + $i++; + } + } + + return $result; + } + private function lexRawData(): void { if (!preg_match($this->regexes['lex_raw_data'], $this->code, $match, \PREG_OFFSET_CAPTURE, $this->cursor)) { @@ -423,7 +498,7 @@ private function lexString(): void $this->moveCursor($match[0]); $this->pushState(self::STATE_INTERPOLATION); } elseif (preg_match(self::REGEX_DQ_STRING_PART, $this->code, $match, 0, $this->cursor) && '' !== $match[0]) { - $this->pushToken(/* Token::STRING_TYPE */ 7, stripcslashes($match[0])); + $this->pushToken(/* Token::STRING_TYPE */ 7, $this->stripcslashes($match[0], '"')); $this->moveCursor($match[0]); } elseif (preg_match(self::REGEX_DQ_STRING_DELIM, $this->code, $match, 0, $this->cursor)) { [$expect, $lineno] = array_pop($this->brackets); diff --git a/tests/LexerTest.php b/tests/LexerTest.php index 38ee2361569..81690829a08 100644 --- a/tests/LexerTest.php +++ b/tests/LexerTest.php @@ -12,6 +12,7 @@ */ use PHPUnit\Framework\TestCase; +use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; use Twig\Environment; use Twig\Error\SyntaxError; use Twig\Lexer; @@ -21,6 +22,8 @@ class LexerTest extends TestCase { + use ExpectDeprecationTrait; + public function testNameLabelForTag() { $template = '{% § %}'; @@ -178,23 +181,89 @@ public function testBigNumbers() $this->assertEquals('922337203685477580700', $node->getValue()); } - public function testStringWithEscapedDelimiter() + /** + * @dataProvider getStringWithEscapedDelimiter + */ + public function testStringWithEscapedDelimiter(string $template, string $expected) + { + $lexer = new Lexer(new Environment(new ArrayLoader())); + $stream = $lexer->tokenize(new Source($template, 'index')); + $stream->expect(Token::VAR_START_TYPE); + $token = $stream->expect(Token::STRING_TYPE); + $this->assertSame($expected, $token->getValue()); + } + + public function getStringWithEscapedDelimiter() { - $tests = [ - "{{ 'foo \' bar' }}" => 'foo \' bar', - '{{ "foo \" bar" }}' => 'foo " bar', + yield '{{ \'\x6\' }} => \x6' => [ + '{{ \'\x6\' }}', + "\x6", ]; + yield '{{ \'\065\x64\' }} => \065\x64' => [ + '{{ \'\065\x64\' }}', + "\065\x64", + ]; + yield '{{ \'App\\\\Test\' }} => App\Test' => [ + '{{ \'App\\\\Test\' }}', + 'App\\Test', + ]; + yield '{{ "App\#{var}" }} => App#{var}' => [ + '{{ "App\#{var}" }}', + 'App#{var}', + ]; + yield '{{ \'foo \\\' bar\' }} => foo \' bar' => [ + '{{ \'foo \\\' bar\' }}', + 'foo \' bar', + ]; + yield '{{ "foo \" bar" }} => foo " bar' => [ + '{{ "foo \\" bar" }}', + 'foo " bar', + ]; + yield '{{ \'\f\n\r\t\v\' }} => \f\n\r\t\v' => [ + '{{ \'\\f\\n\\r\\t\\v\' }}', + "\f\n\r\t\v", + ]; + yield '{{ \'\\\\f\\\\n\\\\r\\\\t\\\\v\' }} => \\f\\n\\r\\t\\v' => [ + '{{ \'\\\\f\\\\n\\\\r\\\\t\\\\v\' }}', + '\\f\\n\\r\\t\\v', + ]; + } + + /** + * @group legacy + * @dataProvider getStringWithEscapedDelimiterProducingDeprecation + */ + public function testStringWithEscapedDelimiterProducingDeprecation(string $template, string $expected, string $expectedDeprecation) + { + $this->expectDeprecation($expectedDeprecation); $lexer = new Lexer(new Environment(new ArrayLoader())); - foreach ($tests as $template => $expected) { - $stream = $lexer->tokenize(new Source($template, 'index')); - $stream->expect(Token::VAR_START_TYPE); - $stream->expect(Token::STRING_TYPE, $expected); + $stream = $lexer->tokenize(new Source($template, 'index')); + $stream->expect(Token::VAR_START_TYPE); + $stream->expect(Token::STRING_TYPE, $expected); - // add a dummy assertion here to satisfy PHPUnit, the only thing we want to test is that the code above - // can be executed without throwing any exceptions - $this->addToAssertionCount(1); - } + // add a dummy assertion here to satisfy PHPUnit, the only thing we want to test is that the code above + // can be executed without throwing any exceptions + $this->addToAssertionCount(1); + } + + public function getStringWithEscapedDelimiterProducingDeprecation() + { + yield '{{ \'App\Test\' }} => AppTest' => [ + '{{ \'App\\Test\' }}', + 'AppTest', + "Since twig/twig 3.12: Character \"T\" at position 5 does not need to be escaped anymore.", + ]; + yield '{{ "foo \\\' bar" }} => foo \' bar' => [ + '{{ "foo \\\' bar" }}', + 'foo \' bar', + "Since twig/twig 3.12: Character \"'\" at position 6 does not need to be escaped anymore.", + ]; + yield '{{ \'foo \" bar\' }} => foo " bar' => [ + '{{ \'foo \\" bar\' }}', + 'foo " bar', + 'Since twig/twig 3.12: Character """ at position 6 does not need to be escaped anymore.', + ]; } public function testStringWithInterpolation() From 8ef12588ead31dfbf32405d3f3e3848cd4ff1579 Mon Sep 17 00:00:00 2001 From: Ruud Kamphuis Date: Sun, 11 Aug 2024 18:59:31 +0200 Subject: [PATCH 336/812] Remove escape --- extra/html-extra/Tests/Fixtures/data_uri.test | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extra/html-extra/Tests/Fixtures/data_uri.test b/extra/html-extra/Tests/Fixtures/data_uri.test index 65ec863f6e7..070e713e074 100644 --- a/extra/html-extra/Tests/Fixtures/data_uri.test +++ b/extra/html-extra/Tests/Fixtures/data_uri.test @@ -1,7 +1,7 @@ --TEST-- "data_uri" filter --TEMPLATE-- -{{ 'foobar#'|data_uri(parameters={charset: "utf-8", foo: "\$bar"}) }} +{{ 'foobar#'|data_uri(parameters={charset: "utf-8", foo: "$bar"}) }} {{ 'foobar'|data_uri(mime="text/html", parameters={charset: "ascii"}) }} --DATA-- From 03012f190f7f6f1555fa042180abb38fc5eeb6be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Andr=C3=A9?= Date: Mon, 12 Aug 2024 08:16:07 +0200 Subject: [PATCH 337/812] (minor) Prefer `className` over `classname` For consistency with how it's written in the tested classes. --- tests/Cache/ChainTest.php | 48 +++++++++++++------------- tests/Cache/FilesystemTest.php | 16 ++++----- tests/Cache/ReadOnlyFilesystemTest.php | 16 ++++----- 3 files changed, 40 insertions(+), 40 deletions(-) diff --git a/tests/Cache/ChainTest.php b/tests/Cache/ChainTest.php index 338cc49de58..3120ab18861 100644 --- a/tests/Cache/ChainTest.php +++ b/tests/Cache/ChainTest.php @@ -18,7 +18,7 @@ class ChainTest extends TestCase { - private $classname; + private $className; private $directory; private $cache; private $key; @@ -26,13 +26,13 @@ class ChainTest extends TestCase protected function setUp(): void { $nonce = hash(\PHP_VERSION_ID < 80100 ? 'sha256' : 'xxh128', random_bytes(32)); - $this->classname = '__Twig_Tests_Cache_ChainTest_Template_'.$nonce; + $this->className = '__Twig_Tests_Cache_ChainTest_Template_'.$nonce; $this->directory = sys_get_temp_dir().'/twig-test'; $this->cache = new ChainCache([ new FilesystemCache($this->directory.'/A'), new FilesystemCache($this->directory.'/B'), ]); - $this->key = $this->cache->generateKey('_test_', $this->classname); + $this->key = $this->cache->generateKey('_test_', $this->className); } protected function tearDown(): void @@ -45,75 +45,75 @@ protected function tearDown(): void public function testLoadInA() { $cache = new FilesystemCache($this->directory.'/A'); - $key = $cache->generateKey('_test_', $this->classname); + $key = $cache->generateKey('_test_', $this->className); $dir = \dirname($key); @mkdir($dir, 0777, true); $this->assertDirectoryExists($dir); - $this->assertFalse(class_exists($this->classname, false)); + $this->assertFalse(class_exists($this->className, false)); $content = $this->generateSource(); file_put_contents($key, $content); $this->cache->load($this->key); - $this->assertTrue(class_exists($this->classname, false)); + $this->assertTrue(class_exists($this->className, false)); } public function testLoadInB() { $cache = new FilesystemCache($this->directory.'/B'); - $key = $cache->generateKey('_test_', $this->classname); + $key = $cache->generateKey('_test_', $this->className); $dir = \dirname($key); @mkdir($dir, 0777, true); $this->assertDirectoryExists($dir); - $this->assertFalse(class_exists($this->classname, false)); + $this->assertFalse(class_exists($this->className, false)); $content = $this->generateSource(); file_put_contents($key, $content); $this->cache->load($this->key); - $this->assertTrue(class_exists($this->classname, false)); + $this->assertTrue(class_exists($this->className, false)); } public function testLoadInBoth() { $cache = new FilesystemCache($this->directory.'/A'); - $key = $cache->generateKey('_test_', $this->classname); + $key = $cache->generateKey('_test_', $this->className); $dir = \dirname($key); @mkdir($dir, 0777, true); $this->assertDirectoryExists($dir); - $this->assertFalse(class_exists($this->classname, false)); + $this->assertFalse(class_exists($this->className, false)); $content = $this->generateSource(); file_put_contents($key, $content); $cache = new FilesystemCache($this->directory.'/B'); - $key = $cache->generateKey('_test_', $this->classname); + $key = $cache->generateKey('_test_', $this->className); $dir = \dirname($key); @mkdir($dir, 0777, true); $this->assertDirectoryExists($dir); - $this->assertFalse(class_exists($this->classname, false)); + $this->assertFalse(class_exists($this->className, false)); $content = $this->generateSource(); file_put_contents($key, $content); $this->cache->load($this->key); - $this->assertTrue(class_exists($this->classname, false)); + $this->assertTrue(class_exists($this->className, false)); } public function testLoadMissing() { - $this->assertFalse(class_exists($this->classname, false)); + $this->assertFalse(class_exists($this->className, false)); $this->cache->load($this->key); - $this->assertFalse(class_exists($this->classname, false)); + $this->assertFalse(class_exists($this->className, false)); } public function testWrite() @@ -121,13 +121,13 @@ public function testWrite() $content = $this->generateSource(); $cacheA = new FilesystemCache($this->directory.'/A'); - $keyA = $cacheA->generateKey('_test_', $this->classname); + $keyA = $cacheA->generateKey('_test_', $this->className); $this->assertFileDoesNotExist($keyA); $this->assertFileDoesNotExist($this->directory.'/A'); $cacheB = new FilesystemCache($this->directory.'/B'); - $keyB = $cacheB->generateKey('_test_', $this->classname); + $keyB = $cacheB->generateKey('_test_', $this->className); $this->assertFileDoesNotExist($keyB); $this->assertFileDoesNotExist($this->directory.'/B'); @@ -146,7 +146,7 @@ public function testWrite() public function testGetTimestampInA() { $cache = new FilesystemCache($this->directory.'/A'); - $key = $cache->generateKey('_test_', $this->classname); + $key = $cache->generateKey('_test_', $this->className); $dir = \dirname($key); @mkdir($dir, 0777, true); @@ -161,7 +161,7 @@ public function testGetTimestampInA() public function testGetTimestampInB() { $cache = new FilesystemCache($this->directory.'/B'); - $key = $cache->generateKey('_test_', $this->classname); + $key = $cache->generateKey('_test_', $this->className); $dir = \dirname($key); @mkdir($dir, 0777, true); @@ -176,7 +176,7 @@ public function testGetTimestampInB() public function testGetTimestampInBoth() { $cacheA = new FilesystemCache($this->directory.'/A'); - $keyA = $cacheA->generateKey('_test_', $this->classname); + $keyA = $cacheA->generateKey('_test_', $this->className); $dir = \dirname($keyA); @mkdir($dir, 0777, true); @@ -186,7 +186,7 @@ public function testGetTimestampInBoth() touch($keyA, 1234567890); $cacheB = new FilesystemCache($this->directory.'/B'); - $keyB = $cacheB->generateKey('_test_', $this->classname); + $keyB = $cacheB->generateKey('_test_', $this->className); $dir = \dirname($keyB); @mkdir($dir, 0777, true); @@ -222,8 +222,8 @@ public static function provideInput() private function generateSource() { - return strtr(' $this->classname, + return strtr(' $this->className, ]); } } diff --git a/tests/Cache/FilesystemTest.php b/tests/Cache/FilesystemTest.php index 934ce016082..574631404f7 100644 --- a/tests/Cache/FilesystemTest.php +++ b/tests/Cache/FilesystemTest.php @@ -17,14 +17,14 @@ class FilesystemTest extends TestCase { - private $classname; + private $className; private $directory; private $cache; protected function setUp(): void { $nonce = hash(\PHP_VERSION_ID < 80100 ? 'sha256' : 'xxh128', random_bytes(32)); - $this->classname = '__Twig_Tests_Cache_FilesystemTest_Template_'.$nonce; + $this->className = '__Twig_Tests_Cache_FilesystemTest_Template_'.$nonce; $this->directory = sys_get_temp_dir().'/twig-test'; $this->cache = new FilesystemCache($this->directory); } @@ -43,25 +43,25 @@ public function testLoad() $dir = \dirname($key); @mkdir($dir, 0777, true); $this->assertDirectoryExists($dir); - $this->assertFalse(class_exists($this->classname, false)); + $this->assertFalse(class_exists($this->className, false)); $content = $this->generateSource(); file_put_contents($key, $content); $this->cache->load($key); - $this->assertTrue(class_exists($this->classname, false)); + $this->assertTrue(class_exists($this->className, false)); } public function testLoadMissing() { $key = $this->directory.'/cache/cachefile.php'; - $this->assertFalse(class_exists($this->classname, false)); + $this->assertFalse(class_exists($this->className, false)); $this->cache->load($key); - $this->assertFalse(class_exists($this->classname, false)); + $this->assertFalse(class_exists($this->className, false)); } public function testWrite() @@ -187,8 +187,8 @@ public function provideDirectories() private function generateSource() { - return strtr(' $this->classname, + return strtr(' $this->className, ]); } } diff --git a/tests/Cache/ReadOnlyFilesystemTest.php b/tests/Cache/ReadOnlyFilesystemTest.php index 424f605239d..34bc14f88d0 100644 --- a/tests/Cache/ReadOnlyFilesystemTest.php +++ b/tests/Cache/ReadOnlyFilesystemTest.php @@ -17,14 +17,14 @@ class ReadOnlyFilesystemTest extends TestCase { - private $classname; + private $className; private $directory; private $cache; protected function setUp(): void { $nonce = hash(\PHP_VERSION_ID < 80100 ? 'sha256' : 'xxh128', random_bytes(32)); - $this->classname = '__Twig_Tests_Cache_ReadOnlyFilesystemTest_Template_'.$nonce; + $this->className = '__Twig_Tests_Cache_ReadOnlyFilesystemTest_Template_'.$nonce; $this->directory = sys_get_temp_dir().'/twig-test'; $this->cache = new ReadOnlyFilesystemCache($this->directory); } @@ -43,25 +43,25 @@ public function testLoad() $dir = \dirname($key); @mkdir($dir, 0777, true); $this->assertDirectoryExists($dir); - $this->assertFalse(class_exists($this->classname, false)); + $this->assertFalse(class_exists($this->className, false)); $content = $this->generateSource(); file_put_contents($key, $content); $this->cache->load($key); - $this->assertTrue(class_exists($this->classname, false)); + $this->assertTrue(class_exists($this->className, false)); } public function testLoadMissing() { $key = $this->directory.'/cache/cachefile.php'; - $this->assertFalse(class_exists($this->classname, false)); + $this->assertFalse(class_exists($this->className, false)); $this->cache->load($key); - $this->assertFalse(class_exists($this->classname, false)); + $this->assertFalse(class_exists($this->className, false)); } public function testWrite() @@ -125,8 +125,8 @@ public static function provideDirectories() private function generateSource() { - return strtr(' $this->classname, + return strtr(' $this->className, ]); } } From bc6e36dee9fc6015e973eb0461bb107ff8080405 Mon Sep 17 00:00:00 2001 From: Ruud Kamphuis Date: Mon, 12 Aug 2024 09:21:09 +0200 Subject: [PATCH 338/812] Optimize stripcslashes --- src/Lexer.php | 86 +++++++++++++++++++++++---------------------------- 1 file changed, 38 insertions(+), 48 deletions(-) diff --git a/src/Lexer.php b/src/Lexer.php index 526eb750b61..dec56d8d78f 100644 --- a/src/Lexer.php +++ b/src/Lexer.php @@ -50,6 +50,14 @@ class Lexer public const REGEX_DQ_STRING_PART = '/[^#"\\\\]*(?:(?:\\\\.|#(?!\{))[^#"\\\\]*)*/As'; public const PUNCTUATION = '()[]{}?:.,|'; + private const SPECIAL_CHARS = [ + 'f' => "\f", + 'n' => "\n", + 'r' => "\r", + 't' => "\t", + 'v' => "\v", + ]; + public function __construct(Environment $env, array $options = []) { $this->env = $env; @@ -384,76 +392,58 @@ private function lexExpression(): void private function stripcslashes(string $str, string $quoteType): string { - if (!str_contains($str, '\\')) { - return $str; - } - $result = ''; $length = strlen($str); - $specialChars = [ - 'f' => "\f", - 'n' => "\n", - 'r' => "\r", - 't' => "\t", - 'v' => "\v", - ]; + + $i = 0; + while ($i < $length) { + if (false === $pos = strpos($str, '\\', $i)) { + $result .= substr($str, $i); + break; + } - for ($i = 0; $i < $length; $i++) { - if ($str[$i] !== '\\' || $i + 1 >= $length) { - $result .= $str[$i]; + $result .= substr($str, $i, $pos - $i); + $i = $pos + 1; - continue; + if ($i >= $length) { + $result .= '\\'; + break; } - $nextChar = $str[$i + 1]; - if (isset($specialChars[$nextChar])) { - $result .= $specialChars[$nextChar]; - $i++; + $nextChar = $str[$i]; + + if (isset(self::SPECIAL_CHARS[$nextChar])) { + $result .= self::SPECIAL_CHARS[$nextChar]; } elseif ($nextChar === '\\') { - $result .= '\\'; - $i++; + $result .= $nextChar; } elseif ($nextChar === "'" || $nextChar === '"') { if ($nextChar !== $quoteType) { - trigger_deprecation('twig/twig', '3.12', 'Character "%s" at position %d does not need to be escaped anymore.', $nextChar, $i + 2); + trigger_deprecation('twig/twig', '3.12', 'Character "%s" at position %d does not need to be escaped anymore.', $nextChar, $i + 1); } $result .= $nextChar; - $i++; - } elseif ($nextChar === '#' && $i + 2 < $length && $str[$i + 2] === '{') { + } elseif ($nextChar === '#' && $i + 1 < $length && $str[$i + 1] === '{') { $result .= '#{'; - $i += 2; - } elseif ($nextChar === 'x' && $i + 2 < $length && ctype_xdigit($str[$i + 2])) { - $hex = $str[$i + 2]; - if ($i + 3 < $length && ctype_xdigit($str[$i + 3])) { - $hex .= $str[$i + 3]; - $i++; + $i++; + } elseif ($nextChar === 'x' && $i + 1 < $length && ctype_xdigit($str[$i + 1])) { + $hex = $str[++$i]; + if ($i + 1 < $length && ctype_xdigit($str[$i + 1])) { + $hex .= $str[++$i]; } $result .= chr(hexdec($hex)); - $i += 2; } elseif (ctype_digit($nextChar) && $nextChar < '8') { $octal = $nextChar; - for ($j = $i + 1; $j <= 3; $j++) { - $o = $j + 1; - if ($o >= $length) { - break; - } - if (!ctype_digit($str[$o])) { - break; - } - if ($str[$o] >= '8') { - break; - } - $octal .= $str[$o]; - $i++; + while ($i + 1 < $length && ctype_digit($str[$i + 1]) && $str[$i + 1] < '8' && strlen($octal) < 3) { + $octal .= $str[++$i]; } $result .= chr(octdec($octal)); - $i++; } else { - trigger_deprecation('twig/twig', '3.12', 'Character "%s" at position %d does not need to be escaped anymore.', $nextChar, $i + 2); + trigger_deprecation('twig/twig', '3.12', sprintf('Character "%s" at position %d does not need to be escaped anymore.', $nextChar, $i + 1)); $result .= $nextChar; - $i++; } + + $i++; } - + return $result; } From cd0ac3cc0a9d430a720b0967599f06d261abbbd8 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Mon, 12 Aug 2024 09:39:57 +0200 Subject: [PATCH 339/812] Remove const optimization --- src/ExpressionParser.php | 140 ++++++++++++------------- src/Lexer.php | 20 ++-- src/Parser.php | 4 +- src/TokenParser/BlockTokenParser.php | 4 +- src/TokenParser/EmbedTokenParser.php | 8 +- src/TokenParser/ForTokenParser.php | 2 +- src/TokenParser/FromTokenParser.php | 8 +- src/TokenParser/ImportTokenParser.php | 4 +- src/TokenParser/IncludeTokenParser.php | 8 +- src/TokenParser/MacroTokenParser.php | 4 +- src/TokenParser/SetTokenParser.php | 2 +- src/TokenParser/UseTokenParser.php | 6 +- src/TokenParser/WithTokenParser.php | 2 +- 13 files changed, 106 insertions(+), 106 deletions(-) diff --git a/src/ExpressionParser.php b/src/ExpressionParser.php index 84484404864..56a749efefd 100644 --- a/src/ExpressionParser.php +++ b/src/ExpressionParser.php @@ -105,52 +105,52 @@ private function parseArrow() $stream = $this->parser->getStream(); // short array syntax (one argument, no parentheses)? - if ($stream->look(1)->test(/* Token::ARROW_TYPE */ 12)) { + if ($stream->look(1)->test(Token::ARROW_TYPE)) { $line = $stream->getCurrent()->getLine(); - $token = $stream->expect(/* Token::NAME_TYPE */ 5); + $token = $stream->expect(Token::NAME_TYPE); $names = [new AssignNameExpression($token->getValue(), $token->getLine())]; - $stream->expect(/* Token::ARROW_TYPE */ 12); + $stream->expect(Token::ARROW_TYPE); return new ArrowFunctionExpression($this->parseExpression(0), new Node($names), $line); } // first, determine if we are parsing an arrow function by finding => (long form) $i = 0; - if (!$stream->look($i)->test(/* Token::PUNCTUATION_TYPE */ 9, '(')) { + if (!$stream->look($i)->test(Token::PUNCTUATION_TYPE, '(')) { return null; } ++$i; while (true) { // variable name ++$i; - if (!$stream->look($i)->test(/* Token::PUNCTUATION_TYPE */ 9, ',')) { + if (!$stream->look($i)->test(Token::PUNCTUATION_TYPE, ',')) { break; } ++$i; } - if (!$stream->look($i)->test(/* Token::PUNCTUATION_TYPE */ 9, ')')) { + if (!$stream->look($i)->test(Token::PUNCTUATION_TYPE, ')')) { return null; } ++$i; - if (!$stream->look($i)->test(/* Token::ARROW_TYPE */ 12)) { + if (!$stream->look($i)->test(Token::ARROW_TYPE)) { return null; } // yes, let's parse it properly - $token = $stream->expect(/* Token::PUNCTUATION_TYPE */ 9, '('); + $token = $stream->expect(Token::PUNCTUATION_TYPE, '('); $line = $token->getLine(); $names = []; while (true) { - $token = $stream->expect(/* Token::NAME_TYPE */ 5); + $token = $stream->expect(Token::NAME_TYPE); $names[] = new AssignNameExpression($token->getValue(), $token->getLine()); - if (!$stream->nextIf(/* Token::PUNCTUATION_TYPE */ 9, ',')) { + if (!$stream->nextIf(Token::PUNCTUATION_TYPE, ',')) { break; } } - $stream->expect(/* Token::PUNCTUATION_TYPE */ 9, ')'); - $stream->expect(/* Token::ARROW_TYPE */ 12); + $stream->expect(Token::PUNCTUATION_TYPE, ')'); + $stream->expect(Token::ARROW_TYPE); return new ArrowFunctionExpression($this->parseExpression(0), new Node($names), $line); } @@ -166,10 +166,10 @@ private function getPrimary(): AbstractExpression $class = $operator['class']; return $this->parsePostfixExpression(new $class($expr, $token->getLine())); - } elseif ($token->test(/* Token::PUNCTUATION_TYPE */ 9, '(')) { + } elseif ($token->test(Token::PUNCTUATION_TYPE, '(')) { $this->parser->getStream()->next(); $expr = $this->parseExpression(); - $this->parser->getStream()->expect(/* Token::PUNCTUATION_TYPE */ 9, ')', 'An opened parenthesis is not properly closed'); + $this->parser->getStream()->expect(Token::PUNCTUATION_TYPE, ')', 'An opened parenthesis is not properly closed'); return $this->parsePostfixExpression($expr); } @@ -179,10 +179,10 @@ private function getPrimary(): AbstractExpression private function parseConditionalExpression($expr): AbstractExpression { - while ($this->parser->getStream()->nextIf(/* Token::PUNCTUATION_TYPE */ 9, '?')) { - if (!$this->parser->getStream()->nextIf(/* Token::PUNCTUATION_TYPE */ 9, ':')) { + while ($this->parser->getStream()->nextIf(Token::PUNCTUATION_TYPE, '?')) { + if (!$this->parser->getStream()->nextIf(Token::PUNCTUATION_TYPE, ':')) { $expr2 = $this->parseExpression(); - if ($this->parser->getStream()->nextIf(/* Token::PUNCTUATION_TYPE */ 9, ':')) { + if ($this->parser->getStream()->nextIf(Token::PUNCTUATION_TYPE, ':')) { // Ternary operator (expr ? expr2 : expr3) $expr3 = $this->parseExpression(); } else { @@ -203,19 +203,19 @@ private function parseConditionalExpression($expr): AbstractExpression private function isUnary(Token $token): bool { - return $token->test(/* Token::OPERATOR_TYPE */ 8) && isset($this->unaryOperators[$token->getValue()]); + return $token->test(Token::OPERATOR_TYPE) && isset($this->unaryOperators[$token->getValue()]); } private function isBinary(Token $token): bool { - return $token->test(/* Token::OPERATOR_TYPE */ 8) && isset($this->binaryOperators[$token->getValue()]); + return $token->test(Token::OPERATOR_TYPE) && isset($this->binaryOperators[$token->getValue()]); } public function parsePrimaryExpression() { $token = $this->parser->getCurrentToken(); switch ($token->getType()) { - case /* Token::NAME_TYPE */ 5: + case Token::NAME_TYPE: $this->parser->getStream()->next(); switch ($token->getValue()) { case 'true': @@ -244,17 +244,17 @@ public function parsePrimaryExpression() } break; - case /* Token::NUMBER_TYPE */ 6: + case Token::NUMBER_TYPE: $this->parser->getStream()->next(); $node = new ConstantExpression($token->getValue(), $token->getLine()); break; - case /* Token::STRING_TYPE */ 7: + case Token::STRING_TYPE: case /* Token::INTERPOLATION_START_TYPE */ 10: $node = $this->parseStringExpression(); break; - case /* Token::OPERATOR_TYPE */ 8: + case Token::OPERATOR_TYPE: if (preg_match(Lexer::REGEX_NAME, $token->getValue(), $matches) && $matches[0] == $token->getValue()) { // in this context, string operators are variable names $this->parser->getStream()->next(); @@ -277,11 +277,11 @@ public function parsePrimaryExpression() // no break default: - if ($token->test(/* Token::PUNCTUATION_TYPE */ 9, '[')) { + if ($token->test(Token::PUNCTUATION_TYPE, '[')) { $node = $this->parseSequenceExpression(); - } elseif ($token->test(/* Token::PUNCTUATION_TYPE */ 9, '{')) { + } elseif ($token->test(Token::PUNCTUATION_TYPE, '{')) { $node = $this->parseMappingExpression(); - } elseif ($token->test(/* Token::OPERATOR_TYPE */ 8, '=') && ('==' === $this->parser->getStream()->look(-1)->getValue() || '!=' === $this->parser->getStream()->look(-1)->getValue())) { + } elseif ($token->test(Token::OPERATOR_TYPE, '=') && ('==' === $this->parser->getStream()->look(-1)->getValue() || '!=' === $this->parser->getStream()->look(-1)->getValue())) { throw new SyntaxError(\sprintf('Unexpected operator of value "%s". Did you try to use "===" or "!==" for strict comparison? Use "is same as(value)" instead.', $token->getValue()), $token->getLine(), $this->parser->getStream()->getSourceContext()); } else { throw new SyntaxError(\sprintf('Unexpected token "%s" of value "%s".', Token::typeToEnglish($token->getType()), $token->getValue()), $token->getLine(), $this->parser->getStream()->getSourceContext()); @@ -299,7 +299,7 @@ public function parseStringExpression() // a string cannot be followed by another string in a single expression $nextCanBeString = true; while (true) { - if ($nextCanBeString && $token = $stream->nextIf(/* Token::STRING_TYPE */ 7)) { + if ($nextCanBeString && $token = $stream->nextIf(Token::STRING_TYPE)) { $nodes[] = new ConstantExpression($token->getValue(), $token->getLine()); $nextCanBeString = false; } elseif ($stream->nextIf(/* Token::INTERPOLATION_START_TYPE */ 10)) { @@ -332,22 +332,22 @@ public function parseArrayExpression() public function parseSequenceExpression() { $stream = $this->parser->getStream(); - $stream->expect(/* Token::PUNCTUATION_TYPE */ 9, '[', 'A sequence element was expected'); + $stream->expect(Token::PUNCTUATION_TYPE, '[', 'A sequence element was expected'); $node = new ArrayExpression([], $stream->getCurrent()->getLine()); $first = true; - while (!$stream->test(/* Token::PUNCTUATION_TYPE */ 9, ']')) { + while (!$stream->test(Token::PUNCTUATION_TYPE, ']')) { if (!$first) { - $stream->expect(/* Token::PUNCTUATION_TYPE */ 9, ',', 'A sequence element must be followed by a comma'); + $stream->expect(Token::PUNCTUATION_TYPE, ',', 'A sequence element must be followed by a comma'); // trailing ,? - if ($stream->test(/* Token::PUNCTUATION_TYPE */ 9, ']')) { + if ($stream->test(Token::PUNCTUATION_TYPE, ']')) { break; } } $first = false; - if ($stream->test(/* Token::SPREAD_TYPE */ 13)) { + if ($stream->test(Token::SPREAD_TYPE)) { $stream->next(); $expr = $this->parseExpression(); $expr->setAttribute('spread', true); @@ -356,7 +356,7 @@ public function parseSequenceExpression() $node->addElement($this->parseExpression()); } } - $stream->expect(/* Token::PUNCTUATION_TYPE */ 9, ']', 'An opened sequence is not properly closed'); + $stream->expect(Token::PUNCTUATION_TYPE, ']', 'An opened sequence is not properly closed'); return $node; } @@ -374,22 +374,22 @@ public function parseHashExpression() public function parseMappingExpression() { $stream = $this->parser->getStream(); - $stream->expect(/* Token::PUNCTUATION_TYPE */ 9, '{', 'A mapping element was expected'); + $stream->expect(Token::PUNCTUATION_TYPE, '{', 'A mapping element was expected'); $node = new ArrayExpression([], $stream->getCurrent()->getLine()); $first = true; - while (!$stream->test(/* Token::PUNCTUATION_TYPE */ 9, '}')) { + while (!$stream->test(Token::PUNCTUATION_TYPE, '}')) { if (!$first) { - $stream->expect(/* Token::PUNCTUATION_TYPE */ 9, ',', 'A mapping value must be followed by a comma'); + $stream->expect(Token::PUNCTUATION_TYPE, ',', 'A mapping value must be followed by a comma'); // trailing ,? - if ($stream->test(/* Token::PUNCTUATION_TYPE */ 9, '}')) { + if ($stream->test(Token::PUNCTUATION_TYPE, '}')) { break; } } $first = false; - if ($stream->test(/* Token::SPREAD_TYPE */ 13)) { + if ($stream->test(Token::SPREAD_TYPE)) { $stream->next(); $value = $this->parseExpression(); $value->setAttribute('spread', true); @@ -403,7 +403,7 @@ public function parseMappingExpression() // * a string -- 'a' // * a name, which is equivalent to a string -- a // * an expression, which must be enclosed in parentheses -- (1 + 2) - if ($token = $stream->nextIf(/* Token::NAME_TYPE */ 5)) { + if ($token = $stream->nextIf(Token::NAME_TYPE)) { $key = new ConstantExpression($token->getValue(), $token->getLine()); // {a} is a shortcut for {a:a} @@ -412,9 +412,9 @@ public function parseMappingExpression() $node->addElement($value, $key); continue; } - } elseif (($token = $stream->nextIf(/* Token::STRING_TYPE */ 7)) || $token = $stream->nextIf(/* Token::NUMBER_TYPE */ 6)) { + } elseif (($token = $stream->nextIf(Token::STRING_TYPE)) || $token = $stream->nextIf(Token::NUMBER_TYPE)) { $key = new ConstantExpression($token->getValue(), $token->getLine()); - } elseif ($stream->test(/* Token::PUNCTUATION_TYPE */ 9, '(')) { + } elseif ($stream->test(Token::PUNCTUATION_TYPE, '(')) { $key = $this->parseExpression(); } else { $current = $stream->getCurrent(); @@ -422,12 +422,12 @@ public function parseMappingExpression() throw new SyntaxError(\sprintf('A mapping key must be a quoted string, a number, a name, or an expression enclosed in parentheses (unexpected token "%s" of value "%s".', Token::typeToEnglish($current->getType()), $current->getValue()), $current->getLine(), $stream->getSourceContext()); } - $stream->expect(/* Token::PUNCTUATION_TYPE */ 9, ':', 'A mapping key must be followed by a colon (:)'); + $stream->expect(Token::PUNCTUATION_TYPE, ':', 'A mapping key must be followed by a colon (:)'); $value = $this->parseExpression(); $node->addElement($value, $key); } - $stream->expect(/* Token::PUNCTUATION_TYPE */ 9, '}', 'An opened mapping is not properly closed'); + $stream->expect(Token::PUNCTUATION_TYPE, '}', 'An opened mapping is not properly closed'); return $node; } @@ -436,7 +436,7 @@ public function parsePostfixExpression($node) { while (true) { $token = $this->parser->getCurrentToken(); - if (/* Token::PUNCTUATION_TYPE */ 9 == $token->getType()) { + if (Token::PUNCTUATION_TYPE == $token->getType()) { if ('.' == $token->getValue() || '[' == $token->getValue()) { $node = $this->parseSubscriptExpression($node); } elseif ('|' == $token->getValue()) { @@ -510,15 +510,15 @@ public function parseSubscriptExpression($node) if ('.' == $token->getValue()) { $token = $stream->next(); if ( - /* Token::NAME_TYPE */ 5 == $token->getType() + Token::NAME_TYPE == $token->getType() || - /* Token::NUMBER_TYPE */ 6 == $token->getType() + Token::NUMBER_TYPE == $token->getType() || - (/* Token::OPERATOR_TYPE */ 8 == $token->getType() && preg_match(Lexer::REGEX_NAME, $token->getValue())) + (Token::OPERATOR_TYPE == $token->getType() && preg_match(Lexer::REGEX_NAME, $token->getValue())) ) { $arg = new ConstantExpression($token->getValue(), $lineno); - if ($stream->test(/* Token::PUNCTUATION_TYPE */ 9, '(')) { + if ($stream->test(Token::PUNCTUATION_TYPE, '(')) { $type = Template::METHOD_CALL; foreach ($this->parseArguments() as $n) { $arguments->addElement($n); @@ -541,19 +541,19 @@ public function parseSubscriptExpression($node) // slice? $slice = false; - if ($stream->test(/* Token::PUNCTUATION_TYPE */ 9, ':')) { + if ($stream->test(Token::PUNCTUATION_TYPE, ':')) { $slice = true; $arg = new ConstantExpression(0, $token->getLine()); } else { $arg = $this->parseExpression(); } - if ($stream->nextIf(/* Token::PUNCTUATION_TYPE */ 9, ':')) { + if ($stream->nextIf(Token::PUNCTUATION_TYPE, ':')) { $slice = true; } if ($slice) { - if ($stream->test(/* Token::PUNCTUATION_TYPE */ 9, ']')) { + if ($stream->test(Token::PUNCTUATION_TYPE, ']')) { $length = new ConstantExpression(null, $token->getLine()); } else { $length = $this->parseExpression(); @@ -563,12 +563,12 @@ public function parseSubscriptExpression($node) $arguments = new Node([$arg, $length]); $filter = new $class($node, new ConstantExpression('slice', $token->getLine()), $arguments, $token->getLine()); - $stream->expect(/* Token::PUNCTUATION_TYPE */ 9, ']'); + $stream->expect(Token::PUNCTUATION_TYPE, ']'); return $filter; } - $stream->expect(/* Token::PUNCTUATION_TYPE */ 9, ']'); + $stream->expect(Token::PUNCTUATION_TYPE, ']'); } return new GetAttrExpression($node, $arg, $arguments, $type, $lineno); @@ -584,10 +584,10 @@ public function parseFilterExpression($node) public function parseFilterExpressionRaw($node, $tag = null) { while (true) { - $token = $this->parser->getStream()->expect(/* Token::NAME_TYPE */ 5); + $token = $this->parser->getStream()->expect(Token::NAME_TYPE); $name = new ConstantExpression($token->getValue(), $token->getLine()); - if (!$this->parser->getStream()->test(/* Token::PUNCTUATION_TYPE */ 9, '(')) { + if (!$this->parser->getStream()->test(Token::PUNCTUATION_TYPE, '(')) { $arguments = new Node(); } else { $arguments = $this->parseArguments(true, false, true); @@ -597,7 +597,7 @@ public function parseFilterExpressionRaw($node, $tag = null) $node = new $class($node, $name, $arguments, $token->getLine(), $tag); - if (!$this->parser->getStream()->test(/* Token::PUNCTUATION_TYPE */ 9, '|')) { + if (!$this->parser->getStream()->test(Token::PUNCTUATION_TYPE, '|')) { break; } @@ -622,26 +622,26 @@ public function parseArguments($namedArguments = false, $definition = false, $al $args = []; $stream = $this->parser->getStream(); - $stream->expect(/* Token::PUNCTUATION_TYPE */ 9, '(', 'A list of arguments must begin with an opening parenthesis'); - while (!$stream->test(/* Token::PUNCTUATION_TYPE */ 9, ')')) { + $stream->expect(Token::PUNCTUATION_TYPE, '(', 'A list of arguments must begin with an opening parenthesis'); + while (!$stream->test(Token::PUNCTUATION_TYPE, ')')) { if (!empty($args)) { - $stream->expect(/* Token::PUNCTUATION_TYPE */ 9, ',', 'Arguments must be separated by a comma'); + $stream->expect(Token::PUNCTUATION_TYPE, ',', 'Arguments must be separated by a comma'); // if the comma above was a trailing comma, early exit the argument parse loop - if ($stream->test(/* Token::PUNCTUATION_TYPE */ 9, ')')) { + if ($stream->test(Token::PUNCTUATION_TYPE, ')')) { break; } } if ($definition) { - $token = $stream->expect(/* Token::NAME_TYPE */ 5, null, 'An argument must be a name'); + $token = $stream->expect(Token::NAME_TYPE, null, 'An argument must be a name'); $value = new NameExpression($token->getValue(), $this->parser->getCurrentToken()->getLine()); } else { $value = $this->parseExpression(0, $allowArrow); } $name = null; - if ($namedArguments && $token = $stream->nextIf(/* Token::OPERATOR_TYPE */ 8, '=')) { + if ($namedArguments && $token = $stream->nextIf(Token::OPERATOR_TYPE, '=')) { if (!$value instanceof NameExpression) { throw new SyntaxError(\sprintf('A parameter name must be a string, "%s" given.', \get_class($value)), $token->getLine(), $stream->getSourceContext()); } @@ -672,7 +672,7 @@ public function parseArguments($namedArguments = false, $definition = false, $al } } } - $stream->expect(/* Token::PUNCTUATION_TYPE */ 9, ')', 'A list of arguments must be closed by a parenthesis'); + $stream->expect(Token::PUNCTUATION_TYPE, ')', 'A list of arguments must be closed by a parenthesis'); return new Node($args); } @@ -683,11 +683,11 @@ public function parseAssignmentExpression() $targets = []; while (true) { $token = $this->parser->getCurrentToken(); - if ($stream->test(/* Token::OPERATOR_TYPE */ 8) && preg_match(Lexer::REGEX_NAME, $token->getValue())) { + if ($stream->test(Token::OPERATOR_TYPE) && preg_match(Lexer::REGEX_NAME, $token->getValue())) { // in this context, string operators are variable names $this->parser->getStream()->next(); } else { - $stream->expect(/* Token::NAME_TYPE */ 5, null, 'Only variables can be assigned to'); + $stream->expect(Token::NAME_TYPE, null, 'Only variables can be assigned to'); } $value = $token->getValue(); if (\in_array(strtr($value, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), ['true', 'false', 'none', 'null'])) { @@ -695,7 +695,7 @@ public function parseAssignmentExpression() } $targets[] = new AssignNameExpression($value, $token->getLine()); - if (!$stream->nextIf(/* Token::PUNCTUATION_TYPE */ 9, ',')) { + if (!$stream->nextIf(Token::PUNCTUATION_TYPE, ',')) { break; } } @@ -708,7 +708,7 @@ public function parseMultitargetExpression() $targets = []; while (true) { $targets[] = $this->parseExpression(); - if (!$this->parser->getStream()->nextIf(/* Token::PUNCTUATION_TYPE */ 9, ',')) { + if (!$this->parser->getStream()->nextIf(Token::PUNCTUATION_TYPE, ',')) { break; } } @@ -728,7 +728,7 @@ private function parseTestExpression(Node $node): TestExpression $class = $this->getTestNodeClass($test); $arguments = null; - if ($stream->test(/* Token::PUNCTUATION_TYPE */ 9, '(')) { + if ($stream->test(Token::PUNCTUATION_TYPE, '(')) { $arguments = $this->parseArguments(true); } elseif ($test->hasOneMandatoryArgument()) { $arguments = new Node([0 => $this->parsePrimaryExpression()]); @@ -745,13 +745,13 @@ private function parseTestExpression(Node $node): TestExpression private function getTest(int $line): array { $stream = $this->parser->getStream(); - $name = $stream->expect(/* Token::NAME_TYPE */ 5)->getValue(); + $name = $stream->expect(Token::NAME_TYPE)->getValue(); if ($test = $this->env->getTest($name)) { return [$name, $test]; } - if ($stream->test(/* Token::NAME_TYPE */ 5)) { + if ($stream->test(Token::NAME_TYPE)) { // try 2-words tests $name = $name.' '.$this->parser->getCurrentToken()->getValue(); diff --git a/src/Lexer.php b/src/Lexer.php index dec56d8d78f..4554b3d427f 100644 --- a/src/Lexer.php +++ b/src/Lexer.php @@ -229,7 +229,7 @@ private function lexData(): void { // if no matches are left we return the rest of the template as simple text token if ($this->position == \count($this->positions[0]) - 1) { - $this->pushToken(/* Token::TEXT_TYPE */ 0, substr($this->code, $this->cursor)); + $this->pushToken(Token::TEXT_TYPE, substr($this->code, $this->cursor)); $this->cursor = $this->end; return; @@ -258,7 +258,7 @@ private function lexData(): void $text = rtrim($text, " \t\0\x0B"); } } - $this->pushToken(/* Token::TEXT_TYPE */ 0, $text); + $this->pushToken(Token::TEXT_TYPE, $text); $this->moveCursor($textContent.$position[0]); switch ($this->positions[1][$this->position][0]) { @@ -335,12 +335,12 @@ private function lexExpression(): void } // operators elseif (preg_match($this->regexes['operator'], $this->code, $match, 0, $this->cursor)) { - $this->pushToken(/* Token::OPERATOR_TYPE */ 8, preg_replace('/\s+/', ' ', $match[0])); + $this->pushToken(Token::OPERATOR_TYPE, preg_replace('/\s+/', ' ', $match[0])); $this->moveCursor($match[0]); } // names elseif (preg_match(self::REGEX_NAME, $this->code, $match, 0, $this->cursor)) { - $this->pushToken(/* Token::NAME_TYPE */ 5, $match[0]); + $this->pushToken(Token::NAME_TYPE, $match[0]); $this->moveCursor($match[0]); } // numbers @@ -349,7 +349,7 @@ private function lexExpression(): void if (ctype_digit($match[0]) && $number <= \PHP_INT_MAX) { $number = (int) $match[0]; // integers lower than the maximum } - $this->pushToken(/* Token::NUMBER_TYPE */ 6, $number); + $this->pushToken(Token::NUMBER_TYPE, $number); $this->moveCursor($match[0]); } // punctuation @@ -370,12 +370,12 @@ private function lexExpression(): void } } - $this->pushToken(/* Token::PUNCTUATION_TYPE */ 9, $this->code[$this->cursor]); + $this->pushToken(Token::PUNCTUATION_TYPE, $this->code[$this->cursor]); ++$this->cursor; } // strings elseif (preg_match(self::REGEX_STRING, $this->code, $match, 0, $this->cursor)) { - $this->pushToken(/* Token::STRING_TYPE */ 7, $this->stripcslashes(substr($match[0], 1, -1), substr($match[0], 0, 1))); + $this->pushToken(Token::STRING_TYPE, $this->stripcslashes(substr($match[0], 1, -1), substr($match[0], 0, 1))); $this->moveCursor($match[0]); } // opening double quoted string @@ -468,7 +468,7 @@ private function lexRawData(): void } } - $this->pushToken(/* Token::TEXT_TYPE */ 0, $text); + $this->pushToken(Token::TEXT_TYPE, $text); } private function lexComment(): void @@ -488,7 +488,7 @@ private function lexString(): void $this->moveCursor($match[0]); $this->pushState(self::STATE_INTERPOLATION); } elseif (preg_match(self::REGEX_DQ_STRING_PART, $this->code, $match, 0, $this->cursor) && '' !== $match[0]) { - $this->pushToken(/* Token::STRING_TYPE */ 7, $this->stripcslashes($match[0], '"')); + $this->pushToken(Token::STRING_TYPE, $this->stripcslashes($match[0], '"')); $this->moveCursor($match[0]); } elseif (preg_match(self::REGEX_DQ_STRING_DELIM, $this->code, $match, 0, $this->cursor)) { [$expect, $lineno] = array_pop($this->brackets); @@ -520,7 +520,7 @@ private function lexInterpolation(): void private function pushToken($type, $value = ''): void { // do not push empty text tokens - if (/* Token::TEXT_TYPE */ 0 === $type && '' === $value) { + if (Token::TEXT_TYPE === $type && '' === $value) { return; } diff --git a/src/Parser.php b/src/Parser.php index 7cfc2058c01..ff163ba7cc3 100644 --- a/src/Parser.php +++ b/src/Parser.php @@ -121,7 +121,7 @@ public function subparse($test, bool $dropNeedle = false): Node $rv = []; while (!$this->stream->isEOF()) { switch ($this->getCurrentToken()->getType()) { - case /* Token::TEXT_TYPE */ 0: + case Token::TEXT_TYPE: $token = $this->stream->next(); $rv[] = new TextNode($token->getValue(), $token->getLine()); break; @@ -137,7 +137,7 @@ public function subparse($test, bool $dropNeedle = false): Node $this->stream->next(); $token = $this->getCurrentToken(); - if (/* Token::NAME_TYPE */ 5 !== $token->getType()) { + if (Token::NAME_TYPE !== $token->getType()) { throw new SyntaxError('A block must start with a tag name.', $token->getLine(), $this->stream->getSourceContext()); } diff --git a/src/TokenParser/BlockTokenParser.php b/src/TokenParser/BlockTokenParser.php index c654d31f940..c9286927724 100644 --- a/src/TokenParser/BlockTokenParser.php +++ b/src/TokenParser/BlockTokenParser.php @@ -35,7 +35,7 @@ public function parse(Token $token): Node { $lineno = $token->getLine(); $stream = $this->parser->getStream(); - $name = $stream->expect(/* Token::NAME_TYPE */ 5)->getValue(); + $name = $stream->expect(Token::NAME_TYPE)->getValue(); if ($this->parser->hasBlock($name)) { throw new SyntaxError(\sprintf("The block '%s' has already been defined line %d.", $name, $this->parser->getBlock($name)->getTemplateLine()), $stream->getCurrent()->getLine(), $stream->getSourceContext()); } @@ -45,7 +45,7 @@ public function parse(Token $token): Node if ($stream->nextIf(/* Token::BLOCK_END_TYPE */ 3)) { $body = $this->parser->subparse([$this, 'decideBlockEnd'], true); - if ($token = $stream->nextIf(/* Token::NAME_TYPE */ 5)) { + if ($token = $stream->nextIf(Token::NAME_TYPE)) { $value = $token->getValue(); if ($value != $name) { diff --git a/src/TokenParser/EmbedTokenParser.php b/src/TokenParser/EmbedTokenParser.php index adf683cc19e..0f840ce1d5c 100644 --- a/src/TokenParser/EmbedTokenParser.php +++ b/src/TokenParser/EmbedTokenParser.php @@ -32,17 +32,17 @@ public function parse(Token $token): Node [$variables, $only, $ignoreMissing] = $this->parseArguments(); - $parentToken = $fakeParentToken = new Token(/* Token::STRING_TYPE */ 7, '__parent__', $token->getLine()); + $parentToken = $fakeParentToken = new Token(Token::STRING_TYPE, '__parent__', $token->getLine()); if ($parent instanceof ConstantExpression) { - $parentToken = new Token(/* Token::STRING_TYPE */ 7, $parent->getAttribute('value'), $token->getLine()); + $parentToken = new Token(Token::STRING_TYPE, $parent->getAttribute('value'), $token->getLine()); } elseif ($parent instanceof NameExpression) { - $parentToken = new Token(/* Token::NAME_TYPE */ 5, $parent->getAttribute('name'), $token->getLine()); + $parentToken = new Token(Token::NAME_TYPE, $parent->getAttribute('name'), $token->getLine()); } // inject a fake parent to make the parent() function work $stream->injectTokens([ new Token(/* Token::BLOCK_START_TYPE */ 1, '', $token->getLine()), - new Token(/* Token::NAME_TYPE */ 5, 'extends', $token->getLine()), + new Token(Token::NAME_TYPE, 'extends', $token->getLine()), $parentToken, new Token(/* Token::BLOCK_END_TYPE */ 3, '', $token->getLine()), ]); diff --git a/src/TokenParser/ForTokenParser.php b/src/TokenParser/ForTokenParser.php index 1af6da8fde5..67c8edf2ae0 100644 --- a/src/TokenParser/ForTokenParser.php +++ b/src/TokenParser/ForTokenParser.php @@ -35,7 +35,7 @@ public function parse(Token $token): Node $lineno = $token->getLine(); $stream = $this->parser->getStream(); $targets = $this->parser->getExpressionParser()->parseAssignmentExpression(); - $stream->expect(/* Token::OPERATOR_TYPE */ 8, 'in'); + $stream->expect(Token::OPERATOR_TYPE, 'in'); $seq = $this->parser->getExpressionParser()->parseExpression(); $stream->expect(/* Token::BLOCK_END_TYPE */ 3); diff --git a/src/TokenParser/FromTokenParser.php b/src/TokenParser/FromTokenParser.php index 31b6cde4148..2793a5b9745 100644 --- a/src/TokenParser/FromTokenParser.php +++ b/src/TokenParser/FromTokenParser.php @@ -29,20 +29,20 @@ public function parse(Token $token): Node { $macro = $this->parser->getExpressionParser()->parseExpression(); $stream = $this->parser->getStream(); - $stream->expect(/* Token::NAME_TYPE */ 5, 'import'); + $stream->expect(Token::NAME_TYPE, 'import'); $targets = []; while (true) { - $name = $stream->expect(/* Token::NAME_TYPE */ 5)->getValue(); + $name = $stream->expect(Token::NAME_TYPE)->getValue(); $alias = $name; if ($stream->nextIf('as')) { - $alias = $stream->expect(/* Token::NAME_TYPE */ 5)->getValue(); + $alias = $stream->expect(Token::NAME_TYPE)->getValue(); } $targets[$name] = $alias; - if (!$stream->nextIf(/* Token::PUNCTUATION_TYPE */ 9, ',')) { + if (!$stream->nextIf(Token::PUNCTUATION_TYPE, ',')) { break; } } diff --git a/src/TokenParser/ImportTokenParser.php b/src/TokenParser/ImportTokenParser.php index 44cb4dad79d..2e54c454254 100644 --- a/src/TokenParser/ImportTokenParser.php +++ b/src/TokenParser/ImportTokenParser.php @@ -28,8 +28,8 @@ final class ImportTokenParser extends AbstractTokenParser public function parse(Token $token): Node { $macro = $this->parser->getExpressionParser()->parseExpression(); - $this->parser->getStream()->expect(/* Token::NAME_TYPE */ 5, 'as'); - $var = new AssignNameExpression($this->parser->getStream()->expect(/* Token::NAME_TYPE */ 5)->getValue(), $token->getLine()); + $this->parser->getStream()->expect(Token::NAME_TYPE, 'as'); + $var = new AssignNameExpression($this->parser->getStream()->expect(Token::NAME_TYPE)->getValue(), $token->getLine()); $this->parser->getStream()->expect(/* Token::BLOCK_END_TYPE */ 3); $this->parser->addImportedSymbol('template', $var->getAttribute('name')); diff --git a/src/TokenParser/IncludeTokenParser.php b/src/TokenParser/IncludeTokenParser.php index fda5bfd8c0a..9c571719a9d 100644 --- a/src/TokenParser/IncludeTokenParser.php +++ b/src/TokenParser/IncludeTokenParser.php @@ -41,19 +41,19 @@ protected function parseArguments() $stream = $this->parser->getStream(); $ignoreMissing = false; - if ($stream->nextIf(/* Token::NAME_TYPE */ 5, 'ignore')) { - $stream->expect(/* Token::NAME_TYPE */ 5, 'missing'); + if ($stream->nextIf(Token::NAME_TYPE, 'ignore')) { + $stream->expect(Token::NAME_TYPE, 'missing'); $ignoreMissing = true; } $variables = null; - if ($stream->nextIf(/* Token::NAME_TYPE */ 5, 'with')) { + if ($stream->nextIf(Token::NAME_TYPE, 'with')) { $variables = $this->parser->getExpressionParser()->parseExpression(); } $only = false; - if ($stream->nextIf(/* Token::NAME_TYPE */ 5, 'only')) { + if ($stream->nextIf(Token::NAME_TYPE, 'only')) { $only = true; } diff --git a/src/TokenParser/MacroTokenParser.php b/src/TokenParser/MacroTokenParser.php index 1f0e3e97fd1..b11590010c5 100644 --- a/src/TokenParser/MacroTokenParser.php +++ b/src/TokenParser/MacroTokenParser.php @@ -32,14 +32,14 @@ public function parse(Token $token): Node { $lineno = $token->getLine(); $stream = $this->parser->getStream(); - $name = $stream->expect(/* Token::NAME_TYPE */ 5)->getValue(); + $name = $stream->expect(Token::NAME_TYPE)->getValue(); $arguments = $this->parser->getExpressionParser()->parseArguments(true, true); $stream->expect(/* Token::BLOCK_END_TYPE */ 3); $this->parser->pushLocalScope(); $body = $this->parser->subparse([$this, 'decideBlockEnd'], true); - if ($token = $stream->nextIf(/* Token::NAME_TYPE */ 5)) { + if ($token = $stream->nextIf(Token::NAME_TYPE)) { $value = $token->getValue(); if ($value != $name) { diff --git a/src/TokenParser/SetTokenParser.php b/src/TokenParser/SetTokenParser.php index 2fbdfe0901f..8c75aa55d7d 100644 --- a/src/TokenParser/SetTokenParser.php +++ b/src/TokenParser/SetTokenParser.php @@ -37,7 +37,7 @@ public function parse(Token $token): Node $names = $this->parser->getExpressionParser()->parseAssignmentExpression(); $capture = false; - if ($stream->nextIf(/* Token::OPERATOR_TYPE */ 8, '=')) { + if ($stream->nextIf(Token::OPERATOR_TYPE, '=')) { $values = $this->parser->getExpressionParser()->parseMultitargetExpression(); $stream->expect(/* Token::BLOCK_END_TYPE */ 3); diff --git a/src/TokenParser/UseTokenParser.php b/src/TokenParser/UseTokenParser.php index 3cdbb98ad01..10714e7191d 100644 --- a/src/TokenParser/UseTokenParser.php +++ b/src/TokenParser/UseTokenParser.php @@ -44,16 +44,16 @@ public function parse(Token $token): Node $targets = []; if ($stream->nextIf('with')) { while (true) { - $name = $stream->expect(/* Token::NAME_TYPE */ 5)->getValue(); + $name = $stream->expect(Token::NAME_TYPE)->getValue(); $alias = $name; if ($stream->nextIf('as')) { - $alias = $stream->expect(/* Token::NAME_TYPE */ 5)->getValue(); + $alias = $stream->expect(Token::NAME_TYPE)->getValue(); } $targets[$name] = new ConstantExpression($alias, -1); - if (!$stream->nextIf(/* Token::PUNCTUATION_TYPE */ 9, ',')) { + if (!$stream->nextIf(Token::PUNCTUATION_TYPE, ',')) { break; } } diff --git a/src/TokenParser/WithTokenParser.php b/src/TokenParser/WithTokenParser.php index 7d8cbe26165..0da6ef99f77 100644 --- a/src/TokenParser/WithTokenParser.php +++ b/src/TokenParser/WithTokenParser.php @@ -32,7 +32,7 @@ public function parse(Token $token): Node $only = false; if (!$stream->test(/* Token::BLOCK_END_TYPE */ 3)) { $variables = $this->parser->getExpressionParser()->parseExpression(); - $only = (bool) $stream->nextIf(/* Token::NAME_TYPE */ 5, 'only'); + $only = (bool) $stream->nextIf(Token::NAME_TYPE, 'only'); } $stream->expect(/* Token::BLOCK_END_TYPE */ 3); From 148d3e079da90cf970e014a91354da96560a0d2d Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sun, 11 Aug 2024 14:21:57 +0200 Subject: [PATCH 340/812] Make various optimization for dynamic Twig callables --- src/AbstractTwigCallable.php | 23 ++++++- src/ExpressionParser.php | 63 +++++++++---------- src/ExtensionSet.php | 55 +++++++++------- src/Node/Expression/CallExpression.php | 2 +- src/Node/Expression/FilterExpression.php | 1 + src/Node/Expression/FunctionExpression.php | 1 + src/Node/Expression/TestExpression.php | 1 + tests/ExpressionParserTest.php | 49 +++++++++++++++ tests/Fixtures/filters/dynamic_filter.test | 2 + .../Fixtures/functions/dynamic_function.test | 2 + 10 files changed, 138 insertions(+), 61 deletions(-) diff --git a/src/AbstractTwigCallable.php b/src/AbstractTwigCallable.php index b4ff5956512..f5739529bf6 100644 --- a/src/AbstractTwigCallable.php +++ b/src/AbstractTwigCallable.php @@ -19,12 +19,13 @@ abstract class AbstractTwigCallable implements TwigCallableInterface protected $options; private $name; + private $dynamicName; private $callable; private $arguments; public function __construct(string $name, $callable = null, array $options = []) { - $this->name = $name; + $this->name = $this->dynamicName = $name; $this->callable = $callable; $this->arguments = []; $this->options = array_merge([ @@ -43,6 +44,11 @@ public function getName(): string return $this->name; } + public function getDynamicName(): string + { + return $this->dynamicName; + } + public function getCallable() { return $this->callable; @@ -68,8 +74,23 @@ public function needsContext(): bool return $this->options['needs_context']; } + public function withDynamicArguments(string $name, string $dynamicName, array $arguments): self + { + $new = clone $this; + $new->name = $name; + $new->dynamicName = $dynamicName; + $new->arguments = $arguments; + + return $new; + } + + /** + * @deprecated since Twig 3.12, use withDynamicArguments() instead + */ public function setArguments(array $arguments): void { + trigger_deprecation('twig/twig', '3.12', 'The "%s::setArguments()" method is deprecated, use "%s::withDynamicArguments()" instead.', static::class, static::class); + $this->arguments = $arguments; } diff --git a/src/ExpressionParser.php b/src/ExpressionParser.php index 56a749efefd..95f4e0f5706 100644 --- a/src/ExpressionParser.php +++ b/src/ExpressionParser.php @@ -494,9 +494,9 @@ public function getFunctionNode($name, $line) } $args = $this->parseArguments(true); - $class = $this->getFunctionNodeClass($name, $line); + $function = $this->getFunction($name, $line); - return new $class($name, $args, $line); + return new ($function->getNodeClass())($function->getName(), $args, $line); } } @@ -559,9 +559,9 @@ public function parseSubscriptExpression($node) $length = $this->parseExpression(); } - $class = $this->getFilterNodeClass('slice', $token->getLine()); + $filter = $this->getFilter('slice', $token->getLine()); $arguments = new Node([$arg, $length]); - $filter = new $class($node, new ConstantExpression('slice', $token->getLine()), $arguments, $token->getLine()); + $filter = new ($filter->getNodeClass())($node, new ConstantExpression('slice', $token->getLine()), $arguments, $token->getLine()); $stream->expect(Token::PUNCTUATION_TYPE, ']'); @@ -586,16 +586,15 @@ public function parseFilterExpressionRaw($node, $tag = null) while (true) { $token = $this->parser->getStream()->expect(Token::NAME_TYPE); - $name = new ConstantExpression($token->getValue(), $token->getLine()); if (!$this->parser->getStream()->test(Token::PUNCTUATION_TYPE, '(')) { $arguments = new Node(); } else { $arguments = $this->parseArguments(true, false, true); } - $class = $this->getFilterNodeClass($name->getAttribute('value'), $token->getLine()); - - $node = new $class($node, $name, $arguments, $token->getLine(), $tag); + $filter = $this->getFilter($token->getValue(), $token->getLine()); + $name = new ConstantExpression($filter->getName(), $token->getLine()); + $node = new ($filter->getNodeClass())($node, $name, $arguments, $token->getLine(), $tag); if (!$this->parser->getStream()->test(Token::PUNCTUATION_TYPE, '|')) { break; @@ -724,9 +723,8 @@ private function parseNotTestExpression(Node $node): NotUnary private function parseTestExpression(Node $node): TestExpression { $stream = $this->parser->getStream(); - [$name, $test] = $this->getTest($node->getTemplateLine()); + $test = $this->getTest($node->getTemplateLine()); - $class = $this->getTestNodeClass($test); $arguments = null; if ($stream->test(Token::PUNCTUATION_TYPE, '(')) { $arguments = $this->parseArguments(true); @@ -734,42 +732,37 @@ private function parseTestExpression(Node $node): TestExpression $arguments = new Node([0 => $this->parsePrimaryExpression()]); } - if ('defined' === $name && $node instanceof NameExpression && null !== $alias = $this->parser->getImportedSymbol('function', $node->getAttribute('name'))) { + if ('defined' === $test->getName() && $node instanceof NameExpression && null !== $alias = $this->parser->getImportedSymbol('function', $node->getAttribute('name'))) { $node = new MethodCallExpression($alias['node'], $alias['name'], new ArrayExpression([], $node->getTemplateLine()), $node->getTemplateLine()); $node->setAttribute('safe', true); } - return new $class($node, $name, $arguments, $this->parser->getCurrentToken()->getLine()); + return new ($test->getNodeClass())($node, $test->getName(), $arguments, $this->parser->getCurrentToken()->getLine()); } - private function getTest(int $line): array + private function getTest(int $line): TwigTest { $stream = $this->parser->getStream(); $name = $stream->expect(Token::NAME_TYPE)->getValue(); - if ($test = $this->env->getTest($name)) { - return [$name, $test]; - } - - if ($stream->test(Token::NAME_TYPE)) { - // try 2-words tests - $name = $name.' '.$this->parser->getCurrentToken()->getValue(); - - if ($test = $this->env->getTest($name)) { - $stream->next(); + if (!$test = $this->env->getTest($name)) { + if ($stream->test(/* Token::NAME_TYPE */ 5)) { + // try 2-words tests + $name = $name.' '.$this->parser->getCurrentToken()->getValue(); - return [$name, $test]; + if ($test = $this->env->getTest($name)) { + $stream->next(); + } } } - $e = new SyntaxError(\sprintf('Unknown "%s" test.', $name), $line, $stream->getSourceContext()); - $e->addSuggestions($name, array_keys($this->env->getTests())); + if (!$test) { + $e = new SyntaxError(\sprintf('Unknown "%s" test.', $name), $line, $stream->getSourceContext()); + $e->addSuggestions($name, array_keys($this->env->getTests())); - throw $e; - } + throw $e; + } - private function getTestNodeClass(TwigTest $test): string - { if ($test->isDeprecated()) { $stream = $this->parser->getStream(); $message = \sprintf('Twig Test "%s" is deprecated', $test->getName()); @@ -783,10 +776,10 @@ private function getTestNodeClass(TwigTest $test): string trigger_deprecation($test->getDeprecatingPackage(), $test->getDeprecatedVersion(), $message); } - return $test->getNodeClass(); + return $test; } - private function getFunctionNodeClass(string $name, int $line): string + private function getFunction(string $name, int $line): TwigFunction { if (!$function = $this->env->getFunction($name)) { $e = new SyntaxError(\sprintf('Unknown "%s" function.', $name), $line, $this->parser->getStream()->getSourceContext()); @@ -806,10 +799,10 @@ private function getFunctionNodeClass(string $name, int $line): string trigger_deprecation($function->getDeprecatingPackage(), $function->getDeprecatedVersion(), $message); } - return $function->getNodeClass(); + return $function; } - private function getFilterNodeClass(string $name, int $line): string + private function getFilter(string $name, int $line): TwigFilter { if (!$filter = $this->env->getFilter($name)) { $e = new SyntaxError(\sprintf('Unknown "%s" filter.', $name), $line, $this->parser->getStream()->getSourceContext()); @@ -829,7 +822,7 @@ private function getFilterNodeClass(string $name, int $line): string trigger_deprecation($filter->getDeprecatingPackage(), $filter->getDeprecatedVersion(), $message); } - return $filter->getNodeClass(); + return $filter; } // checks that the node only contains "constant" elements diff --git a/src/ExtensionSet.php b/src/ExtensionSet.php index 8b59a13e186..32377b0fc7f 100644 --- a/src/ExtensionSet.php +++ b/src/ExtensionSet.php @@ -36,10 +36,16 @@ final class ExtensionSet private $visitors; /** @var array */ private $filters; + /** @var array */ + private $dynamicFilters; /** @var array */ private $tests; + /** @var array */ + private $dynamicTests; /** @var array */ private $functions; + /** @var array */ + private $dynamicFunctions; /** @var array}> */ private $unaryOperators; /** @var array, associativity: ExpressionParser::OPERATOR_*}> */ @@ -167,14 +173,11 @@ public function getFunction(string $name): ?TwigFunction return $this->functions[$name]; } - foreach ($this->functions as $pattern => $function) { - $pattern = str_replace('\\*', '(.*?)', preg_quote($pattern, '#'), $count); - - if ($count && preg_match('#^'.$pattern.'$#', $name, $matches)) { + foreach ($this->dynamicFunctions as $pattern => $function) { + if (preg_match($pattern, $name, $matches)) { array_shift($matches); - $function->setArguments($matches); - return $function; + return $function->withDynamicArguments($name, $function->getName(), $matches); } } @@ -223,14 +226,11 @@ public function getFilter(string $name): ?TwigFilter return $this->filters[$name]; } - foreach ($this->filters as $pattern => $filter) { - $pattern = str_replace('\\*', '(.*?)', preg_quote($pattern, '#'), $count); - - if ($count && preg_match('#^'.$pattern.'$#', $name, $matches)) { + foreach ($this->dynamicFilters as $pattern => $filter) { + if (preg_match($pattern, $name, $matches)) { array_shift($matches); - $filter->setArguments($matches); - return $filter; + return $filter->withDynamicArguments($name, $filter->getName(), $matches); } } @@ -375,16 +375,11 @@ public function getTest(string $name): ?TwigTest return $this->tests[$name]; } - foreach ($this->tests as $pattern => $test) { - $pattern = str_replace('\\*', '(.*?)', preg_quote($pattern, '#'), $count); - - if ($count) { - if (preg_match('#^'.$pattern.'$#', $name, $matches)) { - array_shift($matches); - $test->setArguments($matches); + foreach ($this->dynamicTests as $pattern => $test) { + if (preg_match($pattern, $name, $matches)) { + array_shift($matches); - return $test; - } + return $test->withDynamicArguments($name, $test->getName(), $matches); } } @@ -421,6 +416,9 @@ private function initExtensions(): void $this->filters = []; $this->functions = []; $this->tests = []; + $this->dynamicFilters = []; + $this->dynamicFunctions = []; + $this->dynamicTests = []; $this->visitors = []; $this->unaryOperators = []; $this->binaryOperators = []; @@ -437,17 +435,26 @@ private function initExtension(ExtensionInterface $extension): void { // filters foreach ($extension->getFilters() as $filter) { - $this->filters[$filter->getName()] = $filter; + $this->filters[$name = $filter->getName()] = $filter; + if (str_contains($name, '*')) { + $this->dynamicFilters['#^'.str_replace('\\*', '(.*?)', preg_quote($name, '#')).'$#'] = $filter; + } } // functions foreach ($extension->getFunctions() as $function) { - $this->functions[$function->getName()] = $function; + $this->functions[$name = $function->getName()] = $function; + if (str_contains($name, '*')) { + $this->dynamicFunctions['#^'.str_replace('\\*', '(.*?)', preg_quote($name, '#')).'$#'] = $function; + } } // tests foreach ($extension->getTests() as $test) { - $this->tests[$test->getName()] = $test; + $this->tests[$name = $test->getName()] = $test; + if (str_contains($name, '*')) { + $this->dynamicTests['#^'.str_replace('\\*', '(.*?)', preg_quote($name, '#')).'$#'] = $test; + } } // token parsers diff --git a/src/Node/Expression/CallExpression.php b/src/Node/Expression/CallExpression.php index cd81df47a38..1ef11cea8cd 100644 --- a/src/Node/Expression/CallExpression.php +++ b/src/Node/Expression/CallExpression.php @@ -51,7 +51,7 @@ protected function compileCallable(Compiler $compiler) $compiler->raw(\sprintf('->%s', $callable[1])); } else { - $compiler->raw(\sprintf('$this->env->get%s(\'%s\')->getCallable()', ucfirst($this->getAttribute('type')), $this->getAttribute('name'))); + $compiler->raw(\sprintf('$this->env->get%s(\'%s\')->getCallable()', ucfirst($this->getAttribute('type')), $this->getAttribute('dynamic_name'))); } } diff --git a/src/Node/Expression/FilterExpression.php b/src/Node/Expression/FilterExpression.php index 251870ae552..d410ede53ee 100644 --- a/src/Node/Expression/FilterExpression.php +++ b/src/Node/Expression/FilterExpression.php @@ -44,6 +44,7 @@ public function compile(Compiler $compiler): void $this->setAttribute('arguments', $filter->getArguments()); $this->setAttribute('callable', $filter->getCallable()); $this->setAttribute('is_variadic', $filter->isVariadic()); + $this->setAttribute('dynamic_name', $filter->getDynamicName()); $this->compileCallable($compiler); } diff --git a/src/Node/Expression/FunctionExpression.php b/src/Node/Expression/FunctionExpression.php index ef99c401a94..b7136f6f7a5 100644 --- a/src/Node/Expression/FunctionExpression.php +++ b/src/Node/Expression/FunctionExpression.php @@ -37,6 +37,7 @@ public function compile(Compiler $compiler) } $this->setAttribute('callable', $callable); $this->setAttribute('is_variadic', $function->isVariadic()); + $this->setAttribute('dynamic_name', $function->getDynamicName()); $this->compileCallable($compiler); } diff --git a/src/Node/Expression/TestExpression.php b/src/Node/Expression/TestExpression.php index 29c5a522cd8..6bf6c691bd8 100644 --- a/src/Node/Expression/TestExpression.php +++ b/src/Node/Expression/TestExpression.php @@ -33,6 +33,7 @@ public function compile(Compiler $compiler): void $this->setAttribute('arguments', $test->getArguments()); $this->setAttribute('callable', $test->getCallable()); $this->setAttribute('is_variadic', $test->isVariadic()); + $this->setAttribute('dynamic_name', $test->getDynamicName()); $this->compileCallable($compiler); } diff --git a/tests/ExpressionParserTest.php b/tests/ExpressionParserTest.php index b1e3d5017bd..17524b2457f 100644 --- a/tests/ExpressionParserTest.php +++ b/tests/ExpressionParserTest.php @@ -14,6 +14,7 @@ use PHPUnit\Framework\TestCase; use Twig\Environment; use Twig\Error\SyntaxError; +use Twig\Extension\AbstractExtension; use Twig\Loader\ArrayLoader; use Twig\Node\Expression\ArrayExpression; use Twig\Node\Expression\Binary\ConcatBinary; @@ -21,6 +22,9 @@ use Twig\Node\Expression\NameExpression; use Twig\Parser; use Twig\Source; +use Twig\TwigFilter; +use Twig\TwigFunction; +use Twig\TwigTest; class ExpressionParserTest extends TestCase { @@ -411,6 +415,51 @@ public function testUnknownTestWithoutSuggestions() $parser->parse($env->tokenize(new Source('{{ 1 is foobar }}', 'index'))); } + public function testCompiledCodeForDynamicTest() + { + $env = new Environment(new ArrayLoader(['index' => '{{ "a" is foo_foo_bar_bar }}']), ['cache' => false, 'autoescape' => false]); + $env->addExtension(new class() extends AbstractExtension { + public function getTests() + { + return [ + new TwigTest('*_foo_*_bar', function ($foo, $bar, $a) {}), + ]; + } + }); + + $this->assertStringContainsString('$this->env->getTest(\'*_foo_*_bar\')->getCallable()("foo", "bar", "a")', $env->compile($env->parse($env->tokenize(new Source($env->getLoader()->getSourceContext('index')->getCode(), 'index'))))); + } + + public function testCompiledCodeForDynamicFunction() + { + $env = new Environment(new ArrayLoader(['index' => '{{ foo_foo_bar_bar("a") }}']), ['cache' => false, 'autoescape' => false]); + $env->addExtension(new class() extends AbstractExtension { + public function getFunctions() + { + return [ + new TwigFunction('*_foo_*_bar', function ($foo, $bar, $a) {}), + ]; + } + }); + + $this->assertStringContainsString('$this->env->getFunction(\'*_foo_*_bar\')->getCallable()("foo", "bar", "a")', $env->compile($env->parse($env->tokenize(new Source($env->getLoader()->getSourceContext('index')->getCode(), 'index'))))); + } + + public function testCompiledCodeForDynamicFilter() + { + $env = new Environment(new ArrayLoader(['index' => '{{ "a"|foo_foo_bar_bar }}']), ['cache' => false, 'autoescape' => false]); + $env->addExtension(new class() extends AbstractExtension { + public function getFilters() + { + return [ + new TwigFilter('*_foo_*_bar', function ($foo, $bar, $a) {}), + ]; + } + }); + + $this->assertStringContainsString('$this->env->getFilter(\'*_foo_*_bar\')->getCallable()("foo", "bar", "a")', $env->compile($env->parse($env->tokenize(new Source($env->getLoader()->getSourceContext('index')->getCode(), 'index'))))); + } + private function createNameExpression(string $name, array $attributes) { $expression = new NameExpression($name, 1); diff --git a/tests/Fixtures/filters/dynamic_filter.test b/tests/Fixtures/filters/dynamic_filter.test index 27dc8784c6b..15c47814d2f 100644 --- a/tests/Fixtures/filters/dynamic_filter.test +++ b/tests/Fixtures/filters/dynamic_filter.test @@ -2,9 +2,11 @@ dynamic filter --TEMPLATE-- {{ 'bar'|foo_path }} +{{ 'bar'|bar_path }} {{ 'bar'|a_foo_b_bar }} --DATA-- return [] --EXPECT-- foo/bar +bar/bar a/b/bar diff --git a/tests/Fixtures/functions/dynamic_function.test b/tests/Fixtures/functions/dynamic_function.test index c7b3539c402..ea851b6dc0a 100644 --- a/tests/Fixtures/functions/dynamic_function.test +++ b/tests/Fixtures/functions/dynamic_function.test @@ -2,9 +2,11 @@ dynamic function --TEMPLATE-- {{ foo_path('bar') }} +{{ bar_path('bar') }} {{ a_foo_b_bar('bar') }} --DATA-- return [] --EXPECT-- foo/bar +bar/bar a/b/bar From 1da763408b3cf540d7d62b6734676f1566906d69 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Mon, 12 Aug 2024 10:49:48 +0200 Subject: [PATCH 341/812] Simplify code --- src/Node/Expression/Filter/RawFilter.php | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/Node/Expression/Filter/RawFilter.php b/src/Node/Expression/Filter/RawFilter.php index 584942306f2..6502f68ef57 100644 --- a/src/Node/Expression/Filter/RawFilter.php +++ b/src/Node/Expression/Filter/RawFilter.php @@ -23,14 +23,7 @@ class RawFilter extends FilterExpression { public function __construct(Node $node, ?ConstantExpression $filterName = null, ?Node $arguments = null, int $lineno = 0, ?string $tag = null) { - if (null === $filterName) { - $filterName = new ConstantExpression('raw', $node->getTemplateLine()); - } - if (null === $arguments) { - $arguments = new Node(); - } - - parent::__construct($node, $filterName, $arguments, $lineno ?: $node->getTemplateLine(), $tag ?: $node->getNodeTag()); + parent::__construct($node, $filterName ?: new ConstantExpression('raw', $node->getTemplateLine()), $arguments ?: new Node(), $lineno ?: $node->getTemplateLine(), $tag ?: $node->getNodeTag()); } public function compile(Compiler $compiler): void From 21df1ad7824ced2abcbd33863f04c6636674481f Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Mon, 12 Aug 2024 11:03:48 +0200 Subject: [PATCH 342/812] Fix TwigCallableInterface --- src/TwigCallableInterface.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/TwigCallableInterface.php b/src/TwigCallableInterface.php index ddce193a23d..d4abc9f4206 100644 --- a/src/TwigCallableInterface.php +++ b/src/TwigCallableInterface.php @@ -31,7 +31,7 @@ public function needsEnvironment(): bool; public function needsContext(): bool; - public function setArguments(array $arguments): void; + public function withDynamicArguments(string $name, string $dynamicName, array $arguments): self; public function getArguments(): array; From d3ab783595c0e3ba580666d528ec5ca571dbafc6 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Tue, 13 Aug 2024 11:28:14 +0200 Subject: [PATCH 343/812] Add missing method in TwigCallableInterface --- src/TwigCallableInterface.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/TwigCallableInterface.php b/src/TwigCallableInterface.php index d4abc9f4206..7e5021242cb 100644 --- a/src/TwigCallableInterface.php +++ b/src/TwigCallableInterface.php @@ -18,6 +18,8 @@ interface TwigCallableInterface { public function getName(): string; + public function getDynamicName(): string; + /** * @return callable|array{class-string, string}|null */ From 6ecd1f13505db9c05da0ca8a0cde7a15b8b69617 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Tue, 13 Aug 2024 22:11:33 +0200 Subject: [PATCH 344/812] Cleanup the implementation of the defined test for constants --- src/Extension/CoreExtension.php | 37 +++++++--------------- src/Node/Expression/FunctionExpression.php | 11 +++---- src/Resources/core.php | 2 +- 3 files changed, 17 insertions(+), 33 deletions(-) diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index 260735abbd6..5f3b753c1a8 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -1474,25 +1474,31 @@ public static function enumCases(string $enum): array /** * Provides the ability to get constants from instances as well as class/global constants. * - * @param string $constant The name of the constant - * @param object|null $object The object to get the constant from + * @param string $constant The name of the constant + * @param object|null $object The object to get the constant from + * @param bool $checkDefined Whether to check if the constant is defined or not * * @return mixed Class constants can return many types like scalars, arrays, and * objects depending on the PHP version (\BackedEnum, \UnitEnum, etc.) + * When $checkDefined is true, returns true when the constant is defined, false otherwise * * @internal */ - public static function constant($constant, $object = null) + public static function constant($constant, $object = null, bool $checkDefined = false) { if (null !== $object) { if ('class' === $constant) { - return \get_class($object); + return $checkDefined ? true : \get_class($object); } $constant = \get_class($object).'::'.$constant; } if (!\defined($constant)) { + if ($checkDefined) { + return false; + } + if ('::class' === strtolower(substr($constant, -7))) { throw new RuntimeError(\sprintf('You cannot use the Twig function "constant()" to access "%s". You could provide an object and call constant("class", $object) or use the class name directly as a string.', $constant)); } @@ -1500,28 +1506,7 @@ public static function constant($constant, $object = null) throw new RuntimeError(\sprintf('Constant "%s" is undefined.', $constant)); } - return \constant($constant); - } - - /** - * Checks if a constant exists. - * - * @param string $constant The name of the constant - * @param object|null $object The object to get the constant from - * - * @internal - */ - public static function constantIsDefined($constant, $object = null): bool - { - if (null !== $object) { - if ('class' === $constant) { - return true; - } - - $constant = \get_class($object).'::'.$constant; - } - - return \defined($constant); + return $checkDefined ? true : \constant($constant); } /** diff --git a/src/Node/Expression/FunctionExpression.php b/src/Node/Expression/FunctionExpression.php index b7136f6f7a5..763a0b44d87 100644 --- a/src/Node/Expression/FunctionExpression.php +++ b/src/Node/Expression/FunctionExpression.php @@ -12,7 +12,6 @@ namespace Twig\Node\Expression; use Twig\Compiler; -use Twig\Extension\CoreExtension; use Twig\Node\Node; class FunctionExpression extends CallExpression @@ -31,14 +30,14 @@ public function compile(Compiler $compiler) $this->setAttribute('needs_environment', $function->needsEnvironment()); $this->setAttribute('needs_context', $function->needsContext()); $this->setAttribute('arguments', $function->getArguments()); - $callable = $function->getCallable(); - if ('constant' === $name && $this->getAttribute('is_defined_test')) { - $callable = [CoreExtension::class, 'constantIsDefined']; - } - $this->setAttribute('callable', $callable); + $this->setAttribute('callable', $function->getCallable()); $this->setAttribute('is_variadic', $function->isVariadic()); $this->setAttribute('dynamic_name', $function->getDynamicName()); + if ('constant' === $name && $this->getAttribute('is_defined_test')) { + $this->getNode('arguments')->setNode('checkDefined', new ConstantExpression(true, $this->getTemplateLine())); + } + $this->compileCallable($compiler); } } diff --git a/src/Resources/core.php b/src/Resources/core.php index e5372cda492..6e2fcfb0758 100644 --- a/src/Resources/core.php +++ b/src/Resources/core.php @@ -441,7 +441,7 @@ function twig_constant_is_defined($constant, $object = null) { trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); - return CoreExtension::constantIsDefined($constant, $object); + return CoreExtension::constant($constant, $object, true); } /** From 3156d8093e782925361783be168dd946c7d26634 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Mon, 12 Aug 2024 10:49:48 +0200 Subject: [PATCH 345/812] Move FunctionExpression/FilterExpression/TestExpression attributes from compilation time to parsing time --- CHANGELOG | 4 + doc/deprecated.rst | 92 +++++++++ src/Attribute/FirstClassTwigCallableReady.php | 20 ++ src/ExpressionParser.php | 39 +++- src/Node/Expression/CallExpression.php | 91 ++++++--- src/Node/Expression/Filter/DefaultFilter.php | 21 ++- src/Node/Expression/Filter/RawFilter.php | 7 +- src/Node/Expression/FilterExpression.php | 48 +++-- src/Node/Expression/FunctionExpression.php | 45 ++++- .../Expression/NullCoalesceExpression.php | 5 +- src/Node/Expression/Test/DefinedTest.php | 10 +- src/Node/Expression/TestExpression.php | 38 +++- src/NodeVisitor/EscaperNodeVisitor.php | 21 ++- src/NodeVisitor/SafeAnalysisNodeVisitor.php | 20 +- src/NodeVisitor/SandboxNodeVisitor.php | 4 +- tests/ExpressionParserTest.php | 176 ++++++++++++++++++ tests/Node/DeprecatedTest.php | 4 +- tests/Node/Expression/CallTest.php | 36 ++-- tests/Node/Expression/Filter/RawTest.php | 3 +- tests/Node/Expression/FilterTest.php | 55 +++--- tests/Node/Expression/FunctionTest.php | 36 ++-- tests/Node/Expression/TestTest.php | 20 +- 22 files changed, 640 insertions(+), 155 deletions(-) create mode 100644 src/Attribute/FirstClassTwigCallableReady.php diff --git a/CHANGELOG b/CHANGELOG index b6ef802a151..810b3616722 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,9 @@ # 3.12.0 (2024-XX-XX) + * Deprecate passing a name to `FunctionExpression`, `FilterExpression`, and `TestExpression`; + pass a `TwigFunction`, `TwigFilter`, or `TestFilter` instead + * Deprecate all Twig callable attributes on `TwigFunction`, `TwigFilter`, and `TestFilter` + * Deprecate the `filter` node of `FilterExpression` * Add the notion of Twig callables (functions, filters, and tests) * Bump minimum PHP version to 8.0 * Fix integration tests when a test has more than on data/expect section and deprecations diff --git a/doc/deprecated.rst b/doc/deprecated.rst index 7aeecb7ef7c..2d2860cb8b9 100644 --- a/doc/deprecated.rst +++ b/doc/deprecated.rst @@ -43,6 +43,98 @@ Nodes ``Twig\Node\Expression\NameExpression::isSpecial()`` methods are deprecated as of Twig 3.11 and will be removed in Twig 4.0. +* The ``filter`` node of ``Twig\Node\Expression\FilterExpression`` is + deprecated as of Twig 3.12 and will be removed in 4.0. Use the ``filter`` + attribute instead to get the filter: + + Before: + ``$node->getNode('filter')->getAttribute('value')`` + + After: + ``$node->getAttribute('twig_callable')->getName()`` + +* Passing a name to ``Twig\Node\Expression\FunctionExpression``, + ``Twig\Node\Expression\FilterExpression``, and + ``Twig\Node\Expression\TestExpression`` is deprecated as of Twig 3.12. + As of Twig 4.0, you need to pass a ``TwigFunction``, ``TwigFilter``, or + ``TestFilter`` instead. + + Let's take a ``FunctionExpression`` as an example. + + If you have a node that extends ``FunctionExpression`` and if you don't + override the constructor, you don't need to do anything. But if you override + the constructor, then you need to change the type hint of the name and mark + the constructor with the ``Twig\Attribute\FirstClassTwigCallableReady`` attribute. + + Before:: + + class NotReadyFunctionExpression extends FunctionExpression + { + public function __construct(string $function, Node $arguments, int $lineno) + { + parent::__construct($function, $arguments, $lineno); + } + } + + class NotReadyFilterExpression extends FilterExpression + { + public function __construct(Node $node, ConstantExpression $filter, Node $arguments, int $lineno, ?string $tag = null) + { + parent::__construct($node, $filter, $arguments, $lineno, $tag); + } + } + + class NotReadyTestExpression extends TestExpression + { + public function __construct(Node $node, string $test, ?Node $arguments, int $lineno) + { + parent::__construct($node, $test, $arguments, $lineno); + } + } + + After:: + + class ReadyFunctionExpression extends FunctionExpression + { + #[FirstClassTwigCallableReady] + public function __construct(TwigFunction|string $function, Node $arguments, int $lineno) + { + parent::__construct($function, $arguments, $lineno); + } + } + + class ReadyFilterExpression extends FilterExpression + { + #[FirstClassTwigCallableReady] + public function __construct(Node $node, TwigFilter|ConstantExpression $filter, Node $arguments, int $lineno, ?string $tag = null) + { + parent::__construct($node, $filter, $arguments, $lineno, $tag); + } + } + + class ReadyTestExpression extends TestExpression + { + #[FirstClassTwigCallableReady] + public function __construct(Node $node, TwigTest|string $test, ?Node $arguments, int $lineno) + { + parent::__construct($node, $test, $arguments, $lineno); + } + } + +* The following ``Twig\Node\Expression\FunctionExpression`` attributes are + deprecated as of Twig 3.12: ``needs_charset``, ``needs_environment``, + ``needs_context``, ``arguments``, ``callable``, ``is_variadic``, + and ``dynamic_name``. + +* The following ``Twig\Node\Expression\FilterExpression`` attributes are + deprecated as of Twig 3.12: ``needs_charset``, ``needs_environment``, + ``needs_context``, ``arguments``, ``callable``, ``is_variadic``, + and ``dynamic_name``. + +* The following ``Twig\Node\Expression\TestExpression`` attributes are + deprecated as of Twig 3.12: ``arguments``, ``callable``, ``is_variadic``, + and ``dynamic_name``. + Node Visitors ------------- diff --git a/src/Attribute/FirstClassTwigCallableReady.php b/src/Attribute/FirstClassTwigCallableReady.php new file mode 100644 index 00000000000..ffd8cffc812 --- /dev/null +++ b/src/Attribute/FirstClassTwigCallableReady.php @@ -0,0 +1,20 @@ +, associativity: self::OPERATOR_*}> */ private $binaryOperators; + private $readyNodes = []; public function __construct(Parser $parser, Environment $env) { @@ -496,7 +498,16 @@ public function getFunctionNode($name, $line) $args = $this->parseArguments(true); $function = $this->getFunction($name, $line); - return new ($function->getNodeClass())($function->getName(), $args, $line); + $ready = true; + if (!isset($this->readyNodes[$class = $function->getNodeClass()])) { + $this->readyNodes[$class] = (bool) (new \ReflectionClass($class))->getConstructor()->getAttributes(FirstClassTwigCallableReady::class); + } + + if (!$ready = $this->readyNodes[$class]) { + trigger_deprecation('twig/twig', '3.12', 'Twig node "%s" is not marked as ready for passing a "TwigFunction" in the constructor instead of its name; please update your code and then add #[FirstClassTwigCallableReady] attribute to the constructor.', $class); + } + + return new $class($ready ? $function : $function->getName(), $args, $line); } } @@ -561,7 +572,7 @@ public function parseSubscriptExpression($node) $filter = $this->getFilter('slice', $token->getLine()); $arguments = new Node([$arg, $length]); - $filter = new ($filter->getNodeClass())($node, new ConstantExpression('slice', $token->getLine()), $arguments, $token->getLine()); + $filter = new ($filter->getNodeClass())($node, $filter, $arguments, $token->getLine()); $stream->expect(Token::PUNCTUATION_TYPE, ']'); @@ -593,8 +604,17 @@ public function parseFilterExpressionRaw($node, $tag = null) } $filter = $this->getFilter($token->getValue(), $token->getLine()); - $name = new ConstantExpression($filter->getName(), $token->getLine()); - $node = new ($filter->getNodeClass())($node, $name, $arguments, $token->getLine(), $tag); + + $ready = true; + if (!isset($this->readyNodes[$class = $filter->getNodeClass()])) { + $this->readyNodes[$class] = (bool) (new \ReflectionClass($class))->getConstructor()->getAttributes(FirstClassTwigCallableReady::class); + } + + if (!$ready = $this->readyNodes[$class]) { + trigger_deprecation('twig/twig', '3.12', 'Twig node "%s" is not marked as ready for passing a "TwigFilter" in the constructor instead of its name; please update your code and then add #[FirstClassTwigCallableReady] attribute to the constructor.', $class); + } + + $node = new $class($node, $ready ? $filter : new ConstantExpression($filter->getName(), $token->getLine()), $arguments, $token->getLine(), $tag); if (!$this->parser->getStream()->test(Token::PUNCTUATION_TYPE, '|')) { break; @@ -737,7 +757,16 @@ private function parseTestExpression(Node $node): TestExpression $node->setAttribute('safe', true); } - return new ($test->getNodeClass())($node, $test->getName(), $arguments, $this->parser->getCurrentToken()->getLine()); + $ready = $test instanceof TwigTest; + if (!isset($this->readyNodes[$class = $test->getNodeClass()])) { + $this->readyNodes[$class] = (bool) (new \ReflectionClass($class))->getConstructor()->getAttributes(FirstClassTwigCallableReady::class); + } + + if (!$ready = $this->readyNodes[$class]) { + trigger_deprecation('twig/twig', '3.12', 'Twig node "%s" is not marked as ready for passing a "TwigTest" in the constructor instead of its name; please update your code and then add #[FirstClassTwigCallableReady] attribute to the constructor.', $class); + } + + return new $class($node, $ready ? $test : $test->getName(), $arguments, $this->parser->getCurrentToken()->getLine()); } private function getTest(int $line): TwigTest diff --git a/src/Node/Expression/CallExpression.php b/src/Node/Expression/CallExpression.php index 1ef11cea8cd..53561dd2026 100644 --- a/src/Node/Expression/CallExpression.php +++ b/src/Node/Expression/CallExpression.php @@ -15,6 +15,10 @@ use Twig\Error\SyntaxError; use Twig\Extension\ExtensionInterface; use Twig\Node\Node; +use Twig\TwigCallableInterface; +use Twig\TwigFilter; +use Twig\TwigFunction; +use Twig\TwigTest; use Twig\Util\ReflectionCallable; abstract class CallExpression extends AbstractExpression @@ -23,7 +27,8 @@ abstract class CallExpression extends AbstractExpression protected function compileCallable(Compiler $compiler) { - $callable = $this->getAttribute('callable'); + $twigCallable = $this->getTwigCallable(); + $callable = $twigCallable->getCallable(); if (\is_string($callable) && !str_contains($callable, '::')) { $compiler->raw($callable); @@ -51,7 +56,7 @@ protected function compileCallable(Compiler $compiler) $compiler->raw(\sprintf('->%s', $callable[1])); } else { - $compiler->raw(\sprintf('$this->env->get%s(\'%s\')->getCallable()', ucfirst($this->getAttribute('type')), $this->getAttribute('dynamic_name'))); + $compiler->raw(\sprintf('$this->env->get%s(\'%s\')->getCallable()', ucfirst($this->getAttribute('type')), $twigCallable->getDynamicName())); } } @@ -68,12 +73,14 @@ protected function compileArguments(Compiler $compiler, $isArray = false): void $first = true; - if ($this->hasAttribute('needs_charset') && $this->getAttribute('needs_charset')) { + $twigCallable = $this->getAttribute('twig_callable'); + + if ($twigCallable->needsCharset()) { $compiler->raw('$this->env->getCharset()'); $first = false; } - if ($this->hasAttribute('needs_environment') && $this->getAttribute('needs_environment')) { + if ($twigCallable->needsEnvironment()) { if (!$first) { $compiler->raw(', '); } @@ -81,7 +88,7 @@ protected function compileArguments(Compiler $compiler, $isArray = false): void $first = false; } - if ($this->hasAttribute('needs_context') && $this->getAttribute('needs_context')) { + if ($twigCallable->needsContext()) { if (!$first) { $compiler->raw(', '); } @@ -89,14 +96,12 @@ protected function compileArguments(Compiler $compiler, $isArray = false): void $first = false; } - if ($this->hasAttribute('arguments')) { - foreach ($this->getAttribute('arguments') as $argument) { - if (!$first) { - $compiler->raw(', '); - } - $compiler->string($argument); - $first = false; + foreach ($twigCallable->getArguments() as $argument) { + if (!$first) { + $compiler->raw(', '); } + $compiler->string($argument); + $first = false; } if ($this->hasNode('node')) { @@ -108,7 +113,7 @@ protected function compileArguments(Compiler $compiler, $isArray = false): void } if ($this->hasNode('arguments')) { - $callable = $this->getAttribute('callable'); + $callable = $twigCallable->getCallable(); $arguments = $this->getArguments($callable, $this->getNode('arguments')); foreach ($arguments as $node) { if (!$first) { @@ -140,7 +145,7 @@ protected function getArguments($callable, $arguments) $parameters[$name] = $node; } - $isVariadic = $this->hasAttribute('is_variadic') && $this->getAttribute('is_variadic'); + $isVariadic = $this->getAttribute('twig_callable')->isVariadic(); if (!$named && !$isVariadic) { return $parameters; } @@ -254,6 +259,7 @@ protected function normalizeName(string $name): string private function getCallableParameters($callable, bool $isVariadic): array { + $twigCallable = $this->getAttribute('twig_callable'); $rc = $this->reflectCallable($callable); $r = $rc->getReflector(); $callableName = $rc->getName(); @@ -262,20 +268,19 @@ private function getCallableParameters($callable, bool $isVariadic): array if ($this->hasNode('node')) { array_shift($parameters); } - if ($this->hasAttribute('needs_charset') && $this->getAttribute('needs_charset')) { + if ($twigCallable->needsCharset()) { array_shift($parameters); } - if ($this->hasAttribute('needs_environment') && $this->getAttribute('needs_environment')) { + if ($twigCallable->needsEnvironment()) { array_shift($parameters); } - if ($this->hasAttribute('needs_context') && $this->getAttribute('needs_context')) { + if ($twigCallable->needsContext()) { array_shift($parameters); } - if ($this->hasAttribute('arguments') && null !== $this->getAttribute('arguments')) { - foreach ($this->getAttribute('arguments') as $argument) { - array_shift($parameters); - } + foreach ($twigCallable->getArguments() as $argument) { + array_shift($parameters); } + $isPhpVariadic = false; if ($isVariadic) { $argument = end($parameters); @@ -286,7 +291,7 @@ private function getCallableParameters($callable, bool $isVariadic): array array_pop($parameters); $isPhpVariadic = true; } else { - throw new \LogicException(\sprintf('The last parameter of "%s" for %s "%s" must be an array with default value, eg. "array $arg = []".', $callableName, $this->getAttribute('type'), $this->getAttribute('name'))); + throw new \LogicException(\sprintf('The last parameter of "%s" for %s "%s" must be an array with default value, eg. "array $arg = []".', $callableName, $this->getAttribute('type'), $twigCallable->getName())); } } @@ -301,4 +306,46 @@ private function reflectCallable($callable): ReflectionCallable return $this->reflector; } + + /** + * Overrides the Twig callable based on attributes (as potentially, attributes changed between the creation and the compilation of the node) + * + * To be removed in 4.0 and replace by $this->getAttribute('twig_callable'). + */ + private function getTwigCallable(): TwigCallableInterface + { + $current = $this->getAttribute('twig_callable'); + + $this->setAttribute('twig_callable', match ($this->getAttribute('type')) { + 'test' => (new TwigTest( + $this->getAttribute('name'), + $this->hasAttribute('callable') ? $this->getAttribute('callable') : $current->getCallable(), + [ + 'is_variadic' => $this->hasAttribute('is_variadic') ? $this->getAttribute('is_variadic') : $current->isVariadic(), + ], + ))->withDynamicArguments($this->getAttribute('name'), $this->hasAttribute('dynamic_name') ? $this->getAttribute('dynamic_name') : $current->getDynamicName(), $this->hasAttribute('arguments') ? $this->hasAttribute('arguments') : $current->getArguments()), + 'function' => (new TwigFunction( + $this->hasAttribute('name') ? $this->getAttribute('name') : $current->getName(), + $this->hasAttribute('callable') ? $this->getAttribute('callable') : $current->getCallable(), + [ + 'needs_environment' => $this->hasAttribute('needs_environment') ? $this->getAttribute('needs_environment') : $current->needsEnvironment(), + 'needs_context' => $this->hasAttribute('needs_context') ? $this->getAttribute('needs_context') : $current->needsContext(), + 'needs_charset' => $this->hasAttribute('needs_charset') ? $this->getAttribute('needs_charset') : $current->needsCharset(), + 'is_variadic' => $this->hasAttribute('is_variadic') ? $this->getAttribute('is_variadic') : $current->isVariadic(), + ], + ))->withDynamicArguments($this->getAttribute('name'), $this->hasAttribute('dynamic_name') ? $this->getAttribute('dynamic_name') : $current->getDynamicName(), $this->hasAttribute('arguments') ? $this->hasAttribute('arguments') : $current->getArguments()), + 'filter' => (new TwigFilter( + $this->getAttribute('name'), + $this->hasAttribute('callable') ? $this->getAttribute('callable') : $current->getCallable(), + [ + 'needs_environment' => $this->hasAttribute('needs_environment') ? $this->getAttribute('needs_environment') : $current->needsEnvironment(), + 'needs_context' => $this->hasAttribute('needs_context') ? $this->getAttribute('needs_context') : $current->needsContext(), + 'needs_charset' => $this->hasAttribute('needs_charset') ? $this->getAttribute('needs_charset') : $current->needsCharset(), + 'is_variadic' => $this->hasAttribute('is_variadic') ? $this->getAttribute('is_variadic') : $current->isVariadic(), + ], + ))->withDynamicArguments($this->getAttribute('name'), $this->hasAttribute('dynamic_name') ? $this->getAttribute('dynamic_name') : $current->getDynamicName(), $this->hasAttribute('arguments') ? $this->hasAttribute('arguments') : $current->getArguments()), + }); + + return $this->getAttribute('twig_callable'); + } } diff --git a/src/Node/Expression/Filter/DefaultFilter.php b/src/Node/Expression/Filter/DefaultFilter.php index 7eb0ea7704a..e309c933118 100644 --- a/src/Node/Expression/Filter/DefaultFilter.php +++ b/src/Node/Expression/Filter/DefaultFilter.php @@ -11,7 +11,9 @@ namespace Twig\Node\Expression\Filter; +use Twig\Attribute\FirstClassTwigCallableReady; use Twig\Compiler; +use Twig\Extension\CoreExtension; use Twig\Node\Expression\ConditionalExpression; use Twig\Node\Expression\ConstantExpression; use Twig\Node\Expression\FilterExpression; @@ -19,6 +21,8 @@ use Twig\Node\Expression\NameExpression; use Twig\Node\Expression\Test\DefinedTest; use Twig\Node\Node; +use Twig\TwigFilter; +use Twig\TwigTest; /** * Returns the value or the default value when it is undefined or empty. @@ -29,12 +33,19 @@ */ class DefaultFilter extends FilterExpression { - public function __construct(Node $node, ConstantExpression $filterName, Node $arguments, int $lineno, ?string $tag = null) + #[FirstClassTwigCallableReady] + public function __construct(Node $node, TwigFilter|ConstantExpression $filter, Node $arguments, int $lineno, ?string $tag = null) { - $default = new FilterExpression($node, new ConstantExpression('default', $node->getTemplateLine()), $arguments, $node->getTemplateLine()); + if ($filter instanceof TwigFilter) { + $name = $filter->getName(); + $default = new FilterExpression($node, $filter, $arguments, $node->getTemplateLine()); + } else { + $name = $filter->getAttribute('value'); + $default = new FilterExpression($node, new TwigFilter('default', [CoreExtension::class, 'default']), $arguments, $node->getTemplateLine()); + } - if ('default' === $filterName->getAttribute('value') && ($node instanceof NameExpression || $node instanceof GetAttrExpression)) { - $test = new DefinedTest(clone $node, 'defined', new Node(), $node->getTemplateLine()); + if ('default' === $name && ($node instanceof NameExpression || $node instanceof GetAttrExpression)) { + $test = new DefinedTest(clone $node, new TwigTest('defined'), new Node(), $node->getTemplateLine()); $false = \count($arguments) ? $arguments->getNode('0') : new ConstantExpression('', $node->getTemplateLine()); $node = new ConditionalExpression($test, $default, $false, $node->getTemplateLine()); @@ -42,7 +53,7 @@ public function __construct(Node $node, ConstantExpression $filterName, Node $ar $node = $default; } - parent::__construct($node, $filterName, $arguments, $lineno, $tag); + parent::__construct($node, $filter, $arguments, $lineno, $tag); } public function compile(Compiler $compiler): void diff --git a/src/Node/Expression/Filter/RawFilter.php b/src/Node/Expression/Filter/RawFilter.php index 6502f68ef57..26f53370b6d 100644 --- a/src/Node/Expression/Filter/RawFilter.php +++ b/src/Node/Expression/Filter/RawFilter.php @@ -11,19 +11,22 @@ namespace Twig\Node\Expression\Filter; +use Twig\Attribute\FirstClassTwigCallableReady; use Twig\Compiler; use Twig\Node\Expression\ConstantExpression; use Twig\Node\Expression\FilterExpression; use Twig\Node\Node; +use Twig\TwigFilter; /** * @author Fabien Potencier */ class RawFilter extends FilterExpression { - public function __construct(Node $node, ?ConstantExpression $filterName = null, ?Node $arguments = null, int $lineno = 0, ?string $tag = null) + #[FirstClassTwigCallableReady] + public function __construct(Node $node, TwigFilter|ConstantExpression|null $filter = null, ?Node $arguments = null, int $lineno = 0, ?string $tag = null) { - parent::__construct($node, $filterName ?: new ConstantExpression('raw', $node->getTemplateLine()), $arguments ?: new Node(), $lineno ?: $node->getTemplateLine(), $tag ?: $node->getNodeTag()); + parent::__construct($node, $filter ?: new TwigFilter('raw'), $arguments ?: new Node(), $lineno ?: $node->getTemplateLine(), $tag ?: $node->getNodeTag()); } public function compile(Compiler $compiler): void diff --git a/src/Node/Expression/FilterExpression.php b/src/Node/Expression/FilterExpression.php index d410ede53ee..82a4e4a2ab8 100644 --- a/src/Node/Expression/FilterExpression.php +++ b/src/Node/Expression/FilterExpression.php @@ -12,22 +12,49 @@ namespace Twig\Node\Expression; +use Twig\Attribute\FirstClassTwigCallableReady; use Twig\Compiler; +use Twig\Node\NameDeprecation; use Twig\Node\Node; +use Twig\TwigFilter; class FilterExpression extends CallExpression { - public function __construct(Node $node, ConstantExpression $filterName, Node $arguments, int $lineno, ?string $tag = null) + #[FirstClassTwigCallableReady] + public function __construct(Node $node, TwigFilter|ConstantExpression $filter, Node $arguments, int $lineno, ?string $tag = null) { - parent::__construct(['node' => $node, 'filter' => $filterName, 'arguments' => $arguments], ['name' => $filterName->getAttribute('value'), 'type' => 'filter'], $lineno, $tag); + if ($filter instanceof TwigFilter) { + $name = $filter->getName(); + $filterName = new ConstantExpression($name, $lineno); + } else { + $name = $filter->getAttribute('value'); + $filterName = $filter; + trigger_deprecation('twig/twig', '3.12', 'Not passing an instance of "TwigFilter" when creating a "%s" filter of type "%s" is deprecated.', $name, static::class); + } + + parent::__construct(['node' => $node, 'filter' => $filterName, 'arguments' => $arguments], ['name' => $name, 'type' => 'filter'], $lineno, $tag); + + if ($filter instanceof TwigFilter) { + $this->setAttribute('twig_callable', $filter); + } + + $this->deprecateNode('filter', new NameDeprecation('twig/twig', '3.12')); + + $this->deprecateAttribute('needs_charset', new NameDeprecation('twig/twig', '3.12')); + $this->deprecateAttribute('needs_environment', new NameDeprecation('twig/twig', '3.12')); + $this->deprecateAttribute('needs_context', new NameDeprecation('twig/twig', '3.12')); + $this->deprecateAttribute('arguments', new NameDeprecation('twig/twig', '3.12')); + $this->deprecateAttribute('callable', new NameDeprecation('twig/twig', '3.12')); + $this->deprecateAttribute('is_variadic', new NameDeprecation('twig/twig', '3.12')); + $this->deprecateAttribute('dynamic_name', new NameDeprecation('twig/twig', '3.12')); } public function compile(Compiler $compiler): void { - $name = $this->getNode('filter')->getAttribute('value'); + $name = $this->getNode('filter', false)->getAttribute('value'); if ($name !== $this->getAttribute('name')) { trigger_deprecation('twig/twig', '3.11', 'Changing the value of a "filter" node in a NodeVisitor class is not supported anymore.'); - $this->setAttribute('name', $name); + $this->removeAttribute('twig_callable'); } if ('raw' === $name) { trigger_deprecation('twig/twig', '3.11', 'Creating the "raw" filter via "FilterExpression" is deprecated; use "RawFilter" instead.'); @@ -36,15 +63,10 @@ public function compile(Compiler $compiler): void return; } - $filter = $compiler->getEnvironment()->getFilter($name); - - $this->setAttribute('needs_charset', $filter->needsCharset()); - $this->setAttribute('needs_environment', $filter->needsEnvironment()); - $this->setAttribute('needs_context', $filter->needsContext()); - $this->setAttribute('arguments', $filter->getArguments()); - $this->setAttribute('callable', $filter->getCallable()); - $this->setAttribute('is_variadic', $filter->isVariadic()); - $this->setAttribute('dynamic_name', $filter->getDynamicName()); + + if (!$this->hasAttribute('twig_callable')) { + $this->setAttribute('twig_callable', $compiler->getEnvironment()->getFilter($name)); + } $this->compileCallable($compiler); } diff --git a/src/Node/Expression/FunctionExpression.php b/src/Node/Expression/FunctionExpression.php index 763a0b44d87..6215c6abfbc 100644 --- a/src/Node/Expression/FunctionExpression.php +++ b/src/Node/Expression/FunctionExpression.php @@ -11,28 +11,53 @@ namespace Twig\Node\Expression; +use Twig\Attribute\FirstClassTwigCallableReady; use Twig\Compiler; +use Twig\Node\NameDeprecation; use Twig\Node\Node; +use Twig\TwigFunction; class FunctionExpression extends CallExpression { - public function __construct(string $name, Node $arguments, int $lineno) + #[FirstClassTwigCallableReady] + public function __construct(TwigFunction|string $function, Node $arguments, int $lineno) { + if ($function instanceof TwigFunction) { + $name = $function->getName(); + } else { + $name = $function; + trigger_deprecation('twig/twig', '3.12', 'Not passing an instance of "TwigFunction" when creating a "%s" function of type "%s" is deprecated.', $name, static::class); + } + parent::__construct(['arguments' => $arguments], ['name' => $name, 'type' => 'function', 'is_defined_test' => false], $lineno); + + if ($function instanceof TwigFunction) { + $this->setAttribute('twig_callable', $function); + } + + $this->deprecateAttribute('needs_charset', new NameDeprecation('twig/twig', '3.12')); + $this->deprecateAttribute('needs_environment', new NameDeprecation('twig/twig', '3.12')); + $this->deprecateAttribute('needs_context', new NameDeprecation('twig/twig', '3.12')); + $this->deprecateAttribute('arguments', new NameDeprecation('twig/twig', '3.12')); + $this->deprecateAttribute('callable', new NameDeprecation('twig/twig', '3.12')); + $this->deprecateAttribute('is_variadic', new NameDeprecation('twig/twig', '3.12')); + $this->deprecateAttribute('dynamic_name', new NameDeprecation('twig/twig', '3.12')); } public function compile(Compiler $compiler) { $name = $this->getAttribute('name'); - $function = $compiler->getEnvironment()->getFunction($name); - - $this->setAttribute('needs_charset', $function->needsCharset()); - $this->setAttribute('needs_environment', $function->needsEnvironment()); - $this->setAttribute('needs_context', $function->needsContext()); - $this->setAttribute('arguments', $function->getArguments()); - $this->setAttribute('callable', $function->getCallable()); - $this->setAttribute('is_variadic', $function->isVariadic()); - $this->setAttribute('dynamic_name', $function->getDynamicName()); + if ($this->hasAttribute('twig_callable')) { + $name = $this->getAttribute('twig_callable')->getName(); + if ($name !== $this->getAttribute('name')) { + trigger_deprecation('twig/twig', '3.12', 'Changing the value of a "function" node in a NodeVisitor class is not supported anymore.'); + $this->removeAttribute('twig_callable'); + } + } + + if (!$this->hasAttribute('twig_callable')) { + $this->setAttribute('twig_callable', $compiler->getEnvironment()->getFunction($name)); + } if ('constant' === $name && $this->getAttribute('is_defined_test')) { $this->getNode('arguments')->setNode('checkDefined', new ConstantExpression(true, $this->getTemplateLine())); diff --git a/src/Node/Expression/NullCoalesceExpression.php b/src/Node/Expression/NullCoalesceExpression.php index a72bc4fc654..98630f7f068 100644 --- a/src/Node/Expression/NullCoalesceExpression.php +++ b/src/Node/Expression/NullCoalesceExpression.php @@ -17,17 +17,18 @@ use Twig\Node\Expression\Test\NullTest; use Twig\Node\Expression\Unary\NotUnary; use Twig\Node\Node; +use Twig\TwigTest; class NullCoalesceExpression extends ConditionalExpression { public function __construct(Node $left, Node $right, int $lineno) { - $test = new DefinedTest(clone $left, 'defined', new Node(), $left->getTemplateLine()); + $test = new DefinedTest(clone $left, new TwigTest('defined'), new Node(), $left->getTemplateLine()); // for "block()", we don't need the null test as the return value is always a string if (!$left instanceof BlockReferenceExpression) { $test = new AndBinary( $test, - new NotUnary(new NullTest($left, 'null', new Node(), $left->getTemplateLine()), $left->getTemplateLine()), + new NotUnary(new NullTest($left, new TwigTest('null'), new Node(), $left->getTemplateLine()), $left->getTemplateLine()), $left->getTemplateLine() ); } diff --git a/src/Node/Expression/Test/DefinedTest.php b/src/Node/Expression/Test/DefinedTest.php index 3953bbbe2cc..f4c1b1e4b4f 100644 --- a/src/Node/Expression/Test/DefinedTest.php +++ b/src/Node/Expression/Test/DefinedTest.php @@ -11,8 +11,10 @@ namespace Twig\Node\Expression\Test; +use Twig\Attribute\FirstClassTwigCallableReady; use Twig\Compiler; use Twig\Error\SyntaxError; +use Twig\Extension\CoreExtension; use Twig\Node\Expression\ArrayExpression; use Twig\Node\Expression\BlockReferenceExpression; use Twig\Node\Expression\ConstantExpression; @@ -22,6 +24,7 @@ use Twig\Node\Expression\NameExpression; use Twig\Node\Expression\TestExpression; use Twig\Node\Node; +use Twig\TwigTest; /** * Checks if a variable is defined in the current context. @@ -35,7 +38,8 @@ */ class DefinedTest extends TestExpression { - public function __construct(Node $node, string $name, ?Node $arguments, int $lineno) + #[FirstClassTwigCallableReady] + public function __construct(Node $node, TwigTest|string $name, ?Node $arguments, int $lineno) { if ($node instanceof NameExpression) { $node->setAttribute('is_defined_test', true); @@ -54,6 +58,10 @@ public function __construct(Node $node, string $name, ?Node $arguments, int $lin throw new SyntaxError('The "defined" test only works with simple variables.', $lineno); } + if (is_string($name) && 'defined' !== $name) { + trigger_deprecation('twig/twig', '3.12', 'Creating a "DefinedTest" instance with a test name that is not "defined" is deprecated.'); + } + parent::__construct($node, $name, $arguments, $lineno); } diff --git a/src/Node/Expression/TestExpression.php b/src/Node/Expression/TestExpression.php index 6bf6c691bd8..080d85aaa5f 100644 --- a/src/Node/Expression/TestExpression.php +++ b/src/Node/Expression/TestExpression.php @@ -11,29 +11,55 @@ namespace Twig\Node\Expression; +use Twig\Attribute\FirstClassTwigCallableReady; use Twig\Compiler; +use Twig\Node\NameDeprecation; use Twig\Node\Node; +use Twig\TwigTest; class TestExpression extends CallExpression { - public function __construct(Node $node, string $name, ?Node $arguments, int $lineno) + #[FirstClassTwigCallableReady] + public function __construct(Node $node, string|TwigTest $test, ?Node $arguments, int $lineno) { $nodes = ['node' => $node]; if (null !== $arguments) { $nodes['arguments'] = $arguments; } + if ($test instanceof TwigTest) { + $name = $test->getName(); + } else { + $name = $test; + trigger_deprecation('twig/twig', '3.12', 'Not passing an instance of "TwigTest" when creating a "%s" test of type "%s" is deprecated.', $name, static::class); + } + parent::__construct($nodes, ['name' => $name, 'type' => 'test'], $lineno); + + if ($test instanceof TwigTest) { + $this->setAttribute('twig_callable', $test); + } + + $this->deprecateAttribute('arguments', new NameDeprecation('twig/twig', '3.12')); + $this->deprecateAttribute('callable', new NameDeprecation('twig/twig', '3.12')); + $this->deprecateAttribute('is_variadic', new NameDeprecation('twig/twig', '3.12')); + $this->deprecateAttribute('dynamic_name', new NameDeprecation('twig/twig', '3.12')); } public function compile(Compiler $compiler): void { - $test = $compiler->getEnvironment()->getTest($this->getAttribute('name')); + $name = $this->getAttribute('name'); + if ($this->hasAttribute('twig_callable')) { + $name = $this->getAttribute('twig_callable')->getName(); + if ($name !== $this->getAttribute('name')) { + trigger_deprecation('twig/twig', '3.12', 'Changing the value of a "test" node in a NodeVisitor class is not supported anymore.'); + $this->removeAttribute('twig_callable'); + } + } - $this->setAttribute('arguments', $test->getArguments()); - $this->setAttribute('callable', $test->getCallable()); - $this->setAttribute('is_variadic', $test->isVariadic()); - $this->setAttribute('dynamic_name', $test->getDynamicName()); + if (!$this->hasAttribute('twig_callable')) { + $this->setAttribute('twig_callable', $compiler->getEnvironment()->getTest($this->getAttribute('name'))); + } $this->compileCallable($compiler); } diff --git a/src/NodeVisitor/EscaperNodeVisitor.php b/src/NodeVisitor/EscaperNodeVisitor.php index 91e2ea89392..32f49ab1e8e 100644 --- a/src/NodeVisitor/EscaperNodeVisitor.php +++ b/src/NodeVisitor/EscaperNodeVisitor.php @@ -126,7 +126,7 @@ private function escapeInlinePrintNode(InlinePrint $node, Environment $env, stri return $node; } - return new InlinePrint($this->getEscaperFilter($type, $expression), $node->getTemplateLine()); + return new InlinePrint($this->getEscaperFilter($env, $type, $expression), $node->getTemplateLine()); } private function escapePrintNode(PrintNode $node, Environment $env, string $type): Node @@ -139,14 +139,19 @@ private function escapePrintNode(PrintNode $node, Environment $env, string $type $class = \get_class($node); - return new $class($this->getEscaperFilter($type, $expression), $node->getTemplateLine()); + return new $class($this->getEscaperFilter($env, $type, $expression), $node->getTemplateLine()); } private function preEscapeFilterNode(FilterExpression $filter, Environment $env): FilterExpression { - $name = $filter->getNode('filter')->getAttribute('value'); + if ($filter->hasAttribute('twig_callable')) { + $type = $filter->getAttribute('twig_callable')->getPreEscape(); + } else { + // legacy + $name = $filter->getNode('filter', false)->getAttribute('value'); + $type = $env->getFilter($name)->getPreEscape(); + } - $type = $env->getFilter($name)->getPreEscape(); if (null === $type) { return $filter; } @@ -156,7 +161,7 @@ private function preEscapeFilterNode(FilterExpression $filter, Environment $env) return $filter; } - $filter->setNode('node', $this->getEscaperFilter($type, $node)); + $filter->setNode('node', $this->getEscaperFilter($env, $type, $node)); return $filter; } @@ -188,13 +193,13 @@ private function needEscaping() return $this->defaultStrategy ?: false; } - private function getEscaperFilter(string $type, Node $node): FilterExpression + private function getEscaperFilter(Environment $env, string $type, Node $node): FilterExpression { $line = $node->getTemplateLine(); - $name = new ConstantExpression('escape', $line); + $filter = $env->getFilter('escape'); $args = new Node([new ConstantExpression($type, $line), new ConstantExpression(null, $line), new ConstantExpression(true, $line)]); - return new FilterExpression($node, $name, $args, $line); + return new FilterExpression($node, $filter, $args, $line); } public function getPriority(): int diff --git a/src/NodeVisitor/SafeAnalysisNodeVisitor.php b/src/NodeVisitor/SafeAnalysisNodeVisitor.php index 8a5135aae91..07672164ece 100644 --- a/src/NodeVisitor/SafeAnalysisNodeVisitor.php +++ b/src/NodeVisitor/SafeAnalysisNodeVisitor.php @@ -96,8 +96,14 @@ public function leaveNode(Node $node, Environment $env): ?Node $this->setSafe($node, $safe); } elseif ($node instanceof FilterExpression) { // filter expression is safe when the filter is safe - $name = $node->getNode('filter')->getAttribute('value'); - if ($filter = $env->getFilter($name)) { + if ($node->hasAttribute('twig_callable')) { + $filter = $node->getAttribute('twig_callable'); + } else { + // legacy + $filter = $env->getFilter($node->getAttribute('name')); + } + + if ($filter) { $safe = $filter->getSafe($node->getNode('arguments')); if (null === $safe) { $safe = $this->intersectSafe($this->getSafe($node->getNode('node')), $filter->getPreservesSafety()); @@ -108,8 +114,14 @@ public function leaveNode(Node $node, Environment $env): ?Node } } elseif ($node instanceof FunctionExpression) { // function expression is safe when the function is safe - $name = $node->getAttribute('name'); - if ($function = $env->getFunction($name)) { + if ($node->hasAttribute('twig_callable')) { + $function = $node->getAttribute('twig_callable'); + } else { + // legacy + $function = $env->getFunction($node->getAttribute('name')); + } + + if ($function) { $this->setSafe($node, $function->getSafe($node->getNode('arguments'))); } else { $this->setSafe($node, []); diff --git a/src/NodeVisitor/SandboxNodeVisitor.php b/src/NodeVisitor/SandboxNodeVisitor.php index 68020885e40..37e184a3edc 100644 --- a/src/NodeVisitor/SandboxNodeVisitor.php +++ b/src/NodeVisitor/SandboxNodeVisitor.php @@ -58,8 +58,8 @@ public function enterNode(Node $node, Environment $env): Node } // look for filters - if ($node instanceof FilterExpression && !isset($this->filters[$node->getNode('filter')->getAttribute('value')])) { - $this->filters[$node->getNode('filter')->getAttribute('value')] = $node->getTemplateLine(); + if ($node instanceof FilterExpression && !isset($this->filters[$node->getAttribute('name')])) { + $this->filters[$node->getAttribute('name')] = $node->getTemplateLine(); } // look for functions diff --git a/tests/ExpressionParserTest.php b/tests/ExpressionParserTest.php index 17524b2457f..fe780be3328 100644 --- a/tests/ExpressionParserTest.php +++ b/tests/ExpressionParserTest.php @@ -12,6 +12,8 @@ */ use PHPUnit\Framework\TestCase; +use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; +use Twig\Attribute\FirstClassTwigCallableReady; use Twig\Environment; use Twig\Error\SyntaxError; use Twig\Extension\AbstractExtension; @@ -19,7 +21,11 @@ use Twig\Node\Expression\ArrayExpression; use Twig\Node\Expression\Binary\ConcatBinary; use Twig\Node\Expression\ConstantExpression; +use Twig\Node\Expression\FilterExpression; +use Twig\Node\Expression\FunctionExpression; use Twig\Node\Expression\NameExpression; +use Twig\Node\Expression\TestExpression; +use Twig\Node\Node; use Twig\Parser; use Twig\Source; use Twig\TwigFilter; @@ -28,6 +34,8 @@ class ExpressionParserTest extends TestCase { + use ExpectDeprecationTrait; + /** * @dataProvider getFailingTestsForAssignment */ @@ -460,6 +468,111 @@ public function getFilters() $this->assertStringContainsString('$this->env->getFilter(\'*_foo_*_bar\')->getCallable()("foo", "bar", "a")', $env->compile($env->parse($env->tokenize(new Source($env->getLoader()->getSourceContext('index')->getCode(), 'index'))))); } + public function testNotReadyFunctionWithNoConstructor() + { + $env = new Environment(new ArrayLoader(), ['cache' => false, 'autoescape' => false]); + $env->addFunction(new TwigFunction('foo', 'foo', ['node_class' => NotReadyFunctionExpressionWithNoConstructor::class])); + $parser = new Parser($env); + + $parser->parse($env->tokenize(new Source('{{ foo() }}', 'index'))); + $this->doesNotPerformAssertions(); + } + + public function testNotReadyFilterWithNoConstructor() + { + $env = new Environment(new ArrayLoader(), ['cache' => false, 'autoescape' => false]); + $env->addFilter(new TwigFilter('foo', 'foo', ['node_class' => NotReadyFilterExpressionWithNoConstructor::class])); + $parser = new Parser($env); + + $parser->parse($env->tokenize(new Source('{{ 1|foo }}', 'index'))); + $this->doesNotPerformAssertions(); + } + + public function testNotReadyTestWithNoConstructor() + { + $env = new Environment(new ArrayLoader(), ['cache' => false, 'autoescape' => false]); + $env->addTest(new TwigTest('foo', 'foo', ['node_class' => NotReadyTestExpressionWithNoConstructor::class])); + $parser = new Parser($env); + + $parser->parse($env->tokenize(new Source('{{ 1 is foo }}', 'index'))); + $this->doesNotPerformAssertions(); + } + + /** + * @group legacy + */ + public function testNotReadyFunction() + { + $this->expectDeprecation('Since twig/twig 3.12: Twig node "Twig\Tests\NotReadyFunctionExpression" is not marked as ready for passing a "TwigFunction" in the constructor instead of its name; please update your code and then add #[FirstClassTwigCallableReady] attribute to the constructor.'); + $this->expectDeprecation('Since twig/twig 3.12: Not passing an instance of "TwigFunction" when creating a "foo" function of type "Twig\Tests\NotReadyFunctionExpression" is deprecated.'); + + $env = new Environment(new ArrayLoader(), ['cache' => false, 'autoescape' => false]); + $env->addFunction(new TwigFunction('foo', 'foo', ['node_class' => NotReadyFunctionExpression::class])); + $parser = new Parser($env); + + $parser->parse($env->tokenize(new Source('{{ foo() }}', 'index'))); + } + + /** + * @group legacy + */ + public function testNotReadyFilter() + { + $this->expectDeprecation('Since twig/twig 3.12: Twig node "Twig\Tests\NotReadyFilterExpression" is not marked as ready for passing a "TwigFilter" in the constructor instead of its name; please update your code and then add #[FirstClassTwigCallableReady] attribute to the constructor.'); + $this->expectDeprecation('Since twig/twig 3.12: Not passing an instance of "TwigFilter" when creating a "foo" filter of type "Twig\Tests\NotReadyFilterExpression" is deprecated.'); + + $env = new Environment(new ArrayLoader(), ['cache' => false, 'autoescape' => false]); + $env->addFilter(new TwigFilter('foo', 'foo', ['node_class' => NotReadyFilterExpression::class])); + $parser = new Parser($env); + + $parser->parse($env->tokenize(new Source('{{ 1|foo }}', 'index'))); + } + + /** + * @group legacy + */ + public function testNotReadyTest() + { + $this->expectDeprecation('Since twig/twig 3.12: Twig node "Twig\Tests\NotReadyTestExpression" is not marked as ready for passing a "TwigTest" in the constructor instead of its name; please update your code and then add #[FirstClassTwigCallableReady] attribute to the constructor.'); + $this->expectDeprecation('Since twig/twig 3.12: Not passing an instance of "TwigTest" when creating a "foo" test of type "Twig\Tests\NotReadyTestExpression" is deprecated.'); + + $env = new Environment(new ArrayLoader(), ['cache' => false, 'autoescape' => false]); + $env->addTest(new TwigTest('foo', 'foo', ['node_class' => NotReadyTestExpression::class])); + $parser = new Parser($env); + + $parser->parse($env->tokenize(new Source('{{ 1 is foo }}', 'index'))); + } + + public function testReadyFunction() + { + $env = new Environment(new ArrayLoader(), ['cache' => false, 'autoescape' => false]); + $env->addFunction(new TwigFunction('foo', 'foo', ['node_class' => ReadyFunctionExpression::class])); + $parser = new Parser($env); + + $parser->parse($env->tokenize(new Source('{{ foo() }}', 'index'))); + $this->doesNotPerformAssertions(); + } + + public function testReadyFilter() + { + $env = new Environment(new ArrayLoader(), ['cache' => false, 'autoescape' => false]); + $env->addFilter(new TwigFilter('foo', 'foo', ['node_class' => ReadyFilterExpression::class])); + $parser = new Parser($env); + + $parser->parse($env->tokenize(new Source('{{ 1|foo }}', 'index'))); + $this->doesNotPerformAssertions(); + } + + public function testReadyTest() + { + $env = new Environment(new ArrayLoader(), ['cache' => false, 'autoescape' => false]); + $env->addTest(new TwigTest('foo', 'foo', ['node_class' => ReadyTestExpression::class])); + $parser = new Parser($env); + + $parser->parse($env->tokenize(new Source('{{ 1 is foo }}', 'index'))); + $this->doesNotPerformAssertions(); + } + private function createNameExpression(string $name, array $attributes) { $expression = new NameExpression($name, 1); @@ -470,3 +583,66 @@ private function createNameExpression(string $name, array $attributes) return $expression; } } + +class NotReadyFunctionExpression extends FunctionExpression +{ + public function __construct(string $function, Node $arguments, int $lineno) + { + parent::__construct($function, $arguments, $lineno); + } +} + +class NotReadyFilterExpression extends FilterExpression +{ + public function __construct(Node $node, ConstantExpression $filter, Node $arguments, int $lineno, ?string $tag = null) + { + parent::__construct($node, $filter, $arguments, $lineno, $tag); + } +} + +class NotReadyTestExpression extends TestExpression +{ + public function __construct(Node $node, string $test, ?Node $arguments, int $lineno) + { + parent::__construct($node, $test, $arguments, $lineno); + } +} + +class NotReadyFunctionExpressionWithNoConstructor extends FunctionExpression +{ +} + +class NotReadyFilterExpressionWithNoConstructor extends FilterExpression +{ +} + +class NotReadyTestExpressionWithNoConstructor extends TestExpression +{ +} + +class ReadyFunctionExpression extends FunctionExpression +{ + #[FirstClassTwigCallableReady] + public function __construct(TwigFunction|string $function, Node $arguments, int $lineno) + { + parent::__construct($function, $arguments, $lineno); + } +} + +class ReadyFilterExpression extends FilterExpression +{ + #[FirstClassTwigCallableReady] + public function __construct(Node $node, TwigFilter|ConstantExpression $filter, Node $arguments, int $lineno, ?string $tag = null) + { + parent::__construct($node, $filter, $arguments, $lineno, $tag); + } +} + +class ReadyTestExpression extends TestExpression +{ + #[FirstClassTwigCallableReady] + public function __construct(Node $node, TwigTest|string $test, ?Node $arguments, int $lineno) + { + parent::__construct($node, $test, $arguments, $lineno); + } +} diff --git a/tests/Node/DeprecatedTest.php b/tests/Node/DeprecatedTest.php index 440bd520e52..d79e129f7d7 100644 --- a/tests/Node/DeprecatedTest.php +++ b/tests/Node/DeprecatedTest.php @@ -67,9 +67,9 @@ public function getTests() ]; $environment = new Environment(new ArrayLoader()); - $environment->addFunction(new TwigFunction('foo', 'foo', [])); + $environment->addFunction($function = new TwigFunction('foo', 'foo', [])); - $expr = new FunctionExpression('foo', new Node(), 1); + $expr = new FunctionExpression($function, new Node(), 1); $node = new DeprecatedNode($expr, 1, 'deprecated'); $node->setSourceContext(new Source('', 'foo.twig')); $node->setNode('package', new ConstantExpression('twig/twig', 1)); diff --git a/tests/Node/Expression/CallTest.php b/tests/Node/Expression/CallTest.php index 7f02e68f5c1..427f6ed5618 100644 --- a/tests/Node/Expression/CallTest.php +++ b/tests/Node/Expression/CallTest.php @@ -13,13 +13,15 @@ use PHPUnit\Framework\TestCase; use Twig\Error\SyntaxError; -use Twig\Node\Expression\CallExpression; +use Twig\Node\Expression\FunctionExpression; +use Twig\Node\Node; +use Twig\TwigFunction; class CallTest extends TestCase { public function testGetArguments() { - $node = new Node_Expression_Call([], ['type' => 'function', 'name' => 'date']); + $node = $this->createFunctionExpression('date'); $this->assertEquals(['U', null], $this->getArguments($node, ['date', ['format' => 'U', 'timestamp' => null]])); } @@ -28,7 +30,7 @@ public function testGetArgumentsWhenPositionalArgumentsAfterNamedArguments() $this->expectException(SyntaxError::class); $this->expectExceptionMessage('Positional arguments cannot be used after named arguments for function "date".'); - $node = new Node_Expression_Call([], ['type' => 'function', 'name' => 'date']); + $node = $this->createFunctionExpression('date'); $this->getArguments($node, ['date', ['timestamp' => 123456, 'Y-m-d']]); } @@ -37,7 +39,7 @@ public function testGetArgumentsWhenArgumentIsDefinedTwice() $this->expectException(SyntaxError::class); $this->expectExceptionMessage('Argument "format" is defined twice for function "date".'); - $node = new Node_Expression_Call([], ['type' => 'function', 'name' => 'date']); + $node = $this->createFunctionExpression('date'); $this->getArguments($node, ['date', ['Y-m-d', 'format' => 'U']]); } @@ -46,7 +48,7 @@ public function testGetArgumentsWithWrongNamedArgumentName() $this->expectException(SyntaxError::class); $this->expectExceptionMessage('Unknown argument "unknown" for function "date(format, timestamp)".'); - $node = new Node_Expression_Call([], ['type' => 'function', 'name' => 'date']); + $node = $this->createFunctionExpression('date'); $this->getArguments($node, ['date', ['Y-m-d', 'timestamp' => null, 'unknown' => '']]); } @@ -55,7 +57,7 @@ public function testGetArgumentsWithWrongNamedArgumentNames() $this->expectException(SyntaxError::class); $this->expectExceptionMessage('Unknown arguments "unknown1", "unknown2" for function "date(format, timestamp)".'); - $node = new Node_Expression_Call([], ['type' => 'function', 'name' => 'date']); + $node = $this->createFunctionExpression('date'); $this->getArguments($node, ['date', ['Y-m-d', 'timestamp' => null, 'unknown1' => '', 'unknown2' => '']]); } @@ -68,20 +70,19 @@ public function testResolveArgumentsWithMissingValueForOptionalArgument() $this->expectException(SyntaxError::class); $this->expectExceptionMessage('Argument "case_sensitivity" could not be assigned for function "substr_compare(main_str, str, offset, length, case_sensitivity)" because it is mapped to an internal PHP function which cannot determine default value for optional argument "length".'); - $node = new Node_Expression_Call([], ['type' => 'function', 'name' => 'substr_compare']); + $node = $this->createFunctionExpression('substr_compare'); $this->getArguments($node, ['substr_compare', ['abcd', 'bc', 'offset' => 1, 'case_sensitivity' => true]]); } public function testResolveArgumentsOnlyNecessaryArgumentsForCustomFunction() { - $node = new Node_Expression_Call([], ['type' => 'function', 'name' => 'custom_function']); - + $node = $this->createFunctionExpression('custom_function'); $this->assertEquals(['arg1'], $this->getArguments($node, [[$this, 'customFunction'], ['arg1' => 'arg1']])); } public function testGetArgumentsForStaticMethod() { - $node = new Node_Expression_Call([], ['type' => 'function', 'name' => 'custom_static_function']); + $node = $this->createFunctionExpression('custom_static_function'); $this->assertEquals(['arg1'], $this->getArguments($node, [__CLASS__.'::customStaticFunction', ['arg1' => 'arg1']])); } @@ -90,7 +91,7 @@ public function testResolveArgumentsWithMissingParameterForArbitraryArguments() $this->expectException(\LogicException::class); $this->expectExceptionMessage('The last parameter of "Twig\\Tests\\Node\\Expression\\CallTest::customFunctionWithArbitraryArguments" for function "foo" must be an array with default value, eg. "array $arg = []".'); - $node = new Node_Expression_Call([], ['type' => 'function', 'name' => 'foo', 'is_variadic' => true]); + $node = $this->createFunctionExpression('foo', true); $this->getArguments($node, [[$this, 'customFunctionWithArbitraryArguments'], []]); } @@ -98,7 +99,7 @@ public function testGetArgumentsWithInvalidCallable() { $this->expectException(\LogicException::class); $this->expectExceptionMessage('Callback for function "foo" is not callable in the current scope.'); - $node = new Node_Expression_Call([], ['type' => 'function', 'name' => 'foo', 'is_variadic' => true]); + $node = $this->createFunctionExpression('foo', true); $this->getArguments($node, ['', []]); } @@ -107,7 +108,7 @@ public function testResolveArgumentsWithMissingParameterForArbitraryArgumentsOnF $this->expectException(\LogicException::class); $this->expectExceptionMessageMatches('#^The last parameter of "Twig\\\\Tests\\\\Node\\\\Expression\\\\custom_call_test_function" for function "foo" must be an array with default value, eg\\. "array \\$arg \\= \\[\\]"\\.$#'); - $node = new Node_Expression_Call([], ['type' => 'function', 'name' => 'foo', 'is_variadic' => true]); + $node = $this->createFunctionExpression('foo', true); $this->getArguments($node, ['Twig\Tests\Node\Expression\custom_call_test_function', []]); } @@ -116,7 +117,7 @@ public function testResolveArgumentsWithMissingParameterForArbitraryArgumentsOnO $this->expectException(\LogicException::class); $this->expectExceptionMessageMatches('#^The last parameter of "Twig\\\\Tests\\\\Node\\\\Expression\\\\CallableTestClass\\:\\:__invoke" for function "foo" must be an array with default value, eg\\. "array \\$arg \\= \\[\\]"\\.$#'); - $node = new Node_Expression_Call([], ['type' => 'function', 'name' => 'foo', 'is_variadic' => true]); + $node = $this->createFunctionExpression('foo', true); $this->getArguments($node, [new CallableTestClass(), []]); } @@ -139,9 +140,14 @@ private function getArguments($call, $args) return $m->invokeArgs($call, $args); } + + private function createFunctionExpression($name, $isVariadic = false): Node_Expression_Call + { + return new Node_Expression_Call(new TwigFunction($name, null, ['is_variadic' => $isVariadic]), new Node([]), 0); + } } -class Node_Expression_Call extends CallExpression +class Node_Expression_Call extends FunctionExpression { } diff --git a/tests/Node/Expression/Filter/RawTest.php b/tests/Node/Expression/Filter/RawTest.php index 72999fd4481..558d17f35c9 100644 --- a/tests/Node/Expression/Filter/RawTest.php +++ b/tests/Node/Expression/Filter/RawTest.php @@ -22,7 +22,8 @@ public function testConstructor() $filter = new RawFilter($node = new ConstantExpression('foo', 12)); $this->assertSame(12, $filter->getTemplateLine()); - $this->assertSame('raw', $filter->getNode('filter')->getAttribute('value')); + $this->assertSame('raw', $filter->getAttribute('name')); + $this->assertSame('raw', $filter->getNode('filter', false)->getAttribute('value')); $this->assertSame($node, $filter->getNode('node')); $this->assertCount(0, $filter->getNode('arguments')); } diff --git a/tests/Node/Expression/FilterTest.php b/tests/Node/Expression/FilterTest.php index 3e5feebe238..5159c359b55 100644 --- a/tests/Node/Expression/FilterTest.php +++ b/tests/Node/Expression/FilterTest.php @@ -28,12 +28,12 @@ class FilterTest extends NodeTestCase public function testConstructor() { $expr = new ConstantExpression('foo', 1); - $name = new ConstantExpression('upper', 1); + $filter = new TwigFilter($name = 'upper'); $args = new Node(); - $node = new FilterExpression($expr, $name, $args, 1); + $node = new FilterExpression($expr, $filter, $args, 1); $this->assertEquals($expr, $node->getNode('node')); - $this->assertEquals($name, $node->getNode('filter')); + $this->assertEquals($name, $node->getAttribute('name')); $this->assertEquals($args, $node->getNode('arguments')); } @@ -49,14 +49,14 @@ public function getTests() $tests = []; $expr = new ConstantExpression('foo', 1); - $node = $this->createFilter($expr, 'upper'); - $node = $this->createFilter($node, 'number_format', [new ConstantExpression(2, 1), new ConstantExpression('.', 1), new ConstantExpression(',', 1)]); + $node = $this->createFilter($environment, $expr, 'upper'); + $node = $this->createFilter($environment, $node, 'number_format', [new ConstantExpression(2, 1), new ConstantExpression('.', 1), new ConstantExpression(',', 1)]); $tests[] = [$node, '$this->extensions[\'Twig\Extension\CoreExtension\']->formatNumber(Twig\Extension\CoreExtension::upper($this->env->getCharset(), "foo"), 2, ".", ",")']; // named arguments $date = new ConstantExpression(0, 1); - $node = $this->createFilter($date, 'date', [ + $node = $this->createFilter($environment, $date, 'date', [ 'timezone' => new ConstantExpression('America/Chicago', 1), 'format' => new ConstantExpression('d/m/Y H:i:s P', 1), ]); @@ -64,55 +64,55 @@ public function getTests() // skip an optional argument $date = new ConstantExpression(0, 1); - $node = $this->createFilter($date, 'date', [ + $node = $this->createFilter($environment, $date, 'date', [ 'timezone' => new ConstantExpression('America/Chicago', 1), ]); $tests[] = [$node, '$this->extensions[\'Twig\Extension\CoreExtension\']->formatDate(0, null, "America/Chicago")']; // underscores vs camelCase for named arguments $string = new ConstantExpression('abc', 1); - $node = $this->createFilter($string, 'reverse', [ + $node = $this->createFilter($environment, $string, 'reverse', [ 'preserve_keys' => new ConstantExpression(true, 1), ]); $tests[] = [$node, 'Twig\Extension\CoreExtension::reverse($this->env->getCharset(), "abc", true)']; - $node = $this->createFilter($string, 'reverse', [ + $node = $this->createFilter($environment, $string, 'reverse', [ 'preserveKeys' => new ConstantExpression(true, 1), ]); $tests[] = [$node, 'Twig\Extension\CoreExtension::reverse($this->env->getCharset(), "abc", true)']; // filter as an anonymous function - $node = $this->createFilter(new ConstantExpression('foo', 1), 'anonymous'); + $node = $this->createFilter($environment, new ConstantExpression('foo', 1), 'anonymous'); $tests[] = [$node, '$this->env->getFilter(\'anonymous\')->getCallable()("foo")']; // needs environment - $node = $this->createFilter($string, 'bar'); + $node = $this->createFilter($environment, $string, 'bar'); $tests[] = [$node, 'twig_tests_filter_dummy($this->env, "abc")', $environment]; - $node = $this->createFilter($string, 'bar_closure'); + $node = $this->createFilter($environment, $string, 'bar_closure'); $tests[] = [$node, twig_tests_filter_dummy::class.'($this->env, "abc")', $environment]; - $node = $this->createFilter($string, 'bar', [new ConstantExpression('bar', 1)]); + $node = $this->createFilter($environment, $string, 'bar', [new ConstantExpression('bar', 1)]); $tests[] = [$node, 'twig_tests_filter_dummy($this->env, "abc", "bar")', $environment]; // arbitrary named arguments - $node = $this->createFilter($string, 'barbar'); + $node = $this->createFilter($environment, $string, 'barbar'); $tests[] = [$node, 'Twig\Tests\Node\Expression\twig_tests_filter_barbar($context, "abc")', $environment]; - $node = $this->createFilter($string, 'barbar', ['foo' => new ConstantExpression('bar', 1)]); + $node = $this->createFilter($environment, $string, 'barbar', ['foo' => new ConstantExpression('bar', 1)]); $tests[] = [$node, 'Twig\Tests\Node\Expression\twig_tests_filter_barbar($context, "abc", null, null, ["foo" => "bar"])', $environment]; - $node = $this->createFilter($string, 'barbar', ['arg2' => new ConstantExpression('bar', 1)]); + $node = $this->createFilter($environment, $string, 'barbar', ['arg2' => new ConstantExpression('bar', 1)]); $tests[] = [$node, 'Twig\Tests\Node\Expression\twig_tests_filter_barbar($context, "abc", null, "bar")', $environment]; if (\PHP_VERSION_ID >= 80111) { - $node = $this->createFilter($string, 'first_class_callable_static'); + $node = $this->createFilter($environment, $string, 'first_class_callable_static'); $tests[] = [$node, 'Twig\Tests\Node\Expression\FilterTestExtension::staticMethod("abc")', $environment]; - $node = $this->createFilter($string, 'first_class_callable_object'); + $node = $this->createFilter($environment, $string, 'first_class_callable_object'); $tests[] = [$node, '$this->extensions[\'Twig\Tests\Node\Expression\FilterTestExtension\']->objectMethod("abc")', $environment]; } - $node = $this->createFilter($string, 'barbar', [ + $node = $this->createFilter($environment, $string, 'barbar', [ new ConstantExpression('1', 1), new ConstantExpression('2', 1), new ConstantExpression('3', 1), @@ -121,13 +121,13 @@ public function getTests() $tests[] = [$node, 'Twig\Tests\Node\Expression\twig_tests_filter_barbar($context, "abc", "1", "2", ["3", "foo" => "bar"])', $environment]; // from extension - $node = $this->createFilter($string, 'foo'); + $node = $this->createFilter($environment, $string, 'foo'); $tests[] = [$node, \sprintf('$this->extensions[\'%s\']->foo("abc")', \get_class($this->getExtension())), $environment]; - $node = $this->createFilter($string, 'foobar'); + $node = $this->createFilter($environment, $string, 'foobar'); $tests[] = [$node, '$this->env->getFilter(\'foobar\')->getCallable()("abc")', $environment]; - $node = $this->createFilter($string, 'magic_static'); + $node = $this->createFilter($environment, $string, 'magic_static'); $tests[] = [$node, 'Twig\Tests\Node\Expression\ChildMagicCallStub::magicStaticCall("abc")', $environment]; return $tests; @@ -139,7 +139,7 @@ public function testCompileWithWrongNamedArgumentName() $this->expectExceptionMessage('Unknown argument "foobar" for filter "date(format, timezone)" at line 1.'); $date = new ConstantExpression(0, 1); - $node = $this->createFilter($date, 'date', [ + $node = $this->createFilter($this->getEnvironment(), $date, 'date', [ 'foobar' => new ConstantExpression('America/Chicago', 1), ]); @@ -153,7 +153,7 @@ public function testCompileWithMissingNamedArgument() $this->expectExceptionMessage('Value for argument "from" is required for filter "replace" at line 1.'); $value = new ConstantExpression(0, 1); - $node = $this->createFilter($value, 'replace', [ + $node = $this->createFilter($this->getEnvironment(), $value, 'replace', [ 'to' => new ConstantExpression('foo', 1), ]); @@ -161,12 +161,9 @@ public function testCompileWithMissingNamedArgument() $compiler->compile($node); } - protected function createFilter($node, $name, array $arguments = []) + protected function createFilter(Environment $env, $node, $name, array $arguments = []) { - $name = new ConstantExpression($name, 1); - $arguments = new Node($arguments); - - return new FilterExpression($node, $name, $arguments, 1); + return new FilterExpression($node, $env->getFilter($name), new Node($arguments), 1); } protected function getEnvironment() diff --git a/tests/Node/Expression/FunctionTest.php b/tests/Node/Expression/FunctionTest.php index d8df87696ba..3d16916d62f 100644 --- a/tests/Node/Expression/FunctionTest.php +++ b/tests/Node/Expression/FunctionTest.php @@ -25,7 +25,7 @@ public function testConstructor() { $name = 'function'; $args = new Node(); - $node = new FunctionExpression($name, $args, 1); + $node = new FunctionExpression(new TwigFunction($name), $args, 1); $this->assertEquals($name, $node->getAttribute('name')); $this->assertEquals($args, $node->getNode('arguments')); @@ -37,51 +37,51 @@ public function getTests() $tests = []; - $node = $this->createFunction('foo'); + $node = $this->createFunction($environment, 'foo'); $tests[] = [$node, 'twig_tests_function_dummy()', $environment]; - $node = $this->createFunction('foo_closure'); + $node = $this->createFunction($environment, 'foo_closure'); $tests[] = [$node, twig_tests_function_dummy::class.'()', $environment]; - $node = $this->createFunction('foo', [new ConstantExpression('bar', 1), new ConstantExpression('foobar', 1)]); + $node = $this->createFunction($environment, 'foo', [new ConstantExpression('bar', 1), new ConstantExpression('foobar', 1)]); $tests[] = [$node, 'twig_tests_function_dummy("bar", "foobar")', $environment]; - $node = $this->createFunction('bar'); + $node = $this->createFunction($environment, 'bar'); $tests[] = [$node, 'twig_tests_function_dummy($this->env)', $environment]; - $node = $this->createFunction('bar', [new ConstantExpression('bar', 1)]); + $node = $this->createFunction($environment, 'bar', [new ConstantExpression('bar', 1)]); $tests[] = [$node, 'twig_tests_function_dummy($this->env, "bar")', $environment]; - $node = $this->createFunction('foofoo'); + $node = $this->createFunction($environment, 'foofoo'); $tests[] = [$node, 'twig_tests_function_dummy($context)', $environment]; - $node = $this->createFunction('foofoo', [new ConstantExpression('bar', 1)]); + $node = $this->createFunction($environment, 'foofoo', [new ConstantExpression('bar', 1)]); $tests[] = [$node, 'twig_tests_function_dummy($context, "bar")', $environment]; - $node = $this->createFunction('foobar'); + $node = $this->createFunction($environment, 'foobar'); $tests[] = [$node, 'twig_tests_function_dummy($this->env, $context)', $environment]; - $node = $this->createFunction('foobar', [new ConstantExpression('bar', 1)]); + $node = $this->createFunction($environment, 'foobar', [new ConstantExpression('bar', 1)]); $tests[] = [$node, 'twig_tests_function_dummy($this->env, $context, "bar")', $environment]; // named arguments - $node = $this->createFunction('date', [ + $node = $this->createFunction($environment, 'date', [ 'timezone' => new ConstantExpression('America/Chicago', 1), 'date' => new ConstantExpression(0, 1), ]); $tests[] = [$node, '$this->extensions[\'Twig\Extension\CoreExtension\']->convertDate(0, "America/Chicago")']; // arbitrary named arguments - $node = $this->createFunction('barbar'); + $node = $this->createFunction($environment, 'barbar'); $tests[] = [$node, 'Twig\Tests\Node\Expression\twig_tests_function_barbar()', $environment]; - $node = $this->createFunction('barbar', ['foo' => new ConstantExpression('bar', 1)]); + $node = $this->createFunction($environment, 'barbar', ['foo' => new ConstantExpression('bar', 1)]); $tests[] = [$node, 'Twig\Tests\Node\Expression\twig_tests_function_barbar(null, null, ["foo" => "bar"])', $environment]; - $node = $this->createFunction('barbar', ['arg2' => new ConstantExpression('bar', 1)]); + $node = $this->createFunction($environment, 'barbar', ['arg2' => new ConstantExpression('bar', 1)]); $tests[] = [$node, 'Twig\Tests\Node\Expression\twig_tests_function_barbar(null, "bar")', $environment]; - $node = $this->createFunction('barbar', [ + $node = $this->createFunction($environment, 'barbar', [ new ConstantExpression('1', 1), new ConstantExpression('2', 1), new ConstantExpression('3', 1), @@ -90,15 +90,15 @@ public function getTests() $tests[] = [$node, 'Twig\Tests\Node\Expression\twig_tests_function_barbar("1", "2", ["3", "foo" => "bar"])', $environment]; // function as an anonymous function - $node = $this->createFunction('anonymous', [new ConstantExpression('foo', 1)]); + $node = $this->createFunction($environment, 'anonymous', [new ConstantExpression('foo', 1)]); $tests[] = [$node, '$this->env->getFunction(\'anonymous\')->getCallable()("foo")']; return $tests; } - protected function createFunction($name, array $arguments = []) + protected function createFunction(Environment $env, $name, array $arguments = []) { - return new FunctionExpression($name, new Node($arguments), 1); + return new FunctionExpression($env->getFunction($name), new Node($arguments), 1); } protected function getEnvironment() diff --git a/tests/Node/Expression/TestTest.php b/tests/Node/Expression/TestTest.php index 93c6cd53303..124a8766c97 100644 --- a/tests/Node/Expression/TestTest.php +++ b/tests/Node/Expression/TestTest.php @@ -25,9 +25,9 @@ class TestTest extends NodeTestCase public function testConstructor() { $expr = new ConstantExpression('foo', 1); - $name = new ConstantExpression('null', 1); + $name = 'test_name'; $args = new Node(); - $node = new TestExpression($expr, $name, $args, 1); + $node = new TestExpression($expr, new TwigTest($name), $args, 1); $this->assertEquals($expr, $node->getNode('node')); $this->assertEquals($args, $node->getNode('arguments')); @@ -41,25 +41,25 @@ public function getTests() $tests = []; $expr = new ConstantExpression('foo', 1); - $node = new NullTest($expr, 'null', new Node([]), 1); + $node = new NullTest($expr, $this->getEnvironment()->getTest('null'), new Node([]), 1); $tests[] = [$node, '(null === "foo")']; // test as an anonymous function - $node = $this->createTest(new ConstantExpression('foo', 1), 'anonymous', [new ConstantExpression('foo', 1)]); + $node = $this->createTest($environment, new ConstantExpression('foo', 1), 'anonymous', [new ConstantExpression('foo', 1)]); $tests[] = [$node, '$this->env->getTest(\'anonymous\')->getCallable()("foo", "foo")']; // arbitrary named arguments $string = new ConstantExpression('abc', 1); - $node = $this->createTest($string, 'barbar'); + $node = $this->createTest($environment, $string, 'barbar'); $tests[] = [$node, 'Twig\Tests\Node\Expression\twig_tests_test_barbar("abc")', $environment]; - $node = $this->createTest($string, 'barbar', ['foo' => new ConstantExpression('bar', 1)]); + $node = $this->createTest($environment, $string, 'barbar', ['foo' => new ConstantExpression('bar', 1)]); $tests[] = [$node, 'Twig\Tests\Node\Expression\twig_tests_test_barbar("abc", null, null, ["foo" => "bar"])', $environment]; - $node = $this->createTest($string, 'barbar', ['arg2' => new ConstantExpression('bar', 1)]); + $node = $this->createTest($environment, $string, 'barbar', ['arg2' => new ConstantExpression('bar', 1)]); $tests[] = [$node, 'Twig\Tests\Node\Expression\twig_tests_test_barbar("abc", null, "bar")', $environment]; - $node = $this->createTest($string, 'barbar', [ + $node = $this->createTest($environment, $string, 'barbar', [ new ConstantExpression('1', 1), new ConstantExpression('2', 1), new ConstantExpression('3', 1), @@ -70,9 +70,9 @@ public function getTests() return $tests; } - protected function createTest($node, $name, array $arguments = []) + protected function createTest(Environment $env, $node, $name, array $arguments = []) { - return new TestExpression($node, $name, new Node($arguments), 1); + return new TestExpression($node, $env->getTest($name), new Node($arguments), 1); } protected function getEnvironment() From 397b7c4782fa7083fea64d2769634af9a9acf927 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Wed, 14 Aug 2024 23:42:25 +0200 Subject: [PATCH 346/812] Fix RawFilter --- src/Node/Expression/Filter/RawFilter.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Node/Expression/Filter/RawFilter.php b/src/Node/Expression/Filter/RawFilter.php index 26f53370b6d..12809ff4fdc 100644 --- a/src/Node/Expression/Filter/RawFilter.php +++ b/src/Node/Expression/Filter/RawFilter.php @@ -26,7 +26,7 @@ class RawFilter extends FilterExpression #[FirstClassTwigCallableReady] public function __construct(Node $node, TwigFilter|ConstantExpression|null $filter = null, ?Node $arguments = null, int $lineno = 0, ?string $tag = null) { - parent::__construct($node, $filter ?: new TwigFilter('raw'), $arguments ?: new Node(), $lineno ?: $node->getTemplateLine(), $tag ?: $node->getNodeTag()); + parent::__construct($node, $filter ?: new TwigFilter('raw', null, ['is_safe' => ['all']]), $arguments ?: new Node(), $lineno ?: $node->getTemplateLine(), $tag ?: $node->getNodeTag()); } public function compile(Compiler $compiler): void From c662e0ce274e2d753cddc28d734942289a2361af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Andr=C3=A9?= Date: Thu, 15 Aug 2024 19:11:55 +0200 Subject: [PATCH 347/812] Remove remaining const optimizations After #4198 some `Token::` const optimization remains. This PR remove them.. **in case** it was not intended --- src/ExpressionParser.php | 8 ++++---- src/Lexer.php | 14 +++++++------- src/Parser.php | 6 +++--- src/TokenParser/AutoEscapeTokenParser.php | 6 +++--- src/TokenParser/BlockTokenParser.php | 4 ++-- src/TokenParser/DoTokenParser.php | 2 +- src/TokenParser/EmbedTokenParser.php | 6 +++--- src/TokenParser/ExtendsTokenParser.php | 2 +- src/TokenParser/FlushTokenParser.php | 2 +- src/TokenParser/ForTokenParser.php | 6 +++--- src/TokenParser/FromTokenParser.php | 2 +- src/TokenParser/IfTokenParser.php | 8 ++++---- src/TokenParser/ImportTokenParser.php | 2 +- src/TokenParser/IncludeTokenParser.php | 2 +- src/TokenParser/MacroTokenParser.php | 4 ++-- src/TokenParser/SandboxTokenParser.php | 4 ++-- src/TokenParser/SetTokenParser.php | 6 +++--- src/TokenParser/UseTokenParser.php | 2 +- src/TokenParser/WithTokenParser.php | 6 +++--- src/TokenStream.php | 2 +- 20 files changed, 47 insertions(+), 47 deletions(-) diff --git a/src/ExpressionParser.php b/src/ExpressionParser.php index 4af952d0abd..c2477986b0e 100644 --- a/src/ExpressionParser.php +++ b/src/ExpressionParser.php @@ -252,7 +252,7 @@ public function parsePrimaryExpression() break; case Token::STRING_TYPE: - case /* Token::INTERPOLATION_START_TYPE */ 10: + case Token::INTERPOLATION_START_TYPE: $node = $this->parseStringExpression(); break; @@ -304,9 +304,9 @@ public function parseStringExpression() if ($nextCanBeString && $token = $stream->nextIf(Token::STRING_TYPE)) { $nodes[] = new ConstantExpression($token->getValue(), $token->getLine()); $nextCanBeString = false; - } elseif ($stream->nextIf(/* Token::INTERPOLATION_START_TYPE */ 10)) { + } elseif ($stream->nextIf(Token::INTERPOLATION_START_TYPE)) { $nodes[] = $this->parseExpression(); - $stream->expect(/* Token::INTERPOLATION_END_TYPE */ 11); + $stream->expect(Token::INTERPOLATION_END_TYPE); $nextCanBeString = true; } else { break; @@ -775,7 +775,7 @@ private function getTest(int $line): TwigTest $name = $stream->expect(Token::NAME_TYPE)->getValue(); if (!$test = $this->env->getTest($name)) { - if ($stream->test(/* Token::NAME_TYPE */ 5)) { + if ($stream->test(Token::NAME_TYPE)) { // try 2-words tests $name = $name.' '.$this->parser->getCurrentToken()->getValue(); diff --git a/src/Lexer.php b/src/Lexer.php index 4554b3d427f..d264dea96bc 100644 --- a/src/Lexer.php +++ b/src/Lexer.php @@ -215,7 +215,7 @@ public function tokenize(Source $source): TokenStream } } - $this->pushToken(/* Token::EOF_TYPE */ -1); + $this->pushToken(Token::EOF_TYPE); if (!empty($this->brackets)) { [$expect, $lineno] = array_pop($this->brackets); @@ -276,14 +276,14 @@ private function lexData(): void $this->moveCursor($match[0]); $this->lineno = (int) $match[1]; } else { - $this->pushToken(/* Token::BLOCK_START_TYPE */ 1); + $this->pushToken(Token::BLOCK_START_TYPE); $this->pushState(self::STATE_BLOCK); $this->currentVarBlockLine = $this->lineno; } break; case $this->options['tag_variable'][0]: - $this->pushToken(/* Token::VAR_START_TYPE */ 2); + $this->pushToken(Token::VAR_START_TYPE); $this->pushState(self::STATE_VAR); $this->currentVarBlockLine = $this->lineno; break; @@ -293,7 +293,7 @@ private function lexData(): void private function lexBlock(): void { if (empty($this->brackets) && preg_match($this->regexes['lex_block'], $this->code, $match, 0, $this->cursor)) { - $this->pushToken(/* Token::BLOCK_END_TYPE */ 3); + $this->pushToken(Token::BLOCK_END_TYPE); $this->moveCursor($match[0]); $this->popState(); } else { @@ -304,7 +304,7 @@ private function lexBlock(): void private function lexVar(): void { if (empty($this->brackets) && preg_match($this->regexes['lex_var'], $this->code, $match, 0, $this->cursor)) { - $this->pushToken(/* Token::VAR_END_TYPE */ 4); + $this->pushToken(Token::VAR_END_TYPE); $this->moveCursor($match[0]); $this->popState(); } else { @@ -484,7 +484,7 @@ private function lexString(): void { if (preg_match($this->regexes['interpolation_start'], $this->code, $match, 0, $this->cursor)) { $this->brackets[] = [$this->options['interpolation'][0], $this->lineno]; - $this->pushToken(/* Token::INTERPOLATION_START_TYPE */ 10); + $this->pushToken(Token::INTERPOLATION_START_TYPE); $this->moveCursor($match[0]); $this->pushState(self::STATE_INTERPOLATION); } elseif (preg_match(self::REGEX_DQ_STRING_PART, $this->code, $match, 0, $this->cursor) && '' !== $match[0]) { @@ -509,7 +509,7 @@ private function lexInterpolation(): void $bracket = end($this->brackets); if ($this->options['interpolation'][0] === $bracket[0] && preg_match($this->regexes['interpolation_end'], $this->code, $match, 0, $this->cursor)) { array_pop($this->brackets); - $this->pushToken(/* Token::INTERPOLATION_END_TYPE */ 11); + $this->pushToken(Token::INTERPOLATION_END_TYPE); $this->moveCursor($match[0]); $this->popState(); } else { diff --git a/src/Parser.php b/src/Parser.php index ff163ba7cc3..06d7781e7df 100644 --- a/src/Parser.php +++ b/src/Parser.php @@ -126,14 +126,14 @@ public function subparse($test, bool $dropNeedle = false): Node $rv[] = new TextNode($token->getValue(), $token->getLine()); break; - case /* Token::VAR_START_TYPE */ 2: + case Token::VAR_START_TYPE: $token = $this->stream->next(); $expr = $this->expressionParser->parseExpression(); - $this->stream->expect(/* Token::VAR_END_TYPE */ 4); + $this->stream->expect(Token::VAR_END_TYPE); $rv[] = new PrintNode($expr, $token->getLine()); break; - case /* Token::BLOCK_START_TYPE */ 1: + case Token::BLOCK_START_TYPE: $this->stream->next(); $token = $this->getCurrentToken(); diff --git a/src/TokenParser/AutoEscapeTokenParser.php b/src/TokenParser/AutoEscapeTokenParser.php index b674bea4ab0..46790454a29 100644 --- a/src/TokenParser/AutoEscapeTokenParser.php +++ b/src/TokenParser/AutoEscapeTokenParser.php @@ -29,7 +29,7 @@ public function parse(Token $token): Node $lineno = $token->getLine(); $stream = $this->parser->getStream(); - if ($stream->test(/* Token::BLOCK_END_TYPE */ 3)) { + if ($stream->test(Token::BLOCK_END_TYPE)) { $value = 'html'; } else { $expr = $this->parser->getExpressionParser()->parseExpression(); @@ -39,9 +39,9 @@ public function parse(Token $token): Node $value = $expr->getAttribute('value'); } - $stream->expect(/* Token::BLOCK_END_TYPE */ 3); + $stream->expect(Token::BLOCK_END_TYPE); $body = $this->parser->subparse([$this, 'decideBlockEnd'], true); - $stream->expect(/* Token::BLOCK_END_TYPE */ 3); + $stream->expect(Token::BLOCK_END_TYPE); return new AutoEscapeNode($value, $body, $lineno, $this->getTag()); } diff --git a/src/TokenParser/BlockTokenParser.php b/src/TokenParser/BlockTokenParser.php index c9286927724..0b7126274be 100644 --- a/src/TokenParser/BlockTokenParser.php +++ b/src/TokenParser/BlockTokenParser.php @@ -43,7 +43,7 @@ public function parse(Token $token): Node $this->parser->pushLocalScope(); $this->parser->pushBlockStack($name); - if ($stream->nextIf(/* Token::BLOCK_END_TYPE */ 3)) { + if ($stream->nextIf(Token::BLOCK_END_TYPE)) { $body = $this->parser->subparse([$this, 'decideBlockEnd'], true); if ($token = $stream->nextIf(Token::NAME_TYPE)) { $value = $token->getValue(); @@ -57,7 +57,7 @@ public function parse(Token $token): Node new PrintNode($this->parser->getExpressionParser()->parseExpression(), $lineno), ]); } - $stream->expect(/* Token::BLOCK_END_TYPE */ 3); + $stream->expect(Token::BLOCK_END_TYPE); $block->setNode('body', $body); $this->parser->popBlockStack(); diff --git a/src/TokenParser/DoTokenParser.php b/src/TokenParser/DoTokenParser.php index 32c8f12ff86..6b5c304981f 100644 --- a/src/TokenParser/DoTokenParser.php +++ b/src/TokenParser/DoTokenParser.php @@ -26,7 +26,7 @@ public function parse(Token $token): Node { $expr = $this->parser->getExpressionParser()->parseExpression(); - $this->parser->getStream()->expect(/* Token::BLOCK_END_TYPE */ 3); + $this->parser->getStream()->expect(Token::BLOCK_END_TYPE); return new DoNode($expr, $token->getLine(), $this->getTag()); } diff --git a/src/TokenParser/EmbedTokenParser.php b/src/TokenParser/EmbedTokenParser.php index 0f840ce1d5c..f42f07afc49 100644 --- a/src/TokenParser/EmbedTokenParser.php +++ b/src/TokenParser/EmbedTokenParser.php @@ -41,10 +41,10 @@ public function parse(Token $token): Node // inject a fake parent to make the parent() function work $stream->injectTokens([ - new Token(/* Token::BLOCK_START_TYPE */ 1, '', $token->getLine()), + new Token(Token::BLOCK_START_TYPE, '', $token->getLine()), new Token(Token::NAME_TYPE, 'extends', $token->getLine()), $parentToken, - new Token(/* Token::BLOCK_END_TYPE */ 3, '', $token->getLine()), + new Token(Token::BLOCK_END_TYPE, '', $token->getLine()), ]); $module = $this->parser->parse($stream, [$this, 'decideBlockEnd'], true); @@ -56,7 +56,7 @@ public function parse(Token $token): Node $this->parser->embedTemplate($module); - $stream->expect(/* Token::BLOCK_END_TYPE */ 3); + $stream->expect(Token::BLOCK_END_TYPE); return new EmbedNode($module->getTemplateName(), $module->getAttribute('index'), $variables, $only, $ignoreMissing, $token->getLine(), $this->getTag()); } diff --git a/src/TokenParser/ExtendsTokenParser.php b/src/TokenParser/ExtendsTokenParser.php index 0ca46dd29f7..7368459de92 100644 --- a/src/TokenParser/ExtendsTokenParser.php +++ b/src/TokenParser/ExtendsTokenParser.php @@ -40,7 +40,7 @@ public function parse(Token $token): Node } $this->parser->setParent($this->parser->getExpressionParser()->parseExpression()); - $stream->expect(/* Token::BLOCK_END_TYPE */ 3); + $stream->expect(Token::BLOCK_END_TYPE); return new Node(); } diff --git a/src/TokenParser/FlushTokenParser.php b/src/TokenParser/FlushTokenParser.php index 02c74aa134b..03e98abb484 100644 --- a/src/TokenParser/FlushTokenParser.php +++ b/src/TokenParser/FlushTokenParser.php @@ -26,7 +26,7 @@ final class FlushTokenParser extends AbstractTokenParser { public function parse(Token $token): Node { - $this->parser->getStream()->expect(/* Token::BLOCK_END_TYPE */ 3); + $this->parser->getStream()->expect(Token::BLOCK_END_TYPE); return new FlushNode($token->getLine(), $this->getTag()); } diff --git a/src/TokenParser/ForTokenParser.php b/src/TokenParser/ForTokenParser.php index 67c8edf2ae0..f939c10b334 100644 --- a/src/TokenParser/ForTokenParser.php +++ b/src/TokenParser/ForTokenParser.php @@ -38,15 +38,15 @@ public function parse(Token $token): Node $stream->expect(Token::OPERATOR_TYPE, 'in'); $seq = $this->parser->getExpressionParser()->parseExpression(); - $stream->expect(/* Token::BLOCK_END_TYPE */ 3); + $stream->expect(Token::BLOCK_END_TYPE); $body = $this->parser->subparse([$this, 'decideForFork']); if ('else' == $stream->next()->getValue()) { - $stream->expect(/* Token::BLOCK_END_TYPE */ 3); + $stream->expect(Token::BLOCK_END_TYPE); $else = $this->parser->subparse([$this, 'decideForEnd'], true); } else { $else = null; } - $stream->expect(/* Token::BLOCK_END_TYPE */ 3); + $stream->expect(Token::BLOCK_END_TYPE); if (\count($targets) > 1) { $keyTarget = $targets->getNode('0'); diff --git a/src/TokenParser/FromTokenParser.php b/src/TokenParser/FromTokenParser.php index 2793a5b9745..1619dd3d4b8 100644 --- a/src/TokenParser/FromTokenParser.php +++ b/src/TokenParser/FromTokenParser.php @@ -47,7 +47,7 @@ public function parse(Token $token): Node } } - $stream->expect(/* Token::BLOCK_END_TYPE */ 3); + $stream->expect(Token::BLOCK_END_TYPE); $var = new AssignNameExpression($this->parser->getVarName(), $token->getLine()); $node = new ImportNode($macro, $var, $token->getLine(), $this->getTag(), $this->parser->isMainScope()); diff --git a/src/TokenParser/IfTokenParser.php b/src/TokenParser/IfTokenParser.php index 569ccfaf11a..acb074d9586 100644 --- a/src/TokenParser/IfTokenParser.php +++ b/src/TokenParser/IfTokenParser.php @@ -37,7 +37,7 @@ public function parse(Token $token): Node $lineno = $token->getLine(); $expr = $this->parser->getExpressionParser()->parseExpression(); $stream = $this->parser->getStream(); - $stream->expect(/* Token::BLOCK_END_TYPE */ 3); + $stream->expect(Token::BLOCK_END_TYPE); $body = $this->parser->subparse([$this, 'decideIfFork']); $tests = [$expr, $body]; $else = null; @@ -46,13 +46,13 @@ public function parse(Token $token): Node while (!$end) { switch ($stream->next()->getValue()) { case 'else': - $stream->expect(/* Token::BLOCK_END_TYPE */ 3); + $stream->expect(Token::BLOCK_END_TYPE); $else = $this->parser->subparse([$this, 'decideIfEnd']); break; case 'elseif': $expr = $this->parser->getExpressionParser()->parseExpression(); - $stream->expect(/* Token::BLOCK_END_TYPE */ 3); + $stream->expect(Token::BLOCK_END_TYPE); $body = $this->parser->subparse([$this, 'decideIfFork']); $tests[] = $expr; $tests[] = $body; @@ -67,7 +67,7 @@ public function parse(Token $token): Node } } - $stream->expect(/* Token::BLOCK_END_TYPE */ 3); + $stream->expect(Token::BLOCK_END_TYPE); return new IfNode(new Node($tests), $else, $lineno, $this->getTag()); } diff --git a/src/TokenParser/ImportTokenParser.php b/src/TokenParser/ImportTokenParser.php index 2e54c454254..595875f941b 100644 --- a/src/TokenParser/ImportTokenParser.php +++ b/src/TokenParser/ImportTokenParser.php @@ -30,7 +30,7 @@ public function parse(Token $token): Node $macro = $this->parser->getExpressionParser()->parseExpression(); $this->parser->getStream()->expect(Token::NAME_TYPE, 'as'); $var = new AssignNameExpression($this->parser->getStream()->expect(Token::NAME_TYPE)->getValue(), $token->getLine()); - $this->parser->getStream()->expect(/* Token::BLOCK_END_TYPE */ 3); + $this->parser->getStream()->expect(Token::BLOCK_END_TYPE); $this->parser->addImportedSymbol('template', $var->getAttribute('name')); diff --git a/src/TokenParser/IncludeTokenParser.php b/src/TokenParser/IncludeTokenParser.php index 9c571719a9d..9c3bba042fa 100644 --- a/src/TokenParser/IncludeTokenParser.php +++ b/src/TokenParser/IncludeTokenParser.php @@ -57,7 +57,7 @@ protected function parseArguments() $only = true; } - $stream->expect(/* Token::BLOCK_END_TYPE */ 3); + $stream->expect(Token::BLOCK_END_TYPE); return [$variables, $only, $ignoreMissing]; } diff --git a/src/TokenParser/MacroTokenParser.php b/src/TokenParser/MacroTokenParser.php index b11590010c5..3def0e73442 100644 --- a/src/TokenParser/MacroTokenParser.php +++ b/src/TokenParser/MacroTokenParser.php @@ -36,7 +36,7 @@ public function parse(Token $token): Node $arguments = $this->parser->getExpressionParser()->parseArguments(true, true); - $stream->expect(/* Token::BLOCK_END_TYPE */ 3); + $stream->expect(Token::BLOCK_END_TYPE); $this->parser->pushLocalScope(); $body = $this->parser->subparse([$this, 'decideBlockEnd'], true); if ($token = $stream->nextIf(Token::NAME_TYPE)) { @@ -47,7 +47,7 @@ public function parse(Token $token): Node } } $this->parser->popLocalScope(); - $stream->expect(/* Token::BLOCK_END_TYPE */ 3); + $stream->expect(Token::BLOCK_END_TYPE); $this->parser->setMacro($name, new MacroNode($name, new BodyNode([$body]), $arguments, $lineno, $this->getTag())); diff --git a/src/TokenParser/SandboxTokenParser.php b/src/TokenParser/SandboxTokenParser.php index c919556eccb..f628b29fd9a 100644 --- a/src/TokenParser/SandboxTokenParser.php +++ b/src/TokenParser/SandboxTokenParser.php @@ -34,9 +34,9 @@ final class SandboxTokenParser extends AbstractTokenParser public function parse(Token $token): Node { $stream = $this->parser->getStream(); - $stream->expect(/* Token::BLOCK_END_TYPE */ 3); + $stream->expect(Token::BLOCK_END_TYPE); $body = $this->parser->subparse([$this, 'decideBlockEnd'], true); - $stream->expect(/* Token::BLOCK_END_TYPE */ 3); + $stream->expect(Token::BLOCK_END_TYPE); // in a sandbox tag, only include tags are allowed if (!$body instanceof IncludeNode) { diff --git a/src/TokenParser/SetTokenParser.php b/src/TokenParser/SetTokenParser.php index 8c75aa55d7d..71cd977c0f8 100644 --- a/src/TokenParser/SetTokenParser.php +++ b/src/TokenParser/SetTokenParser.php @@ -40,7 +40,7 @@ public function parse(Token $token): Node if ($stream->nextIf(Token::OPERATOR_TYPE, '=')) { $values = $this->parser->getExpressionParser()->parseMultitargetExpression(); - $stream->expect(/* Token::BLOCK_END_TYPE */ 3); + $stream->expect(Token::BLOCK_END_TYPE); if (\count($names) !== \count($values)) { throw new SyntaxError('When using set, you must have the same number of variables and assignments.', $stream->getCurrent()->getLine(), $stream->getSourceContext()); @@ -52,10 +52,10 @@ public function parse(Token $token): Node throw new SyntaxError('When using set with a block, you cannot have a multi-target.', $stream->getCurrent()->getLine(), $stream->getSourceContext()); } - $stream->expect(/* Token::BLOCK_END_TYPE */ 3); + $stream->expect(Token::BLOCK_END_TYPE); $values = $this->parser->subparse([$this, 'decideBlockEnd'], true); - $stream->expect(/* Token::BLOCK_END_TYPE */ 3); + $stream->expect(Token::BLOCK_END_TYPE); } return new SetNode($capture, $names, $values, $lineno, $this->getTag()); diff --git a/src/TokenParser/UseTokenParser.php b/src/TokenParser/UseTokenParser.php index 10714e7191d..abb647a5905 100644 --- a/src/TokenParser/UseTokenParser.php +++ b/src/TokenParser/UseTokenParser.php @@ -59,7 +59,7 @@ public function parse(Token $token): Node } } - $stream->expect(/* Token::BLOCK_END_TYPE */ 3); + $stream->expect(Token::BLOCK_END_TYPE); $this->parser->addTrait(new Node(['template' => $template, 'targets' => new Node($targets)])); diff --git a/src/TokenParser/WithTokenParser.php b/src/TokenParser/WithTokenParser.php index 0da6ef99f77..8c89a046bed 100644 --- a/src/TokenParser/WithTokenParser.php +++ b/src/TokenParser/WithTokenParser.php @@ -30,16 +30,16 @@ public function parse(Token $token): Node $variables = null; $only = false; - if (!$stream->test(/* Token::BLOCK_END_TYPE */ 3)) { + if (!$stream->test(Token::BLOCK_END_TYPE)) { $variables = $this->parser->getExpressionParser()->parseExpression(); $only = (bool) $stream->nextIf(Token::NAME_TYPE, 'only'); } - $stream->expect(/* Token::BLOCK_END_TYPE */ 3); + $stream->expect(Token::BLOCK_END_TYPE); $body = $this->parser->subparse([$this, 'decideWithEnd'], true); - $stream->expect(/* Token::BLOCK_END_TYPE */ 3); + $stream->expect(Token::BLOCK_END_TYPE); return new WithNode($body, $variables, $only, $token->getLine(), $this->getTag()); } diff --git a/src/TokenStream.php b/src/TokenStream.php index 9921f788d88..32357f9319a 100644 --- a/src/TokenStream.php +++ b/src/TokenStream.php @@ -110,7 +110,7 @@ public function test($primary, $secondary = null): bool */ public function isEOF(): bool { - return /* Token::EOF_TYPE */ -1 === $this->tokens[$this->current]->getType(); + return Token::EOF_TYPE === $this->tokens[$this->current]->getType(); } public function getCurrent(): Token From b431ecad3ae27a85dc2a58bbeb9a9db59a6fedcb Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Thu, 15 Aug 2024 20:14:56 +0200 Subject: [PATCH 348/812] Make Node::__toString() more readable --- src/AbstractTwigCallable.php | 5 +++++ src/Node/Node.php | 10 +++++++++- src/TwigCallableInterface.php | 2 +- tests/Node/NodeTest.php | 14 ++++++++++++++ 4 files changed, 29 insertions(+), 2 deletions(-) diff --git a/src/AbstractTwigCallable.php b/src/AbstractTwigCallable.php index f5739529bf6..756425a99e1 100644 --- a/src/AbstractTwigCallable.php +++ b/src/AbstractTwigCallable.php @@ -39,6 +39,11 @@ public function __construct(string $name, $callable = null, array $options = []) ], $options); } + public function __toString(): string + { + return sprintf('%s(%s)', static::class, $this->name); + } + public function getName(): string { return $this->name; diff --git a/src/Node/Node.php b/src/Node/Node.php index 5ef661f5e37..a9bd84a3ee8 100644 --- a/src/Node/Node.php +++ b/src/Node/Node.php @@ -15,6 +15,7 @@ use Twig\Attribute\YieldReady; use Twig\Compiler; use Twig\Source; +use Twig\TwigCallableInterface; /** * Represents a node in the AST. @@ -58,7 +59,14 @@ public function __toString() { $attributes = []; foreach ($this->attributes as $name => $value) { - $attributes[] = \sprintf('%s: %s', $name, \is_callable($value) ? '\Closure' : str_replace("\n", '', var_export($value, true))); + if (\is_callable($value)) { + $v = '\Closure'; + } elseif ($value instanceof \Stringable) { + $v = (string) $value; + } else { + $v = str_replace("\n", '', var_export($value, true)); + } + $attributes[] = \sprintf('%s: %s', $name, $v); } $repr = [static::class.'('.implode(', ', $attributes)]; diff --git a/src/TwigCallableInterface.php b/src/TwigCallableInterface.php index 7e5021242cb..7706eb4f22b 100644 --- a/src/TwigCallableInterface.php +++ b/src/TwigCallableInterface.php @@ -14,7 +14,7 @@ /** * @author Fabien Potencier */ -interface TwigCallableInterface +interface TwigCallableInterface extends \Stringable { public function getName(): string; diff --git a/tests/Node/NodeTest.php b/tests/Node/NodeTest.php index b4f8eee02aa..b224c36365d 100644 --- a/tests/Node/NodeTest.php +++ b/tests/Node/NodeTest.php @@ -15,6 +15,9 @@ use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; use Twig\Node\NameDeprecation; use Twig\Node\Node; +use Twig\TwigFilter; +use Twig\TwigFunction; +use Twig\TwigTest; class NodeTest extends TestCase { @@ -28,6 +31,17 @@ public function testToString() $this->assertEquals('Twig\Node\Node(value: \Closure)', (string) $node); } + public function testToStringWithTwigCallables() + { + $node = new Node([], [ + 'function' => new TwigFunction('a_function'), + 'filter' => new TwigFilter('a_filter'), + 'test' => new TwigTest('a_test'), + ], 1); + + $this->assertEquals('Twig\Node\Node(function: Twig\TwigFunction(a_function), filter: Twig\TwigFilter(a_filter), test: Twig\TwigTest(a_test))', (string) $node); + } + public function testAttributeDeprecationIgnore() { $node = new Node([], ['foo' => false]); From e1705f88313a85edf2d2ec49862240939e8fff41 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Thu, 15 Aug 2024 10:29:28 +0200 Subject: [PATCH 349/812] Extract a new CallableArgumentsExtractor class --- CHANGELOG | 1 + src/Node/Expression/CallExpression.php | 15 +- src/Util/CallableArgumentsExtractor.php | 215 ++++++++++++++++++ tests/Node/Expression/CallTest.php | 3 + tests/Util/CallableArgumentsExtractorTest.php | 156 +++++++++++++ 5 files changed, 388 insertions(+), 2 deletions(-) create mode 100644 src/Util/CallableArgumentsExtractor.php create mode 100644 tests/Util/CallableArgumentsExtractorTest.php diff --git a/CHANGELOG b/CHANGELOG index 810b3616722..3742c1e6976 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,6 @@ # 3.12.0 (2024-XX-XX) + * Add a `CallableArgumentsExtractor` class * Deprecate passing a name to `FunctionExpression`, `FilterExpression`, and `TestExpression`; pass a `TwigFunction`, `TwigFilter`, or `TestFilter` instead * Deprecate all Twig callable attributes on `TwigFunction`, `TwigFilter`, and `TestFilter` diff --git a/src/Node/Expression/CallExpression.php b/src/Node/Expression/CallExpression.php index 53561dd2026..91faf271acd 100644 --- a/src/Node/Expression/CallExpression.php +++ b/src/Node/Expression/CallExpression.php @@ -19,6 +19,7 @@ use Twig\TwigFilter; use Twig\TwigFunction; use Twig\TwigTest; +use Twig\Util\CallableArgumentsExtractor; use Twig\Util\ReflectionCallable; abstract class CallExpression extends AbstractExpression @@ -113,8 +114,7 @@ protected function compileArguments(Compiler $compiler, $isArray = false): void } if ($this->hasNode('arguments')) { - $callable = $twigCallable->getCallable(); - $arguments = $this->getArguments($callable, $this->getNode('arguments')); + $arguments = (new CallableArgumentsExtractor($this, $this->getTwigCallable()))->extractArguments($this->getNode('arguments')); foreach ($arguments as $node) { if (!$first) { $compiler->raw(', '); @@ -127,8 +127,13 @@ protected function compileArguments(Compiler $compiler, $isArray = false): void $compiler->raw($isArray ? ']' : ')'); } + /** + * @deprecated since 3.12, use Twig\Util\CallableArgumentsExtractor::getArguments() instead + */ protected function getArguments($callable, $arguments) { + trigger_deprecation('twig/twig', '3.12', 'The "%s()" method is deprecated, use Twig\Util\CallableArgumentsExtractor::getArguments() instead.', __METHOD__); + $callType = $this->getAttribute('type'); $callName = $this->getAttribute('name'); @@ -252,11 +257,17 @@ protected function getArguments($callable, $arguments) return $arguments; } + /** + * @deprecated since 3.12 + */ protected function normalizeName(string $name): string { + trigger_deprecation('twig/twig', '3.12', 'The "%s()" method is deprecated.', __METHOD__); + return strtolower(preg_replace(['/([A-Z]+)([A-Z][a-z])/', '/([a-z\d])([A-Z])/'], ['\\1_\\2', '\\1_\\2'], $name)); } + // To be removed in 4.0 private function getCallableParameters($callable, bool $isVariadic): array { $twigCallable = $this->getAttribute('twig_callable'); diff --git a/src/Util/CallableArgumentsExtractor.php b/src/Util/CallableArgumentsExtractor.php new file mode 100644 index 00000000000..5d1d38b03a1 --- /dev/null +++ b/src/Util/CallableArgumentsExtractor.php @@ -0,0 +1,215 @@ + + * + * @internal + */ +final class CallableArgumentsExtractor +{ + private string $type; + private string $name; + + public function __construct( + private Node $node, + private TwigCallableInterface $twigCallable, + ) { + $this->type = match (true) { + $twigCallable instanceof TwigFunction => 'function', + $twigCallable instanceof TwigFilter => 'filter', + $twigCallable instanceof TwigTest => 'test', + default => throw new \LogicException('Unknown callable type.'), + }; + $this->name = $twigCallable->getName(); + } + + /** + * @return array + */ + public function extractArguments(Node $arguments): array + { + $parameters = []; + $named = false; + foreach ($arguments as $name => $node) { + if (!\is_int($name)) { + $named = true; + $name = $this->normalizeName($name); + } elseif ($named) { + throw new SyntaxError(\sprintf('Positional arguments cannot be used after named arguments for %s "%s".', $this->type, $this->name), $this->node->getTemplateLine(), $this->node->getSourceContext()); + } + + $parameters[$name] = $node; + } + + if (!$named && !$this->twigCallable->isVariadic()) { + return $parameters; + } + + if (!$callable = $this->twigCallable->getCallable()) { + if ($named) { + $message = \sprintf('Named arguments are not supported for %s "%s".', $this->type, $this->name); + } else { + $message = \sprintf('Arbitrary positional arguments are not supported for %s "%s".', $this->type, $this->name); + } + + throw new \LogicException($message); + } + + [$callableParameters, $isPhpVariadic] = $this->getCallableParameters(); + $arguments = []; + $names = []; + $missingArguments = []; + $optionalArguments = []; + $pos = 0; + foreach ($callableParameters as $callableParameter) { + $name = $this->normalizeName($callableParameter->name); + if (\PHP_VERSION_ID >= 80000 && 'range' === $callable) { + if ('start' === $name) { + $name = 'low'; + } elseif ('end' === $name) { + $name = 'high'; + } + } + + $names[] = $name; + + if (\array_key_exists($name, $parameters)) { + if (\array_key_exists($pos, $parameters)) { + throw new SyntaxError(\sprintf('Argument "%s" is defined twice for %s "%s".', $name, $this->type, $this->name), $this->node->getTemplateLine(), $this->node->getSourceContext()); + } + + if (\count($missingArguments)) { + throw new SyntaxError(\sprintf( + 'Argument "%s" could not be assigned for %s "%s(%s)" because it is mapped to an internal PHP function which cannot determine default value for optional argument%s "%s".', + $name, $this->type, $this->name, implode(', ', $names), \count($missingArguments) > 1 ? 's' : '', implode('", "', $missingArguments) + ), $this->node->getTemplateLine(), $this->node->getSourceContext()); + } + + $arguments = array_merge($arguments, $optionalArguments); + $arguments[] = $parameters[$name]; + unset($parameters[$name]); + $optionalArguments = []; + } elseif (\array_key_exists($pos, $parameters)) { + $arguments = array_merge($arguments, $optionalArguments); + $arguments[] = $parameters[$pos]; + unset($parameters[$pos]); + $optionalArguments = []; + ++$pos; + } elseif ($callableParameter->isDefaultValueAvailable()) { + $optionalArguments[] = new ConstantExpression($callableParameter->getDefaultValue(), -1); + } elseif ($callableParameter->isOptional()) { + if (empty($parameters)) { + break; + } else { + $missingArguments[] = $name; + } + } else { + throw new SyntaxError(\sprintf('Value for argument "%s" is required for %s "%s".', $name, $this->type, $this->name), $this->node->getTemplateLine(), $this->node->getSourceContext()); + } + } + + if ($this->twigCallable->isVariadic()) { + $arbitraryArguments = $isPhpVariadic ? new VariadicExpression([], -1) : new ArrayExpression([], -1); + foreach ($parameters as $key => $value) { + if (\is_int($key)) { + $arbitraryArguments->addElement($value); + } else { + $arbitraryArguments->addElement($value, new ConstantExpression($key, -1)); + } + unset($parameters[$key]); + } + + if ($arbitraryArguments->count()) { + $arguments = array_merge($arguments, $optionalArguments); + $arguments[] = $arbitraryArguments; + } + } + + if (!empty($parameters)) { + $unknownParameter = null; + foreach ($parameters as $parameter) { + if ($parameter instanceof Node) { + $unknownParameter = $parameter; + break; + } + } + + throw new SyntaxError( + \sprintf( + 'Unknown argument%s "%s" for %s "%s(%s)".', + \count($parameters) > 1 ? 's' : '', implode('", "', array_keys($parameters)), $this->type, $this->name, implode(', ', $names) + ), + $unknownParameter ? $unknownParameter->getTemplateLine() : $this->node->getTemplateLine(), + $unknownParameter ? $unknownParameter->getSourceContext() : $this->node->getSourceContext() + ); + } + + return $arguments; + } + + private function normalizeName(string $name): string + { + return strtolower(preg_replace(['/([A-Z]+)([A-Z][a-z])/', '/([a-z\d])([A-Z])/'], ['\\1_\\2', '\\1_\\2'], $name)); + } + + private function getCallableParameters(): array + { + $rc = new ReflectionCallable($this->twigCallable->getCallable(), $this->type, $this->name); + $r = $rc->getReflector(); + $callableName = $rc->getName(); + + $parameters = $r->getParameters(); + if ($this->node->hasNode('node')) { + array_shift($parameters); + } + if ($this->twigCallable->needsCharset()) { + array_shift($parameters); + } + if ($this->twigCallable->needsEnvironment()) { + array_shift($parameters); + } + if ($this->twigCallable->needsContext()) { + array_shift($parameters); + } + foreach ($this->twigCallable->getArguments() as $argument) { + array_shift($parameters); + } + + $isPhpVariadic = false; + if ($this->twigCallable->isVariadic()) { + $argument = end($parameters); + $isArray = $argument && $argument->hasType() && $argument->getType() instanceof \ReflectionNamedType && 'array' === $argument->getType()->getName(); + if ($isArray && $argument->isDefaultValueAvailable() && [] === $argument->getDefaultValue()) { + array_pop($parameters); + } elseif ($argument && $argument->isVariadic()) { + array_pop($parameters); + $isPhpVariadic = true; + } else { + throw new \LogicException(\sprintf('The last parameter of "%s" for %s "%s" must be an array with default value, eg. "array $arg = []".', $callableName, $this->type, $this->name)); + } + } + + return [$parameters, $isPhpVariadic]; + } +} diff --git a/tests/Node/Expression/CallTest.php b/tests/Node/Expression/CallTest.php index 427f6ed5618..8486a3c4c16 100644 --- a/tests/Node/Expression/CallTest.php +++ b/tests/Node/Expression/CallTest.php @@ -17,6 +17,9 @@ use Twig\Node\Node; use Twig\TwigFunction; +/** + * @group legacy + */ class CallTest extends TestCase { public function testGetArguments() diff --git a/tests/Util/CallableArgumentsExtractorTest.php b/tests/Util/CallableArgumentsExtractorTest.php new file mode 100644 index 00000000000..016118099c5 --- /dev/null +++ b/tests/Util/CallableArgumentsExtractorTest.php @@ -0,0 +1,156 @@ +assertEquals(['U', null], $this->getArguments('date', 'date', ['format' => 'U', 'timestamp' => null])); + } + + public function testGetArgumentsWhenPositionalArgumentsAfterNamedArguments() + { + $this->expectException(SyntaxError::class); + $this->expectExceptionMessage('Positional arguments cannot be used after named arguments for function "date".'); + + $this->getArguments('date', 'date', ['timestamp' => 123456, 'Y-m-d']); + } + + public function testGetArgumentsWhenArgumentIsDefinedTwice() + { + $this->expectException(SyntaxError::class); + $this->expectExceptionMessage('Argument "format" is defined twice for function "date".'); + + $this->getArguments('date', 'date', ['Y-m-d', 'format' => 'U']); + } + + public function testGetArgumentsWithWrongNamedArgumentName() + { + $this->expectException(SyntaxError::class); + $this->expectExceptionMessage('Unknown argument "unknown" for function "date(format, timestamp)".'); + + $this->getArguments('date', 'date', ['Y-m-d', 'timestamp' => null, 'unknown' => '']); + } + + public function testGetArgumentsWithWrongNamedArgumentNames() + { + $this->expectException(SyntaxError::class); + $this->expectExceptionMessage('Unknown arguments "unknown1", "unknown2" for function "date(format, timestamp)".'); + + $this->getArguments('date', 'date', ['Y-m-d', 'timestamp' => null, 'unknown1' => '', 'unknown2' => '']); + } + + public function testResolveArgumentsWithMissingValueForOptionalArgument() + { + if (\PHP_VERSION_ID >= 80000) { + $this->markTestSkipped('substr_compare() has a default value in 8.0, so the test does not work anymore, one should find another PHP built-in function for this test to work in PHP 8.'); + } + + $this->expectException(SyntaxError::class); + $this->expectExceptionMessage('Argument "case_sensitivity" could not be assigned for function "substr_compare(main_str, str, offset, length, case_sensitivity)" because it is mapped to an internal PHP function which cannot determine default value for optional argument "length".'); + + $this->getArguments('substr_compare', 'substr_compare', ['abcd', 'bc', 'offset' => 1, 'case_sensitivity' => true]); + } + + public function testResolveArgumentsOnlyNecessaryArgumentsForCustomFunction() + { + $this->assertEquals(['arg1'], $this->getArguments('custom_function', [$this, 'customFunction'], ['arg1' => 'arg1'])); + } + + public function testGetArgumentsForStaticMethod() + { + $this->assertEquals(['arg1'], $this->getArguments('custom_static_function', __CLASS__.'::customStaticFunction', ['arg1' => 'arg1'])); + } + + public function testResolveArgumentsWithMissingParameterForArbitraryArguments() + { + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('The last parameter of "Twig\\Tests\\Util\\CallableArgumentsExtractorTest::customFunctionWithArbitraryArguments" for function "foo" must be an array with default value, eg. "array $arg = []".'); + + $this->getArguments('foo', [$this, 'customFunctionWithArbitraryArguments'], [], true); + } + + public function testGetArgumentsWithInvalidCallable() + { + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('Callback for function "foo" is not callable in the current scope.'); + $this->getArguments('foo', '', [], true); + } + + public function testResolveArgumentsWithMissingParameterForArbitraryArgumentsOnFunction() + { + $this->expectException(\LogicException::class); + $this->expectExceptionMessageMatches('#^The last parameter of "Twig\\\\Tests\\\\Util\\\\custom_call_test_function" for function "foo" must be an array with default value, eg\\. "array \\$arg \\= \\[\\]"\\.$#'); + + $this->getArguments('foo', 'Twig\Tests\Util\custom_call_test_function', [], true); + } + + public function testResolveArgumentsWithMissingParameterForArbitraryArgumentsOnObject() + { + $this->expectException(\LogicException::class); + $this->expectExceptionMessageMatches('#^The last parameter of "Twig\\\\Tests\\\\Util\\\\CallableTestClass\\:\\:__invoke" for function "foo" must be an array with default value, eg\\. "array \\$arg \\= \\[\\]"\\.$#'); + + $this->getArguments('foo', new CallableTestClass(), [], true); + } + + public static function customStaticFunction($arg1, $arg2 = 'default', $arg3 = []) + { + } + + public function customFunction($arg1, $arg2 = 'default', $arg3 = []) + { + } + + public function customFunctionWithArbitraryArguments() + { + } + + private function getArguments(string $name, $callable, array $args, bool $isVariadic = false): array + { + $function = new TwigFunction($name, $callable, ['is_variadic' => $isVariadic]); + $node = new ExpressionCall($function, new Node([]), 0); + foreach ($args as $name => $arg) { + $args[$name] = new ConstantExpression($arg, 0); + } + + $arguments = (new CallableArgumentsExtractor($node, $function))->extractArguments(new Node($args)); + foreach ($arguments as $name => $argument) { + $arguments[$name] = $argument->getAttribute('value'); + } + + return $arguments; + } +} + +class ExpressionCall extends FunctionExpression +{ +} + +class CallableTestClass +{ + public function __invoke($required) + { + } +} + +function custom_call_test_function($required) +{ +} From 0823d23488954c8a564ff868e6f292fd588a16f1 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Thu, 15 Aug 2024 22:44:06 +0200 Subject: [PATCH 350/812] Throw a SyntaxError exception at compile time when a Twig callable has not the minimum number of required arguments --- CHANGELOG | 1 + src/AbstractTwigCallable.php | 5 ++ src/TwigCallableInterface.php | 2 + src/TwigFilter.php | 5 ++ src/Util/CallableArgumentsExtractor.php | 66 ++++++++++--------- tests/ErrorTest.php | 22 ------- .../functions/cycle_without_enough_args.test | 8 +++ .../Fixtures/functions/max_without_args.test | 8 +++ tests/Node/DeprecatedTest.php | 8 ++- tests/Node/Expression/FilterTest.php | 6 +- tests/Node/Expression/FunctionTest.php | 24 +++---- tests/Util/CallableArgumentsExtractorTest.php | 6 +- 12 files changed, 87 insertions(+), 74 deletions(-) create mode 100644 tests/Fixtures/functions/cycle_without_enough_args.test create mode 100644 tests/Fixtures/functions/max_without_args.test diff --git a/CHANGELOG b/CHANGELOG index 3742c1e6976..34a9e54de10 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,6 @@ # 3.12.0 (2024-XX-XX) + * Throw a SyntaxError exception at compile time when a Twig callable has not the minimum number of required arguments * Add a `CallableArgumentsExtractor` class * Deprecate passing a name to `FunctionExpression`, `FilterExpression`, and `TestExpression`; pass a `TwigFunction`, `TwigFilter`, or `TestFilter` instead diff --git a/src/AbstractTwigCallable.php b/src/AbstractTwigCallable.php index 756425a99e1..d9fc45f2f6a 100644 --- a/src/AbstractTwigCallable.php +++ b/src/AbstractTwigCallable.php @@ -128,4 +128,9 @@ public function getAlternative(): ?string { return $this->options['alternative']; } + + public function getMinimalNumberOfRequiredArguments(): int + { + return ($this->options['needs_charset'] ? 1 : 0) + ($this->options['needs_environment'] ? 1 : 0) + ($this->options['needs_context'] ? 1 : 0) + \count($this->arguments); + } } diff --git a/src/TwigCallableInterface.php b/src/TwigCallableInterface.php index 7706eb4f22b..13a10cd3be2 100644 --- a/src/TwigCallableInterface.php +++ b/src/TwigCallableInterface.php @@ -46,4 +46,6 @@ public function getDeprecatingPackage(): string; public function getDeprecatedVersion(): string; public function getAlternative(): ?string; + + public function getMinimalNumberOfRequiredArguments(): int; } diff --git a/src/TwigFilter.php b/src/TwigFilter.php index 36bb5d817db..7eb66f713a4 100644 --- a/src/TwigFilter.php +++ b/src/TwigFilter.php @@ -61,4 +61,9 @@ public function getPreEscape(): ?string { return $this->options['pre_escape']; } + + public function getMinimalNumberOfRequiredArguments(): int + { + return parent::getMinimalNumberOfRequiredArguments() + 1; + } } diff --git a/src/Util/CallableArgumentsExtractor.php b/src/Util/CallableArgumentsExtractor.php index 5d1d38b03a1..1c82446420b 100644 --- a/src/Util/CallableArgumentsExtractor.php +++ b/src/Util/CallableArgumentsExtractor.php @@ -49,7 +49,8 @@ public function __construct( */ public function extractArguments(Node $arguments): array { - $parameters = []; + $rc = new ReflectionCallable($this->twigCallable->getCallable(), $this->type, $this->name); + $extractedArguments = []; $named = false; foreach ($arguments as $name => $node) { if (!\is_int($name)) { @@ -59,24 +60,27 @@ public function extractArguments(Node $arguments): array throw new SyntaxError(\sprintf('Positional arguments cannot be used after named arguments for %s "%s".', $this->type, $this->name), $this->node->getTemplateLine(), $this->node->getSourceContext()); } - $parameters[$name] = $node; + $extractedArguments[$name] = $node; } if (!$named && !$this->twigCallable->isVariadic()) { - return $parameters; + $min = $this->twigCallable->getMinimalNumberOfRequiredArguments(); + if (count($extractedArguments) < $rc->getReflector()->getNumberOfRequiredParameters() - $min) { + throw new SyntaxError(\sprintf('Value for argument "%s" is required for %s "%s".', $rc->getReflector()->getParameters()[$min + count($extractedArguments)]->getName(), $this->type, $this->name), $this->node->getTemplateLine(), $this->node->getSourceContext()); + } + + return $extractedArguments; } if (!$callable = $this->twigCallable->getCallable()) { if ($named) { - $message = \sprintf('Named arguments are not supported for %s "%s".', $this->type, $this->name); - } else { - $message = \sprintf('Arbitrary positional arguments are not supported for %s "%s".', $this->type, $this->name); + throw new SyntaxError(\sprintf('Named arguments are not supported for %s "%s".', $this->type, $this->name)); } - throw new \LogicException($message); + throw new SyntaxError(\sprintf('Arbitrary positional arguments are not supported for %s "%s".', $this->type, $this->name)); } - [$callableParameters, $isPhpVariadic] = $this->getCallableParameters(); + [$callableParameters, $isPhpVariadic] = $this->getCallableParameters($rc); $arguments = []; $names = []; $missingArguments = []; @@ -94,8 +98,8 @@ public function extractArguments(Node $arguments): array $names[] = $name; - if (\array_key_exists($name, $parameters)) { - if (\array_key_exists($pos, $parameters)) { + if (\array_key_exists($name, $extractedArguments)) { + if (\array_key_exists($pos, $extractedArguments)) { throw new SyntaxError(\sprintf('Argument "%s" is defined twice for %s "%s".', $name, $this->type, $this->name), $this->node->getTemplateLine(), $this->node->getSourceContext()); } @@ -107,23 +111,23 @@ public function extractArguments(Node $arguments): array } $arguments = array_merge($arguments, $optionalArguments); - $arguments[] = $parameters[$name]; - unset($parameters[$name]); + $arguments[] = $extractedArguments[$name]; + unset($extractedArguments[$name]); $optionalArguments = []; - } elseif (\array_key_exists($pos, $parameters)) { + } elseif (\array_key_exists($pos, $extractedArguments)) { $arguments = array_merge($arguments, $optionalArguments); - $arguments[] = $parameters[$pos]; - unset($parameters[$pos]); + $arguments[] = $extractedArguments[$pos]; + unset($extractedArguments[$pos]); $optionalArguments = []; ++$pos; } elseif ($callableParameter->isDefaultValueAvailable()) { $optionalArguments[] = new ConstantExpression($callableParameter->getDefaultValue(), -1); } elseif ($callableParameter->isOptional()) { - if (empty($parameters)) { + if (!$extractedArguments) { break; - } else { - $missingArguments[] = $name; } + + $missingArguments[] = $name; } else { throw new SyntaxError(\sprintf('Value for argument "%s" is required for %s "%s".', $name, $this->type, $this->name), $this->node->getTemplateLine(), $this->node->getSourceContext()); } @@ -131,13 +135,13 @@ public function extractArguments(Node $arguments): array if ($this->twigCallable->isVariadic()) { $arbitraryArguments = $isPhpVariadic ? new VariadicExpression([], -1) : new ArrayExpression([], -1); - foreach ($parameters as $key => $value) { + foreach ($extractedArguments as $key => $value) { if (\is_int($key)) { $arbitraryArguments->addElement($value); } else { $arbitraryArguments->addElement($value, new ConstantExpression($key, -1)); } - unset($parameters[$key]); + unset($extractedArguments[$key]); } if ($arbitraryArguments->count()) { @@ -146,11 +150,11 @@ public function extractArguments(Node $arguments): array } } - if (!empty($parameters)) { - $unknownParameter = null; - foreach ($parameters as $parameter) { - if ($parameter instanceof Node) { - $unknownParameter = $parameter; + if ($extractedArguments) { + $unknownArgument = null; + foreach ($extractedArguments as $extractedArgument) { + if ($extractedArgument instanceof Node) { + $unknownArgument = $extractedArgument; break; } } @@ -158,10 +162,10 @@ public function extractArguments(Node $arguments): array throw new SyntaxError( \sprintf( 'Unknown argument%s "%s" for %s "%s(%s)".', - \count($parameters) > 1 ? 's' : '', implode('", "', array_keys($parameters)), $this->type, $this->name, implode(', ', $names) + \count($extractedArguments) > 1 ? 's' : '', implode('", "', array_keys($extractedArguments)), $this->type, $this->name, implode(', ', $names) ), - $unknownParameter ? $unknownParameter->getTemplateLine() : $this->node->getTemplateLine(), - $unknownParameter ? $unknownParameter->getSourceContext() : $this->node->getSourceContext() + $unknownArgument ? $unknownArgument->getTemplateLine() : $this->node->getTemplateLine(), + $unknownArgument ? $unknownArgument->getSourceContext() : $this->node->getSourceContext() ); } @@ -173,11 +177,9 @@ private function normalizeName(string $name): string return strtolower(preg_replace(['/([A-Z]+)([A-Z][a-z])/', '/([a-z\d])([A-Z])/'], ['\\1_\\2', '\\1_\\2'], $name)); } - private function getCallableParameters(): array + private function getCallableParameters(ReflectionCallable $rc): array { - $rc = new ReflectionCallable($this->twigCallable->getCallable(), $this->type, $this->name); $r = $rc->getReflector(); - $callableName = $rc->getName(); $parameters = $r->getParameters(); if ($this->node->hasNode('node')) { @@ -206,7 +208,7 @@ private function getCallableParameters(): array array_pop($parameters); $isPhpVariadic = true; } else { - throw new \LogicException(\sprintf('The last parameter of "%s" for %s "%s" must be an array with default value, eg. "array $arg = []".', $callableName, $this->type, $this->name)); + throw new SyntaxError(\sprintf('The last parameter of "%s" for %s "%s" must be an array with default value, eg. "array $arg = []".', $rc->getName(), $this->type, $this->name)); } } diff --git a/tests/ErrorTest.php b/tests/ErrorTest.php index 7db1259e4b9..423a1a58d6f 100644 --- a/tests/ErrorTest.php +++ b/tests/ErrorTest.php @@ -234,28 +234,6 @@ public function testTwigArrayReduceThrowsRuntimeExceptions() } } - public function testTwigArgumentCountErrorThrowsRuntimeExceptions() - { - $loader = new ArrayLoader([ - 'argument-error.html' => << true, 'cache' => false]); - - $template = $twig->load('argument-error.html'); - try { - $template->render(); - - $this->fail(); - } catch (RuntimeError $e) { - $this->assertEquals(2, $e->getTemplateLine()); - $this->assertEquals('argument-error.html', $e->getSourceContext()->getName()); - } - } - public function getErroredTemplates() { return [ diff --git a/tests/Fixtures/functions/cycle_without_enough_args.test b/tests/Fixtures/functions/cycle_without_enough_args.test new file mode 100644 index 00000000000..ba4b2adfb18 --- /dev/null +++ b/tests/Fixtures/functions/cycle_without_enough_args.test @@ -0,0 +1,8 @@ +--TEST-- +"cycle" function without enough args and a named argument +--TEMPLATE-- +{{ cycle(position=2) }} +--DATA-- +return [] +--EXCEPTION-- +Twig\Error\SyntaxError: Value for argument "values" is required for function "cycle" in "index.twig" at line 2. diff --git a/tests/Fixtures/functions/max_without_args.test b/tests/Fixtures/functions/max_without_args.test new file mode 100644 index 00000000000..b9522192bcd --- /dev/null +++ b/tests/Fixtures/functions/max_without_args.test @@ -0,0 +1,8 @@ +--TEST-- +"max" function without an argument throws a compile time exception +--TEMPLATE-- +{{ max() }} +--DATA-- +return [] +--EXCEPTION-- +Twig\Error\SyntaxError: Value for argument "value" is required for function "max" in "index.twig" at line 2. diff --git a/tests/Node/DeprecatedTest.php b/tests/Node/DeprecatedTest.php index d79e129f7d7..de72ee70100 100644 --- a/tests/Node/DeprecatedTest.php +++ b/tests/Node/DeprecatedTest.php @@ -67,7 +67,7 @@ public function getTests() ]; $environment = new Environment(new ArrayLoader()); - $environment->addFunction($function = new TwigFunction('foo', 'foo', [])); + $environment->addFunction($function = new TwigFunction('foo', 'Twig\Tests\Node\foo', [])); $expr = new FunctionExpression($function, new Node(), 1); $node = new DeprecatedNode($expr, 1, 'deprecated'); @@ -80,7 +80,7 @@ public function getTests() $tests[] = [$node, <<createFilter($environment, $string, 'bar'); - $tests[] = [$node, 'twig_tests_filter_dummy($this->env, "abc")', $environment]; + $tests[] = [$node, 'Twig\Tests\Node\Expression\twig_tests_filter_dummy($this->env, "abc")', $environment]; $node = $this->createFilter($environment, $string, 'bar_closure'); $tests[] = [$node, twig_tests_filter_dummy::class.'($this->env, "abc")', $environment]; $node = $this->createFilter($environment, $string, 'bar', [new ConstantExpression('bar', 1)]); - $tests[] = [$node, 'twig_tests_filter_dummy($this->env, "abc", "bar")', $environment]; + $tests[] = [$node, 'Twig\Tests\Node\Expression\twig_tests_filter_dummy($this->env, "abc", "bar")', $environment]; // arbitrary named arguments $node = $this->createFilter($environment, $string, 'barbar'); @@ -170,7 +170,7 @@ protected function getEnvironment() { $env = new Environment(new ArrayLoader()); $env->addFilter(new TwigFilter('anonymous', function () {})); - $env->addFilter(new TwigFilter('bar', 'twig_tests_filter_dummy', ['needs_environment' => true])); + $env->addFilter(new TwigFilter('bar', 'Twig\Tests\Node\Expression\twig_tests_filter_dummy', ['needs_environment' => true])); $env->addFilter(new TwigFilter('bar_closure', \Closure::fromCallable(twig_tests_filter_dummy::class), ['needs_environment' => true])); $env->addFilter(new TwigFilter('barbar', 'Twig\Tests\Node\Expression\twig_tests_filter_barbar', ['needs_context' => true, 'is_variadic' => true])); $env->addFilter(new TwigFilter('magic_static', __NAMESPACE__.'\ChildMagicCallStub::magicStaticCall')); diff --git a/tests/Node/Expression/FunctionTest.php b/tests/Node/Expression/FunctionTest.php index 3d16916d62f..eacafa2d133 100644 --- a/tests/Node/Expression/FunctionTest.php +++ b/tests/Node/Expression/FunctionTest.php @@ -38,31 +38,31 @@ public function getTests() $tests = []; $node = $this->createFunction($environment, 'foo'); - $tests[] = [$node, 'twig_tests_function_dummy()', $environment]; + $tests[] = [$node, 'Twig\Tests\Node\Expression\twig_tests_function_dummy()', $environment]; $node = $this->createFunction($environment, 'foo_closure'); $tests[] = [$node, twig_tests_function_dummy::class.'()', $environment]; $node = $this->createFunction($environment, 'foo', [new ConstantExpression('bar', 1), new ConstantExpression('foobar', 1)]); - $tests[] = [$node, 'twig_tests_function_dummy("bar", "foobar")', $environment]; + $tests[] = [$node, 'Twig\Tests\Node\Expression\twig_tests_function_dummy("bar", "foobar")', $environment]; $node = $this->createFunction($environment, 'bar'); - $tests[] = [$node, 'twig_tests_function_dummy($this->env)', $environment]; + $tests[] = [$node, 'Twig\Tests\Node\Expression\twig_tests_function_dummy($this->env)', $environment]; $node = $this->createFunction($environment, 'bar', [new ConstantExpression('bar', 1)]); - $tests[] = [$node, 'twig_tests_function_dummy($this->env, "bar")', $environment]; + $tests[] = [$node, 'Twig\Tests\Node\Expression\twig_tests_function_dummy($this->env, "bar")', $environment]; $node = $this->createFunction($environment, 'foofoo'); - $tests[] = [$node, 'twig_tests_function_dummy($context)', $environment]; + $tests[] = [$node, 'Twig\Tests\Node\Expression\twig_tests_function_dummy($context)', $environment]; $node = $this->createFunction($environment, 'foofoo', [new ConstantExpression('bar', 1)]); - $tests[] = [$node, 'twig_tests_function_dummy($context, "bar")', $environment]; + $tests[] = [$node, 'Twig\Tests\Node\Expression\twig_tests_function_dummy($context, "bar")', $environment]; $node = $this->createFunction($environment, 'foobar'); - $tests[] = [$node, 'twig_tests_function_dummy($this->env, $context)', $environment]; + $tests[] = [$node, 'Twig\Tests\Node\Expression\twig_tests_function_dummy($this->env, $context)', $environment]; $node = $this->createFunction($environment, 'foobar', [new ConstantExpression('bar', 1)]); - $tests[] = [$node, 'twig_tests_function_dummy($this->env, $context, "bar")', $environment]; + $tests[] = [$node, 'Twig\Tests\Node\Expression\twig_tests_function_dummy($this->env, $context, "bar")', $environment]; // named arguments $node = $this->createFunction($environment, 'date', [ @@ -105,11 +105,11 @@ protected function getEnvironment() { $env = new Environment(new ArrayLoader()); $env->addFunction(new TwigFunction('anonymous', function () {})); - $env->addFunction(new TwigFunction('foo', 'twig_tests_function_dummy', [])); + $env->addFunction(new TwigFunction('foo', 'Twig\Tests\Node\Expression\twig_tests_function_dummy', [])); $env->addFunction(new TwigFunction('foo_closure', \Closure::fromCallable(twig_tests_function_dummy::class), [])); - $env->addFunction(new TwigFunction('bar', 'twig_tests_function_dummy', ['needs_environment' => true])); - $env->addFunction(new TwigFunction('foofoo', 'twig_tests_function_dummy', ['needs_context' => true])); - $env->addFunction(new TwigFunction('foobar', 'twig_tests_function_dummy', ['needs_environment' => true, 'needs_context' => true])); + $env->addFunction(new TwigFunction('bar', 'Twig\Tests\Node\Expression\twig_tests_function_dummy', ['needs_environment' => true])); + $env->addFunction(new TwigFunction('foofoo', 'Twig\Tests\Node\Expression\twig_tests_function_dummy', ['needs_context' => true])); + $env->addFunction(new TwigFunction('foobar', 'Twig\Tests\Node\Expression\twig_tests_function_dummy', ['needs_environment' => true, 'needs_context' => true])); $env->addFunction(new TwigFunction('barbar', 'Twig\Tests\Node\Expression\twig_tests_function_barbar', ['is_variadic' => true])); return $env; diff --git a/tests/Util/CallableArgumentsExtractorTest.php b/tests/Util/CallableArgumentsExtractorTest.php index 016118099c5..cfea0f8a941 100644 --- a/tests/Util/CallableArgumentsExtractorTest.php +++ b/tests/Util/CallableArgumentsExtractorTest.php @@ -82,7 +82,7 @@ public function testGetArgumentsForStaticMethod() public function testResolveArgumentsWithMissingParameterForArbitraryArguments() { - $this->expectException(\LogicException::class); + $this->expectException(SyntaxError::class); $this->expectExceptionMessage('The last parameter of "Twig\\Tests\\Util\\CallableArgumentsExtractorTest::customFunctionWithArbitraryArguments" for function "foo" must be an array with default value, eg. "array $arg = []".'); $this->getArguments('foo', [$this, 'customFunctionWithArbitraryArguments'], [], true); @@ -97,7 +97,7 @@ public function testGetArgumentsWithInvalidCallable() public function testResolveArgumentsWithMissingParameterForArbitraryArgumentsOnFunction() { - $this->expectException(\LogicException::class); + $this->expectException(SyntaxError::class); $this->expectExceptionMessageMatches('#^The last parameter of "Twig\\\\Tests\\\\Util\\\\custom_call_test_function" for function "foo" must be an array with default value, eg\\. "array \\$arg \\= \\[\\]"\\.$#'); $this->getArguments('foo', 'Twig\Tests\Util\custom_call_test_function', [], true); @@ -105,7 +105,7 @@ public function testResolveArgumentsWithMissingParameterForArbitraryArgumentsOnF public function testResolveArgumentsWithMissingParameterForArbitraryArgumentsOnObject() { - $this->expectException(\LogicException::class); + $this->expectException(SyntaxError::class); $this->expectExceptionMessageMatches('#^The last parameter of "Twig\\\\Tests\\\\Util\\\\CallableTestClass\\:\\:__invoke" for function "foo" must be an array with default value, eg\\. "array \\$arg \\= \\[\\]"\\.$#'); $this->getArguments('foo', new CallableTestClass(), [], true); From 26bcadeaebd015b5b44c6ec2ec452c55a1419fba Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Fri, 16 Aug 2024 08:13:22 +0200 Subject: [PATCH 351/812] Fix missing code --- src/TwigTest.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/TwigTest.php b/src/TwigTest.php index 8690413336b..570ffd85212 100644 --- a/src/TwigTest.php +++ b/src/TwigTest.php @@ -54,4 +54,9 @@ public function hasOneMandatoryArgument(): bool { return (bool) $this->options['one_mandatory_argument']; } + + public function getMinimalNumberOfRequiredArguments(): int + { + return parent::getMinimalNumberOfRequiredArguments() + 1; + } } From 0c30e78b1c739d41b9306914cf1134f7b080ec81 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Mon, 5 Aug 2024 14:23:35 +0200 Subject: [PATCH 352/812] Add support for named arguments on special functions --- CHANGELOG | 1 + doc/functions/attribute.rst | 7 ++++++ doc/functions/block.rst | 6 +++++ src/ExpressionParser.php | 24 ++++++++++--------- tests/Fixtures/functions/attribute.test | 4 ++++ .../functions/attribute_with_wrong_args.test | 8 +++++++ tests/Fixtures/functions/block.test | 3 ++- .../functions/block_with_template.test | 6 +++++ .../functions/block_without_name.test | 2 +- 9 files changed, 48 insertions(+), 13 deletions(-) create mode 100644 tests/Fixtures/functions/attribute_with_wrong_args.test diff --git a/CHANGELOG b/CHANGELOG index 34a9e54de10..cac6c392010 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,6 @@ # 3.12.0 (2024-XX-XX) + * Add support for named arguments to the `block`, and `attribute` functions * Throw a SyntaxError exception at compile time when a Twig callable has not the minimum number of required arguments * Add a `CallableArgumentsExtractor` class * Deprecate passing a name to `FunctionExpression`, `FilterExpression`, and `TestExpression`; diff --git a/doc/functions/attribute.rst b/doc/functions/attribute.rst index e9d9a842ef3..832859de166 100644 --- a/doc/functions/attribute.rst +++ b/doc/functions/attribute.rst @@ -21,3 +21,10 @@ attribute: The resolution algorithm is the same as the one used for the ``.`` notation, except that the item can be any valid expression. + +Arguments +--------- + +* ``variable``: The variable +* ``attribute``: The attribute name +* ``arguments``: An array of arguments to pass to the call diff --git a/doc/functions/block.rst b/doc/functions/block.rst index 117e160f584..efc516eb1f6 100644 --- a/doc/functions/block.rst +++ b/doc/functions/block.rst @@ -32,6 +32,12 @@ current template: ... {% endif %} +Arguments +--------- + +* ``name``: The block name +* ``template``: The template where to look for the block + .. seealso:: :doc:`extends<../tags/extends>`, :doc:`parent<../functions/parent>` diff --git a/src/ExpressionParser.php b/src/ExpressionParser.php index c2477986b0e..f9031b9ca5e 100644 --- a/src/ExpressionParser.php +++ b/src/ExpressionParser.php @@ -33,6 +33,7 @@ use Twig\Node\Expression\Unary\NotUnary; use Twig\Node\Expression\Unary\PosUnary; use Twig\Node\Node; +use Twig\Util\CallableArgumentsExtractor; /** * Parses expressions. @@ -458,7 +459,6 @@ public function getFunctionNode($name, $line) { switch ($name) { case 'parent': - $this->parseArguments(); if (!\count($this->parser->getBlockStack())) { throw new SyntaxError('Calling "parent" outside a block is forbidden.', $line, $this->parser->getStream()->getSourceContext()); } @@ -467,21 +467,23 @@ public function getFunctionNode($name, $line) throw new SyntaxError('Calling "parent" on a template that does not extend nor "use" another template is forbidden.', $line, $this->parser->getStream()->getSourceContext()); } + $this->parseArguments(true); + return new ParentExpression($this->parser->peekBlockStack(), $line); case 'block': - $args = $this->parseArguments(); - if (\count($args) < 1) { - throw new SyntaxError('The "block" function takes one argument (the block name).', $line, $this->parser->getStream()->getSourceContext()); - } + $fakeNode = new Node(lineno: $line); + $fakeNode->setSourceContext($this->parser->getStream()->getSourceContext()); + $fakeFunction = new TwigFunction('block', fn ($name, $template = null) => null); + $args = (new CallableArgumentsExtractor($fakeNode, $fakeFunction))->extractArguments($this->parseArguments(true)); - return new BlockReferenceExpression($args->getNode('0'), \count($args) > 1 ? $args->getNode('1') : null, $line); + return new BlockReferenceExpression($args[0], $args[1] ?? null, $line); case 'attribute': - $args = $this->parseArguments(); - if (\count($args) < 2) { - throw new SyntaxError('The "attribute" function takes at least two arguments (the variable and the attributes).', $line, $this->parser->getStream()->getSourceContext()); - } + $fakeNode = new Node(lineno: $line); + $fakeNode->setSourceContext($this->parser->getStream()->getSourceContext()); + $fakeFunction = new TwigFunction('attribute', fn ($variable, $attribute, $arguments = []) => null); + $args = (new CallableArgumentsExtractor($fakeNode, $fakeFunction))->extractArguments($this->parseArguments(true)); - return new GetAttrExpression($args->getNode('0'), $args->getNode('1'), \count($args) > 2 ? $args->getNode('2') : null, Template::ANY_CALL, $line); + return new GetAttrExpression($args[0], $args[1], $args[2] ?? null, Template::ANY_CALL, $line); default: if (null !== $alias = $this->parser->getImportedSymbol('function', $name)) { $arguments = new ArrayExpression([], $line); diff --git a/tests/Fixtures/functions/attribute.test b/tests/Fixtures/functions/attribute.test index 4499ad4bdee..2f70b08a456 100644 --- a/tests/Fixtures/functions/attribute.test +++ b/tests/Fixtures/functions/attribute.test @@ -2,17 +2,21 @@ "attribute" function --TEMPLATE-- {{ attribute(obj, method) }} +{{ attribute(variable=obj, attribute=method) }} {{ attribute(array, item) }} {{ attribute(obj, "bar", ["a", "b"]) }} {{ attribute(obj, "bar", arguments) }} +{{ attribute(variable=obj, attribute="bar", arguments=arguments) }} {{ attribute(obj, method) is defined ? 'ok' : 'ko' }} {{ attribute(obj, nonmethod) is defined ? 'ok' : 'ko' }} --DATA-- return ['obj' => new Twig\Tests\TwigTestFoo(), 'method' => 'foo', 'array' => ['foo' => 'bar'], 'item' => 'foo', 'nonmethod' => 'xxx', 'arguments' => ['a', 'b']] --EXPECT-- foo +foo bar bar_a-b bar_a-b +bar_a-b ok ko diff --git a/tests/Fixtures/functions/attribute_with_wrong_args.test b/tests/Fixtures/functions/attribute_with_wrong_args.test new file mode 100644 index 00000000000..6e8a17cf8d6 --- /dev/null +++ b/tests/Fixtures/functions/attribute_with_wrong_args.test @@ -0,0 +1,8 @@ +--TEST-- +"attribute" function +--TEMPLATE-- +{{ attribute(var=var, template="tpl") }} +--DATA-- +return ['var' => null] +--EXCEPTION-- +Twig\Error\SyntaxError: Value for argument "variable" is required for function "attribute" in "index.twig" at line 2. diff --git a/tests/Fixtures/functions/block.test b/tests/Fixtures/functions/block.test index 1a4fd5492f8..bf8decd1e64 100644 --- a/tests/Fixtures/functions/block.test +++ b/tests/Fixtures/functions/block.test @@ -5,8 +5,9 @@ {% block bar %}BAR{% endblock %} --TEMPLATE(base.twig)-- {% block foo %}{{ block('bar') }}{% endblock %} +{% block baz %}{{ block(name='bar') }}{% endblock %} {% block bar %}BAR_BASE{% endblock %} --DATA-- return [] --EXPECT-- -BARBAR +BARBARBAR diff --git a/tests/Fixtures/functions/block_with_template.test b/tests/Fixtures/functions/block_with_template.test index 37cb7a4813f..2e0d2916c63 100644 --- a/tests/Fixtures/functions/block_with_template.test +++ b/tests/Fixtures/functions/block_with_template.test @@ -6,6 +6,10 @@ {{ block('foo', included_loaded_internal) }} {% set output = block('foo', 'included.twig') %} {{ output }} +{% set output = block(name='foo', template='included.twig') %} +{{ output }} +{% set output = block(template='included.twig', name='foo') %} +{{ output }} {% block foo %}NOT FOO{% endblock %} --TEMPLATE(included.twig)-- {% block foo %}FOO{% endblock %} @@ -19,4 +23,6 @@ FOO FOO FOO FOO +FOO +FOO NOT FOO diff --git a/tests/Fixtures/functions/block_without_name.test b/tests/Fixtures/functions/block_without_name.test index 236df945109..61896a8a22a 100644 --- a/tests/Fixtures/functions/block_without_name.test +++ b/tests/Fixtures/functions/block_without_name.test @@ -9,4 +9,4 @@ --DATA-- return [] --EXCEPTION-- -Twig\Error\SyntaxError: The "block" function takes one argument (the block name) in "base.twig" at line 2. +Twig\Error\SyntaxError: Value for argument "name" is required for function "block" in "base.twig" at line 2. From 2e43eaa4adeface6030460ab41b59191218815a6 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Fri, 16 Aug 2024 22:51:19 +0200 Subject: [PATCH 353/812] Fix some minor issues --- src/ExpressionParser.php | 2 +- src/Util/CallableArgumentsExtractor.php | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/ExpressionParser.php b/src/ExpressionParser.php index f9031b9ca5e..535c459a7dc 100644 --- a/src/ExpressionParser.php +++ b/src/ExpressionParser.php @@ -480,7 +480,7 @@ public function getFunctionNode($name, $line) case 'attribute': $fakeNode = new Node(lineno: $line); $fakeNode->setSourceContext($this->parser->getStream()->getSourceContext()); - $fakeFunction = new TwigFunction('attribute', fn ($variable, $attribute, $arguments = []) => null); + $fakeFunction = new TwigFunction('attribute', fn ($variable, $attribute, $arguments = null) => null); $args = (new CallableArgumentsExtractor($fakeNode, $fakeFunction))->extractArguments($this->parseArguments(true)); return new GetAttrExpression($args[0], $args[1], $args[2] ?? null, Template::ANY_CALL, $line); diff --git a/src/Util/CallableArgumentsExtractor.php b/src/Util/CallableArgumentsExtractor.php index 1c82446420b..1eb6a45249a 100644 --- a/src/Util/CallableArgumentsExtractor.php +++ b/src/Util/CallableArgumentsExtractor.php @@ -121,7 +121,7 @@ public function extractArguments(Node $arguments): array $optionalArguments = []; ++$pos; } elseif ($callableParameter->isDefaultValueAvailable()) { - $optionalArguments[] = new ConstantExpression($callableParameter->getDefaultValue(), -1); + $optionalArguments[] = new ConstantExpression($callableParameter->getDefaultValue(), $this->node->getTemplateLine()); } elseif ($callableParameter->isOptional()) { if (!$extractedArguments) { break; @@ -134,12 +134,12 @@ public function extractArguments(Node $arguments): array } if ($this->twigCallable->isVariadic()) { - $arbitraryArguments = $isPhpVariadic ? new VariadicExpression([], -1) : new ArrayExpression([], -1); + $arbitraryArguments = $isPhpVariadic ? new VariadicExpression([], $this->node->getTemplateLine()) : new ArrayExpression([], $this->node->getTemplateLine()); foreach ($extractedArguments as $key => $value) { if (\is_int($key)) { $arbitraryArguments->addElement($value); } else { - $arbitraryArguments->addElement($value, new ConstantExpression($key, -1)); + $arbitraryArguments->addElement($value, new ConstantExpression($key, $this->node->getTemplateLine())); } unset($extractedArguments[$key]); } From dbc934ba784f95ac4eff8e23a933324ca0d31fed Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sat, 17 Aug 2024 11:28:14 +0200 Subject: [PATCH 354/812] Fix typo --- doc/filters/markdown_to_html.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/filters/markdown_to_html.rst b/doc/filters/markdown_to_html.rst index 8e3fede00f4..886cda4d40a 100644 --- a/doc/filters/markdown_to_html.rst +++ b/doc/filters/markdown_to_html.rst @@ -7,7 +7,7 @@ The ``markdown_to_html`` filter converts a block of Markdown to HTML: {% apply markdown_to_html %} Title - ====== + ===== Hello! {% endapply %} @@ -19,7 +19,7 @@ removed consistently before conversion: {% apply markdown_to_html %} Title - ====== + ===== Hello! {% endapply %} From ff55738f1979ed748de88e26b87ebe319ffb59d1 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sat, 17 Aug 2024 17:59:11 +0200 Subject: [PATCH 355/812] Fix typos in CHANGELOG --- CHANGELOG | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index cac6c392010..966c83219f2 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,15 +1,15 @@ # 3.12.0 (2024-XX-XX) - * Add support for named arguments to the `block`, and `attribute` functions + * Add support for named arguments to the `block` and `attribute` functions * Throw a SyntaxError exception at compile time when a Twig callable has not the minimum number of required arguments * Add a `CallableArgumentsExtractor` class * Deprecate passing a name to `FunctionExpression`, `FilterExpression`, and `TestExpression`; pass a `TwigFunction`, `TwigFilter`, or `TestFilter` instead - * Deprecate all Twig callable attributes on `TwigFunction`, `TwigFilter`, and `TestFilter` + * Deprecate all Twig callable attributes on `FunctionExpression`, `FilterExpression`, and `TestExpression` * Deprecate the `filter` node of `FilterExpression` * Add the notion of Twig callables (functions, filters, and tests) * Bump minimum PHP version to 8.0 - * Fix integration tests when a test has more than on data/expect section and deprecations + * Fix integration tests when a test has more than one data/expect section and deprecations * Add the `enum_cases` function # 3.11.0 (2024-08-08) From 442807900342f83ab410be191317c99526c8f531 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Fri, 16 Aug 2024 23:16:48 +0200 Subject: [PATCH 356/812] Add an extension point for parsing special functions --- src/ExpressionParser.php | 69 +++++++++++---------------------- src/Extension/CoreExtension.php | 49 +++++++++++++++++++++++ src/TwigFunction.php | 6 +++ 3 files changed, 78 insertions(+), 46 deletions(-) diff --git a/src/ExpressionParser.php b/src/ExpressionParser.php index 535c459a7dc..7477d0bec75 100644 --- a/src/ExpressionParser.php +++ b/src/ExpressionParser.php @@ -457,60 +457,37 @@ public function parsePostfixExpression($node) public function getFunctionNode($name, $line) { - switch ($name) { - case 'parent': - if (!\count($this->parser->getBlockStack())) { - throw new SyntaxError('Calling "parent" outside a block is forbidden.', $line, $this->parser->getStream()->getSourceContext()); - } - - if (!$this->parser->getParent() && !$this->parser->hasTraits()) { - throw new SyntaxError('Calling "parent" on a template that does not extend nor "use" another template is forbidden.', $line, $this->parser->getStream()->getSourceContext()); - } - - $this->parseArguments(true); - - return new ParentExpression($this->parser->peekBlockStack(), $line); - case 'block': - $fakeNode = new Node(lineno: $line); - $fakeNode->setSourceContext($this->parser->getStream()->getSourceContext()); - $fakeFunction = new TwigFunction('block', fn ($name, $template = null) => null); - $args = (new CallableArgumentsExtractor($fakeNode, $fakeFunction))->extractArguments($this->parseArguments(true)); - - return new BlockReferenceExpression($args[0], $args[1] ?? null, $line); - case 'attribute': - $fakeNode = new Node(lineno: $line); - $fakeNode->setSourceContext($this->parser->getStream()->getSourceContext()); - $fakeFunction = new TwigFunction('attribute', fn ($variable, $attribute, $arguments = null) => null); - $args = (new CallableArgumentsExtractor($fakeNode, $fakeFunction))->extractArguments($this->parseArguments(true)); + if (null !== $alias = $this->parser->getImportedSymbol('function', $name)) { + $arguments = new ArrayExpression([], $line); + foreach ($this->parseArguments() as $n) { + $arguments->addElement($n); + } - return new GetAttrExpression($args[0], $args[1], $args[2] ?? null, Template::ANY_CALL, $line); - default: - if (null !== $alias = $this->parser->getImportedSymbol('function', $name)) { - $arguments = new ArrayExpression([], $line); - foreach ($this->parseArguments() as $n) { - $arguments->addElement($n); - } + $node = new MethodCallExpression($alias['node'], $alias['name'], $arguments, $line); + $node->setAttribute('safe', true); - $node = new MethodCallExpression($alias['node'], $alias['name'], $arguments, $line); - $node->setAttribute('safe', true); + return $node; + } - return $node; - } + $args = $this->parseArguments(true); + $function = $this->getFunction($name, $line); - $args = $this->parseArguments(true); - $function = $this->getFunction($name, $line); + if ($function->getParserCallable()) { + $fakeNode = new Node(lineno: $line); + $fakeNode->setSourceContext($this->parser->getStream()->getSourceContext()); - $ready = true; - if (!isset($this->readyNodes[$class = $function->getNodeClass()])) { - $this->readyNodes[$class] = (bool) (new \ReflectionClass($class))->getConstructor()->getAttributes(FirstClassTwigCallableReady::class); - } + return ($function->getParserCallable())($this->parser, $fakeNode, $args, $line); + } - if (!$ready = $this->readyNodes[$class]) { - trigger_deprecation('twig/twig', '3.12', 'Twig node "%s" is not marked as ready for passing a "TwigFunction" in the constructor instead of its name; please update your code and then add #[FirstClassTwigCallableReady] attribute to the constructor.', $class); - } + if (!isset($this->readyNodes[$class = $function->getNodeClass()])) { + $this->readyNodes[$class] = (bool) (new \ReflectionClass($class))->getConstructor()->getAttributes(FirstClassTwigCallableReady::class); + } - return new $class($ready ? $function : $function->getName(), $args, $line); + if (!$ready = $this->readyNodes[$class]) { + trigger_deprecation('twig/twig', '3.12', 'Twig node "%s" is not marked as ready for passing a "TwigFunction" in the constructor instead of its name; please update your code and then add #[FirstClassTwigCallableReady] attribute to the constructor.', $class); } + + return new $class($ready ? $function : $function->getName(), $args, $line); } public function parseSubscriptExpression($node) diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index 5f3b753c1a8..1efb7ecea7e 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -14,8 +14,10 @@ use Twig\Environment; use Twig\Error\LoaderError; use Twig\Error\RuntimeError; +use Twig\Error\SyntaxError; use Twig\ExpressionParser; use Twig\Markup; +use Twig\Node\Expression\AbstractExpression; use Twig\Node\Expression\Binary\AddBinary; use Twig\Node\Expression\Binary\AndBinary; use Twig\Node\Expression\Binary\BitwiseAndBinary; @@ -44,9 +46,12 @@ use Twig\Node\Expression\Binary\SpaceshipBinary; use Twig\Node\Expression\Binary\StartsWithBinary; use Twig\Node\Expression\Binary\SubBinary; +use Twig\Node\Expression\BlockReferenceExpression; use Twig\Node\Expression\Filter\DefaultFilter; use Twig\Node\Expression\FunctionNode\EnumCasesFunction; +use Twig\Node\Expression\GetAttrExpression; use Twig\Node\Expression\NullCoalesceExpression; +use Twig\Node\Expression\ParentExpression; use Twig\Node\Expression\Test\ConstantTest; use Twig\Node\Expression\Test\DefinedTest; use Twig\Node\Expression\Test\DivisiblebyTest; @@ -57,7 +62,9 @@ use Twig\Node\Expression\Unary\NegUnary; use Twig\Node\Expression\Unary\NotUnary; use Twig\Node\Expression\Unary\PosUnary; +use Twig\Node\Node; use Twig\NodeVisitor\MacroAutoImportNodeVisitor; +use Twig\Parser; use Twig\Source; use Twig\Template; use Twig\TemplateWrapper; @@ -80,6 +87,7 @@ use Twig\TwigFilter; use Twig\TwigFunction; use Twig\TwigTest; +use Twig\Util\CallableArgumentsExtractor; final class CoreExtension extends AbstractExtension { @@ -238,6 +246,9 @@ public function getFilters(): array public function getFunctions(): array { return [ + new TwigFunction('parent', null, ['parser_callable' => [$this, 'parseParentFunction']]), + new TwigFunction('block', null, ['parser_callable' => [$this, 'parseBlockFunction']]), + new TwigFunction('attribute', null, ['parser_callable' => [$this, 'parseAttributeFunction']]), new TwigFunction('max', 'max'), new TwigFunction('min', 'min'), new TwigFunction('range', 'range'), @@ -1905,4 +1916,42 @@ public static function captureOutput(iterable $body): string return $output; } + + /** + * @internal + */ + public function parseParentFunction(Parser $parser, Node $fakeNode, $args, int $line): AbstractExpression + { + if (!\count($parser->getBlockStack())) { + throw new SyntaxError('Calling "parent" outside a block is forbidden.', $line, $parser->getStream()->getSourceContext()); + } + + if (!$parser->getParent() && !$parser->hasTraits()) { + throw new SyntaxError('Calling "parent" on a template that does not extend nor "use" another template is forbidden.', $line, $parser->getStream()->getSourceContext()); + } + + return new ParentExpression($parser->peekBlockStack(), $line); + } + + /** + * @internal + */ + public function parseBlockFunction(Parser $parser, Node $fakeNode, $args, int $line): AbstractExpression + { + $fakeFunction = new TwigFunction('block', fn ($name, $template = null) => null); + $args = (new CallableArgumentsExtractor($fakeNode, $fakeFunction))->extractArguments($args); + + return new BlockReferenceExpression($args[0], $args[1] ?? null, $line); + } + + /** + * @internal + */ + public function parseAttributeFunction(Parser $parser, Node $fakeNode, $args, int $line): AbstractExpression + { + $fakeFunction = new TwigFunction('attribute', fn ($variable, $attribute, $arguments = null) => null); + $args = (new CallableArgumentsExtractor($fakeNode, $fakeFunction))->extractArguments($args); + + return new GetAttrExpression($args[0], $args[1], $args[2] ?? null, Template::ANY_CALL, $line); + } } diff --git a/src/TwigFunction.php b/src/TwigFunction.php index 4797320ebbd..d90a7e42cce 100644 --- a/src/TwigFunction.php +++ b/src/TwigFunction.php @@ -34,9 +34,15 @@ public function __construct(string $name, $callable = null, array $options = []) 'is_safe' => null, 'is_safe_callback' => null, 'node_class' => FunctionExpression::class, + 'parser_callable' => null, ], $this->options); } + public function getParserCallable(): ?callable + { + return $this->options['parser_callable']; + } + public function getSafe(Node $functionArgs): ?array { if (null !== $this->options['is_safe']) { From 8370e8a9a7042d50df4f35b770b9178ac45ca053 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sat, 17 Aug 2024 23:15:53 +0200 Subject: [PATCH 357/812] Move some methods to static calls --- src/Extension/CoreExtension.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index 1efb7ecea7e..2cf889e7a58 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -246,9 +246,9 @@ public function getFilters(): array public function getFunctions(): array { return [ - new TwigFunction('parent', null, ['parser_callable' => [$this, 'parseParentFunction']]), - new TwigFunction('block', null, ['parser_callable' => [$this, 'parseBlockFunction']]), - new TwigFunction('attribute', null, ['parser_callable' => [$this, 'parseAttributeFunction']]), + new TwigFunction('parent', null, ['parser_callable' => [self::class, 'parseParentFunction']]), + new TwigFunction('block', null, ['parser_callable' => [self::class, 'parseBlockFunction']]), + new TwigFunction('attribute', null, ['parser_callable' => [self::class, 'parseAttributeFunction']]), new TwigFunction('max', 'max'), new TwigFunction('min', 'min'), new TwigFunction('range', 'range'), @@ -1920,7 +1920,7 @@ public static function captureOutput(iterable $body): string /** * @internal */ - public function parseParentFunction(Parser $parser, Node $fakeNode, $args, int $line): AbstractExpression + public static function parseParentFunction(Parser $parser, Node $fakeNode, $args, int $line): AbstractExpression { if (!\count($parser->getBlockStack())) { throw new SyntaxError('Calling "parent" outside a block is forbidden.', $line, $parser->getStream()->getSourceContext()); @@ -1936,7 +1936,7 @@ public function parseParentFunction(Parser $parser, Node $fakeNode, $args, int $ /** * @internal */ - public function parseBlockFunction(Parser $parser, Node $fakeNode, $args, int $line): AbstractExpression + public static function parseBlockFunction(Parser $parser, Node $fakeNode, $args, int $line): AbstractExpression { $fakeFunction = new TwigFunction('block', fn ($name, $template = null) => null); $args = (new CallableArgumentsExtractor($fakeNode, $fakeFunction))->extractArguments($args); @@ -1947,7 +1947,7 @@ public function parseBlockFunction(Parser $parser, Node $fakeNode, $args, int $l /** * @internal */ - public function parseAttributeFunction(Parser $parser, Node $fakeNode, $args, int $line): AbstractExpression + public static function parseAttributeFunction(Parser $parser, Node $fakeNode, $args, int $line): AbstractExpression { $fakeFunction = new TwigFunction('attribute', fn ($variable, $attribute, $arguments = null) => null); $args = (new CallableArgumentsExtractor($fakeNode, $fakeFunction))->extractArguments($args); From e3ac14e1c8abf1fa981934c667f5565d37e59653 Mon Sep 17 00:00:00 2001 From: Matheo Daninos Date: Sat, 9 Mar 2024 17:07:31 +0100 Subject: [PATCH 358/812] Introduce CVA to html-extra --- doc/functions/html_cva.rst | 185 +++++ extra/html-extra/Cva.php | 129 ++++ extra/html-extra/HtmlExtension.php | 14 + extra/html-extra/README.md | 3 + extra/html-extra/Tests/CvaTest.php | 668 ++++++++++++++++++ extra/html-extra/Tests/Fixtures/html_cva.test | 37 + .../Fixtures/html_cva_pass_to_template.test | 19 + 7 files changed, 1055 insertions(+) create mode 100644 doc/functions/html_cva.rst create mode 100644 extra/html-extra/Cva.php create mode 100644 extra/html-extra/Tests/CvaTest.php create mode 100644 extra/html-extra/Tests/Fixtures/html_cva.test create mode 100644 extra/html-extra/Tests/Fixtures/html_cva_pass_to_template.test diff --git a/doc/functions/html_cva.rst b/doc/functions/html_cva.rst new file mode 100644 index 00000000000..70226e77bd1 --- /dev/null +++ b/doc/functions/html_cva.rst @@ -0,0 +1,185 @@ +``html_cva`` +============ + +`CVA (Class Variant Authority)`_ is a concept from the JavaScript world and used +by the well-known `shadcn/ui`_ library. +The CVA concept is used to render multiple variations of components, applying +a set of conditions and recipes to dynamically compose CSS class strings (color, size, etc.), +to create highly reusable and customizable templates. + +The concept of CVA is powered by a ``html_cva()`` Twig +function where you define ``base`` classes that should always be present and then different +``variants`` and the corresponding classes: + +.. code-block:: html+twig + + {# templates/alert.html.twig #} + {% set alert = html_cva( + base='alert ', + variants={ + color: { + blue: 'bg-blue', + red: 'bg-red', + green: 'bg-green', + }, + size: { + sm: 'text-sm', + md: 'text-md', + lg: 'text-lg', + } + } + ) %} + +
    + ... +
    + +Then use the ``color`` and ``size`` variants to select the needed classes: + +.. code-block:: twig + + {# index.html.twig #} + {{ include('alert.html.twig', {'color': 'blue', 'size': 'md'}) }} + // class="alert bg-red text-lg" + + {{ include('alert.html.twig', {'color': 'green', 'size': 'sm'}) }} + // class="alert bg-green text-sm" + + {{ include('alert.html.twig', {'color': 'red', 'class': 'flex items-center justify-center'}) }} + // class="alert bg-red text-md flex items-center justify-center" + +CVA and Tailwind CSS +-------------------- + +CVA work perfectly with Tailwind CSS. The only drawback is that you can have class conflicts. +To "merge" conflicting classes together and keep only the ones you need, use the +``tailwind_merge()`` filter from `tales-from-a-dev/twig-tailwind-extra`_ +with the ``html_cva()`` function: + +.. code-block:: terminal + + $ composer require tales-from-a-dev/twig-tailwind-extra + +.. code-block:: html+twig + + {% set alert = html_cva( + // ... + ) %} + +
    + ... +
    + +Compound Variants +----------------- + +You can define compound variants. A compound variant is a variant that applies +when multiple other variant conditions are met: + +.. code-block:: html+twig + + {% set alert = html_cva( + base='alert', + variants={ + color: { + blue: 'bg-blue', + red: 'bg-red', + green: 'bg-green', + }, + size: { + sm: 'text-sm', + md: 'text-md', + lg: 'text-lg', + } + }, + compoundVariants=[{ + // if color = red AND size = (md or lg), add the `font-bold` class + color: ['red'], + size: ['md', 'lg'], + class: 'font-bold' + }] + ) %} + +
    + ... +
    + + {# index.html.twig #} + + {{ include('alert.html.twig', {color: 'red', size: 'lg'}) }} + // class="alert bg-red text-lg font-bold" + + {{ include('alert.html.twig', {color: 'green', size: 'sm'}) }} + // class="alert bg-green text-sm" + + {{ include('alert.html.twig', {color: 'red', size: 'md'}) }} + // class="alert bg-green text-lg font-bold" + +Default Variants +---------------- + +If no variants match, you can define a default set of classes to apply: + +.. code-block:: html+twig + + {% set alert = html_cva( + base='alert ', + variants={ + color: { + blue: 'bg-blue', + red: 'bg-red', + green: 'bg-green', + }, + size: { + sm: 'text-sm', + md: 'text-md', + lg: 'text-lg', + }, + rounded: { + sm: 'rounded-sm', + md: 'rounded-md', + lg: 'rounded-lg', + } + }, + defaultVariants={ + rounded: 'md', + } + ) %} + +
    + ... +
    + + {# index.html.twig #} + + {{ include('alert.html.twig', {color: 'red', size: 'lg'}) }} + // class="alert bg-red text-lg font-bold rounded-md" + +.. note:: + + The ``html_cva`` function is part of the ``HtmlExtension`` which is not + installed by default. Install it first: + + .. code-block:: bash + + $ composer require twig/html-extra + + Then, on Symfony projects, install the ``twig/extra-bundle``: + + .. code-block:: bash + + $ composer require twig/extra-bundle + + Otherwise, add the extension explicitly on the Twig environment:: + + use Twig\Extra\Html\HtmlExtension; + + $twig = new \Twig\Environment(...); + $twig->addExtension(new HtmlExtension()); + +This function works best when used with `TwigComponent`_. + +.. _`CVA (Class Variant Authority)`: https://cva.style/docs/getting-started/variants +.. _`shadcn/ui`: https://ui.shadcn.com +.. _`tales-from-a-dev/twig-tailwind-extra`: https://github.com/tales-from-a-dev/twig-tailwind-extra +.. _`TwigComponent`: https://symfony.com/bundles/ux-twig-component/current/index.html \ No newline at end of file diff --git a/extra/html-extra/Cva.php b/extra/html-extra/Cva.php new file mode 100644 index 00000000000..a77d7dfdf20 --- /dev/null +++ b/extra/html-extra/Cva.php @@ -0,0 +1,129 @@ + + */ +final class Cva +{ + /** + * @var list + */ + private array $base; + + /** + * @param string|list $base The base classes to apply to the component + */ + public function __construct( + string|array $base = [], + /** + * The variants to apply based on recipes. + * + * Format: [variantCategory => [variantName => classes]] + * + * Example: + * 'colors' => [ + * 'primary' => 'bleu-8000', + * 'danger' => 'red-800 text-bold', + * ], + * 'size' => [...], + * + * @var array>> + */ + private array $variants = [], + + /** + * The compound variants to apply based on recipes. + * + * Format: [variantsCategory => ['variantName', 'variantName'], class: classes] + * + * Example: + * [ + * 'colors' => ['primary'], + * 'size' => ['small'], + * 'class' => 'text-red-500', + * ], + * [ + * 'size' => ['large'], + * 'class' => 'font-weight-500', + * ] + * + * @var array>> + */ + private array $compoundVariants = [], + + /** + * The default variants to apply if specific recipes aren't provided. + * + * Format: [variantCategory => variantName] + * + * Example: + * 'colors' => 'primary', + * + * @var array + */ + private array $defaultVariants = [], + ) { + $this->base = (array) $base; + } + + public function apply(array $recipes, ?string ...$additionalClasses): string + { + $classes = $this->base; + + // Resolve recipes against variants + foreach ($recipes as $recipeName => $recipeValue) { + if (\is_bool($recipeValue)) { + $recipeValue = $recipeValue ? 'true' : 'false'; + } + $recipeClasses = $this->variants[$recipeName][$recipeValue] ?? []; + $classes = [...$classes, ...(array) $recipeClasses]; + } + + // Resolve compound variants + foreach ($this->compoundVariants as $compound) { + $compoundClasses = $this->resolveCompoundVariant($compound, $recipes) ?? []; + $classes = [...$classes, ...$compoundClasses]; + } + + // Apply default variants if specific recipes aren't provided + foreach ($this->defaultVariants as $defaultVariantName => $defaultVariantValue) { + if (!isset($recipes[$defaultVariantName])) { + $variantClasses = $this->variants[$defaultVariantName][$defaultVariantValue] ?? []; + $classes = [...$classes, ...(array) $variantClasses]; + } + } + $classes = [...$classes, ...array_values($additionalClasses)]; + + $classes = implode(' ', array_filter($classes, 'is_string')); + $classes = preg_split('#\s+#', $classes, -1, \PREG_SPLIT_NO_EMPTY) ?: []; + + return implode(' ', array_unique($classes)); + } + + private function resolveCompoundVariant(array $compound, array $recipes): array + { + foreach ($compound as $compoundName => $compoundValues) { + if ('class' === $compoundName) { + continue; + } + if (!isset($recipes[$compoundName]) || !\in_array($recipes[$compoundName], (array) $compoundValues)) { + return []; + } + } + + return (array) ($compound['class'] ?? []); + } +} diff --git a/extra/html-extra/HtmlExtension.php b/extra/html-extra/HtmlExtension.php index 83fcc58b195..06329977f55 100644 --- a/extra/html-extra/HtmlExtension.php +++ b/extra/html-extra/HtmlExtension.php @@ -37,6 +37,7 @@ public function getFunctions(): array { return [ new TwigFunction('html_classes', [self::class, 'htmlClasses']), + new TwigFunction('html_cva', [self::class, 'htmlCva']), ]; } @@ -110,4 +111,17 @@ public static function htmlClasses(...$args): string return implode(' ', array_unique(array_filter($classes, static function ($v) { return '' !== $v; }))); } + + /** + * @param string|list $base + * @param array> $variants + * @param array>> $compoundVariants + * @param array $defaultVariant + * + * @internal + */ + public static function htmlCva(array|string $base = [], array $variants = [], array $compoundVariants = [], array $defaultVariant = []): Cva + { + return new Cva($base, $variants, $compoundVariants, $defaultVariant); + } } diff --git a/extra/html-extra/README.md b/extra/html-extra/README.md index e2c46b08e4a..9cd51fe2884 100644 --- a/extra/html-extra/README.md +++ b/extra/html-extra/README.md @@ -9,5 +9,8 @@ This package is a Twig extension that provides the following: * [`html_classes`][2] function: returns a string by conditionally joining class names together. + * [`html_cva`][3] function: returns a `Cva` object to handle class variants. + [1]: https://twig.symfony.com/data_uri [2]: https://twig.symfony.com/html_classes +[3]: https://twig.symfony.com/html_cva diff --git a/extra/html-extra/Tests/CvaTest.php b/extra/html-extra/Tests/CvaTest.php new file mode 100644 index 00000000000..b8b32fbef1b --- /dev/null +++ b/extra/html-extra/Tests/CvaTest.php @@ -0,0 +1,668 @@ +assertEquals($expected, $recipeClass->apply($recipes)); + } + + public function testApply() + { + $recipe = new Cva('font-semibold border rounded', [ + 'colors' => [ + 'primary' => 'text-primary', + 'secondary' => 'text-secondary', + ], + 'sizes' => [ + 'sm' => 'text-sm', + 'md' => 'text-md', + 'lg' => 'text-lg', + ], + ], [ + [ + 'colors' => ['primary'], + 'sizes' => ['sm'], + 'class' => 'text-red-500', + ], + ]); + + $this->assertEquals('font-semibold border rounded text-primary text-sm text-red-500', $recipe->apply(['colors' => 'primary', 'sizes' => 'sm'])); + } + + public function testApplyWithNullString() + { + $recipe = new Cva('font-semibold border rounded', [ + 'colors' => [ + 'primary' => 'text-primary', + 'secondary' => 'text-secondary', + ], + 'sizes' => [ + 'sm' => 'text-sm', + 'md' => 'text-md', + 'lg' => 'text-lg', + ], + ], [ + [ + 'colors' => ['primary'], + 'sizes' => ['sm'], + 'class' => 'text-red-500', + ], + ]); + + $this->assertEquals('font-semibold border rounded text-primary text-sm text-red-500 flex justify-center', $recipe->apply(['colors' => 'primary', 'sizes' => 'sm'], 'flex', null, 'justify-center')); + } + + public static function recipeProvider(): iterable + { + yield 'base null' => [ + ['variants' => [ + 'colors' => [ + 'primary' => 'text-primary', + 'secondary' => 'text-secondary', + ], + 'sizes' => [ + 'sm' => 'text-sm', + 'md' => 'text-md', + 'lg' => 'text-lg', + ], + ]], + ['colors' => 'primary', 'sizes' => 'sm'], + 'text-primary text-sm', + ]; + + yield 'base empty' => [ + [ + 'base' => '', + 'variants' => [ + 'colors' => [ + 'primary' => 'text-primary', + 'secondary' => 'text-secondary', + ], + 'sizes' => [ + 'sm' => 'text-sm', + 'md' => 'text-md', + 'lg' => 'text-lg', + ], + ]], + ['colors' => 'primary', 'sizes' => 'sm'], + 'text-primary text-sm', + ]; + + yield 'base array' => [ + [ + 'base' => ['font-semibold', 'border', 'rounded'], + 'variants' => [ + 'colors' => [ + 'primary' => 'text-primary', + 'secondary' => 'text-secondary', + ], + 'sizes' => [ + 'sm' => 'text-sm', + 'md' => 'text-md', + 'lg' => 'text-lg', + ], + ]], + ['colors' => 'primary', 'sizes' => 'sm'], + 'font-semibold border rounded text-primary text-sm', + ]; + + yield 'no recipes match' => [ + [ + 'base' => 'font-semibold border rounded', + 'variants' => [ + 'colors' => [ + 'primary' => 'text-primary', + 'secondary' => 'text-secondary', + ], + 'sizes' => [ + 'sm' => 'text-sm', + 'md' => 'text-md', + 'lg' => 'text-lg', + ], + ], + ], + ['colors' => 'red', 'sizes' => 'test'], + 'font-semibold border rounded', + ]; + + yield 'simple variants' => [ + [ + 'base' => 'font-semibold border rounded', + 'variants' => [ + 'colors' => [ + 'primary' => 'text-primary', + 'secondary' => 'text-secondary', + ], + 'sizes' => [ + 'sm' => 'text-sm', + 'md' => 'text-md', + 'lg' => 'text-lg', + ], + ], + ], + ['colors' => 'primary', 'sizes' => 'sm'], + 'font-semibold border rounded text-primary text-sm', + ]; + + yield 'simple variants as array' => [ + [ + 'base' => 'font-semibold border rounded', + 'variants' => [ + 'colors' => [ + 'primary' => ['text-primary', 'uppercase'], + 'secondary' => ['text-secondary', 'uppercase'], + ], + 'sizes' => [ + 'sm' => 'text-sm', + 'md' => 'text-md', + 'lg' => 'text-lg', + ], + ], + ], + ['colors' => 'primary', 'sizes' => 'sm'], + 'font-semibold border rounded text-primary uppercase text-sm', + ]; + + yield 'simple variants with custom' => [ + [ + 'base' => 'font-semibold border rounded', + 'variants' => [ + 'colors' => [ + 'primary' => 'text-primary', + 'secondary' => 'text-secondary', + ], + 'sizes' => [ + 'sm' => 'text-sm', + 'md' => 'text-md', + 'lg' => 'text-lg', + ], + ], + ], + ['colors' => 'secondary', 'sizes' => 'md'], + 'font-semibold border rounded text-secondary text-md', + ]; + + yield 'compound variants' => [ + [ + 'base' => 'font-semibold border rounded', + 'variants' => [ + 'colors' => [ + 'primary' => 'text-primary', + 'secondary' => 'text-secondary', + ], + 'sizes' => [ + 'sm' => 'text-sm', + 'md' => 'text-md', + 'lg' => 'text-lg', + ], + ], + 'compounds' => [ + [ + 'colors' => 'primary', + 'sizes' => ['sm'], + 'class' => 'text-red-100', + ], + ], + ], + ['colors' => 'primary', 'sizes' => 'sm'], + 'font-semibold border rounded text-primary text-sm text-red-100', + ]; + + yield 'compound variants with true' => [ + [ + 'base' => 'button', + 'variants' => [ + 'colors' => [ + 'blue' => 'btn-blue', + 'red' => 'btn-red', + ], + 'disabled' => [ + 'true' => 'disabled', + ], + ], + 'compounds' => [ + [ + 'colors' => 'blue', + 'disabled' => ['true'], + 'class' => 'font-bold', + ], + ], + ], + ['colors' => 'blue', 'disabled' => 'true'], + 'button btn-blue disabled font-bold', + ]; + + yield 'compound variants as array' => [ + [ + 'base' => 'font-semibold border rounded', + 'variants' => [ + 'colors' => [ + 'primary' => 'text-primary', + 'secondary' => 'text-secondary', + ], + 'sizes' => [ + 'sm' => 'text-sm', + 'md' => 'text-md', + 'lg' => 'text-lg', + ], + ], + 'compounds' => [ + [ + 'colors' => ['primary'], + 'sizes' => ['sm'], + 'class' => ['text-red-900', 'bold'], + ], + ], + ], + ['colors' => 'primary', 'sizes' => 'sm'], + 'font-semibold border rounded text-primary text-sm text-red-900 bold', + ]; + + yield 'multiple compound variants' => [ + [ + 'base' => 'font-semibold border rounded', + 'variants' => [ + 'colors' => [ + 'primary' => 'text-primary', + 'secondary' => 'text-secondary', + ], + 'sizes' => [ + 'sm' => 'text-sm', + 'md' => 'text-md', + 'lg' => 'text-lg', + ], + ], + 'compounds' => [ + [ + 'colors' => ['primary'], + 'sizes' => ['sm'], + 'class' => 'text-red-300', + ], + [ + 'colors' => ['primary'], + 'sizes' => ['md'], + 'class' => 'text-blue-300', + ], + ], + ], + ['colors' => 'primary', 'sizes' => 'sm'], + 'font-semibold border rounded text-primary text-sm text-red-300', + ]; + + yield 'compound with multiple variants' => [ + [ + 'base' => 'font-semibold border rounded', + 'variants' => [ + 'colors' => [ + 'primary' => 'text-primary', + 'secondary' => 'text-secondary', + ], + 'sizes' => [ + 'sm' => 'text-sm', + 'md' => 'text-md', + 'lg' => 'text-lg', + ], + ], + 'compounds' => [ + [ + 'colors' => ['primary', 'secondary'], + 'sizes' => ['sm'], + 'class' => 'text-red-800', + ], + ], + ], + ['colors' => 'primary', 'sizes' => 'sm'], + 'font-semibold border rounded text-primary text-sm text-red-800', + ]; + + yield 'compound doesn\'t match' => [ + [ + 'base' => 'font-semibold border rounded', + 'variants' => [ + 'colors' => [ + 'primary' => 'text-primary', + 'secondary' => 'text-secondary', + ], + 'sizes' => [ + 'sm' => 'text-sm', + 'md' => 'text-md', + 'lg' => 'text-lg', + ], + ], + 'compounds' => [ + [ + 'colors' => ['danger', 'secondary'], + 'sizes' => ['sm'], + 'class' => 'text-red-500', + ], + ], + ], + ['colors' => 'primary', 'sizes' => 'sm'], + 'font-semibold border rounded text-primary text-sm', + ]; + + yield 'default variables' => [ + [ + 'base' => 'font-semibold border rounded', + 'variants' => [ + 'colors' => [ + 'primary' => 'text-primary', + 'secondary' => 'text-secondary', + ], + 'sizes' => [ + 'sm' => 'text-sm', + 'md' => 'text-md', + 'lg' => 'text-lg', + ], + 'rounded' => [ + 'sm' => 'rounded-sm', + 'md' => 'rounded-md', + 'lg' => 'rounded-lg', + ], + ], + 'compounds' => [ + [ + 'colors' => ['danger', 'secondary'], + 'sizes' => 'sm', + 'class' => 'text-red-500', + ], + ], + 'defaultVariants' => [ + 'colors' => 'primary', + 'sizes' => 'sm', + 'rounded' => 'md', + ], + ], + ['colors' => 'primary', 'sizes' => 'sm'], + 'font-semibold border rounded text-primary text-sm rounded-md', + ]; + + yield 'default variables all overwrite' => [ + [ + 'base' => 'font-semibold border rounded', + 'variants' => [ + 'colors' => [ + 'primary' => 'text-primary', + 'secondary' => 'text-secondary', + ], + 'sizes' => [ + 'sm' => 'text-sm', + 'md' => 'text-md', + 'lg' => 'text-lg', + ], + 'rounded' => [ + 'sm' => 'rounded-sm', + 'md' => 'rounded-md', + 'lg' => 'rounded-lg', + ], + ], + 'compounds' => [ + [ + 'colors' => ['danger', 'secondary'], + 'sizes' => ['sm'], + 'class' => 'text-red-500', + ], + ], + 'defaultVariants' => [ + 'colors' => 'primary', + 'sizes' => 'sm', + 'rounded' => 'md', + ], + ], + ['colors' => 'primary', 'sizes' => 'sm', 'rounded' => 'lg'], + 'font-semibold border rounded text-primary text-sm rounded-lg', + ]; + + yield 'default variables without matching variants' => [ + [ + 'base' => 'font-semibold border rounded', + 'variants' => [ + 'colors' => [ + 'primary' => 'text-primary', + 'secondary' => 'text-secondary', + ], + 'sizes' => [ + 'sm' => 'text-sm', + 'md' => 'text-md', + 'lg' => 'text-lg', + ], + 'rounded' => [ + 'sm' => 'rounded-sm', + 'md' => 'rounded-md', + 'lg' => 'rounded-lg', + ], + ], + 'compounds' => [ + [ + 'colors' => ['danger', 'secondary'], + 'sizes' => ['sm'], + 'class' => 'text-red-500', + ], + ], + 'defaultVariants' => [ + 'colors' => 'primary', + 'sizes' => 'sm', + 'rounded' => 'md', + ], + ], + [], + 'font-semibold border rounded text-primary text-sm rounded-md', + ]; + + yield 'default variables with boolean' => [ + [ + 'base' => 'button', + 'variants' => [ + 'colors' => [ + 'blue' => 'btn-blue', + 'red' => 'btn-red', + ], + 'disabled' => [ + 'true' => 'disabled', + 'false' => 'opacity-100', + ], + ], + 'defaultVariants' => [ + 'colors' => 'blue', + 'disabled' => 'false', + ], + ], + [], + 'button btn-blue opacity-100', + ]; + + yield 'boolean string variants true / true' => [ + [ + 'variants' => [ + 'colors' => [ + 'primary' => 'text-primary', + 'secondary' => 'text-secondary', + ], + 'disabled' => [ + 'true' => 'disable', + ], + ], + ], + ['colors' => 'primary', 'disabled' => true], + 'text-primary disable', + ]; + + yield 'boolean string variants true / false' => [ + [ + 'variants' => [ + 'colors' => [ + 'primary' => 'text-primary', + 'secondary' => 'text-secondary', + ], + 'disabled' => [ + 'true' => 'disable', + ], + ], + ], + ['colors' => 'primary', 'disabled' => false], + 'text-primary', + ]; + + yield 'boolean string variants false / true' => [ + [ + 'variants' => [ + 'colors' => [ + 'primary' => 'text-primary', + 'secondary' => 'text-secondary', + ], + 'disabled' => [ + 'false' => 'disable', + ], + ], + ], + ['colors' => 'primary', 'disabled' => true], + 'text-primary', + ]; + + yield 'boolean string variants false / false' => [ + [ + 'variants' => [ + 'colors' => [ + 'primary' => 'text-primary', + 'secondary' => 'text-secondary', + ], + 'disabled' => [ + 'false' => 'disable', + ], + ], + ], + ['colors' => 'primary', 'disabled' => false], + 'text-primary disable', + ]; + + yield 'boolean string variants missing' => [ + [ + 'variants' => [ + 'colors' => [ + 'primary' => 'text-primary', + 'secondary' => 'text-secondary', + ], + 'disabled' => [ + 'true' => 'disable', + ], + ], + ], + ['colors' => 'primary'], + 'text-primary', + ]; + + yield 'boolean list variants true' => [ + [ + 'variants' => [ + 'colors' => [ + 'primary' => 'text-primary', + 'secondary' => 'text-secondary', + ], + 'disabled' => [ + 'true' => ['disable', 'opacity-50'], + ], + ], + ], + ['colors' => 'primary', 'disabled' => true], + 'text-primary disable opacity-50', + ]; + + yield 'boolean list variants false' => [ + [ + 'variants' => [ + 'colors' => [ + 'primary' => 'text-primary', + 'secondary' => 'text-secondary', + ], + 'disabled' => [ + 'true' => ['disable', 'opacity-50'], + ], + ], + ], + ['colors' => 'primary', 'disabled' => false], + 'text-primary', + ]; + + yield 'boolean list variants missing' => [ + [ + 'variants' => [ + 'colors' => [ + 'primary' => 'text-primary', + 'secondary' => 'text-secondary', + ], + 'disabled' => [ + 'true' => ['disable', 'opacity-50'], + ], + ], + ], + ['colors' => 'primary'], + 'text-primary', + ]; + } + + /** + * @dataProvider provideAdditionalClassesCases + */ + public function testAdditionalClasses(string|array $base, array|string $additionals, string $expected) + { + $cva = new Cva($base); + if (!$additionals) { + $this->assertEquals($expected, $cva->apply([])); + } else { + $this->assertEquals($expected, $cva->apply([], ...(array) $additionals)); + } + } + + public static function provideAdditionalClassesCases(): iterable + { + yield 'additionals_are_optional' => [ + '', + 'foo', + 'foo', + ]; + + yield 'additional_are_used' => [ + '', + 'foo', + 'foo', + ]; + + yield 'additionals_are_used' => [ + '', + ['foo', 'bar'], + 'foo bar', + ]; + + yield 'additionals_preserve_order' => [ + ['foo'], + ['bar', 'foo'], + 'foo bar', + ]; + + yield 'additional_are_deduplicated' => [ + '', + ['bar', 'bar'], + 'bar', + ]; + } +} diff --git a/extra/html-extra/Tests/Fixtures/html_cva.test b/extra/html-extra/Tests/Fixtures/html_cva.test new file mode 100644 index 00000000000..71e16036adb --- /dev/null +++ b/extra/html-extra/Tests/Fixtures/html_cva.test @@ -0,0 +1,37 @@ +--TEST-- +"html_cva" function +--TEMPLATE-- +{% set alert = html_cva( + ['alert'], + { + color: { + blue: 'alert-blue', + red: 'alert-red', + green: 'alert-green', + yellow: 'alert-yellow', + }, + size: { + sm: 'alert-sm', + md: 'alert-md', + lg: 'alert-lg', + }, + rounded: { + sm: 'rounded-sm', + md: 'rounded-md', + lg: 'rounded-lg', + } + }, + [{ + color: ['red'], + size: ['lg'], + class: 'font-semibold' + }], + { + rounded: 'md' + } +) %} +{{ alert.apply({color: 'blue', size: 'sm'}) }} +--DATA-- +return [] +--EXPECT-- +alert alert-blue alert-sm rounded-md diff --git a/extra/html-extra/Tests/Fixtures/html_cva_pass_to_template.test b/extra/html-extra/Tests/Fixtures/html_cva_pass_to_template.test new file mode 100644 index 00000000000..49b842fac1a --- /dev/null +++ b/extra/html-extra/Tests/Fixtures/html_cva_pass_to_template.test @@ -0,0 +1,19 @@ +--TEST-- +pass Cva object to template +--TEMPLATE-- +{{ alert.apply({colors: 'primary', sizes: 'sm'}) }} +--DATA-- +return [ + 'alert' => new Twig\Extra\Html\Cva('font-semibold border rounded', [ + 'colors' => [ + 'primary' => 'text-primary', + 'secondary' => 'text-secondary' + ], + 'sizes' => [ + 'sm' => 'text-sm', + 'lg' => 'text-lg' + ] + ]) +]; +--EXPECT-- +font-semibold border rounded text-primary text-sm From e85519fcd9c223191cd34bbbe8db6cc6e517b775 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sun, 18 Aug 2024 09:38:02 +0200 Subject: [PATCH 359/812] Fix html_cva docs --- CHANGELOG | 1 + doc/functions/index.rst | 1 + 2 files changed, 2 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index 966c83219f2..8379fc55d56 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,6 @@ # 3.12.0 (2024-XX-XX) + * Add the `html_cva` function (in the HTML extra package) * Add support for named arguments to the `block` and `attribute` functions * Throw a SyntaxError exception at compile time when a Twig callable has not the minimum number of required arguments * Add a `CallableArgumentsExtractor` class diff --git a/doc/functions/index.rst b/doc/functions/index.rst index 0809fd0be5f..27fd2438352 100644 --- a/doc/functions/index.rst +++ b/doc/functions/index.rst @@ -12,6 +12,7 @@ Functions dump enum_cases html_classes + html_cva include max min From 1d5a8dc98b072fce45d1bd6562f792c525a9035a Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sun, 18 Aug 2024 09:38:48 +0200 Subject: [PATCH 360/812] Fix html_cva docs --- doc/functions/html_cva.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/doc/functions/html_cva.rst b/doc/functions/html_cva.rst index 70226e77bd1..4079581570c 100644 --- a/doc/functions/html_cva.rst +++ b/doc/functions/html_cva.rst @@ -1,6 +1,11 @@ ``html_cva`` ============ +.. versionadded:: 3.12 + + The ``html_cva`` function was added in Twig 3.12. + + `CVA (Class Variant Authority)`_ is a concept from the JavaScript world and used by the well-known `shadcn/ui`_ library. The CVA concept is used to render multiple variations of components, applying From 84116e5ff71584516ab4f980ab1a480fd474235a Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sat, 17 Aug 2024 17:59:11 +0200 Subject: [PATCH 361/812] Fix typos in CHANGELOG --- CHANGELOG | 1 + doc/coding_standards.rst | 6 ++++++ doc/filters/data_uri.rst | 6 +++--- doc/filters/format_currency.rst | 2 +- doc/filters/format_datetime.rst | 14 +++++++------- doc/filters/format_number.rst | 6 +++--- doc/filters/trim.rst | 2 +- doc/templates.rst | 19 ++++++++++++------- src/ExpressionParser.php | 2 +- tests/Fixtures/functions/attribute.test | 4 ++++ tests/Fixtures/functions/date_namedargs.test | 2 ++ 11 files changed, 41 insertions(+), 23 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 8379fc55d56..8770369b4d6 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,6 @@ # 3.12.0 (2024-XX-XX) + * Accept colons (`:`) in addition to equals (`=`) to separate argument names and values in named arguments * Add the `html_cva` function (in the HTML extra package) * Add support for named arguments to the `block` and `attribute` functions * Throw a SyntaxError exception at compile time when a Twig callable has not the minimum number of required arguments diff --git a/doc/coding_standards.rst b/doc/coding_standards.rst index d10906fa643..46be8eca6cb 100644 --- a/doc/coding_standards.rst +++ b/doc/coding_standards.rst @@ -105,3 +105,9 @@ standards: true {% endif %} {% endblock %} + +* Use ``:`` instead of ``=`` to separate argument names and values: + + .. code-block:: twig + + {{ data|convert_encoding(from: 'iso-2022-jp', to: 'UTF-8') }} diff --git a/doc/filters/data_uri.rst b/doc/filters/data_uri.rst index 131a71bb8b9..a68deb0f285 100644 --- a/doc/filters/data_uri.rst +++ b/doc/filters/data_uri.rst @@ -11,13 +11,13 @@ The ``data_uri`` filter generates a URL using the data scheme as defined in {{ source('path_to_image')|data_uri }} {# force the mime type, disable the guessing of the mime type #} - {{ image_data|data_uri(mime="image/svg") }} + {{ image_data|data_uri(mime: "image/svg") }} {# also works with plain text #} - {{ 'foobar'|data_uri(mime="text/html") }} + {{ 'foobar'|data_uri(mime: "text/html") }} {# add some extra parameters #} - {{ 'foobar'|data_uri(mime="text/html", parameters={charset: "ascii"}) }} + {{ 'foobar'|data_uri(mime: "text/html", parameters: {charset: "ascii"}) }} .. note:: diff --git a/doc/filters/format_currency.rst b/doc/filters/format_currency.rst index 8b649bf5d94..c4c364a00f9 100644 --- a/doc/filters/format_currency.rst +++ b/doc/filters/format_currency.rst @@ -45,7 +45,7 @@ By default, the filter uses the current locale. You can pass it explicitly: .. code-block:: twig {# 1.000.000,00 € #} - {{ '1000000'|format_currency('EUR', locale='de') }} + {{ '1000000'|format_currency('EUR', locale: 'de') }} .. note:: diff --git a/doc/filters/format_datetime.rst b/doc/filters/format_datetime.rst index 8f3b46d479a..a47d49731b0 100644 --- a/doc/filters/format_datetime.rst +++ b/doc/filters/format_datetime.rst @@ -16,13 +16,13 @@ You can tweak the output for the date part and the time part: .. code-block:: twig {# 23:39 #} - {{ '2019-08-07 23:39:12'|format_datetime('none', 'short', locale='fr') }} + {{ '2019-08-07 23:39:12'|format_datetime('none', 'short', locale: 'fr') }} {# 07/08/2019 #} - {{ '2019-08-07 23:39:12'|format_datetime('short', 'none', locale='fr') }} + {{ '2019-08-07 23:39:12'|format_datetime('short', 'none', locale: 'fr') }} {# mercredi 7 août 2019 23:39:12 UTC #} - {{ '2019-08-07 23:39:12'|format_datetime('full', 'full', locale='fr') }} + {{ '2019-08-07 23:39:12'|format_datetime('full', 'full', locale: 'fr') }} Supported values are: ``none``, ``short``, ``medium``, ``long``, and ``full``. @@ -38,7 +38,7 @@ For greater flexibility, you can even define your own pattern .. code-block:: twig {# 11 oclock PM, GMT #} - {{ '2019-08-07 23:39:12'|format_datetime(pattern="hh 'oclock' a, zzzz") }} + {{ '2019-08-07 23:39:12'|format_datetime(pattern: "hh 'oclock' a, zzzz") }} Locale ------ @@ -48,7 +48,7 @@ By default, the filter uses the current locale. You can pass it explicitly: .. code-block:: twig {# 7 août 2019 23:39:12 #} - {{ '2019-08-07 23:39:12'|format_datetime(locale='fr') }} + {{ '2019-08-07 23:39:12'|format_datetime(locale: 'fr') }} Timezone -------- @@ -59,14 +59,14 @@ it by explicitly specifying a timezone: .. code-block:: twig - {{ datetime|format_datetime(locale='en', timezone='Pacific/Midway') }} + {{ datetime|format_datetime(locale: 'en', timezone: 'Pacific/Midway') }} If the date is already a DateTime object, and if you want to keep its current timezone, pass ``false`` as the timezone value: .. code-block:: twig - {{ datetime|format_datetime(locale='en', timezone=false) }} + {{ datetime|format_datetime(locale: 'en', timezone: false) }} The default timezone can also be set globally by calling ``setTimezone()``:: diff --git a/doc/filters/format_number.rst b/doc/filters/format_number.rst index 994404e07d2..900fa5ea9f4 100644 --- a/doc/filters/format_number.rst +++ b/doc/filters/format_number.rst @@ -44,10 +44,10 @@ Besides plain numbers, the filter can also format numbers in various styles: .. code-block:: twig {# 1,234% #} - {{ '12.345'|format_number(style='percent') }} + {{ '12.345'|format_number(style: 'percent') }} {# twelve point three four five #} - {{ '12.345'|format_number(style='spellout') }} + {{ '12.345'|format_number(style: 'spellout') }} {# 12 sec. #} {{ '12'|format_duration_number }} @@ -85,7 +85,7 @@ By default, the filter uses the current locale. You can pass it explicitly: .. code-block:: twig {# 12,345 #} - {{ '12.345'|format_number(locale='fr') }} + {{ '12.345'|format_number(locale: 'fr') }} .. note:: diff --git a/doc/filters/trim.rst b/doc/filters/trim.rst index 238928bc78a..ba01cbe8588 100644 --- a/doc/filters/trim.rst +++ b/doc/filters/trim.rst @@ -14,7 +14,7 @@ and end of a string: {# outputs ' I like Twig' #} - {{ ' I like Twig. '|trim(side='left') }} + {{ ' I like Twig. '|trim(side: 'left') }} {# outputs 'I like Twig. ' #} diff --git a/doc/templates.rst b/doc/templates.rst index 2746d90f6a7..63853561478 100644 --- a/doc/templates.rst +++ b/doc/templates.rst @@ -210,11 +210,16 @@ built-in functions. Named Arguments --------------- -Named arguments are supported in functions, filters and tests. +Named arguments are supported in functions, filters, and tests. + +.. versionadded:: 3.12 + + Twig supports both ``=`` and ``:`` as separators between argument names and + values, but support for ``:`` was introduced in Twig 3.12. .. code-block:: twig - {% for i in range(low=1, high=10, step=2) %} + {% for i in range(low: 1, high: 10, step: 2) %} {{ i }}, {% endfor %} @@ -227,7 +232,7 @@ the values you pass as arguments: {# versus #} - {{ data|convert_encoding(from='iso-2022-jp', to='UTF-8') }} + {{ data|convert_encoding(from: 'iso-2022-jp', to: 'UTF-8') }} Named arguments also allow you to skip some arguments for which you don't want to change the default value: @@ -238,19 +243,19 @@ to change the default value: {{ "now"|date(null, "Europe/Paris") }} {# or skip the format value by using a named argument for the time zone #} - {{ "now"|date(timezone="Europe/Paris") }} + {{ "now"|date(timezone: "Europe/Paris") }} You can also use both positional and named arguments in one call, in which case positional arguments must always come before named arguments: .. code-block:: twig - {{ "now"|date('d/m/Y H:i', timezone="Europe/Paris") }} + {{ "now"|date('d/m/Y H:i', timezone: "Europe/Paris") }} .. tip:: - Each function and filter documentation page has a section where the names - of all arguments are listed when supported. + Each function, filter, and test documentation page has a section where the + names of all supported arguments are listed. Control Structure ----------------- diff --git a/src/ExpressionParser.php b/src/ExpressionParser.php index 7477d0bec75..51cda50a12a 100644 --- a/src/ExpressionParser.php +++ b/src/ExpressionParser.php @@ -639,7 +639,7 @@ public function parseArguments($namedArguments = false, $definition = false, $al } $name = null; - if ($namedArguments && $token = $stream->nextIf(Token::OPERATOR_TYPE, '=')) { + if ($namedArguments && (($token = $stream->nextIf(Token::OPERATOR_TYPE, '=')) || ($token = $stream->nextIf(Token::PUNCTUATION_TYPE, ':')))) { if (!$value instanceof NameExpression) { throw new SyntaxError(\sprintf('A parameter name must be a string, "%s" given.', \get_class($value)), $token->getLine(), $stream->getSourceContext()); } diff --git a/tests/Fixtures/functions/attribute.test b/tests/Fixtures/functions/attribute.test index 2f70b08a456..31cca8c4661 100644 --- a/tests/Fixtures/functions/attribute.test +++ b/tests/Fixtures/functions/attribute.test @@ -3,10 +3,12 @@ --TEMPLATE-- {{ attribute(obj, method) }} {{ attribute(variable=obj, attribute=method) }} +{{ attribute(variable: obj, attribute: method) }} {{ attribute(array, item) }} {{ attribute(obj, "bar", ["a", "b"]) }} {{ attribute(obj, "bar", arguments) }} {{ attribute(variable=obj, attribute="bar", arguments=arguments) }} +{{ attribute(variable: obj, attribute: "bar", arguments: arguments) }} {{ attribute(obj, method) is defined ? 'ok' : 'ko' }} {{ attribute(obj, nonmethod) is defined ? 'ok' : 'ko' }} --DATA-- @@ -14,9 +16,11 @@ return ['obj' => new Twig\Tests\TwigTestFoo(), 'method' => 'foo', 'array' => ['f --EXPECT-- foo foo +foo bar bar_a-b bar_a-b bar_a-b +bar_a-b ok ko diff --git a/tests/Fixtures/functions/date_namedargs.test b/tests/Fixtures/functions/date_namedargs.test index 11f60ee8bf2..819e8326b39 100644 --- a/tests/Fixtures/functions/date_namedargs.test +++ b/tests/Fixtures/functions/date_namedargs.test @@ -3,9 +3,11 @@ --TEMPLATE-- {{ date(date, "America/New_York")|date('d/m/Y H:i:s P', false) }} {{ date(timezone="America/New_York", date=date)|date('d/m/Y H:i:s P', false) }} +{{ date(timezone: "America/New_York", date: date)|date('d/m/Y H:i:s P', false) }} --DATA-- date_default_timezone_set('UTC'); return ['date' => mktime(13, 45, 0, 10, 4, 2010)] --EXPECT-- 04/10/2010 09:45:00 -04:00 04/10/2010 09:45:00 -04:00 +04/10/2010 09:45:00 -04:00 From 254abc8c4c01b0fb12523f017a57919518f61cd0 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Sun, 18 Aug 2024 18:25:33 +0200 Subject: [PATCH 362/812] Add test about version --- tests/EnvironmentTest.php | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/EnvironmentTest.php b/tests/EnvironmentTest.php index d2547e7b2fc..6311fdfca7a 100644 --- a/tests/EnvironmentTest.php +++ b/tests/EnvironmentTest.php @@ -38,6 +38,21 @@ class EnvironmentTest extends TestCase { use ExpectDeprecationTrait; + public function testVersionConstants() + { + $version = Environment::VERSION; + $exploded = explode('-', $version); + $this->assertEquals(Environment::EXTRA_VERSION, $exploded[1] ?? ''); + + $version = $exploded[0]; + $exploded = explode('.', $version); + $this->assertEquals(Environment::MAJOR_VERSION, $exploded[0]); + $this->assertEquals(Environment::MINOR_VERSION, $exploded[1]); + $this->assertEquals(Environment::RELEASE_VERSION, $exploded[2]); + + $this->assertEquals(Environment::VERSION_ID, \sprintf('%s0%s0%s', $exploded[0], $exploded[1], $exploded[2])); + } + public function testAutoescapeOption() { $loader = new ArrayLoader([ From 675cb2d1415bffab22e71c260c1bd04ddf4a0d40 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sun, 18 Aug 2024 19:25:19 +0200 Subject: [PATCH 363/812] Fix CS --- extra/html-extra/HtmlExtension.php | 2 +- src/AbstractTwigCallable.php | 2 +- src/ExpressionParser.php | 3 --- src/Lexer.php | 24 +++++++++---------- src/Node/DeprecatedNode.php | 2 +- src/Node/Expression/CallExpression.php | 8 +++---- .../FunctionNode/EnumCasesFunction.php | 2 +- src/Node/Expression/Test/DefinedTest.php | 3 +-- src/Node/Node.php | 1 - src/Util/CallableArgumentsExtractor.php | 4 ++-- tests/LexerTest.php | 4 ++-- tests/Node/NodeTest.php | 6 ++--- 12 files changed, 28 insertions(+), 33 deletions(-) diff --git a/extra/html-extra/HtmlExtension.php b/extra/html-extra/HtmlExtension.php index 06329977f55..e1766ad45d4 100644 --- a/extra/html-extra/HtmlExtension.php +++ b/extra/html-extra/HtmlExtension.php @@ -116,7 +116,7 @@ public static function htmlClasses(...$args): string * @param string|list $base * @param array> $variants * @param array>> $compoundVariants - * @param array $defaultVariant + * @param array $defaultVariant * * @internal */ diff --git a/src/AbstractTwigCallable.php b/src/AbstractTwigCallable.php index d9fc45f2f6a..f6718430064 100644 --- a/src/AbstractTwigCallable.php +++ b/src/AbstractTwigCallable.php @@ -41,7 +41,7 @@ public function __construct(string $name, $callable = null, array $options = []) public function __toString(): string { - return sprintf('%s(%s)', static::class, $this->name); + return \sprintf('%s(%s)', static::class, $this->name); } public function getName(): string diff --git a/src/ExpressionParser.php b/src/ExpressionParser.php index 51cda50a12a..28b556ccca0 100644 --- a/src/ExpressionParser.php +++ b/src/ExpressionParser.php @@ -20,20 +20,17 @@ use Twig\Node\Expression\AssignNameExpression; use Twig\Node\Expression\Binary\AbstractBinary; use Twig\Node\Expression\Binary\ConcatBinary; -use Twig\Node\Expression\BlockReferenceExpression; use Twig\Node\Expression\ConditionalExpression; use Twig\Node\Expression\ConstantExpression; use Twig\Node\Expression\GetAttrExpression; use Twig\Node\Expression\MethodCallExpression; use Twig\Node\Expression\NameExpression; -use Twig\Node\Expression\ParentExpression; use Twig\Node\Expression\TestExpression; use Twig\Node\Expression\Unary\AbstractUnary; use Twig\Node\Expression\Unary\NegUnary; use Twig\Node\Expression\Unary\NotUnary; use Twig\Node\Expression\Unary\PosUnary; use Twig\Node\Node; -use Twig\Util\CallableArgumentsExtractor; /** * Parses expressions. diff --git a/src/Lexer.php b/src/Lexer.php index d264dea96bc..78b07156506 100644 --- a/src/Lexer.php +++ b/src/Lexer.php @@ -393,8 +393,8 @@ private function lexExpression(): void private function stripcslashes(string $str, string $quoteType): string { $result = ''; - $length = strlen($str); - + $length = \strlen($str); + $i = 0; while ($i < $length) { if (false === $pos = strpos($str, '\\', $i)) { @@ -414,34 +414,34 @@ private function stripcslashes(string $str, string $quoteType): string if (isset(self::SPECIAL_CHARS[$nextChar])) { $result .= self::SPECIAL_CHARS[$nextChar]; - } elseif ($nextChar === '\\') { + } elseif ('\\' === $nextChar) { $result .= $nextChar; - } elseif ($nextChar === "'" || $nextChar === '"') { + } elseif ("'" === $nextChar || '"' === $nextChar) { if ($nextChar !== $quoteType) { trigger_deprecation('twig/twig', '3.12', 'Character "%s" at position %d does not need to be escaped anymore.', $nextChar, $i + 1); } $result .= $nextChar; - } elseif ($nextChar === '#' && $i + 1 < $length && $str[$i + 1] === '{') { + } elseif ('#' === $nextChar && $i + 1 < $length && '{' === $str[$i + 1]) { $result .= '#{'; - $i++; - } elseif ($nextChar === 'x' && $i + 1 < $length && ctype_xdigit($str[$i + 1])) { + ++$i; + } elseif ('x' === $nextChar && $i + 1 < $length && ctype_xdigit($str[$i + 1])) { $hex = $str[++$i]; if ($i + 1 < $length && ctype_xdigit($str[$i + 1])) { $hex .= $str[++$i]; } - $result .= chr(hexdec($hex)); + $result .= \chr(hexdec($hex)); } elseif (ctype_digit($nextChar) && $nextChar < '8') { $octal = $nextChar; - while ($i + 1 < $length && ctype_digit($str[$i + 1]) && $str[$i + 1] < '8' && strlen($octal) < 3) { + while ($i + 1 < $length && ctype_digit($str[$i + 1]) && $str[$i + 1] < '8' && \strlen($octal) < 3) { $octal .= $str[++$i]; } - $result .= chr(octdec($octal)); + $result .= \chr(octdec($octal)); } else { - trigger_deprecation('twig/twig', '3.12', sprintf('Character "%s" at position %d does not need to be escaped anymore.', $nextChar, $i + 1)); + trigger_deprecation('twig/twig', '3.12', \sprintf('Character "%s" at position %d does not need to be escaped anymore.', $nextChar, $i + 1)); $result .= $nextChar; } - $i++; + ++$i; } return $result; diff --git a/src/Node/DeprecatedNode.php b/src/Node/DeprecatedNode.php index afeb8332e29..c4c4a8aecb5 100644 --- a/src/Node/DeprecatedNode.php +++ b/src/Node/DeprecatedNode.php @@ -65,7 +65,7 @@ public function compile(Compiler $compiler): void } $compiler - ->raw(".") + ->raw('.') ->string(\sprintf(' in "%s" at line %d.', $this->getTemplateName(), $this->getTemplateLine())) ->raw(");\n") ; diff --git a/src/Node/Expression/CallExpression.php b/src/Node/Expression/CallExpression.php index 91faf271acd..f61adc7254e 100644 --- a/src/Node/Expression/CallExpression.php +++ b/src/Node/Expression/CallExpression.php @@ -319,7 +319,7 @@ private function reflectCallable($callable): ReflectionCallable } /** - * Overrides the Twig callable based on attributes (as potentially, attributes changed between the creation and the compilation of the node) + * Overrides the Twig callable based on attributes (as potentially, attributes changed between the creation and the compilation of the node). * * To be removed in 4.0 and replace by $this->getAttribute('twig_callable'). */ @@ -334,7 +334,7 @@ private function getTwigCallable(): TwigCallableInterface [ 'is_variadic' => $this->hasAttribute('is_variadic') ? $this->getAttribute('is_variadic') : $current->isVariadic(), ], - ))->withDynamicArguments($this->getAttribute('name'), $this->hasAttribute('dynamic_name') ? $this->getAttribute('dynamic_name') : $current->getDynamicName(), $this->hasAttribute('arguments') ? $this->hasAttribute('arguments') : $current->getArguments()), + ))->withDynamicArguments($this->getAttribute('name'), $this->hasAttribute('dynamic_name') ? $this->getAttribute('dynamic_name') : $current->getDynamicName(), $this->hasAttribute('arguments') ?: $current->getArguments()), 'function' => (new TwigFunction( $this->hasAttribute('name') ? $this->getAttribute('name') : $current->getName(), $this->hasAttribute('callable') ? $this->getAttribute('callable') : $current->getCallable(), @@ -344,7 +344,7 @@ private function getTwigCallable(): TwigCallableInterface 'needs_charset' => $this->hasAttribute('needs_charset') ? $this->getAttribute('needs_charset') : $current->needsCharset(), 'is_variadic' => $this->hasAttribute('is_variadic') ? $this->getAttribute('is_variadic') : $current->isVariadic(), ], - ))->withDynamicArguments($this->getAttribute('name'), $this->hasAttribute('dynamic_name') ? $this->getAttribute('dynamic_name') : $current->getDynamicName(), $this->hasAttribute('arguments') ? $this->hasAttribute('arguments') : $current->getArguments()), + ))->withDynamicArguments($this->getAttribute('name'), $this->hasAttribute('dynamic_name') ? $this->getAttribute('dynamic_name') : $current->getDynamicName(), $this->hasAttribute('arguments') ?: $current->getArguments()), 'filter' => (new TwigFilter( $this->getAttribute('name'), $this->hasAttribute('callable') ? $this->getAttribute('callable') : $current->getCallable(), @@ -354,7 +354,7 @@ private function getTwigCallable(): TwigCallableInterface 'needs_charset' => $this->hasAttribute('needs_charset') ? $this->getAttribute('needs_charset') : $current->needsCharset(), 'is_variadic' => $this->hasAttribute('is_variadic') ? $this->getAttribute('is_variadic') : $current->isVariadic(), ], - ))->withDynamicArguments($this->getAttribute('name'), $this->hasAttribute('dynamic_name') ? $this->getAttribute('dynamic_name') : $current->getDynamicName(), $this->hasAttribute('arguments') ? $this->hasAttribute('arguments') : $current->getArguments()), + ))->withDynamicArguments($this->getAttribute('name'), $this->hasAttribute('dynamic_name') ? $this->getAttribute('dynamic_name') : $current->getDynamicName(), $this->hasAttribute('arguments') ?: $current->getArguments()), }); return $this->getAttribute('twig_callable'); diff --git a/src/Node/Expression/FunctionNode/EnumCasesFunction.php b/src/Node/Expression/FunctionNode/EnumCasesFunction.php index 171f611de23..7e5c25ff46a 100644 --- a/src/Node/Expression/FunctionNode/EnumCasesFunction.php +++ b/src/Node/Expression/FunctionNode/EnumCasesFunction.php @@ -20,7 +20,7 @@ public function compile(Compiler $compiler): void $firstArgument = null; } - if (!$firstArgument instanceof ConstantExpression || \count($arguments) !== 1) { + if (!$firstArgument instanceof ConstantExpression || 1 !== \count($arguments)) { parent::compile($compiler); return; diff --git a/src/Node/Expression/Test/DefinedTest.php b/src/Node/Expression/Test/DefinedTest.php index f4c1b1e4b4f..24d3ee82c9c 100644 --- a/src/Node/Expression/Test/DefinedTest.php +++ b/src/Node/Expression/Test/DefinedTest.php @@ -14,7 +14,6 @@ use Twig\Attribute\FirstClassTwigCallableReady; use Twig\Compiler; use Twig\Error\SyntaxError; -use Twig\Extension\CoreExtension; use Twig\Node\Expression\ArrayExpression; use Twig\Node\Expression\BlockReferenceExpression; use Twig\Node\Expression\ConstantExpression; @@ -58,7 +57,7 @@ public function __construct(Node $node, TwigTest|string $name, ?Node $arguments, throw new SyntaxError('The "defined" test only works with simple variables.', $lineno); } - if (is_string($name) && 'defined' !== $name) { + if (\is_string($name) && 'defined' !== $name) { trigger_deprecation('twig/twig', '3.12', 'Creating a "DefinedTest" instance with a test name that is not "defined" is deprecated.'); } diff --git a/src/Node/Node.php b/src/Node/Node.php index a9bd84a3ee8..770cdaf4342 100644 --- a/src/Node/Node.php +++ b/src/Node/Node.php @@ -15,7 +15,6 @@ use Twig\Attribute\YieldReady; use Twig\Compiler; use Twig\Source; -use Twig\TwigCallableInterface; /** * Represents a node in the AST. diff --git a/src/Util/CallableArgumentsExtractor.php b/src/Util/CallableArgumentsExtractor.php index 1eb6a45249a..e8dea521798 100644 --- a/src/Util/CallableArgumentsExtractor.php +++ b/src/Util/CallableArgumentsExtractor.php @@ -65,8 +65,8 @@ public function extractArguments(Node $arguments): array if (!$named && !$this->twigCallable->isVariadic()) { $min = $this->twigCallable->getMinimalNumberOfRequiredArguments(); - if (count($extractedArguments) < $rc->getReflector()->getNumberOfRequiredParameters() - $min) { - throw new SyntaxError(\sprintf('Value for argument "%s" is required for %s "%s".', $rc->getReflector()->getParameters()[$min + count($extractedArguments)]->getName(), $this->type, $this->name), $this->node->getTemplateLine(), $this->node->getSourceContext()); + if (\count($extractedArguments) < $rc->getReflector()->getNumberOfRequiredParameters() - $min) { + throw new SyntaxError(\sprintf('Value for argument "%s" is required for %s "%s".', $rc->getReflector()->getParameters()[$min + \count($extractedArguments)]->getName(), $this->type, $this->name), $this->node->getTemplateLine(), $this->node->getSourceContext()); } return $extractedArguments; diff --git a/tests/LexerTest.php b/tests/LexerTest.php index 81690829a08..87dfd2e8d1e 100644 --- a/tests/LexerTest.php +++ b/tests/LexerTest.php @@ -495,7 +495,7 @@ public function testStrings(string $expected) public function getTemplateForStrings() { - yield ["日本では、春になると桜の花が咲きます。多くの人々は、公園や川の近くに集まり、お花見を楽しみます。桜の花びらが風に舞い、まるで雪のように見える瞬間は、とても美しいです。"]; - yield ["في العالم العربي، يُعتبر الخط العربي أحد أجمل أشكال الفن. يُستخدم الخط في تزيين المساجد والكتب والمخطوطات القديمة. يتميز الخط العربي بجماله وتناسقه، ويُعتبر رمزًا للثقافة الإسلامية."]; + yield ['日本では、春になると桜の花が咲きます。多くの人々は、公園や川の近くに集まり、お花見を楽しみます。桜の花びらが風に舞い、まるで雪のように見える瞬間は、とても美しいです。']; + yield ['في العالم العربي، يُعتبر الخط العربي أحد أجمل أشكال الفن. يُستخدم الخط في تزيين المساجد والكتب والمخطوطات القديمة. يتميز الخط العربي بجماله وتناسقه، ويُعتبر رمزًا للثقافة الإسلامية.']; } } diff --git a/tests/Node/NodeTest.php b/tests/Node/NodeTest.php index b224c36365d..94c9197b50b 100644 --- a/tests/Node/NodeTest.php +++ b/tests/Node/NodeTest.php @@ -47,7 +47,7 @@ public function testAttributeDeprecationIgnore() $node = new Node([], ['foo' => false]); $node->deprecateAttribute('foo', new NameDeprecation('foo/bar', '2.0', 'bar')); - $this->assertSame(false, $node->getAttribute('foo', false)); + $this->assertFalse($node->getAttribute('foo', false)); } /** @@ -59,7 +59,7 @@ public function testAttributeDeprecationWithoutAlternative() $node->deprecateAttribute('foo', new NameDeprecation('foo/bar', '2.0')); $this->expectDeprecation('Since foo/bar 2.0: Getting attribute "foo" on a "Twig\Node\Node" class is deprecated.'); - $this->assertSame(false, $node->getAttribute('foo')); + $this->assertFalse($node->getAttribute('foo')); } /** @@ -71,7 +71,7 @@ public function testAttributeDeprecationWithAlternative() $node->deprecateAttribute('foo', new NameDeprecation('foo/bar', '2.0', 'bar')); $this->expectDeprecation('Since foo/bar 2.0: Getting attribute "foo" on a "Twig\Node\Node" class is deprecated, get the "bar" attribute instead.'); - $this->assertSame(false, $node->getAttribute('foo')); + $this->assertFalse($node->getAttribute('foo')); } public function testNodeDeprecationIgnore() From 5d1a19a80cbf52009682cd314ebeacd6aac5b4d8 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Mon, 19 Aug 2024 17:17:59 +0200 Subject: [PATCH 364/812] Swap BC layer for yield-ready and reclaim perf loss --- src/Node/FlushNode.php | 11 +++-- src/Template.php | 107 ++++++++++++++++++++--------------------- 2 files changed, 60 insertions(+), 58 deletions(-) diff --git a/src/Node/FlushNode.php b/src/Node/FlushNode.php index 8a3dde6fcc2..3f5a2111447 100644 --- a/src/Node/FlushNode.php +++ b/src/Node/FlushNode.php @@ -29,9 +29,12 @@ public function __construct(int $lineno, string $tag) public function compile(Compiler $compiler): void { - $compiler - ->addDebugInfo($this) - ->write("flush();\n") - ; + $compiler->addDebugInfo($this); + + if ($compiler->getEnvironment()->useYield()) { + $compiler->write("yield '';\n"); + } + + $compiler->write("flush();\n"); } } diff --git a/src/Template.php b/src/Template.php index 04c530cc9c8..d3c0c229d2b 100644 --- a/src/Template.php +++ b/src/Template.php @@ -166,6 +166,17 @@ public function displayBlock($name, array $context, array $blocks = [], $useBloc */ public function renderParentBlock($name, array $context, array $blocks = []) { + if (!$this->useYield) { + if ($this->env->isDebug()) { + ob_start(); + } else { + ob_start(function () { return ''; }); + } + $this->displayParentBlock($name, $context, $blocks); + + return ob_get_clean(); + } + $content = ''; foreach ($this->yieldParentBlock($name, $context, $blocks) as $data) { $content .= $data; @@ -189,6 +200,26 @@ public function renderParentBlock($name, array $context, array $blocks = []) */ public function renderBlock($name, array $context, array $blocks = [], $useBlocks = true) { + if (!$this->useYield) { + $level = ob_get_level(); + if ($this->env->isDebug()) { + ob_start(); + } else { + ob_start(function () { return ''; }); + } + try { + $this->displayBlock($name, $context, $blocks, $useBlocks); + } catch (\Throwable $e) { + while (ob_get_level() > $level) { + ob_end_clean(); + } + + throw $e; + } + + return ob_get_clean(); + } + $content = ''; foreach ($this->yieldBlock($name, $context, $blocks, $useBlocks) as $data) { $content .= $data; @@ -331,6 +362,26 @@ public function display(array $context, array $blocks = []): void public function render(array $context): string { + if (!$this->useYield) { + $level = ob_get_level(); + if ($this->env->isDebug()) { + ob_start(); + } else { + ob_start(function () { return ''; }); + } + try { + $this->display($context); + } catch (\Throwable $e) { + while (ob_get_level() > $level) { + ob_end_clean(); + } + + throw $e; + } + + return ob_get_clean(); + } + $content = ''; foreach ($this->yield($context) as $data) { $content .= $data; @@ -348,27 +399,7 @@ public function yield(array $context, array $blocks = []): iterable $blocks = array_merge($this->blocks, $blocks); try { - if ($this->useYield) { - yield from $this->doDisplay($context, $blocks); - - return; - } - - $level = ob_get_level(); - ob_start(); - - foreach ($this->doDisplay($context, $blocks) as $data) { - if (ob_get_length()) { - $data = ob_get_clean().$data; - ob_start(); - } - - yield $data; - } - - if (ob_get_length()) { - yield ob_get_clean(); - } + yield from $this->doDisplay($context, $blocks); } catch (Error $e) { if (!$e->getSourceContext()) { $e->setSourceContext($this->getSourceContext()); @@ -386,12 +417,6 @@ public function yield(array $context, array $blocks = []): iterable $e->guess(); throw $e; - } finally { - if (!$this->useYield) { - while (ob_get_level() > $level) { - ob_end_clean(); - } - } } } @@ -418,27 +443,7 @@ public function yieldBlock($name, array $context, array $blocks = [], $useBlocks if (null !== $template) { try { - if ($this->useYield) { - yield from $template->$block($context, $blocks); - - return; - } - - $level = ob_get_level(); - ob_start(); - - foreach ($template->$block($context, $blocks) as $data) { - if (ob_get_length()) { - $data = ob_get_clean().$data; - ob_start(); - } - - yield $data; - } - - if (ob_get_length()) { - yield ob_get_clean(); - } + yield from $template->$block($context, $blocks); } catch (Error $e) { if (!$e->getSourceContext()) { $e->setSourceContext($template->getSourceContext()); @@ -456,12 +461,6 @@ public function yieldBlock($name, array $context, array $blocks = [], $useBlocks $e->guess(); throw $e; - } finally { - if (!$this->useYield) { - while (ob_get_level() > $level) { - ob_end_clean(); - } - } } } elseif ($parent = $this->getParent($context)) { yield from $parent->unwrap()->yieldBlock($name, $context, array_merge($this->blocks, $blocks), false, $templateContext ?? $this); From 3a307cd41da4946d73d8b7d65f4ceacb4adec971 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Mon, 19 Aug 2024 20:06:41 +0200 Subject: [PATCH 365/812] Update CHANGELOG and docs --- CHANGELOG | 2 ++ src/Environment.php | 6 +++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 8770369b4d6..fa50ff1e5ea 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,7 @@ # 3.12.0 (2024-XX-XX) + * Fix performance regression when `use_yield` is `false` (which is the default) + * Improve compatibility when `use_yield` is `false` (as extensions still using `echo` will work as is) * Accept colons (`:`) in addition to equals (`=`) to separate argument names and values in named arguments * Add the `html_cva` function (in the HTML extra package) * Add support for named arguments to the `block` and `attribute` functions diff --git a/src/Environment.php b/src/Environment.php index 96bf8afbdb1..237d598032a 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -103,9 +103,9 @@ class Environment * (default to -1 which means that all optimizations are enabled; * set it to 0 to disable). * - * * use_yield: Enable templates to exclusively use "yield" instead of "echo" - * (default to "false", but switch it to "true" when possible - * as this will be the only supported mode in Twig 4.0) + * * use_yield: true: forces templates to exclusively use "yield" instead of "echo" (all extensions must be yield ready) + * false (default): allows templates to use a mix of "yield" and "echo" calls to allow for a progressive migration + * Switch to "true" when possible as this will be the only supported mode in Twig 4.0 */ public function __construct(LoaderInterface $loader, $options = []) { From 63e35b9620c248de74f4ec20bd099b37d21b7ebc Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Tue, 20 Aug 2024 09:34:22 +0200 Subject: [PATCH 366/812] Add a test --- .../macros/unknown_macro_different_template.test | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 tests/Fixtures/macros/unknown_macro_different_template.test diff --git a/tests/Fixtures/macros/unknown_macro_different_template.test b/tests/Fixtures/macros/unknown_macro_different_template.test new file mode 100644 index 00000000000..61604e8a93f --- /dev/null +++ b/tests/Fixtures/macros/unknown_macro_different_template.test @@ -0,0 +1,11 @@ +--TEST-- +Exception for unknown macro in different template +--TEMPLATE-- +{% import foo_template as macros %} +{{ macros.foo() }} +--TEMPLATE(foo.twig)-- +foo +--DATA-- +return array('foo_template' => 'foo.twig') +--EXCEPTION-- +Twig\Error\RuntimeError: Macro "foo" is not defined in template "foo.twig" in "index.twig" at line 3. From 31037d0e51dde5f0386477d502bbe813001e3a7b Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Tue, 20 Aug 2024 15:19:57 +0200 Subject: [PATCH 367/812] Refactor code --- src/Node/Expression/CallExpression.php | 8 ++--- src/Parser.php | 2 +- src/TwigCallableInterface.php | 2 ++ src/TwigFilter.php | 5 +++ src/TwigFunction.php | 5 +++ src/TwigTest.php | 5 +++ src/Util/CallableArgumentsExtractor.php | 43 +++++++++---------------- src/Util/ReflectionCallable.php | 10 ++++-- tests/Node/Expression/CallTest.php | 28 ++++++++-------- 9 files changed, 58 insertions(+), 50 deletions(-) diff --git a/src/Node/Expression/CallExpression.php b/src/Node/Expression/CallExpression.php index f61adc7254e..6fc6f66e0ad 100644 --- a/src/Node/Expression/CallExpression.php +++ b/src/Node/Expression/CallExpression.php @@ -34,7 +34,7 @@ protected function compileCallable(Compiler $compiler) if (\is_string($callable) && !str_contains($callable, '::')) { $compiler->raw($callable); } else { - $rc = $this->reflectCallable($callable); + $rc = $this->reflectCallable($twigCallable); $r = $rc->getReflector(); $callable = $rc->getCallable(); @@ -271,7 +271,7 @@ protected function normalizeName(string $name): string private function getCallableParameters($callable, bool $isVariadic): array { $twigCallable = $this->getAttribute('twig_callable'); - $rc = $this->reflectCallable($callable); + $rc = $this->reflectCallable($twigCallable); $r = $rc->getReflector(); $callableName = $rc->getName(); @@ -309,10 +309,10 @@ private function getCallableParameters($callable, bool $isVariadic): array return [$parameters, $isPhpVariadic]; } - private function reflectCallable($callable): ReflectionCallable + private function reflectCallable(TwigCallableInterface $callable): ReflectionCallable { if (!$this->reflector) { - $this->reflector = new ReflectionCallable($callable, $this->getAttribute('type'), $this->getAttribute('name')); + $this->reflector = new ReflectionCallable($callable); } return $this->reflector; diff --git a/src/Parser.php b/src/Parser.php index 06d7781e7df..28cc8a0e11a 100644 --- a/src/Parser.php +++ b/src/Parser.php @@ -157,7 +157,7 @@ public function subparse($test, bool $dropNeedle = false): Node if (null !== $test) { $e = new SyntaxError(\sprintf('Unexpected "%s" tag', $token->getValue()), $token->getLine(), $this->stream->getSourceContext()); - $callable = (new ReflectionCallable($test))->getCallable(); + $callable = (new ReflectionCallable(new TwigTest('decision', $test)))->getCallable(); if (\is_array($callable) && $callable[0] instanceof TokenParserInterface) { $e->appendMessage(\sprintf(' (expecting closing tag for the "%s" tag defined near line %s).', $callable[0]->getTag(), $lineno)); } diff --git a/src/TwigCallableInterface.php b/src/TwigCallableInterface.php index 13a10cd3be2..2a8ff6116bc 100644 --- a/src/TwigCallableInterface.php +++ b/src/TwigCallableInterface.php @@ -18,6 +18,8 @@ interface TwigCallableInterface extends \Stringable { public function getName(): string; + public function getType(): string; + public function getDynamicName(): string; /** diff --git a/src/TwigFilter.php b/src/TwigFilter.php index 7eb66f713a4..70b1f8f3fc6 100644 --- a/src/TwigFilter.php +++ b/src/TwigFilter.php @@ -39,6 +39,11 @@ public function __construct(string $name, $callable = null, array $options = []) ], $this->options); } + public function getType(): string + { + return 'filter'; + } + public function getSafe(Node $filterArgs): ?array { if (null !== $this->options['is_safe']) { diff --git a/src/TwigFunction.php b/src/TwigFunction.php index d90a7e42cce..4a10df95e65 100644 --- a/src/TwigFunction.php +++ b/src/TwigFunction.php @@ -38,6 +38,11 @@ public function __construct(string $name, $callable = null, array $options = []) ], $this->options); } + public function getType(): string + { + return 'function'; + } + public function getParserCallable(): ?callable { return $this->options['parser_callable']; diff --git a/src/TwigTest.php b/src/TwigTest.php index 570ffd85212..5e58ad8b0e8 100644 --- a/src/TwigTest.php +++ b/src/TwigTest.php @@ -35,6 +35,11 @@ public function __construct(string $name, $callable = null, array $options = []) ], $this->options); } + public function getType(): string + { + return 'test'; + } + public function needsCharset(): bool { return false; diff --git a/src/Util/CallableArgumentsExtractor.php b/src/Util/CallableArgumentsExtractor.php index e8dea521798..8811ca9c87e 100644 --- a/src/Util/CallableArgumentsExtractor.php +++ b/src/Util/CallableArgumentsExtractor.php @@ -17,9 +17,6 @@ use Twig\Node\Expression\VariadicExpression; use Twig\Node\Node; use Twig\TwigCallableInterface; -use Twig\TwigFilter; -use Twig\TwigFunction; -use Twig\TwigTest; /** * @author Fabien Potencier @@ -28,20 +25,13 @@ */ final class CallableArgumentsExtractor { - private string $type; - private string $name; + private ReflectionCallable $rc; public function __construct( private Node $node, private TwigCallableInterface $twigCallable, ) { - $this->type = match (true) { - $twigCallable instanceof TwigFunction => 'function', - $twigCallable instanceof TwigFilter => 'filter', - $twigCallable instanceof TwigTest => 'test', - default => throw new \LogicException('Unknown callable type.'), - }; - $this->name = $twigCallable->getName(); + $this->rc = new ReflectionCallable($twigCallable); } /** @@ -49,7 +39,6 @@ public function __construct( */ public function extractArguments(Node $arguments): array { - $rc = new ReflectionCallable($this->twigCallable->getCallable(), $this->type, $this->name); $extractedArguments = []; $named = false; foreach ($arguments as $name => $node) { @@ -57,7 +46,7 @@ public function extractArguments(Node $arguments): array $named = true; $name = $this->normalizeName($name); } elseif ($named) { - throw new SyntaxError(\sprintf('Positional arguments cannot be used after named arguments for %s "%s".', $this->type, $this->name), $this->node->getTemplateLine(), $this->node->getSourceContext()); + throw new SyntaxError(\sprintf('Positional arguments cannot be used after named arguments for %s "%s".', $this->twigCallable->getType(), $this->twigCallable->getName()), $this->node->getTemplateLine(), $this->node->getSourceContext()); } $extractedArguments[$name] = $node; @@ -65,8 +54,8 @@ public function extractArguments(Node $arguments): array if (!$named && !$this->twigCallable->isVariadic()) { $min = $this->twigCallable->getMinimalNumberOfRequiredArguments(); - if (\count($extractedArguments) < $rc->getReflector()->getNumberOfRequiredParameters() - $min) { - throw new SyntaxError(\sprintf('Value for argument "%s" is required for %s "%s".', $rc->getReflector()->getParameters()[$min + \count($extractedArguments)]->getName(), $this->type, $this->name), $this->node->getTemplateLine(), $this->node->getSourceContext()); + if (\count($extractedArguments) < $this->rc->getReflector()->getNumberOfRequiredParameters() - $min) { + throw new SyntaxError(\sprintf('Value for argument "%s" is required for %s "%s".', $this->rc->getReflector()->getParameters()[$min + \count($extractedArguments)]->getName(), $this->twigCallable->getType(), $this->twigCallable->getName()), $this->node->getTemplateLine(), $this->node->getSourceContext()); } return $extractedArguments; @@ -74,13 +63,13 @@ public function extractArguments(Node $arguments): array if (!$callable = $this->twigCallable->getCallable()) { if ($named) { - throw new SyntaxError(\sprintf('Named arguments are not supported for %s "%s".', $this->type, $this->name)); + throw new SyntaxError(\sprintf('Named arguments are not supported for %s "%s".', $this->twigCallable->getType(), $this->twigCallable->getName())); } - throw new SyntaxError(\sprintf('Arbitrary positional arguments are not supported for %s "%s".', $this->type, $this->name)); + throw new SyntaxError(\sprintf('Arbitrary positional arguments are not supported for %s "%s".', $this->twigCallable->getType(), $this->twigCallable->getName())); } - [$callableParameters, $isPhpVariadic] = $this->getCallableParameters($rc); + [$callableParameters, $isPhpVariadic] = $this->getCallableParameters(); $arguments = []; $names = []; $missingArguments = []; @@ -100,13 +89,13 @@ public function extractArguments(Node $arguments): array if (\array_key_exists($name, $extractedArguments)) { if (\array_key_exists($pos, $extractedArguments)) { - throw new SyntaxError(\sprintf('Argument "%s" is defined twice for %s "%s".', $name, $this->type, $this->name), $this->node->getTemplateLine(), $this->node->getSourceContext()); + throw new SyntaxError(\sprintf('Argument "%s" is defined twice for %s "%s".', $name, $this->twigCallable->getType(), $this->twigCallable->getName()), $this->node->getTemplateLine(), $this->node->getSourceContext()); } if (\count($missingArguments)) { throw new SyntaxError(\sprintf( 'Argument "%s" could not be assigned for %s "%s(%s)" because it is mapped to an internal PHP function which cannot determine default value for optional argument%s "%s".', - $name, $this->type, $this->name, implode(', ', $names), \count($missingArguments) > 1 ? 's' : '', implode('", "', $missingArguments) + $name, $this->twigCallable->getType(), $this->twigCallable->getName(), implode(', ', $names), \count($missingArguments) > 1 ? 's' : '', implode('", "', $missingArguments) ), $this->node->getTemplateLine(), $this->node->getSourceContext()); } @@ -129,7 +118,7 @@ public function extractArguments(Node $arguments): array $missingArguments[] = $name; } else { - throw new SyntaxError(\sprintf('Value for argument "%s" is required for %s "%s".', $name, $this->type, $this->name), $this->node->getTemplateLine(), $this->node->getSourceContext()); + throw new SyntaxError(\sprintf('Value for argument "%s" is required for %s "%s".', $name, $this->twigCallable->getType(), $this->twigCallable->getName()), $this->node->getTemplateLine(), $this->node->getSourceContext()); } } @@ -162,7 +151,7 @@ public function extractArguments(Node $arguments): array throw new SyntaxError( \sprintf( 'Unknown argument%s "%s" for %s "%s(%s)".', - \count($extractedArguments) > 1 ? 's' : '', implode('", "', array_keys($extractedArguments)), $this->type, $this->name, implode(', ', $names) + \count($extractedArguments) > 1 ? 's' : '', implode('", "', array_keys($extractedArguments)), $this->twigCallable->getType(), $this->twigCallable->getName(), implode(', ', $names) ), $unknownArgument ? $unknownArgument->getTemplateLine() : $this->node->getTemplateLine(), $unknownArgument ? $unknownArgument->getSourceContext() : $this->node->getSourceContext() @@ -177,11 +166,9 @@ private function normalizeName(string $name): string return strtolower(preg_replace(['/([A-Z]+)([A-Z][a-z])/', '/([a-z\d])([A-Z])/'], ['\\1_\\2', '\\1_\\2'], $name)); } - private function getCallableParameters(ReflectionCallable $rc): array + private function getCallableParameters(): array { - $r = $rc->getReflector(); - - $parameters = $r->getParameters(); + $parameters = $this->rc->getReflector()->getParameters(); if ($this->node->hasNode('node')) { array_shift($parameters); } @@ -208,7 +195,7 @@ private function getCallableParameters(ReflectionCallable $rc): array array_pop($parameters); $isPhpVariadic = true; } else { - throw new SyntaxError(\sprintf('The last parameter of "%s" for %s "%s" must be an array with default value, eg. "array $arg = []".', $rc->getName(), $this->type, $this->name)); + throw new SyntaxError(\sprintf('The last parameter of "%s" for %s "%s" must be an array with default value, eg. "array $arg = []".', $this->rc->getName(), $this->twigCallable->getType(), $this->twigCallable->getName())); } } diff --git a/src/Util/ReflectionCallable.php b/src/Util/ReflectionCallable.php index 54384e14bd8..b4a70939979 100644 --- a/src/Util/ReflectionCallable.php +++ b/src/Util/ReflectionCallable.php @@ -11,6 +11,8 @@ namespace Twig\Util; +use Twig\TwigCallableInterface; + /** * @author Fabien Potencier * @@ -22,8 +24,10 @@ final class ReflectionCallable private $callable = null; private $name; - public function __construct($callable, string $debugType = 'unknown', string $debugName = 'unknown') - { + public function __construct( + private TwigCallableInterface $twigCallable, + ) { + $callable = $twigCallable->getCallable(); if (\is_string($callable) && false !== $pos = strpos($callable, '::')) { $callable = [substr($callable, 0, $pos), substr($callable, 2 + $pos)]; } @@ -40,7 +44,7 @@ public function __construct($callable, string $debugType = 'unknown', string $de try { $closure = \Closure::fromCallable($callable); } catch (\TypeError $e) { - throw new \LogicException(\sprintf('Callback for %s "%s" is not callable in the current scope.', $debugType, $debugName), 0, $e); + throw new \LogicException(\sprintf('Callback for %s "%s" is not callable in the current scope.', $twigCallable->getType(), $twigCallable->getName()), 0, $e); } $this->reflector = $r = new \ReflectionFunction($closure); diff --git a/tests/Node/Expression/CallTest.php b/tests/Node/Expression/CallTest.php index 8486a3c4c16..a1ea76fd445 100644 --- a/tests/Node/Expression/CallTest.php +++ b/tests/Node/Expression/CallTest.php @@ -24,7 +24,7 @@ class CallTest extends TestCase { public function testGetArguments() { - $node = $this->createFunctionExpression('date'); + $node = $this->createFunctionExpression('date', 'date'); $this->assertEquals(['U', null], $this->getArguments($node, ['date', ['format' => 'U', 'timestamp' => null]])); } @@ -33,7 +33,7 @@ public function testGetArgumentsWhenPositionalArgumentsAfterNamedArguments() $this->expectException(SyntaxError::class); $this->expectExceptionMessage('Positional arguments cannot be used after named arguments for function "date".'); - $node = $this->createFunctionExpression('date'); + $node = $this->createFunctionExpression('date', 'date'); $this->getArguments($node, ['date', ['timestamp' => 123456, 'Y-m-d']]); } @@ -42,7 +42,7 @@ public function testGetArgumentsWhenArgumentIsDefinedTwice() $this->expectException(SyntaxError::class); $this->expectExceptionMessage('Argument "format" is defined twice for function "date".'); - $node = $this->createFunctionExpression('date'); + $node = $this->createFunctionExpression('date', 'date'); $this->getArguments($node, ['date', ['Y-m-d', 'format' => 'U']]); } @@ -51,7 +51,7 @@ public function testGetArgumentsWithWrongNamedArgumentName() $this->expectException(SyntaxError::class); $this->expectExceptionMessage('Unknown argument "unknown" for function "date(format, timestamp)".'); - $node = $this->createFunctionExpression('date'); + $node = $this->createFunctionExpression('date', 'date'); $this->getArguments($node, ['date', ['Y-m-d', 'timestamp' => null, 'unknown' => '']]); } @@ -60,7 +60,7 @@ public function testGetArgumentsWithWrongNamedArgumentNames() $this->expectException(SyntaxError::class); $this->expectExceptionMessage('Unknown arguments "unknown1", "unknown2" for function "date(format, timestamp)".'); - $node = $this->createFunctionExpression('date'); + $node = $this->createFunctionExpression('date', 'date'); $this->getArguments($node, ['date', ['Y-m-d', 'timestamp' => null, 'unknown1' => '', 'unknown2' => '']]); } @@ -73,19 +73,19 @@ public function testResolveArgumentsWithMissingValueForOptionalArgument() $this->expectException(SyntaxError::class); $this->expectExceptionMessage('Argument "case_sensitivity" could not be assigned for function "substr_compare(main_str, str, offset, length, case_sensitivity)" because it is mapped to an internal PHP function which cannot determine default value for optional argument "length".'); - $node = $this->createFunctionExpression('substr_compare'); + $node = $this->createFunctionExpression('substr_compare', 'substr_compare'); $this->getArguments($node, ['substr_compare', ['abcd', 'bc', 'offset' => 1, 'case_sensitivity' => true]]); } public function testResolveArgumentsOnlyNecessaryArgumentsForCustomFunction() { - $node = $this->createFunctionExpression('custom_function'); + $node = $this->createFunctionExpression('custom_function', [$this, 'customFunction']); $this->assertEquals(['arg1'], $this->getArguments($node, [[$this, 'customFunction'], ['arg1' => 'arg1']])); } public function testGetArgumentsForStaticMethod() { - $node = $this->createFunctionExpression('custom_static_function'); + $node = $this->createFunctionExpression('custom_static_function', __CLASS__.'::customStaticFunction'); $this->assertEquals(['arg1'], $this->getArguments($node, [__CLASS__.'::customStaticFunction', ['arg1' => 'arg1']])); } @@ -94,7 +94,7 @@ public function testResolveArgumentsWithMissingParameterForArbitraryArguments() $this->expectException(\LogicException::class); $this->expectExceptionMessage('The last parameter of "Twig\\Tests\\Node\\Expression\\CallTest::customFunctionWithArbitraryArguments" for function "foo" must be an array with default value, eg. "array $arg = []".'); - $node = $this->createFunctionExpression('foo', true); + $node = $this->createFunctionExpression('foo', [$this, 'customFunctionWithArbitraryArguments'], true); $this->getArguments($node, [[$this, 'customFunctionWithArbitraryArguments'], []]); } @@ -102,7 +102,7 @@ public function testGetArgumentsWithInvalidCallable() { $this->expectException(\LogicException::class); $this->expectExceptionMessage('Callback for function "foo" is not callable in the current scope.'); - $node = $this->createFunctionExpression('foo', true); + $node = $this->createFunctionExpression('foo', '', true); $this->getArguments($node, ['', []]); } @@ -111,7 +111,7 @@ public function testResolveArgumentsWithMissingParameterForArbitraryArgumentsOnF $this->expectException(\LogicException::class); $this->expectExceptionMessageMatches('#^The last parameter of "Twig\\\\Tests\\\\Node\\\\Expression\\\\custom_call_test_function" for function "foo" must be an array with default value, eg\\. "array \\$arg \\= \\[\\]"\\.$#'); - $node = $this->createFunctionExpression('foo', true); + $node = $this->createFunctionExpression('foo', 'Twig\Tests\Node\Expression\custom_call_test_function', true); $this->getArguments($node, ['Twig\Tests\Node\Expression\custom_call_test_function', []]); } @@ -120,7 +120,7 @@ public function testResolveArgumentsWithMissingParameterForArbitraryArgumentsOnO $this->expectException(\LogicException::class); $this->expectExceptionMessageMatches('#^The last parameter of "Twig\\\\Tests\\\\Node\\\\Expression\\\\CallableTestClass\\:\\:__invoke" for function "foo" must be an array with default value, eg\\. "array \\$arg \\= \\[\\]"\\.$#'); - $node = $this->createFunctionExpression('foo', true); + $node = $this->createFunctionExpression('foo', new CallableTestClass(), true); $this->getArguments($node, [new CallableTestClass(), []]); } @@ -144,9 +144,9 @@ private function getArguments($call, $args) return $m->invokeArgs($call, $args); } - private function createFunctionExpression($name, $isVariadic = false): Node_Expression_Call + private function createFunctionExpression($name, $callable, $isVariadic = false): Node_Expression_Call { - return new Node_Expression_Call(new TwigFunction($name, null, ['is_variadic' => $isVariadic]), new Node([]), 0); + return new Node_Expression_Call(new TwigFunction($name, $callable, ['is_variadic' => $isVariadic]), new Node([]), 0); } } From 6ef13d1e0b2fb0adea25c557d42d503f52d2d1a9 Mon Sep 17 00:00:00 2001 From: "Nikola Svitlica a.k.a TheCelavi" Date: Sat, 17 Aug 2024 19:45:41 +0200 Subject: [PATCH 368/812] Resolves #4200 --- src/Loader/ChainLoader.php | 40 +++++++++++++++++++++++++++----------- tests/Loader/ChainTest.php | 28 ++++++++++++++++++++++---- 2 files changed, 53 insertions(+), 15 deletions(-) diff --git a/src/Loader/ChainLoader.php b/src/Loader/ChainLoader.php index 163c029f848..8393711fc92 100644 --- a/src/Loader/ChainLoader.php +++ b/src/Loader/ChainLoader.php @@ -21,22 +21,33 @@ */ final class ChainLoader implements LoaderInterface { + /** + * @var \Traversable|LoaderInterface[] + */ + private $loaders; + + /** + * @var array + */ private $hasSourceCache = []; - private $loaders = []; /** - * @param LoaderInterface[] $loaders + * @param iterable $loaders */ - public function __construct(array $loaders = []) + public function __construct(iterable $loaders = []) { - foreach ($loaders as $loader) { - $this->addLoader($loader); - } + $this->loaders = $loaders; } public function addLoader(LoaderInterface $loader): void { - $this->loaders[] = $loader; + $current = $this->loaders; + + $this->loaders = (static function () use ($current, $loader): \Generator { + yield from $current; + yield $loader; + })(); + $this->hasSourceCache = []; } @@ -45,13 +56,18 @@ public function addLoader(LoaderInterface $loader): void */ public function getLoaders(): array { + if (!\is_array($this->loaders)) { + $this->loaders = \iterator_to_array($this->loaders, false); + } + return $this->loaders; } public function getSourceContext(string $name): Source { $exceptions = []; - foreach ($this->loaders as $loader) { + + foreach ($this->getLoaders() as $loader) { if (!$loader->exists($name)) { continue; } @@ -72,7 +88,7 @@ public function exists(string $name): bool return $this->hasSourceCache[$name]; } - foreach ($this->loaders as $loader) { + foreach ($this->getLoaders() as $loader) { if ($loader->exists($name)) { return $this->hasSourceCache[$name] = true; } @@ -84,7 +100,8 @@ public function exists(string $name): bool public function getCacheKey(string $name): string { $exceptions = []; - foreach ($this->loaders as $loader) { + + foreach ($this->getLoaders() as $loader) { if (!$loader->exists($name)) { continue; } @@ -102,7 +119,8 @@ public function getCacheKey(string $name): string public function isFresh(string $name, int $time): bool { $exceptions = []; - foreach ($this->loaders as $loader) { + + foreach ($this->getLoaders() as $loader) { if (!$loader->exists($name)) { continue; } diff --git a/tests/Loader/ChainTest.php b/tests/Loader/ChainTest.php index faaaebe33ae..52d6d4c7202 100644 --- a/tests/Loader/ChainTest.php +++ b/tests/Loader/ChainTest.php @@ -72,10 +72,30 @@ public function testGetCacheKeyWhenTemplateDoesNotExist() public function testAddLoader() { - $loader = new ChainLoader(); - $loader->addLoader(new ArrayLoader(['foo' => 'bar'])); - - $this->assertEquals('bar', $loader->getSourceContext('foo')->getCode()); + $fooLoader = new ArrayLoader(['foo' => 'foo:code']); + $barLoader = new ArrayLoader(['bar' => 'bar:code']); + $bazLoader = new ArrayLoader(['baz' => 'baz:code']); + $quxLoader = new ArrayLoader(['qux' => 'qux:code']); + + $loader = new ChainLoader((static function () use ($fooLoader, $barLoader): \Generator { + yield $fooLoader; + yield $barLoader; + })()); + + $loader->addLoader($bazLoader); + $loader->addLoader($quxLoader); + + $this->assertEquals('foo:code', $loader->getSourceContext('foo')->getCode()); + $this->assertEquals('bar:code', $loader->getSourceContext('bar')->getCode()); + $this->assertEquals('baz:code', $loader->getSourceContext('baz')->getCode()); + $this->assertEquals('qux:code', $loader->getSourceContext('qux')->getCode()); + + $this->assertEquals([ + $fooLoader, + $barLoader, + $bazLoader, + $quxLoader, + ], $loader->getLoaders()); } public function testExists() From f4bca4fa448c3045e5c0317c49d117d5c9d59cef Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Tue, 20 Aug 2024 18:06:11 +0200 Subject: [PATCH 369/812] Remove obsolete comment --- src/Extension/CoreExtension.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index 2cf889e7a58..73caf8a5d30 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -817,9 +817,6 @@ public static function split(string $charset, $value, $delimiter, $limit = null) return $r; } - // The '_default' filter is used internally to avoid using the ternary operator - // which costs a lot for big contexts (before PHP 5.4). So, on average, - // a function call is cheaper. /** * @internal */ From 302971f34ec5dd93364674b5c5776e8a7c8d266b Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Tue, 20 Aug 2024 18:10:26 +0200 Subject: [PATCH 370/812] Fix CS --- src/Loader/ChainLoader.php | 2 +- src/Util/ReflectionCallable.php | 2 +- tests/LexerTest.php | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Loader/ChainLoader.php b/src/Loader/ChainLoader.php index 8393711fc92..90f798db3fe 100644 --- a/src/Loader/ChainLoader.php +++ b/src/Loader/ChainLoader.php @@ -57,7 +57,7 @@ public function addLoader(LoaderInterface $loader): void public function getLoaders(): array { if (!\is_array($this->loaders)) { - $this->loaders = \iterator_to_array($this->loaders, false); + $this->loaders = iterator_to_array($this->loaders, false); } return $this->loaders; diff --git a/src/Util/ReflectionCallable.php b/src/Util/ReflectionCallable.php index b4a70939979..9b183f14d27 100644 --- a/src/Util/ReflectionCallable.php +++ b/src/Util/ReflectionCallable.php @@ -21,7 +21,7 @@ final class ReflectionCallable { private $reflector; - private $callable = null; + private $callable; private $name; public function __construct( diff --git a/tests/LexerTest.php b/tests/LexerTest.php index 87dfd2e8d1e..158836ac447 100644 --- a/tests/LexerTest.php +++ b/tests/LexerTest.php @@ -252,7 +252,7 @@ public function getStringWithEscapedDelimiterProducingDeprecation() yield '{{ \'App\Test\' }} => AppTest' => [ '{{ \'App\\Test\' }}', 'AppTest', - "Since twig/twig 3.12: Character \"T\" at position 5 does not need to be escaped anymore.", + 'Since twig/twig 3.12: Character "T" at position 5 does not need to be escaped anymore.', ]; yield '{{ "foo \\\' bar" }} => foo \' bar' => [ '{{ "foo \\\' bar" }}', From 90107929daf0cc3ebdbcc41bc963e0371ca61ddd Mon Sep 17 00:00:00 2001 From: Ruud Kamphuis Date: Wed, 21 Aug 2024 19:36:27 +0200 Subject: [PATCH 371/812] Improve deprecation message See https://github.com/twigphp/Twig/pull/4199#discussion_r1723456834 --- src/Lexer.php | 4 ++-- tests/LexerTest.php | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Lexer.php b/src/Lexer.php index 78b07156506..28feaa2c128 100644 --- a/src/Lexer.php +++ b/src/Lexer.php @@ -418,7 +418,7 @@ private function stripcslashes(string $str, string $quoteType): string $result .= $nextChar; } elseif ("'" === $nextChar || '"' === $nextChar) { if ($nextChar !== $quoteType) { - trigger_deprecation('twig/twig', '3.12', 'Character "%s" at position %d does not need to be escaped anymore.', $nextChar, $i + 1); + trigger_deprecation('twig/twig', '3.12', 'Character "%s" at position %d should not be escaped; the "\" character is ignored in Twig v3 but will not be in v4. Please remove the extra "\" character.', $nextChar, $i + 1); } $result .= $nextChar; } elseif ('#' === $nextChar && $i + 1 < $length && '{' === $str[$i + 1]) { @@ -437,7 +437,7 @@ private function stripcslashes(string $str, string $quoteType): string } $result .= \chr(octdec($octal)); } else { - trigger_deprecation('twig/twig', '3.12', \sprintf('Character "%s" at position %d does not need to be escaped anymore.', $nextChar, $i + 1)); + trigger_deprecation('twig/twig', '3.12', 'Character "%s" at position %d should not be escaped; the "\" character is ignored in Twig v3 but will not be in v4. Please remove the extra "\" character.', $nextChar, $i + 1); $result .= $nextChar; } diff --git a/tests/LexerTest.php b/tests/LexerTest.php index 158836ac447..f07a0868412 100644 --- a/tests/LexerTest.php +++ b/tests/LexerTest.php @@ -252,17 +252,17 @@ public function getStringWithEscapedDelimiterProducingDeprecation() yield '{{ \'App\Test\' }} => AppTest' => [ '{{ \'App\\Test\' }}', 'AppTest', - 'Since twig/twig 3.12: Character "T" at position 5 does not need to be escaped anymore.', + 'Since twig/twig 3.12: Character "T" at position 5 should not be escaped; the "\" character is ignored in Twig v3 but will not be in v4. Please remove the extra "\" character.', ]; yield '{{ "foo \\\' bar" }} => foo \' bar' => [ '{{ "foo \\\' bar" }}', 'foo \' bar', - "Since twig/twig 3.12: Character \"'\" at position 6 does not need to be escaped anymore.", + 'Since twig/twig 3.12: Character "\'" at position 6 should not be escaped; the "\" character is ignored in Twig v3 but will not be in v4. Please remove the extra "\" character.', ]; yield '{{ \'foo \" bar\' }} => foo " bar' => [ '{{ \'foo \\" bar\' }}', 'foo " bar', - 'Since twig/twig 3.12: Character """ at position 6 does not need to be escaped anymore.', + 'Since twig/twig 3.12: Character """ at position 6 should not be escaped; the "\" character is ignored in Twig v3 but will not be in v4. Please remove the extra "\" character.', ]; } From 7121673c9c7eb21632883743e364fa3c44cfca52 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Thu, 22 Aug 2024 22:01:44 +0200 Subject: [PATCH 372/812] Deprecate OptimizerNodeVisitor::OPTIMIZE_TEXT_NODES --- CHANGELOG | 2 ++ doc/deprecated.rst | 5 +++ src/NodeVisitor/OptimizerNodeVisitor.php | 40 +++--------------------- tests/NodeVisitor/OptimizerTest.php | 6 ++-- 4 files changed, 13 insertions(+), 40 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index fa50ff1e5ea..30e00839f2c 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,6 @@ # 3.12.0 (2024-XX-XX) + * Deprecate `OptimizerNodeVisitor::OPTIMIZE_TEXT_NODES` * Fix performance regression when `use_yield` is `false` (which is the default) * Improve compatibility when `use_yield` is `false` (as extensions still using `echo` will work as is) * Accept colons (`:`) in addition to equals (`=`) to separate argument names and values in named arguments @@ -18,6 +19,7 @@ # 3.11.0 (2024-08-08) + * Deprecate `OptimizerNodeVisitor::OPTIMIZE_RAW_FILTER` * Add `Twig\Cache\ChainCache` and `Twig\Cache\ReadOnlyFilesystemCache` * Add the possibility to deprecate attributes and nodes on `Node` * Add the possibility to add a package and a version to the `deprecated` tag diff --git a/doc/deprecated.rst b/doc/deprecated.rst index 2d2860cb8b9..8320a2290f3 100644 --- a/doc/deprecated.rst +++ b/doc/deprecated.rst @@ -141,6 +141,11 @@ Node Visitors * The ``Twig\NodeVisitor\AbstractNodeVisitor`` class is deprecated, implement the ``Twig\NodeVisitor\NodeVisitorInterface`` interface instead. +* The ``Twig\NodeVisitor\OptimizerNodeVisitor::OPTIMIZE_RAW_FILTER`` and the + ``Twig\NodeVisitor\OptimizerNodeVisitor::OPTIMIZE_TEXT_NODES`` options are + deprecated as of Twig 3.12 and will be removed in Twig 4.0; they don't do + anything anymore. + Parser ------ diff --git a/src/NodeVisitor/OptimizerNodeVisitor.php b/src/NodeVisitor/OptimizerNodeVisitor.php index 55f5d6eb960..0d2dc02d5c0 100644 --- a/src/NodeVisitor/OptimizerNodeVisitor.php +++ b/src/NodeVisitor/OptimizerNodeVisitor.php @@ -62,6 +62,10 @@ public function __construct(int $optimizers = -1) trigger_deprecation('twig/twig', '3.11', 'The "Twig\NodeVisitor\OptimizerNodeVisitor::OPTIMIZE_RAW_FILTER" option is deprecated and does nothing.'); } + if (-1 !== $optimizers && self::OPTIMIZE_TEXT_NODES === (self::OPTIMIZE_TEXT_NODES & $optimizers)) { + trigger_deprecation('twig/twig', '3.12', 'The "Twig\NodeVisitor\OptimizerNodeVisitor::OPTIMIZE_TEXT_NODES" option is deprecated and does nothing.'); + } + $this->optimizers = $optimizers; } @@ -82,42 +86,6 @@ public function leaveNode(Node $node, Environment $env): ?Node $node = $this->optimizePrintNode($node); - if (self::OPTIMIZE_TEXT_NODES === (self::OPTIMIZE_TEXT_NODES & $this->optimizers)) { - $node = $this->mergeTextNodeCalls($node); - } - - return $node; - } - - private function mergeTextNodeCalls(Node $node): Node - { - $text = ''; - $names = []; - foreach ($node as $k => $n) { - if (!$n instanceof TextNode) { - return $node; - } - - $text .= $n->getAttribute('data'); - $names[] = $k; - } - - if (!$text) { - return $node; - } - - if (Node::class === \get_class($node)) { - return new TextNode($text, $node->getTemplateLine()); - } - - foreach ($names as $i => $name) { - if (0 === $i) { - $node->setNode($name, new TextNode($text, $node->getTemplateLine())); - } else { - $node->removeNode($name); - } - } - return $node; } diff --git a/tests/NodeVisitor/OptimizerTest.php b/tests/NodeVisitor/OptimizerTest.php index b670d89495b..12dc1214dc3 100644 --- a/tests/NodeVisitor/OptimizerTest.php +++ b/tests/NodeVisitor/OptimizerTest.php @@ -27,10 +27,8 @@ class OptimizerTest extends TestCase public function testConstructor() { $this->expectNotToPerformAssertions(); - new OptimizerNodeVisitor( - OptimizerNodeVisitor::OPTIMIZE_FOR - | OptimizerNodeVisitor::OPTIMIZE_TEXT_NODES - ); + + new OptimizerNodeVisitor(OptimizerNodeVisitor::OPTIMIZE_FOR); } public function testRenderBlockOptimizer() From 86330fb9bcd101780d9b470339af5e92b4120e79 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Fri, 23 Aug 2024 09:26:18 +0200 Subject: [PATCH 373/812] Update docs --- doc/api.rst | 3 --- 1 file changed, 3 deletions(-) diff --git a/doc/api.rst b/doc/api.rst index 219bdec3395..7e2fd9a7f7d 100644 --- a/doc/api.rst +++ b/doc/api.rst @@ -560,9 +560,6 @@ Twig supports the following optimizations: * ``\Twig\NodeVisitor\OptimizerNodeVisitor::OPTIMIZE_FOR``, optimizes the ``for`` tag by removing the ``loop`` variable creation whenever possible. -* ``\Twig\NodeVisitor\OptimizerNodeVisitor::OPTIMIZE_TEXT_NODES``, optimizes the text - nodes by merging consecutive text nodes into a single one. - Exceptions ---------- From 1ee4210ba9f81b7bafe66a3dd7253ed91ee46575 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Fri, 23 Aug 2024 19:44:16 +0200 Subject: [PATCH 374/812] Deprecate node names that are not strings or integers --- CHANGELOG | 1 + doc/deprecated.rst | 5 +++++ src/Node/Node.php | 26 ++++++++++++++++++++++---- 3 files changed, 28 insertions(+), 4 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 30e00839f2c..62742337074 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,6 @@ # 3.12.0 (2024-XX-XX) + * Add support for integers in methods of `Twig\Node\Node` that take a Node name * Deprecate `OptimizerNodeVisitor::OPTIMIZE_TEXT_NODES` * Fix performance regression when `use_yield` is `false` (which is the default) * Improve compatibility when `use_yield` is `false` (as extensions still using `echo` will work as is) diff --git a/doc/deprecated.rst b/doc/deprecated.rst index 8320a2290f3..f5fa4a77ec5 100644 --- a/doc/deprecated.rst +++ b/doc/deprecated.rst @@ -35,6 +35,11 @@ Extensions Nodes ----- +* The following ``Twig\Node\Node`` methods will take a string or an integer + (instead of just a string) in Twig 4.0 for their "name" argument: + ``getNode()``, ``hasNode()``, ``setNode()``, ``removeNode()``, and + ``deprecateNode()``. + * The second argument of the ``Twig\Node\Expression\CallExpression::compileArguments()`` method is deprecated. diff --git a/src/Node/Node.php b/src/Node/Node.php index 770cdaf4342..b239060566f 100644 --- a/src/Node/Node.php +++ b/src/Node/Node.php @@ -24,6 +24,9 @@ #[YieldReady] class Node implements \Countable, \IteratorAggregate { + /** + * @var array + */ protected $nodes; protected $attributes; protected $lineno; @@ -36,10 +39,10 @@ class Node implements \Countable, \IteratorAggregate private $attributeNameDeprecations = []; /** - * @param array $nodes An array of named nodes - * @param array $attributes An array of attributes (should not be nodes) - * @param int $lineno The line number - * @param string $tag The tag name associated with the Node + * @param array $nodes An array of named nodes + * @param array $attributes An array of attributes (should not be nodes) + * @param int $lineno The line number + * @param string $tag The tag name associated with the Node */ public function __construct(array $nodes = [], array $attributes = [], int $lineno = 0, ?string $tag = null) { @@ -158,11 +161,17 @@ public function removeAttribute(string $name): void unset($this->attributes[$name]); } + /** + * @param string|int $name + */ public function hasNode(string $name): bool { return isset($this->nodes[$name]); } + /** + * @param string|int $name + */ public function getNode(string $name): self { if (!isset($this->nodes[$name])) { @@ -182,6 +191,9 @@ public function getNode(string $name): self return $this->nodes[$name]; } + /** + * @param string|int $name + */ public function setNode(string $name, self $node): void { $triggerDeprecation = \func_num_args() > 2 ? func_get_arg(2) : true; @@ -200,11 +212,17 @@ public function setNode(string $name, self $node): void $this->nodes[$name] = $node; } + /** + * @param string|int $name + */ public function removeNode(string $name): void { unset($this->nodes[$name]); } + /** + * @param string|int $name + */ public function deprecateNode(string $name, NameDeprecation $dep): void { $this->nodeNameDeprecations[$name] = $dep; From 313303b366fef24ebbc27dfb4893abab5536fb15 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sat, 24 Aug 2024 09:51:58 +0200 Subject: [PATCH 375/812] Update a phpdoc (ModuleNode is final now) --- src/Node/ModuleNode.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Node/ModuleNode.php b/src/Node/ModuleNode.php index fb85cd89546..158f8f53e41 100644 --- a/src/Node/ModuleNode.php +++ b/src/Node/ModuleNode.php @@ -21,9 +21,9 @@ /** * Represents a module node. * - * Consider this class as being final. If you need to customize the behavior of - * the generated class, consider adding nodes to the following nodes: display_start, - * display_end, constructor_start, constructor_end, and class_end. + * If you need to customize the behavior of the generated class, add nodes to + * the following nodes: display_start, display_end, constructor_start, + * constructor_end, and class_end. * * @author Fabien Potencier */ From 094892aa3ba1c5778e5030c00d32ecbd1f6f1e8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Andr=C3=A9?= Date: Sat, 24 Aug 2024 10:03:24 +0200 Subject: [PATCH 376/812] Remove `Template::*_CALL` const optimisations * Template::ANY_CALL * Template::ARRAY_CALL * Template::METHOD_CALL --- src/Extension/CoreExtension.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index 73caf8a5d30..3a6a2e65525 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -1565,10 +1565,10 @@ public static function batch($items, $size, $fill = null, $preserveKeys = true): * * @internal */ - public static function getAttribute(Environment $env, Source $source, $object, $item, array $arguments = [], $type = /* Template::ANY_CALL */ 'any', $isDefinedTest = false, $ignoreStrictCheck = false, $sandboxed = false, int $lineno = -1) + public static function getAttribute(Environment $env, Source $source, $object, $item, array $arguments = [], $type = Template::ANY_CALL, $isDefinedTest = false, $ignoreStrictCheck = false, $sandboxed = false, int $lineno = -1) { // array - if (/* Template::METHOD_CALL */ 'method' !== $type) { + if (Template::METHOD_CALL !== $type) { $arrayItem = \is_bool($item) || \is_float($item) ? (int) $item : $item; if (((\is_array($object) || $object instanceof \ArrayObject) && (isset($object[$arrayItem]) || \array_key_exists($arrayItem, (array) $object))) @@ -1581,7 +1581,7 @@ public static function getAttribute(Environment $env, Source $source, $object, $ return $object[$arrayItem]; } - if (/* Template::ARRAY_CALL */ 'array' === $type || !\is_object($object)) { + if (Template::ARRAY_CALL === $type || !\is_object($object)) { if ($isDefinedTest) { return false; } @@ -1600,7 +1600,7 @@ public static function getAttribute(Environment $env, Source $source, $object, $ } else { $message = \sprintf('Key "%s" for sequence/mapping with keys "%s" does not exist.', $arrayItem, implode(', ', array_keys($object))); } - } elseif (/* Template::ARRAY_CALL */ 'array' === $type) { + } elseif (Template::ARRAY_CALL === $type) { if (null === $object) { $message = \sprintf('Impossible to access a key ("%s") on a null variable.', $item); } else { @@ -1641,7 +1641,7 @@ public static function getAttribute(Environment $env, Source $source, $object, $ } // object property - if (/* Template::METHOD_CALL */ 'method' !== $type) { + if (Template::METHOD_CALL !== $type) { if (isset($object->$item) || \array_key_exists((string) $item, (array) $object)) { if ($isDefinedTest) { return true; From 51e93aa130fb63fec0001c57c4ea1c5a21c9e0e8 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sat, 24 Aug 2024 10:49:53 +0200 Subject: [PATCH 377/812] Deprecate not passing a BodyNode instance as the body of a ModuleNode constructor --- CHANGELOG | 1 + doc/deprecated.rst | 3 +++ src/Node/MacroNode.php | 7 +++++++ src/Node/ModuleNode.php | 7 +++++++ tests/Node/MacroTest.php | 5 +++-- tests/Node/ModuleTest.php | 9 +++++---- tests/NodeVisitor/SandboxTest.php | 5 +++-- 7 files changed, 29 insertions(+), 8 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 62742337074..e72d8af3609 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,7 @@ # 3.12.0 (2024-XX-XX) * Add support for integers in methods of `Twig\Node\Node` that take a Node name + * Deprecate not passing a `BodyNode` instance as the body of a `ModuleNode` or `MacroNode` constructor * Deprecate `OptimizerNodeVisitor::OPTIMIZE_TEXT_NODES` * Fix performance regression when `use_yield` is `false` (which is the default) * Improve compatibility when `use_yield` is `false` (as extensions still using `echo` will work as is) diff --git a/doc/deprecated.rst b/doc/deprecated.rst index f5fa4a77ec5..6eceed15fa4 100644 --- a/doc/deprecated.rst +++ b/doc/deprecated.rst @@ -40,6 +40,9 @@ Nodes ``getNode()``, ``hasNode()``, ``setNode()``, ``removeNode()``, and ``deprecateNode()``. +* Not passing a ``BodyNode`` instance as the body of a ``ModuleNode`` or + ``MacroNode`` constructor is deprecated as of Twig 3.12. + * The second argument of the ``Twig\Node\Expression\CallExpression::compileArguments()`` method is deprecated. diff --git a/src/Node/MacroNode.php b/src/Node/MacroNode.php index a6048de9bd8..54a04ddc29f 100644 --- a/src/Node/MacroNode.php +++ b/src/Node/MacroNode.php @@ -25,8 +25,15 @@ class MacroNode extends Node { public const VARARGS_NAME = 'varargs'; + /** + * @param BodyNode $body + */ public function __construct(string $name, Node $body, Node $arguments, int $lineno, ?string $tag = null) { + if (!$body instanceof BodyNode) { + trigger_deprecation('twig/twig', '3.12', sprintf('Not passing a "%s" instance as the "body" argument of the "%s" constructor is deprecated.', BodyNode::class, __CLASS__)); + } + foreach ($arguments as $argumentName => $argument) { if (self::VARARGS_NAME === $argumentName) { throw new SyntaxError(\sprintf('The argument "%s" in macro "%s" cannot be defined because the variable "%s" is reserved for arbitrary arguments.', self::VARARGS_NAME, $name, self::VARARGS_NAME), $argument->getTemplateLine(), $argument->getSourceContext()); diff --git a/src/Node/ModuleNode.php b/src/Node/ModuleNode.php index 158f8f53e41..96f0c9cccbb 100644 --- a/src/Node/ModuleNode.php +++ b/src/Node/ModuleNode.php @@ -30,8 +30,15 @@ #[YieldReady] final class ModuleNode extends Node { + /** + * @param BodyNode $body + */ public function __construct(Node $body, ?AbstractExpression $parent, Node $blocks, Node $macros, Node $traits, $embeddedTemplates, Source $source) { + if (!$body instanceof BodyNode) { + trigger_deprecation('twig/twig', '3.12', sprintf('Not passing a "%s" instance as the "body" argument of the "%s" constructor is deprecated.', BodyNode::class, __CLASS__)); + } + $nodes = [ 'body' => $body, 'blocks' => $blocks, diff --git a/tests/Node/MacroTest.php b/tests/Node/MacroTest.php index ce51187be8c..ef36774f7e8 100644 --- a/tests/Node/MacroTest.php +++ b/tests/Node/MacroTest.php @@ -13,6 +13,7 @@ use Twig\Environment; use Twig\Loader\ArrayLoader; +use Twig\Node\BodyNode; use Twig\Node\Expression\ConstantExpression; use Twig\Node\Expression\NameExpression; use Twig\Node\MacroNode; @@ -24,7 +25,7 @@ class MacroTest extends NodeTestCase { public function testConstructor() { - $body = new TextNode('foo', 1); + $body = new BodyNode([new TextNode('foo', 1)]); $arguments = new Node([new NameExpression('foo', 1)], [], 1); $node = new MacroNode('foo', $body, $arguments, 1); @@ -42,7 +43,7 @@ public function getTests() 'bar' => new ConstantExpression('Foo', 1), ], [], 1); - $body = new TextNode('foo', 1); + $body = new BodyNode([new TextNode('foo', 1)]); $node = new MacroNode('foo', $body, $arguments, 1); $text[] = [$node, <<setAttribute('is_generator', true); - $node = new ModuleNode(new PrintNode($expr, 1), null, new Node(), new Node(), new Node(), new Node([]), new Source('foo', 'foo')); + $node = new ModuleNode(new BodyNode([new PrintNode($expr, 1)]), null, new Node(), new Node(), new Node(), new Node([]), new Source('foo', 'foo')); $traverser = new NodeTraverser($env, [new SandboxNodeVisitor($env)]); $node = $traverser->traverse($node); - $this->assertNotInstanceOf(CheckToStringNode::class, $node->getNode('body')->getNode('expr')); + $this->assertNotInstanceOf(CheckToStringNode::class, $node->getNode('body')->getNode(0)->getNode('expr')); $this->assertSame("// line 1\nyield from (\$context[\"foo\"] ?? null);\n", $env->compile($node->getNode('body'))); } } From 0c7513616256c8e6107d108c64b8249e222f9bf3 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sat, 24 Aug 2024 10:20:03 +0200 Subject: [PATCH 378/812] Deprecate returning null from TokenParserInterface::parse() --- CHANGELOG | 1 + doc/deprecated.rst | 3 +++ src/Parser.php | 5 +++-- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index e72d8af3609..c428793db31 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -2,6 +2,7 @@ * Add support for integers in methods of `Twig\Node\Node` that take a Node name * Deprecate not passing a `BodyNode` instance as the body of a `ModuleNode` or `MacroNode` constructor + * Deprecate returning "null" from "TokenParserInterface::parse()". * Deprecate `OptimizerNodeVisitor::OPTIMIZE_TEXT_NODES` * Fix performance regression when `use_yield` is `false` (which is the default) * Improve compatibility when `use_yield` is `false` (as extensions still using `echo` will work as is) diff --git a/doc/deprecated.rst b/doc/deprecated.rst index 6eceed15fa4..fb174a1b47a 100644 --- a/doc/deprecated.rst +++ b/doc/deprecated.rst @@ -43,6 +43,9 @@ Nodes * Not passing a ``BodyNode`` instance as the body of a ``ModuleNode`` or ``MacroNode`` constructor is deprecated as of Twig 3.12. +* Returning ``null`` from ``TokenParserInterface::parse()`` is deprecated as of + Twig 3.12 (as forbidden by the interface). + * The second argument of the ``Twig\Node\Expression\CallExpression::compileArguments()`` method is deprecated. diff --git a/src/Parser.php b/src/Parser.php index 28cc8a0e11a..fe0d23eb6ae 100644 --- a/src/Parser.php +++ b/src/Parser.php @@ -173,9 +173,10 @@ public function subparse($test, bool $dropNeedle = false): Node $subparser->setParser($this); $node = $subparser->parse($token); - if (null !== $node) { - $rv[] = $node; + if (!$node) { + trigger_deprecation('twig/twig', '3.12', 'Returning "null" from "%s" is deprecated and forbidden by "TokenParserInterface".', $subparser::class); } + $rv[] = $node; break; default: From 71731825372a3515e05a5b5dd93fe98659da87ba Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sat, 24 Aug 2024 10:55:34 +0200 Subject: [PATCH 379/812] Fix code --- src/Parser.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Parser.php b/src/Parser.php index fe0d23eb6ae..86c2d61138b 100644 --- a/src/Parser.php +++ b/src/Parser.php @@ -175,8 +175,9 @@ public function subparse($test, bool $dropNeedle = false): Node $node = $subparser->parse($token); if (!$node) { trigger_deprecation('twig/twig', '3.12', 'Returning "null" from "%s" is deprecated and forbidden by "TokenParserInterface".', $subparser::class); + } else { + $rv[] = $node; } - $rv[] = $node; break; default: From 758be4dde3967035699bbc3c3ee80b600a75ebdb Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sat, 24 Aug 2024 12:57:28 +0200 Subject: [PATCH 380/812] Fix error message --- src/Parser.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Parser.php b/src/Parser.php index 86c2d61138b..65a7a64e624 100644 --- a/src/Parser.php +++ b/src/Parser.php @@ -181,7 +181,7 @@ public function subparse($test, bool $dropNeedle = false): Node break; default: - throw new SyntaxError('Lexer or parser ended up in unsupported state.', $this->getCurrentToken()->getLine(), $this->stream->getSourceContext()); + throw new SyntaxError('The lexer or the parser ended up in an unsupported state.', $this->getCurrentToken()->getLine(), $this->stream->getSourceContext()); } } From 4b99692e2a61a8db7cbf29c076b904546f516641 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sat, 24 Aug 2024 17:12:56 +0200 Subject: [PATCH 381/812] Fix class name --- src/Node/MacroNode.php | 2 +- src/Node/ModuleNode.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Node/MacroNode.php b/src/Node/MacroNode.php index 54a04ddc29f..6eeabc27563 100644 --- a/src/Node/MacroNode.php +++ b/src/Node/MacroNode.php @@ -31,7 +31,7 @@ class MacroNode extends Node public function __construct(string $name, Node $body, Node $arguments, int $lineno, ?string $tag = null) { if (!$body instanceof BodyNode) { - trigger_deprecation('twig/twig', '3.12', sprintf('Not passing a "%s" instance as the "body" argument of the "%s" constructor is deprecated.', BodyNode::class, __CLASS__)); + trigger_deprecation('twig/twig', '3.12', sprintf('Not passing a "%s" instance as the "body" argument of the "%s" constructor is deprecated.', BodyNode::class, static::class)); } foreach ($arguments as $argumentName => $argument) { diff --git a/src/Node/ModuleNode.php b/src/Node/ModuleNode.php index 96f0c9cccbb..a6df6b102e5 100644 --- a/src/Node/ModuleNode.php +++ b/src/Node/ModuleNode.php @@ -36,7 +36,7 @@ final class ModuleNode extends Node public function __construct(Node $body, ?AbstractExpression $parent, Node $blocks, Node $macros, Node $traits, $embeddedTemplates, Source $source) { if (!$body instanceof BodyNode) { - trigger_deprecation('twig/twig', '3.12', sprintf('Not passing a "%s" instance as the "body" argument of the "%s" constructor is deprecated.', BodyNode::class, __CLASS__)); + trigger_deprecation('twig/twig', '3.12', sprintf('Not passing a "%s" instance as the "body" argument of the "%s" constructor is deprecated.', BodyNode::class, static::class)); } $nodes = [ From 157d36ae43a033aa8df4b424fe8a3e8322924e54 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sat, 24 Aug 2024 17:25:12 +0200 Subject: [PATCH 382/812] Update Node::__toString() to include the node tag if set --- CHANGELOG | 1 + src/Node/Node.php | 21 +++++++++++++-------- tests/Node/NodeTest.php | 28 ++++++++++++++++++++++++++-- 3 files changed, 40 insertions(+), 10 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index c428793db31..fa700246e08 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,6 @@ # 3.12.0 (2024-XX-XX) + * Update `Node::__toString()` to include the node tag if set * Add support for integers in methods of `Twig\Node\Node` that take a Node name * Deprecate not passing a `BodyNode` instance as the body of a `ModuleNode` or `MacroNode` constructor * Deprecate returning "null" from "TokenParserInterface::parse()". diff --git a/src/Node/Node.php b/src/Node/Node.php index b239060566f..3683dc383b6 100644 --- a/src/Node/Node.php +++ b/src/Node/Node.php @@ -59,6 +59,12 @@ public function __construct(array $nodes = [], array $attributes = [], int $line public function __toString() { + $repr = static::class; + + if ($this->tag) { + $repr .= \sprintf("\n tag: %s", $this->tag); + } + $attributes = []; foreach ($this->attributes as $name => $value) { if (\is_callable($value)) { @@ -71,25 +77,24 @@ public function __toString() $attributes[] = \sprintf('%s: %s', $name, $v); } - $repr = [static::class.'('.implode(', ', $attributes)]; + if ($attributes) { + $repr .= \sprintf("\n attributes:\n %s", implode("\n ", $attributes)); + } if (\count($this->nodes)) { + $repr .= \sprintf("\n nodes:"); foreach ($this->nodes as $name => $node) { - $len = \strlen($name) + 4; + $len = \strlen($name) + 6; $noderepr = []; foreach (explode("\n", (string) $node) as $line) { $noderepr[] = str_repeat(' ', $len).$line; } - $repr[] = \sprintf(' %s: %s', $name, ltrim(implode("\n", $noderepr))); + $repr .= \sprintf("\n %s: %s", $name, ltrim(implode("\n", $noderepr))); } - - $repr[] = ')'; - } else { - $repr[0] .= ')'; } - return implode("\n", $repr); + return $repr; } /** diff --git a/tests/Node/NodeTest.php b/tests/Node/NodeTest.php index 94c9197b50b..5a300323026 100644 --- a/tests/Node/NodeTest.php +++ b/tests/Node/NodeTest.php @@ -28,7 +28,13 @@ public function testToString() // callable is not a supported type for a Node attribute, but Drupal uses some apparently $node = new Node([], ['value' => function () { return '1'; }], 1); - $this->assertEquals('Twig\Node\Node(value: \Closure)', (string) $node); + $this->assertEquals(<< new TwigTest('a_test'), ], 1); - $this->assertEquals('Twig\Node\Node(function: Twig\TwigFunction(a_function), filter: Twig\TwigFilter(a_filter), test: Twig\TwigTest(a_test))', (string) $node); + $this->assertEquals(<<assertEquals(<< Date: Sat, 24 Aug 2024 23:22:24 +0200 Subject: [PATCH 383/812] Move code from ExtendsTokenParser to Parser --- CHANGELOG | 1 + doc/deprecated.rst | 3 +++ src/Parser.php | 8 ++++++++ src/TokenParser/ExtendsTokenParser.php | 3 --- 4 files changed, 12 insertions(+), 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index fa700246e08..052713737df 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,6 @@ # 3.12.0 (2024-XX-XX) + * Deprecate passing `null` to `Twig\Parser::setParent()` * Update `Node::__toString()` to include the node tag if set * Add support for integers in methods of `Twig\Node\Node` that take a Node name * Deprecate not passing a `BodyNode` instance as the body of a `ModuleNode` or `MacroNode` constructor diff --git a/doc/deprecated.rst b/doc/deprecated.rst index fb174a1b47a..8469250ba81 100644 --- a/doc/deprecated.rst +++ b/doc/deprecated.rst @@ -166,6 +166,9 @@ Parser * The ``Twig\ExpressionParser::parseArrayExpression()`` method is deprecated, use ``Twig\ExpressionParser::parseSequenceExpression()`` instead. +* Passing ``null`` to ``Twig\Parser::setParent()`` is deprecated as of Twig + 3.12. + Templates --------- diff --git a/src/Parser.php b/src/Parser.php index 65a7a64e624..f8a3dd36642 100644 --- a/src/Parser.php +++ b/src/Parser.php @@ -292,6 +292,14 @@ public function getParent(): ?Node public function setParent(?Node $parent): void { + if (null === $parent) { + trigger_deprecation('twig/twig', '3.12', 'Passing "null" to "%s()" is deprecated.', __METHOD__); + } + + if (null !== $this->parent) { + throw new SyntaxError('Multiple extends tags are forbidden.', $parent->getTemplateLine(), $parent->getSourceContext()); + } + $this->parent = $parent; } diff --git a/src/TokenParser/ExtendsTokenParser.php b/src/TokenParser/ExtendsTokenParser.php index 7368459de92..7fba7da0448 100644 --- a/src/TokenParser/ExtendsTokenParser.php +++ b/src/TokenParser/ExtendsTokenParser.php @@ -35,9 +35,6 @@ public function parse(Token $token): Node throw new SyntaxError('Cannot use "extend" in a macro.', $token->getLine(), $stream->getSourceContext()); } - if (null !== $this->parser->getParent()) { - throw new SyntaxError('Multiple extends tags are forbidden.', $token->getLine(), $stream->getSourceContext()); - } $this->parser->setParent($this->parser->getExpressionParser()->parseExpression()); $stream->expect(Token::BLOCK_END_TYPE); From 9e6c4a6ac1e7f84c25e898c40f4971945685d0e6 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Mon, 26 Aug 2024 09:01:52 +0200 Subject: [PATCH 384/812] Deprecate some internal methods from Parser --- CHANGELOG | 1 + doc/deprecated.rst | 4 ++++ src/Extension/CoreExtension.php | 10 ++++---- src/Parser.php | 23 ++++++++++++++++++- src/TokenParser/BlockTokenParser.php | 3 --- .../functions/parent_outside_of_a_block.test | 10 ++++++++ .../inheritance/parent_without_extends.test | 2 +- tests/ParserTest.php | 4 +++- 8 files changed, 46 insertions(+), 11 deletions(-) create mode 100644 tests/Fixtures/functions/parent_outside_of_a_block.test diff --git a/CHANGELOG b/CHANGELOG index 052713737df..a29d4019e14 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,6 @@ # 3.12.0 (2024-XX-XX) + * Deprecate some internal methods from `Parser`: `getBlockStack()`, `hasBlock()`, `getBlock()`, `hasMacro()`, `hasTraits()`, `getParent()` * Deprecate passing `null` to `Twig\Parser::setParent()` * Update `Node::__toString()` to include the node tag if set * Add support for integers in methods of `Twig\Node\Node` that take a Node name diff --git a/doc/deprecated.rst b/doc/deprecated.rst index 8469250ba81..83a33a5377d 100644 --- a/doc/deprecated.rst +++ b/doc/deprecated.rst @@ -160,6 +160,10 @@ Node Visitors Parser ------ +* The following methods from ``Twig\Parser`` are deprecated as of Twig 3.12: + ``getBlockStack()``, ``hasBlock()``, ``getBlock()``, ``hasMacro()``, + ``hasTraits()``, ``getParent()``. + * The ``Twig\ExpressionParser::parseHashExpression()`` method is deprecated, use ``Twig\ExpressionParser::parseMappingExpression()`` instead. diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index 3a6a2e65525..cd2f5d7e627 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -1919,15 +1919,15 @@ public static function captureOutput(iterable $body): string */ public static function parseParentFunction(Parser $parser, Node $fakeNode, $args, int $line): AbstractExpression { - if (!\count($parser->getBlockStack())) { - throw new SyntaxError('Calling "parent" outside a block is forbidden.', $line, $parser->getStream()->getSourceContext()); + if (!$blockName = $parser->peekBlockStack()) { + throw new SyntaxError('Calling the "parent" function outside of a block is forbidden.', $line, $parser->getStream()->getSourceContext()); } - if (!$parser->getParent() && !$parser->hasTraits()) { - throw new SyntaxError('Calling "parent" on a template that does not extend nor "use" another template is forbidden.', $line, $parser->getStream()->getSourceContext()); + if (!$parser->hasInheritance()) { + throw new SyntaxError('Calling the "parent" function on a template that does not call "extends" or "use" is forbidden.', $line, $parser->getStream()->getSourceContext()); } - return new ParentExpression($parser->peekBlockStack(), $line); + return new ParentExpression($blockName, $line); } /** diff --git a/src/Parser.php b/src/Parser.php index f8a3dd36642..e3273230cfa 100644 --- a/src/Parser.php +++ b/src/Parser.php @@ -92,7 +92,7 @@ public function parse(TokenStream $stream, $test = null, bool $dropNeedle = fals } if (!$e->getTemplateLine()) { - $e->setTemplateLine($this->stream->getCurrent()->getLine()); + $e->setTemplateLine($this->getCurrentToken()->getLine()); } throw $e; @@ -194,6 +194,8 @@ public function subparse($test, bool $dropNeedle = false): Node public function getBlockStack(): array { + trigger_deprecation('twig/twig', '3.12', 'Method "%s()" is deprecated.', __METHOD__); + return $this->blockStack; } @@ -214,21 +216,31 @@ public function pushBlockStack($name): void public function hasBlock(string $name): bool { + trigger_deprecation('twig/twig', '3.12', 'Method "%s()" is deprecated.', __METHOD__); + return isset($this->blocks[$name]); } public function getBlock(string $name): Node { + trigger_deprecation('twig/twig', '3.12', 'Method "%s()" is deprecated.', __METHOD__); + return $this->blocks[$name]; } public function setBlock(string $name, BlockNode $value): void { + if (isset($this->blocks[$name])) { + throw new SyntaxError(\sprintf("The block '%s' has already been defined line %d.", $name, $this->blocks[$name]->getTemplateLine()), $this->getCurrentToken()->getLine(), $this->blocks[$name]->getSourceContext()); + } + $this->blocks[$name] = new BodyNode([$value], [], $value->getTemplateLine()); } public function hasMacro(string $name): bool { + trigger_deprecation('twig/twig', '3.12', 'Method "%s()" is deprecated.', __METHOD__); + return isset($this->macros[$name]); } @@ -244,6 +256,8 @@ public function addTrait($trait): void public function hasTraits(): bool { + trigger_deprecation('twig/twig', '3.12', 'Method "%s()" is deprecated.', __METHOD__); + return \count($this->traits) > 0; } @@ -287,9 +301,16 @@ public function getExpressionParser(): ExpressionParser public function getParent(): ?Node { + trigger_deprecation('twig/twig', '3.12', 'Method "%s()" is deprecated.', __METHOD__); + return $this->parent; } + public function hasInheritance() + { + return $this->parent || 0 < \count($this->traits); + } + public function setParent(?Node $parent): void { if (null === $parent) { diff --git a/src/TokenParser/BlockTokenParser.php b/src/TokenParser/BlockTokenParser.php index 0b7126274be..b3768742dcd 100644 --- a/src/TokenParser/BlockTokenParser.php +++ b/src/TokenParser/BlockTokenParser.php @@ -36,9 +36,6 @@ public function parse(Token $token): Node $lineno = $token->getLine(); $stream = $this->parser->getStream(); $name = $stream->expect(Token::NAME_TYPE)->getValue(); - if ($this->parser->hasBlock($name)) { - throw new SyntaxError(\sprintf("The block '%s' has already been defined line %d.", $name, $this->parser->getBlock($name)->getTemplateLine()), $stream->getCurrent()->getLine(), $stream->getSourceContext()); - } $this->parser->setBlock($name, $block = new BlockNode($name, new Node([]), $lineno)); $this->parser->pushLocalScope(); $this->parser->pushBlockStack($name); diff --git a/tests/Fixtures/functions/parent_outside_of_a_block.test b/tests/Fixtures/functions/parent_outside_of_a_block.test new file mode 100644 index 00000000000..03d4f5d662d --- /dev/null +++ b/tests/Fixtures/functions/parent_outside_of_a_block.test @@ -0,0 +1,10 @@ +--TEST-- +"parent" cannot be called outside of a block +--TEMPLATE-- +{% extends "parent.twig" %} +{{ parent() }} +--TEMPLATE(parent.twig)-- +--DATA-- +return [] +--EXCEPTION-- +Twig\Error\SyntaxError: Calling the "parent" function outside of a block is forbidden in "index.twig" at line 3. diff --git a/tests/Fixtures/tags/inheritance/parent_without_extends.test b/tests/Fixtures/tags/inheritance/parent_without_extends.test index 6d98891553d..c2025f60ca7 100644 --- a/tests/Fixtures/tags/inheritance/parent_without_extends.test +++ b/tests/Fixtures/tags/inheritance/parent_without_extends.test @@ -5,4 +5,4 @@ {{ parent() }} {% endblock %} --EXCEPTION-- -Twig\Error\SyntaxError: Calling "parent" on a template that does not extend nor "use" another template is forbidden in "index.twig" at line 3. +Twig\Error\SyntaxError: Calling the "parent" function on a template that does not call "extends" or "use" is forbidden in "index.twig" at line 3. diff --git a/tests/ParserTest.php b/tests/ParserTest.php index 73de41e6fdd..621c9d83ca0 100644 --- a/tests/ParserTest.php +++ b/tests/ParserTest.php @@ -151,7 +151,9 @@ public function testParseIsReentrant() new Token(Token::EOF_TYPE, '', 1), ])); - $this->assertNull($parser->getParent()); + $p = new \ReflectionProperty($parser, 'parent'); + $p->setAccessible(true); + $this->assertNull($p->getValue($parser)); } public function testGetVarName() From 9fd4c487e73db4c0825bfb33fa9d4cdf27570800 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Mon, 26 Aug 2024 14:14:49 +0200 Subject: [PATCH 385/812] Deprecate the spaceless filter --- CHANGELOG | 1 + doc/deprecated.rst | 6 ++++++ doc/filters/spaceless.rst | 4 ++++ src/Extension/CoreExtension.php | 2 +- tests/Fixtures/filters/spaceless.legacy.test | 16 ++++++++++++++++ tests/Fixtures/filters/spaceless.test | 12 ------------ tests/Fixtures/macros/macro_with_capture.test | 4 ++-- tests/Fixtures/tags/apply/scope.test | 2 +- 8 files changed, 31 insertions(+), 16 deletions(-) create mode 100644 tests/Fixtures/filters/spaceless.legacy.test delete mode 100644 tests/Fixtures/filters/spaceless.test diff --git a/CHANGELOG b/CHANGELOG index a29d4019e14..ee6f4d87aa6 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,6 @@ # 3.12.0 (2024-XX-XX) + * Deprecate the `spaceless` filter * Deprecate some internal methods from `Parser`: `getBlockStack()`, `hasBlock()`, `getBlock()`, `hasMacro()`, `hasTraits()`, `getParent()` * Deprecate passing `null` to `Twig\Parser::setParent()` * Update `Node::__toString()` to include the node tag if set diff --git a/doc/deprecated.rst b/doc/deprecated.rst index 83a33a5377d..950d961ddf4 100644 --- a/doc/deprecated.rst +++ b/doc/deprecated.rst @@ -180,3 +180,9 @@ Templates in ``Environment::resolveTemplate()``, ``Environment::load()``, and ``Template::loadTemplate()``); pass instances of ``Twig\TemplateWrapper`` instead. + +Filters +------- + +* The ``spaceless`` filter is deprecated as of Twig 3.12 and will be removed in + Twig 4.0. diff --git a/doc/filters/spaceless.rst b/doc/filters/spaceless.rst index 9a213c370b1..7a8e4093eeb 100644 --- a/doc/filters/spaceless.rst +++ b/doc/filters/spaceless.rst @@ -1,6 +1,10 @@ ``spaceless`` ============= +.. warning:: + + The ``spaceless`` filter is deprecated as of Twig 3.12. + Use the ``spaceless`` filter to remove whitespace *between HTML tags*, not whitespace within HTML tags or whitespace in plain text: diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index cd2f5d7e627..14dde136aa4 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -215,7 +215,7 @@ public function getFilters(): array new TwigFilter('striptags', [self::class, 'striptags']), new TwigFilter('trim', [self::class, 'trim']), new TwigFilter('nl2br', [self::class, 'nl2br'], ['pre_escape' => 'html', 'is_safe' => ['html']]), - new TwigFilter('spaceless', [self::class, 'spaceless'], ['is_safe' => ['html']]), + new TwigFilter('spaceless', [self::class, 'spaceless'], ['is_safe' => ['html'], 'deprecated' => '3.12', 'deprecating_package' => 'twig/twig']), // array helpers new TwigFilter('join', [self::class, 'join']), diff --git a/tests/Fixtures/filters/spaceless.legacy.test b/tests/Fixtures/filters/spaceless.legacy.test new file mode 100644 index 00000000000..6a01241e738 --- /dev/null +++ b/tests/Fixtures/filters/spaceless.legacy.test @@ -0,0 +1,16 @@ +--TEST-- +"spaceless" filter +--DEPRECATION-- +Since twig/twig 3.12: Twig Filter "spaceless" is deprecated in index.twig at line 2. +Since twig/twig 3.12: Twig Filter "spaceless" is deprecated in index.twig at line 3. +Since twig/twig 3.12: Twig Filter "spaceless" is deprecated in index.twig at line 4. +--TEMPLATE-- +{{ "
    foo
    "|spaceless }} +*{{ ""|spaceless }}* +*{{ null|spaceless }}* +--DATA-- +return [] +--EXPECT-- +
    foo
    +** +** diff --git a/tests/Fixtures/filters/spaceless.test b/tests/Fixtures/filters/spaceless.test deleted file mode 100644 index 166a7ea0bce..00000000000 --- a/tests/Fixtures/filters/spaceless.test +++ /dev/null @@ -1,12 +0,0 @@ ---TEST-- -"spaceless" filter ---TEMPLATE-- -{{ "
    foo
    "|spaceless }} -*{{ ""|spaceless }}* -*{{ null|spaceless }}* ---DATA-- -return [] ---EXPECT-- -
    foo
    -** -** diff --git a/tests/Fixtures/macros/macro_with_capture.test b/tests/Fixtures/macros/macro_with_capture.test index f67a7fcdb9b..2f9caed6eea 100644 --- a/tests/Fixtures/macros/macro_with_capture.test +++ b/tests/Fixtures/macros/macro_with_capture.test @@ -4,11 +4,11 @@ macro {{ _self.some_macro() }} {% macro some_macro() %} - {% apply spaceless %} + {% apply upper %} {% if true %}foo{% endif %} {% endapply %} {% endmacro %} --DATA-- return [] --EXPECT-- -foo +FOO diff --git a/tests/Fixtures/tags/apply/scope.test b/tests/Fixtures/tags/apply/scope.test index a87ff9116ba..ff8a23116f7 100644 --- a/tests/Fixtures/tags/apply/scope.test +++ b/tests/Fixtures/tags/apply/scope.test @@ -2,7 +2,7 @@ "apply" tag does not create a new scope --TEMPLATE-- {% set foo = 'baz' %} -{% apply spaceless %} +{% apply upper %} {% set foo = 'foo' %} {% set bar = 'bar' %} {% endapply %} From 2511b658da5e7cc262d19d029fc0143ebeef1d20 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Mon, 26 Aug 2024 17:26:29 +0200 Subject: [PATCH 386/812] Fix tests --- .../Tests/Fixtures/cache_complex.test | 4 +- .../Tests/Fixtures/inline_css.test | 49 +++++++++---------- 2 files changed, 25 insertions(+), 28 deletions(-) diff --git a/extra/cache-extra/Tests/Fixtures/cache_complex.test b/extra/cache-extra/Tests/Fixtures/cache_complex.test index 30aa729a26b..206865088b1 100644 --- a/extra/cache-extra/Tests/Fixtures/cache_complex.test +++ b/extra/cache-extra/Tests/Fixtures/cache_complex.test @@ -3,9 +3,9 @@ --TEMPLATE-- {% cache 'test_%s_%s'|format(10, 10000) ttl(36000) %} {% set content %} - OK + ok {% endset %} - {% apply spaceless %} + {% apply upper %} {{ content }} {% endapply %} {% endcache %} diff --git a/extra/cssinliner-extra/Tests/Fixtures/inline_css.test b/extra/cssinliner-extra/Tests/Fixtures/inline_css.test index 8ba561f55f4..7142b9e7126 100644 --- a/extra/cssinliner-extra/Tests/Fixtures/inline_css.test +++ b/extra/cssinliner-extra/Tests/Fixtures/inline_css.test @@ -1,40 +1,29 @@ --TEST-- "inline_css" filter --TEMPLATE-- -{% apply inline_css|spaceless %} - - -

    Great!

    - +{% apply inline_css %} +

    Great!

    {% endapply %} -{% apply inline_css(source('css'))|spaceless %} - -

    Great!

    - +{% apply inline_css(source('css')) %} +

    Great!

    {% endapply %} -{% apply inline_css(source('css'), source('more_css'))|spaceless %} - -

    Great!

    - +{% apply inline_css(source('css'), source('more_css')) %} +

    Great!

    {% endapply %} -{% apply inline_css(source('css') ~ source('more_css'))|spaceless %} - -

    Great!

    - +{% apply inline_css(source('css') ~ source('more_css')) %} +

    Great!

    {% endapply %} -{{ include('html')|inline_css(source('css') ~ source('more_css'))|spaceless }} +{{ include('html')|inline_css(source('css') ~ source('more_css')) }} --TEMPLATE(html)-- - -

    Great!

    - +

    Great!

    --TEMPLATE(css)-- p { color: red } --TEMPLATE(more_css)-- @@ -42,12 +31,20 @@ p { color: blue } --DATA-- return [] --EXPECT-- -

    Great!

    + + + +

    Great!

    + -

    Great!

    + +

    Great!

    -

    Great!

    + +

    Great!

    -

    Great!

    + +

    Great!

    -

    Great!

    + +

    Great!

    From 11813da84d4cf3eaaa631dbaac7d517b9f524c96 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Mon, 26 Aug 2024 17:09:29 +0200 Subject: [PATCH 387/812] Use Stringable when possible --- src/Error/Error.php | 2 +- src/Extension/CoreExtension.php | 4 ++-- src/Extension/SandboxExtension.php | 2 +- src/Extension/StringLoaderExtension.php | 5 ++--- src/Markup.php | 2 +- src/Runtime/EscaperRuntime.php | 2 +- 6 files changed, 8 insertions(+), 9 deletions(-) diff --git a/src/Error/Error.php b/src/Error/Error.php index 4efd9cafba9..61c309fa16e 100644 --- a/src/Error/Error.php +++ b/src/Error/Error.php @@ -142,7 +142,7 @@ private function updateRepr(): void } if ($this->name) { - if (\is_string($this->name) || (\is_object($this->name) && method_exists($this->name, '__toString'))) { + if (\is_string($this->name) || $this->name instanceof \Stringable) { $name = \sprintf('"%s"', $this->name); } else { $name = json_encode($this->name); diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index 14dde136aa4..e8cf5f6f206 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -1191,7 +1191,7 @@ public static function length(string $charset, $thing): int return iterator_count($thing); } - if (method_exists($thing, '__toString')) { + if ($thing instanceof \Stringable) { return mb_strlen((string) $thing, $charset); } @@ -1328,7 +1328,7 @@ public static function testEmpty($value): bool return !iterator_count($value); } - if (\is_object($value) && method_exists($value, '__toString')) { + if ($value instanceof \Stringable) { return '' === (string) $value; } diff --git a/src/Extension/SandboxExtension.php b/src/Extension/SandboxExtension.php index 921df287a44..4e96760f7d4 100644 --- a/src/Extension/SandboxExtension.php +++ b/src/Extension/SandboxExtension.php @@ -119,7 +119,7 @@ public function checkPropertyAllowed($obj, $property, int $lineno = -1, ?Source public function ensureToStringAllowed($obj, int $lineno = -1, ?Source $source = null) { - if ($this->isSandboxed($source) && \is_object($obj) && method_exists($obj, '__toString')) { + if ($this->isSandboxed($source) && $obj instanceof \Stringable) { try { $this->policy->checkMethodAllowed($obj, '__toString'); } catch (SecurityNotAllowedMethodError $e) { diff --git a/src/Extension/StringLoaderExtension.php b/src/Extension/StringLoaderExtension.php index 12f5c30aa91..698d181f177 100644 --- a/src/Extension/StringLoaderExtension.php +++ b/src/Extension/StringLoaderExtension.php @@ -29,12 +29,11 @@ public function getFunctions(): array * * {{ include(template_from_string("Hello {{ name }}")) }} * - * @param string $template A template as a string or object implementing __toString() - * @param string|null $name An optional name of the template to be used in error messages + * @param string|null $name An optional name of the template to be used in error messages * * @internal */ - public static function templateFromString(Environment $env, $template, ?string $name = null): TemplateWrapper + public static function templateFromString(Environment $env, string|\Stringable $template, ?string $name = null): TemplateWrapper { return $env->createTemplate((string) $template, $name); } diff --git a/src/Markup.php b/src/Markup.php index 1788acc4f73..4e83c9184bc 100644 --- a/src/Markup.php +++ b/src/Markup.php @@ -16,7 +16,7 @@ * * @author Fabien Potencier */ -class Markup implements \Countable, \JsonSerializable +class Markup implements \Countable, \JsonSerializable, \Stringable { private $content; private $charset; diff --git a/src/Runtime/EscaperRuntime.php b/src/Runtime/EscaperRuntime.php index b1dac964022..5388b0d4198 100644 --- a/src/Runtime/EscaperRuntime.php +++ b/src/Runtime/EscaperRuntime.php @@ -93,7 +93,7 @@ public function escape($string, string $strategy = 'html', ?string $charset = nu } if (!\is_string($string)) { - if (\is_object($string) && method_exists($string, '__toString')) { + if ($string instanceof \Stringable) { if ($autoescape) { $c = \get_class($string); if (!isset($this->safeClasses[$c])) { From 58d5780ef38fcf087bbad6e7ecae9f43d5b8de27 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Mon, 26 Aug 2024 15:04:26 +0200 Subject: [PATCH 388/812] Fix two-word tests precedence over one-word tests --- CHANGELOG | 1 + src/ExpressionParser.php | 14 +++++++------- tests/ExpressionParserTest.php | 11 +++++++++++ 3 files changed, 19 insertions(+), 7 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index ee6f4d87aa6..8d27a072d4f 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,6 @@ # 3.12.0 (2024-XX-XX) + * Fix precedence of two-word tests when the first word is a valid test * Deprecate the `spaceless` filter * Deprecate some internal methods from `Parser`: `getBlockStack()`, `hasBlock()`, `getBlock()`, `hasMacro()`, `hasTraits()`, `getParent()` * Deprecate passing `null` to `Twig\Parser::setParent()` diff --git a/src/ExpressionParser.php b/src/ExpressionParser.php index 28b556ccca0..e6bd3b48ce1 100644 --- a/src/ExpressionParser.php +++ b/src/ExpressionParser.php @@ -750,15 +750,15 @@ private function getTest(int $line): TwigTest $stream = $this->parser->getStream(); $name = $stream->expect(Token::NAME_TYPE)->getValue(); - if (!$test = $this->env->getTest($name)) { - if ($stream->test(Token::NAME_TYPE)) { - // try 2-words tests - $name = $name.' '.$this->parser->getCurrentToken()->getValue(); + if ($stream->test(Token::NAME_TYPE)) { + // try 2-words tests + $name = $name.' '.$this->parser->getCurrentToken()->getValue(); - if ($test = $this->env->getTest($name)) { - $stream->next(); - } + if ($test = $this->env->getTest($name)) { + $stream->next(); } + } else { + $test = $this->env->getTest($name); } if (!$test) { diff --git a/tests/ExpressionParserTest.php b/tests/ExpressionParserTest.php index fe780be3328..49e42dcec02 100644 --- a/tests/ExpressionParserTest.php +++ b/tests/ExpressionParserTest.php @@ -573,6 +573,17 @@ public function testReadyTest() $this->doesNotPerformAssertions(); } + public function testTwoWordTestPrecedence() + { + // a "empty element" test must have precedence over "empty" + $env = new Environment(new ArrayLoader(), ['cache' => false, 'autoescape' => false]); + $env->addTest(new TwigTest('empty element', 'foo')); + $parser = new Parser($env); + + $parser->parse($env->tokenize(new Source('{{ 1 is empty element }}', 'index'))); + $this->doesNotPerformAssertions(); + } + private function createNameExpression(string $name, array $attributes) { $expression = new NameExpression($name, 1); From 41459e0ddb386205eac919221039213fe3f6205c Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Tue, 27 Aug 2024 13:18:23 +0200 Subject: [PATCH 389/812] Fix CS --- src/Node/MacroNode.php | 2 +- src/Node/ModuleNode.php | 2 +- src/Node/Node.php | 2 +- tests/Node/NodeTest.php | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Node/MacroNode.php b/src/Node/MacroNode.php index 6eeabc27563..ffd6b628d5a 100644 --- a/src/Node/MacroNode.php +++ b/src/Node/MacroNode.php @@ -31,7 +31,7 @@ class MacroNode extends Node public function __construct(string $name, Node $body, Node $arguments, int $lineno, ?string $tag = null) { if (!$body instanceof BodyNode) { - trigger_deprecation('twig/twig', '3.12', sprintf('Not passing a "%s" instance as the "body" argument of the "%s" constructor is deprecated.', BodyNode::class, static::class)); + trigger_deprecation('twig/twig', '3.12', \sprintf('Not passing a "%s" instance as the "body" argument of the "%s" constructor is deprecated.', BodyNode::class, static::class)); } foreach ($arguments as $argumentName => $argument) { diff --git a/src/Node/ModuleNode.php b/src/Node/ModuleNode.php index a6df6b102e5..dc6191e91ee 100644 --- a/src/Node/ModuleNode.php +++ b/src/Node/ModuleNode.php @@ -36,7 +36,7 @@ final class ModuleNode extends Node public function __construct(Node $body, ?AbstractExpression $parent, Node $blocks, Node $macros, Node $traits, $embeddedTemplates, Source $source) { if (!$body instanceof BodyNode) { - trigger_deprecation('twig/twig', '3.12', sprintf('Not passing a "%s" instance as the "body" argument of the "%s" constructor is deprecated.', BodyNode::class, static::class)); + trigger_deprecation('twig/twig', '3.12', \sprintf('Not passing a "%s" instance as the "body" argument of the "%s" constructor is deprecated.', BodyNode::class, static::class)); } $nodes = [ diff --git a/src/Node/Node.php b/src/Node/Node.php index 3683dc383b6..a93ac29133e 100644 --- a/src/Node/Node.php +++ b/src/Node/Node.php @@ -82,7 +82,7 @@ public function __toString() } if (\count($this->nodes)) { - $repr .= \sprintf("\n nodes:"); + $repr .= "\n nodes:"; foreach ($this->nodes as $name => $node) { $len = \strlen($name) + 6; $noderepr = []; diff --git a/tests/Node/NodeTest.php b/tests/Node/NodeTest.php index 5a300323026..2a592403433 100644 --- a/tests/Node/NodeTest.php +++ b/tests/Node/NodeTest.php @@ -52,7 +52,7 @@ function: Twig\TwigFunction(a_function) filter: Twig\TwigFilter(a_filter) test: Twig\TwigTest(a_test) EOF - , (string) $node); + , (string) $node); } public function testToStringWithTag() @@ -63,7 +63,7 @@ public function testToStringWithTag() Twig\Node\Node tag: tag EOF - , (string) $node); + , (string) $node); } public function testAttributeDeprecationIgnore() From e83a8028f0e0b02f61d00eb83a1995378488a43c Mon Sep 17 00:00:00 2001 From: Jeroen Versteeg Date: Sat, 16 Mar 2024 16:44:23 +0100 Subject: [PATCH 390/812] Mark implicit macro argument default values as such with an attribute in AST --- src/ExpressionParser.php | 3 ++- tests/ParserTest.php | 23 +++++++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/src/ExpressionParser.php b/src/ExpressionParser.php index 6839bc93204..2a73a2721ca 100644 --- a/src/ExpressionParser.php +++ b/src/ExpressionParser.php @@ -591,7 +591,7 @@ public function parseFilterExpressionRaw($node, $tag = null) * Parses arguments. * * @param bool $namedArguments Whether to allow named arguments or not - * @param bool $definition Whether we are parsing arguments for a function definition + * @param bool $definition Whether we are parsing arguments for a function (or macro) definition * * @return Node * @@ -642,6 +642,7 @@ public function parseArguments($namedArguments = false, $definition = false, $al if (null === $name) { $name = $value->getAttribute('name'); $value = new ConstantExpression(null, $this->parser->getCurrentToken()->getLine()); + $value->setAttribute('is_implicit', true); } $args[$name] = $value; } else { diff --git a/tests/ParserTest.php b/tests/ParserTest.php index cdd8e875743..4731dea8fda 100644 --- a/tests/ParserTest.php +++ b/tests/ParserTest.php @@ -14,6 +14,8 @@ use PHPUnit\Framework\TestCase; use Twig\Environment; use Twig\Error\SyntaxError; +use Twig\Lexer; +use Twig\Loader\ArrayLoader; use Twig\Loader\LoaderInterface; use Twig\Node\Node; use Twig\Node\SetNode; @@ -175,6 +177,27 @@ public function testGetVarName() $this->addToAssertionCount(1); } + public function testImplicitMacroArgumentDefaultValues() + { + $template = '{% macro marco (po, lo = true) %}{% endmacro %}'; + $lexer = new Lexer(new Environment(new ArrayLoader())); + $stream = $lexer->tokenize(new Source($template, 'index')); + + $argumentNodes = $this->getParser() + ->parse($stream) + ->getNode('macros') + ->getNode('marco') + ->getNode('arguments') + ; + + $this->assertTrue($argumentNodes->getNode('po')->hasAttribute('is_implicit')); + $this->assertTrue($argumentNodes->getNode('po')->getAttribute('is_implicit')); + $this->assertNull($argumentNodes->getNode('po')->getAttribute('value')); + + $this->assertFalse($argumentNodes->getNode('lo')->hasAttribute('is_implicit')); + $this->assertSame(true, $argumentNodes->getNode('lo')->getAttribute('value')); + } + protected function getParser() { $parser = new Parser(new Environment($this->createMock(LoaderInterface::class))); From 7ba6866759f979ce1787d46b057852fdf95a1d8d Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Tue, 27 Aug 2024 15:54:22 +0200 Subject: [PATCH 391/812] Fix CoreExtension::captureOutput --- src/Extension/CoreExtension.php | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index e8cf5f6f206..a58e1e2c9ca 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -1888,30 +1888,22 @@ public static function checkArrowInSandbox(Environment $env, $arrow, $thing, $ty */ public static function captureOutput(iterable $body): string { - $output = ''; $level = ob_get_level(); ob_start(); try { foreach ($body as $data) { - if (ob_get_length()) { - $output .= ob_get_clean(); - ob_start(); - } - - $output .= $data; - } - - if (ob_get_length()) { - $output .= ob_get_clean(); + echo $data; } - } finally { + } catch (\Throwable $e) { while (ob_get_level() > $level) { ob_end_clean(); } + + throw $e; } - return $output; + return ob_get_clean(); } /** From 8c3df0c0bc6a1a5bcc39bca085fa6c539f6e2367 Mon Sep 17 00:00:00 2001 From: Ruud Kamphuis Date: Wed, 28 Aug 2024 08:20:39 +0200 Subject: [PATCH 392/812] Clarify block function It actually renders the block again. --- doc/functions/block.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/functions/block.rst b/doc/functions/block.rst index efc516eb1f6..ef8e5f4b614 100644 --- a/doc/functions/block.rst +++ b/doc/functions/block.rst @@ -1,7 +1,7 @@ ``block`` ========= -When a template uses inheritance and if you want to print a block multiple +When a template uses inheritance and if you want to render a block multiple times, use the ``block`` function: .. code-block:: html+twig From 5fca700cbde4474e1fc73f24d9260d43e9d20bf4 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sat, 24 Aug 2024 10:06:44 +0200 Subject: [PATCH 393/812] Deprecate the fact that the `extends` and `use` tags are always allowed in a sandboxed template --- CHANGELOG | 3 + doc/advanced.rst | 6 +- doc/api.rst | 8 ++- doc/deprecated.rst | 22 +++++-- extra/cache-extra/Node/CacheNode.php | 4 +- .../TokenParser/CacheTokenParser.php | 4 +- extra/cache-extra/composer.json | 2 +- src/ExpressionParser.php | 8 ++- src/Node/AutoEscapeNode.php | 4 +- src/Node/BlockNode.php | 4 +- src/Node/BlockReferenceNode.php | 4 +- src/Node/CaptureNode.php | 4 +- src/Node/CheckToStringNode.php | 2 +- src/Node/DeprecatedNode.php | 4 +- src/Node/DoNode.php | 4 +- src/Node/EmbedNode.php | 4 +- .../Expression/ArrowFunctionExpression.php | 4 +- .../Expression/BlockReferenceExpression.php | 4 +- src/Node/Expression/Filter/DefaultFilter.php | 4 +- src/Node/Expression/Filter/RawFilter.php | 4 +- src/Node/Expression/FilterExpression.php | 4 +- src/Node/Expression/ParentExpression.php | 4 +- src/Node/FlushNode.php | 4 +- src/Node/ForLoopNode.php | 4 +- src/Node/ForNode.php | 6 +- src/Node/IfNode.php | 4 +- src/Node/ImportNode.php | 2 +- src/Node/IncludeNode.php | 4 +- src/Node/MacroNode.php | 4 +- src/Node/Node.php | 20 +++++- src/Node/PrintNode.php | 4 +- src/Node/SandboxNode.php | 4 +- src/Node/SetNode.php | 4 +- src/Node/WithNode.php | 4 +- .../MacroAutoImportNodeVisitor.php | 2 +- src/Parser.php | 1 + src/Sandbox/SecurityPolicy.php | 8 ++- src/TokenParser/ApplyTokenParser.php | 8 +-- src/TokenParser/AutoEscapeTokenParser.php | 2 +- src/TokenParser/BlockTokenParser.php | 2 +- src/TokenParser/DeprecatedTokenParser.php | 2 +- src/TokenParser/DoTokenParser.php | 2 +- src/TokenParser/EmbedTokenParser.php | 2 +- src/TokenParser/ExtendsTokenParser.php | 2 +- src/TokenParser/FlushTokenParser.php | 2 +- src/TokenParser/ForTokenParser.php | 2 +- src/TokenParser/FromTokenParser.php | 2 +- src/TokenParser/IfTokenParser.php | 2 +- src/TokenParser/ImportTokenParser.php | 2 +- src/TokenParser/IncludeTokenParser.php | 2 +- src/TokenParser/MacroTokenParser.php | 4 +- src/TokenParser/SandboxTokenParser.php | 2 +- src/TokenParser/SetTokenParser.php | 2 +- src/TokenParser/UseTokenParser.php | 2 +- src/TokenParser/WithTokenParser.php | 2 +- tests/EnvironmentTest.php | 2 +- tests/ExpressionParserTest.php | 8 +-- tests/Extension/SandboxTest.php | 61 ++++++++++++++++++- tests/Node/DeprecatedTest.php | 6 +- tests/Node/NodeTest.php | 3 +- tests/ParserTest.php | 2 +- 61 files changed, 210 insertions(+), 102 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 8d27a072d4f..4d54148ac4f 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,8 @@ # 3.12.0 (2024-XX-XX) + * Deprecate the fact that the `extends` and `use` tags are always allowed in a sandboxed template. + This behavior will change in 4.0 where these tags will need to be explicitly allowed like any other tag. + * Deprecate the "tag" constructor argument of the "Twig\Node\Node" class as the tag is now automatically set by the Parser when needed * Fix precedence of two-word tests when the first word is a valid test * Deprecate the `spaceless` filter * Deprecate some internal methods from `Parser`: `getBlockStack()`, `hasBlock()`, `getBlock()`, `hasMacro()`, `hasTraits()`, `getParent()` diff --git a/doc/advanced.rst b/doc/advanced.rst index 1e89694e7e2..b3eaa1a2e96 100644 --- a/doc/advanced.rst +++ b/doc/advanced.rst @@ -487,7 +487,7 @@ Now, let's see the actual code of this class:: $value = $parser->getExpressionParser()->parseExpression(); $stream->expect(\Twig\Token::BLOCK_END_TYPE); - return new CustomSetNode($name, $value, $token->getLine(), $this->getTag()); + return new CustomSetNode($name, $value, $token->getLine()); } public function getTag() @@ -534,9 +534,9 @@ The ``CustomSetNode`` class itself is quite short:: class CustomSetNode extends \Twig\Node\Node { - public function __construct($name, \Twig\Node\Expression\AbstractExpression $value, $line, $tag = null) + public function __construct($name, \Twig\Node\Expression\AbstractExpression $value, $line) { - parent::__construct(['value' => $value], ['name' => $name], $line, $tag); + parent::__construct(['value' => $value], ['name' => $name], $line); } public function compile(\Twig\Compiler $compiler) diff --git a/doc/api.rst b/doc/api.rst index 7e2fd9a7f7d..09c553175e1 100644 --- a/doc/api.rst +++ b/doc/api.rst @@ -467,7 +467,7 @@ The ``sandbox`` extension can be used to evaluate untrusted code. Access to unsafe attributes and methods is prohibited. The sandbox security is managed by a policy instance. By default, Twig comes with one policy class: ``\Twig\Sandbox\SecurityPolicy``. This class allows you to white-list some -tags, filters, properties, and methods:: +tags, filters, functions, properties, and methods:: $tags = ['if']; $filters = ['upper']; @@ -486,6 +486,12 @@ able to call the ``getTitle()`` and ``getBody()`` methods on ``Article`` objects, and the ``title`` and ``body`` public properties. Everything else won't be allowed and will generate a ``\Twig\Sandbox\SecurityError`` exception. +.. caution:: + + The ``extends`` and ``use`` tags are always allowed in a sandboxed + template. That behavior will change in 4.0 where these tags will need to be + explicitly allowed like any other tag. + The policy object is the first argument of the sandbox constructor:: $sandbox = new \Twig\Extension\SandboxExtension($policy); diff --git a/doc/deprecated.rst b/doc/deprecated.rst index 950d961ddf4..affdd2ba924 100644 --- a/doc/deprecated.rst +++ b/doc/deprecated.rst @@ -35,6 +35,13 @@ Extensions Nodes ----- +* The "tag" constructor parameter of the ``Twig\Node\Node`` class is deprecated + as of Twig 3.12 as the tag is now automatically set by the Parser when + needed. + +* Passing a second argument to "ExpressionParser::parseFilterExpressionRaw()" + is deprecated as of Twig 3.12. + * The following ``Twig\Node\Node`` methods will take a string or an integer (instead of just a string) in Twig 4.0 for their "name" argument: ``getNode()``, ``hasNode()``, ``setNode()``, ``removeNode()``, and @@ -89,9 +96,9 @@ Nodes class NotReadyFilterExpression extends FilterExpression { - public function __construct(Node $node, ConstantExpression $filter, Node $arguments, int $lineno, ?string $tag = null) + public function __construct(Node $node, ConstantExpression $filter, Node $arguments, int $lineno) { - parent::__construct($node, $filter, $arguments, $lineno, $tag); + parent::__construct($node, $filter, $arguments, $lineno); } } @@ -117,9 +124,9 @@ Nodes class ReadyFilterExpression extends FilterExpression { #[FirstClassTwigCallableReady] - public function __construct(Node $node, TwigFilter|ConstantExpression $filter, Node $arguments, int $lineno, ?string $tag = null) + public function __construct(Node $node, TwigFilter|ConstantExpression $filter, Node $arguments, int $lineno) { - parent::__construct($node, $filter, $arguments, $lineno, $tag); + parent::__construct($node, $filter, $arguments, $lineno); } } @@ -186,3 +193,10 @@ Filters * The ``spaceless`` filter is deprecated as of Twig 3.12 and will be removed in Twig 4.0. + +Sandbox +------- + +* Having the ``extends`` and ``use`` tags allowed by default in a sandbox is + deprecated as of Twig 3.12. You will need to explicitly allow them if needed + in 4.0. diff --git a/extra/cache-extra/Node/CacheNode.php b/extra/cache-extra/Node/CacheNode.php index 5cb73c592c1..7308b6bc9fc 100644 --- a/extra/cache-extra/Node/CacheNode.php +++ b/extra/cache-extra/Node/CacheNode.php @@ -18,7 +18,7 @@ class CacheNode extends AbstractExpression { - public function __construct(AbstractExpression $key, ?AbstractExpression $ttl, ?AbstractExpression $tags, Node $body, int $lineno, string $tag) + public function __construct(AbstractExpression $key, ?AbstractExpression $ttl, ?AbstractExpression $tags, Node $body, int $lineno) { $body = new CaptureNode($body, $lineno); $body->setAttribute('raw', true); @@ -31,7 +31,7 @@ public function __construct(AbstractExpression $key, ?AbstractExpression $ttl, ? $nodes['tags'] = $tags; } - parent::__construct($nodes, [], $lineno, $tag); + parent::__construct($nodes, [], $lineno); } public function compile(Compiler $compiler): void diff --git a/extra/cache-extra/TokenParser/CacheTokenParser.php b/extra/cache-extra/TokenParser/CacheTokenParser.php index 61d5d2877c1..a5ea4840c14 100644 --- a/extra/cache-extra/TokenParser/CacheTokenParser.php +++ b/extra/cache-extra/TokenParser/CacheTokenParser.php @@ -56,9 +56,9 @@ public function parse(Token $token): Node $body = $this->parser->subparse([$this, 'decideCacheEnd'], true); $stream->expect(Token::BLOCK_END_TYPE); - $body = new CacheNode($key, $ttl, $tags, $body, $token->getLine(), $this->getTag()); + $body = new CacheNode($key, $ttl, $tags, $body, $token->getLine()); - return new PrintNode(new RawFilter($body), $token->getLine(), $this->getTag()); + return new PrintNode(new RawFilter($body), $token->getLine()); } public function decideCacheEnd(Token $token): bool diff --git a/extra/cache-extra/composer.json b/extra/cache-extra/composer.json index 09b67186e1d..74e377e9525 100644 --- a/extra/cache-extra/composer.json +++ b/extra/cache-extra/composer.json @@ -17,7 +17,7 @@ "require": { "php": ">=8.0.2", "symfony/cache": "^5.4|^6.4|^7.0", - "twig/twig": "^3.11" + "twig/twig": "^3.12" }, "require-dev": { "symfony/phpunit-bridge": "^6.4|^7.0" diff --git a/src/ExpressionParser.php b/src/ExpressionParser.php index 3b1d751a3ad..7ddbb937030 100644 --- a/src/ExpressionParser.php +++ b/src/ExpressionParser.php @@ -568,8 +568,12 @@ public function parseFilterExpression($node) return $this->parseFilterExpressionRaw($node); } - public function parseFilterExpressionRaw($node, $tag = null) + public function parseFilterExpressionRaw($node) { + if (func_num_args() > 1) { + trigger_deprecation('twig/twig', '3.12', 'Passing a second argument to "%s()" is deprecated.', __METHOD__); + } + while (true) { $token = $this->parser->getStream()->expect(Token::NAME_TYPE); @@ -590,7 +594,7 @@ public function parseFilterExpressionRaw($node, $tag = null) trigger_deprecation('twig/twig', '3.12', 'Twig node "%s" is not marked as ready for passing a "TwigFilter" in the constructor instead of its name; please update your code and then add #[FirstClassTwigCallableReady] attribute to the constructor.', $class); } - $node = new $class($node, $ready ? $filter : new ConstantExpression($filter->getName(), $token->getLine()), $arguments, $token->getLine(), $tag); + $node = new $class($node, $ready ? $filter : new ConstantExpression($filter->getName(), $token->getLine()), $arguments, $token->getLine()); if (!$this->parser->getStream()->test(Token::PUNCTUATION_TYPE, '|')) { break; diff --git a/src/Node/AutoEscapeNode.php b/src/Node/AutoEscapeNode.php index f9bc17e078f..ee806396ece 100644 --- a/src/Node/AutoEscapeNode.php +++ b/src/Node/AutoEscapeNode.php @@ -28,9 +28,9 @@ #[YieldReady] class AutoEscapeNode extends Node { - public function __construct($value, Node $body, int $lineno, string $tag = 'autoescape') + public function __construct($value, Node $body, int $lineno) { - parent::__construct(['body' => $body], ['value' => $value], $lineno, $tag); + parent::__construct(['body' => $body], ['value' => $value], $lineno); } public function compile(Compiler $compiler): void diff --git a/src/Node/BlockNode.php b/src/Node/BlockNode.php index 15973a343fb..d2cfc3bd8c4 100644 --- a/src/Node/BlockNode.php +++ b/src/Node/BlockNode.php @@ -23,9 +23,9 @@ #[YieldReady] class BlockNode extends Node { - public function __construct(string $name, Node $body, int $lineno, ?string $tag = null) + public function __construct(string $name, Node $body, int $lineno) { - parent::__construct(['body' => $body], ['name' => $name], $lineno, $tag); + parent::__construct(['body' => $body], ['name' => $name], $lineno); } public function compile(Compiler $compiler): void diff --git a/src/Node/BlockReferenceNode.php b/src/Node/BlockReferenceNode.php index 23c73eabee9..7c313a04cec 100644 --- a/src/Node/BlockReferenceNode.php +++ b/src/Node/BlockReferenceNode.php @@ -23,9 +23,9 @@ #[YieldReady] class BlockReferenceNode extends Node implements NodeOutputInterface { - public function __construct(string $name, int $lineno, ?string $tag = null) + public function __construct(string $name, int $lineno) { - parent::__construct([], ['name' => $name], $lineno, $tag); + parent::__construct([], ['name' => $name], $lineno); } public function compile(Compiler $compiler): void diff --git a/src/Node/CaptureNode.php b/src/Node/CaptureNode.php index b1cb357f569..0162113c14e 100644 --- a/src/Node/CaptureNode.php +++ b/src/Node/CaptureNode.php @@ -22,9 +22,9 @@ #[YieldReady] class CaptureNode extends Node { - public function __construct(Node $body, int $lineno, ?string $tag = null) + public function __construct(Node $body, int $lineno) { - parent::__construct(['body' => $body], ['raw' => false], $lineno, $tag); + parent::__construct(['body' => $body], ['raw' => false], $lineno); } public function compile(Compiler $compiler): void diff --git a/src/Node/CheckToStringNode.php b/src/Node/CheckToStringNode.php index 81fb92404f0..937240c1d3a 100644 --- a/src/Node/CheckToStringNode.php +++ b/src/Node/CheckToStringNode.php @@ -30,7 +30,7 @@ class CheckToStringNode extends AbstractExpression { public function __construct(AbstractExpression $expr) { - parent::__construct(['expr' => $expr], [], $expr->getTemplateLine(), $expr->getNodeTag()); + parent::__construct(['expr' => $expr], [], $expr->getTemplateLine()); } public function compile(Compiler $compiler): void diff --git a/src/Node/DeprecatedNode.php b/src/Node/DeprecatedNode.php index c4c4a8aecb5..0772adfc361 100644 --- a/src/Node/DeprecatedNode.php +++ b/src/Node/DeprecatedNode.php @@ -24,9 +24,9 @@ #[YieldReady] class DeprecatedNode extends Node { - public function __construct(AbstractExpression $expr, int $lineno, ?string $tag = null) + public function __construct(AbstractExpression $expr, int $lineno) { - parent::__construct(['expr' => $expr], [], $lineno, $tag); + parent::__construct(['expr' => $expr], [], $lineno); } public function compile(Compiler $compiler): void diff --git a/src/Node/DoNode.php b/src/Node/DoNode.php index 445016ab285..1593fd05024 100644 --- a/src/Node/DoNode.php +++ b/src/Node/DoNode.php @@ -23,9 +23,9 @@ #[YieldReady] class DoNode extends Node { - public function __construct(AbstractExpression $expr, int $lineno, ?string $tag = null) + public function __construct(AbstractExpression $expr, int $lineno) { - parent::__construct(['expr' => $expr], [], $lineno, $tag); + parent::__construct(['expr' => $expr], [], $lineno); } public function compile(Compiler $compiler): void diff --git a/src/Node/EmbedNode.php b/src/Node/EmbedNode.php index 54550946215..4cd3b38f2de 100644 --- a/src/Node/EmbedNode.php +++ b/src/Node/EmbedNode.php @@ -25,9 +25,9 @@ class EmbedNode extends IncludeNode { // we don't inject the module to avoid node visitors to traverse it twice (as it will be already visited in the main module) - public function __construct(string $name, int $index, ?AbstractExpression $variables, bool $only, bool $ignoreMissing, int $lineno, ?string $tag = null) + public function __construct(string $name, int $index, ?AbstractExpression $variables, bool $only, bool $ignoreMissing, int $lineno) { - parent::__construct(new ConstantExpression('not_used', $lineno), $variables, $only, $ignoreMissing, $lineno, $tag); + parent::__construct(new ConstantExpression('not_used', $lineno), $variables, $only, $ignoreMissing, $lineno); $this->setAttribute('name', $name); $this->setAttribute('index', $index); diff --git a/src/Node/Expression/ArrowFunctionExpression.php b/src/Node/Expression/ArrowFunctionExpression.php index eaad03c9c05..2bae4edd75f 100644 --- a/src/Node/Expression/ArrowFunctionExpression.php +++ b/src/Node/Expression/ArrowFunctionExpression.php @@ -21,9 +21,9 @@ */ class ArrowFunctionExpression extends AbstractExpression { - public function __construct(AbstractExpression $expr, Node $names, $lineno, $tag = null) + public function __construct(AbstractExpression $expr, Node $names, $lineno) { - parent::__construct(['expr' => $expr, 'names' => $names], [], $lineno, $tag); + parent::__construct(['expr' => $expr, 'names' => $names], [], $lineno); } public function compile(Compiler $compiler): void diff --git a/src/Node/Expression/BlockReferenceExpression.php b/src/Node/Expression/BlockReferenceExpression.php index 62938223371..acd231e15bc 100644 --- a/src/Node/Expression/BlockReferenceExpression.php +++ b/src/Node/Expression/BlockReferenceExpression.php @@ -22,14 +22,14 @@ */ class BlockReferenceExpression extends AbstractExpression { - public function __construct(Node $name, ?Node $template, int $lineno, ?string $tag = null) + public function __construct(Node $name, ?Node $template, int $lineno) { $nodes = ['name' => $name]; if (null !== $template) { $nodes['template'] = $template; } - parent::__construct($nodes, ['is_defined_test' => false, 'output' => false], $lineno, $tag); + parent::__construct($nodes, ['is_defined_test' => false, 'output' => false], $lineno); } public function compile(Compiler $compiler): void diff --git a/src/Node/Expression/Filter/DefaultFilter.php b/src/Node/Expression/Filter/DefaultFilter.php index e309c933118..75b6d18c287 100644 --- a/src/Node/Expression/Filter/DefaultFilter.php +++ b/src/Node/Expression/Filter/DefaultFilter.php @@ -34,7 +34,7 @@ class DefaultFilter extends FilterExpression { #[FirstClassTwigCallableReady] - public function __construct(Node $node, TwigFilter|ConstantExpression $filter, Node $arguments, int $lineno, ?string $tag = null) + public function __construct(Node $node, TwigFilter|ConstantExpression $filter, Node $arguments, int $lineno) { if ($filter instanceof TwigFilter) { $name = $filter->getName(); @@ -53,7 +53,7 @@ public function __construct(Node $node, TwigFilter|ConstantExpression $filter, N $node = $default; } - parent::__construct($node, $filter, $arguments, $lineno, $tag); + parent::__construct($node, $filter, $arguments, $lineno); } public function compile(Compiler $compiler): void diff --git a/src/Node/Expression/Filter/RawFilter.php b/src/Node/Expression/Filter/RawFilter.php index 12809ff4fdc..e115ab19410 100644 --- a/src/Node/Expression/Filter/RawFilter.php +++ b/src/Node/Expression/Filter/RawFilter.php @@ -24,9 +24,9 @@ class RawFilter extends FilterExpression { #[FirstClassTwigCallableReady] - public function __construct(Node $node, TwigFilter|ConstantExpression|null $filter = null, ?Node $arguments = null, int $lineno = 0, ?string $tag = null) + public function __construct(Node $node, TwigFilter|ConstantExpression|null $filter = null, ?Node $arguments = null, int $lineno = 0) { - parent::__construct($node, $filter ?: new TwigFilter('raw', null, ['is_safe' => ['all']]), $arguments ?: new Node(), $lineno ?: $node->getTemplateLine(), $tag ?: $node->getNodeTag()); + parent::__construct($node, $filter ?: new TwigFilter('raw', null, ['is_safe' => ['all']]), $arguments ?: new Node(), $lineno ?: $node->getTemplateLine()); } public function compile(Compiler $compiler): void diff --git a/src/Node/Expression/FilterExpression.php b/src/Node/Expression/FilterExpression.php index 82a4e4a2ab8..efc91193eac 100644 --- a/src/Node/Expression/FilterExpression.php +++ b/src/Node/Expression/FilterExpression.php @@ -21,7 +21,7 @@ class FilterExpression extends CallExpression { #[FirstClassTwigCallableReady] - public function __construct(Node $node, TwigFilter|ConstantExpression $filter, Node $arguments, int $lineno, ?string $tag = null) + public function __construct(Node $node, TwigFilter|ConstantExpression $filter, Node $arguments, int $lineno) { if ($filter instanceof TwigFilter) { $name = $filter->getName(); @@ -32,7 +32,7 @@ public function __construct(Node $node, TwigFilter|ConstantExpression $filter, N trigger_deprecation('twig/twig', '3.12', 'Not passing an instance of "TwigFilter" when creating a "%s" filter of type "%s" is deprecated.', $name, static::class); } - parent::__construct(['node' => $node, 'filter' => $filterName, 'arguments' => $arguments], ['name' => $name, 'type' => 'filter'], $lineno, $tag); + parent::__construct(['node' => $node, 'filter' => $filterName, 'arguments' => $arguments], ['name' => $name, 'type' => 'filter'], $lineno); if ($filter instanceof TwigFilter) { $this->setAttribute('twig_callable', $filter); diff --git a/src/Node/Expression/ParentExpression.php b/src/Node/Expression/ParentExpression.php index 59d833ac99d..22fe38f6a2d 100644 --- a/src/Node/Expression/ParentExpression.php +++ b/src/Node/Expression/ParentExpression.php @@ -21,9 +21,9 @@ */ class ParentExpression extends AbstractExpression { - public function __construct(string $name, int $lineno, ?string $tag = null) + public function __construct(string $name, int $lineno) { - parent::__construct([], ['output' => false, 'name' => $name], $lineno, $tag); + parent::__construct([], ['output' => false, 'name' => $name], $lineno); } public function compile(Compiler $compiler): void diff --git a/src/Node/FlushNode.php b/src/Node/FlushNode.php index 3f5a2111447..ff3bd1cf194 100644 --- a/src/Node/FlushNode.php +++ b/src/Node/FlushNode.php @@ -22,9 +22,9 @@ #[YieldReady] class FlushNode extends Node { - public function __construct(int $lineno, string $tag) + public function __construct(int $lineno) { - parent::__construct([], [], $lineno, $tag); + parent::__construct([], [], $lineno); } public function compile(Compiler $compiler): void diff --git a/src/Node/ForLoopNode.php b/src/Node/ForLoopNode.php index 503687c2b2e..3e044bbb09f 100644 --- a/src/Node/ForLoopNode.php +++ b/src/Node/ForLoopNode.php @@ -22,9 +22,9 @@ #[YieldReady] class ForLoopNode extends Node { - public function __construct(int $lineno, ?string $tag = null) + public function __construct(int $lineno) { - parent::__construct([], ['with_loop' => false, 'ifexpr' => false, 'else' => false], $lineno, $tag); + parent::__construct([], ['with_loop' => false, 'ifexpr' => false, 'else' => false], $lineno); } public function compile(Compiler $compiler): void diff --git a/src/Node/ForNode.php b/src/Node/ForNode.php index 5222cf9bf9f..d8af8962418 100644 --- a/src/Node/ForNode.php +++ b/src/Node/ForNode.php @@ -27,16 +27,16 @@ class ForNode extends Node { private $loop; - public function __construct(AssignNameExpression $keyTarget, AssignNameExpression $valueTarget, AbstractExpression $seq, ?Node $ifexpr, Node $body, ?Node $else, int $lineno, ?string $tag = null) + public function __construct(AssignNameExpression $keyTarget, AssignNameExpression $valueTarget, AbstractExpression $seq, ?Node $ifexpr, Node $body, ?Node $else, int $lineno) { - $body = new Node([$body, $this->loop = new ForLoopNode($lineno, $tag)]); + $body = new Node([$body, $this->loop = new ForLoopNode($lineno)]); $nodes = ['key_target' => $keyTarget, 'value_target' => $valueTarget, 'seq' => $seq, 'body' => $body]; if (null !== $else) { $nodes['else'] = $else; } - parent::__construct($nodes, ['with_loop' => true], $lineno, $tag); + parent::__construct($nodes, ['with_loop' => true], $lineno); } public function compile(Compiler $compiler): void diff --git a/src/Node/IfNode.php b/src/Node/IfNode.php index 1b883305ad4..2af48fa8159 100644 --- a/src/Node/IfNode.php +++ b/src/Node/IfNode.php @@ -23,14 +23,14 @@ #[YieldReady] class IfNode extends Node { - public function __construct(Node $tests, ?Node $else, int $lineno, ?string $tag = null) + public function __construct(Node $tests, ?Node $else, int $lineno) { $nodes = ['tests' => $tests]; if (null !== $else) { $nodes['else'] = $else; } - parent::__construct($nodes, [], $lineno, $tag); + parent::__construct($nodes, [], $lineno); } public function compile(Compiler $compiler): void diff --git a/src/Node/ImportNode.php b/src/Node/ImportNode.php index db47bfe61c9..e80b1fd1c66 100644 --- a/src/Node/ImportNode.php +++ b/src/Node/ImportNode.php @@ -26,7 +26,7 @@ class ImportNode extends Node { public function __construct(AbstractExpression $expr, AbstractExpression $var, int $lineno, ?string $tag = null, bool $global = true) { - parent::__construct(['expr' => $expr, 'var' => $var], ['global' => $global], $lineno, $tag); + parent::__construct(['expr' => $expr, 'var' => $var], ['global' => $global], $lineno); } public function compile(Compiler $compiler): void diff --git a/src/Node/IncludeNode.php b/src/Node/IncludeNode.php index 7073fa4ac38..1c18292c58e 100644 --- a/src/Node/IncludeNode.php +++ b/src/Node/IncludeNode.php @@ -24,14 +24,14 @@ #[YieldReady] class IncludeNode extends Node implements NodeOutputInterface { - public function __construct(AbstractExpression $expr, ?AbstractExpression $variables, bool $only, bool $ignoreMissing, int $lineno, ?string $tag = null) + public function __construct(AbstractExpression $expr, ?AbstractExpression $variables, bool $only, bool $ignoreMissing, int $lineno) { $nodes = ['expr' => $expr]; if (null !== $variables) { $nodes['variables'] = $variables; } - parent::__construct($nodes, ['only' => $only, 'ignore_missing' => $ignoreMissing], $lineno, $tag); + parent::__construct($nodes, ['only' => $only, 'ignore_missing' => $ignoreMissing], $lineno); } public function compile(Compiler $compiler): void diff --git a/src/Node/MacroNode.php b/src/Node/MacroNode.php index ffd6b628d5a..d54b8ac723b 100644 --- a/src/Node/MacroNode.php +++ b/src/Node/MacroNode.php @@ -28,7 +28,7 @@ class MacroNode extends Node /** * @param BodyNode $body */ - public function __construct(string $name, Node $body, Node $arguments, int $lineno, ?string $tag = null) + public function __construct(string $name, Node $body, Node $arguments, int $lineno) { if (!$body instanceof BodyNode) { trigger_deprecation('twig/twig', '3.12', \sprintf('Not passing a "%s" instance as the "body" argument of the "%s" constructor is deprecated.', BodyNode::class, static::class)); @@ -40,7 +40,7 @@ public function __construct(string $name, Node $body, Node $arguments, int $line } } - parent::__construct(['body' => $body, 'arguments' => $arguments], ['name' => $name], $lineno, $tag); + parent::__construct(['body' => $body, 'arguments' => $arguments], ['name' => $name], $lineno); } public function compile(Compiler $compiler): void diff --git a/src/Node/Node.php b/src/Node/Node.php index a93ac29133e..38ebdfa8c68 100644 --- a/src/Node/Node.php +++ b/src/Node/Node.php @@ -42,9 +42,8 @@ class Node implements \Countable, \IteratorAggregate * @param array $nodes An array of named nodes * @param array $attributes An array of attributes (should not be nodes) * @param int $lineno The line number - * @param string $tag The tag name associated with the Node */ - public function __construct(array $nodes = [], array $attributes = [], int $lineno = 0, ?string $tag = null) + public function __construct(array $nodes = [], array $attributes = [], int $lineno = 0) { foreach ($nodes as $name => $node) { if (!$node instanceof self) { @@ -54,7 +53,10 @@ public function __construct(array $nodes = [], array $attributes = [], int $line $this->nodes = $nodes; $this->attributes = $attributes; $this->lineno = $lineno; - $this->tag = $tag; + + if (func_num_args() > 3) { + trigger_deprecation('twig/twig', '3.12', sprintf('The "tag" constructor argument of the "%s" class is deprecated and ignored (check which TokenParser class set it to "%s"), the tag is now automatically set by the Parser when needed.', static::class, func_get_arg(3) ?: 'null')); + } } public function __toString() @@ -117,6 +119,18 @@ public function getNodeTag(): ?string return $this->tag; } + /** + * @internal + */ + public function setNodeTag(string $tag): void + { + if ($this->tag) { + throw new \LogicException('The tag of a node can only be set once.'); + } + + $this->tag = $tag; + } + public function hasAttribute(string $name): bool { return \array_key_exists($name, $this->attributes); diff --git a/src/Node/PrintNode.php b/src/Node/PrintNode.php index da442d85207..e3c23bbfa1c 100644 --- a/src/Node/PrintNode.php +++ b/src/Node/PrintNode.php @@ -24,9 +24,9 @@ #[YieldReady] class PrintNode extends Node implements NodeOutputInterface { - public function __construct(AbstractExpression $expr, int $lineno, ?string $tag = null) + public function __construct(AbstractExpression $expr, int $lineno) { - parent::__construct(['expr' => $expr], [], $lineno, $tag); + parent::__construct(['expr' => $expr], [], $lineno); } public function compile(Compiler $compiler): void diff --git a/src/Node/SandboxNode.php b/src/Node/SandboxNode.php index 80aecbdba36..d51cea44b48 100644 --- a/src/Node/SandboxNode.php +++ b/src/Node/SandboxNode.php @@ -22,9 +22,9 @@ #[YieldReady] class SandboxNode extends Node { - public function __construct(Node $body, int $lineno, ?string $tag = null) + public function __construct(Node $body, int $lineno) { - parent::__construct(['body' => $body], [], $lineno, $tag); + parent::__construct(['body' => $body], [], $lineno); } public function compile(Compiler $compiler): void diff --git a/src/Node/SetNode.php b/src/Node/SetNode.php index 0900f1542a9..67725104ece 100644 --- a/src/Node/SetNode.php +++ b/src/Node/SetNode.php @@ -23,7 +23,7 @@ #[YieldReady] class SetNode extends Node implements NodeCaptureInterface { - public function __construct(bool $capture, Node $names, Node $values, int $lineno, ?string $tag = null) + public function __construct(bool $capture, Node $names, Node $values, int $lineno) { /* * Optimizes the node when capture is used for a large block of text. @@ -41,7 +41,7 @@ public function __construct(bool $capture, Node $names, Node $values, int $linen } } - parent::__construct(['names' => $names, 'values' => $values], ['capture' => $capture, 'safe' => $safe], $lineno, $tag); + parent::__construct(['names' => $names, 'values' => $values], ['capture' => $capture, 'safe' => $safe], $lineno); } public function compile(Compiler $compiler): void diff --git a/src/Node/WithNode.php b/src/Node/WithNode.php index a7b7e70d9dd..f9104948b5e 100644 --- a/src/Node/WithNode.php +++ b/src/Node/WithNode.php @@ -22,14 +22,14 @@ #[YieldReady] class WithNode extends Node { - public function __construct(Node $body, ?Node $variables, bool $only, int $lineno, ?string $tag = null) + public function __construct(Node $body, ?Node $variables, bool $only, int $lineno) { $nodes = ['body' => $body]; if (null !== $variables) { $nodes['variables'] = $variables; } - parent::__construct($nodes, ['only' => $only], $lineno, $tag); + parent::__construct($nodes, ['only' => $only], $lineno); } public function compile(Compiler $compiler): void diff --git a/src/NodeVisitor/MacroAutoImportNodeVisitor.php b/src/NodeVisitor/MacroAutoImportNodeVisitor.php index d6a7781ba27..556d9dfbb19 100644 --- a/src/NodeVisitor/MacroAutoImportNodeVisitor.php +++ b/src/NodeVisitor/MacroAutoImportNodeVisitor.php @@ -46,7 +46,7 @@ public function leaveNode(Node $node, Environment $env): Node if ($node instanceof ModuleNode) { $this->inAModule = false; if ($this->hasMacroCalls) { - $node->getNode('constructor_end')->setNode('_auto_macro_import', new ImportNode(new NameExpression('_self', 0), new AssignNameExpression('_self', 0), 0, 'import', true)); + $node->getNode('constructor_end')->setNode('_auto_macro_import', new ImportNode(new NameExpression('_self', 0), new AssignNameExpression('_self', 0), 0, null, true)); } } elseif ($this->inAModule) { if ( diff --git a/src/Parser.php b/src/Parser.php index e3273230cfa..8923d4db67f 100644 --- a/src/Parser.php +++ b/src/Parser.php @@ -176,6 +176,7 @@ public function subparse($test, bool $dropNeedle = false): Node if (!$node) { trigger_deprecation('twig/twig', '3.12', 'Returning "null" from "%s" is deprecated and forbidden by "TokenParserInterface".', $subparser::class); } else { + $node->setNodeTag($subparser->getTag()); $rv[] = $node; } break; diff --git a/src/Sandbox/SecurityPolicy.php b/src/Sandbox/SecurityPolicy.php index 417d38a8d4e..f7e26962696 100644 --- a/src/Sandbox/SecurityPolicy.php +++ b/src/Sandbox/SecurityPolicy.php @@ -68,7 +68,13 @@ public function checkSecurity($tags, $filters, $functions): void { foreach ($tags as $tag) { if (!\in_array($tag, $this->allowedTags)) { - throw new SecurityNotAllowedTagError(\sprintf('Tag "%s" is not allowed.', $tag), $tag); + if ('extends' === $tag) { + trigger_deprecation('twig/twig', '3.12', 'The "extends" tag is always allowed in sandboxes, but won\'t be in 4.0, please enable it explicitely in your sandbox policy if needed.'); + } elseif ('use' === $tag) { + trigger_deprecation('twig/twig', '3.12', 'The "use" tag is always allowed in sandboxes, but won\'t be in 4.0, please enable it explicitely in your sandbox policy if needed.'); + } else { + throw new SecurityNotAllowedTagError(\sprintf('Tag "%s" is not allowed.', $tag), $tag); + } } } diff --git a/src/TokenParser/ApplyTokenParser.php b/src/TokenParser/ApplyTokenParser.php index 4dbf30406b0..0a6c1afb513 100644 --- a/src/TokenParser/ApplyTokenParser.php +++ b/src/TokenParser/ApplyTokenParser.php @@ -36,16 +36,16 @@ public function parse(Token $token): Node $ref = new TempNameExpression($name, $lineno); $ref->setAttribute('always_defined', true); - $filter = $this->parser->getExpressionParser()->parseFilterExpressionRaw($ref, $this->getTag()); + $filter = $this->parser->getExpressionParser()->parseFilterExpressionRaw($ref); $this->parser->getStream()->expect(Token::BLOCK_END_TYPE); $body = $this->parser->subparse([$this, 'decideApplyEnd'], true); $this->parser->getStream()->expect(Token::BLOCK_END_TYPE); return new Node([ - new SetNode(true, $ref, $body, $lineno, $this->getTag()), - new PrintNode($filter, $lineno, $this->getTag()), - ]); + new SetNode(true, $ref, $body, $lineno), + new PrintNode($filter, $lineno), + ], [], $lineno); } public function decideApplyEnd(Token $token): bool diff --git a/src/TokenParser/AutoEscapeTokenParser.php b/src/TokenParser/AutoEscapeTokenParser.php index 46790454a29..b50b29e659e 100644 --- a/src/TokenParser/AutoEscapeTokenParser.php +++ b/src/TokenParser/AutoEscapeTokenParser.php @@ -43,7 +43,7 @@ public function parse(Token $token): Node $body = $this->parser->subparse([$this, 'decideBlockEnd'], true); $stream->expect(Token::BLOCK_END_TYPE); - return new AutoEscapeNode($value, $body, $lineno, $this->getTag()); + return new AutoEscapeNode($value, $body, $lineno); } public function decideBlockEnd(Token $token): bool diff --git a/src/TokenParser/BlockTokenParser.php b/src/TokenParser/BlockTokenParser.php index b3768742dcd..81d675db0d9 100644 --- a/src/TokenParser/BlockTokenParser.php +++ b/src/TokenParser/BlockTokenParser.php @@ -60,7 +60,7 @@ public function parse(Token $token): Node $this->parser->popBlockStack(); $this->parser->popLocalScope(); - return new BlockReferenceNode($name, $lineno, $this->getTag()); + return new BlockReferenceNode($name, $lineno); } public function decideBlockEnd(Token $token): bool diff --git a/src/TokenParser/DeprecatedTokenParser.php b/src/TokenParser/DeprecatedTokenParser.php index c17c4aadc29..164ef26eec3 100644 --- a/src/TokenParser/DeprecatedTokenParser.php +++ b/src/TokenParser/DeprecatedTokenParser.php @@ -35,7 +35,7 @@ public function parse(Token $token): Node $stream = $this->parser->getStream(); $expressionParser = $this->parser->getExpressionParser(); $expr = $expressionParser->parseExpression(); - $node = new DeprecatedNode($expr, $token->getLine(), $this->getTag()); + $node = new DeprecatedNode($expr, $token->getLine()); while ($stream->test(Token::NAME_TYPE)) { $k = $stream->getCurrent()->getValue(); diff --git a/src/TokenParser/DoTokenParser.php b/src/TokenParser/DoTokenParser.php index 6b5c304981f..8afd4855937 100644 --- a/src/TokenParser/DoTokenParser.php +++ b/src/TokenParser/DoTokenParser.php @@ -28,7 +28,7 @@ public function parse(Token $token): Node $this->parser->getStream()->expect(Token::BLOCK_END_TYPE); - return new DoNode($expr, $token->getLine(), $this->getTag()); + return new DoNode($expr, $token->getLine()); } public function getTag(): string diff --git a/src/TokenParser/EmbedTokenParser.php b/src/TokenParser/EmbedTokenParser.php index f42f07afc49..7bf3233e238 100644 --- a/src/TokenParser/EmbedTokenParser.php +++ b/src/TokenParser/EmbedTokenParser.php @@ -58,7 +58,7 @@ public function parse(Token $token): Node $stream->expect(Token::BLOCK_END_TYPE); - return new EmbedNode($module->getTemplateName(), $module->getAttribute('index'), $variables, $only, $ignoreMissing, $token->getLine(), $this->getTag()); + return new EmbedNode($module->getTemplateName(), $module->getAttribute('index'), $variables, $only, $ignoreMissing, $token->getLine()); } public function decideBlockEnd(Token $token): bool diff --git a/src/TokenParser/ExtendsTokenParser.php b/src/TokenParser/ExtendsTokenParser.php index 7fba7da0448..86ddfdfba34 100644 --- a/src/TokenParser/ExtendsTokenParser.php +++ b/src/TokenParser/ExtendsTokenParser.php @@ -39,7 +39,7 @@ public function parse(Token $token): Node $stream->expect(Token::BLOCK_END_TYPE); - return new Node(); + return new Node([], [], $token->getLine()); } public function getTag(): string diff --git a/src/TokenParser/FlushTokenParser.php b/src/TokenParser/FlushTokenParser.php index 03e98abb484..0d238874579 100644 --- a/src/TokenParser/FlushTokenParser.php +++ b/src/TokenParser/FlushTokenParser.php @@ -28,7 +28,7 @@ public function parse(Token $token): Node { $this->parser->getStream()->expect(Token::BLOCK_END_TYPE); - return new FlushNode($token->getLine(), $this->getTag()); + return new FlushNode($token->getLine()); } public function getTag(): string diff --git a/src/TokenParser/ForTokenParser.php b/src/TokenParser/ForTokenParser.php index f939c10b334..cf655f8427a 100644 --- a/src/TokenParser/ForTokenParser.php +++ b/src/TokenParser/ForTokenParser.php @@ -58,7 +58,7 @@ public function parse(Token $token): Node } $valueTarget = new AssignNameExpression($valueTarget->getAttribute('name'), $valueTarget->getTemplateLine()); - return new ForNode($keyTarget, $valueTarget, $seq, null, $body, $else, $lineno, $this->getTag()); + return new ForNode($keyTarget, $valueTarget, $seq, null, $body, $else, $lineno); } public function decideForFork(Token $token): bool diff --git a/src/TokenParser/FromTokenParser.php b/src/TokenParser/FromTokenParser.php index 1619dd3d4b8..b54fadba859 100644 --- a/src/TokenParser/FromTokenParser.php +++ b/src/TokenParser/FromTokenParser.php @@ -50,7 +50,7 @@ public function parse(Token $token): Node $stream->expect(Token::BLOCK_END_TYPE); $var = new AssignNameExpression($this->parser->getVarName(), $token->getLine()); - $node = new ImportNode($macro, $var, $token->getLine(), $this->getTag(), $this->parser->isMainScope()); + $node = new ImportNode($macro, $var, $token->getLine(), null, $this->parser->isMainScope()); foreach ($targets as $name => $alias) { $this->parser->addImportedSymbol('function', $alias, 'macro_'.$name, $var); diff --git a/src/TokenParser/IfTokenParser.php b/src/TokenParser/IfTokenParser.php index acb074d9586..4ea6f3df9c3 100644 --- a/src/TokenParser/IfTokenParser.php +++ b/src/TokenParser/IfTokenParser.php @@ -69,7 +69,7 @@ public function parse(Token $token): Node $stream->expect(Token::BLOCK_END_TYPE); - return new IfNode(new Node($tests), $else, $lineno, $this->getTag()); + return new IfNode(new Node($tests), $else, $lineno); } public function decideIfFork(Token $token): bool diff --git a/src/TokenParser/ImportTokenParser.php b/src/TokenParser/ImportTokenParser.php index 595875f941b..808da58ccf5 100644 --- a/src/TokenParser/ImportTokenParser.php +++ b/src/TokenParser/ImportTokenParser.php @@ -34,7 +34,7 @@ public function parse(Token $token): Node $this->parser->addImportedSymbol('template', $var->getAttribute('name')); - return new ImportNode($macro, $var, $token->getLine(), $this->getTag(), $this->parser->isMainScope()); + return new ImportNode($macro, $var, $token->getLine(), null, $this->parser->isMainScope()); } public function getTag(): string diff --git a/src/TokenParser/IncludeTokenParser.php b/src/TokenParser/IncludeTokenParser.php index 9c3bba042fa..466f2288c63 100644 --- a/src/TokenParser/IncludeTokenParser.php +++ b/src/TokenParser/IncludeTokenParser.php @@ -33,7 +33,7 @@ public function parse(Token $token): Node [$variables, $only, $ignoreMissing] = $this->parseArguments(); - return new IncludeNode($expr, $variables, $only, $ignoreMissing, $token->getLine(), $this->getTag()); + return new IncludeNode($expr, $variables, $only, $ignoreMissing, $token->getLine()); } protected function parseArguments() diff --git a/src/TokenParser/MacroTokenParser.php b/src/TokenParser/MacroTokenParser.php index 3def0e73442..c7762075c56 100644 --- a/src/TokenParser/MacroTokenParser.php +++ b/src/TokenParser/MacroTokenParser.php @@ -49,9 +49,9 @@ public function parse(Token $token): Node $this->parser->popLocalScope(); $stream->expect(Token::BLOCK_END_TYPE); - $this->parser->setMacro($name, new MacroNode($name, new BodyNode([$body]), $arguments, $lineno, $this->getTag())); + $this->parser->setMacro($name, new MacroNode($name, new BodyNode([$body]), $arguments, $lineno)); - return new Node(); + return new Node([], [], $lineno); } public function decideBlockEnd(Token $token): bool diff --git a/src/TokenParser/SandboxTokenParser.php b/src/TokenParser/SandboxTokenParser.php index f628b29fd9a..70869fbc53d 100644 --- a/src/TokenParser/SandboxTokenParser.php +++ b/src/TokenParser/SandboxTokenParser.php @@ -51,7 +51,7 @@ public function parse(Token $token): Node } } - return new SandboxNode($body, $token->getLine(), $this->getTag()); + return new SandboxNode($body, $token->getLine()); } public function decideBlockEnd(Token $token): bool diff --git a/src/TokenParser/SetTokenParser.php b/src/TokenParser/SetTokenParser.php index 71cd977c0f8..bb43907bd24 100644 --- a/src/TokenParser/SetTokenParser.php +++ b/src/TokenParser/SetTokenParser.php @@ -58,7 +58,7 @@ public function parse(Token $token): Node $stream->expect(Token::BLOCK_END_TYPE); } - return new SetNode($capture, $names, $values, $lineno, $this->getTag()); + return new SetNode($capture, $names, $values, $lineno); } public function decideBlockEnd(Token $token): bool diff --git a/src/TokenParser/UseTokenParser.php b/src/TokenParser/UseTokenParser.php index abb647a5905..1b96b40478e 100644 --- a/src/TokenParser/UseTokenParser.php +++ b/src/TokenParser/UseTokenParser.php @@ -63,7 +63,7 @@ public function parse(Token $token): Node $this->parser->addTrait(new Node(['template' => $template, 'targets' => new Node($targets)])); - return new Node(); + return new Node([], [], $token->getLine()); } public function getTag(): string diff --git a/src/TokenParser/WithTokenParser.php b/src/TokenParser/WithTokenParser.php index 8c89a046bed..8ce4f02b2c5 100644 --- a/src/TokenParser/WithTokenParser.php +++ b/src/TokenParser/WithTokenParser.php @@ -41,7 +41,7 @@ public function parse(Token $token): Node $stream->expect(Token::BLOCK_END_TYPE); - return new WithNode($body, $variables, $only, $token->getLine(), $this->getTag()); + return new WithNode($body, $variables, $only, $token->getLine()); } public function decideWithEnd(Token $token): bool diff --git a/tests/EnvironmentTest.php b/tests/EnvironmentTest.php index 6311fdfca7a..9338e78f2f4 100644 --- a/tests/EnvironmentTest.php +++ b/tests/EnvironmentTest.php @@ -533,7 +533,7 @@ public function parse(Token $token): Node { $this->parser->getStream()->expect(Token::BLOCK_END_TYPE); - return new EnvironmentTest_LegacyEchoingNode(); + return new EnvironmentTest_LegacyEchoingNode([], [], 1); } public function getTag(): string diff --git a/tests/ExpressionParserTest.php b/tests/ExpressionParserTest.php index 49e42dcec02..3f28cca1e1a 100644 --- a/tests/ExpressionParserTest.php +++ b/tests/ExpressionParserTest.php @@ -605,9 +605,9 @@ public function __construct(string $function, Node $arguments, int $lineno) class NotReadyFilterExpression extends FilterExpression { - public function __construct(Node $node, ConstantExpression $filter, Node $arguments, int $lineno, ?string $tag = null) + public function __construct(Node $node, ConstantExpression $filter, Node $arguments, int $lineno) { - parent::__construct($node, $filter, $arguments, $lineno, $tag); + parent::__construct($node, $filter, $arguments, $lineno); } } @@ -643,9 +643,9 @@ public function __construct(TwigFunction|string $function, Node $arguments, int class ReadyFilterExpression extends FilterExpression { #[FirstClassTwigCallableReady] - public function __construct(Node $node, TwigFilter|ConstantExpression $filter, Node $arguments, int $lineno, ?string $tag = null) + public function __construct(Node $node, TwigFilter|ConstantExpression $filter, Node $arguments, int $lineno) { - parent::__construct($node, $filter, $arguments, $lineno, $tag); + parent::__construct($node, $filter, $arguments, $lineno); } } diff --git a/tests/Extension/SandboxTest.php b/tests/Extension/SandboxTest.php index cbe6175787b..611eec54171 100644 --- a/tests/Extension/SandboxTest.php +++ b/tests/Extension/SandboxTest.php @@ -12,6 +12,7 @@ */ use PHPUnit\Framework\TestCase; +use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; use Twig\Environment; use Twig\Error\SyntaxError; use Twig\Extension\SandboxExtension; @@ -28,6 +29,8 @@ class SandboxTest extends TestCase { + use ExpectDeprecationTrait; + protected static $params; protected static $templates; @@ -60,15 +63,71 @@ protected function setUp(): void '1_syntax_error' => '{% syntax error }}', '1_childobj_parentmethod' => '{{ child_obj.ParentMethod() }}', '1_childobj_childmethod' => '{{ child_obj.ChildMethod() }}', + '1_empty' => '', ]; } + /** + * @dataProvider getSandboxedForCoreTagsTests + */ + public function testSandboxForCoreTags(string $tag, string $template) + { + $this->expectException(SecurityError::class); + $this->expectExceptionMessageMatches(sprintf('/Tag "%s" is not allowed in "index \(string template .+?\)" at line 1/', $tag)); + + $twig = $this->getEnvironment(true, [], self::$templates, []); + $twig->createTemplate($template, 'index')->render([]); + } + + public function getSandboxedForCoreTagsTests() + { + yield ['apply', '{% apply upper %}foo{% endapply %}']; + yield ['autoescape', '{% autoescape %}foo{% endautoescape %}']; + yield ['block', '{% block foo %}foo{% endblock %}']; + yield ['deprecated', '{% deprecated "message" %}']; + yield ['do', '{% do 1 + 2 %}']; + yield ['embed', '{% embed "base.twig" %}{% endembed %}']; + // To be uncommented in 4.0 + //yield ['extends', '{% extends "base.twig" %}']; + yield ['flush', '{% flush %}']; + yield ['for', '{% for i in 1..2 %}{% endfor %}']; + yield ['from', '{% from "macros" import foo %}']; + yield ['if', '{% if false %}{% endif %}']; + yield ['import', '{% import "macros" as macros %}']; + yield ['include', '{% include "macros" %}']; + yield ['macro', '{% macro foo() %}{% endmacro %}']; + yield ['sandbox', '{% sandbox %}{% endsandbox %}']; + yield ['set', '{% set foo = 1 %}']; + // To be uncommented in 4.0 + //yield ['use', '{% use "1_empty" %}']; + yield ['with', '{% with foo %}{% endwith %}']; + } + + /** + * @dataProvider getSandboxedForExtendsAndUseTagsTests + * + * @group legacy + */ + public function testSandboxForExtendsAndUseTags(string $tag, string $template) + { + $this->expectDeprecation(sprintf('Since twig/twig 3.12: The "%s" tag is always allowed in sandboxes, but won\'t be in 4.0, please enable it explicitely in your sandbox policy if needed.', $tag)); + + $twig = $this->getEnvironment(true, [], self::$templates, []); + $twig->createTemplate($template, 'index')->render([]); + } + + public function getSandboxedForExtendsAndUseTagsTests() + { + yield ['extends', '{% extends "1_empty" %}']; + yield ['use', '{% use "1_empty" %}']; + } + public function testSandboxWithInheritance() { $this->expectException(SecurityError::class); $this->expectExceptionMessage('Filter "json_encode" is not allowed in "1_child" at line 3.'); - $twig = $this->getEnvironment(true, [], self::$templates, ['block']); + $twig = $this->getEnvironment(true, [], self::$templates, ['extends', 'block']); $twig->load('1_child')->render([]); } diff --git a/tests/Node/DeprecatedTest.php b/tests/Node/DeprecatedTest.php index de72ee70100..2bc23a6966d 100644 --- a/tests/Node/DeprecatedTest.php +++ b/tests/Node/DeprecatedTest.php @@ -37,7 +37,7 @@ public function getTests() $tests = []; $expr = new ConstantExpression('This section is deprecated', 1); - $node = new DeprecatedNode($expr, 1, 'deprecated'); + $node = new DeprecatedNode($expr, 1); $node->setSourceContext(new Source('', 'foo.twig')); $node->setNode('package', new ConstantExpression('twig/twig', 1)); $node->setNode('version', new ConstantExpression('1.1', 1)); @@ -50,7 +50,7 @@ public function getTests() $t = new Node([ new ConstantExpression(true, 1), - $dep = new DeprecatedNode($expr, 2, 'deprecated'), + $dep = new DeprecatedNode($expr, 2), ], [], 1); $node = new IfNode($t, null, 1); $node->setSourceContext(new Source('', 'foo.twig')); @@ -70,7 +70,7 @@ public function getTests() $environment->addFunction($function = new TwigFunction('foo', 'Twig\Tests\Node\foo', [])); $expr = new FunctionExpression($function, new Node(), 1); - $node = new DeprecatedNode($expr, 1, 'deprecated'); + $node = new DeprecatedNode($expr, 1); $node->setSourceContext(new Source('', 'foo.twig')); $node->setNode('package', new ConstantExpression('twig/twig', 1)); $node->setNode('version', new ConstantExpression('1.1', 1)); diff --git a/tests/Node/NodeTest.php b/tests/Node/NodeTest.php index 2a592403433..4bb913ecf0a 100644 --- a/tests/Node/NodeTest.php +++ b/tests/Node/NodeTest.php @@ -57,7 +57,8 @@ function: Twig\TwigFunction(a_function) public function testToStringWithTag() { - $node = new Node([], [], 1, 'tag'); + $node = new Node([], [], 1); + $node->setNodeTag('tag'); $this->assertEquals(<<parser->getStream()->expect(Token::BLOCK_END_TYPE); - return new Node([]); + return new Node([], [], 1); } public function getTag(): string From 8663ec242b4796b0816783fd4f97525b5aefcbfa Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Wed, 28 Aug 2024 14:17:54 +0200 Subject: [PATCH 394/812] fix typo --- doc/templates.rst | 2 +- src/Sandbox/SecurityPolicy.php | 4 ++-- tests/Extension/SandboxTest.php | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/doc/templates.rst b/doc/templates.rst index 63853561478..3cd8f9b90c7 100644 --- a/doc/templates.rst +++ b/doc/templates.rst @@ -869,7 +869,7 @@ determine how to convert the code to PHP: {# it is converted to the following PHP code: (6 & 2) || (6 & 16) #} -Change the default precedence by explicitely grouping expressions with parentheses: +Change the default precedence by explicitly grouping expressions with parentheses: .. code-block:: twig diff --git a/src/Sandbox/SecurityPolicy.php b/src/Sandbox/SecurityPolicy.php index f7e26962696..988e37216cf 100644 --- a/src/Sandbox/SecurityPolicy.php +++ b/src/Sandbox/SecurityPolicy.php @@ -69,9 +69,9 @@ public function checkSecurity($tags, $filters, $functions): void foreach ($tags as $tag) { if (!\in_array($tag, $this->allowedTags)) { if ('extends' === $tag) { - trigger_deprecation('twig/twig', '3.12', 'The "extends" tag is always allowed in sandboxes, but won\'t be in 4.0, please enable it explicitely in your sandbox policy if needed.'); + trigger_deprecation('twig/twig', '3.12', 'The "extends" tag is always allowed in sandboxes, but won\'t be in 4.0, please enable it explicitly in your sandbox policy if needed.'); } elseif ('use' === $tag) { - trigger_deprecation('twig/twig', '3.12', 'The "use" tag is always allowed in sandboxes, but won\'t be in 4.0, please enable it explicitely in your sandbox policy if needed.'); + trigger_deprecation('twig/twig', '3.12', 'The "use" tag is always allowed in sandboxes, but won\'t be in 4.0, please enable it explicitly in your sandbox policy if needed.'); } else { throw new SecurityNotAllowedTagError(\sprintf('Tag "%s" is not allowed.', $tag), $tag); } diff --git a/tests/Extension/SandboxTest.php b/tests/Extension/SandboxTest.php index 611eec54171..d193e7ef903 100644 --- a/tests/Extension/SandboxTest.php +++ b/tests/Extension/SandboxTest.php @@ -110,7 +110,7 @@ public function getSandboxedForCoreTagsTests() */ public function testSandboxForExtendsAndUseTags(string $tag, string $template) { - $this->expectDeprecation(sprintf('Since twig/twig 3.12: The "%s" tag is always allowed in sandboxes, but won\'t be in 4.0, please enable it explicitely in your sandbox policy if needed.', $tag)); + $this->expectDeprecation(sprintf('Since twig/twig 3.12: The "%s" tag is always allowed in sandboxes, but won\'t be in 4.0, please enable it explicitly in your sandbox policy if needed.', $tag)); $twig = $this->getEnvironment(true, [], self::$templates, []); $twig->createTemplate($template, 'index')->render([]); From 70396d75dc6d00f2f4a6241e090c3090d1864fb4 Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Wed, 28 Aug 2024 14:36:04 +0200 Subject: [PATCH 395/812] deprecate passing a tag to ImportNode --- src/Node/ImportNode.php | 12 +++++++++++- src/NodeVisitor/MacroAutoImportNodeVisitor.php | 2 +- src/TokenParser/FromTokenParser.php | 2 +- src/TokenParser/ImportTokenParser.php | 2 +- 4 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/Node/ImportNode.php b/src/Node/ImportNode.php index e80b1fd1c66..31acbe9d19d 100644 --- a/src/Node/ImportNode.php +++ b/src/Node/ImportNode.php @@ -24,8 +24,18 @@ #[YieldReady] class ImportNode extends Node { - public function __construct(AbstractExpression $expr, AbstractExpression $var, int $lineno, ?string $tag = null, bool $global = true) + /** + * @param bool $global + */ + public function __construct(AbstractExpression $expr, AbstractExpression $var, int $lineno, $global = true) { + if (null === $global || is_string($global)) { + trigger_deprecation('twig/twig', '3.12', 'Passing a tag to %s() is deprecated.', __METHOD__); + $global = func_num_args() > 4 ? func_get_arg(4) : true; + } elseif (!is_bool($global)) { + throw new \TypeError(\sprintf('Argument 4 passed to "%s()" must be a boolean, "%s" given.', __METHOD__, get_debug_type($global))); + } + parent::__construct(['expr' => $expr, 'var' => $var], ['global' => $global], $lineno); } diff --git a/src/NodeVisitor/MacroAutoImportNodeVisitor.php b/src/NodeVisitor/MacroAutoImportNodeVisitor.php index 556d9dfbb19..01d5a997fff 100644 --- a/src/NodeVisitor/MacroAutoImportNodeVisitor.php +++ b/src/NodeVisitor/MacroAutoImportNodeVisitor.php @@ -46,7 +46,7 @@ public function leaveNode(Node $node, Environment $env): Node if ($node instanceof ModuleNode) { $this->inAModule = false; if ($this->hasMacroCalls) { - $node->getNode('constructor_end')->setNode('_auto_macro_import', new ImportNode(new NameExpression('_self', 0), new AssignNameExpression('_self', 0), 0, null, true)); + $node->getNode('constructor_end')->setNode('_auto_macro_import', new ImportNode(new NameExpression('_self', 0), new AssignNameExpression('_self', 0), 0, true)); } } elseif ($this->inAModule) { if ( diff --git a/src/TokenParser/FromTokenParser.php b/src/TokenParser/FromTokenParser.php index b54fadba859..2ccff5fbe6c 100644 --- a/src/TokenParser/FromTokenParser.php +++ b/src/TokenParser/FromTokenParser.php @@ -50,7 +50,7 @@ public function parse(Token $token): Node $stream->expect(Token::BLOCK_END_TYPE); $var = new AssignNameExpression($this->parser->getVarName(), $token->getLine()); - $node = new ImportNode($macro, $var, $token->getLine(), null, $this->parser->isMainScope()); + $node = new ImportNode($macro, $var, $token->getLine(), $this->parser->isMainScope()); foreach ($targets as $name => $alias) { $this->parser->addImportedSymbol('function', $alias, 'macro_'.$name, $var); diff --git a/src/TokenParser/ImportTokenParser.php b/src/TokenParser/ImportTokenParser.php index 808da58ccf5..f20f35ab3c3 100644 --- a/src/TokenParser/ImportTokenParser.php +++ b/src/TokenParser/ImportTokenParser.php @@ -34,7 +34,7 @@ public function parse(Token $token): Node $this->parser->addImportedSymbol('template', $var->getAttribute('name')); - return new ImportNode($macro, $var, $token->getLine(), null, $this->parser->isMainScope()); + return new ImportNode($macro, $var, $token->getLine(), $this->parser->isMainScope()); } public function getTag(): string From 42245310eebc429d545b55997361639a0f411e43 Mon Sep 17 00:00:00 2001 From: Ruud Kamphuis Date: Wed, 28 Aug 2024 15:47:07 +0200 Subject: [PATCH 396/812] Only unset loop when with_loop Even though PHP does not complain, PHPStan does. Since this is compiled code, we can easily produce a bit more valid code in the eyes of PHPStan. --- src/Node/ForNode.php | 6 +++++- tests/Node/ForTest.php | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Node/ForNode.php b/src/Node/ForNode.php index d8af8962418..122063105b5 100644 --- a/src/Node/ForNode.php +++ b/src/Node/ForNode.php @@ -101,7 +101,11 @@ public function compile(Compiler $compiler): void $compiler->write("\$_parent = \$context['_parent'];\n"); // remove some "private" loop variables (needed for nested loops) - $compiler->write('unset($context[\'_seq\'], $context[\'_iterated\'], $context[\''.$this->getNode('key_target')->getAttribute('name').'\'], $context[\''.$this->getNode('value_target')->getAttribute('name').'\'], $context[\'_parent\'], $context[\'loop\']);'."\n"); + $compiler->write('unset($context[\'_seq\'], $context[\'_iterated\'], $context[\''.$this->getNode('key_target')->getAttribute('name').'\'], $context[\''.$this->getNode('value_target')->getAttribute('name').'\'], $context[\'_parent\']'); + if ($this->getAttribute('with_loop')) { + $compiler->raw(', $context[\'loop\']'); + } + $compiler->raw(");\n"); // keep the values set in the inner context for variables defined in the outer context $compiler->write("\$context = array_intersect_key(\$context, \$_parent) + \$_parent;\n"); diff --git a/tests/Node/ForTest.php b/tests/Node/ForTest.php index 3ca4b22a304..5d59ae80471 100644 --- a/tests/Node/ForTest.php +++ b/tests/Node/ForTest.php @@ -62,7 +62,7 @@ public function getTests() yield {$this->getVariableGetter('foo')}; } \$_parent = \$context['_parent']; -unset(\$context['_seq'], \$context['_iterated'], \$context['key'], \$context['item'], \$context['_parent'], \$context['loop']); +unset(\$context['_seq'], \$context['_iterated'], \$context['key'], \$context['item'], \$context['_parent']); \$context = array_intersect_key(\$context, \$_parent) + \$_parent; EOF ]; From bb8b9c197ce2bb89bd65f29664eb27c915562eeb Mon Sep 17 00:00:00 2001 From: Ruud Kamphuis Date: Wed, 28 Aug 2024 19:23:03 +0200 Subject: [PATCH 397/812] Add return type `isTraitable` --- src/Node/ModuleNode.php | 21 ++++++++------ src/Template.php | 16 ++++------- tests/Node/ModuleTest.php | 58 ++++++++++++++++++++++++--------------- tests/TemplateTest.php | 9 +++--- 4 files changed, 60 insertions(+), 44 deletions(-) diff --git a/src/Node/ModuleNode.php b/src/Node/ModuleNode.php index dc6191e91ee..deb05a16f55 100644 --- a/src/Node/ModuleNode.php +++ b/src/Node/ModuleNode.php @@ -115,7 +115,7 @@ protected function compileGetParent(Compiler $compiler) $parent = $this->getNode('parent'); $compiler - ->write("protected function doGetParent(array \$context)\n", "{\n") + ->write("protected function doGetParent(array \$context): bool|string|Template|TemplateWrapper\n", "{\n") ->indent() ->addDebugInfo($parent) ->write('return ') @@ -160,7 +160,9 @@ protected function compileClassHeader(Compiler $compiler) ->write("use Twig\Sandbox\SecurityNotAllowedFilterError;\n") ->write("use Twig\Sandbox\SecurityNotAllowedFunctionError;\n") ->write("use Twig\Source;\n") - ->write("use Twig\Template;\n\n") + ->write("use Twig\Template;\n") + ->write("use Twig\TemplateWrapper;\n") + ->write("\n") ; } $compiler @@ -170,8 +172,11 @@ protected function compileClassHeader(Compiler $compiler) ->raw(" extends Template\n") ->write("{\n") ->indent() - ->write("private \$source;\n") - ->write("private \$macros = [];\n\n") + ->write("private Source \$source;\n") + ->write("/**\n") + ->write(" * @var array\n") + ->write(" */\n") + ->write("private array \$macros = [];\n\n") ; } @@ -377,7 +382,7 @@ protected function compileGetTemplateName(Compiler $compiler) ->write("/**\n") ->write(" * @codeCoverageIgnore\n") ->write(" */\n") - ->write("public function getTemplateName()\n", "{\n") + ->write("public function getTemplateName(): string\n", "{\n") ->indent() ->write('return ') ->repr($this->getSourceContext()->getName()) @@ -434,7 +439,7 @@ protected function compileIsTraitable(Compiler $compiler) ->write("/**\n") ->write(" * @codeCoverageIgnore\n") ->write(" */\n") - ->write("public function isTraitable()\n", "{\n") + ->write("public function isTraitable(): bool\n", "{\n") ->indent() ->write("return false;\n") ->outdent() @@ -448,7 +453,7 @@ protected function compileDebugInfo(Compiler $compiler) ->write("/**\n") ->write(" * @codeCoverageIgnore\n") ->write(" */\n") - ->write("public function getDebugInfo()\n", "{\n") + ->write("public function getDebugInfo(): array\n", "{\n") ->indent() ->write(\sprintf("return %s;\n", str_replace("\n", '', var_export(array_reverse($compiler->getDebugInfo(), true), true)))) ->outdent() @@ -459,7 +464,7 @@ protected function compileDebugInfo(Compiler $compiler) protected function compileGetSourceContext(Compiler $compiler) { $compiler - ->write("public function getSourceContext()\n", "{\n") + ->write("public function getSourceContext(): Source\n", "{\n") ->indent() ->write('return new Source(') ->string($compiler->getEnvironment()->isDebug() ? $this->getSourceContext()->getCode() : '') diff --git a/src/Template.php b/src/Template.php index d3c0c229d2b..e0cbb94e229 100644 --- a/src/Template.php +++ b/src/Template.php @@ -52,24 +52,20 @@ public function __construct(Environment $env) /** * Returns the template name. - * - * @return string The template name */ - abstract public function getTemplateName(); + abstract public function getTemplateName(): string; /** * Returns debug information about the template. * - * @return array Debug information + * @return array Debug information */ - abstract public function getDebugInfo(); + abstract public function getDebugInfo(): array; /** * Returns information about the original template source code. - * - * @return Source */ - abstract public function getSourceContext(); + abstract public function getSourceContext(): Source; /** * Returns the parent template. @@ -107,12 +103,12 @@ public function getParent(array $context) return $this->parents[$parent]; } - protected function doGetParent(array $context) + protected function doGetParent(array $context): bool|string|self|TemplateWrapper { return false; } - public function isTraitable() + public function isTraitable(): bool { return true; } diff --git a/tests/Node/ModuleTest.php b/tests/Node/ModuleTest.php index 674e8113296..f3081dff9a8 100644 --- a/tests/Node/ModuleTest.php +++ b/tests/Node/ModuleTest.php @@ -23,6 +23,8 @@ use Twig\Node\SetNode; use Twig\Node\TextNode; use Twig\Source; +use Twig\Template; +use Twig\TemplateWrapper; use Twig\Test\NodeTestCase; class ModuleTest extends NodeTestCase @@ -73,12 +75,16 @@ public function getTests() use Twig\Sandbox\SecurityNotAllowedFunctionError; use Twig\Source; use Twig\Template; +use Twig\TemplateWrapper; /* foo.twig */ class __TwigTemplate_%x extends Template { - private \$source; - private \$macros = []; + private Source \$source; + /** + * @var array + */ + private array \$macros = []; public function __construct(Environment \$env) { @@ -103,7 +109,7 @@ protected function doDisplay(array \$context, array \$blocks = []) /** * @codeCoverageIgnore */ - public function getTemplateName() + public function getTemplateName(): string { return "foo.twig"; } @@ -111,12 +117,12 @@ public function getTemplateName() /** * @codeCoverageIgnore */ - public function getDebugInfo() + public function getDebugInfo(): array { - return array ( 38 => 1,); + return array ( 42 => 1,); } - public function getSourceContext() + public function getSourceContext(): Source { return new Source("", "foo.twig", ""); } @@ -145,12 +151,16 @@ public function getSourceContext() use Twig\Sandbox\SecurityNotAllowedFunctionError; use Twig\Source; use Twig\Template; +use Twig\TemplateWrapper; /* foo.twig */ class __TwigTemplate_%x extends Template { - private \$source; - private \$macros = []; + private Source \$source; + /** + * @var array + */ + private array \$macros = []; public function __construct(Environment \$env) { @@ -162,7 +172,7 @@ public function __construct(Environment \$env) ]; } - protected function doGetParent(array \$context) + protected function doGetParent(array \$context): bool|string|Template|TemplateWrapper { // line 1 return "layout.twig"; @@ -181,7 +191,7 @@ protected function doDisplay(array \$context, array \$blocks = []) /** * @codeCoverageIgnore */ - public function getTemplateName() + public function getTemplateName(): string { return "foo.twig"; } @@ -189,7 +199,7 @@ public function getTemplateName() /** * @codeCoverageIgnore */ - public function isTraitable() + public function isTraitable(): bool { return false; } @@ -197,12 +207,12 @@ public function isTraitable() /** * @codeCoverageIgnore */ - public function getDebugInfo() + public function getDebugInfo(): array { - return array ( 44 => 1, 42 => 2, 35 => 1,); + return array ( 48 => 1, 46 => 2, 39 => 1,); } - public function getSourceContext() + public function getSourceContext(): Source { return new Source("", "foo.twig", ""); } @@ -236,12 +246,16 @@ public function getSourceContext() use Twig\Sandbox\SecurityNotAllowedFunctionError; use Twig\Source; use Twig\Template; +use Twig\TemplateWrapper; /* foo.twig */ class __TwigTemplate_%x extends Template { - private \$source; - private \$macros = []; + private Source \$source; + /** + * @var array + */ + private array \$macros = []; public function __construct(Environment \$env) { @@ -253,7 +267,7 @@ public function __construct(Environment \$env) ]; } - protected function doGetParent(array \$context) + protected function doGetParent(array \$context): bool|string|Template|TemplateWrapper { // line 2 return \$this->loadTemplate(((true) ? ("foo") : ("foo")), "foo.twig", 2); @@ -271,7 +285,7 @@ protected function doDisplay(array \$context, array \$blocks = []) /** * @codeCoverageIgnore */ - public function getTemplateName() + public function getTemplateName(): string { return "foo.twig"; } @@ -279,7 +293,7 @@ public function getTemplateName() /** * @codeCoverageIgnore */ - public function isTraitable() + public function isTraitable(): bool { return false; } @@ -287,12 +301,12 @@ public function isTraitable() /** * @codeCoverageIgnore */ - public function getDebugInfo() + public function getDebugInfo(): array { - return array ( 44 => 2, 42 => 4, 35 => 2,); + return array ( 48 => 2, 46 => 4, 39 => 2,); } - public function getSourceContext() + public function getSourceContext(): Source { return new Source("{{ foo }}", "foo.twig", ""); } diff --git a/tests/TemplateTest.php b/tests/TemplateTest.php index 37d6fe62a8c..884226e0a62 100644 --- a/tests/TemplateTest.php +++ b/tests/TemplateTest.php @@ -21,6 +21,7 @@ use Twig\Sandbox\SecurityPolicy; use Twig\Source; use Twig\Template; +use Twig\TemplateWrapper; class TemplateTest extends TestCase { @@ -443,22 +444,22 @@ public function getTrue() return true; } - public function getTemplateName() + public function getTemplateName(): string { return $this->name; } - public function getDebugInfo() + public function getDebugInfo() : array { return []; } - public function getSourceContext() + public function getSourceContext() : Source { return new Source('', $this->getTemplateName()); } - protected function doGetParent(array $context) + protected function doGetParent(array $context): bool|string|Template|TemplateWrapper { return false; } From 956d09bf08c312d66ea52990dfec938c5456dde7 Mon Sep 17 00:00:00 2001 From: Ruud Kamphuis Date: Wed, 28 Aug 2024 14:51:36 +0200 Subject: [PATCH 398/812] Add conditional return types to `ensureTraversable` --- src/Extension/CoreExtension.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index a58e1e2c9ca..f2b0c17942f 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -1280,6 +1280,9 @@ public static function callMacro(Template $template, string $method, array $args /** * @internal + * @template TSequence + * @param TSequence $seq + * @return ($seq is iterable ? TSequence : array{}) */ public static function ensureTraversable($seq) { From 33c4879dd8de769eb3ce4f904efb7bae7e18e146 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Thu, 29 Aug 2024 10:31:04 +0200 Subject: [PATCH 399/812] Fix CS --- src/Extension/CoreExtension.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index f2b0c17942f..fdb11dc7aa8 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -1279,10 +1279,13 @@ public static function callMacro(Template $template, string $method, array $args } /** - * @internal * @template TSequence + * * @param TSequence $seq + * * @return ($seq is iterable ? TSequence : array{}) + * + * @internal */ public static function ensureTraversable($seq) { From 4d19472d4ac1838e0b1f0e029ce1fa4040eb34ea Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Thu, 29 Aug 2024 11:51:12 +0200 Subject: [PATCH 400/812] Prepare the 3.12.0 release --- CHANGELOG | 2 +- src/Environment.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 4d54148ac4f..ff6951e6dec 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,4 +1,4 @@ -# 3.12.0 (2024-XX-XX) +# 3.12.0 (2024-08-29) * Deprecate the fact that the `extends` and `use` tags are always allowed in a sandboxed template. This behavior will change in 4.0 where these tags will need to be explicitly allowed like any other tag. diff --git a/src/Environment.php b/src/Environment.php index 237d598032a..097d1ca04f5 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -43,12 +43,12 @@ */ class Environment { - public const VERSION = '3.12.0-DEV'; + public const VERSION = '3.12.0'; public const VERSION_ID = 301200; public const MAJOR_VERSION = 3; public const MINOR_VERSION = 12; public const RELEASE_VERSION = 0; - public const EXTRA_VERSION = 'DEV'; + public const EXTRA_VERSION = ''; private $charset; private $loader; From edcea8bce0d14f871d58e9bb2ad93ff72c28b348 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Thu, 29 Aug 2024 11:53:10 +0200 Subject: [PATCH 401/812] Bump version --- CHANGELOG | 4 ++++ src/Environment.php | 8 ++++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index ff6951e6dec..6b9fdea1907 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +# 3.12.1 (2024-XX-XX) + + * n/a + # 3.12.0 (2024-08-29) * Deprecate the fact that the `extends` and `use` tags are always allowed in a sandboxed template. diff --git a/src/Environment.php b/src/Environment.php index 097d1ca04f5..f8dbe235236 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -43,12 +43,12 @@ */ class Environment { - public const VERSION = '3.12.0'; - public const VERSION_ID = 301200; + public const VERSION = '3.12.1-DEV'; + public const VERSION_ID = 301201; public const MAJOR_VERSION = 3; public const MINOR_VERSION = 12; - public const RELEASE_VERSION = 0; - public const EXTRA_VERSION = ''; + public const RELEASE_VERSION = 1; + public const EXTRA_VERSION = 'DEV'; private $charset; private $loader; From 9120e9f3d1f08ced708a9d43ec58b31c300b5ea0 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Thu, 29 Aug 2024 16:16:01 +0200 Subject: [PATCH 402/812] Bump version --- CHANGELOG | 2 +- src/Environment.php | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 6b9fdea1907..64ea20ce8f7 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,4 +1,4 @@ -# 3.12.1 (2024-XX-XX) +# 3.13.0 (2024-XX-XX) * n/a diff --git a/src/Environment.php b/src/Environment.php index f8dbe235236..d612ee5e5a3 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -43,11 +43,11 @@ */ class Environment { - public const VERSION = '3.12.1-DEV'; - public const VERSION_ID = 301201; + public const VERSION = '3.13.0-DEV'; + public const VERSION_ID = 301300; public const MAJOR_VERSION = 3; - public const MINOR_VERSION = 12; - public const RELEASE_VERSION = 1; + public const MINOR_VERSION = 13; + public const RELEASE_VERSION = 0; public const EXTRA_VERSION = 'DEV'; private $charset; From 67dabd8182bccd557070cffce843b3f4226e231b Mon Sep 17 00:00:00 2001 From: Jeroen Versteeg Date: Fri, 23 Aug 2024 16:15:55 +0200 Subject: [PATCH 403/812] Add types tag --- CHANGELOG | 2 +- doc/tags/index.rst | 1 + doc/tags/types.rst | 42 +++++++++++ src/Extension/CoreExtension.php | 2 + src/Node/TypesNode.php | 28 +++++++ src/TokenParser/TypesTokenParser.php | 85 ++++++++++++++++++++++ tests/Node/TypesTest.php | 43 +++++++++++ tests/TokenParser/TypesTokenParserTest.php | 69 ++++++++++++++++++ 8 files changed, 271 insertions(+), 1 deletion(-) create mode 100644 doc/tags/types.rst create mode 100644 src/Node/TypesNode.php create mode 100644 src/TokenParser/TypesTokenParser.php create mode 100644 tests/Node/TypesTest.php create mode 100644 tests/TokenParser/TypesTokenParserTest.php diff --git a/CHANGELOG b/CHANGELOG index 64ea20ce8f7..86f696fc422 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,6 @@ # 3.13.0 (2024-XX-XX) - * n/a + * Add the `types` tag (experimental) # 3.12.0 (2024-08-29) diff --git a/doc/tags/index.rst b/doc/tags/index.rst index b3c10408071..692ca6094d1 100644 --- a/doc/tags/index.rst +++ b/doc/tags/index.rst @@ -21,6 +21,7 @@ Tags macro sandbox set + types use verbatim with diff --git a/doc/tags/types.rst b/doc/tags/types.rst new file mode 100644 index 00000000000..389afc1fc0b --- /dev/null +++ b/doc/tags/types.rst @@ -0,0 +1,42 @@ +``types`` +========= + +.. versionadded:: 3.13 + + The ``types`` tag was added in Twig 3.13. This tag is **experimental** and can change based on usage and feedback. + +The ``types`` tag declares the types of template variables. + +To do this, specify a :ref:`mapping ` of names to their types as strings. + +Here is how to declare that ``foo`` is a boolean, while ``bar`` is an integer (see note below): + +.. code-block:: twig + + {% types { + foo: 'bool', + bar: 'int', + } %} + +You can declare variables as optional by adding the ``?`` suffix: + +.. code-block:: twig + + {% types { + foo: 'bool', + bar?: 'int', + } %} + +By default, this tag does not affect the template compilation or runtime behavior. + +Its purpose is to enable designers and developers to document and specify the context's available +and/or required variables. While Twig itself does not validate variables or their types, this tag enables extensions +to do this. + +Additionally, :ref:`Twig extensions ` can analyze these tags to perform compile-time and +runtime analysis of templates. + +.. note:: + + The syntax for and contents of type strings are intentionally left out of scope. + diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index fdb11dc7aa8..92c5fd5a820 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -82,6 +82,7 @@ use Twig\TokenParser\IncludeTokenParser; use Twig\TokenParser\MacroTokenParser; use Twig\TokenParser\SetTokenParser; +use Twig\TokenParser\TypesTokenParser; use Twig\TokenParser\UseTokenParser; use Twig\TokenParser\WithTokenParser; use Twig\TwigFilter; @@ -182,6 +183,7 @@ public function getTokenParsers(): array new ImportTokenParser(), new FromTokenParser(), new SetTokenParser(), + new TypesTokenParser(), new FlushTokenParser(), new DoTokenParser(), new EmbedTokenParser(), diff --git a/src/Node/TypesNode.php b/src/Node/TypesNode.php new file mode 100644 index 00000000000..5cdece665cf --- /dev/null +++ b/src/Node/TypesNode.php @@ -0,0 +1,28 @@ + + */ +#[YieldReady] +class TypesNode extends Node implements NodeCaptureInterface +{ + /** + * @param array $types + */ + public function __construct(array $types, int $lineno) + { + parent::__construct([], ['mapping' => $types], $lineno); + } + + public function compile(Compiler $compiler) + { + // Don't compile anything. + } +} diff --git a/src/TokenParser/TypesTokenParser.php b/src/TokenParser/TypesTokenParser.php new file mode 100644 index 00000000000..b579fd0c04e --- /dev/null +++ b/src/TokenParser/TypesTokenParser.php @@ -0,0 +1,85 @@ + + * @internal + */ +final class TypesTokenParser extends AbstractTokenParser +{ + public function parse(Token $token): Node + { + $stream = $this->parser->getStream(); + + $types = $this->parseSimpleMappingExpression($stream); + + $stream->expect(Token::BLOCK_END_TYPE); + + return new TypesNode($types, $token->getLine()); + } + + /** + * @return array + * + * @throws SyntaxError + */ + private function parseSimpleMappingExpression(TokenStream $stream): array + { + $stream->expect(Token::PUNCTUATION_TYPE, '{', 'A mapping element was expected'); + + $types = []; + + $first = true; + while (!$stream->test(Token::PUNCTUATION_TYPE, '}')) { + if (!$first) { + $stream->expect(Token::PUNCTUATION_TYPE, ',', 'A type string must be followed by a comma'); + + // trailing ,? + if ($stream->test(Token::PUNCTUATION_TYPE, '}')) { + break; + } + } + $first = false; + + $nameToken = $stream->expect(Token::NAME_TYPE); + $isOptional = $stream->nextIf(Token::PUNCTUATION_TYPE, '?') !== null; + + $stream->expect(Token::PUNCTUATION_TYPE, ':', 'A type name must be followed by a colon (:)'); + + $valueToken = $stream->expect(Token::STRING_TYPE); + + $types[$nameToken->getValue()] = [ + 'type' => $valueToken->getValue(), + 'optional' => $isOptional, + ]; + } + $stream->expect(Token::PUNCTUATION_TYPE, '}', 'An opened mapping is not properly closed'); + + return $types; + } + + public function getTag(): string + { + return 'types'; + } +} diff --git a/tests/Node/TypesTest.php b/tests/Node/TypesTest.php new file mode 100644 index 00000000000..ff6c2dfac12 --- /dev/null +++ b/tests/Node/TypesTest.php @@ -0,0 +1,43 @@ + [ + 'type' => 'string', + 'optional' => false, + ], + 'bar' => [ + 'type' => 'int', + 'optional' => true, + ] + ]; + } + + public function testConstructor() + { + $types = $this->getValidMapping(); + $node = new TypesNode($types, 1); + + $this->assertEquals($types, $node->getAttribute('mapping')); + } + + public function getTests() + { + return [ + // 1st test: Node shouldn't compile at all + [ + new TypesNode($this->getValidMapping(), 1), + '' + ] + ]; + } +} diff --git a/tests/TokenParser/TypesTokenParserTest.php b/tests/TokenParser/TypesTokenParserTest.php new file mode 100644 index 00000000000..1d27d9d151a --- /dev/null +++ b/tests/TokenParser/TypesTokenParserTest.php @@ -0,0 +1,69 @@ + false, 'autoescape' => false]); + $stream = $env->tokenize($source = new Source($template, '')); + $parser = new Parser($env); + + $typesNode = $parser->parse($stream)->getNode('body')->getNode('0'); + + self::assertEquals($expected, $typesNode->getAttribute('mapping')); + } + + public function getMappingTests(): array + { + return [ + // empty mapping + [ + '{% types {} %}', + [], + ], + + // simple + [ + '{% types {foo: "bar"} %}', + [ + 'foo' => ['type' => 'bar', 'optional' => false] + ], + ], + + // trailing comma + [ + '{% types {foo: "bar",} %}', + [ + 'foo' => ['type' => 'bar', 'optional' => false] + ], + ], + + // optional name + [ + '{% types {foo?: "bar"} %}', + [ + 'foo' => ['type' => 'bar', 'optional' => true] + ], + ], + + // multiple pairs, duplicate values + [ + '{% types {foo: "foo", bar?: "foo", baz: "baz"} %}', + [ + 'foo' => ['type' => 'foo', 'optional' => false], + 'bar' => ['type' => 'foo', 'optional' => true], + 'baz' => ['type' => 'baz', 'optional' => false] + ], + ], + ]; + } +} From 28923f4269bafe82c7cd6f9d3707dd11cf0aba79 Mon Sep 17 00:00:00 2001 From: "andreybolonin1989@gmail.com" Date: Sat, 24 Aug 2024 11:31:27 +0300 Subject: [PATCH 404/812] Use CPP in full code base --- src/Cache/ChainCache.php | 8 +++----- src/Compiler.php | 7 +++---- src/Environment.php | 2 +- src/ExpressionParser.php | 10 ++++------ src/Extension/OptimizerExtension.php | 8 +++----- src/Extension/YieldNotReadyExtension.php | 8 +++----- src/Loader/ArrayLoader.php | 8 +++----- src/Loader/ChainLoader.php | 11 +++-------- src/Markup.php | 2 +- src/NodeVisitor/OptimizerNodeVisitor.php | 8 +++----- src/NodeVisitor/YieldNotReadyNodeVisitor.php | 7 +++---- src/Parser.php | 7 +++---- src/Profiler/NodeVisitor/ProfilerNodeVisitor.php | 7 +++---- src/Profiler/Profile.php | 13 +++++-------- src/Runtime/EscaperRuntime.php | 8 +++----- src/RuntimeLoader/ContainerRuntimeLoader.php | 8 +++----- src/RuntimeLoader/FactoryRuntimeLoader.php | 8 +++----- src/Source.php | 14 +++++--------- src/Template.php | 7 +++---- src/TemplateWrapper.php | 11 ++++------- src/Token.php | 14 +++++--------- src/TokenStream.php | 9 ++++----- src/Util/DeprecationCollector.php | 8 +++----- 23 files changed, 74 insertions(+), 119 deletions(-) diff --git a/src/Cache/ChainCache.php b/src/Cache/ChainCache.php index 18c66f35f95..c94afdb4346 100644 --- a/src/Cache/ChainCache.php +++ b/src/Cache/ChainCache.php @@ -21,14 +21,12 @@ */ final class ChainCache implements CacheInterface { - private $caches; - /** * @param iterable $caches The ordered list of caches used to store and fetch cached items */ - public function __construct(iterable $caches) - { - $this->caches = $caches; + public function __construct( + private iterable $caches, + ) { } public function generateKey(string $name, string $className): string diff --git a/src/Compiler.php b/src/Compiler.php index 1e7ed04c61d..1a43aa7f676 100644 --- a/src/Compiler.php +++ b/src/Compiler.php @@ -22,7 +22,6 @@ class Compiler private $lastLine; private $source; private $indentation; - private $env; private $debugInfo = []; private $sourceOffset; private $sourceLine; @@ -30,9 +29,9 @@ class Compiler private $didUseEcho = false; private $didUseEchoStack = []; - public function __construct(Environment $env) - { - $this->env = $env; + public function __construct( + private Environment $env, + ) { } public function getEnvironment(): Environment diff --git a/src/Environment.php b/src/Environment.php index 237d598032a..bb4bc09ae4f 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -107,7 +107,7 @@ class Environment * false (default): allows templates to use a mix of "yield" and "echo" calls to allow for a progressive migration * Switch to "true" when possible as this will be the only supported mode in Twig 4.0 */ - public function __construct(LoaderInterface $loader, $options = []) + public function __construct(LoaderInterface $loader, array $options = []) { $this->setLoader($loader); diff --git a/src/ExpressionParser.php b/src/ExpressionParser.php index 28b556ccca0..bfc1a1d0482 100644 --- a/src/ExpressionParser.php +++ b/src/ExpressionParser.php @@ -47,18 +47,16 @@ class ExpressionParser public const OPERATOR_LEFT = 1; public const OPERATOR_RIGHT = 2; - private $parser; - private $env; /** @var array}> */ private $unaryOperators; /** @var array, associativity: self::OPERATOR_*}> */ private $binaryOperators; private $readyNodes = []; - public function __construct(Parser $parser, Environment $env) - { - $this->parser = $parser; - $this->env = $env; + public function __construct( + private Parser $parser, + private Environment $env, + ) { $this->unaryOperators = $env->getUnaryOperators(); $this->binaryOperators = $env->getBinaryOperators(); } diff --git a/src/Extension/OptimizerExtension.php b/src/Extension/OptimizerExtension.php index 965bfdb041d..d3fe46a675f 100644 --- a/src/Extension/OptimizerExtension.php +++ b/src/Extension/OptimizerExtension.php @@ -15,11 +15,9 @@ final class OptimizerExtension extends AbstractExtension { - private $optimizers; - - public function __construct(int $optimizers = -1) - { - $this->optimizers = $optimizers; + public function __construct( + private int $optimizers = -1, + ) { } public function getNodeVisitors(): array diff --git a/src/Extension/YieldNotReadyExtension.php b/src/Extension/YieldNotReadyExtension.php index 2503c8d8140..49dfb808569 100644 --- a/src/Extension/YieldNotReadyExtension.php +++ b/src/Extension/YieldNotReadyExtension.php @@ -18,11 +18,9 @@ */ final class YieldNotReadyExtension extends AbstractExtension { - private $useYield; - - public function __construct(bool $useYield) - { - $this->useYield = $useYield; + public function __construct( + private bool $useYield, + ) { } public function getNodeVisitors(): array diff --git a/src/Loader/ArrayLoader.php b/src/Loader/ArrayLoader.php index ce613c9cc1e..2bb54b7a8df 100644 --- a/src/Loader/ArrayLoader.php +++ b/src/Loader/ArrayLoader.php @@ -28,14 +28,12 @@ */ final class ArrayLoader implements LoaderInterface { - private $templates = []; - /** * @param array $templates An array of templates (keys are the names, and values are the source code) */ - public function __construct(array $templates = []) - { - $this->templates = $templates; + public function __construct( + private array $templates = [], + ) { } public function setTemplate(string $name, string $template): void diff --git a/src/Loader/ChainLoader.php b/src/Loader/ChainLoader.php index 90f798db3fe..6e4f9511c5d 100644 --- a/src/Loader/ChainLoader.php +++ b/src/Loader/ChainLoader.php @@ -21,11 +21,6 @@ */ final class ChainLoader implements LoaderInterface { - /** - * @var \Traversable|LoaderInterface[] - */ - private $loaders; - /** * @var array */ @@ -34,9 +29,9 @@ final class ChainLoader implements LoaderInterface /** * @param iterable $loaders */ - public function __construct(iterable $loaders = []) - { - $this->loaders = $loaders; + public function __construct( + private iterable $loaders = [], + ) { } public function addLoader(LoaderInterface $loader): void diff --git a/src/Markup.php b/src/Markup.php index 1788acc4f73..3020c60c4cd 100644 --- a/src/Markup.php +++ b/src/Markup.php @@ -19,7 +19,7 @@ class Markup implements \Countable, \JsonSerializable { private $content; - private $charset; + private ?string $charset; public function __construct($content, $charset) { diff --git a/src/NodeVisitor/OptimizerNodeVisitor.php b/src/NodeVisitor/OptimizerNodeVisitor.php index 0d2dc02d5c0..a943f45c309 100644 --- a/src/NodeVisitor/OptimizerNodeVisitor.php +++ b/src/NodeVisitor/OptimizerNodeVisitor.php @@ -47,13 +47,13 @@ final class OptimizerNodeVisitor implements NodeVisitorInterface private $loops = []; private $loopsTargets = []; - private $optimizers; /** * @param int $optimizers The optimizer mode */ - public function __construct(int $optimizers = -1) - { + public function __construct( + private int $optimizers = -1, + ) { if ($optimizers > (self::OPTIMIZE_FOR | self::OPTIMIZE_RAW_FILTER | self::OPTIMIZE_TEXT_NODES)) { throw new \InvalidArgumentException(\sprintf('Optimizer mode "%s" is not valid.', $optimizers)); } @@ -65,8 +65,6 @@ public function __construct(int $optimizers = -1) if (-1 !== $optimizers && self::OPTIMIZE_TEXT_NODES === (self::OPTIMIZE_TEXT_NODES & $optimizers)) { trigger_deprecation('twig/twig', '3.12', 'The "Twig\NodeVisitor\OptimizerNodeVisitor::OPTIMIZE_TEXT_NODES" option is deprecated and does nothing.'); } - - $this->optimizers = $optimizers; } public function enterNode(Node $node, Environment $env): Node diff --git a/src/NodeVisitor/YieldNotReadyNodeVisitor.php b/src/NodeVisitor/YieldNotReadyNodeVisitor.php index 6470bdabc86..4b190b41415 100644 --- a/src/NodeVisitor/YieldNotReadyNodeVisitor.php +++ b/src/NodeVisitor/YieldNotReadyNodeVisitor.php @@ -21,12 +21,11 @@ */ final class YieldNotReadyNodeVisitor implements NodeVisitorInterface { - private $useYield; private $yieldReadyNodes = []; - public function __construct(bool $useYield) - { - $this->useYield = $useYield; + public function __construct( + private bool $useYield, + ) { } public function enterNode(Node $node, Environment $env): Node diff --git a/src/Parser.php b/src/Parser.php index 28cc8a0e11a..cd8da2b8eb0 100644 --- a/src/Parser.php +++ b/src/Parser.php @@ -40,15 +40,14 @@ class Parser private $blocks; private $blockStack; private $macros; - private $env; private $importedSymbols; private $traits; private $embeddedTemplates = []; private $varNameSalt = 0; - public function __construct(Environment $env) - { - $this->env = $env; + public function __construct( + private Environment $env, + ) { } public function getVarName(): string diff --git a/src/Profiler/NodeVisitor/ProfilerNodeVisitor.php b/src/Profiler/NodeVisitor/ProfilerNodeVisitor.php index 4d2a581054b..1458bc5fcc8 100644 --- a/src/Profiler/NodeVisitor/ProfilerNodeVisitor.php +++ b/src/Profiler/NodeVisitor/ProfilerNodeVisitor.php @@ -27,12 +27,11 @@ */ final class ProfilerNodeVisitor implements NodeVisitorInterface { - private $extensionName; private $varName; - public function __construct(string $extensionName) - { - $this->extensionName = $extensionName; + public function __construct( + private string $extensionName, + ) { $this->varName = \sprintf('__internal_%s', hash(\PHP_VERSION_ID < 80100 ? 'sha256' : 'xxh128', $extensionName)); } diff --git a/src/Profiler/Profile.php b/src/Profiler/Profile.php index 72506b7c8da..2928e164640 100644 --- a/src/Profiler/Profile.php +++ b/src/Profiler/Profile.php @@ -20,18 +20,15 @@ final class Profile implements \IteratorAggregate, \Serializable public const BLOCK = 'block'; public const TEMPLATE = 'template'; public const MACRO = 'macro'; - - private $template; - private $name; - private $type; private $starts = []; private $ends = []; private $profiles = []; - public function __construct(string $template = 'main', string $type = self::ROOT, string $name = 'main') - { - $this->template = $template; - $this->type = $type; + public function __construct( + private string $template = 'main', + private string $type = self::ROOT, + private string $name = 'main', + ) { $this->name = str_starts_with($name, '__internal_') ? 'INTERNAL' : $name; $this->enter(); } diff --git a/src/Runtime/EscaperRuntime.php b/src/Runtime/EscaperRuntime.php index b1dac964022..e4aee629ae2 100644 --- a/src/Runtime/EscaperRuntime.php +++ b/src/Runtime/EscaperRuntime.php @@ -26,11 +26,9 @@ final class EscaperRuntime implements RuntimeExtensionInterface /** @internal */ public $safeLookup = []; - private $charset; - - public function __construct($charset = 'UTF-8') - { - $this->charset = $charset; + public function __construct( + private $charset = 'UTF-8', + ) { } /** diff --git a/src/RuntimeLoader/ContainerRuntimeLoader.php b/src/RuntimeLoader/ContainerRuntimeLoader.php index b360d7beaf1..05106680c4f 100644 --- a/src/RuntimeLoader/ContainerRuntimeLoader.php +++ b/src/RuntimeLoader/ContainerRuntimeLoader.php @@ -23,11 +23,9 @@ */ class ContainerRuntimeLoader implements RuntimeLoaderInterface { - private $container; - - public function __construct(ContainerInterface $container) - { - $this->container = $container; + public function __construct( + private ContainerInterface $container, + ) { } public function load(string $class) diff --git a/src/RuntimeLoader/FactoryRuntimeLoader.php b/src/RuntimeLoader/FactoryRuntimeLoader.php index 13064839267..5d4e70b921d 100644 --- a/src/RuntimeLoader/FactoryRuntimeLoader.php +++ b/src/RuntimeLoader/FactoryRuntimeLoader.php @@ -18,14 +18,12 @@ */ class FactoryRuntimeLoader implements RuntimeLoaderInterface { - private $map; - /** * @param array $map An array where keys are class names and values factory callables */ - public function __construct(array $map = []) - { - $this->map = $map; + public function __construct( + private array $map = [], + ) { } public function load(string $class) diff --git a/src/Source.php b/src/Source.php index 3cb02403c1a..0f626b62d37 100644 --- a/src/Source.php +++ b/src/Source.php @@ -18,20 +18,16 @@ */ final class Source { - private $code; - private $name; - private $path; - /** * @param string $code The template source code * @param string $name The template logical name * @param string $path The filesystem path of the template if any */ - public function __construct(string $code, string $name, string $path = '') - { - $this->code = $code; - $this->name = $name; - $this->path = $path; + public function __construct( + private string $code, + private string $name, + private string $path = '', + ) { } public function getCode(): string diff --git a/src/Template.php b/src/Template.php index d3c0c229d2b..498c35a6df2 100644 --- a/src/Template.php +++ b/src/Template.php @@ -35,7 +35,6 @@ abstract class Template protected $parent; protected $parents = []; - protected $env; protected $blocks = []; protected $traits = []; protected $extensions = []; @@ -43,9 +42,9 @@ abstract class Template private $useYield; - public function __construct(Environment $env) - { - $this->env = $env; + public function __construct( + protected Environment $env, + ) { $this->useYield = $env->useYield(); $this->extensions = $env->getExtensions(); } diff --git a/src/TemplateWrapper.php b/src/TemplateWrapper.php index fcfb070c799..c31f5016159 100644 --- a/src/TemplateWrapper.php +++ b/src/TemplateWrapper.php @@ -18,19 +18,16 @@ */ final class TemplateWrapper { - private $env; - private $template; - /** * This method is for internal use only and should never be called * directly (use Twig\Environment::load() instead). * * @internal */ - public function __construct(Environment $env, Template $template) - { - $this->env = $env; - $this->template = $template; + public function __construct( + private Environment $env, + private Template $template, + ) { } public function render(array $context = []): string diff --git a/src/Token.php b/src/Token.php index 5be39bdc7ee..237634ad137 100644 --- a/src/Token.php +++ b/src/Token.php @@ -17,10 +17,6 @@ */ final class Token { - private $value; - private $type; - private $lineno; - public const EOF_TYPE = -1; public const TEXT_TYPE = 0; public const BLOCK_START_TYPE = 1; @@ -37,11 +33,11 @@ final class Token public const ARROW_TYPE = 12; public const SPREAD_TYPE = 13; - public function __construct(int $type, $value, int $lineno) - { - $this->type = $type; - $this->value = $value; - $this->lineno = $lineno; + public function __construct( + private int $type, + private $value, + private int $lineno, + ) { } public function __toString() diff --git a/src/TokenStream.php b/src/TokenStream.php index 32357f9319a..c91701bfe12 100644 --- a/src/TokenStream.php +++ b/src/TokenStream.php @@ -21,13 +21,12 @@ */ final class TokenStream { - private $tokens; private $current = 0; - private $source; - public function __construct(array $tokens, ?Source $source = null) - { - $this->tokens = $tokens; + public function __construct( + private array $tokens, + private ?Source $source = null, + ) { $this->source = $source ?: new Source('', ''); } diff --git a/src/Util/DeprecationCollector.php b/src/Util/DeprecationCollector.php index ad531061716..0ea26ed4baf 100644 --- a/src/Util/DeprecationCollector.php +++ b/src/Util/DeprecationCollector.php @@ -20,11 +20,9 @@ */ final class DeprecationCollector { - private $twig; - - public function __construct(Environment $twig) - { - $this->twig = $twig; + public function __construct( + private Environment $twig, + ) { } /** From 5e3f9d9f2637fdd4a4417b684d2a18c0bdda3f40 Mon Sep 17 00:00:00 2001 From: Ruud Kamphuis Date: Thu, 29 Aug 2024 19:39:20 +0200 Subject: [PATCH 405/812] Remove NodeCaptureInterface from TypesNode This was accidentally copied from SetNode --- src/Node/TypesNode.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Node/TypesNode.php b/src/Node/TypesNode.php index 5cdece665cf..ebb304d49f8 100644 --- a/src/Node/TypesNode.php +++ b/src/Node/TypesNode.php @@ -11,7 +11,7 @@ * @author Jeroen Versteeg */ #[YieldReady] -class TypesNode extends Node implements NodeCaptureInterface +class TypesNode extends Node { /** * @param array $types From a68804d6149fbfa75e511a5b9e7ab196a062082e Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Fri, 30 Aug 2024 11:00:01 +0200 Subject: [PATCH 406/812] Add an example on how to iterate over a string --- doc/tags/for.rst | 15 +++++++++++++++ tests/Fixtures/tags/for/for_on_strings.test | 13 +++++++++++++ 2 files changed, 28 insertions(+) create mode 100644 tests/Fixtures/tags/for/for_on_strings.test diff --git a/doc/tags/for.rst b/doc/tags/for.rst index 656d9c07b17..4160d751f30 100644 --- a/doc/tags/for.rst +++ b/doc/tags/for.rst @@ -139,3 +139,18 @@ the :doc:`slice <../filters/slice>` filter:
  • {{ user.username|e }}
  • {% endfor %} + +Iterating over a String +----------------------- + +To iterate over the characters of a string, use the +:doc:`split <../filters/split>` filter: + +.. code-block:: html+twig + +

    Characters

    +
      + {% for char in "諺 / ことわざ"|split('') -%} +
    • {{ char }}
    • + {%- endfor %} +
    diff --git a/tests/Fixtures/tags/for/for_on_strings.test b/tests/Fixtures/tags/for/for_on_strings.test new file mode 100644 index 00000000000..62d48426978 --- /dev/null +++ b/tests/Fixtures/tags/for/for_on_strings.test @@ -0,0 +1,13 @@ +--TEST-- +"for" tag can iterate over a string via the "split" filter +--TEMPLATE-- +{% set jp = "諺 / ことわざ" %} + +{% for letter in jp|split('') -%} + -{{- letter }} + {{- loop.last ? '.' }} +{%- endfor %} +--DATA-- +return [] +--EXPECT-- +-諺- -/- -こ-と-わ-ざ. From 2f8c62189845fa3cdc26d1a92d334e3d727bdbd5 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Fri, 30 Aug 2024 11:43:48 +0200 Subject: [PATCH 407/812] Fix doc markup --- doc/tags/for.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/tags/for.rst b/doc/tags/for.rst index 4160d751f30..1e78db63c19 100644 --- a/doc/tags/for.rst +++ b/doc/tags/for.rst @@ -45,7 +45,7 @@ The ``..`` operator can take any expression at both sides: * {{ letter }} {% endfor %} -.. tip: +.. tip:: If you need a step different from 1, you can use the ``range`` function instead. From 2efb955ab50f386f8cadcef1939fd08af1ecfa32 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Fri, 30 Aug 2024 12:40:51 +0200 Subject: [PATCH 408/812] Remove unneeded whitespace --- doc/tags/types.rst | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/doc/tags/types.rst b/doc/tags/types.rst index 389afc1fc0b..49e2807f4bf 100644 --- a/doc/tags/types.rst +++ b/doc/tags/types.rst @@ -7,7 +7,7 @@ The ``types`` tag declares the types of template variables. -To do this, specify a :ref:`mapping ` of names to their types as strings. +To do this, specify a :ref:`mapping ` of names to their types as strings. Here is how to declare that ``foo`` is a boolean, while ``bar`` is an integer (see note below): @@ -38,5 +38,4 @@ runtime analysis of templates. .. note:: - The syntax for and contents of type strings are intentionally left out of scope. - + The syntax for and contents of type strings are intentionally left out of scope. From 22abdfafae88b7be4d8c562aba027a1fd05c7296 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Fri, 30 Aug 2024 12:44:06 +0200 Subject: [PATCH 409/812] Remove unneeded whitespace --- tests/TokenParser/TypesTokenParserTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/TokenParser/TypesTokenParserTest.php b/tests/TokenParser/TypesTokenParserTest.php index 1d27d9d151a..67794c09779 100644 --- a/tests/TokenParser/TypesTokenParserTest.php +++ b/tests/TokenParser/TypesTokenParserTest.php @@ -59,8 +59,8 @@ public function getMappingTests(): array [ '{% types {foo: "foo", bar?: "foo", baz: "baz"} %}', [ - 'foo' => ['type' => 'foo', 'optional' => false], - 'bar' => ['type' => 'foo', 'optional' => true], + 'foo' => ['type' => 'foo', 'optional' => false], + 'bar' => ['type' => 'foo', 'optional' => true], 'baz' => ['type' => 'baz', 'optional' => false] ], ], From 7e52a6809719fa7dc4eacf91380b181a16668a4a Mon Sep 17 00:00:00 2001 From: Ruud Kamphuis Date: Thu, 29 Aug 2024 20:21:36 +0200 Subject: [PATCH 410/812] Add more types to Template class and compiled templates --- doc/internals.rst | 2 +- src/Node/BlockNode.php | 5 ++++- src/Node/ModuleNode.php | 2 +- src/Template.php | 34 ++++++++++++++++------------------ tests/Node/BlockTest.php | 5 ++++- tests/Node/ModuleTest.php | 6 +++--- tests/TemplateTest.php | 2 +- 7 files changed, 30 insertions(+), 26 deletions(-) diff --git a/doc/internals.rst b/doc/internals.rst index 97661411dd4..07ff85534de 100644 --- a/doc/internals.rst +++ b/doc/internals.rst @@ -124,7 +124,7 @@ using):: /* Hello {{ name }} */ class __TwigTemplate_1121b6f109fe93ebe8c6e22e3712bceb extends Template { - protected function doDisplay(array $context, array $blocks = []) + protected function doDisplay(array $context, array $blocks = []): iterable { $macros = $this->macros; // line 1 diff --git a/src/Node/BlockNode.php b/src/Node/BlockNode.php index d2cfc3bd8c4..2ee74a8d2f4 100644 --- a/src/Node/BlockNode.php +++ b/src/Node/BlockNode.php @@ -32,7 +32,10 @@ public function compile(Compiler $compiler): void { $compiler ->addDebugInfo($this) - ->write(\sprintf("public function block_%s(\$context, array \$blocks = [])\n", $this->getAttribute('name')), "{\n") + ->write("/**\n") + ->write(" * @return iterable\n") + ->write(" */\n") + ->write(\sprintf("public function block_%s(array \$context, array \$blocks = []): iterable\n", $this->getAttribute('name')), "{\n") ->indent() ->write("\$macros = \$this->macros;\n") ; diff --git a/src/Node/ModuleNode.php b/src/Node/ModuleNode.php index deb05a16f55..264a0e67fbe 100644 --- a/src/Node/ModuleNode.php +++ b/src/Node/ModuleNode.php @@ -318,7 +318,7 @@ protected function compileConstructor(Compiler $compiler) protected function compileDisplay(Compiler $compiler) { $compiler - ->write("protected function doDisplay(array \$context, array \$blocks = [])\n", "{\n") + ->write("protected function doDisplay(array \$context, array \$blocks = []): iterable\n", "{\n") ->indent() ->write("\$macros = \$this->macros;\n") ->subcompile($this->getNode('display_start')) diff --git a/src/Template.php b/src/Template.php index 653462db66d..ec9db9bf66a 100644 --- a/src/Template.php +++ b/src/Template.php @@ -74,7 +74,7 @@ abstract public function getSourceContext(): Source; * * @return self|TemplateWrapper|false The parent template or false if there is no parent */ - public function getParent(array $context) + public function getParent(array $context): self|TemplateWrapper|false { if (null !== $this->parent) { return $this->parent; @@ -122,7 +122,7 @@ public function isTraitable(): bool * @param array $context The context * @param array $blocks The current set of blocks */ - public function displayParentBlock($name, array $context, array $blocks = []) + public function displayParentBlock($name, array $context, array $blocks = []): void { foreach ($this->yieldParentBlock($name, $context, $blocks) as $data) { echo $data; @@ -140,7 +140,7 @@ public function displayParentBlock($name, array $context, array $blocks = []) * @param array $blocks The current set of blocks * @param bool $useBlocks Whether to use the current set of blocks */ - public function displayBlock($name, array $context, array $blocks = [], $useBlocks = true, ?self $templateContext = null) + public function displayBlock($name, array $context, array $blocks = [], $useBlocks = true, ?self $templateContext = null): void { foreach ($this->yieldBlock($name, $context, $blocks, $useBlocks, $templateContext) as $data) { echo $data; @@ -159,7 +159,7 @@ public function displayBlock($name, array $context, array $blocks = [], $useBloc * * @return string The rendered block */ - public function renderParentBlock($name, array $context, array $blocks = []) + public function renderParentBlock($name, array $context, array $blocks = []): string { if (!$this->useYield) { if ($this->env->isDebug()) { @@ -193,7 +193,7 @@ public function renderParentBlock($name, array $context, array $blocks = []) * * @return string The rendered block */ - public function renderBlock($name, array $context, array $blocks = [], $useBlocks = true) + public function renderBlock($name, array $context, array $blocks = [], $useBlocks = true): string { if (!$this->useYield) { $level = ob_get_level(); @@ -235,7 +235,7 @@ public function renderBlock($name, array $context, array $blocks = [], $useBlock * * @return bool true if the block exists, false otherwise */ - public function hasBlock($name, array $context, array $blocks = []) + public function hasBlock($name, array $context, array $blocks = []): bool { if (isset($blocks[$name])) { return $blocks[$name][0] instanceof self; @@ -261,9 +261,9 @@ public function hasBlock($name, array $context, array $blocks = []) * @param array $context The context * @param array $blocks The current set of blocks * - * @return array An array of block names + * @return array An array of block names */ - public function getBlockNames(array $context, array $blocks = []) + public function getBlockNames(array $context, array $blocks = []): array { $names = array_merge(array_keys($blocks), array_keys($this->blocks)); @@ -276,10 +276,8 @@ public function getBlockNames(array $context, array $blocks = []) /** * @param string|TemplateWrapper|array $template - * - * @return self|TemplateWrapper */ - protected function loadTemplate($template, $templateName = null, $line = null, $index = null) + protected function loadTemplate($template, $templateName = null, $line = null, $index = null): self|TemplateWrapper { try { if (\is_array($template)) { @@ -327,10 +325,8 @@ protected function loadTemplate($template, $templateName = null, $line = null, $ /** * @internal - * - * @return self */ - public function unwrap() + public function unwrap(): self { return $this; } @@ -343,7 +339,7 @@ public function unwrap() * * @return array An array of blocks */ - public function getBlocks() + public function getBlocks(): array { return $this->blocks; } @@ -418,7 +414,7 @@ public function yield(array $context, array $blocks = []): iterable /** * @return iterable */ - public function yieldBlock($name, array $context, array $blocks = [], $useBlocks = true, ?self $templateContext = null) + public function yieldBlock($name, array $context, array $blocks = [], $useBlocks = true, ?self $templateContext = null): iterable { if ($useBlocks && isset($blocks[$name])) { $template = $blocks[$name][0]; @@ -478,7 +474,7 @@ public function yieldBlock($name, array $context, array $blocks = [], $useBlocks * * @return iterable */ - public function yieldParentBlock($name, array $context, array $blocks = []) + public function yieldParentBlock($name, array $context, array $blocks = []): iterable { if (isset($this->traits[$name])) { yield from $this->traits[$name][0]->yieldBlock($name, $context, $blocks, false); @@ -494,6 +490,8 @@ public function yieldParentBlock($name, array $context, array $blocks = []) * * @param array $context An array of parameters to pass to the template * @param array $blocks An array of blocks to pass to the template + + * @return iterable */ - abstract protected function doDisplay(array $context, array $blocks = []); + abstract protected function doDisplay(array $context, array $blocks = []): iterable; } diff --git a/tests/Node/BlockTest.php b/tests/Node/BlockTest.php index 0938e74a180..02de54b4a60 100644 --- a/tests/Node/BlockTest.php +++ b/tests/Node/BlockTest.php @@ -33,7 +33,10 @@ public function getTests() $tests = []; $tests[] = [new BlockNode('foo', new TextNode('foo', 1), 1), << + */ +public function block_foo(array \$context, array \$blocks = []): iterable { \$macros = \$this->macros; yield "foo"; diff --git a/tests/Node/ModuleTest.php b/tests/Node/ModuleTest.php index f3081dff9a8..0b7dd5ddb7a 100644 --- a/tests/Node/ModuleTest.php +++ b/tests/Node/ModuleTest.php @@ -98,7 +98,7 @@ public function __construct(Environment \$env) ]; } - protected function doDisplay(array \$context, array \$blocks = []) + protected function doDisplay(array \$context, array \$blocks = []): iterable { \$macros = \$this->macros; // line 1 @@ -178,7 +178,7 @@ protected function doGetParent(array \$context): bool|string|Template|TemplateWr return "layout.twig"; } - protected function doDisplay(array \$context, array \$blocks = []) + protected function doDisplay(array \$context, array \$blocks = []): iterable { \$macros = \$this->macros; // line 2 @@ -273,7 +273,7 @@ protected function doGetParent(array \$context): bool|string|Template|TemplateWr return \$this->loadTemplate(((true) ? ("foo") : ("foo")), "foo.twig", 2); } - protected function doDisplay(array \$context, array \$blocks = []) + protected function doDisplay(array \$context, array \$blocks = []): iterable { \$macros = \$this->macros; // line 4 diff --git a/tests/TemplateTest.php b/tests/TemplateTest.php index 884226e0a62..2756c5774ec 100644 --- a/tests/TemplateTest.php +++ b/tests/TemplateTest.php @@ -464,7 +464,7 @@ protected function doGetParent(array $context): bool|string|Template|TemplateWra return false; } - protected function doDisplay(array $context, array $blocks = []) + protected function doDisplay(array $context, array $blocks = []): iterable { } From 938ee704b42a66e85be2bc626345fa71584f7594 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Fri, 30 Aug 2024 16:22:35 +0200 Subject: [PATCH 411/812] Fix CS --- src/Template.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Template.php b/src/Template.php index ec9db9bf66a..2dc05686dd5 100644 --- a/src/Template.php +++ b/src/Template.php @@ -490,7 +490,7 @@ public function yieldParentBlock($name, array $context, array $blocks = []): ite * * @param array $context An array of parameters to pass to the template * @param array $blocks An array of blocks to pass to the template - + * * @return iterable */ abstract protected function doDisplay(array $context, array $blocks = []): iterable; From 7c3c16154ea1e018e442312e25b50dfe52886da5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Andr=C3=A9?= Date: Mon, 26 Aug 2024 23:10:00 +0200 Subject: [PATCH 412/812] Adjust `cycle` implementation --- doc/functions/cycle.rst | 10 +++-- src/Extension/CoreExtension.php | 33 ++++++++++----- tests/Extension/CoreTest.php | 40 +++++++++++++++++++ tests/Fixtures/functions/cycle.test | 22 +++++----- .../functions/cycle_empty_mapping.test | 2 +- .../functions/cycle_empty_sequence.test | 2 +- 6 files changed, 84 insertions(+), 25 deletions(-) diff --git a/doc/functions/cycle.rst b/doc/functions/cycle.rst index 3b6db61c431..8b159e1e526 100644 --- a/doc/functions/cycle.rst +++ b/doc/functions/cycle.rst @@ -1,7 +1,7 @@ ``cycle`` ========= -The ``cycle`` function cycles on a sequence or mapping: +The ``cycle`` function cycles on a sequence: .. code-block:: twig @@ -23,7 +23,9 @@ The ``cycle`` function cycles on a sequence or mapping: #} -The array can contain any number of values: +The ``cycle`` function takes two arguments: the ``sequence`` to cycle through and the ``position`` in the sequence. + +The ``sequence`` must be non-empty and can contain any number of values: .. code-block:: twig @@ -52,5 +54,5 @@ The array can contain any number of values: Arguments --------- -* ``values``: The list of values to cycle on -* ``position``: The cycle position +* ``values``: The sequence to cycle on +* ``position``: The position in the sequence diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index e8cf5f6f206..fa2d90c450b 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -330,26 +330,39 @@ public function getOperators(): array } /** - * Cycles over a value. + * Cycles over a sequence. * - * @param \ArrayAccess|array $values - * @param int $position The cycle position + * @param array|\ArrayAccess $values A non-empty sequence of values + * @param positive-int $position The position of the value to return in the cycle * - * @return string The next value in the cycle + * @return mixed The value at the given position in the sequence, wrapping around as needed * * @internal */ - public static function cycle($values, $position): string + public static function cycle($values, $position): mixed { - if (!\is_array($values) && !$values instanceof \ArrayAccess) { - return $values; + if (!\is_array($values)) { + if (!$values instanceof \ArrayAccess) { + throw new RuntimeError('The "cycle" function expects an array or "ArrayAccess" as first argument.'); + } + + if (!\is_countable($values)) { + // To be uncommented in 4.0 + // throw new RuntimeError('The "cycle" function expects a countable sequence as first argument.'); + + trigger_deprecation('twig/twig', '3.12', 'Passing a non-countable sequence of values to "%s()" is deprecated.', __METHOD__); + + return $values; + } + + $values = self::toArray($values, false); } - if (!\count($values)) { - throw new RuntimeError('The "cycle" function does not work on empty sequences/mappings.'); + if (!$count = \count($values)) { + throw new RuntimeError('The "cycle" function does not work on empty sequences.'); } - return $values[$position % \count($values)]; + return $values[$position % $count]; } /** diff --git a/tests/Extension/CoreTest.php b/tests/Extension/CoreTest.php index 31458628115..f61980bf987 100644 --- a/tests/Extension/CoreTest.php +++ b/tests/Extension/CoreTest.php @@ -17,6 +17,46 @@ class CoreTest extends TestCase { + /** + * @dataProvider provideCycleCases + */ + public function testCycleFunction($values, $position, $expected) + { + $this->assertSame($expected, CoreExtension::cycle($values, $position)); + } + + public static function provideCycleCases() + { + return [ + [[1, 2, 3], 0, 1], + [[1, 2, 3], 1, 2], + [[1, 2, 3], 2, 3], + [[1, 2, 3], 3, 1], + [[false, 0, null], 0, false], + [[false, 0, null], 1, 0], + [[false, 0, null], 2, null], + + [[['a', 'b'], ['c', 'd']], 3, ['c', 'd']], + ]; + } + + /** + * @dataProvider provideCycleInvalidCases + */ + public function testCycleFunctionThrowRuntimeError($values, mixed $position = null) + { + $this->expectException(RuntimeError::class); + CoreExtension::cycle($values, $position ?? 0); + } + + public static function provideCycleInvalidCases() + { + return [ + 'empty' => [[]], + 'non-countable' => [new class extends \ArrayObject{}], + ]; + } + /** * @dataProvider getRandomFunctionTestData */ diff --git a/tests/Fixtures/functions/cycle.test b/tests/Fixtures/functions/cycle.test index 0ac6dccd3ae..e54f433187f 100644 --- a/tests/Fixtures/functions/cycle.test +++ b/tests/Fixtures/functions/cycle.test @@ -2,15 +2,19 @@ "cycle" function --TEMPLATE-- {% for i in 0..6 %} -{{ cycle(array1, i) }}-{{ cycle(array2, i) }} +{{ cycle(array1, i) }}-{{ cycle(array2, i) }}-{{ cycle(array3, i) }} {% endfor %} --DATA-- -return ['array1' => ['odd', 'even'], 'array2' => ['apple', 'orange', 'citrus']] +return [ + 'array1' => ['odd', 'even'], + 'array2' => ['apple', 'orange', 'citrus'], + 'array3' => [1, 2, false, null], +]; --EXPECT-- -odd-apple -even-orange -odd-citrus -even-apple -odd-orange -even-citrus -odd-apple +odd-apple-1 +even-orange-2 +odd-citrus- +even-apple- +odd-orange-1 +even-citrus-2 +odd-apple- diff --git a/tests/Fixtures/functions/cycle_empty_mapping.test b/tests/Fixtures/functions/cycle_empty_mapping.test index 6296c2c39ff..ca241d8f312 100644 --- a/tests/Fixtures/functions/cycle_empty_mapping.test +++ b/tests/Fixtures/functions/cycle_empty_mapping.test @@ -5,4 +5,4 @@ --DATA-- return [] --EXCEPTION-- -Twig\Error\RuntimeError: The "cycle" function does not work on empty sequences/mappings in "index.twig" at line 2. +Twig\Error\RuntimeError: The "cycle" function does not work on empty sequences in "index.twig" at line 2. diff --git a/tests/Fixtures/functions/cycle_empty_sequence.test b/tests/Fixtures/functions/cycle_empty_sequence.test index 01d9fe127ac..846913b2004 100644 --- a/tests/Fixtures/functions/cycle_empty_sequence.test +++ b/tests/Fixtures/functions/cycle_empty_sequence.test @@ -5,4 +5,4 @@ --DATA-- return [] --EXCEPTION-- -Twig\Error\RuntimeError: The "cycle" function does not work on empty sequences/mappings in "index.twig" at line 2. +Twig\Error\RuntimeError: The "cycle" function does not work on empty sequences in "index.twig" at line 2. From a16910e14d446264ccc2609d0ce7984b2520c0f5 Mon Sep 17 00:00:00 2001 From: Ruud Kamphuis Date: Sun, 1 Sep 2024 08:53:55 +0200 Subject: [PATCH 413/812] Only unset `_iterated` when there is an `else` node Same as #4249 Even though PHP does not complain, PHPStan does. > Cannot unset offset '_iterated' on array{ ... the context values ... } Since this is compiled code, we can easily produce a bit more valid code in the eyes of PHPStan. --- src/Node/ForNode.php | 5 ++++- tests/Node/ForTest.php | 8 ++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/Node/ForNode.php b/src/Node/ForNode.php index 122063105b5..2fc014792ab 100644 --- a/src/Node/ForNode.php +++ b/src/Node/ForNode.php @@ -101,7 +101,10 @@ public function compile(Compiler $compiler): void $compiler->write("\$_parent = \$context['_parent'];\n"); // remove some "private" loop variables (needed for nested loops) - $compiler->write('unset($context[\'_seq\'], $context[\'_iterated\'], $context[\''.$this->getNode('key_target')->getAttribute('name').'\'], $context[\''.$this->getNode('value_target')->getAttribute('name').'\'], $context[\'_parent\']'); + $compiler->write('unset($context[\'_seq\'], $context[\''.$this->getNode('key_target')->getAttribute('name').'\'], $context[\''.$this->getNode('value_target')->getAttribute('name').'\'], $context[\'_parent\']'); + if ($this->hasNode('else')) { + $compiler->raw(', $context[\'_iterated\']'); + } if ($this->getAttribute('with_loop')) { $compiler->raw(', $context[\'loop\']'); } diff --git a/tests/Node/ForTest.php b/tests/Node/ForTest.php index 5d59ae80471..eb17c6e913a 100644 --- a/tests/Node/ForTest.php +++ b/tests/Node/ForTest.php @@ -62,7 +62,7 @@ public function getTests() yield {$this->getVariableGetter('foo')}; } \$_parent = \$context['_parent']; -unset(\$context['_seq'], \$context['_iterated'], \$context['key'], \$context['item'], \$context['_parent']); +unset(\$context['_seq'], \$context['key'], \$context['item'], \$context['_parent']); \$context = array_intersect_key(\$context, \$_parent) + \$_parent; EOF ]; @@ -104,7 +104,7 @@ public function getTests() } } \$_parent = \$context['_parent']; -unset(\$context['_seq'], \$context['_iterated'], \$context['k'], \$context['v'], \$context['_parent'], \$context['loop']); +unset(\$context['_seq'], \$context['k'], \$context['v'], \$context['_parent'], \$context['loop']); \$context = array_intersect_key(\$context, \$_parent) + \$_parent; EOF ]; @@ -146,7 +146,7 @@ public function getTests() } } \$_parent = \$context['_parent']; -unset(\$context['_seq'], \$context['_iterated'], \$context['k'], \$context['v'], \$context['_parent'], \$context['loop']); +unset(\$context['_seq'], \$context['k'], \$context['v'], \$context['_parent'], \$context['loop']); \$context = array_intersect_key(\$context, \$_parent) + \$_parent; EOF ]; @@ -193,7 +193,7 @@ public function getTests() yield {$this->getVariableGetter('foo')}; } \$_parent = \$context['_parent']; -unset(\$context['_seq'], \$context['_iterated'], \$context['k'], \$context['v'], \$context['_parent'], \$context['loop']); +unset(\$context['_seq'], \$context['k'], \$context['v'], \$context['_parent'], \$context['_iterated'], \$context['loop']); \$context = array_intersect_key(\$context, \$_parent) + \$_parent; EOF ]; From 031c7bda4d39bd87aa8fb1a416d1e4849576c97a Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Wed, 28 Aug 2024 14:04:21 +0200 Subject: [PATCH 414/812] Allow extra extensions to use Twig 4 --- extra/cache-extra/composer.json | 2 +- extra/cssinliner-extra/composer.json | 2 +- extra/html-extra/composer.json | 2 +- extra/inky-extra/composer.json | 2 +- extra/intl-extra/composer.json | 2 +- extra/markdown-extra/composer.json | 2 +- extra/string-extra/composer.json | 2 +- extra/twig-extra-bundle/composer.json | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/extra/cache-extra/composer.json b/extra/cache-extra/composer.json index 74e377e9525..ab354ddc9c1 100644 --- a/extra/cache-extra/composer.json +++ b/extra/cache-extra/composer.json @@ -17,7 +17,7 @@ "require": { "php": ">=8.0.2", "symfony/cache": "^5.4|^6.4|^7.0", - "twig/twig": "^3.12" + "twig/twig": "^3.12|^4.0" }, "require-dev": { "symfony/phpunit-bridge": "^6.4|^7.0" diff --git a/extra/cssinliner-extra/composer.json b/extra/cssinliner-extra/composer.json index 66b4ace78e5..96e59f2ff14 100644 --- a/extra/cssinliner-extra/composer.json +++ b/extra/cssinliner-extra/composer.json @@ -18,7 +18,7 @@ "php": ">=8.0.2", "symfony/deprecation-contracts": "^2.5|^3", "tijsverkoyen/css-to-inline-styles": "^2.0", - "twig/twig": "^3.0" + "twig/twig": "^3.0|^4.0" }, "require-dev": { "symfony/phpunit-bridge": "^6.4|^7.0" diff --git a/extra/html-extra/composer.json b/extra/html-extra/composer.json index a5b65d00bce..46ec29e696f 100644 --- a/extra/html-extra/composer.json +++ b/extra/html-extra/composer.json @@ -18,7 +18,7 @@ "php": ">=8.0.2", "symfony/deprecation-contracts": "^2.5|^3", "symfony/mime": "^5.4|^6.4|^7.0", - "twig/twig": "^3.0" + "twig/twig": "^3.0|^4.0" }, "require-dev": { "symfony/phpunit-bridge": "^6.4|^7.0" diff --git a/extra/inky-extra/composer.json b/extra/inky-extra/composer.json index 77d83e2ff92..c4192d420c5 100644 --- a/extra/inky-extra/composer.json +++ b/extra/inky-extra/composer.json @@ -18,7 +18,7 @@ "php": ">=8.0.2", "symfony/deprecation-contracts": "^2.5|^3", "lorenzo/pinky": "^1.0.5", - "twig/twig": "^3.0" + "twig/twig": "^3.0|^4.0" }, "require-dev": { "symfony/phpunit-bridge": "^6.4|^7.0" diff --git a/extra/intl-extra/composer.json b/extra/intl-extra/composer.json index 54b3f8956d9..0c5ba1cc012 100644 --- a/extra/intl-extra/composer.json +++ b/extra/intl-extra/composer.json @@ -16,7 +16,7 @@ ], "require": { "php": ">=8.0.2", - "twig/twig": "^3.10", + "twig/twig": "^3.10|^4.0", "symfony/intl": "^5.4|^6.4|^7.0" }, "require-dev": { diff --git a/extra/markdown-extra/composer.json b/extra/markdown-extra/composer.json index 8dfe2fa844f..91101e2c1ee 100644 --- a/extra/markdown-extra/composer.json +++ b/extra/markdown-extra/composer.json @@ -17,7 +17,7 @@ "require": { "php": ">=8.0.2", "symfony/deprecation-contracts": "^2.5|^3", - "twig/twig": "^3.0" + "twig/twig": "^3.0|^4.0" }, "require-dev": { "symfony/phpunit-bridge": "^6.4|^7.0", diff --git a/extra/string-extra/composer.json b/extra/string-extra/composer.json index 321394691e1..d6c0bfa6467 100644 --- a/extra/string-extra/composer.json +++ b/extra/string-extra/composer.json @@ -18,7 +18,7 @@ "php": ">=8.0.2", "symfony/string": "^5.4|^6.4|^7.0", "symfony/translation-contracts": "^1.1|^2|^3", - "twig/twig": "^3.0" + "twig/twig": "^3.0|^4.0" }, "require-dev": { "symfony/phpunit-bridge": "^6.4|^7.0" diff --git a/extra/twig-extra-bundle/composer.json b/extra/twig-extra-bundle/composer.json index 15e365a3878..d6047277a25 100644 --- a/extra/twig-extra-bundle/composer.json +++ b/extra/twig-extra-bundle/composer.json @@ -18,7 +18,7 @@ "php": ">=8.0.2", "symfony/framework-bundle": "^5.4|^6.4|^7.0", "symfony/twig-bundle": "^5.4|^6.4|^7.0", - "twig/twig": "^3.0" + "twig/twig": "^3.0|^4.0" }, "require-dev": { "league/commonmark": "^1.0|^2.0", From 976cea04035074f0481bfc62e9ec70321b6dbe20 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sun, 1 Sep 2024 22:31:33 +0200 Subject: [PATCH 415/812] Fix tests --- extra/cache-extra/CacheExtension.php | 2 +- extra/cache-extra/Tests/FunctionalTest.php | 6 ++---- extra/cache-extra/Tests/IntegrationTest.php | 6 ++---- extra/cssinliner-extra/CssInlinerExtension.php | 2 +- extra/inky-extra/InkyExtension.php | 2 +- extra/intl-extra/IntlExtension.php | 4 ++-- extra/markdown-extra/MarkdownExtension.php | 2 +- extra/markdown-extra/Tests/FunctionalTest.php | 8 +++----- extra/string-extra/StringExtension.php | 2 +- 9 files changed, 14 insertions(+), 20 deletions(-) diff --git a/extra/cache-extra/CacheExtension.php b/extra/cache-extra/CacheExtension.php index 5cf849dc634..3898b6e0f50 100644 --- a/extra/cache-extra/CacheExtension.php +++ b/extra/cache-extra/CacheExtension.php @@ -16,7 +16,7 @@ final class CacheExtension extends AbstractExtension { - public function getTokenParsers() + public function getTokenParsers(): array { return [ new CacheTokenParser(), diff --git a/extra/cache-extra/Tests/FunctionalTest.php b/extra/cache-extra/Tests/FunctionalTest.php index 0ae24436ec5..a91858c9175 100644 --- a/extra/cache-extra/Tests/FunctionalTest.php +++ b/extra/cache-extra/Tests/FunctionalTest.php @@ -78,11 +78,9 @@ public function __construct(CacheInterface $cache) $this->cache = $cache; } - public function load($class) + public function load(string $class): ?object { - if (CacheRuntime::class === $class) { - return new CacheRuntime($this->cache); - } + return CacheRuntime::class === $class ? new CacheRuntime($this->cache) : null; } }); diff --git a/extra/cache-extra/Tests/IntegrationTest.php b/extra/cache-extra/Tests/IntegrationTest.php index 8e216aaa51e..4f597b0aa1f 100644 --- a/extra/cache-extra/Tests/IntegrationTest.php +++ b/extra/cache-extra/Tests/IntegrationTest.php @@ -30,11 +30,9 @@ protected function getRuntimeLoaders() { return [ new class() implements RuntimeLoaderInterface { - public function load($class) + public function load(string $class): ?object { - if (CacheRuntime::class === $class) { - return new CacheRuntime(new ArrayAdapter()); - } + return CacheRuntime::class === $class ? new CacheRuntime(new ArrayAdapter()) : null; } }, ]; diff --git a/extra/cssinliner-extra/CssInlinerExtension.php b/extra/cssinliner-extra/CssInlinerExtension.php index 2ceb2e08598..94d3c4b7f4d 100644 --- a/extra/cssinliner-extra/CssInlinerExtension.php +++ b/extra/cssinliner-extra/CssInlinerExtension.php @@ -17,7 +17,7 @@ class CssInlinerExtension extends AbstractExtension { - public function getFilters() + public function getFilters(): array { return [ new TwigFilter('inline_css', [self::class, 'inlineCss'], ['is_safe' => ['all']]), diff --git a/extra/inky-extra/InkyExtension.php b/extra/inky-extra/InkyExtension.php index 374cb7efbc5..9ee4f823abc 100644 --- a/extra/inky-extra/InkyExtension.php +++ b/extra/inky-extra/InkyExtension.php @@ -17,7 +17,7 @@ class InkyExtension extends AbstractExtension { - public function getFilters() + public function getFilters(): array { return [ new TwigFilter('inky_to_html', [self::class, 'inky'], ['is_safe' => ['html']]), diff --git a/extra/intl-extra/IntlExtension.php b/extra/intl-extra/IntlExtension.php index 7278db21400..43fd1c66e55 100644 --- a/extra/intl-extra/IntlExtension.php +++ b/extra/intl-extra/IntlExtension.php @@ -156,7 +156,7 @@ public function __construct(?\IntlDateFormatter $dateFormatterPrototype = null, $this->numberFormatterPrototype = $numberFormatterPrototype; } - public function getFilters() + public function getFilters(): array { return [ // internationalized names @@ -177,7 +177,7 @@ public function getFilters() ]; } - public function getFunctions() + public function getFunctions(): array { return [ // internationalized names diff --git a/extra/markdown-extra/MarkdownExtension.php b/extra/markdown-extra/MarkdownExtension.php index 6a245009556..7bc737a29f9 100644 --- a/extra/markdown-extra/MarkdownExtension.php +++ b/extra/markdown-extra/MarkdownExtension.php @@ -17,7 +17,7 @@ final class MarkdownExtension extends AbstractExtension { - public function getFilters() + public function getFilters(): array { return [ new TwigFilter('markdown_to_html', ['Twig\\Extra\\Markdown\\MarkdownRuntime', 'convert'], ['is_safe' => ['all']]), diff --git a/extra/markdown-extra/Tests/FunctionalTest.php b/extra/markdown-extra/Tests/FunctionalTest.php index 0d9b73a59d0..62c7928feec 100644 --- a/extra/markdown-extra/Tests/FunctionalTest.php +++ b/extra/markdown-extra/Tests/FunctionalTest.php @@ -27,7 +27,7 @@ class FunctionalTest extends TestCase /** * @dataProvider getMarkdownTests */ - public function testMarkdown(string $template, string $expected): void + public function testMarkdown(string $template, string $expected) { foreach ([LeagueMarkdown::class, ErusevMarkdown::class, /* MichelfMarkdown::class, */ DefaultMarkdown::class] as $class) { $twig = new Environment(new ArrayLoader([ @@ -48,11 +48,9 @@ public function __construct(string $class) $this->class = $class; } - public function load($c) + public function load(string $c): ?object { - if (MarkdownRuntime::class === $c) { - return new $c(new $this->class()); - } + return MarkdownRuntime::class === $c ? new $c(new $this->class()) : null; } }); $this->assertMatchesRegularExpression('{'.$expected.'}m', trim($twig->render('index'))); diff --git a/extra/string-extra/StringExtension.php b/extra/string-extra/StringExtension.php index e0abb845f9f..bd575f788c6 100644 --- a/extra/string-extra/StringExtension.php +++ b/extra/string-extra/StringExtension.php @@ -32,7 +32,7 @@ public function __construct(?SluggerInterface $slugger = null) $this->slugger = $slugger ?: new AsciiSlugger(); } - public function getFilters() + public function getFilters(): array { return [ new TwigFilter('u', [$this, 'createUnicodeString']), From 1e5dea44f954f5f495ff7e5133f4aa91b0b7dd93 Mon Sep 17 00:00:00 2001 From: "Alexander M. Turek" Date: Mon, 2 Sep 2024 18:18:00 +0200 Subject: [PATCH 416/812] Fix MacroTest --- tests/Node/MacroTest.php | 32 +++++++++++++++++++++++++------- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/tests/Node/MacroTest.php b/tests/Node/MacroTest.php index ef36774f7e8..f05adf6364d 100644 --- a/tests/Node/MacroTest.php +++ b/tests/Node/MacroTest.php @@ -36,8 +36,6 @@ public function testConstructor() public function getTests() { - $tests = []; - $arguments = new Node([ 'foo' => new ConstantExpression(null, 1), 'bar' => new ConstantExpression('Foo', 1), @@ -46,7 +44,7 @@ public function getTests() $body = new BodyNode([new TextNode('foo', 1)]); $node = new MacroNode('foo', $body, $arguments, 1); - $text[] = [$node, << [$node, <<env->getCharset()); + })(), false))) ? '' : new Markup(\$tmp, \$this->env->getCharset()); } EOF - , new Environment(new ArrayLoader()), + , new Environment(new ArrayLoader(), ['use_yield' => true]), ]; - return $tests; + yield 'with use_yield = false' => [$node, <<macros; + \$context = \$this->env->mergeGlobals([ + "foo" => \$__foo__, + "bar" => \$__bar__, + "varargs" => \$__varargs__, + ]); + + \$blocks = []; + + return ('' === \$tmp = \\Twig\\Extension\\CoreExtension::captureOutput((function () use (&\$context, \$macros, \$blocks) { + yield "foo"; + return; yield ''; + })())) ? '' : new Markup(\$tmp, \$this->env->getCharset()); +} +EOF + , new Environment(new ArrayLoader(), ['use_yield' => false]), + ]; } } From 3a4d513ffed965a2898375147a0b877c84d9d721 Mon Sep 17 00:00:00 2001 From: "Alexander M. Turek" Date: Mon, 2 Sep 2024 17:04:53 +0200 Subject: [PATCH 417/812] Fix testExtensionsAreNotInitializedWhenRenderingACompiledTemplate --- tests/EnvironmentTest.php | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/tests/EnvironmentTest.php b/tests/EnvironmentTest.php index 9338e78f2f4..a5f082ecb99 100644 --- a/tests/EnvironmentTest.php +++ b/tests/EnvironmentTest.php @@ -178,19 +178,26 @@ public function testExtensionsAreNotInitializedWhenRenderingACompiledTemplate() // force compilation $twig = new Environment($loader = new ArrayLoader(['index' => '{{ foo }}']), $options); + $twig->addExtension($extension = new class extends AbstractExtension { + public bool $throw = false; + + public function getFilters(): array + { + if ($this->throw) { + throw new \RuntimeException('Extension are not supposed to be initialized.'); + } + + return parent::getFilters(); + } + }); $key = $cache->generateKey('index', $twig->getTemplateClass('index')); $cache->write($key, $twig->compileSource(new Source('{{ foo }}', 'index'))); // check that extensions won't be initialized when rendering a template that is already in the cache - $twig = $this - ->getMockBuilder(Environment::class) - ->setConstructorArgs([$loader, $options]) - ->setMethods(['initExtensions']) - ->getMock() - ; - - $twig->expects($this->never())->method('initExtensions'); + $twig = new Environment($loader, $options); + $extension->throw = true; + $twig->addExtension($extension); // render template $output = $twig->render('index', ['foo' => 'bar']); From 09a43a9f6fff2fc703e62e3555d10df0e5beffae Mon Sep 17 00:00:00 2001 From: Ruud Kamphuis Date: Tue, 3 Sep 2024 09:54:27 +0200 Subject: [PATCH 418/812] Replace `return; yield` with `yield from []` This has the same effect, but looks less hacky and makes PHPStan happy. https://phpstan.org/r/df7fcc88-1df8-428e-b675-5dc6965c34d6 Previously this was needed to work properly with output capturing. --- src/Node/BlockNode.php | 2 +- src/Node/CaptureNode.php | 2 +- src/Node/ModuleNode.php | 2 +- tests/Node/BlockTest.php | 2 +- tests/Node/MacroTest.php | 4 ++-- tests/Node/ModuleTest.php | 2 +- tests/Node/SetTest.php | 4 ++-- 7 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Node/BlockNode.php b/src/Node/BlockNode.php index 2ee74a8d2f4..3c06f155be8 100644 --- a/src/Node/BlockNode.php +++ b/src/Node/BlockNode.php @@ -42,7 +42,7 @@ public function compile(Compiler $compiler): void $compiler ->subcompile($this->getNode('body')) - ->write("return; yield '';\n") // needed when body doesn't yield anything + ->write("yield from [];\n") ->outdent() ->write("}\n\n") ; diff --git a/src/Node/CaptureNode.php b/src/Node/CaptureNode.php index 0162113c14e..3b7f0b6d838 100644 --- a/src/Node/CaptureNode.php +++ b/src/Node/CaptureNode.php @@ -39,7 +39,7 @@ public function compile(Compiler $compiler): void ->raw("(function () use (&\$context, \$macros, \$blocks) {\n") ->indent() ->subcompile($this->getNode('body')) - ->write("return; yield '';\n") + ->write("yield from [];\n") ->outdent() ->write('})()') ; diff --git a/src/Node/ModuleNode.php b/src/Node/ModuleNode.php index 264a0e67fbe..b57e643e852 100644 --- a/src/Node/ModuleNode.php +++ b/src/Node/ModuleNode.php @@ -353,7 +353,7 @@ protected function compileDisplay(Compiler $compiler) $compiler->subcompile($this->getNode('display_end')); if (!$this->hasNode('parent')) { - $compiler->write("return; yield '';\n"); // ensure at least one yield call even for templates with no output + $compiler->write("yield from [];\n"); } $compiler diff --git a/tests/Node/BlockTest.php b/tests/Node/BlockTest.php index 02de54b4a60..06405d1c3da 100644 --- a/tests/Node/BlockTest.php +++ b/tests/Node/BlockTest.php @@ -40,7 +40,7 @@ public function block_foo(array \$context, array \$blocks = []): iterable { \$macros = \$this->macros; yield "foo"; - return; yield ''; + yield from []; } EOF , new Environment(new ArrayLoader()), diff --git a/tests/Node/MacroTest.php b/tests/Node/MacroTest.php index f05adf6364d..405046b37fd 100644 --- a/tests/Node/MacroTest.php +++ b/tests/Node/MacroTest.php @@ -59,7 +59,7 @@ public function macro_foo(\$__foo__ = null, \$__bar__ = "Foo", ...\$__varargs__) return ('' === \$tmp = implode('', iterator_to_array((function () use (&\$context, \$macros, \$blocks) { yield "foo"; - return; yield ''; + yield from []; })(), false))) ? '' : new Markup(\$tmp, \$this->env->getCharset()); } EOF @@ -81,7 +81,7 @@ public function macro_foo(\$__foo__ = null, \$__bar__ = "Foo", ...\$__varargs__) return ('' === \$tmp = \\Twig\\Extension\\CoreExtension::captureOutput((function () use (&\$context, \$macros, \$blocks) { yield "foo"; - return; yield ''; + yield from []; })())) ? '' : new Markup(\$tmp, \$this->env->getCharset()); } EOF diff --git a/tests/Node/ModuleTest.php b/tests/Node/ModuleTest.php index 0b7dd5ddb7a..10c4d7fa558 100644 --- a/tests/Node/ModuleTest.php +++ b/tests/Node/ModuleTest.php @@ -103,7 +103,7 @@ protected function doDisplay(array \$context, array \$blocks = []): iterable \$macros = \$this->macros; // line 1 yield "foo"; - return; yield ''; + yield from []; } /** diff --git a/tests/Node/SetTest.php b/tests/Node/SetTest.php index f250b80eed3..9bb93a367de 100644 --- a/tests/Node/SetTest.php +++ b/tests/Node/SetTest.php @@ -57,7 +57,7 @@ public function getTests() // line 1 \$context["foo"] = ('' === \$tmp = implode('', iterator_to_array((function () use (&\$context, \$macros, \$blocks) { yield "foo"; - return; yield ''; + yield from []; })(), false))) ? '' : new Markup(\$tmp, \$this->env->getCharset()); EOF , new Environment(new ArrayLoader()), @@ -67,7 +67,7 @@ public function getTests() // line 1 $context["foo"] = ('' === $tmp = \Twig\Extension\CoreExtension::captureOutput((function () use (&$context, $macros, $blocks) { yield "foo"; - return; yield ''; + yield from []; })())) ? '' : new Markup($tmp, $this->env->getCharset()); EOF , new Environment(new ArrayLoader()), From 1e87185d262c33184d590d1f301935707fb7f927 Mon Sep 17 00:00:00 2001 From: "Alexander M. Turek" Date: Tue, 3 Sep 2024 10:17:14 +0200 Subject: [PATCH 419/812] Fix Xdebug detection --- phpunit.xml.dist | 1 - src/Extension/DebugExtension.php | 6 ++---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 24d5bd9ea45..0caf8dad519 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -8,7 +8,6 @@
    - diff --git a/src/Extension/DebugExtension.php b/src/Extension/DebugExtension.php index cefb44c5b8d..dac21c31797 100644 --- a/src/Extension/DebugExtension.php +++ b/src/Extension/DebugExtension.php @@ -22,10 +22,8 @@ public function getFunctions(): array { // dump is safe if var_dump is overridden by xdebug $isDumpOutputHtmlSafe = \extension_loaded('xdebug') - // false means that it was not set (and the default is on) or it explicitly enabled - && (false === \ini_get('xdebug.overload_var_dump') || \ini_get('xdebug.overload_var_dump')) - // false means that it was not set (and the default is on) or it explicitly enabled - // xdebug.overload_var_dump produces HTML only when html_errors is also enabled + // Xdebug overloads var_dump in develop mode when html_errors is enabled + && str_contains(\ini_get('xdebug.mode'), 'develop') && (false === \ini_get('html_errors') || \ini_get('html_errors')) || 'cli' === \PHP_SAPI ; From f555a33cafb30499f1de0bbb8cbdcc19435a9931 Mon Sep 17 00:00:00 2001 From: "Alexander M. Turek" Date: Mon, 2 Sep 2024 21:29:30 +0200 Subject: [PATCH 420/812] Migrate NodeTestCase to static data providers --- CHANGELOG | 4 ++ doc/deprecated.rst | 16 +++++ src/Test/NodeTestCase.php | 61 +++++++++++++++-- tests/Node/AutoEscapeTest.php | 2 +- tests/Node/BlockReferenceTest.php | 2 +- tests/Node/BlockTest.php | 2 +- tests/Node/DeprecatedTest.php | 5 +- tests/Node/DoTest.php | 2 +- tests/Node/Expression/ArrayTest.php | 2 +- tests/Node/Expression/AssignNameTest.php | 2 +- tests/Node/Expression/Binary/AddTest.php | 2 +- tests/Node/Expression/Binary/AndTest.php | 2 +- tests/Node/Expression/Binary/ConcatTest.php | 2 +- tests/Node/Expression/Binary/DivTest.php | 2 +- tests/Node/Expression/Binary/FloorDivTest.php | 2 +- tests/Node/Expression/Binary/ModTest.php | 2 +- tests/Node/Expression/Binary/MulTest.php | 2 +- tests/Node/Expression/Binary/OrTest.php | 2 +- tests/Node/Expression/Binary/SubTest.php | 2 +- tests/Node/Expression/ConditionalTest.php | 2 +- tests/Node/Expression/ConstantTest.php | 2 +- tests/Node/Expression/Filter/RawTest.php | 2 +- tests/Node/Expression/FilterTest.php | 65 ++++++++----------- tests/Node/Expression/FunctionTest.php | 38 +++++------ tests/Node/Expression/GetAttrTest.php | 6 +- tests/Node/Expression/NameTest.php | 4 +- tests/Node/Expression/NullCoalesceTest.php | 2 +- tests/Node/Expression/ParentTest.php | 2 +- tests/Node/Expression/TestTest.php | 20 +++--- tests/Node/Expression/Unary/NegTest.php | 2 +- tests/Node/Expression/Unary/NotTest.php | 2 +- tests/Node/Expression/Unary/PosTest.php | 2 +- tests/Node/ForTest.php | 24 ++++--- tests/Node/IfTest.php | 15 +++-- tests/Node/ImportTest.php | 2 +- tests/Node/IncludeTest.php | 2 +- tests/Node/MacroTest.php | 2 +- tests/Node/ModuleTest.php | 4 +- tests/Node/PrintTest.php | 2 +- tests/Node/SandboxTest.php | 2 +- tests/Node/SetTest.php | 18 +++-- tests/Node/TextTest.php | 2 +- tests/Node/TypesTest.php | 14 ++-- 43 files changed, 208 insertions(+), 144 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 86f696fc422..19fe5863ca8 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,10 @@ # 3.13.0 (2024-XX-XX) * Add the `types` tag (experimental) + * Deprecate the `Twig\Test\NodeTestCase::getTests()` data provider, override `provideTests()` instead. + * Mark `Twig\Test\NodeTestCase::getEnvironment()` as final, override `createEnvironment()` instead. + * Deprecate `Twig\Test\NodeTestCase::getVariableGetter()`, call `createVariableGetter()` instead. + * Deprecate `Twig\Test\NodeTestCase::getAttributeGetter()`, call `createAttributeGetter()` instead. # 3.12.0 (2024-08-29) diff --git a/doc/deprecated.rst b/doc/deprecated.rst index affdd2ba924..71a17996f2d 100644 --- a/doc/deprecated.rst +++ b/doc/deprecated.rst @@ -200,3 +200,19 @@ Sandbox * Having the ``extends`` and ``use`` tags allowed by default in a sandbox is deprecated as of Twig 3.12. You will need to explicitly allow them if needed in 4.0. + +Testing Utilities +----------------- + +* Implementing the data provider method ``Twig\Test\NodeTestCase::getTests()`` + is deprecated as of Twig 3.13. Instead, implement the static data provider + ``provideTests()``. + +* In order to make their functionality available for static data providers, the + helper methods ``getVariableGetter()`` and ``getAttributeGetter()`` on + ``Twig\Test\NodeTestCase`` have been deprecated. Call the new methods + ``createVariableGetter()`` and ``createAttributeGetter()`` instead. + +* The method ``Twig\Test\NodeTestCase::getEnvironment()`` is considered final + as of Twig 3.13. If you want to override how the Twig environment is + constructed, override ``createEnvironment()`` instead. diff --git a/src/Test/NodeTestCase.php b/src/Test/NodeTestCase.php index 4046f08cdc9..09b16ace793 100644 --- a/src/Test/NodeTestCase.php +++ b/src/Test/NodeTestCase.php @@ -11,6 +11,8 @@ namespace Twig\Test; +use PHPUnit\Framework\Attributes\BeforeClass; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Twig\Compiler; use Twig\Environment; @@ -24,11 +26,26 @@ abstract class NodeTestCase extends TestCase */ private $currentEnv; - abstract public function getTests(); + public function getTests() + { + return []; + } + + /** + * @return iterable + */ + public static function provideTests(): iterable + { + trigger_deprecation('twig/twig', '3.13', 'Not implementing "%s()" in "%s" is deprecated. This method will be abstract in 4.0.', __METHOD__, static::class); + + return []; + } /** * @dataProvider getTests + * @dataProvider provideTests */ + #[DataProvider('getTests'), DataProvider('provideTests')] public function testCompile($node, $source, $environment = null, $isPattern = false) { $this->assertNodeCompilation($source, $node, $environment, $isPattern); @@ -51,24 +68,58 @@ protected function getCompiler(?Environment $environment = null) return new Compiler($environment ?? $this->getEnvironment()); } + /** + * @final since Twig 3.13 + */ protected function getEnvironment() { - if (!$this->currentEnv) { - $this->currentEnv = new Environment(new ArrayLoader()); - } + return $this->currentEnv ??= static::createEnvironment(); + } - return $this->currentEnv; + protected static function createEnvironment(): Environment + { + return new Environment(new ArrayLoader()); } + /** + * @deprecated since Twig 3.13, use createVariableGetter() instead. + */ protected function getVariableGetter($name, $line = false) + { + trigger_deprecation('twig/twig', '3.13', 'Method "%s()" is deprecated, use "createVariableGetter()" instead.', __METHOD__); + + return self::createVariableGetter($name, $line); + } + + final protected static function createVariableGetter(string $name, bool $line = false): string { $line = $line > 0 ? "// line $line\n" : ''; return \sprintf('%s($context["%s"] ?? null)', $line, $name); } + /** + * @deprecated since Twig 3.13, use createAttributeGetter() instead. + */ protected function getAttributeGetter() + { + trigger_deprecation('twig/twig', '3.13', 'Method "%s()" is deprecated, use "createAttributeGetter()" instead.', __METHOD__); + + return self::createAttributeGetter(); + } + + final protected static function createAttributeGetter(): string { return 'CoreExtension::getAttribute($this->env, $this->source, '; } + + /** @beforeClass */ + #[BeforeClass] + final public static function checkDataProvider(): void + { + $r = new \ReflectionMethod(static::class, 'getTests'); + if ($r->getDeclaringClass()->getName() !== self::class) { + trigger_deprecation('twig/twig', '3.13', 'Implementing "%s::getTests()" in "%s" is deprecated, implement "provideTests()" instead.', self::class, static::class); + } + } } diff --git a/tests/Node/AutoEscapeTest.php b/tests/Node/AutoEscapeTest.php index 9cf18742bb7..546d4345546 100644 --- a/tests/Node/AutoEscapeTest.php +++ b/tests/Node/AutoEscapeTest.php @@ -27,7 +27,7 @@ public function testConstructor() $this->assertTrue($node->getAttribute('value')); } - public function getTests() + public static function provideTests(): iterable { $body = new Node([new TextNode('foo', 1)]); $node = new AutoEscapeNode(true, $body, 1); diff --git a/tests/Node/BlockReferenceTest.php b/tests/Node/BlockReferenceTest.php index 1211ee17b83..dd26baa9428 100644 --- a/tests/Node/BlockReferenceTest.php +++ b/tests/Node/BlockReferenceTest.php @@ -23,7 +23,7 @@ public function testConstructor() $this->assertEquals('foo', $node->getAttribute('name')); } - public function getTests() + public static function provideTests(): iterable { return [ [new BlockReferenceNode('foo', 1), <<<'EOF' diff --git a/tests/Node/BlockTest.php b/tests/Node/BlockTest.php index 02de54b4a60..e6bcbc0b6e6 100644 --- a/tests/Node/BlockTest.php +++ b/tests/Node/BlockTest.php @@ -28,7 +28,7 @@ public function testConstructor() $this->assertEquals('foo', $node->getAttribute('name')); } - public function getTests() + public static function provideTests(): iterable { $tests = []; $tests[] = [new BlockNode('foo', new TextNode('foo', 1), 1), <<assertEquals($expr, $node->getNode('expr')); } - public function getTests() + public static function provideTests(): iterable { $tests = []; @@ -75,7 +76,7 @@ public function getTests() $node->setNode('package', new ConstantExpression('twig/twig', 1)); $node->setNode('version', new ConstantExpression('1.1', 1)); - $compiler = $this->getCompiler($environment); + $compiler = new Compiler($environment); $varName = $compiler->getVarName(); $tests[] = [$node, <<assertEquals($expr, $node->getNode('expr')); } - public function getTests() + public static function provideTests(): iterable { $tests = []; diff --git a/tests/Node/Expression/ArrayTest.php b/tests/Node/Expression/ArrayTest.php index f72eeab757b..bf88c8d097c 100644 --- a/tests/Node/Expression/ArrayTest.php +++ b/tests/Node/Expression/ArrayTest.php @@ -25,7 +25,7 @@ public function testConstructor() $this->assertEquals($foo, $node->getNode('1')); } - public function getTests() + public static function provideTests(): iterable { $elements = [ new ConstantExpression('foo', 1), diff --git a/tests/Node/Expression/AssignNameTest.php b/tests/Node/Expression/AssignNameTest.php index 80dbe94c6c0..3ed8511d88e 100644 --- a/tests/Node/Expression/AssignNameTest.php +++ b/tests/Node/Expression/AssignNameTest.php @@ -23,7 +23,7 @@ public function testConstructor() $this->assertEquals('foo', $node->getAttribute('name')); } - public function getTests() + public static function provideTests(): iterable { $node = new AssignNameExpression('foo', 1); diff --git a/tests/Node/Expression/Binary/AddTest.php b/tests/Node/Expression/Binary/AddTest.php index 5cff2bcff1d..60cfb2ce177 100644 --- a/tests/Node/Expression/Binary/AddTest.php +++ b/tests/Node/Expression/Binary/AddTest.php @@ -27,7 +27,7 @@ public function testConstructor() $this->assertEquals($right, $node->getNode('right')); } - public function getTests() + public static function provideTests(): iterable { $left = new ConstantExpression(1, 1); $right = new ConstantExpression(2, 1); diff --git a/tests/Node/Expression/Binary/AndTest.php b/tests/Node/Expression/Binary/AndTest.php index d83aed04d96..c254f0b37f2 100644 --- a/tests/Node/Expression/Binary/AndTest.php +++ b/tests/Node/Expression/Binary/AndTest.php @@ -27,7 +27,7 @@ public function testConstructor() $this->assertEquals($right, $node->getNode('right')); } - public function getTests() + public static function provideTests(): iterable { $left = new ConstantExpression(1, 1); $right = new ConstantExpression(2, 1); diff --git a/tests/Node/Expression/Binary/ConcatTest.php b/tests/Node/Expression/Binary/ConcatTest.php index 0eff603ba28..2ca0054794e 100644 --- a/tests/Node/Expression/Binary/ConcatTest.php +++ b/tests/Node/Expression/Binary/ConcatTest.php @@ -27,7 +27,7 @@ public function testConstructor() $this->assertEquals($right, $node->getNode('right')); } - public function getTests() + public static function provideTests(): iterable { $left = new ConstantExpression(1, 1); $right = new ConstantExpression(2, 1); diff --git a/tests/Node/Expression/Binary/DivTest.php b/tests/Node/Expression/Binary/DivTest.php index 20cf4646f85..0a999d42b47 100644 --- a/tests/Node/Expression/Binary/DivTest.php +++ b/tests/Node/Expression/Binary/DivTest.php @@ -27,7 +27,7 @@ public function testConstructor() $this->assertEquals($right, $node->getNode('right')); } - public function getTests() + public static function provideTests(): iterable { $left = new ConstantExpression(1, 1); $right = new ConstantExpression(2, 1); diff --git a/tests/Node/Expression/Binary/FloorDivTest.php b/tests/Node/Expression/Binary/FloorDivTest.php index 826859851bb..0b58efdf740 100644 --- a/tests/Node/Expression/Binary/FloorDivTest.php +++ b/tests/Node/Expression/Binary/FloorDivTest.php @@ -27,7 +27,7 @@ public function testConstructor() $this->assertEquals($right, $node->getNode('right')); } - public function getTests() + public static function provideTests(): iterable { $left = new ConstantExpression(1, 1); $right = new ConstantExpression(2, 1); diff --git a/tests/Node/Expression/Binary/ModTest.php b/tests/Node/Expression/Binary/ModTest.php index 2069ef08950..812204a68a1 100644 --- a/tests/Node/Expression/Binary/ModTest.php +++ b/tests/Node/Expression/Binary/ModTest.php @@ -27,7 +27,7 @@ public function testConstructor() $this->assertEquals($right, $node->getNode('right')); } - public function getTests() + public static function provideTests(): iterable { $left = new ConstantExpression(1, 1); $right = new ConstantExpression(2, 1); diff --git a/tests/Node/Expression/Binary/MulTest.php b/tests/Node/Expression/Binary/MulTest.php index c50dfc12b1b..dc18571ad6d 100644 --- a/tests/Node/Expression/Binary/MulTest.php +++ b/tests/Node/Expression/Binary/MulTest.php @@ -27,7 +27,7 @@ public function testConstructor() $this->assertEquals($right, $node->getNode('right')); } - public function getTests() + public static function provideTests(): iterable { $left = new ConstantExpression(1, 1); $right = new ConstantExpression(2, 1); diff --git a/tests/Node/Expression/Binary/OrTest.php b/tests/Node/Expression/Binary/OrTest.php index 94df7c0b165..ee8883852bf 100644 --- a/tests/Node/Expression/Binary/OrTest.php +++ b/tests/Node/Expression/Binary/OrTest.php @@ -27,7 +27,7 @@ public function testConstructor() $this->assertEquals($right, $node->getNode('right')); } - public function getTests() + public static function provideTests(): iterable { $left = new ConstantExpression(1, 1); $right = new ConstantExpression(2, 1); diff --git a/tests/Node/Expression/Binary/SubTest.php b/tests/Node/Expression/Binary/SubTest.php index 04eebe290d6..71118a70a94 100644 --- a/tests/Node/Expression/Binary/SubTest.php +++ b/tests/Node/Expression/Binary/SubTest.php @@ -27,7 +27,7 @@ public function testConstructor() $this->assertEquals($right, $node->getNode('right')); } - public function getTests() + public static function provideTests(): iterable { $left = new ConstantExpression(1, 1); $right = new ConstantExpression(2, 1); diff --git a/tests/Node/Expression/ConditionalTest.php b/tests/Node/Expression/ConditionalTest.php index 004e9c9513e..fb8235dd514 100644 --- a/tests/Node/Expression/ConditionalTest.php +++ b/tests/Node/Expression/ConditionalTest.php @@ -29,7 +29,7 @@ public function testConstructor() $this->assertEquals($expr3, $node->getNode('expr3')); } - public function getTests() + public static function provideTests(): iterable { $tests = []; diff --git a/tests/Node/Expression/ConstantTest.php b/tests/Node/Expression/ConstantTest.php index 920892e942d..bf1be2e4859 100644 --- a/tests/Node/Expression/ConstantTest.php +++ b/tests/Node/Expression/ConstantTest.php @@ -23,7 +23,7 @@ public function testConstructor() $this->assertEquals('foo', $node->getAttribute('value')); } - public function getTests() + public static function provideTests(): iterable { $tests = []; diff --git a/tests/Node/Expression/Filter/RawTest.php b/tests/Node/Expression/Filter/RawTest.php index 558d17f35c9..82d120d6f59 100644 --- a/tests/Node/Expression/Filter/RawTest.php +++ b/tests/Node/Expression/Filter/RawTest.php @@ -28,7 +28,7 @@ public function testConstructor() $this->assertCount(0, $filter->getNode('arguments')); } - public function getTests() + public static function provideTests(): iterable { $node = new RawFilter(new ConstantExpression('foo', 12)); diff --git a/tests/Node/Expression/FilterTest.php b/tests/Node/Expression/FilterTest.php index 996f34e9203..ca792bc4d86 100644 --- a/tests/Node/Expression/FilterTest.php +++ b/tests/Node/Expression/FilterTest.php @@ -23,8 +23,6 @@ class FilterTest extends NodeTestCase { - private $extension = null; - public function testConstructor() { $expr = new ConstantExpression('foo', 1); @@ -37,26 +35,21 @@ public function testConstructor() $this->assertEquals($args, $node->getNode('arguments')); } - protected function tearDown(): void - { - $this->extension = null; - } - - public function getTests() + public static function provideTests(): iterable { - $environment = $this->getEnvironment(); + $environment = static::createEnvironment(); $tests = []; $expr = new ConstantExpression('foo', 1); - $node = $this->createFilter($environment, $expr, 'upper'); - $node = $this->createFilter($environment, $node, 'number_format', [new ConstantExpression(2, 1), new ConstantExpression('.', 1), new ConstantExpression(',', 1)]); + $node = self::createFilter($environment, $expr, 'upper'); + $node = self::createFilter($environment, $node, 'number_format', [new ConstantExpression(2, 1), new ConstantExpression('.', 1), new ConstantExpression(',', 1)]); $tests[] = [$node, '$this->extensions[\'Twig\Extension\CoreExtension\']->formatNumber(Twig\Extension\CoreExtension::upper($this->env->getCharset(), "foo"), 2, ".", ",")']; // named arguments $date = new ConstantExpression(0, 1); - $node = $this->createFilter($environment, $date, 'date', [ + $node = self::createFilter($environment, $date, 'date', [ 'timezone' => new ConstantExpression('America/Chicago', 1), 'format' => new ConstantExpression('d/m/Y H:i:s P', 1), ]); @@ -64,55 +57,55 @@ public function getTests() // skip an optional argument $date = new ConstantExpression(0, 1); - $node = $this->createFilter($environment, $date, 'date', [ + $node = self::createFilter($environment, $date, 'date', [ 'timezone' => new ConstantExpression('America/Chicago', 1), ]); $tests[] = [$node, '$this->extensions[\'Twig\Extension\CoreExtension\']->formatDate(0, null, "America/Chicago")']; // underscores vs camelCase for named arguments $string = new ConstantExpression('abc', 1); - $node = $this->createFilter($environment, $string, 'reverse', [ + $node = self::createFilter($environment, $string, 'reverse', [ 'preserve_keys' => new ConstantExpression(true, 1), ]); $tests[] = [$node, 'Twig\Extension\CoreExtension::reverse($this->env->getCharset(), "abc", true)']; - $node = $this->createFilter($environment, $string, 'reverse', [ + $node = self::createFilter($environment, $string, 'reverse', [ 'preserveKeys' => new ConstantExpression(true, 1), ]); $tests[] = [$node, 'Twig\Extension\CoreExtension::reverse($this->env->getCharset(), "abc", true)']; // filter as an anonymous function - $node = $this->createFilter($environment, new ConstantExpression('foo', 1), 'anonymous'); + $node = self::createFilter($environment, new ConstantExpression('foo', 1), 'anonymous'); $tests[] = [$node, '$this->env->getFilter(\'anonymous\')->getCallable()("foo")']; // needs environment - $node = $this->createFilter($environment, $string, 'bar'); + $node = self::createFilter($environment, $string, 'bar'); $tests[] = [$node, 'Twig\Tests\Node\Expression\twig_tests_filter_dummy($this->env, "abc")', $environment]; - $node = $this->createFilter($environment, $string, 'bar_closure'); + $node = self::createFilter($environment, $string, 'bar_closure'); $tests[] = [$node, twig_tests_filter_dummy::class.'($this->env, "abc")', $environment]; - $node = $this->createFilter($environment, $string, 'bar', [new ConstantExpression('bar', 1)]); + $node = self::createFilter($environment, $string, 'bar', [new ConstantExpression('bar', 1)]); $tests[] = [$node, 'Twig\Tests\Node\Expression\twig_tests_filter_dummy($this->env, "abc", "bar")', $environment]; // arbitrary named arguments - $node = $this->createFilter($environment, $string, 'barbar'); + $node = self::createFilter($environment, $string, 'barbar'); $tests[] = [$node, 'Twig\Tests\Node\Expression\twig_tests_filter_barbar($context, "abc")', $environment]; - $node = $this->createFilter($environment, $string, 'barbar', ['foo' => new ConstantExpression('bar', 1)]); + $node = self::createFilter($environment, $string, 'barbar', ['foo' => new ConstantExpression('bar', 1)]); $tests[] = [$node, 'Twig\Tests\Node\Expression\twig_tests_filter_barbar($context, "abc", null, null, ["foo" => "bar"])', $environment]; - $node = $this->createFilter($environment, $string, 'barbar', ['arg2' => new ConstantExpression('bar', 1)]); + $node = self::createFilter($environment, $string, 'barbar', ['arg2' => new ConstantExpression('bar', 1)]); $tests[] = [$node, 'Twig\Tests\Node\Expression\twig_tests_filter_barbar($context, "abc", null, "bar")', $environment]; if (\PHP_VERSION_ID >= 80111) { - $node = $this->createFilter($environment, $string, 'first_class_callable_static'); + $node = self::createFilter($environment, $string, 'first_class_callable_static'); $tests[] = [$node, 'Twig\Tests\Node\Expression\FilterTestExtension::staticMethod("abc")', $environment]; - $node = $this->createFilter($environment, $string, 'first_class_callable_object'); + $node = self::createFilter($environment, $string, 'first_class_callable_object'); $tests[] = [$node, '$this->extensions[\'Twig\Tests\Node\Expression\FilterTestExtension\']->objectMethod("abc")', $environment]; } - $node = $this->createFilter($environment, $string, 'barbar', [ + $node = self::createFilter($environment, $string, 'barbar', [ new ConstantExpression('1', 1), new ConstantExpression('2', 1), new ConstantExpression('3', 1), @@ -121,13 +114,13 @@ public function getTests() $tests[] = [$node, 'Twig\Tests\Node\Expression\twig_tests_filter_barbar($context, "abc", "1", "2", ["3", "foo" => "bar"])', $environment]; // from extension - $node = $this->createFilter($environment, $string, 'foo'); - $tests[] = [$node, \sprintf('$this->extensions[\'%s\']->foo("abc")', \get_class($this->getExtension())), $environment]; + $node = self::createFilter($environment, $string, 'foo'); + $tests[] = [$node, \sprintf('$this->extensions[\'%s\']->foo("abc")', \get_class(self::createExtension())), $environment]; - $node = $this->createFilter($environment, $string, 'foobar'); + $node = self::createFilter($environment, $string, 'foobar'); $tests[] = [$node, '$this->env->getFilter(\'foobar\')->getCallable()("abc")', $environment]; - $node = $this->createFilter($environment, $string, 'magic_static'); + $node = self::createFilter($environment, $string, 'magic_static'); $tests[] = [$node, 'Twig\Tests\Node\Expression\ChildMagicCallStub::magicStaticCall("abc")', $environment]; return $tests; @@ -161,12 +154,12 @@ public function testCompileWithMissingNamedArgument() $compiler->compile($node); } - protected function createFilter(Environment $env, $node, $name, array $arguments = []) + private static function createFilter(Environment $env, $node, $name, array $arguments = []): FilterExpression { return new FilterExpression($node, $env->getFilter($name), new Node($arguments), 1); } - protected function getEnvironment() + protected static function createEnvironment(): Environment { $env = new Environment(new ArrayLoader()); $env->addFilter(new TwigFilter('anonymous', function () {})); @@ -177,18 +170,14 @@ protected function getEnvironment() if (\PHP_VERSION_ID >= 80111) { $env->addExtension(new FilterTestExtension()); } - $env->addExtension($this->getExtension()); + $env->addExtension(self::createExtension()); return $env; } - private function getExtension() + private static function createExtension(): AbstractExtension { - if ($this->extension) { - return $this->extension; - } - - return $this->extension = new class() extends AbstractExtension { + return new class extends AbstractExtension { public function getFilters(): array { return [ diff --git a/tests/Node/Expression/FunctionTest.php b/tests/Node/Expression/FunctionTest.php index eacafa2d133..a6e562c06ce 100644 --- a/tests/Node/Expression/FunctionTest.php +++ b/tests/Node/Expression/FunctionTest.php @@ -31,57 +31,57 @@ public function testConstructor() $this->assertEquals($args, $node->getNode('arguments')); } - public function getTests() + public static function provideTests(): iterable { - $environment = $this->getEnvironment(); + $environment = static::createEnvironment(); $tests = []; - $node = $this->createFunction($environment, 'foo'); + $node = self::createFunction($environment, 'foo'); $tests[] = [$node, 'Twig\Tests\Node\Expression\twig_tests_function_dummy()', $environment]; - $node = $this->createFunction($environment, 'foo_closure'); + $node = self::createFunction($environment, 'foo_closure'); $tests[] = [$node, twig_tests_function_dummy::class.'()', $environment]; - $node = $this->createFunction($environment, 'foo', [new ConstantExpression('bar', 1), new ConstantExpression('foobar', 1)]); + $node = self::createFunction($environment, 'foo', [new ConstantExpression('bar', 1), new ConstantExpression('foobar', 1)]); $tests[] = [$node, 'Twig\Tests\Node\Expression\twig_tests_function_dummy("bar", "foobar")', $environment]; - $node = $this->createFunction($environment, 'bar'); + $node = self::createFunction($environment, 'bar'); $tests[] = [$node, 'Twig\Tests\Node\Expression\twig_tests_function_dummy($this->env)', $environment]; - $node = $this->createFunction($environment, 'bar', [new ConstantExpression('bar', 1)]); + $node = self::createFunction($environment, 'bar', [new ConstantExpression('bar', 1)]); $tests[] = [$node, 'Twig\Tests\Node\Expression\twig_tests_function_dummy($this->env, "bar")', $environment]; - $node = $this->createFunction($environment, 'foofoo'); + $node = self::createFunction($environment, 'foofoo'); $tests[] = [$node, 'Twig\Tests\Node\Expression\twig_tests_function_dummy($context)', $environment]; - $node = $this->createFunction($environment, 'foofoo', [new ConstantExpression('bar', 1)]); + $node = self::createFunction($environment, 'foofoo', [new ConstantExpression('bar', 1)]); $tests[] = [$node, 'Twig\Tests\Node\Expression\twig_tests_function_dummy($context, "bar")', $environment]; - $node = $this->createFunction($environment, 'foobar'); + $node = self::createFunction($environment, 'foobar'); $tests[] = [$node, 'Twig\Tests\Node\Expression\twig_tests_function_dummy($this->env, $context)', $environment]; - $node = $this->createFunction($environment, 'foobar', [new ConstantExpression('bar', 1)]); + $node = self::createFunction($environment, 'foobar', [new ConstantExpression('bar', 1)]); $tests[] = [$node, 'Twig\Tests\Node\Expression\twig_tests_function_dummy($this->env, $context, "bar")', $environment]; // named arguments - $node = $this->createFunction($environment, 'date', [ + $node = self::createFunction($environment, 'date', [ 'timezone' => new ConstantExpression('America/Chicago', 1), 'date' => new ConstantExpression(0, 1), ]); $tests[] = [$node, '$this->extensions[\'Twig\Extension\CoreExtension\']->convertDate(0, "America/Chicago")']; // arbitrary named arguments - $node = $this->createFunction($environment, 'barbar'); + $node = self::createFunction($environment, 'barbar'); $tests[] = [$node, 'Twig\Tests\Node\Expression\twig_tests_function_barbar()', $environment]; - $node = $this->createFunction($environment, 'barbar', ['foo' => new ConstantExpression('bar', 1)]); + $node = self::createFunction($environment, 'barbar', ['foo' => new ConstantExpression('bar', 1)]); $tests[] = [$node, 'Twig\Tests\Node\Expression\twig_tests_function_barbar(null, null, ["foo" => "bar"])', $environment]; - $node = $this->createFunction($environment, 'barbar', ['arg2' => new ConstantExpression('bar', 1)]); + $node = self::createFunction($environment, 'barbar', ['arg2' => new ConstantExpression('bar', 1)]); $tests[] = [$node, 'Twig\Tests\Node\Expression\twig_tests_function_barbar(null, "bar")', $environment]; - $node = $this->createFunction($environment, 'barbar', [ + $node = self::createFunction($environment, 'barbar', [ new ConstantExpression('1', 1), new ConstantExpression('2', 1), new ConstantExpression('3', 1), @@ -90,18 +90,18 @@ public function getTests() $tests[] = [$node, 'Twig\Tests\Node\Expression\twig_tests_function_barbar("1", "2", ["3", "foo" => "bar"])', $environment]; // function as an anonymous function - $node = $this->createFunction($environment, 'anonymous', [new ConstantExpression('foo', 1)]); + $node = self::createFunction($environment, 'anonymous', [new ConstantExpression('foo', 1)]); $tests[] = [$node, '$this->env->getFunction(\'anonymous\')->getCallable()("foo")']; return $tests; } - protected function createFunction(Environment $env, $name, array $arguments = []) + private static function createFunction(Environment $env, $name, array $arguments = []): FunctionExpression { return new FunctionExpression($env->getFunction($name), new Node($arguments), 1); } - protected function getEnvironment() + protected static function createEnvironment(): Environment { $env = new Environment(new ArrayLoader()); $env->addFunction(new TwigFunction('anonymous', function () {})); diff --git a/tests/Node/Expression/GetAttrTest.php b/tests/Node/Expression/GetAttrTest.php index 38b70555c33..0446c73eb79 100644 --- a/tests/Node/Expression/GetAttrTest.php +++ b/tests/Node/Expression/GetAttrTest.php @@ -35,7 +35,7 @@ public function testConstructor() $this->assertEquals(Template::ARRAY_CALL, $node->getAttribute('type')); } - public function getTests() + public static function provideTests(): iterable { $tests = []; @@ -43,7 +43,7 @@ public function getTests() $attr = new ConstantExpression('bar', 1); $args = new ArrayExpression([], 1); $node = new GetAttrExpression($expr, $attr, $args, Template::ANY_CALL, 1); - $tests[] = [$node, \sprintf('%s%s, "bar", [], "any", false, false, false, 1)', $this->getAttributeGetter(), $this->getVariableGetter('foo', 1))]; + $tests[] = [$node, \sprintf('%s%s, "bar", [], "any", false, false, false, 1)', self::createAttributeGetter(), self::createVariableGetter('foo', 1))]; $node = new GetAttrExpression($expr, $attr, $args, Template::ARRAY_CALL, 1); $tests[] = [$node, '(($__internal_%s = // line 1'."\n". @@ -53,7 +53,7 @@ public function getTests() $args->addElement(new NameExpression('foo', 1)); $args->addElement(new ConstantExpression('bar', 1)); $node = new GetAttrExpression($expr, $attr, $args, Template::METHOD_CALL, 1); - $tests[] = [$node, \sprintf('%s%s, "bar", [%s, "bar"], "method", false, false, false, 1)', $this->getAttributeGetter(), $this->getVariableGetter('foo', 1), $this->getVariableGetter('foo'))]; + $tests[] = [$node, \sprintf('%s%s, "bar", [%s, "bar"], "method", false, false, false, 1)', self::createAttributeGetter(), self::createVariableGetter('foo', 1), self::createVariableGetter('foo'))]; return $tests; } diff --git a/tests/Node/Expression/NameTest.php b/tests/Node/Expression/NameTest.php index 3e5437444ba..b38d446def1 100644 --- a/tests/Node/Expression/NameTest.php +++ b/tests/Node/Expression/NameTest.php @@ -25,7 +25,7 @@ public function testConstructor() $this->assertEquals('foo', $node->getAttribute('name')); } - public function getTests() + public static function provideTests(): iterable { $node = new NameExpression('foo', 1); $self = new NameExpression('_self', 1); @@ -38,7 +38,7 @@ public function getTests() return [ [$node, "// line 1\n".$output, $env], - [$node, $this->getVariableGetter('foo', 1), $env1], + [$node, self::createVariableGetter('foo', 1), $env1], [$self, "// line 1\n\$this->getTemplateName()"], [$context, "// line 1\n\$context"], ]; diff --git a/tests/Node/Expression/NullCoalesceTest.php b/tests/Node/Expression/NullCoalesceTest.php index 188631c7a75..e529db736da 100644 --- a/tests/Node/Expression/NullCoalesceTest.php +++ b/tests/Node/Expression/NullCoalesceTest.php @@ -18,7 +18,7 @@ class NullCoalesceTest extends NodeTestCase { - public function getTests() + public static function provideTests(): iterable { $left = new NameExpression('foo', 1); $right = new ConstantExpression(2, 1); diff --git a/tests/Node/Expression/ParentTest.php b/tests/Node/Expression/ParentTest.php index 1a67b77fe40..40ad88c3ead 100644 --- a/tests/Node/Expression/ParentTest.php +++ b/tests/Node/Expression/ParentTest.php @@ -23,7 +23,7 @@ public function testConstructor() $this->assertEquals('foo', $node->getAttribute('name')); } - public function getTests() + public static function provideTests(): iterable { $tests = []; $tests[] = [new ParentExpression('foo', 1), '$this->renderParentBlock("foo", $context, $blocks)']; diff --git a/tests/Node/Expression/TestTest.php b/tests/Node/Expression/TestTest.php index 124a8766c97..ddda8a486b4 100644 --- a/tests/Node/Expression/TestTest.php +++ b/tests/Node/Expression/TestTest.php @@ -34,32 +34,32 @@ public function testConstructor() $this->assertEquals($name, $node->getAttribute('name')); } - public function getTests() + public static function provideTests(): iterable { - $environment = $this->getEnvironment(); + $environment = static::createEnvironment(); $tests = []; $expr = new ConstantExpression('foo', 1); - $node = new NullTest($expr, $this->getEnvironment()->getTest('null'), new Node([]), 1); + $node = new NullTest($expr, $environment->getTest('null'), new Node([]), 1); $tests[] = [$node, '(null === "foo")']; // test as an anonymous function - $node = $this->createTest($environment, new ConstantExpression('foo', 1), 'anonymous', [new ConstantExpression('foo', 1)]); + $node = self::createTest($environment, new ConstantExpression('foo', 1), 'anonymous', [new ConstantExpression('foo', 1)]); $tests[] = [$node, '$this->env->getTest(\'anonymous\')->getCallable()("foo", "foo")']; // arbitrary named arguments $string = new ConstantExpression('abc', 1); - $node = $this->createTest($environment, $string, 'barbar'); + $node = self::createTest($environment, $string, 'barbar'); $tests[] = [$node, 'Twig\Tests\Node\Expression\twig_tests_test_barbar("abc")', $environment]; - $node = $this->createTest($environment, $string, 'barbar', ['foo' => new ConstantExpression('bar', 1)]); + $node = self::createTest($environment, $string, 'barbar', ['foo' => new ConstantExpression('bar', 1)]); $tests[] = [$node, 'Twig\Tests\Node\Expression\twig_tests_test_barbar("abc", null, null, ["foo" => "bar"])', $environment]; - $node = $this->createTest($environment, $string, 'barbar', ['arg2' => new ConstantExpression('bar', 1)]); + $node = self::createTest($environment, $string, 'barbar', ['arg2' => new ConstantExpression('bar', 1)]); $tests[] = [$node, 'Twig\Tests\Node\Expression\twig_tests_test_barbar("abc", null, "bar")', $environment]; - $node = $this->createTest($environment, $string, 'barbar', [ + $node = self::createTest($environment, $string, 'barbar', [ new ConstantExpression('1', 1), new ConstantExpression('2', 1), new ConstantExpression('3', 1), @@ -70,12 +70,12 @@ public function getTests() return $tests; } - protected function createTest(Environment $env, $node, $name, array $arguments = []) + private static function createTest(Environment $env, $node, $name, array $arguments = []): TestExpression { return new TestExpression($node, $env->getTest($name), new Node($arguments), 1); } - protected function getEnvironment() + protected static function createEnvironment(): Environment { $env = new Environment(new ArrayLoader()); $env->addTest(new TwigTest('anonymous', function () {})); diff --git a/tests/Node/Expression/Unary/NegTest.php b/tests/Node/Expression/Unary/NegTest.php index fcbf66ece8f..cd4e7dff508 100644 --- a/tests/Node/Expression/Unary/NegTest.php +++ b/tests/Node/Expression/Unary/NegTest.php @@ -25,7 +25,7 @@ public function testConstructor() $this->assertEquals($expr, $node->getNode('node')); } - public function getTests() + public static function provideTests(): iterable { $node = new ConstantExpression(1, 1); $node = new NegUnary($node, 1); diff --git a/tests/Node/Expression/Unary/NotTest.php b/tests/Node/Expression/Unary/NotTest.php index 8197111e17a..a4e6625c67b 100644 --- a/tests/Node/Expression/Unary/NotTest.php +++ b/tests/Node/Expression/Unary/NotTest.php @@ -25,7 +25,7 @@ public function testConstructor() $this->assertEquals($expr, $node->getNode('node')); } - public function getTests() + public static function provideTests(): iterable { $node = new ConstantExpression(1, 1); $node = new NotUnary($node, 1); diff --git a/tests/Node/Expression/Unary/PosTest.php b/tests/Node/Expression/Unary/PosTest.php index 780e339e0cf..0e9c7e5a702 100644 --- a/tests/Node/Expression/Unary/PosTest.php +++ b/tests/Node/Expression/Unary/PosTest.php @@ -25,7 +25,7 @@ public function testConstructor() $this->assertEquals($expr, $node->getNode('node')); } - public function getTests() + public static function provideTests(): iterable { $node = new ConstantExpression(1, 1); $node = new PosUnary($node, 1); diff --git a/tests/Node/ForTest.php b/tests/Node/ForTest.php index eb17c6e913a..c4c11253d00 100644 --- a/tests/Node/ForTest.php +++ b/tests/Node/ForTest.php @@ -42,7 +42,7 @@ public function testConstructor() $this->assertEquals($else, $node->getNode('else')); } - public function getTests() + public static function provideTests(): iterable { $tests = []; @@ -54,12 +54,16 @@ public function getTests() $node = new ForNode($keyTarget, $valueTarget, $seq, null, $body, $else, 1); $node->setAttribute('with_loop', false); + $itemsGetter = self::createVariableGetter('items'); + $fooGetter = self::createVariableGetter('foo'); + $valuesGetter = self::createVariableGetter('values'); + $tests[] = [$node, <<getVariableGetter('items')}); +\$context['_seq'] = CoreExtension::ensureTraversable($itemsGetter); foreach (\$context['_seq'] as \$context["key"] => \$context["item"]) { - yield {$this->getVariableGetter('foo')}; + yield $fooGetter; } \$_parent = \$context['_parent']; unset(\$context['_seq'], \$context['key'], \$context['item'], \$context['_parent']); @@ -78,7 +82,7 @@ public function getTests() $tests[] = [$node, <<getVariableGetter('values')}); +\$context['_seq'] = CoreExtension::ensureTraversable($valuesGetter); \$context['loop'] = [ 'parent' => \$context['_parent'], 'index0' => 0, @@ -93,7 +97,7 @@ public function getTests() \$context['loop']['last'] = 1 === \$length; } foreach (\$context['_seq'] as \$context["k"] => \$context["v"]) { - yield {$this->getVariableGetter('foo')}; + yield $fooGetter; ++\$context['loop']['index0']; ++\$context['loop']['index']; \$context['loop']['first'] = false; @@ -120,7 +124,7 @@ public function getTests() $tests[] = [$node, <<getVariableGetter('values')}); +\$context['_seq'] = CoreExtension::ensureTraversable($valuesGetter); \$context['loop'] = [ 'parent' => \$context['_parent'], 'index0' => 0, @@ -135,7 +139,7 @@ public function getTests() \$context['loop']['last'] = 1 === \$length; } foreach (\$context['_seq'] as \$context["k"] => \$context["v"]) { - yield {$this->getVariableGetter('foo')}; + yield $fooGetter; ++\$context['loop']['index0']; ++\$context['loop']['index']; \$context['loop']['first'] = false; @@ -162,7 +166,7 @@ public function getTests() $tests[] = [$node, <<getVariableGetter('values')}); +\$context['_seq'] = CoreExtension::ensureTraversable($valuesGetter); \$context['_iterated'] = false; \$context['loop'] = [ 'parent' => \$context['_parent'], @@ -178,7 +182,7 @@ public function getTests() \$context['loop']['last'] = 1 === \$length; } foreach (\$context['_seq'] as \$context["k"] => \$context["v"]) { - yield {$this->getVariableGetter('foo')}; + yield $fooGetter; \$context['_iterated'] = true; ++\$context['loop']['index0']; ++\$context['loop']['index']; @@ -190,7 +194,7 @@ public function getTests() } } if (!\$context['_iterated']) { - yield {$this->getVariableGetter('foo')}; + yield $fooGetter; } \$_parent = \$context['_parent']; unset(\$context['_seq'], \$context['k'], \$context['v'], \$context['_parent'], \$context['_iterated'], \$context['loop']); diff --git a/tests/Node/IfTest.php b/tests/Node/IfTest.php index 26821a39b35..4736def3ffa 100644 --- a/tests/Node/IfTest.php +++ b/tests/Node/IfTest.php @@ -37,7 +37,7 @@ public function testConstructor() $this->assertEquals($else, $node->getNode('else')); } - public function getTests() + public static function provideTests(): iterable { $tests = []; @@ -48,10 +48,13 @@ public function getTests() $else = null; $node = new IfNode($t, $else, 1); + $fooGetter = self::createVariableGetter('foo'); + $barGetter = self::createVariableGetter('bar'); + $tests[] = [$node, <<getVariableGetter('foo')}; + yield $fooGetter; } EOF ]; @@ -68,9 +71,9 @@ public function getTests() $tests[] = [$node, <<getVariableGetter('foo')}; + yield $fooGetter; } elseif (false) { - yield {$this->getVariableGetter('bar')}; + yield $barGetter; } EOF ]; @@ -85,9 +88,9 @@ public function getTests() $tests[] = [$node, <<getVariableGetter('foo')}; + yield $fooGetter; } else { - yield {$this->getVariableGetter('bar')}; + yield $barGetter; } EOF ]; diff --git a/tests/Node/ImportTest.php b/tests/Node/ImportTest.php index b069cabe5f6..eb69e218384 100644 --- a/tests/Node/ImportTest.php +++ b/tests/Node/ImportTest.php @@ -28,7 +28,7 @@ public function testConstructor() $this->assertEquals($var, $node->getNode('var')); } - public function getTests() + public static function provideTests(): iterable { $tests = []; diff --git a/tests/Node/IncludeTest.php b/tests/Node/IncludeTest.php index 446fbd29395..b485100588c 100644 --- a/tests/Node/IncludeTest.php +++ b/tests/Node/IncludeTest.php @@ -34,7 +34,7 @@ public function testConstructor() $this->assertTrue($node->getAttribute('only')); } - public function getTests() + public static function provideTests(): iterable { $tests = []; diff --git a/tests/Node/MacroTest.php b/tests/Node/MacroTest.php index f05adf6364d..c0944f42068 100644 --- a/tests/Node/MacroTest.php +++ b/tests/Node/MacroTest.php @@ -34,7 +34,7 @@ public function testConstructor() $this->assertEquals('foo', $node->getAttribute('name')); } - public function getTests() + public static function provideTests(): iterable { $arguments = new Node([ 'foo' => new ConstantExpression(null, 1), diff --git a/tests/Node/ModuleTest.php b/tests/Node/ModuleTest.php index 0b7dd5ddb7a..79463af2fa5 100644 --- a/tests/Node/ModuleTest.php +++ b/tests/Node/ModuleTest.php @@ -23,8 +23,6 @@ use Twig\Node\SetNode; use Twig\Node\TextNode; use Twig\Source; -use Twig\Template; -use Twig\TemplateWrapper; use Twig\Test\NodeTestCase; class ModuleTest extends NodeTestCase @@ -46,7 +44,7 @@ public function testConstructor() $this->assertEquals($source->getName(), $node->getTemplateName()); } - public function getTests() + public static function provideTests(): iterable { $twig = new Environment(new ArrayLoader(['foo.twig' => '{{ foo }}'])); diff --git a/tests/Node/PrintTest.php b/tests/Node/PrintTest.php index 09c2a19ab17..f91c41eb864 100644 --- a/tests/Node/PrintTest.php +++ b/tests/Node/PrintTest.php @@ -28,7 +28,7 @@ public function testConstructor() $this->assertEquals($expr, $node->getNode('expr')); } - public function getTests() + public static function provideTests(): iterable { $tests = []; $tests[] = [new PrintNode(new ConstantExpression('foo', 1), 1), "// line 1\nyield \"foo\";"]; diff --git a/tests/Node/SandboxTest.php b/tests/Node/SandboxTest.php index c74feba42f3..05669a2dcf1 100644 --- a/tests/Node/SandboxTest.php +++ b/tests/Node/SandboxTest.php @@ -25,7 +25,7 @@ public function testConstructor() $this->assertEquals($body, $node->getNode('body')); } - public function getTests() + public static function provideTests(): iterable { $tests = []; diff --git a/tests/Node/SetTest.php b/tests/Node/SetTest.php index f250b80eed3..8d2a9275036 100644 --- a/tests/Node/SetTest.php +++ b/tests/Node/SetTest.php @@ -35,7 +35,7 @@ public function testConstructor() $this->assertFalse($node->getAttribute('capture')); } - public function getTests() + public static function provideTests(): iterable { $tests = []; @@ -52,27 +52,25 @@ public function getTests() $values = new Node([new PrintNode(new ConstantExpression('foo', 1), 1)], [], 1); $node = new SetNode(true, $names, $values, 1); - if ($this->getEnvironment()->useYield()) { - $tests[] = [$node, <<env->getCharset()); EOF - , new Environment(new ArrayLoader()), - ]; - } else { - $tests[] = [$node, <<<'EOF' + , new Environment(new ArrayLoader(), ['use_yield' => true]), + ]; + + $tests[] = [$node, <<<'EOF' // line 1 $context["foo"] = ('' === $tmp = \Twig\Extension\CoreExtension::captureOutput((function () use (&$context, $macros, $blocks) { yield "foo"; return; yield ''; })())) ? '' : new Markup($tmp, $this->env->getCharset()); EOF - , new Environment(new ArrayLoader()), - ]; - } + , new Environment(new ArrayLoader(), ['use_yield' => false]), + ]; $names = new Node([new AssignNameExpression('foo', 1)], [], 1); $values = new TextNode('foo', 1); diff --git a/tests/Node/TextTest.php b/tests/Node/TextTest.php index 357362c3c7e..2d2fe9151a7 100644 --- a/tests/Node/TextTest.php +++ b/tests/Node/TextTest.php @@ -23,7 +23,7 @@ public function testConstructor() $this->assertEquals('foo', $node->getAttribute('data')); } - public function getTests() + public static function provideTests(): iterable { $tests = []; $tests[] = [new TextNode('foo', 1), "// line 1\nyield \"foo\";"]; diff --git a/tests/Node/TypesTest.php b/tests/Node/TypesTest.php index ff6c2dfac12..e5a94b6a995 100644 --- a/tests/Node/TypesTest.php +++ b/tests/Node/TypesTest.php @@ -7,7 +7,7 @@ class TypesTest extends NodeTestCase { - private function getValidMapping(): array + private static function getValidMapping(): array { // {foo: 'string', bar?: 'int'} return [ @@ -18,26 +18,26 @@ private function getValidMapping(): array 'bar' => [ 'type' => 'int', 'optional' => true, - ] + ], ]; } public function testConstructor() { - $types = $this->getValidMapping(); + $types = self::getValidMapping(); $node = new TypesNode($types, 1); $this->assertEquals($types, $node->getAttribute('mapping')); } - public function getTests() + public static function provideTests(): iterable { return [ // 1st test: Node shouldn't compile at all [ - new TypesNode($this->getValidMapping(), 1), - '' - ] + new TypesNode(self::getValidMapping(), 1), + '', + ], ]; } } From 18f4203827008ca836ce820b0947817146f800e0 Mon Sep 17 00:00:00 2001 From: "Alexander M. Turek" Date: Tue, 3 Sep 2024 08:58:43 +0200 Subject: [PATCH 421/812] Prepare IntegrationTestCase for static data providers --- CHANGELOG | 2 ++ doc/deprecated.rst | 7 +++++ extra/cache-extra/Tests/IntegrationTest.php | 2 +- extra/cache-extra/composer.json | 2 +- .../Tests/IntegrationTest.php | 2 +- extra/cssinliner-extra/composer.json | 2 +- extra/html-extra/Tests/IntegrationTest.php | 2 +- extra/html-extra/composer.json | 2 +- extra/inky-extra/Tests/IntegrationTest.php | 2 +- extra/inky-extra/composer.json | 2 +- extra/intl-extra/Tests/IntegrationTest.php | 2 +- extra/intl-extra/composer.json | 2 +- .../markdown-extra/Tests/IntegrationTest.php | 2 +- extra/markdown-extra/composer.json | 2 +- extra/string-extra/Tests/IntegrationTest.php | 2 +- extra/string-extra/composer.json | 2 +- src/Test/IntegrationTestCase.php | 26 +++++++++++++++++-- tests/IntegrationTest.php | 2 +- 18 files changed, 48 insertions(+), 17 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 19fe5863ca8..0b3cca7444f 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -5,6 +5,8 @@ * Mark `Twig\Test\NodeTestCase::getEnvironment()` as final, override `createEnvironment()` instead. * Deprecate `Twig\Test\NodeTestCase::getVariableGetter()`, call `createVariableGetter()` instead. * Deprecate `Twig\Test\NodeTestCase::getAttributeGetter()`, call `createAttributeGetter()` instead. + * Deprecate not overriding `Twig\Test\IntegrationTestCase::getFixturesDirectory()`, this method will be abstract in 4.0 + * Marked `Twig\Test\IntegrationTestCase::getTests()` and `getLegacyTests()` as final # 3.12.0 (2024-08-29) diff --git a/doc/deprecated.rst b/doc/deprecated.rst index 71a17996f2d..e28ecc62bd8 100644 --- a/doc/deprecated.rst +++ b/doc/deprecated.rst @@ -216,3 +216,10 @@ Testing Utilities * The method ``Twig\Test\NodeTestCase::getEnvironment()`` is considered final as of Twig 3.13. If you want to override how the Twig environment is constructed, override ``createEnvironment()`` instead. + +* The method ``getFixturesDir()`` on ``Twig\Test\IntegrationTestCase`` is + deprecated, implement the new static method ``getFixturesDirectory()`` + instead, which will be abstract in 4.0. + +* The data providers ``getTests()`` and ``getLegacyTests()`` on + ``Twig\Test\IntegrationTestCase`` are considered final als of Twig 3.13. diff --git a/extra/cache-extra/Tests/IntegrationTest.php b/extra/cache-extra/Tests/IntegrationTest.php index 4f597b0aa1f..c439976f9b9 100644 --- a/extra/cache-extra/Tests/IntegrationTest.php +++ b/extra/cache-extra/Tests/IntegrationTest.php @@ -38,7 +38,7 @@ public function load(string $class): ?object ]; } - public function getFixturesDir() + protected static function getFixturesDirectory(): string { return __DIR__.'/Fixtures/'; } diff --git a/extra/cache-extra/composer.json b/extra/cache-extra/composer.json index ab354ddc9c1..a3650499563 100644 --- a/extra/cache-extra/composer.json +++ b/extra/cache-extra/composer.json @@ -17,7 +17,7 @@ "require": { "php": ">=8.0.2", "symfony/cache": "^5.4|^6.4|^7.0", - "twig/twig": "^3.12|^4.0" + "twig/twig": "^3.13|^4.0" }, "require-dev": { "symfony/phpunit-bridge": "^6.4|^7.0" diff --git a/extra/cssinliner-extra/Tests/IntegrationTest.php b/extra/cssinliner-extra/Tests/IntegrationTest.php index 5ab6ec9b4ab..7004b5e99ac 100644 --- a/extra/cssinliner-extra/Tests/IntegrationTest.php +++ b/extra/cssinliner-extra/Tests/IntegrationTest.php @@ -23,7 +23,7 @@ public function getExtensions() ]; } - public function getFixturesDir() + protected static function getFixturesDirectory(): string { return __DIR__.'/Fixtures/'; } diff --git a/extra/cssinliner-extra/composer.json b/extra/cssinliner-extra/composer.json index 96e59f2ff14..229843f50e2 100644 --- a/extra/cssinliner-extra/composer.json +++ b/extra/cssinliner-extra/composer.json @@ -18,7 +18,7 @@ "php": ">=8.0.2", "symfony/deprecation-contracts": "^2.5|^3", "tijsverkoyen/css-to-inline-styles": "^2.0", - "twig/twig": "^3.0|^4.0" + "twig/twig": "^3.13|^4.0" }, "require-dev": { "symfony/phpunit-bridge": "^6.4|^7.0" diff --git a/extra/html-extra/Tests/IntegrationTest.php b/extra/html-extra/Tests/IntegrationTest.php index 8f464c152e0..8e2f94e38b9 100644 --- a/extra/html-extra/Tests/IntegrationTest.php +++ b/extra/html-extra/Tests/IntegrationTest.php @@ -23,7 +23,7 @@ public function getExtensions() ]; } - public function getFixturesDir() + protected static function getFixturesDirectory(): string { return __DIR__.'/Fixtures/'; } diff --git a/extra/html-extra/composer.json b/extra/html-extra/composer.json index 46ec29e696f..d902b396742 100644 --- a/extra/html-extra/composer.json +++ b/extra/html-extra/composer.json @@ -18,7 +18,7 @@ "php": ">=8.0.2", "symfony/deprecation-contracts": "^2.5|^3", "symfony/mime": "^5.4|^6.4|^7.0", - "twig/twig": "^3.0|^4.0" + "twig/twig": "^3.13|^4.0" }, "require-dev": { "symfony/phpunit-bridge": "^6.4|^7.0" diff --git a/extra/inky-extra/Tests/IntegrationTest.php b/extra/inky-extra/Tests/IntegrationTest.php index 317d364763a..d9420dd09bf 100644 --- a/extra/inky-extra/Tests/IntegrationTest.php +++ b/extra/inky-extra/Tests/IntegrationTest.php @@ -23,7 +23,7 @@ public function getExtensions() ]; } - public function getFixturesDir() + protected static function getFixturesDirectory(): string { return __DIR__.'/Fixtures/'; } diff --git a/extra/inky-extra/composer.json b/extra/inky-extra/composer.json index c4192d420c5..cb630b96efa 100644 --- a/extra/inky-extra/composer.json +++ b/extra/inky-extra/composer.json @@ -18,7 +18,7 @@ "php": ">=8.0.2", "symfony/deprecation-contracts": "^2.5|^3", "lorenzo/pinky": "^1.0.5", - "twig/twig": "^3.0|^4.0" + "twig/twig": "^3.13|^4.0" }, "require-dev": { "symfony/phpunit-bridge": "^6.4|^7.0" diff --git a/extra/intl-extra/Tests/IntegrationTest.php b/extra/intl-extra/Tests/IntegrationTest.php index 7b191bacd01..fa22b570801 100644 --- a/extra/intl-extra/Tests/IntegrationTest.php +++ b/extra/intl-extra/Tests/IntegrationTest.php @@ -23,7 +23,7 @@ public function getExtensions() ]; } - public function getFixturesDir() + protected static function getFixturesDirectory(): string { return __DIR__.'/Fixtures/'; } diff --git a/extra/intl-extra/composer.json b/extra/intl-extra/composer.json index 0c5ba1cc012..8355df43ce9 100644 --- a/extra/intl-extra/composer.json +++ b/extra/intl-extra/composer.json @@ -16,7 +16,7 @@ ], "require": { "php": ">=8.0.2", - "twig/twig": "^3.10|^4.0", + "twig/twig": "^3.13|^4.0", "symfony/intl": "^5.4|^6.4|^7.0" }, "require-dev": { diff --git a/extra/markdown-extra/Tests/IntegrationTest.php b/extra/markdown-extra/Tests/IntegrationTest.php index 7474ec7693c..7db95c9190f 100644 --- a/extra/markdown-extra/Tests/IntegrationTest.php +++ b/extra/markdown-extra/Tests/IntegrationTest.php @@ -23,7 +23,7 @@ public function getExtensions() ]; } - public function getFixturesDir() + protected static function getFixturesDirectory(): string { return __DIR__.'/Fixtures/'; } diff --git a/extra/markdown-extra/composer.json b/extra/markdown-extra/composer.json index 91101e2c1ee..cf545836882 100644 --- a/extra/markdown-extra/composer.json +++ b/extra/markdown-extra/composer.json @@ -17,7 +17,7 @@ "require": { "php": ">=8.0.2", "symfony/deprecation-contracts": "^2.5|^3", - "twig/twig": "^3.0|^4.0" + "twig/twig": "^3.13|^4.0" }, "require-dev": { "symfony/phpunit-bridge": "^6.4|^7.0", diff --git a/extra/string-extra/Tests/IntegrationTest.php b/extra/string-extra/Tests/IntegrationTest.php index 032c9a9d9a2..ddf6abfe509 100644 --- a/extra/string-extra/Tests/IntegrationTest.php +++ b/extra/string-extra/Tests/IntegrationTest.php @@ -23,7 +23,7 @@ public function getExtensions() ]; } - public function getFixturesDir() + protected static function getFixturesDirectory(): string { return __DIR__.'/Fixtures/'; } diff --git a/extra/string-extra/composer.json b/extra/string-extra/composer.json index d6c0bfa6467..becf9de89c8 100644 --- a/extra/string-extra/composer.json +++ b/extra/string-extra/composer.json @@ -18,7 +18,7 @@ "php": ">=8.0.2", "symfony/string": "^5.4|^6.4|^7.0", "symfony/translation-contracts": "^1.1|^2|^3", - "twig/twig": "^3.0|^4.0" + "twig/twig": "^3.13|^4.0" }, "require-dev": { "symfony/phpunit-bridge": "^6.4|^7.0" diff --git a/src/Test/IntegrationTestCase.php b/src/Test/IntegrationTestCase.php index 88b3349c4ed..78b0171e12f 100644 --- a/src/Test/IntegrationTestCase.php +++ b/src/Test/IntegrationTestCase.php @@ -30,9 +30,18 @@ abstract class IntegrationTestCase extends TestCase { /** + * @deprecated since Twig 3.13, use getFixturesDirectory() instead. * @return string */ - abstract protected function getFixturesDir(); + protected function getFixturesDir() + { + throw new \BadMethodCallException('Not implemented.'); + } + + protected static function getFixturesDirectory(): string + { + throw new \BadMethodCallException('Not implemented.'); + } /** * @return RuntimeLoaderInterface[] @@ -92,9 +101,19 @@ public function testLegacyIntegration($file, $message, $condition, $templates, $ $this->doIntegrationTest($file, $message, $condition, $templates, $exception, $outputs, $deprecation); } + /** + * @final since Twig 3.13 + */ public function getTests($name, $legacyTests = false) { - $fixturesDir = realpath($this->getFixturesDir()); + try { + $fixturesDir = static::getFixturesDirectory(); + } catch (\BadMethodCallException) { + trigger_deprecation('twig/twig', '3.13', 'Not overriding "%s::getFixturesDirectory()" in "%s" is deprecated. This method will be abstract in 4.0.', self::class, static::class); + $fixturesDir = $this->getFixturesDir(); + } + + $fixturesDir = realpath($fixturesDir); $tests = []; foreach (new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($fixturesDir), \RecursiveIteratorIterator::LEAVES_ONLY) as $file) { @@ -137,6 +156,9 @@ public function getTests($name, $legacyTests = false) return $tests; } + /** + * @final since Twig 3.13 + */ public function getLegacyTests() { return $this->getTests('testLegacyIntegration', true); diff --git a/tests/IntegrationTest.php b/tests/IntegrationTest.php index f68ac15cf8c..65f207d90bb 100644 --- a/tests/IntegrationTest.php +++ b/tests/IntegrationTest.php @@ -48,7 +48,7 @@ public function getExtensions() ]; } - public function getFixturesDir() + protected static function getFixturesDirectory(): string { return __DIR__.'/Fixtures/'; } From 6ddb76bb760e828155c31944f2d25b56b6b09f1c Mon Sep 17 00:00:00 2001 From: "Alexander M. Turek" Date: Mon, 2 Sep 2024 17:29:38 +0200 Subject: [PATCH 422/812] Make data providers static --- extra/markdown-extra/Tests/FunctionalTest.php | 2 +- tests/Cache/FilesystemTest.php | 2 +- tests/CustomExtensionTest.php | 2 +- tests/ErrorTest.php | 2 +- tests/ExpressionParserTest.php | 18 +++++++++--------- tests/Extension/CoreTest.php | 14 +++++++------- tests/Extension/EscaperTest.php | 2 +- tests/Extension/SandboxTest.php | 8 ++++---- tests/FileExtensionEscapingStrategyTest.php | 2 +- tests/LexerTest.php | 8 ++++---- tests/Loader/FilesystemTest.php | 6 +++--- tests/NodeVisitor/OptimizerTest.php | 2 +- tests/ParserTest.php | 6 +++--- tests/Runtime/EscaperRuntimeTest.php | 4 ++-- tests/TemplateTest.php | 8 ++++---- tests/TokenParser/TypesTokenParserTest.php | 2 +- 16 files changed, 44 insertions(+), 44 deletions(-) diff --git a/extra/markdown-extra/Tests/FunctionalTest.php b/extra/markdown-extra/Tests/FunctionalTest.php index 62c7928feec..154d75a9c39 100644 --- a/extra/markdown-extra/Tests/FunctionalTest.php +++ b/extra/markdown-extra/Tests/FunctionalTest.php @@ -57,7 +57,7 @@ public function load(string $c): ?object } } - public function getMarkdownTests() + public static function getMarkdownTests() { return [ [<<assertMatchesRegularExpression($expected, $cache->generateKey('_test_', static::class)); } - public function provideDirectories() + public static function provideDirectories() { $pattern = '#a/b/[a-zA-Z0-9]+/[a-zA-Z0-9]+.php$#'; diff --git a/tests/CustomExtensionTest.php b/tests/CustomExtensionTest.php index a2ac0dbed5b..fab7813b11c 100644 --- a/tests/CustomExtensionTest.php +++ b/tests/CustomExtensionTest.php @@ -31,7 +31,7 @@ public function testGetInvalidOperators(ExtensionInterface $extension, $expected $env->getUnaryOperators(); } - public function provideInvalidExtensions() + public static function provideInvalidExtensions() { return [ [new InvalidOperatorExtension([1, 2, 3]), '"Twig\Tests\InvalidOperatorExtension::getOperators()" must return an array of 2 elements, got 3.'], diff --git a/tests/ErrorTest.php b/tests/ErrorTest.php index 423a1a58d6f..d29112cd741 100644 --- a/tests/ErrorTest.php +++ b/tests/ErrorTest.php @@ -234,7 +234,7 @@ public function testTwigArrayReduceThrowsRuntimeExceptions() } } - public function getErroredTemplates() + public static function getErroredTemplates() { return [ // error occurs in a template diff --git a/tests/ExpressionParserTest.php b/tests/ExpressionParserTest.php index 3f28cca1e1a..e94f8d1841a 100644 --- a/tests/ExpressionParserTest.php +++ b/tests/ExpressionParserTest.php @@ -48,7 +48,7 @@ public function testCanOnlyAssignToNames($template) $parser->parse($env->tokenize(new Source($template, 'index'))); } - public function getFailingTestsForAssignment() + public static function getFailingTestsForAssignment() { return [ ['{% set false = "foo" %}'], @@ -91,7 +91,7 @@ public function testSequenceSyntaxError($template) $parser->parse($env->tokenize(new Source($template, 'index'))); } - public function getFailingTestsForSequence() + public static function getFailingTestsForSequence() { return [ ['{{ [1, "a": "b"] }}'], @@ -100,7 +100,7 @@ public function getFailingTestsForSequence() ]; } - public function getTestsForSequence() + public static function getTestsForSequence() { return [ // simple sequence @@ -190,7 +190,7 @@ public function getTestsForSequence() new ConstantExpression(2, 1), new ConstantExpression(2, 1), - $this->createNameExpression('foo', ['spread' => true]), + self::createNameExpression('foo', ['spread' => true]), ], 1)], // mapping with spread operator @@ -203,7 +203,7 @@ public function getTestsForSequence() new ConstantExpression('c', 1), new ConstantExpression(0, 1), - $this->createNameExpression('otherLetters', ['spread' => true]), + self::createNameExpression('otherLetters', ['spread' => true]), ], 1)], ]; } @@ -232,7 +232,7 @@ public function testStringExpression($template, $expected) $this->assertEquals($expected, $parser->parse($stream)->getNode('body')->getNode('0')->getNode('expr')); } - public function getTestsForString() + public static function getTestsForString() { return [ [ @@ -321,7 +321,7 @@ public function testMacroDefinitionDoesNotSupportNonConstantDefaultValues($templ $parser->parse($env->tokenize(new Source($template, 'index'))); } - public function getMacroDefinitionDoesNotSupportNonConstantDefaultValues() + public static function getMacroDefinitionDoesNotSupportNonConstantDefaultValues() { return [ ['{% macro foo(name = "a #{foo} a") %}{% endmacro %}'], @@ -344,7 +344,7 @@ public function testMacroDefinitionSupportsConstantDefaultValues($template) $this->addToAssertionCount(1); } - public function getMacroDefinitionSupportsConstantDefaultValues() + public static function getMacroDefinitionSupportsConstantDefaultValues() { return [ ['{% macro foo(name = "aa") %}{% endmacro %}'], @@ -584,7 +584,7 @@ public function testTwoWordTestPrecedence() $this->doesNotPerformAssertions(); } - private function createNameExpression(string $name, array $attributes) + private static function createNameExpression(string $name, array $attributes): NameExpression { $expression = new NameExpression($name, 1); foreach ($attributes as $key => $value) { diff --git a/tests/Extension/CoreTest.php b/tests/Extension/CoreTest.php index f61980bf987..2272bb4d03c 100644 --- a/tests/Extension/CoreTest.php +++ b/tests/Extension/CoreTest.php @@ -67,7 +67,7 @@ public function testRandomFunction(array $expectedInArray, $value1, $value2 = nu } } - public function getRandomFunctionTestData() + public static function getRandomFunctionTestData() { return [ 'array' => [ @@ -165,7 +165,7 @@ public function testTwigFirst($expected, $input) $this->assertSame($expected, CoreExtension::first('UTF-8', $input)); } - public function provideTwigFirstCases() + public static function provideTwigFirstCases() { $i = [1 => 'a', 2 => 'b', 3 => 'c']; @@ -186,7 +186,7 @@ public function testTwigLast($expected, $input) $this->assertSame($expected, CoreExtension::last('UTF-8', $input)); } - public function provideTwigLastCases() + public static function provideTwigLastCases() { $i = [1 => 'a', 2 => 'b', 3 => 'c']; @@ -207,7 +207,7 @@ public function testArrayKeysFilter(array $expected, $input) $this->assertSame($expected, CoreExtension::keys($input)); } - public function provideArrayKeyCases() + public static function provideArrayKeyCases() { $array = ['a' => 'a1', 'b' => 'b1', 'c' => 'c1']; $keys = array_keys($array); @@ -230,7 +230,7 @@ public function testInFilter($expected, $value, $compare) $this->assertSame($expected, CoreExtension::inFilter($value, $compare)); } - public function provideInFilterCases() + public static function provideInFilterCases() { $array = [1, 2, 'a' => 3, 5, 6, 7]; $keys = array_keys($array); @@ -258,7 +258,7 @@ public function testSliceFilter($expected, $input, $start, $length = null, $pres $this->assertSame($expected, CoreExtension::slice('UTF-8', $input, $start, $length, $preserveKeys)); } - public function provideSliceFilterCases() + public static function provideSliceFilterCases() { $i = ['a' => 1, 'b' => 2, 'c' => 3, 'd' => 4]; $keys = array_keys($i); @@ -296,7 +296,7 @@ public function testCompareNAN() $this->assertSame(1, CoreExtension::compare('foo', \NAN)); } - public function provideCompareCases() + public static function provideCompareCases() { return [ [0, 'a', 'a'], diff --git a/tests/Extension/EscaperTest.php b/tests/Extension/EscaperTest.php index 4f707184085..436d1790f52 100644 --- a/tests/Extension/EscaperTest.php +++ b/tests/Extension/EscaperTest.php @@ -32,7 +32,7 @@ public function testCustomEscaper($expected, $string, $strategy) $this->assertSame($expected, $twig->getRuntime(EscaperRuntime::class)->escape($string, $strategy, 'ISO-8859-1')); } - public function provideCustomEscaperCases() + public static function provideCustomEscaperCases() { return [ ['foo**ISO-8859-1**UTF-8', 'foo', 'foo'], diff --git a/tests/Extension/SandboxTest.php b/tests/Extension/SandboxTest.php index d193e7ef903..810bbf74e58 100644 --- a/tests/Extension/SandboxTest.php +++ b/tests/Extension/SandboxTest.php @@ -79,7 +79,7 @@ public function testSandboxForCoreTags(string $tag, string $template) $twig->createTemplate($template, 'index')->render([]); } - public function getSandboxedForCoreTagsTests() + public static function getSandboxedForCoreTagsTests() { yield ['apply', '{% apply upper %}foo{% endapply %}']; yield ['autoescape', '{% autoescape %}foo{% endautoescape %}']; @@ -116,7 +116,7 @@ public function testSandboxForExtendsAndUseTags(string $tag, string $template) $twig->createTemplate($template, 'index')->render([]); } - public function getSandboxedForExtendsAndUseTagsTests() + public static function getSandboxedForExtendsAndUseTagsTests() { yield ['extends', '{% extends "1_empty" %}']; yield ['use', '{% use "1_empty" %}']; @@ -253,7 +253,7 @@ public function testSandboxUnallowedToString($template) } } - public function getSandboxUnallowedToStringTests() + public static function getSandboxUnallowedToStringTests() { return [ 'simple' => ['{{ obj }}'], @@ -281,7 +281,7 @@ public function testSandboxAllowedToString($template, $output) $this->assertEquals($output, $twig->load('index')->render(self::$params)); } - public function getSandboxAllowedToStringTests() + public static function getSandboxAllowedToStringTests() { return [ 'constant_test' => ['{{ obj is constant("PHP_INT_MAX") }}', ''], diff --git a/tests/FileExtensionEscapingStrategyTest.php b/tests/FileExtensionEscapingStrategyTest.php index 883aa882ce5..ed5ce060ba9 100644 --- a/tests/FileExtensionEscapingStrategyTest.php +++ b/tests/FileExtensionEscapingStrategyTest.php @@ -24,7 +24,7 @@ public function testGuess($strategy, $filename) $this->assertSame($strategy, FileExtensionEscapingStrategy::guess($filename)); } - public function getGuessData() + public static function getGuessData() { return [ // default diff --git a/tests/LexerTest.php b/tests/LexerTest.php index f07a0868412..4763731300c 100644 --- a/tests/LexerTest.php +++ b/tests/LexerTest.php @@ -193,7 +193,7 @@ public function testStringWithEscapedDelimiter(string $template, string $expecte $this->assertSame($expected, $token->getValue()); } - public function getStringWithEscapedDelimiter() + public static function getStringWithEscapedDelimiter() { yield '{{ \'\x6\' }} => \x6' => [ '{{ \'\x6\' }}', @@ -247,7 +247,7 @@ public function testStringWithEscapedDelimiterProducingDeprecation(string $templ $this->addToAssertionCount(1); } - public function getStringWithEscapedDelimiterProducingDeprecation() + public static function getStringWithEscapedDelimiterProducingDeprecation() { yield '{{ \'App\Test\' }} => AppTest' => [ '{{ \'App\\Test\' }}', @@ -465,7 +465,7 @@ public function testErrorsAtTheEndOfTheStream(string $template) } } - public function getTemplateForErrorsAtTheEndOfTheStream() + public static function getTemplateForErrorsAtTheEndOfTheStream() { yield ['{{ =']; yield ['{{ ..']; @@ -493,7 +493,7 @@ public function testStrings(string $expected) $this->addToAssertionCount(1); } - public function getTemplateForStrings() + public static function getTemplateForStrings() { yield ['日本では、春になると桜の花が咲きます。多くの人々は、公園や川の近くに集まり、お花見を楽しみます。桜の花びらが風に舞い、まるで雪のように見える瞬間は、とても美しいです。']; yield ['في العالم العربي، يُعتبر الخط العربي أحد أجمل أشكال الفن. يُستخدم الخط في تزيين المساجد والكتب والمخطوطات القديمة. يتميز الخط العربي بجماله وتناسقه، ويُعتبر رمزًا للثقافة الإسلامية.']; diff --git a/tests/Loader/FilesystemTest.php b/tests/Loader/FilesystemTest.php index 44b3c170d0f..c7315ea80b6 100644 --- a/tests/Loader/FilesystemTest.php +++ b/tests/Loader/FilesystemTest.php @@ -42,7 +42,7 @@ public function testSecurity($template) } } - public function getSecurityTests() + public static function getSecurityTests() { return [ ["AutoloaderTest\0.php"], @@ -105,7 +105,7 @@ public function testPaths($basePath, $cacheKey, $rootPath) $this->assertEquals("named path (final)\n", $loader->getSourceContext('@named/index.html')->getCode()); } - public function getBasePaths() + public static function getBasePaths() { return [ [ @@ -197,7 +197,7 @@ public function testLoadTemplateAndRenderBlockWithCache() $this->assertSame('block from theme 2', $template->renderBlock('b2', [])); } - public function getArrayInheritanceTests() + public static function getArrayInheritanceTests() { return [ 'valid array inheritance' => ['array_inheritance_valid_parent.html.twig'], diff --git a/tests/NodeVisitor/OptimizerTest.php b/tests/NodeVisitor/OptimizerTest.php index 12dc1214dc3..12bdc12d127 100644 --- a/tests/NodeVisitor/OptimizerTest.php +++ b/tests/NodeVisitor/OptimizerTest.php @@ -92,7 +92,7 @@ public function testForLoopOptimizer($template, $expected) } } - public function getTestsForForLoopOptimizer() + public static function getTestsForForLoopOptimizer() { return [ ['{% for i in foo %}{% endfor %}', ['i' => false]], diff --git a/tests/ParserTest.php b/tests/ParserTest.php index e0c0de7fe93..72709283704 100644 --- a/tests/ParserTest.php +++ b/tests/ParserTest.php @@ -69,7 +69,7 @@ public function testFilterBodyNodes($input, $expected) $this->assertEquals($expected, $m->invoke($parser, $input)); } - public function getFilterBodyNodesData() + public static function getFilterBodyNodesData() { return [ [ @@ -102,7 +102,7 @@ public function testFilterBodyNodesThrowsException($input) $m->invoke($parser, $input); } - public function getFilterBodyNodesDataThrowsException() + public static function getFilterBodyNodesDataThrowsException() { return [ [new TextNode('foo', 1)], @@ -122,7 +122,7 @@ public function testFilterBodyNodesWithBOM($emptyNode) $this->assertNull($m->invoke($parser, new TextNode(\chr(0xEF).\chr(0xBB).\chr(0xBF).$emptyNode, 1))); } - public function getFilterBodyNodesWithBOMData() + public static function getFilterBodyNodesWithBOMData() { return [ [' '], diff --git a/tests/Runtime/EscaperRuntimeTest.php b/tests/Runtime/EscaperRuntimeTest.php index 593563a3b2a..706c0074ff9 100644 --- a/tests/Runtime/EscaperRuntimeTest.php +++ b/tests/Runtime/EscaperRuntimeTest.php @@ -357,7 +357,7 @@ public function testCustomEscaper($expected, $string, $strategy, $charset) $this->assertSame($expected, $escaper->escape($string, $strategy, $charset)); } - public function provideCustomEscaperCases() + public static function provideCustomEscaperCases() { return [ ['foo**ISO-8859-1', 'foo', 'foo', 'ISO-8859-1'], @@ -378,7 +378,7 @@ public function testObjectEscaping(string $escapedHtml, string $escapedJs, array $this->assertSame($escapedJs, $escaper->escape($obj, 'js', null, true)); } - public function provideObjectsForEscaping() + public static function provideObjectsForEscaping() { return [ ['<br />', '
    ', ['\Twig\Tests\Runtime\Extension_TestClass' => ['js']]], diff --git a/tests/TemplateTest.php b/tests/TemplateTest.php index 2756c5774ec..1f2a8d28165 100644 --- a/tests/TemplateTest.php +++ b/tests/TemplateTest.php @@ -61,7 +61,7 @@ public function testGetAttributeExceptions($template, $message) } } - public function getAttributeExceptions() + public static function getAttributeExceptions() { return [ ['{{ string["a"] }}', 'Impossible to access a key ("a") on a string variable ("foo") in "%s" at line 1.'], @@ -113,7 +113,7 @@ public function testGetAttributeWithSandbox($object, $item, $allowed) } } - public function getGetAttributeWithSandbox() + public static function getGetAttributeWithSandbox() { return [ [new TemplatePropertyObject(), 'defined', false], @@ -132,7 +132,7 @@ public function testRenderTemplateWithoutOutput(string $template) $this->assertSame('', $twig->render('index')); } - public function getRenderTemplateWithoutOutputData() + public static function getRenderTemplateWithoutOutputData() { return [ [''], @@ -265,7 +265,7 @@ public function testGetAttributeCallExceptions() $this->assertNull(CoreExtension::getAttribute($twig, $template->getSourceContext(), $object, 'foo')); } - public function getGetAttributeTests() + public static function getGetAttributeTests() { $array = [ 'defined' => 'defined', diff --git a/tests/TokenParser/TypesTokenParserTest.php b/tests/TokenParser/TypesTokenParserTest.php index 67794c09779..57ccb77cb03 100644 --- a/tests/TokenParser/TypesTokenParserTest.php +++ b/tests/TokenParser/TypesTokenParserTest.php @@ -22,7 +22,7 @@ public function testMappingParsing(string $template, array $expected): void self::assertEquals($expected, $typesNode->getAttribute('mapping')); } - public function getMappingTests(): array + public static function getMappingTests(): array { return [ // empty mapping From fc20997c9e94060a83a4c5eeadad2096df0d2706 Mon Sep 17 00:00:00 2001 From: "Alexander M. Turek" Date: Mon, 2 Sep 2024 17:25:25 +0200 Subject: [PATCH 423/812] Rename AbstractTest to ProfilerTestCase --- tests/Profiler/Dumper/BlackfireTest.php | 2 +- tests/Profiler/Dumper/HtmlTest.php | 2 +- .../Profiler/Dumper/{AbstractTest.php => ProfilerTestCase.php} | 2 +- tests/Profiler/Dumper/TextTest.php | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) rename tests/Profiler/Dumper/{AbstractTest.php => ProfilerTestCase.php} (98%) diff --git a/tests/Profiler/Dumper/BlackfireTest.php b/tests/Profiler/Dumper/BlackfireTest.php index 3a33d94031b..ecf709a3d23 100644 --- a/tests/Profiler/Dumper/BlackfireTest.php +++ b/tests/Profiler/Dumper/BlackfireTest.php @@ -13,7 +13,7 @@ use Twig\Profiler\Dumper\BlackfireDumper; -class BlackfireTest extends AbstractTest +class BlackfireTest extends ProfilerTestCase { public function testDump() { diff --git a/tests/Profiler/Dumper/HtmlTest.php b/tests/Profiler/Dumper/HtmlTest.php index 2dcbb9aec57..64e459bab22 100644 --- a/tests/Profiler/Dumper/HtmlTest.php +++ b/tests/Profiler/Dumper/HtmlTest.php @@ -13,7 +13,7 @@ use Twig\Profiler\Dumper\HtmlDumper; -class HtmlTest extends AbstractTest +class HtmlTest extends ProfilerTestCase { public function testDump() { diff --git a/tests/Profiler/Dumper/AbstractTest.php b/tests/Profiler/Dumper/ProfilerTestCase.php similarity index 98% rename from tests/Profiler/Dumper/AbstractTest.php rename to tests/Profiler/Dumper/ProfilerTestCase.php index 1891c27507a..aa5b0510930 100644 --- a/tests/Profiler/Dumper/AbstractTest.php +++ b/tests/Profiler/Dumper/ProfilerTestCase.php @@ -14,7 +14,7 @@ use PHPUnit\Framework\TestCase; use Twig\Profiler\Profile; -abstract class AbstractTest extends TestCase +abstract class ProfilerTestCase extends TestCase { protected function getProfile() { diff --git a/tests/Profiler/Dumper/TextTest.php b/tests/Profiler/Dumper/TextTest.php index ba19c2c90dc..e488edc0649 100644 --- a/tests/Profiler/Dumper/TextTest.php +++ b/tests/Profiler/Dumper/TextTest.php @@ -13,7 +13,7 @@ use Twig\Profiler\Dumper\TextDumper; -class TextTest extends AbstractTest +class TextTest extends ProfilerTestCase { public function testDump() { From a035204afb9530c3acaabbf0cecc060c5ee9327e Mon Sep 17 00:00:00 2001 From: "Alexander M. Turek" Date: Tue, 3 Sep 2024 17:18:47 +0200 Subject: [PATCH 424/812] Validate the input of CoreExtension::map() --- src/Extension/CoreExtension.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index 6d1a0ed543b..9dc6d6e6a34 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -1833,6 +1833,10 @@ public static function find(Environment $env, $array, $arrow) */ public static function map(Environment $env, $array, $arrow) { + if (!is_iterable($array)) { + throw new RuntimeError(\sprintf('The "map" filter expects a sequence/mapping or "Traversable", got "%s".', get_debug_type($array))); + } + self::checkArrowInSandbox($env, $arrow, 'map', 'filter'); $r = []; From db22abfadfa1ad3622cca5bb91b23654c3987c9c Mon Sep 17 00:00:00 2001 From: "Alexander M. Turek" Date: Tue, 3 Sep 2024 22:51:23 +0200 Subject: [PATCH 425/812] Fix tests that don't perform assertions --- tests/ExpressionParserTest.php | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/ExpressionParserTest.php b/tests/ExpressionParserTest.php index e94f8d1841a..94d67033e98 100644 --- a/tests/ExpressionParserTest.php +++ b/tests/ExpressionParserTest.php @@ -475,7 +475,7 @@ public function testNotReadyFunctionWithNoConstructor() $parser = new Parser($env); $parser->parse($env->tokenize(new Source('{{ foo() }}', 'index'))); - $this->doesNotPerformAssertions(); + $this->expectNotToPerformAssertions(); } public function testNotReadyFilterWithNoConstructor() @@ -485,7 +485,7 @@ public function testNotReadyFilterWithNoConstructor() $parser = new Parser($env); $parser->parse($env->tokenize(new Source('{{ 1|foo }}', 'index'))); - $this->doesNotPerformAssertions(); + $this->expectNotToPerformAssertions(); } public function testNotReadyTestWithNoConstructor() @@ -495,7 +495,7 @@ public function testNotReadyTestWithNoConstructor() $parser = new Parser($env); $parser->parse($env->tokenize(new Source('{{ 1 is foo }}', 'index'))); - $this->doesNotPerformAssertions(); + $this->expectNotToPerformAssertions(); } /** @@ -550,7 +550,7 @@ public function testReadyFunction() $parser = new Parser($env); $parser->parse($env->tokenize(new Source('{{ foo() }}', 'index'))); - $this->doesNotPerformAssertions(); + $this->expectNotToPerformAssertions(); } public function testReadyFilter() @@ -560,7 +560,7 @@ public function testReadyFilter() $parser = new Parser($env); $parser->parse($env->tokenize(new Source('{{ 1|foo }}', 'index'))); - $this->doesNotPerformAssertions(); + $this->expectNotToPerformAssertions(); } public function testReadyTest() @@ -570,7 +570,7 @@ public function testReadyTest() $parser = new Parser($env); $parser->parse($env->tokenize(new Source('{{ 1 is foo }}', 'index'))); - $this->doesNotPerformAssertions(); + $this->expectNotToPerformAssertions(); } public function testTwoWordTestPrecedence() @@ -581,7 +581,7 @@ public function testTwoWordTestPrecedence() $parser = new Parser($env); $parser->parse($env->tokenize(new Source('{{ 1 is empty element }}', 'index'))); - $this->doesNotPerformAssertions(); + $this->expectNotToPerformAssertions(); } private static function createNameExpression(string $name, array $attributes): NameExpression From 797e49035654c9cb3bcbaef89057136c26286daf Mon Sep 17 00:00:00 2001 From: Alexandre Daubois Date: Wed, 4 Sep 2024 15:20:36 +0200 Subject: [PATCH 426/812] Improve exception expectations reliability --- tests/Cache/FilesystemTest.php | 18 ++++---- tests/CustomExtensionTest.php | 5 ++- tests/EnvironmentTest.php | 11 ++--- tests/ExpressionParserTest.php | 66 ++++++++++++++-------------- tests/Extension/SandboxTest.php | 13 +++--- tests/LexerTest.php | 19 ++++---- tests/Loader/ArrayTest.php | 9 ++-- tests/Loader/ChainTest.php | 6 +-- tests/Node/Expression/CallTest.php | 28 ++++++++---- tests/Node/Expression/FilterTest.php | 14 +++--- tests/ParserTest.php | 17 +++---- tests/TemplateTest.php | 19 ++++---- tests/TokenStreamTest.php | 14 +++--- 13 files changed, 128 insertions(+), 111 deletions(-) diff --git a/tests/Cache/FilesystemTest.php b/tests/Cache/FilesystemTest.php index dbb085b3fb3..e880562fff9 100644 --- a/tests/Cache/FilesystemTest.php +++ b/tests/Cache/FilesystemTest.php @@ -81,9 +81,6 @@ public function testWrite() public function testWriteFailMkdir() { - $this->expectException(\RuntimeException::class); - $this->expectExceptionMessage('Unable to create the cache directory'); - if (\defined('PHP_WINDOWS_VERSION_BUILD')) { $this->markTestSkipped('Read-only directories not possible on Windows.'); } @@ -97,14 +94,14 @@ public function testWriteFailMkdir() @mkdir($this->directory, 0555, true); $this->assertDirectoryExists($this->directory); + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('Unable to create the cache directory'); + $this->cache->write($key, $content); } public function testWriteFailDirWritable() { - $this->expectException(\RuntimeException::class); - $this->expectExceptionMessage('Unable to write in the cache directory'); - if (\defined('PHP_WINDOWS_VERSION_BUILD')) { $this->markTestSkipped('Read-only directories not possible on Windows.'); } @@ -120,14 +117,14 @@ public function testWriteFailDirWritable() @mkdir($this->directory.'/cache', 0555); $this->assertDirectoryExists($this->directory.'/cache'); + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('Unable to write in the cache directory'); + $this->cache->write($key, $content); } public function testWriteFailWriteFile() { - $this->expectException(\RuntimeException::class); - $this->expectExceptionMessage('Failed to write cache file'); - $key = $this->directory.'/cache/cachefile.php'; $content = $this->generateSource(); @@ -137,6 +134,9 @@ public function testWriteFailWriteFile() @mkdir($key, 0777, true); $this->assertDirectoryExists($key); + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('Failed to write cache file'); + $this->cache->write($key, $content); } diff --git a/tests/CustomExtensionTest.php b/tests/CustomExtensionTest.php index fab7813b11c..40be3b38293 100644 --- a/tests/CustomExtensionTest.php +++ b/tests/CustomExtensionTest.php @@ -23,11 +23,12 @@ class CustomExtensionTest extends TestCase */ public function testGetInvalidOperators(ExtensionInterface $extension, $expectedExceptionMessage) { + $env = new Environment(new ArrayLoader()); + $env->addExtension($extension); + $this->expectException(\InvalidArgumentException::class); $this->expectExceptionMessage($expectedExceptionMessage); - $env = new Environment(new ArrayLoader()); - $env->addExtension($extension); $env->getUnaryOperators(); } diff --git a/tests/EnvironmentTest.php b/tests/EnvironmentTest.php index a5f082ecb99..ff8dc741bc1 100644 --- a/tests/EnvironmentTest.php +++ b/tests/EnvironmentTest.php @@ -333,12 +333,12 @@ public function testAddMockExtension() public function testOverrideExtension() { + $twig = new Environment(new ArrayLoader()); + $twig->addExtension(new EnvironmentTest_Extension()); + $this->expectException(\LogicException::class); $this->expectExceptionMessage('Unable to register extension "Twig\Tests\EnvironmentTest_Extension" as it is already registered.'); - $twig = new Environment(new ArrayLoader()); - - $twig->addExtension(new EnvironmentTest_Extension()); $twig->addExtension(new EnvironmentTest_Extension()); } @@ -370,11 +370,12 @@ public function testAddRuntimeLoader() public function testFailLoadTemplate() { + $template = 'testFailLoadTemplate.twig'; + $twig = new Environment(new ArrayLoader([$template => false])); + $this->expectException(RuntimeError::class); $this->expectExceptionMessage('Failed to load Twig template "testFailLoadTemplate.twig", index "112233": cache might be corrupted in "testFailLoadTemplate.twig".'); - $template = 'testFailLoadTemplate.twig'; - $twig = new Environment(new ArrayLoader([$template => false])); $twig->loadTemplate($twig->getTemplateClass($template), $template, 112233); } diff --git a/tests/ExpressionParserTest.php b/tests/ExpressionParserTest.php index 94d67033e98..82ff47d44de 100644 --- a/tests/ExpressionParserTest.php +++ b/tests/ExpressionParserTest.php @@ -41,10 +41,10 @@ class ExpressionParserTest extends TestCase */ public function testCanOnlyAssignToNames($template) { - $this->expectException(SyntaxError::class); - $env = new Environment(new ArrayLoader(), ['cache' => false, 'autoescape' => false]); $parser = new Parser($env); + + $this->expectException(SyntaxError::class); $parser->parse($env->tokenize(new Source($template, 'index'))); } @@ -84,10 +84,10 @@ public function testSequenceExpression($template, $expected) */ public function testSequenceSyntaxError($template) { - $this->expectException(SyntaxError::class); - $env = new Environment(new ArrayLoader(), ['cache' => false, 'autoescape' => false]); $parser = new Parser($env); + + $this->expectException(SyntaxError::class); $parser->parse($env->tokenize(new Source($template, 'index'))); } @@ -210,12 +210,11 @@ public static function getTestsForSequence() public function testStringExpressionDoesNotConcatenateTwoConsecutiveStrings() { - $this->expectException(SyntaxError::class); - $env = new Environment(new ArrayLoader(), ['cache' => false, 'autoescape' => false, 'optimizations' => 0]); $stream = $env->tokenize(new Source('{{ "a" "b" }}', 'index')); $parser = new Parser($env); + $this->expectException(SyntaxError::class); $parser->parse($stream); } @@ -278,32 +277,30 @@ public static function getTestsForString() public function testAttributeCallDoesNotSupportNamedArguments() { - $this->expectException(SyntaxError::class); - $env = new Environment(new ArrayLoader(), ['cache' => false, 'autoescape' => false]); $parser = new Parser($env); + $this->expectException(SyntaxError::class); $parser->parse($env->tokenize(new Source('{{ foo.bar(name="Foo") }}', 'index'))); } public function testMacroCallDoesNotSupportNamedArguments() { - $this->expectException(SyntaxError::class); - $env = new Environment(new ArrayLoader(), ['cache' => false, 'autoescape' => false]); $parser = new Parser($env); + $this->expectException(SyntaxError::class); $parser->parse($env->tokenize(new Source('{% from _self import foo %}{% macro foo() %}{% endmacro %}{{ foo(name="Foo") }}', 'index'))); } public function testMacroDefinitionDoesNotSupportNonNameVariableName() { - $this->expectException(SyntaxError::class); - $this->expectExceptionMessage('An argument must be a name. Unexpected token "string" of value "a" ("name" expected) in "index" at line 1.'); - $env = new Environment(new ArrayLoader(), ['cache' => false, 'autoescape' => false]); $parser = new Parser($env); + $this->expectException(SyntaxError::class); + $this->expectExceptionMessage('An argument must be a name. Unexpected token "string" of value "a" ("name" expected) in "index" at line 1.'); + $parser->parse($env->tokenize(new Source('{% macro foo("a") %}{% endmacro %}', 'index'))); } @@ -312,12 +309,12 @@ public function testMacroDefinitionDoesNotSupportNonNameVariableName() */ public function testMacroDefinitionDoesNotSupportNonConstantDefaultValues($template) { - $this->expectException(SyntaxError::class); - $this->expectExceptionMessage('A default value for an argument must be a constant (a boolean, a string, a number, a sequence, or a mapping) in "index" at line 1'); - $env = new Environment(new ArrayLoader(), ['cache' => false, 'autoescape' => false]); $parser = new Parser($env); + $this->expectException(SyntaxError::class); + $this->expectExceptionMessage('A default value for an argument must be a constant (a boolean, a string, a number, a sequence, or a mapping) in "index" at line 1'); + $parser->parse($env->tokenize(new Source($template, 'index'))); } @@ -359,67 +356,68 @@ public static function getMacroDefinitionSupportsConstantDefaultValues() public function testUnknownFunction() { - $this->expectException(SyntaxError::class); - $this->expectExceptionMessage('Unknown "cycl" function. Did you mean "cycle" in "index" at line 1?'); - $env = new Environment(new ArrayLoader(), ['cache' => false, 'autoescape' => false]); $parser = new Parser($env); + $this->expectException(SyntaxError::class); + $this->expectExceptionMessage('Unknown "cycl" function. Did you mean "cycle" in "index" at line 1?'); + $parser->parse($env->tokenize(new Source('{{ cycl() }}', 'index'))); } public function testUnknownFunctionWithoutSuggestions() { - $this->expectException(SyntaxError::class); - $this->expectExceptionMessage('Unknown "foobar" function in "index" at line 1.'); - $env = new Environment(new ArrayLoader(), ['cache' => false, 'autoescape' => false]); $parser = new Parser($env); + $this->expectException(SyntaxError::class); + $this->expectExceptionMessage('Unknown "foobar" function in "index" at line 1.'); + $parser->parse($env->tokenize(new Source('{{ foobar() }}', 'index'))); } public function testUnknownFilter() { - $this->expectException(SyntaxError::class); - $this->expectExceptionMessage('Unknown "lowe" filter. Did you mean "lower" in "index" at line 1?'); - $env = new Environment(new ArrayLoader(), ['cache' => false, 'autoescape' => false]); $parser = new Parser($env); + $this->expectException(SyntaxError::class); + $this->expectExceptionMessage('Unknown "lowe" filter. Did you mean "lower" in "index" at line 1?'); + $parser->parse($env->tokenize(new Source('{{ 1|lowe }}', 'index'))); } public function testUnknownFilterWithoutSuggestions() { - $this->expectException(SyntaxError::class); - $this->expectExceptionMessage('Unknown "foobar" filter in "index" at line 1.'); - $env = new Environment(new ArrayLoader(), ['cache' => false, 'autoescape' => false]); $parser = new Parser($env); + $this->expectException(SyntaxError::class); + $this->expectExceptionMessage('Unknown "foobar" filter in "index" at line 1.'); + $parser->parse($env->tokenize(new Source('{{ 1|foobar }}', 'index'))); } public function testUnknownTest() { - $this->expectException(SyntaxError::class); - $this->expectExceptionMessage('Unknown "nul" test. Did you mean "null" in "index" at line 1'); - $env = new Environment(new ArrayLoader(), ['cache' => false, 'autoescape' => false]); $parser = new Parser($env); $stream = $env->tokenize(new Source('{{ 1 is nul }}', 'index')); + + $this->expectException(SyntaxError::class); + $this->expectExceptionMessage('Unknown "nul" test. Did you mean "null" in "index" at line 1'); + $parser->parse($stream); } public function testUnknownTestWithoutSuggestions() { - $this->expectException(SyntaxError::class); - $this->expectExceptionMessage('Unknown "foobar" test in "index" at line 1.'); - $env = new Environment(new ArrayLoader(), ['cache' => false, 'autoescape' => false]); $parser = new Parser($env); + $this->expectException(SyntaxError::class); + $this->expectExceptionMessage('Unknown "foobar" test in "index" at line 1.'); + $parser->parse($env->tokenize(new Source('{{ 1 is foobar }}', 'index'))); } diff --git a/tests/Extension/SandboxTest.php b/tests/Extension/SandboxTest.php index 810bbf74e58..70052b859c8 100644 --- a/tests/Extension/SandboxTest.php +++ b/tests/Extension/SandboxTest.php @@ -14,6 +14,7 @@ use PHPUnit\Framework\TestCase; use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; use Twig\Environment; +use Twig\Error\RuntimeError; use Twig\Error\SyntaxError; use Twig\Extension\SandboxExtension; use Twig\Extension\StringLoaderExtension; @@ -72,10 +73,11 @@ protected function setUp(): void */ public function testSandboxForCoreTags(string $tag, string $template) { + $twig = $this->getEnvironment(true, [], self::$templates, []); + $this->expectException(SecurityError::class); $this->expectExceptionMessageMatches(sprintf('/Tag "%s" is not allowed in "index \(string template .+?\)" at line 1/', $tag)); - $twig = $this->getEnvironment(true, [], self::$templates, []); $twig->createTemplate($template, 'index')->render([]); } @@ -124,10 +126,11 @@ public static function getSandboxedForExtendsAndUseTagsTests() public function testSandboxWithInheritance() { + $twig = $this->getEnvironment(true, [], self::$templates, ['extends', 'block']); + $this->expectException(SecurityError::class); $this->expectExceptionMessage('Filter "json_encode" is not allowed in "1_child" at line 3.'); - $twig = $this->getEnvironment(true, [], self::$templates, ['extends', 'block']); $twig->load('1_child')->render([]); } @@ -441,14 +444,14 @@ public function testSandboxDisabledAfterIncludeFunctionError() public function testSandboxWithNoClosureFilter() { - $this->expectException('\Twig\Error\RuntimeError'); - $this->expectExceptionMessage('The callable passed to the "filter" filter must be a Closure in sandbox mode in "index" at line 1.'); - $twig = $this->getEnvironment(true, ['autoescape' => 'html'], ['index' => <<expectException(RuntimeError::class); + $this->expectExceptionMessage('The callable passed to the "filter" filter must be a Closure in sandbox mode in "index" at line 1.'); + $twig->load('index')->render([]); } diff --git a/tests/LexerTest.php b/tests/LexerTest.php index 4763731300c..d8da5545821 100644 --- a/tests/LexerTest.php +++ b/tests/LexerTest.php @@ -319,12 +319,12 @@ public function testStringWithHash() public function testStringWithUnterminatedInterpolation() { + $template = '{{ "bar #{x" }}'; + $lexer = new Lexer(new Environment(new ArrayLoader())); + $this->expectException(SyntaxError::class); $this->expectExceptionMessage('Unclosed """'); - $template = '{{ "bar #{x" }}'; - - $lexer = new Lexer(new Environment(new ArrayLoader())); $lexer->tokenize(new Source($template, 'index')); } @@ -388,9 +388,6 @@ public function testOperatorEndingWithALetterAtTheEndOfALine() public function testUnterminatedVariable() { - $this->expectException(SyntaxError::class); - $this->expectExceptionMessage('Unclosed "variable" in "index" at line 3'); - $template = ' {{ @@ -401,14 +398,14 @@ public function testUnterminatedVariable() '; $lexer = new Lexer(new Environment(new ArrayLoader())); + + $this->expectException(SyntaxError::class); + $this->expectExceptionMessage('Unclosed "variable" in "index" at line 3'); $lexer->tokenize(new Source($template, 'index')); } public function testUnterminatedBlock() { - $this->expectException(SyntaxError::class); - $this->expectExceptionMessage('Unclosed "block" in "index" at line 3'); - $template = ' {% @@ -419,6 +416,10 @@ public function testUnterminatedBlock() '; $lexer = new Lexer(new Environment(new ArrayLoader())); + + $this->expectException(SyntaxError::class); + $this->expectExceptionMessage('Unclosed "block" in "index" at line 3'); + $lexer->tokenize(new Source($template, 'index')); } diff --git a/tests/Loader/ArrayTest.php b/tests/Loader/ArrayTest.php index 543fe9ff6a9..478adf9317c 100644 --- a/tests/Loader/ArrayTest.php +++ b/tests/Loader/ArrayTest.php @@ -19,10 +19,9 @@ class ArrayTest extends TestCase { public function testGetSourceContextWhenTemplateDoesNotExist() { - $this->expectException(LoaderError::class); - $loader = new ArrayLoader(); + $this->expectException(LoaderError::class); $loader->getSourceContext('foo'); } @@ -57,10 +56,9 @@ public function testGetCacheKeyIsProtectedFromEdgeCollisions() public function testGetCacheKeyWhenTemplateDoesNotExist() { - $this->expectException(LoaderError::class); - $loader = new ArrayLoader(); + $this->expectException(LoaderError::class); $loader->getCacheKey('foo'); } @@ -80,10 +78,9 @@ public function testIsFresh() public function testIsFreshWhenTemplateDoesNotExist() { - $this->expectException(LoaderError::class); - $loader = new ArrayLoader(); + $this->expectException(LoaderError::class); $loader->isFresh('foo', time()); } } diff --git a/tests/Loader/ChainTest.php b/tests/Loader/ChainTest.php index 52d6d4c7202..16ccb9473c2 100644 --- a/tests/Loader/ChainTest.php +++ b/tests/Loader/ChainTest.php @@ -43,10 +43,9 @@ public function testGetSourceContext() public function testGetSourceContextWhenTemplateDoesNotExist() { - $this->expectException(LoaderError::class); - $loader = new ChainLoader([]); + $this->expectException(LoaderError::class); $loader->getSourceContext('foo'); } @@ -63,10 +62,9 @@ public function testGetCacheKey() public function testGetCacheKeyWhenTemplateDoesNotExist() { - $this->expectException(LoaderError::class); - $loader = new ChainLoader([]); + $this->expectException(LoaderError::class); $loader->getCacheKey('foo'); } diff --git a/tests/Node/Expression/CallTest.php b/tests/Node/Expression/CallTest.php index a1ea76fd445..75cac054734 100644 --- a/tests/Node/Expression/CallTest.php +++ b/tests/Node/Expression/CallTest.php @@ -30,37 +30,41 @@ public function testGetArguments() public function testGetArgumentsWhenPositionalArgumentsAfterNamedArguments() { + $node = $this->createFunctionExpression('date', 'date'); + $this->expectException(SyntaxError::class); $this->expectExceptionMessage('Positional arguments cannot be used after named arguments for function "date".'); - $node = $this->createFunctionExpression('date', 'date'); $this->getArguments($node, ['date', ['timestamp' => 123456, 'Y-m-d']]); } public function testGetArgumentsWhenArgumentIsDefinedTwice() { + $node = $this->createFunctionExpression('date', 'date'); + $this->expectException(SyntaxError::class); $this->expectExceptionMessage('Argument "format" is defined twice for function "date".'); - $node = $this->createFunctionExpression('date', 'date'); $this->getArguments($node, ['date', ['Y-m-d', 'format' => 'U']]); } public function testGetArgumentsWithWrongNamedArgumentName() { + $node = $this->createFunctionExpression('date', 'date'); + $this->expectException(SyntaxError::class); $this->expectExceptionMessage('Unknown argument "unknown" for function "date(format, timestamp)".'); - $node = $this->createFunctionExpression('date', 'date'); $this->getArguments($node, ['date', ['Y-m-d', 'timestamp' => null, 'unknown' => '']]); } public function testGetArgumentsWithWrongNamedArgumentNames() { + $node = $this->createFunctionExpression('date', 'date'); + $this->expectException(SyntaxError::class); $this->expectExceptionMessage('Unknown arguments "unknown1", "unknown2" for function "date(format, timestamp)".'); - $node = $this->createFunctionExpression('date', 'date'); $this->getArguments($node, ['date', ['Y-m-d', 'timestamp' => null, 'unknown1' => '', 'unknown2' => '']]); } @@ -70,10 +74,11 @@ public function testResolveArgumentsWithMissingValueForOptionalArgument() $this->markTestSkipped('substr_compare() has a default value in 8.0, so the test does not work anymore, one should find another PHP built-in function for this test to work in PHP 8.'); } + $node = $this->createFunctionExpression('substr_compare', 'substr_compare'); + $this->expectException(SyntaxError::class); $this->expectExceptionMessage('Argument "case_sensitivity" could not be assigned for function "substr_compare(main_str, str, offset, length, case_sensitivity)" because it is mapped to an internal PHP function which cannot determine default value for optional argument "length".'); - $node = $this->createFunctionExpression('substr_compare', 'substr_compare'); $this->getArguments($node, ['substr_compare', ['abcd', 'bc', 'offset' => 1, 'case_sensitivity' => true]]); } @@ -91,36 +96,41 @@ public function testGetArgumentsForStaticMethod() public function testResolveArgumentsWithMissingParameterForArbitraryArguments() { + $node = $this->createFunctionExpression('foo', [$this, 'customFunctionWithArbitraryArguments'], true); + $this->expectException(\LogicException::class); $this->expectExceptionMessage('The last parameter of "Twig\\Tests\\Node\\Expression\\CallTest::customFunctionWithArbitraryArguments" for function "foo" must be an array with default value, eg. "array $arg = []".'); - $node = $this->createFunctionExpression('foo', [$this, 'customFunctionWithArbitraryArguments'], true); $this->getArguments($node, [[$this, 'customFunctionWithArbitraryArguments'], []]); } public function testGetArgumentsWithInvalidCallable() { + $node = $this->createFunctionExpression('foo', '', true); + $this->expectException(\LogicException::class); $this->expectExceptionMessage('Callback for function "foo" is not callable in the current scope.'); - $node = $this->createFunctionExpression('foo', '', true); + $this->getArguments($node, ['', []]); } public function testResolveArgumentsWithMissingParameterForArbitraryArgumentsOnFunction() { + $node = $this->createFunctionExpression('foo', 'Twig\Tests\Node\Expression\custom_call_test_function', true); + $this->expectException(\LogicException::class); $this->expectExceptionMessageMatches('#^The last parameter of "Twig\\\\Tests\\\\Node\\\\Expression\\\\custom_call_test_function" for function "foo" must be an array with default value, eg\\. "array \\$arg \\= \\[\\]"\\.$#'); - $node = $this->createFunctionExpression('foo', 'Twig\Tests\Node\Expression\custom_call_test_function', true); $this->getArguments($node, ['Twig\Tests\Node\Expression\custom_call_test_function', []]); } public function testResolveArgumentsWithMissingParameterForArbitraryArgumentsOnObject() { + $node = $this->createFunctionExpression('foo', new CallableTestClass(), true); + $this->expectException(\LogicException::class); $this->expectExceptionMessageMatches('#^The last parameter of "Twig\\\\Tests\\\\Node\\\\Expression\\\\CallableTestClass\\:\\:__invoke" for function "foo" must be an array with default value, eg\\. "array \\$arg \\= \\[\\]"\\.$#'); - $node = $this->createFunctionExpression('foo', new CallableTestClass(), true); $this->getArguments($node, [new CallableTestClass(), []]); } diff --git a/tests/Node/Expression/FilterTest.php b/tests/Node/Expression/FilterTest.php index ca792bc4d86..1339b07dd46 100644 --- a/tests/Node/Expression/FilterTest.php +++ b/tests/Node/Expression/FilterTest.php @@ -128,29 +128,31 @@ public static function provideTests(): iterable public function testCompileWithWrongNamedArgumentName() { - $this->expectException(SyntaxError::class); - $this->expectExceptionMessage('Unknown argument "foobar" for filter "date(format, timezone)" at line 1.'); - $date = new ConstantExpression(0, 1); $node = $this->createFilter($this->getEnvironment(), $date, 'date', [ 'foobar' => new ConstantExpression('America/Chicago', 1), ]); $compiler = $this->getCompiler(); + + $this->expectException(SyntaxError::class); + $this->expectExceptionMessage('Unknown argument "foobar" for filter "date(format, timezone)" at line 1.'); + $compiler->compile($node); } public function testCompileWithMissingNamedArgument() { - $this->expectException(SyntaxError::class); - $this->expectExceptionMessage('Value for argument "from" is required for filter "replace" at line 1.'); - $value = new ConstantExpression(0, 1); $node = $this->createFilter($this->getEnvironment(), $value, 'replace', [ 'to' => new ConstantExpression('foo', 1), ]); $compiler = $this->getCompiler(); + + $this->expectException(SyntaxError::class); + $this->expectExceptionMessage('Value for argument "from" is required for filter "replace" at line 1.'); + $compiler->compile($node); } diff --git a/tests/ParserTest.php b/tests/ParserTest.php index 72709283704..4d61c2341d0 100644 --- a/tests/ParserTest.php +++ b/tests/ParserTest.php @@ -29,9 +29,6 @@ class ParserTest extends TestCase { public function testUnknownTag() { - $this->expectException(SyntaxError::class); - $this->expectExceptionMessage('Unknown "foo" tag. Did you mean "for" at line 1?'); - $stream = new TokenStream([ new Token(Token::BLOCK_START_TYPE, '', 1), new Token(Token::NAME_TYPE, 'foo', 1), @@ -39,14 +36,15 @@ public function testUnknownTag() new Token(Token::EOF_TYPE, '', 1), ]); $parser = new Parser(new Environment(new ArrayLoader())); + + $this->expectException(SyntaxError::class); + $this->expectExceptionMessage('Unknown "foo" tag. Did you mean "for" at line 1?'); + $parser->parse($stream); } public function testUnknownTagWithoutSuggestions() { - $this->expectException(SyntaxError::class); - $this->expectExceptionMessage('Unknown "foobar" tag at line 1.'); - $stream = new TokenStream([ new Token(Token::BLOCK_START_TYPE, '', 1), new Token(Token::NAME_TYPE, 'foobar', 1), @@ -54,6 +52,10 @@ public function testUnknownTagWithoutSuggestions() new Token(Token::EOF_TYPE, '', 1), ]); $parser = new Parser(new Environment(new ArrayLoader())); + + $this->expectException(SyntaxError::class); + $this->expectExceptionMessage('Unknown "foobar" tag at line 1.'); + $parser->parse($stream); } @@ -92,13 +94,12 @@ public static function getFilterBodyNodesData() */ public function testFilterBodyNodesThrowsException($input) { - $this->expectException(SyntaxError::class); - $parser = $this->getParser(); $m = new \ReflectionMethod($parser, 'filterBodyNodes'); $m->setAccessible(true); + $this->expectException(SyntaxError::class); $m->invoke($parser, $input); } diff --git a/tests/TemplateTest.php b/tests/TemplateTest.php index 1f2a8d28165..801701af2cc 100644 --- a/tests/TemplateTest.php +++ b/tests/TemplateTest.php @@ -27,10 +27,10 @@ class TemplateTest extends TestCase { public function testDisplayBlocksAcceptTemplateOnlyAsBlocks() { - $this->expectException(\LogicException::class); - $twig = new Environment(new ArrayLoader()); $template = new TemplateForTest($twig); + + $this->expectException(\LogicException::class); $template->displayBlock('foo', [], ['foo' => [new \stdClass(), 'foo']]); } @@ -143,31 +143,34 @@ public static function getRenderTemplateWithoutOutputData() public function testRenderBlockWithUndefinedBlock() { + $twig = new Environment(new ArrayLoader()); + $template = new TemplateForTest($twig, 'index.twig'); + $this->expectException(RuntimeError::class); $this->expectExceptionMessage('Block "unknown" on template "index.twig" does not exist in "index.twig".'); - $twig = new Environment(new ArrayLoader()); - $template = new TemplateForTest($twig, 'index.twig'); $template->renderBlock('unknown', []); } public function testDisplayBlockWithUndefinedBlock() { + $twig = new Environment(new ArrayLoader()); + $template = new TemplateForTest($twig, 'index.twig'); + $this->expectException(RuntimeError::class); $this->expectExceptionMessage('Block "unknown" on template "index.twig" does not exist in "index.twig".'); - $twig = new Environment(new ArrayLoader()); - $template = new TemplateForTest($twig, 'index.twig'); $template->displayBlock('unknown', []); } public function testDisplayBlockWithUndefinedParentBlock() { + $twig = new Environment(new ArrayLoader()); + $template = new TemplateForTest($twig, 'parent.twig'); + $this->expectException(RuntimeError::class); $this->expectExceptionMessage('Block "foo" should not call parent() in "index.twig" as the block does not exist in the parent template "parent.twig"'); - $twig = new Environment(new ArrayLoader()); - $template = new TemplateForTest($twig, 'parent.twig'); $template->displayBlock('foo', [], ['foo' => [new TemplateForTest($twig, 'index.twig'), 'block_foo']], false); } diff --git a/tests/TokenStreamTest.php b/tests/TokenStreamTest.php index e8cb474b38f..8f86ac87a7b 100644 --- a/tests/TokenStreamTest.php +++ b/tests/TokenStreamTest.php @@ -48,12 +48,13 @@ public function testNext() public function testEndOfTemplateNext() { - $this->expectException(SyntaxError::class); - $this->expectExceptionMessage('Unexpected end of template'); - $stream = new TokenStream([ new Token(Token::BLOCK_START_TYPE, 1, 1), ]); + + $this->expectException(SyntaxError::class); + $this->expectExceptionMessage('Unexpected end of template'); + while (!$stream->isEOF()) { $stream->next(); } @@ -61,12 +62,13 @@ public function testEndOfTemplateNext() public function testEndOfTemplateLook() { - $this->expectException(SyntaxError::class); - $this->expectExceptionMessage('Unexpected end of template'); - $stream = new TokenStream([ new Token(Token::BLOCK_START_TYPE, 1, 1), ]); + + $this->expectException(SyntaxError::class); + $this->expectExceptionMessage('Unexpected end of template'); + while (!$stream->isEOF()) { $stream->look(); $stream->next(); From 4cd8955f74fdecbb91b35cd1c898e08c0075dd53 Mon Sep 17 00:00:00 2001 From: Ruud Kamphuis Date: Thu, 5 Sep 2024 09:24:38 +0200 Subject: [PATCH 427/812] Fix iterable return type Currently, the PHPDoc states that a string should be returned, but that's not correct. It can be anything that is echo-able. That means any scalar, Stringable and even null. --- src/Node/BlockNode.php | 2 +- src/Template.php | 8 ++++---- tests/Node/BlockTest.php | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Node/BlockNode.php b/src/Node/BlockNode.php index 3c06f155be8..b4f939cf630 100644 --- a/src/Node/BlockNode.php +++ b/src/Node/BlockNode.php @@ -33,7 +33,7 @@ public function compile(Compiler $compiler): void $compiler ->addDebugInfo($this) ->write("/**\n") - ->write(" * @return iterable\n") + ->write(" * @return iterable\n") ->write(" */\n") ->write(\sprintf("public function block_%s(array \$context, array \$blocks = []): iterable\n", $this->getAttribute('name')), "{\n") ->indent() diff --git a/src/Template.php b/src/Template.php index 2dc05686dd5..e6bf2486aaf 100644 --- a/src/Template.php +++ b/src/Template.php @@ -382,7 +382,7 @@ public function render(array $context): string } /** - * @return iterable + * @return iterable */ public function yield(array $context, array $blocks = []): iterable { @@ -412,7 +412,7 @@ public function yield(array $context, array $blocks = []): iterable } /** - * @return iterable + * @return iterable */ public function yieldBlock($name, array $context, array $blocks = [], $useBlocks = true, ?self $templateContext = null): iterable { @@ -472,7 +472,7 @@ public function yieldBlock($name, array $context, array $blocks = [], $useBlocks * @param array $context The context * @param array $blocks The current set of blocks * - * @return iterable + * @return iterable */ public function yieldParentBlock($name, array $context, array $blocks = []): iterable { @@ -491,7 +491,7 @@ public function yieldParentBlock($name, array $context, array $blocks = []): ite * @param array $context An array of parameters to pass to the template * @param array $blocks An array of blocks to pass to the template * - * @return iterable + * @return iterable */ abstract protected function doDisplay(array $context, array $blocks = []): iterable; } diff --git a/tests/Node/BlockTest.php b/tests/Node/BlockTest.php index af110e32a86..2304c8abd03 100644 --- a/tests/Node/BlockTest.php +++ b/tests/Node/BlockTest.php @@ -34,7 +34,7 @@ public static function provideTests(): iterable $tests[] = [new BlockNode('foo', new TextNode('foo', 1), 1), << + * @return iterable */ public function block_foo(array \$context, array \$blocks = []): iterable { From 2b887e64d5e823376d965e500604fc31e3c8aae6 Mon Sep 17 00:00:00 2001 From: Ruud Kamphuis Date: Fri, 6 Sep 2024 09:42:11 +0200 Subject: [PATCH 428/812] Fix isset in ForLoopNode Even though the code works perfectly fine, PHPStan doesn't understand it. Since we want to work with `revindex` and `revindex0`, we can better check for their existence, instead of `length`. --- src/Node/ForLoopNode.php | 2 +- tests/Node/ForTest.php | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Node/ForLoopNode.php b/src/Node/ForLoopNode.php index 3e044bbb09f..1f0a4f32134 100644 --- a/src/Node/ForLoopNode.php +++ b/src/Node/ForLoopNode.php @@ -38,7 +38,7 @@ public function compile(Compiler $compiler): void ->write("++\$context['loop']['index0'];\n") ->write("++\$context['loop']['index'];\n") ->write("\$context['loop']['first'] = false;\n") - ->write("if (isset(\$context['loop']['length'])) {\n") + ->write("if (isset(\$context['loop']['revindex0'], \$context['loop']['revindex'])) {\n") ->indent() ->write("--\$context['loop']['revindex0'];\n") ->write("--\$context['loop']['revindex'];\n") diff --git a/tests/Node/ForTest.php b/tests/Node/ForTest.php index c4c11253d00..047c2a1092d 100644 --- a/tests/Node/ForTest.php +++ b/tests/Node/ForTest.php @@ -101,7 +101,7 @@ public static function provideTests(): iterable ++\$context['loop']['index0']; ++\$context['loop']['index']; \$context['loop']['first'] = false; - if (isset(\$context['loop']['length'])) { + if (isset(\$context['loop']['revindex0'], \$context['loop']['revindex'])) { --\$context['loop']['revindex0']; --\$context['loop']['revindex']; \$context['loop']['last'] = 0 === \$context['loop']['revindex0']; @@ -143,7 +143,7 @@ public static function provideTests(): iterable ++\$context['loop']['index0']; ++\$context['loop']['index']; \$context['loop']['first'] = false; - if (isset(\$context['loop']['length'])) { + if (isset(\$context['loop']['revindex0'], \$context['loop']['revindex'])) { --\$context['loop']['revindex0']; --\$context['loop']['revindex']; \$context['loop']['last'] = 0 === \$context['loop']['revindex0']; @@ -187,7 +187,7 @@ public static function provideTests(): iterable ++\$context['loop']['index0']; ++\$context['loop']['index']; \$context['loop']['first'] = false; - if (isset(\$context['loop']['length'])) { + if (isset(\$context['loop']['revindex0'], \$context['loop']['revindex'])) { --\$context['loop']['revindex0']; --\$context['loop']['revindex']; \$context['loop']['last'] = 0 === \$context['loop']['revindex0']; From 2ae0c0d38c9042ed1bfcdef53c57eca46c0e7dd5 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Fri, 6 Sep 2024 12:33:38 +0200 Subject: [PATCH 429/812] Fix CS --- src/ExpressionParser.php | 2 +- src/Extension/CoreExtension.php | 2 +- src/Node/ImportNode.php | 6 +++--- src/Node/Node.php | 6 +++--- src/Template.php | 8 ++++---- src/Test/NodeTestCase.php | 2 +- src/TokenParser/TypesTokenParser.php | 4 ++-- tests/EnvironmentTest.php | 2 +- tests/Extension/CoreTest.php | 5 +++-- tests/Extension/SandboxTest.php | 8 ++++---- tests/Node/Expression/FilterTest.php | 2 +- tests/ParserTest.php | 2 +- tests/TemplateTest.php | 4 ++-- tests/TokenParser/TypesTokenParserTest.php | 8 ++++---- 14 files changed, 31 insertions(+), 30 deletions(-) diff --git a/src/ExpressionParser.php b/src/ExpressionParser.php index 8cd8b194a18..dc4a6015d7b 100644 --- a/src/ExpressionParser.php +++ b/src/ExpressionParser.php @@ -568,7 +568,7 @@ public function parseFilterExpression($node) public function parseFilterExpressionRaw($node) { - if (func_num_args() > 1) { + if (\func_num_args() > 1) { trigger_deprecation('twig/twig', '3.12', 'Passing a second argument to "%s()" is deprecated.', __METHOD__); } diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index 9dc6d6e6a34..ee0a1dbaf47 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -348,7 +348,7 @@ public static function cycle($values, $position): mixed throw new RuntimeError('The "cycle" function expects an array or "ArrayAccess" as first argument.'); } - if (!\is_countable($values)) { + if (!is_countable($values)) { // To be uncommented in 4.0 // throw new RuntimeError('The "cycle" function expects a countable sequence as first argument.'); diff --git a/src/Node/ImportNode.php b/src/Node/ImportNode.php index 31acbe9d19d..9a6033f215b 100644 --- a/src/Node/ImportNode.php +++ b/src/Node/ImportNode.php @@ -29,10 +29,10 @@ class ImportNode extends Node */ public function __construct(AbstractExpression $expr, AbstractExpression $var, int $lineno, $global = true) { - if (null === $global || is_string($global)) { + if (null === $global || \is_string($global)) { trigger_deprecation('twig/twig', '3.12', 'Passing a tag to %s() is deprecated.', __METHOD__); - $global = func_num_args() > 4 ? func_get_arg(4) : true; - } elseif (!is_bool($global)) { + $global = \func_num_args() > 4 ? func_get_arg(4) : true; + } elseif (!\is_bool($global)) { throw new \TypeError(\sprintf('Argument 4 passed to "%s()" must be a boolean, "%s" given.', __METHOD__, get_debug_type($global))); } diff --git a/src/Node/Node.php b/src/Node/Node.php index 38ebdfa8c68..2ccbcf6101f 100644 --- a/src/Node/Node.php +++ b/src/Node/Node.php @@ -47,15 +47,15 @@ public function __construct(array $nodes = [], array $attributes = [], int $line { foreach ($nodes as $name => $node) { if (!$node instanceof self) { - throw new \InvalidArgumentException(\sprintf('Using "%s" for the value of node "%s" of "%s" is not supported. You must pass a \Twig\Node\Node instance.', \is_object($node) ? \get_class($node) : (null === $node ? 'null' : \gettype($node)), $name, static::class)); + throw new \InvalidArgumentException(\sprintf('Using "%s" for the value of node "%s" of "%s" is not supported. You must pass a \Twig\Node\Node instance.', \is_object($node) ? $node::class : (null === $node ? 'null' : \gettype($node)), $name, static::class)); } } $this->nodes = $nodes; $this->attributes = $attributes; $this->lineno = $lineno; - if (func_num_args() > 3) { - trigger_deprecation('twig/twig', '3.12', sprintf('The "tag" constructor argument of the "%s" class is deprecated and ignored (check which TokenParser class set it to "%s"), the tag is now automatically set by the Parser when needed.', static::class, func_get_arg(3) ?: 'null')); + if (\func_num_args() > 3) { + trigger_deprecation('twig/twig', '3.12', \sprintf('The "tag" constructor argument of the "%s" class is deprecated and ignored (check which TokenParser class set it to "%s"), the tag is now automatically set by the Parser when needed.', static::class, func_get_arg(3) ?: 'null')); } } diff --git a/src/Template.php b/src/Template.php index e6bf2486aaf..e8368c5d964 100644 --- a/src/Template.php +++ b/src/Template.php @@ -382,7 +382,7 @@ public function render(array $context): string } /** - * @return iterable + * @return iterable */ public function yield(array $context, array $blocks = []): iterable { @@ -412,7 +412,7 @@ public function yield(array $context, array $blocks = []): iterable } /** - * @return iterable + * @return iterable */ public function yieldBlock($name, array $context, array $blocks = [], $useBlocks = true, ?self $templateContext = null): iterable { @@ -472,7 +472,7 @@ public function yieldBlock($name, array $context, array $blocks = [], $useBlocks * @param array $context The context * @param array $blocks The current set of blocks * - * @return iterable + * @return iterable */ public function yieldParentBlock($name, array $context, array $blocks = []): iterable { @@ -491,7 +491,7 @@ public function yieldParentBlock($name, array $context, array $blocks = []): ite * @param array $context An array of parameters to pass to the template * @param array $blocks An array of blocks to pass to the template * - * @return iterable + * @return iterable */ abstract protected function doDisplay(array $context, array $blocks = []): iterable; } diff --git a/src/Test/NodeTestCase.php b/src/Test/NodeTestCase.php index 09b16ace793..bac0ea6d036 100644 --- a/src/Test/NodeTestCase.php +++ b/src/Test/NodeTestCase.php @@ -118,7 +118,7 @@ final protected static function createAttributeGetter(): string final public static function checkDataProvider(): void { $r = new \ReflectionMethod(static::class, 'getTests'); - if ($r->getDeclaringClass()->getName() !== self::class) { + if (self::class !== $r->getDeclaringClass()->getName()) { trigger_deprecation('twig/twig', '3.13', 'Implementing "%s::getTests()" in "%s" is deprecated, implement "provideTests()" instead.', self::class, static::class); } } diff --git a/src/TokenParser/TypesTokenParser.php b/src/TokenParser/TypesTokenParser.php index b579fd0c04e..2172ed26361 100644 --- a/src/TokenParser/TypesTokenParser.php +++ b/src/TokenParser/TypesTokenParser.php @@ -62,14 +62,14 @@ private function parseSimpleMappingExpression(TokenStream $stream): array $first = false; $nameToken = $stream->expect(Token::NAME_TYPE); - $isOptional = $stream->nextIf(Token::PUNCTUATION_TYPE, '?') !== null; + $isOptional = null !== $stream->nextIf(Token::PUNCTUATION_TYPE, '?'); $stream->expect(Token::PUNCTUATION_TYPE, ':', 'A type name must be followed by a colon (:)'); $valueToken = $stream->expect(Token::STRING_TYPE); $types[$nameToken->getValue()] = [ - 'type' => $valueToken->getValue(), + 'type' => $valueToken->getValue(), 'optional' => $isOptional, ]; } diff --git a/tests/EnvironmentTest.php b/tests/EnvironmentTest.php index ff8dc741bc1..5f8d21bb92a 100644 --- a/tests/EnvironmentTest.php +++ b/tests/EnvironmentTest.php @@ -178,7 +178,7 @@ public function testExtensionsAreNotInitializedWhenRenderingACompiledTemplate() // force compilation $twig = new Environment($loader = new ArrayLoader(['index' => '{{ foo }}']), $options); - $twig->addExtension($extension = new class extends AbstractExtension { + $twig->addExtension($extension = new class() extends AbstractExtension { public bool $throw = false; public function getFilters(): array diff --git a/tests/Extension/CoreTest.php b/tests/Extension/CoreTest.php index 2272bb4d03c..e869f96c7b5 100644 --- a/tests/Extension/CoreTest.php +++ b/tests/Extension/CoreTest.php @@ -43,7 +43,7 @@ public static function provideCycleCases() /** * @dataProvider provideCycleInvalidCases */ - public function testCycleFunctionThrowRuntimeError($values, mixed $position = null) + public function testCycleFunctionThrowRuntimeError($values, mixed $position = null) { $this->expectException(RuntimeError::class); CoreExtension::cycle($values, $position ?? 0); @@ -53,7 +53,8 @@ public static function provideCycleInvalidCases() { return [ 'empty' => [[]], - 'non-countable' => [new class extends \ArrayObject{}], + 'non-countable' => [new class() extends \ArrayObject { + }], ]; } diff --git a/tests/Extension/SandboxTest.php b/tests/Extension/SandboxTest.php index 70052b859c8..d24a06c6720 100644 --- a/tests/Extension/SandboxTest.php +++ b/tests/Extension/SandboxTest.php @@ -76,7 +76,7 @@ public function testSandboxForCoreTags(string $tag, string $template) $twig = $this->getEnvironment(true, [], self::$templates, []); $this->expectException(SecurityError::class); - $this->expectExceptionMessageMatches(sprintf('/Tag "%s" is not allowed in "index \(string template .+?\)" at line 1/', $tag)); + $this->expectExceptionMessageMatches(\sprintf('/Tag "%s" is not allowed in "index \(string template .+?\)" at line 1/', $tag)); $twig->createTemplate($template, 'index')->render([]); } @@ -90,7 +90,7 @@ public static function getSandboxedForCoreTagsTests() yield ['do', '{% do 1 + 2 %}']; yield ['embed', '{% embed "base.twig" %}{% endembed %}']; // To be uncommented in 4.0 - //yield ['extends', '{% extends "base.twig" %}']; + // yield ['extends', '{% extends "base.twig" %}']; yield ['flush', '{% flush %}']; yield ['for', '{% for i in 1..2 %}{% endfor %}']; yield ['from', '{% from "macros" import foo %}']; @@ -101,7 +101,7 @@ public static function getSandboxedForCoreTagsTests() yield ['sandbox', '{% sandbox %}{% endsandbox %}']; yield ['set', '{% set foo = 1 %}']; // To be uncommented in 4.0 - //yield ['use', '{% use "1_empty" %}']; + // yield ['use', '{% use "1_empty" %}']; yield ['with', '{% with foo %}{% endwith %}']; } @@ -112,7 +112,7 @@ public static function getSandboxedForCoreTagsTests() */ public function testSandboxForExtendsAndUseTags(string $tag, string $template) { - $this->expectDeprecation(sprintf('Since twig/twig 3.12: The "%s" tag is always allowed in sandboxes, but won\'t be in 4.0, please enable it explicitly in your sandbox policy if needed.', $tag)); + $this->expectDeprecation(\sprintf('Since twig/twig 3.12: The "%s" tag is always allowed in sandboxes, but won\'t be in 4.0, please enable it explicitly in your sandbox policy if needed.', $tag)); $twig = $this->getEnvironment(true, [], self::$templates, []); $twig->createTemplate($template, 'index')->render([]); diff --git a/tests/Node/Expression/FilterTest.php b/tests/Node/Expression/FilterTest.php index 1339b07dd46..78bf5066426 100644 --- a/tests/Node/Expression/FilterTest.php +++ b/tests/Node/Expression/FilterTest.php @@ -179,7 +179,7 @@ protected static function createEnvironment(): Environment private static function createExtension(): AbstractExtension { - return new class extends AbstractExtension { + return new class() extends AbstractExtension { public function getFilters(): array { return [ diff --git a/tests/ParserTest.php b/tests/ParserTest.php index 4d61c2341d0..dc45fd66d30 100644 --- a/tests/ParserTest.php +++ b/tests/ParserTest.php @@ -197,7 +197,7 @@ public function testImplicitMacroArgumentDefaultValues() $this->assertNull($argumentNodes->getNode('po')->getAttribute('value')); $this->assertFalse($argumentNodes->getNode('lo')->hasAttribute('is_implicit')); - $this->assertSame(true, $argumentNodes->getNode('lo')->getAttribute('value')); + $this->assertTrue($argumentNodes->getNode('lo')->getAttribute('value')); } protected function getParser() diff --git a/tests/TemplateTest.php b/tests/TemplateTest.php index 801701af2cc..7018f4afba8 100644 --- a/tests/TemplateTest.php +++ b/tests/TemplateTest.php @@ -452,12 +452,12 @@ public function getTemplateName(): string return $this->name; } - public function getDebugInfo() : array + public function getDebugInfo(): array { return []; } - public function getSourceContext() : Source + public function getSourceContext(): Source { return new Source('', $this->getTemplateName()); } diff --git a/tests/TokenParser/TypesTokenParserTest.php b/tests/TokenParser/TypesTokenParserTest.php index 57ccb77cb03..85ce6f7a590 100644 --- a/tests/TokenParser/TypesTokenParserTest.php +++ b/tests/TokenParser/TypesTokenParserTest.php @@ -35,7 +35,7 @@ public static function getMappingTests(): array [ '{% types {foo: "bar"} %}', [ - 'foo' => ['type' => 'bar', 'optional' => false] + 'foo' => ['type' => 'bar', 'optional' => false], ], ], @@ -43,7 +43,7 @@ public static function getMappingTests(): array [ '{% types {foo: "bar",} %}', [ - 'foo' => ['type' => 'bar', 'optional' => false] + 'foo' => ['type' => 'bar', 'optional' => false], ], ], @@ -51,7 +51,7 @@ public static function getMappingTests(): array [ '{% types {foo?: "bar"} %}', [ - 'foo' => ['type' => 'bar', 'optional' => true] + 'foo' => ['type' => 'bar', 'optional' => true], ], ], @@ -61,7 +61,7 @@ public static function getMappingTests(): array [ 'foo' => ['type' => 'foo', 'optional' => false], 'bar' => ['type' => 'foo', 'optional' => true], - 'baz' => ['type' => 'baz', 'optional' => false] + 'baz' => ['type' => 'baz', 'optional' => false], ], ], ]; From 978f749b43ccfe2ed0b7d9c160a534907fe9ca3e Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Fri, 6 Sep 2024 12:58:38 +0200 Subject: [PATCH 430/812] Fix minor things --- src/Node/MacroNode.php | 2 +- src/Util/ReflectionCallable.php | 2 +- tests/TokenParser/TypesTokenParserTest.php | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Node/MacroNode.php b/src/Node/MacroNode.php index d54b8ac723b..f3e9d7a1026 100644 --- a/src/Node/MacroNode.php +++ b/src/Node/MacroNode.php @@ -86,7 +86,7 @@ public function compile(Compiler $compiler): void ; } - $node = new CaptureNode($this->getNode('body'), $this->getNode('body')->lineno, $this->getNode('body')->tag); + $node = new CaptureNode($this->getNode('body'), $this->getNode('body')->lineno); $compiler ->write('') diff --git a/src/Util/ReflectionCallable.php b/src/Util/ReflectionCallable.php index 9b183f14d27..16734d9df78 100644 --- a/src/Util/ReflectionCallable.php +++ b/src/Util/ReflectionCallable.php @@ -25,7 +25,7 @@ final class ReflectionCallable private $name; public function __construct( - private TwigCallableInterface $twigCallable, + TwigCallableInterface $twigCallable, ) { $callable = $twigCallable->getCallable(); if (\is_string($callable) && false !== $pos = strpos($callable, '::')) { diff --git a/tests/TokenParser/TypesTokenParserTest.php b/tests/TokenParser/TypesTokenParserTest.php index 85ce6f7a590..400820fcabb 100644 --- a/tests/TokenParser/TypesTokenParserTest.php +++ b/tests/TokenParser/TypesTokenParserTest.php @@ -14,7 +14,7 @@ class TypesTokenParserTest extends TestCase public function testMappingParsing(string $template, array $expected): void { $env = new Environment(new ArrayLoader(), ['cache' => false, 'autoescape' => false]); - $stream = $env->tokenize($source = new Source($template, '')); + $stream = $env->tokenize(new Source($template, '')); $parser = new Parser($env); $typesNode = $parser->parse($stream)->getNode('body')->getNode('0'); From a219d9e77f318d499de4bd4ce145667949c579b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edi=20Modri=C4=87?= Date: Fri, 6 Sep 2024 12:28:44 +0200 Subject: [PATCH 431/812] Fix wrong format of `Environment::VERSION_ID` constant --- src/Environment.php | 2 +- tests/EnvironmentTest.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Environment.php b/src/Environment.php index dbdb4e7d937..5b076a781ca 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -44,7 +44,7 @@ class Environment { public const VERSION = '3.13.0-DEV'; - public const VERSION_ID = 301300; + public const VERSION_ID = 31300; public const MAJOR_VERSION = 3; public const MINOR_VERSION = 13; public const RELEASE_VERSION = 0; diff --git a/tests/EnvironmentTest.php b/tests/EnvironmentTest.php index ff8dc741bc1..7ae31155f12 100644 --- a/tests/EnvironmentTest.php +++ b/tests/EnvironmentTest.php @@ -50,7 +50,7 @@ public function testVersionConstants() $this->assertEquals(Environment::MINOR_VERSION, $exploded[1]); $this->assertEquals(Environment::RELEASE_VERSION, $exploded[2]); - $this->assertEquals(Environment::VERSION_ID, \sprintf('%s0%s0%s', $exploded[0], $exploded[1], $exploded[2])); + $this->assertEquals(Environment::VERSION_ID, Environment::MAJOR_VERSION * 10000 + Environment::MINOR_VERSION * 100 + Environment::RELEASE_VERSION); } public function testAutoescapeOption() From afc0eb63dc66c248c5a94504dc2b255bc9b86575 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sat, 7 Sep 2024 10:01:12 +0200 Subject: [PATCH 432/812] Prepare the 3.13.0 release --- CHANGELOG | 2 +- src/Environment.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 0b3cca7444f..88c1552ccee 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,4 +1,4 @@ -# 3.13.0 (2024-XX-XX) +# 3.13.0 (2024-09-07) * Add the `types` tag (experimental) * Deprecate the `Twig\Test\NodeTestCase::getTests()` data provider, override `provideTests()` instead. diff --git a/src/Environment.php b/src/Environment.php index 5b076a781ca..b8c2989e654 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -43,12 +43,12 @@ */ class Environment { - public const VERSION = '3.13.0-DEV'; + public const VERSION = '3.13.0'; public const VERSION_ID = 31300; public const MAJOR_VERSION = 3; public const MINOR_VERSION = 13; public const RELEASE_VERSION = 0; - public const EXTRA_VERSION = 'DEV'; + public const EXTRA_VERSION = ''; private $charset; private $loader; From e1b64b1937d8e1f7ca4cd0cf9e62ff48fa240b07 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sat, 7 Sep 2024 12:53:18 +0200 Subject: [PATCH 433/812] Bump version --- CHANGELOG | 4 ++++ src/Environment.php | 8 ++++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 88c1552ccee..70dc4acd8bb 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +# 3.13.1 (2024-XX-XX) + + * n/a + # 3.13.0 (2024-09-07) * Add the `types` tag (experimental) diff --git a/src/Environment.php b/src/Environment.php index b8c2989e654..1456d374445 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -43,12 +43,12 @@ */ class Environment { - public const VERSION = '3.13.0'; - public const VERSION_ID = 31300; + public const VERSION = '3.13.1-DEV'; + public const VERSION_ID = 31301; public const MAJOR_VERSION = 3; public const MINOR_VERSION = 13; - public const RELEASE_VERSION = 0; - public const EXTRA_VERSION = ''; + public const RELEASE_VERSION = 1; + public const EXTRA_VERSION = 'DEV'; private $charset; private $loader; From b86575cfd6ad0c1872a2c16aefaeb6a0c8ef8174 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sat, 7 Sep 2024 14:00:41 +0200 Subject: [PATCH 434/812] Deprecate Environment::mergeGlobals() --- CHANGELOG | 2 +- doc/deprecated.rst | 14 ++++++++++++++ src/Environment.php | 13 +++++-------- src/Node/MacroNode.php | 4 ++-- src/Node/WithNode.php | 2 +- src/Template.php | 2 +- src/TemplateWrapper.php | 4 ++-- tests/Node/MacroTest.php | 8 ++++---- 8 files changed, 30 insertions(+), 19 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 70dc4acd8bb..c0f26c20df2 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,6 @@ # 3.13.1 (2024-XX-XX) - * n/a + * Deprecate `Environment::mergeGlobals()` # 3.13.0 (2024-09-07) diff --git a/doc/deprecated.rst b/doc/deprecated.rst index e28ecc62bd8..0490c2deda7 100644 --- a/doc/deprecated.rst +++ b/doc/deprecated.rst @@ -223,3 +223,17 @@ Testing Utilities * The data providers ``getTests()`` and ``getLegacyTests()`` on ``Twig\Test\IntegrationTestCase`` are considered final als of Twig 3.13. + +Environment +----------- + +* The ``Twig\Environment::mergeGlobals()`` method is deprecated as of Twig 3.13 + and will be removed in Twig 4.0: + + Before:: + + $context = $twig->mergeGlobals($context); + + After:: + + $context += $twig->getGlobals(); diff --git a/src/Environment.php b/src/Environment.php index 1456d374445..297efa9cc87 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -830,17 +830,14 @@ public function getGlobals(): array return array_merge($this->extensionSet->getGlobals(), $this->globals); } + /** + * @deprecated since Twig 3.13 + */ public function mergeGlobals(array $context): array { - // we don't use array_merge as the context being generally - // bigger than globals, this code is faster. - foreach ($this->getGlobals() as $key => $value) { - if (!\array_key_exists($key, $context)) { - $context[$key] = $value; - } - } + trigger_deprecation('twig/twig', '3.13', 'The "%s" method is deprecated.', __METHOD__); - return $context; + return $context + $this->getGlobals(); } /** diff --git a/src/Node/MacroNode.php b/src/Node/MacroNode.php index f3e9d7a1026..5a2543a9fd4 100644 --- a/src/Node/MacroNode.php +++ b/src/Node/MacroNode.php @@ -73,7 +73,7 @@ public function compile(Compiler $compiler): void ->write("{\n") ->indent() ->write("\$macros = \$this->macros;\n") - ->write("\$context = \$this->env->mergeGlobals([\n") + ->write("\$context = [\n") ->indent() ; @@ -94,7 +94,7 @@ public function compile(Compiler $compiler): void ->raw(' => ') ->raw("\$__varargs__,\n") ->outdent() - ->write("]);\n\n") + ->write("] + \$this->env->getGlobals();\n\n") ->write("\$blocks = [];\n\n") ->write('return ') ->subcompile($node) diff --git a/src/Node/WithNode.php b/src/Node/WithNode.php index f9104948b5e..487e2800bfd 100644 --- a/src/Node/WithNode.php +++ b/src/Node/WithNode.php @@ -61,7 +61,7 @@ public function compile(Compiler $compiler): void $compiler->write("\$context = [];\n"); } - $compiler->write(\sprintf("\$context = \$this->env->mergeGlobals(array_merge(\$context, \$%s));\n", $varsName)); + $compiler->write(\sprintf("\$context = \$%s + \$context + \$this->env->getGlobals();\n", $varsName)); } $compiler diff --git a/src/Template.php b/src/Template.php index e8368c5d964..7b3ce81611c 100644 --- a/src/Template.php +++ b/src/Template.php @@ -386,7 +386,7 @@ public function render(array $context): string */ public function yield(array $context, array $blocks = []): iterable { - $context = $this->env->mergeGlobals($context); + $context += $this->env->getGlobals(); $blocks = array_merge($this->blocks, $blocks); try { diff --git a/src/TemplateWrapper.php b/src/TemplateWrapper.php index c31f5016159..135c59188c2 100644 --- a/src/TemplateWrapper.php +++ b/src/TemplateWrapper.php @@ -57,12 +57,12 @@ public function getBlockNames(array $context = []): array public function renderBlock(string $name, array $context = []): string { - return $this->template->renderBlock($name, $this->env->mergeGlobals($context)); + return $this->template->renderBlock($name, $context + $this->env->getGlobals()); } public function displayBlock(string $name, array $context = []) { - $context = $this->env->mergeGlobals($context); + $context += $this->env->getGlobals(); foreach ($this->template->yieldBlock($name, $context) as $data) { echo $data; } diff --git a/tests/Node/MacroTest.php b/tests/Node/MacroTest.php index f738c275f1a..6fb6062cbd3 100644 --- a/tests/Node/MacroTest.php +++ b/tests/Node/MacroTest.php @@ -49,11 +49,11 @@ public static function provideTests(): iterable public function macro_foo(\$__foo__ = null, \$__bar__ = "Foo", ...\$__varargs__) { \$macros = \$this->macros; - \$context = \$this->env->mergeGlobals([ + \$context = [ "foo" => \$__foo__, "bar" => \$__bar__, "varargs" => \$__varargs__, - ]); + ] + \$this->env->getGlobals(); \$blocks = []; @@ -71,11 +71,11 @@ public function macro_foo(\$__foo__ = null, \$__bar__ = "Foo", ...\$__varargs__) public function macro_foo(\$__foo__ = null, \$__bar__ = "Foo", ...\$__varargs__) { \$macros = \$this->macros; - \$context = \$this->env->mergeGlobals([ + \$context = [ "foo" => \$__foo__, "bar" => \$__bar__, "varargs" => \$__varargs__, - ]); + ] + \$this->env->getGlobals(); \$blocks = []; From 57f6119d31a7fad57ef233edd609864e574eee6a Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sat, 7 Sep 2024 14:36:55 +0200 Subject: [PATCH 435/812] Remove obsolete code --- src/ExtensionSet.php | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/ExtensionSet.php b/src/ExtensionSet.php index 32377b0fc7f..d2848b59cf2 100644 --- a/src/ExtensionSet.php +++ b/src/ExtensionSet.php @@ -329,12 +329,7 @@ public function getGlobals(): array continue; } - $extGlobals = $extension->getGlobals(); - if (!\is_array($extGlobals)) { - throw new \UnexpectedValueException(\sprintf('"%s::getGlobals()" must return an array of globals.', \get_class($extension))); - } - - $globals = array_merge($globals, $extGlobals); + $globals = array_merge($globals, $extension->getGlobals()); } if ($this->initialized) { From ef58791fa4be989d7f016ce88e2b31cd3c69b45e Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sat, 7 Sep 2024 15:05:45 +0200 Subject: [PATCH 436/812] Add the possibility to reset globals --- CHANGELOG | 1 + doc/advanced.rst | 14 ++++++++++++-- src/Environment.php | 6 ++++++ src/Extension/GlobalsInterface.php | 5 +---- src/ExtensionSet.php | 5 +++++ tests/EnvironmentTest.php | 27 +++++++++++++++++++++++++++ 6 files changed, 52 insertions(+), 6 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index c0f26c20df2..3bc9a51d5d1 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,6 @@ # 3.13.1 (2024-XX-XX) + * Add the possibility to reset globals via `Environment::resetGlobals()` * Deprecate `Environment::mergeGlobals()` # 3.13.0 (2024-09-07) diff --git a/doc/advanced.rst b/doc/advanced.rst index b3eaa1a2e96..6b3f1118ea2 100644 --- a/doc/advanced.rst +++ b/doc/advanced.rst @@ -104,8 +104,8 @@ What? Implementation difficulty? How often? When? Globals ------- -A global variable is like any other template variable, except that it's -available in all templates and macros:: +Global variables are available in all templates and macros. Use ``addGlobal()`` +to add a global variable to a Twig environment:: $twig = new \Twig\Environment($loader); $twig->addGlobal('text', new Text()); @@ -680,6 +680,16 @@ method:: // ... } +.. caution:: + + Globals are fetched once from extensions and then cached for the lifetime + of the Twig environment. It means that globals should not be used to store + values that can change during the lifetime of the Twig environment. For + instance, if you're using an application server like RoadRunner or + FrakenPHP, you should not store values related to the current context (like + the HTTP request). If you do so, don't forget to reset the cache between + requests by calling ``Environment::resetGlobals()``. + Functions ~~~~~~~~~ diff --git a/src/Environment.php b/src/Environment.php index 297efa9cc87..41bb518eb26 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -830,6 +830,12 @@ public function getGlobals(): array return array_merge($this->extensionSet->getGlobals(), $this->globals); } + public function resetGlobals(): void + { + $this->resolvedGlobals = null; + $this->extensionSet->resetGlobals(); + } + /** * @deprecated since Twig 3.13 */ diff --git a/src/Extension/GlobalsInterface.php b/src/Extension/GlobalsInterface.php index 6f1dfe8a73e..d52cd107e5e 100644 --- a/src/Extension/GlobalsInterface.php +++ b/src/Extension/GlobalsInterface.php @@ -12,10 +12,7 @@ namespace Twig\Extension; /** - * Enables usage of the deprecated Twig\Extension\AbstractExtension::getGlobals() method. - * - * Explicitly implement this interface if you really need to implement the - * deprecated getGlobals() method in your extensions. + * Allows Twig extensions to add globals to the context. * * @author Fabien Potencier */ diff --git a/src/ExtensionSet.php b/src/ExtensionSet.php index d2848b59cf2..28d57a41c61 100644 --- a/src/ExtensionSet.php +++ b/src/ExtensionSet.php @@ -339,6 +339,11 @@ public function getGlobals(): array return $globals; } + public function resetGlobals(): void + { + $this->globals = null; + } + public function addTest(TwigTest $test): void { if ($this->initialized) { diff --git a/tests/EnvironmentTest.php b/tests/EnvironmentTest.php index 192b8474f42..b5100a0aadc 100644 --- a/tests/EnvironmentTest.php +++ b/tests/EnvironmentTest.php @@ -470,6 +470,33 @@ protected function getMockLoader($templateName, $templateContent) return $loader; } + + public function testResettingGlobals() + { + $twig = new Environment(new ArrayLoader(['index' => ''])); + $twig->addExtension(new class() extends AbstractExtension implements GlobalsInterface { + public function getGlobals(): array + { + return [ + 'global_ext' => bin2hex(random_bytes(16)), + ]; + } + }); + + // Force extensions initialization + $twig->load('index'); + + // Simulate request + $g1 = $twig->getGlobals(); + // Simulate another call from request 1 (the globals are cached) + $g2 = $twig->getGlobals(); + $this->assertSame($g1['global_ext'], $g2['global_ext']); + + // Simulate request 2 + $twig->resetGlobals(); + $g3 = $twig->getGlobals(); + $this->assertNotSame($g3['global_ext'], $g2['global_ext']); + } } class EnvironmentTest_Extension_WithGlobals extends AbstractExtension From bf6b7ae53dbb5253f0b965cd0f9b54110150bd8a Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sun, 8 Sep 2024 17:26:18 +0200 Subject: [PATCH 437/812] Bump version --- CHANGELOG | 2 +- src/Environment.php | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 3bc9a51d5d1..8d16b942846 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,4 +1,4 @@ -# 3.13.1 (2024-XX-XX) +# 3.14.0 (2024-XX-XX) * Add the possibility to reset globals via `Environment::resetGlobals()` * Deprecate `Environment::mergeGlobals()` diff --git a/src/Environment.php b/src/Environment.php index 41bb518eb26..de241b64369 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -43,11 +43,11 @@ */ class Environment { - public const VERSION = '3.13.1-DEV'; - public const VERSION_ID = 31301; + public const VERSION = '3.14.0-DEV'; + public const VERSION_ID = 31400; public const MAJOR_VERSION = 3; - public const MINOR_VERSION = 13; - public const RELEASE_VERSION = 1; + public const MINOR_VERSION = 14; + public const RELEASE_VERSION = 0; public const EXTRA_VERSION = 'DEV'; private $charset; From 28dc912c0e2b3dfec22ee0d92c334fee21599873 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Mon, 9 Sep 2024 08:15:28 +0200 Subject: [PATCH 438/812] Remove unused private methods --- src/Node/ModuleNode.php | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/src/Node/ModuleNode.php b/src/Node/ModuleNode.php index b57e643e852..d2fb216b1e2 100644 --- a/src/Node/ModuleNode.php +++ b/src/Node/ModuleNode.php @@ -418,14 +418,6 @@ protected function compileIsTraitable(Compiler $compiler) continue; } - if ($node instanceof TextNode && ctype_space($node->getAttribute('data'))) { - continue; - } - - if ($node instanceof BlockReferenceNode) { - continue; - } - $traitable = false; break; } @@ -494,19 +486,4 @@ protected function compileLoadTemplate(Compiler $compiler, $node, $var) throw new \LogicException('Trait templates can only be constant nodes.'); } } - - private function hasNodeOutputNodes(Node $node): bool - { - if ($node instanceof NodeOutputInterface) { - return true; - } - - foreach ($node as $child) { - if ($this->hasNodeOutputNodes($child)) { - return true; - } - } - - return false; - } } From 45e167a514ff0d53878be6a7c145336ccfdc4f03 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Mon, 9 Sep 2024 08:41:21 +0200 Subject: [PATCH 439/812] Add more tests --- .../inheritance/conditional_block_nested.test | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 tests/Fixtures/tags/inheritance/conditional_block_nested.test diff --git a/tests/Fixtures/tags/inheritance/conditional_block_nested.test b/tests/Fixtures/tags/inheritance/conditional_block_nested.test new file mode 100644 index 00000000000..1f99dfb6167 --- /dev/null +++ b/tests/Fixtures/tags/inheritance/conditional_block_nested.test @@ -0,0 +1,38 @@ +--TEST-- +conditional "block" tag with "extends" tag (nested) +--TEMPLATE-- +{% extends "layout.twig" %} + +{% block content_base %} + {{ parent() -}} + index +{% endblock %} + +{% block content_layout -%} + {{ parent() -}} + nested_index +{% endblock %} +--TEMPLATE(layout.twig)-- +{% extends "base.twig" %} + +{% block content_base %} + {{ parent() -}} + layout + {% if true -%} + {% block content_layout -%} + nested_layout + {% endblock -%} + {% endif %} +{% endblock %} +--TEMPLATE(base.twig)-- +{% block content_base %} + base +{% endblock %} +--DATA-- +return [] +--EXPECT-- +base +layout + nested_layout + nested_index +index From 995e7c2270789c21ae3df5b5061101e139cd7026 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Mon, 9 Sep 2024 08:49:09 +0200 Subject: [PATCH 440/812] Fix CS --- src/Test/IntegrationTestCase.php | 1 + src/TokenParser/TypesTokenParser.php | 1 + tests/LexerTest.php | 1 + 3 files changed, 3 insertions(+) diff --git a/src/Test/IntegrationTestCase.php b/src/Test/IntegrationTestCase.php index 78b0171e12f..8690a809e38 100644 --- a/src/Test/IntegrationTestCase.php +++ b/src/Test/IntegrationTestCase.php @@ -31,6 +31,7 @@ abstract class IntegrationTestCase extends TestCase { /** * @deprecated since Twig 3.13, use getFixturesDirectory() instead. + * * @return string */ protected function getFixturesDir() diff --git a/src/TokenParser/TypesTokenParser.php b/src/TokenParser/TypesTokenParser.php index 2172ed26361..2e0850e7e9b 100644 --- a/src/TokenParser/TypesTokenParser.php +++ b/src/TokenParser/TypesTokenParser.php @@ -23,6 +23,7 @@ * {% types {foo: 'int', bar?: 'string'} %} * * @author Jeroen Versteeg + * * @internal */ final class TypesTokenParser extends AbstractTokenParser diff --git a/tests/LexerTest.php b/tests/LexerTest.php index d8da5545821..a9cb7cb6205 100644 --- a/tests/LexerTest.php +++ b/tests/LexerTest.php @@ -231,6 +231,7 @@ public static function getStringWithEscapedDelimiter() /** * @group legacy + * * @dataProvider getStringWithEscapedDelimiterProducingDeprecation */ public function testStringWithEscapedDelimiterProducingDeprecation(string $template, string $expected, string $expectedDeprecation) From 064e07965465eb33f2b2ca47c188681039f6b876 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Mon, 9 Sep 2024 10:51:41 +0200 Subject: [PATCH 441/812] Tweak code --- src/Extension/CoreExtension.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index ee0a1dbaf47..fa97918b653 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -1448,9 +1448,11 @@ public static function include(Environment $env, $context, $template, $variables if (!$ignoreMissing) { throw $e; } + + return ''; } - return $loaded ? $loaded->render($variables) : ''; + return $loaded->render($variables); } finally { if ($isSandboxed && !$alreadySandboxed) { $sandbox->disableSandbox(); From f72c93dbd22310f729203881351c75c3d44b56ec Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Mon, 9 Sep 2024 10:58:08 +0200 Subject: [PATCH 442/812] fix the version mergeGlobals() is deprecated since --- doc/deprecated.rst | 2 +- src/Environment.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/deprecated.rst b/doc/deprecated.rst index 0490c2deda7..a353518f000 100644 --- a/doc/deprecated.rst +++ b/doc/deprecated.rst @@ -227,7 +227,7 @@ Testing Utilities Environment ----------- -* The ``Twig\Environment::mergeGlobals()`` method is deprecated as of Twig 3.13 +* The ``Twig\Environment::mergeGlobals()`` method is deprecated as of Twig 3.14 and will be removed in Twig 4.0: Before:: diff --git a/src/Environment.php b/src/Environment.php index de241b64369..fb5342b62c0 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -837,11 +837,11 @@ public function resetGlobals(): void } /** - * @deprecated since Twig 3.13 + * @deprecated since Twig 3.14 */ public function mergeGlobals(array $context): array { - trigger_deprecation('twig/twig', '3.13', 'The "%s" method is deprecated.', __METHOD__); + trigger_deprecation('twig/twig', '3.14', 'The "%s" method is deprecated.', __METHOD__); return $context + $this->getGlobals(); } From 795720230c8e90d91dfaed3bfd749160f7dc1e03 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Mon, 9 Sep 2024 11:10:45 +0200 Subject: [PATCH 443/812] Fix test --- tests/Fixtures/functions/include/sandbox.test | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Fixtures/functions/include/sandbox.test b/tests/Fixtures/functions/include/sandbox.test index ebfdb1eb8ff..ec7e61e9703 100644 --- a/tests/Fixtures/functions/include/sandbox.test +++ b/tests/Fixtures/functions/include/sandbox.test @@ -5,8 +5,8 @@ --TEMPLATE(foo.twig)-- -{{ foo|e }} -{{ foo|e }} +{{ 'foo'|e }} +{{ 'foo'|e }} --DATA-- return [] --EXCEPTION-- From 11f68e2aeb526bfaf638e30d4420d8a710f3f7c6 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Mon, 9 Sep 2024 10:51:06 +0200 Subject: [PATCH 444/812] Fix a security issue when an included sandboxed template has been loaded before without the sandbox context --- src/Extension/CoreExtension.php | 11 ++++------ tests/Extension/CoreTest.php | 39 +++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 7 deletions(-) diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index fa97918b653..3ed27a35cc3 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -1431,13 +1431,6 @@ public static function include(Environment $env, $context, $template, $variables if (!$alreadySandboxed = $sandbox->isSandboxed()) { $sandbox->enableSandbox(); } - - foreach ((\is_array($template) ? $template : [$template]) as $name) { - // if a Template instance is passed, it might have been instantiated outside of a sandbox, check security - if ($name instanceof TemplateWrapper || $name instanceof Template) { - $name->unwrap()->checkSecurity(); - } - } } try { @@ -1452,6 +1445,10 @@ public static function include(Environment $env, $context, $template, $variables return ''; } + if ($isSandboxed) { + $loaded->unwrap()->checkSecurity(); + } + return $loaded->render($variables); } finally { if ($isSandboxed && !$alreadySandboxed) { diff --git a/tests/Extension/CoreTest.php b/tests/Extension/CoreTest.php index e869f96c7b5..6bb74807f86 100644 --- a/tests/Extension/CoreTest.php +++ b/tests/Extension/CoreTest.php @@ -12,8 +12,13 @@ */ use PHPUnit\Framework\TestCase; +use Twig\Environment; use Twig\Error\RuntimeError; use Twig\Extension\CoreExtension; +use Twig\Extension\SandboxExtension; +use Twig\Loader\ArrayLoader; +use Twig\Sandbox\SecurityError; +use Twig\Sandbox\SecurityPolicy; class CoreTest extends TestCase { @@ -354,6 +359,40 @@ public static function provideCompareCases() [1, 42, "\x00\x34\x32"], ]; } + + public function testSandboxedInclude() + { + $twig = new Environment(new ArrayLoader([ + 'index' => '{{ include("included", sandboxed: true) }}', + 'included' => '{{ "included"|e }}', + ])); + $policy = new SecurityPolicy(allowedFunctions: ['include']); + $sandbox = new SandboxExtension($policy, false); + $twig->addExtension($sandbox); + + // We expect a compile error + $this->expectException(SecurityError::class); + $twig->render('index'); + } + + public function testSandboxedIncludeWithPreloadedTemplate() + { + $twig = new Environment(new ArrayLoader([ + 'index' => '{{ include("included", sandboxed: true) }}', + 'included' => '{{ "included"|e }}', + ])); + $policy = new SecurityPolicy(allowedFunctions: ['include']); + $sandbox = new SandboxExtension($policy, false); + $twig->addExtension($sandbox); + + // The template is loaded without the sandbox enabled + // so, no compile error + $twig->load('included'); + + // We expect a runtime error + $this->expectException(SecurityError::class); + $twig->render('index'); + } } final class CoreTestIteratorAggregate implements \IteratorAggregate From 126b2c97818dbff0cdf3fbfc881aedb3d40aae72 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Mon, 9 Sep 2024 19:55:12 +0200 Subject: [PATCH 445/812] Prepare the 3.14.0 release --- CHANGELOG | 3 ++- src/Environment.php | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 8d16b942846..44c79b13325 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,6 @@ -# 3.14.0 (2024-XX-XX) +# 3.14.0 (2024-09-09) + * Fix a security issue when an included sandboxed template has been loaded before without the sandbox context * Add the possibility to reset globals via `Environment::resetGlobals()` * Deprecate `Environment::mergeGlobals()` diff --git a/src/Environment.php b/src/Environment.php index fb5342b62c0..24e55e979fe 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -43,12 +43,12 @@ */ class Environment { - public const VERSION = '3.14.0-DEV'; + public const VERSION = '3.14.0'; public const VERSION_ID = 31400; public const MAJOR_VERSION = 3; public const MINOR_VERSION = 14; public const RELEASE_VERSION = 0; - public const EXTRA_VERSION = 'DEV'; + public const EXTRA_VERSION = ''; private $charset; private $loader; From 247ee93df93f82104546169d35d1582b03defb06 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Mon, 9 Sep 2024 21:04:19 +0200 Subject: [PATCH 446/812] Bump version --- CHANGELOG | 4 ++++ src/Environment.php | 8 ++++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 44c79b13325..d76769421b7 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +# 3.15.0 (2024-XX-XX) + + * n/a + # 3.14.0 (2024-09-09) * Fix a security issue when an included sandboxed template has been loaded before without the sandbox context diff --git a/src/Environment.php b/src/Environment.php index 24e55e979fe..06a306b89c6 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -43,12 +43,12 @@ */ class Environment { - public const VERSION = '3.14.0'; - public const VERSION_ID = 31400; + public const VERSION = '3.15.0-DEV'; + public const VERSION_ID = 31500; public const MAJOR_VERSION = 3; - public const MINOR_VERSION = 14; + public const MINOR_VERSION = 15; public const RELEASE_VERSION = 0; - public const EXTRA_VERSION = ''; + public const EXTRA_VERSION = 'DEV'; private $charset; private $loader; From 34ae0311d94a97ae43282837ca2e12d7275b1b4a Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Mon, 9 Sep 2024 16:17:33 +0200 Subject: [PATCH 447/812] Improve the way one can deprecate a Twig callable --- CHANGELOG | 2 +- doc/advanced.rst | 48 ++++++++++---- doc/deprecated.rst | 19 ++++++ src/AbstractTwigCallable.php | 47 +++++++++++++- src/DeprecatedCallableInfo.php | 67 ++++++++++++++++++++ src/ExpressionParser.php | 25 +------- src/Extension/CoreExtension.php | 3 +- tests/DeprecatedCallableInfoTest.php | 80 ++++++++++++++++++++++++ tests/Fixtures/functions/deprecated.test | 4 +- tests/IntegrationTest.php | 3 +- tests/Util/DeprecationCollectorTest.php | 3 +- 11 files changed, 260 insertions(+), 41 deletions(-) create mode 100644 src/DeprecatedCallableInfo.php create mode 100644 tests/DeprecatedCallableInfoTest.php diff --git a/CHANGELOG b/CHANGELOG index d76769421b7..eefd094ac1b 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,6 @@ # 3.15.0 (2024-XX-XX) - * n/a + * Improve the way one can deprecate a Twig callable (use `deprecation_info` instead of the other callable options) # 3.14.0 (2024-09-09) diff --git a/doc/advanced.rst b/doc/advanced.rst index 6b3f1118ea2..01f03d98078 100644 --- a/doc/advanced.rst +++ b/doc/advanced.rst @@ -277,29 +277,53 @@ filter: ``('a', 'b', 'foo')``. Deprecated Filters ~~~~~~~~~~~~~~~~~~ -You can mark a filter as being deprecated by setting the ``deprecated`` option -to ``true``. You can also give an alternative filter that replaces the -deprecated one when that makes sense:: +.. versionadded:: 3.15 + + The ``deprecation_info`` option was added in Twig 3.15. + +You can mark a filter as being deprecated by setting the ``deprecation_info`` +option:: $filter = new \Twig\TwigFilter('obsolete', function () { // ... - }, ['deprecated' => true, 'alternative' => 'new_one']); + }, ['deprecation_info' => new DeprecatedCallableInfo('twig/twig', '3.11', 'new_one')]); -.. versionadded:: 3.11 +The ``DeprecatedCallableInfo`` constructor takes the following parameters: - The ``deprecating_package`` option was added in Twig 3.11. +* The Composer package name that defines the filter; +* The version when the filter was deprecated. -You can also set the ``deprecating_package`` option to specify the package that -is deprecating the filter, and ``deprecated`` can be set to the package version -when the filter was deprecated:: +Optionally, you can also provide the following parameters about an alternative: - $filter = new \Twig\TwigFilter('obsolete', function () { - // ... - }, ['deprecated' => '1.1', 'deprecating_package' => 'foo/bar']); +* The package name that contains the alternative filter; +* The alternative filter name that replaces the deprecated one; +* The package version that added the alternative filter. When a filter is deprecated, Twig emits a deprecation notice when compiling a template using it. See :ref:`deprecation-notices` for more information. +.. note:: + + Before Twig 3.15, you can mark a filter as being deprecated by setting the + ``deprecated`` option to ``true``. You can also give an alternative filter + that replaces the deprecated one when that makes sense:: + + $filter = new \Twig\TwigFilter('obsolete', function () { + // ... + }, ['deprecated' => true, 'alternative' => 'new_one']); + + .. versionadded:: 3.11 + + The ``deprecating_package`` option was added in Twig 3.11. + + You can also set the ``deprecating_package`` option to specify the package + that is deprecating the filter, and ``deprecated`` can be set to the + package version when the filter was deprecated:: + + $filter = new \Twig\TwigFilter('obsolete', function () { + // ... + }, ['deprecated' => '1.1', 'deprecating_package' => 'foo/bar']); + Functions --------- diff --git a/doc/deprecated.rst b/doc/deprecated.rst index a353518f000..c280702a5cb 100644 --- a/doc/deprecated.rst +++ b/doc/deprecated.rst @@ -237,3 +237,22 @@ Environment After:: $context += $twig->getGlobals(); + +Functions/Filters/Tests +----------------------- + +* The ``deprecated``, ``deprecating_package``, ``alternative`` options on Twig + functions/filters/Tests are deprecated as of Twig 3.15, and will be removed + in Twig 4.0. Use the ``deprecation_info`` option instead: + + Before:: + + $twig->addFunction(new TwigFunction('foo', 'foo', [ + 'deprecated' => '3.12', 'deprecating_package' => 'twig/twig', + ])); + + After:: + + $twig->addFunction(new TwigFunction('foo', 'foo', [ + 'deprecation_info' => new DeprecatedCallableInfo('twig/twig', '3.12'), + ])); diff --git a/src/AbstractTwigCallable.php b/src/AbstractTwigCallable.php index f6718430064..2e9b34d18be 100644 --- a/src/AbstractTwigCallable.php +++ b/src/AbstractTwigCallable.php @@ -33,10 +33,35 @@ public function __construct(string $name, $callable = null, array $options = []) 'needs_context' => false, 'needs_charset' => false, 'is_variadic' => false, + 'deprecation_info' => null, 'deprecated' => false, 'deprecating_package' => '', 'alternative' => null, ], $options); + + if ($this->options['deprecation_info'] && !$this->options['deprecation_info'] instanceof DeprecatedCallableInfo) { + throw new \LogicException(\sprintf('The "deprecation_info" option must be an instance of "%s".', DeprecatedCallableInfo::class)); + } + + if ($this->options['deprecated']) { + if ($this->options['deprecation_info']) { + throw new \LogicException('When setting the "deprecation_info" option, you need to remove the obsolete deprecated options.'); + } + + trigger_deprecation('twig/twig', '3.15', 'Using the "deprecated", "deprecating_package", and "alternative" options is deprecated, pass a "deprecation_info" one instead.'); + + $this->options['deprecation_info'] = new DeprecatedCallableInfo( + $this->options['deprecating_package'], + $this->options['deprecated'], + null, + $this->options['alternative'], + ); + } + + if ($this->options['deprecation_info']) { + $this->options['deprecation_info']->setName($name); + $this->options['deprecation_info']->setType($this->getType()); + } } public function __toString(): string @@ -111,21 +136,41 @@ public function isVariadic(): bool public function isDeprecated(): bool { - return (bool) $this->options['deprecated']; + return (bool) $this->options['deprecation_info']; } + public function triggerDeprecation(?string $file = null, ?int $line = null): void + { + $this->options['deprecation_info']->triggerDeprecation($file, $line); + } + + /** + * @deprecated since Twig 3.15 + */ public function getDeprecatingPackage(): string { + trigger_deprecation('twig/twig', '3.15', 'The "%s" method is deprecated, use "%s::triggerDeprecation()" instead.', __METHOD__, static::class); + return $this->options['deprecating_package']; } + /** + * @deprecated since Twig 3.15 + */ public function getDeprecatedVersion(): string { + trigger_deprecation('twig/twig', '3.15', 'The "%s" method is deprecated, use "%s::triggerDeprecation()" instead.', __METHOD__, static::class); + return \is_bool($this->options['deprecated']) ? '' : $this->options['deprecated']; } + /** + * @deprecated since Twig 3.15 + */ public function getAlternative(): ?string { + trigger_deprecation('twig/twig', '3.15', 'The "%s" method is deprecated, use "%s::triggerDeprecation()" instead.', __METHOD__, static::class); + return $this->options['alternative']; } diff --git a/src/DeprecatedCallableInfo.php b/src/DeprecatedCallableInfo.php new file mode 100644 index 00000000000..2db9f3d28af --- /dev/null +++ b/src/DeprecatedCallableInfo.php @@ -0,0 +1,67 @@ + + */ +final class DeprecatedCallableInfo +{ + private string $type; + private string $name; + + public function __construct( + private string $package, + private string $version, + private ?string $altName = null, + private ?string $altPackage = null, + private ?string $altVersion = null, + ) { + } + + public function setType(string $type): void + { + $this->type = $type; + } + + public function setName(string $name): void + { + $this->name = $name; + } + + public function triggerDeprecation(?string $file = null, ?int $line = null): void + { + $message = \sprintf('Twig %s "%s" is deprecated', ucfirst($this->type), $this->name); + + if ($this->altName) { + $message .= \sprintf('; use "%s"', $this->altName); + if ($this->altPackage) { + $message .= \sprintf(' from the "%s" package', $this->altPackage); + } + if ($this->altVersion) { + $message .= \sprintf(' (available since version %s)', $this->altVersion); + } + $message .= ' instead'; + } + + if ($file) { + $message .= \sprintf(' in %s', $file); + if ($line) { + $message .= \sprintf(' at line %d', $line); + } + } + + $message .= '.'; + + trigger_deprecation($this->package, $this->version, $message); + } +} diff --git a/src/ExpressionParser.php b/src/ExpressionParser.php index dc4a6015d7b..a610a390e58 100644 --- a/src/ExpressionParser.php +++ b/src/ExpressionParser.php @@ -773,15 +773,8 @@ private function getTest(int $line): TwigTest if ($test->isDeprecated()) { $stream = $this->parser->getStream(); - $message = \sprintf('Twig Test "%s" is deprecated', $test->getName()); - - if ($test->getAlternative()) { - $message .= \sprintf('. Use "%s" instead', $test->getAlternative()); - } $src = $stream->getSourceContext(); - $message .= \sprintf(' in %s at line %d.', $src->getPath() ?: $src->getName(), $stream->getCurrent()->getLine()); - - trigger_deprecation($test->getDeprecatingPackage(), $test->getDeprecatedVersion(), $message); + $test->triggerDeprecation($src->getPath() ?: $src->getName(), $stream->getCurrent()->getLine()); } return $test; @@ -797,14 +790,8 @@ private function getFunction(string $name, int $line): TwigFunction } if ($function->isDeprecated()) { - $message = \sprintf('Twig Function "%s" is deprecated', $function->getName()); - if ($function->getAlternative()) { - $message .= \sprintf('. Use "%s" instead', $function->getAlternative()); - } $src = $this->parser->getStream()->getSourceContext(); - $message .= \sprintf(' in %s at line %d.', $src->getPath() ?: $src->getName(), $line); - - trigger_deprecation($function->getDeprecatingPackage(), $function->getDeprecatedVersion(), $message); + $function->triggerDeprecation($src->getPath() ?: $src->getName(), $line); } return $function; @@ -820,14 +807,8 @@ private function getFilter(string $name, int $line): TwigFilter } if ($filter->isDeprecated()) { - $message = \sprintf('Twig Filter "%s" is deprecated', $filter->getName()); - if ($filter->getAlternative()) { - $message .= \sprintf('. Use "%s" instead', $filter->getAlternative()); - } $src = $this->parser->getStream()->getSourceContext(); - $message .= \sprintf(' in %s at line %d.', $src->getPath() ?: $src->getName(), $line); - - trigger_deprecation($filter->getDeprecatingPackage(), $filter->getDeprecatedVersion(), $message); + $filter->triggerDeprecation($src->getPath() ?: $src->getName(), $line); } return $filter; diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index 3ed27a35cc3..550dc0f3851 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -11,6 +11,7 @@ namespace Twig\Extension; +use Twig\DeprecatedCallableInfo; use Twig\Environment; use Twig\Error\LoaderError; use Twig\Error\RuntimeError; @@ -217,7 +218,7 @@ public function getFilters(): array new TwigFilter('striptags', [self::class, 'striptags']), new TwigFilter('trim', [self::class, 'trim']), new TwigFilter('nl2br', [self::class, 'nl2br'], ['pre_escape' => 'html', 'is_safe' => ['html']]), - new TwigFilter('spaceless', [self::class, 'spaceless'], ['is_safe' => ['html'], 'deprecated' => '3.12', 'deprecating_package' => 'twig/twig']), + new TwigFilter('spaceless', [self::class, 'spaceless'], ['is_safe' => ['html'], 'deprecation_info' => new DeprecatedCallableInfo('twig/twig', '3.12')]), // array helpers new TwigFilter('join', [self::class, 'join']), diff --git a/tests/DeprecatedCallableInfoTest.php b/tests/DeprecatedCallableInfoTest.php new file mode 100644 index 00000000000..4e6d1583ce5 --- /dev/null +++ b/tests/DeprecatedCallableInfoTest.php @@ -0,0 +1,80 @@ +setType('function'); + $info->setName('foo'); + + $deprecations = []; + try { + set_error_handler(function ($type, $msg) use (&$deprecations) { + if (\E_USER_DEPRECATED === $type) { + $deprecations[] = $msg; + } + + return false; + }); + + $info->triggerDeprecation('foo.twig', 1); + } finally { + restore_error_handler(); + } + + $this->assertSame([$expected], $deprecations); + } + + public static function provideTestsForTriggerDeprecation(): iterable + { + yield ['Since foo/bar 1.1: Twig Function "foo" is deprecated in foo.twig at line 1.', new DeprecatedCallableInfo('foo/bar', '1.1')]; + yield ['Since foo/bar 1.1: Twig Function "foo" is deprecated; use "alt_foo" from the "all/bar" package (available since version 12.10) instead in foo.twig at line 1.', new DeprecatedCallableInfo('foo/bar', '1.1', 'alt_foo', 'all/bar', '12.10')]; + yield ['Since foo/bar 1.1: Twig Function "foo" is deprecated; use "alt_foo" from the "all/bar" package instead in foo.twig at line 1.', new DeprecatedCallableInfo('foo/bar', '1.1', 'alt_foo', 'all/bar')]; + yield ['Since foo/bar 1.1: Twig Function "foo" is deprecated; use "alt_foo" instead in foo.twig at line 1.', new DeprecatedCallableInfo('foo/bar', '1.1', 'alt_foo')]; + } + + public function testTriggerDeprecationWithoutFileOrLine() + { + $info = new DeprecatedCallableInfo('foo/bar', '1.1'); + $info->setType('function'); + $info->setName('foo'); + + $deprecations = []; + try { + set_error_handler(function ($type, $msg) use (&$deprecations) { + if (\E_USER_DEPRECATED === $type) { + $deprecations[] = $msg; + } + + return false; + }); + + $info->triggerDeprecation(); + $info->triggerDeprecation('foo.twig'); + } finally { + restore_error_handler(); + } + + $this->assertSame([ + 'Since foo/bar 1.1: Twig Function "foo" is deprecated.', + 'Since foo/bar 1.1: Twig Function "foo" is deprecated in foo.twig.', + ], $deprecations); + } +} diff --git a/tests/Fixtures/functions/deprecated.test b/tests/Fixtures/functions/deprecated.test index 355e43303dd..42f2a5dd6f3 100644 --- a/tests/Fixtures/functions/deprecated.test +++ b/tests/Fixtures/functions/deprecated.test @@ -1,8 +1,8 @@ --TEST-- Functions can be deprecated_function --DEPRECATION-- -Since foo/bar 1.1: Twig Function "deprecated_function" is deprecated. Use "not_deprecated_function" instead in index.twig at line 2. -Since foo/bar 1.1: Twig Function "deprecated_function" is deprecated. Use "not_deprecated_function" instead in index.twig at line 4. +Since foo/bar 1.1: Twig Function "deprecated_function" is deprecated; use "not_deprecated_function" instead in index.twig at line 2. +Since foo/bar 1.1: Twig Function "deprecated_function" is deprecated; use "not_deprecated_function" instead in index.twig at line 4. --TEMPLATE-- {{ deprecated_function() }} diff --git a/tests/IntegrationTest.php b/tests/IntegrationTest.php index 65f207d90bb..49ccf76cb96 100644 --- a/tests/IntegrationTest.php +++ b/tests/IntegrationTest.php @@ -11,6 +11,7 @@ * file that was distributed with this source code. */ +use Twig\DeprecatedCallableInfo; use Twig\Extension\AbstractExtension; use Twig\Extension\DebugExtension; use Twig\Extension\SandboxExtension; @@ -185,7 +186,7 @@ public function getFunctions(): array new TwigFunction('*_path', [$this, 'dynamic_path']), new TwigFunction('*_foo_*_bar', [$this, 'dynamic_foo']), new TwigFunction('anon_foo', function ($name) { return '*'.$name.'*'; }), - new TwigFunction('deprecated_function', function () { return 'foo'; }, ['deprecated' => '1.1', 'deprecating_package' => 'foo/bar', 'alternative' => 'not_deprecated_function']), + new TwigFunction('deprecated_function', function () { return 'foo'; }, ['deprecation_info' => new DeprecatedCallableInfo('foo/bar', '1.1', 'not_deprecated_function')]), ]; } diff --git a/tests/Util/DeprecationCollectorTest.php b/tests/Util/DeprecationCollectorTest.php index a3010d14d57..635a67f40cc 100644 --- a/tests/Util/DeprecationCollectorTest.php +++ b/tests/Util/DeprecationCollectorTest.php @@ -12,6 +12,7 @@ */ use PHPUnit\Framework\TestCase; +use Twig\DeprecatedCallableInfo; use Twig\Environment; use Twig\Loader\ArrayLoader; use Twig\TwigFunction; @@ -25,7 +26,7 @@ class DeprecationCollectorTest extends TestCase public function testCollect() { $twig = new Environment(new ArrayLoader()); - $twig->addFunction(new TwigFunction('deprec', [$this, 'deprec'], ['deprecated' => '1.1', 'deprecating_package' => 'foo/bar'])); + $twig->addFunction(new TwigFunction('deprec', [$this, 'deprec'], ['deprecation_info' => new DeprecatedCallableInfo('foo/bar', '1.1')])); $collector = new DeprecationCollector($twig); $deprecations = $collector->collect(new Iterator()); From a240eff47ef19eb86f0427c9d78b6290361b7fd1 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Tue, 10 Sep 2024 08:27:46 +0200 Subject: [PATCH 448/812] Simplify code --- .../Compiler/MissingExtensionSuggestorPass.php | 6 +----- extra/twig-extra-bundle/composer.json | 2 +- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/extra/twig-extra-bundle/DependencyInjection/Compiler/MissingExtensionSuggestorPass.php b/extra/twig-extra-bundle/DependencyInjection/Compiler/MissingExtensionSuggestorPass.php index 245e5bfd1da..22e04c012d5 100644 --- a/extra/twig-extra-bundle/DependencyInjection/Compiler/MissingExtensionSuggestorPass.php +++ b/extra/twig-extra-bundle/DependencyInjection/Compiler/MissingExtensionSuggestorPass.php @@ -26,12 +26,8 @@ public function process(ContainerBuilder $container) $twigDefinition ->addMethodCall('registerUndefinedFilterCallback', [[new Reference('twig.missing_extension_suggestor'), 'suggestFilter']]) ->addMethodCall('registerUndefinedFunctionCallback', [[new Reference('twig.missing_extension_suggestor'), 'suggestFunction']]) + ->addMethodCall('registerUndefinedTokenParserCallback', [[new Reference('twig.missing_extension_suggestor'), 'suggestTag']]) ; - - // this method was added in Twig 3.2 - if (method_exists(Environment::class, 'registerUndefinedTokenParserCallback')) { - $twigDefinition->addMethodCall('registerUndefinedTokenParserCallback', [[new Reference('twig.missing_extension_suggestor'), 'suggestTag']]); - } } } } diff --git a/extra/twig-extra-bundle/composer.json b/extra/twig-extra-bundle/composer.json index d6047277a25..ad3bb807b12 100644 --- a/extra/twig-extra-bundle/composer.json +++ b/extra/twig-extra-bundle/composer.json @@ -18,7 +18,7 @@ "php": ">=8.0.2", "symfony/framework-bundle": "^5.4|^6.4|^7.0", "symfony/twig-bundle": "^5.4|^6.4|^7.0", - "twig/twig": "^3.0|^4.0" + "twig/twig": "^3.2|^4.0" }, "require-dev": { "league/commonmark": "^1.0|^2.0", From d1b05519afb62b12b8306c6cd25128e134626404 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Tue, 10 Sep 2024 08:34:26 +0200 Subject: [PATCH 449/812] Clarify docs on registerUndefinedTokenParserCallback() --- doc/recipes.rst | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/doc/recipes.rst b/doc/recipes.rst index c091d34fc63..5144f211139 100644 --- a/doc/recipes.rst +++ b/doc/recipes.rst @@ -300,6 +300,14 @@ does not return ``false``. As the resolution of functions/filters/tags is done during compilation, there is no overhead when registering these callbacks. +.. warning:: + + As parsing a tag is specific to each tag (the syntax is free form), the + ``registerUndefinedTokenParserCallback()`` cannot be used to define a + default implementation for all unknown tags. It's mainly useful to override + the default exception or to register on the fly TokenParser instances for + specific known tags. + Validating the Template Syntax ------------------------------ From 41103dcdc2daab4c83cdd05b5b4fde5b7e41e635 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Mon, 9 Sep 2024 10:51:06 +0200 Subject: [PATCH 450/812] Fix a security issue when an included sandboxed template has been loaded before without the sandbox context --- src/Extension/CoreExtension.php | 11 ++++------ tests/Extension/CoreTest.php | 39 +++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 7 deletions(-) diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index 5ac80884a58..4b014b8df93 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -1400,13 +1400,6 @@ public static function include(Environment $env, $context, $template, $variables if (!$alreadySandboxed = $sandbox->isSandboxed()) { $sandbox->enableSandbox(); } - - foreach ((\is_array($template) ? $template : [$template]) as $name) { - // if a Template instance is passed, it might have been instantiated outside of a sandbox, check security - if ($name instanceof TemplateWrapper || $name instanceof Template) { - $name->unwrap()->checkSecurity(); - } - } } try { @@ -1419,6 +1412,10 @@ public static function include(Environment $env, $context, $template, $variables } } + if ($isSandboxed && $loaded) { + $loaded->unwrap()->checkSecurity(); + } + return $loaded ? $loaded->render($variables) : ''; } finally { if ($isSandboxed && !$alreadySandboxed) { diff --git a/tests/Extension/CoreTest.php b/tests/Extension/CoreTest.php index 31458628115..5b8268492a2 100644 --- a/tests/Extension/CoreTest.php +++ b/tests/Extension/CoreTest.php @@ -12,8 +12,13 @@ */ use PHPUnit\Framework\TestCase; +use Twig\Environment; use Twig\Error\RuntimeError; use Twig\Extension\CoreExtension; +use Twig\Extension\SandboxExtension; +use Twig\Loader\ArrayLoader; +use Twig\Sandbox\SecurityError; +use Twig\Sandbox\SecurityPolicy; class CoreTest extends TestCase { @@ -313,6 +318,40 @@ public function provideCompareCases() [1, 42, "\x00\x34\x32"], ]; } + + public function testSandboxedInclude() + { + $twig = new Environment(new ArrayLoader([ + 'index' => '{{ include("included", sandboxed=true) }}', + 'included' => '{{ "included"|e }}', + ])); + $policy = new SecurityPolicy(allowedFunctions: ['include']); + $sandbox = new SandboxExtension($policy, false); + $twig->addExtension($sandbox); + + // We expect a compile error + $this->expectException(SecurityError::class); + $twig->render('index'); + } + + public function testSandboxedIncludeWithPreloadedTemplate() + { + $twig = new Environment(new ArrayLoader([ + 'index' => '{{ include("included", sandboxed=true) }}', + 'included' => '{{ "included"|e }}', + ])); + $policy = new SecurityPolicy(allowedFunctions: ['include']); + $sandbox = new SandboxExtension($policy, false); + $twig->addExtension($sandbox); + + // The template is loaded without the sandbox enabled + // so, no compile error + $twig->load('included'); + + // We expect a runtime error + $this->expectException(SecurityError::class); + $twig->render('index'); + } } final class CoreTestIteratorAggregate implements \IteratorAggregate From ff063afc691e1cfda6714f1915ed766cb108d188 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Tue, 10 Sep 2024 12:38:17 +0200 Subject: [PATCH 451/812] Prepare the 3.11.1 release --- CHANGELOG | 4 ++++ src/Environment.php | 6 +++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index dd776e083b8..55285d681cc 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +# 3.11.1 (2024-09-10) + + * Fix a security issue when an included sandboxed template has been loaded before without the sandbox context + # 3.11.0 (2024-08-08) * Add `Twig\Cache\ChainCache` and `Twig\Cache\ReadOnlyFilesystemCache` diff --git a/src/Environment.php b/src/Environment.php index 32b13135c69..e928e63955e 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -43,11 +43,11 @@ */ class Environment { - public const VERSION = '3.11.0'; - public const VERSION_ID = 301100; + public const VERSION = '3.11.1'; + public const VERSION_ID = 301101; public const MAJOR_VERSION = 4; public const MINOR_VERSION = 11; - public const RELEASE_VERSION = 0; + public const RELEASE_VERSION = 1; public const EXTRA_VERSION = ''; private $charset; From 194cced47b766fb49ac587ff7d4a02529d1431d4 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Tue, 10 Sep 2024 17:11:21 +0200 Subject: [PATCH 452/812] Removed @internal on Environment::getGlobals() --- src/Environment.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Environment.php b/src/Environment.php index 06a306b89c6..fe95adc574d 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -813,8 +813,6 @@ public function addGlobal(string $name, $value) } /** - * @internal - * * @return array */ public function getGlobals(): array From 9369a48c53bf9943a6c05ee4bf846dfdb9e96e68 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Mon, 9 Sep 2024 22:30:28 +0200 Subject: [PATCH 453/812] Deprecate the sandbox tag --- CHANGELOG | 1 + doc/api.rst | 7 +++-- doc/deprecated.rst | 13 +++++++++ doc/tags/sandbox.rst | 5 ++++ src/TokenParser/SandboxTokenParser.php | 2 ++ tests/Extension/SandboxTest.php | 27 ++++++++++++++----- .../sandbox/{array.test => array.legacy.test} | 2 ++ ...not_valid1.test => not_valid1.legacy.test} | 0 ...not_valid2.test => not_valid2.legacy.test} | 0 .../{simple.test => simple.legacy.test} | 4 +++ 10 files changed, 51 insertions(+), 10 deletions(-) rename tests/Fixtures/tags/sandbox/{array.test => array.legacy.test} (73%) rename tests/Fixtures/tags/sandbox/{not_valid1.test => not_valid1.legacy.test} (100%) rename tests/Fixtures/tags/sandbox/{not_valid2.test => not_valid2.legacy.test} (100%) rename tests/Fixtures/tags/sandbox/{simple.test => simple.legacy.test} (55%) diff --git a/CHANGELOG b/CHANGELOG index eefd094ac1b..ef2f219bcce 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,6 @@ # 3.15.0 (2024-XX-XX) + * Deprecate the `sandbox` tag * Improve the way one can deprecate a Twig callable (use `deprecation_info` instead of the other callable options) # 3.14.0 (2024-09-09) diff --git a/doc/api.rst b/doc/api.rst index 09c553175e1..8207231b3e0 100644 --- a/doc/api.rst +++ b/doc/api.rst @@ -498,13 +498,12 @@ The policy object is the first argument of the sandbox constructor:: $twig->addExtension($sandbox); By default, the sandbox mode is disabled and should be enabled when including -untrusted template code by using the ``sandbox`` tag: +untrusted template code by using the ``sandboxed`` option of the ``include`` +function: .. code-block:: twig - {% sandbox %} - {% include 'user.html' %} - {% endsandbox %} + {{ include('user.html', sandboxed: true) }} You can sandbox all templates by passing ``true`` as the second argument of the extension constructor:: diff --git a/doc/deprecated.rst b/doc/deprecated.rst index c280702a5cb..00b90c3a5d7 100644 --- a/doc/deprecated.rst +++ b/doc/deprecated.rst @@ -201,6 +201,19 @@ Sandbox deprecated as of Twig 3.12. You will need to explicitly allow them if needed in 4.0. +* Deprecate the ``sandbox`` tag, use the ``sandboxed`` option of the + ``include`` function instead: + + Before:: + + {% sandbox %} + {% include 'foo.twig' %} + {% endsandbox %} + + After:: + + {{ include('foo.twig', sandboxed: true) }} + Testing Utilities ----------------- diff --git a/doc/tags/sandbox.rst b/doc/tags/sandbox.rst index b331fdb8e69..57f1e5d1e17 100644 --- a/doc/tags/sandbox.rst +++ b/doc/tags/sandbox.rst @@ -1,6 +1,11 @@ ``sandbox`` =========== +.. warning:: + + The ``sandbox`` tag is deprecated as of Twig 3.15. + Use the ``sandboxed`` option of the ``include`` function instead. + The ``sandbox`` tag can be used to enable the sandboxing mode for an included template, when sandboxing is not enabled globally for the Twig environment: diff --git a/src/TokenParser/SandboxTokenParser.php b/src/TokenParser/SandboxTokenParser.php index 70869fbc53d..a7260ac46fc 100644 --- a/src/TokenParser/SandboxTokenParser.php +++ b/src/TokenParser/SandboxTokenParser.php @@ -34,6 +34,8 @@ final class SandboxTokenParser extends AbstractTokenParser public function parse(Token $token): Node { $stream = $this->parser->getStream(); + trigger_deprecation('twig/twig', '3.15', \sprintf('The "sandbox" tag is deprecated in "%s" at line %d.', $stream->getSourceContext()->getName(), $token->getLine())); + $stream->expect(Token::BLOCK_END_TYPE); $body = $this->parser->subparse([$this, 'decideBlockEnd'], true); $stream->expect(Token::BLOCK_END_TYPE); diff --git a/tests/Extension/SandboxTest.php b/tests/Extension/SandboxTest.php index d24a06c6720..e9115fa498e 100644 --- a/tests/Extension/SandboxTest.php +++ b/tests/Extension/SandboxTest.php @@ -60,7 +60,8 @@ protected function setUp(): void '1_basic2_include_template_from_string_sandboxed' => '{{ include(template_from_string("{{ name|upper }}"), sandboxed=true) }}', '1_basic2_include_template_from_string' => '{{ include(template_from_string("{{ name|upper }}")) }}', '1_range_operator' => '{{ (1..2)[0] }}', - '1_syntax_error_wrapper' => '{% sandbox %}{% include "1_syntax_error" %}{% endsandbox %}', + '1_syntax_error_wrapper_legacy' => '{% sandbox %}{% include "1_syntax_error" %}{% endsandbox %}', + '1_syntax_error_wrapper' => '{{ include("1_syntax_error", sandboxed: true) }}', '1_syntax_error' => '{% syntax error }}', '1_childobj_parentmethod' => '{{ child_obj.ParentMethod() }}', '1_childobj_childmethod' => '{{ child_obj.ChildMethod() }}', @@ -98,7 +99,6 @@ public static function getSandboxedForCoreTagsTests() yield ['import', '{% import "macros" as macros %}']; yield ['include', '{% include "macros" %}']; yield ['macro', '{% macro foo() %}{% endmacro %}']; - yield ['sandbox', '{% sandbox %}{% endsandbox %}']; yield ['set', '{% set foo = 1 %}']; // To be uncommented in 4.0 // yield ['use', '{% use "1_empty" %}']; @@ -152,6 +152,21 @@ public function testSandboxUnallowedMethodAccessor() } } + /** + * @group legacy + */ + public function testIfSandBoxIsDisabledAfterSyntaxErrorLegacy() + { + $twig = $this->getEnvironment(false, [], self::$templates); + try { + $twig->load('1_syntax_error_wrapper_legacy')->render(self::$params); + } catch (SyntaxError $e) { + /** @var SandboxExtension $sandbox */ + $sandbox = $twig->getExtension(SandboxExtension::class); + $this->assertFalse($sandbox->isSandboxed()); + } + } + public function testIfSandBoxIsDisabledAfterSyntaxError() { $twig = $this->getEnvironment(false, [], self::$templates); @@ -399,16 +414,16 @@ public function testSandboxLocallySetForAnInclude() $this->assertEquals('fooFOOfoo', $twig->load('2_basic')->render(self::$params), 'Sandbox does nothing if disabled globally and sandboxed not used for the include'); self::$templates = [ - '3_basic' => '{{ obj.foo }}{% sandbox %}{% include "3_included" %}{% endsandbox %}{{ obj.foo }}', - '3_included' => '{% if obj.foo %}{{ obj.foo|upper }}{% endif %}', + '3_basic' => '{{ include("3_included", sandboxed: true) }}', + '3_included' => '{% if true %}{{ "foo"|upper }}{% endif %}', ]; - $twig = $this->getEnvironment(true, [], self::$templates); + $twig = $this->getEnvironment(true, [], self::$templates, functions: ['include']); try { $twig->load('3_basic')->render(self::$params); $this->fail('Sandbox throws a SecurityError exception when the included file is sandboxed'); } catch (SecurityNotAllowedTagError $e) { - $this->assertEquals('sandbox', $e->getTagName()); + $this->assertEquals('if', $e->getTagName()); } } diff --git a/tests/Fixtures/tags/sandbox/array.test b/tests/Fixtures/tags/sandbox/array.legacy.test similarity index 73% rename from tests/Fixtures/tags/sandbox/array.test rename to tests/Fixtures/tags/sandbox/array.legacy.test index b432427e4a6..df9d1e405a1 100644 --- a/tests/Fixtures/tags/sandbox/array.test +++ b/tests/Fixtures/tags/sandbox/array.legacy.test @@ -1,5 +1,7 @@ --TEST-- sandbox tag +--DEPRECATION-- +Since twig/twig 3.15: The "sandbox" tag is deprecated in "index.twig" at line 2. --TEMPLATE-- {%- sandbox %} {%- include "foo.twig" %} diff --git a/tests/Fixtures/tags/sandbox/not_valid1.test b/tests/Fixtures/tags/sandbox/not_valid1.legacy.test similarity index 100% rename from tests/Fixtures/tags/sandbox/not_valid1.test rename to tests/Fixtures/tags/sandbox/not_valid1.legacy.test diff --git a/tests/Fixtures/tags/sandbox/not_valid2.test b/tests/Fixtures/tags/sandbox/not_valid2.legacy.test similarity index 100% rename from tests/Fixtures/tags/sandbox/not_valid2.test rename to tests/Fixtures/tags/sandbox/not_valid2.legacy.test diff --git a/tests/Fixtures/tags/sandbox/simple.test b/tests/Fixtures/tags/sandbox/simple.legacy.test similarity index 55% rename from tests/Fixtures/tags/sandbox/simple.test rename to tests/Fixtures/tags/sandbox/simple.legacy.test index 4d232d8bbd2..7126344753a 100644 --- a/tests/Fixtures/tags/sandbox/simple.test +++ b/tests/Fixtures/tags/sandbox/simple.legacy.test @@ -1,5 +1,9 @@ --TEST-- sandbox tag +--DEPRECATION-- +Since twig/twig 3.15: The "sandbox" tag is deprecated in "index.twig" at line 2. +Since twig/twig 3.15: The "sandbox" tag is deprecated in "index.twig" at line 6. +Since twig/twig 3.15: The "sandbox" tag is deprecated in "index.twig" at line 11. --TEMPLATE-- {%- sandbox %} {%- include "foo.twig" %} From 3f07d2c034b4130523c705661d6842ce7f8ed130 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Tue, 10 Sep 2024 23:33:04 +0200 Subject: [PATCH 454/812] Simplify parser calls when possible --- src/ExpressionParser.php | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/ExpressionParser.php b/src/ExpressionParser.php index a610a390e58..657af4e7814 100644 --- a/src/ExpressionParser.php +++ b/src/ExpressionParser.php @@ -345,8 +345,7 @@ public function parseSequenceExpression() } $first = false; - if ($stream->test(Token::SPREAD_TYPE)) { - $stream->next(); + if ($stream->nextIf(Token::SPREAD_TYPE)) { $expr = $this->parseExpression(); $expr->setAttribute('spread', true); $node->addElement($expr); @@ -387,8 +386,7 @@ public function parseMappingExpression() } $first = false; - if ($stream->test(Token::SPREAD_TYPE)) { - $stream->next(); + if ($stream->nextIf(Token::SPREAD_TYPE)) { $value = $this->parseExpression(); $value->setAttribute('spread', true); $node->addElement($value); From 9e9f8110bde3cb287e16916b5e8b347a5dd61429 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Tue, 10 Sep 2024 23:24:37 +0200 Subject: [PATCH 455/812] Replace most empty() calls --- src/ExpressionParser.php | 2 +- src/Extension/CoreExtension.php | 4 ++-- src/Lexer.php | 8 ++++---- src/Node/Expression/CallExpression.php | 4 ++-- src/Test/IntegrationTestCase.php | 2 +- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/ExpressionParser.php b/src/ExpressionParser.php index a610a390e58..b789f12be86 100644 --- a/src/ExpressionParser.php +++ b/src/ExpressionParser.php @@ -621,7 +621,7 @@ public function parseArguments($namedArguments = false, $definition = false, $al $stream->expect(Token::PUNCTUATION_TYPE, '(', 'A list of arguments must begin with an opening parenthesis'); while (!$stream->test(Token::PUNCTUATION_TYPE, ')')) { - if (!empty($args)) { + if ($args) { $stream->expect(Token::PUNCTUATION_TYPE, ',', 'Arguments must be separated by a comma'); // if the comma above was a trailing comma, early exit the argument parse loop diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index 550dc0f3851..f8abe1080a5 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -545,7 +545,7 @@ public function convertDate($date = null, $timezone = null) } $asString = (string) $date; - if (ctype_digit($asString) || (!empty($asString) && '-' === $asString[0] && ctype_digit(substr($asString, 1)))) { + if (ctype_digit($asString) || (isset($asString[0]) && '-' === $asString[0] && ctype_digit(substr($asString, 1)))) { $date = new \DateTime('@'.$date); } else { $date = new \DateTime($date, $this->getTimezone()); @@ -1616,7 +1616,7 @@ public static function getAttribute(Environment $env, Source $source, $object, $ } elseif (\is_object($object)) { $message = \sprintf('Impossible to access a key "%s" on an object of class "%s" that does not implement ArrayAccess interface.', $item, \get_class($object)); } elseif (\is_array($object)) { - if (empty($object)) { + if (!$object) { $message = \sprintf('Key "%s" does not exist as the sequence/mapping is empty.', $arrayItem); } else { $message = \sprintf('Key "%s" for sequence/mapping with keys "%s" does not exist.', $arrayItem, implode(', ', array_keys($object))); diff --git a/src/Lexer.php b/src/Lexer.php index 28feaa2c128..32d4e97008e 100644 --- a/src/Lexer.php +++ b/src/Lexer.php @@ -217,7 +217,7 @@ public function tokenize(Source $source): TokenStream $this->pushToken(Token::EOF_TYPE); - if (!empty($this->brackets)) { + if ($this->brackets) { [$expect, $lineno] = array_pop($this->brackets); throw new SyntaxError(\sprintf('Unclosed "%s".', $expect), $lineno, $this->source); } @@ -292,7 +292,7 @@ private function lexData(): void private function lexBlock(): void { - if (empty($this->brackets) && preg_match($this->regexes['lex_block'], $this->code, $match, 0, $this->cursor)) { + if (!$this->brackets && preg_match($this->regexes['lex_block'], $this->code, $match, 0, $this->cursor)) { $this->pushToken(Token::BLOCK_END_TYPE); $this->moveCursor($match[0]); $this->popState(); @@ -303,7 +303,7 @@ private function lexBlock(): void private function lexVar(): void { - if (empty($this->brackets) && preg_match($this->regexes['lex_var'], $this->code, $match, 0, $this->cursor)) { + if (!$this->brackets && preg_match($this->regexes['lex_var'], $this->code, $match, 0, $this->cursor)) { $this->pushToken(Token::VAR_END_TYPE); $this->moveCursor($match[0]); $this->popState(); @@ -360,7 +360,7 @@ private function lexExpression(): void } // closing bracket elseif (str_contains(')]}', $this->code[$this->cursor])) { - if (empty($this->brackets)) { + if (!$this->brackets) { throw new SyntaxError(\sprintf('Unexpected "%s".', $this->code[$this->cursor]), $this->lineno, $this->source); } diff --git a/src/Node/Expression/CallExpression.php b/src/Node/Expression/CallExpression.php index 6fc6f66e0ad..920f65caffb 100644 --- a/src/Node/Expression/CallExpression.php +++ b/src/Node/Expression/CallExpression.php @@ -208,7 +208,7 @@ protected function getArguments($callable, $arguments) } elseif ($callableParameter->isDefaultValueAvailable()) { $optionalArguments[] = new ConstantExpression($callableParameter->getDefaultValue(), -1); } elseif ($callableParameter->isOptional()) { - if (empty($parameters)) { + if (!$parameters) { break; } else { $missingArguments[] = $name; @@ -235,7 +235,7 @@ protected function getArguments($callable, $arguments) } } - if (!empty($parameters)) { + if ($parameters) { $unknownParameter = null; foreach ($parameters as $parameter) { if ($parameter instanceof Node) { diff --git a/src/Test/IntegrationTestCase.php b/src/Test/IntegrationTestCase.php index 8690a809e38..67c168090ed 100644 --- a/src/Test/IntegrationTestCase.php +++ b/src/Test/IntegrationTestCase.php @@ -149,7 +149,7 @@ public function getTests($name, $legacyTests = false) $tests[] = [str_replace($fixturesDir.'/', '', $file), $message, $condition, $templates, $exception, $outputs, $deprecation]; } - if ($legacyTests && empty($tests)) { + if ($legacyTests && !$tests) { // add a dummy test to avoid a PHPUnit message return [['not', '-', '', [], '', []]]; } From ccfe6ad26e4d6c8ffed9e7caba90337714dd3eef Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Wed, 11 Sep 2024 08:32:31 +0200 Subject: [PATCH 456/812] Stringify dynamic mapping keys --- CHANGELOG | 1 + src/Node/Expression/ArrayExpression.php | 3 +++ src/Node/Expression/NameExpression.php | 16 ++++++++++++++-- tests/Fixtures/expressions/array.test | 11 ++++++----- 4 files changed, 24 insertions(+), 7 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index ef2f219bcce..c029bb8e51a 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,6 @@ # 3.15.0 (2024-XX-XX) + * Support Markup instances (and any other \Stringable) as dynamic mapping keys * Deprecate the `sandbox` tag * Improve the way one can deprecate a Twig callable (use `deprecation_info` instead of the other callable options) diff --git a/src/Node/Expression/ArrayExpression.php b/src/Node/Expression/ArrayExpression.php index 5f8b0f63f37..9b6c55c29f8 100644 --- a/src/Node/Expression/ArrayExpression.php +++ b/src/Node/Expression/ArrayExpression.php @@ -98,6 +98,9 @@ public function compile(Compiler $compiler): void ++$nextIndex; } else { $key = $pair['key'] instanceof ConstantExpression ? $pair['key']->getAttribute('value') : null; + if ($pair['key'] instanceof NameExpression) { + $pair['key']->setAttribute('stringify', true); + } if ($nextIndex !== $key) { if (\is_int($key)) { diff --git a/src/Node/Expression/NameExpression.php b/src/Node/Expression/NameExpression.php index 286aa5ae20d..a3f42cd0c85 100644 --- a/src/Node/Expression/NameExpression.php +++ b/src/Node/Expression/NameExpression.php @@ -24,7 +24,7 @@ class NameExpression extends AbstractExpression public function __construct(string $name, int $lineno) { - parent::__construct([], ['name' => $name, 'is_defined_test' => false, 'ignore_strict_check' => false, 'always_defined' => false], $lineno); + parent::__construct([], ['name' => $name, 'is_defined_test' => false, 'ignore_strict_check' => false, 'always_defined' => false, 'stringify' => false], $lineno); } public function compile(Compiler $compiler): void @@ -54,6 +54,9 @@ public function compile(Compiler $compiler): void } elseif (isset($this->specialVars[$name])) { $compiler->raw($this->specialVars[$name]); } elseif ($this->getAttribute('always_defined')) { + if ($this->getAttribute('stringify')) { + $compiler->raw(' (string)'); + } $compiler ->raw('$context[') ->string($name) @@ -61,6 +64,9 @@ public function compile(Compiler $compiler): void ; } else { if ($this->getAttribute('ignore_strict_check') || !$compiler->getEnvironment()->isStrictVariables()) { + if ($this->getAttribute('stringify')) { + $compiler->raw(' (string)'); + } $compiler ->raw('($context[') ->string($name) @@ -72,7 +78,13 @@ public function compile(Compiler $compiler): void ->string($name) ->raw(']) || array_key_exists(') ->string($name) - ->raw(', $context) ? $context[') + ->raw(', $context) ?') + ; + if ($this->getAttribute('stringify')) { + $compiler->raw(' (string)'); + } + $compiler + ->raw(' $context[') ->string($name) ->raw('] : (function () { throw new RuntimeError(\'Variable ') ->string($name) diff --git a/tests/Fixtures/expressions/array.test b/tests/Fixtures/expressions/array.test index 35579dc13d2..1de76e93ee2 100644 --- a/tests/Fixtures/expressions/array.test +++ b/tests/Fixtures/expressions/array.test @@ -34,7 +34,8 @@ Twig supports array notation {# keys can be any expression #} {% set a = 1 %} {% set b = "foo" %} -{% set ary = { (a): 'a', (b): 'b', 'c': 'c', (a ~ b): 'd' } %} +{% set markup_instance %}fooe{% endset %} +{% set ary = { (a): 'a', (b): 'b', 'c': 'c', (a ~ b): 'd', (markup_instance): 'e' } %} {{ ary|keys|join(',') }} {{ ary|join(',') }} @@ -65,8 +66,8 @@ FOO,BAR, 1,2 -1,foo,c,1foo -a,b,c,d +1,foo,c,1foo,fooe +a,b,c,d,e b @@ -95,8 +96,8 @@ FOO,BAR, 1,2 -1,foo,c,1foo -a,b,c,d +1,foo,c,1foo,fooe +a,b,c,d,e b From c616783674f9b00689382e12c1a29eabdb777d3f Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Wed, 11 Sep 2024 10:01:02 +0200 Subject: [PATCH 457/812] Add JSON support for the file extension escaping strategy --- CHANGELOG | 1 + src/FileExtensionEscapingStrategy.php | 1 + 2 files changed, 2 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index c029bb8e51a..89f4289bf82 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,6 @@ # 3.15.0 (2024-XX-XX) + * Add JSON support for the file extension escaping strategy * Support Markup instances (and any other \Stringable) as dynamic mapping keys * Deprecate the `sandbox` tag * Improve the way one can deprecate a Twig callable (use `deprecation_info` instead of the other callable options) diff --git a/src/FileExtensionEscapingStrategy.php b/src/FileExtensionEscapingStrategy.php index 812071bf971..5308158d36a 100644 --- a/src/FileExtensionEscapingStrategy.php +++ b/src/FileExtensionEscapingStrategy.php @@ -45,6 +45,7 @@ public static function guess(string $name) switch ($extension) { case 'js': + case 'json': return 'js'; case 'css': From 1a20e961742de10bca43d4d02b7f92b005e3e2a2 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Wed, 11 Sep 2024 10:45:54 +0200 Subject: [PATCH 458/812] Fix CS --- src/Extension/CoreExtension.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index f8abe1080a5..b40d41ffb0f 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -545,7 +545,7 @@ public function convertDate($date = null, $timezone = null) } $asString = (string) $date; - if (ctype_digit($asString) || (isset($asString[0]) && '-' === $asString[0] && ctype_digit(substr($asString, 1)))) { + if (ctype_digit($asString) || ('' !== $asString && '-' === $asString[0] && ctype_digit(substr($asString, 1)))) { $date = new \DateTime('@'.$date); } else { $date = new \DateTime($date, $this->getTimezone()); From 112de8dcfaec86b96d25ea7232cbaa4791c15090 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Tue, 10 Sep 2024 23:40:39 +0200 Subject: [PATCH 459/812] Add support for argument unpacking --- CHANGELOG | 1 + doc/templates.rst | 11 ++++++++-- src/ExpressionParser.php | 11 +++++++++- src/Node/Expression/Unary/SpreadUnary.php | 22 +++++++++++++++++++ .../expressions/call_argument_unpacking.test | 16 ++++++++++++++ ...call_argument_unpacking_before_normal.test | 8 +++++++ 6 files changed, 66 insertions(+), 3 deletions(-) create mode 100644 src/Node/Expression/Unary/SpreadUnary.php create mode 100644 tests/Fixtures/expressions/call_argument_unpacking.test create mode 100644 tests/Fixtures/expressions/call_argument_unpacking_before_normal.test diff --git a/CHANGELOG b/CHANGELOG index 89f4289bf82..ec47be48949 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,6 @@ # 3.15.0 (2024-XX-XX) + * Add support for argument unpackaging * Add JSON support for the file extension escaping strategy * Support Markup instances (and any other \Stringable) as dynamic mapping keys * Deprecate the `sandbox` tag diff --git a/doc/templates.rst b/doc/templates.rst index 3cd8f9b90c7..199d40fe421 100644 --- a/doc/templates.rst +++ b/doc/templates.rst @@ -815,14 +815,21 @@ The following operators don't fit into any of the other categories: {# returns the value of foo if it is defined and not null, 'no' otherwise #} {{ foo ?? 'no' }} -* ``...``: The spread operator can be used to expand sequences or mappings (it - cannot be used to expand the arguments of a function call): +* ``...``: The spread operator can be used to expand sequences or mappings or + to expand the arguments of a function call: .. code-block:: twig {% set numbers = [1, 2, ...moreNumbers] %} {% set ratings = {'foo': 10, 'bar': 5, ...moreRatings} %} + {{ 'Hello %s %s!'|format(...['Fabien', 'Potencier']) }} + + .. versionadded:: 3.15 + + Support for expanding the arguments of a function call was introduced in + Twig 3.15. + Operators ~~~~~~~~~ diff --git a/src/ExpressionParser.php b/src/ExpressionParser.php index c77e093053d..37e94c6baed 100644 --- a/src/ExpressionParser.php +++ b/src/ExpressionParser.php @@ -30,6 +30,7 @@ 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; /** @@ -618,6 +619,7 @@ public function parseArguments($namedArguments = false, $definition = false, $al $stream = $this->parser->getStream(); $stream->expect(Token::PUNCTUATION_TYPE, '(', 'A list of arguments must begin with an opening parenthesis'); + $hasSpread = false; while (!$stream->test(Token::PUNCTUATION_TYPE, ')')) { if ($args) { $stream->expect(Token::PUNCTUATION_TYPE, ',', 'Arguments must be separated by a comma'); @@ -632,7 +634,14 @@ public function parseArguments($namedArguments = false, $definition = false, $al $token = $stream->expect(Token::NAME_TYPE, null, 'An argument must be a name'); $value = new NameExpression($token->getValue(), $this->parser->getCurrentToken()->getLine()); } else { - $value = $this->parseExpression(0, $allowArrow); + if ($stream->nextIf(Token::SPREAD_TYPE)) { + $hasSpread = true; + $value = new SpreadUnary($this->parseExpression(0, $allowArrow), $stream->getCurrent()->getLine()); + } elseif ($hasSpread) { + throw new SyntaxError('Normal arguments must be placed before argument unpacking.', $stream->getCurrent()->getLine(), $stream->getSourceContext()); + } else { + $value = $this->parseExpression(0, $allowArrow); + } } $name = null; diff --git a/src/Node/Expression/Unary/SpreadUnary.php b/src/Node/Expression/Unary/SpreadUnary.php new file mode 100644 index 00000000000..f99072c257f --- /dev/null +++ b/src/Node/Expression/Unary/SpreadUnary.php @@ -0,0 +1,22 @@ +raw('...'); + } +} diff --git a/tests/Fixtures/expressions/call_argument_unpacking.test b/tests/Fixtures/expressions/call_argument_unpacking.test new file mode 100644 index 00000000000..8e92310bfc9 --- /dev/null +++ b/tests/Fixtures/expressions/call_argument_unpacking.test @@ -0,0 +1,16 @@ +--TEST-- +Twig supports array unpacking for function calls +--TEMPLATE-- +{{ '%s %s %s'|format(...[1, 2, 3]) }} +{{ '%s %s %s'|format(...[1], ...[2, 3]) }} +{{ '%s %s %s'|format(1, ...[2, 3]) }} +{{ '%s %s %s'|format(1, ...[2], ...[3]) }} +{{ '%s %s %s'|format(...it) }} +--DATA-- +return ['it' => new \ArrayIterator([1, 2, 3])] +--EXPECT-- +1 2 3 +1 2 3 +1 2 3 +1 2 3 +1 2 3 diff --git a/tests/Fixtures/expressions/call_argument_unpacking_before_normal.test b/tests/Fixtures/expressions/call_argument_unpacking_before_normal.test new file mode 100644 index 00000000000..24259393fcd --- /dev/null +++ b/tests/Fixtures/expressions/call_argument_unpacking_before_normal.test @@ -0,0 +1,8 @@ +--TEST-- +Twig supports array unpacking for function calls (but not before normal args) +--TEMPLATE-- +{{ '%s %s %s'|format(...[1, 2], 3) }} +--DATA-- +return [] +--EXCEPTION-- +Twig\Error\SyntaxError: Normal arguments must be placed before argument unpacking in "index.twig" at line 2. From c7d9a5072720f06191d8f005523480ce62f4550f Mon Sep 17 00:00:00 2001 From: Rylix <54363222+rylixs@users.noreply.github.com> Date: Wed, 11 Sep 2024 14:41:00 +0200 Subject: [PATCH 460/812] Fix typo --- doc/advanced.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/advanced.rst b/doc/advanced.rst index 01f03d98078..c54d961a3a0 100644 --- a/doc/advanced.rst +++ b/doc/advanced.rst @@ -710,7 +710,7 @@ method:: of the Twig environment. It means that globals should not be used to store values that can change during the lifetime of the Twig environment. For instance, if you're using an application server like RoadRunner or - FrakenPHP, you should not store values related to the current context (like + FrankenPHP, you should not store values related to the current context (like the HTTP request). If you do so, don't forget to reset the cache between requests by calling ``Environment::resetGlobals()``. From bfafa5b613a6ee7ef1662b2f55cf679f3c487ffe Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Wed, 11 Sep 2024 16:28:41 +0200 Subject: [PATCH 461/812] Add StringCastUnary --- src/Node/Expression/ArrayExpression.php | 3 ++- src/Node/Expression/NameExpression.php | 11 +--------- src/Node/Expression/Unary/StringCastUnary.php | 22 +++++++++++++++++++ 3 files changed, 25 insertions(+), 11 deletions(-) create mode 100644 src/Node/Expression/Unary/StringCastUnary.php diff --git a/src/Node/Expression/ArrayExpression.php b/src/Node/Expression/ArrayExpression.php index 9b6c55c29f8..6c6efee13c4 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\StringCastUnary; class ArrayExpression extends AbstractExpression { @@ -99,7 +100,7 @@ public function compile(Compiler $compiler): void } else { $key = $pair['key'] instanceof ConstantExpression ? $pair['key']->getAttribute('value') : null; if ($pair['key'] instanceof NameExpression) { - $pair['key']->setAttribute('stringify', true); + $pair['key'] = new StringCastUnary($pair['key'], $pair['key']->getTemplateLine()); } if ($nextIndex !== $key) { diff --git a/src/Node/Expression/NameExpression.php b/src/Node/Expression/NameExpression.php index a3f42cd0c85..12a9bb71cc6 100644 --- a/src/Node/Expression/NameExpression.php +++ b/src/Node/Expression/NameExpression.php @@ -24,7 +24,7 @@ class NameExpression extends AbstractExpression public function __construct(string $name, int $lineno) { - parent::__construct([], ['name' => $name, 'is_defined_test' => false, 'ignore_strict_check' => false, 'always_defined' => false, 'stringify' => false], $lineno); + parent::__construct([], ['name' => $name, 'is_defined_test' => false, 'ignore_strict_check' => false, 'always_defined' => false], $lineno); } public function compile(Compiler $compiler): void @@ -54,9 +54,6 @@ public function compile(Compiler $compiler): void } elseif (isset($this->specialVars[$name])) { $compiler->raw($this->specialVars[$name]); } elseif ($this->getAttribute('always_defined')) { - if ($this->getAttribute('stringify')) { - $compiler->raw(' (string)'); - } $compiler ->raw('$context[') ->string($name) @@ -64,9 +61,6 @@ public function compile(Compiler $compiler): void ; } else { if ($this->getAttribute('ignore_strict_check') || !$compiler->getEnvironment()->isStrictVariables()) { - if ($this->getAttribute('stringify')) { - $compiler->raw(' (string)'); - } $compiler ->raw('($context[') ->string($name) @@ -80,9 +74,6 @@ public function compile(Compiler $compiler): void ->string($name) ->raw(', $context) ?') ; - if ($this->getAttribute('stringify')) { - $compiler->raw(' (string)'); - } $compiler ->raw(' $context[') ->string($name) diff --git a/src/Node/Expression/Unary/StringCastUnary.php b/src/Node/Expression/Unary/StringCastUnary.php new file mode 100644 index 00000000000..87ea17ca8ad --- /dev/null +++ b/src/Node/Expression/Unary/StringCastUnary.php @@ -0,0 +1,22 @@ +raw('(string)'); + } +} From b5d19ee4be0e4376a18df6901da973f25fe83062 Mon Sep 17 00:00:00 2001 From: Javier Eguiluz Date: Thu, 12 Sep 2024 11:42:41 +0200 Subject: [PATCH 462/812] [Doc] Provide an alternative for the deprecated spaceless filter --- doc/filters/spaceless.rst | 3 ++- doc/templates.rst | 15 --------------- 2 files changed, 2 insertions(+), 16 deletions(-) diff --git a/doc/filters/spaceless.rst b/doc/filters/spaceless.rst index 7a8e4093eeb..0300faf69bd 100644 --- a/doc/filters/spaceless.rst +++ b/doc/filters/spaceless.rst @@ -3,7 +3,8 @@ .. warning:: - The ``spaceless`` filter is deprecated as of Twig 3.12. + The ``spaceless`` filter is deprecated as of Twig 3.12. Instead, use the + :ref:`whitespace control features `. Use the ``spaceless`` filter to remove whitespace *between HTML tags*, not whitespace within HTML tags or whitespace in plain text: diff --git a/doc/templates.rst b/doc/templates.rst index 199d40fe421..8fd5222dc25 100644 --- a/doc/templates.rst +++ b/doc/templates.rst @@ -934,21 +934,6 @@ the modifiers on one side of a tag or on both sides: {{~ value }} {# outputs '
  • \nno spaces
  • ' #} -.. tip:: - - In addition to the whitespace modifiers, Twig also has a ``spaceless`` filter - that removes whitespace **between HTML tags**: - - .. code-block:: html+twig - - {% apply spaceless %} -
    - foo bar -
    - {% endapply %} - - {# output will be
    foo bar
    #} - Extensions ---------- From b1c35cd1fc4d5b584d0a494b9c2d2a077367a9be Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Thu, 12 Sep 2024 19:12:58 +0200 Subject: [PATCH 463/812] Tweak docs --- doc/filters/slice.rst | 2 +- doc/functions/attribute.rst | 2 +- doc/templates.rst | 100 +++++++++++++++++++++--------------- 3 files changed, 61 insertions(+), 43 deletions(-) diff --git a/doc/filters/slice.rst b/doc/filters/slice.rst index 3642b85d830..47af2b923e1 100644 --- a/doc/filters/slice.rst +++ b/doc/filters/slice.rst @@ -21,7 +21,7 @@ You can use any valid expression for both the start and the length: {# ... #} {% endfor %} -As syntactic sugar, you can also use the ``[]`` notation: +As syntactic sugar, you can also use the ``[]`` operator: .. code-block:: twig diff --git a/doc/functions/attribute.rst b/doc/functions/attribute.rst index 832859de166..96ec6ea1137 100644 --- a/doc/functions/attribute.rst +++ b/doc/functions/attribute.rst @@ -20,7 +20,7 @@ attribute: .. note:: The resolution algorithm is the same as the one used for the ``.`` - notation, except that the item can be any valid expression. + operator, except that the item can be any valid expression. Arguments --------- diff --git a/doc/templates.rst b/doc/templates.rst index 199d40fe421..6a4ab80c96d 100644 --- a/doc/templates.rst +++ b/doc/templates.rst @@ -72,17 +72,16 @@ You might also be interested in: Variables --------- -The application passes variables to the templates for manipulation in the -template. Variables may have attributes or elements you can access, too. The -visual representation of a variable depends heavily on the application providing -it. +Twig templates have access to variables provided by the PHP application and +variables created in templates via the :doc:`set ` tag. These +variables can be manipulated and displayed in the template. Use a dot (``.``) to access attributes of a variable (methods or properties of a PHP object, or items of a PHP array): .. code-block:: twig - {{ foo.bar }} + {{ user.name }} .. note:: @@ -97,41 +96,7 @@ If a variable or attribute does not exist, the behavior depends on the * When ``false``, it returns ``null``; * When ``true``, it throws an exception. -.. sidebar:: Implementation - - For convenience's sake ``foo.bar`` does the following things on the PHP - layer: - - * check if ``foo`` is a sequence or a mapping and ``bar`` a valid element; - * if not, and if ``foo`` is an object, check that ``bar`` is a valid property; - * if not, and if ``foo`` is an object, check that ``bar`` is a valid method - (even if ``bar`` is the constructor - use ``__construct()`` instead); - * if not, and if ``foo`` is an object, check that ``getBar`` is a valid method; - * if not, and if ``foo`` is an object, check that ``isBar`` is a valid method; - * if not, and if ``foo`` is an object, check that ``hasBar`` is a valid method; - * if not, and if ``strict_variables`` is ``false``, return ``null``; - * if not, throw an exception. - - Twig also supports a specific syntax for accessing items on PHP arrays, - ``foo['bar']``: - - * check if ``foo`` is a sequence or a mapping and ``bar`` a valid element; - * if not, and if ``strict_variables`` is ``false``, return ``null``; - * if not, throw an exception. - -.. note:: - - If you want to access a dynamic attribute of a variable, use the - :doc:`attribute` function instead. - - The ``attribute`` function is also useful when the attribute contains - special characters (like ``-`` that would be interpreted as the minus - operator): - - .. code-block:: twig - - {# equivalent to the non-working foo.data-foo #} - {{ attribute(foo, 'data-foo') }} +Learn more about the :ref:`dot operator `. Global Variables ~~~~~~~~~~~~~~~~ @@ -798,8 +763,60 @@ The following operators don't fit into any of the other categories: " ~ name ~ "!" }}`` would return (assuming ``name`` is ``'John'``) ``Hello John!``. +.. _dot_operator: + * ``.``, ``[]``: Gets an attribute of a variable. + The (``.``) operator abstracts getting an attribute of a variable (methods + or properties of a PHP object, or items of a PHP array): + + .. code-block:: twig + + {{ user.name }} + + .. sidebar:: PHP Implementation + + To resolve ``user.name`` to a PHP call, Twig uses the following algorithm + at runtime: + + * check if ``user`` is a PHP array or a ArrayObject/ArrayAccess object and + ``name`` a valid element; + * if not, and if ``user`` is a PHP object, check that ``name`` is a valid property; + * if not, and if ``user`` is a PHP object, check the following methods and + call the first valid one: ``name()``, ``getName()``, ``isName()``, or + ``hasName()``; + * if not, and if ``strict_variables`` is ``false``, return ``null``; + * if not, throw an exception. + + Twig supports a specific syntax via the ``[]`` operator for accessing items + on sequences and mappings, like in ``user['name']``: + + * check if ``user`` is an array and ``name`` a valid element; + * if not, and if ``strict_variables`` is ``false``, return ``null``; + * if not, throw an exception. + + Twig supports a specific syntax via the ``()`` operator for calling methods + on objects, like in ``user.name()``: + + * check if ``user`` is a object and has the ``name()``, ``getName()``, + ``isName()``, or ``hasName()`` method; + * if not, and if ``strict_variables`` is ``false``, return ``null``; + * if not, throw an exception. + + .. note:: + + If you want to access a dynamic attribute of a variable, use the + :doc:`attribute` function instead. + + The ``attribute`` function is also useful when the attribute contains + special characters (like ``-`` that would be interpreted as the minus + operator): + + .. code-block:: twig + + {# equivalent to the non-working user.first-name #} + {{ attribute(user, 'first-name') }} + * ``?:``: The ternary operator: .. code-block:: twig @@ -876,7 +893,8 @@ determine how to convert the code to PHP: {# it is converted to the following PHP code: (6 & 2) || (6 & 16) #} -Change the default precedence by explicitly grouping expressions with parentheses: +Change the default precedence by explicitly grouping expressions with +parentheses: .. code-block:: twig From 08f28fd06cc4391a71223faa5e324ef0a8b05049 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Thu, 12 Sep 2024 21:22:08 +0200 Subject: [PATCH 464/812] Remove most usage of foo/bar/baz in the docs --- doc/advanced.rst | 18 +++++++-------- doc/api.rst | 24 ++++++++++---------- doc/coding_standards.rst | 32 +++++++++++++------------- doc/deprecated.rst | 8 +++---- doc/filters/default.rst | 17 +++++++------- doc/filters/escape.rst | 2 +- doc/filters/format.rst | 6 ++--- doc/filters/split.rst | 16 ++++++------- doc/filters/url_encode.rst | 4 ++-- doc/functions/include.rst | 6 ++--- doc/tags/embed.rst | 16 ++++++------- doc/tags/include.rst | 10 ++++----- doc/tags/set.rst | 40 ++++++++++++++++----------------- doc/tags/types.rst | 11 ++++----- doc/tags/with.rst | 22 +++++++++--------- doc/templates.rst | 46 +++++++++++++++++++------------------- doc/tests/defined.rst | 8 +++---- doc/tests/empty.rst | 2 +- doc/tests/sameas.rst | 4 ++-- 19 files changed, 147 insertions(+), 145 deletions(-) diff --git a/doc/advanced.rst b/doc/advanced.rst index c54d961a3a0..e07764b8770 100644 --- a/doc/advanced.rst +++ b/doc/advanced.rst @@ -271,8 +271,8 @@ A dynamic filter can define more than one dynamic parts:: The filter receives all dynamic part values before the normal filter arguments, but after the environment and the context. For instance, a call to -``'foo'|a_path_b()`` will result in the following arguments to be passed to the -filter: ``('a', 'b', 'foo')``. +``'Paris'|a_path_b()`` will result in the following arguments to be passed to the +filter: ``('a', 'b', 'Paris')``. Deprecated Filters ~~~~~~~~~~~~~~~~~~ @@ -322,7 +322,7 @@ template using it. See :ref:`deprecation-notices` for more information. $filter = new \Twig\TwigFilter('obsolete', function () { // ... - }, ['deprecated' => '1.1', 'deprecating_package' => 'foo/bar']); + }, ['deprecated' => '1.1', 'deprecating_package' => 'twig/some-package']); Functions --------- @@ -924,14 +924,14 @@ structure in your test directory:: Fixtures/ filters/ - foo.test - bar.test + lower.test + upper.test functions/ - foo.test - bar.test + date.test + format.test tags/ - foo.test - bar.test + for.test + if.test IntegrationTest.php The ``IntegrationTest.php`` file should look like this:: diff --git a/doc/api.rst b/doc/api.rst index 8207231b3e0..b1af42b9b60 100644 --- a/doc/api.rst +++ b/doc/api.rst @@ -398,14 +398,14 @@ The escaping rules are implemented as follows: .. code-block:: html+twig - {{ foo ? "Twig
    " : "
    Twig" }} {# won't be escaped #} + {{ any_value ? "Twig
    " : "
    Twig" }} {# won't be escaped #} {% set text = "Twig
    " %} {{ true ? text : "
    Twig" }} {# will be escaped #} {{ false ? text : "
    Twig" }} {# won't be escaped #} {% set text = "Twig
    " %} - {{ foo ? text|raw : "
    Twig" }} {# won't be escaped #} + {{ any_value ? text|raw : "
    Twig" }} {# won't be escaped #} * Objects with a ``__toString`` method are converted to strings and escaped. You can mark some classes and/or interfaces as being safe for some @@ -413,17 +413,17 @@ The escaping rules are implemented as follows: .. code-block:: twig - // mark object of class Foo as safe for the HTML strategy - $escaper->addSafeClass('Foo', ['html']); + // mark objects of class "HtmlGenerator" as safe for the HTML strategy + $escaper->addSafeClass('HtmlGenerator', ['html']); - // mark object of interface Foo as safe for the HTML strategy - $escaper->addSafeClass('FooInterface', ['html']); + // mark objects of interface "HtmlGeneratorInterface" as safe for the HTML strategy + $escaper->addSafeClass('HtmlGeneratorInterface', ['html']); - // mark object of class Foo as safe for the HTML and JS strategies - $escaper->addSafeClass('Foo', ['html', 'js']); + // mark objects of class "HtmlGenerator" as safe for the HTML and JS strategies + $escaper->addSafeClass('HtmlGenerator', ['html', 'js']); - // mark object of class Foo as safe for all strategies - $escaper->addSafeClass('Foo', ['all']); + // mark objects of class "HtmlGenerator" as safe for all strategies + $escaper->addSafeClass('HtmlGenerator', ['all']); * Escaping is applied before printing, after any other filter is applied: @@ -456,8 +456,8 @@ The escaping rules are implemented as follows: Note that autoescaping has some limitations as escaping is applied on expressions after evaluation. For instance, when working with - concatenation, ``{{ foo|raw ~ bar }}`` won't give the expected result as - escaping is applied on the result of the concatenation, not on the + concatenation, ``{{ value|raw ~ other }}`` won't give the expected result + as escaping is applied on the result of the concatenation, not on the individual variables (so, the ``raw`` filter won't have any effect here). Sandbox Extension diff --git a/doc/coding_standards.rst b/doc/coding_standards.rst index 46be8eca6cb..cf4343f7792 100644 --- a/doc/coding_standards.rst +++ b/doc/coding_standards.rst @@ -15,18 +15,18 @@ standards: .. code-block:: twig - {{ foo }} + {{ user }} {# comment #} - {% if foo %}{% endif %} + {% if user %}{% endif %} When using the whitespace control character, do not put any spaces between it and the delimiter: .. code-block:: twig - {{- foo -}} + {{- user -}} {#- comment -#} - {%- if foo -%}{%- endif -%} + {%- if user -%}{%- endif -%} * Put exactly one space before and after the following operators: comparison operators (``==``, ``!=``, ``<``, ``>``, ``>=``, ``<=``), math @@ -37,8 +37,8 @@ standards: .. code-block:: twig {{ 1 + 2 }} - {{ foo ~ bar }} - {{ true ? true : false }} + {{ first_name ~ ' ' ~ last_name }} + {{ is_correct ? true : false }} * Put exactly one space after the ``:`` sign in mappings and ``,`` in sequences and mappings: @@ -46,7 +46,7 @@ standards: .. code-block:: twig {{ [1, 2, 3] }} - {{ {'foo': 'bar'} }} + {{ {'name': 'Fabien'} }} * Do not put any spaces after an opening parenthesis and before a closing parenthesis in expressions: @@ -59,15 +59,15 @@ standards: .. code-block:: twig - {{ 'foo' }} - {{ "foo" }} + {{ 'Twig' }} + {{ "Twig" }} * Do not put any spaces before and after the following operators: ``|``, ``.``, ``..``, ``[]``: .. code-block:: twig - {{ foo|upper|lower }} + {{ name|upper|lower }} {{ user.name }} {{ user[name] }} {% for i in 1..12 %}{% endfor %} @@ -77,7 +77,7 @@ standards: .. code-block:: twig - {{ foo|default('foo') }} + {{ name|default('Fabien') }} {{ range(1..10) }} * Do not put any spaces before and after the opening and the closing of @@ -85,22 +85,22 @@ standards: .. code-block:: twig - {{ [1, 2, 3] }} - {{ {'foo': 'bar'} }} + [1, 2, 3] + {'name': 'Fabien'} * Use lower cased and underscored variable names: .. code-block:: twig - {% set foo = 'foo' %} - {% set foo_bar = 'foo' %} + {% set name = 'Fabien' %} + {% set first_name = 'Fabien' %} * Indent your code inside tags (use the same indentation as the one used for the target language of the rendered template): .. code-block:: twig - {% block foo %} + {% block content %} {% if true %} true {% endif %} diff --git a/doc/deprecated.rst b/doc/deprecated.rst index 00b90c3a5d7..599f8d8ff44 100644 --- a/doc/deprecated.rst +++ b/doc/deprecated.rst @@ -207,12 +207,12 @@ Sandbox Before:: {% sandbox %} - {% include 'foo.twig' %} + {% include 'user_defined.twig' %} {% endsandbox %} After:: - {{ include('foo.twig', sandboxed: true) }} + {{ include('user_defined.twig', sandboxed: true) }} Testing Utilities ----------------- @@ -260,12 +260,12 @@ Functions/Filters/Tests Before:: - $twig->addFunction(new TwigFunction('foo', 'foo', [ + $twig->addFunction(new TwigFunction('upper', 'upper', [ 'deprecated' => '3.12', 'deprecating_package' => 'twig/twig', ])); After:: - $twig->addFunction(new TwigFunction('foo', 'foo', [ + $twig->addFunction(new TwigFunction('upper', 'upper', [ 'deprecation_info' => new DeprecatedCallableInfo('twig/twig', '3.12'), ])); diff --git a/doc/filters/default.rst b/doc/filters/default.rst index 2376fe7a6d9..35495389049 100644 --- a/doc/filters/default.rst +++ b/doc/filters/default.rst @@ -8,9 +8,9 @@ undefined or empty, otherwise the value of the variable: {{ var|default('var is not defined') }} - {{ var.foo|default('foo item on var is not defined') }} + {{ user.name|default('name item on user is not defined') }} - {{ var['foo']|default('foo item on var is not defined') }} + {{ user['name']|default('name item on user is not defined') }} {{ ''|default('passed var is empty') }} @@ -20,16 +20,17 @@ undefined: .. code-block:: twig - {{ var.method(foo|default('foo'))|default('foo') }} + {{ user.value(name|default('username'))|default('not defined') }} -Using the ``default`` filter on a boolean variable might trigger unexpected behavior, as -``false`` is treated as an empty value. Consider using ``??`` instead: +Using the ``default`` filter on a boolean variable might trigger unexpected +behavior, as ``false`` is treated as an empty value. Consider using ``??`` +instead: .. code-block:: twig - {% set foo = false %} - {{ foo|default(true) }} {# true #} - {{ foo ?? true }} {# false #} + {% set value = false %} + {{ value|default(true) }} {# true #} + {{ value ?? true }} {# false #} .. note:: diff --git a/doc/filters/escape.rst b/doc/filters/escape.rst index 70d18eb5722..574724e35bd 100644 --- a/doc/filters/escape.rst +++ b/doc/filters/escape.rst @@ -120,7 +120,7 @@ callable that accepts a string to escape and the charset:: $twig->getRuntime(EscaperRuntime::class)->setEscaper('identity', $escaper); # Usage in a template: - # {{ 'foo'|escape('identity') }} + # {{ 'Twig'|escape('identity') }} .. note:: diff --git a/doc/filters/format.rst b/doc/filters/format.rst index 68551a3dda3..d7a27f68ab4 100644 --- a/doc/filters/format.rst +++ b/doc/filters/format.rst @@ -6,10 +6,10 @@ The ``format`` filter formats a given string by replacing the placeholders .. code-block:: twig - {{ "I like %s and %s."|format(foo, "bar") }} + {% set fruit = 'apples' %} + {{ "I like %s and %s."|format(fruit, "oranges") }} - {# outputs I like foo and bar - if the foo parameter equals to the foo string. #} + {# outputs I like apples and oranges #} .. seealso:: diff --git a/doc/filters/split.rst b/doc/filters/split.rst index f6728ddad3b..ba0c0044894 100644 --- a/doc/filters/split.rst +++ b/doc/filters/split.rst @@ -6,8 +6,8 @@ of strings: .. code-block:: twig - {% set foo = "one,two,three"|split(',') %} - {# foo contains ['one', 'two', 'three'] #} + {% set items = "one,two,three"|split(',') %} + {# items contains ['one', 'two', 'three'] #} You can also pass a ``limit`` argument: @@ -21,19 +21,19 @@ You can also pass a ``limit`` argument: .. code-block:: twig - {% set foo = "one,two,three,four,five"|split(',', 3) %} - {# foo contains ['one', 'two', 'three,four,five'] #} + {% set items = "one,two,three,four,five"|split(',', 3) %} + {# items contains ['one', 'two', 'three,four,five'] #} If the ``delimiter`` is an empty string, then value will be split by equal chunks. Length is set by the ``limit`` argument (one character by default). .. code-block:: twig - {% set foo = "123"|split('') %} - {# foo contains ['1', '2', '3'] #} + {% set items = "123"|split('') %} + {# items contains ['1', '2', '3'] #} - {% set bar = "aabbcc"|split('', 2) %} - {# bar contains ['aa', 'bb', 'cc'] #} + {% set items = "aabbcc"|split('', 2) %} + {# items contains ['aa', 'bb', 'cc'] #} .. note:: diff --git a/doc/filters/url_encode.rst b/doc/filters/url_encode.rst index f7898313497..c43011fc366 100644 --- a/doc/filters/url_encode.rst +++ b/doc/filters/url_encode.rst @@ -12,8 +12,8 @@ mapping as query string: {{ "string with spaces"|url_encode }} {# outputs "string%20with%20spaces" #} - {{ {'param': 'value', 'foo': 'bar'}|url_encode }} - {# outputs "param=value&foo=bar" #} + {{ {'name': 'Fabien', 'city': 'Paris'}|url_encode }} + {# outputs "name=Fabien&city=Paris" #} .. note:: diff --git a/doc/functions/include.rst b/doc/functions/include.rst index f49971a8fed..a9ebce4e2b3 100644 --- a/doc/functions/include.rst +++ b/doc/functions/include.rst @@ -19,15 +19,15 @@ additional variables: .. code-block:: twig {# template.html will have access to the variables from the current context and the additional ones provided #} - {{ include('template.html', {foo: 'bar'}) }} + {{ include('template.html', {name: 'Fabien'}) }} You can disable access to the context by setting ``with_context`` to ``false``: .. code-block:: twig - {# only the foo variable will be accessible #} - {{ include('template.html', {foo: 'bar'}, with_context = false) }} + {# only the name variable will be accessible #} + {{ include('template.html', {name: 'Fabien'}, with_context = false) }} .. code-block:: twig diff --git a/doc/tags/embed.rst b/doc/tags/embed.rst index 42e33b1955a..232403a1128 100644 --- a/doc/tags/embed.rst +++ b/doc/tags/embed.rst @@ -47,7 +47,7 @@ named "content": │ │ └─────────────────────────────────────┘ -Some pages ("foo" and "bar") share the same content structure - +Some pages ("page_1" and "page_2") share the same content structure - two vertically stacked boxes: .. code-block:: text @@ -65,7 +65,7 @@ two vertically stacked boxes: │ │ └─────────────────────────────────────┘ -While other pages ("boom" and "baz") share a different content structure - +While other pages ("page_3" and "page_4") share a different content structure - two boxes side by side: .. code-block:: text @@ -86,9 +86,9 @@ two boxes side by side: Without the ``embed`` tag, you have two ways to design your templates: * Create two "intermediate" base templates that extend the master layout - template: one with vertically stacked boxes to be used by the "foo" and - "bar" pages and another one with side-by-side boxes for the "boom" and - "baz" pages. + template: one with vertically stacked boxes to be used by the "page_1" and + "page_2" pages and another one with side-by-side boxes for the "page_3" and + "page_4" pages. * Embed the markup for the top/bottom and left/right boxes into each page template directly. @@ -111,7 +111,7 @@ code can live in a single base template, and the two different content structure let's call them "micro layouts" go into separate templates which are embedded as necessary: -Page template ``foo.twig``: +Page template ``page_1.twig``: .. code-block:: twig @@ -152,11 +152,11 @@ The ``embed`` tag takes the exact same arguments as the ``include`` tag: .. code-block:: twig - {% embed "base" with {'foo': 'bar'} %} + {% embed "base" with {'name': 'Fabien'} %} ... {% endembed %} - {% embed "base" with {'foo': 'bar'} only %} + {% embed "base" with {'name': 'Fabien'} only %} ... {% endembed %} diff --git a/doc/tags/include.rst b/doc/tags/include.rst index 93fb0371b8e..a1668d22d86 100644 --- a/doc/tags/include.rst +++ b/doc/tags/include.rst @@ -50,17 +50,17 @@ You can add additional variables by passing them after the ``with`` keyword: .. code-block:: twig {# template.html will have access to the variables from the current context and the additional ones provided #} - {% include 'template.html' with {'foo': 'bar'} %} + {% include 'template.html' with {'name': 'Fabien'} %} - {% set vars = {'foo': 'bar'} %} + {% set vars = {'name': 'Fabien'} %} {% include 'template.html' with vars %} You can disable access to the context by appending the ``only`` keyword: .. code-block:: twig - {# only the foo variable will be accessible #} - {% include 'template.html' with {'foo': 'bar'} only %} + {# only the name variable will be accessible #} + {% include 'template.html' with {'name': 'Fabien'} only %} .. code-block:: twig @@ -96,7 +96,7 @@ placed just after the template name. Here some valid examples: .. code-block:: twig {% include 'sidebar.html' ignore missing %} - {% include 'sidebar.html' ignore missing with {'foo': 'bar'} %} + {% include 'sidebar.html' ignore missing with {'name': 'Fabien'} %} {% include 'sidebar.html' ignore missing only %} You can also provide a list of templates that are checked for existence before diff --git a/doc/tags/set.rst b/doc/tags/set.rst index 7a3a784f503..0ebf7ad5d65 100644 --- a/doc/tags/set.rst +++ b/doc/tags/set.rst @@ -4,45 +4,45 @@ Inside code blocks you can also assign values to variables. Assignments use the ``set`` tag and can have multiple targets. -Here is how you can assign the ``bar`` value to the ``foo`` variable: +Here is how you can assign the ``Fabien`` value to the ``name`` variable: .. code-block:: twig - {% set foo = 'bar' %} + {% set name = 'Fabien' %} -After the ``set`` call, the ``foo`` variable is available in the template like +After the ``set`` call, the ``name`` variable is available in the template like any other ones: .. code-block:: twig - {# displays bar #} - {{ foo }} + {# displays Fabien #} + {{ name }} The assigned value can be any valid :ref:`Twig expression `: .. code-block:: twig - {% set foo = [1, 2] %} - {% set foo = {'foo': 'bar'} %} - {% set foo = 'foo' ~ 'bar' %} + {% set numbers = [1, 2] %} + {% set user = {'name': 'Fabien'} %} + {% set name = 'Fabien' ~ ' ' ~ 'Potencier' %} Several variables can be assigned in one block: .. code-block:: twig - {% set foo, bar = 'foo', 'bar' %} + {% set first, last = 'Fabien', 'Potencier' %} {# is equivalent to #} - {% set foo = 'foo' %} - {% set bar = 'bar' %} + {% set first = 'Fabien' %} + {% set last = 'Potencier' %} -The ``set`` tag can also be used to 'capture' chunks of text: +The ``set`` tag can also be used to "capture" chunks of text: .. code-block:: html+twig - {% set foo %} + {% set content %} @@ -60,19 +60,19 @@ The ``set`` tag can also be used to 'capture' chunks of text: .. code-block:: twig - {% for item in list %} - {% set foo = item %} + {% for item in items %} + {% set value = item %} {% endfor %} - {# foo is NOT available #} + {# value is NOT available #} If you want to access the variable, just declare it before the loop: .. code-block:: twig - {% set foo = "" %} - {% for item in list %} - {% set foo = item %} + {% set value = "" %} + {% for item in items %} + {% set value = item %} {% endfor %} - {# foo is available #} + {# value is available #} diff --git a/doc/tags/types.rst b/doc/tags/types.rst index 49e2807f4bf..c5710ce8ec7 100644 --- a/doc/tags/types.rst +++ b/doc/tags/types.rst @@ -9,13 +9,14 @@ The ``types`` tag declares the types of template variables. To do this, specify a :ref:`mapping ` of names to their types as strings. -Here is how to declare that ``foo`` is a boolean, while ``bar`` is an integer (see note below): +Here is how to declare that ``is_correct`` is a boolean, while ``score`` is an +integer (see note below): .. code-block:: twig {% types { - foo: 'bool', - bar: 'int', + is_correct: 'bool', + score: 'int', } %} You can declare variables as optional by adding the ``?`` suffix: @@ -23,8 +24,8 @@ You can declare variables as optional by adding the ``?`` suffix: .. code-block:: twig {% types { - foo: 'bool', - bar?: 'int', + is_correct: 'bool', + score?: 'int', } %} By default, this tag does not affect the template compilation or runtime behavior. diff --git a/doc/tags/with.rst b/doc/tags/with.rst index 268bb373dd4..7c5e8cf3a82 100644 --- a/doc/tags/with.rst +++ b/doc/tags/with.rst @@ -7,10 +7,10 @@ scope are not visible outside of the scope: .. code-block:: twig {% with %} - {% set foo = 42 %} - {{ foo }} {# foo is 42 here #} + {% set value = 42 %} + {{ value }} {# value is 42 here #} {% endwith %} - foo is not visible here any longer + value is not visible here any longer Instead of defining variables at the beginning of the scope, you can pass a mapping of variables you want to define in the ``with`` tag; the previous @@ -18,13 +18,13 @@ example is equivalent to the following one: .. code-block:: twig - {% with {foo: 42} %} - {{ foo }} {# foo is 42 here #} + {% with {value: 42} %} + {{ value }} {# value is 42 here #} {% endwith %} - foo is not visible here any longer + value is not visible here any longer {# it works with any expression that resolves to a mapping #} - {% set vars = {foo: 42} %} + {% set vars = {value: 42} %} {% with vars %} ... {% endwith %} @@ -34,8 +34,8 @@ disable this behavior by appending the ``only`` keyword: .. code-block:: twig - {% set bar = 'bar' %} - {% with {foo: 42} only %} - {# only foo is defined #} - {# bar is not defined #} + {% set zero = 0 %} + {% with {value: 42} only %} + {# only value is defined #} + {# zero is not defined #} {% endwith %} diff --git a/doc/templates.rst b/doc/templates.rst index 6a4ab80c96d..e1c637ae00a 100644 --- a/doc/templates.rst +++ b/doc/templates.rst @@ -115,9 +115,9 @@ You can assign values to variables inside code blocks. Assignments use the .. code-block:: twig - {% set foo = 'foo' %} - {% set foo = [1, 2] %} - {% set foo = {'foo': 'bar'} %} + {% set name = 'Fabien' %} + {% set numbers = [1, 2] %} + {% set map = {'city': 'Paris'} %} Filters ------- @@ -516,31 +516,31 @@ exist: writing the number down. If a dot is present the number is a float, otherwise an integer. -* ``["foo", "bar"]``: Sequences are defined by a sequence of expressions +* ``["first_name", "last_name"]``: Sequences are defined by a sequence of expressions separated by a comma (``,``) and wrapped with squared brackets (``[]``). -* ``{"foo": "bar"}``: Mappings are defined by a list of keys and values +* ``{"name": "Fabien"}``: Mappings are defined by a list of keys and values separated by a comma (``,``) and wrapped with curly braces (``{}``): .. code-block:: twig {# keys as string #} - {'foo': 'foo', 'bar': 'bar'} + {'name': 'Fabien', 'city': 'Paris'} {# keys as names (equivalent to the previous mapping) #} - {foo: 'foo', bar: 'bar'} + {name: 'Fabien', city: 'Paris'} {# keys as integer #} - {2: 'foo', 4: 'bar'} + {2: 'Twig', 4: 'Symfony'} {# keys can be omitted if it is the same as the variable name #} - {foo} + {Paris} {# is equivalent to the following #} - {'foo': foo} + {'Paris': Paris} {# keys as expressions (the expression must be enclosed into parentheses) #} - {% set foo = 'foo' %} - {(foo): 'foo', (1 + 1): 'bar', (foo ~ 'b'): 'baz'} + {% set key = 'name' %} + {(key): 'Fabien', (1 + 1): 2, ('ci' ~ 'ty'): 'city'} * ``true`` / ``false``: ``true`` represents the true value, ``false`` represents the false value. @@ -552,7 +552,7 @@ Sequences and mappings can be nested: .. code-block:: twig - {% set foo = [1, {"foo": "bar"}] %} + {% set complex = [1, {"name": "Fabien"}] %} .. tip:: @@ -571,8 +571,8 @@ inserted into the string: .. code-block:: twig - {{ "foo #{bar} baz" }} - {{ "foo #{1 + 2} baz" }} + {{ "first #{middle} last" }} + {{ "first #{1 + 2} last" }} .. tip:: @@ -581,8 +581,8 @@ inserted into the string: .. code-block:: twig - {# outputs foo #{1 + 2} baz #} - {{ "foo \#{1 + 2} baz" }} + {# outputs first #{1 + 2} last #} + {{ "first \#{1 + 2} last" }} Math ~~~~ @@ -821,16 +821,16 @@ The following operators don't fit into any of the other categories: .. code-block:: twig - {{ foo ? 'yes' : 'no' }} - {{ foo ?: 'no' }} is the same as {{ foo ? foo : 'no' }} - {{ foo ? 'yes' }} is the same as {{ foo ? 'yes' : '' }} + {{ result ? 'yes' : 'no' }} + {{ result ?: 'no' }} is the same as {{ result ? result : 'no' }} + {{ result ? 'yes' }} is the same as {{ result ? 'yes' : '' }} * ``??``: The null-coalescing operator: .. code-block:: twig - {# returns the value of foo if it is defined and not null, 'no' otherwise #} - {{ foo ?? 'no' }} + {# returns the value of result if it is defined and not null, 'no' otherwise #} + {{ result ?? 'no' }} * ``...``: The spread operator can be used to expand sequences or mappings or to expand the arguments of a function call: @@ -838,7 +838,7 @@ The following operators don't fit into any of the other categories: .. code-block:: twig {% set numbers = [1, 2, ...moreNumbers] %} - {% set ratings = {'foo': 10, 'bar': 5, ...moreRatings} %} + {% set ratings = {'q1': 10, 'q2': 5, ...moreRatings} %} {{ 'Hello %s %s!'|format(...['Fabien', 'Potencier']) }} diff --git a/doc/tests/defined.rst b/doc/tests/defined.rst index 234a28988a0..6e642d11607 100644 --- a/doc/tests/defined.rst +++ b/doc/tests/defined.rst @@ -7,16 +7,16 @@ useful if you use the ``strict_variables`` option: .. code-block:: twig {# defined works with variable names #} - {% if foo is defined %} + {% if user is defined %} ... {% endif %} {# and attributes on variables names #} - {% if foo.bar is defined %} + {% if user.name is defined %} ... {% endif %} - {% if foo['bar'] is defined %} + {% if user['name'] is defined %} ... {% endif %} @@ -25,6 +25,6 @@ method calls, be sure that they are all defined first: .. code-block:: twig - {% if var is defined and foo.method(var) is defined %} + {% if var is defined and user.name(var) is defined %} ... {% endif %} diff --git a/doc/tests/empty.rst b/doc/tests/empty.rst index 3abdb8bbd86..0b45f1d0e31 100644 --- a/doc/tests/empty.rst +++ b/doc/tests/empty.rst @@ -12,7 +12,7 @@ it will check if an empty string is returned. .. code-block:: twig - {% if foo is empty %} + {% if user is empty %} ... {% endif %} diff --git a/doc/tests/sameas.rst b/doc/tests/sameas.rst index c09297114bb..1854f0c81d6 100644 --- a/doc/tests/sameas.rst +++ b/doc/tests/sameas.rst @@ -6,6 +6,6 @@ This is equivalent to ``===`` in PHP: .. code-block:: twig - {% if foo.attribute is same as(false) %} - the foo attribute really is the 'false' PHP value + {% if user.name is same as(false) %} + the user attribute is the 'false' PHP value {% endif %} From 333638e8e6e0f37492fa6ca1a30d58f47f9e8298 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Thu, 12 Sep 2024 08:45:21 +0200 Subject: [PATCH 465/812] Allow to use a dynamic attribute on the . operator via () --- CHANGELOG | 1 + doc/deprecated.rst | 20 +++++++++- doc/functions/attribute.rst | 5 +++ doc/templates.rst | 35 +++++++++------- src/ExpressionParser.php | 12 ++++++ src/Extension/CoreExtension.php | 6 +++ .../expressions/dynamic_attribute.test | 18 +++++++++ .../Fixtures/functions/attribute.legacy.test | 37 +++++++++++++++++ tests/Fixtures/functions/attribute.test | 26 ------------ ... => attribute_with_wrong_args.legacy.test} | 0 .../tests/defined_for_attribute.legacy.test | 40 +++++++++++++++++++ .../Fixtures/tests/defined_for_attribute.test | 10 ++--- tests/TemplateTest.php | 2 +- 13 files changed, 164 insertions(+), 48 deletions(-) create mode 100644 tests/Fixtures/expressions/dynamic_attribute.test create mode 100644 tests/Fixtures/functions/attribute.legacy.test delete mode 100644 tests/Fixtures/functions/attribute.test rename tests/Fixtures/functions/{attribute_with_wrong_args.test => attribute_with_wrong_args.legacy.test} (100%) create mode 100644 tests/Fixtures/tests/defined_for_attribute.legacy.test diff --git a/CHANGELOG b/CHANGELOG index ec47be48949..ad3841a5de9 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,6 @@ # 3.15.0 (2024-XX-XX) + * Deprecate the `attribute` function; use the `.` notation and wrap the name with parenthesis instead * Add support for argument unpackaging * Add JSON support for the file extension escaping strategy * Support Markup instances (and any other \Stringable) as dynamic mapping keys diff --git a/doc/deprecated.rst b/doc/deprecated.rst index 599f8d8ff44..a63b447bb84 100644 --- a/doc/deprecated.rst +++ b/doc/deprecated.rst @@ -8,8 +8,24 @@ feature that was deprecated in Twig 3.x is removed in Twig 4.0). Functions --------- - * The ``twig_test_iterable`` function is deprecated; use the native PHP - ``is_iterable`` function instead. +* The ``twig_test_iterable`` function is deprecated; use the native PHP + ``is_iterable`` function instead. + +* The ``attribute`` function is deprecated as of Twig 3.15 and will be removed + in Twig 4.0. Use the ``.`` operator instead and wrap the name with + parenthesis: + + .. code-block:: twig + + {# before #} + {{ attribute(object, method) }} + {{ attribute(object, method, arguments) }} + {{ attribute(array, item) }} + + {# after #} + {{ object.(method) }} + {{ object.(method)(arguments) }} + {{ array[item] }} Extensions ---------- diff --git a/doc/functions/attribute.rst b/doc/functions/attribute.rst index 96ec6ea1137..e1693b2a1e6 100644 --- a/doc/functions/attribute.rst +++ b/doc/functions/attribute.rst @@ -1,6 +1,11 @@ ``attribute`` ============= +.. warning:: + + The ``attribute`` filter is deprecated as of Twig 3.15. Use the ``.`` + operator that now accepts any expression when wrapped with parenthesis. + The ``attribute`` function can be used to access a "dynamic" attribute of a variable: diff --git a/doc/templates.rst b/doc/templates.rst index e1c637ae00a..f84c4163fde 100644 --- a/doc/templates.rst +++ b/doc/templates.rst @@ -774,6 +774,27 @@ The following operators don't fit into any of the other categories: {{ user.name }} + After the ``.``, you can use any expression by wrapping it with parenthesis + ``()``. + + One use case is when the attribute contains special characters (like ``-`` + that would be interpreted as the minus operator): + + .. code-block:: twig + + {# equivalent to the non-working user.first-name #} + {{ user.('first-name') }} + + Another use case is when the attribute is "dynamic" (defined via a variable): + + .. code-block:: twig + + {{ user.(name) }} + {{ user.('get' ~ name) }} + + Before Twig 3.15, use the :doc:`attribute ` function + instead for the two previous use cases. + .. sidebar:: PHP Implementation To resolve ``user.name`` to a PHP call, Twig uses the following algorithm @@ -803,20 +824,6 @@ The following operators don't fit into any of the other categories: * if not, and if ``strict_variables`` is ``false``, return ``null``; * if not, throw an exception. - .. note:: - - If you want to access a dynamic attribute of a variable, use the - :doc:`attribute` function instead. - - The ``attribute`` function is also useful when the attribute contains - special characters (like ``-`` that would be interpreted as the minus - operator): - - .. code-block:: twig - - {# equivalent to the non-working user.first-name #} - {{ attribute(user, 'first-name') }} - * ``?:``: The ternary operator: .. code-block:: twig diff --git a/src/ExpressionParser.php b/src/ExpressionParser.php index 37e94c6baed..07786eeacc7 100644 --- a/src/ExpressionParser.php +++ b/src/ExpressionParser.php @@ -492,6 +492,18 @@ public function parseSubscriptExpression($node) $arguments = new ArrayExpression([], $lineno); $type = Template::ANY_CALL; if ('.' == $token->getValue()) { + if ($stream->nextIf(Token::PUNCTUATION_TYPE, '(')) { + $arg = $this->parseExpression(); + $stream->expect(Token::PUNCTUATION_TYPE, ')'); + if ($stream->test(Token::PUNCTUATION_TYPE, '(')) { + $type = Template::METHOD_CALL; + foreach ($this->parseArguments() as $n) { + $arguments->addElement($n); + } + } + + return new GetAttrExpression($node, $arg, $arguments, $type, $lineno); + } $token = $stream->next(); if ( Token::NAME_TYPE == $token->getType() diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index b40d41ffb0f..664f1eee237 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -1966,6 +1966,12 @@ public static function parseAttributeFunction(Parser $parser, Node $fakeNode, $a $fakeFunction = new TwigFunction('attribute', fn ($variable, $attribute, $arguments = null) => null); $args = (new CallableArgumentsExtractor($fakeNode, $fakeFunction))->extractArguments($args); + $src = $parser->getStream()->getSourceContext(); + $dep = new DeprecatedCallableInfo('twig/twig', '3.15', 'The "attribute" function is deprecated, use the "." notation instead.'); + $dep->setName('attribute'); + $dep->setType('function'); + $dep->triggerDeprecation($src->getPath() ?: $src->getName(), $line); + return new GetAttrExpression($args[0], $args[1], $args[2] ?? null, Template::ANY_CALL, $line); } } diff --git a/tests/Fixtures/expressions/dynamic_attribute.test b/tests/Fixtures/expressions/dynamic_attribute.test new file mode 100644 index 00000000000..930c6f17498 --- /dev/null +++ b/tests/Fixtures/expressions/dynamic_attribute.test @@ -0,0 +1,18 @@ +--TEST-- +"." notation with dynamic attributes +--TEMPLATE-- +{{ obj.(method) }} +{{ array.(item) }} +{{ obj.("bar")("a", "b") }} +{{ obj.("bar")(...arguments) }} +{{ obj.(method) is defined ? 'ok' : 'ko' }} +{{ obj.(nonmethod) is defined ? 'ok' : 'ko' }} +--DATA-- +return ['obj' => new Twig\Tests\TwigTestFoo(), 'method' => 'foo', 'array' => ['foo' => 'bar'], 'item' => 'foo', 'nonmethod' => 'xxx', 'arguments' => ['a', 'b']] +--EXPECT-- +foo +bar +bar_a-b +bar_a-b +ok +ko diff --git a/tests/Fixtures/functions/attribute.legacy.test b/tests/Fixtures/functions/attribute.legacy.test new file mode 100644 index 00000000000..e8c5863e974 --- /dev/null +++ b/tests/Fixtures/functions/attribute.legacy.test @@ -0,0 +1,37 @@ +--TEST-- +"attribute" function +--DEPRECATION-- +Since twig/twig 3.15: Twig Function "attribute" is deprecated; use "The "attribute" function is deprecated, use the "." notation instead." instead in index.twig at line 2. +Since twig/twig 3.15: Twig Function "attribute" is deprecated; use "The "attribute" function is deprecated, use the "." notation instead." instead in index.twig at line 3. +Since twig/twig 3.15: Twig Function "attribute" is deprecated; use "The "attribute" function is deprecated, use the "." notation instead." instead in index.twig at line 4. +Since twig/twig 3.15: Twig Function "attribute" is deprecated; use "The "attribute" function is deprecated, use the "." notation instead." instead in index.twig at line 5. +Since twig/twig 3.15: Twig Function "attribute" is deprecated; use "The "attribute" function is deprecated, use the "." notation instead." instead in index.twig at line 6. +Since twig/twig 3.15: Twig Function "attribute" is deprecated; use "The "attribute" function is deprecated, use the "." notation instead." instead in index.twig at line 7. +Since twig/twig 3.15: Twig Function "attribute" is deprecated; use "The "attribute" function is deprecated, use the "." notation instead." instead in index.twig at line 8. +Since twig/twig 3.15: Twig Function "attribute" is deprecated; use "The "attribute" function is deprecated, use the "." notation instead." instead in index.twig at line 9. +Since twig/twig 3.15: Twig Function "attribute" is deprecated; use "The "attribute" function is deprecated, use the "." notation instead." instead in index.twig at line 10. +Since twig/twig 3.15: Twig Function "attribute" is deprecated; use "The "attribute" function is deprecated, use the "." notation instead." instead in index.twig at line 11. +--TEMPLATE-- +{{ attribute(obj, method) }} +{{ attribute(variable=obj, attribute=method) }} +{{ attribute(variable: obj, attribute: method) }} +{{ attribute(array, item) }} +{{ attribute(obj, "bar", ["a", "b"]) }} +{{ attribute(obj, "bar", arguments) }} +{{ attribute(variable=obj, attribute="bar", arguments=arguments) }} +{{ attribute(variable: obj, attribute: "bar", arguments: arguments) }} +{{ attribute(obj, method) is defined ? 'ok' : 'ko' }} +{{ attribute(obj, nonmethod) is defined ? 'ok' : 'ko' }} +--DATA-- +return ['obj' => new Twig\Tests\TwigTestFoo(), 'method' => 'foo', 'array' => ['foo' => 'bar'], 'item' => 'foo', 'nonmethod' => 'xxx', 'arguments' => ['a', 'b']] +--EXPECT-- +foo +foo +foo +bar +bar_a-b +bar_a-b +bar_a-b +bar_a-b +ok +ko diff --git a/tests/Fixtures/functions/attribute.test b/tests/Fixtures/functions/attribute.test deleted file mode 100644 index 31cca8c4661..00000000000 --- a/tests/Fixtures/functions/attribute.test +++ /dev/null @@ -1,26 +0,0 @@ ---TEST-- -"attribute" function ---TEMPLATE-- -{{ attribute(obj, method) }} -{{ attribute(variable=obj, attribute=method) }} -{{ attribute(variable: obj, attribute: method) }} -{{ attribute(array, item) }} -{{ attribute(obj, "bar", ["a", "b"]) }} -{{ attribute(obj, "bar", arguments) }} -{{ attribute(variable=obj, attribute="bar", arguments=arguments) }} -{{ attribute(variable: obj, attribute: "bar", arguments: arguments) }} -{{ attribute(obj, method) is defined ? 'ok' : 'ko' }} -{{ attribute(obj, nonmethod) is defined ? 'ok' : 'ko' }} ---DATA-- -return ['obj' => new Twig\Tests\TwigTestFoo(), 'method' => 'foo', 'array' => ['foo' => 'bar'], 'item' => 'foo', 'nonmethod' => 'xxx', 'arguments' => ['a', 'b']] ---EXPECT-- -foo -foo -foo -bar -bar_a-b -bar_a-b -bar_a-b -bar_a-b -ok -ko diff --git a/tests/Fixtures/functions/attribute_with_wrong_args.test b/tests/Fixtures/functions/attribute_with_wrong_args.legacy.test similarity index 100% rename from tests/Fixtures/functions/attribute_with_wrong_args.test rename to tests/Fixtures/functions/attribute_with_wrong_args.legacy.test diff --git a/tests/Fixtures/tests/defined_for_attribute.legacy.test b/tests/Fixtures/tests/defined_for_attribute.legacy.test new file mode 100644 index 00000000000..1117ff3e9e1 --- /dev/null +++ b/tests/Fixtures/tests/defined_for_attribute.legacy.test @@ -0,0 +1,40 @@ +--TEST-- +"defined" support for attribute +--DEPRECATION-- +Since twig/twig 3.15: Twig Function "attribute" is deprecated; use "The "attribute" function is deprecated, use the "." notation instead." instead in index.twig at line 2. +Since twig/twig 3.15: Twig Function "attribute" is deprecated; use "The "attribute" function is deprecated, use the "." notation instead." instead in index.twig at line 3. +Since twig/twig 3.15: Twig Function "attribute" is deprecated; use "The "attribute" function is deprecated, use the "." notation instead." instead in index.twig at line 4. +Since twig/twig 3.15: Twig Function "attribute" is deprecated; use "The "attribute" function is deprecated, use the "." notation instead." instead in index.twig at line 5. +--TEMPLATE-- +{{ attribute(nested, "definedVar") is defined ? 'ok' : 'ko' }} +{{ attribute(nested, "undefinedVar") is not defined ? 'ok' : 'ko' }} +{{ attribute(nested, definedVarName) is defined ? 'ok' : 'ko' }} +{{ attribute(nested, undefinedVarName) is not defined ? 'ok' : 'ko' }} +--DATA-- +return [ + 'nested' => [ + 'definedVar' => 'defined', + ], + 'definedVarName' => 'definedVar', + 'undefinedVarName' => 'undefinedVar', +] +--EXPECT-- +ok +ok +ok +ok +--DATA-- +return [ + 'nested' => [ + 'definedVar' => 'defined', + ], + 'definedVarName' => 'definedVar', + 'undefinedVarName' => 'undefinedVar', +] +--CONFIG-- +return ['strict_variables' => false] +--EXPECT-- +ok +ok +ok +ok diff --git a/tests/Fixtures/tests/defined_for_attribute.test b/tests/Fixtures/tests/defined_for_attribute.test index 5fd2fe3f2d2..6fc6eb68a1e 100644 --- a/tests/Fixtures/tests/defined_for_attribute.test +++ b/tests/Fixtures/tests/defined_for_attribute.test @@ -1,10 +1,10 @@ --TEST-- -"defined" support for attribute +"defined" support for dynamic attribute --TEMPLATE-- -{{ attribute(nested, "definedVar") is defined ? 'ok' : 'ko' }} -{{ attribute(nested, "undefinedVar") is not defined ? 'ok' : 'ko' }} -{{ attribute(nested, definedVarName) is defined ? 'ok' : 'ko' }} -{{ attribute(nested, undefinedVarName) is not defined ? 'ok' : 'ko' }} +{{ nested.("definedVar") is defined ? 'ok' : 'ko' }} +{{ nested.("undefinedVar") is not defined ? 'ok' : 'ko' }} +{{ nested.(definedVarName) is defined ? 'ok' : 'ko' }} +{{ nested.(undefinedVarName) is not defined ? 'ok' : 'ko' }} --DATA-- return [ 'nested' => [ diff --git a/tests/TemplateTest.php b/tests/TemplateTest.php index 7018f4afba8..402c4f3504e 100644 --- a/tests/TemplateTest.php +++ b/tests/TemplateTest.php @@ -76,7 +76,7 @@ public static function getAttributeExceptions() ['{{ array.a() }}', 'Impossible to invoke a method ("a") on a sequence/mapping in "%s" at line 1.'], ['{{ empty_array.a }}', 'Key "a" does not exist as the sequence/mapping is empty in "%s" at line 1.'], ['{{ array.a }}', 'Key "a" for sequence/mapping with keys "foo" does not exist in "%s" at line 1.'], - ['{{ attribute(array, -10) }}', 'Key "-10" for sequence/mapping with keys "foo" does not exist in "%s" at line 1.'], + ['{{ array.(-10) }}', 'Key "-10" for sequence/mapping with keys "foo" does not exist in "%s" at line 1.'], ['{{ array_access.a }}', 'Neither the property "a" nor one of the methods "a()", "geta()"/"isa()"/"hasa()" or "__call()" exist and have public access in class "Twig\Tests\TemplateArrayAccessObject" in "%s" at line 1.'], ['{% from _self import foo %}{% macro foo(obj) %}{{ obj.missing_method() }}{% endmacro %}{{ foo(array_access) }}', 'Neither the property "missing_method" nor one of the methods "missing_method()", "getmissing_method()"/"ismissing_method()"/"hasmissing_method()" or "__call()" exist and have public access in class "Twig\Tests\TemplateArrayAccessObject" in "%s" at line 1.'], ['{{ magic_exception.test }}', 'An exception has been thrown during the rendering of a template ("Hey! Don\'t try to isset me!") in "%s" at line 1.'], From 7961788224e420d095a6075fd7dc24e159050e1d Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Thu, 12 Sep 2024 23:07:08 +0200 Subject: [PATCH 466/812] Clarify docs for the u filter --- doc/filters/u.rst | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/doc/filters/u.rst b/doc/filters/u.rst index 20bb0d5cfe8..f27cb1812c2 100644 --- a/doc/filters/u.rst +++ b/doc/filters/u.rst @@ -18,6 +18,9 @@ Wrapping a text to a given number of characters: Twig = <3 +Here, ``u`` is the filter and ``wordwrap(5)`` is a method called on the result +of the filter; it's equivalent to ``(text|u).wordwrap(5)``. + Truncating a string: .. code-block:: twig @@ -56,14 +59,6 @@ You can also chain methods: TWIG = <3 -For large strings manipulation, use the ``apply`` tag: - -.. code-block:: twig - - {% apply u.wordwrap(5) %} - Some large amount of text... - {% endapply %} - .. note:: The ``u`` filter is part of the ``StringExtension`` which is not installed From b6e3ad895ce318421d5c1dfbc6c04279450cca83 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Fri, 13 Sep 2024 08:14:14 +0200 Subject: [PATCH 467/812] Fix markup --- doc/coding_standards.rst | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/doc/coding_standards.rst b/doc/coding_standards.rst index cf4343f7792..ee404166f5e 100644 --- a/doc/coding_standards.rst +++ b/doc/coding_standards.rst @@ -36,17 +36,17 @@ standards: .. code-block:: twig - {{ 1 + 2 }} - {{ first_name ~ ' ' ~ last_name }} - {{ is_correct ? true : false }} + {{ 1 + 2 }} + {{ first_name ~ ' ' ~ last_name }} + {{ is_correct ? true : false }} * Put exactly one space after the ``:`` sign in mappings and ``,`` in sequences and mappings: .. code-block:: twig - {{ [1, 2, 3] }} - {{ {'name': 'Fabien'} }} + [1, 2, 3] + {'name': 'Fabien'} * Do not put any spaces after an opening parenthesis and before a closing parenthesis in expressions: @@ -77,16 +77,16 @@ standards: .. code-block:: twig - {{ name|default('Fabien') }} - {{ range(1..10) }} + {{ name|default('Fabien') }} + {{ range(1..10) }} * Do not put any spaces before and after the opening and the closing of sequences and mappings: .. code-block:: twig - [1, 2, 3] - {'name': 'Fabien'} + [1, 2, 3] + {'name': 'Fabien'} * Use lower cased and underscored variable names: @@ -100,14 +100,14 @@ standards: .. code-block:: twig - {% block content %} - {% if true %} - true - {% endif %} - {% endblock %} + {% block content %} + {% if true %} + true + {% endif %} + {% endblock %} * Use ``:`` instead of ``=`` to separate argument names and values: .. code-block:: twig - {{ data|convert_encoding(from: 'iso-2022-jp', to: 'UTF-8') }} + {{ data|convert_encoding(from: 'iso-2022-jp', to: 'UTF-8') }} From c8bb3b1b69502c4ecc9924ed694c24327a5c1352 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Fri, 13 Sep 2024 08:15:49 +0200 Subject: [PATCH 468/812] Fix markup --- doc/coding_standards.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/coding_standards.rst b/doc/coding_standards.rst index ee404166f5e..3cc7ba7a6d0 100644 --- a/doc/coding_standards.rst +++ b/doc/coding_standards.rst @@ -92,8 +92,8 @@ standards: .. code-block:: twig - {% set name = 'Fabien' %} - {% set first_name = 'Fabien' %} + {% set name = 'Fabien' %} + {% set first_name = 'Fabien' %} * Indent your code inside tags (use the same indentation as the one used for the target language of the rendered template): From 1dfdd82cfec5f0f1329446526f9b1eb00874cc78 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Fri, 13 Sep 2024 08:14:28 +0200 Subject: [PATCH 469/812] Clarify coding standards --- doc/coding_standards.rst | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/doc/coding_standards.rst b/doc/coding_standards.rst index 3cc7ba7a6d0..d7daa08b506 100644 --- a/doc/coding_standards.rst +++ b/doc/coding_standards.rst @@ -88,13 +88,21 @@ standards: [1, 2, 3] {'name': 'Fabien'} -* Use lower cased and underscored variable names: +* Use snake case for all variable names (provided by the application and + created in templates): .. code-block:: twig {% set name = 'Fabien' %} {% set first_name = 'Fabien' %} +* Use snake case for all function/filter/test names: + + .. code-block:: twig + + {{ 'Fabien Potencier'|to_lower_case }} + {{ generate_random_number() }} + * Indent your code inside tags (use the same indentation as the one used for the target language of the rendered template): From 8330936415806c9906978fd212e7e0faf1e4307c Mon Sep 17 00:00:00 2001 From: Christophe Coevoet Date: Fri, 13 Sep 2024 12:58:50 +0200 Subject: [PATCH 470/812] Add more precise types for the registration of safe classes Safe classes only impact the auto-escaping of the string casting of instance of those classes. By defining that this method only expects names of stringable classes, it allows static analysis tools to report non-working configurations (for instance for people thinking this would impact the auto-escaping for values returned by getters of a safe class). --- src/Runtime/EscaperRuntime.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/Runtime/EscaperRuntime.php b/src/Runtime/EscaperRuntime.php index a3ce1714626..ce41e0a8b87 100644 --- a/src/Runtime/EscaperRuntime.php +++ b/src/Runtime/EscaperRuntime.php @@ -52,6 +52,9 @@ public function getEscapers() return $this->escapers; } + /** + * @param array, string[]> $safeClasses + */ public function setSafeClasses(array $safeClasses = []) { $this->safeClasses = []; @@ -61,6 +64,10 @@ public function setSafeClasses(array $safeClasses = []) } } + /** + * @param class-string<\Stringable> $class + * @param string[] $strategies + */ public function addSafeClass(string $class, array $strategies) { $class = ltrim($class, '\\'); From 8d1af0fc03314a2786decd6457447a583f7dd146 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Fri, 13 Sep 2024 13:56:05 +0200 Subject: [PATCH 471/812] Let's no deprecate the attribute function yet --- doc/deprecated.rst | 7 ++++--- doc/functions/attribute.rst | 2 ++ src/Extension/CoreExtension.php | 3 +++ tests/Fixtures/functions/attribute.legacy.test | 11 ----------- .../Fixtures/tests/defined_for_attribute.legacy.test | 5 ----- 5 files changed, 9 insertions(+), 19 deletions(-) diff --git a/doc/deprecated.rst b/doc/deprecated.rst index a63b447bb84..a648b054a39 100644 --- a/doc/deprecated.rst +++ b/doc/deprecated.rst @@ -11,9 +11,8 @@ Functions * The ``twig_test_iterable`` function is deprecated; use the native PHP ``is_iterable`` function instead. -* The ``attribute`` function is deprecated as of Twig 3.15 and will be removed - in Twig 4.0. Use the ``.`` operator instead and wrap the name with - parenthesis: +* The ``attribute`` function is deprecated as of Twig 3.15. Use the ``.`` + operator instead and wrap the name with parenthesis: .. code-block:: twig @@ -27,6 +26,8 @@ Functions {{ object.(method)(arguments) }} {{ array[item] }} + Note that it won't be removed in 4.0 to allow a smoother upgrade path. + Extensions ---------- diff --git a/doc/functions/attribute.rst b/doc/functions/attribute.rst index e1693b2a1e6..6587921dac6 100644 --- a/doc/functions/attribute.rst +++ b/doc/functions/attribute.rst @@ -5,6 +5,8 @@ The ``attribute`` filter is deprecated as of Twig 3.15. Use the ``.`` operator that now accepts any expression when wrapped with parenthesis. + Note that this filter will still be available in Twig 4.0 to allow a + smoother upgrade path. The ``attribute`` function can be used to access a "dynamic" attribute of a variable: diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index 664f1eee237..e0e6ed5d32f 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -1966,11 +1966,14 @@ public static function parseAttributeFunction(Parser $parser, Node $fakeNode, $a $fakeFunction = new TwigFunction('attribute', fn ($variable, $attribute, $arguments = null) => null); $args = (new CallableArgumentsExtractor($fakeNode, $fakeFunction))->extractArguments($args); + /* + Deprecation to uncomment sometimes during the lifetime of the 4.x branch $src = $parser->getStream()->getSourceContext(); $dep = new DeprecatedCallableInfo('twig/twig', '3.15', 'The "attribute" function is deprecated, use the "." notation instead.'); $dep->setName('attribute'); $dep->setType('function'); $dep->triggerDeprecation($src->getPath() ?: $src->getName(), $line); + */ return new GetAttrExpression($args[0], $args[1], $args[2] ?? null, Template::ANY_CALL, $line); } diff --git a/tests/Fixtures/functions/attribute.legacy.test b/tests/Fixtures/functions/attribute.legacy.test index e8c5863e974..31cca8c4661 100644 --- a/tests/Fixtures/functions/attribute.legacy.test +++ b/tests/Fixtures/functions/attribute.legacy.test @@ -1,16 +1,5 @@ --TEST-- "attribute" function ---DEPRECATION-- -Since twig/twig 3.15: Twig Function "attribute" is deprecated; use "The "attribute" function is deprecated, use the "." notation instead." instead in index.twig at line 2. -Since twig/twig 3.15: Twig Function "attribute" is deprecated; use "The "attribute" function is deprecated, use the "." notation instead." instead in index.twig at line 3. -Since twig/twig 3.15: Twig Function "attribute" is deprecated; use "The "attribute" function is deprecated, use the "." notation instead." instead in index.twig at line 4. -Since twig/twig 3.15: Twig Function "attribute" is deprecated; use "The "attribute" function is deprecated, use the "." notation instead." instead in index.twig at line 5. -Since twig/twig 3.15: Twig Function "attribute" is deprecated; use "The "attribute" function is deprecated, use the "." notation instead." instead in index.twig at line 6. -Since twig/twig 3.15: Twig Function "attribute" is deprecated; use "The "attribute" function is deprecated, use the "." notation instead." instead in index.twig at line 7. -Since twig/twig 3.15: Twig Function "attribute" is deprecated; use "The "attribute" function is deprecated, use the "." notation instead." instead in index.twig at line 8. -Since twig/twig 3.15: Twig Function "attribute" is deprecated; use "The "attribute" function is deprecated, use the "." notation instead." instead in index.twig at line 9. -Since twig/twig 3.15: Twig Function "attribute" is deprecated; use "The "attribute" function is deprecated, use the "." notation instead." instead in index.twig at line 10. -Since twig/twig 3.15: Twig Function "attribute" is deprecated; use "The "attribute" function is deprecated, use the "." notation instead." instead in index.twig at line 11. --TEMPLATE-- {{ attribute(obj, method) }} {{ attribute(variable=obj, attribute=method) }} diff --git a/tests/Fixtures/tests/defined_for_attribute.legacy.test b/tests/Fixtures/tests/defined_for_attribute.legacy.test index 1117ff3e9e1..5fd2fe3f2d2 100644 --- a/tests/Fixtures/tests/defined_for_attribute.legacy.test +++ b/tests/Fixtures/tests/defined_for_attribute.legacy.test @@ -1,10 +1,5 @@ --TEST-- "defined" support for attribute ---DEPRECATION-- -Since twig/twig 3.15: Twig Function "attribute" is deprecated; use "The "attribute" function is deprecated, use the "." notation instead." instead in index.twig at line 2. -Since twig/twig 3.15: Twig Function "attribute" is deprecated; use "The "attribute" function is deprecated, use the "." notation instead." instead in index.twig at line 3. -Since twig/twig 3.15: Twig Function "attribute" is deprecated; use "The "attribute" function is deprecated, use the "." notation instead." instead in index.twig at line 4. -Since twig/twig 3.15: Twig Function "attribute" is deprecated; use "The "attribute" function is deprecated, use the "." notation instead." instead in index.twig at line 5. --TEMPLATE-- {{ attribute(nested, "definedVar") is defined ? 'ok' : 'ko' }} {{ attribute(nested, "undefinedVar") is not defined ? 'ok' : 'ko' }} From 048183eaa690d10e98d70d4aa71603e6d081fd15 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Fri, 13 Sep 2024 14:06:34 +0200 Subject: [PATCH 472/812] Add cross-link --- doc/functions/attribute.rst | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/doc/functions/attribute.rst b/doc/functions/attribute.rst index 6587921dac6..7757fa08f0d 100644 --- a/doc/functions/attribute.rst +++ b/doc/functions/attribute.rst @@ -3,8 +3,10 @@ .. warning:: - The ``attribute`` filter is deprecated as of Twig 3.15. Use the ``.`` - operator that now accepts any expression when wrapped with parenthesis. + The ``attribute`` filter is deprecated as of Twig 3.15. Use the + :ref:`dot operator ` that now accepts any expression + when wrapped with parenthesis. + Note that this filter will still be available in Twig 4.0 to allow a smoother upgrade path. From ba6e856d92db7f0566980d3477bee246acea0550 Mon Sep 17 00:00:00 2001 From: Ruud Kamphuis Date: Fri, 13 Sep 2024 15:04:20 +0200 Subject: [PATCH 473/812] Set IteratorAggregate generics for Node This will help PHPStan and Psalm to understand what happens when iterating over a Node. --- src/Node/Node.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Node/Node.php b/src/Node/Node.php index 2ccbcf6101f..275e61e2d68 100644 --- a/src/Node/Node.php +++ b/src/Node/Node.php @@ -20,6 +20,8 @@ * Represents a node in the AST. * * @author Fabien Potencier + * + * @implements \IteratorAggregate */ #[YieldReady] class Node implements \Countable, \IteratorAggregate From 4c9526a31d911d3f66a1927336859286c3fc123e Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sat, 14 Sep 2024 10:31:13 +0200 Subject: [PATCH 474/812] Rename a variable for more clarity --- src/Util/CallableArgumentsExtractor.php | 30 ++++++++++++------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/Util/CallableArgumentsExtractor.php b/src/Util/CallableArgumentsExtractor.php index 8811ca9c87e..fedee621581 100644 --- a/src/Util/CallableArgumentsExtractor.php +++ b/src/Util/CallableArgumentsExtractor.php @@ -71,37 +71,37 @@ public function extractArguments(Node $arguments): array [$callableParameters, $isPhpVariadic] = $this->getCallableParameters(); $arguments = []; - $names = []; + $callableParameterNames = []; $missingArguments = []; $optionalArguments = []; $pos = 0; foreach ($callableParameters as $callableParameter) { - $name = $this->normalizeName($callableParameter->name); + $callableParameterName = $this->normalizeName($callableParameter->name); if (\PHP_VERSION_ID >= 80000 && 'range' === $callable) { - if ('start' === $name) { - $name = 'low'; - } elseif ('end' === $name) { - $name = 'high'; + if ('start' === $callableParameterName) { + $callableParameterName = 'low'; + } elseif ('end' === $callableParameterName) { + $callableParameterName = 'high'; } } - $names[] = $name; + $callableParameterNames[] = $callableParameterName; - if (\array_key_exists($name, $extractedArguments)) { + if (\array_key_exists($callableParameterName, $extractedArguments)) { if (\array_key_exists($pos, $extractedArguments)) { - throw new SyntaxError(\sprintf('Argument "%s" is defined twice for %s "%s".', $name, $this->twigCallable->getType(), $this->twigCallable->getName()), $this->node->getTemplateLine(), $this->node->getSourceContext()); + throw new SyntaxError(\sprintf('Argument "%s" is defined twice for %s "%s".', $callableParameterName, $this->twigCallable->getType(), $this->twigCallable->getName()), $this->node->getTemplateLine(), $this->node->getSourceContext()); } if (\count($missingArguments)) { throw new SyntaxError(\sprintf( 'Argument "%s" could not be assigned for %s "%s(%s)" because it is mapped to an internal PHP function which cannot determine default value for optional argument%s "%s".', - $name, $this->twigCallable->getType(), $this->twigCallable->getName(), implode(', ', $names), \count($missingArguments) > 1 ? 's' : '', implode('", "', $missingArguments) + $callableParameterName, $this->twigCallable->getType(), $this->twigCallable->getName(), implode(', ', $callableParameterNames), \count($missingArguments) > 1 ? 's' : '', implode('", "', $missingArguments) ), $this->node->getTemplateLine(), $this->node->getSourceContext()); } $arguments = array_merge($arguments, $optionalArguments); - $arguments[] = $extractedArguments[$name]; - unset($extractedArguments[$name]); + $arguments[] = $extractedArguments[$callableParameterName]; + unset($extractedArguments[$callableParameterName]); $optionalArguments = []; } elseif (\array_key_exists($pos, $extractedArguments)) { $arguments = array_merge($arguments, $optionalArguments); @@ -116,9 +116,9 @@ public function extractArguments(Node $arguments): array break; } - $missingArguments[] = $name; + $missingArguments[] = $callableParameterName; } else { - throw new SyntaxError(\sprintf('Value for argument "%s" is required for %s "%s".', $name, $this->twigCallable->getType(), $this->twigCallable->getName()), $this->node->getTemplateLine(), $this->node->getSourceContext()); + throw new SyntaxError(\sprintf('Value for argument "%s" is required for %s "%s".', $callableParameterName, $this->twigCallable->getType(), $this->twigCallable->getName()), $this->node->getTemplateLine(), $this->node->getSourceContext()); } } @@ -151,7 +151,7 @@ public function extractArguments(Node $arguments): array throw new SyntaxError( \sprintf( 'Unknown argument%s "%s" for %s "%s(%s)".', - \count($extractedArguments) > 1 ? 's' : '', implode('", "', array_keys($extractedArguments)), $this->twigCallable->getType(), $this->twigCallable->getName(), implode(', ', $names) + \count($extractedArguments) > 1 ? 's' : '', implode('", "', array_keys($extractedArguments)), $this->twigCallable->getType(), $this->twigCallable->getName(), implode(', ', $callableParameterNames) ), $unknownArgument ? $unknownArgument->getTemplateLine() : $this->node->getTemplateLine(), $unknownArgument ? $unknownArgument->getSourceContext() : $this->node->getSourceContext() From 5a94ddc76ee1f6d86e546feb98ee0d0ea5f4f2b1 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sat, 14 Sep 2024 10:56:07 +0200 Subject: [PATCH 475/812] Suggest wording --- doc/filters/spaceless.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/filters/spaceless.rst b/doc/filters/spaceless.rst index 0300faf69bd..7fab7fb2602 100644 --- a/doc/filters/spaceless.rst +++ b/doc/filters/spaceless.rst @@ -3,8 +3,8 @@ .. warning:: - The ``spaceless`` filter is deprecated as of Twig 3.12. Instead, use the - :ref:`whitespace control features `. + The ``spaceless`` filter is deprecated as of Twig 3.12. While not a full + replacement, you can check the :ref:`whitespace control features `. Use the ``spaceless`` filter to remove whitespace *between HTML tags*, not whitespace within HTML tags or whitespace in plain text: From 0d1821b0d3e2a46634c654d8224adaff5debd59a Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Mon, 16 Sep 2024 12:03:13 +0200 Subject: [PATCH 476/812] fix typo --- doc/deprecated.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/deprecated.rst b/doc/deprecated.rst index a648b054a39..7222b4f664b 100644 --- a/doc/deprecated.rst +++ b/doc/deprecated.rst @@ -252,7 +252,7 @@ Testing Utilities instead, which will be abstract in 4.0. * The data providers ``getTests()`` and ``getLegacyTests()`` on - ``Twig\Test\IntegrationTestCase`` are considered final als of Twig 3.13. + ``Twig\Test\IntegrationTestCase`` are considered final as of Twig 3.13. Environment ----------- From c5486b07b930bca151c2e983b724abcc58438769 Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Mon, 16 Sep 2024 12:21:35 +0200 Subject: [PATCH 477/812] fix intl-extra tests --- extra/intl-extra/Tests/Fixtures/currency_names.test | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/extra/intl-extra/Tests/Fixtures/currency_names.test b/extra/intl-extra/Tests/Fixtures/currency_names.test index bc2c54d0274..7834d721a2f 100644 --- a/extra/intl-extra/Tests/Fixtures/currency_names.test +++ b/extra/intl-extra/Tests/Fixtures/currency_names.test @@ -10,7 +10,7 @@ return []; --EXPECT-- 0 -292 -292 +293 +293 US Dollar dollar des États-Unis From 52ded207d33d5bc721d19d5f1db8e8c39a8e1b76 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sat, 14 Sep 2024 10:30:39 +0200 Subject: [PATCH 478/812] Allow Twig callable arguments to use camel or snake names --- CHANGELOG | 2 + doc/deprecated.rst | 3 + src/Util/CallableArgumentsExtractor.php | 39 +++++++---- tests/Util/CallableArgumentsExtractorTest.php | 66 +++++++++++++++++-- 4 files changed, 94 insertions(+), 16 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index ad3841a5de9..ea986469c38 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,7 @@ # 3.15.0 (2024-XX-XX) + * Allow Twig callable argument names to be free-form (snake-case or camelCase) independently of the PHP callable signature + They were automatically converted to snake-cased before * Deprecate the `attribute` function; use the `.` notation and wrap the name with parenthesis instead * Add support for argument unpackaging * Add JSON support for the file extension escaping strategy diff --git a/doc/deprecated.rst b/doc/deprecated.rst index 7222b4f664b..3a80f632249 100644 --- a/doc/deprecated.rst +++ b/doc/deprecated.rst @@ -286,3 +286,6 @@ Functions/Filters/Tests $twig->addFunction(new TwigFunction('upper', 'upper', [ 'deprecation_info' => new DeprecatedCallableInfo('twig/twig', '3.12'), ])); + +* For variadic arguments, use snake-case for the argument name to ease the + transition to 4.0. diff --git a/src/Util/CallableArgumentsExtractor.php b/src/Util/CallableArgumentsExtractor.php index fedee621581..d8625169d70 100644 --- a/src/Util/CallableArgumentsExtractor.php +++ b/src/Util/CallableArgumentsExtractor.php @@ -40,22 +40,25 @@ public function __construct( public function extractArguments(Node $arguments): array { $extractedArguments = []; + $extractedArgumentNameMap = []; $named = false; foreach ($arguments as $name => $node) { if (!\is_int($name)) { $named = true; - $name = $this->normalizeName($name); } elseif ($named) { throw new SyntaxError(\sprintf('Positional arguments cannot be used after named arguments for %s "%s".', $this->twigCallable->getType(), $this->twigCallable->getName()), $this->node->getTemplateLine(), $this->node->getSourceContext()); } - $extractedArguments[$name] = $node; + $extractedArguments[$normalizedName = $this->normalizeName($name)] = $node; + $extractedArgumentNameMap[$normalizedName] = $name; } if (!$named && !$this->twigCallable->isVariadic()) { $min = $this->twigCallable->getMinimalNumberOfRequiredArguments(); if (\count($extractedArguments) < $this->rc->getReflector()->getNumberOfRequiredParameters() - $min) { - throw new SyntaxError(\sprintf('Value for argument "%s" is required for %s "%s".', $this->rc->getReflector()->getParameters()[$min + \count($extractedArguments)]->getName(), $this->twigCallable->getType(), $this->twigCallable->getName()), $this->node->getTemplateLine(), $this->node->getSourceContext()); + $argName = $this->toSnakeCase($this->rc->getReflector()->getParameters()[$min + \count($extractedArguments)]->getName()); + + throw new SyntaxError(\sprintf('Value for argument "%s" is required for %s "%s".', $argName, $this->twigCallable->getType(), $this->twigCallable->getName()), $this->node->getTemplateLine(), $this->node->getSourceContext()); } return $extractedArguments; @@ -76,7 +79,7 @@ public function extractArguments(Node $arguments): array $optionalArguments = []; $pos = 0; foreach ($callableParameters as $callableParameter) { - $callableParameterName = $this->normalizeName($callableParameter->name); + $callableParameterName = $callableParameter->name; if (\PHP_VERSION_ID >= 80000 && 'range' === $callable) { if ('start' === $callableParameterName) { $callableParameterName = 'low'; @@ -86,8 +89,9 @@ public function extractArguments(Node $arguments): array } $callableParameterNames[] = $callableParameterName; + $normalizedCallableParameterName = $this->normalizeName($callableParameterName); - if (\array_key_exists($callableParameterName, $extractedArguments)) { + if (\array_key_exists($normalizedCallableParameterName, $extractedArguments)) { if (\array_key_exists($pos, $extractedArguments)) { throw new SyntaxError(\sprintf('Argument "%s" is defined twice for %s "%s".', $callableParameterName, $this->twigCallable->getType(), $this->twigCallable->getName()), $this->node->getTemplateLine(), $this->node->getSourceContext()); } @@ -95,13 +99,13 @@ public function extractArguments(Node $arguments): array if (\count($missingArguments)) { throw new SyntaxError(\sprintf( 'Argument "%s" could not be assigned for %s "%s(%s)" because it is mapped to an internal PHP function which cannot determine default value for optional argument%s "%s".', - $callableParameterName, $this->twigCallable->getType(), $this->twigCallable->getName(), implode(', ', $callableParameterNames), \count($missingArguments) > 1 ? 's' : '', implode('", "', $missingArguments) + $callableParameterName, $this->twigCallable->getType(), $this->twigCallable->getName(), implode(', ', array_map([$this, 'toSnakeCase'], $callableParameterNames)), \count($missingArguments) > 1 ? 's' : '', implode('", "', $missingArguments) ), $this->node->getTemplateLine(), $this->node->getSourceContext()); } $arguments = array_merge($arguments, $optionalArguments); - $arguments[] = $extractedArguments[$callableParameterName]; - unset($extractedArguments[$callableParameterName]); + $arguments[] = $extractedArguments[$normalizedCallableParameterName]; + unset($extractedArguments[$normalizedCallableParameterName]); $optionalArguments = []; } elseif (\array_key_exists($pos, $extractedArguments)) { $arguments = array_merge($arguments, $optionalArguments); @@ -118,7 +122,7 @@ public function extractArguments(Node $arguments): array $missingArguments[] = $callableParameterName; } else { - throw new SyntaxError(\sprintf('Value for argument "%s" is required for %s "%s".', $callableParameterName, $this->twigCallable->getType(), $this->twigCallable->getName()), $this->node->getTemplateLine(), $this->node->getSourceContext()); + throw new SyntaxError(\sprintf('Value for argument "%s" is required for %s "%s".', $this->toSnakeCase($callableParameterName), $this->twigCallable->getType(), $this->twigCallable->getName()), $this->node->getTemplateLine(), $this->node->getSourceContext()); } } @@ -128,7 +132,13 @@ public function extractArguments(Node $arguments): array if (\is_int($key)) { $arbitraryArguments->addElement($value); } else { - $arbitraryArguments->addElement($value, new ConstantExpression($key, $this->node->getTemplateLine())); + $originalKey = $extractedArgumentNameMap[$key]; + if ($originalKey !== $this->toSnakeCase($originalKey)) { + trigger_deprecation('twig/twig', '3.15', \sprintf('Using "snake_case" for variadic arguments is required for a smooth upgrade with Twig 4.0; rename "%s" to "%s" in "%s" at line %d.', $originalKey, $this->toSnakeCase($originalKey), $this->node->getSourceContext()->getName(), $this->node->getTemplateLine())); + } + $arbitraryArguments->addElement($value, new ConstantExpression($this->toSnakeCase($originalKey), $this->node->getTemplateLine())); + // I Twig 4.0, don't convert the key: + // $arbitraryArguments->addElement($value, new ConstantExpression($originalKey, $this->node->getTemplateLine())); } unset($extractedArguments[$key]); } @@ -151,7 +161,7 @@ public function extractArguments(Node $arguments): array throw new SyntaxError( \sprintf( 'Unknown argument%s "%s" for %s "%s(%s)".', - \count($extractedArguments) > 1 ? 's' : '', implode('", "', array_keys($extractedArguments)), $this->twigCallable->getType(), $this->twigCallable->getName(), implode(', ', $callableParameterNames) + \count($extractedArguments) > 1 ? 's' : '', implode('", "', array_keys($extractedArguments)), $this->twigCallable->getType(), $this->twigCallable->getName(), implode(', ', array_map([$this, 'toSnakeCase'], $callableParameterNames)) ), $unknownArgument ? $unknownArgument->getTemplateLine() : $this->node->getTemplateLine(), $unknownArgument ? $unknownArgument->getSourceContext() : $this->node->getSourceContext() @@ -163,7 +173,12 @@ public function extractArguments(Node $arguments): array private function normalizeName(string $name): string { - return strtolower(preg_replace(['/([A-Z]+)([A-Z][a-z])/', '/([a-z\d])([A-Z])/'], ['\\1_\\2', '\\1_\\2'], $name)); + return strtolower(str_replace('_', '', $name)); + } + + private function toSnakeCase(string $name): string + { + return strtolower(preg_replace(['/([A-Z]+)([A-Z][a-z])/', '/([a-z0-9])([A-Z])/'], '\1_\2', $name)); } private function getCallableParameters(): array diff --git a/tests/Util/CallableArgumentsExtractorTest.php b/tests/Util/CallableArgumentsExtractorTest.php index cfea0f8a941..182283183ad 100644 --- a/tests/Util/CallableArgumentsExtractorTest.php +++ b/tests/Util/CallableArgumentsExtractorTest.php @@ -12,15 +12,20 @@ */ use PHPUnit\Framework\TestCase; +use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; use Twig\Error\SyntaxError; use Twig\Node\Expression\ConstantExpression; use Twig\Node\Expression\FunctionExpression; +use Twig\Node\Expression\VariadicExpression; use Twig\Node\Node; +use Twig\Source; use Twig\TwigFunction; use Twig\Util\CallableArgumentsExtractor; class CallableArgumentsExtractorTest extends TestCase { + use ExpectDeprecationTrait; + public function testGetArguments() { $this->assertEquals(['U', null], $this->getArguments('date', 'date', ['format' => 'U', 'timestamp' => null])); @@ -29,7 +34,7 @@ public function testGetArguments() public function testGetArgumentsWhenPositionalArgumentsAfterNamedArguments() { $this->expectException(SyntaxError::class); - $this->expectExceptionMessage('Positional arguments cannot be used after named arguments for function "date".'); + $this->expectExceptionMessage('Positional arguments cannot be used after named arguments for function "date" in "test.twig" at line 2.'); $this->getArguments('date', 'date', ['timestamp' => 123456, 'Y-m-d']); } @@ -37,7 +42,7 @@ public function testGetArgumentsWhenPositionalArgumentsAfterNamedArguments() public function testGetArgumentsWhenArgumentIsDefinedTwice() { $this->expectException(SyntaxError::class); - $this->expectExceptionMessage('Argument "format" is defined twice for function "date".'); + $this->expectExceptionMessage('Argument "format" is defined twice for function "date" in "test.twig" at line 2.'); $this->getArguments('date', 'date', ['Y-m-d', 'format' => 'U']); } @@ -80,6 +85,54 @@ public function testGetArgumentsForStaticMethod() $this->assertEquals(['arg1'], $this->getArguments('custom_static_function', __CLASS__.'::customStaticFunction', ['arg1' => 'arg1'])); } + /** + * @dataProvider getGetArgumentsConversionData + */ + public function testGetArgumentsConversion($arg1, $arg2) + { + $this->assertEquals([null], $this->getArguments('custom', eval("return fn (\$$arg1) => '';"), [$arg1 => null])); + $this->assertEquals([null], $this->getArguments('custom', eval("return fn (\$$arg2) => '';"), [$arg2 => null])); + $this->assertEquals([null], $this->getArguments('custom', eval("return fn (\$$arg1) => '';"), [$arg2 => null])); + $this->assertEquals([null], $this->getArguments('custom', eval("return fn (\$$arg2) => '';"), [$arg1 => null])); + } + + public static function getGetArgumentsConversionData() + { + yield ['some_name', 'some_name']; + yield ['someName', 'some_name']; + yield ['no_svg', 'noSVG']; + yield ['error_404', 'error404']; + yield ['errCode_404', 'err_code_404']; + yield ['errCode404', 'err_code_404']; + yield ['aBc', 'a_b_c']; + yield ['aBC', 'a_b_c']; + } + + /** + * @group legacy + */ + public function testGetArgumentsConversionForVariadics() + { + $this->expectDeprecation('Since twig/twig 3.15: Using "snake_case" for variadic arguments is required for a smooth upgrade with Twig 4.0; rename "someNumberVariadic" to "some_number_variadic" in "test.twig" at line 2.'); + + $this->assertEquals([ + new ConstantExpression('a', 0), + new ConstantExpression(12, 0), + new VariadicExpression([ + new ConstantExpression('some_text_variadic', 2), new ConstantExpression('a', 0), + new ConstantExpression('some_number_variadic', 2), new ConstantExpression(12, 0), + ], 2), + ], $this->getArguments('custom', eval("return fn (string \$someText, int \$some_number, ...\$args) => '';"), ['some_text' => 'a', 'someNumber' => 12, 'some_text_variadic' => 'a', 'someNumberVariadic' => 12], true)); + } + + public function testGetArgumentsError() + { + $this->expectException(SyntaxError::class); + $this->expectExceptionMessage('Value for argument "some_name" is required for function "custom_static_function" in "test.twig" at line 2.'); + + $this->getArguments('custom_static_function', [$this, 'customFunctionSnakeCamel'], ['someCity' => 'Paris']); + } + public function testResolveArgumentsWithMissingParameterForArbitraryArguments() { $this->expectException(SyntaxError::class); @@ -119,6 +172,10 @@ public function customFunction($arg1, $arg2 = 'default', $arg3 = []) { } + public function customFunctionSnakeCamel($someName, $some_city) + { + } + public function customFunctionWithArbitraryArguments() { } @@ -126,14 +183,15 @@ public function customFunctionWithArbitraryArguments() private function getArguments(string $name, $callable, array $args, bool $isVariadic = false): array { $function = new TwigFunction($name, $callable, ['is_variadic' => $isVariadic]); - $node = new ExpressionCall($function, new Node([]), 0); + $node = new ExpressionCall($function, new Node([]), 2); + $node->setSourceContext(new Source('', 'test.twig')); foreach ($args as $name => $arg) { $args[$name] = new ConstantExpression($arg, 0); } $arguments = (new CallableArgumentsExtractor($node, $function))->extractArguments(new Node($args)); foreach ($arguments as $name => $argument) { - $arguments[$name] = $argument->getAttribute('value'); + $arguments[$name] = $isVariadic ? $argument : $argument->getAttribute('value'); } return $arguments; From 37fa03681eb05a807b2859895c574a6e22743dba Mon Sep 17 00:00:00 2001 From: Christophe Coevoet Date: Thu, 19 Sep 2024 11:48:28 +0200 Subject: [PATCH 479/812] fix the handling of predefined arguments in CallExpression --- src/Node/Expression/CallExpression.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Node/Expression/CallExpression.php b/src/Node/Expression/CallExpression.php index 920f65caffb..5154d8bf9ea 100644 --- a/src/Node/Expression/CallExpression.php +++ b/src/Node/Expression/CallExpression.php @@ -334,7 +334,7 @@ private function getTwigCallable(): TwigCallableInterface [ 'is_variadic' => $this->hasAttribute('is_variadic') ? $this->getAttribute('is_variadic') : $current->isVariadic(), ], - ))->withDynamicArguments($this->getAttribute('name'), $this->hasAttribute('dynamic_name') ? $this->getAttribute('dynamic_name') : $current->getDynamicName(), $this->hasAttribute('arguments') ?: $current->getArguments()), + ))->withDynamicArguments($this->getAttribute('name'), $this->hasAttribute('dynamic_name') ? $this->getAttribute('dynamic_name') : $current->getDynamicName(), $this->hasAttribute('arguments') ? $this->getAttribute('arguments') : $current->getArguments()), 'function' => (new TwigFunction( $this->hasAttribute('name') ? $this->getAttribute('name') : $current->getName(), $this->hasAttribute('callable') ? $this->getAttribute('callable') : $current->getCallable(), @@ -344,7 +344,7 @@ private function getTwigCallable(): TwigCallableInterface 'needs_charset' => $this->hasAttribute('needs_charset') ? $this->getAttribute('needs_charset') : $current->needsCharset(), 'is_variadic' => $this->hasAttribute('is_variadic') ? $this->getAttribute('is_variadic') : $current->isVariadic(), ], - ))->withDynamicArguments($this->getAttribute('name'), $this->hasAttribute('dynamic_name') ? $this->getAttribute('dynamic_name') : $current->getDynamicName(), $this->hasAttribute('arguments') ?: $current->getArguments()), + ))->withDynamicArguments($this->getAttribute('name'), $this->hasAttribute('dynamic_name') ? $this->getAttribute('dynamic_name') : $current->getDynamicName(), $this->hasAttribute('arguments') ? $this->getAttribute('arguments') : $current->getArguments()), 'filter' => (new TwigFilter( $this->getAttribute('name'), $this->hasAttribute('callable') ? $this->getAttribute('callable') : $current->getCallable(), @@ -354,7 +354,7 @@ private function getTwigCallable(): TwigCallableInterface 'needs_charset' => $this->hasAttribute('needs_charset') ? $this->getAttribute('needs_charset') : $current->needsCharset(), 'is_variadic' => $this->hasAttribute('is_variadic') ? $this->getAttribute('is_variadic') : $current->isVariadic(), ], - ))->withDynamicArguments($this->getAttribute('name'), $this->hasAttribute('dynamic_name') ? $this->getAttribute('dynamic_name') : $current->getDynamicName(), $this->hasAttribute('arguments') ?: $current->getArguments()), + ))->withDynamicArguments($this->getAttribute('name'), $this->hasAttribute('dynamic_name') ? $this->getAttribute('dynamic_name') : $current->getDynamicName(), $this->hasAttribute('arguments') ? $this->getAttribute('arguments') : $current->getArguments()), }); return $this->getAttribute('twig_callable'); From ebbbaf7069b402b4d1b9baeb569cd9ac66be911b Mon Sep 17 00:00:00 2001 From: Christophe Coevoet Date: Thu, 19 Sep 2024 13:43:42 +0200 Subject: [PATCH 480/812] Use a more precise return type in AbstractTwigCallable --- src/AbstractTwigCallable.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/AbstractTwigCallable.php b/src/AbstractTwigCallable.php index 2e9b34d18be..d85f0f861f8 100644 --- a/src/AbstractTwigCallable.php +++ b/src/AbstractTwigCallable.php @@ -104,6 +104,9 @@ public function needsContext(): bool return $this->options['needs_context']; } + /** + * @return static + */ public function withDynamicArguments(string $name, string $dynamicName, array $arguments): self { $new = clone $this; From 07b626a96fed66fe03a67b9b0699d0f8418643eb Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Thu, 19 Sep 2024 23:13:12 +0200 Subject: [PATCH 481/812] Fix deprecation messages --- src/ExpressionParser.php | 4 ++-- src/Node/Expression/CallExpression.php | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/ExpressionParser.php b/src/ExpressionParser.php index 07786eeacc7..d09cfbb48f8 100644 --- a/src/ExpressionParser.php +++ b/src/ExpressionParser.php @@ -319,7 +319,7 @@ public function parseStringExpression() } /** - * @deprecated since 3.11, use parseSequenceExpression() instead + * @deprecated since Twig 3.11, use parseSequenceExpression() instead */ public function parseArrayExpression() { @@ -360,7 +360,7 @@ public function parseSequenceExpression() } /** - * @deprecated since 3.11, use parseMappingExpression() instead + * @deprecated since Twig 3.11, use parseMappingExpression() instead */ public function parseHashExpression() { diff --git a/src/Node/Expression/CallExpression.php b/src/Node/Expression/CallExpression.php index 5154d8bf9ea..8e999c7eb9e 100644 --- a/src/Node/Expression/CallExpression.php +++ b/src/Node/Expression/CallExpression.php @@ -128,7 +128,7 @@ protected function compileArguments(Compiler $compiler, $isArray = false): void } /** - * @deprecated since 3.12, use Twig\Util\CallableArgumentsExtractor::getArguments() instead + * @deprecated since Twig 3.12, use Twig\Util\CallableArgumentsExtractor::getArguments() instead */ protected function getArguments($callable, $arguments) { @@ -258,7 +258,7 @@ protected function getArguments($callable, $arguments) } /** - * @deprecated since 3.12 + * @deprecated since Twig 3.12 */ protected function normalizeName(string $name): string { From 3be417d2ebafadd6c74e7347e0747e094835386b Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Fri, 20 Sep 2024 11:43:16 +0200 Subject: [PATCH 482/812] Improve date methods phpdoc --- src/Extension/CoreExtension.php | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index e0e6ed5d32f..59937aec517 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -443,9 +443,9 @@ public static function random(string $charset, $values = null, $max = null) * * {{ post.published_at|date("m/d/Y") }} * - * @param \DateTimeInterface|\DateInterval|string $date A date - * @param string|null $format The target format, null to use the default - * @param \DateTimeZone|string|false|null $timezone The target timezone, null to use the default, false to leave unchanged + * @param \DateTimeInterface|\DateInterval|string|int|null $date A date, a timestamp or null to use the current time + * @param string|null $format The target format, null to use the default + * @param \DateTimeZone|string|false|null $timezone The target timezone, null to use the default, false to leave unchanged */ public function formatDate($date, $format = null, $timezone = null): string { @@ -466,8 +466,8 @@ public function formatDate($date, $format = null, $timezone = null): string * * {{ post.published_at|date_modify("-1day")|date("m/d/Y") }} * - * @param \DateTimeInterface|string $date A date - * @param string $modifier A modifier string + * @param \DateTimeInterface|string|int|null $date A date, a timestamp or null to use the current time + * @param string $modifier A modifier string * * @return \DateTime|\DateTimeImmutable * @@ -506,8 +506,8 @@ public static function dateConverter(Environment $env, $date, $format = null, $t * {# do something #} * {% endif %} * - * @param \DateTimeInterface|string|null $date A date or null to use the current time - * @param \DateTimeZone|string|false|null $timezone The target timezone, null to use the default, false to leave unchanged + * @param \DateTimeInterface|string|int|null $date A date, a timestamp or null to use the current time + * @param \DateTimeZone|string|false|null $timezone The target timezone, null to use the default, false to leave unchanged * * @return \DateTime|\DateTimeImmutable */ From ac88c9ae757d3eebee6ef5721bed591d131a551e Mon Sep 17 00:00:00 2001 From: Hugo Alliaume Date: Thu, 19 Sep 2024 11:57:45 +0200 Subject: [PATCH 483/812] [Doc] Document about `html` and `html_attr` strategies Co-authored-by: Fabien Potencier --- doc/filters/escape.rst | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/doc/filters/escape.rst b/doc/filters/escape.rst index 574724e35bd..cf1ad61efa4 100644 --- a/doc/filters/escape.rst +++ b/doc/filters/escape.rst @@ -39,7 +39,8 @@ And here is how to escape variables included in JavaScript code: The ``escape`` filter supports the following escaping strategies for HTML documents: -* ``html``: escapes a string for the **HTML body** context. +* ``html``: escapes a string for the **HTML body** context, + or for HTML attributes values **inside quotes**. * ``js``: escapes a string for the **JavaScript** context. @@ -50,7 +51,8 @@ documents: * ``url``: escapes a string for the **URI or parameter** contexts. This should not be used to escape an entire URI; only a subcomponent being inserted. -* ``html_attr``: escapes a string for the **HTML attribute** context. +* ``html_attr``: escapes a string for the **HTML attribute** context, + **without quotes** around HTML attribute values. Note that doing contextual escaping in HTML documents is hard and choosing the right escaping strategy depends on a lot of factors. Please, read related @@ -90,6 +92,27 @@ to learn more about this topic. {{ var|escape(strategy)|raw }} {# won't be double-escaped #} {% endautoescape %} +.. tip:: + + The ``html_attr`` escaping strategy can be useful when you need to + escape a **dynamic HTML attribute name**: + + .. code-block:: html+twig + +

    + + It can also be used for escaping a **dynamic HTML attribute value** + if it is not quoted, but this is **less performant**. + Instead, it is recommended to quote the HTML attribute value and use + the ``html`` escaping strategy: + + .. code-block:: html+twig + +

    + + {# is equivalent to, but is less performant #} +

    + Custom Escapers --------------- From d1beac4dfa0fbc0bc6d00eaaf9f4d18ee65db7f5 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sat, 21 Sep 2024 09:32:42 +0200 Subject: [PATCH 484/812] Add compile-time checks for the "matches" operator --- src/Node/Expression/Binary/MatchesBinary.php | 18 ++++++++++++++++++ src/Test/IntegrationTestCase.php | 2 +- tests/Fixtures/expressions/matches.test | 4 ++++ ...ror.test => matches_error_compilation.test} | 2 +- .../expressions/matches_error_runtime.test | 8 ++++++++ 5 files changed, 32 insertions(+), 2 deletions(-) rename tests/Fixtures/expressions/{matches_error.test => matches_error_compilation.test} (53%) create mode 100644 tests/Fixtures/expressions/matches_error_runtime.test diff --git a/src/Node/Expression/Binary/MatchesBinary.php b/src/Node/Expression/Binary/MatchesBinary.php index 4669044e01a..ba25313c003 100644 --- a/src/Node/Expression/Binary/MatchesBinary.php +++ b/src/Node/Expression/Binary/MatchesBinary.php @@ -12,9 +12,27 @@ namespace Twig\Node\Expression\Binary; use Twig\Compiler; +use Twig\Error\SyntaxError; +use Twig\Node\Node; +use Twig\Node\Expression\ConstantExpression; class MatchesBinary extends AbstractBinary { + public function __construct(Node $left, Node $right, int $lineno) + { + if ($right instanceof ConstantExpression) { + $regexp = $right->getAttribute('value'); + set_error_handler(static fn ($t, $m) => throw new SyntaxError(\sprintf('Regexp "%s" passed to "matches" is not valid: %s.', $regexp, substr($m, 14)), $lineno)); + try { + preg_match($regexp, ''); + } finally { + restore_error_handler(); + } + } + + parent::__construct($left, $right, $lineno); + } + public function compile(Compiler $compiler): void { $compiler diff --git a/src/Test/IntegrationTestCase.php b/src/Test/IntegrationTestCase.php index 67c168090ed..b66c17fd2b9 100644 --- a/src/Test/IntegrationTestCase.php +++ b/src/Test/IntegrationTestCase.php @@ -245,7 +245,7 @@ protected function doIntegrationTest($file, $message, $condition, $templates, $e $output = trim($template->render(eval($match[1].';')), "\n "); } catch (\Exception $e) { if (false !== $exception) { - $this->assertSame(trim($exception), trim(\sprintf('%s: %s', \get_class($e), $e->getMessage()))); + $this->assertStringMatchesFormat(trim($exception), trim(\sprintf('%s: %s', \get_class($e), $e->getMessage()))); return; } diff --git a/tests/Fixtures/expressions/matches.test b/tests/Fixtures/expressions/matches.test index 8f5e3669e61..00a5702b4a8 100644 --- a/tests/Fixtures/expressions/matches.test +++ b/tests/Fixtures/expressions/matches.test @@ -2,7 +2,9 @@ Twig supports the "matches" operator --TEMPLATE-- {{ 'foo' matches '/o/' ? 'OK' : 'KO' }} +{{ 'foo' matches '/o/'|lower ? 'OK' : 'KO' }} {{ 'foo' matches '/^fo/' ? 'OK' : 'KO' }} +{{ 'foo' matches '/^' ~ 'fo/' ? 'OK' : 'KO' }} {{ 'foo' matches '/O/i' ? 'OK' : 'KO' }} {{ null matches '/o/' }} --DATA-- @@ -11,4 +13,6 @@ return [] OK OK OK +OK +OK 0 diff --git a/tests/Fixtures/expressions/matches_error.test b/tests/Fixtures/expressions/matches_error_compilation.test similarity index 53% rename from tests/Fixtures/expressions/matches_error.test rename to tests/Fixtures/expressions/matches_error_compilation.test index 1220eb42212..c251be13484 100644 --- a/tests/Fixtures/expressions/matches_error.test +++ b/tests/Fixtures/expressions/matches_error_compilation.test @@ -5,4 +5,4 @@ Twig supports the "matches" operator with a great error message --DATA-- return [] --EXCEPTION-- -Twig\Error\RuntimeError: Regexp "/o" passed to "matches" is not valid: No ending delimiter '/' found in "index.twig" at line 2 +Twig\Error\SyntaxError: Regexp "/o" passed to "matches" is not valid: No ending delimiter '/' found in "index.twig" at line 2. diff --git a/tests/Fixtures/expressions/matches_error_runtime.test b/tests/Fixtures/expressions/matches_error_runtime.test new file mode 100644 index 00000000000..4a2bb594352 --- /dev/null +++ b/tests/Fixtures/expressions/matches_error_runtime.test @@ -0,0 +1,8 @@ +--TEST-- +Twig supports the "matches" operator with a great error message +--TEMPLATE-- +{{ 'foo' matches 1 + 2 }} +--DATA-- +return [] +--EXCEPTION-- +Twig\Error\RuntimeError: Regexp "3" passed to "matches" is not valid: Delimiter must not be alphanumeric%sbackslash%sin "index.twig" at line 2 From 15279ca8ee0ce1024456d8e8886226533b2fddd6 Mon Sep 17 00:00:00 2001 From: Ruud Kamphuis Date: Mon, 23 Sep 2024 09:41:35 +0200 Subject: [PATCH 485/812] Add return types in FileystemLoader --- src/Loader/FilesystemLoader.php | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/Loader/FilesystemLoader.php b/src/Loader/FilesystemLoader.php index c60964f5fc8..cad163eadf2 100644 --- a/src/Loader/FilesystemLoader.php +++ b/src/Loader/FilesystemLoader.php @@ -24,6 +24,9 @@ class FilesystemLoader implements LoaderInterface /** Identifier of the main namespace. */ public const MAIN_NAMESPACE = '__main__'; + /** + * @var array> + */ protected $paths = []; protected $cache = []; protected $errorCache = []; @@ -31,7 +34,7 @@ class FilesystemLoader implements LoaderInterface private $rootPath; /** - * @param string|array $paths A path or an array of paths where to look for templates + * @param string|string[] $paths A path or an array of paths where to look for templates * @param string|null $rootPath The root path common to all relative paths (null for getcwd()) */ public function __construct($paths = [], ?string $rootPath = null) @@ -48,6 +51,8 @@ public function __construct($paths = [], ?string $rootPath = null) /** * Returns the paths to the templates. + * + * @return list */ public function getPaths(string $namespace = self::MAIN_NAMESPACE): array { @@ -58,6 +63,8 @@ public function getPaths(string $namespace = self::MAIN_NAMESPACE): array * Returns the path namespaces. * * The main namespace is always defined. + * + * @return list */ public function getNamespaces(): array { @@ -65,7 +72,7 @@ public function getNamespaces(): array } /** - * @param string|array $paths A path or an array of paths where to look for templates + * @param string|string[] $paths A path or an array of paths where to look for templates */ public function setPaths($paths, string $namespace = self::MAIN_NAMESPACE): void { From 3930d9084279dc082bbbd958280d0abf2fe8398b Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Tue, 24 Sep 2024 13:28:51 +0200 Subject: [PATCH 486/812] Add more tests --- tests/Fixtures/expressions/matches.test | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/Fixtures/expressions/matches.test b/tests/Fixtures/expressions/matches.test index 00a5702b4a8..843e5e89c0e 100644 --- a/tests/Fixtures/expressions/matches.test +++ b/tests/Fixtures/expressions/matches.test @@ -7,8 +7,9 @@ Twig supports the "matches" operator {{ 'foo' matches '/^' ~ 'fo/' ? 'OK' : 'KO' }} {{ 'foo' matches '/O/i' ? 'OK' : 'KO' }} {{ null matches '/o/' }} +{{ markup matches '/test/' ? 'OK': 'KO' }} --DATA-- -return [] +return ['markup' => new \Twig\Markup('test', 'UTF-8')] --EXPECT-- OK OK @@ -16,3 +17,4 @@ OK OK OK 0 +OK From 10c3142d3b036910f63080070c101bfff61e0743 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Wed, 18 Sep 2024 09:41:27 +0200 Subject: [PATCH 487/812] Improve how trim behaves --- src/Extension/CoreExtension.php | 33 ++++++++++++++++---------------- src/Markup.php | 5 +++++ tests/Fixtures/filters/trim.test | 18 ++++++++++++++--- 3 files changed, 37 insertions(+), 19 deletions(-) diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index e0e6ed5d32f..f1488b5952b 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -93,6 +93,8 @@ final class CoreExtension extends AbstractExtension { + private const DEFAULT_TRIM_CHARS = " \t\n\r\0\x0B"; + private $dateFormats = ['F j, Y H:i', '%d days']; private $numberFormat = [0, '.', ',']; private $timezone = null; @@ -1116,30 +1118,29 @@ public static function matches(string $regexp, ?string $str): int /** * Returns a trimmed string. * - * @param string|null $string - * @param string|null $characterMask - * @param string $side + * @param string|\Stringable|null $string + * @param string|null $characterMask + * @param string $side left, right, or both * - * @throws RuntimeError When an invalid trimming side is used (not a string or not 'left', 'right', or 'both') + * @throws RuntimeError When an invalid trimming side is used * * @internal */ - public static function trim($string, $characterMask = null, $side = 'both'): string + public static function trim($string, $characterMask = null, $side = 'both'): string|\Stringable { if (null === $characterMask) { - $characterMask = " \t\n\r\0\x0B"; + $characterMask = self::DEFAULT_TRIM_CHARS; } - switch ($side) { - case 'both': - return trim($string ?? '', $characterMask); - case 'left': - return ltrim($string ?? '', $characterMask); - case 'right': - return rtrim($string ?? '', $characterMask); - default: - throw new RuntimeError('Trimming side must be "left", "right" or "both".'); - } + $trimmed = match ($side) { + 'both' => trim($string ?? '', $characterMask), + 'left' => ltrim($string ?? '', $characterMask), + 'right' => rtrim($string ?? '', $characterMask), + default => throw new RuntimeError('Trimming side must be "left", "right" or "both".'), + }; + + // trimming a safe string with the default character mask always returns a safe string (independently of the context) + return $string instanceof Markup && self::DEFAULT_TRIM_CHARS === $characterMask ? new Markup($trimmed, $string->getCharset()) : $trimmed; } /** diff --git a/src/Markup.php b/src/Markup.php index 4fae779ee33..c7aa65bdad5 100644 --- a/src/Markup.php +++ b/src/Markup.php @@ -32,6 +32,11 @@ public function __toString() return $this->content; } + public function getCharset(): string + { + return $this->charset; + } + /** * @return int */ diff --git a/tests/Fixtures/filters/trim.test b/tests/Fixtures/filters/trim.test index 141f863572b..c8d10c52828 100644 --- a/tests/Fixtures/filters/trim.test +++ b/tests/Fixtures/filters/trim.test @@ -4,11 +4,11 @@ {{ " I like Twig. "|trim }} {{ text|trim }} {{ " foo/"|trim("/") }} -{{ "xxxI like Twig.xxx"|trim(character_mask="x", side="left") }} -{{ "xxxI like Twig.xxx"|trim(side="right", character_mask="x") }} +{{ "xxxI like Twig.xxx"|trim(character_mask: "x", side: "left") }} +{{ "xxxI like Twig.xxx"|trim(side: "right", character_mask: "x") }} {{ "xxxI like Twig.xxx"|trim("x", "right") }} {{ "/ foo/"|trim("/", "left") }} -{{ "/ foo/"|trim(character_mask="/", side="left") }} +{{ "/ foo/"|trim(character_mask: "/", side: "left") }} {{ " do nothing. "|trim("", "right") }} *{{ ""|trim }}* *{{ ""|trim("", "left") }}* @@ -16,6 +16,14 @@ *{{ null|trim }}* *{{ null|trim("", "left") }}* *{{ null|trim("", "right") }}* + +{% set myhtml %} + Here is
    my HTML +{% endset %} +{% set myunsafestring = " I <3 u " %} +{{ myhtml | trim }} +{{ myunsafestring | trim }} +{{ myhtml | trim(character_mask: "f") }} --DATA-- return ['text' => " If you have some HTML it will be escaped. "] --EXPECT-- @@ -34,3 +42,7 @@ xxxI like Twig. ** ** ** + +Here is
    my HTML +I <3 u + Here is<br>my HTML From 9855e35d44d666db884f18ce1a1ae732a2c14fab Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Tue, 24 Sep 2024 13:36:37 +0200 Subject: [PATCH 488/812] Optimize compiled code for "set" tag --- src/Node/SetNode.php | 30 ++++++++++++++++++++++++------ tests/Node/SetTest.php | 20 +++++++++++++++++++- 2 files changed, 43 insertions(+), 7 deletions(-) diff --git a/src/Node/SetNode.php b/src/Node/SetNode.php index 67725104ece..288911a5c22 100644 --- a/src/Node/SetNode.php +++ b/src/Node/SetNode.php @@ -33,9 +33,15 @@ public function __construct(bool $capture, Node $names, Node $values, int $linen $safe = false; if ($capture) { $safe = true; - if ($values instanceof TextNode) { + if (Node::class === get_class($values) && !count($values)) { + $values = new ConstantExpression('', $values->getTemplateLine()); + $capture = false; + } elseif ($values instanceof TextNode) { $values = new ConstantExpression($values->getAttribute('data'), $values->getTemplateLine()); $capture = false; + } elseif ($values instanceof PrintNode && $values->getNode('expr') instanceof ConstantExpression) { + $values = $values->getNode('expr'); + $capture = false; } else { $values = new CaptureNode($values, $values->getTemplateLine()); } @@ -78,11 +84,23 @@ public function compile(Compiler $compiler): void $compiler->raw(']'); } else { if ($this->getAttribute('safe')) { - $compiler - ->raw("('' === \$tmp = ") - ->subcompile($this->getNode('values')) - ->raw(") ? '' : new Markup(\$tmp, \$this->env->getCharset())") - ; + if ($this->getNode('values') instanceof ConstantExpression) { + if ('' === $this->getNode('values')->getAttribute('value')) { + $compiler->raw('""'); + } else { + $compiler + ->raw('new Markup(') + ->subcompile($this->getNode('values')) + ->raw(', $this->env->getCharset())') + ; + } + } else { + $compiler + ->raw("('' === \$tmp = ") + ->subcompile($this->getNode('values')) + ->raw(") ? '' : new Markup(\$tmp, \$this->env->getCharset())") + ; + } } else { $compiler->subcompile($this->getNode('values')); } diff --git a/tests/Node/SetTest.php b/tests/Node/SetTest.php index dd2dca2bc0d..06f0407dd2b 100644 --- a/tests/Node/SetTest.php +++ b/tests/Node/SetTest.php @@ -77,7 +77,25 @@ public static function provideTests(): iterable $node = new SetNode(true, $names, $values, 1); $tests[] = [$node, <<env->getCharset()); +\$context["foo"] = new Markup("foo", \$this->env->getCharset()); +EOF + ]; + + $names = new Node([new AssignNameExpression('foo', 1)], [], 1); + $values = new TextNode('', 1); + $node = new SetNode(true, $names, $values, 1); + $tests[] = [$node, <<env->getCharset()); EOF ]; From 4824f2a4a3442349754402402e4dd5856134c03a Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Tue, 24 Sep 2024 20:53:17 +0200 Subject: [PATCH 489/812] Fix template name in error for an unknown dynamic extends called from an include --- src/Template.php | 23 +++++++------------ .../dynamic_parent_from_include.test | 14 +++++++++++ 2 files changed, 22 insertions(+), 15 deletions(-) create mode 100644 tests/Fixtures/tags/inheritance/dynamic_parent_from_include.test diff --git a/src/Template.php b/src/Template.php index 7b3ce81611c..6add7beac50 100644 --- a/src/Template.php +++ b/src/Template.php @@ -80,23 +80,16 @@ public function getParent(array $context): self|TemplateWrapper|false return $this->parent; } - try { - if (!$parent = $this->doGetParent($context)) { - return false; - } - - if ($parent instanceof self || $parent instanceof TemplateWrapper) { - return $this->parents[$parent->getSourceContext()->getName()] = $parent; - } + if (!$parent = $this->doGetParent($context)) { + return false; + } - if (!isset($this->parents[$parent])) { - $this->parents[$parent] = $this->loadTemplate($parent); - } - } catch (LoaderError $e) { - $e->setSourceContext(null); - $e->guess(); + if ($parent instanceof self || $parent instanceof TemplateWrapper) { + return $this->parents[$parent->getSourceContext()->getName()] = $parent; + } - throw $e; + if (!isset($this->parents[$parent])) { + $this->parents[$parent] = $this->loadTemplate($parent); } return $this->parents[$parent]; diff --git a/tests/Fixtures/tags/inheritance/dynamic_parent_from_include.test b/tests/Fixtures/tags/inheritance/dynamic_parent_from_include.test new file mode 100644 index 00000000000..42a3d60e278 --- /dev/null +++ b/tests/Fixtures/tags/inheritance/dynamic_parent_from_include.test @@ -0,0 +1,14 @@ +--TEST-- +"extends" tag +--TEMPLATE-- +{{ include('included.twig') }} + +--TEMPLATE(included.twig)-- + + + +{% extends dynamic %} +--DATA-- +return ['dynamic' => 'unknown.twig'] +--EXCEPTION-- +Twig\Error\LoaderError: Template "unknown.twig" is not defined in "included.twig" at line 5. From 85c2ba5147f729a87e5637da7c80e308d6f9dc4b Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Tue, 24 Sep 2024 22:13:47 +0200 Subject: [PATCH 490/812] Add some tests --- .../extends_as_array_with_nested_blocks.test | 31 +++++++++++++++++++ .../extends_with_nested_blocks.test | 31 +++++++++++++++++++ 2 files changed, 62 insertions(+) create mode 100644 tests/Fixtures/tags/inheritance/extends_as_array_with_nested_blocks.test create mode 100644 tests/Fixtures/tags/inheritance/extends_with_nested_blocks.test diff --git a/tests/Fixtures/tags/inheritance/extends_as_array_with_nested_blocks.test b/tests/Fixtures/tags/inheritance/extends_as_array_with_nested_blocks.test new file mode 100644 index 00000000000..01d23fce2c2 --- /dev/null +++ b/tests/Fixtures/tags/inheritance/extends_as_array_with_nested_blocks.test @@ -0,0 +1,31 @@ +--TEST-- +"extends" tag +--TEMPLATE-- +{% extends ["parent.twig"] %} + +{% block outer %} + outer wrap start + {{~ parent() }} + outer wrap end +{% endblock %} + +{% block inner -%} + inner actual +{% endblock %} +--TEMPLATE(parent.twig)-- +{% block outer %} + outer start + {% block inner %} + inner default + {% endblock %} + outer end +{% endblock %} +--DATA-- +return [] +--EXPECT-- + outer wrap start + outer start + inner actual + outer end + + outer wrap end diff --git a/tests/Fixtures/tags/inheritance/extends_with_nested_blocks.test b/tests/Fixtures/tags/inheritance/extends_with_nested_blocks.test new file mode 100644 index 00000000000..496f278cf38 --- /dev/null +++ b/tests/Fixtures/tags/inheritance/extends_with_nested_blocks.test @@ -0,0 +1,31 @@ +--TEST-- +"extends" tag +--TEMPLATE-- +{% extends "parent.twig" %} + +{% block outer %} + outer wrap start + {{~ parent() }} + outer wrap end +{% endblock %} + +{% block inner -%} + inner actual +{% endblock %} +--TEMPLATE(parent.twig)-- +{% block outer %} + outer start + {% block inner %} + inner default + {% endblock %} + outer end +{% endblock %} +--DATA-- +return [] +--EXPECT-- + outer wrap start + outer start + inner actual + outer end + + outer wrap end From 786a67b1a956cc2afa5c1eb8ba2c8108a319e0e1 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Wed, 25 Sep 2024 08:07:40 +0200 Subject: [PATCH 491/812] Add RemovableCacheInterface --- CHANGELOG | 1 + src/Cache/ChainCache.php | 11 ++++++++++- src/Cache/FilesystemCache.php | 10 +++++++++- src/Cache/NullCache.php | 6 +++++- src/Cache/RemovableCacheInterface.php | 20 ++++++++++++++++++++ src/Environment.php | 10 ++++++++++ 6 files changed, 55 insertions(+), 3 deletions(-) create mode 100644 src/Cache/RemovableCacheInterface.php diff --git a/CHANGELOG b/CHANGELOG index ea986469c38..bb7d956ac55 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,6 @@ # 3.15.0 (2024-XX-XX) + * Add template cache hot reload * Allow Twig callable argument names to be free-form (snake-case or camelCase) independently of the PHP callable signature They were automatically converted to snake-cased before * Deprecate the `attribute` function; use the `.` notation and wrap the name with parenthesis instead diff --git a/src/Cache/ChainCache.php b/src/Cache/ChainCache.php index c94afdb4346..1c2098f1f98 100644 --- a/src/Cache/ChainCache.php +++ b/src/Cache/ChainCache.php @@ -19,7 +19,7 @@ * * @author Quentin Devos */ -final class ChainCache implements CacheInterface +final class ChainCache implements CacheInterface, RemovableCacheInterface { /** * @param iterable $caches The ordered list of caches used to store and fetch cached items @@ -69,6 +69,15 @@ public function getTimestamp(string $key): int return 0; } + public function remove(string $name, string $cls): void + { + foreach ($this->caches as $cache) { + if ($cache instanceof RemovableCacheInterface) { + $cache->remove($name, $cls); + } + } + } + /** * @return string[] */ diff --git a/src/Cache/FilesystemCache.php b/src/Cache/FilesystemCache.php index 2e79fac0508..5840585e3e9 100644 --- a/src/Cache/FilesystemCache.php +++ b/src/Cache/FilesystemCache.php @@ -16,7 +16,7 @@ * * @author Andrew Tch */ -class FilesystemCache implements CacheInterface +class FilesystemCache implements CacheInterface, RemovableCacheInterface { public const FORCE_BYTECODE_INVALIDATION = 1; @@ -76,6 +76,14 @@ public function write(string $key, string $content): void throw new \RuntimeException(\sprintf('Failed to write cache file "%s".', $key)); } + public function remove(string $name, string $cls): void + { + $key = $this->generateKey($name, $cls); + if (!@unlink($key) && file_exists($key)) { + throw new \RuntimeException(\sprintf('Failed to delete cache file "%s".', $key)); + } + } + public function getTimestamp(string $key): int { if (!is_file($key)) { diff --git a/src/Cache/NullCache.php b/src/Cache/NullCache.php index 8d20d59d8b3..1ae21692800 100644 --- a/src/Cache/NullCache.php +++ b/src/Cache/NullCache.php @@ -16,7 +16,7 @@ * * @author Fabien Potencier */ -final class NullCache implements CacheInterface +final class NullCache implements CacheInterface, RemovableCacheInterface { public function generateKey(string $name, string $className): string { @@ -35,4 +35,8 @@ public function getTimestamp(string $key): int { return 0; } + + public function remove(string $name, string $cls): void + { + } } diff --git a/src/Cache/RemovableCacheInterface.php b/src/Cache/RemovableCacheInterface.php new file mode 100644 index 00000000000..05da569136b --- /dev/null +++ b/src/Cache/RemovableCacheInterface.php @@ -0,0 +1,20 @@ + + */ +interface RemovableCacheInterface +{ + public function remove(string $name, string $cls): void; +} diff --git a/src/Environment.php b/src/Environment.php index fe95adc574d..339d8f7ffd3 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -14,6 +14,7 @@ use Twig\Cache\CacheInterface; use Twig\Cache\FilesystemCache; use Twig\Cache\NullCache; +use Twig\Cache\RemovableCacheInterface; use Twig\Error\Error; use Twig\Error\LoaderError; use Twig\Error\RuntimeError; @@ -233,6 +234,15 @@ public function isStrictVariables() return $this->strictVariables; } + public function removeCache(string $name): void + { + if ($this->cache instanceof RemovableCacheInterface) { + $this->cache->remove($name, $this->getTemplateClass($name)); + } else { + throw new \LogicException(\sprintf('The "%s" cache class does not support removing template cache as it does not implement the "RemovableCacheInterface" interface.', \get_class($this->cache))); + } + } + /** * Gets the current cache implementation. * From dca5099aad0c386346e076b88c242e8917a72911 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Wed, 25 Sep 2024 11:13:10 +0200 Subject: [PATCH 492/812] Add hot cache reload for templates --- src/Environment.php | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/Environment.php b/src/Environment.php index 339d8f7ffd3..c8862af3d9c 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -72,6 +72,7 @@ class Environment /** @var bool */ private $useYield; private $defaultRuntimeLoader; + private array $hotCache = []; /** * Constructor. @@ -236,8 +237,11 @@ public function isStrictVariables() public function removeCache(string $name): void { + $cls = $this->getTemplateClass($name); + $this->hotCache[$name] = $cls.'_'.bin2hex(random_bytes(16)); + if ($this->cache instanceof RemovableCacheInterface) { - $this->cache->remove($name, $this->getTemplateClass($name)); + $this->cache->remove($name, $cls); } else { throw new \LogicException(\sprintf('The "%s" cache class does not support removing template cache as it does not implement the "RemovableCacheInterface" interface.', \get_class($this->cache))); } @@ -297,7 +301,7 @@ public function setCache($cache) */ public function getTemplateClass(string $name, ?int $index = null): string { - $key = $this->getLoader()->getCacheKey($name).$this->optionsHash; + $key = ($this->hotCache[$name] ?? $this->getLoader()->getCacheKey($name)).$this->optionsHash; return '__TwigTemplate_'.hash(\PHP_VERSION_ID < 80100 ? 'sha256' : 'xxh128', $key).(null === $index ? '' : '___'.$index); } @@ -389,8 +393,10 @@ public function loadTemplate(string $cls, string $name, ?int $index = null): Tem if (!class_exists($cls, false)) { $source = $this->getLoader()->getSourceContext($name); $content = $this->compileSource($source); - $this->cache->write($key, $content); - $this->cache->load($key); + if (!isset($this->hotCache[$name])) { + $this->cache->write($key, $content); + $this->cache->load($key); + } if (!class_exists($mainCls, false)) { /* Last line of defense if either $this->bcWriteCacheFile was used, From 00ba8046b0e8b004556dd065845e14777e326aee Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Wed, 25 Sep 2024 16:15:25 +0200 Subject: [PATCH 493/812] Add test --- tests/EnvironmentTest.php | 48 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/tests/EnvironmentTest.php b/tests/EnvironmentTest.php index b5100a0aadc..7bbc61cef58 100644 --- a/tests/EnvironmentTest.php +++ b/tests/EnvironmentTest.php @@ -22,6 +22,7 @@ use Twig\Extension\ExtensionInterface; use Twig\Extension\GlobalsInterface; use Twig\Loader\ArrayLoader; +use Twig\Loader\FilesystemLoader; use Twig\Loader\LoaderInterface; use Twig\Node\Node; use Twig\NodeVisitor\NodeVisitorInterface; @@ -497,6 +498,53 @@ public function getGlobals(): array $g3 = $twig->getGlobals(); $this->assertNotSame($g3['global_ext'], $g2['global_ext']); } + + public function testHotCache() + { + $dir = sys_get_temp_dir().'/twig-hot-cache-test'; + if (is_dir($dir)) { + FilesystemHelper::removeDir($dir); + } + mkdir($dir); + file_put_contents($dir.'/index.twig', 'x'); + try { + $twig = new Environment(new FilesystemLoader($dir), [ + 'debug' => false, + 'auto_reload' => false, + 'cache' => $dir.'/cache', + ]); + + // prime the cache + $this->assertSame('x', $twig->load('index.twig')->render([])); + + // update the template + file_put_contents($dir.'/index.twig', 'y'); + + // re-render, should use the cached version + $this->assertSame('x', $twig->load('index.twig')->render([])); + + // clear the cache + $twig->removeCache('index.twig'); + + // re-render, should use the updated template + $this->assertSame('y', $twig->load('index.twig')->render([])); + + // the new template should not be cached + $iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($dir.'/cache', \FilesystemIterator::SKIP_DOTS), \RecursiveIteratorIterator::CHILD_FIRST); + $count = 0; + foreach ($iterator as $fileInfo) { + if (!$fileInfo->isDir()) { + ++$count; + } + } + $this->assertSame(0, $count); + + // re-render, should use the updated template + $this->assertSame('y', $twig->load('index.twig')->render([])); + } finally { + FilesystemHelper::removeDir($dir); + } + } } class EnvironmentTest_Extension_WithGlobals extends AbstractExtension From 955611d5bbd989bb51376914ea5129c4faa49712 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Wed, 25 Sep 2024 22:07:53 +0200 Subject: [PATCH 494/812] Fix the possibility to override an aliased block (via use) --- CHANGELOG | 1 + src/Node/ModuleNode.php | 6 +++++- src/Template.php | 3 ++- .../use/use_aliased_block_overridden.test | 21 +++++++++++++++++++ 4 files changed, 29 insertions(+), 2 deletions(-) create mode 100644 tests/Fixtures/tags/use/use_aliased_block_overridden.test diff --git a/CHANGELOG b/CHANGELOG index bb7d956ac55..708389656a5 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,6 @@ # 3.15.0 (2024-XX-XX) + * Fix the possibility to override an aliased block (via use) * Add template cache hot reload * Allow Twig callable argument names to be free-form (snake-case or camelCase) independently of the PHP callable signature They were automatically converted to snake-cased before diff --git a/src/Node/ModuleNode.php b/src/Node/ModuleNode.php index d2fb216b1e2..f67c3b0e76c 100644 --- a/src/Node/ModuleNode.php +++ b/src/Node/ModuleNode.php @@ -244,7 +244,11 @@ protected function compileConstructor(Compiler $compiler) ->string($key) ->raw(\sprintf(']; unset($_trait_%s_blocks[', $i)) ->string($key) - ->raw("]);\n\n") + ->raw("]); \$this->traitAliases[") + ->subcompile($value) + ->raw("] = ") + ->string($key) + ->raw(";\n\n") ; } } diff --git a/src/Template.php b/src/Template.php index 7b3ce81611c..cb042b59566 100644 --- a/src/Template.php +++ b/src/Template.php @@ -37,6 +37,7 @@ abstract class Template protected $parents = []; protected $blocks = []; protected $traits = []; + protected $traitAliases = []; protected $extensions = []; protected $sandbox; @@ -477,7 +478,7 @@ public function yieldBlock($name, array $context, array $blocks = [], $useBlocks public function yieldParentBlock($name, array $context, array $blocks = []): iterable { if (isset($this->traits[$name])) { - yield from $this->traits[$name][0]->yieldBlock($name, $context, $blocks, false); + yield from $this->traits[$name][0]->yieldBlock($this->traitAliases[$name] ?? $name, $context, $blocks, false); } elseif ($parent = $this->getParent($context)) { yield from $parent->unwrap()->yieldBlock($name, $context, $blocks, false); } else { diff --git a/tests/Fixtures/tags/use/use_aliased_block_overridden.test b/tests/Fixtures/tags/use/use_aliased_block_overridden.test new file mode 100644 index 00000000000..8396c6f5864 --- /dev/null +++ b/tests/Fixtures/tags/use/use_aliased_block_overridden.test @@ -0,0 +1,21 @@ +--TEST-- +"use" tag with an overridden block that is aliased +--TEMPLATE-- +{% use "blocks.twig" with bar as baz %} + +{% block foo %}{{ parent() }}+{% endblock %} + +{% block baz %}{{ parent() }}+{% endblock %} + +{{ block('foo') }} +{{ block('baz') }} +--TEMPLATE(blocks.twig)-- +{% block foo %}Foo{% endblock %} +{% block bar %}Bar{% endblock %} +--DATA-- +return [] +--EXPECT-- +Foo+ +Bar+ +Foo+ +Bar+ From 09790a7542a8b4584bf1604892dd1f24711ecec8 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Thu, 26 Sep 2024 07:32:30 +0200 Subject: [PATCH 495/812] Fix 'ignore missing' when used on an 'embed' tag --- CHANGELOG | 1 + src/Node/EmbedNode.php | 10 ++++++++-- src/Node/IncludeNode.php | 7 ++++--- tests/Fixtures/tags/embed/embed_ignore_missing.test | 10 ++++++++++ tests/Node/IncludeTest.php | 3 ++- 5 files changed, 25 insertions(+), 6 deletions(-) create mode 100644 tests/Fixtures/tags/embed/embed_ignore_missing.test diff --git a/CHANGELOG b/CHANGELOG index 708389656a5..4d0616ceae9 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,6 @@ # 3.15.0 (2024-XX-XX) + * Fix "ignore missing" when used on an "embed" tag * Fix the possibility to override an aliased block (via use) * Add template cache hot reload * Allow Twig callable argument names to be free-form (snake-case or camelCase) independently of the PHP callable signature diff --git a/src/Node/EmbedNode.php b/src/Node/EmbedNode.php index 4cd3b38f2de..597f95e4413 100644 --- a/src/Node/EmbedNode.php +++ b/src/Node/EmbedNode.php @@ -33,10 +33,10 @@ public function __construct(string $name, int $index, ?AbstractExpression $varia $this->setAttribute('index', $index); } - protected function addGetTemplate(Compiler $compiler): void + protected function addGetTemplate(Compiler $compiler, string $template = ''): void { $compiler - ->write('$this->loadTemplate(') + ->raw('$this->loadTemplate(') ->string($this->getAttribute('name')) ->raw(', ') ->repr($this->getTemplateName()) @@ -46,5 +46,11 @@ protected function addGetTemplate(Compiler $compiler): void ->string($this->getAttribute('index')) ->raw(')') ; + if ($this->getAttribute('ignore_missing')) { + $compiler + ->raw(";\n") + ->write(\sprintf("\$%s->getParent(\$context);\n", $template)) + ; + } } } diff --git a/src/Node/IncludeNode.php b/src/Node/IncludeNode.php index 1c18292c58e..33267170d27 100644 --- a/src/Node/IncludeNode.php +++ b/src/Node/IncludeNode.php @@ -48,7 +48,7 @@ public function compile(Compiler $compiler): void ->write(\sprintf('$%s = ', $template)) ; - $this->addGetTemplate($compiler); + $this->addGetTemplate($compiler, $template); $compiler ->raw(";\n") @@ -56,6 +56,7 @@ public function compile(Compiler $compiler): void ->write("} catch (LoaderError \$e) {\n") ->indent() ->write("// ignore missing template\n") + ->write(\sprintf("\$$template = null;\n", $template)) ->outdent() ->write("}\n") ->write(\sprintf("if ($%s) {\n", $template)) @@ -78,10 +79,10 @@ public function compile(Compiler $compiler): void } } - protected function addGetTemplate(Compiler $compiler) + protected function addGetTemplate(Compiler $compiler/* , string $template = '' */) { $compiler - ->write('$this->loadTemplate(') + ->raw('$this->loadTemplate(') ->subcompile($this->getNode('expr')) ->raw(', ') ->repr($this->getTemplateName()) diff --git a/tests/Fixtures/tags/embed/embed_ignore_missing.test b/tests/Fixtures/tags/embed/embed_ignore_missing.test new file mode 100644 index 00000000000..f6f27a10abe --- /dev/null +++ b/tests/Fixtures/tags/embed/embed_ignore_missing.test @@ -0,0 +1,10 @@ +--TEST-- +"embed" tag +--TEMPLATE-- +{% set x = 'bad' %} +{% embed x ~ 'ger.twig' ignore missing %}{% endembed %} +HERE +--DATA-- +return [] +--EXPECT-- +HERE diff --git a/tests/Node/IncludeTest.php b/tests/Node/IncludeTest.php index b485100588c..e0c23cd6137 100644 --- a/tests/Node/IncludeTest.php +++ b/tests/Node/IncludeTest.php @@ -80,9 +80,10 @@ public static function provideTests(): iterable // line 1 \$__internal_%s = null; try { - \$__internal_%s = \$this->loadTemplate("foo.twig", null, 1); + \$__internal_%s = \$this->loadTemplate("foo.twig", null, 1); } catch (LoaderError \$e) { // ignore missing template + \$__internal_%s = null; } if (\$__internal_%s) { yield from \$__internal_%s->unwrap()->yield(CoreExtension::toArray(["foo" => true])); From f83c844b7d8bd44c3f53858d418d3b8c48d17a5d Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Thu, 26 Sep 2024 08:15:08 +0200 Subject: [PATCH 496/812] Add start/end time accessors on Profile --- CHANGELOG | 1 + src/Profiler/Profile.php | 16 ++++++++++++++++ tests/Profiler/ProfileTest.php | 9 +++++++++ 3 files changed, 26 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index 4d0616ceae9..2fc4b934756 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,6 @@ # 3.15.0 (2024-XX-XX) + * Add `Profile::getStartTime()` and `Profile::getEndTime()` * Fix "ignore missing" when used on an "embed" tag * Fix the possibility to override an aliased block (via use) * Add template cache hot reload diff --git a/src/Profiler/Profile.php b/src/Profiler/Profile.php index 2928e164640..a3c6ee02e5a 100644 --- a/src/Profiler/Profile.php +++ b/src/Profiler/Profile.php @@ -99,6 +99,22 @@ public function getDuration(): float return isset($this->ends['wt']) && isset($this->starts['wt']) ? $this->ends['wt'] - $this->starts['wt'] : 0; } + /** + * Returns the start time in microseconds. + */ + public function getStartTime(): float + { + return $this->starts['wt'] ?? 0.0; + } + + /** + * Returns the end time in microseconds. + */ + public function getEndTime(): float + { + return $this->ends['wt'] ?? 0.0; + } + /** * Returns the memory usage in bytes. */ diff --git a/tests/Profiler/ProfileTest.php b/tests/Profiler/ProfileTest.php index 5f41fc9c1df..8c00e65bf19 100644 --- a/tests/Profiler/ProfileTest.php +++ b/tests/Profiler/ProfileTest.php @@ -80,6 +80,15 @@ public function testGetDuration() $this->assertTrue($profile->getDuration() > 0, \sprintf('Expected duration > 0, got: %f', $profile->getDuration())); } + public function testTimeAccessors() + { + $current = microtime(true); + $profile = new Profile(); + + $this->assertEqualsWithDelta($current, $profile->getStartTime(), 1); + $this->assertSame(0.0, $profile->getEndTime()); + } + public function testSerialize() { $profile = new Profile('template', 'type', 'name'); From fe3d615aa46d28a35f5327e3646cf0c6f50a7d25 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Thu, 26 Sep 2024 08:58:21 +0200 Subject: [PATCH 497/812] Be more precise about double-escaping --- doc/tags/autoescape.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/doc/tags/autoescape.rst b/doc/tags/autoescape.rst index 8c621d3a849..2708009c624 100644 --- a/doc/tags/autoescape.rst +++ b/doc/tags/autoescape.rst @@ -41,7 +41,8 @@ Functions returning template data (like :doc:`macros` and .. note:: Twig is smart enough to not escape an already escaped value by the - :doc:`escape<../filters/escape>` filter. + :doc:`escape<../filters/escape>` filter when the automatic escaping + strategy is the same as the one applied by the escape filter. .. note:: From 633d70103d96e56908fa3258580a6ebc6e9e9b7f Mon Sep 17 00:00:00 2001 From: Ruud Kamphuis Date: Thu, 26 Sep 2024 13:47:17 +0200 Subject: [PATCH 498/812] Add fixture filename as key This way, it's easier to see which test failed and what the file was. In PHPStorm this is visualized very nice. --- src/Test/IntegrationTestCase.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Test/IntegrationTestCase.php b/src/Test/IntegrationTestCase.php index b66c17fd2b9..8c9d41f2a44 100644 --- a/src/Test/IntegrationTestCase.php +++ b/src/Test/IntegrationTestCase.php @@ -146,7 +146,7 @@ public function getTests($name, $legacyTests = false) throw new \InvalidArgumentException(\sprintf('Test "%s" is not valid.', str_replace($fixturesDir.'/', '', $file))); } - $tests[] = [str_replace($fixturesDir.'/', '', $file), $message, $condition, $templates, $exception, $outputs, $deprecation]; + $tests[str_replace($fixturesDir.'/', '', $file)] = [str_replace($fixturesDir.'/', '', $file), $message, $condition, $templates, $exception, $outputs, $deprecation]; } if ($legacyTests && !$tests) { From 5c73c6695cb57729589c53af0e6aab8412d82d05 Mon Sep 17 00:00:00 2001 From: Ruud Kamphuis Date: Thu, 26 Sep 2024 14:33:44 +0200 Subject: [PATCH 499/812] Remove useless assign in compiled code --- src/Node/IncludeNode.php | 1 - tests/Node/IncludeTest.php | 1 - 2 files changed, 2 deletions(-) diff --git a/src/Node/IncludeNode.php b/src/Node/IncludeNode.php index 33267170d27..5e0c6deb05b 100644 --- a/src/Node/IncludeNode.php +++ b/src/Node/IncludeNode.php @@ -42,7 +42,6 @@ public function compile(Compiler $compiler): void $template = $compiler->getVarName(); $compiler - ->write(\sprintf("$%s = null;\n", $template)) ->write("try {\n") ->indent() ->write(\sprintf('$%s = ', $template)) diff --git a/tests/Node/IncludeTest.php b/tests/Node/IncludeTest.php index e0c23cd6137..45bebaf21b8 100644 --- a/tests/Node/IncludeTest.php +++ b/tests/Node/IncludeTest.php @@ -78,7 +78,6 @@ public static function provideTests(): iterable $node = new IncludeNode($expr, $vars, true, true, 1); $tests[] = [$node, <<loadTemplate("foo.twig", null, 1); } catch (LoaderError \$e) { From 7256713c86645974667e5c2071828911e77e6ea7 Mon Sep 17 00:00:00 2001 From: Ruud Kamphuis Date: Thu, 26 Sep 2024 13:06:37 +0200 Subject: [PATCH 500/812] Improve escape deprecation message --- src/Lexer.php | 4 +-- tests/LexerTest.php | 88 +++++++++++++++++++++++++++++++-------------- 2 files changed, 63 insertions(+), 29 deletions(-) diff --git a/src/Lexer.php b/src/Lexer.php index 32d4e97008e..982c87e3f7f 100644 --- a/src/Lexer.php +++ b/src/Lexer.php @@ -418,7 +418,7 @@ private function stripcslashes(string $str, string $quoteType): string $result .= $nextChar; } elseif ("'" === $nextChar || '"' === $nextChar) { if ($nextChar !== $quoteType) { - trigger_deprecation('twig/twig', '3.12', 'Character "%s" at position %d should not be escaped; the "\" character is ignored in Twig v3 but will not be in v4. Please remove the extra "\" character.', $nextChar, $i + 1); + trigger_deprecation('twig/twig', '3.12', 'Character "%s" at position %d in string on line %d should not be escaped; the "\" character is ignored in Twig v3 but will not be in v4. Please remove the extra "\" character.', $nextChar, $i + 1, $this->lineno); } $result .= $nextChar; } elseif ('#' === $nextChar && $i + 1 < $length && '{' === $str[$i + 1]) { @@ -437,7 +437,7 @@ private function stripcslashes(string $str, string $quoteType): string } $result .= \chr(octdec($octal)); } else { - trigger_deprecation('twig/twig', '3.12', 'Character "%s" at position %d should not be escaped; the "\" character is ignored in Twig v3 but will not be in v4. Please remove the extra "\" character.', $nextChar, $i + 1); + trigger_deprecation('twig/twig', '3.12', 'Character "%s" at position %d in string on line %d should not be escaped; the "\" character is ignored in Twig v3 but will not be in v4. Please remove the extra "\" character.', $nextChar, $i + 1, $this->lineno); $result .= $nextChar; } diff --git a/tests/LexerTest.php b/tests/LexerTest.php index a9cb7cb6205..04e9930b912 100644 --- a/tests/LexerTest.php +++ b/tests/LexerTest.php @@ -195,38 +195,64 @@ public function testStringWithEscapedDelimiter(string $template, string $expecte public static function getStringWithEscapedDelimiter() { - yield '{{ \'\x6\' }} => \x6' => [ - '{{ \'\x6\' }}', + yield [ + <<<'EOF' + {{ '\x6' }} + EOF, "\x6", ]; - yield '{{ \'\065\x64\' }} => \065\x64' => [ - '{{ \'\065\x64\' }}', + yield [ + <<<'EOF' + {{ '\065\x64' }} + EOF, "\065\x64", ]; - yield '{{ \'App\\\\Test\' }} => App\Test' => [ - '{{ \'App\\\\Test\' }}', + yield [ + <<<'EOF' + {{ 'App\\Test' }} + EOF, 'App\\Test', ]; - yield '{{ "App\#{var}" }} => App#{var}' => [ - '{{ "App\#{var}" }}', + yield [ + <<<'EOF' + {{ "App\#{var}" }} + EOF, 'App#{var}', ]; - yield '{{ \'foo \\\' bar\' }} => foo \' bar' => [ - '{{ \'foo \\\' bar\' }}', - 'foo \' bar', + yield [ + <<<'EOF' + {{ 'foo \' bar' }} + EOF, + <<<'EOF' + foo ' bar + EOF, ]; - yield '{{ "foo \" bar" }} => foo " bar' => [ - '{{ "foo \\" bar" }}', + yield [ + <<<'EOF' + {{ "foo \" bar" }} + EOF, 'foo " bar', ]; - yield '{{ \'\f\n\r\t\v\' }} => \f\n\r\t\v' => [ - '{{ \'\\f\\n\\r\\t\\v\' }}', + yield [ + <<<'EOF' + {{ '\f\n\r\t\v' }} + EOF, "\f\n\r\t\v", ]; - yield '{{ \'\\\\f\\\\n\\\\r\\\\t\\\\v\' }} => \\f\\n\\r\\t\\v' => [ - '{{ \'\\\\f\\\\n\\\\r\\\\t\\\\v\' }}', + yield [ + <<<'EOF' + {{ '\\f\\n\\r\\t\\v' }} + EOF, '\\f\\n\\r\\t\\v', ]; + yield [ + <<<'EOF' + {{ 'Ymd\\THis' }} + EOF, + <<<'EOF' + Ymd\THis + EOF, + ]; } /** @@ -250,20 +276,28 @@ public function testStringWithEscapedDelimiterProducingDeprecation(string $templ public static function getStringWithEscapedDelimiterProducingDeprecation() { - yield '{{ \'App\Test\' }} => AppTest' => [ - '{{ \'App\\Test\' }}', + yield [ + <<<'EOF' + {{ 'App\Test' }} + EOF, 'AppTest', - 'Since twig/twig 3.12: Character "T" at position 5 should not be escaped; the "\" character is ignored in Twig v3 but will not be in v4. Please remove the extra "\" character.', + 'Since twig/twig 3.12: Character "T" at position 5 in string on line 1 should not be escaped; the "\" character is ignored in Twig v3 but will not be in v4. Please remove the extra "\" character.', ]; - yield '{{ "foo \\\' bar" }} => foo \' bar' => [ - '{{ "foo \\\' bar" }}', - 'foo \' bar', - 'Since twig/twig 3.12: Character "\'" at position 6 should not be escaped; the "\" character is ignored in Twig v3 but will not be in v4. Please remove the extra "\" character.', + yield [ + <<<'EOF' + {{ "foo \' bar" }} + EOF, + <<<'EOF' + foo ' bar + EOF, + 'Since twig/twig 3.12: Character "\'" at position 6 in string on line 1 should not be escaped; the "\" character is ignored in Twig v3 but will not be in v4. Please remove the extra "\" character.', ]; - yield '{{ \'foo \" bar\' }} => foo " bar' => [ - '{{ \'foo \\" bar\' }}', + yield [ + <<<'EOF' + {{ 'foo \" bar' }} + EOF, 'foo " bar', - 'Since twig/twig 3.12: Character """ at position 6 should not be escaped; the "\" character is ignored in Twig v3 but will not be in v4. Please remove the extra "\" character.', + 'Since twig/twig 3.12: Character """ at position 6 in string on line 1 should not be escaped; the "\" character is ignored in Twig v3 but will not be in v4. Please remove the extra "\" character.', ]; } From 04471ab8cd0adf3e69bffb78a95404801d9c109a Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Thu, 26 Sep 2024 17:43:04 +0200 Subject: [PATCH 501/812] Replace strtr() by strtolower() --- src/ExpressionParser.php | 2 +- src/Extension/CoreExtension.php | 4 ++-- src/Sandbox/SecurityPolicy.php | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/ExpressionParser.php b/src/ExpressionParser.php index d09cfbb48f8..d45860840a8 100644 --- a/src/ExpressionParser.php +++ b/src/ExpressionParser.php @@ -707,7 +707,7 @@ public function parseAssignmentExpression() $stream->expect(Token::NAME_TYPE, null, 'Only variables can be assigned to'); } $value = $token->getValue(); - if (\in_array(strtr($value, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), ['true', 'false', 'none', 'null'])) { + if (\in_array(strtolower($value), ['true', 'false', 'none', 'null'])) { throw new SyntaxError(\sprintf('You cannot assign a value to "%s".', $value), $token->getLine(), $stream->getSourceContext()); } $targets[] = new AssignNameExpression($value, $token->getLine()); diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index fe5ed184d35..06b19d8bb8f 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -1686,7 +1686,7 @@ public static function getAttribute(Environment $env, Source $source, $object, $ if (!isset($cache[$class])) { $methods = get_class_methods($object); sort($methods); - $lcMethods = array_map(function ($value) { return strtr($value, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'); }, $methods); + $lcMethods = array_map('strtolower', $methods); $classCache = []; foreach ($methods as $i => $method) { $classCache[$method] = $method; @@ -1725,7 +1725,7 @@ public static function getAttribute(Environment $env, Source $source, $object, $ $call = false; if (isset($cache[$class][$item])) { $method = $cache[$class][$item]; - } elseif (isset($cache[$class][$lcItem = strtr($item, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz')])) { + } elseif (isset($cache[$class][$lcItem = strtolower($item)])) { $method = $cache[$class][$lcItem]; } elseif (isset($cache[$class]['__call'])) { $method = $item; diff --git a/src/Sandbox/SecurityPolicy.php b/src/Sandbox/SecurityPolicy.php index 988e37216cf..b0d054260f1 100644 --- a/src/Sandbox/SecurityPolicy.php +++ b/src/Sandbox/SecurityPolicy.php @@ -50,7 +50,7 @@ public function setAllowedMethods(array $methods): void { $this->allowedMethods = []; foreach ($methods as $class => $m) { - $this->allowedMethods[$class] = array_map(function ($value) { return strtr($value, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'); }, \is_array($m) ? $m : [$m]); + $this->allowedMethods[$class] = array_map('strtolower', \is_array($m) ? $m : [$m]); } } @@ -98,7 +98,7 @@ public function checkMethodAllowed($obj, $method): void } $allowed = false; - $method = strtr($method, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'); + $method = strtolower($method); foreach ($this->allowedMethods as $class => $methods) { if ($obj instanceof $class && \in_array($method, $methods)) { $allowed = true; From 7d5269e100a856cb5cdcd5f2f96b6bdf9b8fae54 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Thu, 26 Sep 2024 17:54:50 +0200 Subject: [PATCH 502/812] Fix getting a property on an object casted to an array --- src/Extension/CoreExtension.php | 2 +- tests/Fixtures/expressions/attributes.test | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 tests/Fixtures/expressions/attributes.test diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index fe5ed184d35..dd72f3fb4ab 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -1673,7 +1673,7 @@ public static function getAttribute(Environment $env, Source $source, $object, $ $env->getExtension(SandboxExtension::class)->checkPropertyAllowed($object, $item, $lineno, $source); } - return $object->$item; + return isset($object->$item) ? $object->$item : ((array) $object)[(string) $item]; } } diff --git a/tests/Fixtures/expressions/attributes.test b/tests/Fixtures/expressions/attributes.test new file mode 100644 index 00000000000..9be8197a610 --- /dev/null +++ b/tests/Fixtures/expressions/attributes.test @@ -0,0 +1,13 @@ +--TEST-- +"." notation +--TEMPLATE-- +{{ property.foo }} +{{ date.timezone }} +--DATA-- +return [ + 'date' => new \DateTimeImmutable('now', new \DateTimeZone('Europe/Paris')), + 'property' => (object) array('foo' => 'bar'), +] +--EXPECT-- +bar +Europe/Paris From 1ea04523232309cb89b9f443e428989d1984bc3b Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Thu, 26 Sep 2024 17:54:24 +0200 Subject: [PATCH 503/812] Add support for accessing class constants with the dot operator --- CHANGELOG | 1 + doc/templates.rst | 9 +++++---- src/Extension/CoreExtension.php | 12 ++++++++++++ tests/Fixtures/expressions/const.test | 10 ++++++++++ 4 files changed, 28 insertions(+), 4 deletions(-) create mode 100644 tests/Fixtures/expressions/const.test diff --git a/CHANGELOG b/CHANGELOG index 2fc4b934756..6522bb120ff 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,6 @@ # 3.15.0 (2024-XX-XX) + * Add support for accessing class constants with the dot operator * Add `Profile::getStartTime()` and `Profile::getEndTime()` * Fix "ignore missing" when used on an "embed" tag * Fix the possibility to override an aliased block (via use) diff --git a/doc/templates.rst b/doc/templates.rst index 97e8b59263e..1da89bcf214 100644 --- a/doc/templates.rst +++ b/doc/templates.rst @@ -76,8 +76,8 @@ Twig templates have access to variables provided by the PHP application and variables created in templates via the :doc:`set ` tag. These variables can be manipulated and displayed in the template. -Use a dot (``.``) to access attributes of a variable (methods or properties of a -PHP object, or items of a PHP array): +Use a dot (``.``) to access attributes of a variable (methods, properties +or constants of a PHP object, or items of a PHP array): .. code-block:: twig @@ -767,8 +767,8 @@ The following operators don't fit into any of the other categories: * ``.``, ``[]``: Gets an attribute of a variable. - The (``.``) operator abstracts getting an attribute of a variable (methods - or properties of a PHP object, or items of a PHP array): + The (``.``) operator abstracts getting an attribute of a variable (methods, + properties or constants of a PHP object, or items of a PHP array): .. code-block:: twig @@ -803,6 +803,7 @@ The following operators don't fit into any of the other categories: * check if ``user`` is a PHP array or a ArrayObject/ArrayAccess object and ``name`` a valid element; * if not, and if ``user`` is a PHP object, check that ``name`` is a valid property; + * if not, and if ``user`` is a PHP object, check that ``name`` is a class constant; * if not, and if ``user`` is a PHP object, check the following methods and call the first valid one: ``name()``, ``getName()``, ``isName()``, or ``hasName()``; diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index fe5ed184d35..9472a2b54d5 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -1675,6 +1675,18 @@ public static function getAttribute(Environment $env, Source $source, $object, $ return $object->$item; } + + if (\defined($object::class.'::'.$item)) { + if ($isDefinedTest) { + return true; + } + + if ($sandboxed) { + $env->getExtension(SandboxExtension::class)->checkPropertyAllowed($object, $item, $lineno, $source); + } + + return \constant($object::class.'::'.$item); + } } static $cache = []; diff --git a/tests/Fixtures/expressions/const.test b/tests/Fixtures/expressions/const.test new file mode 100644 index 00000000000..336fc60012a --- /dev/null +++ b/tests/Fixtures/expressions/const.test @@ -0,0 +1,10 @@ +--TEST-- +Twig supports accessing constants +--TEMPLATE-- +{{ foo.BAR_NAME }} +--DATA-- +return ['foo' => new Twig\Tests\TwigTestFoo()] +--CONFIG-- +return ['strict_variables' => false] +--EXPECT-- +bar From a23cd9feeb0d03e737f532cc8989371181492859 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Thu, 26 Sep 2024 21:22:23 +0200 Subject: [PATCH 504/812] Fix CS --- .../Compiler/MissingExtensionSuggestorPass.php | 1 - src/ExtensionSet.php | 2 -- src/Node/Expression/Binary/MatchesBinary.php | 2 +- src/Template.php | 1 - 4 files changed, 1 insertion(+), 5 deletions(-) diff --git a/extra/twig-extra-bundle/DependencyInjection/Compiler/MissingExtensionSuggestorPass.php b/extra/twig-extra-bundle/DependencyInjection/Compiler/MissingExtensionSuggestorPass.php index 22e04c012d5..83c6643f09d 100644 --- a/extra/twig-extra-bundle/DependencyInjection/Compiler/MissingExtensionSuggestorPass.php +++ b/extra/twig-extra-bundle/DependencyInjection/Compiler/MissingExtensionSuggestorPass.php @@ -14,7 +14,6 @@ use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Reference; -use Twig\Environment; class MissingExtensionSuggestorPass implements CompilerPassInterface { diff --git a/src/ExtensionSet.php b/src/ExtensionSet.php index 28d57a41c61..3a5b24be499 100644 --- a/src/ExtensionSet.php +++ b/src/ExtensionSet.php @@ -16,8 +16,6 @@ use Twig\Extension\GlobalsInterface; use Twig\Extension\StagingExtension; use Twig\Node\Expression\AbstractExpression; -use Twig\Node\Expression\Binary\AbstractBinary; -use Twig\Node\Expression\Unary\AbstractUnary; use Twig\NodeVisitor\NodeVisitorInterface; use Twig\TokenParser\TokenParserInterface; diff --git a/src/Node/Expression/Binary/MatchesBinary.php b/src/Node/Expression/Binary/MatchesBinary.php index ba25313c003..0a523c21611 100644 --- a/src/Node/Expression/Binary/MatchesBinary.php +++ b/src/Node/Expression/Binary/MatchesBinary.php @@ -13,8 +13,8 @@ use Twig\Compiler; use Twig\Error\SyntaxError; -use Twig\Node\Node; use Twig\Node\Expression\ConstantExpression; +use Twig\Node\Node; class MatchesBinary extends AbstractBinary { diff --git a/src/Template.php b/src/Template.php index 2a50d9100fa..86cb560e977 100644 --- a/src/Template.php +++ b/src/Template.php @@ -13,7 +13,6 @@ namespace Twig; use Twig\Error\Error; -use Twig\Error\LoaderError; use Twig\Error\RuntimeError; /** From cc3a2b7bacfb170f369f262b0954328d08ef8dd9 Mon Sep 17 00:00:00 2001 From: Thomas Landauer Date: Fri, 27 Sep 2024 01:23:50 +0200 Subject: [PATCH 505/812] Update plural.rst: Fixing(?) broken links More important question: Is the filter's name `plural` or `pluralize`?? --- doc/filters/plural.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/filters/plural.rst b/doc/filters/plural.rst index 1c9db960f35..50c18cdc01a 100644 --- a/doc/filters/plural.rst +++ b/doc/filters/plural.rst @@ -49,5 +49,5 @@ Arguments Internally, Twig uses the `pluralize`_ method from the Symfony String component. -.. _`inflector`: -.. _`pluralize`: +.. _`inflector`: https://symfony.com/doc/current/components/string.html#inflector +.. _`pluralize`: https://symfony.com/doc/current/components/string.html#inflector From d9d8bb0df3a850e50624f82213a342e69a64b07a Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Thu, 26 Sep 2024 16:41:07 +0200 Subject: [PATCH 506/812] Add support for inline comments --- CHANGELOG | 1 + doc/templates.rst | 44 ++++++++++++++++++-- src/Lexer.php | 5 +++ tests/LexerTest.php | 98 +++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 145 insertions(+), 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 6522bb120ff..8fcd964030f 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,6 @@ # 3.15.0 (2024-XX-XX) + * Add support for inline comments * Add support for accessing class constants with the dot operator * Add `Profile::getStartTime()` and `Profile::getEndTime()` * Fix "ignore missing" when used on an "embed" tag diff --git a/doc/templates.rst b/doc/templates.rst index 1da89bcf214..0cd9092253c 100644 --- a/doc/templates.rst +++ b/doc/templates.rst @@ -259,9 +259,9 @@ Go to the :doc:`tags` page to learn more about the built-in tags. Comments -------- -To comment-out part of a line in a template, use the comment syntax ``{# ... -#}``. This is useful for debugging or to add information for other template -designers or yourself: +To comment-out part of a template, use the comment syntax ``{# ... #}``. This +is useful for debugging or to add information for other template designers or +yourself: .. code-block:: twig @@ -271,6 +271,44 @@ designers or yourself: {% endfor %} #} +.. versionadded:: 3.15 + + Inline comments were added in Twig 3.15. + +If you want to add comments inside a block, variable, or comment, use an inline +comment. They start with ``#`` and continue to the end of the line: + +.. code-block:: twig + + {{ + # this is an inline comment + "Hello World"|upper + # this is an inline comment + }} + + {{ + { + # this is an inline comment + fruit: 'apple', # this is an inline comment + color: 'red', # this is an inline comment + }|join(', ') + }} + +Inline comments can also be on the same line as the expression: + +.. code-block:: twig + + {{ + "Hello World"|upper # this is an inline comment + }} + +As inline comments continue until the end of the current line, the following +code does not work as ``}}``would be part of the comment: + +.. code-block:: twig + + {{ "Hello World"|upper # this is an inline comment }} + Including other Templates ------------------------- diff --git a/src/Lexer.php b/src/Lexer.php index 982c87e3f7f..1754791265e 100644 --- a/src/Lexer.php +++ b/src/Lexer.php @@ -48,6 +48,7 @@ class Lexer public const REGEX_STRING = '/"([^#"\\\\]*(?:\\\\.[^#"\\\\]*)*)"|\'([^\'\\\\]*(?:\\\\.[^\'\\\\]*)*)\'/As'; public const REGEX_DQ_STRING_DELIM = '/"/A'; public const REGEX_DQ_STRING_PART = '/[^#"\\\\]*(?:(?:\\\\.|#(?!\{))[^#"\\\\]*)*/As'; + public const REGEX_INLINE_COMMENT = '/#[^\n]*/A'; public const PUNCTUATION = '()[]{}?:.,|'; private const SPECIAL_CHARS = [ @@ -384,6 +385,10 @@ private function lexExpression(): void $this->pushState(self::STATE_STRING); $this->moveCursor($match[0]); } + // inline comment + elseif (preg_match(self::REGEX_INLINE_COMMENT, $this->code, $match, 0, $this->cursor)) { + $this->moveCursor($match[0]); + } // unlexable else { throw new SyntaxError(\sprintf('Unexpected character "%s".', $this->code[$this->cursor]), $this->lineno, $this->source); diff --git a/tests/LexerTest.php b/tests/LexerTest.php index 04e9930b912..53a6cb155dd 100644 --- a/tests/LexerTest.php +++ b/tests/LexerTest.php @@ -534,4 +534,102 @@ public static function getTemplateForStrings() yield ['日本では、春になると桜の花が咲きます。多くの人々は、公園や川の近くに集まり、お花見を楽しみます。桜の花びらが風に舞い、まるで雪のように見える瞬間は、とても美しいです。']; yield ['في العالم العربي، يُعتبر الخط العربي أحد أجمل أشكال الفن. يُستخدم الخط في تزيين المساجد والكتب والمخطوطات القديمة. يتميز الخط العربي بجماله وتناسقه، ويُعتبر رمزًا للثقافة الإسلامية.']; } + + public function testInlineCommentWithHashInString() + { + $lexer = new Lexer(new Environment(new ArrayLoader())); + $stream = $lexer->tokenize(new Source('{{ "me # this is NOT an inline comment" }}', 'index')); + $stream->expect(Token::VAR_START_TYPE); + $stream->expect(Token::STRING_TYPE, 'me # this is NOT an inline comment'); + $stream->expect(Token::VAR_END_TYPE); + $this->assertTrue($stream->isEOF()); + } + + /** + * @dataProvider getTemplateForInlineCommentsForVariable + */ + public function testInlineCommentForVariable(string $template) + { + $lexer = new Lexer(new Environment(new ArrayLoader())); + $stream = $lexer->tokenize(new Source($template, 'index')); + $stream->expect(Token::VAR_START_TYPE); + $stream->expect(Token::STRING_TYPE, 'me'); + $stream->expect(Token::VAR_END_TYPE); + $this->assertTrue($stream->isEOF()); + } + + public static function getTemplateForInlineCommentsForVariable() + { + yield ['{{ + "me" + # this is an inline comment + }}']; + yield ['{{ + # this is an inline comment + "me" + }}']; + yield ['{{ + "me" # this is an inline comment + }}']; + yield ['{{ + # this is an inline comment + "me" # this is an inline comment + # this is an inline comment + }}']; + } + + /** + * @dataProvider getTemplateForInlineCommentsForBlock + */ + public function testInlineCommentForBlock(string $template) + { + $lexer = new Lexer(new Environment(new ArrayLoader())); + $stream = $lexer->tokenize(new Source($template, 'index')); + $stream->expect(Token::BLOCK_START_TYPE); + $stream->expect(Token::NAME_TYPE, 'if'); + $stream->expect(Token::NAME_TYPE, 'true'); + $stream->expect(Token::BLOCK_END_TYPE); + $stream->expect(Token::TEXT_TYPE, 'me'); + $stream->expect(Token::BLOCK_START_TYPE); + $stream->expect(Token::NAME_TYPE, 'endif'); + $stream->expect(Token::BLOCK_END_TYPE); + $this->assertTrue($stream->isEOF()); + } + + public static function getTemplateForInlineCommentsForBlock() + { + yield ['{% + if true + # this is an inline comment + %}me{% endif %}']; + yield ['{% + # this is an inline comment + if true + %}me{% endif %}']; + yield ['{% + if true # this is an inline comment + %}me{% endif %}']; + yield ['{% + # this is an inline comment + if true # this is an inline comment + # this is an inline comment + %}me{% endif %}']; + } + + /** + * @dataProvider getTemplateForInlineCommentsForComment + */ + public function testInlineCommentForComment(string $template) + { + $lexer = new Lexer(new Environment(new ArrayLoader())); + $stream = $lexer->tokenize(new Source($template, 'index')); + $this->assertTrue($stream->isEOF()); + } + + public static function getTemplateForInlineCommentsForComment() + { + yield ['{# + Some regular comment # this is an inline comment + #}']; + } } From 8b278986b8645512dd013807404cc6be903de652 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Tue, 24 Sep 2024 17:36:20 +0200 Subject: [PATCH 507/812] Deprecate using Node directly, introduce EmptyNode and Nodes --- CHANGELOG | 1 + doc/deprecated.rst | 7 ++++ src/ExpressionParser.php | 20 ++++++----- src/Node/EmptyNode.php | 28 +++++++++++++++ src/Node/Expression/Filter/DefaultFilter.php | 3 +- src/Node/Expression/Filter/RawFilter.php | 3 +- .../Expression/NullCoalesceExpression.php | 5 +-- src/Node/ForNode.php | 2 +- src/Node/ModuleNode.php | 12 +++---- src/Node/Node.php | 4 +++ src/Node/Nodes.php | 28 +++++++++++++++ src/Node/SetNode.php | 3 +- src/NodeVisitor/EscaperNodeVisitor.php | 3 +- src/NodeVisitor/SandboxNodeVisitor.php | 5 +-- src/Parser.php | 13 ++++--- .../NodeVisitor/ProfilerNodeVisitor.php | 5 +-- src/TokenParser/ApplyTokenParser.php | 5 +-- src/TokenParser/BlockTokenParser.php | 6 ++-- src/TokenParser/ExtendsTokenParser.php | 3 +- src/TokenParser/IfTokenParser.php | 3 +- src/TokenParser/MacroTokenParser.php | 3 +- src/TokenParser/UseTokenParser.php | 6 ++-- tests/Node/AutoEscapeTest.php | 6 ++-- tests/Node/DeprecatedTest.php | 9 ++--- tests/Node/Expression/CallTest.php | 4 +-- tests/Node/Expression/FilterTest.php | 7 ++-- tests/Node/Expression/FunctionTest.php | 7 ++-- tests/Node/Expression/TestTest.php | 9 ++--- tests/Node/ForTest.php | 12 +++---- tests/Node/IfTest.php | 18 +++++----- tests/Node/MacroTest.php | 8 ++--- tests/Node/ModuleTest.php | 25 ++++++------- tests/Node/NodeTest.php | 36 ++++++++++--------- tests/Node/SetTest.php | 24 ++++++------- tests/NodeVisitor/SandboxTest.php | 4 +-- tests/ParserTest.php | 16 +++++---- tests/Util/CallableArgumentsExtractorTest.php | 7 ++-- 37 files changed, 230 insertions(+), 130 deletions(-) create mode 100644 src/Node/EmptyNode.php create mode 100644 src/Node/Nodes.php diff --git a/CHANGELOG b/CHANGELOG index 8fcd964030f..e1d01be3c41 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,6 @@ # 3.15.0 (2024-XX-XX) + * Deprecate instantiating `Node` directly. Use `EmptyNode` or `Nodes` instead. * Add support for inline comments * Add support for accessing class constants with the dot operator * Add `Profile::getStartTime()` and `Profile::getEndTime()` diff --git a/doc/deprecated.rst b/doc/deprecated.rst index 3a80f632249..2488e761dd5 100644 --- a/doc/deprecated.rst +++ b/doc/deprecated.rst @@ -289,3 +289,10 @@ Functions/Filters/Tests * For variadic arguments, use snake-case for the argument name to ease the transition to 4.0. + +Node +---- + +* Instantiating ``Twig\Node\Node`` directly is deprecated as of Twig 3.15. Use + ``EmptyNode`` or ``Nodes`` instead depending on the use case. The + ``Twig\Node\Node`` class will be abstract in Twig 4.0. diff --git a/src/ExpressionParser.php b/src/ExpressionParser.php index d45860840a8..cd1b1993eb6 100644 --- a/src/ExpressionParser.php +++ b/src/ExpressionParser.php @@ -14,6 +14,7 @@ use Twig\Attribute\FirstClassTwigCallableReady; use Twig\Error\SyntaxError; +use Twig\Node\EmptyNode; use Twig\Node\Expression\AbstractExpression; use Twig\Node\Expression\ArrayExpression; use Twig\Node\Expression\ArrowFunctionExpression; @@ -32,6 +33,7 @@ use Twig\Node\Expression\Unary\PosUnary; use Twig\Node\Expression\Unary\SpreadUnary; use Twig\Node\Node; +use Twig\Node\Nodes; /** * Parses expressions. @@ -110,7 +112,7 @@ private function parseArrow() $names = [new AssignNameExpression($token->getValue(), $token->getLine())]; $stream->expect(Token::ARROW_TYPE); - return new ArrowFunctionExpression($this->parseExpression(0), new Node($names), $line); + return new ArrowFunctionExpression($this->parseExpression(0), new Nodes($names), $line); } // first, determine if we are parsing an arrow function by finding => (long form) @@ -151,7 +153,7 @@ private function parseArrow() $stream->expect(Token::PUNCTUATION_TYPE, ')'); $stream->expect(Token::ARROW_TYPE); - return new ArrowFunctionExpression($this->parseExpression(0), new Node($names), $line); + return new ArrowFunctionExpression($this->parseExpression(0), new Nodes($names), $line); } private function getPrimary(): AbstractExpression @@ -467,7 +469,7 @@ public function getFunctionNode($name, $line) $function = $this->getFunction($name, $line); if ($function->getParserCallable()) { - $fakeNode = new Node(lineno: $line); + $fakeNode = new EmptyNode($line); $fakeNode->setSourceContext($this->parser->getStream()->getSourceContext()); return ($function->getParserCallable())($this->parser, $fakeNode, $args, $line); @@ -556,7 +558,7 @@ public function parseSubscriptExpression($node) } $filter = $this->getFilter('slice', $token->getLine()); - $arguments = new Node([$arg, $length]); + $arguments = new Nodes([$arg, $length]); $filter = new ($filter->getNodeClass())($node, $filter, $arguments, $token->getLine()); $stream->expect(Token::PUNCTUATION_TYPE, ']'); @@ -587,7 +589,7 @@ public function parseFilterExpressionRaw($node) $token = $this->parser->getStream()->expect(Token::NAME_TYPE); if (!$this->parser->getStream()->test(Token::PUNCTUATION_TYPE, '(')) { - $arguments = new Node(); + $arguments = new EmptyNode(); } else { $arguments = $this->parseArguments(true, false, true); } @@ -691,7 +693,7 @@ public function parseArguments($namedArguments = false, $definition = false, $al } $stream->expect(Token::PUNCTUATION_TYPE, ')', 'A list of arguments must be closed by a parenthesis'); - return new Node($args); + return new Nodes($args); } public function parseAssignmentExpression() @@ -717,7 +719,7 @@ public function parseAssignmentExpression() } } - return new Node($targets); + return new Nodes($targets); } public function parseMultitargetExpression() @@ -730,7 +732,7 @@ public function parseMultitargetExpression() } } - return new Node($targets); + return new Nodes($targets); } private function parseNotTestExpression(Node $node): NotUnary @@ -747,7 +749,7 @@ private function parseTestExpression(Node $node): TestExpression if ($stream->test(Token::PUNCTUATION_TYPE, '(')) { $arguments = $this->parseArguments(true); } elseif ($test->hasOneMandatoryArgument()) { - $arguments = new Node([0 => $this->parsePrimaryExpression()]); + $arguments = new Nodes([0 => $this->parsePrimaryExpression()]); } if ('defined' === $test->getName() && $node instanceof NameExpression && null !== $alias = $this->parser->getImportedSymbol('function', $node->getAttribute('name'))) { diff --git a/src/Node/EmptyNode.php b/src/Node/EmptyNode.php new file mode 100644 index 00000000000..95ee5408e4b --- /dev/null +++ b/src/Node/EmptyNode.php @@ -0,0 +1,28 @@ + + */ +#[YieldReady] +final class EmptyNode extends Node +{ + public function __construct(int $lineno = 0) + { + parent::__construct([], [], $lineno); + } +} diff --git a/src/Node/Expression/Filter/DefaultFilter.php b/src/Node/Expression/Filter/DefaultFilter.php index 75b6d18c287..fa93e227ca4 100644 --- a/src/Node/Expression/Filter/DefaultFilter.php +++ b/src/Node/Expression/Filter/DefaultFilter.php @@ -14,6 +14,7 @@ use Twig\Attribute\FirstClassTwigCallableReady; use Twig\Compiler; use Twig\Extension\CoreExtension; +use Twig\Node\EmptyNode; use Twig\Node\Expression\ConditionalExpression; use Twig\Node\Expression\ConstantExpression; use Twig\Node\Expression\FilterExpression; @@ -45,7 +46,7 @@ public function __construct(Node $node, TwigFilter|ConstantExpression $filter, N } if ('default' === $name && ($node instanceof NameExpression || $node instanceof GetAttrExpression)) { - $test = new DefinedTest(clone $node, new TwigTest('defined'), new Node(), $node->getTemplateLine()); + $test = new DefinedTest(clone $node, new TwigTest('defined'), new EmptyNode(), $node->getTemplateLine()); $false = \count($arguments) ? $arguments->getNode('0') : new ConstantExpression('', $node->getTemplateLine()); $node = new ConditionalExpression($test, $default, $false, $node->getTemplateLine()); diff --git a/src/Node/Expression/Filter/RawFilter.php b/src/Node/Expression/Filter/RawFilter.php index e115ab19410..adb6de7895f 100644 --- a/src/Node/Expression/Filter/RawFilter.php +++ b/src/Node/Expression/Filter/RawFilter.php @@ -13,6 +13,7 @@ use Twig\Attribute\FirstClassTwigCallableReady; use Twig\Compiler; +use Twig\Node\EmptyNode; use Twig\Node\Expression\ConstantExpression; use Twig\Node\Expression\FilterExpression; use Twig\Node\Node; @@ -26,7 +27,7 @@ class RawFilter extends FilterExpression #[FirstClassTwigCallableReady] public function __construct(Node $node, TwigFilter|ConstantExpression|null $filter = null, ?Node $arguments = null, int $lineno = 0) { - parent::__construct($node, $filter ?: new TwigFilter('raw', null, ['is_safe' => ['all']]), $arguments ?: new Node(), $lineno ?: $node->getTemplateLine()); + parent::__construct($node, $filter ?: new TwigFilter('raw', null, ['is_safe' => ['all']]), $arguments ?: new EmptyNode(), $lineno ?: $node->getTemplateLine()); } public function compile(Compiler $compiler): void diff --git a/src/Node/Expression/NullCoalesceExpression.php b/src/Node/Expression/NullCoalesceExpression.php index 98630f7f068..dd2384f9562 100644 --- a/src/Node/Expression/NullCoalesceExpression.php +++ b/src/Node/Expression/NullCoalesceExpression.php @@ -12,6 +12,7 @@ namespace Twig\Node\Expression; use Twig\Compiler; +use Twig\Node\EmptyNode; use Twig\Node\Expression\Binary\AndBinary; use Twig\Node\Expression\Test\DefinedTest; use Twig\Node\Expression\Test\NullTest; @@ -23,12 +24,12 @@ class NullCoalesceExpression extends ConditionalExpression { public function __construct(Node $left, Node $right, int $lineno) { - $test = new DefinedTest(clone $left, new TwigTest('defined'), new Node(), $left->getTemplateLine()); + $test = new DefinedTest(clone $left, new TwigTest('defined'), new EmptyNode(), $left->getTemplateLine()); // for "block()", we don't need the null test as the return value is always a string if (!$left instanceof BlockReferenceExpression) { $test = new AndBinary( $test, - new NotUnary(new NullTest($left, new TwigTest('null'), new Node(), $left->getTemplateLine()), $left->getTemplateLine()), + new NotUnary(new NullTest($left, new TwigTest('null'), new EmptyNode(), $left->getTemplateLine()), $left->getTemplateLine()), $left->getTemplateLine() ); } diff --git a/src/Node/ForNode.php b/src/Node/ForNode.php index 2fc014792ab..53a950a90ca 100644 --- a/src/Node/ForNode.php +++ b/src/Node/ForNode.php @@ -29,7 +29,7 @@ class ForNode extends Node public function __construct(AssignNameExpression $keyTarget, AssignNameExpression $valueTarget, AbstractExpression $seq, ?Node $ifexpr, Node $body, ?Node $else, int $lineno) { - $body = new Node([$body, $this->loop = new ForLoopNode($lineno)]); + $body = new Nodes([$body, $this->loop = new ForLoopNode($lineno)]); $nodes = ['key_target' => $keyTarget, 'value_target' => $valueTarget, 'seq' => $seq, 'body' => $body]; if (null !== $else) { diff --git a/src/Node/ModuleNode.php b/src/Node/ModuleNode.php index f67c3b0e76c..ddb3f734622 100644 --- a/src/Node/ModuleNode.php +++ b/src/Node/ModuleNode.php @@ -44,11 +44,11 @@ public function __construct(Node $body, ?AbstractExpression $parent, Node $block 'blocks' => $blocks, 'macros' => $macros, 'traits' => $traits, - 'display_start' => new Node(), - 'display_end' => new Node(), - 'constructor_start' => new Node(), - 'constructor_end' => new Node(), - 'class_end' => new Node(), + 'display_start' => new EmptyNode(), + 'display_end' => new EmptyNode(), + 'constructor_start' => new EmptyNode(), + 'constructor_end' => new EmptyNode(), + 'class_end' => new EmptyNode(), ]; if (null !== $parent) { $nodes['parent'] = $parent; @@ -414,7 +414,7 @@ protected function compileIsTraitable(Compiler $compiler) } if (!\count($nodes)) { - $nodes = new Node([$nodes]); + $nodes = new Nodes([$nodes]); } foreach ($nodes as $node) { diff --git a/src/Node/Node.php b/src/Node/Node.php index 275e61e2d68..d2fcb30ce37 100644 --- a/src/Node/Node.php +++ b/src/Node/Node.php @@ -47,6 +47,10 @@ class Node implements \Countable, \IteratorAggregate */ public function __construct(array $nodes = [], array $attributes = [], int $lineno = 0) { + if (self::class === static::class) { + trigger_deprecation('twig/twig', '3.15', \sprintf('Instantiating "%s" directly is deprecated; the class will become abstract in 4.0.', self::class)); + } + foreach ($nodes as $name => $node) { if (!$node instanceof self) { throw new \InvalidArgumentException(\sprintf('Using "%s" for the value of node "%s" of "%s" is not supported. You must pass a \Twig\Node\Node instance.', \is_object($node) ? $node::class : (null === $node ? 'null' : \gettype($node)), $name, static::class)); diff --git a/src/Node/Nodes.php b/src/Node/Nodes.php new file mode 100644 index 00000000000..bd67053abe1 --- /dev/null +++ b/src/Node/Nodes.php @@ -0,0 +1,28 @@ + + */ +#[YieldReady] +final class Nodes extends Node +{ + public function __construct(array $nodes = [], int $lineno = 0) + { + parent::__construct($nodes, [], $lineno); + } +} diff --git a/src/Node/SetNode.php b/src/Node/SetNode.php index 288911a5c22..6e0661edb51 100644 --- a/src/Node/SetNode.php +++ b/src/Node/SetNode.php @@ -33,7 +33,8 @@ public function __construct(bool $capture, Node $names, Node $values, int $linen $safe = false; if ($capture) { $safe = true; - if (Node::class === get_class($values) && !count($values)) { + // Node::class === get_class($values) should be removed in Twig 4.0 + if (($values instanceof Nodes || Node::class === get_class($values)) && !count($values)) { $values = new ConstantExpression('', $values->getTemplateLine()); $capture = false; } elseif ($values instanceof TextNode) { diff --git a/src/NodeVisitor/EscaperNodeVisitor.php b/src/NodeVisitor/EscaperNodeVisitor.php index 32f49ab1e8e..a4a415c9a39 100644 --- a/src/NodeVisitor/EscaperNodeVisitor.php +++ b/src/NodeVisitor/EscaperNodeVisitor.php @@ -24,6 +24,7 @@ use Twig\Node\ImportNode; use Twig\Node\ModuleNode; use Twig\Node\Node; +use Twig\Node\Nodes; use Twig\Node\PrintNode; use Twig\NodeTraverser; @@ -197,7 +198,7 @@ private function getEscaperFilter(Environment $env, string $type, Node $node): F { $line = $node->getTemplateLine(); $filter = $env->getFilter('escape'); - $args = new Node([new ConstantExpression($type, $line), new ConstantExpression(null, $line), new ConstantExpression(true, $line)]); + $args = new Nodes([new ConstantExpression($type, $line), new ConstantExpression(null, $line), new ConstantExpression(true, $line)]); return new FilterExpression($node, $filter, $args, $line); } diff --git a/src/NodeVisitor/SandboxNodeVisitor.php b/src/NodeVisitor/SandboxNodeVisitor.php index 37e184a3edc..ab51d33d4a0 100644 --- a/src/NodeVisitor/SandboxNodeVisitor.php +++ b/src/NodeVisitor/SandboxNodeVisitor.php @@ -23,6 +23,7 @@ use Twig\Node\Expression\NameExpression; use Twig\Node\ModuleNode; use Twig\Node\Node; +use Twig\Node\Nodes; use Twig\Node\PrintNode; use Twig\Node\SetNode; @@ -105,8 +106,8 @@ public function leaveNode(Node $node, Environment $env): ?Node if ($node instanceof ModuleNode) { $this->inAModule = false; - $node->setNode('constructor_end', new Node([new CheckSecurityCallNode(), $node->getNode('constructor_end')])); - $node->setNode('class_end', new Node([new CheckSecurityNode($this->filters, $this->tags, $this->functions), $node->getNode('class_end')])); + $node->setNode('constructor_end', new Nodes([new CheckSecurityCallNode(), $node->getNode('constructor_end')])); + $node->setNode('class_end', new Nodes([new CheckSecurityNode($this->filters, $this->tags, $this->functions), $node->getNode('class_end')])); } elseif ($this->inAModule) { if ($node instanceof PrintNode || $node instanceof SetNode) { $this->needsToStringWrap = false; diff --git a/src/Parser.php b/src/Parser.php index 40370bb1bf2..d30868a069a 100644 --- a/src/Parser.php +++ b/src/Parser.php @@ -16,12 +16,14 @@ use Twig\Node\BlockNode; use Twig\Node\BlockReferenceNode; use Twig\Node\BodyNode; +use Twig\Node\EmptyNode; use Twig\Node\Expression\AbstractExpression; use Twig\Node\MacroNode; use Twig\Node\ModuleNode; use Twig\Node\Node; use Twig\Node\NodeCaptureInterface; use Twig\Node\NodeOutputInterface; +use Twig\Node\Nodes; use Twig\Node\PrintNode; use Twig\Node\TextNode; use Twig\TokenParser\TokenParserInterface; @@ -83,7 +85,7 @@ public function parse(TokenStream $stream, $test = null, bool $dropNeedle = fals $body = $this->subparse($test, $dropNeedle); if (null !== $this->parent && null === $body = $this->filterBodyNodes($body)) { - $body = new Node(); + $body = new EmptyNode(); } } catch (SyntaxError $e) { if (!$e->getSourceContext()) { @@ -97,7 +99,7 @@ public function parse(TokenStream $stream, $test = null, bool $dropNeedle = fals throw $e; } - $node = new ModuleNode(new BodyNode([$body]), $this->parent, new Node($this->blocks), new Node($this->macros), new Node($this->traits), $this->embeddedTemplates, $stream->getSourceContext()); + $node = new ModuleNode(new BodyNode([$body]), $this->parent, new Nodes($this->blocks), new Nodes($this->macros), new Nodes($this->traits), $this->embeddedTemplates, $stream->getSourceContext()); $traverser = new NodeTraverser($this->env, $this->visitors); @@ -149,7 +151,7 @@ public function subparse($test, bool $dropNeedle = false): Node return $rv[0]; } - return new Node($rv, [], $lineno); + return new Nodes($rv, $lineno); } if (!$subparser = $this->env->getTokenParser($token->getValue())) { @@ -189,7 +191,7 @@ public function subparse($test, bool $dropNeedle = false): Node return $rv[0]; } - return new Node($rv, [], $lineno); + return new Nodes($rv, $lineno); } public function getBlockStack(): array @@ -371,7 +373,8 @@ private function filterBodyNodes(Node $node, bool $nested = false): ?Node // here, $nested means "being at the root level of a child template" // we need to discard the wrapping "Node" for the "body" node - $nested = $nested || Node::class !== \get_class($node); + // Node::class !== \get_class($node) should be removed in Twig 4.0 + $nested = $nested || (Node::class !== \get_class($node) && !$node instanceof Nodes); foreach ($node as $k => $n) { if (null !== $n && null === $this->filterBodyNodes($n, $nested)) { $node->removeNode($k); diff --git a/src/Profiler/NodeVisitor/ProfilerNodeVisitor.php b/src/Profiler/NodeVisitor/ProfilerNodeVisitor.php index 1458bc5fcc8..4c5c2005d21 100644 --- a/src/Profiler/NodeVisitor/ProfilerNodeVisitor.php +++ b/src/Profiler/NodeVisitor/ProfilerNodeVisitor.php @@ -17,6 +17,7 @@ use Twig\Node\MacroNode; use Twig\Node\ModuleNode; use Twig\Node\Node; +use Twig\Node\Nodes; use Twig\NodeVisitor\NodeVisitorInterface; use Twig\Profiler\Node\EnterProfileNode; use Twig\Profiler\Node\LeaveProfileNode; @@ -43,8 +44,8 @@ public function enterNode(Node $node, Environment $env): Node public function leaveNode(Node $node, Environment $env): ?Node { if ($node instanceof ModuleNode) { - $node->setNode('display_start', new Node([new EnterProfileNode($this->extensionName, Profile::TEMPLATE, $node->getTemplateName(), $this->varName), $node->getNode('display_start')])); - $node->setNode('display_end', new Node([new LeaveProfileNode($this->varName), $node->getNode('display_end')])); + $node->setNode('display_start', new Nodes([new EnterProfileNode($this->extensionName, Profile::TEMPLATE, $node->getTemplateName(), $this->varName), $node->getNode('display_start')])); + $node->setNode('display_end', new Nodes([new LeaveProfileNode($this->varName), $node->getNode('display_end')])); } elseif ($node instanceof BlockNode) { $node->setNode('body', new BodyNode([ new EnterProfileNode($this->extensionName, Profile::BLOCK, $node->getAttribute('name'), $this->varName), diff --git a/src/TokenParser/ApplyTokenParser.php b/src/TokenParser/ApplyTokenParser.php index 0a6c1afb513..58c2a3ee4e2 100644 --- a/src/TokenParser/ApplyTokenParser.php +++ b/src/TokenParser/ApplyTokenParser.php @@ -13,6 +13,7 @@ use Twig\Node\Expression\TempNameExpression; use Twig\Node\Node; +use Twig\Node\Nodes; use Twig\Node\PrintNode; use Twig\Node\SetNode; use Twig\Token; @@ -42,10 +43,10 @@ public function parse(Token $token): Node $body = $this->parser->subparse([$this, 'decideApplyEnd'], true); $this->parser->getStream()->expect(Token::BLOCK_END_TYPE); - return new Node([ + return new Nodes([ new SetNode(true, $ref, $body, $lineno), new PrintNode($filter, $lineno), - ], [], $lineno); + ], $lineno); } public function decideApplyEnd(Token $token): bool diff --git a/src/TokenParser/BlockTokenParser.php b/src/TokenParser/BlockTokenParser.php index 81d675db0d9..3561b99cdd7 100644 --- a/src/TokenParser/BlockTokenParser.php +++ b/src/TokenParser/BlockTokenParser.php @@ -15,7 +15,9 @@ use Twig\Error\SyntaxError; use Twig\Node\BlockNode; use Twig\Node\BlockReferenceNode; +use Twig\Node\EmptyNode; use Twig\Node\Node; +use Twig\Node\Nodes; use Twig\Node\PrintNode; use Twig\Token; @@ -36,7 +38,7 @@ public function parse(Token $token): Node $lineno = $token->getLine(); $stream = $this->parser->getStream(); $name = $stream->expect(Token::NAME_TYPE)->getValue(); - $this->parser->setBlock($name, $block = new BlockNode($name, new Node([]), $lineno)); + $this->parser->setBlock($name, $block = new BlockNode($name, new EmptyNode(), $lineno)); $this->parser->pushLocalScope(); $this->parser->pushBlockStack($name); @@ -50,7 +52,7 @@ public function parse(Token $token): Node } } } else { - $body = new Node([ + $body = new Nodes([ new PrintNode($this->parser->getExpressionParser()->parseExpression(), $lineno), ]); } diff --git a/src/TokenParser/ExtendsTokenParser.php b/src/TokenParser/ExtendsTokenParser.php index 86ddfdfba34..a93afe8cd59 100644 --- a/src/TokenParser/ExtendsTokenParser.php +++ b/src/TokenParser/ExtendsTokenParser.php @@ -13,6 +13,7 @@ namespace Twig\TokenParser; use Twig\Error\SyntaxError; +use Twig\Node\EmptyNode; use Twig\Node\Node; use Twig\Token; @@ -39,7 +40,7 @@ public function parse(Token $token): Node $stream->expect(Token::BLOCK_END_TYPE); - return new Node([], [], $token->getLine()); + return new EmptyNode($token->getLine()); } public function getTag(): string diff --git a/src/TokenParser/IfTokenParser.php b/src/TokenParser/IfTokenParser.php index 4ea6f3df9c3..6b90105633b 100644 --- a/src/TokenParser/IfTokenParser.php +++ b/src/TokenParser/IfTokenParser.php @@ -15,6 +15,7 @@ use Twig\Error\SyntaxError; use Twig\Node\IfNode; use Twig\Node\Node; +use Twig\Node\Nodes; use Twig\Token; /** @@ -69,7 +70,7 @@ public function parse(Token $token): Node $stream->expect(Token::BLOCK_END_TYPE); - return new IfNode(new Node($tests), $else, $lineno); + return new IfNode(new Nodes($tests), $else, $lineno); } public function decideIfFork(Token $token): bool diff --git a/src/TokenParser/MacroTokenParser.php b/src/TokenParser/MacroTokenParser.php index c7762075c56..8dab480f9d2 100644 --- a/src/TokenParser/MacroTokenParser.php +++ b/src/TokenParser/MacroTokenParser.php @@ -13,6 +13,7 @@ use Twig\Error\SyntaxError; use Twig\Node\BodyNode; +use Twig\Node\EmptyNode; use Twig\Node\MacroNode; use Twig\Node\Node; use Twig\Token; @@ -51,7 +52,7 @@ public function parse(Token $token): Node $this->parser->setMacro($name, new MacroNode($name, new BodyNode([$body]), $arguments, $lineno)); - return new Node([], [], $lineno); + return new EmptyNode($lineno); } public function decideBlockEnd(Token $token): bool diff --git a/src/TokenParser/UseTokenParser.php b/src/TokenParser/UseTokenParser.php index 1b96b40478e..ebd95aa317f 100644 --- a/src/TokenParser/UseTokenParser.php +++ b/src/TokenParser/UseTokenParser.php @@ -12,8 +12,10 @@ namespace Twig\TokenParser; use Twig\Error\SyntaxError; +use Twig\Node\EmptyNode; use Twig\Node\Expression\ConstantExpression; use Twig\Node\Node; +use Twig\Node\Nodes; use Twig\Token; /** @@ -61,9 +63,9 @@ public function parse(Token $token): Node $stream->expect(Token::BLOCK_END_TYPE); - $this->parser->addTrait(new Node(['template' => $template, 'targets' => new Node($targets)])); + $this->parser->addTrait(new Nodes(['template' => $template, 'targets' => new Nodes($targets)])); - return new Node([], [], $token->getLine()); + return new EmptyNode($token->getLine()); } public function getTag(): string diff --git a/tests/Node/AutoEscapeTest.php b/tests/Node/AutoEscapeTest.php index 546d4345546..29dbf83a538 100644 --- a/tests/Node/AutoEscapeTest.php +++ b/tests/Node/AutoEscapeTest.php @@ -12,7 +12,7 @@ */ use Twig\Node\AutoEscapeNode; -use Twig\Node\Node; +use Twig\Node\Nodes; use Twig\Node\TextNode; use Twig\Test\NodeTestCase; @@ -20,7 +20,7 @@ class AutoEscapeTest extends NodeTestCase { public function testConstructor() { - $body = new Node([new TextNode('foo', 1)]); + $body = new Nodes([new TextNode('foo', 1)]); $node = new AutoEscapeNode(true, $body, 1); $this->assertEquals($body, $node->getNode('body')); @@ -29,7 +29,7 @@ public function testConstructor() public static function provideTests(): iterable { - $body = new Node([new TextNode('foo', 1)]); + $body = new Nodes([new TextNode('foo', 1)]); $node = new AutoEscapeNode(true, $body, 1); return [ diff --git a/tests/Node/DeprecatedTest.php b/tests/Node/DeprecatedTest.php index 9738b2ff4cf..fe646a0355a 100644 --- a/tests/Node/DeprecatedTest.php +++ b/tests/Node/DeprecatedTest.php @@ -15,10 +15,11 @@ use Twig\Environment; use Twig\Loader\ArrayLoader; use Twig\Node\DeprecatedNode; +use Twig\Node\EmptyNode; use Twig\Node\Expression\ConstantExpression; use Twig\Node\Expression\FunctionExpression; use Twig\Node\IfNode; -use Twig\Node\Node; +use Twig\Node\Nodes; use Twig\Source; use Twig\Test\NodeTestCase; use Twig\TwigFunction; @@ -49,10 +50,10 @@ public static function provideTests(): iterable EOF ]; - $t = new Node([ + $t = new Nodes([ new ConstantExpression(true, 1), $dep = new DeprecatedNode($expr, 2), - ], [], 1); + ], 1); $node = new IfNode($t, null, 1); $node->setSourceContext(new Source('', 'foo.twig')); $dep->setNode('package', new ConstantExpression('twig/twig', 1)); @@ -70,7 +71,7 @@ public static function provideTests(): iterable $environment = new Environment(new ArrayLoader()); $environment->addFunction($function = new TwigFunction('foo', 'Twig\Tests\Node\foo', [])); - $expr = new FunctionExpression($function, new Node(), 1); + $expr = new FunctionExpression($function, new EmptyNode(), 1); $node = new DeprecatedNode($expr, 1); $node->setSourceContext(new Source('', 'foo.twig')); $node->setNode('package', new ConstantExpression('twig/twig', 1)); diff --git a/tests/Node/Expression/CallTest.php b/tests/Node/Expression/CallTest.php index 75cac054734..a77834ced56 100644 --- a/tests/Node/Expression/CallTest.php +++ b/tests/Node/Expression/CallTest.php @@ -13,8 +13,8 @@ use PHPUnit\Framework\TestCase; use Twig\Error\SyntaxError; +use Twig\Node\EmptyNode; use Twig\Node\Expression\FunctionExpression; -use Twig\Node\Node; use Twig\TwigFunction; /** @@ -156,7 +156,7 @@ private function getArguments($call, $args) private function createFunctionExpression($name, $callable, $isVariadic = false): Node_Expression_Call { - return new Node_Expression_Call(new TwigFunction($name, $callable, ['is_variadic' => $isVariadic]), new Node([]), 0); + return new Node_Expression_Call(new TwigFunction($name, $callable, ['is_variadic' => $isVariadic]), new EmptyNode(), 0); } } diff --git a/tests/Node/Expression/FilterTest.php b/tests/Node/Expression/FilterTest.php index 78bf5066426..ff4484d4efd 100644 --- a/tests/Node/Expression/FilterTest.php +++ b/tests/Node/Expression/FilterTest.php @@ -15,9 +15,10 @@ use Twig\Error\SyntaxError; use Twig\Extension\AbstractExtension; use Twig\Loader\ArrayLoader; +use Twig\Node\EmptyNode; use Twig\Node\Expression\ConstantExpression; use Twig\Node\Expression\FilterExpression; -use Twig\Node\Node; +use Twig\Node\Nodes; use Twig\Test\NodeTestCase; use Twig\TwigFilter; @@ -27,7 +28,7 @@ public function testConstructor() { $expr = new ConstantExpression('foo', 1); $filter = new TwigFilter($name = 'upper'); - $args = new Node(); + $args = new EmptyNode(); $node = new FilterExpression($expr, $filter, $args, 1); $this->assertEquals($expr, $node->getNode('node')); @@ -158,7 +159,7 @@ public function testCompileWithMissingNamedArgument() private static function createFilter(Environment $env, $node, $name, array $arguments = []): FilterExpression { - return new FilterExpression($node, $env->getFilter($name), new Node($arguments), 1); + return new FilterExpression($node, $env->getFilter($name), new Nodes($arguments), 1); } protected static function createEnvironment(): Environment diff --git a/tests/Node/Expression/FunctionTest.php b/tests/Node/Expression/FunctionTest.php index a6e562c06ce..97c215c398e 100644 --- a/tests/Node/Expression/FunctionTest.php +++ b/tests/Node/Expression/FunctionTest.php @@ -13,9 +13,10 @@ use Twig\Environment; use Twig\Loader\ArrayLoader; +use Twig\Node\EmptyNode; use Twig\Node\Expression\ConstantExpression; use Twig\Node\Expression\FunctionExpression; -use Twig\Node\Node; +use Twig\Node\Nodes; use Twig\Test\NodeTestCase; use Twig\TwigFunction; @@ -24,7 +25,7 @@ class FunctionTest extends NodeTestCase public function testConstructor() { $name = 'function'; - $args = new Node(); + $args = new EmptyNode(); $node = new FunctionExpression(new TwigFunction($name), $args, 1); $this->assertEquals($name, $node->getAttribute('name')); @@ -98,7 +99,7 @@ public static function provideTests(): iterable private static function createFunction(Environment $env, $name, array $arguments = []): FunctionExpression { - return new FunctionExpression($env->getFunction($name), new Node($arguments), 1); + return new FunctionExpression($env->getFunction($name), new Nodes($arguments), 1); } protected static function createEnvironment(): Environment diff --git a/tests/Node/Expression/TestTest.php b/tests/Node/Expression/TestTest.php index ddda8a486b4..0e9459c3bd9 100644 --- a/tests/Node/Expression/TestTest.php +++ b/tests/Node/Expression/TestTest.php @@ -13,10 +13,11 @@ use Twig\Environment; use Twig\Loader\ArrayLoader; +use Twig\Node\EmptyNode; use Twig\Node\Expression\ConstantExpression; use Twig\Node\Expression\Test\NullTest; use Twig\Node\Expression\TestExpression; -use Twig\Node\Node; +use Twig\Node\Nodes; use Twig\Test\NodeTestCase; use Twig\TwigTest; @@ -26,7 +27,7 @@ public function testConstructor() { $expr = new ConstantExpression('foo', 1); $name = 'test_name'; - $args = new Node(); + $args = new EmptyNode(); $node = new TestExpression($expr, new TwigTest($name), $args, 1); $this->assertEquals($expr, $node->getNode('node')); @@ -41,7 +42,7 @@ public static function provideTests(): iterable $tests = []; $expr = new ConstantExpression('foo', 1); - $node = new NullTest($expr, $environment->getTest('null'), new Node([]), 1); + $node = new NullTest($expr, $environment->getTest('null'), new EmptyNode(), 1); $tests[] = [$node, '(null === "foo")']; // test as an anonymous function @@ -72,7 +73,7 @@ public static function provideTests(): iterable private static function createTest(Environment $env, $node, $name, array $arguments = []): TestExpression { - return new TestExpression($node, $env->getTest($name), new Node($arguments), 1); + return new TestExpression($node, $env->getTest($name), new Nodes($arguments), 1); } protected static function createEnvironment(): Environment diff --git a/tests/Node/ForTest.php b/tests/Node/ForTest.php index 047c2a1092d..4f96e94c4f8 100644 --- a/tests/Node/ForTest.php +++ b/tests/Node/ForTest.php @@ -14,7 +14,7 @@ use Twig\Node\Expression\AssignNameExpression; use Twig\Node\Expression\NameExpression; use Twig\Node\ForNode; -use Twig\Node\Node; +use Twig\Node\Nodes; use Twig\Node\PrintNode; use Twig\Test\NodeTestCase; @@ -25,7 +25,7 @@ public function testConstructor() $keyTarget = new AssignNameExpression('key', 1); $valueTarget = new AssignNameExpression('item', 1); $seq = new NameExpression('items', 1); - $body = new Node([new PrintNode(new NameExpression('foo', 1), 1)], [], 1); + $body = new Nodes([new PrintNode(new NameExpression('foo', 1), 1)], 1); $else = null; $node = new ForNode($keyTarget, $valueTarget, $seq, null, $body, $else, 1); $node->setAttribute('with_loop', false); @@ -49,7 +49,7 @@ public static function provideTests(): iterable $keyTarget = new AssignNameExpression('key', 1); $valueTarget = new AssignNameExpression('item', 1); $seq = new NameExpression('items', 1); - $body = new Node([new PrintNode(new NameExpression('foo', 1), 1)], [], 1); + $body = new Nodes([new PrintNode(new NameExpression('foo', 1), 1)], 1); $else = null; $node = new ForNode($keyTarget, $valueTarget, $seq, null, $body, $else, 1); $node->setAttribute('with_loop', false); @@ -74,7 +74,7 @@ public static function provideTests(): iterable $keyTarget = new AssignNameExpression('k', 1); $valueTarget = new AssignNameExpression('v', 1); $seq = new NameExpression('values', 1); - $body = new Node([new PrintNode(new NameExpression('foo', 1), 1)], [], 1); + $body = new Nodes([new PrintNode(new NameExpression('foo', 1), 1)], 1); $else = null; $node = new ForNode($keyTarget, $valueTarget, $seq, null, $body, $else, 1); $node->setAttribute('with_loop', true); @@ -116,7 +116,7 @@ public static function provideTests(): iterable $keyTarget = new AssignNameExpression('k', 1); $valueTarget = new AssignNameExpression('v', 1); $seq = new NameExpression('values', 1); - $body = new Node([new PrintNode(new NameExpression('foo', 1), 1)], [], 1); + $body = new Nodes([new PrintNode(new NameExpression('foo', 1), 1)], 1); $else = null; $node = new ForNode($keyTarget, $valueTarget, $seq, null, $body, $else, 1); $node->setAttribute('with_loop', true); @@ -158,7 +158,7 @@ public static function provideTests(): iterable $keyTarget = new AssignNameExpression('k', 1); $valueTarget = new AssignNameExpression('v', 1); $seq = new NameExpression('values', 1); - $body = new Node([new PrintNode(new NameExpression('foo', 1), 1)], [], 1); + $body = new Nodes([new PrintNode(new NameExpression('foo', 1), 1)], 1); $else = new PrintNode(new NameExpression('foo', 1), 1); $node = new ForNode($keyTarget, $valueTarget, $seq, null, $body, $else, 1); $node->setAttribute('with_loop', true); diff --git a/tests/Node/IfTest.php b/tests/Node/IfTest.php index 4736def3ffa..c3939f1e000 100644 --- a/tests/Node/IfTest.php +++ b/tests/Node/IfTest.php @@ -14,7 +14,7 @@ use Twig\Node\Expression\ConstantExpression; use Twig\Node\Expression\NameExpression; use Twig\Node\IfNode; -use Twig\Node\Node; +use Twig\Node\Nodes; use Twig\Node\PrintNode; use Twig\Test\NodeTestCase; @@ -22,10 +22,10 @@ class IfTest extends NodeTestCase { public function testConstructor() { - $t = new Node([ + $t = new Nodes([ new ConstantExpression(true, 1), new PrintNode(new NameExpression('foo', 1), 1), - ], [], 1); + ], 1); $else = null; $node = new IfNode($t, $else, 1); @@ -41,10 +41,10 @@ public static function provideTests(): iterable { $tests = []; - $t = new Node([ + $t = new Nodes([ new ConstantExpression(true, 1), new PrintNode(new NameExpression('foo', 1), 1), - ], [], 1); + ], 1); $else = null; $node = new IfNode($t, $else, 1); @@ -59,12 +59,12 @@ public static function provideTests(): iterable EOF ]; - $t = new Node([ + $t = new Nodes([ new ConstantExpression(true, 1), new PrintNode(new NameExpression('foo', 1), 1), new ConstantExpression(false, 1), new PrintNode(new NameExpression('bar', 1), 1), - ], [], 1); + ], 1); $else = null; $node = new IfNode($t, $else, 1); @@ -78,10 +78,10 @@ public static function provideTests(): iterable EOF ]; - $t = new Node([ + $t = new Nodes([ new ConstantExpression(true, 1), new PrintNode(new NameExpression('foo', 1), 1), - ], [], 1); + ], 1); $else = new PrintNode(new NameExpression('bar', 1), 1); $node = new IfNode($t, $else, 1); diff --git a/tests/Node/MacroTest.php b/tests/Node/MacroTest.php index 6fb6062cbd3..7321c780197 100644 --- a/tests/Node/MacroTest.php +++ b/tests/Node/MacroTest.php @@ -17,7 +17,7 @@ use Twig\Node\Expression\ConstantExpression; use Twig\Node\Expression\NameExpression; use Twig\Node\MacroNode; -use Twig\Node\Node; +use Twig\Node\Nodes; use Twig\Node\TextNode; use Twig\Test\NodeTestCase; @@ -26,7 +26,7 @@ class MacroTest extends NodeTestCase public function testConstructor() { $body = new BodyNode([new TextNode('foo', 1)]); - $arguments = new Node([new NameExpression('foo', 1)], [], 1); + $arguments = new Nodes([new NameExpression('foo', 1)], 1); $node = new MacroNode('foo', $body, $arguments, 1); $this->assertEquals($body, $node->getNode('body')); @@ -36,10 +36,10 @@ public function testConstructor() public static function provideTests(): iterable { - $arguments = new Node([ + $arguments = new Nodes([ 'foo' => new ConstantExpression(null, 1), 'bar' => new ConstantExpression('Foo', 1), - ], [], 1); + ], 1); $body = new BodyNode([new TextNode('foo', 1)]); $node = new MacroNode('foo', $body, $arguments, 1); diff --git a/tests/Node/ModuleTest.php b/tests/Node/ModuleTest.php index 21f59c8b0cf..bee23c28066 100644 --- a/tests/Node/ModuleTest.php +++ b/tests/Node/ModuleTest.php @@ -14,12 +14,13 @@ use Twig\Environment; use Twig\Loader\ArrayLoader; use Twig\Node\BodyNode; +use Twig\Node\EmptyNode; use Twig\Node\Expression\AssignNameExpression; use Twig\Node\Expression\ConditionalExpression; use Twig\Node\Expression\ConstantExpression; use Twig\Node\ImportNode; use Twig\Node\ModuleNode; -use Twig\Node\Node; +use Twig\Node\Nodes; use Twig\Node\SetNode; use Twig\Node\TextNode; use Twig\Source; @@ -31,11 +32,11 @@ public function testConstructor() { $body = new BodyNode([new TextNode('foo', 1)]); $parent = new ConstantExpression('layout.twig', 1); - $blocks = new Node(); - $macros = new Node(); - $traits = new Node(); + $blocks = new EmptyNode(); + $macros = new EmptyNode(); + $traits = new EmptyNode(); $source = new Source('{{ foo }}', 'foo.twig'); - $node = new ModuleNode($body, $parent, $blocks, $macros, $traits, new Node([]), $source); + $node = new ModuleNode($body, $parent, $blocks, $macros, $traits, new EmptyNode(), $source); $this->assertEquals($body, $node->getNode('body')); $this->assertEquals($blocks, $node->getNode('blocks')); @@ -52,12 +53,12 @@ public static function provideTests(): iterable $body = new BodyNode([new TextNode('foo', 1)]); $extends = null; - $blocks = new Node(); - $macros = new Node(); - $traits = new Node(); + $blocks = new EmptyNode(); + $macros = new EmptyNode(); + $traits = new EmptyNode(); $source = new Source('{{ foo }}', 'foo.twig'); - $node = new ModuleNode($body, $extends, $blocks, $macros, $traits, new Node([]), $source); + $node = new ModuleNode($body, $extends, $blocks, $macros, $traits, new EmptyNode(), $source); $tests[] = [$node, << '{{ foo }}']), ['debug' => true]); - $node = new ModuleNode($body, $extends, $blocks, $macros, $traits, new Node([]), $source); + $node = new ModuleNode($body, $extends, $blocks, $macros, $traits, new EmptyNode(), $source); $tests[] = [$node, << function () { return '1'; }], 1); + $node = new NodeForTest([], ['value' => function () { return '1'; }], 1); $this->assertEquals(<< new TwigFunction('a_function'), 'filter' => new TwigFilter('a_filter'), 'test' => new TwigTest('a_test'), ], 1); $this->assertEquals(<<setNodeTag('tag'); $this->assertEquals(<< false]); + $node = new NodeForTest([], ['foo' => false]); $node->deprecateAttribute('foo', new NameDeprecation('foo/bar', '2.0', 'bar')); $this->assertFalse($node->getAttribute('foo', false)); @@ -80,10 +80,10 @@ public function testAttributeDeprecationIgnore() */ public function testAttributeDeprecationWithoutAlternative() { - $node = new Node([], ['foo' => false]); + $node = new NodeForTest([], ['foo' => false]); $node->deprecateAttribute('foo', new NameDeprecation('foo/bar', '2.0')); - $this->expectDeprecation('Since foo/bar 2.0: Getting attribute "foo" on a "Twig\Node\Node" class is deprecated.'); + $this->expectDeprecation('Since foo/bar 2.0: Getting attribute "foo" on a "Twig\Tests\Node\NodeForTest" class is deprecated.'); $this->assertFalse($node->getAttribute('foo')); } @@ -92,16 +92,16 @@ public function testAttributeDeprecationWithoutAlternative() */ public function testAttributeDeprecationWithAlternative() { - $node = new Node([], ['foo' => false]); + $node = new NodeForTest([], ['foo' => false]); $node->deprecateAttribute('foo', new NameDeprecation('foo/bar', '2.0', 'bar')); - $this->expectDeprecation('Since foo/bar 2.0: Getting attribute "foo" on a "Twig\Node\Node" class is deprecated, get the "bar" attribute instead.'); + $this->expectDeprecation('Since foo/bar 2.0: Getting attribute "foo" on a "Twig\Tests\Node\NodeForTest" class is deprecated, get the "bar" attribute instead.'); $this->assertFalse($node->getAttribute('foo')); } public function testNodeDeprecationIgnore() { - $node = new Node(['foo' => $foo = new Node()], []); + $node = new NodeForTest(['foo' => $foo = new NodeForTest()]); $node->deprecateNode('foo', new NameDeprecation('foo/bar', '2.0')); $this->assertSame($foo, $node->getNode('foo', false)); @@ -112,10 +112,10 @@ public function testNodeDeprecationIgnore() */ public function testNodeDeprecationWithoutAlternative() { - $node = new Node(['foo' => $foo = new Node()], []); + $node = new NodeForTest(['foo' => $foo = new NodeForTest()]); $node->deprecateNode('foo', new NameDeprecation('foo/bar', '2.0')); - $this->expectDeprecation('Since foo/bar 2.0: Getting node "foo" on a "Twig\Node\Node" class is deprecated.'); + $this->expectDeprecation('Since foo/bar 2.0: Getting node "foo" on a "Twig\Tests\Node\NodeForTest" class is deprecated.'); $this->assertSame($foo, $node->getNode('foo')); } @@ -124,10 +124,14 @@ public function testNodeDeprecationWithoutAlternative() */ public function testNodeAttributeDeprecationWithAlternative() { - $node = new Node(['foo' => $foo = new Node()], []); + $node = new NodeForTest(['foo' => $foo = new NodeForTest()]); $node->deprecateNode('foo', new NameDeprecation('foo/bar', '2.0', 'bar')); - $this->expectDeprecation('Since foo/bar 2.0: Getting node "foo" on a "Twig\Node\Node" class is deprecated, get the "bar" node instead.'); + $this->expectDeprecation('Since foo/bar 2.0: Getting node "foo" on a "Twig\Tests\Node\NodeForTest" class is deprecated, get the "bar" node instead.'); $this->assertSame($foo, $node->getNode('foo')); } } + +class NodeForTest extends Node +{ +} \ No newline at end of file diff --git a/tests/Node/SetTest.php b/tests/Node/SetTest.php index 06f0407dd2b..6283884afe8 100644 --- a/tests/Node/SetTest.php +++ b/tests/Node/SetTest.php @@ -16,7 +16,7 @@ use Twig\Node\Expression\AssignNameExpression; use Twig\Node\Expression\ConstantExpression; use Twig\Node\Expression\NameExpression; -use Twig\Node\Node; +use Twig\Node\Nodes; use Twig\Node\PrintNode; use Twig\Node\SetNode; use Twig\Node\TextNode; @@ -26,8 +26,8 @@ class SetTest extends NodeTestCase { public function testConstructor() { - $names = new Node([new AssignNameExpression('foo', 1)], [], 1); - $values = new Node([new ConstantExpression('foo', 1)], [], 1); + $names = new Nodes([new AssignNameExpression('foo', 1)], 1); + $values = new Nodes([new ConstantExpression('foo', 1)], 1); $node = new SetNode(false, $names, $values, 1); $this->assertEquals($names, $node->getNode('names')); @@ -39,8 +39,8 @@ public static function provideTests(): iterable { $tests = []; - $names = new Node([new AssignNameExpression('foo', 1)], [], 1); - $values = new Node([new ConstantExpression('foo', 1)], [], 1); + $names = new Nodes([new AssignNameExpression('foo', 1)], 1); + $values = new Nodes([new ConstantExpression('foo', 1)], 1); $node = new SetNode(false, $names, $values, 1); $tests[] = [$node, << false]), ]; - $names = new Node([new AssignNameExpression('foo', 1)], [], 1); + $names = new Nodes([new AssignNameExpression('foo', 1)], 1); $values = new TextNode('foo', 1); $node = new SetNode(true, $names, $values, 1); $tests[] = [$node, <<setAttribute('is_generator', true); - $node = new ModuleNode(new BodyNode([new PrintNode($expr, 1)]), null, new Node(), new Node(), new Node(), new Node([]), new Source('foo', 'foo')); + $node = new ModuleNode(new BodyNode([new PrintNode($expr, 1)]), null, new EmptyNode(), new EmptyNode(), new EmptyNode(), new EmptyNode(), new Source('foo', 'foo')); $traverser = new NodeTraverser($env, [new SandboxNodeVisitor($env)]); $node = $traverser->traverse($node); diff --git a/tests/ParserTest.php b/tests/ParserTest.php index dc45fd66d30..0602d8d0f77 100644 --- a/tests/ParserTest.php +++ b/tests/ParserTest.php @@ -16,7 +16,9 @@ use Twig\Error\SyntaxError; use Twig\Lexer; use Twig\Loader\ArrayLoader; +use Twig\Node\EmptyNode; use Twig\Node\Node; +use Twig\Node\Nodes; use Twig\Node\SetNode; use Twig\Node\TextNode; use Twig\Parser; @@ -75,15 +77,15 @@ public static function getFilterBodyNodesData() { return [ [ - new Node([new TextNode(' ', 1)]), - new Node([]), + new Nodes([new TextNode(' ', 1)]), + new Nodes([]), ], [ - $input = new Node([new SetNode(false, new Node(), new Node(), 1)]), + $input = new Nodes([new SetNode(false, new EmptyNode(), new EmptyNode(), 1)]), $input, ], [ - $input = new Node([new SetNode(true, new Node(), new Node([new Node([new TextNode('foo', 1)])]), 1)]), + $input = new Nodes([new SetNode(true, new EmptyNode(), new Nodes([new Nodes([new TextNode('foo', 1)])]), 1)]), $input, ], ]; @@ -107,7 +109,7 @@ public static function getFilterBodyNodesDataThrowsException() { return [ [new TextNode('foo', 1)], - [new Node([new Node([new TextNode('foo', 1)])])], + [new Nodes([new Nodes([new TextNode('foo', 1)])])], ]; } @@ -203,7 +205,7 @@ public function testImplicitMacroArgumentDefaultValues() protected function getParser() { $parser = new Parser(new Environment(new ArrayLoader())); - $parser->setParent(new Node()); + $parser->setParent(new EmptyNode()); $p = new \ReflectionProperty($parser, 'stream'); $p->setAccessible(true); @@ -228,7 +230,7 @@ public function parse(Token $token): Node $this->parser->getStream()->expect(Token::BLOCK_END_TYPE); - return new Node([], [], 1); + return new EmptyNode(1); } public function getTag(): string diff --git a/tests/Util/CallableArgumentsExtractorTest.php b/tests/Util/CallableArgumentsExtractorTest.php index 182283183ad..a06e19aa9a1 100644 --- a/tests/Util/CallableArgumentsExtractorTest.php +++ b/tests/Util/CallableArgumentsExtractorTest.php @@ -14,10 +14,11 @@ use PHPUnit\Framework\TestCase; use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; use Twig\Error\SyntaxError; +use Twig\Node\EmptyNode; use Twig\Node\Expression\ConstantExpression; use Twig\Node\Expression\FunctionExpression; use Twig\Node\Expression\VariadicExpression; -use Twig\Node\Node; +use Twig\Node\Nodes; use Twig\Source; use Twig\TwigFunction; use Twig\Util\CallableArgumentsExtractor; @@ -183,13 +184,13 @@ public function customFunctionWithArbitraryArguments() private function getArguments(string $name, $callable, array $args, bool $isVariadic = false): array { $function = new TwigFunction($name, $callable, ['is_variadic' => $isVariadic]); - $node = new ExpressionCall($function, new Node([]), 2); + $node = new ExpressionCall($function, new EmptyNode(), 2); $node->setSourceContext(new Source('', 'test.twig')); foreach ($args as $name => $arg) { $args[$name] = new ConstantExpression($arg, 0); } - $arguments = (new CallableArgumentsExtractor($node, $function))->extractArguments(new Node($args)); + $arguments = (new CallableArgumentsExtractor($node, $function))->extractArguments(new Nodes($args)); foreach ($arguments as $name => $argument) { $arguments[$name] = $isVariadic ? $argument : $argument->getAttribute('value'); } From cff6f1ca80603f19fc748996369c8b466db571e9 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Thu, 26 Sep 2024 17:30:58 +0200 Subject: [PATCH 508/812] Implement the `enum` function --- CHANGELOG | 1 + doc/functions/enum.rst | 26 +++++++++++ doc/functions/index.rst | 1 + src/Extension/CoreExtension.php | 26 +++++++++++ .../Expression/FunctionNode/EnumFunction.php | 45 +++++++++++++++++++ .../functions/enum/invalid_dynamic_enum.test | 13 ++++++ .../Fixtures/functions/enum/invalid_enum.test | 10 +++++ .../functions/enum/invalid_enum_escaping.test | 10 +++++ .../functions/enum/invalid_literal_type.test | 10 +++++ tests/Fixtures/functions/enum/valid.test | 30 +++++++++++++ 10 files changed, 172 insertions(+) create mode 100644 doc/functions/enum.rst create mode 100644 src/Node/Expression/FunctionNode/EnumFunction.php create mode 100644 tests/Fixtures/functions/enum/invalid_dynamic_enum.test create mode 100644 tests/Fixtures/functions/enum/invalid_enum.test create mode 100644 tests/Fixtures/functions/enum/invalid_enum_escaping.test create mode 100644 tests/Fixtures/functions/enum/invalid_literal_type.test create mode 100644 tests/Fixtures/functions/enum/valid.test diff --git a/CHANGELOG b/CHANGELOG index e1d01be3c41..f2311502a4a 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -15,6 +15,7 @@ * Support Markup instances (and any other \Stringable) as dynamic mapping keys * Deprecate the `sandbox` tag * Improve the way one can deprecate a Twig callable (use `deprecation_info` instead of the other callable options) + * Add the `enum` function # 3.14.0 (2024-09-09) diff --git a/doc/functions/enum.rst b/doc/functions/enum.rst new file mode 100644 index 00000000000..3c9d1a7b498 --- /dev/null +++ b/doc/functions/enum.rst @@ -0,0 +1,26 @@ +``enum_cases`` +============== + +.. versionadded:: 3.15 + + The ``enum`` function was added in Twig 3.15. + +``enum`` gives access to enums: + +.. code-block:: twig + + {# display one specific case of a backed enum #} + {{ enum('App\\MyEnum').SomeCase.value }} + + {# get all cases of an enum #} + {% enum('App\\MyEnum').cases() %} + + {# call any methods of the enum class #} + {% enum('App\\MyEnum').someMethod() %} + +When using a string literal for the ``enum`` argument, it will be validated during compile time to be a valid enum name. + +Arguments +--------- + +* ``enum``: The FQCN of the enum diff --git a/doc/functions/index.rst b/doc/functions/index.rst index 27fd2438352..557f6938a30 100644 --- a/doc/functions/index.rst +++ b/doc/functions/index.rst @@ -10,6 +10,7 @@ Functions cycle date dump + enum enum_cases html_classes html_cva diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index 02613c5cdcf..66f08b5d142 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -50,6 +50,7 @@ use Twig\Node\Expression\BlockReferenceExpression; use Twig\Node\Expression\Filter\DefaultFilter; use Twig\Node\Expression\FunctionNode\EnumCasesFunction; +use Twig\Node\Expression\FunctionNode\EnumFunction; use Twig\Node\Expression\GetAttrExpression; use Twig\Node\Expression\NullCoalesceExpression; use Twig\Node\Expression\ParentExpression; @@ -264,6 +265,7 @@ public function getFunctions(): array new TwigFunction('include', [self::class, 'include'], ['needs_environment' => true, 'needs_context' => true, 'is_safe' => ['all']]), new TwigFunction('source', [self::class, 'source'], ['needs_environment' => true, 'is_safe' => ['all']]), new TwigFunction('enum_cases', [self::class, 'enumCases'], ['node_class' => EnumCasesFunction::class]), + new TwigFunction('enum', [self::class, 'enum'], ['node_class' => EnumFunction::class]), ]; } @@ -1501,6 +1503,30 @@ public static function enumCases(string $enum): array return $enum::cases(); } + /** + * Provides the ability to access enums by their class names. + * + * @template T of \UnitEnum + * + * @param class-string $enum + * + * @return T + * + * @internal + */ + public static function enum(string $enum): \UnitEnum + { + if (!enum_exists($enum)) { + throw new RuntimeError(sprintf('"%s" is not an enum.', $enum)); + } + + if (!$cases = $enum::cases()) { + throw new RuntimeError(sprintf('"%s" is an empty enum.', $enum)); + } + + return $cases[0]; + } + /** * Provides the ability to get constants from instances as well as class/global constants. * diff --git a/src/Node/Expression/FunctionNode/EnumFunction.php b/src/Node/Expression/FunctionNode/EnumFunction.php new file mode 100644 index 00000000000..1f8b0ecf1ff --- /dev/null +++ b/src/Node/Expression/FunctionNode/EnumFunction.php @@ -0,0 +1,45 @@ +getNode('arguments'); + if ($arguments->hasNode('enum')) { + $firstArgument = $arguments->getNode('enum'); + } elseif ($arguments->hasNode('0')) { + $firstArgument = $arguments->getNode('0'); + } else { + $firstArgument = null; + } + + if (!$firstArgument instanceof ConstantExpression || 1 !== \count($arguments)) { + parent::compile($compiler); + + return; + } + + $value = $firstArgument->getAttribute('value'); + + if (!\is_string($value)) { + throw new SyntaxError('The first argument of the "enum" function must be a string.', $this->getTemplateLine(), $this->getSourceContext()); + } + + if (!enum_exists($value)) { + throw new SyntaxError(\sprintf('The first argument of the "enum" function must be the name of an enum, "%s" given.', $value), $this->getTemplateLine(), $this->getSourceContext()); + } + + if (!$cases = $value::cases()) { + throw new SyntaxError(\sprintf('The first argument of the "enum" function must be a non-empty enum, "%s" given.', $value), $this->getTemplateLine(), $this->getSourceContext()); + } + + $compiler->raw(\sprintf('%s::%s', $value, $cases[0]->name)); + } +} diff --git a/tests/Fixtures/functions/enum/invalid_dynamic_enum.test b/tests/Fixtures/functions/enum/invalid_dynamic_enum.test new file mode 100644 index 00000000000..1ae27ffe566 --- /dev/null +++ b/tests/Fixtures/functions/enum/invalid_dynamic_enum.test @@ -0,0 +1,13 @@ +--TEST-- +"enum" function with invalid dynamic enum class +--CONDITION-- +\PHP_VERSION_ID >= 80100 +--TEMPLATE-- +{% set from_variable = 'Twig\\Tests\\NonExistentEnum' %} +{% for c in enum(from_variable).cases() %} + {{~ c.name }} +{% endfor %} +--DATA-- +return [] +--EXCEPTION-- +Twig\Error\RuntimeError: "Twig\Tests\NonExistentEnum" is not an enum in "index.twig" at line 3. diff --git a/tests/Fixtures/functions/enum/invalid_enum.test b/tests/Fixtures/functions/enum/invalid_enum.test new file mode 100644 index 00000000000..b38e7fc9ad1 --- /dev/null +++ b/tests/Fixtures/functions/enum/invalid_enum.test @@ -0,0 +1,10 @@ +--TEST-- +"enum" function with invalid enum class +--CONDITION-- +\PHP_VERSION_ID >= 80100 +--TEMPLATE-- +{% for c in enum('Twig\\Tests\\NonExistentEnum').cases() %} + {{~ c.name }} +{% endfor %} +--EXCEPTION-- +Twig\Error\SyntaxError: The first argument of the "enum" function must be the name of an enum, "Twig\Tests\NonExistentEnum" given in "index.twig" at line 2. diff --git a/tests/Fixtures/functions/enum/invalid_enum_escaping.test b/tests/Fixtures/functions/enum/invalid_enum_escaping.test new file mode 100644 index 00000000000..5c10afb10e7 --- /dev/null +++ b/tests/Fixtures/functions/enum/invalid_enum_escaping.test @@ -0,0 +1,10 @@ +--TEST-- +"enum" function with missing \ escaping +--CONDITION-- +\PHP_VERSION_ID >= 80100 +--TEMPLATE-- +{% for c in enum('Twig\Tests\DummyBackedEnum').cases() %} + {{~ c.name }} +{% endfor %} +--EXCEPTION-- +Twig\Error\SyntaxError: The first argument of the "enum" function must be the name of an enum, "TwigTestsDummyBackedEnum" given in "index.twig" at line 2. diff --git a/tests/Fixtures/functions/enum/invalid_literal_type.test b/tests/Fixtures/functions/enum/invalid_literal_type.test new file mode 100644 index 00000000000..9b79dec0625 --- /dev/null +++ b/tests/Fixtures/functions/enum/invalid_literal_type.test @@ -0,0 +1,10 @@ +--TEST-- +"enum" function with invalid literal type +--CONDITION-- +\PHP_VERSION_ID >= 80100 +--TEMPLATE-- +{% for c in enum(13).cases() %} + {{~ c.name }} +{% endfor %} +--EXCEPTION-- +Twig\Error\SyntaxError: The first argument of the "enum" function must be a string in "index.twig" at line 2. diff --git a/tests/Fixtures/functions/enum/valid.test b/tests/Fixtures/functions/enum/valid.test new file mode 100644 index 00000000000..8eb1a510feb --- /dev/null +++ b/tests/Fixtures/functions/enum/valid.test @@ -0,0 +1,30 @@ +--TEST-- +"enum" function +--CONDITION-- +\PHP_VERSION_ID >= 80100 +--TEMPLATE-- +{{ enum('Twig\\Tests\\DummyBackedEnum').FOO.value }} +{% for c in enum('Twig\\Tests\\DummyBackedEnum').cases() %} + {{~ c.name }}: {{ c.value }} +{% endfor %} +{{ enum('Twig\\Tests\\DummyUnitEnum').BAR.name }} +{% for c in enum('Twig\\Tests\\DummyUnitEnum').cases() %} + {{~ c.name }} +{% endfor %} +{% set from_variable='Twig\\Tests\\DummyUnitEnum' %} +{{ enum(from_variable).BAR.name }} +{% for c in enum(from_variable).cases() %} + {{~ c.name }} +{% endfor %} +--DATA-- +return [] +--EXPECT-- +foo +FOO: foo +BAR: bar +BAR +BAR +BAZ +BAR +BAR +BAZ From bd33033fd18761be616f52757834cc0a1c32acab Mon Sep 17 00:00:00 2001 From: Thomas Landauer Date: Fri, 27 Sep 2024 10:46:21 +0200 Subject: [PATCH 509/812] Update plural.rst: Fixing filter name Info is taken from https://github.com/twigphp/Twig/pull/4357#issuecomment-2378435199 Besides, I changed to a more illustrative example word. One of the rare cases where ChatGPT was indeed useful... ;-) --- doc/filters/plural.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/doc/filters/plural.rst b/doc/filters/plural.rst index 50c18cdc01a..7703452196e 100644 --- a/doc/filters/plural.rst +++ b/doc/filters/plural.rst @@ -11,11 +11,11 @@ plural version: .. code-block:: twig {# English (en) rules are used by default #} - {{ 'partition'|pluralize() }} - partitions + {{ 'animal'|plural() }} + animals - {{ 'partition'|pluralize('fr') }} - partitions + {{ 'animal'|plural('fr') }} + animaux .. note:: From fae6cfc32f3867fdfd98ff7def9c8ffb6df4dbd4 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sat, 28 Sep 2024 09:21:06 +0200 Subject: [PATCH 510/812] Add more information about the filter and the negative operators --- doc/templates.rst | 39 +++++++++++++++++++++++++++++++++++---- 1 file changed, 35 insertions(+), 4 deletions(-) diff --git a/doc/templates.rst b/doc/templates.rst index 0cd9092253c..b87ee1ed7f9 100644 --- a/doc/templates.rst +++ b/doc/templates.rst @@ -122,9 +122,9 @@ You can assign values to variables inside code blocks. Assignments use the Filters ------- -Variables can be modified by **filters**. Filters are separated from the -variable by a pipe symbol (``|``). Multiple filters can be chained. The output -of one filter is applied to the next. +Variables and expressions can be modified by **filters**. Filters are separated +from the variable by a pipe symbol (``|``). Multiple filters can be chained. +The output of one filter is applied to the next. The following example removes all HTML tags from the ``name`` and title-cases it: @@ -152,6 +152,37 @@ To apply a filter on a section of code, wrap it with the Go to the :doc:`filters` page to learn more about built-in filters. +.. warning:: + + As the ``filter`` operator has the highest :ref:`precedence + `, use parentheses when filtering more "complex" + expressions: + + .. code-block:: twig + + {{ (1..5)|join(', ') }} + + {{ ('HELLO' ~ 'FABIEN')|lower }} + + A common mistake is to forget using parentheses for filters on negative + numbers as a negative number in Twig is represented by the ``-`` operator + followed by a positive number. As the ``-`` operator has a lower precedence + than the filter operator, it leads to confusion: + + .. code-block:: twig + + {{ -1|abs }} {# returns -1 #} + + {# as it is equivalent to #} + + {{ -(1|abs) }} + + For such cases, use parentheses to force the precedence: + + .. code-block:: twig + + {{ (-1)|abs }} {# returns 1 as expected #} + Functions --------- @@ -795,7 +826,7 @@ The following operators don't fit into any of the other categories: .. code-block:: twig - (1..5)|join(', ') + {{ (1..5)|join(', ') }} * ``~``: Converts all operands into strings and concatenates them. ``{{ "Hello " ~ name ~ "!" }}`` would return (assuming ``name`` is ``'John'``) ``Hello From 871639260384ac0c8f4e32154839dc56863e0816 Mon Sep 17 00:00:00 2001 From: unknown Date: Thu, 25 Jan 2024 12:26:39 +0100 Subject: [PATCH 511/812] Added details in the doc of filters and functions of IntlExtension --- doc/filters/country_name.rst | 12 +++++++++++- doc/filters/currency_name.rst | 7 +++++-- doc/filters/currency_symbol.rst | 7 +++++-- doc/filters/format_currency.rst | 5 ++++- doc/filters/format_date.rst | 5 ++++- doc/filters/format_datetime.rst | 4 +++- doc/filters/format_number.rst | 5 ++++- doc/filters/format_time.rst | 5 ++++- doc/filters/language_name.rst | 9 ++++++--- doc/filters/locale_name.rst | 8 +++++--- doc/filters/slug.rst | 5 ++++- doc/filters/timezone_name.rst | 7 +++++-- doc/functions/country_names.rst | 5 ++++- doc/functions/country_timezones.rst | 9 ++++++++- doc/functions/currency_names.rst | 5 ++++- doc/functions/language_names.rst | 5 ++++- doc/functions/locale_names.rst | 5 ++++- doc/functions/script_names.rst | 5 ++++- doc/functions/timezone_names.rst | 5 ++++- 19 files changed, 92 insertions(+), 26 deletions(-) diff --git a/doc/filters/country_name.rst b/doc/filters/country_name.rst index 434b0bda7a1..394e9f5b766 100644 --- a/doc/filters/country_name.rst +++ b/doc/filters/country_name.rst @@ -16,6 +16,13 @@ By default, the filter uses the current locale. You can pass it explicitly: {# États-Unis #} {{ 'US'|country_name('fr') }} +The locale can contain more than two letters depending on the region: + +.. code-block:: twig + + {# 美國 #} + {{ 'US'|country_name('zh_Hant_HK') }} + .. note:: The ``country_name`` filter is part of the ``IntlExtension`` which is not @@ -41,4 +48,7 @@ By default, the filter uses the current locale. You can pass it explicitly: Arguments --------- -* ``locale``: The locale +* ``locale``: The locale code as defined in `RFC 5646`_. They are also documented in the `PHP Locale class`_. + +.. _`RFC 5646`: https://www.rfc-editor.org/info/rfc5646 +.. _`PHP Locale class`: https://www.php.net/manual/en/class.locale.php \ No newline at end of file diff --git a/doc/filters/currency_name.rst b/doc/filters/currency_name.rst index a35c499988d..9c8aba2a2ad 100644 --- a/doc/filters/currency_name.rst +++ b/doc/filters/currency_name.rst @@ -1,7 +1,7 @@ ``currency_name`` ================= -The ``currency_name`` filter returns the currency name given its three-letter +The ``currency_name`` filter returns the currency name given its ISO 4217 three-letter code: .. code-block:: twig @@ -44,4 +44,7 @@ By default, the filter uses the current locale. You can pass it explicitly: Arguments --------- -* ``locale``: The locale +* ``locale``: The locale code as defined in `RFC 5646`_. They are also documented in the `PHP Locale class`_. + +.. _`RFC 5646`: https://www.rfc-editor.org/info/rfc5646 +.. _`PHP Locale class`: https://www.php.net/manual/en/class.locale.php diff --git a/doc/filters/currency_symbol.rst b/doc/filters/currency_symbol.rst index 84a048ed52c..716a7f3104f 100644 --- a/doc/filters/currency_symbol.rst +++ b/doc/filters/currency_symbol.rst @@ -1,7 +1,7 @@ ``currency_symbol`` =================== -The ``currency_symbol`` filter returns the currency symbol given its three-letter +The ``currency_symbol`` filter returns the currency symbol given its ISO 4217 three-letter code: .. code-block:: twig @@ -44,4 +44,7 @@ By default, the filter uses the current locale. You can pass it explicitly: Arguments --------- -* ``locale``: The locale +* ``locale``: The locale code as defined in `RFC 5646`_. They are also documented in the `PHP Locale class`_. + +.. _`RFC 5646`: https://www.rfc-editor.org/info/rfc5646 +.. _`PHP Locale class`: https://www.php.net/manual/en/class.locale.php diff --git a/doc/filters/format_currency.rst b/doc/filters/format_currency.rst index 8b649bf5d94..6c140f951ae 100644 --- a/doc/filters/format_currency.rst +++ b/doc/filters/format_currency.rst @@ -74,4 +74,7 @@ Arguments * ``currency``: The currency * ``attrs``: A map of attributes -* ``locale``: The locale +* ``locale``: The locale code as defined in `RFC 5646`_. They are also documented in the `PHP Locale class`_. + +.. _`RFC 5646`: https://www.rfc-editor.org/info/rfc5646 +.. _`PHP Locale class`: https://www.php.net/manual/en/class.locale.php diff --git a/doc/filters/format_date.rst b/doc/filters/format_date.rst index cd6beba9f5b..c36748eccfc 100644 --- a/doc/filters/format_date.rst +++ b/doc/filters/format_date.rst @@ -29,8 +29,11 @@ the :doc:`format_datetime` filter, but without the time. Arguments --------- -* ``locale``: The locale +* ``locale``: The locale code as defined in `RFC 5646`_. They are also documented in the `PHP Locale class`_. * ``dateFormat``: The date format * ``pattern``: A date time pattern * ``timezone``: The date timezone * ``calendar``: The calendar ("gregorian" by default) + +.. _`RFC 5646`: https://www.rfc-editor.org/info/rfc5646 +.. _`PHP Locale class`: https://www.php.net/manual/en/class.locale.php \ No newline at end of file diff --git a/doc/filters/format_datetime.rst b/doc/filters/format_datetime.rst index 8f3b46d479a..085af1294b3 100644 --- a/doc/filters/format_datetime.rst +++ b/doc/filters/format_datetime.rst @@ -98,7 +98,7 @@ The default timezone can also be set globally by calling ``setTimezone()``:: Arguments --------- -* ``locale``: The locale +* ``locale``: The locale code as defined in `RFC 5646`_. They are also documented in the `PHP Locale class`_. * ``dateFormat``: The date format * ``timeFormat``: The time format * ``pattern``: A date time pattern @@ -106,3 +106,5 @@ Arguments * ``calendar``: The calendar ("gregorian" by default) .. _ICU user guide: https://unicode-org.github.io/icu/userguide/format_parse/datetime/#datetime-format-syntax +.. _`RFC 5646`: https://www.rfc-editor.org/info/rfc5646 +.. _`PHP Locale class`: https://www.php.net/manual/en/class.locale.php \ No newline at end of file diff --git a/doc/filters/format_number.rst b/doc/filters/format_number.rst index a1c2804ab4b..301b1034803 100644 --- a/doc/filters/format_number.rst +++ b/doc/filters/format_number.rst @@ -112,6 +112,9 @@ By default, the filter uses the current locale. You can pass it explicitly: Arguments --------- -* ``locale``: The locale +* ``locale``: The locale code as defined in `RFC 5646`_. They are also documented in the `PHP Locale class`_. * ``attrs``: A map of attributes * ``style``: The style of the number output + +.. _`RFC 5646`: https://www.rfc-editor.org/info/rfc5646 +.. _`PHP Locale class`: https://www.php.net/manual/en/class.locale.php \ No newline at end of file diff --git a/doc/filters/format_time.rst b/doc/filters/format_time.rst index 1e213e6163b..25710e5ce23 100644 --- a/doc/filters/format_time.rst +++ b/doc/filters/format_time.rst @@ -29,8 +29,11 @@ the :doc:`format_datetime` filter, but without the date. Arguments --------- -* ``locale``: The locale +* ``locale``: The locale code as defined in `RFC 5646`_. They are also documented in the `PHP Locale class`_. * ``timeFormat``: The time format * ``pattern``: A date time pattern * ``timezone``: The date timezone * ``calendar``: The calendar ("gregorian" by default) + +.. _`RFC 5646`: https://www.rfc-editor.org/info/rfc5646 +.. _`PHP Locale class`: https://www.php.net/manual/en/class.locale.php \ No newline at end of file diff --git a/doc/filters/language_name.rst b/doc/filters/language_name.rst index 55c2439207a..96295950ec4 100644 --- a/doc/filters/language_name.rst +++ b/doc/filters/language_name.rst @@ -1,8 +1,8 @@ ``language_name`` ================= -The ``language_name`` filter returns the language name given its two-letter -code: +The ``language_name`` filter returns the language name based on its two-letter code (ISO 639-1), +three-letter code (ISO 639-2) or other specific localized code: .. code-block:: twig @@ -44,4 +44,7 @@ By default, the filter uses the current locale. You can pass it explicitly: Arguments --------- -* ``locale``: The locale +* ``locale``: The locale code as defined in `RFC 5646`_. They are also documented in the `PHP Locale class`_. + +.. _`RFC 5646`: https://www.rfc-editor.org/info/rfc5646 +.. _`PHP Locale class`: https://www.php.net/manual/en/class.locale.php diff --git a/doc/filters/locale_name.rst b/doc/filters/locale_name.rst index c6d34cbc019..dd835055c25 100644 --- a/doc/filters/locale_name.rst +++ b/doc/filters/locale_name.rst @@ -1,8 +1,7 @@ ``locale_name`` =============== -The ``locale_name`` filter returns the locale name given its two-letter -code: +The ``locale_name`` filter returns the locale name given its code: .. code-block:: twig @@ -44,4 +43,7 @@ By default, the filter uses the current locale. You can pass it explicitly: Arguments --------- -* ``locale``: The locale +* ``locale``: The locale code as defined in `RFC 5646`_. They are also documented in the `PHP Locale class`_. + +.. _`RFC 5646`: https://www.rfc-editor.org/info/rfc5646 +.. _`PHP Locale class`: https://www.php.net/manual/en/class.locale.php diff --git a/doc/filters/slug.rst b/doc/filters/slug.rst index 773a42fac26..57085f767bf 100644 --- a/doc/filters/slug.rst +++ b/doc/filters/slug.rst @@ -56,4 +56,7 @@ Arguments --------- * ``separator``: The separator that is used to join words (defaults to ``-``) -* ``locale``: The locale of the original string (if none is specified, it will be automatically detected) +* ``locale``: The locale code of the original string as defined in `RFC 5646`_ (if none is specified, it will be automatically detected). They are also documented in the `PHP Locale class`_. + +.. _`RFC 5646`: https://www.rfc-editor.org/info/rfc5646 +.. _`PHP Locale class`: https://www.php.net/manual/en/class.locale.php \ No newline at end of file diff --git a/doc/filters/timezone_name.rst b/doc/filters/timezone_name.rst index dfb22818bad..1391ffe077b 100644 --- a/doc/filters/timezone_name.rst +++ b/doc/filters/timezone_name.rst @@ -1,7 +1,7 @@ ``timezone_name`` ================= -The ``timezone_name`` filter returns the timezone name given a timezone identifier: +The ``timezone_name`` filter returns the timezone name given its ISO 8601 timezone identifier: .. code-block:: twig @@ -43,4 +43,7 @@ By default, the filter uses the current locale. You can pass it explicitly: Arguments --------- -* ``locale``: The locale +* ``locale``: The locale code as defined in `RFC 5646`_. They are also documented in the `PHP Locale class`_. + +.. _`RFC 5646`: https://www.rfc-editor.org/info/rfc5646 +.. _`PHP Locale class`: https://www.php.net/manual/en/class.locale.php diff --git a/doc/functions/country_names.rst b/doc/functions/country_names.rst index 692137b0431..fe03ba522f3 100644 --- a/doc/functions/country_names.rst +++ b/doc/functions/country_names.rst @@ -44,4 +44,7 @@ By default, the function uses the current locale. You can pass it explicitly: Arguments --------- -* ``locale``: The locale +* ``locale``: The locale code as defined in `RFC 5646`_. They are also documented in the `PHP Locale class`_. + +.. _`RFC 5646`: https://www.rfc-editor.org/info/rfc5646 +.. _`PHP Locale class`: https://www.php.net/manual/en/class.locale.php diff --git a/doc/functions/country_timezones.rst b/doc/functions/country_timezones.rst index ecbbc1c9941..370a4befff3 100644 --- a/doc/functions/country_timezones.rst +++ b/doc/functions/country_timezones.rst @@ -2,13 +2,15 @@ ===================== The ``country_timezones`` function returns the names of the timezones associated -with a given country code: +with a given country its ISO-3166 code: .. code-block:: twig {# Europe/Paris #} {{ country_timezones('FR')|join(', ') }} +If the specified country were to be unknown, it will return an empty array + .. note:: The ``country_timezones`` function is part of the ``IntlExtension`` which is not @@ -30,3 +32,8 @@ with a given country code: $twig = new \Twig\Environment(...); $twig->addExtension(new IntlExtension()); + +Arguments +--------- + +* ``country``: The country code \ No newline at end of file diff --git a/doc/functions/currency_names.rst b/doc/functions/currency_names.rst index dfb446c8650..8df215eb2f9 100644 --- a/doc/functions/currency_names.rst +++ b/doc/functions/currency_names.rst @@ -44,4 +44,7 @@ By default, the function uses the current locale. You can pass it explicitly: Arguments --------- -* ``locale``: The locale +* ``locale``: The locale code as defined in `RFC 5646`_. They are also documented in the `PHP Locale class`_. + +.. _`RFC 5646`: https://www.rfc-editor.org/info/rfc5646 +.. _`PHP Locale class`: https://www.php.net/manual/en/class.locale.php diff --git a/doc/functions/language_names.rst b/doc/functions/language_names.rst index f1cce488a73..dd9b8930f23 100644 --- a/doc/functions/language_names.rst +++ b/doc/functions/language_names.rst @@ -44,4 +44,7 @@ By default, the function uses the current locale. You can pass it explicitly: Arguments --------- -* ``locale``: The locale +* ``locale``: The locale code as defined in `RFC 5646`_. They are also documented in the `PHP Locale class`_. + +.. _`RFC 5646`: https://www.rfc-editor.org/info/rfc5646 +.. _`PHP Locale class`: https://www.php.net/manual/en/class.locale.php diff --git a/doc/functions/locale_names.rst b/doc/functions/locale_names.rst index 320ab672470..3a8b71fe4d2 100644 --- a/doc/functions/locale_names.rst +++ b/doc/functions/locale_names.rst @@ -44,4 +44,7 @@ By default, the function uses the current locale. You can pass it explicitly: Arguments --------- -* ``locale``: The locale +* ``locale``: The locale code as defined in `RFC 5646`_. They are also documented in the `PHP Locale class`_. + +.. _`RFC 5646`: https://www.rfc-editor.org/info/rfc5646 +.. _`PHP Locale class`: https://www.php.net/manual/en/class.locale.php diff --git a/doc/functions/script_names.rst b/doc/functions/script_names.rst index 79b20c65fe2..963526f66cd 100644 --- a/doc/functions/script_names.rst +++ b/doc/functions/script_names.rst @@ -44,4 +44,7 @@ By default, the function uses the current locale. You can pass it explicitly: Arguments --------- -* ``locale``: The locale +* ``locale``: The locale code as defined in `RFC 5646`_. They are also documented in the `PHP Locale class`_. + +.. _`RFC 5646`: https://www.rfc-editor.org/info/rfc5646 +.. _`PHP Locale class`: https://www.php.net/manual/en/class.locale.php diff --git a/doc/functions/timezone_names.rst b/doc/functions/timezone_names.rst index 69db196fddd..5aa76991073 100644 --- a/doc/functions/timezone_names.rst +++ b/doc/functions/timezone_names.rst @@ -44,4 +44,7 @@ By default, the function uses the current locale. You can pass it explicitly: Arguments --------- -* ``locale``: The locale +* ``locale``: The locale code as defined in `RFC 5646`_. They are also documented in the `PHP Locale class`_. + +.. _`RFC 5646`: https://www.rfc-editor.org/info/rfc5646 +.. _`PHP Locale class`: https://www.php.net/manual/en/class.locale.php From 1d31ac4b77f4c63e2d93aa9aea6af2b486b65de6 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sat, 28 Sep 2024 09:51:56 +0200 Subject: [PATCH 512/812] Tweak previous merge --- doc/filters/country_name.rst | 12 +++--------- doc/filters/currency_name.rst | 8 +++----- doc/filters/currency_symbol.rst | 7 +++---- doc/filters/format_currency.rst | 5 ++--- doc/filters/format_date.rst | 5 ++--- doc/filters/format_datetime.rst | 5 ++--- doc/filters/format_number.rst | 5 ++--- doc/filters/format_time.rst | 5 ++--- doc/filters/language_name.rst | 9 ++++----- doc/filters/locale_name.rst | 5 ++--- doc/filters/slug.rst | 5 ++--- doc/filters/timezone_name.rst | 5 ++--- doc/functions/country_names.rst | 5 ++--- doc/functions/currency_names.rst | 5 ++--- doc/functions/language_names.rst | 5 ++--- doc/functions/locale_names.rst | 5 ++--- doc/functions/script_names.rst | 5 ++--- doc/functions/timezone_names.rst | 5 ++--- 18 files changed, 41 insertions(+), 65 deletions(-) diff --git a/doc/filters/country_name.rst b/doc/filters/country_name.rst index 394e9f5b766..a30184de9da 100644 --- a/doc/filters/country_name.rst +++ b/doc/filters/country_name.rst @@ -1,8 +1,7 @@ ``country_name`` ================ -The ``country_name`` filter returns the country name given its ISO-3166 -two-letter code: +The ``country_name`` filter returns the country name given its ISO-3166 code: .. code-block:: twig @@ -16,10 +15,6 @@ By default, the filter uses the current locale. You can pass it explicitly: {# États-Unis #} {{ 'US'|country_name('fr') }} -The locale can contain more than two letters depending on the region: - -.. code-block:: twig - {# 美國 #} {{ 'US'|country_name('zh_Hant_HK') }} @@ -48,7 +43,6 @@ The locale can contain more than two letters depending on the region: Arguments --------- -* ``locale``: The locale code as defined in `RFC 5646`_. They are also documented in the `PHP Locale class`_. +* ``locale``: The locale code as defined in `RFC 5646`_ -.. _`RFC 5646`: https://www.rfc-editor.org/info/rfc5646 -.. _`PHP Locale class`: https://www.php.net/manual/en/class.locale.php \ No newline at end of file +.. _RFC 5646: https://www.rfc-editor.org/info/rfc5646 diff --git a/doc/filters/currency_name.rst b/doc/filters/currency_name.rst index 9c8aba2a2ad..498dc9423d7 100644 --- a/doc/filters/currency_name.rst +++ b/doc/filters/currency_name.rst @@ -1,8 +1,7 @@ ``currency_name`` ================= -The ``currency_name`` filter returns the currency name given its ISO 4217 three-letter -code: +The ``currency_name`` filter returns the currency name given its ISO 4217 code: .. code-block:: twig @@ -44,7 +43,6 @@ By default, the filter uses the current locale. You can pass it explicitly: Arguments --------- -* ``locale``: The locale code as defined in `RFC 5646`_. They are also documented in the `PHP Locale class`_. +* ``locale``: The locale code as defined in `RFC 5646`_ -.. _`RFC 5646`: https://www.rfc-editor.org/info/rfc5646 -.. _`PHP Locale class`: https://www.php.net/manual/en/class.locale.php +.. _RFC 5646: https://www.rfc-editor.org/info/rfc5646 diff --git a/doc/filters/currency_symbol.rst b/doc/filters/currency_symbol.rst index 716a7f3104f..80843fba023 100644 --- a/doc/filters/currency_symbol.rst +++ b/doc/filters/currency_symbol.rst @@ -1,7 +1,7 @@ ``currency_symbol`` =================== -The ``currency_symbol`` filter returns the currency symbol given its ISO 4217 three-letter +The ``currency_symbol`` filter returns the currency symbol given its ISO 4217 code: .. code-block:: twig @@ -44,7 +44,6 @@ By default, the filter uses the current locale. You can pass it explicitly: Arguments --------- -* ``locale``: The locale code as defined in `RFC 5646`_. They are also documented in the `PHP Locale class`_. +* ``locale``: The locale code as defined in `RFC 5646`_ -.. _`RFC 5646`: https://www.rfc-editor.org/info/rfc5646 -.. _`PHP Locale class`: https://www.php.net/manual/en/class.locale.php +.. _RFC 5646: https://www.rfc-editor.org/info/rfc5646 diff --git a/doc/filters/format_currency.rst b/doc/filters/format_currency.rst index 702409ee98b..6f1036d33a5 100644 --- a/doc/filters/format_currency.rst +++ b/doc/filters/format_currency.rst @@ -74,7 +74,6 @@ Arguments * ``currency``: The currency * ``attrs``: A map of attributes -* ``locale``: The locale code as defined in `RFC 5646`_. They are also documented in the `PHP Locale class`_. +* ``locale``: The locale code as defined in `RFC 5646`_ -.. _`RFC 5646`: https://www.rfc-editor.org/info/rfc5646 -.. _`PHP Locale class`: https://www.php.net/manual/en/class.locale.php +.. _RFC 5646: https://www.rfc-editor.org/info/rfc5646 diff --git a/doc/filters/format_date.rst b/doc/filters/format_date.rst index c36748eccfc..ab0d609df50 100644 --- a/doc/filters/format_date.rst +++ b/doc/filters/format_date.rst @@ -29,11 +29,10 @@ the :doc:`format_datetime` filter, but without the time. Arguments --------- -* ``locale``: The locale code as defined in `RFC 5646`_. They are also documented in the `PHP Locale class`_. +* ``locale``: The locale code as defined in `RFC 5646`_ * ``dateFormat``: The date format * ``pattern``: A date time pattern * ``timezone``: The date timezone * ``calendar``: The calendar ("gregorian" by default) -.. _`RFC 5646`: https://www.rfc-editor.org/info/rfc5646 -.. _`PHP Locale class`: https://www.php.net/manual/en/class.locale.php \ No newline at end of file +.. _RFC 5646: https://www.rfc-editor.org/info/rfc5646 diff --git a/doc/filters/format_datetime.rst b/doc/filters/format_datetime.rst index 4420a8f2ccb..5f3a49b0c16 100644 --- a/doc/filters/format_datetime.rst +++ b/doc/filters/format_datetime.rst @@ -98,7 +98,7 @@ The default timezone can also be set globally by calling ``setTimezone()``:: Arguments --------- -* ``locale``: The locale code as defined in `RFC 5646`_. They are also documented in the `PHP Locale class`_. +* ``locale``: The locale code as defined in `RFC 5646`_ * ``dateFormat``: The date format * ``timeFormat``: The time format * ``pattern``: A date time pattern @@ -106,5 +106,4 @@ Arguments * ``calendar``: The calendar ("gregorian" by default) .. _ICU user guide: https://unicode-org.github.io/icu/userguide/format_parse/datetime/#datetime-format-syntax -.. _`RFC 5646`: https://www.rfc-editor.org/info/rfc5646 -.. _`PHP Locale class`: https://www.php.net/manual/en/class.locale.php \ No newline at end of file +.. _RFC 5646: https://www.rfc-editor.org/info/rfc5646 diff --git a/doc/filters/format_number.rst b/doc/filters/format_number.rst index f964d398472..e1bedc2fc8d 100644 --- a/doc/filters/format_number.rst +++ b/doc/filters/format_number.rst @@ -112,9 +112,8 @@ By default, the filter uses the current locale. You can pass it explicitly: Arguments --------- -* ``locale``: The locale code as defined in `RFC 5646`_. They are also documented in the `PHP Locale class`_. +* ``locale``: The locale code as defined in `RFC 5646`_ * ``attrs``: A map of attributes * ``style``: The style of the number output -.. _`RFC 5646`: https://www.rfc-editor.org/info/rfc5646 -.. _`PHP Locale class`: https://www.php.net/manual/en/class.locale.php \ No newline at end of file +.. _RFC 5646: https://www.rfc-editor.org/info/rfc5646 diff --git a/doc/filters/format_time.rst b/doc/filters/format_time.rst index 25710e5ce23..8709a6bcf4e 100644 --- a/doc/filters/format_time.rst +++ b/doc/filters/format_time.rst @@ -29,11 +29,10 @@ the :doc:`format_datetime` filter, but without the date. Arguments --------- -* ``locale``: The locale code as defined in `RFC 5646`_. They are also documented in the `PHP Locale class`_. +* ``locale``: The locale code as defined in `RFC 5646`_ * ``timeFormat``: The time format * ``pattern``: A date time pattern * ``timezone``: The date timezone * ``calendar``: The calendar ("gregorian" by default) -.. _`RFC 5646`: https://www.rfc-editor.org/info/rfc5646 -.. _`PHP Locale class`: https://www.php.net/manual/en/class.locale.php \ No newline at end of file +.. _RFC 5646: https://www.rfc-editor.org/info/rfc5646 diff --git a/doc/filters/language_name.rst b/doc/filters/language_name.rst index 96295950ec4..ccffd20eb04 100644 --- a/doc/filters/language_name.rst +++ b/doc/filters/language_name.rst @@ -1,8 +1,8 @@ ``language_name`` ================= -The ``language_name`` filter returns the language name based on its two-letter code (ISO 639-1), -three-letter code (ISO 639-2) or other specific localized code: +The ``language_name`` filter returns the language name based on its ISO 639-1 +code, ISO 639-2 code, or other specific localized code: .. code-block:: twig @@ -44,7 +44,6 @@ By default, the filter uses the current locale. You can pass it explicitly: Arguments --------- -* ``locale``: The locale code as defined in `RFC 5646`_. They are also documented in the `PHP Locale class`_. +* ``locale``: The locale code as defined in `RFC 5646`_ -.. _`RFC 5646`: https://www.rfc-editor.org/info/rfc5646 -.. _`PHP Locale class`: https://www.php.net/manual/en/class.locale.php +.. _RFC 5646: https://www.rfc-editor.org/info/rfc5646 diff --git a/doc/filters/locale_name.rst b/doc/filters/locale_name.rst index dd835055c25..9e0df073854 100644 --- a/doc/filters/locale_name.rst +++ b/doc/filters/locale_name.rst @@ -43,7 +43,6 @@ By default, the filter uses the current locale. You can pass it explicitly: Arguments --------- -* ``locale``: The locale code as defined in `RFC 5646`_. They are also documented in the `PHP Locale class`_. +* ``locale``: The locale code as defined in `RFC 5646`_ -.. _`RFC 5646`: https://www.rfc-editor.org/info/rfc5646 -.. _`PHP Locale class`: https://www.php.net/manual/en/class.locale.php +.. _RFC 5646: https://www.rfc-editor.org/info/rfc5646 diff --git a/doc/filters/slug.rst b/doc/filters/slug.rst index 57085f767bf..f5b91b2daf6 100644 --- a/doc/filters/slug.rst +++ b/doc/filters/slug.rst @@ -56,7 +56,6 @@ Arguments --------- * ``separator``: The separator that is used to join words (defaults to ``-``) -* ``locale``: The locale code of the original string as defined in `RFC 5646`_ (if none is specified, it will be automatically detected). They are also documented in the `PHP Locale class`_. +* ``locale``: The locale code of the original string as defined in `RFC 5646`_ -.. _`RFC 5646`: https://www.rfc-editor.org/info/rfc5646 -.. _`PHP Locale class`: https://www.php.net/manual/en/class.locale.php \ No newline at end of file +.. _RFC 5646: https://www.rfc-editor.org/info/rfc5646 diff --git a/doc/filters/timezone_name.rst b/doc/filters/timezone_name.rst index 1391ffe077b..26c6f8917ed 100644 --- a/doc/filters/timezone_name.rst +++ b/doc/filters/timezone_name.rst @@ -43,7 +43,6 @@ By default, the filter uses the current locale. You can pass it explicitly: Arguments --------- -* ``locale``: The locale code as defined in `RFC 5646`_. They are also documented in the `PHP Locale class`_. +* ``locale``: The locale code as defined in `RFC 5646`_ -.. _`RFC 5646`: https://www.rfc-editor.org/info/rfc5646 -.. _`PHP Locale class`: https://www.php.net/manual/en/class.locale.php +.. _RFC 5646: https://www.rfc-editor.org/info/rfc5646 diff --git a/doc/functions/country_names.rst b/doc/functions/country_names.rst index fe03ba522f3..f65b265edc9 100644 --- a/doc/functions/country_names.rst +++ b/doc/functions/country_names.rst @@ -44,7 +44,6 @@ By default, the function uses the current locale. You can pass it explicitly: Arguments --------- -* ``locale``: The locale code as defined in `RFC 5646`_. They are also documented in the `PHP Locale class`_. +* ``locale``: The locale code as defined in `RFC 5646`_ -.. _`RFC 5646`: https://www.rfc-editor.org/info/rfc5646 -.. _`PHP Locale class`: https://www.php.net/manual/en/class.locale.php +.. _RFC 5646: https://www.rfc-editor.org/info/rfc5646 diff --git a/doc/functions/currency_names.rst b/doc/functions/currency_names.rst index 8df215eb2f9..9113aa08866 100644 --- a/doc/functions/currency_names.rst +++ b/doc/functions/currency_names.rst @@ -44,7 +44,6 @@ By default, the function uses the current locale. You can pass it explicitly: Arguments --------- -* ``locale``: The locale code as defined in `RFC 5646`_. They are also documented in the `PHP Locale class`_. +* ``locale``: The locale code as defined in `RFC 5646`_ -.. _`RFC 5646`: https://www.rfc-editor.org/info/rfc5646 -.. _`PHP Locale class`: https://www.php.net/manual/en/class.locale.php +.. _RFC 5646: https://www.rfc-editor.org/info/rfc5646 diff --git a/doc/functions/language_names.rst b/doc/functions/language_names.rst index dd9b8930f23..145a4955722 100644 --- a/doc/functions/language_names.rst +++ b/doc/functions/language_names.rst @@ -44,7 +44,6 @@ By default, the function uses the current locale. You can pass it explicitly: Arguments --------- -* ``locale``: The locale code as defined in `RFC 5646`_. They are also documented in the `PHP Locale class`_. +* ``locale``: The locale code as defined in `RFC 5646`_ -.. _`RFC 5646`: https://www.rfc-editor.org/info/rfc5646 -.. _`PHP Locale class`: https://www.php.net/manual/en/class.locale.php +.. _RFC 5646: https://www.rfc-editor.org/info/rfc5646 diff --git a/doc/functions/locale_names.rst b/doc/functions/locale_names.rst index 3a8b71fe4d2..b8597f98079 100644 --- a/doc/functions/locale_names.rst +++ b/doc/functions/locale_names.rst @@ -44,7 +44,6 @@ By default, the function uses the current locale. You can pass it explicitly: Arguments --------- -* ``locale``: The locale code as defined in `RFC 5646`_. They are also documented in the `PHP Locale class`_. +* ``locale``: The locale code as defined in `RFC 5646`_ -.. _`RFC 5646`: https://www.rfc-editor.org/info/rfc5646 -.. _`PHP Locale class`: https://www.php.net/manual/en/class.locale.php +.. _RFC 5646: https://www.rfc-editor.org/info/rfc5646 diff --git a/doc/functions/script_names.rst b/doc/functions/script_names.rst index 963526f66cd..cddf9310ad3 100644 --- a/doc/functions/script_names.rst +++ b/doc/functions/script_names.rst @@ -44,7 +44,6 @@ By default, the function uses the current locale. You can pass it explicitly: Arguments --------- -* ``locale``: The locale code as defined in `RFC 5646`_. They are also documented in the `PHP Locale class`_. +* ``locale``: The locale code as defined in `RFC 5646`_ -.. _`RFC 5646`: https://www.rfc-editor.org/info/rfc5646 -.. _`PHP Locale class`: https://www.php.net/manual/en/class.locale.php +.. _RFC 5646: https://www.rfc-editor.org/info/rfc5646 diff --git a/doc/functions/timezone_names.rst b/doc/functions/timezone_names.rst index 5aa76991073..98c5871ea22 100644 --- a/doc/functions/timezone_names.rst +++ b/doc/functions/timezone_names.rst @@ -44,7 +44,6 @@ By default, the function uses the current locale. You can pass it explicitly: Arguments --------- -* ``locale``: The locale code as defined in `RFC 5646`_. They are also documented in the `PHP Locale class`_. +* ``locale``: The locale code as defined in `RFC 5646`_ -.. _`RFC 5646`: https://www.rfc-editor.org/info/rfc5646 -.. _`PHP Locale class`: https://www.php.net/manual/en/class.locale.php +.. _RFC 5646: https://www.rfc-editor.org/info/rfc5646 From 1488238e3287eab00bb9351b1803046ae0d95ddc Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Fri, 27 Sep 2024 22:51:56 +0200 Subject: [PATCH 513/812] Fix ignored parenthesis in expressions --- CHANGELOG | 1 + src/ExpressionParser.php | 7 ++++++- src/Node/Expression/Unary/AbstractUnary.php | 16 ++++++++++++++-- tests/Fixtures/expressions/power.test | 8 ++++++++ 4 files changed, 29 insertions(+), 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index f2311502a4a..9edf14f51bf 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,6 @@ # 3.15.0 (2024-XX-XX) + * Fix `power` expressions with a negative number in parenthesis (`(-1) ** 2`) * Deprecate instantiating `Node` directly. Use `EmptyNode` or `Nodes` instead. * Add support for inline comments * Add support for accessing class constants with the dot operator diff --git a/src/ExpressionParser.php b/src/ExpressionParser.php index cd1b1993eb6..97e3ae00316 100644 --- a/src/ExpressionParser.php +++ b/src/ExpressionParser.php @@ -172,7 +172,12 @@ private function getPrimary(): AbstractExpression $expr = $this->parseExpression(); $this->parser->getStream()->expect(Token::PUNCTUATION_TYPE, ')', 'An opened parenthesis is not properly closed'); - return $this->parsePostfixExpression($expr); + $expr = $this->parsePostfixExpression($expr); + if ($expr instanceof NegUnary) { + $expr->wrapInParentheses(); + } + + return $expr; } return $this->parsePrimaryExpression(); diff --git a/src/Node/Expression/Unary/AbstractUnary.php b/src/Node/Expression/Unary/AbstractUnary.php index e31e3f84b07..b9b44ed4f41 100644 --- a/src/Node/Expression/Unary/AbstractUnary.php +++ b/src/Node/Expression/Unary/AbstractUnary.php @@ -20,14 +20,26 @@ abstract class AbstractUnary extends AbstractExpression { public function __construct(Node $node, int $lineno) { - parent::__construct(['node' => $node], [], $lineno); + parent::__construct(['node' => $node], ['with_parentheses' => false], $lineno); + } + + public function wrapInParentheses(): void + { + $this->setAttribute('with_parentheses', true); } public function compile(Compiler $compiler): void { - $compiler->raw(' '); + if ($this->getAttribute('with_parentheses')) { + $compiler->raw('('); + } else { + $compiler->raw(' '); + } $this->operator($compiler); $compiler->subcompile($this->getNode('node')); + if ($this->getAttribute('with_parentheses')) { + $compiler->raw(')'); + } } abstract public function operator(Compiler $compiler): Compiler; diff --git a/tests/Fixtures/expressions/power.test b/tests/Fixtures/expressions/power.test index 84fd23692ce..5fb3fa4b561 100644 --- a/tests/Fixtures/expressions/power.test +++ b/tests/Fixtures/expressions/power.test @@ -8,6 +8,10 @@ Twig parses power expressions {{ a ** b }} {{ b ** a }} {{ b ** b }} +{{ -1**0 }} +{{ (-1)**0 }} +{{ -a**0 }} +{{ (-a)**0 }} --DATA-- return ['a' => 4, 'b' => -2] --EXPECT-- @@ -18,3 +22,7 @@ return ['a' => 4, 'b' => -2] 0.0625 16 0.25 +-1 +1 +-1 +1 From e83c3c888be0d176ff061791bf269ce8e2ffc284 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sun, 29 Sep 2024 17:54:02 +0200 Subject: [PATCH 514/812] Add an additional example in the docs --- doc/templates.rst | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/doc/templates.rst b/doc/templates.rst index b87ee1ed7f9..6626d4ee896 100644 --- a/doc/templates.rst +++ b/doc/templates.rst @@ -167,21 +167,24 @@ filters. A common mistake is to forget using parentheses for filters on negative numbers as a negative number in Twig is represented by the ``-`` operator followed by a positive number. As the ``-`` operator has a lower precedence - than the filter operator, it leads to confusion: + than the filter operator, it can lead to confusion: .. code-block:: twig {{ -1|abs }} {# returns -1 #} + {{ -1**0 }} {% returns -1 %} {# as it is equivalent to #} {{ -(1|abs) }} + {{ -(1**0) }} For such cases, use parentheses to force the precedence: .. code-block:: twig {{ (-1)|abs }} {# returns 1 as expected #} + {{ (-1)**0 }} {% returns 1 %} Functions --------- From 0da899d911021dc043b5162c85402b48fd355d55 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sun, 29 Sep 2024 18:08:29 +0200 Subject: [PATCH 515/812] Use get_debug_type() everywhere --- extra/html-extra/HtmlExtension.php | 4 ++-- src/Extension/CoreExtension.php | 20 ++++++++++---------- src/ExtensionSet.php | 2 +- src/Node/Node.php | 2 +- tests/TemplateTest.php | 2 +- 5 files changed, 15 insertions(+), 15 deletions(-) diff --git a/extra/html-extra/HtmlExtension.php b/extra/html-extra/HtmlExtension.php index e1766ad45d4..4231c6aa5b1 100644 --- a/extra/html-extra/HtmlExtension.php +++ b/extra/html-extra/HtmlExtension.php @@ -97,7 +97,7 @@ public static function htmlClasses(...$args): string } elseif (\is_array($arg)) { foreach ($arg as $class => $condition) { if (!\is_string($class)) { - throw new RuntimeError(\sprintf('The html_classes function argument %d (key %d) should be a string, got "%s".', $i, $class, \gettype($class))); + throw new RuntimeError(\sprintf('The html_classes function argument %d (key %d) should be a string, got "%s".', $i, $class, get_debug_type($class))); } if (!$condition) { continue; @@ -105,7 +105,7 @@ public static function htmlClasses(...$args): string $classes[] = $class; } } else { - throw new RuntimeError(\sprintf('The html_classes function argument %d should be either a string or an array, got "%s".', $i, \gettype($arg))); + throw new RuntimeError(\sprintf('The html_classes function argument %d should be either a string or an array, got "%s".', $i, get_debug_type($arg))); } } diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index 66f08b5d142..0d4c2fe516b 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -573,7 +573,7 @@ public function convertDate($date = null, $timezone = null) public static function replace($str, $from): string { if (!is_iterable($from)) { - throw new RuntimeError(\sprintf('The "replace" filter expects a sequence/mapping or "Traversable" as replace values, got "%s".', \is_object($from) ? \get_class($from) : \gettype($from))); + throw new RuntimeError(\sprintf('The "replace" filter expects a sequence/mapping or "Traversable" as replace values, got "%s".', get_debug_type($from))); } return strtr($str ?? '', self::toArray($from)); @@ -670,7 +670,7 @@ public static function merge(...$arrays): array foreach ($arrays as $argNumber => $array) { if (!is_iterable($array)) { - throw new RuntimeError(\sprintf('The merge filter only works with sequences/mappings or "Traversable", got "%s" for argument %d.', \gettype($array), $argNumber + 1)); + throw new RuntimeError(\sprintf('The merge filter only works with sequences/mappings or "Traversable", got "%s" for argument %d.', get_debug_type($array), $argNumber + 1)); } $result = array_merge($result, self::toArray($array)); @@ -977,7 +977,7 @@ public static function sort(Environment $env, $array, $arrow = null): array if ($array instanceof \Traversable) { $array = iterator_to_array($array); } elseif (!\is_array($array)) { - throw new RuntimeError(\sprintf('The sort filter only works with sequences/mappings or "Traversable", got "%s".', \gettype($array))); + throw new RuntimeError(\sprintf('The sort filter only works with sequences/mappings or "Traversable", got "%s".', get_debug_type($array))); } if (null !== $arrow) { @@ -1577,7 +1577,7 @@ public static function constant($constant, $object = null, bool $checkDefined = public static function batch($items, $size, $fill = null, $preserveKeys = true): array { if (!is_iterable($items)) { - throw new RuntimeError(\sprintf('The "batch" filter expects a sequence/mapping or "Traversable", got "%s".', \is_object($items) ? \get_class($items) : \gettype($items))); + throw new RuntimeError(\sprintf('The "batch" filter expects a sequence/mapping or "Traversable", got "%s".', get_debug_type($items))); } $size = (int) ceil($size); @@ -1652,12 +1652,12 @@ public static function getAttribute(Environment $env, Source $source, $object, $ if (null === $object) { $message = \sprintf('Impossible to access a key ("%s") on a null variable.', $item); } else { - $message = \sprintf('Impossible to access a key ("%s") on a %s variable ("%s").', $item, \gettype($object), $object); + $message = \sprintf('Impossible to access a key ("%s") on a %s variable ("%s").', $item, get_debug_type($object), $object); } } elseif (null === $object) { $message = \sprintf('Impossible to access an attribute ("%s") on a null variable.', $item); } else { - $message = \sprintf('Impossible to access an attribute ("%s") on a %s variable ("%s").', $item, \gettype($object), $object); + $message = \sprintf('Impossible to access an attribute ("%s") on a %s variable ("%s").', $item, get_debug_type($object), $object); } throw new RuntimeError($message, $lineno, $source); @@ -1678,7 +1678,7 @@ public static function getAttribute(Environment $env, Source $source, $object, $ } elseif (\is_array($object)) { $message = \sprintf('Impossible to invoke a method ("%s") on a sequence/mapping.', $item); } else { - $message = \sprintf('Impossible to invoke a method ("%s") on a %s variable ("%s").', $item, \gettype($object), $object); + $message = \sprintf('Impossible to invoke a method ("%s") on a %s variable ("%s").', $item, get_debug_type($object), $object); } throw new RuntimeError($message, $lineno, $source); @@ -1826,7 +1826,7 @@ public static function column($array, $name, $index = null): array if ($array instanceof \Traversable) { $array = iterator_to_array($array); } elseif (!\is_array($array)) { - throw new RuntimeError(\sprintf('The column filter only works with sequences/mappings or "Traversable", got "%s" as first argument.', \gettype($array))); + throw new RuntimeError(\sprintf('The column filter only works with sequences/mappings or "Traversable", got "%s" as first argument.', get_debug_type($array))); } return array_column($array, $name, $index); @@ -1838,7 +1838,7 @@ public static function column($array, $name, $index = null): array public static function filter(Environment $env, $array, $arrow) { if (!is_iterable($array)) { - throw new RuntimeError(\sprintf('The "filter" filter expects a sequence/mapping or "Traversable", got "%s".', \is_object($array) ? \get_class($array) : \gettype($array))); + throw new RuntimeError(\sprintf('The "filter" filter expects a sequence/mapping or "Traversable", got "%s".', get_debug_type($array))); } self::checkArrowInSandbox($env, $arrow, 'filter', 'filter'); @@ -1894,7 +1894,7 @@ public static function reduce(Environment $env, $array, $arrow, $initial = null) self::checkArrowInSandbox($env, $arrow, 'reduce', 'filter'); if (!\is_array($array) && !$array instanceof \Traversable) { - throw new RuntimeError(\sprintf('The "reduce" filter only works with sequences/mappings or "Traversable", got "%s" as first argument.', \gettype($array))); + throw new RuntimeError(\sprintf('The "reduce" filter only works with sequences/mappings or "Traversable", got "%s" as first argument.', get_debug_type($array))); } $accumulator = $initial; diff --git a/src/ExtensionSet.php b/src/ExtensionSet.php index 3a5b24be499..8466cb955bf 100644 --- a/src/ExtensionSet.php +++ b/src/ExtensionSet.php @@ -472,7 +472,7 @@ private function initExtension(ExtensionInterface $extension): void // operators if ($operators = $extension->getOperators()) { if (!\is_array($operators)) { - throw new \InvalidArgumentException(\sprintf('"%s::getOperators()" must return an array with operators, got "%s".', \get_class($extension), \is_object($operators) ? \get_class($operators) : \gettype($operators).(\is_resource($operators) ? '' : '#'.$operators))); + throw new \InvalidArgumentException(\sprintf('"%s::getOperators()" must return an array with operators, got "%s".', \get_class($extension), get_debug_type($operators).(\is_resource($operators) ? '' : '#'.$operators))); } if (2 !== \count($operators)) { diff --git a/src/Node/Node.php b/src/Node/Node.php index d2fcb30ce37..7b4044c3f34 100644 --- a/src/Node/Node.php +++ b/src/Node/Node.php @@ -53,7 +53,7 @@ public function __construct(array $nodes = [], array $attributes = [], int $line foreach ($nodes as $name => $node) { if (!$node instanceof self) { - throw new \InvalidArgumentException(\sprintf('Using "%s" for the value of node "%s" of "%s" is not supported. You must pass a \Twig\Node\Node instance.', \is_object($node) ? $node::class : (null === $node ? 'null' : \gettype($node)), $name, static::class)); + throw new \InvalidArgumentException(\sprintf('Using "%s" for the value of node "%s" of "%s" is not supported. You must pass a \Twig\Node\Node instance.', get_debug_type($node), $name, static::class)); } } $this->nodes = $nodes; diff --git a/tests/TemplateTest.php b/tests/TemplateTest.php index 402c4f3504e..4d2489962f7 100644 --- a/tests/TemplateTest.php +++ b/tests/TemplateTest.php @@ -396,7 +396,7 @@ public static function getGetAttributeTests() // tests when input is not an array or object $tests = array_merge($tests, [ - [false, null, 42, 'a', [], $anyType, 'Impossible to access an attribute ("a") on a integer variable ("42") in "index.twig".'], + [false, null, 42, 'a', [], $anyType, 'Impossible to access an attribute ("a") on a int variable ("42") in "index.twig".'], [false, null, 'string', 'a', [], $anyType, 'Impossible to access an attribute ("a") on a string variable ("string") in "index.twig".'], [false, null, [], 'a', [], $anyType, 'Key "a" does not exist as the sequence/mapping is empty in "index.twig".'], ]); From bfa44742da8f7f9d5018c15a55ee93e9566ecc98 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sun, 29 Sep 2024 18:18:48 +0200 Subject: [PATCH 516/812] Add quotes on filters/functions in CoreExtension --- src/Extension/CoreExtension.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index 0d4c2fe516b..de26ad8b418 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -436,7 +436,7 @@ public static function random(string $charset, $values = null, $max = null) $values = self::toArray($values); if (0 === \count($values)) { - throw new RuntimeError('The random function cannot pick from an empty sequence/mapping.'); + throw new RuntimeError('The "random" function cannot pick from an empty sequence/mapping.'); } return $values[array_rand($values, 1)]; @@ -599,7 +599,7 @@ public static function round($value, $precision = 0, $method = 'common') } if ('ceil' !== $method && 'floor' !== $method) { - throw new RuntimeError('The round filter only supports the "common", "ceil", and "floor" methods.'); + throw new RuntimeError('The "round" filter only supports the "common", "ceil", and "floor" methods.'); } return $method($value * 10 ** $precision) / 10 ** $precision; @@ -670,7 +670,7 @@ public static function merge(...$arrays): array foreach ($arrays as $argNumber => $array) { if (!is_iterable($array)) { - throw new RuntimeError(\sprintf('The merge filter only works with sequences/mappings or "Traversable", got "%s" for argument %d.', get_debug_type($array), $argNumber + 1)); + throw new RuntimeError(\sprintf('The "merge" filter only works with sequences/mappings or "Traversable", got "%s" for argument %d.', get_debug_type($array), $argNumber + 1)); } $result = array_merge($result, self::toArray($array)); @@ -977,7 +977,7 @@ public static function sort(Environment $env, $array, $arrow = null): array if ($array instanceof \Traversable) { $array = iterator_to_array($array); } elseif (!\is_array($array)) { - throw new RuntimeError(\sprintf('The sort filter only works with sequences/mappings or "Traversable", got "%s".', get_debug_type($array))); + throw new RuntimeError(\sprintf('The "sort" filter only works with sequences/mappings or "Traversable", got "%s".', get_debug_type($array))); } if (null !== $arrow) { @@ -1556,7 +1556,7 @@ public static function constant($constant, $object = null, bool $checkDefined = } if ('::class' === strtolower(substr($constant, -7))) { - throw new RuntimeError(\sprintf('You cannot use the Twig function "constant()" to access "%s". You could provide an object and call constant("class", $object) or use the class name directly as a string.', $constant)); + throw new RuntimeError(\sprintf('You cannot use the Twig function "constant" to access "%s". You could provide an object and call constant("class", $object) or use the class name directly as a string.', $constant)); } throw new RuntimeError(\sprintf('Constant "%s" is undefined.', $constant)); @@ -1826,7 +1826,7 @@ public static function column($array, $name, $index = null): array if ($array instanceof \Traversable) { $array = iterator_to_array($array); } elseif (!\is_array($array)) { - throw new RuntimeError(\sprintf('The column filter only works with sequences/mappings or "Traversable", got "%s" as first argument.', get_debug_type($array))); + throw new RuntimeError(\sprintf('The "column" filter only works with sequences/mappings or "Traversable", got "%s" as first argument.', get_debug_type($array))); } return array_column($array, $name, $index); From 0f47aa7d1fe8e4ba0d6c7193ecd880320140b57c Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sun, 29 Sep 2024 18:22:59 +0200 Subject: [PATCH 517/812] Fix tests --- tests/Fixtures/exceptions/exception_in_extension_extends.test | 2 +- tests/Fixtures/exceptions/exception_in_extension_include.test | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Fixtures/exceptions/exception_in_extension_extends.test b/tests/Fixtures/exceptions/exception_in_extension_extends.test index fee521878d1..d3361bf09f9 100644 --- a/tests/Fixtures/exceptions/exception_in_extension_extends.test +++ b/tests/Fixtures/exceptions/exception_in_extension_extends.test @@ -9,4 +9,4 @@ Exception thrown from a child for an extension error --DATA-- return [] --EXCEPTION-- -Twig\Error\RuntimeError: The random function cannot pick from an empty sequence/mapping in "base.twig" at line 4. +Twig\Error\RuntimeError: The "random" function cannot pick from an empty sequence/mapping in "base.twig" at line 4. diff --git a/tests/Fixtures/exceptions/exception_in_extension_include.test b/tests/Fixtures/exceptions/exception_in_extension_include.test index ab09cabb759..be25db3b8d5 100644 --- a/tests/Fixtures/exceptions/exception_in_extension_include.test +++ b/tests/Fixtures/exceptions/exception_in_extension_include.test @@ -9,4 +9,4 @@ Exception thrown from an include for an extension error --DATA-- return [] --EXCEPTION-- -Twig\Error\RuntimeError: The random function cannot pick from an empty sequence/mapping in "content.twig" at line 4. +Twig\Error\RuntimeError: The "random" function cannot pick from an empty sequence/mapping in "content.twig" at line 4. From 3523d34b50c97bf28f924ebef684aea4052a94fe Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sun, 29 Sep 2024 18:23:14 +0200 Subject: [PATCH 518/812] Add missing is_iterable() checks --- src/Extension/CoreExtension.php | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index de26ad8b418..c8e854658da 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -1823,10 +1823,12 @@ public static function getAttribute(Environment $env, Source $source, $object, $ */ public static function column($array, $name, $index = null): array { + if (!is_iterable($array)) { + throw new RuntimeError(\sprintf('The "column" filter only works with sequences/mappings or "Traversable", got "%s" as first argument.', get_debug_type($array))); + } + if ($array instanceof \Traversable) { $array = iterator_to_array($array); - } elseif (!\is_array($array)) { - throw new RuntimeError(\sprintf('The "column" filter only works with sequences/mappings or "Traversable", got "%s" as first argument.', get_debug_type($array))); } return array_column($array, $name, $index); @@ -1856,6 +1858,10 @@ public static function filter(Environment $env, $array, $arrow) */ public static function find(Environment $env, $array, $arrow) { + if (!is_iterable($array)) { + throw new RuntimeError(\sprintf('The "find" filter expects a sequence/mapping or "Traversable", got "%s".', get_debug_type($array))); + } + self::checkArrowInSandbox($env, $arrow, 'find', 'filter'); foreach ($array as $k => $v) { @@ -1891,12 +1897,12 @@ public static function map(Environment $env, $array, $arrow) */ public static function reduce(Environment $env, $array, $arrow, $initial = null) { - self::checkArrowInSandbox($env, $arrow, 'reduce', 'filter'); - - if (!\is_array($array) && !$array instanceof \Traversable) { + if (!is_iterable($array)) { throw new RuntimeError(\sprintf('The "reduce" filter only works with sequences/mappings or "Traversable", got "%s" as first argument.', get_debug_type($array))); } + self::checkArrowInSandbox($env, $arrow, 'reduce', 'filter'); + $accumulator = $initial; foreach ($array as $key => $value) { $accumulator = $arrow($accumulator, $value, $key); @@ -1910,6 +1916,10 @@ public static function reduce(Environment $env, $array, $arrow, $initial = null) */ public static function arraySome(Environment $env, $array, $arrow) { + if (!is_iterable($array)) { + throw new RuntimeError(\sprintf('The "has some" filter only works with sequences/mappings or "Traversable", got "%s" as first argument.', get_debug_type($array))); + } + self::checkArrowInSandbox($env, $arrow, 'has some', 'operator'); foreach ($array as $k => $v) { @@ -1926,6 +1936,10 @@ public static function arraySome(Environment $env, $array, $arrow) */ public static function arrayEvery(Environment $env, $array, $arrow) { + if (!is_iterable($array)) { + throw new RuntimeError(\sprintf('The "has every" filter only works with sequences/mappings or "Traversable", got "%s" as first argument.', get_debug_type($array))); + } + self::checkArrowInSandbox($env, $arrow, 'has every', 'operator'); foreach ($array as $k => $v) { From c599125a39f05d43fb40c1c93a897b29d234b499 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Thu, 29 Aug 2024 15:13:33 +0200 Subject: [PATCH 519/812] Unify error messages when a filter expects a mapping/sequence --- src/Extension/CoreExtension.php | 24 +++++++++---------- .../exception_in_extension_extends.test | 2 +- .../exception_in_extension_include.test | 2 +- .../Fixtures/filters/replace_invalid_arg.test | 2 +- .../functions/cycle_empty_mapping.test | 2 +- .../functions/cycle_empty_sequence.test | 2 +- 6 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index c8e854658da..4a5bf233f71 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -366,7 +366,7 @@ public static function cycle($values, $position): mixed } if (!$count = \count($values)) { - throw new RuntimeError('The "cycle" function does not work on empty sequences.'); + throw new RuntimeError('The "cycle" function expects a non-empty sequence.'); } return $values[$position % $count]; @@ -436,7 +436,7 @@ public static function random(string $charset, $values = null, $max = null) $values = self::toArray($values); if (0 === \count($values)) { - throw new RuntimeError('The "random" function cannot pick from an empty sequence/mapping.'); + throw new RuntimeError('The "random" function cannot pick from an empty sequence or mapping.'); } return $values[array_rand($values, 1)]; @@ -573,7 +573,7 @@ public function convertDate($date = null, $timezone = null) public static function replace($str, $from): string { if (!is_iterable($from)) { - throw new RuntimeError(\sprintf('The "replace" filter expects a sequence/mapping or "Traversable" as replace values, got "%s".', get_debug_type($from))); + throw new RuntimeError(\sprintf('The "replace" filter expects a sequence or a mapping, got "%s".', get_debug_type($from))); } return strtr($str ?? '', self::toArray($from)); @@ -670,7 +670,7 @@ public static function merge(...$arrays): array foreach ($arrays as $argNumber => $array) { if (!is_iterable($array)) { - throw new RuntimeError(\sprintf('The "merge" filter only works with sequences/mappings or "Traversable", got "%s" for argument %d.', get_debug_type($array), $argNumber + 1)); + throw new RuntimeError(\sprintf('The "merge" filter expects a sequence or a mapping, got "%s" for argument %d.', get_debug_type($array), $argNumber + 1)); } $result = array_merge($result, self::toArray($array)); @@ -977,7 +977,7 @@ public static function sort(Environment $env, $array, $arrow = null): array if ($array instanceof \Traversable) { $array = iterator_to_array($array); } elseif (!\is_array($array)) { - throw new RuntimeError(\sprintf('The "sort" filter only works with sequences/mappings or "Traversable", got "%s".', get_debug_type($array))); + throw new RuntimeError(\sprintf('The "sort" filter expects a sequence or a mapping, got "%s".', get_debug_type($array))); } if (null !== $arrow) { @@ -1577,7 +1577,7 @@ public static function constant($constant, $object = null, bool $checkDefined = public static function batch($items, $size, $fill = null, $preserveKeys = true): array { if (!is_iterable($items)) { - throw new RuntimeError(\sprintf('The "batch" filter expects a sequence/mapping or "Traversable", got "%s".', get_debug_type($items))); + throw new RuntimeError(\sprintf('The "batch" filter expects a sequence or a mapping, got "%s".', get_debug_type($items))); } $size = (int) ceil($size); @@ -1824,7 +1824,7 @@ public static function getAttribute(Environment $env, Source $source, $object, $ public static function column($array, $name, $index = null): array { if (!is_iterable($array)) { - throw new RuntimeError(\sprintf('The "column" filter only works with sequences/mappings or "Traversable", got "%s" as first argument.', get_debug_type($array))); + throw new RuntimeError(\sprintf('The "column" filter expects a sequence or a mapping, got "%s".', get_debug_type($array))); } if ($array instanceof \Traversable) { @@ -1859,7 +1859,7 @@ public static function filter(Environment $env, $array, $arrow) public static function find(Environment $env, $array, $arrow) { if (!is_iterable($array)) { - throw new RuntimeError(\sprintf('The "find" filter expects a sequence/mapping or "Traversable", got "%s".', get_debug_type($array))); + throw new RuntimeError(\sprintf('The "find" filter expects a sequence or a mapping, got "%s".', get_debug_type($array))); } self::checkArrowInSandbox($env, $arrow, 'find', 'filter'); @@ -1879,7 +1879,7 @@ public static function find(Environment $env, $array, $arrow) public static function map(Environment $env, $array, $arrow) { if (!is_iterable($array)) { - throw new RuntimeError(\sprintf('The "map" filter expects a sequence/mapping or "Traversable", got "%s".', get_debug_type($array))); + throw new RuntimeError(\sprintf('The "map" filter expects a sequence or a mapping, got "%s".', get_debug_type($array))); } self::checkArrowInSandbox($env, $arrow, 'map', 'filter'); @@ -1898,7 +1898,7 @@ public static function map(Environment $env, $array, $arrow) public static function reduce(Environment $env, $array, $arrow, $initial = null) { if (!is_iterable($array)) { - throw new RuntimeError(\sprintf('The "reduce" filter only works with sequences/mappings or "Traversable", got "%s" as first argument.', get_debug_type($array))); + throw new RuntimeError(\sprintf('The "reduce" filter expects a sequence or a mapping, got "%s".', get_debug_type($array))); } self::checkArrowInSandbox($env, $arrow, 'reduce', 'filter'); @@ -1917,7 +1917,7 @@ public static function reduce(Environment $env, $array, $arrow, $initial = null) public static function arraySome(Environment $env, $array, $arrow) { if (!is_iterable($array)) { - throw new RuntimeError(\sprintf('The "has some" filter only works with sequences/mappings or "Traversable", got "%s" as first argument.', get_debug_type($array))); + throw new RuntimeError(\sprintf('The "has some" test expects a sequence or a mapping, got "%s".', get_debug_type($array))); } self::checkArrowInSandbox($env, $arrow, 'has some', 'operator'); @@ -1937,7 +1937,7 @@ public static function arraySome(Environment $env, $array, $arrow) public static function arrayEvery(Environment $env, $array, $arrow) { if (!is_iterable($array)) { - throw new RuntimeError(\sprintf('The "has every" filter only works with sequences/mappings or "Traversable", got "%s" as first argument.', get_debug_type($array))); + throw new RuntimeError(\sprintf('The "has every" test expects a sequence or a mapping, got "%s".', get_debug_type($array))); } self::checkArrowInSandbox($env, $arrow, 'has every', 'operator'); diff --git a/tests/Fixtures/exceptions/exception_in_extension_extends.test b/tests/Fixtures/exceptions/exception_in_extension_extends.test index d3361bf09f9..3b9ddeec84d 100644 --- a/tests/Fixtures/exceptions/exception_in_extension_extends.test +++ b/tests/Fixtures/exceptions/exception_in_extension_extends.test @@ -9,4 +9,4 @@ Exception thrown from a child for an extension error --DATA-- return [] --EXCEPTION-- -Twig\Error\RuntimeError: The "random" function cannot pick from an empty sequence/mapping in "base.twig" at line 4. +Twig\Error\RuntimeError: The "random" function cannot pick from an empty sequence or mapping in "base.twig" at line 4. diff --git a/tests/Fixtures/exceptions/exception_in_extension_include.test b/tests/Fixtures/exceptions/exception_in_extension_include.test index be25db3b8d5..42927c2f6bb 100644 --- a/tests/Fixtures/exceptions/exception_in_extension_include.test +++ b/tests/Fixtures/exceptions/exception_in_extension_include.test @@ -9,4 +9,4 @@ Exception thrown from an include for an extension error --DATA-- return [] --EXCEPTION-- -Twig\Error\RuntimeError: The "random" function cannot pick from an empty sequence/mapping in "content.twig" at line 4. +Twig\Error\RuntimeError: The "random" function cannot pick from an empty sequence or mapping in "content.twig" at line 4. diff --git a/tests/Fixtures/filters/replace_invalid_arg.test b/tests/Fixtures/filters/replace_invalid_arg.test index ea163250093..3b1429c90ad 100644 --- a/tests/Fixtures/filters/replace_invalid_arg.test +++ b/tests/Fixtures/filters/replace_invalid_arg.test @@ -5,4 +5,4 @@ Exception for invalid argument type in replace call --DATA-- return ['stdClass' => new \stdClass()] --EXCEPTION-- -Twig\Error\RuntimeError: The "replace" filter expects a sequence/mapping or "Traversable" as replace values, got "stdClass" in "index.twig" at line 2. +Twig\Error\RuntimeError: The "replace" filter expects a sequence or a mapping, got "stdClass" in "index.twig" at line 2. diff --git a/tests/Fixtures/functions/cycle_empty_mapping.test b/tests/Fixtures/functions/cycle_empty_mapping.test index ca241d8f312..65b1d949148 100644 --- a/tests/Fixtures/functions/cycle_empty_mapping.test +++ b/tests/Fixtures/functions/cycle_empty_mapping.test @@ -5,4 +5,4 @@ --DATA-- return [] --EXCEPTION-- -Twig\Error\RuntimeError: The "cycle" function does not work on empty sequences in "index.twig" at line 2. +Twig\Error\RuntimeError: The "cycle" function expects a non-empty sequence in "index.twig" at line 2. diff --git a/tests/Fixtures/functions/cycle_empty_sequence.test b/tests/Fixtures/functions/cycle_empty_sequence.test index 846913b2004..5d60bb1faea 100644 --- a/tests/Fixtures/functions/cycle_empty_sequence.test +++ b/tests/Fixtures/functions/cycle_empty_sequence.test @@ -5,4 +5,4 @@ --DATA-- return [] --EXCEPTION-- -Twig\Error\RuntimeError: The "cycle" function does not work on empty sequences in "index.twig" at line 2. +Twig\Error\RuntimeError: The "cycle" function expects a non-empty sequence in "index.twig" at line 2. From 4e08e3ef5f83f49877591351451721225c741a1f Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Mon, 30 Sep 2024 08:41:48 +0200 Subject: [PATCH 520/812] Fix tests --- extra/html-extra/HtmlExtension.php | 4 ++-- .../Tests/Fixtures/html_classes_with_unsupported_arg.test | 2 +- .../Tests/Fixtures/html_classes_with_unsupported_key.test | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/extra/html-extra/HtmlExtension.php b/extra/html-extra/HtmlExtension.php index 4231c6aa5b1..d8a6c0036d1 100644 --- a/extra/html-extra/HtmlExtension.php +++ b/extra/html-extra/HtmlExtension.php @@ -97,7 +97,7 @@ public static function htmlClasses(...$args): string } elseif (\is_array($arg)) { foreach ($arg as $class => $condition) { if (!\is_string($class)) { - throw new RuntimeError(\sprintf('The html_classes function argument %d (key %d) should be a string, got "%s".', $i, $class, get_debug_type($class))); + throw new RuntimeError(\sprintf('The "html_classes" function argument %d (key %d) should be a string, got "%s".', $i, $class, get_debug_type($class))); } if (!$condition) { continue; @@ -105,7 +105,7 @@ public static function htmlClasses(...$args): string $classes[] = $class; } } else { - throw new RuntimeError(\sprintf('The html_classes function argument %d should be either a string or an array, got "%s".', $i, get_debug_type($arg))); + throw new RuntimeError(\sprintf('The "html_classes" function argument %d should be either a string or an array, got "%s".', $i, get_debug_type($arg))); } } diff --git a/extra/html-extra/Tests/Fixtures/html_classes_with_unsupported_arg.test b/extra/html-extra/Tests/Fixtures/html_classes_with_unsupported_arg.test index 85faed6247c..21ca373f818 100644 --- a/extra/html-extra/Tests/Fixtures/html_classes_with_unsupported_arg.test +++ b/extra/html-extra/Tests/Fixtures/html_classes_with_unsupported_arg.test @@ -5,4 +5,4 @@ --DATA-- return [] --EXCEPTION-- -Twig\Error\RuntimeError: The html_classes function argument 0 should be either a string or an array, got "boolean" in "index.twig" at line 2. +Twig\Error\RuntimeError: The "html_classes" function argument 0 should be either a string or an array, got "bool" in "index.twig" at line 2. diff --git a/extra/html-extra/Tests/Fixtures/html_classes_with_unsupported_key.test b/extra/html-extra/Tests/Fixtures/html_classes_with_unsupported_key.test index b74748c58f5..708cb255bbc 100644 --- a/extra/html-extra/Tests/Fixtures/html_classes_with_unsupported_key.test +++ b/extra/html-extra/Tests/Fixtures/html_classes_with_unsupported_key.test @@ -5,4 +5,4 @@ --DATA-- return [] --EXCEPTION-- -Twig\Error\RuntimeError: The html_classes function argument 0 (key 0) should be a string, got "integer" in "index.twig" at line 2. +Twig\Error\RuntimeError: The "html_classes" function argument 0 (key 0) should be a string, got "int" in "index.twig" at line 2. From d133ab7e708b75e320efaf6039b827e7e94306fc Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Mon, 30 Sep 2024 11:52:45 +0200 Subject: [PATCH 521/812] Add support for detecting if an expression had explicit parentheses --- src/ExpressionParser.php | 9 ++------- src/Node/Expression/AbstractExpression.php | 15 +++++++++++++++ src/Node/Expression/Unary/AbstractUnary.php | 4 ++-- 3 files changed, 19 insertions(+), 9 deletions(-) diff --git a/src/ExpressionParser.php b/src/ExpressionParser.php index 97e3ae00316..33da2b29e77 100644 --- a/src/ExpressionParser.php +++ b/src/ExpressionParser.php @@ -169,15 +169,10 @@ private function getPrimary(): AbstractExpression return $this->parsePostfixExpression(new $class($expr, $token->getLine())); } elseif ($token->test(Token::PUNCTUATION_TYPE, '(')) { $this->parser->getStream()->next(); - $expr = $this->parseExpression(); + $expr = $this->parseExpression()->setExplicitParentheses(); $this->parser->getStream()->expect(Token::PUNCTUATION_TYPE, ')', 'An opened parenthesis is not properly closed'); - $expr = $this->parsePostfixExpression($expr); - if ($expr instanceof NegUnary) { - $expr->wrapInParentheses(); - } - - return $expr; + return $this->parsePostfixExpression($expr); } return $this->parsePrimaryExpression(); diff --git a/src/Node/Expression/AbstractExpression.php b/src/Node/Expression/AbstractExpression.php index 1692f5671ef..22d8617cd72 100644 --- a/src/Node/Expression/AbstractExpression.php +++ b/src/Node/Expression/AbstractExpression.php @@ -25,4 +25,19 @@ public function isGenerator(): bool { return $this->hasAttribute('is_generator') && $this->getAttribute('is_generator'); } + + /** + * @return static + */ + public function setExplicitParentheses(): self + { + $this->setAttribute('with_parentheses', true); + + return $this; + } + + public function hasExplicitParentheses(): bool + { + return $this->hasAttribute('with_parentheses') && $this->getAttribute('with_parentheses'); + } } diff --git a/src/Node/Expression/Unary/AbstractUnary.php b/src/Node/Expression/Unary/AbstractUnary.php index b9b44ed4f41..2482d1e70a2 100644 --- a/src/Node/Expression/Unary/AbstractUnary.php +++ b/src/Node/Expression/Unary/AbstractUnary.php @@ -30,14 +30,14 @@ public function wrapInParentheses(): void public function compile(Compiler $compiler): void { - if ($this->getAttribute('with_parentheses')) { + if ($this->hasExplicitParentheses()) { $compiler->raw('('); } else { $compiler->raw(' '); } $this->operator($compiler); $compiler->subcompile($this->getNode('node')); - if ($this->getAttribute('with_parentheses')) { + if ($this->hasExplicitParentheses()) { $compiler->raw(')'); } } From b46ca43f6168d6c91f4fb6acfc4fc42a88f1475c Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Mon, 30 Sep 2024 12:00:38 +0200 Subject: [PATCH 522/812] Remove obsolete method --- src/Node/Expression/Unary/AbstractUnary.php | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/Node/Expression/Unary/AbstractUnary.php b/src/Node/Expression/Unary/AbstractUnary.php index 2482d1e70a2..e6943272eaa 100644 --- a/src/Node/Expression/Unary/AbstractUnary.php +++ b/src/Node/Expression/Unary/AbstractUnary.php @@ -23,11 +23,6 @@ public function __construct(Node $node, int $lineno) parent::__construct(['node' => $node], ['with_parentheses' => false], $lineno); } - public function wrapInParentheses(): void - { - $this->setAttribute('with_parentheses', true); - } - public function compile(Compiler $compiler): void { if ($this->hasExplicitParentheses()) { From 824143d5e59a2639c4502dd7e962925a6d4d760b Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Mon, 30 Sep 2024 10:55:51 +0200 Subject: [PATCH 523/812] Deprecate not passing AbstractExpression args to most constructor arguments for classes extending AbstractExpression --- CHANGELOG | 1 + doc/deprecated.rst | 14 ++++++++++++++ src/Node/Expression/Binary/AbstractBinary.php | 11 +++++++++++ src/Node/Expression/BlockReferenceExpression.php | 7 +++++++ src/Node/Expression/Filter/DefaultFilter.php | 8 ++++++++ src/Node/Expression/Filter/RawFilter.php | 8 ++++++++ src/Node/Expression/FilterExpression.php | 7 +++++++ src/Node/Expression/InlinePrint.php | 7 +++++++ src/Node/Expression/NullCoalesceExpression.php | 11 +++++++++++ src/Node/Expression/Test/DefinedTest.php | 8 ++++++++ src/Node/Expression/TestExpression.php | 7 +++++++ src/Node/Expression/Unary/AbstractUnary.php | 7 +++++++ 12 files changed, 96 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index 9edf14f51bf..f4a07a9c71c 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,6 @@ # 3.15.0 (2024-XX-XX) + * Deprecate not passing `AbstractExpression` args to most constructor arguments for classes extending `AbstractExpression` * Fix `power` expressions with a negative number in parenthesis (`(-1) ** 2`) * Deprecate instantiating `Node` directly. Use `EmptyNode` or `Nodes` instead. * Add support for inline comments diff --git a/doc/deprecated.rst b/doc/deprecated.rst index 2488e761dd5..afcec62f597 100644 --- a/doc/deprecated.rst +++ b/doc/deprecated.rst @@ -296,3 +296,17 @@ Node * Instantiating ``Twig\Node\Node`` directly is deprecated as of Twig 3.15. Use ``EmptyNode`` or ``Nodes`` instead depending on the use case. The ``Twig\Node\Node`` class will be abstract in Twig 4.0. + +* Not passing ``AbstractExpression`` arguments to the following ``Node`` class + constructors is deprecated as of Twig 3.15: + + * ``AbstractBinary`` + * ``AbstractUnary`` + * ``BlockReferenceExpression`` + * ``TestExpression`` + * ``DefinedTest`` + * ``FilterExpression`` + * ``RawFilter`` + * ``DefaultFilter`` + * ``InlinePrint`` + * ``NullCoalesceExpression`` diff --git a/src/Node/Expression/Binary/AbstractBinary.php b/src/Node/Expression/Binary/AbstractBinary.php index c424e5cc5f0..ccd3be44fc6 100644 --- a/src/Node/Expression/Binary/AbstractBinary.php +++ b/src/Node/Expression/Binary/AbstractBinary.php @@ -18,8 +18,19 @@ abstract class AbstractBinary extends AbstractExpression { + /** + * @param AbstractExpression $left + * @param AbstractExpression $right + */ public function __construct(Node $left, Node $right, int $lineno) { + if (!$left instanceof AbstractExpression) { + trigger_deprecation('twig/twig', '3.15', 'Not passing a "%s" instance to the "left" argument of "%s" is deprecated ("%s" given).', AbstractExpression::class, static::class, get_class($left)); + } + if (!$right instanceof AbstractExpression) { + trigger_deprecation('twig/twig', '3.15', 'Not passing a "%s" instance to the "right" argument of "%s" is deprecated ("%s" given).', AbstractExpression::class, static::class, get_class($right)); + } + parent::__construct(['left' => $left, 'right' => $right], [], $lineno); } diff --git a/src/Node/Expression/BlockReferenceExpression.php b/src/Node/Expression/BlockReferenceExpression.php index acd231e15bc..ed88c6094d6 100644 --- a/src/Node/Expression/BlockReferenceExpression.php +++ b/src/Node/Expression/BlockReferenceExpression.php @@ -22,8 +22,15 @@ */ class BlockReferenceExpression extends AbstractExpression { + /** + * @param AbstractExpression $name + */ public function __construct(Node $name, ?Node $template, int $lineno) { + if (!$name instanceof AbstractExpression) { + trigger_deprecation('twig/twig', '3.15', 'Not passing a "%s" instance to the "node" argument of "%s" is deprecated ("%s" given).', AbstractExpression::class, static::class, get_class($node)); + } + $nodes = ['name' => $name]; if (null !== $template) { $nodes['template'] = $template; diff --git a/src/Node/Expression/Filter/DefaultFilter.php b/src/Node/Expression/Filter/DefaultFilter.php index fa93e227ca4..a3ee66e47b1 100644 --- a/src/Node/Expression/Filter/DefaultFilter.php +++ b/src/Node/Expression/Filter/DefaultFilter.php @@ -15,6 +15,7 @@ use Twig\Compiler; use Twig\Extension\CoreExtension; use Twig\Node\EmptyNode; +use Twig\Node\Expression\AbstractExpression; use Twig\Node\Expression\ConditionalExpression; use Twig\Node\Expression\ConstantExpression; use Twig\Node\Expression\FilterExpression; @@ -34,9 +35,16 @@ */ class DefaultFilter extends FilterExpression { + /** + * @param AbstractExpression $node + */ #[FirstClassTwigCallableReady] public function __construct(Node $node, TwigFilter|ConstantExpression $filter, Node $arguments, int $lineno) { + if (!$node instanceof AbstractExpression) { + trigger_deprecation('twig/twig', '3.15', 'Not passing a "%s" instance to the "node" argument of "%s" is deprecated ("%s" given).', AbstractExpression::class, static::class, get_class($node)); + } + if ($filter instanceof TwigFilter) { $name = $filter->getName(); $default = new FilterExpression($node, $filter, $arguments, $node->getTemplateLine()); diff --git a/src/Node/Expression/Filter/RawFilter.php b/src/Node/Expression/Filter/RawFilter.php index adb6de7895f..e70501e24cf 100644 --- a/src/Node/Expression/Filter/RawFilter.php +++ b/src/Node/Expression/Filter/RawFilter.php @@ -14,6 +14,7 @@ use Twig\Attribute\FirstClassTwigCallableReady; use Twig\Compiler; use Twig\Node\EmptyNode; +use Twig\Node\Expression\AbstractExpression; use Twig\Node\Expression\ConstantExpression; use Twig\Node\Expression\FilterExpression; use Twig\Node\Node; @@ -24,9 +25,16 @@ */ class RawFilter extends FilterExpression { + /** + * @param AbstractExpression $node + */ #[FirstClassTwigCallableReady] public function __construct(Node $node, TwigFilter|ConstantExpression|null $filter = null, ?Node $arguments = null, int $lineno = 0) { + if (!$node instanceof AbstractExpression) { + trigger_deprecation('twig/twig', '3.15', 'Not passing a "%s" instance to the "node" argument of "%s" is deprecated ("%s" given).', AbstractExpression::class, static::class, get_class($node)); + } + parent::__construct($node, $filter ?: new TwigFilter('raw', null, ['is_safe' => ['all']]), $arguments ?: new EmptyNode(), $lineno ?: $node->getTemplateLine()); } diff --git a/src/Node/Expression/FilterExpression.php b/src/Node/Expression/FilterExpression.php index efc91193eac..31fb90f1e09 100644 --- a/src/Node/Expression/FilterExpression.php +++ b/src/Node/Expression/FilterExpression.php @@ -20,9 +20,16 @@ class FilterExpression extends CallExpression { + /** + * @param AbstractExpression $node + */ #[FirstClassTwigCallableReady] public function __construct(Node $node, TwigFilter|ConstantExpression $filter, Node $arguments, int $lineno) { + if (!$node instanceof AbstractExpression) { + trigger_deprecation('twig/twig', '3.15', 'Not passing a "%s" instance to the "node" argument of "%s" is deprecated ("%s" given).', AbstractExpression::class, static::class, get_class($node)); + } + if ($filter instanceof TwigFilter) { $name = $filter->getName(); $filterName = new ConstantExpression($name, $lineno); diff --git a/src/Node/Expression/InlinePrint.php b/src/Node/Expression/InlinePrint.php index 0a3c2e4f9ec..82d17d626de 100644 --- a/src/Node/Expression/InlinePrint.php +++ b/src/Node/Expression/InlinePrint.php @@ -19,8 +19,15 @@ */ final class InlinePrint extends AbstractExpression { + /** + * @param AbstractExpression $node + */ public function __construct(Node $node, int $lineno) { + if (!$node instanceof AbstractExpression) { + trigger_deprecation('twig/twig', '3.15', 'Not passing a "%s" instance to the "node" argument of "%s" is deprecated ("%s" given).', AbstractExpression::class, static::class, get_class($node)); + } + parent::__construct(['node' => $node], [], $lineno); } diff --git a/src/Node/Expression/NullCoalesceExpression.php b/src/Node/Expression/NullCoalesceExpression.php index dd2384f9562..1a5d90286b7 100644 --- a/src/Node/Expression/NullCoalesceExpression.php +++ b/src/Node/Expression/NullCoalesceExpression.php @@ -22,8 +22,19 @@ class NullCoalesceExpression extends ConditionalExpression { + /** + * @param AbstractExpression $left + * @param AbstractExpression $right + */ public function __construct(Node $left, Node $right, int $lineno) { + if (!$left instanceof AbstractExpression) { + trigger_deprecation('twig/twig', '3.15', 'Not passing a "%s" instance to the "left" argument of "%s" is deprecated ("%s" given).', AbstractExpression::class, static::class, get_class($left)); + } + if (!$right instanceof AbstractExpression) { + trigger_deprecation('twig/twig', '3.15', 'Not passing a "%s" instance to the "right" argument of "%s" is deprecated ("%s" given).', AbstractExpression::class, static::class, get_class($right)); + } + $test = new DefinedTest(clone $left, new TwigTest('defined'), new EmptyNode(), $left->getTemplateLine()); // for "block()", we don't need the null test as the return value is always a string if (!$left instanceof BlockReferenceExpression) { diff --git a/src/Node/Expression/Test/DefinedTest.php b/src/Node/Expression/Test/DefinedTest.php index 24d3ee82c9c..005ba39cc4b 100644 --- a/src/Node/Expression/Test/DefinedTest.php +++ b/src/Node/Expression/Test/DefinedTest.php @@ -14,6 +14,7 @@ use Twig\Attribute\FirstClassTwigCallableReady; use Twig\Compiler; use Twig\Error\SyntaxError; +use Twig\Node\Expression\AbstractExpression; use Twig\Node\Expression\ArrayExpression; use Twig\Node\Expression\BlockReferenceExpression; use Twig\Node\Expression\ConstantExpression; @@ -37,9 +38,16 @@ */ class DefinedTest extends TestExpression { + /** + * @param AbstractExpression $node + */ #[FirstClassTwigCallableReady] public function __construct(Node $node, TwigTest|string $name, ?Node $arguments, int $lineno) { + if (!$node instanceof AbstractExpression) { + trigger_deprecation('twig/twig', '3.15', 'Not passing a "%s" instance to the "node" argument of "%s" is deprecated ("%s" given).', AbstractExpression::class, static::class, get_class($node)); + } + if ($node instanceof NameExpression) { $node->setAttribute('is_defined_test', true); } elseif ($node instanceof GetAttrExpression) { diff --git a/src/Node/Expression/TestExpression.php b/src/Node/Expression/TestExpression.php index 080d85aaa5f..3ad6ac5579a 100644 --- a/src/Node/Expression/TestExpression.php +++ b/src/Node/Expression/TestExpression.php @@ -20,8 +20,15 @@ class TestExpression extends CallExpression { #[FirstClassTwigCallableReady] + /** + * @param AbstractExpression $node + */ public function __construct(Node $node, string|TwigTest $test, ?Node $arguments, int $lineno) { + if (!$node instanceof AbstractExpression) { + trigger_deprecation('twig/twig', '3.15', 'Not passing a "%s" instance to the "node" argument of "%s" is deprecated ("%s" given).', AbstractExpression::class, static::class, get_class($node)); + } + $nodes = ['node' => $node]; if (null !== $arguments) { $nodes['arguments'] = $arguments; diff --git a/src/Node/Expression/Unary/AbstractUnary.php b/src/Node/Expression/Unary/AbstractUnary.php index b9b44ed4f41..eeb6af24c3f 100644 --- a/src/Node/Expression/Unary/AbstractUnary.php +++ b/src/Node/Expression/Unary/AbstractUnary.php @@ -18,8 +18,15 @@ abstract class AbstractUnary extends AbstractExpression { + /** + * @param AbstractExpression $node + */ public function __construct(Node $node, int $lineno) { + if (!$node instanceof AbstractExpression) { + trigger_deprecation('twig/twig', '3.15', 'Not passing a "%s" instance argument to "%s" is deprecated ("%s" given).', AbstractExpression::class, static::class, get_class($node)); + } + parent::__construct(['node' => $node], ['with_parentheses' => false], $lineno); } From 43b6e098df51fe8e163e9e335d388e3a116757d5 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Mon, 30 Sep 2024 13:47:10 +0200 Subject: [PATCH 524/812] Change some type hints to be more precise --- src/NodeVisitor/EscaperNodeVisitor.php | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/src/NodeVisitor/EscaperNodeVisitor.php b/src/NodeVisitor/EscaperNodeVisitor.php index a4a415c9a39..c942f825b34 100644 --- a/src/NodeVisitor/EscaperNodeVisitor.php +++ b/src/NodeVisitor/EscaperNodeVisitor.php @@ -17,6 +17,7 @@ use Twig\Node\BlockNode; use Twig\Node\BlockReferenceNode; use Twig\Node\DoNode; +use Twig\Node\Expression\AbstractExpression; use Twig\Node\Expression\ConditionalExpression; use Twig\Node\Expression\ConstantExpression; use Twig\Node\Expression\FilterExpression; @@ -94,8 +95,13 @@ public function leaveNode(Node $node, Environment $env): ?Node private function shouldUnwrapConditional(ConditionalExpression $expression, Environment $env, string $type): bool { - $expr2Safe = $this->isSafeFor($type, $expression->getNode('expr2'), $env); - $expr3Safe = $this->isSafeFor($type, $expression->getNode('expr3'), $env); + /** @var AbstractExpression $expr2 */ + $expr2 = $expression->getNode('expr2'); + /** @var AbstractExpression $expr3 */ + $expr3 = $expression->getNode('expr3'); + + $expr2Safe = $this->isSafeFor($type, $expr2, $env); + $expr3Safe = $this->isSafeFor($type, $expr3, $env); return $expr2Safe !== $expr3Safe; } @@ -103,12 +109,14 @@ private function shouldUnwrapConditional(ConditionalExpression $expression, Envi private function unwrapConditional(ConditionalExpression $expression, Environment $env, string $type): ConditionalExpression { // convert "echo a ? b : c" to "a ? echo b : echo c" recursively + /** @var AbstractExpression $expr2 */ $expr2 = $expression->getNode('expr2'); if ($expr2 instanceof ConditionalExpression && $this->shouldUnwrapConditional($expr2, $env, $type)) { $expr2 = $this->unwrapConditional($expr2, $env, $type); } else { $expr2 = $this->escapeInlinePrintNode(new InlinePrint($expr2, $expr2->getTemplateLine()), $env, $type); } + /** @var AbstractExpression $expr3 */ $expr3 = $expression->getNode('expr3'); if ($expr3 instanceof ConditionalExpression && $this->shouldUnwrapConditional($expr3, $env, $type)) { $expr3 = $this->unwrapConditional($expr3, $env, $type); @@ -116,11 +124,15 @@ private function unwrapConditional(ConditionalExpression $expression, Environmen $expr3 = $this->escapeInlinePrintNode(new InlinePrint($expr3, $expr3->getTemplateLine()), $env, $type); } - return new ConditionalExpression($expression->getNode('expr1'), $expr2, $expr3, $expression->getTemplateLine()); + /** @var AbstractExpression $expr1 */ + $expr1 = $expression->getNode('expr1'); + + return new ConditionalExpression($expr1, $expr2, $expr3, $expression->getTemplateLine()); } - private function escapeInlinePrintNode(InlinePrint $node, Environment $env, string $type): Node + private function escapeInlinePrintNode(InlinePrint $node, Environment $env, string $type): AbstractExpression { + /** @var AbstractExpression $expression */ $expression = $node->getNode('node'); if ($this->isSafeFor($type, $expression, $env)) { @@ -132,6 +144,7 @@ private function escapeInlinePrintNode(InlinePrint $node, Environment $env, stri private function escapePrintNode(PrintNode $node, Environment $env, string $type): Node { + /** @var AbstractExpression $expression */ $expression = $node->getNode('expr'); if ($this->isSafeFor($type, $expression, $env)) { @@ -157,6 +170,7 @@ private function preEscapeFilterNode(FilterExpression $filter, Environment $env) return $filter; } + /** @var AbstractExpression $node */ $node = $filter->getNode('node'); if ($this->isSafeFor($type, $node, $env)) { return $filter; @@ -167,7 +181,7 @@ private function preEscapeFilterNode(FilterExpression $filter, Environment $env) return $filter; } - private function isSafeFor(string $type, Node $expression, Environment $env): bool + private function isSafeFor(string $type, AbstractExpression $expression, Environment $env): bool { $safe = $this->safeAnalysis->getSafe($expression); @@ -194,7 +208,7 @@ private function needEscaping() return $this->defaultStrategy ?: false; } - private function getEscaperFilter(Environment $env, string $type, Node $node): FilterExpression + private function getEscaperFilter(Environment $env, string $type, AbstractExpression $node): FilterExpression { $line = $node->getTemplateLine(); $filter = $env->getFilter('escape'); From 816c510cef96d99cf489b08f742992c4fdcb0b03 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sun, 29 Sep 2024 22:59:41 +0200 Subject: [PATCH 525/812] =?UTF-8?q?Deprecate=20using=20~=C2=A0with=20+=20o?= =?UTF-8?q?r=20-=20in=20an=20expression=20without=20using=20parentheses=20?= =?UTF-8?q?to=20clarify=20precedence?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG | 1 + doc/deprecated.rst | 21 +++++++++++++++++++ src/ExpressionParser.php | 14 +++++++++++++ src/Extension/CoreExtension.php | 1 + .../Fixtures/operators/concat_vs_add_sub.test | 16 ++++++++++++++ .../operators/contat_vs_add_sub.legacy.test | 13 ++++++++++++ 6 files changed, 66 insertions(+) create mode 100644 tests/Fixtures/operators/concat_vs_add_sub.test create mode 100644 tests/Fixtures/operators/contat_vs_add_sub.legacy.test diff --git a/CHANGELOG b/CHANGELOG index f4a07a9c71c..6100fefe9be 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,6 @@ # 3.15.0 (2024-XX-XX) + * Deprecate using `~` with `+` or `-` in an expression without using parentheses to clarify precedence * Deprecate not passing `AbstractExpression` args to most constructor arguments for classes extending `AbstractExpression` * Fix `power` expressions with a negative number in parenthesis (`(-1) ** 2`) * Deprecate instantiating `Node` directly. Use `EmptyNode` or `Nodes` instead. diff --git a/doc/deprecated.rst b/doc/deprecated.rst index afcec62f597..10cc1214cdd 100644 --- a/doc/deprecated.rst +++ b/doc/deprecated.rst @@ -310,3 +310,24 @@ Node * ``DefaultFilter`` * ``InlinePrint`` * ``NullCoalesceExpression`` + +Operators +--------- + +* Using ``~`` with ``+`` or ``-`` in an expression without using parentheses to + clarify precedence is deprecated as of Twig 3.15 (in Twig 4.0, parentheses + won't be needed anymore as ``+`` / ``-`` will have a higher precedence than + ``~``). + + For example, the following expression will trigger a deprecation in Twig 3.15:: + + {{ '42' ~ 1 + 41 }} + + To avoid the deprecation, wrap the concatenation in parentheses to clarify + the precedence:: + + {{ ('42' ~ 1) + 41 }} {# this is equivalent to what Twig 3.x does without the parentheses #} + + {# or #} + + {{ '42' ~ (1 + 41) }} {# this is equivalent to what Twig 4.x will do without the parentheses #} diff --git a/src/ExpressionParser.php b/src/ExpressionParser.php index 33da2b29e77..db6b1555031 100644 --- a/src/ExpressionParser.php +++ b/src/ExpressionParser.php @@ -20,7 +20,9 @@ use Twig\Node\Expression\ArrowFunctionExpression; use Twig\Node\Expression\AssignNameExpression; use Twig\Node\Expression\Binary\AbstractBinary; +use Twig\Node\Expression\Binary\AddBinary; use Twig\Node\Expression\Binary\ConcatBinary; +use Twig\Node\Expression\Binary\SubBinary; use Twig\Node\Expression\ConditionalExpression; use Twig\Node\Expression\ConstantExpression; use Twig\Node\Expression\GetAttrExpression; @@ -91,6 +93,18 @@ public function parseExpression($precedence = 0, $allowArrow = false) $token = $this->parser->getCurrentToken(); } + if ( + ($expr instanceof AddBinary || $expr instanceof SubBinary) + && + ( + ($expr->getNode('left') instanceof ConcatBinary && !$expr->getNode('left')->hasExplicitParentheses()) + || + ($expr->getNode('right') instanceof ConcatBinary && !$expr->getNode('right')->hasExplicitParentheses()) + ) + ) { + trigger_deprecation('twig/twig', '3.15', \sprintf('As "+" / "-" will have a higher precedence than "~" in Twig 4.0, please add parentheses to keep the current behavior in "%s" at line %d.', $this->parser->getStream()->getSourceContext()->getName(), $token->getLine())); + } + if (0 === $precedence) { return $this->parseConditionalExpression($expr); } diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index 4a5bf233f71..1dc051d8b2a 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -323,6 +323,7 @@ public function getOperators(): array '..' => ['precedence' => 25, 'class' => RangeBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT], '+' => ['precedence' => 30, 'class' => AddBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT], '-' => ['precedence' => 30, 'class' => SubBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT], + // Precedence of the ~ operator will change to 27 in Twig 4.0 '~' => ['precedence' => 40, 'class' => ConcatBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT], '*' => ['precedence' => 60, 'class' => MulBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT], '/' => ['precedence' => 60, 'class' => DivBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT], diff --git a/tests/Fixtures/operators/concat_vs_add_sub.test b/tests/Fixtures/operators/concat_vs_add_sub.test new file mode 100644 index 00000000000..298a3499b5e --- /dev/null +++ b/tests/Fixtures/operators/concat_vs_add_sub.test @@ -0,0 +1,16 @@ +--TEST-- ++/- will have a higher precedence over ~ in Twig 4.0 +--TEMPLATE-- +{{ 1 + 41 }} +{{ '42==' ~ '42' }} +{{ '42==' ~ (1 + 41) }} +{{ '42==' ~ (43 - 1) }} +{{ ('42' ~ 43) - 1 }} +--DATA-- +return [] +--EXPECT-- +42 +42==42 +42==42 +42==42 +4242 diff --git a/tests/Fixtures/operators/contat_vs_add_sub.legacy.test b/tests/Fixtures/operators/contat_vs_add_sub.legacy.test new file mode 100644 index 00000000000..dc4a1ef5077 --- /dev/null +++ b/tests/Fixtures/operators/contat_vs_add_sub.legacy.test @@ -0,0 +1,13 @@ +--TEST-- ++/- will have a higher precedence over ~ in Twig 4.0 +--DEPRECATION-- +Since twig/twig 3.15: As "+" / "-" will have a higher precedence than "~" in Twig 4.0, please add parentheses to keep the current behavior in "index.twig" at line 2. +Since twig/twig 3.15: As "+" / "-" will have a higher precedence than "~" in Twig 4.0, please add parentheses to keep the current behavior in "index.twig" at line 3. +--TEMPLATE-- +{{ '42' ~ 1 + 41 }} +{{ '42' ~ 43 - 1 }} +--DATA-- +return [] +--EXPECT-- +462 +4242 From 9f8c52d022c4180157809051c804ed39be25288e Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Mon, 30 Sep 2024 14:02:17 +0200 Subject: [PATCH 526/812] Add some type hints --- src/ExpressionParser.php | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/ExpressionParser.php b/src/ExpressionParser.php index db6b1555031..23b24aee26a 100644 --- a/src/ExpressionParser.php +++ b/src/ExpressionParser.php @@ -95,14 +95,18 @@ public function parseExpression($precedence = 0, $allowArrow = false) if ( ($expr instanceof AddBinary || $expr instanceof SubBinary) - && - ( - ($expr->getNode('left') instanceof ConcatBinary && !$expr->getNode('left')->hasExplicitParentheses()) - || - ($expr->getNode('right') instanceof ConcatBinary && !$expr->getNode('right')->hasExplicitParentheses()) - ) ) { - trigger_deprecation('twig/twig', '3.15', \sprintf('As "+" / "-" will have a higher precedence than "~" in Twig 4.0, please add parentheses to keep the current behavior in "%s" at line %d.', $this->parser->getStream()->getSourceContext()->getName(), $token->getLine())); + /** @var AbstractExpression $left */ + $left = $expr->getNode('left'); + /** @var AbstractExpression $right */ + $right = $expr->getNode('right'); + if ( + ($left instanceof ConcatBinary && !$left->hasExplicitParentheses()) + || + ($right instanceof ConcatBinary && !$right->hasExplicitParentheses()) + ) { + trigger_deprecation('twig/twig', '3.15', \sprintf('As "+" / "-" will have a higher precedence than "~" in Twig 4.0, please add parentheses to keep the current behavior in "%s" at line %d.', $this->parser->getStream()->getSourceContext()->getName(), $token->getLine())); + } } if (0 === $precedence) { From b93eb3ce5830ba64bcaeb0eaca02244d67d2e663 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Mon, 30 Sep 2024 14:05:03 +0200 Subject: [PATCH 527/812] Refactor code --- src/ExpressionParser.php | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/src/ExpressionParser.php b/src/ExpressionParser.php index 23b24aee26a..ed61dcc2afb 100644 --- a/src/ExpressionParser.php +++ b/src/ExpressionParser.php @@ -93,9 +93,19 @@ public function parseExpression($precedence = 0, $allowArrow = false) $token = $this->parser->getCurrentToken(); } - if ( - ($expr instanceof AddBinary || $expr instanceof SubBinary) - ) { + $this->triggerPrecedenceDeprecations($expr, $token); + + if (0 === $precedence) { + return $this->parseConditionalExpression($expr); + } + + return $expr; + } + + private function triggerPrecedenceDeprecations(AbstractExpression $expr, Token $token): void + { + // Precedence of the ~ operator will be lower than + and - in Twig 4.0 + if ($expr instanceof AddBinary || $expr instanceof SubBinary) { /** @var AbstractExpression $left */ $left = $expr->getNode('left'); /** @var AbstractExpression $right */ @@ -108,12 +118,6 @@ public function parseExpression($precedence = 0, $allowArrow = false) trigger_deprecation('twig/twig', '3.15', \sprintf('As "+" / "-" will have a higher precedence than "~" in Twig 4.0, please add parentheses to keep the current behavior in "%s" at line %d.', $this->parser->getStream()->getSourceContext()->getName(), $token->getLine())); } } - - if (0 === $precedence) { - return $this->parseConditionalExpression($expr); - } - - return $expr; } /** From 9690e7bbbf88076ddbc9c4728a7435c4cd5c204f Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sun, 29 Sep 2024 18:29:35 +0200 Subject: [PATCH 528/812] Document Twig vs PHP types --- doc/templates.rst | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/doc/templates.rst b/doc/templates.rst index 6626d4ee896..b651851ee91 100644 --- a/doc/templates.rst +++ b/doc/templates.rst @@ -76,8 +76,24 @@ Twig templates have access to variables provided by the PHP application and variables created in templates via the :doc:`set ` tag. These variables can be manipulated and displayed in the template. -Use a dot (``.``) to access attributes of a variable (methods, properties -or constants of a PHP object, or items of a PHP array): +Twig tries to abstract PHP types as much as possible and works with a few basic +types, supported by ``filters``, ``functions``, and ``tests`` among others: + +=================== =============================== +Twig Type PHP Type +=================== =============================== +string A string or a Stringable object +number An integer or a float +boolean ``true`` or ``false`` +null ``null`` +iterable (mapping) An array +iterable (sequence) An array +iterable (object) An iterable object +object An object +=================== =============================== + +The ``iterable`` and ``object`` types expose attributes you can access via the +dot (``.``) operator: .. code-block:: twig From f16382ba914889b1e40d4fcedfb7a188a2d4e74b Mon Sep 17 00:00:00 2001 From: nicolas-joubert Date: Mon, 30 Sep 2024 17:23:51 +0200 Subject: [PATCH 529/812] [Doc] Fix enum title Copy/Paste fix from enum_case documentation page --- doc/functions/enum.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/functions/enum.rst b/doc/functions/enum.rst index 3c9d1a7b498..0c1b5b200aa 100644 --- a/doc/functions/enum.rst +++ b/doc/functions/enum.rst @@ -1,5 +1,5 @@ -``enum_cases`` -============== +``enum`` +======== .. versionadded:: 3.15 From 6276d143c31e8a60070fe7132ddc4a41bdec656b Mon Sep 17 00:00:00 2001 From: HypeMC Date: Tue, 1 Oct 2024 19:57:51 +0200 Subject: [PATCH 530/812] Allow running CI on all branches --- .github/workflows/ci.yml | 2 -- .github/workflows/documentation.yml | 3 --- 2 files changed, 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3de8aa10cdd..3b3472cf321 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,8 +3,6 @@ name: "CI" on: pull_request: push: - branches: - - '3.x' env: SYMFONY_PHPUNIT_DISABLE_RESULT_CACHE: 1 diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml index 8fe7c868c29..8e6d5011810 100644 --- a/.github/workflows/documentation.yml +++ b/.github/workflows/documentation.yml @@ -3,9 +3,6 @@ name: "Documentation" on: pull_request: push: - branches: - - '2.x' - - '3.x' permissions: contents: read From 8893944e0cdf8e355b16773de065a7a7f2547d4b Mon Sep 17 00:00:00 2001 From: HypeMC Date: Tue, 1 Oct 2024 18:43:07 +0200 Subject: [PATCH 531/812] Add support for logical `xor` operator --- CHANGELOG | 1 + doc/templates.rst | 3 +++ src/Extension/CoreExtension.php | 2 ++ src/Node/Expression/Binary/XorBinary.php | 23 +++++++++++++++++++++++ tests/Fixtures/expressions/binary.test | 14 +++++++++++++- 5 files changed, 42 insertions(+), 1 deletion(-) create mode 100644 src/Node/Expression/Binary/XorBinary.php diff --git a/CHANGELOG b/CHANGELOG index 6100fefe9be..90427309b5e 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -19,6 +19,7 @@ * Deprecate the `sandbox` tag * Improve the way one can deprecate a Twig callable (use `deprecation_info` instead of the other callable options) * Add the `enum` function + * Add support for logical `xor` operator # 3.14.0 (2024-09-09) diff --git a/doc/templates.rst b/doc/templates.rst index b651851ee91..bfe2a1c2079 100644 --- a/doc/templates.rst +++ b/doc/templates.rst @@ -708,6 +708,8 @@ You can combine multiple expressions with the following operators: * ``and``: Returns true if the left and the right operands are both true. +* ``xor``: Returns true if **either** the left or the right operand is true, but not both. + * ``or``: Returns true if the left or the right operand is true. * ``not``: Negates a statement. @@ -958,6 +960,7 @@ Operator Score of precedence Description ============================= =================================== ===================================================== ``?:`` 0 Ternary operator, conditional statement ``or`` 10 Logical OR operation between two boolean expressions +``xor`` 12 Logical XOR operation between two boolean expressions ``and`` 15 Logical AND operation between two boolean expressions ``b-or`` 16 Bitwise OR operation on integers ``b-xor`` 17 Bitwise XOR operation on integers diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index 1dc051d8b2a..43211d3eed8 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -47,6 +47,7 @@ use Twig\Node\Expression\Binary\SpaceshipBinary; use Twig\Node\Expression\Binary\StartsWithBinary; use Twig\Node\Expression\Binary\SubBinary; +use Twig\Node\Expression\Binary\XorBinary; use Twig\Node\Expression\BlockReferenceExpression; use Twig\Node\Expression\Filter\DefaultFilter; use Twig\Node\Expression\FunctionNode\EnumCasesFunction; @@ -302,6 +303,7 @@ public function getOperators(): array ], [ 'or' => ['precedence' => 10, 'class' => OrBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT], + 'xor' => ['precedence' => 12, 'class' => XorBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT], 'and' => ['precedence' => 15, 'class' => AndBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT], 'b-or' => ['precedence' => 16, 'class' => BitwiseOrBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT], 'b-xor' => ['precedence' => 17, 'class' => BitwiseXorBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT], diff --git a/src/Node/Expression/Binary/XorBinary.php b/src/Node/Expression/Binary/XorBinary.php new file mode 100644 index 00000000000..d8ccd785362 --- /dev/null +++ b/src/Node/Expression/Binary/XorBinary.php @@ -0,0 +1,23 @@ +raw('xor'); + } +} diff --git a/tests/Fixtures/expressions/binary.test b/tests/Fixtures/expressions/binary.test index b4e8be58d3c..f7252e95477 100644 --- a/tests/Fixtures/expressions/binary.test +++ b/tests/Fixtures/expressions/binary.test @@ -1,5 +1,5 @@ --TEST-- -Twig supports binary operations (+, -, *, /, ~, %, and, or) +Twig supports binary operations (+, -, *, /, ~, %, and, xor, or) --TEMPLATE-- {{ 1 + 1 }} {{ 2 - 1 }} @@ -16,6 +16,12 @@ Twig supports binary operations (+, -, *, /, ~, %, and, or) {{ 0 or 0 }} {{ 0 or 1 and 0 }} {{ 1 or 0 and 1 }} +{{ 1 xor 1 }} +{{ 1 xor 0 }} +{{ 0 xor 1 }} +{{ 0 xor 0 }} +{{ 0 and 1 or 1 xor 1 }} +{{ 0 and 1 or 0 xor 1 }} {{ "foo" ~ "bar" }} {{ foo ~ "bar" }} {{ "foo" ~ bar }} @@ -38,6 +44,12 @@ return ['foo' => 'bar', 'bar' => 'foo'] 1 +1 + +1 +1 + + 1 foobar barbar From f4aacafd78505186ee2c6aa8f35eade954420a9b Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Mon, 30 Sep 2024 11:51:58 +0200 Subject: [PATCH 532/812] Deprecate using ?? without explicit parentheses --- CHANGELOG | 1 + doc/deprecated.rst | 23 +++++++-- src/ExpressionParser.php | 49 +++++++++++-------- src/Extension/CoreExtension.php | 5 +- tests/EnvironmentTest.php | 2 +- .../operators/contat_vs_add_sub.legacy.test | 4 +- .../Fixtures/tests/null_coalesce.legacy.test | 16 ++++++ tests/Fixtures/tests/null_coalesce.test | 6 +-- 8 files changed, 73 insertions(+), 33 deletions(-) create mode 100644 tests/Fixtures/tests/null_coalesce.legacy.test diff --git a/CHANGELOG b/CHANGELOG index 6100fefe9be..05c8d5615ec 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,6 @@ # 3.15.0 (2024-XX-XX) + * Deprecate using `??` without explicit parentheses * Deprecate using `~` with `+` or `-` in an expression without using parentheses to clarify precedence * Deprecate not passing `AbstractExpression` args to most constructor arguments for classes extending `AbstractExpression` * Fix `power` expressions with a negative number in parenthesis (`(-1) ** 2`) diff --git a/doc/deprecated.rst b/doc/deprecated.rst index 10cc1214cdd..7bcf5d249b5 100644 --- a/doc/deprecated.rst +++ b/doc/deprecated.rst @@ -315,9 +315,9 @@ Operators --------- * Using ``~`` with ``+`` or ``-`` in an expression without using parentheses to - clarify precedence is deprecated as of Twig 3.15 (in Twig 4.0, parentheses - won't be needed anymore as ``+`` / ``-`` will have a higher precedence than - ``~``). + clarify precedence triggers a deprecation as of Twig 3.15 (in Twig 4.0, + parentheses won't be needed anymore as ``+`` / ``-`` will have a higher + precedence than ``~``). For example, the following expression will trigger a deprecation in Twig 3.15:: @@ -331,3 +331,20 @@ Operators {# or #} {{ '42' ~ (1 + 41) }} {# this is equivalent to what Twig 4.x will do without the parentheses #} + +* Using ``??`` without explicit parentheses to clarify precedence triggers a + deprecation as of Twig 3.15 (in Twig 4.0, parentheses won't be needed anymore + as ``??`` will have the lowest precedence). + + For example, the following expression will trigger a deprecation in Twig 3.15:: + + {{ 'notnull' ?? 'foo' ~ '_bar' }} + + To avoid the deprecation, wrap the ``??`` expressionin parentheses to clarify + the precedence:: + + {{ ('notnull' ?? 'foo') ~ '_bar' }} {# this is equivalent to what Twig 3.x does without the parentheses #} + + {# or #} + + {{ 'notnull' ?? ('foo' ~ '_bar') }} {# this is equivalent to what Twig 4.x will do without the parentheses #} diff --git a/src/ExpressionParser.php b/src/ExpressionParser.php index ed61dcc2afb..f7cf488ba44 100644 --- a/src/ExpressionParser.php +++ b/src/ExpressionParser.php @@ -20,9 +20,7 @@ use Twig\Node\Expression\ArrowFunctionExpression; use Twig\Node\Expression\AssignNameExpression; use Twig\Node\Expression\Binary\AbstractBinary; -use Twig\Node\Expression\Binary\AddBinary; use Twig\Node\Expression\Binary\ConcatBinary; -use Twig\Node\Expression\Binary\SubBinary; use Twig\Node\Expression\ConditionalExpression; use Twig\Node\Expression\ConstantExpression; use Twig\Node\Expression\GetAttrExpression; @@ -57,6 +55,7 @@ class ExpressionParser /** @var array, associativity: self::OPERATOR_*}> */ private $binaryOperators; private $readyNodes = []; + private array $precedenceChanges = []; public function __construct( private Parser $parser, @@ -64,6 +63,20 @@ public function __construct( ) { $this->unaryOperators = $env->getUnaryOperators(); $this->binaryOperators = $env->getBinaryOperators(); + + foreach ($this->binaryOperators as $name => $config) { + if (!isset($config['future_precedence'])) { + continue; + } + + $min = min($config['future_precedence'], $config['precedence']); + $max = max($config['future_precedence'], $config['precedence']); + foreach ($this->binaryOperators as $n => $c) { + if ($c['precedence'] > $min && $c['precedence'] < $max) { + $this->precedenceChanges[$n][] = $name; + } + } + } } public function parseExpression($precedence = 0, $allowArrow = false) @@ -90,10 +103,22 @@ public function parseExpression($precedence = 0, $allowArrow = false) $expr = new $class($expr, $expr1, $token->getLine()); } + $expr->setAttribute('operator', $token->getValue()); + $token = $this->parser->getCurrentToken(); } - $this->triggerPrecedenceDeprecations($expr, $token); + // Check that the all nodes that are between the 2 precedences have explicit parentheses + if ($expr->hasAttribute('operator') && isset($this->precedenceChanges[$expr->getAttribute('operator')])) { + foreach ($this->precedenceChanges[$expr->getAttribute('operator')] as $operatorName) { + foreach ($expr as $node) { + /** @var AbstractExpression $node */ + if ($node->hasAttribute('operator') && $operatorName === $node->getAttribute('operator') && !$node->hasExplicitParentheses()) { + trigger_deprecation('twig/twig', '3.15', \sprintf('Add explicit parentheses around the "%s" operator to avoid behavior change in Twig 4.0 as its precedence will change in "%s" at line %d.', $operatorName, $this->parser->getStream()->getSourceContext()->getName(), $token->getLine())); + } + } + } + } if (0 === $precedence) { return $this->parseConditionalExpression($expr); @@ -102,24 +127,6 @@ public function parseExpression($precedence = 0, $allowArrow = false) return $expr; } - private function triggerPrecedenceDeprecations(AbstractExpression $expr, Token $token): void - { - // Precedence of the ~ operator will be lower than + and - in Twig 4.0 - if ($expr instanceof AddBinary || $expr instanceof SubBinary) { - /** @var AbstractExpression $left */ - $left = $expr->getNode('left'); - /** @var AbstractExpression $right */ - $right = $expr->getNode('right'); - if ( - ($left instanceof ConcatBinary && !$left->hasExplicitParentheses()) - || - ($right instanceof ConcatBinary && !$right->hasExplicitParentheses()) - ) { - trigger_deprecation('twig/twig', '3.15', \sprintf('As "+" / "-" will have a higher precedence than "~" in Twig 4.0, please add parentheses to keep the current behavior in "%s" at line %d.', $this->parser->getStream()->getSourceContext()->getName(), $token->getLine())); - } - } - } - /** * @return ArrowFunctionExpression|null */ diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index 1dc051d8b2a..489c2c266b0 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -323,8 +323,7 @@ public function getOperators(): array '..' => ['precedence' => 25, 'class' => RangeBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT], '+' => ['precedence' => 30, 'class' => AddBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT], '-' => ['precedence' => 30, 'class' => SubBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT], - // Precedence of the ~ operator will change to 27 in Twig 4.0 - '~' => ['precedence' => 40, 'class' => ConcatBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT], + '~' => ['precedence' => 40, 'future_precedence' => 27, 'class' => ConcatBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT], '*' => ['precedence' => 60, 'class' => MulBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT], '/' => ['precedence' => 60, 'class' => DivBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT], '//' => ['precedence' => 60, 'class' => FloorDivBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT], @@ -332,7 +331,7 @@ public function getOperators(): array 'is' => ['precedence' => 100, 'associativity' => ExpressionParser::OPERATOR_LEFT], 'is not' => ['precedence' => 100, 'associativity' => ExpressionParser::OPERATOR_LEFT], '**' => ['precedence' => 200, 'class' => PowerBinary::class, 'associativity' => ExpressionParser::OPERATOR_RIGHT], - '??' => ['precedence' => 300, 'class' => NullCoalesceExpression::class, 'associativity' => ExpressionParser::OPERATOR_RIGHT], + '??' => ['precedence' => 300, 'future_precedence' => 5, 'class' => NullCoalesceExpression::class, 'associativity' => ExpressionParser::OPERATOR_RIGHT], ], ]; } diff --git a/tests/EnvironmentTest.php b/tests/EnvironmentTest.php index 7bbc61cef58..d5aee8d1bbe 100644 --- a/tests/EnvironmentTest.php +++ b/tests/EnvironmentTest.php @@ -598,7 +598,7 @@ public function getOperators(): array { return [ ['foo_unary' => []], - ['foo_binary' => []], + ['foo_binary' => ['precedence' => 0]], ]; } diff --git a/tests/Fixtures/operators/contat_vs_add_sub.legacy.test b/tests/Fixtures/operators/contat_vs_add_sub.legacy.test index dc4a1ef5077..c032cf2907b 100644 --- a/tests/Fixtures/operators/contat_vs_add_sub.legacy.test +++ b/tests/Fixtures/operators/contat_vs_add_sub.legacy.test @@ -1,8 +1,8 @@ --TEST-- +/- will have a higher precedence over ~ in Twig 4.0 --DEPRECATION-- -Since twig/twig 3.15: As "+" / "-" will have a higher precedence than "~" in Twig 4.0, please add parentheses to keep the current behavior in "index.twig" at line 2. -Since twig/twig 3.15: As "+" / "-" will have a higher precedence than "~" in Twig 4.0, please add parentheses to keep the current behavior in "index.twig" at line 3. +Since twig/twig 3.15: Add explicit parentheses around the "~" operator to avoid behavior change in Twig 4.0 as its precedence will change in "index.twig" at line 2. +Since twig/twig 3.15: Add explicit parentheses around the "~" operator to avoid behavior change in Twig 4.0 as its precedence will change in "index.twig" at line 3. --TEMPLATE-- {{ '42' ~ 1 + 41 }} {{ '42' ~ 43 - 1 }} diff --git a/tests/Fixtures/tests/null_coalesce.legacy.test b/tests/Fixtures/tests/null_coalesce.legacy.test new file mode 100644 index 00000000000..b9e3a87754c --- /dev/null +++ b/tests/Fixtures/tests/null_coalesce.legacy.test @@ -0,0 +1,16 @@ +--TEST-- +Twig supports the ?? operator +--DEPRECATION-- +Since twig/twig 3.15: Add explicit parentheses around the "??" operator to avoid behavior change in Twig 4.0 as its precedence will change in "index.twig" at line 4. +Since twig/twig 3.15: Add explicit parentheses around the "??" operator to avoid behavior change in Twig 4.0 as its precedence will change in "index.twig" at line 5. +--TEMPLATE-- +{{ nope ?? nada ?? 'OK' -}} {# no deprecation as the operators have the same precedence #} + +{{ 1 + nope ?? nada ?? 2 }} +{{ 1 + nope ?? 3 + nada ?? 2 }} +--DATA-- +return [] +--EXPECT-- +OK +3 +6 diff --git a/tests/Fixtures/tests/null_coalesce.test b/tests/Fixtures/tests/null_coalesce.test index 7af3255d61d..f80b907178d 100644 --- a/tests/Fixtures/tests/null_coalesce.test +++ b/tests/Fixtures/tests/null_coalesce.test @@ -10,9 +10,9 @@ Twig supports the ?? operator {{ foo.bar.baz.missing ?? 'OK' }} {{ foo['bar'] ?? 'KO' }} {{ foo['missing'] ?? 'OK' }} -{{ nope ?? nada ?? 'OK' }} -{{ 1 + nope ?? nada ?? 2 }} -{{ 1 + nope ?? 3 + nada ?? 2 }} +{{ nope ?? (nada ?? 'OK') }} +{{ 1 + (nope ?? (nada ?? 2)) }} +{{ 1 + (nope ?? 3) + (nada ?? 2) }} --DATA-- return ['bar' => 'OK', 'foo' => ['bar' => 'OK']] --EXPECT-- From 2081b1ff74aeb1ee3a5732487c8de7fe1420284e Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Tue, 1 Oct 2024 22:30:54 +0200 Subject: [PATCH 533/812] Deprecate using the not unary operator without parenthesis --- CHANGELOG | 6 +- doc/deprecated.rst | 31 +++++-- src/ExpressionParser.php | 80 ++++++++++++++----- src/Extension/CoreExtension.php | 7 +- src/Extension/ExtensionInterface.php | 4 +- src/OperatorPrecedenceChange.php | 42 ++++++++++ tests/EnvironmentTest.php | 2 +- .../operators/contat_vs_add_sub.legacy.test | 4 +- .../operators/not_precedence.legacy.test | 9 +++ tests/Fixtures/operators/not_precedence.test | 9 +++ .../Fixtures/tests/null_coalesce.legacy.test | 4 +- 11 files changed, 159 insertions(+), 39 deletions(-) create mode 100644 src/OperatorPrecedenceChange.php create mode 100644 tests/Fixtures/operators/not_precedence.legacy.test create mode 100644 tests/Fixtures/operators/not_precedence.test diff --git a/CHANGELOG b/CHANGELOG index 05c8d5615ec..c4a17f7352c 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,7 +1,9 @@ # 3.15.0 (2024-XX-XX) - * Deprecate using `??` without explicit parentheses - * Deprecate using `~` with `+` or `-` in an expression without using parentheses to clarify precedence + * Add support for triggering deprecations for future operator precedence changes + * Deprecate using the `not` unary operator in an expression with ``*``, ``/``, ``//``, or ``%`` without using explicit parentheses to clarify precedence + * Deprecate using the `??` binary operator without explicit parentheses + * Deprecate using the `~` binary operator in an expression with `+` or `-` without using parentheses to clarify precedence * Deprecate not passing `AbstractExpression` args to most constructor arguments for classes extending `AbstractExpression` * Fix `power` expressions with a negative number in parenthesis (`(-1) ** 2`) * Deprecate instantiating `Node` directly. Use `EmptyNode` or `Nodes` instead. diff --git a/doc/deprecated.rst b/doc/deprecated.rst index 7bcf5d249b5..e1967addaae 100644 --- a/doc/deprecated.rst +++ b/doc/deprecated.rst @@ -314,10 +314,9 @@ Node Operators --------- -* Using ``~`` with ``+`` or ``-`` in an expression without using parentheses to - clarify precedence triggers a deprecation as of Twig 3.15 (in Twig 4.0, - parentheses won't be needed anymore as ``+`` / ``-`` will have a higher - precedence than ``~``). +* Using ``~`` in an expression with the ``+`` or ``-`` operators without using + parentheses to clarify precedence triggers a deprecation as of Twig 3.15 (in + Twig 4.0, ``+`` / ``-`` will have a higher precedence than ``~``). For example, the following expression will trigger a deprecation in Twig 3.15:: @@ -333,14 +332,14 @@ Operators {{ '42' ~ (1 + 41) }} {# this is equivalent to what Twig 4.x will do without the parentheses #} * Using ``??`` without explicit parentheses to clarify precedence triggers a - deprecation as of Twig 3.15 (in Twig 4.0, parentheses won't be needed anymore - as ``??`` will have the lowest precedence). + deprecation as of Twig 3.15 (in Twig 4.0, ``??`` will have the lowest + precedence). For example, the following expression will trigger a deprecation in Twig 3.15:: {{ 'notnull' ?? 'foo' ~ '_bar' }} - To avoid the deprecation, wrap the ``??`` expressionin parentheses to clarify + To avoid the deprecation, wrap the ``??`` expression in parentheses to clarify the precedence:: {{ ('notnull' ?? 'foo') ~ '_bar' }} {# this is equivalent to what Twig 3.x does without the parentheses #} @@ -348,3 +347,21 @@ Operators {# or #} {{ 'notnull' ?? ('foo' ~ '_bar') }} {# this is equivalent to what Twig 4.x will do without the parentheses #} + +* Using the ``not`` unary operator in an expression with ``*``, ``/``, ``//``, + or ``%`` operators without explicit parentheses to clarify precedence + triggers a deprecation as of Twig 3.15 (in Twig 4.0, ``not`` will have a + higher precedence than ``*``, ``/``, ``//``, and ``%``). + + For example, the following expression will trigger a deprecation in Twig 3.15:: + + {{ not 1 * 2 }} + + To avoid the deprecation, wrap the concatenation in parentheses to clarify + the precedence:: + + {{ (not 1 * 2) }} {# this is equivalent to what Twig 3.x does without the parentheses #} + + {# or #} + + {{ (not 1) * 2 }} {# this is equivalent to what Twig 4.x will do without the parentheses #} diff --git a/src/ExpressionParser.php b/src/ExpressionParser.php index f7cf488ba44..01cb9b93461 100644 --- a/src/ExpressionParser.php +++ b/src/ExpressionParser.php @@ -64,29 +64,36 @@ public function __construct( $this->unaryOperators = $env->getUnaryOperators(); $this->binaryOperators = $env->getBinaryOperators(); - foreach ($this->binaryOperators as $name => $config) { - if (!isset($config['future_precedence'])) { + $ops = []; + foreach ($this->unaryOperators as $n => $c) { + $ops[] = $c + ['name' => $n, 'type' => 'unary']; + } + foreach ($this->binaryOperators as $n => $c) { + $ops[] = $c + ['name' => $n, 'type' => 'binary']; + } + foreach ($ops as $config) { + if (!isset($config['precedence_change'])) { continue; } - - $min = min($config['future_precedence'], $config['precedence']); - $max = max($config['future_precedence'], $config['precedence']); - foreach ($this->binaryOperators as $n => $c) { + $name = $config['type'].'_'.$config['name']; + $min = min($config['precedence_change']->getNewPrecedence(), $config['precedence']); + $max = max($config['precedence_change']->getNewPrecedence(), $config['precedence']); + foreach ($ops as $c) { if ($c['precedence'] > $min && $c['precedence'] < $max) { - $this->precedenceChanges[$n][] = $name; + $this->precedenceChanges[$c['type'].'_'.$c['name']][] = $name; } } } } - public function parseExpression($precedence = 0, $allowArrow = false) + public function parseExpression($precedence = 0, $allowArrow = false, bool $deprecationCheck = true) { if ($allowArrow && $arrow = $this->parseArrow()) { return $arrow; } $expr = $this->getPrimary(); - $token = $this->parser->getCurrentToken(); + $previousToken = $token = $this->parser->getCurrentToken(); while ($this->isBinary($token) && $this->binaryOperators[$token->getValue()]['precedence'] >= $precedence) { $op = $this->binaryOperators[$token->getValue()]; $this->parser->getStream()->next(); @@ -103,28 +110,58 @@ public function parseExpression($precedence = 0, $allowArrow = false) $expr = new $class($expr, $expr1, $token->getLine()); } - $expr->setAttribute('operator', $token->getValue()); + $expr->setAttribute('operator', 'binary_'.$token->getValue()); + $previousToken = $token; $token = $this->parser->getCurrentToken(); } + if ($deprecationCheck) { + $this->triggerPrecedenceDeprecations($expr, $previousToken); + } + + if (0 === $precedence) { + return $this->parseConditionalExpression($expr); + } + + return $expr; + } + + private function triggerPrecedenceDeprecations(AbstractExpression $expr, Token $token): void + { // Check that the all nodes that are between the 2 precedences have explicit parentheses - if ($expr->hasAttribute('operator') && isset($this->precedenceChanges[$expr->getAttribute('operator')])) { + if (!$expr->hasAttribute('operator') || !isset($this->precedenceChanges[$expr->getAttribute('operator')])) { + return; + } + + if (str_starts_with($unaryOp = $expr->getAttribute('operator'), 'unary')) { + if ($expr->hasExplicitParentheses()) { + return; + } + $target = explode('_', $unaryOp)[1]; + $change = $this->unaryOperators[$target]['precedence_change']; + /** @var AbstractExpression $node */ + $node = $expr->getNode('node'); + foreach ($this->precedenceChanges as $operatorName => $changes) { + if (!in_array($unaryOp, $changes)) { + continue; + } + if ($node->hasAttribute('operator') && $operatorName === $node->getAttribute('operator')) { + trigger_deprecation($change->getPackage(), $change->getVersion(), \sprintf('Add explicit parentheses around the "%s" unary operator to avoid behavior change in the next major version as its precedence will change in "%s" at line %d.', $target, $this->parser->getStream()->getSourceContext()->getName(), $token->getLine())); + } + } + } else { foreach ($this->precedenceChanges[$expr->getAttribute('operator')] as $operatorName) { foreach ($expr as $node) { /** @var AbstractExpression $node */ if ($node->hasAttribute('operator') && $operatorName === $node->getAttribute('operator') && !$node->hasExplicitParentheses()) { - trigger_deprecation('twig/twig', '3.15', \sprintf('Add explicit parentheses around the "%s" operator to avoid behavior change in Twig 4.0 as its precedence will change in "%s" at line %d.', $operatorName, $this->parser->getStream()->getSourceContext()->getName(), $token->getLine())); + $op = explode('_', $operatorName)[1]; + $change = $this->binaryOperators[$op]['precedence_change']; + trigger_deprecation($change->getPackage(), $change->getVersion(), \sprintf('Add explicit parentheses around the "%s" binary operator to avoid behavior change in the next major version as its precedence will change in "%s" at line %d.', $op, $this->parser->getStream()->getSourceContext()->getName(), $token->getLine())); } } } } - - if (0 === $precedence) { - return $this->parseConditionalExpression($expr); - } - - return $expr; } /** @@ -195,10 +232,13 @@ private function getPrimary(): AbstractExpression $expr = $this->parseExpression($operator['precedence']); $class = $operator['class']; - return $this->parsePostfixExpression(new $class($expr, $token->getLine())); + $expr = new $class($expr, $token->getLine()); + $expr->setAttribute('operator', 'unary_'.$token->getValue()); + + return $this->parsePostfixExpression($expr); } elseif ($token->test(Token::PUNCTUATION_TYPE, '(')) { $this->parser->getStream()->next(); - $expr = $this->parseExpression()->setExplicitParentheses(); + $expr = $this->parseExpression(deprecationCheck: false)->setExplicitParentheses(); $this->parser->getStream()->expect(Token::PUNCTUATION_TYPE, ')', 'An opened parenthesis is not properly closed'); return $this->parsePostfixExpression($expr); diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index 489c2c266b0..6f27e9acdc5 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -66,6 +66,7 @@ use Twig\Node\Expression\Unary\PosUnary; use Twig\Node\Node; use Twig\NodeVisitor\MacroAutoImportNodeVisitor; +use Twig\OperatorPrecedenceChange; use Twig\Parser; use Twig\Source; use Twig\Template; @@ -296,7 +297,7 @@ public function getOperators(): array { return [ [ - 'not' => ['precedence' => 50, 'class' => NotUnary::class], + 'not' => ['precedence' => 50, 'precedence_change' => new OperatorPrecedenceChange('twig/twig', '3.15', 70), 'class' => NotUnary::class], '-' => ['precedence' => 500, 'class' => NegUnary::class], '+' => ['precedence' => 500, 'class' => PosUnary::class], ], @@ -323,7 +324,7 @@ public function getOperators(): array '..' => ['precedence' => 25, 'class' => RangeBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT], '+' => ['precedence' => 30, 'class' => AddBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT], '-' => ['precedence' => 30, 'class' => SubBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT], - '~' => ['precedence' => 40, 'future_precedence' => 27, 'class' => ConcatBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT], + '~' => ['precedence' => 40, 'precedence_change' => new OperatorPrecedenceChange('twig/twig', '3.15', 27), 'class' => ConcatBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT], '*' => ['precedence' => 60, 'class' => MulBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT], '/' => ['precedence' => 60, 'class' => DivBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT], '//' => ['precedence' => 60, 'class' => FloorDivBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT], @@ -331,7 +332,7 @@ public function getOperators(): array 'is' => ['precedence' => 100, 'associativity' => ExpressionParser::OPERATOR_LEFT], 'is not' => ['precedence' => 100, 'associativity' => ExpressionParser::OPERATOR_LEFT], '**' => ['precedence' => 200, 'class' => PowerBinary::class, 'associativity' => ExpressionParser::OPERATOR_RIGHT], - '??' => ['precedence' => 300, 'future_precedence' => 5, 'class' => NullCoalesceExpression::class, 'associativity' => ExpressionParser::OPERATOR_RIGHT], + '??' => ['precedence' => 300, 'precedence_change' => new OperatorPrecedenceChange('twig/twig', '3.15', 5), 'class' => NullCoalesceExpression::class, 'associativity' => ExpressionParser::OPERATOR_RIGHT], ], ]; } diff --git a/src/Extension/ExtensionInterface.php b/src/Extension/ExtensionInterface.php index 10a42b6b161..1b7be44c11a 100644 --- a/src/Extension/ExtensionInterface.php +++ b/src/Extension/ExtensionInterface.php @@ -67,8 +67,8 @@ public function getFunctions(); * @return array First array of unary operators, second array of binary operators * * @psalm-return array{ - * array}>, - * array, associativity: ExpressionParser::OPERATOR_*}> + * array}>, + * array, associativity: ExpressionParser::OPERATOR_*}> * } */ public function getOperators(); diff --git a/src/OperatorPrecedenceChange.php b/src/OperatorPrecedenceChange.php new file mode 100644 index 00000000000..12fd98c8d24 --- /dev/null +++ b/src/OperatorPrecedenceChange.php @@ -0,0 +1,42 @@ + + */ +class OperatorPrecedenceChange +{ + public function __construct( + private string $package, + private string $version, + private int $newPrecedence, + ) { + } + + public function getPackage(): string + { + return $this->package; + } + + public function getVersion(): string + { + return $this->version; + } + + public function getNewPrecedence(): string + { + return $this->newPrecedence; + } +} diff --git a/tests/EnvironmentTest.php b/tests/EnvironmentTest.php index d5aee8d1bbe..802f9dd3af2 100644 --- a/tests/EnvironmentTest.php +++ b/tests/EnvironmentTest.php @@ -597,7 +597,7 @@ public function getFunctions(): array public function getOperators(): array { return [ - ['foo_unary' => []], + ['foo_unary' => ['precedence' => 0]], ['foo_binary' => ['precedence' => 0]], ]; } diff --git a/tests/Fixtures/operators/contat_vs_add_sub.legacy.test b/tests/Fixtures/operators/contat_vs_add_sub.legacy.test index c032cf2907b..541e4f7cb8a 100644 --- a/tests/Fixtures/operators/contat_vs_add_sub.legacy.test +++ b/tests/Fixtures/operators/contat_vs_add_sub.legacy.test @@ -1,8 +1,8 @@ --TEST-- +/- will have a higher precedence over ~ in Twig 4.0 --DEPRECATION-- -Since twig/twig 3.15: Add explicit parentheses around the "~" operator to avoid behavior change in Twig 4.0 as its precedence will change in "index.twig" at line 2. -Since twig/twig 3.15: Add explicit parentheses around the "~" operator to avoid behavior change in Twig 4.0 as its precedence will change in "index.twig" at line 3. +Since twig/twig 3.15: Add explicit parentheses around the "~" binary operator to avoid behavior change in the next major version as its precedence will change in "index.twig" at line 2. +Since twig/twig 3.15: Add explicit parentheses around the "~" binary operator to avoid behavior change in the next major version as its precedence will change in "index.twig" at line 3. --TEMPLATE-- {{ '42' ~ 1 + 41 }} {{ '42' ~ 43 - 1 }} diff --git a/tests/Fixtures/operators/not_precedence.legacy.test b/tests/Fixtures/operators/not_precedence.legacy.test new file mode 100644 index 00000000000..5178288e950 --- /dev/null +++ b/tests/Fixtures/operators/not_precedence.legacy.test @@ -0,0 +1,9 @@ +--TEST-- +*, /, //, and % will have a higher precedence over not in Twig 4.0 +--DEPRECATION-- +Since twig/twig 3.15: Add explicit parentheses around the "not" unary operator to avoid behavior change in the next major version as its precedence will change in "index.twig" at line 2. +--TEMPLATE-- +{{ not 1 * 2 }} +--DATA-- +return [] +--EXPECT-- diff --git a/tests/Fixtures/operators/not_precedence.test b/tests/Fixtures/operators/not_precedence.test new file mode 100644 index 00000000000..592b1c33440 --- /dev/null +++ b/tests/Fixtures/operators/not_precedence.test @@ -0,0 +1,9 @@ +--TEST-- +*, /, //, and % will have a higher precedence over not in Twig 4.0 +--TEMPLATE-- +{{ (not 1) * 2 }} +{{ (not 1 * 2) }} +--DATA-- +return [] +--EXPECT-- +0 diff --git a/tests/Fixtures/tests/null_coalesce.legacy.test b/tests/Fixtures/tests/null_coalesce.legacy.test index b9e3a87754c..3d33d0a797f 100644 --- a/tests/Fixtures/tests/null_coalesce.legacy.test +++ b/tests/Fixtures/tests/null_coalesce.legacy.test @@ -1,8 +1,8 @@ --TEST-- Twig supports the ?? operator --DEPRECATION-- -Since twig/twig 3.15: Add explicit parentheses around the "??" operator to avoid behavior change in Twig 4.0 as its precedence will change in "index.twig" at line 4. -Since twig/twig 3.15: Add explicit parentheses around the "??" operator to avoid behavior change in Twig 4.0 as its precedence will change in "index.twig" at line 5. +Since twig/twig 3.15: Add explicit parentheses around the "??" binary operator to avoid behavior change in the next major version as its precedence will change in "index.twig" at line 4. +Since twig/twig 3.15: Add explicit parentheses around the "??" binary operator to avoid behavior change in the next major version as its precedence will change in "index.twig" at line 5. --TEMPLATE-- {{ nope ?? nada ?? 'OK' -}} {# no deprecation as the operators have the same precedence #} From 0d73fd232a41369770c1a516ffa7d76c617d9b19 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Wed, 2 Oct 2024 15:08:42 +0200 Subject: [PATCH 534/812] Clarify docs for some operators --- doc/templates.rst | 96 +++++++++++++++++++++++++++-------------------- 1 file changed, 56 insertions(+), 40 deletions(-) diff --git a/doc/templates.rst b/doc/templates.rst index bfe2a1c2079..764c5cfda4b 100644 --- a/doc/templates.rst +++ b/doc/templates.rst @@ -727,35 +727,26 @@ You can combine multiple expressions with the following operators: Comparisons ~~~~~~~~~~~ -The following comparison operators are supported in any expression: ``==``, -``!=``, ``<``, ``>``, ``>=``, and ``<=``. +The following mathematical comparison operators are supported in any +expression: ``==``, ``!=``, ``<``, ``>``, ``>=``, and ``<=``. -Check if a string ``starts with`` or ``ends with`` another string: +Spaceship Operator +~~~~~~~~~~~~~~~~~~ -.. code-block:: twig - - {% if 'Fabien' starts with 'F' %} - {% endif %} - - {% if 'Fabien' ends with 'n' %} - {% endif %} - -Check that a string contains another string via the containment operator (see -next section). +The spaceship operator (``<=>``) is used for comparing two expressions. It +returns ``-1``, ``0`` or ``1`` when the first operand is respectively less +than, equal to, or greater than the second operand. .. note:: - For complex string comparisons, the ``matches`` operator allows you to use - `regular expressions`_: - - .. code-block:: twig + Read more about in the `PHP spaceship operator documentation`_. - {% if phone matches '/^[\\d\\.]+$/' %} - {% endif %} +Iterable Operators +~~~~~~~~~~~~~~~~~~ -Check that a sequence or a mapping ``has every`` or ``has some`` of its -elements return ``true`` using an arrow function. The arrow function receives -the value of the sequence or mapping: +Check that an iterable ``has every`` or ``has some`` of its elements return +``true`` using an arrow function. The arrow function receives the value of the +iterable as its argument: .. code-block:: twig @@ -767,8 +758,11 @@ the value of the sequence or mapping: {% set hasOver38 = sizes has some v => v > 38 %} {# hasOver38 is true #} -Containment Operator -~~~~~~~~~~~~~~~~~~~~ +For an empty iterable, ``has every`` returns ``true`` and ``has some`` returns +``false``. + +Containment Operators +~~~~~~~~~~~~~~~~~~~~~ The ``in`` operator performs containment test. It returns ``true`` if the left operand is contained in the right: @@ -783,7 +777,7 @@ operand is contained in the right: .. tip:: - You can use this filter to perform a containment test on strings, + You can use this operator to perform a containment test on strings, sequences, mappings, or objects implementing the ``Traversable`` interface. To perform a negative test, use the ``not in`` operator: @@ -795,6 +789,27 @@ To perform a negative test, use the ``not in`` operator: {# is equivalent to #} {% if not (1 in [1, 2, 3]) %} +The ``starts with`` and ``ends with`` operators are used to check if a string +starts or ends with a given substring: + +.. code-block:: twig + + {% if 'Fabien' starts with 'F' %} + {% endif %} + + {% if 'Fabien' ends with 'n' %} + {% endif %} + +.. note:: + + For complex string comparisons, the ``matches`` operator allows you to use + `regular expressions`_: + + .. code-block:: twig + + {% if phone matches '/^[\\d\\.]+$/' %} + {% endif %} + Test Operator ~~~~~~~~~~~~~ @@ -1057,18 +1072,19 @@ Extensions Twig can be extended. If you want to create your own extensions, read the :ref:`Creating an Extension ` chapter. -.. _`Twig bundle`: https://github.com/uhnomoli/PHP-Twig.tmbundle -.. _`vim-twig plugin`: https://github.com/lumiliet/vim-twig -.. _`Twig plugin`: https://github.com/pulse00/Twig-Eclipse-Plugin -.. _`Twig language definition`: https://github.com/gabrielcorpse/gedit-twig-template-language -.. _`Twig syntax mode`: https://github.com/bobthecow/Twig-HTML.mode -.. _`other Twig syntax mode`: https://github.com/muxx/Twig-HTML.mode -.. _`Notepad++ Twig Highlighter`: https://github.com/Banane9/notepadplusplus-twig -.. _`web-mode.el`: https://web-mode.org/ -.. _`regular expressions`: https://www.php.net/manual/en/pcre.pattern.php -.. _`PHP-twig for atom`: https://github.com/reesef/php-twig -.. _`TwigFiddle`: https://twigfiddle.com/ -.. _`Twig pack`: https://marketplace.visualstudio.com/items?itemName=bajdzis.vscode-twig-pack -.. _`Modern Twig`: https://marketplace.visualstudio.com/items?itemName=Stanislav.vscode-twig -.. _`Twig Language Server`: https://github.com/kaermorchen/twig-language-server/tree/master/packages/language-server -.. _`Twiggy`: https://marketplace.visualstudio.com/items?itemName=moetelo.twiggy +.. _`Twig bundle`: https://github.com/uhnomoli/PHP-Twig.tmbundle +.. _`vim-twig plugin`: https://github.com/lumiliet/vim-twig +.. _`Twig plugin`: https://github.com/pulse00/Twig-Eclipse-Plugin +.. _`Twig language definition`: https://github.com/gabrielcorpse/gedit-twig-template-language +.. _`Twig syntax mode`: https://github.com/bobthecow/Twig-HTML.mode +.. _`other Twig syntax mode`: https://github.com/muxx/Twig-HTML.mode +.. _`Notepad++ Twig Highlighter`: https://github.com/Banane9/notepadplusplus-twig +.. _`web-mode.el`: https://web-mode.org/ +.. _`regular expressions`: https://www.php.net/manual/en/pcre.pattern.php +.. _`PHP-twig for atom`: https://github.com/reesef/php-twig +.. _`TwigFiddle`: https://twigfiddle.com/ +.. _`Twig pack`: https://marketplace.visualstudio.com/items?itemName=bajdzis.vscode-twig-pack +.. _`Modern Twig`: https://marketplace.visualstudio.com/items?itemName=Stanislav.vscode-twig +.. _`Twig Language Server`: https://github.com/kaermorchen/twig-language-server/tree/master/packages/language-server +.. _`Twiggy`: https://marketplace.visualstudio.com/items?itemName=moetelo.twiggy +.. _`PHP spaceship operator documentation`: https://www.php.net/manual/en/language.operators.comparison.php \ No newline at end of file From e08ca837dfa67237fb0eed924ce2b763d0afad09 Mon Sep 17 00:00:00 2001 From: unknown Date: Thu, 25 Jan 2024 19:20:25 +0100 Subject: [PATCH 535/812] Added informations in format_currency and format_number docs --- doc/filters/format_currency.rst | 108 ++++++++++++++++++----- doc/filters/format_number.rst | 148 ++++++++++++++++++++++++++------ 2 files changed, 210 insertions(+), 46 deletions(-) diff --git a/doc/filters/format_currency.rst b/doc/filters/format_currency.rst index 6f1036d33a5..b61823d04b4 100644 --- a/doc/filters/format_currency.rst +++ b/doc/filters/format_currency.rst @@ -20,25 +20,88 @@ You can pass attributes to tweak the output: The list of supported options: -* ``grouping_used``; -* ``decimal_always_shown``; -* ``max_integer_digit``; -* ``min_integer_digit``; -* ``integer_digit``; -* ``max_fraction_digit``; -* ``min_fraction_digit``; -* ``fraction_digit``; -* ``multiplier``; -* ``grouping_size``; -* ``rounding_mode``; -* ``rounding_increment``; -* ``format_width``; -* ``padding_position``; -* ``secondary_grouping_size``; -* ``significant_digits_used``; -* ``min_significant_digits_used``; -* ``max_significant_digits_used``; -* ``lenient_parse``. +* ``grouping_used``: Specifies whether to use grouping separator for thousands. + .. code-block:: twig + + {# €1,234,567.89 #} + {{ 1234567.89 | format_currency('EUR', {grouping_used:true}, 'en') }} + +* ``decimal_always_shown``: Specifies whether to always show the decimal part, even if it's zero. + .. code-block:: twig + + {# €123.00 #} + {{ 123 | format_currency('EUR', {decimal_always_shown:true}, 'en') }} + +* ``max_integer_digit``: +* ``min_integer_digit``: +* ``integer_digit``: Define constraints on the integer part. + .. code-block:: twig + + {# €345.68 #} + {{ 12345.6789 | format_currency('EUR', {max_integer_digit:3, min_integer_digit:2}, 'en') }} + +* ``max_fraction_digit``: +* ``min_fraction_digit``: +* ``fraction_digit``: Define constraints on the fraction part. + .. code-block:: twig + + {# €123.46 #} + {{ 123.456789 | format_currency('EUR', {max_fraction_digit:2, min_fraction_digit:1}, 'en') }} + +* ``multiplier``: Multiplies the value before formatting. + .. code-block:: twig + + {# €123,000.00 #} + {{ 123 | format_currency('EUR', {multiplier:1000}, 'en') }} + +* ``grouping_size``: +* ``secondary_grouping_size``: Set the size of the primary and secondary grouping separators. + .. code-block:: twig + + {# €1,23,45,678.00 #} + {{ 12345678 | format_currency('EUR', {grouping_size:3, secondary_grouping_size:2}, 'en') }} + +* ``rounding_mode``: +* ``rounding_increment``: Control rounding behavior, here is a list of all rounding_mode available: + * ``ceil``: Ceiling rounding. + * ``floor``: Floor rounding. + * ``down``: Rounding towards zero. + * ``up``: Rounding away from zero. + * ``half_even``: Round halves to the nearest even integer. + * ``half_up``: Round halves up. + * ``half_down``: Round halves down. + + .. code-block:: twig + + {# €123.50 #} + {{ 123.456 | format_currency('EUR', {rounding_mode:'ceiling', rounding_increment:0.05}, 'en') }} + +* ``format_width``: +* ``padding_position``: Set width and padding for the formatted number, here is a list of all padding_position available: + * ``before_prefix``: Pad before the currency symbol. + * ``after_prefix``: Pad after the currency symbol. + * ``before_suffix``: Pad before the suffix (currency symbol). + * ``after_suffix``: Pad after the suffix (currency symbol). + + .. code-block:: twig + + {# €123.00 #} + {{ 123 | format_currency('EUR', {format_width:10, padding_position:'before_suffix'}, 'en') }} + +* ``significant_digits_used``: +* ``min_significant_digits_used``: +* ``max_significant_digits_used``: Control significant digits in formatting. + .. code-block:: twig + + {# €123.4568 #} + {{ 123.456789 | format_currency('EUR', {significant_digits_used:true, min_significant_digits_used:4, max_significant_digits_used:7}, 'en') }} + + +* ``lenient_parse``: If true, allows lenient parsing of the input. + .. code-block:: twig + + {# €123.00 #} + {{ 123 | format_currency('EUR', {lenient_parse:true}, 'en') }} By default, the filter uses the current locale. You can pass it explicitly: @@ -72,8 +135,13 @@ By default, the filter uses the current locale. You can pass it explicitly: Arguments --------- -* ``currency``: The currency +* ``currency``: The currency (any 3 letter code of ISO 4217) * ``attrs``: A map of attributes * ``locale``: The locale code as defined in `RFC 5646`_ +.. note:: + + Internally, Twig uses the PHP `NumberFormatter::formatCurrency`_ function. + .. _RFC 5646: https://www.rfc-editor.org/info/rfc5646 +.. _`NumberFormatter::formatCurrency`: https://www.php.net/manual/en/numberformatter.formatcurrency.php diff --git a/doc/filters/format_number.rst b/doc/filters/format_number.rst index e1bedc2fc8d..e6060c675cb 100644 --- a/doc/filters/format_number.rst +++ b/doc/filters/format_number.rst @@ -19,25 +19,87 @@ You can pass attributes to tweak the output: The list of supported options: -* ``grouping_used``; -* ``decimal_always_shown``; -* ``max_integer_digit``; -* ``min_integer_digit``; -* ``integer_digit``; -* ``max_fraction_digit``; -* ``min_fraction_digit``; -* ``fraction_digit``; -* ``multiplier``; -* ``grouping_size``; -* ``rounding_mode``; -* ``rounding_increment``; -* ``format_width``; -* ``padding_position``; -* ``secondary_grouping_size``; -* ``significant_digits_used``; -* ``min_significant_digits_used``; -* ``max_significant_digits_used``; -* ``lenient_parse``. +* ``grouping_used``: Specifies whether to use grouping separator for thousands. + .. code-block:: twig + + {# 1,234,567.89 #} + {{ 1234567.89|format_number({grouping_used:true}, locale='en') }} + +* ``decimal_always_shown``: Specifies whether to always show the decimal part, even if it's zero. + .. code-block:: twig + + {# 123. #} + {{ 123|format_number({decimal_always_shown:true}, locale='en') }} + +* ``max_integer_digit``: +* ``min_integer_digit``: +* ``integer_digit``: Define constraints on the integer part. + .. code-block:: twig + + {# 345.679 #} + {{ 12345.6789|format_number({max_integer_digit:3, min_integer_digit:2}, locale='en') }} + +* ``max_fraction_digit``: +* ``min_fraction_digit``: +* ``fraction_digit``: Define constraints on the fraction part. + .. code-block:: twig + + {# 123.46 #} + {{ 123.456789|format_number({max_fraction_digit:2, min_fraction_digit:1}, locale='en') }} + +* ``multiplier``: Multiplies the value before formatting. + .. code-block:: twig + + {# 123,000 #} + {{ 123|format_number({multiplier:1000}, locale='en') }} + +* ``grouping_size``: +* ``secondary_grouping_size``: Set the size of the primary and secondary grouping separators. + .. code-block:: twig + + {# 1,23,45,678 #} + {{ 12345678|format_number({grouping_size:3, secondary_grouping_size:2}, locale='en') }} + +* ``rounding_mode``: +* ``rounding_increment``: Control rounding behavior, here is a list of all rounding_mode available: + * ``ceil``: Ceiling rounding. + * ``floor``: Floor rounding. + * ``down``: Rounding towards zero. + * ``up``: Rounding away from zero. + * ``half_even``: Round halves to the nearest even integer. + * ``half_up``: Round halves up. + * ``half_down``: Round halves down. + + .. code-block:: twig + + {# 123.5 #} + {{ 123.456|format_number({rounding_mode:'ceiling', rounding_increment:0.05}, locale='en') }} + +* ``format_width``: +* ``padding_position``: Set width and padding for the formatted number, here is a list of all padding_position available: + * ``before_prefix``: Pad before the currency symbol. + * ``after_prefix``: Pad after the currency symbol. + * ``before_suffix``: Pad before the suffix (currency symbol). + * ``after_suffix``: Pad after the suffix (currency symbol). + + .. code-block:: twig + + {# 123 #} + {{ 123|format_number({format_width:10, padding_position:'before_suffix'}, locale='en') }} + +* ``significant_digits_used``: +* ``min_significant_digits_used``: +* ``max_significant_digits_used``: Control significant digits in formatting. + .. code-block:: twig + + {# 123.4568 #} + {{ 123.456789|format_number({significant_digits_used:true, min_significant_digits_used:4, max_significant_digits_used:7}, locale='en') }} + +* ``lenient_parse``: If true, allows lenient parsing of the input. + .. code-block:: twig + + {# 123 #} + {{ 123|format_number({lenient_parse:true}, locale='en') }} Besides plain numbers, the filter can also format numbers in various styles: @@ -54,13 +116,47 @@ Besides plain numbers, the filter can also format numbers in various styles: The list of supported styles: -* ``decimal``; -* ``currency``; -* ``percent``; -* ``scientific``; -* ``spellout``; -* ``ordinal``; -* ``duration``. +* ``decimal``: + .. code-block:: twig + + {# 1,234.568 #} + {{ 1234.56789 | format_number(style='decimal', locale='en') }} + +* ``currency``: + .. code-block:: twig + + {# $1,234.56 #} + {{ 1234.56 | format_number(style='currency', locale='en') }} + +* ``percent``: + .. code-block:: twig + + {# 12% #} + {{ 0.1234 | format_number(style='percent', locale='en') }} + +* ``scientific``: + .. code-block:: twig + + {# 1.23456789e+3 #} + {{ 1234.56789 | format_number(style='scientific', locale='en') }} + +* ``spellout``: + .. code-block:: twig + + {# one thousand two hundred thirty-four point five six seven eight nine #} + {{ 1234.56789 | format_number(style='spellout', locale='en') }} + +* ``ordinal``: + .. code-block:: twig + + {# 1st #} + {{ 1 | format_number(style='ordinal', locale='en') }} + +* ``duration``: + .. code-block:: twig + + {# 2:30:00 #} + {{ 9000 | format_number(style='duration', locale='en') }} As a shortcut, you can use the ``format_*_number`` filters by replacing ``*`` with a style: From c4d4141187c7dad337ffa4515110e1805a97d19d Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Thu, 3 Oct 2024 14:09:21 +0200 Subject: [PATCH 536/812] Fix markup --- doc/filters/format_currency.rst | 37 +++++++------------ doc/filters/format_number.rst | 65 +++++++++++---------------------- 2 files changed, 35 insertions(+), 67 deletions(-) diff --git a/doc/filters/format_currency.rst b/doc/filters/format_currency.rst index b61823d04b4..2f60be0b052 100644 --- a/doc/filters/format_currency.rst +++ b/doc/filters/format_currency.rst @@ -20,49 +20,44 @@ You can pass attributes to tweak the output: The list of supported options: -* ``grouping_used``: Specifies whether to use grouping separator for thousands. - .. code-block:: twig +* ``grouping_used``: Specifies whether to use grouping separator for thousands:: {# €1,234,567.89 #} {{ 1234567.89 | format_currency('EUR', {grouping_used:true}, 'en') }} -* ``decimal_always_shown``: Specifies whether to always show the decimal part, even if it's zero. - .. code-block:: twig +* ``decimal_always_shown``: Specifies whether to always show the decimal part, even if it's zero:: {# €123.00 #} {{ 123 | format_currency('EUR', {decimal_always_shown:true}, 'en') }} * ``max_integer_digit``: * ``min_integer_digit``: -* ``integer_digit``: Define constraints on the integer part. - .. code-block:: twig +* ``integer_digit``: Define constraints on the integer part:: {# €345.68 #} {{ 12345.6789 | format_currency('EUR', {max_integer_digit:3, min_integer_digit:2}, 'en') }} * ``max_fraction_digit``: * ``min_fraction_digit``: -* ``fraction_digit``: Define constraints on the fraction part. - .. code-block:: twig +* ``fraction_digit``: Define constraints on the fraction part:: {# €123.46 #} {{ 123.456789 | format_currency('EUR', {max_fraction_digit:2, min_fraction_digit:1}, 'en') }} -* ``multiplier``: Multiplies the value before formatting. - .. code-block:: twig +* ``multiplier``: Multiplies the value before formatting:: {# €123,000.00 #} {{ 123 | format_currency('EUR', {multiplier:1000}, 'en') }} * ``grouping_size``: -* ``secondary_grouping_size``: Set the size of the primary and secondary grouping separators. - .. code-block:: twig +* ``secondary_grouping_size``: Set the size of the primary and secondary grouping separators:: {# €1,23,45,678.00 #} {{ 12345678 | format_currency('EUR', {grouping_size:3, secondary_grouping_size:2}, 'en') }} * ``rounding_mode``: * ``rounding_increment``: Control rounding behavior, here is a list of all rounding_mode available: + * ``ceil``: Ceiling rounding. * ``floor``: Floor rounding. * ``down``: Rounding towards zero. @@ -73,11 +68,12 @@ The list of supported options: .. code-block:: twig - {# €123.50 #} - {{ 123.456 | format_currency('EUR', {rounding_mode:'ceiling', rounding_increment:0.05}, 'en') }} + {# €123.50 #} + {{ 123.456 | format_currency('EUR', {rounding_mode:'ceiling', rounding_increment:0.05}, 'en') }} * ``format_width``: * ``padding_position``: Set width and padding for the formatted number, here is a list of all padding_position available: + * ``before_prefix``: Pad before the currency symbol. * ``after_prefix``: Pad after the currency symbol. * ``before_suffix``: Pad before the suffix (currency symbol). @@ -90,22 +86,17 @@ The list of supported options: * ``significant_digits_used``: * ``min_significant_digits_used``: -* ``max_significant_digits_used``: Control significant digits in formatting. - .. code-block:: twig +* ``max_significant_digits_used``: Control significant digits in formatting:: {# €123.4568 #} {{ 123.456789 | format_currency('EUR', {significant_digits_used:true, min_significant_digits_used:4, max_significant_digits_used:7}, 'en') }} - -* ``lenient_parse``: If true, allows lenient parsing of the input. - .. code-block:: twig +* ``lenient_parse``: If true, allows lenient parsing of the input:: {# €123.00 #} {{ 123 | format_currency('EUR', {lenient_parse:true}, 'en') }} -By default, the filter uses the current locale. You can pass it explicitly: - -.. code-block:: twig +By default, the filter uses the current locale. You can pass it explicitly:: {# 1.000.000,00 € #} {{ '1000000'|format_currency('EUR', locale: 'de') }} @@ -135,7 +126,7 @@ By default, the filter uses the current locale. You can pass it explicitly: Arguments --------- -* ``currency``: The currency (any 3 letter code of ISO 4217) +* ``currency``: The currency (ISO 4217 code) * ``attrs``: A map of attributes * ``locale``: The locale code as defined in `RFC 5646`_ diff --git a/doc/filters/format_number.rst b/doc/filters/format_number.rst index e6060c675cb..488b7af1436 100644 --- a/doc/filters/format_number.rst +++ b/doc/filters/format_number.rst @@ -19,43 +19,37 @@ You can pass attributes to tweak the output: The list of supported options: -* ``grouping_used``: Specifies whether to use grouping separator for thousands. - .. code-block:: twig +* ``grouping_used``: Specifies whether to use grouping separator for thousands:: {# 1,234,567.89 #} {{ 1234567.89|format_number({grouping_used:true}, locale='en') }} -* ``decimal_always_shown``: Specifies whether to always show the decimal part, even if it's zero. - .. code-block:: twig +* ``decimal_always_shown``: Specifies whether to always show the decimal part, even if it's zero:: {# 123. #} {{ 123|format_number({decimal_always_shown:true}, locale='en') }} * ``max_integer_digit``: * ``min_integer_digit``: -* ``integer_digit``: Define constraints on the integer part. - .. code-block:: twig +* ``integer_digit``: Define constraints on the integer part:: {# 345.679 #} {{ 12345.6789|format_number({max_integer_digit:3, min_integer_digit:2}, locale='en') }} * ``max_fraction_digit``: * ``min_fraction_digit``: -* ``fraction_digit``: Define constraints on the fraction part. - .. code-block:: twig +* ``fraction_digit``: Define constraints on the fraction part:: {# 123.46 #} {{ 123.456789|format_number({max_fraction_digit:2, min_fraction_digit:1}, locale='en') }} -* ``multiplier``: Multiplies the value before formatting. - .. code-block:: twig +* ``multiplier``: Multiplies the value before formatting:: {# 123,000 #} {{ 123|format_number({multiplier:1000}, locale='en') }} * ``grouping_size``: -* ``secondary_grouping_size``: Set the size of the primary and secondary grouping separators. - .. code-block:: twig +* ``secondary_grouping_size``: Set the size of the primary and secondary grouping separators:: {# 1,23,45,678 #} {{ 12345678|format_number({grouping_size:3, secondary_grouping_size:2}, locale='en') }} @@ -89,21 +83,17 @@ The list of supported options: * ``significant_digits_used``: * ``min_significant_digits_used``: -* ``max_significant_digits_used``: Control significant digits in formatting. - .. code-block:: twig +* ``max_significant_digits_used``: Control significant digits in formatting:: {# 123.4568 #} {{ 123.456789|format_number({significant_digits_used:true, min_significant_digits_used:4, max_significant_digits_used:7}, locale='en') }} -* ``lenient_parse``: If true, allows lenient parsing of the input. - .. code-block:: twig +* ``lenient_parse``: If true, allows lenient parsing of the input:: {# 123 #} {{ 123|format_number({lenient_parse:true}, locale='en') }} -Besides plain numbers, the filter can also format numbers in various styles: - -.. code-block:: twig +Besides plain numbers, the filter can also format numbers in various styles:: {# 1,234% #} {{ '12.345'|format_number(style: 'percent') }} @@ -116,52 +106,43 @@ Besides plain numbers, the filter can also format numbers in various styles: The list of supported styles: -* ``decimal``: - .. code-block:: twig +* ``decimal``:: {# 1,234.568 #} {{ 1234.56789 | format_number(style='decimal', locale='en') }} -* ``currency``: - .. code-block:: twig +* ``currency``:: {# $1,234.56 #} {{ 1234.56 | format_number(style='currency', locale='en') }} -* ``percent``: - .. code-block:: twig +* ``percent``:: {# 12% #} {{ 0.1234 | format_number(style='percent', locale='en') }} -* ``scientific``: - .. code-block:: twig +* ``scientific``:: {# 1.23456789e+3 #} {{ 1234.56789 | format_number(style='scientific', locale='en') }} -* ``spellout``: - .. code-block:: twig +* ``spellout``:: {# one thousand two hundred thirty-four point five six seven eight nine #} {{ 1234.56789 | format_number(style='spellout', locale='en') }} -* ``ordinal``: - .. code-block:: twig +* ``ordinal``:: {# 1st #} {{ 1 | format_number(style='ordinal', locale='en') }} -* ``duration``: - .. code-block:: twig +* ``duration``:: {# 2:30:00 #} {{ 9000 | format_number(style='duration', locale='en') }} As a shortcut, you can use the ``format_*_number`` filters by replacing ``*`` -with a style: - -.. code-block:: twig +with a style:: {# 1,234% #} {{ '12.345'|format_percent_number }} @@ -169,16 +150,12 @@ with a style: {# twelve point three four five #} {{ '12.345'|format_spellout_number }} -You can pass attributes to tweak the output: - -.. code-block:: twig +You can pass attributes to tweak the output:: {# 12.3% #} {{ '0.12345'|format_percent_number({rounding_mode: 'floor', fraction_digit: 1}) }} -By default, the filter uses the current locale. You can pass it explicitly: - -.. code-block:: twig +By default, the filter uses the current locale. You can pass it explicitly:: {# 12,345 #} {{ '12.345'|format_number(locale: 'fr') }} @@ -188,13 +165,13 @@ By default, the filter uses the current locale. You can pass it explicitly: The ``format_number`` filter is part of the ``IntlExtension`` which is not installed by default. Install it first: - .. code-block:: bash + .. code-block:: sh $ composer require twig/intl-extra Then, on Symfony projects, install the ``twig/extra-bundle``: - .. code-block:: bash + .. code-block:: sh $ composer require twig/extra-bundle From 53441af5ffc5fc3952d0580e63c3575a0ee3c7c4 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Thu, 3 Oct 2024 14:23:13 +0200 Subject: [PATCH 537/812] Fix markup --- doc/filters/format_currency.rst | 22 +++++++++++----------- doc/filters/format_number.rst | 22 +++++++++++----------- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/doc/filters/format_currency.rst b/doc/filters/format_currency.rst index 2f60be0b052..eff465af48c 100644 --- a/doc/filters/format_currency.rst +++ b/doc/filters/format_currency.rst @@ -58,13 +58,13 @@ The list of supported options: * ``rounding_mode``: * ``rounding_increment``: Control rounding behavior, here is a list of all rounding_mode available: - * ``ceil``: Ceiling rounding. - * ``floor``: Floor rounding. - * ``down``: Rounding towards zero. - * ``up``: Rounding away from zero. - * ``half_even``: Round halves to the nearest even integer. - * ``half_up``: Round halves up. - * ``half_down``: Round halves down. + * ``ceil``: Ceiling rounding + * ``floor``: Floor rounding + * ``down``: Rounding towards zero + * ``up``: Rounding away from zero + * ``half_even``: Round halves to the nearest even integer + * ``half_up``: Round halves up + * ``half_down``: Round halves down .. code-block:: twig @@ -74,10 +74,10 @@ The list of supported options: * ``format_width``: * ``padding_position``: Set width and padding for the formatted number, here is a list of all padding_position available: - * ``before_prefix``: Pad before the currency symbol. - * ``after_prefix``: Pad after the currency symbol. - * ``before_suffix``: Pad before the suffix (currency symbol). - * ``after_suffix``: Pad after the suffix (currency symbol). + * ``before_prefix``: Pad before the currency symbol + * ``after_prefix``: Pad after the currency symbol + * ``before_suffix``: Pad before the suffix (currency symbol) + * ``after_suffix``: Pad after the suffix (currency symbol) .. code-block:: twig diff --git a/doc/filters/format_number.rst b/doc/filters/format_number.rst index 488b7af1436..9bda44b076b 100644 --- a/doc/filters/format_number.rst +++ b/doc/filters/format_number.rst @@ -56,13 +56,13 @@ The list of supported options: * ``rounding_mode``: * ``rounding_increment``: Control rounding behavior, here is a list of all rounding_mode available: - * ``ceil``: Ceiling rounding. - * ``floor``: Floor rounding. - * ``down``: Rounding towards zero. - * ``up``: Rounding away from zero. - * ``half_even``: Round halves to the nearest even integer. - * ``half_up``: Round halves up. - * ``half_down``: Round halves down. + * ``ceil``: Ceiling rounding + * ``floor``: Floor rounding + * ``down``: Rounding towards zero + * ``up``: Rounding away from zero + * ``half_even``: Round halves to the nearest even integer + * ``half_up``: Round halves up + * ``half_down``: Round halves down .. code-block:: twig @@ -71,10 +71,10 @@ The list of supported options: * ``format_width``: * ``padding_position``: Set width and padding for the formatted number, here is a list of all padding_position available: - * ``before_prefix``: Pad before the currency symbol. - * ``after_prefix``: Pad after the currency symbol. - * ``before_suffix``: Pad before the suffix (currency symbol). - * ``after_suffix``: Pad after the suffix (currency symbol). + * ``before_prefix``: Pad before the currency symbol + * ``after_prefix``: Pad after the currency symbol + * ``before_suffix``: Pad before the suffix (currency symbol) + * ``after_suffix``: Pad after the suffix (currency symbol) .. code-block:: twig From 86b23eba2346d9890a91d3477184fb223c9af0bc Mon Sep 17 00:00:00 2001 From: seb-jean Date: Fri, 4 Oct 2024 07:32:20 +0200 Subject: [PATCH 538/812] Update html_cva.rst --- doc/functions/html_cva.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/functions/html_cva.rst b/doc/functions/html_cva.rst index 4079581570c..0c19293fb43 100644 --- a/doc/functions/html_cva.rst +++ b/doc/functions/html_cva.rst @@ -146,7 +146,7 @@ If no variants match, you can define a default set of classes to apply: lg: 'rounded-lg', } }, - defaultVariants={ + defaultVariant={ rounded: 'md', } ) %} @@ -187,4 +187,4 @@ This function works best when used with `TwigComponent`_. .. _`CVA (Class Variant Authority)`: https://cva.style/docs/getting-started/variants .. _`shadcn/ui`: https://ui.shadcn.com .. _`tales-from-a-dev/twig-tailwind-extra`: https://github.com/tales-from-a-dev/twig-tailwind-extra -.. _`TwigComponent`: https://symfony.com/bundles/ux-twig-component/current/index.html \ No newline at end of file +.. _`TwigComponent`: https://symfony.com/bundles/ux-twig-component/current/index.html From f363ab258b957c45f29280f2e96570080371957c Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Fri, 4 Oct 2024 08:05:19 +0200 Subject: [PATCH 539/812] Fix markup --- doc/filters/escape.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/filters/escape.rst b/doc/filters/escape.rst index cf1ad61efa4..e9ff539b69f 100644 --- a/doc/filters/escape.rst +++ b/doc/filters/escape.rst @@ -51,7 +51,7 @@ documents: * ``url``: escapes a string for the **URI or parameter** contexts. This should not be used to escape an entire URI; only a subcomponent being inserted. -* ``html_attr``: escapes a string for the **HTML attribute** context, +* ``html_attr``: escapes a string for the **HTML attribute** context, **without quotes** around HTML attribute values. Note that doing contextual escaping in HTML documents is hard and choosing the From 5e9448310d6650254e411022c46eee7f820757aa Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Fri, 4 Oct 2024 18:48:30 +0200 Subject: [PATCH 540/812] Deprecate passing a string or an array to Twig callable arguments accepting arrow functions (pass a Closure) --- CHANGELOG | 1 + doc/deprecated.rst | 4 ++++ src/Extension/CoreExtension.php | 37 +++++++++++++++++++++++++-------- src/Resources/core.php | 2 +- 4 files changed, 34 insertions(+), 10 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index eb273f399f9..dda9d5191bf 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,6 @@ # 3.15.0 (2024-XX-XX) + * Deprecate passing a string or an array to Twig callable arguments accepting arrow functions (pass a `\Closure`) * Add support for triggering deprecations for future operator precedence changes * Deprecate using the `not` unary operator in an expression with ``*``, ``/``, ``//``, or ``%`` without using explicit parentheses to clarify precedence * Deprecate using the `??` binary operator without explicit parentheses diff --git a/doc/deprecated.rst b/doc/deprecated.rst index e1967addaae..1239c77bcea 100644 --- a/doc/deprecated.rst +++ b/doc/deprecated.rst @@ -290,6 +290,10 @@ Functions/Filters/Tests * For variadic arguments, use snake-case for the argument name to ease the transition to 4.0. +* Passing a ``string`` or an ``array`` to Twig callable arguments accepting + arrow functions is deprecated as of Twig 3.15; these arguments will have a + ``\Closure`` type hint in 4.0. + Node ---- diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index 89cbbaa6f8b..334ab408495 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -972,6 +972,7 @@ public static function shuffle(string $charset, $item) * Sorts an array. * * @param array|\Traversable $array + * @param ?\Closure $arrow * * @internal */ @@ -984,7 +985,7 @@ public static function sort(Environment $env, $array, $arrow = null): array } if (null !== $arrow) { - self::checkArrowInSandbox($env, $arrow, 'sort', 'filter'); + self::checkArrow($env, $arrow, 'sort', 'filter'); uasort($array, $arrow); } else { @@ -1838,6 +1839,8 @@ public static function column($array, $name, $index = null): array } /** + * @param \Closure $arrow + * * @internal */ public static function filter(Environment $env, $array, $arrow) @@ -1846,7 +1849,7 @@ public static function filter(Environment $env, $array, $arrow) throw new RuntimeError(\sprintf('The "filter" filter expects a sequence/mapping or "Traversable", got "%s".', get_debug_type($array))); } - self::checkArrowInSandbox($env, $arrow, 'filter', 'filter'); + self::checkArrow($env, $arrow, 'filter', 'filter'); if (\is_array($array)) { return array_filter($array, $arrow, \ARRAY_FILTER_USE_BOTH); @@ -1857,6 +1860,8 @@ public static function filter(Environment $env, $array, $arrow) } /** + * @param \Closure $arrow + * * @internal */ public static function find(Environment $env, $array, $arrow) @@ -1865,7 +1870,7 @@ public static function find(Environment $env, $array, $arrow) throw new RuntimeError(\sprintf('The "find" filter expects a sequence or a mapping, got "%s".', get_debug_type($array))); } - self::checkArrowInSandbox($env, $arrow, 'find', 'filter'); + self::checkArrow($env, $arrow, 'find', 'filter'); foreach ($array as $k => $v) { if ($arrow($v, $k)) { @@ -1877,6 +1882,8 @@ public static function find(Environment $env, $array, $arrow) } /** + * @param \Closure $arrow + * * @internal */ public static function map(Environment $env, $array, $arrow) @@ -1885,7 +1892,7 @@ public static function map(Environment $env, $array, $arrow) throw new RuntimeError(\sprintf('The "map" filter expects a sequence or a mapping, got "%s".', get_debug_type($array))); } - self::checkArrowInSandbox($env, $arrow, 'map', 'filter'); + self::checkArrow($env, $arrow, 'map', 'filter'); $r = []; foreach ($array as $k => $v) { @@ -1896,6 +1903,8 @@ public static function map(Environment $env, $array, $arrow) } /** + * @param \Closure $arrow + * * @internal */ public static function reduce(Environment $env, $array, $arrow, $initial = null) @@ -1904,7 +1913,7 @@ public static function reduce(Environment $env, $array, $arrow, $initial = null) throw new RuntimeError(\sprintf('The "reduce" filter expects a sequence or a mapping, got "%s".', get_debug_type($array))); } - self::checkArrowInSandbox($env, $arrow, 'reduce', 'filter'); + self::checkArrow($env, $arrow, 'reduce', 'filter'); $accumulator = $initial; foreach ($array as $key => $value) { @@ -1915,6 +1924,8 @@ public static function reduce(Environment $env, $array, $arrow, $initial = null) } /** + * @param \Closure $arrow + * * @internal */ public static function arraySome(Environment $env, $array, $arrow) @@ -1923,7 +1934,7 @@ public static function arraySome(Environment $env, $array, $arrow) throw new RuntimeError(\sprintf('The "has some" test expects a sequence or a mapping, got "%s".', get_debug_type($array))); } - self::checkArrowInSandbox($env, $arrow, 'has some', 'operator'); + self::checkArrow($env, $arrow, 'has some', 'operator'); foreach ($array as $k => $v) { if ($arrow($v, $k)) { @@ -1935,6 +1946,8 @@ public static function arraySome(Environment $env, $array, $arrow) } /** + * @param \Closure $arrow + * * @internal */ public static function arrayEvery(Environment $env, $array, $arrow) @@ -1943,7 +1956,7 @@ public static function arrayEvery(Environment $env, $array, $arrow) throw new RuntimeError(\sprintf('The "has every" test expects a sequence or a mapping, got "%s".', get_debug_type($array))); } - self::checkArrowInSandbox($env, $arrow, 'has every', 'operator'); + self::checkArrow($env, $arrow, 'has every', 'operator'); foreach ($array as $k => $v) { if (!$arrow($v, $k)) { @@ -1957,11 +1970,17 @@ public static function arrayEvery(Environment $env, $array, $arrow) /** * @internal */ - public static function checkArrowInSandbox(Environment $env, $arrow, $thing, $type) + public static function checkArrow(Environment $env, $arrow, $thing, $type) { - if (!$arrow instanceof \Closure && $env->hasExtension(SandboxExtension::class) && $env->getExtension(SandboxExtension::class)->isSandboxed()) { + if ($arrow instanceof \Closure) { + return; + } + + if ($env->hasExtension(SandboxExtension::class) && $env->getExtension(SandboxExtension::class)->isSandboxed()) { throw new RuntimeError(\sprintf('The callable passed to the "%s" %s must be a Closure in sandbox mode.', $thing, $type)); } + + trigger_deprecation('twig/twig', '3.15', 'Passing a callable that is not a PHP \Closure as an argument to the "%s" %s is deprecated.', $thing, $type); } /** diff --git a/src/Resources/core.php b/src/Resources/core.php index 6e2fcfb0758..bc0b27104b0 100644 --- a/src/Resources/core.php +++ b/src/Resources/core.php @@ -537,5 +537,5 @@ function twig_check_arrow_in_sandbox(Environment $env, $arrow, $thing, $type) { trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); - return CoreExtension::checkArrowInSandbox($env, $arrow, $thing, $type); + CoreExtension::checkArrow($env, $arrow, $thing, $type); } From 6688a8df16d5ed8b8ba0eaaf67edfe9251110a3e Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Fri, 4 Oct 2024 18:47:42 +0200 Subject: [PATCH 541/812] Move sandbox docs to its own chapter in the docs --- doc/api.rst | 48 +------------------- doc/functions/include.rst | 4 +- doc/index.rst | 1 + doc/sandbox.rst | 95 +++++++++++++++++++++++++++++++++++++++ doc/tags/include.rst | 4 +- 5 files changed, 102 insertions(+), 50 deletions(-) create mode 100644 doc/sandbox.rst diff --git a/doc/api.rst b/doc/api.rst index b1af42b9b60..18427713340 100644 --- a/doc/api.rst +++ b/doc/api.rst @@ -463,52 +463,8 @@ The escaping rules are implemented as follows: Sandbox Extension ~~~~~~~~~~~~~~~~~ -The ``sandbox`` extension can be used to evaluate untrusted code. Access to -unsafe attributes and methods is prohibited. The sandbox security is managed -by a policy instance. By default, Twig comes with one policy class: -``\Twig\Sandbox\SecurityPolicy``. This class allows you to white-list some -tags, filters, functions, properties, and methods:: - - $tags = ['if']; - $filters = ['upper']; - $methods = [ - 'Article' => ['getTitle', 'getBody'], - ]; - $properties = [ - 'Article' => ['title', 'body'], - ]; - $functions = ['range']; - $policy = new \Twig\Sandbox\SecurityPolicy($tags, $filters, $methods, $properties, $functions); - -With the previous configuration, the security policy will only allow usage of -the ``if`` tag, and the ``upper`` filter. Moreover, the templates will only be -able to call the ``getTitle()`` and ``getBody()`` methods on ``Article`` -objects, and the ``title`` and ``body`` public properties. Everything else -won't be allowed and will generate a ``\Twig\Sandbox\SecurityError`` exception. - -.. caution:: - - The ``extends`` and ``use`` tags are always allowed in a sandboxed - template. That behavior will change in 4.0 where these tags will need to be - explicitly allowed like any other tag. - -The policy object is the first argument of the sandbox constructor:: - - $sandbox = new \Twig\Extension\SandboxExtension($policy); - $twig->addExtension($sandbox); - -By default, the sandbox mode is disabled and should be enabled when including -untrusted template code by using the ``sandboxed`` option of the ``include`` -function: - -.. code-block:: twig - - {{ include('user.html', sandboxed: true) }} - -You can sandbox all templates by passing ``true`` as the second argument of -the extension constructor:: - - $sandbox = new \Twig\Extension\SandboxExtension($policy, true); +The ``sandbox`` extension can be used to evaluate untrusted code. Read more +about it in the :doc:`sandbox` chapter. Profiler Extension ~~~~~~~~~~~~~~~~~~ diff --git a/doc/functions/include.rst b/doc/functions/include.rst index a9ebce4e2b3..c5dfc9a9f6d 100644 --- a/doc/functions/include.rst +++ b/doc/functions/include.rst @@ -61,11 +61,11 @@ If ``ignore_missing`` is set, it will fall back to rendering nothing if none of the templates exist, otherwise it will throw an exception. When including a template created by an end user, you should consider -sandboxing it: +:doc:`sandboxing<../sandbox>` it: .. code-block:: twig - {{ include('page.html', sandboxed = true) }} + {{ include('page.html', sandboxed: true) }} Arguments --------- diff --git a/doc/index.rst b/doc/index.rst index 358bd738ed0..8fc8db977a8 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -9,6 +9,7 @@ Twig templates api advanced + sandbox internals deprecated recipes diff --git a/doc/sandbox.rst b/doc/sandbox.rst new file mode 100644 index 00000000000..6f8623b5735 --- /dev/null +++ b/doc/sandbox.rst @@ -0,0 +1,95 @@ +Twig Sandbox +============ + +The ``sandbox`` extension can be used to evaluate untrusted code. + +Registering the Sandbox +----------------------- + +Register the ``SandboxExtension`` extension via the ``addExtension()`` method:: + + $twig->addExtension(new \Twig\Extension\SandboxExtension($policy)); + +Configuring the Sandbox Policy +------------------------------ + +The sandbox security is managed by a policy instance, which must be passed to +the ``SandboxExtension`` constructor. + +By default, Twig comes with one policy class: ``\Twig\Sandbox\SecurityPolicy``. +This class allows you to allow-list some tags, filters, functions, but also +properties and methods on objects:: + + $tags = ['if']; + $filters = ['upper']; + $methods = [ + 'Article' => ['getTitle', 'getBody'], + ]; + $properties = [ + 'Article' => ['title', 'body'], + ]; + $functions = ['range']; + $policy = new \Twig\Sandbox\SecurityPolicy($tags, $filters, $methods, $properties, $functions); + +With the previous configuration, the security policy will only allow usage of +the ``if`` tag, and the ``upper`` filter. Moreover, the templates will only be +able to call the ``getTitle()`` and ``getBody()`` methods on ``Article`` +objects, and the ``title`` and ``body`` public properties. Everything else +won't be allowed and will generate a ``\Twig\Sandbox\SecurityError`` exception. + +.. caution:: + + The ``extends`` and ``use`` tags are always allowed in a sandboxed + template. That behavior will change in 4.0 where these tags will need to be + explicitly allowed like any other tag. + +Enabling the Sandbox +-------------------- + +By default, the sandbox mode is disabled and should be enabled when including +untrusted template code by using the ``sandboxed`` option of the ``include`` +function: + +.. code-block:: twig + + {{ include('user.html', sandboxed: true) }} + +You can sandbox all templates by passing ``true`` as the second argument of +the extension constructor:: + + $sandbox = new \Twig\Extension\SandboxExtension($policy, true); + +Accepting Callables Arguments +----------------------------- + +The Twig sandbox allows you to configure which functions, filters, tests and +dot operations are allowed. Many of these calls can accept arguments. As these +arguments are not validated by the sandbox, you must be very careful. + +For instance, accepting a PHP ``callable`` as an argument is dangerous as it +allows end user to call any PHP function (by passing a ``string``) or any +static methods (by passing an ``array``). For instance, it would accept any PHP +built-in functions like ``system()`` or ``exec()``:: + + $twig->addFilter(new \Twig\TwigFilter('custom', function (callable $callable) { + // ... + $callable(); + // ... + })); + +To avoid this security issue, don't type-hint such arguments with ``callable`` +but use ``\Closure`` instead (not using a type-hint would also be problematic). +This restricts the allowed callables to PHP closures only, which is enough to +accept Twig arrow functions:: + + $twig->addFilter(new \Twig\TwigFilter('custom', function (\Closure $callable) { + // ... + $callable(); + // ... + })); + + {{ people|custom(p => p.username|join(', ') }} + +Any PHP callable can easily be converted to a closure by using the `first-class callable syntax`_. + +.. _`first-class callable syntax`: https://www.php.net/manual/en/functions.first_class_callable_syntax.php diff --git a/doc/tags/include.rst b/doc/tags/include.rst index a1668d22d86..c0a3c042592 100644 --- a/doc/tags/include.rst +++ b/doc/tags/include.rst @@ -70,8 +70,8 @@ You can disable access to the context by appending the ``only`` keyword: .. tip:: When including a template created by an end user, you should consider - sandboxing it. More information in the :doc:`Twig for Developers<../api>` - chapter and in the :doc:`sandbox<../tags/sandbox>` tag documentation. + sandboxing it. More information in the :doc:`Twig Sandbox<../sandbox>` + chapter. The template name can be any valid Twig expression: From 4f7d487a179e2ddf3eea022abd873d2e02ab128b Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sun, 6 Oct 2024 22:36:59 +0200 Subject: [PATCH 542/812] Fix deprecation dep feature --- src/ExpressionParser.php | 46 +++++++++++++------ .../Fixtures/tests/null_coalesce.legacy.test | 28 ++++++++++- 2 files changed, 60 insertions(+), 14 deletions(-) diff --git a/src/ExpressionParser.php b/src/ExpressionParser.php index 01cb9b93461..987d64e1f35 100644 --- a/src/ExpressionParser.php +++ b/src/ExpressionParser.php @@ -56,6 +56,7 @@ class ExpressionParser private $binaryOperators; private $readyNodes = []; private array $precedenceChanges = []; + private bool $deprecationCheck = true; public function __construct( private Parser $parser, @@ -86,14 +87,14 @@ public function __construct( } } - public function parseExpression($precedence = 0, $allowArrow = false, bool $deprecationCheck = true) + public function parseExpression($precedence = 0, $allowArrow = false) { if ($allowArrow && $arrow = $this->parseArrow()) { return $arrow; } $expr = $this->getPrimary(); - $previousToken = $token = $this->parser->getCurrentToken(); + $token = $this->parser->getCurrentToken(); while ($this->isBinary($token) && $this->binaryOperators[$token->getValue()]['precedence'] >= $precedence) { $op = $this->binaryOperators[$token->getValue()]; $this->parser->getStream()->next(); @@ -105,19 +106,21 @@ public function parseExpression($precedence = 0, $allowArrow = false, bool $depr } elseif (isset($op['callable'])) { $expr = $op['callable']($this->parser, $expr); } else { - $expr1 = $this->parseExpression(self::OPERATOR_LEFT === $op['associativity'] ? $op['precedence'] + 1 : $op['precedence'], true); + $previous = $this->setDeprecationCheck(true); + try { + $expr1 = $this->parseExpression(self::OPERATOR_LEFT === $op['associativity'] ? $op['precedence'] + 1 : $op['precedence'], true); + } finally { + $this->setDeprecationCheck($previous); + } $class = $op['class']; $expr = new $class($expr, $expr1, $token->getLine()); } $expr->setAttribute('operator', 'binary_'.$token->getValue()); - $previousToken = $token; - $token = $this->parser->getCurrentToken(); - } + $this->triggerPrecedenceDeprecations($expr, $token); - if ($deprecationCheck) { - $this->triggerPrecedenceDeprecations($expr, $previousToken); + $token = $this->parser->getCurrentToken(); } if (0 === $precedence) { @@ -147,7 +150,7 @@ private function triggerPrecedenceDeprecations(AbstractExpression $expr, Token $ continue; } if ($node->hasAttribute('operator') && $operatorName === $node->getAttribute('operator')) { - trigger_deprecation($change->getPackage(), $change->getVersion(), \sprintf('Add explicit parentheses around the "%s" unary operator to avoid behavior change in the next major version as its precedence will change in "%s" at line %d.', $target, $this->parser->getStream()->getSourceContext()->getName(), $token->getLine())); + trigger_deprecation($change->getPackage(), $change->getVersion(), \sprintf('Add explicit parentheses around the "%s" unary operator to avoid behavior change in the next major version as its precedence will change in "%s" at line %d.', $target, $this->parser->getStream()->getSourceContext()->getName(), $node->getTemplateLine())); } } } else { @@ -157,7 +160,7 @@ private function triggerPrecedenceDeprecations(AbstractExpression $expr, Token $ if ($node->hasAttribute('operator') && $operatorName === $node->getAttribute('operator') && !$node->hasExplicitParentheses()) { $op = explode('_', $operatorName)[1]; $change = $this->binaryOperators[$op]['precedence_change']; - trigger_deprecation($change->getPackage(), $change->getVersion(), \sprintf('Add explicit parentheses around the "%s" binary operator to avoid behavior change in the next major version as its precedence will change in "%s" at line %d.', $op, $this->parser->getStream()->getSourceContext()->getName(), $token->getLine())); + trigger_deprecation($change->getPackage(), $change->getVersion(), \sprintf('Add explicit parentheses around the "%s" binary operator to avoid behavior change in the next major version as its precedence will change in "%s" at line %d.', $op, $this->parser->getStream()->getSourceContext()->getName(), $node->getTemplateLine())); } } } @@ -178,7 +181,7 @@ private function parseArrow() $names = [new AssignNameExpression($token->getValue(), $token->getLine())]; $stream->expect(Token::ARROW_TYPE); - return new ArrowFunctionExpression($this->parseExpression(0), new Nodes($names), $line); + return new ArrowFunctionExpression($this->parseExpression(), new Nodes($names), $line); } // first, determine if we are parsing an arrow function by finding => (long form) @@ -219,7 +222,7 @@ private function parseArrow() $stream->expect(Token::PUNCTUATION_TYPE, ')'); $stream->expect(Token::ARROW_TYPE); - return new ArrowFunctionExpression($this->parseExpression(0), new Nodes($names), $line); + return new ArrowFunctionExpression($this->parseExpression(), new Nodes($names), $line); } private function getPrimary(): AbstractExpression @@ -235,10 +238,19 @@ private function getPrimary(): AbstractExpression $expr = new $class($expr, $token->getLine()); $expr->setAttribute('operator', 'unary_'.$token->getValue()); + if ($this->deprecationCheck) { + $this->triggerPrecedenceDeprecations($expr, $token); + } + return $this->parsePostfixExpression($expr); } elseif ($token->test(Token::PUNCTUATION_TYPE, '(')) { $this->parser->getStream()->next(); - $expr = $this->parseExpression(deprecationCheck: false)->setExplicitParentheses(); + $previous = $this->setDeprecationCheck(false); + try { + $expr = $this->parseExpression()->setExplicitParentheses(); + } finally { + $this->setDeprecationCheck($previous); + } $this->parser->getStream()->expect(Token::PUNCTUATION_TYPE, ')', 'An opened parenthesis is not properly closed'); return $this->parsePostfixExpression($expr); @@ -921,4 +933,12 @@ private function checkConstantExpression(Node $node): bool return true; } + + private function setDeprecationCheck(bool $deprecationCheck): bool + { + $current = $this->deprecationCheck; + $this->deprecationCheck = $deprecationCheck; + + return $current; + } } diff --git a/tests/Fixtures/tests/null_coalesce.legacy.test b/tests/Fixtures/tests/null_coalesce.legacy.test index 3d33d0a797f..2b2036660c9 100644 --- a/tests/Fixtures/tests/null_coalesce.legacy.test +++ b/tests/Fixtures/tests/null_coalesce.legacy.test @@ -3,14 +3,40 @@ Twig supports the ?? operator --DEPRECATION-- Since twig/twig 3.15: Add explicit parentheses around the "??" binary operator to avoid behavior change in the next major version as its precedence will change in "index.twig" at line 4. Since twig/twig 3.15: Add explicit parentheses around the "??" binary operator to avoid behavior change in the next major version as its precedence will change in "index.twig" at line 5. +Since twig/twig 3.15: Add explicit parentheses around the "??" binary operator to avoid behavior change in the next major version as its precedence will change in "index.twig" at line 6. +Since twig/twig 3.15: Add explicit parentheses around the "??" binary operator to avoid behavior change in the next major version as its precedence will change in "index.twig" at line 7. +Since twig/twig 3.15: Add explicit parentheses around the "??" binary operator to avoid behavior change in the next major version as its precedence will change in "index.twig" at line 10. +Since twig/twig 3.15: Add explicit parentheses around the "~" binary operator to avoid behavior change in the next major version as its precedence will change in "index.twig" at line 9. +Since twig/twig 3.15: Add explicit parentheses around the "~" binary operator to avoid behavior change in the next major version as its precedence will change in "index.twig" at line 11. +Since twig/twig 3.15: Add explicit parentheses around the "??" binary operator to avoid behavior change in the next major version as its precedence will change in "index.twig" at line 16. +Since twig/twig 3.15: Add explicit parentheses around the "~" binary operator to avoid behavior change in the next major version as its precedence will change in "index.twig" at line 15. +Since twig/twig 3.15: Add explicit parentheses around the "~" binary operator to avoid behavior change in the next major version as its precedence will change in "index.twig" at line 17. --TEMPLATE-- {{ nope ?? nada ?? 'OK' -}} {# no deprecation as the operators have the same precedence #} {{ 1 + nope ?? nada ?? 2 }} -{{ 1 + nope ?? 3 + nada ?? 2 }} +{{ 1 + nope ?? + 3 + nada ?? 2 }} +{{ 1 ~ 'notnull' ?? 'foo' ~ '_bar' }} +{{ + 1 ~ 2 + 3 + ?? + 1 ~ + 2 + 4 +}} +{{ ( + 1 ~ 2 + 3 + ?? + 1 ~ + 2 + 4 + ) +}} --DATA-- return [] --EXPECT-- OK 3 6 +1notnull_bar +48 +48 From aca4d22e89cccf9194a1d17efc8d070477395012 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Fri, 4 Oct 2024 21:21:57 +0200 Subject: [PATCH 543/812] Allow arrow functions everywhere --- CHANGELOG | 1 + doc/templates.rst | 26 +++++++++++++++++++++++++ src/ExpressionParser.php | 24 +++++++++++++++-------- tests/Fixtures/macros/arrow_as_arg.test | 19 ++++++++++++++++++ 4 files changed, 62 insertions(+), 8 deletions(-) create mode 100644 tests/Fixtures/macros/arrow_as_arg.test diff --git a/CHANGELOG b/CHANGELOG index dda9d5191bf..2a20109f6cb 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,6 @@ # 3.15.0 (2024-XX-XX) + * Allow arrow functions everywhere * Deprecate passing a string or an array to Twig callable arguments accepting arrow functions (pass a `\Closure`) * Add support for triggering deprecations for future operator precedence changes * Deprecate using the `not` unary operator in an expression with ``*``, ``/``, ``//``, or ``%`` without using explicit parentheses to clarify precedence diff --git a/doc/templates.rst b/doc/templates.rst index 764c5cfda4b..54f26c4b6e6 100644 --- a/doc/templates.rst +++ b/doc/templates.rst @@ -960,6 +960,32 @@ The following operators don't fit into any of the other categories: Support for expanding the arguments of a function call was introduced in Twig 3.15. +* ``=>``: The arrow operator allows the creation of functions. A function is + made of arguments (use parentheses for multiple arguments) and an arrow + (``=>``) followed by an expression to execute. The expression has access to + all passed arguments. Arrow functions are supported as arguments for filters, + functions, tests, macros, and method calls. + + For instance, the built-in ``map``, ``reduce``, ``sort``, ``filter``, and + ``find`` filters accept arrow functions as arguments: + + .. code-block:: twig + + {{ people|map(p => p.first_name)|join(', ') }} + + Arrow functions can be stored in variables: + + .. code-block:: twig + + {% set first_name_fn = (p) => p.first_name %} + + {{ people|map(first_name_fn)|join(', ') }} + + .. versionadded:: 3.15 + + Arrow function support for functions, macros, and method calls was added in + Twig 3.15 (filters and tests were already supported). + Operators ~~~~~~~~~ diff --git a/src/ExpressionParser.php b/src/ExpressionParser.php index 987d64e1f35..4e570ff765a 100644 --- a/src/ExpressionParser.php +++ b/src/ExpressionParser.php @@ -87,9 +87,13 @@ public function __construct( } } - public function parseExpression($precedence = 0, $allowArrow = false) + public function parseExpression($precedence = 0) { - if ($allowArrow && $arrow = $this->parseArrow()) { + if (func_num_args() > 1) { + trigger_deprecation('twig/twig', '3.15', 'Passing a second argument ($allowArrow) to "%s()" is deprecated.', __METHOD__); + } + + if ($arrow = $this->parseArrow()) { return $arrow; } @@ -108,7 +112,7 @@ public function parseExpression($precedence = 0, $allowArrow = false) } else { $previous = $this->setDeprecationCheck(true); try { - $expr1 = $this->parseExpression(self::OPERATOR_LEFT === $op['associativity'] ? $op['precedence'] + 1 : $op['precedence'], true); + $expr1 = $this->parseExpression(self::OPERATOR_LEFT === $op['associativity'] ? $op['precedence'] + 1 : $op['precedence']); } finally { $this->setDeprecationCheck($previous); } @@ -672,7 +676,7 @@ public function parseFilterExpressionRaw($node) if (!$this->parser->getStream()->test(Token::PUNCTUATION_TYPE, '(')) { $arguments = new EmptyNode(); } else { - $arguments = $this->parseArguments(true, false, true); + $arguments = $this->parseArguments(true); } $filter = $this->getFilter($token->getValue(), $token->getLine()); @@ -708,8 +712,12 @@ public function parseFilterExpressionRaw($node) * * @throws SyntaxError */ - public function parseArguments($namedArguments = false, $definition = false, $allowArrow = false) + public function parseArguments($namedArguments = false, $definition = false) { + if (func_num_args() > 2) { + trigger_deprecation('twig/twig', '3.15', 'Passing a third argument ($allowArrow) to "%s()" is deprecated.', __METHOD__); + } + $args = []; $stream = $this->parser->getStream(); @@ -731,11 +739,11 @@ public function parseArguments($namedArguments = false, $definition = false, $al } else { if ($stream->nextIf(Token::SPREAD_TYPE)) { $hasSpread = true; - $value = new SpreadUnary($this->parseExpression(0, $allowArrow), $stream->getCurrent()->getLine()); + $value = new SpreadUnary($this->parseExpression(), $stream->getCurrent()->getLine()); } elseif ($hasSpread) { throw new SyntaxError('Normal arguments must be placed before argument unpacking.', $stream->getCurrent()->getLine(), $stream->getSourceContext()); } else { - $value = $this->parseExpression(0, $allowArrow); + $value = $this->parseExpression(); } } @@ -753,7 +761,7 @@ public function parseArguments($namedArguments = false, $definition = false, $al throw new SyntaxError('A default value for an argument must be a constant (a boolean, a string, a number, a sequence, or a mapping).', $token->getLine(), $stream->getSourceContext()); } } else { - $value = $this->parseExpression(0, $allowArrow); + $value = $this->parseExpression(); } } diff --git a/tests/Fixtures/macros/arrow_as_arg.test b/tests/Fixtures/macros/arrow_as_arg.test new file mode 100644 index 00000000000..5bae34652f4 --- /dev/null +++ b/tests/Fixtures/macros/arrow_as_arg.test @@ -0,0 +1,19 @@ +--TEST-- +macro +--TEMPLATE-- +{% set people = [ + {first: "Bob", last: "Smith"}, + {first: "Alice", last: "Dupond"}, +] %} + +{% set first_name_fn = (p) => p.first %} + +{{ _self.display_people(people, first_name_fn) }} + +{% macro display_people(people, fn) %} + {{ people|map(fn)|join(', ') }} +{% endmacro %} +--DATA-- +return [] +--EXPECT-- +Bob, Alice From 62f0a96b68e62c762d72bd6b8243f312370bf69a Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sun, 15 Sep 2024 10:07:31 +0200 Subject: [PATCH 544/812] Introduce a way to test Twig callables at compile time --- CHANGELOG | 1 + doc/tags/guard.rst | 28 ++++++++++ doc/tags/index.rst | 1 + src/ExpressionParser.php | 10 ++++ src/Extension/CoreExtension.php | 2 + src/Parser.php | 22 ++++++++ src/TokenParser/GuardTokenParser.php | 69 ++++++++++++++++++++++++ tests/Fixtures/tags/guard/basic.test | 21 ++++++++ tests/Fixtures/tags/guard/exception.test | 12 +++++ tests/Fixtures/tags/guard/nested.test | 60 +++++++++++++++++++++ 10 files changed, 226 insertions(+) create mode 100644 doc/tags/guard.rst create mode 100644 src/TokenParser/GuardTokenParser.php create mode 100644 tests/Fixtures/tags/guard/basic.test create mode 100644 tests/Fixtures/tags/guard/exception.test create mode 100644 tests/Fixtures/tags/guard/nested.test diff --git a/CHANGELOG b/CHANGELOG index 2a20109f6cb..ef500e8aa75 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,6 @@ # 3.15.0 (2024-XX-XX) + * Add a new `guard` tag that allows to test if some Twig callables are available at compilation time * Allow arrow functions everywhere * Deprecate passing a string or an array to Twig callable arguments accepting arrow functions (pass a `\Closure`) * Add support for triggering deprecations for future operator precedence changes diff --git a/doc/tags/guard.rst b/doc/tags/guard.rst new file mode 100644 index 00000000000..8c9ef7cfeab --- /dev/null +++ b/doc/tags/guard.rst @@ -0,0 +1,28 @@ +``guard`` +========= + +.. versionadded:: 3.13 + + The ``guard`` tag was added in Twig 3.15. + +The ``guard`` statement checks if some Twig callables are available at +**compilation time** to bypass code compilation that would otherwise fail. + +.. code-block:: twig + + {% guard function importmap %} + {{ importmap('app') }} + {% endguard %} + +The first argument is the Twig callable to test: ``filter``, ``function``, or +``test``. The second argument is the Twig callable name you want to test. + +You can also generate different code if the callable does not exist: + +.. code-block:: twig + + {% guard function importmap %} + {{ importmap('app') }} + {% else %} + {# the importmap function doesn't exist, generate fallback code #} + {% endguard %} diff --git a/doc/tags/index.rst b/doc/tags/index.rst index 692ca6094d1..2c456630963 100644 --- a/doc/tags/index.rst +++ b/doc/tags/index.rst @@ -12,6 +12,7 @@ Tags do embed extends + guard flush for from diff --git a/src/ExpressionParser.php b/src/ExpressionParser.php index 4e570ff765a..dbfba20d443 100644 --- a/src/ExpressionParser.php +++ b/src/ExpressionParser.php @@ -13,6 +13,7 @@ namespace Twig; use Twig\Attribute\FirstClassTwigCallableReady; +use Twig\Error\RuntimeError; use Twig\Error\SyntaxError; use Twig\Node\EmptyNode; use Twig\Node\Expression\AbstractExpression; @@ -875,6 +876,9 @@ private function getTest(int $line): TwigTest } if (!$test) { + if ($this->parser->shouldIgnoreUnknownTwigCallables()) { + return new TwigTest($name, fn () => ''); + } $e = new SyntaxError(\sprintf('Unknown "%s" test.', $name), $line, $stream->getSourceContext()); $e->addSuggestions($name, array_keys($this->env->getTests())); @@ -893,6 +897,9 @@ private function getTest(int $line): TwigTest private function getFunction(string $name, int $line): TwigFunction { if (!$function = $this->env->getFunction($name)) { + if ($this->parser->shouldIgnoreUnknownTwigCallables()) { + return new TwigFunction($name, fn () => ''); + } $e = new SyntaxError(\sprintf('Unknown "%s" function.', $name), $line, $this->parser->getStream()->getSourceContext()); $e->addSuggestions($name, array_keys($this->env->getFunctions())); @@ -910,6 +917,9 @@ private function getFunction(string $name, int $line): TwigFunction private function getFilter(string $name, int $line): TwigFilter { if (!$filter = $this->env->getFilter($name)) { + if ($this->parser->shouldIgnoreUnknownTwigCallables()) { + return new TwigFilter($name, fn () => ''); + } $e = new SyntaxError(\sprintf('Unknown "%s" filter.', $name), $line, $this->parser->getStream()->getSourceContext()); $e->addSuggestions($name, array_keys($this->env->getFilters())); diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index 334ab408495..21a67c217a9 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -81,6 +81,7 @@ use Twig\TokenParser\FlushTokenParser; use Twig\TokenParser\ForTokenParser; use Twig\TokenParser\FromTokenParser; +use Twig\TokenParser\GuardTokenParser; use Twig\TokenParser\IfTokenParser; use Twig\TokenParser\ImportTokenParser; use Twig\TokenParser\IncludeTokenParser; @@ -195,6 +196,7 @@ public function getTokenParsers(): array new EmbedTokenParser(), new WithTokenParser(), new DeprecatedTokenParser(), + new GuardTokenParser(), ]; } diff --git a/src/Parser.php b/src/Parser.php index d30868a069a..05ad88ce878 100644 --- a/src/Parser.php +++ b/src/Parser.php @@ -46,12 +46,18 @@ class Parser private $traits; private $embeddedTemplates = []; private $varNameSalt = 0; + private $ignoreUnknownTwigCallables = false; public function __construct( private Environment $env, ) { } + public function getEnvironment(): Environment + { + return $this->env; + } + public function getVarName(): string { return \sprintf('__internal_parse_%d', $this->varNameSalt++); @@ -116,6 +122,22 @@ public function parse(TokenStream $stream, $test = null, bool $dropNeedle = fals return $node; } + public function shouldIgnoreUnknownTwigCallables(): bool + { + return $this->ignoreUnknownTwigCallables; + } + + public function subparseIgnoreUnknownTwigCallables($test, bool $dropNeedle = false): void + { + $previous = $this->ignoreUnknownTwigCallables; + $this->ignoreUnknownTwigCallables = true; + try { + $this->subparse($test, $dropNeedle); + } finally { + $this->ignoreUnknownTwigCallables = $previous; + } + } + public function subparse($test, bool $dropNeedle = false): Node { $lineno = $this->getCurrentToken()->getLine(); diff --git a/src/TokenParser/GuardTokenParser.php b/src/TokenParser/GuardTokenParser.php new file mode 100644 index 00000000000..17b221d4c85 --- /dev/null +++ b/src/TokenParser/GuardTokenParser.php @@ -0,0 +1,69 @@ +parser->getStream(); + $typeToken = $stream->expect(Token::NAME_TYPE); + if (!in_array($typeToken->getValue(), ['function', 'filter', 'test'])) { + throw new SyntaxError(\sprintf('Supported guard types are function, filter and test, "%s" given.', $typeToken->getValue()), $typeToken->getLine(), $stream->getSourceContext()); + } + $method = 'get'.$typeToken->getValue(); + + $nameToken = $stream->expect(Token::NAME_TYPE); + + $exists = null !== $this->parser->getEnvironment()->$method($nameToken->getValue()); + + $stream->expect(Token::BLOCK_END_TYPE); + if ($exists) { + $body = $this->parser->subparse([$this, 'decideGuardFork']); + } else { + $body = new EmptyNode(); + $this->parser->subparseIgnoreUnknownTwigCallables([$this, 'decideGuardFork']); + } + $else = new EmptyNode(); + if ('else' === $stream->next()->getValue()) { + $stream->expect(Token::BLOCK_END_TYPE); + $else = $this->parser->subparse([$this, 'decideGuardEnd'], true); + } + $stream->expect(Token::BLOCK_END_TYPE); + + return new Nodes([$exists ? $body : $else]); + } + + public function decideGuardFork(Token $token): bool + { + return $token->test(['else', 'endguard']); + } + + public function decideGuardEnd(Token $token): bool + { + return $token->test(['endguard']); + } + + public function getTag(): string + { + return 'guard'; + } +} diff --git a/tests/Fixtures/tags/guard/basic.test b/tests/Fixtures/tags/guard/basic.test new file mode 100644 index 00000000000..2c27a5ae7a4 --- /dev/null +++ b/tests/Fixtures/tags/guard/basic.test @@ -0,0 +1,21 @@ +--TEST-- +"guard" creates a compilation time condition on Twig callables availability +--TEMPLATE-- +{% guard filter foobar %} + NEVER + {{ 'a'|foobar }} +{% else -%} + The foobar filter doesn't exist +{% endguard %} + +{% guard function constant -%} + The constant function does exist +{% else %} + NEVER +{% endguard %} +--DATA-- +return [] +--EXPECT-- +The foobar filter doesn't exist + +The constant function does exist diff --git a/tests/Fixtures/tags/guard/exception.test b/tests/Fixtures/tags/guard/exception.test new file mode 100644 index 00000000000..644eccd5486 --- /dev/null +++ b/tests/Fixtures/tags/guard/exception.test @@ -0,0 +1,12 @@ +--TEST-- +"guard" creates a compilation time condition on Twig callables availability +--TEMPLATE-- +{% guard function foobar %} + {{ foobar() }} +{% else %} + {{ foobar() }} +{% endguard %} +--DATA-- +return [] +--EXCEPTION-- +Twig\Error\SyntaxError: Unknown "foobar" function in "index.twig" at line 5. diff --git a/tests/Fixtures/tags/guard/nested.test b/tests/Fixtures/tags/guard/nested.test new file mode 100644 index 00000000000..2918b141673 --- /dev/null +++ b/tests/Fixtures/tags/guard/nested.test @@ -0,0 +1,60 @@ +--TEST-- +"guard" creates a compilation time condition on Twig callables availability +--TEMPLATE-- +{% guard function constant %} + {% guard filter foobar %} + NEVER + {{ 'a'|foobar }} + {% else %} + The constant function does exist, but the foobar filter does not + {%- endguard %} +{% else %} + NEVER +{% endguard %} + +{% guard function constant -%} + {% guard filter upper -%} + The constant function does exist, and the upper filter as well + {%- else %} + NEVER + {% endguard %} +{% else %} + NEVER +{% endguard %} + +{% guard filter foobar %} + NEVER + {{ 'a'|foobar }} +{% else -%} + {% guard function barfoo %} + NEVER + {% else -%} + The foobar filter does not exist, and the barfoo function does not exist + {%- endguard %} +{% endguard %} + +{% guard filter foobar %} + NEVER + {{ 'a'|foobar }} +{% else %} + {%- guard function constant -%} + The foobar filter does not exist, but the constant function exists + {% else -%} + NEVER + {% endguard %} +{% endguard %} + +{% guard function first %} + {% guard function second %} + NEVER + {{ second() }} + {% endguard %} + {{ first() }} +{% endguard %} +--DATA-- +return [] +--EXPECT-- +The constant function does exist, but the foobar filter does not +The constant function does exist, and the upper filter as well +The foobar filter does not exist, and the barfoo function does not exist +The foobar filter does not exist, but the constant function exists From 7bf0f6f376dfe9df01b8082d316e3a71326b8fac Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Tue, 8 Oct 2024 21:51:29 +0200 Subject: [PATCH 545/812] Cleanup code --- src/ExpressionParser.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/ExpressionParser.php b/src/ExpressionParser.php index dbfba20d443..937d1d704ed 100644 --- a/src/ExpressionParser.php +++ b/src/ExpressionParser.php @@ -13,7 +13,6 @@ namespace Twig; use Twig\Attribute\FirstClassTwigCallableReady; -use Twig\Error\RuntimeError; use Twig\Error\SyntaxError; use Twig\Node\EmptyNode; use Twig\Node\Expression\AbstractExpression; @@ -135,7 +134,7 @@ public function parseExpression($precedence = 0) return $expr; } - private function triggerPrecedenceDeprecations(AbstractExpression $expr, Token $token): void + private function triggerPrecedenceDeprecations(AbstractExpression $expr): void { // Check that the all nodes that are between the 2 precedences have explicit parentheses if (!$expr->hasAttribute('operator') || !isset($this->precedenceChanges[$expr->getAttribute('operator')])) { From 309d3f50cb4f3a90d8174518138ebac3810916e7 Mon Sep 17 00:00:00 2001 From: Antoine M Date: Tue, 8 Oct 2024 22:19:25 +0200 Subject: [PATCH 546/812] chore: fix version added comming from slack message of @javiereguiluz nice addition! --- doc/tags/guard.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/tags/guard.rst b/doc/tags/guard.rst index 8c9ef7cfeab..c655bcf9080 100644 --- a/doc/tags/guard.rst +++ b/doc/tags/guard.rst @@ -1,7 +1,7 @@ ``guard`` ========= -.. versionadded:: 3.13 +.. versionadded:: 3.15 The ``guard`` tag was added in Twig 3.15. From 2cdca8cbeb4ef1abbf753dbeac0bf5d718ebe93e Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Wed, 9 Oct 2024 09:12:24 +0200 Subject: [PATCH 547/812] Refactor expression parser --- src/ExpressionParser.php | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/src/ExpressionParser.php b/src/ExpressionParser.php index dbfba20d443..3c89a95900c 100644 --- a/src/ExpressionParser.php +++ b/src/ExpressionParser.php @@ -341,6 +341,14 @@ public function parsePrimaryExpression() $node = $this->parseStringExpression(); break; + case Token::PUNCTUATION_TYPE: + $node = match ($token->getValue()) { + '[' => $this->parseSequenceExpression(), + '{' => $this->parseMappingExpression(), + default => throw new SyntaxError(\sprintf('Unexpected token "%s" of value "%s".', Token::typeToEnglish($token->getType()), $token->getValue()), $token->getLine(), $this->parser->getStream()->getSourceContext()), + }; + break; + case Token::OPERATOR_TYPE: if (preg_match(Lexer::REGEX_NAME, $token->getValue(), $matches) && $matches[0] == $token->getValue()) { // in this context, string operators are variable names @@ -362,17 +370,13 @@ public function parsePrimaryExpression() break; } - // no break - default: - if ($token->test(Token::PUNCTUATION_TYPE, '[')) { - $node = $this->parseSequenceExpression(); - } elseif ($token->test(Token::PUNCTUATION_TYPE, '{')) { - $node = $this->parseMappingExpression(); - } elseif ($token->test(Token::OPERATOR_TYPE, '=') && ('==' === $this->parser->getStream()->look(-1)->getValue() || '!=' === $this->parser->getStream()->look(-1)->getValue())) { + if ('=' === $token->getValue() && ('==' === $this->parser->getStream()->look(-1)->getValue() || '!=' === $this->parser->getStream()->look(-1)->getValue())) { throw new SyntaxError(\sprintf('Unexpected operator of value "%s". Did you try to use "===" or "!==" for strict comparison? Use "is same as(value)" instead.', $token->getValue()), $token->getLine(), $this->parser->getStream()->getSourceContext()); - } else { - throw new SyntaxError(\sprintf('Unexpected token "%s" of value "%s".', Token::typeToEnglish($token->getType()), $token->getValue()), $token->getLine(), $this->parser->getStream()->getSourceContext()); } + + // no break + default: + throw new SyntaxError(\sprintf('Unexpected token "%s" of value "%s".', Token::typeToEnglish($token->getType()), $token->getValue()), $token->getLine(), $this->parser->getStream()->getSourceContext()); } return $this->parsePostfixExpression($node); From 0ec3f4a7fe032815f56aa98624e2c52f8e6f3e5a Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Wed, 9 Oct 2024 10:33:51 +0200 Subject: [PATCH 548/812] Simplify expression parser code --- src/ExpressionParser.php | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/src/ExpressionParser.php b/src/ExpressionParser.php index efb6a576b6a..56d6d4df594 100644 --- a/src/ExpressionParser.php +++ b/src/ExpressionParser.php @@ -356,19 +356,6 @@ public function parsePrimaryExpression() break; } - if (isset($this->unaryOperators[$token->getValue()])) { - $class = $this->unaryOperators[$token->getValue()]['class']; - if (!\in_array($class, [NegUnary::class, PosUnary::class])) { - throw new SyntaxError(\sprintf('Unexpected unary operator "%s".', $token->getValue()), $token->getLine(), $this->parser->getStream()->getSourceContext()); - } - - $this->parser->getStream()->next(); - $expr = $this->parsePrimaryExpression(); - - $node = new $class($expr, $token->getLine()); - break; - } - if ('=' === $token->getValue() && ('==' === $this->parser->getStream()->look(-1)->getValue() || '!=' === $this->parser->getStream()->look(-1)->getValue())) { throw new SyntaxError(\sprintf('Unexpected operator of value "%s". Did you try to use "===" or "!==" for strict comparison? Use "is same as(value)" instead.', $token->getValue()), $token->getLine(), $this->parser->getStream()->getSourceContext()); } @@ -759,7 +746,7 @@ public function parseArguments($namedArguments = false, $definition = false) $name = $value->getAttribute('name'); if ($definition) { - $value = $this->parsePrimaryExpression(); + $value = $this->getPrimary(); if (!$this->checkConstantExpression($value)) { throw new SyntaxError('A default value for an argument must be a constant (a boolean, a string, a number, a sequence, or a mapping).', $token->getLine(), $stream->getSourceContext()); @@ -842,7 +829,7 @@ private function parseTestExpression(Node $node): TestExpression if ($stream->test(Token::PUNCTUATION_TYPE, '(')) { $arguments = $this->parseArguments(true); } elseif ($test->hasOneMandatoryArgument()) { - $arguments = new Nodes([0 => $this->parsePrimaryExpression()]); + $arguments = new Nodes([0 => $this->getPrimary()]); } if ('defined' === $test->getName() && $node instanceof NameExpression && null !== $alias = $this->parser->getImportedSymbol('function', $node->getAttribute('name'))) { From ed1b44470f3e66c04dad82b4869579ce24520593 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Fri, 11 Oct 2024 08:16:49 +0200 Subject: [PATCH 549/812] Improve deprecation messages --- src/Lexer.php | 4 ++-- tests/LexerTest.php | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Lexer.php b/src/Lexer.php index 1754791265e..4983313cf03 100644 --- a/src/Lexer.php +++ b/src/Lexer.php @@ -423,7 +423,7 @@ private function stripcslashes(string $str, string $quoteType): string $result .= $nextChar; } elseif ("'" === $nextChar || '"' === $nextChar) { if ($nextChar !== $quoteType) { - trigger_deprecation('twig/twig', '3.12', 'Character "%s" at position %d in string on line %d should not be escaped; the "\" character is ignored in Twig v3 but will not be in v4. Please remove the extra "\" character.', $nextChar, $i + 1, $this->lineno); + trigger_deprecation('twig/twig', '3.12', 'Character "%s" should not be escaped; the "\" character is ignored in Twig 3 but will not be in Twig 4. Please remove the extra "\" character at position %d in "%s" at line %d.', $nextChar, $i + 1, $this->source->getName(), $this->lineno); } $result .= $nextChar; } elseif ('#' === $nextChar && $i + 1 < $length && '{' === $str[$i + 1]) { @@ -442,7 +442,7 @@ private function stripcslashes(string $str, string $quoteType): string } $result .= \chr(octdec($octal)); } else { - trigger_deprecation('twig/twig', '3.12', 'Character "%s" at position %d in string on line %d should not be escaped; the "\" character is ignored in Twig v3 but will not be in v4. Please remove the extra "\" character.', $nextChar, $i + 1, $this->lineno); + trigger_deprecation('twig/twig', '3.12', 'Character "%s" should not be escaped; the "\" character is ignored in Twig 3 but will not be in Twig 4. Please remove the extra "\" character at position %d in "%s" at line %d.', $nextChar, $i + 1, $this->source->getName(), $this->lineno); $result .= $nextChar; } diff --git a/tests/LexerTest.php b/tests/LexerTest.php index 53a6cb155dd..d5640c8daa7 100644 --- a/tests/LexerTest.php +++ b/tests/LexerTest.php @@ -281,7 +281,7 @@ public static function getStringWithEscapedDelimiterProducingDeprecation() {{ 'App\Test' }} EOF, 'AppTest', - 'Since twig/twig 3.12: Character "T" at position 5 in string on line 1 should not be escaped; the "\" character is ignored in Twig v3 but will not be in v4. Please remove the extra "\" character.', + 'Since twig/twig 3.12: Character "T" should not be escaped; the "\" character is ignored in Twig 3 but will not be in Twig 4. Please remove the extra "\" character at position 5 in "index" at line 1.', ]; yield [ <<<'EOF' @@ -290,14 +290,14 @@ public static function getStringWithEscapedDelimiterProducingDeprecation() <<<'EOF' foo ' bar EOF, - 'Since twig/twig 3.12: Character "\'" at position 6 in string on line 1 should not be escaped; the "\" character is ignored in Twig v3 but will not be in v4. Please remove the extra "\" character.', + 'Since twig/twig 3.12: Character "\'" should not be escaped; the "\" character is ignored in Twig 3 but will not be in Twig 4. Please remove the extra "\" character at position 6 in "index" at line 1.', ]; yield [ <<<'EOF' {{ 'foo \" bar' }} EOF, 'foo " bar', - 'Since twig/twig 3.12: Character """ at position 6 in string on line 1 should not be escaped; the "\" character is ignored in Twig v3 but will not be in v4. Please remove the extra "\" character.', + 'Since twig/twig 3.12: Character """ should not be escaped; the "\" character is ignored in Twig 3 but will not be in Twig 4. Please remove the extra "\" character at position 6 in "index" at line 1.', ]; } From 223d36bbae72768d3f21510c0ee9815f4c0e5b9d Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Thu, 10 Oct 2024 10:43:40 +0200 Subject: [PATCH 550/812] Add support for named arguments on macro calls and dot operator arguments --- CHANGELOG | 2 ++ doc/tags/macro.rst | 7 ++-- doc/templates.rst | 33 ++++++++++++++++--- src/ExpressionParser.php | 28 +++++++++------- src/Node/Expression/ArrayExpression.php | 9 ++++- src/Node/Expression/GetAttrExpression.php | 8 +++++ src/Node/Expression/MethodCallExpression.php | 18 ++-------- src/Node/Expression/TempNameExpression.php | 16 +++++---- src/Node/MacroNode.php | 17 ++++++---- tests/ExpressionParserTest.php | 18 ---------- .../expressions/dynamic_attribute.test | 6 ++++ tests/Fixtures/expressions/method_call.test | 6 ++++ .../Fixtures/tags/macro/named_arguments.test | 14 ++++++++ tests/Node/MacroTest.php | 16 ++++----- 14 files changed, 127 insertions(+), 71 deletions(-) create mode 100644 tests/Fixtures/tags/macro/named_arguments.test diff --git a/CHANGELOG b/CHANGELOG index ef500e8aa75..5b07b5955bd 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,7 @@ # 3.15.0 (2024-XX-XX) + * Add named arguments support for the dot operator arguments (`foo.bar(some: arg)`) + * Add named arguments support for macros * Add a new `guard` tag that allows to test if some Twig callables are available at compilation time * Allow arrow functions everywhere * Deprecate passing a string or an array to Twig callable arguments accepting arrow functions (pass a `\Closure`) diff --git a/doc/tags/macro.rst b/doc/tags/macro.rst index d1d0641c090..13c032915a9 100644 --- a/doc/tags/macro.rst +++ b/doc/tags/macro.rst @@ -52,8 +52,8 @@ tag: {% import "forms.twig" as forms %} The above ``import`` call imports the ``forms.twig`` file (which can contain -only macros, or a template and some macros), and import the macros as items of -the ``forms`` local variable. +only macros, or a template and some macros), and import the macros as +attributes of the ``forms`` local variable. The macros can then be called at will in the *current* template: @@ -61,6 +61,8 @@ The macros can then be called at will in the *current* template:

    {{ forms.input('username') }}

    {{ forms.input('password', null, 'password') }}

    + {# You can also use named arguments #} +

    {{ forms.input(name: 'password', type: 'password') }}

    Alternatively you can import names from the template into the current namespace via the ``from`` tag: @@ -70,6 +72,7 @@ via the ``from`` tag: {% from 'forms.twig' import input as input_field, textarea %}

    {{ input_field('password', '', 'password') }}

    +

    {{ input_field(name: 'password', type: 'password') }}

    {{ textarea('comment') }}

    .. caution:: diff --git a/doc/templates.rst b/doc/templates.rst index 54f26c4b6e6..4fbbe2fec64 100644 --- a/doc/templates.rst +++ b/doc/templates.rst @@ -225,7 +225,13 @@ built-in functions. Named Arguments --------------- -Named arguments are supported in functions, filters, and tests. +Named arguments are supported everywhere you can pass arguments: functions, +filters, tests, macros, and dot operator arguments. + +.. versionadded:: 3.15 + + Named arguments for macros and dot operator arguments were added in Twig + 3.15. .. versionadded:: 3.12 @@ -873,12 +879,15 @@ The following operators don't fit into any of the other categories: * ``.``, ``[]``: Gets an attribute of a variable. The (``.``) operator abstracts getting an attribute of a variable (methods, - properties or constants of a PHP object, or items of a PHP array): + properties or constants of a PHP object, or items of a PHP array): .. code-block:: twig {{ user.name }} + Twig supports a specific syntax via the ``[]`` operator for accessing items + on sequences and mappings, like in ``user['name']``: + After the ``.``, you can use any expression by wrapping it with parenthesis ``()``. @@ -900,6 +909,22 @@ The following operators don't fit into any of the other categories: Before Twig 3.15, use the :doc:`attribute ` function instead for the two previous use cases. + Twig supports a specific syntax via the ``[]`` operator for accessing items + on sequences and mappings: + + .. code-block:: twig + + {{ user['name'] }} + + When calling a method, you can pass arguments using the ``()`` operator: + + .. code-block:: twig + + {{ html.generate_input() }} + {{ html.generate_input('pwd', 'password') }} + {# or using named arguments #} + {{ html.generate_input(name: 'pwd', type: 'password') }} + .. sidebar:: PHP Implementation To resolve ``user.name`` to a PHP call, Twig uses the following algorithm @@ -915,8 +940,8 @@ The following operators don't fit into any of the other categories: * if not, and if ``strict_variables`` is ``false``, return ``null``; * if not, throw an exception. - Twig supports a specific syntax via the ``[]`` operator for accessing items - on sequences and mappings, like in ``user['name']``: + To resolve ``user['name']`` to a PHP call, Twig uses the following algorithm + at runtime: * check if ``user`` is an array and ``name`` a valid element; * if not, and if ``strict_variables`` is ``false``, return ``null``; diff --git a/src/ExpressionParser.php b/src/ExpressionParser.php index 56d6d4df594..ddfbd2a47fd 100644 --- a/src/ExpressionParser.php +++ b/src/ExpressionParser.php @@ -26,6 +26,7 @@ use Twig\Node\Expression\GetAttrExpression; use Twig\Node\Expression\MethodCallExpression; use Twig\Node\Expression\NameExpression; +use Twig\Node\Expression\TempNameExpression; use Twig\Node\Expression\TestExpression; use Twig\Node\Expression\Unary\AbstractUnary; use Twig\Node\Expression\Unary\NegUnary; @@ -530,11 +531,7 @@ public function parsePostfixExpression($node) public function getFunctionNode($name, $line) { if (null !== $alias = $this->parser->getImportedSymbol('function', $name)) { - $arguments = new ArrayExpression([], $line); - foreach ($this->parseArguments() as $n) { - $arguments->addElement($n); - } - + $arguments = $this->createArguments($line); $node = new MethodCallExpression($alias['node'], $alias['name'], $arguments, $line); $node->setAttribute('safe', true); @@ -575,9 +572,7 @@ public function parseSubscriptExpression($node) $stream->expect(Token::PUNCTUATION_TYPE, ')'); if ($stream->test(Token::PUNCTUATION_TYPE, '(')) { $type = Template::METHOD_CALL; - foreach ($this->parseArguments() as $n) { - $arguments->addElement($n); - } + $arguments = $this->createArguments($lineno); } return new GetAttrExpression($node, $arg, $arguments, $type, $lineno); @@ -594,9 +589,7 @@ public function parseSubscriptExpression($node) if ($stream->test(Token::PUNCTUATION_TYPE, '(')) { $type = Template::METHOD_CALL; - foreach ($this->parseArguments() as $n) { - $arguments->addElement($n); - } + $arguments = $this->createArguments($lineno); } } else { throw new SyntaxError(\sprintf('Expected name or number, got value "%s" of type %s.', $token->getValue(), Token::typeToEnglish($token->getType())), $lineno, $stream->getSourceContext()); @@ -708,6 +701,9 @@ public function parseArguments($namedArguments = false, $definition = false) if (func_num_args() > 2) { trigger_deprecation('twig/twig', '3.15', 'Passing a third argument ($allowArrow) to "%s()" is deprecated.', __METHOD__); } + if (!$namedArguments) { + trigger_deprecation('twig/twig', '3.15', 'Passing "false" for the first argument ($namedArguments) to "%s()" is deprecated.', __METHOD__); + } $args = []; $stream = $this->parser->getStream(); @@ -949,4 +945,14 @@ private function setDeprecationCheck(bool $deprecationCheck): bool return $current; } + + private function createArguments(int $line): ArrayExpression + { + $arguments = new ArrayExpression([], $line); + foreach ($this->parseArguments(true) as $k => $n) { + $arguments->addElement($n, new TempNameExpression($k, $line)); + } + + return $arguments; + } } diff --git a/src/Node/Expression/ArrayExpression.php b/src/Node/Expression/ArrayExpression.php index 6c6efee13c4..9769b719e36 100644 --- a/src/Node/Expression/ArrayExpression.php +++ b/src/Node/Expression/ArrayExpression.php @@ -98,10 +98,17 @@ public function compile(Compiler $compiler): void $compiler->raw('...')->subcompile($pair['value']); ++$nextIndex; } else { - $key = $pair['key'] instanceof ConstantExpression ? $pair['key']->getAttribute('value') : null; + $key = null; if ($pair['key'] instanceof NameExpression) { $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) { if (\is_int($key)) { diff --git a/src/Node/Expression/GetAttrExpression.php b/src/Node/Expression/GetAttrExpression.php index 29a446b881b..571f6aea17c 100644 --- a/src/Node/Expression/GetAttrExpression.php +++ b/src/Node/Expression/GetAttrExpression.php @@ -18,6 +18,10 @@ class GetAttrExpression extends AbstractExpression { + + /** + * @param ArrayExpression|NameExpression|null $arguments + */ public function __construct(AbstractExpression $node, AbstractExpression $attribute, ?AbstractExpression $arguments, string $type, int $lineno) { $nodes = ['node' => $node, 'attribute' => $attribute]; @@ -25,6 +29,10 @@ public function __construct(AbstractExpression $node, AbstractExpression $attrib $nodes['arguments'] = $arguments; } + if ($arguments && !$arguments instanceof ArrayExpression && !$arguments instanceof NameExpression) { + trigger_deprecation('twig/twig', '3.15', \sprintf('Not passing a "%s" instance as the "arguments" argument of the "%s" constructor is deprecated ("%s" given).', ArrayExpression::class, static::class, $arguments::class)); + } + parent::__construct($nodes, ['type' => $type, 'is_defined_test' => false, 'ignore_strict_check' => false, 'optimizable' => true], $lineno); } diff --git a/src/Node/Expression/MethodCallExpression.php b/src/Node/Expression/MethodCallExpression.php index 01806f91d10..1b337960d87 100644 --- a/src/Node/Expression/MethodCallExpression.php +++ b/src/Node/Expression/MethodCallExpression.php @@ -43,21 +43,9 @@ public function compile(Compiler $compiler): void ->repr($this->getNode('node')->getAttribute('name')) ->raw('], ') ->repr($this->getAttribute('method')) - ->raw(', [') - ; - $first = true; - /** @var ArrayExpression */ - $args = $this->getNode('arguments'); - foreach ($args->getKeyValuePairs() as $pair) { - if (!$first) { - $compiler->raw(', '); - } - $first = false; - - $compiler->subcompile($pair['value']); - } - $compiler - ->raw('], ') + ->raw(', ') + ->subcompile($this->getNode('arguments')) + ->raw(', ') ->repr($this->getTemplateLine()) ->raw(', $context, $this->getSourceContext())'); } diff --git a/src/Node/Expression/TempNameExpression.php b/src/Node/Expression/TempNameExpression.php index 004c704a588..c1f09165670 100644 --- a/src/Node/Expression/TempNameExpression.php +++ b/src/Node/Expression/TempNameExpression.php @@ -15,17 +15,21 @@ class TempNameExpression extends AbstractExpression { - public function __construct(string $name, int $lineno) + public const RESERVED_NAMES = ['varargs', 'context', 'macros', 'blocks', 'this']; + + public function __construct(string|int $name, int $lineno) { + if (is_int($name) || ctype_digit($name)) { + $name = (int) $name; + } elseif (in_array($name, self::RESERVED_NAMES)) { + $name = '_'.$name.'_'; + } + parent::__construct([], ['name' => $name], $lineno); } public function compile(Compiler $compiler): void { - $compiler - ->raw('$_') - ->raw($this->getAttribute('name')) - ->raw('_') - ; + $compiler->raw('$'.$this->getAttribute('name')); } } diff --git a/src/Node/MacroNode.php b/src/Node/MacroNode.php index 5a2543a9fd4..f3120dbd00c 100644 --- a/src/Node/MacroNode.php +++ b/src/Node/MacroNode.php @@ -14,6 +14,7 @@ use Twig\Attribute\YieldReady; use Twig\Compiler; use Twig\Error\SyntaxError; +use Twig\Node\Expression\TempNameExpression; /** * Represents a macro node. @@ -31,13 +32,17 @@ class MacroNode extends Node public function __construct(string $name, Node $body, Node $arguments, int $lineno) { if (!$body instanceof BodyNode) { - trigger_deprecation('twig/twig', '3.12', \sprintf('Not passing a "%s" instance as the "body" argument of the "%s" constructor is deprecated.', BodyNode::class, static::class)); + trigger_deprecation('twig/twig', '3.12', \sprintf('Not passing a "%s" instance as the "body" argument of the "%s" constructor is deprecated ("%s" given).', BodyNode::class, static::class, $body::class)); } foreach ($arguments as $argumentName => $argument) { if (self::VARARGS_NAME === $argumentName) { throw new SyntaxError(\sprintf('The argument "%s" in macro "%s" cannot be defined because the variable "%s" is reserved for arbitrary arguments.', self::VARARGS_NAME, $name, self::VARARGS_NAME), $argument->getTemplateLine(), $argument->getSourceContext()); } + if (in_array($argumentName, TempNameExpression::RESERVED_NAMES)) { + $arguments->setNode('_'.$argumentName.'_', $argument); + $arguments->removeNode($argumentName); + } } parent::__construct(['body' => $body, 'arguments' => $arguments], ['name' => $name], $lineno); @@ -54,7 +59,7 @@ public function compile(Compiler $compiler): void $pos = 0; foreach ($this->getNode('arguments') as $name => $default) { $compiler - ->raw('$__'.$name.'__ = ') + ->raw('$'.$name.' = ') ->subcompile($default) ; @@ -68,7 +73,7 @@ public function compile(Compiler $compiler): void } $compiler - ->raw('...$__varargs__') + ->raw('...$varargs') ->raw(")\n") ->write("{\n") ->indent() @@ -80,8 +85,8 @@ public function compile(Compiler $compiler): void foreach ($this->getNode('arguments') as $name => $default) { $compiler ->write('') - ->string($name) - ->raw(' => $__'.$name.'__') + ->string(trim($name, '_')) + ->raw(' => $'.$name) ->raw(",\n") ; } @@ -92,7 +97,7 @@ public function compile(Compiler $compiler): void ->write('') ->string(self::VARARGS_NAME) ->raw(' => ') - ->raw("\$__varargs__,\n") + ->raw("\$varargs,\n") ->outdent() ->write("] + \$this->env->getGlobals();\n\n") ->write("\$blocks = [];\n\n") diff --git a/tests/ExpressionParserTest.php b/tests/ExpressionParserTest.php index 82ff47d44de..def632c1932 100644 --- a/tests/ExpressionParserTest.php +++ b/tests/ExpressionParserTest.php @@ -275,24 +275,6 @@ public static function getTestsForString() ]; } - public function testAttributeCallDoesNotSupportNamedArguments() - { - $env = new Environment(new ArrayLoader(), ['cache' => false, 'autoescape' => false]); - $parser = new Parser($env); - - $this->expectException(SyntaxError::class); - $parser->parse($env->tokenize(new Source('{{ foo.bar(name="Foo") }}', 'index'))); - } - - public function testMacroCallDoesNotSupportNamedArguments() - { - $env = new Environment(new ArrayLoader(), ['cache' => false, 'autoescape' => false]); - $parser = new Parser($env); - - $this->expectException(SyntaxError::class); - $parser->parse($env->tokenize(new Source('{% from _self import foo %}{% macro foo() %}{% endmacro %}{{ foo(name="Foo") }}', 'index'))); - } - public function testMacroDefinitionDoesNotSupportNonNameVariableName() { $env = new Environment(new ArrayLoader(), ['cache' => false, 'autoescape' => false]); diff --git a/tests/Fixtures/expressions/dynamic_attribute.test b/tests/Fixtures/expressions/dynamic_attribute.test index 930c6f17498..93731076ab8 100644 --- a/tests/Fixtures/expressions/dynamic_attribute.test +++ b/tests/Fixtures/expressions/dynamic_attribute.test @@ -4,6 +4,9 @@ {{ obj.(method) }} {{ array.(item) }} {{ obj.("bar")("a", "b") }} +{{ obj.("bar")(param1: "a", param2: "b") }} +{{ obj.("bar")(param2: "b", param1: "a") }} +{{ obj.("bar")("a", param2: "b") }} {{ obj.("bar")(...arguments) }} {{ obj.(method) is defined ? 'ok' : 'ko' }} {{ obj.(nonmethod) is defined ? 'ok' : 'ko' }} @@ -14,5 +17,8 @@ foo bar bar_a-b bar_a-b +bar_a-b +bar_a-b +bar_a-b ok ko diff --git a/tests/Fixtures/expressions/method_call.test b/tests/Fixtures/expressions/method_call.test index bf49f389e00..ee700f80f69 100644 --- a/tests/Fixtures/expressions/method_call.test +++ b/tests/Fixtures/expressions/method_call.test @@ -6,6 +6,9 @@ Twig supports method calls {{ items.foo.bar }} {{ items.foo['bar'] }} {{ items.foo.bar('a', 43) }} +{{ items.foo.bar(param1: 'a', param2: 43) }} +{{ items.foo.bar(param2: 43, param1: 'a') }} +{{ items.foo.bar('a', param2: 43) }} {{ items.foo.bar(foo) }} {{ items.foo.self.foo() }} {{ items.foo.is }} @@ -20,6 +23,9 @@ foo foo bar +bar_a-43 +bar_a-43 +bar_a-43 bar_a-43 bar_bar foo diff --git a/tests/Fixtures/tags/macro/named_arguments.test b/tests/Fixtures/tags/macro/named_arguments.test new file mode 100644 index 00000000000..58bd15b2024 --- /dev/null +++ b/tests/Fixtures/tags/macro/named_arguments.test @@ -0,0 +1,14 @@ +--TEST-- +"macro" tag +--TEMPLATE-- +{% import _self as forms %} + +{{ forms.input(size: 10, name: 'username') }} + +{% macro input(name, value, type, size) %} + +{% endmacro %} +--DATA-- +return [] +--EXPECT-- + diff --git a/tests/Node/MacroTest.php b/tests/Node/MacroTest.php index 7321c780197..1902b73f0c2 100644 --- a/tests/Node/MacroTest.php +++ b/tests/Node/MacroTest.php @@ -46,13 +46,13 @@ public static function provideTests(): iterable yield 'with use_yield = true' => [$node, <<macros; \$context = [ - "foo" => \$__foo__, - "bar" => \$__bar__, - "varargs" => \$__varargs__, + "foo" => \$foo, + "bar" => \$bar, + "varargs" => \$varargs, ] + \$this->env->getGlobals(); \$blocks = []; @@ -68,13 +68,13 @@ public function macro_foo(\$__foo__ = null, \$__bar__ = "Foo", ...\$__varargs__) yield 'with use_yield = false' => [$node, <<macros; \$context = [ - "foo" => \$__foo__, - "bar" => \$__bar__, - "varargs" => \$__varargs__, + "foo" => \$foo, + "bar" => \$bar, + "varargs" => \$varargs, ] + \$this->env->getGlobals(); \$blocks = []; From 831e69b77e20efe49e102f322b602321a655c8b0 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Mon, 14 Oct 2024 10:41:48 +0200 Subject: [PATCH 551/812] Do not allow : as macro definition separator --- src/ExpressionParser.php | 2 +- .../colon_not_supported_as_default_separator.test | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) create mode 100644 tests/Fixtures/tags/macro/colon_not_supported_as_default_separator.test diff --git a/src/ExpressionParser.php b/src/ExpressionParser.php index ddfbd2a47fd..9e3debeeaca 100644 --- a/src/ExpressionParser.php +++ b/src/ExpressionParser.php @@ -735,7 +735,7 @@ public function parseArguments($namedArguments = false, $definition = false) } $name = null; - if ($namedArguments && (($token = $stream->nextIf(Token::OPERATOR_TYPE, '=')) || ($token = $stream->nextIf(Token::PUNCTUATION_TYPE, ':')))) { + if ($namedArguments && (($token = $stream->nextIf(Token::OPERATOR_TYPE, '=')) || (!$definition && $token = $stream->nextIf(Token::PUNCTUATION_TYPE, ':')))) { if (!$value instanceof NameExpression) { throw new SyntaxError(\sprintf('A parameter name must be a string, "%s" given.', \get_class($value)), $token->getLine(), $stream->getSourceContext()); } diff --git a/tests/Fixtures/tags/macro/colon_not_supported_as_default_separator.test b/tests/Fixtures/tags/macro/colon_not_supported_as_default_separator.test new file mode 100644 index 00000000000..6b0bb25fbc0 --- /dev/null +++ b/tests/Fixtures/tags/macro/colon_not_supported_as_default_separator.test @@ -0,0 +1,10 @@ +--TEST-- +"macro" tag does not support : as a separator in definition, only = is supported +--TEMPLATE-- +{% macro test(foo: "foo") -%} + {{ foo }} +{%- endmacro %} +--DATA-- +return [] +--EXCEPTION-- +Twig\Error\SyntaxError: Arguments must be separated by a comma. Unexpected token "punctuation" of value ":" ("punctuation" expected with value ",") in "index.twig" at line 2. From 9af72e6323e0c9714a676b36f776b254bba81434 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Mon, 14 Oct 2024 13:36:30 +0200 Subject: [PATCH 552/812] Fix AssignNameExpression to forbid using names that won't work --- src/ExpressionParser.php | 6 +----- src/Node/Expression/AssignNameExpression.php | 11 +++++++++++ src/TokenParser/FromTokenParser.php | 7 ++++--- tests/Fixtures/filters/arrow_reserved_names.test | 8 ++++++++ tests/Fixtures/tags/for/reserved_names.test | 9 +++++++++ tests/Fixtures/tags/macro/from_reserved_names.test | 13 +++++++++++++ .../Fixtures/tags/macro/import_reserved_names.test | 8 ++++++++ tests/Fixtures/tags/set/reserved_names.test | 8 ++++++++ 8 files changed, 62 insertions(+), 8 deletions(-) create mode 100644 tests/Fixtures/filters/arrow_reserved_names.test create mode 100644 tests/Fixtures/tags/for/reserved_names.test create mode 100644 tests/Fixtures/tags/macro/from_reserved_names.test create mode 100644 tests/Fixtures/tags/macro/import_reserved_names.test create mode 100644 tests/Fixtures/tags/set/reserved_names.test diff --git a/src/ExpressionParser.php b/src/ExpressionParser.php index ddfbd2a47fd..449e78c2e12 100644 --- a/src/ExpressionParser.php +++ b/src/ExpressionParser.php @@ -784,11 +784,7 @@ public function parseAssignmentExpression() } else { $stream->expect(Token::NAME_TYPE, null, 'Only variables can be assigned to'); } - $value = $token->getValue(); - if (\in_array(strtolower($value), ['true', 'false', 'none', 'null'])) { - throw new SyntaxError(\sprintf('You cannot assign a value to "%s".', $value), $token->getLine(), $stream->getSourceContext()); - } - $targets[] = new AssignNameExpression($value, $token->getLine()); + $targets[] = new AssignNameExpression($token->getValue(), $token->getLine()); if (!$stream->nextIf(Token::PUNCTUATION_TYPE, ',')) { break; diff --git a/src/Node/Expression/AssignNameExpression.php b/src/Node/Expression/AssignNameExpression.php index 7dd1bc4a372..a0df3b7a36a 100644 --- a/src/Node/Expression/AssignNameExpression.php +++ b/src/Node/Expression/AssignNameExpression.php @@ -13,9 +13,20 @@ namespace Twig\Node\Expression; use Twig\Compiler; +use Twig\Error\SyntaxError; class AssignNameExpression extends NameExpression { + public function __construct(string $name, int $lineno) + { + // All names supported by ExpressionParser::parsePrimaryExpression() should be excluded + if (\in_array(strtolower($name), ['true', 'false', 'none', 'null'])) { + throw new SyntaxError(\sprintf('You cannot assign a value to "%s".', $name), $lineno); + } + + parent::__construct($name, $lineno); + } + public function compile(Compiler $compiler): void { $compiler diff --git a/src/TokenParser/FromTokenParser.php b/src/TokenParser/FromTokenParser.php index 2ccff5fbe6c..92811aa140e 100644 --- a/src/TokenParser/FromTokenParser.php +++ b/src/TokenParser/FromTokenParser.php @@ -35,9 +35,10 @@ public function parse(Token $token): Node while (true) { $name = $stream->expect(Token::NAME_TYPE)->getValue(); - $alias = $name; if ($stream->nextIf('as')) { - $alias = $stream->expect(Token::NAME_TYPE)->getValue(); + $alias = new AssignNameExpression($stream->expect(Token::NAME_TYPE)->getValue(), $token->getLine()); + } else { + $alias = new AssignNameExpression($name, $token->getLine()); } $targets[$name] = $alias; @@ -53,7 +54,7 @@ public function parse(Token $token): Node $node = new ImportNode($macro, $var, $token->getLine(), $this->parser->isMainScope()); foreach ($targets as $name => $alias) { - $this->parser->addImportedSymbol('function', $alias, 'macro_'.$name, $var); + $this->parser->addImportedSymbol('function', $alias->getAttribute('name'), 'macro_'.$name, $var); } return $node; diff --git a/tests/Fixtures/filters/arrow_reserved_names.test b/tests/Fixtures/filters/arrow_reserved_names.test new file mode 100644 index 00000000000..3e5d0722b1a --- /dev/null +++ b/tests/Fixtures/filters/arrow_reserved_names.test @@ -0,0 +1,8 @@ +--TEST-- +"map" filter +--TEMPLATE-- +{{ [1, 2]|map(true => true * 2)|join(', ') }} +--DATA-- +return [] +--EXCEPTION-- +Twig\Error\SyntaxError: You cannot assign a value to "true" in "index.twig" at line 2. diff --git a/tests/Fixtures/tags/for/reserved_names.test b/tests/Fixtures/tags/for/reserved_names.test new file mode 100644 index 00000000000..6f9953175d1 --- /dev/null +++ b/tests/Fixtures/tags/for/reserved_names.test @@ -0,0 +1,9 @@ +--TEST-- +"for" tag +--TEMPLATE-- +{% for true in [1, 2] %} +{% endfor %} +--DATA-- +return [] +--EXCEPTION-- +Twig\Error\SyntaxError: You cannot assign a value to "true" in "index.twig" at line 2. diff --git a/tests/Fixtures/tags/macro/from_reserved_names.test b/tests/Fixtures/tags/macro/from_reserved_names.test new file mode 100644 index 00000000000..243fe6250b5 --- /dev/null +++ b/tests/Fixtures/tags/macro/from_reserved_names.test @@ -0,0 +1,13 @@ +--TEST-- +"from" tag +--TEMPLATE-- +{% from _self import input as true %} + +{{ true('username') }} + +{% macro input(name) -%} +{% endmacro %} +--DATA-- +return [] +--EXCEPTION-- +Twig\Error\SyntaxError: You cannot assign a value to "true" in "index.twig" at line 2. diff --git a/tests/Fixtures/tags/macro/import_reserved_names.test b/tests/Fixtures/tags/macro/import_reserved_names.test new file mode 100644 index 00000000000..4e32089b283 --- /dev/null +++ b/tests/Fixtures/tags/macro/import_reserved_names.test @@ -0,0 +1,8 @@ +--TEST-- +"import" tag +--TEMPLATE-- +{% import _self as true %} +--DATA-- +return [] +--EXCEPTION-- +Twig\Error\SyntaxError: You cannot assign a value to "true" in "index.twig" at line 2. diff --git a/tests/Fixtures/tags/set/reserved_names.test b/tests/Fixtures/tags/set/reserved_names.test new file mode 100644 index 00000000000..aa9ad9fb077 --- /dev/null +++ b/tests/Fixtures/tags/set/reserved_names.test @@ -0,0 +1,8 @@ +--TEST-- +"set" tag +--TEMPLATE-- +{% set true = 'foo' %} +--DATA-- +return [] +--EXCEPTION-- +Twig\Error\SyntaxError: You cannot assign a value to "true" in "index.twig" at line 2. From 66658a3c1d0ef09d70508bb2a953f9f6c6c46d8b Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Mon, 14 Oct 2024 15:46:31 +0200 Subject: [PATCH 553/812] Check reserved names in TempNameExpression --- src/ExpressionParser.php | 70 ++++++++++++++++--- src/Node/Expression/TempNameExpression.php | 6 ++ src/Node/MacroNode.php | 48 +++++++------ src/TokenParser/MacroTokenParser.php | 62 +++++++++++++++- .../tags/macro/argument_reserved_names.test | 12 ++++ tests/Node/MacroTest.php | 12 ++-- tests/ParserTest.php | 9 ++- 7 files changed, 176 insertions(+), 43 deletions(-) create mode 100644 tests/Fixtures/tags/macro/argument_reserved_names.test diff --git a/src/ExpressionParser.php b/src/ExpressionParser.php index 11a707581cf..51a10f4f28c 100644 --- a/src/ExpressionParser.php +++ b/src/ExpressionParser.php @@ -538,7 +538,7 @@ public function getFunctionNode($name, $line) return $node; } - $args = $this->parseArguments(true); + $args = $this->parseOnlyArguments(); $function = $this->getFunction($name, $line); if ($function->getParserCallable()) { @@ -660,7 +660,7 @@ public function parseFilterExpressionRaw($node) if (!$this->parser->getStream()->test(Token::PUNCTUATION_TYPE, '(')) { $arguments = new EmptyNode(); } else { - $arguments = $this->parseArguments(true); + $arguments = $this->parseOnlyArguments(); } $filter = $this->getFilter($token->getValue(), $token->getLine()); @@ -689,20 +689,24 @@ public function parseFilterExpressionRaw($node) /** * Parses arguments. * - * @param bool $namedArguments Whether to allow named arguments or not - * @param bool $definition Whether we are parsing arguments for a function (or macro) definition - * * @return Node * * @throws SyntaxError */ - public function parseArguments($namedArguments = false, $definition = false) + public function parseArguments() { + $namedArguments = false; + $definition = false; if (func_num_args() > 2) { trigger_deprecation('twig/twig', '3.15', 'Passing a third argument ($allowArrow) to "%s()" is deprecated.', __METHOD__); } - if (!$namedArguments) { - trigger_deprecation('twig/twig', '3.15', 'Passing "false" for the first argument ($namedArguments) to "%s()" is deprecated.', __METHOD__); + if (func_num_args() > 1) { + trigger_deprecation('twig/twig', '3.15', 'Passing a second argument ($definition) to "%s()" is deprecated.', __METHOD__); + $definition = func_get_arg(1); + } + if (func_num_args() > 0) { + trigger_deprecation('twig/twig', '3.15', 'Passing a first argument ($namedArguments) to "%s()" is deprecated.', __METHOD__); + $namedArguments = func_get_arg(0); } $args = []; @@ -819,7 +823,7 @@ private function parseTestExpression(Node $node): TestExpression $arguments = null; if ($stream->test(Token::PUNCTUATION_TYPE, '(')) { - $arguments = $this->parseArguments(true); + $arguments = $this->parseOnlyArguments(); } elseif ($test->hasOneMandatoryArgument()) { $arguments = new Nodes([0 => $this->getPrimary()]); } @@ -917,6 +921,7 @@ private function getFilter(string $name, int $line): TwigFilter } // checks that the node only contains "constant" elements + // to be removed in 4.0 private function checkConstantExpression(Node $node): bool { if (!($node instanceof ConstantExpression || $node instanceof ArrayExpression @@ -945,10 +950,55 @@ private function setDeprecationCheck(bool $deprecationCheck): bool private function createArguments(int $line): ArrayExpression { $arguments = new ArrayExpression([], $line); - foreach ($this->parseArguments(true) as $k => $n) { + foreach ($this->parseOnlyArguments() as $k => $n) { $arguments->addElement($n, new TempNameExpression($k, $line)); } return $arguments; } + + public function parseOnlyArguments() + { + $args = []; + $stream = $this->parser->getStream(); + $stream->expect(Token::PUNCTUATION_TYPE, '(', 'A list of arguments must begin with an opening parenthesis'); + $hasSpread = false; + while (!$stream->test(Token::PUNCTUATION_TYPE, ')')) { + if ($args) { + $stream->expect(Token::PUNCTUATION_TYPE, ',', 'Arguments must be separated by a comma'); + + // if the comma above was a trailing comma, early exit the argument parse loop + if ($stream->test(Token::PUNCTUATION_TYPE, ')')) { + break; + } + } + + if ($stream->nextIf(Token::SPREAD_TYPE)) { + $hasSpread = true; + $value = new SpreadUnary($this->parseExpression(), $stream->getCurrent()->getLine()); + } elseif ($hasSpread) { + throw new SyntaxError('Normal arguments must be placed before argument unpacking.', $stream->getCurrent()->getLine(), $stream->getSourceContext()); + } else { + $value = $this->parseExpression(); + } + + $name = null; + if (($token = $stream->nextIf(Token::OPERATOR_TYPE, '=')) || ($token = $stream->nextIf(Token::PUNCTUATION_TYPE, ':'))) { + if (!$value instanceof NameExpression) { + throw new SyntaxError(\sprintf('A parameter name must be a string, "%s" given.', \get_class($value)), $token->getLine(), $stream->getSourceContext()); + } + $name = $value->getAttribute('name'); + $value = $this->parseExpression(); + } + + if (null === $name) { + $args[] = $value; + } else { + $args[$name] = $value; + } + } + $stream->expect(Token::PUNCTUATION_TYPE, ')', 'A list of arguments must be closed by a parenthesis'); + + return new Nodes($args); + } } diff --git a/src/Node/Expression/TempNameExpression.php b/src/Node/Expression/TempNameExpression.php index c1f09165670..8bdb456ef09 100644 --- a/src/Node/Expression/TempNameExpression.php +++ b/src/Node/Expression/TempNameExpression.php @@ -12,6 +12,7 @@ namespace Twig\Node\Expression; use Twig\Compiler; +use Twig\Error\SyntaxError; class TempNameExpression extends AbstractExpression { @@ -19,6 +20,11 @@ class TempNameExpression extends AbstractExpression public function __construct(string|int $name, int $lineno) { + // All names supported by ExpressionParser::parsePrimaryExpression() should be excluded + if (\in_array(strtolower($name), ['true', 'false', 'none', 'null'])) { + throw new SyntaxError(\sprintf('You cannot assign a value to "%s".', $name), $lineno); + } + if (is_int($name) || ctype_digit($name)) { $name = (int) $name; } elseif (in_array($name, self::RESERVED_NAMES)) { diff --git a/src/Node/MacroNode.php b/src/Node/MacroNode.php index f3120dbd00c..7c2603a622c 100644 --- a/src/Node/MacroNode.php +++ b/src/Node/MacroNode.php @@ -14,6 +14,7 @@ use Twig\Attribute\YieldReady; use Twig\Compiler; use Twig\Error\SyntaxError; +use Twig\Node\Expression\ArrayExpression; use Twig\Node\Expression\TempNameExpression; /** @@ -27,7 +28,8 @@ class MacroNode extends Node public const VARARGS_NAME = 'varargs'; /** - * @param BodyNode $body + * @param BodyNode $body + * @param ArrayExpression $arguments */ public function __construct(string $name, Node $body, Node $arguments, int $lineno) { @@ -35,13 +37,19 @@ public function __construct(string $name, Node $body, Node $arguments, int $line trigger_deprecation('twig/twig', '3.12', \sprintf('Not passing a "%s" instance as the "body" argument of the "%s" constructor is deprecated ("%s" given).', BodyNode::class, static::class, $body::class)); } - foreach ($arguments as $argumentName => $argument) { - if (self::VARARGS_NAME === $argumentName) { - throw new SyntaxError(\sprintf('The argument "%s" in macro "%s" cannot be defined because the variable "%s" is reserved for arbitrary arguments.', self::VARARGS_NAME, $name, self::VARARGS_NAME), $argument->getTemplateLine(), $argument->getSourceContext()); + if (!$arguments instanceof ArrayExpression) { + trigger_deprecation('twig/twig', '3.15', \sprintf('Not passing a "%s" instance as the "arguments" argument of the "%s" constructor is deprecated ("%s" given).', ArrayExpression::class, static::class, $arguments::class)); + + $args = new ArrayExpression([], $arguments->getTemplateLine()); + foreach ($arguments as $name => $default) { + $args->addElement($default, new TempNameExpression($name, $default->getTemplateLine())); } - if (in_array($argumentName, TempNameExpression::RESERVED_NAMES)) { - $arguments->setNode('_'.$argumentName.'_', $argument); - $arguments->removeNode($argumentName); + $arguments = $args; + } + + foreach ($arguments->getKeyValuePairs() as $pair) { + if ('_'.self::VARARGS_NAME.'_' === $pair['key']->getAttribute('name')) { + throw new SyntaxError(\sprintf('The argument "%s" in macro "%s" cannot be defined because the variable "%s" is reserved for arbitrary arguments.', self::VARARGS_NAME, $name, self::VARARGS_NAME), $pair['value']->getTemplateLine(), $pair['value']->getSourceContext()); } } @@ -55,21 +63,15 @@ public function compile(Compiler $compiler): void ->write(\sprintf('public function macro_%s(', $this->getAttribute('name'))) ; - $count = \count($this->getNode('arguments')); - $pos = 0; - foreach ($this->getNode('arguments') as $name => $default) { + foreach ($this->getNode('arguments')->getKeyValuePairs() as $pair) { + $name = $pair['key']; + $default = $pair['value']; $compiler - ->raw('$'.$name.' = ') + ->subcompile($name) + ->raw(' = ') ->subcompile($default) + ->raw(', ') ; - - if (++$pos < $count) { - $compiler->raw(', '); - } - } - - if ($count) { - $compiler->raw(', '); } $compiler @@ -82,11 +84,13 @@ public function compile(Compiler $compiler): void ->indent() ; - foreach ($this->getNode('arguments') as $name => $default) { + foreach ($this->getNode('arguments')->getKeyValuePairs() as $pair) { + $name = $pair['key']; $compiler ->write('') - ->string(trim($name, '_')) - ->raw(' => $'.$name) + ->string(trim($name->getAttribute('name'), '_')) + ->raw(' => ') + ->subcompile($name) ->raw(",\n") ; } diff --git a/src/TokenParser/MacroTokenParser.php b/src/TokenParser/MacroTokenParser.php index 8dab480f9d2..19260302079 100644 --- a/src/TokenParser/MacroTokenParser.php +++ b/src/TokenParser/MacroTokenParser.php @@ -14,8 +14,15 @@ use Twig\Error\SyntaxError; use Twig\Node\BodyNode; use Twig\Node\EmptyNode; +use Twig\Node\Expression\ArrayExpression; +use Twig\Node\Expression\ConstantExpression; +use Twig\Node\Expression\NameExpression; +use Twig\Node\Expression\TempNameExpression; +use Twig\Node\Expression\Unary\NegUnary; +use Twig\Node\Expression\Unary\PosUnary; use Twig\Node\MacroNode; use Twig\Node\Node; +use Twig\Node\Nodes; use Twig\Token; /** @@ -34,8 +41,7 @@ public function parse(Token $token): Node $lineno = $token->getLine(); $stream = $this->parser->getStream(); $name = $stream->expect(Token::NAME_TYPE)->getValue(); - - $arguments = $this->parser->getExpressionParser()->parseArguments(true, true); + $arguments = $this->parseDefinition(); $stream->expect(Token::BLOCK_END_TYPE); $this->parser->pushLocalScope(); @@ -64,4 +70,56 @@ public function getTag(): string { return 'macro'; } + + private function parseDefinition(): ArrayExpression + { + $arguments = new ArrayExpression([], $this->parser->getCurrentToken()->getLine()); + $stream = $this->parser->getStream(); + $stream->expect(Token::PUNCTUATION_TYPE, '(', 'A list of arguments must begin with an opening parenthesis'); + while (!$stream->test(Token::PUNCTUATION_TYPE, ')')) { + if (count($arguments)) { + $stream->expect(Token::PUNCTUATION_TYPE, ',', 'Arguments must be separated by a comma'); + + // if the comma above was a trailing comma, early exit the argument parse loop + if ($stream->test(Token::PUNCTUATION_TYPE, ')')) { + break; + } + } + + $token = $stream->expect(Token::NAME_TYPE, null, 'An argument must be a name'); + $name = new TempNameExpression($token->getValue(), $this->parser->getCurrentToken()->getLine()); + if ($token = $stream->nextIf(Token::OPERATOR_TYPE, '=')) { + $default = $this->parser->getExpressionParser()->parseExpression(); + } else { + $default = new ConstantExpression(null, $this->parser->getCurrentToken()->getLine()); + $default->setAttribute('is_implicit', true); + } + + if (!$this->checkConstantExpression($default)) { + throw new SyntaxError('A default value for an argument must be a constant (a boolean, a string, a number, a sequence, or a mapping).', $token->getLine(), $stream->getSourceContext()); + } + $arguments->addElement($default, $name); + } + $stream->expect(Token::PUNCTUATION_TYPE, ')', 'A list of arguments must be closed by a parenthesis'); + + return $arguments; + } + + // checks that the node only contains "constant" elements + private function checkConstantExpression(Node $node): bool + { + if (!($node instanceof ConstantExpression || $node instanceof ArrayExpression + || $node instanceof NegUnary || $node instanceof PosUnary + )) { + return false; + } + + foreach ($node as $n) { + if (!$this->checkConstantExpression($n)) { + return false; + } + } + + return true; + } } diff --git a/tests/Fixtures/tags/macro/argument_reserved_names.test b/tests/Fixtures/tags/macro/argument_reserved_names.test new file mode 100644 index 00000000000..736c26a67fc --- /dev/null +++ b/tests/Fixtures/tags/macro/argument_reserved_names.test @@ -0,0 +1,12 @@ +--TEST-- +"macro" tag +--TEMPLATE-- +{% import _self as macros %} + +{% macro input(true, false, null) %} + {{ true }} +{% endmacro %} +--DATA-- +return [] +--EXCEPTION-- +Twig\Error\SyntaxError: You cannot assign a value to "true" in "index.twig" at line 4. diff --git a/tests/Node/MacroTest.php b/tests/Node/MacroTest.php index 1902b73f0c2..2d22ac52fb9 100644 --- a/tests/Node/MacroTest.php +++ b/tests/Node/MacroTest.php @@ -14,8 +14,10 @@ use Twig\Environment; use Twig\Loader\ArrayLoader; use Twig\Node\BodyNode; +use Twig\Node\Expression\ArrayExpression; use Twig\Node\Expression\ConstantExpression; use Twig\Node\Expression\NameExpression; +use Twig\Node\Expression\TempNameExpression; use Twig\Node\MacroNode; use Twig\Node\Nodes; use Twig\Node\TextNode; @@ -26,7 +28,7 @@ class MacroTest extends NodeTestCase public function testConstructor() { $body = new BodyNode([new TextNode('foo', 1)]); - $arguments = new Nodes([new NameExpression('foo', 1)], 1); + $arguments = new ArrayExpression([new NameExpression('foo', 1), new ConstantExpression(null, 1)], 1); $node = new MacroNode('foo', $body, $arguments, 1); $this->assertEquals($body, $node->getNode('body')); @@ -36,9 +38,11 @@ public function testConstructor() public static function provideTests(): iterable { - $arguments = new Nodes([ - 'foo' => new ConstantExpression(null, 1), - 'bar' => new ConstantExpression('Foo', 1), + $arguments = new ArrayExpression([ + new TempNameExpression('foo', 1), + new ConstantExpression(null, 1), + new TempNameExpression('bar', 1), + new ConstantExpression('Foo', 1), ], 1); $body = new BodyNode([new TextNode('foo', 1)]); diff --git a/tests/ParserTest.php b/tests/ParserTest.php index 0602d8d0f77..d1d23bb1b48 100644 --- a/tests/ParserTest.php +++ b/tests/ParserTest.php @@ -194,12 +194,11 @@ public function testImplicitMacroArgumentDefaultValues() ->getNode('arguments') ; - $this->assertTrue($argumentNodes->getNode('po')->hasAttribute('is_implicit')); - $this->assertTrue($argumentNodes->getNode('po')->getAttribute('is_implicit')); - $this->assertNull($argumentNodes->getNode('po')->getAttribute('value')); + $this->assertTrue($argumentNodes->getNode(1)->hasAttribute('is_implicit')); + $this->assertNull($argumentNodes->getNode(1)->getAttribute('value')); - $this->assertFalse($argumentNodes->getNode('lo')->hasAttribute('is_implicit')); - $this->assertTrue($argumentNodes->getNode('lo')->getAttribute('value')); + $this->assertFalse($argumentNodes->getNode(3)->hasAttribute('is_implicit')); + $this->assertTrue($argumentNodes->getNode(3)->getAttribute('value')); } protected function getParser() From 26256fc4880fc9c94902abedc6338b23dc3c012f Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Fri, 18 Oct 2024 11:16:02 +0200 Subject: [PATCH 554/812] reduce the deprecation noise when more than one argument is passed When more than one argument is passed to parseArguments(), one deprecation is triggered for each argument. Since passing any argument is deprecated the number of triggered deprecations can be reduced for more than one argument being passed. --- src/ExpressionParser.php | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/ExpressionParser.php b/src/ExpressionParser.php index 51a10f4f28c..23ecb43a8b7 100644 --- a/src/ExpressionParser.php +++ b/src/ExpressionParser.php @@ -697,15 +697,11 @@ public function parseArguments() { $namedArguments = false; $definition = false; - if (func_num_args() > 2) { - trigger_deprecation('twig/twig', '3.15', 'Passing a third argument ($allowArrow) to "%s()" is deprecated.', __METHOD__); - } if (func_num_args() > 1) { - trigger_deprecation('twig/twig', '3.15', 'Passing a second argument ($definition) to "%s()" is deprecated.', __METHOD__); $definition = func_get_arg(1); } if (func_num_args() > 0) { - trigger_deprecation('twig/twig', '3.15', 'Passing a first argument ($namedArguments) to "%s()" is deprecated.', __METHOD__); + trigger_deprecation('twig/twig', '3.15', 'Passing arguments to "%s()" is deprecated.', __METHOD__); $namedArguments = func_get_arg(0); } From bde4efac5a3f1d0ad3aa19bf579e7f8f12df21ec Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sun, 20 Oct 2024 11:39:45 +0200 Subject: [PATCH 555/812] Remove default value for an argument --- src/NodeVisitor/MacroAutoImportNodeVisitor.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NodeVisitor/MacroAutoImportNodeVisitor.php b/src/NodeVisitor/MacroAutoImportNodeVisitor.php index 01d5a997fff..5f6d273f4c2 100644 --- a/src/NodeVisitor/MacroAutoImportNodeVisitor.php +++ b/src/NodeVisitor/MacroAutoImportNodeVisitor.php @@ -46,7 +46,7 @@ public function leaveNode(Node $node, Environment $env): Node if ($node instanceof ModuleNode) { $this->inAModule = false; if ($this->hasMacroCalls) { - $node->getNode('constructor_end')->setNode('_auto_macro_import', new ImportNode(new NameExpression('_self', 0), new AssignNameExpression('_self', 0), 0, true)); + $node->getNode('constructor_end')->setNode('_auto_macro_import', new ImportNode(new NameExpression('_self', 0), new AssignNameExpression('_self', 0), 0)); } } elseif ($this->inAModule) { if ( From 1105964873c74a5247d8359e7af980a47dcb3d0d Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Fri, 18 Oct 2024 12:00:57 +0200 Subject: [PATCH 556/812] Rework macros handling --- CHANGELOG | 4 + doc/deprecated.rst | 6 + src/ExpressionParser.php | 177 ++++++++++-------- src/Extension/CoreExtension.php | 3 +- .../Expression/MacroReferenceExpression.php | 56 ++++++ src/Node/Expression/MethodCallExpression.php | 2 + src/Node/Expression/Test/DefinedTest.php | 3 + .../Expression/Variable/TemplateVariable.php | 31 +++ src/Node/ImportNode.php | 17 +- src/NodeVisitor/EscaperNodeVisitor.php | 2 +- .../MacroAutoImportNodeVisitor.php | 38 +--- src/NodeVisitor/SafeAnalysisNodeVisitor.php | 3 +- src/Parser.php | 10 +- src/Template.php | 29 +++ src/TokenParser/FromTokenParser.php | 6 +- src/TokenParser/ImportTokenParser.php | 6 +- tests/Fixtures/tests/defined_for_macros.test | 75 ++++++-- tests/Node/ImportTest.php | 9 +- tests/Node/ModuleTest.php | 2 +- 19 files changed, 324 insertions(+), 155 deletions(-) create mode 100644 src/Node/Expression/MacroReferenceExpression.php create mode 100644 src/Node/Expression/Variable/TemplateVariable.php diff --git a/CHANGELOG b/CHANGELOG index 5b07b5955bd..c4646d55743 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,9 @@ # 3.15.0 (2024-XX-XX) + * Deprecate `MacroAutoImportNodeVisitor` + * Deprecate `MethodCallExpression` in favor of `MacroReferenceExpression` + * Fix support for the "is defined" test on `_self.xxx` (auto-imported) macros + * Fix support for the "is defined" test on inherited macros * Add named arguments support for the dot operator arguments (`foo.bar(some: arg)`) * Add named arguments support for macros * Add a new `guard` tag that allows to test if some Twig callables are available at compilation time diff --git a/doc/deprecated.rst b/doc/deprecated.rst index 1239c77bcea..d2074dd46b3 100644 --- a/doc/deprecated.rst +++ b/doc/deprecated.rst @@ -170,6 +170,9 @@ Nodes deprecated as of Twig 3.12: ``arguments``, ``callable``, ``is_variadic``, and ``dynamic_name``. +* The ``MethodCallExpression`` class is deprecated as of Twig 3.15, use + ``MacroReferenceExpression`` instead. + Node Visitors ------------- @@ -181,6 +184,9 @@ Node Visitors deprecated as of Twig 3.12 and will be removed in Twig 4.0; they don't do anything anymore. +* The ``Twig\NodeVisitor\MacroAutoImportNodeVisitor`` class is deprecated as of + Twig 3.15. + Parser ------ diff --git a/src/ExpressionParser.php b/src/ExpressionParser.php index 51a10f4f28c..08a6f0bb394 100644 --- a/src/ExpressionParser.php +++ b/src/ExpressionParser.php @@ -24,7 +24,7 @@ use Twig\Node\Expression\ConditionalExpression; use Twig\Node\Expression\ConstantExpression; use Twig\Node\Expression\GetAttrExpression; -use Twig\Node\Expression\MethodCallExpression; +use Twig\Node\Expression\MacroReferenceExpression; use Twig\Node\Expression\NameExpression; use Twig\Node\Expression\TempNameExpression; use Twig\Node\Expression\TestExpression; @@ -33,6 +33,7 @@ use Twig\Node\Expression\Unary\NotUnary; use Twig\Node\Expression\Unary\PosUnary; use Twig\Node\Expression\Unary\SpreadUnary; +use Twig\Node\Expression\Variable\TemplateVariable; use Twig\Node\Node; use Twig\Node\Nodes; @@ -532,7 +533,7 @@ public function getFunctionNode($name, $line) { if (null !== $alias = $this->parser->getImportedSymbol('function', $name)) { $arguments = $this->createArguments($line); - $node = new MethodCallExpression($alias['node'], $alias['name'], $arguments, $line); + $node = new MacroReferenceExpression(new TemplateVariable($alias['node'], $line), $alias['name'], $arguments, $line); $node->setAttribute('safe', true); return $node; @@ -561,84 +562,11 @@ public function getFunctionNode($name, $line) public function parseSubscriptExpression($node) { - $stream = $this->parser->getStream(); - $token = $stream->next(); - $lineno = $token->getLine(); - $arguments = new ArrayExpression([], $lineno); - $type = Template::ANY_CALL; - if ('.' == $token->getValue()) { - if ($stream->nextIf(Token::PUNCTUATION_TYPE, '(')) { - $arg = $this->parseExpression(); - $stream->expect(Token::PUNCTUATION_TYPE, ')'); - if ($stream->test(Token::PUNCTUATION_TYPE, '(')) { - $type = Template::METHOD_CALL; - $arguments = $this->createArguments($lineno); - } - - return new GetAttrExpression($node, $arg, $arguments, $type, $lineno); - } - $token = $stream->next(); - if ( - Token::NAME_TYPE == $token->getType() - || - Token::NUMBER_TYPE == $token->getType() - || - (Token::OPERATOR_TYPE == $token->getType() && preg_match(Lexer::REGEX_NAME, $token->getValue())) - ) { - $arg = new ConstantExpression($token->getValue(), $lineno); - - if ($stream->test(Token::PUNCTUATION_TYPE, '(')) { - $type = Template::METHOD_CALL; - $arguments = $this->createArguments($lineno); - } - } else { - throw new SyntaxError(\sprintf('Expected name or number, got value "%s" of type %s.', $token->getValue(), Token::typeToEnglish($token->getType())), $lineno, $stream->getSourceContext()); - } - - if ($node instanceof NameExpression && null !== $this->parser->getImportedSymbol('template', $node->getAttribute('name'))) { - $name = $arg->getAttribute('value'); - - $node = new MethodCallExpression($node, 'macro_'.$name, $arguments, $lineno); - $node->setAttribute('safe', true); - - return $node; - } - } else { - $type = Template::ARRAY_CALL; - - // slice? - $slice = false; - if ($stream->test(Token::PUNCTUATION_TYPE, ':')) { - $slice = true; - $arg = new ConstantExpression(0, $token->getLine()); - } else { - $arg = $this->parseExpression(); - } - - if ($stream->nextIf(Token::PUNCTUATION_TYPE, ':')) { - $slice = true; - } - - if ($slice) { - if ($stream->test(Token::PUNCTUATION_TYPE, ']')) { - $length = new ConstantExpression(null, $token->getLine()); - } else { - $length = $this->parseExpression(); - } - - $filter = $this->getFilter('slice', $token->getLine()); - $arguments = new Nodes([$arg, $length]); - $filter = new ($filter->getNodeClass())($node, $filter, $arguments, $token->getLine()); - - $stream->expect(Token::PUNCTUATION_TYPE, ']'); - - return $filter; - } - - $stream->expect(Token::PUNCTUATION_TYPE, ']'); + if ('.' === $this->parser->getStream()->next()->getValue()) { + return $this->parseSubscriptExpressionDot($node); } - return new GetAttrExpression($node, $arg, $arguments, $type, $lineno); + return $this->parseSubscriptExpressionArray($node); } public function parseFilterExpression($node) @@ -829,7 +757,7 @@ private function parseTestExpression(Node $node): TestExpression } if ('defined' === $test->getName() && $node instanceof NameExpression && null !== $alias = $this->parser->getImportedSymbol('function', $node->getAttribute('name'))) { - $node = new MethodCallExpression($alias['node'], $alias['name'], new ArrayExpression([], $node->getTemplateLine()), $node->getTemplateLine()); + $node = new MacroReferenceExpression(new TemplateVariable($alias['node'], $node->getTemplateLine()), $alias['name'], new ArrayExpression([], $node->getTemplateLine()), $node->getTemplateLine()); $node->setAttribute('safe', true); } @@ -1001,4 +929,95 @@ public function parseOnlyArguments() return new Nodes($args); } + + private function parseSubscriptExpressionDot(Node $node): AbstractExpression + { + $stream = $this->parser->getStream(); + $token = $stream->getCurrent(); + $lineno = $token->getLine(); + $arguments = new ArrayExpression([], $lineno); + $type = Template::ANY_CALL; + + if ($stream->nextIf(Token::PUNCTUATION_TYPE, '(')) { + $attribute = $this->parseExpression(); + $stream->expect(Token::PUNCTUATION_TYPE, ')'); + } else { + $token = $stream->next(); + if ( + Token::NAME_TYPE == $token->getType() + || + Token::NUMBER_TYPE == $token->getType() + || + (Token::OPERATOR_TYPE == $token->getType() && preg_match(Lexer::REGEX_NAME, $token->getValue())) + ) { + $attribute = new ConstantExpression($token->getValue(), $token->getLine()); + } else { + throw new SyntaxError(\sprintf('Expected name or number, got value "%s" of type %s.', $token->getValue(), Token::typeToEnglish($token->getType())), $token->getLine(), $stream->getSourceContext()); + } + } + + if ($stream->test(Token::PUNCTUATION_TYPE, '(')) { + $type = Template::METHOD_CALL; + $arguments = $this->createArguments($token->getLine()); + } + + if ( + $node instanceof NameExpression + && + ( + null !== $this->parser->getImportedSymbol('template', $node->getAttribute('name')) + || + '_self' === $node->getAttribute('name') && $attribute instanceof ConstantExpression + ) + ) { + $name = $attribute->getAttribute('value'); + $node = new MacroReferenceExpression(new TemplateVariable($node->getAttribute('name'), $node->getTemplateLine()), 'macro_'.$name, $arguments, $node->getTemplateLine()); + $node->setAttribute('safe', true); + + return $node; + } + + return new GetAttrExpression($node, $attribute, $arguments, $type, $lineno); + } + + private function parseSubscriptExpressionArray(Node $node): AbstractExpression + { + $stream = $this->parser->getStream(); + $token = $stream->getCurrent(); + $lineno = $token->getLine(); + $arguments = new ArrayExpression([], $lineno); + + // slice? + $slice = false; + if ($stream->test(Token::PUNCTUATION_TYPE, ':')) { + $slice = true; + $attribute = new ConstantExpression(0, $token->getLine()); + } else { + $attribute = $this->parseExpression(); + } + + if ($stream->nextIf(Token::PUNCTUATION_TYPE, ':')) { + $slice = true; + } + + if ($slice) { + if ($stream->test(Token::PUNCTUATION_TYPE, ']')) { + $length = new ConstantExpression(null, $token->getLine()); + } else { + $length = $this->parseExpression(); + } + + $filter = $this->getFilter('slice', $token->getLine()); + $arguments = new Nodes([$attribute, $length]); + $filter = new ($filter->getNodeClass())($node, $filter, $arguments, $token->getLine()); + + $stream->expect(Token::PUNCTUATION_TYPE, ']'); + + return $filter; + } + + $stream->expect(Token::PUNCTUATION_TYPE, ']'); + + return new GetAttrExpression($node, $attribute, $arguments, Template::ARRAY_CALL, $lineno); + } } diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index 21a67c217a9..cf3b417e451 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -66,7 +66,6 @@ use Twig\Node\Expression\Unary\NotUnary; use Twig\Node\Expression\Unary\PosUnary; use Twig\Node\Node; -use Twig\NodeVisitor\MacroAutoImportNodeVisitor; use Twig\OperatorPrecedenceChange; use Twig\Parser; use Twig\Source; @@ -293,7 +292,7 @@ public function getTests(): array public function getNodeVisitors(): array { - return [new MacroAutoImportNodeVisitor()]; + return []; } public function getOperators(): array diff --git a/src/Node/Expression/MacroReferenceExpression.php b/src/Node/Expression/MacroReferenceExpression.php new file mode 100644 index 00000000000..abe99aa3564 --- /dev/null +++ b/src/Node/Expression/MacroReferenceExpression.php @@ -0,0 +1,56 @@ + + */ +class MacroReferenceExpression extends AbstractExpression +{ + public function __construct(TemplateVariable $template, string $name, AbstractExpression $arguments, int $lineno) + { + parent::__construct(['template' => $template, 'arguments' => $arguments], ['name' => $name, 'is_defined_test' => false], $lineno); + } + + public function compile(Compiler $compiler): void + { + if ($this->getAttribute('is_defined_test')) { + $compiler + ->subcompile($this->getNode('template')) + ->raw('->hasMacro(') + ->repr($this->getAttribute('name')) + ->raw(', $context') + ->raw(')') + ; + + return; + } + + $compiler + ->subcompile($this->getNode('template')) + ->raw('->getTemplateForMacro(') + ->repr($this->getAttribute('name')) + ->raw(', $context, ') + ->repr($this->getTemplateLine()) + ->raw(', $this->getSourceContext())') + ->raw(\sprintf('->%s', $this->getAttribute('name'))) + ->raw('(...') + ->subcompile($this->getNode('arguments')) + ->raw(')') + ; + } +} diff --git a/src/Node/Expression/MethodCallExpression.php b/src/Node/Expression/MethodCallExpression.php index 1b337960d87..9aede826ca0 100644 --- a/src/Node/Expression/MethodCallExpression.php +++ b/src/Node/Expression/MethodCallExpression.php @@ -17,6 +17,8 @@ class MethodCallExpression extends AbstractExpression { public function __construct(AbstractExpression $node, string $method, ArrayExpression $arguments, int $lineno) { + trigger_deprecation('twig/twig', '3.15', 'The "%s" class is deprecated, use "%s" instead.', __CLASS__, MacroReferenceExpression::class); + parent::__construct(['node' => $node, 'arguments' => $arguments], ['method' => $method, 'safe' => false, 'is_defined_test' => false], $lineno); if ($node instanceof NameExpression) { diff --git a/src/Node/Expression/Test/DefinedTest.php b/src/Node/Expression/Test/DefinedTest.php index 005ba39cc4b..b6c3ff6f963 100644 --- a/src/Node/Expression/Test/DefinedTest.php +++ b/src/Node/Expression/Test/DefinedTest.php @@ -20,6 +20,7 @@ use Twig\Node\Expression\ConstantExpression; use Twig\Node\Expression\FunctionExpression; use Twig\Node\Expression\GetAttrExpression; +use Twig\Node\Expression\MacroReferenceExpression; use Twig\Node\Expression\MethodCallExpression; use Twig\Node\Expression\NameExpression; use Twig\Node\Expression\TestExpression; @@ -55,6 +56,8 @@ public function __construct(Node $node, TwigTest|string $name, ?Node $arguments, $this->changeIgnoreStrictCheck($node); } elseif ($node instanceof BlockReferenceExpression) { $node->setAttribute('is_defined_test', true); + } elseif ($node instanceof MacroReferenceExpression) { + $node->setAttribute('is_defined_test', true); } elseif ($node instanceof FunctionExpression && 'constant' === $node->getAttribute('name')) { $node->setAttribute('is_defined_test', true); } elseif ($node instanceof ConstantExpression || $node instanceof ArrayExpression) { diff --git a/src/Node/Expression/Variable/TemplateVariable.php b/src/Node/Expression/Variable/TemplateVariable.php new file mode 100644 index 00000000000..cde52d0b456 --- /dev/null +++ b/src/Node/Expression/Variable/TemplateVariable.php @@ -0,0 +1,31 @@ +getAttribute('name')) { + $compiler->raw('$this'); + } else { + $compiler + ->raw('$macros[') + ->string($this->getAttribute('name')) + ->raw(']') + ; + } + } +} diff --git a/src/Node/ImportNode.php b/src/Node/ImportNode.php index 9a6033f215b..7f057ed9733 100644 --- a/src/Node/ImportNode.php +++ b/src/Node/ImportNode.php @@ -14,6 +14,7 @@ use Twig\Attribute\YieldReady; use Twig\Compiler; use Twig\Node\Expression\AbstractExpression; +use Twig\Node\Expression\AssignNameExpression; use Twig\Node\Expression\NameExpression; /** @@ -27,7 +28,7 @@ class ImportNode extends Node /** * @param bool $global */ - public function __construct(AbstractExpression $expr, AbstractExpression $var, int $lineno, $global = true) + public function __construct(AbstractExpression $expr, AbstractExpression|string $var, int $lineno, $global = true) { if (null === $global || \is_string($global)) { trigger_deprecation('twig/twig', '3.12', 'Passing a tag to %s() is deprecated.', __METHOD__); @@ -36,7 +37,15 @@ public function __construct(AbstractExpression $expr, AbstractExpression $var, i throw new \TypeError(\sprintf('Argument 4 passed to "%s()" must be a boolean, "%s" given.', __METHOD__, get_debug_type($global))); } - parent::__construct(['expr' => $expr, 'var' => $var], ['global' => $global], $lineno); + if (!\is_string($var)) { + trigger_deprecation('twig/twig', '3.15', \sprintf('Passing a "%s" instance as the second argument of "%s" is deprecated, pass a "string" instead.', $var::class, __CLASS__)); + } else { + $var = new AssignNameExpression($var, $lineno); + } + + $this->deprecateNode('var', new NameDeprecation('var', '3.15')); + + parent::__construct(['expr' => $expr, 'var' => $var], ['global' => $global, 'var' => $var->getAttribute('name')], $lineno); } public function compile(Compiler $compiler): void @@ -44,14 +53,14 @@ public function compile(Compiler $compiler): void $compiler ->addDebugInfo($this) ->write('$macros[') - ->repr($this->getNode('var')->getAttribute('name')) + ->repr($this->getAttribute('var')) ->raw('] = ') ; if ($this->getAttribute('global')) { $compiler ->raw('$this->macros[') - ->repr($this->getNode('var')->getAttribute('name')) + ->repr($this->getAttribute('var')) ->raw('] = ') ; } diff --git a/src/NodeVisitor/EscaperNodeVisitor.php b/src/NodeVisitor/EscaperNodeVisitor.php index c942f825b34..6f5aa10bfe5 100644 --- a/src/NodeVisitor/EscaperNodeVisitor.php +++ b/src/NodeVisitor/EscaperNodeVisitor.php @@ -61,7 +61,7 @@ public function enterNode(Node $node, Environment $env): Node } elseif ($node instanceof BlockNode) { $this->statusStack[] = $this->blocks[$node->getAttribute('name')] ?? $this->needEscaping(); } elseif ($node instanceof ImportNode) { - $this->safeVars[] = $node->getNode('var')->getAttribute('name'); + $this->safeVars[] = $node->getAttribute('var'); } return $node; diff --git a/src/NodeVisitor/MacroAutoImportNodeVisitor.php b/src/NodeVisitor/MacroAutoImportNodeVisitor.php index 5f6d273f4c2..7e450f00ebe 100644 --- a/src/NodeVisitor/MacroAutoImportNodeVisitor.php +++ b/src/NodeVisitor/MacroAutoImportNodeVisitor.php @@ -12,13 +12,6 @@ namespace Twig\NodeVisitor; use Twig\Environment; -use Twig\Node\Expression\AssignNameExpression; -use Twig\Node\Expression\ConstantExpression; -use Twig\Node\Expression\GetAttrExpression; -use Twig\Node\Expression\MethodCallExpression; -use Twig\Node\Expression\NameExpression; -use Twig\Node\ImportNode; -use Twig\Node\ModuleNode; use Twig\Node\Node; /** @@ -28,41 +21,18 @@ */ final class MacroAutoImportNodeVisitor implements NodeVisitorInterface { - private $inAModule = false; - private $hasMacroCalls = false; + public function __construct() + { + trigger_deprecation('twig/twig', '3.15', 'The "%s" class is deprecated.', __CLASS__); + } public function enterNode(Node $node, Environment $env): Node { - if ($node instanceof ModuleNode) { - $this->inAModule = true; - $this->hasMacroCalls = false; - } - return $node; } public function leaveNode(Node $node, Environment $env): Node { - if ($node instanceof ModuleNode) { - $this->inAModule = false; - if ($this->hasMacroCalls) { - $node->getNode('constructor_end')->setNode('_auto_macro_import', new ImportNode(new NameExpression('_self', 0), new AssignNameExpression('_self', 0), 0)); - } - } elseif ($this->inAModule) { - if ( - $node instanceof GetAttrExpression - && $node->getNode('node') instanceof NameExpression - && '_self' === $node->getNode('node')->getAttribute('name') - && $node->getNode('attribute') instanceof ConstantExpression - ) { - $this->hasMacroCalls = true; - - $name = $node->getNode('attribute')->getAttribute('value'); - $node = new MethodCallExpression($node->getNode('node'), 'macro_'.$name, $node->getNode('arguments'), $node->getTemplateLine()); - $node->setAttribute('safe', true); - } - } - return $node; } diff --git a/src/NodeVisitor/SafeAnalysisNodeVisitor.php b/src/NodeVisitor/SafeAnalysisNodeVisitor.php index 07672164ece..033f2c905ab 100644 --- a/src/NodeVisitor/SafeAnalysisNodeVisitor.php +++ b/src/NodeVisitor/SafeAnalysisNodeVisitor.php @@ -18,6 +18,7 @@ use Twig\Node\Expression\FilterExpression; use Twig\Node\Expression\FunctionExpression; use Twig\Node\Expression\GetAttrExpression; +use Twig\Node\Expression\MacroReferenceExpression; use Twig\Node\Expression\MethodCallExpression; use Twig\Node\Expression\NameExpression; use Twig\Node\Expression\ParentExpression; @@ -126,7 +127,7 @@ public function leaveNode(Node $node, Environment $env): ?Node } else { $this->setSafe($node, []); } - } elseif ($node instanceof MethodCallExpression) { + } elseif ($node instanceof MethodCallExpression || $node instanceof MacroReferenceExpression) { if ($node->getAttribute('safe')) { $this->setSafe($node, ['all']); } else { diff --git a/src/Parser.php b/src/Parser.php index 05ad88ce878..249c23de1d7 100644 --- a/src/Parser.php +++ b/src/Parser.php @@ -292,9 +292,15 @@ public function embedTemplate(ModuleNode $template) $this->embeddedTemplates[] = $template; } - public function addImportedSymbol(string $type, string $alias, ?string $name = null, ?AbstractExpression $node = null): void + public function addImportedSymbol(string $type, string $alias, ?string $name = null, AbstractExpression|string|null $internalRef = null): void { - $this->importedSymbols[0][$type][$alias] = ['name' => $name, 'node' => $node]; + if ($internalRef instanceof AbstractExpression) { + trigger_deprecation('twig/twig', '3.15', 'Passing a non-string internal reference name to "%s" is deprecated ("%s" given).', __METHOD__, $internalRef::class); + + $internalRef = $internalRef->getAttribute('name'); + } + + $this->importedSymbols[0][$type][$alias] = ['name' => $name, 'node' => $internalRef]; } public function getImportedSymbol(string $type, string $alias) diff --git a/src/Template.php b/src/Template.php index 86cb560e977..cf36da1192d 100644 --- a/src/Template.php +++ b/src/Template.php @@ -478,6 +478,35 @@ public function yieldParentBlock($name, array $context, array $blocks = []): ite } } + protected function hasMacro(string $name, array $context): bool + { + if (method_exists($this, $name)) { + return true; + } + + if (!$parent = $this->getParent($context)) { + return false; + } + + return $parent->hasMacro($name, $context); + } + + protected function getTemplateForMacro(string $name, array $context, int $line, Source $source): Template + { + if (method_exists($this, $name)) { + return $this; + } + + $parent = $this; + while ($parent = $parent->getParent($context)) { + if (method_exists($parent, $name)) { + return $parent; + } + } + + throw new RuntimeError(\sprintf('Macro "%s" is not defined in template "%s".', substr($name, \strlen('macro_')), $this->getTemplateName()), $line, $source); + } + /** * Auto-generated method to display the template with the given context. * diff --git a/src/TokenParser/FromTokenParser.php b/src/TokenParser/FromTokenParser.php index 92811aa140e..fbd8f560fde 100644 --- a/src/TokenParser/FromTokenParser.php +++ b/src/TokenParser/FromTokenParser.php @@ -50,11 +50,11 @@ public function parse(Token $token): Node $stream->expect(Token::BLOCK_END_TYPE); - $var = new AssignNameExpression($this->parser->getVarName(), $token->getLine()); - $node = new ImportNode($macro, $var, $token->getLine(), $this->parser->isMainScope()); + $internalRef = $this->parser->getVarName(); + $node = new ImportNode($macro, $internalRef, $token->getLine(), $this->parser->isMainScope()); foreach ($targets as $name => $alias) { - $this->parser->addImportedSymbol('function', $alias->getAttribute('name'), 'macro_'.$name, $var); + $this->parser->addImportedSymbol('function', $alias->getAttribute('name'), 'macro_'.$name, $internalRef); } return $node; diff --git a/src/TokenParser/ImportTokenParser.php b/src/TokenParser/ImportTokenParser.php index f20f35ab3c3..f4f1acd3d73 100644 --- a/src/TokenParser/ImportTokenParser.php +++ b/src/TokenParser/ImportTokenParser.php @@ -11,7 +11,6 @@ namespace Twig\TokenParser; -use Twig\Node\Expression\AssignNameExpression; use Twig\Node\ImportNode; use Twig\Node\Node; use Twig\Token; @@ -29,10 +28,9 @@ public function parse(Token $token): Node { $macro = $this->parser->getExpressionParser()->parseExpression(); $this->parser->getStream()->expect(Token::NAME_TYPE, 'as'); - $var = new AssignNameExpression($this->parser->getStream()->expect(Token::NAME_TYPE)->getValue(), $token->getLine()); + $var = $this->parser->getStream()->expect(Token::NAME_TYPE)->getValue(); $this->parser->getStream()->expect(Token::BLOCK_END_TYPE); - - $this->parser->addImportedSymbol('template', $var->getAttribute('name')); + $this->parser->addImportedSymbol('template', $var); return new ImportNode($macro, $var, $token->getLine(), $this->parser->isMainScope()); } diff --git a/tests/Fixtures/tests/defined_for_macros.test b/tests/Fixtures/tests/defined_for_macros.test index 1aa45fc8268..657b43e1788 100644 --- a/tests/Fixtures/tests/defined_for_macros.test +++ b/tests/Fixtures/tests/defined_for_macros.test @@ -1,41 +1,80 @@ --TEST-- "defined" support for macros --TEMPLATE-- +{% extends 'macros.twig' %} + +{% import 'macros.twig' as macros_ext %} +{% from 'macros.twig' import lol, baz %} {% import _self as macros %} {% from _self import hello, bar %} -{% if macros.hello is defined -%} - OK -{% endif %} +{% block content %} + {{~ macros.hello is defined ? 'OK' : 'KO' }} + {{~ macros.hello() is defined ? 'OK' : 'KO' }} + + {{~ macros_ext.lol is defined ? 'OK' : 'KO' }} + {{~ macros_ext.lol() is defined ? 'OK' : 'KO' }} + + {{~ macros.foo is not defined ? 'OK' : 'KO' }} + {{~ macros.foo() is not defined ? 'OK' : 'KO' }} + + {{~ macros_ext.hello is not defined ? 'OK' : 'KO' }} + {{~ macros_ext.hello() is not defined ? 'OK' : 'KO' }} + + {{~ hello is defined ? 'OK' : 'KO' }} + {{~ hello() is defined ? 'OK' : 'KO' }} + + {{~ lol is defined ? 'OK' : 'KO' }} + {{~ lol() is defined ? 'OK' : 'KO' }} + + {{~ baz is not defined ? 'OK' : 'KO' }} + {{~ baz() is not defined ? 'OK' : 'KO' }} -{% if macros.foo is not defined -%} - OK -{% endif %} + {{~ _self.hello is defined ? 'OK' : 'KO' }} + {{~ _self.hello() is defined ? 'OK' : 'KO' }} -{% if hello is defined -%} - OK -{% endif %} + {{~ _self.bar is not defined ? 'OK' : 'KO' }} + {{~ _self.bar() is not defined ? 'OK' : 'KO' }} -{% if bar is not defined -%} - OK -{% endif %} + {{~ _self.lol is defined ? 'OK' : 'KO' }} + {{~ _self.lol() is defined ? 'OK' : 'KO' }} +{% endblock %} -{% if foo is not defined -%} - OK -{% endif %} +{% macro hello(name) %}{% endmacro %} +--TEMPLATE(macros.twig)-- +{% block content %} +{% endblock %} -{% macro hello(name) %} - Hello {{ name }} -{% endmacro %} +{% macro lol(name) -%}{% endmacro %} --DATA-- return [] --EXPECT-- OK +OK +OK OK +OK OK +OK +OK + +OK OK OK +OK + +OK +OK + +OK +OK + +OK +OK + +OK +OK diff --git a/tests/Node/ImportTest.php b/tests/Node/ImportTest.php index eb69e218384..4337f968ac7 100644 --- a/tests/Node/ImportTest.php +++ b/tests/Node/ImportTest.php @@ -11,7 +11,6 @@ * file that was distributed with this source code. */ -use Twig\Node\Expression\AssignNameExpression; use Twig\Node\Expression\ConstantExpression; use Twig\Node\ImportNode; use Twig\Test\NodeTestCase; @@ -21,11 +20,10 @@ class ImportTest extends NodeTestCase public function testConstructor() { $macro = new ConstantExpression('foo.twig', 1); - $var = new AssignNameExpression('macro', 1); - $node = new ImportNode($macro, $var, 1); + $node = new ImportNode($macro, $var = 'macro', 1); $this->assertEquals($macro, $node->getNode('expr')); - $this->assertEquals($var, $node->getNode('var')); + $this->assertEquals($var, $node->getAttribute('var')); } public static function provideTests(): iterable @@ -33,8 +31,7 @@ public static function provideTests(): iterable $tests = []; $macro = new ConstantExpression('foo.twig', 1); - $var = new AssignNameExpression('macro', 1); - $node = new ImportNode($macro, $var, 1); + $node = new ImportNode($macro, 'macro', 1); $tests[] = [$node, << Date: Sun, 20 Oct 2024 22:43:58 +0200 Subject: [PATCH 557/812] Simplify code --- src/ExpressionParser.php | 13 ++----------- src/Extension/CoreExtension.php | 2 ++ src/NodeVisitor/SafeAnalysisNodeVisitor.php | 7 ++----- src/TokenParser/MacroTokenParser.php | 2 -- tests/Node/MacroTest.php | 1 - 5 files changed, 6 insertions(+), 19 deletions(-) diff --git a/src/ExpressionParser.php b/src/ExpressionParser.php index 08a6f0bb394..856ed53fcbc 100644 --- a/src/ExpressionParser.php +++ b/src/ExpressionParser.php @@ -532,11 +532,7 @@ public function parsePostfixExpression($node) public function getFunctionNode($name, $line) { if (null !== $alias = $this->parser->getImportedSymbol('function', $name)) { - $arguments = $this->createArguments($line); - $node = new MacroReferenceExpression(new TemplateVariable($alias['node'], $line), $alias['name'], $arguments, $line); - $node->setAttribute('safe', true); - - return $node; + return new MacroReferenceExpression(new TemplateVariable($alias['node'], $line), $alias['name'], $this->createArguments($line), $line); } $args = $this->parseOnlyArguments(); @@ -758,7 +754,6 @@ private function parseTestExpression(Node $node): TestExpression if ('defined' === $test->getName() && $node instanceof NameExpression && null !== $alias = $this->parser->getImportedSymbol('function', $node->getAttribute('name'))) { $node = new MacroReferenceExpression(new TemplateVariable($alias['node'], $node->getTemplateLine()), $alias['name'], new ArrayExpression([], $node->getTemplateLine()), $node->getTemplateLine()); - $node->setAttribute('safe', true); } $ready = $test instanceof TwigTest; @@ -970,11 +965,7 @@ private function parseSubscriptExpressionDot(Node $node): AbstractExpression '_self' === $node->getAttribute('name') && $attribute instanceof ConstantExpression ) ) { - $name = $attribute->getAttribute('value'); - $node = new MacroReferenceExpression(new TemplateVariable($node->getAttribute('name'), $node->getTemplateLine()), 'macro_'.$name, $arguments, $node->getTemplateLine()); - $node->setAttribute('safe', true); - - return $node; + return new MacroReferenceExpression(new TemplateVariable($node->getAttribute('name'), $node->getTemplateLine()), 'macro_'.$attribute->getAttribute('value'), $arguments, $node->getTemplateLine()); } return new GetAttrExpression($node, $attribute, $arguments, $type, $lineno); diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index cf3b417e451..f8f80ac2f0c 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -1285,6 +1285,8 @@ public static function capitalize(string $charset, $string): string /** * @internal + * + * to be removed in 4.0 */ public static function callMacro(Template $template, string $method, array $args, int $lineno, array $context, Source $source) { diff --git a/src/NodeVisitor/SafeAnalysisNodeVisitor.php b/src/NodeVisitor/SafeAnalysisNodeVisitor.php index 033f2c905ab..dbe7150c933 100644 --- a/src/NodeVisitor/SafeAnalysisNodeVisitor.php +++ b/src/NodeVisitor/SafeAnalysisNodeVisitor.php @@ -128,11 +128,8 @@ public function leaveNode(Node $node, Environment $env): ?Node $this->setSafe($node, []); } } elseif ($node instanceof MethodCallExpression || $node instanceof MacroReferenceExpression) { - if ($node->getAttribute('safe')) { - $this->setSafe($node, ['all']); - } else { - $this->setSafe($node, []); - } + // all macro calls are safe + $this->setSafe($node, ['all']); } elseif ($node instanceof GetAttrExpression && $node->getNode('node') instanceof NameExpression) { $name = $node->getNode('node')->getAttribute('name'); if (\in_array($name, $this->safeVars)) { diff --git a/src/TokenParser/MacroTokenParser.php b/src/TokenParser/MacroTokenParser.php index 19260302079..7b34eeef689 100644 --- a/src/TokenParser/MacroTokenParser.php +++ b/src/TokenParser/MacroTokenParser.php @@ -16,13 +16,11 @@ use Twig\Node\EmptyNode; use Twig\Node\Expression\ArrayExpression; use Twig\Node\Expression\ConstantExpression; -use Twig\Node\Expression\NameExpression; use Twig\Node\Expression\TempNameExpression; use Twig\Node\Expression\Unary\NegUnary; use Twig\Node\Expression\Unary\PosUnary; use Twig\Node\MacroNode; use Twig\Node\Node; -use Twig\Node\Nodes; use Twig\Token; /** diff --git a/tests/Node/MacroTest.php b/tests/Node/MacroTest.php index 2d22ac52fb9..8db7d92779a 100644 --- a/tests/Node/MacroTest.php +++ b/tests/Node/MacroTest.php @@ -19,7 +19,6 @@ use Twig\Node\Expression\NameExpression; use Twig\Node\Expression\TempNameExpression; use Twig\Node\MacroNode; -use Twig\Node\Nodes; use Twig\Node\TextNode; use Twig\Test\NodeTestCase; From f1481be00f982fd52a7b1cea1bbf493714008326 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Mon, 21 Oct 2024 12:09:30 +0200 Subject: [PATCH 558/812] Remove MacroAutoImportNodeVisitor --- CHANGELOG | 2 +- doc/deprecated.rst | 3 -- .../MacroAutoImportNodeVisitor.php | 44 ------------------- 3 files changed, 1 insertion(+), 48 deletions(-) delete mode 100644 src/NodeVisitor/MacroAutoImportNodeVisitor.php diff --git a/CHANGELOG b/CHANGELOG index c4646d55743..4bd3a1c8deb 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,6 @@ # 3.15.0 (2024-XX-XX) - * Deprecate `MacroAutoImportNodeVisitor` + * Remove `MacroAutoImportNodeVisitor` * Deprecate `MethodCallExpression` in favor of `MacroReferenceExpression` * Fix support for the "is defined" test on `_self.xxx` (auto-imported) macros * Fix support for the "is defined" test on inherited macros diff --git a/doc/deprecated.rst b/doc/deprecated.rst index d2074dd46b3..7d66534130a 100644 --- a/doc/deprecated.rst +++ b/doc/deprecated.rst @@ -184,9 +184,6 @@ Node Visitors deprecated as of Twig 3.12 and will be removed in Twig 4.0; they don't do anything anymore. -* The ``Twig\NodeVisitor\MacroAutoImportNodeVisitor`` class is deprecated as of - Twig 3.15. - Parser ------ diff --git a/src/NodeVisitor/MacroAutoImportNodeVisitor.php b/src/NodeVisitor/MacroAutoImportNodeVisitor.php deleted file mode 100644 index 7e450f00ebe..00000000000 --- a/src/NodeVisitor/MacroAutoImportNodeVisitor.php +++ /dev/null @@ -1,44 +0,0 @@ - - * - * @internal - */ -final class MacroAutoImportNodeVisitor implements NodeVisitorInterface -{ - public function __construct() - { - trigger_deprecation('twig/twig', '3.15', 'The "%s" class is deprecated.', __CLASS__); - } - - public function enterNode(Node $node, Environment $env): Node - { - return $node; - } - - public function leaveNode(Node $node, Environment $env): Node - { - return $node; - } - - public function getPriority(): int - { - // we must be ran before auto-escaping - return -10; - } -} From fc15e7ccbc91478270ec47e58b79f95f6fb66520 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Wed, 16 Oct 2024 08:45:38 +0200 Subject: [PATCH 559/812] Rename Node classes related to variables --- CHANGELOG | 3 ++ doc/deprecated.rst | 10 ++++ src/Compiler.php | 2 +- src/ExpressionParser.php | 27 ++++++----- src/Node/Expression/AssignNameExpression.php | 5 ++ src/Node/Expression/NameExpression.php | 5 ++ src/Node/Expression/TempNameExpression.php | 14 ++++-- .../Variable/AssignContextVariable.php | 18 +++++++ .../Expression/Variable/ContextVariable.php | 18 +++++++ .../Variable/GlobalTemplateVariable.php | 34 +++++++++++++ .../Expression/Variable/LocalVariable.php | 18 +++++++ .../Expression/Variable/TemplateVariable.php | 11 ++++- src/Node/ImportNode.php | 28 +++++------ src/Node/MacroNode.php | 4 +- src/NodeVisitor/EscaperNodeVisitor.php | 2 +- src/Parser.php | 11 +++-- src/TokenParser/ApplyTokenParser.php | 8 +--- src/TokenParser/ForTokenParser.php | 8 ++-- src/TokenParser/FromTokenParser.php | 9 ++-- src/TokenParser/ImportTokenParser.php | 5 +- src/TokenParser/MacroTokenParser.php | 4 +- tests/ExpressionParserTest.php | 20 ++++---- tests/Node/Expression/GetAttrTest.php | 14 +++--- tests/Node/Expression/NullCoalesceTest.php | 4 +- .../AssignContextVariableTest.php} | 8 ++-- .../ContextVariableTest.php} | 12 ++--- tests/Node/ForTest.php | 48 +++++++++---------- tests/Node/IfTest.php | 16 +++---- tests/Node/ImportTest.php | 7 +-- tests/Node/IncludeTest.php | 8 ++-- tests/Node/MacroTest.php | 10 ++-- tests/Node/ModuleTest.php | 7 +-- tests/Node/PrintTest.php | 4 +- tests/Node/SetTest.php | 20 ++++---- tests/NodeVisitor/SandboxTest.php | 4 +- 35 files changed, 277 insertions(+), 149 deletions(-) create mode 100644 src/Node/Expression/Variable/AssignContextVariable.php create mode 100644 src/Node/Expression/Variable/ContextVariable.php create mode 100644 src/Node/Expression/Variable/GlobalTemplateVariable.php create mode 100644 src/Node/Expression/Variable/LocalVariable.php rename tests/Node/Expression/{AssignNameTest.php => Variable/AssignContextVariableTest.php} (70%) rename tests/Node/Expression/{NameTest.php => Variable/ContextVariableTest.php} (79%) diff --git a/CHANGELOG b/CHANGELOG index 4bd3a1c8deb..6eac17dc9a9 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,8 @@ # 3.15.0 (2024-XX-XX) + * Deprecate `TempNameExpression` in favor of `LocalVariable` + * Deprecate `NameExpression` in favor of `ContextVariable` + * Deprecate `AssignNameExpression` in favor of `AssignContextVariable` * Remove `MacroAutoImportNodeVisitor` * Deprecate `MethodCallExpression` in favor of `MacroReferenceExpression` * Fix support for the "is defined" test on `_self.xxx` (auto-imported) macros diff --git a/doc/deprecated.rst b/doc/deprecated.rst index 7d66534130a..fed5cb38017 100644 --- a/doc/deprecated.rst +++ b/doc/deprecated.rst @@ -173,6 +173,16 @@ Nodes * The ``MethodCallExpression`` class is deprecated as of Twig 3.15, use ``MacroReferenceExpression`` instead. +* The ``Twig\Node\Expression\TempNameExpression`` class is deprecated as of + Twig 3.15; use ``Twig\Node\Expression\Variable\LocalVariable`` instead. + +* The ``Twig\Node\Expression\NameExpression`` class is deprecated as of Twig + 3.15; use ``Twig\Node\Expression\Variable\ContextVariable`` instead. + +* The ``Twig\Node\Expression\AssignNameExpression`` class is deprecated as of + Twig 3.15; use ``Twig\Node\Expression\Variable\AssignContextVariable`` + instead. + Node Visitors ------------- diff --git a/src/Compiler.php b/src/Compiler.php index 1a43aa7f676..ba7b6cbaa94 100644 --- a/src/Compiler.php +++ b/src/Compiler.php @@ -243,7 +243,7 @@ public function outdent(int $step = 1) public function getVarName(): string { - return \sprintf('__internal_compile_%d', $this->varNameSalt++); + return \sprintf('_v%d', $this->varNameSalt++); } private function checkForEcho(string $string): void diff --git a/src/ExpressionParser.php b/src/ExpressionParser.php index 6892f7c789c..8af4ea046b2 100644 --- a/src/ExpressionParser.php +++ b/src/ExpressionParser.php @@ -18,7 +18,6 @@ use Twig\Node\Expression\AbstractExpression; use Twig\Node\Expression\ArrayExpression; use Twig\Node\Expression\ArrowFunctionExpression; -use Twig\Node\Expression\AssignNameExpression; use Twig\Node\Expression\Binary\AbstractBinary; use Twig\Node\Expression\Binary\ConcatBinary; use Twig\Node\Expression\ConditionalExpression; @@ -26,13 +25,15 @@ use Twig\Node\Expression\GetAttrExpression; use Twig\Node\Expression\MacroReferenceExpression; use Twig\Node\Expression\NameExpression; -use Twig\Node\Expression\TempNameExpression; use Twig\Node\Expression\TestExpression; use Twig\Node\Expression\Unary\AbstractUnary; 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\Expression\Variable\AssignContextVariable; +use Twig\Node\Expression\Variable\ContextVariable; +use Twig\Node\Expression\Variable\LocalVariable; use Twig\Node\Expression\Variable\TemplateVariable; use Twig\Node\Node; use Twig\Node\Nodes; @@ -184,7 +185,7 @@ private function parseArrow() if ($stream->look(1)->test(Token::ARROW_TYPE)) { $line = $stream->getCurrent()->getLine(); $token = $stream->expect(Token::NAME_TYPE); - $names = [new AssignNameExpression($token->getValue(), $token->getLine())]; + $names = [new AssignContextVariable($token->getValue(), $token->getLine())]; $stream->expect(Token::ARROW_TYPE); return new ArrowFunctionExpression($this->parseExpression(), new Nodes($names), $line); @@ -219,7 +220,7 @@ private function parseArrow() $names = []; while (true) { $token = $stream->expect(Token::NAME_TYPE); - $names[] = new AssignNameExpression($token->getValue(), $token->getLine()); + $names[] = new AssignContextVariable($token->getValue(), $token->getLine()); if (!$stream->nextIf(Token::PUNCTUATION_TYPE, ',')) { break; @@ -327,7 +328,7 @@ public function parsePrimaryExpression() if ('(' === $this->parser->getCurrentToken()->getValue()) { $node = $this->getFunctionNode($token->getValue(), $token->getLine()); } else { - $node = new NameExpression($token->getValue(), $token->getLine()); + $node = new ContextVariable($token->getValue(), $token->getLine()); } } break; @@ -354,7 +355,7 @@ public function parsePrimaryExpression() if (preg_match(Lexer::REGEX_NAME, $token->getValue(), $matches) && $matches[0] == $token->getValue()) { // in this context, string operators are variable names $this->parser->getStream()->next(); - $node = new NameExpression($token->getValue(), $token->getLine()); + $node = new ContextVariable($token->getValue(), $token->getLine()); break; } @@ -485,7 +486,7 @@ public function parseMappingExpression() // {a} is a shortcut for {a:a} if ($stream->test(Token::PUNCTUATION_TYPE, [',', '}'])) { - $value = new NameExpression($key->getAttribute('value'), $key->getTemplateLine()); + $value = new ContextVariable($key->getAttribute('value'), $key->getTemplateLine()); $node->addElement($value, $key); continue; } @@ -532,7 +533,7 @@ public function parsePostfixExpression($node) public function getFunctionNode($name, $line) { if (null !== $alias = $this->parser->getImportedSymbol('function', $name)) { - return new MacroReferenceExpression(new TemplateVariable($alias['node'], $line), $alias['name'], $this->createArguments($line), $line); + return new MacroReferenceExpression(new TemplateVariable($alias['node']->getAttribute('name'), $line), $alias['name'], $this->createArguments($line), $line); } $args = $this->parseOnlyArguments(); @@ -646,7 +647,7 @@ public function parseArguments() if ($definition) { $token = $stream->expect(Token::NAME_TYPE, null, 'An argument must be a name'); - $value = new NameExpression($token->getValue(), $this->parser->getCurrentToken()->getLine()); + $value = new ContextVariable($token->getValue(), $this->parser->getCurrentToken()->getLine()); } else { if ($stream->nextIf(Token::SPREAD_TYPE)) { $hasSpread = true; @@ -708,7 +709,7 @@ public function parseAssignmentExpression() } else { $stream->expect(Token::NAME_TYPE, null, 'Only variables can be assigned to'); } - $targets[] = new AssignNameExpression($token->getValue(), $token->getLine()); + $targets[] = new AssignContextVariable($token->getValue(), $token->getLine()); if (!$stream->nextIf(Token::PUNCTUATION_TYPE, ',')) { break; @@ -749,7 +750,7 @@ private function parseTestExpression(Node $node): TestExpression } if ('defined' === $test->getName() && $node instanceof NameExpression && null !== $alias = $this->parser->getImportedSymbol('function', $node->getAttribute('name'))) { - $node = new MacroReferenceExpression(new TemplateVariable($alias['node'], $node->getTemplateLine()), $alias['name'], new ArrayExpression([], $node->getTemplateLine()), $node->getTemplateLine()); + $node = new MacroReferenceExpression(new TemplateVariable($alias['node']->getAttribute('name'), $node->getTemplateLine()), $alias['name'], new ArrayExpression([], $node->getTemplateLine()), $node->getTemplateLine()); } $ready = $test instanceof TwigTest; @@ -870,7 +871,7 @@ private function createArguments(int $line): ArrayExpression { $arguments = new ArrayExpression([], $line); foreach ($this->parseOnlyArguments() as $k => $n) { - $arguments->addElement($n, new TempNameExpression($k, $line)); + $arguments->addElement($n, new LocalVariable($k, $line)); } return $arguments; @@ -956,7 +957,7 @@ private function parseSubscriptExpressionDot(Node $node): AbstractExpression $node instanceof NameExpression && ( - null !== $this->parser->getImportedSymbol('template', $node->getAttribute('name')) + null !== $this->parser->getImportedSymbol('template', (new TemplateVariable($node->getAttribute('name'), $node->getTemplateLine()))->getAttribute('name')) || '_self' === $node->getAttribute('name') && $attribute instanceof ConstantExpression ) diff --git a/src/Node/Expression/AssignNameExpression.php b/src/Node/Expression/AssignNameExpression.php index a0df3b7a36a..c2cbb8e4a43 100644 --- a/src/Node/Expression/AssignNameExpression.php +++ b/src/Node/Expression/AssignNameExpression.php @@ -14,11 +14,16 @@ use Twig\Compiler; use Twig\Error\SyntaxError; +use Twig\Node\Expression\Variable\AssignContextVariable; class AssignNameExpression extends NameExpression { public function __construct(string $name, int $lineno) { + if (self::class === static::class) { + trigger_deprecation('twig/twig', '3.15', 'The "%s" class is deprecated, use "%s" instead.', self::class, AssignContextVariable::class); + } + // All names supported by ExpressionParser::parsePrimaryExpression() should be excluded if (\in_array(strtolower($name), ['true', 'false', 'none', 'null'])) { throw new SyntaxError(\sprintf('You cannot assign a value to "%s".', $name), $lineno); diff --git a/src/Node/Expression/NameExpression.php b/src/Node/Expression/NameExpression.php index 12a9bb71cc6..322fbc2e083 100644 --- a/src/Node/Expression/NameExpression.php +++ b/src/Node/Expression/NameExpression.php @@ -13,6 +13,7 @@ namespace Twig\Node\Expression; use Twig\Compiler; +use Twig\Node\Expression\Variable\ContextVariable; class NameExpression extends AbstractExpression { @@ -24,6 +25,10 @@ class NameExpression extends AbstractExpression public function __construct(string $name, int $lineno) { + if (self::class === static::class) { + trigger_deprecation('twig/twig', '3.15', 'The "%s" class is deprecated, use "%s" instead.', self::class, ContextVariable::class); + } + parent::__construct([], ['name' => $name, 'is_defined_test' => false, 'ignore_strict_check' => false, 'always_defined' => false], $lineno); } diff --git a/src/Node/Expression/TempNameExpression.php b/src/Node/Expression/TempNameExpression.php index 8bdb456ef09..bf304a98cb7 100644 --- a/src/Node/Expression/TempNameExpression.php +++ b/src/Node/Expression/TempNameExpression.php @@ -18,14 +18,18 @@ class TempNameExpression extends AbstractExpression { public const RESERVED_NAMES = ['varargs', 'context', 'macros', 'blocks', 'this']; - public function __construct(string|int $name, int $lineno) + public function __construct(string|int|null $name, int $lineno) { // All names supported by ExpressionParser::parsePrimaryExpression() should be excluded - if (\in_array(strtolower($name), ['true', 'false', 'none', 'null'])) { + if ($name && \in_array(strtolower($name), ['true', 'false', 'none', 'null'])) { throw new SyntaxError(\sprintf('You cannot assign a value to "%s".', $name), $lineno); } - if (is_int($name) || ctype_digit($name)) { + if (self::class === static::class) { + trigger_deprecation('twig/twig', '3.15', 'The "%s" class is deprecated.', self::class); + } + + if (null !== $name && (is_int($name) || ctype_digit($name))) { $name = (int) $name; } elseif (in_array($name, self::RESERVED_NAMES)) { $name = '_'.$name.'_'; @@ -36,6 +40,10 @@ public function __construct(string|int $name, int $lineno) public function compile(Compiler $compiler): void { + if (null === $this->getAttribute('name')) { + $this->setAttribute('name', \sprintf('_l%d', $compiler->getVarName())); + } + $compiler->raw('$'.$this->getAttribute('name')); } } diff --git a/src/Node/Expression/Variable/AssignContextVariable.php b/src/Node/Expression/Variable/AssignContextVariable.php new file mode 100644 index 00000000000..30d8106751b --- /dev/null +++ b/src/Node/Expression/Variable/AssignContextVariable.php @@ -0,0 +1,18 @@ +getAttribute('name')) { + $this->setAttribute('name', \sprintf('_l%d', $compiler->getVarName())); + } + + if ('_self' === $this->getAttribute('name')) { + $compiler->raw('$this'); + } else { + $compiler + ->raw('$this->macros[') + ->string($this->getAttribute('name')) + ->raw(']') + ; + } + } +} diff --git a/src/Node/Expression/Variable/LocalVariable.php b/src/Node/Expression/Variable/LocalVariable.php new file mode 100644 index 00000000000..a5ee17566b0 --- /dev/null +++ b/src/Node/Expression/Variable/LocalVariable.php @@ -0,0 +1,18 @@ +getAttribute('name')) { + $this->setAttribute('name', \sprintf('_l%d', $compiler->getVarName())); + } + if ('_self' === $this->getAttribute('name')) { $compiler->raw('$this'); } else { diff --git a/src/Node/ImportNode.php b/src/Node/ImportNode.php index 7f057ed9733..b083d064dcc 100644 --- a/src/Node/ImportNode.php +++ b/src/Node/ImportNode.php @@ -14,8 +14,9 @@ use Twig\Attribute\YieldReady; use Twig\Compiler; use Twig\Node\Expression\AbstractExpression; -use Twig\Node\Expression\AssignNameExpression; use Twig\Node\Expression\NameExpression; +use Twig\Node\Expression\Variable\GlobalTemplateVariable; +use Twig\Node\Expression\Variable\TemplateVariable; /** * Represents an import node. @@ -28,7 +29,7 @@ class ImportNode extends Node /** * @param bool $global */ - public function __construct(AbstractExpression $expr, AbstractExpression|string $var, int $lineno, $global = true) + public function __construct(AbstractExpression $expr, AbstractExpression|TemplateVariable $var, int $lineno, $global = true) { if (null === $global || \is_string($global)) { trigger_deprecation('twig/twig', '3.12', 'Passing a tag to %s() is deprecated.', __METHOD__); @@ -37,31 +38,28 @@ public function __construct(AbstractExpression $expr, AbstractExpression|string throw new \TypeError(\sprintf('Argument 4 passed to "%s()" must be a boolean, "%s" given.', __METHOD__, get_debug_type($global))); } - if (!\is_string($var)) { - trigger_deprecation('twig/twig', '3.15', \sprintf('Passing a "%s" instance as the second argument of "%s" is deprecated, pass a "string" instead.', $var::class, __CLASS__)); - } else { - $var = new AssignNameExpression($var, $lineno); - } + if (!$var instanceof TemplateVariable) { + trigger_deprecation('twig/twig', '3.15', \sprintf('Passing a "%s" instance as the second argument of "%s" is deprecated, pass a "%s" instead.', $var::class, __CLASS__, TemplateVariable::class)); - $this->deprecateNode('var', new NameDeprecation('var', '3.15')); + $var = new TemplateVariable($var->getAttribute('name'), $lineno); + } - parent::__construct(['expr' => $expr, 'var' => $var], ['global' => $global, 'var' => $var->getAttribute('name')], $lineno); + parent::__construct(['expr' => $expr, 'var' => $var], ['global' => $global], $lineno); } public function compile(Compiler $compiler): void { $compiler ->addDebugInfo($this) - ->write('$macros[') - ->repr($this->getAttribute('var')) - ->raw('] = ') + ->write('') + ->subcompile($this->getNode('var')) + ->raw(' = ') ; if ($this->getAttribute('global')) { $compiler - ->raw('$this->macros[') - ->repr($this->getAttribute('var')) - ->raw('] = ') + ->subcompile(new GlobalTemplateVariable($this->getNode('var')->getAttribute('name'), $this->getTemplateLine())) + ->raw(' = ') ; } diff --git a/src/Node/MacroNode.php b/src/Node/MacroNode.php index 7c2603a622c..5ada1b2206d 100644 --- a/src/Node/MacroNode.php +++ b/src/Node/MacroNode.php @@ -15,7 +15,7 @@ use Twig\Compiler; use Twig\Error\SyntaxError; use Twig\Node\Expression\ArrayExpression; -use Twig\Node\Expression\TempNameExpression; +use Twig\Node\Expression\Variable\LocalVariable; /** * Represents a macro node. @@ -42,7 +42,7 @@ public function __construct(string $name, Node $body, Node $arguments, int $line $args = new ArrayExpression([], $arguments->getTemplateLine()); foreach ($arguments as $name => $default) { - $args->addElement($default, new TempNameExpression($name, $default->getTemplateLine())); + $args->addElement($default, new LocalVariable($name, $default->getTemplateLine())); } $arguments = $args; } diff --git a/src/NodeVisitor/EscaperNodeVisitor.php b/src/NodeVisitor/EscaperNodeVisitor.php index 6f5aa10bfe5..c942f825b34 100644 --- a/src/NodeVisitor/EscaperNodeVisitor.php +++ b/src/NodeVisitor/EscaperNodeVisitor.php @@ -61,7 +61,7 @@ public function enterNode(Node $node, Environment $env): Node } elseif ($node instanceof BlockNode) { $this->statusStack[] = $this->blocks[$node->getAttribute('name')] ?? $this->needEscaping(); } elseif ($node instanceof ImportNode) { - $this->safeVars[] = $node->getAttribute('var'); + $this->safeVars[] = $node->getNode('var')->getAttribute('name'); } return $node; diff --git a/src/Parser.php b/src/Parser.php index 249c23de1d7..b55c0148a50 100644 --- a/src/Parser.php +++ b/src/Parser.php @@ -18,6 +18,7 @@ use Twig\Node\BodyNode; use Twig\Node\EmptyNode; use Twig\Node\Expression\AbstractExpression; +use Twig\Node\Expression\Variable\TemplateVariable; use Twig\Node\MacroNode; use Twig\Node\ModuleNode; use Twig\Node\Node; @@ -60,6 +61,8 @@ public function getEnvironment(): Environment public function getVarName(): string { + trigger_deprecation('twig/twig', '3.15', 'The "%s()" method is deprecated.', __METHOD__); + return \sprintf('__internal_parse_%d', $this->varNameSalt++); } @@ -292,12 +295,12 @@ public function embedTemplate(ModuleNode $template) $this->embeddedTemplates[] = $template; } - public function addImportedSymbol(string $type, string $alias, ?string $name = null, AbstractExpression|string|null $internalRef = null): void + public function addImportedSymbol(string $type, string $alias, ?string $name = null, AbstractExpression|TemplateVariable|null $internalRef = null): void { - if ($internalRef instanceof AbstractExpression) { - trigger_deprecation('twig/twig', '3.15', 'Passing a non-string internal reference name to "%s" is deprecated ("%s" given).', __METHOD__, $internalRef::class); + if ($internalRef && !$internalRef instanceof TemplateVariable) { + trigger_deprecation('twig/twig', '3.15', 'Not passing a "%s" instance as an internal reference is deprecated ("%s" given).', __METHOD__, TemplateVariable::class, $internalRef::class); - $internalRef = $internalRef->getAttribute('name'); + $internalRef = new TemplateVariable($internalRef->getAttribute('name'), $internalRef->getTemplateLine()); } $this->importedSymbols[0][$type][$alias] = ['name' => $name, 'node' => $internalRef]; diff --git a/src/TokenParser/ApplyTokenParser.php b/src/TokenParser/ApplyTokenParser.php index 58c2a3ee4e2..0c95074828a 100644 --- a/src/TokenParser/ApplyTokenParser.php +++ b/src/TokenParser/ApplyTokenParser.php @@ -11,7 +11,7 @@ namespace Twig\TokenParser; -use Twig\Node\Expression\TempNameExpression; +use Twig\Node\Expression\Variable\LocalVariable; use Twig\Node\Node; use Twig\Node\Nodes; use Twig\Node\PrintNode; @@ -32,11 +32,7 @@ final class ApplyTokenParser extends AbstractTokenParser public function parse(Token $token): Node { $lineno = $token->getLine(); - $name = $this->parser->getVarName(); - - $ref = new TempNameExpression($name, $lineno); - $ref->setAttribute('always_defined', true); - + $ref = new LocalVariable(null, $lineno); $filter = $this->parser->getExpressionParser()->parseFilterExpressionRaw($ref); $this->parser->getStream()->expect(Token::BLOCK_END_TYPE); diff --git a/src/TokenParser/ForTokenParser.php b/src/TokenParser/ForTokenParser.php index cf655f8427a..c0a0e3c2950 100644 --- a/src/TokenParser/ForTokenParser.php +++ b/src/TokenParser/ForTokenParser.php @@ -12,7 +12,7 @@ namespace Twig\TokenParser; -use Twig\Node\Expression\AssignNameExpression; +use Twig\Node\Expression\Variable\AssignContextVariable; use Twig\Node\ForNode; use Twig\Node\Node; use Twig\Token; @@ -50,13 +50,13 @@ public function parse(Token $token): Node if (\count($targets) > 1) { $keyTarget = $targets->getNode('0'); - $keyTarget = new AssignNameExpression($keyTarget->getAttribute('name'), $keyTarget->getTemplateLine()); + $keyTarget = new AssignContextVariable($keyTarget->getAttribute('name'), $keyTarget->getTemplateLine()); $valueTarget = $targets->getNode('1'); } else { - $keyTarget = new AssignNameExpression('_key', $lineno); + $keyTarget = new AssignContextVariable('_key', $lineno); $valueTarget = $targets->getNode('0'); } - $valueTarget = new AssignNameExpression($valueTarget->getAttribute('name'), $valueTarget->getTemplateLine()); + $valueTarget = new AssignContextVariable($valueTarget->getAttribute('name'), $valueTarget->getTemplateLine()); return new ForNode($keyTarget, $valueTarget, $seq, null, $body, $else, $lineno); } diff --git a/src/TokenParser/FromTokenParser.php b/src/TokenParser/FromTokenParser.php index fbd8f560fde..b5f985f55e3 100644 --- a/src/TokenParser/FromTokenParser.php +++ b/src/TokenParser/FromTokenParser.php @@ -11,7 +11,8 @@ namespace Twig\TokenParser; -use Twig\Node\Expression\AssignNameExpression; +use Twig\Node\Expression\Variable\AssignContextVariable; +use Twig\Node\Expression\Variable\TemplateVariable; use Twig\Node\ImportNode; use Twig\Node\Node; use Twig\Token; @@ -36,9 +37,9 @@ public function parse(Token $token): Node $name = $stream->expect(Token::NAME_TYPE)->getValue(); if ($stream->nextIf('as')) { - $alias = new AssignNameExpression($stream->expect(Token::NAME_TYPE)->getValue(), $token->getLine()); + $alias = new AssignContextVariable($stream->expect(Token::NAME_TYPE)->getValue(), $token->getLine()); } else { - $alias = new AssignNameExpression($name, $token->getLine()); + $alias = new AssignContextVariable($name, $token->getLine()); } $targets[$name] = $alias; @@ -50,7 +51,7 @@ public function parse(Token $token): Node $stream->expect(Token::BLOCK_END_TYPE); - $internalRef = $this->parser->getVarName(); + $internalRef = new TemplateVariable(null, $token->getLine()); $node = new ImportNode($macro, $internalRef, $token->getLine(), $this->parser->isMainScope()); foreach ($targets as $name => $alias) { diff --git a/src/TokenParser/ImportTokenParser.php b/src/TokenParser/ImportTokenParser.php index f4f1acd3d73..0f1dd1c97b8 100644 --- a/src/TokenParser/ImportTokenParser.php +++ b/src/TokenParser/ImportTokenParser.php @@ -11,6 +11,7 @@ namespace Twig\TokenParser; +use Twig\Node\Expression\Variable\TemplateVariable; use Twig\Node\ImportNode; use Twig\Node\Node; use Twig\Token; @@ -28,9 +29,9 @@ public function parse(Token $token): Node { $macro = $this->parser->getExpressionParser()->parseExpression(); $this->parser->getStream()->expect(Token::NAME_TYPE, 'as'); - $var = $this->parser->getStream()->expect(Token::NAME_TYPE)->getValue(); + $var = new TemplateVariable($this->parser->getStream()->expect(Token::NAME_TYPE)->getValue(), $token->getLine()); $this->parser->getStream()->expect(Token::BLOCK_END_TYPE); - $this->parser->addImportedSymbol('template', $var); + $this->parser->addImportedSymbol('template', $var->getAttribute('name')); return new ImportNode($macro, $var, $token->getLine(), $this->parser->isMainScope()); } diff --git a/src/TokenParser/MacroTokenParser.php b/src/TokenParser/MacroTokenParser.php index 7b34eeef689..7d47821b2be 100644 --- a/src/TokenParser/MacroTokenParser.php +++ b/src/TokenParser/MacroTokenParser.php @@ -16,9 +16,9 @@ use Twig\Node\EmptyNode; use Twig\Node\Expression\ArrayExpression; use Twig\Node\Expression\ConstantExpression; -use Twig\Node\Expression\TempNameExpression; use Twig\Node\Expression\Unary\NegUnary; use Twig\Node\Expression\Unary\PosUnary; +use Twig\Node\Expression\Variable\LocalVariable; use Twig\Node\MacroNode; use Twig\Node\Node; use Twig\Token; @@ -85,7 +85,7 @@ private function parseDefinition(): ArrayExpression } $token = $stream->expect(Token::NAME_TYPE, null, 'An argument must be a name'); - $name = new TempNameExpression($token->getValue(), $this->parser->getCurrentToken()->getLine()); + $name = new LocalVariable($token->getValue(), $this->parser->getCurrentToken()->getLine()); if ($token = $stream->nextIf(Token::OPERATOR_TYPE, '=')) { $default = $this->parser->getExpressionParser()->parseExpression(); } else { diff --git a/tests/ExpressionParserTest.php b/tests/ExpressionParserTest.php index def632c1932..162f8836c1e 100644 --- a/tests/ExpressionParserTest.php +++ b/tests/ExpressionParserTest.php @@ -23,8 +23,8 @@ use Twig\Node\Expression\ConstantExpression; use Twig\Node\Expression\FilterExpression; use Twig\Node\Expression\FunctionExpression; -use Twig\Node\Expression\NameExpression; use Twig\Node\Expression\TestExpression; +use Twig\Node\Expression\Variable\ContextVariable; use Twig\Node\Node; use Twig\Parser; use Twig\Source; @@ -175,9 +175,9 @@ public static function getTestsForSequence() ], ['{{ {a, b} }}', new ArrayExpression([ new ConstantExpression('a', 1), - new NameExpression('a', 1), + new ContextVariable('a', 1), new ConstantExpression('b', 1), - new NameExpression('b', 1), + new ContextVariable('b', 1), ], 1)], // sequence with spread operator @@ -190,7 +190,7 @@ public static function getTestsForSequence() new ConstantExpression(2, 1), new ConstantExpression(2, 1), - self::createNameExpression('foo', ['spread' => true]), + self::createContextVariable('foo', ['spread' => true]), ], 1)], // mapping with spread operator @@ -203,7 +203,7 @@ public static function getTestsForSequence() new ConstantExpression('c', 1), new ConstantExpression(0, 1), - self::createNameExpression('otherLetters', ['spread' => true]), + self::createContextVariable('otherLetters', ['spread' => true]), ], 1)], ]; } @@ -237,7 +237,7 @@ public static function getTestsForString() [ '{{ "foo #{bar}" }}', new ConcatBinary( new ConstantExpression('foo ', 1), - new NameExpression('bar', 1), + new ContextVariable('bar', 1), 1 ), ], @@ -245,7 +245,7 @@ public static function getTestsForString() '{{ "foo #{bar} baz" }}', new ConcatBinary( new ConcatBinary( new ConstantExpression('foo ', 1), - new NameExpression('bar', 1), + new ContextVariable('bar', 1), 1 ), new ConstantExpression(' baz', 1), @@ -260,7 +260,7 @@ public static function getTestsForString() new ConcatBinary( new ConcatBinary( new ConstantExpression('foo ', 1), - new NameExpression('bar', 1), + new ContextVariable('bar', 1), 1 ), new ConstantExpression(' baz', 1), @@ -564,9 +564,9 @@ public function testTwoWordTestPrecedence() $this->expectNotToPerformAssertions(); } - private static function createNameExpression(string $name, array $attributes): NameExpression + private static function createContextVariable(string $name, array $attributes): ContextVariable { - $expression = new NameExpression($name, 1); + $expression = new ContextVariable($name, 1); foreach ($attributes as $key => $value) { $expression->setAttribute($key, $value); } diff --git a/tests/Node/Expression/GetAttrTest.php b/tests/Node/Expression/GetAttrTest.php index 0446c73eb79..537b010ac06 100644 --- a/tests/Node/Expression/GetAttrTest.php +++ b/tests/Node/Expression/GetAttrTest.php @@ -14,7 +14,7 @@ use Twig\Node\Expression\ArrayExpression; use Twig\Node\Expression\ConstantExpression; use Twig\Node\Expression\GetAttrExpression; -use Twig\Node\Expression\NameExpression; +use Twig\Node\Expression\Variable\ContextVariable; use Twig\Template; use Twig\Test\NodeTestCase; @@ -22,10 +22,10 @@ class GetAttrTest extends NodeTestCase { public function testConstructor() { - $expr = new NameExpression('foo', 1); + $expr = new ContextVariable('foo', 1); $attr = new ConstantExpression('bar', 1); $args = new ArrayExpression([], 1); - $args->addElement(new NameExpression('foo', 1)); + $args->addElement(new ContextVariable('foo', 1)); $args->addElement(new ConstantExpression('bar', 1)); $node = new GetAttrExpression($expr, $attr, $args, Template::ARRAY_CALL, 1); @@ -39,18 +39,18 @@ public static function provideTests(): iterable { $tests = []; - $expr = new NameExpression('foo', 1); + $expr = new ContextVariable('foo', 1); $attr = new ConstantExpression('bar', 1); $args = new ArrayExpression([], 1); $node = new GetAttrExpression($expr, $attr, $args, Template::ANY_CALL, 1); $tests[] = [$node, \sprintf('%s%s, "bar", [], "any", false, false, false, 1)', self::createAttributeGetter(), self::createVariableGetter('foo', 1))]; $node = new GetAttrExpression($expr, $attr, $args, Template::ARRAY_CALL, 1); - $tests[] = [$node, '(($__internal_%s = // line 1'."\n". - '($context["foo"] ?? null)) && is_array($__internal_%s) || $__internal_%s instanceof ArrayAccess ? ($__internal_%s["bar"] ?? null) : null)', null, true, ]; + $tests[] = [$node, '(($_v%s = // line 1'."\n". + '($context["foo"] ?? null)) && is_array($_v%s) || $_v%s instanceof ArrayAccess ? ($_v%s["bar"] ?? null) : null)', null, true, ]; $args = new ArrayExpression([], 1); - $args->addElement(new NameExpression('foo', 1)); + $args->addElement(new ContextVariable('foo', 1)); $args->addElement(new ConstantExpression('bar', 1)); $node = new GetAttrExpression($expr, $attr, $args, Template::METHOD_CALL, 1); $tests[] = [$node, \sprintf('%s%s, "bar", [%s, "bar"], "method", false, false, false, 1)', self::createAttributeGetter(), self::createVariableGetter('foo', 1), self::createVariableGetter('foo'))]; diff --git a/tests/Node/Expression/NullCoalesceTest.php b/tests/Node/Expression/NullCoalesceTest.php index e529db736da..88f8a95f151 100644 --- a/tests/Node/Expression/NullCoalesceTest.php +++ b/tests/Node/Expression/NullCoalesceTest.php @@ -12,15 +12,15 @@ */ use Twig\Node\Expression\ConstantExpression; -use Twig\Node\Expression\NameExpression; use Twig\Node\Expression\NullCoalesceExpression; +use Twig\Node\Expression\Variable\ContextVariable; use Twig\Test\NodeTestCase; class NullCoalesceTest extends NodeTestCase { public static function provideTests(): iterable { - $left = new NameExpression('foo', 1); + $left = new ContextVariable('foo', 1); $right = new ConstantExpression(2, 1); $node = new NullCoalesceExpression($left, $right, 1); diff --git a/tests/Node/Expression/AssignNameTest.php b/tests/Node/Expression/Variable/AssignContextVariableTest.php similarity index 70% rename from tests/Node/Expression/AssignNameTest.php rename to tests/Node/Expression/Variable/AssignContextVariableTest.php index 3ed8511d88e..4ff469eaa98 100644 --- a/tests/Node/Expression/AssignNameTest.php +++ b/tests/Node/Expression/Variable/AssignContextVariableTest.php @@ -11,21 +11,21 @@ * file that was distributed with this source code. */ -use Twig\Node\Expression\AssignNameExpression; +use Twig\Node\Expression\Variable\AssignContextVariable; use Twig\Test\NodeTestCase; -class AssignNameTest extends NodeTestCase +class AssignContextVariableTest extends NodeTestCase { public function testConstructor() { - $node = new AssignNameExpression('foo', 1); + $node = new AssignContextVariable('foo', 1); $this->assertEquals('foo', $node->getAttribute('name')); } public static function provideTests(): iterable { - $node = new AssignNameExpression('foo', 1); + $node = new AssignContextVariable('foo', 1); return [ [$node, '$context["foo"]'], diff --git a/tests/Node/Expression/NameTest.php b/tests/Node/Expression/Variable/ContextVariableTest.php similarity index 79% rename from tests/Node/Expression/NameTest.php rename to tests/Node/Expression/Variable/ContextVariableTest.php index b38d446def1..be9f6917b05 100644 --- a/tests/Node/Expression/NameTest.php +++ b/tests/Node/Expression/Variable/ContextVariableTest.php @@ -13,23 +13,23 @@ use Twig\Environment; use Twig\Loader\ArrayLoader; -use Twig\Node\Expression\NameExpression; +use Twig\Node\Expression\Variable\ContextVariable; use Twig\Test\NodeTestCase; -class NameTest extends NodeTestCase +class ContextVariableTest extends NodeTestCase { public function testConstructor() { - $node = new NameExpression('foo', 1); + $node = new ContextVariable('foo', 1); $this->assertEquals('foo', $node->getAttribute('name')); } public static function provideTests(): iterable { - $node = new NameExpression('foo', 1); - $self = new NameExpression('_self', 1); - $context = new NameExpression('_context', 1); + $node = new ContextVariable('foo', 1); + $self = new ContextVariable('_self', 1); + $context = new ContextVariable('_context', 1); $env = new Environment(new ArrayLoader(), ['strict_variables' => true]); $env1 = new Environment(new ArrayLoader(), ['strict_variables' => false]); diff --git a/tests/Node/ForTest.php b/tests/Node/ForTest.php index 4f96e94c4f8..814b6086a58 100644 --- a/tests/Node/ForTest.php +++ b/tests/Node/ForTest.php @@ -11,8 +11,8 @@ * file that was distributed with this source code. */ -use Twig\Node\Expression\AssignNameExpression; -use Twig\Node\Expression\NameExpression; +use Twig\Node\Expression\Variable\AssignContextVariable; +use Twig\Node\Expression\Variable\ContextVariable; use Twig\Node\ForNode; use Twig\Node\Nodes; use Twig\Node\PrintNode; @@ -22,10 +22,10 @@ class ForTest extends NodeTestCase { public function testConstructor() { - $keyTarget = new AssignNameExpression('key', 1); - $valueTarget = new AssignNameExpression('item', 1); - $seq = new NameExpression('items', 1); - $body = new Nodes([new PrintNode(new NameExpression('foo', 1), 1)], 1); + $keyTarget = new AssignContextVariable('key', 1); + $valueTarget = new AssignContextVariable('item', 1); + $seq = new ContextVariable('items', 1); + $body = new Nodes([new PrintNode(new ContextVariable('foo', 1), 1)], 1); $else = null; $node = new ForNode($keyTarget, $valueTarget, $seq, null, $body, $else, 1); $node->setAttribute('with_loop', false); @@ -36,7 +36,7 @@ public function testConstructor() $this->assertEquals($body, $node->getNode('body')->getNode('0')); $this->assertFalse($node->hasNode('else')); - $else = new PrintNode(new NameExpression('foo', 1), 1); + $else = new PrintNode(new ContextVariable('foo', 1), 1); $node = new ForNode($keyTarget, $valueTarget, $seq, null, $body, $else, 1); $node->setAttribute('with_loop', false); $this->assertEquals($else, $node->getNode('else')); @@ -46,10 +46,10 @@ public static function provideTests(): iterable { $tests = []; - $keyTarget = new AssignNameExpression('key', 1); - $valueTarget = new AssignNameExpression('item', 1); - $seq = new NameExpression('items', 1); - $body = new Nodes([new PrintNode(new NameExpression('foo', 1), 1)], 1); + $keyTarget = new AssignContextVariable('key', 1); + $valueTarget = new AssignContextVariable('item', 1); + $seq = new ContextVariable('items', 1); + $body = new Nodes([new PrintNode(new ContextVariable('foo', 1), 1)], 1); $else = null; $node = new ForNode($keyTarget, $valueTarget, $seq, null, $body, $else, 1); $node->setAttribute('with_loop', false); @@ -71,10 +71,10 @@ public static function provideTests(): iterable EOF ]; - $keyTarget = new AssignNameExpression('k', 1); - $valueTarget = new AssignNameExpression('v', 1); - $seq = new NameExpression('values', 1); - $body = new Nodes([new PrintNode(new NameExpression('foo', 1), 1)], 1); + $keyTarget = new AssignContextVariable('k', 1); + $valueTarget = new AssignContextVariable('v', 1); + $seq = new ContextVariable('values', 1); + $body = new Nodes([new PrintNode(new ContextVariable('foo', 1), 1)], 1); $else = null; $node = new ForNode($keyTarget, $valueTarget, $seq, null, $body, $else, 1); $node->setAttribute('with_loop', true); @@ -113,10 +113,10 @@ public static function provideTests(): iterable EOF ]; - $keyTarget = new AssignNameExpression('k', 1); - $valueTarget = new AssignNameExpression('v', 1); - $seq = new NameExpression('values', 1); - $body = new Nodes([new PrintNode(new NameExpression('foo', 1), 1)], 1); + $keyTarget = new AssignContextVariable('k', 1); + $valueTarget = new AssignContextVariable('v', 1); + $seq = new ContextVariable('values', 1); + $body = new Nodes([new PrintNode(new ContextVariable('foo', 1), 1)], 1); $else = null; $node = new ForNode($keyTarget, $valueTarget, $seq, null, $body, $else, 1); $node->setAttribute('with_loop', true); @@ -155,11 +155,11 @@ public static function provideTests(): iterable EOF ]; - $keyTarget = new AssignNameExpression('k', 1); - $valueTarget = new AssignNameExpression('v', 1); - $seq = new NameExpression('values', 1); - $body = new Nodes([new PrintNode(new NameExpression('foo', 1), 1)], 1); - $else = new PrintNode(new NameExpression('foo', 1), 1); + $keyTarget = new AssignContextVariable('k', 1); + $valueTarget = new AssignContextVariable('v', 1); + $seq = new ContextVariable('values', 1); + $body = new Nodes([new PrintNode(new ContextVariable('foo', 1), 1)], 1); + $else = new PrintNode(new ContextVariable('foo', 1), 1); $node = new ForNode($keyTarget, $valueTarget, $seq, null, $body, $else, 1); $node->setAttribute('with_loop', true); diff --git a/tests/Node/IfTest.php b/tests/Node/IfTest.php index c3939f1e000..47b99509fc7 100644 --- a/tests/Node/IfTest.php +++ b/tests/Node/IfTest.php @@ -12,7 +12,7 @@ */ use Twig\Node\Expression\ConstantExpression; -use Twig\Node\Expression\NameExpression; +use Twig\Node\Expression\Variable\ContextVariable; use Twig\Node\IfNode; use Twig\Node\Nodes; use Twig\Node\PrintNode; @@ -24,7 +24,7 @@ public function testConstructor() { $t = new Nodes([ new ConstantExpression(true, 1), - new PrintNode(new NameExpression('foo', 1), 1), + new PrintNode(new ContextVariable('foo', 1), 1), ], 1); $else = null; $node = new IfNode($t, $else, 1); @@ -32,7 +32,7 @@ public function testConstructor() $this->assertEquals($t, $node->getNode('tests')); $this->assertFalse($node->hasNode('else')); - $else = new PrintNode(new NameExpression('bar', 1), 1); + $else = new PrintNode(new ContextVariable('bar', 1), 1); $node = new IfNode($t, $else, 1); $this->assertEquals($else, $node->getNode('else')); } @@ -43,7 +43,7 @@ public static function provideTests(): iterable $t = new Nodes([ new ConstantExpression(true, 1), - new PrintNode(new NameExpression('foo', 1), 1), + new PrintNode(new ContextVariable('foo', 1), 1), ], 1); $else = null; $node = new IfNode($t, $else, 1); @@ -61,9 +61,9 @@ public static function provideTests(): iterable $t = new Nodes([ new ConstantExpression(true, 1), - new PrintNode(new NameExpression('foo', 1), 1), + new PrintNode(new ContextVariable('foo', 1), 1), new ConstantExpression(false, 1), - new PrintNode(new NameExpression('bar', 1), 1), + new PrintNode(new ContextVariable('bar', 1), 1), ], 1); $else = null; $node = new IfNode($t, $else, 1); @@ -80,9 +80,9 @@ public static function provideTests(): iterable $t = new Nodes([ new ConstantExpression(true, 1), - new PrintNode(new NameExpression('foo', 1), 1), + new PrintNode(new ContextVariable('foo', 1), 1), ], 1); - $else = new PrintNode(new NameExpression('bar', 1), 1); + $else = new PrintNode(new ContextVariable('bar', 1), 1); $node = new IfNode($t, $else, 1); $tests[] = [$node, <<assertEquals($macro, $node->getNode('expr')); - $this->assertEquals($var, $node->getAttribute('var')); + $this->assertEquals('macro', $node->getNode('var')->getAttribute('name')); } public static function provideTests(): iterable @@ -31,7 +32,7 @@ public static function provideTests(): iterable $tests = []; $macro = new ConstantExpression('foo.twig', 1); - $node = new ImportNode($macro, 'macro', 1); + $node = new ImportNode($macro, new TemplateVariable('macro', 1), 1); $tests[] = [$node, <<loadTemplate("foo.twig", null, 1); + \$_v%s = \$this->loadTemplate("foo.twig", null, 1); } catch (LoaderError \$e) { // ignore missing template - \$__internal_%s = null; + \$_v%s = null; } -if (\$__internal_%s) { - yield from \$__internal_%s->unwrap()->yield(CoreExtension::toArray(["foo" => true])); +if (\$_v%s) { + yield from \$_v%s->unwrap()->yield(CoreExtension::toArray(["foo" => true])); } EOF , null, true]; diff --git a/tests/Node/MacroTest.php b/tests/Node/MacroTest.php index 8db7d92779a..39ff9cd1f96 100644 --- a/tests/Node/MacroTest.php +++ b/tests/Node/MacroTest.php @@ -16,8 +16,8 @@ use Twig\Node\BodyNode; use Twig\Node\Expression\ArrayExpression; use Twig\Node\Expression\ConstantExpression; -use Twig\Node\Expression\NameExpression; -use Twig\Node\Expression\TempNameExpression; +use Twig\Node\Expression\Variable\ContextVariable; +use Twig\Node\Expression\Variable\LocalVariable; use Twig\Node\MacroNode; use Twig\Node\TextNode; use Twig\Test\NodeTestCase; @@ -27,7 +27,7 @@ class MacroTest extends NodeTestCase public function testConstructor() { $body = new BodyNode([new TextNode('foo', 1)]); - $arguments = new ArrayExpression([new NameExpression('foo', 1), new ConstantExpression(null, 1)], 1); + $arguments = new ArrayExpression([new ContextVariable('foo', 1), new ConstantExpression(null, 1)], 1); $node = new MacroNode('foo', $body, $arguments, 1); $this->assertEquals($body, $node->getNode('body')); @@ -38,9 +38,9 @@ public function testConstructor() public static function provideTests(): iterable { $arguments = new ArrayExpression([ - new TempNameExpression('foo', 1), + new LocalVariable('foo', 1), new ConstantExpression(null, 1), - new TempNameExpression('bar', 1), + new LocalVariable('bar', 1), new ConstantExpression('Foo', 1), ], 1); diff --git a/tests/Node/ModuleTest.php b/tests/Node/ModuleTest.php index 8edbfab460c..ea208f26cb6 100644 --- a/tests/Node/ModuleTest.php +++ b/tests/Node/ModuleTest.php @@ -15,9 +15,10 @@ use Twig\Loader\ArrayLoader; use Twig\Node\BodyNode; use Twig\Node\EmptyNode; -use Twig\Node\Expression\AssignNameExpression; use Twig\Node\Expression\ConditionalExpression; use Twig\Node\Expression\ConstantExpression; +use Twig\Node\Expression\Variable\AssignContextVariable; +use Twig\Node\Expression\Variable\TemplateVariable; use Twig\Node\ImportNode; use Twig\Node\ModuleNode; use Twig\Node\Nodes; @@ -129,7 +130,7 @@ public function getSourceContext(): Source EOF , $twig, true]; - $import = new ImportNode(new ConstantExpression('foo.twig', 1), 'macro', 2); + $import = new ImportNode(new ConstantExpression('foo.twig', 1), new TemplateVariable('macro', 2), 2); $body = new BodyNode([$import]); $extends = new ConstantExpression('layout.twig', 1); @@ -219,7 +220,7 @@ public function getSourceContext(): Source EOF , $twig, true]; - $set = new SetNode(false, new Nodes([new AssignNameExpression('foo', 4)]), new Nodes([new ConstantExpression('foo', 4)]), 4); + $set = new SetNode(false, new Nodes([new AssignContextVariable('foo', 4)]), new Nodes([new ConstantExpression('foo', 4)]), 4); $body = new BodyNode([$set]); $extends = new ConditionalExpression( new ConstantExpression(true, 2), diff --git a/tests/Node/PrintTest.php b/tests/Node/PrintTest.php index f91c41eb864..a28f21a3dba 100644 --- a/tests/Node/PrintTest.php +++ b/tests/Node/PrintTest.php @@ -13,7 +13,7 @@ use Twig\Node\Expression\ConstantExpression; use Twig\Node\Expression\GetAttrExpression; -use Twig\Node\Expression\NameExpression; +use Twig\Node\Expression\Variable\ContextVariable; use Twig\Node\PrintNode; use Twig\Template; use Twig\Test\NodeTestCase; @@ -33,7 +33,7 @@ public static function provideTests(): iterable $tests = []; $tests[] = [new PrintNode(new ConstantExpression('foo', 1), 1), "// line 1\nyield \"foo\";"]; - $expr = new NameExpression('foo', 1); + $expr = new ContextVariable('foo', 1); $attr = new ConstantExpression('bar', 1); $node = new GetAttrExpression($expr, $attr, null, Template::METHOD_CALL, 1); $node->setAttribute('is_generator', true); diff --git a/tests/Node/SetTest.php b/tests/Node/SetTest.php index 6283884afe8..d55376145a4 100644 --- a/tests/Node/SetTest.php +++ b/tests/Node/SetTest.php @@ -13,9 +13,9 @@ use Twig\Environment; use Twig\Loader\ArrayLoader; -use Twig\Node\Expression\AssignNameExpression; use Twig\Node\Expression\ConstantExpression; -use Twig\Node\Expression\NameExpression; +use Twig\Node\Expression\Variable\AssignContextVariable; +use Twig\Node\Expression\Variable\ContextVariable; use Twig\Node\Nodes; use Twig\Node\PrintNode; use Twig\Node\SetNode; @@ -26,7 +26,7 @@ class SetTest extends NodeTestCase { public function testConstructor() { - $names = new Nodes([new AssignNameExpression('foo', 1)], 1); + $names = new Nodes([new AssignContextVariable('foo', 1)], 1); $values = new Nodes([new ConstantExpression('foo', 1)], 1); $node = new SetNode(false, $names, $values, 1); @@ -39,7 +39,7 @@ public static function provideTests(): iterable { $tests = []; - $names = new Nodes([new AssignNameExpression('foo', 1)], 1); + $names = new Nodes([new AssignContextVariable('foo', 1)], 1); $values = new Nodes([new ConstantExpression('foo', 1)], 1); $node = new SetNode(false, $names, $values, 1); $tests[] = [$node, << false]), ]; - $names = new Nodes([new AssignNameExpression('foo', 1)], 1); + $names = new Nodes([new AssignContextVariable('foo', 1)], 1); $values = new TextNode('foo', 1); $node = new SetNode(true, $names, $values, 1); $tests[] = [$node, <<setAttribute('is_generator', true); $node = new ModuleNode(new BodyNode([new PrintNode($expr, 1)]), null, new EmptyNode(), new EmptyNode(), new EmptyNode(), new EmptyNode(), new Source('foo', 'foo')); $traverser = new NodeTraverser($env, [new SandboxNodeVisitor($env)]); From f3e0a00cf0e128e932e084a778b4789ea71ecd50 Mon Sep 17 00:00:00 2001 From: Jeroen Versteeg Date: Mon, 21 Oct 2024 09:38:42 +0200 Subject: [PATCH 560/812] Documentation for types tag uses Twig types in examples instead of PHP --- doc/tags/types.rst | 11 +++++------ src/TokenParser/TypesTokenParser.php | 2 +- tests/Node/TypesTest.php | 4 ++-- 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/doc/tags/types.rst b/doc/tags/types.rst index c5710ce8ec7..468a2b24a59 100644 --- a/doc/tags/types.rst +++ b/doc/tags/types.rst @@ -9,14 +9,13 @@ The ``types`` tag declares the types of template variables. To do this, specify a :ref:`mapping ` of names to their types as strings. -Here is how to declare that ``is_correct`` is a boolean, while ``score`` is an -integer (see note below): +Here is how to declare that ``is_correct`` is a boolean, while ``score`` is a number (see note below): .. code-block:: twig {% types { - is_correct: 'bool', - score: 'int', + is_correct: 'boolean', + score: 'number', } %} You can declare variables as optional by adding the ``?`` suffix: @@ -24,8 +23,8 @@ You can declare variables as optional by adding the ``?`` suffix: .. code-block:: twig {% types { - is_correct: 'bool', - score?: 'int', + is_correct: 'boolean', + score?: 'number', } %} By default, this tag does not affect the template compilation or runtime behavior. diff --git a/src/TokenParser/TypesTokenParser.php b/src/TokenParser/TypesTokenParser.php index 2e0850e7e9b..b97eb3b2e4c 100644 --- a/src/TokenParser/TypesTokenParser.php +++ b/src/TokenParser/TypesTokenParser.php @@ -20,7 +20,7 @@ /** * Declare variable types. * - * {% types {foo: 'int', bar?: 'string'} %} + * {% types {foo: 'number', bar?: 'string'} %} * * @author Jeroen Versteeg * diff --git a/tests/Node/TypesTest.php b/tests/Node/TypesTest.php index e5a94b6a995..7c74a5f7aa2 100644 --- a/tests/Node/TypesTest.php +++ b/tests/Node/TypesTest.php @@ -9,14 +9,14 @@ class TypesTest extends NodeTestCase { private static function getValidMapping(): array { - // {foo: 'string', bar?: 'int'} + // {foo: 'string', bar?: 'number'} return [ 'foo' => [ 'type' => 'string', 'optional' => false, ], 'bar' => [ - 'type' => 'int', + 'type' => 'number', 'optional' => true, ], ]; From 612c7a14be618b71e9df4a96738d209a9396260d Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Thu, 24 Oct 2024 07:21:25 +0200 Subject: [PATCH 561/812] Improve ImportNode impl --- ...ariable.php => AssignTemplateVariable.php} | 22 +++++++++--- .../Expression/Variable/TemplateVariable.php | 3 -- src/Node/ImportNode.php | 34 +++++-------------- src/TokenParser/FromTokenParser.php | 6 ++-- src/TokenParser/ImportTokenParser.php | 6 ++-- tests/Node/ImportTest.php | 6 ++-- tests/Node/ModuleTest.php | 4 +-- 7 files changed, 37 insertions(+), 44 deletions(-) rename src/Node/Expression/Variable/{GlobalTemplateVariable.php => AssignTemplateVariable.php} (55%) diff --git a/src/Node/Expression/Variable/GlobalTemplateVariable.php b/src/Node/Expression/Variable/AssignTemplateVariable.php similarity index 55% rename from src/Node/Expression/Variable/GlobalTemplateVariable.php rename to src/Node/Expression/Variable/AssignTemplateVariable.php index c41223ccb83..86a23026c75 100644 --- a/src/Node/Expression/Variable/GlobalTemplateVariable.php +++ b/src/Node/Expression/Variable/AssignTemplateVariable.php @@ -13,21 +13,33 @@ use Twig\Compiler; -final class GlobalTemplateVariable extends TemplateVariable +final class AssignTemplateVariable extends TemplateVariable { + public function __construct(string|int|null $name, int $lineno, bool $global = true) + { + parent::__construct($name, $lineno); + + $this->setAttribute('global', $global); + } + public function compile(Compiler $compiler): void { if (null === $this->getAttribute('name')) { $this->setAttribute('name', \sprintf('_l%d', $compiler->getVarName())); } - if ('_self' === $this->getAttribute('name')) { - $compiler->raw('$this'); - } else { + $compiler + ->addDebugInfo($this) + ->write('$macros[') + ->string($this->getAttribute('name')) + ->raw('] = ') + ; + + if ($this->getAttribute('global')) { $compiler ->raw('$this->macros[') ->string($this->getAttribute('name')) - ->raw(']') + ->raw('] = ') ; } } diff --git a/src/Node/Expression/Variable/TemplateVariable.php b/src/Node/Expression/Variable/TemplateVariable.php index ded0b73e4e2..e2a36eafaa3 100644 --- a/src/Node/Expression/Variable/TemplateVariable.php +++ b/src/Node/Expression/Variable/TemplateVariable.php @@ -14,9 +14,6 @@ use Twig\Compiler; use Twig\Node\Expression\TempNameExpression; -/** - * @final - */ class TemplateVariable extends TempNameExpression { public function compile(Compiler $compiler): void diff --git a/src/Node/ImportNode.php b/src/Node/ImportNode.php index b083d064dcc..ab9ca746934 100644 --- a/src/Node/ImportNode.php +++ b/src/Node/ImportNode.php @@ -15,8 +15,7 @@ use Twig\Compiler; use Twig\Node\Expression\AbstractExpression; use Twig\Node\Expression\NameExpression; -use Twig\Node\Expression\Variable\GlobalTemplateVariable; -use Twig\Node\Expression\Variable\TemplateVariable; +use Twig\Node\Expression\Variable\AssignTemplateVariable; /** * Represents an import node. @@ -29,39 +28,24 @@ class ImportNode extends Node /** * @param bool $global */ - public function __construct(AbstractExpression $expr, AbstractExpression|TemplateVariable $var, int $lineno, $global = true) + public function __construct(AbstractExpression $expr, AbstractExpression|AssignTemplateVariable $var, int $lineno) { - if (null === $global || \is_string($global)) { - trigger_deprecation('twig/twig', '3.12', 'Passing a tag to %s() is deprecated.', __METHOD__); - $global = \func_num_args() > 4 ? func_get_arg(4) : true; - } elseif (!\is_bool($global)) { - throw new \TypeError(\sprintf('Argument 4 passed to "%s()" must be a boolean, "%s" given.', __METHOD__, get_debug_type($global))); + if (!\is_bool(\func_num_args() > 3)) { + trigger_deprecation('twig/twig', '3.15', \sprintf('Passing more than 3 arguments to "%s()" is deprecated.', __METHOD__)); } - if (!$var instanceof TemplateVariable) { - trigger_deprecation('twig/twig', '3.15', \sprintf('Passing a "%s" instance as the second argument of "%s" is deprecated, pass a "%s" instead.', $var::class, __CLASS__, TemplateVariable::class)); + if (!$var instanceof AssignTemplateVariable) { + trigger_deprecation('twig/twig', '3.15', \sprintf('Passing a "%s" instance as the second argument of "%s" is deprecated, pass a "%s" instead.', $var::class, __CLASS__, AssignTemplateVariable::class)); - $var = new TemplateVariable($var->getAttribute('name'), $lineno); + $var = new AssignTemplateVariable($var->getAttribute('name'), $lineno); } - parent::__construct(['expr' => $expr, 'var' => $var], ['global' => $global], $lineno); + parent::__construct(['expr' => $expr, 'var' => $var], [], $lineno); } public function compile(Compiler $compiler): void { - $compiler - ->addDebugInfo($this) - ->write('') - ->subcompile($this->getNode('var')) - ->raw(' = ') - ; - - if ($this->getAttribute('global')) { - $compiler - ->subcompile(new GlobalTemplateVariable($this->getNode('var')->getAttribute('name'), $this->getTemplateLine())) - ->raw(' = ') - ; - } + $compiler->subcompile($this->getNode('var')); if ($this->getNode('expr') instanceof NameExpression && '_self' === $this->getNode('expr')->getAttribute('name')) { $compiler->raw('$this'); diff --git a/src/TokenParser/FromTokenParser.php b/src/TokenParser/FromTokenParser.php index b5f985f55e3..b0cccb82ee4 100644 --- a/src/TokenParser/FromTokenParser.php +++ b/src/TokenParser/FromTokenParser.php @@ -12,7 +12,7 @@ namespace Twig\TokenParser; use Twig\Node\Expression\Variable\AssignContextVariable; -use Twig\Node\Expression\Variable\TemplateVariable; +use Twig\Node\Expression\Variable\AssignTemplateVariable; use Twig\Node\ImportNode; use Twig\Node\Node; use Twig\Token; @@ -51,8 +51,8 @@ public function parse(Token $token): Node $stream->expect(Token::BLOCK_END_TYPE); - $internalRef = new TemplateVariable(null, $token->getLine()); - $node = new ImportNode($macro, $internalRef, $token->getLine(), $this->parser->isMainScope()); + $internalRef = new AssignTemplateVariable(null, $token->getLine(), $this->parser->isMainScope()); + $node = new ImportNode($macro, $internalRef, $token->getLine()); foreach ($targets as $name => $alias) { $this->parser->addImportedSymbol('function', $alias->getAttribute('name'), 'macro_'.$name, $internalRef); diff --git a/src/TokenParser/ImportTokenParser.php b/src/TokenParser/ImportTokenParser.php index 0f1dd1c97b8..d20d73162bf 100644 --- a/src/TokenParser/ImportTokenParser.php +++ b/src/TokenParser/ImportTokenParser.php @@ -11,7 +11,7 @@ namespace Twig\TokenParser; -use Twig\Node\Expression\Variable\TemplateVariable; +use Twig\Node\Expression\Variable\AssignTemplateVariable; use Twig\Node\ImportNode; use Twig\Node\Node; use Twig\Token; @@ -29,11 +29,11 @@ public function parse(Token $token): Node { $macro = $this->parser->getExpressionParser()->parseExpression(); $this->parser->getStream()->expect(Token::NAME_TYPE, 'as'); - $var = new TemplateVariable($this->parser->getStream()->expect(Token::NAME_TYPE)->getValue(), $token->getLine()); + $var = new AssignTemplateVariable($this->parser->getStream()->expect(Token::NAME_TYPE)->getValue(), $token->getLine(), $this->parser->isMainScope()); $this->parser->getStream()->expect(Token::BLOCK_END_TYPE); $this->parser->addImportedSymbol('template', $var->getAttribute('name')); - return new ImportNode($macro, $var, $token->getLine(), $this->parser->isMainScope()); + return new ImportNode($macro, $var, $token->getLine()); } public function getTag(): string diff --git a/tests/Node/ImportTest.php b/tests/Node/ImportTest.php index b1f76cb8621..59d99b71ff7 100644 --- a/tests/Node/ImportTest.php +++ b/tests/Node/ImportTest.php @@ -12,7 +12,7 @@ */ use Twig\Node\Expression\ConstantExpression; -use Twig\Node\Expression\Variable\TemplateVariable; +use Twig\Node\Expression\Variable\AssignTemplateVariable; use Twig\Node\ImportNode; use Twig\Test\NodeTestCase; @@ -21,7 +21,7 @@ class ImportTest extends NodeTestCase public function testConstructor() { $macro = new ConstantExpression('foo.twig', 1); - $node = new ImportNode($macro, new TemplateVariable('macro', 1), 1); + $node = new ImportNode($macro, new AssignTemplateVariable('macro', 1), 1); $this->assertEquals($macro, $node->getNode('expr')); $this->assertEquals('macro', $node->getNode('var')->getAttribute('name')); @@ -32,7 +32,7 @@ public static function provideTests(): iterable $tests = []; $macro = new ConstantExpression('foo.twig', 1); - $node = new ImportNode($macro, new TemplateVariable('macro', 1), 1); + $node = new ImportNode($macro, new AssignTemplateVariable('macro', 1), 1); $tests[] = [$node, << Date: Thu, 24 Oct 2024 13:37:17 +0200 Subject: [PATCH 562/812] Add return type to getDebugInfo --- src/Node/ModuleNode.php | 1 + tests/Node/ModuleTest.php | 3 +++ 2 files changed, 4 insertions(+) diff --git a/src/Node/ModuleNode.php b/src/Node/ModuleNode.php index ddb3f734622..a127a399a53 100644 --- a/src/Node/ModuleNode.php +++ b/src/Node/ModuleNode.php @@ -448,6 +448,7 @@ protected function compileDebugInfo(Compiler $compiler) $compiler ->write("/**\n") ->write(" * @codeCoverageIgnore\n") + ->write(" * @return array\n") ->write(" */\n") ->write("public function getDebugInfo(): array\n", "{\n") ->indent() diff --git a/tests/Node/ModuleTest.php b/tests/Node/ModuleTest.php index f017b483bea..97ae7bf3f6b 100644 --- a/tests/Node/ModuleTest.php +++ b/tests/Node/ModuleTest.php @@ -116,6 +116,7 @@ public function getTemplateName(): string /** * @codeCoverageIgnore + * @return array */ public function getDebugInfo(): array { @@ -206,6 +207,7 @@ public function isTraitable(): bool /** * @codeCoverageIgnore + * @return array */ public function getDebugInfo(): array { @@ -300,6 +302,7 @@ public function isTraitable(): bool /** * @codeCoverageIgnore + * @return array */ public function getDebugInfo(): array { From 1e7c719e24eb318ae67a1added4b361cb499ff8e Mon Sep 17 00:00:00 2001 From: Ruud Kamphuis Date: Thu, 24 Oct 2024 13:51:09 +0200 Subject: [PATCH 563/812] Add return type to compiled macro This makes it easier for TwigStan to analyze the return type. --- src/Node/MacroNode.php | 3 ++- tests/Node/MacroTest.php | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Node/MacroNode.php b/src/Node/MacroNode.php index 5ada1b2206d..d96e98cf24e 100644 --- a/src/Node/MacroNode.php +++ b/src/Node/MacroNode.php @@ -14,6 +14,7 @@ use Twig\Attribute\YieldReady; use Twig\Compiler; use Twig\Error\SyntaxError; +use Twig\Markup; use Twig\Node\Expression\ArrayExpression; use Twig\Node\Expression\Variable\LocalVariable; @@ -76,7 +77,7 @@ public function compile(Compiler $compiler): void $compiler ->raw('...$varargs') - ->raw(")\n") + ->raw("): string|Markup\n") ->write("{\n") ->indent() ->write("\$macros = \$this->macros;\n") diff --git a/tests/Node/MacroTest.php b/tests/Node/MacroTest.php index 39ff9cd1f96..fcd5db4029c 100644 --- a/tests/Node/MacroTest.php +++ b/tests/Node/MacroTest.php @@ -49,7 +49,7 @@ public static function provideTests(): iterable yield 'with use_yield = true' => [$node, <<macros; \$context = [ @@ -71,7 +71,7 @@ public function macro_foo(\$foo = null, \$bar = "Foo", ...\$varargs) yield 'with use_yield = false' => [$node, <<macros; \$context = [ From 3d99e595c81015d739424d24a80f48464fbd7192 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Thu, 24 Oct 2024 10:38:15 +0200 Subject: [PATCH 564/812] Deprecate the range function --- CHANGELOG | 1 + doc/deprecated.rst | 3 +++ doc/functions/range.rst | 5 +++++ src/Extension/CoreExtension.php | 2 +- src/NodeVisitor/SandboxNodeVisitor.php | 1 + tests/Fixtures/functions/{range.test => range.legacy.test} | 2 ++ tests/Fixtures/whitespace/trim_block.test | 4 ++-- 7 files changed, 15 insertions(+), 3 deletions(-) rename tests/Fixtures/functions/{range.test => range.legacy.test} (56%) diff --git a/CHANGELOG b/CHANGELOG index 6eac17dc9a9..02a8f2d5190 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,6 @@ # 3.15.0 (2024-XX-XX) + * Deprecate the `range` function, use the `..` operator * Deprecate `TempNameExpression` in favor of `LocalVariable` * Deprecate `NameExpression` in favor of `ContextVariable` * Deprecate `AssignNameExpression` in favor of `AssignContextVariable` diff --git a/doc/deprecated.rst b/doc/deprecated.rst index fed5cb38017..0c8483ba228 100644 --- a/doc/deprecated.rst +++ b/doc/deprecated.rst @@ -28,6 +28,9 @@ Functions Note that it won't be removed in 4.0 to allow a smoother upgrade path. +* The ``range`` function is deprecated as of Twig 3.15, use the ``..`` operator + instead. + Extensions ---------- diff --git a/doc/functions/range.rst b/doc/functions/range.rst index e537b11a652..ba80b61be47 100644 --- a/doc/functions/range.rst +++ b/doc/functions/range.rst @@ -1,6 +1,11 @@ ``range`` ========= +.. warning:: + + The ``range`` function is deprecated as of Twig 3.15. Use the ``..`` + operator instead. + Returns a list containing an arithmetic progression of integers: .. code-block:: twig diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index f8f80ac2f0c..645c797e75d 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -260,7 +260,7 @@ public function getFunctions(): array new TwigFunction('attribute', null, ['parser_callable' => [self::class, 'parseAttributeFunction']]), new TwigFunction('max', 'max'), new TwigFunction('min', 'min'), - new TwigFunction('range', 'range'), + new TwigFunction('range', 'range', ['deprecation_info' => new DeprecatedCallableInfo('twig/twig', '3.15')]), new TwigFunction('constant', [self::class, 'constant']), new TwigFunction('cycle', [self::class, 'cycle']), new TwigFunction('random', [self::class, 'random'], ['needs_charset' => true]), diff --git a/src/NodeVisitor/SandboxNodeVisitor.php b/src/NodeVisitor/SandboxNodeVisitor.php index ab51d33d4a0..c1b80030cdf 100644 --- a/src/NodeVisitor/SandboxNodeVisitor.php +++ b/src/NodeVisitor/SandboxNodeVisitor.php @@ -69,6 +69,7 @@ public function enterNode(Node $node, Environment $env): Node } // the .. operator is equivalent to the range() function + // To be removed in 4.0 if ($node instanceof RangeBinary && !isset($this->functions['range'])) { $this->functions['range'] = $node->getTemplateLine(); } diff --git a/tests/Fixtures/functions/range.test b/tests/Fixtures/functions/range.legacy.test similarity index 56% rename from tests/Fixtures/functions/range.test rename to tests/Fixtures/functions/range.legacy.test index 2927333b97f..3fa6a3777ff 100644 --- a/tests/Fixtures/functions/range.test +++ b/tests/Fixtures/functions/range.legacy.test @@ -1,5 +1,7 @@ --TEST-- "range" function +--DEPRECATION-- +Since twig/twig 3.15: Twig Function "range" is deprecated in index.twig at line 2. --TEMPLATE-- {{ range(low=0+1, high=10+0, step=2)|join(',') }} --DATA-- diff --git a/tests/Fixtures/whitespace/trim_block.test b/tests/Fixtures/whitespace/trim_block.test index 346a11076c3..2f85a0e5617 100644 --- a/tests/Fixtures/whitespace/trim_block.test +++ b/tests/Fixtures/whitespace/trim_block.test @@ -2,13 +2,13 @@ Whitespace trimming on tags. --TEMPLATE-- Trim on control tag: -{% for i in range(1, 9) -%} +{% for i in 1..9 -%} {{ i }} {%- endfor %} Trim on output tag: -{% for i in range(1, 9) %} +{% for i in 1..9 %} {{- i -}} {% endfor %} From 1d22d300faa7cb2df8b6f6a3a8c96a4999f9955b Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Thu, 24 Oct 2024 15:37:54 +0200 Subject: [PATCH 565/812] Revert "feature #4409 Deprecate the range function (fabpot)" This reverts commit 99c6fbd517ed8aa4b604b21d042b36486706dcd0, reversing changes made to 868b4298539a4c39d9fb1c43a7098404313e0ac5. --- CHANGELOG | 1 - doc/deprecated.rst | 3 --- doc/functions/range.rst | 5 ----- src/Extension/CoreExtension.php | 2 +- src/NodeVisitor/SandboxNodeVisitor.php | 1 - tests/Fixtures/functions/{range.legacy.test => range.test} | 2 -- tests/Fixtures/whitespace/trim_block.test | 4 ++-- 7 files changed, 3 insertions(+), 15 deletions(-) rename tests/Fixtures/functions/{range.legacy.test => range.test} (56%) diff --git a/CHANGELOG b/CHANGELOG index 02a8f2d5190..6eac17dc9a9 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,5 @@ # 3.15.0 (2024-XX-XX) - * Deprecate the `range` function, use the `..` operator * Deprecate `TempNameExpression` in favor of `LocalVariable` * Deprecate `NameExpression` in favor of `ContextVariable` * Deprecate `AssignNameExpression` in favor of `AssignContextVariable` diff --git a/doc/deprecated.rst b/doc/deprecated.rst index 0c8483ba228..fed5cb38017 100644 --- a/doc/deprecated.rst +++ b/doc/deprecated.rst @@ -28,9 +28,6 @@ Functions Note that it won't be removed in 4.0 to allow a smoother upgrade path. -* The ``range`` function is deprecated as of Twig 3.15, use the ``..`` operator - instead. - Extensions ---------- diff --git a/doc/functions/range.rst b/doc/functions/range.rst index ba80b61be47..e537b11a652 100644 --- a/doc/functions/range.rst +++ b/doc/functions/range.rst @@ -1,11 +1,6 @@ ``range`` ========= -.. warning:: - - The ``range`` function is deprecated as of Twig 3.15. Use the ``..`` - operator instead. - Returns a list containing an arithmetic progression of integers: .. code-block:: twig diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index 645c797e75d..f8f80ac2f0c 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -260,7 +260,7 @@ public function getFunctions(): array new TwigFunction('attribute', null, ['parser_callable' => [self::class, 'parseAttributeFunction']]), new TwigFunction('max', 'max'), new TwigFunction('min', 'min'), - new TwigFunction('range', 'range', ['deprecation_info' => new DeprecatedCallableInfo('twig/twig', '3.15')]), + new TwigFunction('range', 'range'), new TwigFunction('constant', [self::class, 'constant']), new TwigFunction('cycle', [self::class, 'cycle']), new TwigFunction('random', [self::class, 'random'], ['needs_charset' => true]), diff --git a/src/NodeVisitor/SandboxNodeVisitor.php b/src/NodeVisitor/SandboxNodeVisitor.php index c1b80030cdf..ab51d33d4a0 100644 --- a/src/NodeVisitor/SandboxNodeVisitor.php +++ b/src/NodeVisitor/SandboxNodeVisitor.php @@ -69,7 +69,6 @@ public function enterNode(Node $node, Environment $env): Node } // the .. operator is equivalent to the range() function - // To be removed in 4.0 if ($node instanceof RangeBinary && !isset($this->functions['range'])) { $this->functions['range'] = $node->getTemplateLine(); } diff --git a/tests/Fixtures/functions/range.legacy.test b/tests/Fixtures/functions/range.test similarity index 56% rename from tests/Fixtures/functions/range.legacy.test rename to tests/Fixtures/functions/range.test index 3fa6a3777ff..2927333b97f 100644 --- a/tests/Fixtures/functions/range.legacy.test +++ b/tests/Fixtures/functions/range.test @@ -1,7 +1,5 @@ --TEST-- "range" function ---DEPRECATION-- -Since twig/twig 3.15: Twig Function "range" is deprecated in index.twig at line 2. --TEMPLATE-- {{ range(low=0+1, high=10+0, step=2)|join(',') }} --DATA-- diff --git a/tests/Fixtures/whitespace/trim_block.test b/tests/Fixtures/whitespace/trim_block.test index 2f85a0e5617..346a11076c3 100644 --- a/tests/Fixtures/whitespace/trim_block.test +++ b/tests/Fixtures/whitespace/trim_block.test @@ -2,13 +2,13 @@ Whitespace trimming on tags. --TEMPLATE-- Trim on control tag: -{% for i in 1..9 -%} +{% for i in range(1, 9) -%} {{ i }} {%- endfor %} Trim on output tag: -{% for i in 1..9 %} +{% for i in range(1, 9) %} {{- i -}} {% endfor %} From 5b0209ffa38f7f7080a7a90f04cf69bc619d3a42 Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Thu, 24 Oct 2024 17:35:30 +0200 Subject: [PATCH 566/812] do not drop none digit characters from generated variable names --- src/Node/Expression/TempNameExpression.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Node/Expression/TempNameExpression.php b/src/Node/Expression/TempNameExpression.php index bf304a98cb7..b6ce30c8b74 100644 --- a/src/Node/Expression/TempNameExpression.php +++ b/src/Node/Expression/TempNameExpression.php @@ -41,7 +41,7 @@ public function __construct(string|int|null $name, int $lineno) public function compile(Compiler $compiler): void { if (null === $this->getAttribute('name')) { - $this->setAttribute('name', \sprintf('_l%d', $compiler->getVarName())); + $this->setAttribute('name', $compiler->getVarName()); } $compiler->raw('$'.$this->getAttribute('name')); From 89fdc7d31d41eda806f07effe1d8ced5063ca6ca Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Fri, 25 Oct 2024 07:35:14 +0200 Subject: [PATCH 567/812] Refactor code --- src/ExpressionParser.php | 6 +++--- .../Variable/AssignTemplateVariable.php | 17 ++++++----------- .../Expression/Variable/TemplateVariable.php | 15 +++++++++++---- src/NodeVisitor/EscaperNodeVisitor.php | 2 +- src/Parser.php | 9 +++++---- src/TokenParser/FromTokenParser.php | 3 ++- src/TokenParser/ImportTokenParser.php | 6 ++++-- tests/Node/ImportTest.php | 7 ++++--- tests/Node/ModuleTest.php | 3 ++- 9 files changed, 38 insertions(+), 30 deletions(-) diff --git a/src/ExpressionParser.php b/src/ExpressionParser.php index 8af4ea046b2..b043f3e3422 100644 --- a/src/ExpressionParser.php +++ b/src/ExpressionParser.php @@ -533,7 +533,7 @@ public function parsePostfixExpression($node) public function getFunctionNode($name, $line) { if (null !== $alias = $this->parser->getImportedSymbol('function', $name)) { - return new MacroReferenceExpression(new TemplateVariable($alias['node']->getAttribute('name'), $line), $alias['name'], $this->createArguments($line), $line); + return new MacroReferenceExpression($alias['node']->getNode('var'), $alias['name'], $this->createArguments($line), $line); } $args = $this->parseOnlyArguments(); @@ -750,7 +750,7 @@ private function parseTestExpression(Node $node): TestExpression } if ('defined' === $test->getName() && $node instanceof NameExpression && null !== $alias = $this->parser->getImportedSymbol('function', $node->getAttribute('name'))) { - $node = new MacroReferenceExpression(new TemplateVariable($alias['node']->getAttribute('name'), $node->getTemplateLine()), $alias['name'], new ArrayExpression([], $node->getTemplateLine()), $node->getTemplateLine()); + $node = new MacroReferenceExpression($alias['node']->getNode('var'), $alias['name'], new ArrayExpression([], $node->getTemplateLine()), $node->getTemplateLine()); } $ready = $test instanceof TwigTest; @@ -957,7 +957,7 @@ private function parseSubscriptExpressionDot(Node $node): AbstractExpression $node instanceof NameExpression && ( - null !== $this->parser->getImportedSymbol('template', (new TemplateVariable($node->getAttribute('name'), $node->getTemplateLine()))->getAttribute('name')) + null !== $this->parser->getImportedSymbol('template', $node->getAttribute('name')) || '_self' === $node->getAttribute('name') && $attribute instanceof ConstantExpression ) diff --git a/src/Node/Expression/Variable/AssignTemplateVariable.php b/src/Node/Expression/Variable/AssignTemplateVariable.php index 86a23026c75..aa9ed11919f 100644 --- a/src/Node/Expression/Variable/AssignTemplateVariable.php +++ b/src/Node/Expression/Variable/AssignTemplateVariable.php @@ -12,33 +12,28 @@ namespace Twig\Node\Expression\Variable; use Twig\Compiler; +use Twig\Node\Expression\AbstractExpression; -final class AssignTemplateVariable extends TemplateVariable +final class AssignTemplateVariable extends AbstractExpression { - public function __construct(string|int|null $name, int $lineno, bool $global = true) + public function __construct(TemplateVariable $var, bool $global = true) { - parent::__construct($name, $lineno); - - $this->setAttribute('global', $global); + parent::__construct(['var' => $var], ['global' => $global], $var->getTemplateLine()); } public function compile(Compiler $compiler): void { - if (null === $this->getAttribute('name')) { - $this->setAttribute('name', \sprintf('_l%d', $compiler->getVarName())); - } - $compiler ->addDebugInfo($this) ->write('$macros[') - ->string($this->getAttribute('name')) + ->string($this->nodes['var']->getName($compiler)) ->raw('] = ') ; if ($this->getAttribute('global')) { $compiler ->raw('$this->macros[') - ->string($this->getAttribute('name')) + ->string($this->nodes['var']->getName($compiler)) ->raw('] = ') ; } diff --git a/src/Node/Expression/Variable/TemplateVariable.php b/src/Node/Expression/Variable/TemplateVariable.php index e2a36eafaa3..4dd06627359 100644 --- a/src/Node/Expression/Variable/TemplateVariable.php +++ b/src/Node/Expression/Variable/TemplateVariable.php @@ -16,18 +16,25 @@ class TemplateVariable extends TempNameExpression { - public function compile(Compiler $compiler): void + public function getName(Compiler $compiler): string { if (null === $this->getAttribute('name')) { - $this->setAttribute('name', \sprintf('_l%d', $compiler->getVarName())); + $this->setAttribute('name', $compiler->getVarName()); } - if ('_self' === $this->getAttribute('name')) { + return $this->getAttribute('name'); + } + + public function compile(Compiler $compiler): void + { + $name = $this->getName($compiler); + + if ('_self' === $name) { $compiler->raw('$this'); } else { $compiler ->raw('$macros[') - ->string($this->getAttribute('name')) + ->string($name) ->raw(']') ; } diff --git a/src/NodeVisitor/EscaperNodeVisitor.php b/src/NodeVisitor/EscaperNodeVisitor.php index c942f825b34..5334d0c0486 100644 --- a/src/NodeVisitor/EscaperNodeVisitor.php +++ b/src/NodeVisitor/EscaperNodeVisitor.php @@ -61,7 +61,7 @@ public function enterNode(Node $node, Environment $env): Node } elseif ($node instanceof BlockNode) { $this->statusStack[] = $this->blocks[$node->getAttribute('name')] ?? $this->needEscaping(); } elseif ($node instanceof ImportNode) { - $this->safeVars[] = $node->getNode('var')->getAttribute('name'); + $this->safeVars[] = $node->getNode('var')->getNode('var')->getAttribute('name'); } return $node; diff --git a/src/Parser.php b/src/Parser.php index b55c0148a50..7bf51b73c20 100644 --- a/src/Parser.php +++ b/src/Parser.php @@ -18,6 +18,7 @@ use Twig\Node\BodyNode; use Twig\Node\EmptyNode; use Twig\Node\Expression\AbstractExpression; +use Twig\Node\Expression\Variable\AssignTemplateVariable; use Twig\Node\Expression\Variable\TemplateVariable; use Twig\Node\MacroNode; use Twig\Node\ModuleNode; @@ -295,12 +296,12 @@ public function embedTemplate(ModuleNode $template) $this->embeddedTemplates[] = $template; } - public function addImportedSymbol(string $type, string $alias, ?string $name = null, AbstractExpression|TemplateVariable|null $internalRef = null): void + public function addImportedSymbol(string $type, string $alias, ?string $name = null, AbstractExpression|AssignTemplateVariable|null $internalRef = null): void { - if ($internalRef && !$internalRef instanceof TemplateVariable) { - trigger_deprecation('twig/twig', '3.15', 'Not passing a "%s" instance as an internal reference is deprecated ("%s" given).', __METHOD__, TemplateVariable::class, $internalRef::class); + if ($internalRef && !$internalRef instanceof AssignTemplateVariable) { + trigger_deprecation('twig/twig', '3.15', 'Not passing a "%s" instance as an internal reference is deprecated ("%s" given).', __METHOD__, AssignTemplateVariable::class, $internalRef::class); - $internalRef = new TemplateVariable($internalRef->getAttribute('name'), $internalRef->getTemplateLine()); + $internalRef = new AssignTemplateVariable(new TemplateVariable($internalRef->getAttribute('name'), $internalRef->getTemplateLine()), $internalRef->getAttribute('global')); } $this->importedSymbols[0][$type][$alias] = ['name' => $name, 'node' => $internalRef]; diff --git a/src/TokenParser/FromTokenParser.php b/src/TokenParser/FromTokenParser.php index b0cccb82ee4..3bb4201a3dd 100644 --- a/src/TokenParser/FromTokenParser.php +++ b/src/TokenParser/FromTokenParser.php @@ -13,6 +13,7 @@ use Twig\Node\Expression\Variable\AssignContextVariable; use Twig\Node\Expression\Variable\AssignTemplateVariable; +use Twig\Node\Expression\Variable\TemplateVariable; use Twig\Node\ImportNode; use Twig\Node\Node; use Twig\Token; @@ -51,7 +52,7 @@ public function parse(Token $token): Node $stream->expect(Token::BLOCK_END_TYPE); - $internalRef = new AssignTemplateVariable(null, $token->getLine(), $this->parser->isMainScope()); + $internalRef = new AssignTemplateVariable(new TemplateVariable(null, $token->getLine()), $this->parser->isMainScope()); $node = new ImportNode($macro, $internalRef, $token->getLine()); foreach ($targets as $name => $alias) { diff --git a/src/TokenParser/ImportTokenParser.php b/src/TokenParser/ImportTokenParser.php index d20d73162bf..5b3a5f2b8d4 100644 --- a/src/TokenParser/ImportTokenParser.php +++ b/src/TokenParser/ImportTokenParser.php @@ -12,6 +12,7 @@ namespace Twig\TokenParser; use Twig\Node\Expression\Variable\AssignTemplateVariable; +use Twig\Node\Expression\Variable\TemplateVariable; use Twig\Node\ImportNode; use Twig\Node\Node; use Twig\Token; @@ -29,9 +30,10 @@ public function parse(Token $token): Node { $macro = $this->parser->getExpressionParser()->parseExpression(); $this->parser->getStream()->expect(Token::NAME_TYPE, 'as'); - $var = new AssignTemplateVariable($this->parser->getStream()->expect(Token::NAME_TYPE)->getValue(), $token->getLine(), $this->parser->isMainScope()); + $name = $this->parser->getStream()->expect(Token::NAME_TYPE)->getValue(); + $var = new AssignTemplateVariable(new TemplateVariable($name, $token->getLine()), $this->parser->isMainScope()); $this->parser->getStream()->expect(Token::BLOCK_END_TYPE); - $this->parser->addImportedSymbol('template', $var->getAttribute('name')); + $this->parser->addImportedSymbol('template', $name); return new ImportNode($macro, $var, $token->getLine()); } diff --git a/tests/Node/ImportTest.php b/tests/Node/ImportTest.php index 59d99b71ff7..a55137063cc 100644 --- a/tests/Node/ImportTest.php +++ b/tests/Node/ImportTest.php @@ -13,6 +13,7 @@ use Twig\Node\Expression\ConstantExpression; use Twig\Node\Expression\Variable\AssignTemplateVariable; +use Twig\Node\Expression\Variable\TemplateVariable; use Twig\Node\ImportNode; use Twig\Test\NodeTestCase; @@ -21,10 +22,10 @@ class ImportTest extends NodeTestCase public function testConstructor() { $macro = new ConstantExpression('foo.twig', 1); - $node = new ImportNode($macro, new AssignTemplateVariable('macro', 1), 1); + $node = new ImportNode($macro, new AssignTemplateVariable(new TemplateVariable('macro', 1), true), 1); $this->assertEquals($macro, $node->getNode('expr')); - $this->assertEquals('macro', $node->getNode('var')->getAttribute('name')); + $this->assertEquals('macro', $node->getNode('var')->getNode('var')->getAttribute('name')); } public static function provideTests(): iterable @@ -32,7 +33,7 @@ public static function provideTests(): iterable $tests = []; $macro = new ConstantExpression('foo.twig', 1); - $node = new ImportNode($macro, new AssignTemplateVariable('macro', 1), 1); + $node = new ImportNode($macro, new AssignTemplateVariable(new TemplateVariable('macro', 1), true), 1); $tests[] = [$node, << Date: Thu, 24 Oct 2024 14:18:02 +0200 Subject: [PATCH 568/812] Add link to TwigQI in docs "You might also be interested in" section --- doc/templates.rst | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/doc/templates.rst b/doc/templates.rst index 4fbbe2fec64..2f2775ab707 100644 --- a/doc/templates.rst +++ b/doc/templates.rst @@ -69,6 +69,10 @@ You might also be interested in: * `Twig Language Server`_: provides some language features like syntax highlighting, diagnostics, auto complete, ... +* `TwigQI`_: an extension which analyzes your templates for common bugs during compilation + +* `TwigStan`_: a static analyzer for Twig templates powered by PHPStan + Variables --------- @@ -597,7 +601,7 @@ exist: * ``\x``: Hexadecimal escape sequence * ``\0`` to ``\377``: Octal escape sequences representing characters * ``\``: Backslash - + When using single-quoted strings, the single quote character (``'``) needs to be escaped with a backslash (``\'``). When using double-quoted strings, the double quote character (``"``) needs to be escaped with a backslash (``\"``). @@ -1134,6 +1138,8 @@ Twig can be extended. If you want to create your own extensions, read the .. _`regular expressions`: https://www.php.net/manual/en/pcre.pattern.php .. _`PHP-twig for atom`: https://github.com/reesef/php-twig .. _`TwigFiddle`: https://twigfiddle.com/ +.. _`TwigQI`: https://github.com/alisqi/TwigQI +.. _`TwigStan`: https://github.com/twigstan/twigstan .. _`Twig pack`: https://marketplace.visualstudio.com/items?itemName=bajdzis.vscode-twig-pack .. _`Modern Twig`: https://marketplace.visualstudio.com/items?itemName=Stanislav.vscode-twig .. _`Twig Language Server`: https://github.com/kaermorchen/twig-language-server/tree/master/packages/language-server From 92e4989002c53313f32ad20ad843e518529cf1c6 Mon Sep 17 00:00:00 2001 From: Ruud Kamphuis Date: Fri, 25 Oct 2024 13:19:27 +0200 Subject: [PATCH 569/812] Add `$this` return type to Template::unwrap See https://phpstan.org/writing-php-code/phpdoc-types#static-and-%24this This way, PHPStan understands that the returned template is the exact same instance, and not just a Template. --- src/Template.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Template.php b/src/Template.php index cf36da1192d..26f5b5d8154 100644 --- a/src/Template.php +++ b/src/Template.php @@ -318,6 +318,7 @@ protected function loadTemplate($template, $templateName = null, $line = null, $ /** * @internal + * @return $this */ public function unwrap(): self { From 494f010d29bf86e600fed21ce1d8477b66c475da Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Fri, 25 Oct 2024 16:01:04 +0200 Subject: [PATCH 570/812] Revert "minor #4411 Add return type to getDebugInfo (ruudk)" This reverts commit 868b4298539a4c39d9fb1c43a7098404313e0ac5, reversing changes made to b0017ad8c37548c842ba4e6a314caa13557a3cb1. --- src/Node/ModuleNode.php | 1 - tests/Node/ModuleTest.php | 3 --- 2 files changed, 4 deletions(-) diff --git a/src/Node/ModuleNode.php b/src/Node/ModuleNode.php index a127a399a53..ddb3f734622 100644 --- a/src/Node/ModuleNode.php +++ b/src/Node/ModuleNode.php @@ -448,7 +448,6 @@ protected function compileDebugInfo(Compiler $compiler) $compiler ->write("/**\n") ->write(" * @codeCoverageIgnore\n") - ->write(" * @return array\n") ->write(" */\n") ->write("public function getDebugInfo(): array\n", "{\n") ->indent() diff --git a/tests/Node/ModuleTest.php b/tests/Node/ModuleTest.php index b27e554bd20..4dbce150485 100644 --- a/tests/Node/ModuleTest.php +++ b/tests/Node/ModuleTest.php @@ -117,7 +117,6 @@ public function getTemplateName(): string /** * @codeCoverageIgnore - * @return array */ public function getDebugInfo(): array { @@ -208,7 +207,6 @@ public function isTraitable(): bool /** * @codeCoverageIgnore - * @return array */ public function getDebugInfo(): array { @@ -303,7 +301,6 @@ public function isTraitable(): bool /** * @codeCoverageIgnore - * @return array */ public function getDebugInfo(): array { From d59cc8baa5b5504c72ddbd31e99d04c22375203e Mon Sep 17 00:00:00 2001 From: Jacob Dreesen Date: Mon, 28 Oct 2024 11:25:54 +0100 Subject: [PATCH 571/812] Remove duplicate test case --- tests/Fixtures/filters/find.test | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tests/Fixtures/filters/find.test b/tests/Fixtures/filters/find.test index 3d1dbd422c1..86deae1717b 100644 --- a/tests/Fixtures/filters/find.test +++ b/tests/Fixtures/filters/find.test @@ -6,8 +6,6 @@ {{ [1, 5, 3, 4, 5]|find((v) => v > 3) }} -{{ [1, 5, 3, 4, 5]|find((v) => v > 3) }} - {{ {a: 1, b: 2, c: 5, d: 8}|find(v => v > 3) }} {{ {a: 1, b: 2, c: 5, d: 8}|find((v, k) => (v > 3) and (k != "c")) }} @@ -33,8 +31,6 @@ return [ 5 -5 - 8 5 From 226f0ff0dd5e605719ad4adebb2debd85ca334ee Mon Sep 17 00:00:00 2001 From: Ruud Kamphuis Date: Wed, 30 Oct 2024 15:44:01 +0100 Subject: [PATCH 572/812] Add type for join method Looking at the code, this method is very forgiving in what it accepts. --- src/Extension/CoreExtension.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index f8f80ac2f0c..450e31b5fb8 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -766,9 +766,9 @@ public static function last(string $charset, $item) * {{ [1, 2, 3]|join }} * {# returns 123 #} * - * @param array $value An array - * @param string $glue The separator - * @param string|null $and The separator for the last pair + * @param iterable|array|string|float|int|bool|null $value An array + * @param string $glue The separator + * @param string|null $and The separator for the last pair * * @internal */ From b4ed19b5e1b2d9f6177db91c666ddf2a7bf51bbe Mon Sep 17 00:00:00 2001 From: Ruud Kamphuis Date: Thu, 31 Oct 2024 15:57:00 +0100 Subject: [PATCH 573/812] Add `find` to `Filters` docs https://twig.symfony.com/doc/3.x/filters/index.html https://twig.symfony.com/doc/3.x/filters/find.html --- doc/filters/index.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/filters/index.rst b/doc/filters/index.rst index 7d2bde1b1b0..475b35c804d 100644 --- a/doc/filters/index.rst +++ b/doc/filters/index.rst @@ -18,6 +18,7 @@ Filters default escape filter + find first format format_currency From 290a923a4241367c5121d4aaf1e679ddfd2f483e Mon Sep 17 00:00:00 2001 From: Dennis Tobar Date: Wed, 30 Oct 2024 08:22:28 -0300 Subject: [PATCH 574/812] [String] Add SpanishInflector support for singular and plural --- extra/string-extra/StringExtension.php | 14 +++++++++++--- extra/string-extra/Tests/Fixtures/plural.test | 4 ++++ extra/string-extra/Tests/Fixtures/singular.test | 4 ++++ 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/extra/string-extra/StringExtension.php b/extra/string-extra/StringExtension.php index bd575f788c6..5327b9d0f82 100644 --- a/extra/string-extra/StringExtension.php +++ b/extra/string-extra/StringExtension.php @@ -15,17 +15,20 @@ use Symfony\Component\String\Inflector\EnglishInflector; use Symfony\Component\String\Inflector\FrenchInflector; use Symfony\Component\String\Inflector\InflectorInterface; +use Symfony\Component\String\Inflector\SpanishInflector; use Symfony\Component\String\Slugger\AsciiSlugger; use Symfony\Component\String\Slugger\SluggerInterface; use Symfony\Component\String\UnicodeString; +use Twig\Error\RuntimeError; use Twig\Extension\AbstractExtension; use Twig\TwigFilter; final class StringExtension extends AbstractExtension { private $slugger; - private $frenchInflector; private $englishInflector; + private $spanishInflector; + private $frenchInflector; public function __construct(?SluggerInterface $slugger = null) { @@ -79,10 +82,15 @@ public function singular(string $value, string $locale = 'en', bool $all = false private function getInflector(string $locale): InflectorInterface { switch ($locale) { - case 'fr': - return $this->frenchInflector ?? $this->frenchInflector = new FrenchInflector(); case 'en': return $this->englishInflector ?? $this->englishInflector = new EnglishInflector(); + case 'es': + if (!class_exists(SpanishInflector::class)) { + throw new RuntimeError('SpanishInflector is not available.'); + } + return $this->spanishInflector ?? $this->spanishInflector = new SpanishInflector(); + case 'fr': + return $this->frenchInflector ?? $this->frenchInflector = new FrenchInflector(); default: throw new \InvalidArgumentException(\sprintf('Locale "%s" is not supported.', $locale)); } diff --git a/extra/string-extra/Tests/Fixtures/plural.test b/extra/string-extra/Tests/Fixtures/plural.test index b561e2ad0d6..d5dc3b0b19e 100755 --- a/extra/string-extra/Tests/Fixtures/plural.test +++ b/extra/string-extra/Tests/Fixtures/plural.test @@ -5,6 +5,8 @@ {{ 'partition'|plural('fr', all=true)|join(',') }} {{ 'person'|plural('fr') }} {{ 'person'|plural('en', all=true)|join(',') }} +{{ 'avión'|plural('es') }} +{{ 'avión'|plural('es', all=true)|join(',') }} --DATA-- return [] @@ -13,3 +15,5 @@ partitions partitions persons persons,people +aviones +aviones diff --git a/extra/string-extra/Tests/Fixtures/singular.test b/extra/string-extra/Tests/Fixtures/singular.test index 01e03db66a6..f500636a018 100755 --- a/extra/string-extra/Tests/Fixtures/singular.test +++ b/extra/string-extra/Tests/Fixtures/singular.test @@ -7,6 +7,8 @@ {{ 'persons'|singular('en', all=true)|join(',') }} {{ 'people'|singular('en') }} {{ 'people'|singular('en', all=true)|join(',') }} +{{ 'personas'|singular('es') }} +{{ 'personas'|singular('es', all=true)|join(',') }} --DATA-- return [] @@ -17,3 +19,5 @@ person person person person +persona +persona From b99d73c9274996e9d14b74a2f864073b4096bcea Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sun, 3 Nov 2024 15:09:58 +0100 Subject: [PATCH 575/812] Update CHANGELOG --- CHANGELOG | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 6eac17dc9a9..7e6e5b153cb 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,6 @@ # 3.15.0 (2024-XX-XX) - + + * Add Spanish inflector support for the `plural` and `singular` filters in the String extension * Deprecate `TempNameExpression` in favor of `LocalVariable` * Deprecate `NameExpression` in favor of `ContextVariable` * Deprecate `AssignNameExpression` in favor of `AssignContextVariable` From deb37c30c461674754d4e7e38030eaf25bde5f39 Mon Sep 17 00:00:00 2001 From: Ruud Kamphuis Date: Sat, 2 Nov 2024 15:38:17 +0100 Subject: [PATCH 576/812] Fix mistake in docs for `keys` filter --- doc/filters/keys.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/filters/keys.rst b/doc/filters/keys.rst index 26c61bcd1e4..bcd638d8c08 100644 --- a/doc/filters/keys.rst +++ b/doc/filters/keys.rst @@ -6,10 +6,10 @@ when you want to iterate over the keys of a sequence or a mapping: .. code-block:: twig - {% for key in [1, 2, 3, 4]|keys %} + {% for key in ['a', 'b', 'c', 'd']|keys %} {{ key }} {% endfor %} - {# outputs: 1 2 3 4 #} + {# outputs: 0 1 2 3 #} {% for key in {a: 'a_value', b: 'b_value'}|keys %} {{ key }} From c78499bda9fc47b1378b38c1f597a8a178bc3133 Mon Sep 17 00:00:00 2001 From: Ruud Kamphuis Date: Mon, 4 Nov 2024 11:31:38 +0100 Subject: [PATCH 577/812] Fix wrong type for cycle position It also accepts 0, which is not a positive int. --- src/Extension/CoreExtension.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index f8f80ac2f0c..8b6be12fc7f 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -344,7 +344,7 @@ public function getOperators(): array * Cycles over a sequence. * * @param array|\ArrayAccess $values A non-empty sequence of values - * @param positive-int $position The position of the value to return in the cycle + * @param int<0, max> $position The position of the value to return in the cycle * * @return mixed The value at the given position in the sequence, wrapping around as needed * From 2bb8c2460a2c519c498df9b643d5277117155a73 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Fri, 25 Oct 2024 11:04:18 +0200 Subject: [PATCH 578/812] Fix sandbox handling for __toString() --- src/Extension/SandboxExtension.php | 8 ++++++++ src/NodeVisitor/SandboxNodeVisitor.php | 15 ++++++++++++++- tests/Extension/SandboxTest.php | 15 +++++++++++++-- 3 files changed, 35 insertions(+), 3 deletions(-) diff --git a/src/Extension/SandboxExtension.php b/src/Extension/SandboxExtension.php index 4e96760f7d4..c9ffe6477bd 100644 --- a/src/Extension/SandboxExtension.php +++ b/src/Extension/SandboxExtension.php @@ -119,6 +119,14 @@ public function checkPropertyAllowed($obj, $property, int $lineno = -1, ?Source public function ensureToStringAllowed($obj, int $lineno = -1, ?Source $source = null) { + if (\is_array($obj)) { + foreach ($obj as $v) { + $this->ensureToStringAllowed($v, $lineno, $source); + } + + return $obj; + } + if ($this->isSandboxed($source) && $obj instanceof \Stringable) { try { $this->policy->checkMethodAllowed($obj, '__toString'); diff --git a/src/NodeVisitor/SandboxNodeVisitor.php b/src/NodeVisitor/SandboxNodeVisitor.php index 37e184a3edc..8c15db0d6f2 100644 --- a/src/NodeVisitor/SandboxNodeVisitor.php +++ b/src/NodeVisitor/SandboxNodeVisitor.php @@ -15,12 +15,14 @@ use Twig\Node\CheckSecurityCallNode; use Twig\Node\CheckSecurityNode; use Twig\Node\CheckToStringNode; +use Twig\Node\Expression\ArrayExpression; use Twig\Node\Expression\Binary\ConcatBinary; use Twig\Node\Expression\Binary\RangeBinary; use Twig\Node\Expression\FilterExpression; use Twig\Node\Expression\FunctionExpression; use Twig\Node\Expression\GetAttrExpression; use Twig\Node\Expression\NameExpression; +use Twig\Node\Expression\Unary\SpreadUnary; use Twig\Node\ModuleNode; use Twig\Node\Node; use Twig\Node\PrintNode; @@ -120,7 +122,18 @@ private function wrapNode(Node $node, string $name): void { $expr = $node->getNode($name); if (($expr instanceof NameExpression || $expr instanceof GetAttrExpression) && !$expr->isGenerator()) { - $node->setNode($name, new CheckToStringNode($expr)); + // 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); + } elseif ($expr instanceof SpreadUnary) { + $this->wrapNode($expr, 'node'); + } elseif ($expr instanceof ArrayExpression) { + foreach ($expr as $name => $_) { + $this->wrapNode($expr, $name); + } } } diff --git a/tests/Extension/SandboxTest.php b/tests/Extension/SandboxTest.php index d24a06c6720..59e68f67ec2 100644 --- a/tests/Extension/SandboxTest.php +++ b/tests/Extension/SandboxTest.php @@ -42,6 +42,7 @@ protected function setUp(): void 'obj' => new FooObject(), 'arr' => ['obj' => new FooObject()], 'child_obj' => new ChildClass(), + 'some_array' => [5, 6, 7, new FooObject()], ]; self::$templates = [ @@ -246,10 +247,10 @@ public function testSandboxUnallowedProperty() */ public function testSandboxUnallowedToString($template) { - $twig = $this->getEnvironment(true, [], ['index' => $template], [], ['upper'], ['Twig\Tests\Extension\FooObject' => 'getAnotherFooObject'], [], ['random']); + $twig = $this->getEnvironment(true, [], ['index' => $template], [], ['upper', 'join', 'replace'], ['Twig\Tests\Extension\FooObject' => 'getAnotherFooObject'], [], ['random']); try { $twig->load('index')->render(self::$params); - $this->fail('Sandbox throws a SecurityError exception if an unallowed method (__toString()) is called in the template'); + $this->fail('Sandbox throws a SecurityError exception if an unallowed method "__toString()" method is called in the template'); } catch (SecurityNotAllowedMethodError $e) { $this->assertEquals('Twig\Tests\Extension\FooObject', $e->getClassName(), 'Exception should be raised on the "Twig\Tests\Extension\FooObject" class'); $this->assertEquals('__tostring', $e->getMethodName(), 'Exception should be raised on the "__toString" method'); @@ -272,6 +273,16 @@ public static function getSandboxUnallowedToStringTests() 'object_chain_and_function' => ['{{ random(obj.anotherFooObject) }}'], 'concat' => ['{{ obj ~ "" }}'], 'concat_again' => ['{{ "" ~ obj }}'], + 'object_in_arguments' => ['{{ "__toString"|replace({"__toString": obj}) }}'], + 'object_in_array' => ['{{ [12, "foo", obj]|join(", ") }}'], + 'object_in_array_var' => ['{{ some_array|join(", ") }}'], + 'object_in_array_nested' => ['{{ [12, "foo", [12, "foo", obj]]|join(", ") }}'], + 'object_in_array_var_nested' => ['{{ [12, "foo", some_array]|join(", ") }}'], + 'object_in_array_dynamic_key' => ['{{ {(obj): "foo"}|join(", ") }}'], + 'object_in_array_dynamic_key_nested' => ['{{ {"foo": { (obj): "foo" }}|join(", ") }}'], + 'context' => ['{{ _context|join(", ") }}'], + 'spread_array_operator' => ['{{ [1, 2, ...[5, 6, 7, obj]]|join(",") }}'], + 'spread_array_operator_var' => ['{{ [1, 2, ...some_array]|join(",") }}'], ]; } From 831c148e786178e5f2fde9db67266be3bf241c21 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Tue, 29 Oct 2024 15:00:56 +0100 Subject: [PATCH 579/812] Sandbox ArrayAccess and do sandbox checks before isset() checks --- doc/api.rst | 9 ++++ src/Extension/CoreExtension.php | 64 +++++++++++++++++++--- src/Node/Expression/GetAttrExpression.php | 33 ++++++++++-- tests/Extension/SandboxTest.php | 66 ++++++++++++++++++++--- 4 files changed, 153 insertions(+), 19 deletions(-) diff --git a/doc/api.rst b/doc/api.rst index 09c553175e1..d5e484aa87b 100644 --- a/doc/api.rst +++ b/doc/api.rst @@ -486,6 +486,15 @@ able to call the ``getTitle()`` and ``getBody()`` methods on ``Article`` objects, and the ``title`` and ``body`` public properties. Everything else won't be allowed and will generate a ``\Twig\Sandbox\SecurityError`` exception. +.. note:: + + As of Twig 1.14.1 (and on Twig 3.11.2), if the ``Article`` class implements + the ``ArrayAccess`` interface, the templates will only be able to access + the ``title`` and ``body`` attributes. + + Note that native array-like classes (like ``ArrayObject``) are always + allowed, you don't need to configure them. + .. caution:: The ``extends`` and ``use`` tags are always allowed in a sandboxed diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index 3ed27a35cc3..e1fd3962e0e 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -65,6 +65,8 @@ use Twig\Node\Node; use Twig\NodeVisitor\MacroAutoImportNodeVisitor; use Twig\Parser; +use Twig\Sandbox\SecurityNotAllowedMethodError; +use Twig\Sandbox\SecurityNotAllowedPropertyError; use Twig\Source; use Twig\Template; use Twig\TemplateWrapper; @@ -92,6 +94,20 @@ final class CoreExtension extends AbstractExtension { + public const ARRAY_LIKE_CLASSES = [ + 'ArrayIterator', + 'ArrayObject', + 'CachingIterator', + 'RecursiveArrayIterator', + 'RecursiveCachingIterator', + 'SplDoublyLinkedList', + 'SplFixedArray', + 'SplObjectStorage', + 'SplQueue', + 'SplStack', + 'WeakMap', + ]; + private $dateFormats = ['F j, Y H:i', '%d days']; private $numberFormat = [0, '.', ',']; private $timezone = null; @@ -1587,10 +1603,20 @@ public static function batch($items, $size, $fill = null, $preserveKeys = true): */ public static function getAttribute(Environment $env, Source $source, $object, $item, array $arguments = [], $type = Template::ANY_CALL, $isDefinedTest = false, $ignoreStrictCheck = false, $sandboxed = false, int $lineno = -1) { + $propertyNotAllowedError = null; + // array if (Template::METHOD_CALL !== $type) { $arrayItem = \is_bool($item) || \is_float($item) ? (int) $item : $item; + if ($sandboxed && $object instanceof \ArrayAccess && !\in_array($object::class, self::ARRAY_LIKE_CLASSES, true)) { + try { + $env->getExtension(SandboxExtension::class)->checkPropertyAllowed($object, $arrayItem, $lineno, $source); + } catch (SecurityNotAllowedPropertyError $propertyNotAllowedError) { + goto methodCheck; + } + } + if (((\is_array($object) || $object instanceof \ArrayObject) && (isset($object[$arrayItem]) || \array_key_exists($arrayItem, (array) $object))) || ($object instanceof \ArrayAccess && isset($object[$arrayItem])) ) { @@ -1662,19 +1688,25 @@ public static function getAttribute(Environment $env, Source $source, $object, $ // object property if (Template::METHOD_CALL !== $type) { + if ($sandboxed) { + try { + $env->getExtension(SandboxExtension::class)->checkPropertyAllowed($object, $item, $lineno, $source); + } catch (SecurityNotAllowedPropertyError $propertyNotAllowedError) { + goto methodCheck; + } + } + if (isset($object->$item) || \array_key_exists((string) $item, (array) $object)) { if ($isDefinedTest) { return true; } - if ($sandboxed) { - $env->getExtension(SandboxExtension::class)->checkPropertyAllowed($object, $item, $lineno, $source); - } - return $object->$item; } } + methodCheck: + static $cache = []; $class = \get_class($object); @@ -1733,6 +1765,10 @@ public static function getAttribute(Environment $env, Source $source, $object, $ return false; } + if ($propertyNotAllowedError) { + throw $propertyNotAllowedError; + } + if ($ignoreStrictCheck || !$env->isStrictVariables()) { return; } @@ -1740,12 +1776,24 @@ public static function getAttribute(Environment $env, Source $source, $object, $ throw new RuntimeError(\sprintf('Neither the property "%1$s" nor one of the methods "%1$s()", "get%1$s()"/"is%1$s()"/"has%1$s()" or "__call()" exist and have public access in class "%2$s".', $item, $class), $lineno, $source); } - if ($isDefinedTest) { - return true; + if ($sandboxed) { + try { + $env->getExtension(SandboxExtension::class)->checkMethodAllowed($object, $method, $lineno, $source); + } catch (SecurityNotAllowedMethodError $e) { + if ($isDefinedTest) { + return false; + } + + if ($propertyNotAllowedError) { + throw $propertyNotAllowedError; + } + + throw $e; + } } - if ($sandboxed) { - $env->getExtension(SandboxExtension::class)->checkMethodAllowed($object, $method, $lineno, $source); + if ($isDefinedTest) { + return true; } // Some objects throw exceptions when they have __call, and the method we try diff --git a/src/Node/Expression/GetAttrExpression.php b/src/Node/Expression/GetAttrExpression.php index 29a446b881b..2181b0f7862 100644 --- a/src/Node/Expression/GetAttrExpression.php +++ b/src/Node/Expression/GetAttrExpression.php @@ -31,6 +31,7 @@ public function __construct(AbstractExpression $node, AbstractExpression $attrib public function compile(Compiler $compiler): void { $env = $compiler->getEnvironment(); + $arrayAccessSandbox = false; // optimize array calls if ( @@ -44,17 +45,35 @@ public function compile(Compiler $compiler): void ->raw('(('.$var.' = ') ->subcompile($this->getNode('node')) ->raw(') && is_array(') - ->raw($var) + ->raw($var); + + if (!$env->hasExtension(SandboxExtension::class)) { + $compiler + ->raw(') || ') + ->raw($var) + ->raw(' instanceof ArrayAccess ? (') + ->raw($var) + ->raw('[') + ->subcompile($this->getNode('attribute')) + ->raw('] ?? null) : null)') + ; + + return; + } + + $arrayAccessSandbox = true; + + $compiler ->raw(') || ') ->raw($var) - ->raw(' instanceof ArrayAccess ? (') + ->raw(' instanceof ArrayAccess && in_array(') + ->raw($var.'::class') + ->raw(', CoreExtension::ARRAY_LIKE_CLASSES, true) ? (') ->raw($var) ->raw('[') ->subcompile($this->getNode('attribute')) - ->raw('] ?? null) : null)') + ->raw('] ?? null) : ') ; - - return; } $compiler->raw('CoreExtension::getAttribute($this->env, $this->source, '); @@ -83,5 +102,9 @@ public function compile(Compiler $compiler): void ->raw(', ')->repr($this->getNode('node')->getTemplateLine()) ->raw(')') ; + + if ($arrayAccessSandbox) { + $compiler->raw(')'); + } } } diff --git a/tests/Extension/SandboxTest.php b/tests/Extension/SandboxTest.php index 59e68f67ec2..999483241ae 100644 --- a/tests/Extension/SandboxTest.php +++ b/tests/Extension/SandboxTest.php @@ -43,6 +43,8 @@ protected function setUp(): void 'arr' => ['obj' => new FooObject()], 'child_obj' => new ChildClass(), 'some_array' => [5, 6, 7, new FooObject()], + 'array_like' => new ArrayLikeObject(), + 'magic' => new MagicObject(), ]; self::$templates = [ @@ -66,6 +68,7 @@ protected function setUp(): void '1_childobj_parentmethod' => '{{ child_obj.ParentMethod() }}', '1_childobj_childmethod' => '{{ child_obj.ChildMethod() }}', '1_empty' => '', + '1_array_like' => '{{ array_like["foo"] }}', ]; } @@ -141,15 +144,31 @@ public function testSandboxGloballySet() $this->assertEquals('FOO', $twig->load('1_basic')->render(self::$params), 'Sandbox does nothing if it is disabled globally'); } - public function testSandboxUnallowedMethodAccessor() + public function testSandboxUnallowedPropertyAccessor() { $twig = $this->getEnvironment(true, [], self::$templates); try { - $twig->load('1_basic1')->render(self::$params); + $twig->load('1_basic1')->render(['obj' => new MagicObject()]); $this->fail('Sandbox throws a SecurityError exception if an unallowed method is called'); - } catch (SecurityNotAllowedMethodError $e) { - $this->assertEquals('Twig\Tests\Extension\FooObject', $e->getClassName(), 'Exception should be raised on the "Twig\Tests\Extension\FooObject" class'); - $this->assertEquals('foo', $e->getMethodName(), 'Exception should be raised on the "foo" method'); + } catch (SecurityNotAllowedPropertyError $e) { + $this->assertEquals('Twig\Tests\Extension\MagicObject', $e->getClassName(), 'Exception should be raised on the "Twig\Tests\Extension\MagicObject" class'); + $this->assertEquals('foo', $e->getPropertyName(), 'Exception should be raised on the "foo" property'); + } + } + + public function testSandboxUnallowedArrayIndexAccessor() + { + $twig = $this->getEnvironment(true, [], self::$templates); + + // ArrayObject and other internal array-like classes are exempted from sandbox restrictions + $this->assertSame('bar', $twig->load('1_array_like')->render(['array_like' => new \ArrayObject(['foo' => 'bar'])])); + + try { + $twig->load('1_array_like')->render(self::$params); + $this->fail('Sandbox throws a SecurityError exception if an unallowed method is called'); + } catch (SecurityNotAllowedPropertyError $e) { + $this->assertEquals('Twig\Tests\Extension\ArrayLikeObject', $e->getClassName(), 'Exception should be raised on the "Twig\Tests\Extension\ArrayLikeObject" class'); + $this->assertEquals('foo', $e->getPropertyName(), 'Exception should be raised on the "foo" property'); } } @@ -300,7 +319,8 @@ public static function getSandboxAllowedToStringTests() return [ 'constant_test' => ['{{ obj is constant("PHP_INT_MAX") }}', ''], 'set_object' => ['{% set a = obj.anotherFooObject %}{{ a.foo }}', 'foo'], - 'is_defined' => ['{{ obj.anotherFooObject is defined }}', '1'], + 'is_defined1' => ['{{ obj.anotherFooObject is defined }}', '1'], + 'is_defined2' => ['{{ magic.foo is defined }}', ''], 'is_null' => ['{{ obj is null }}', ''], 'is_sameas' => ['{{ obj is same as(obj) }}', '1'], 'is_sameas_no_brackets' => ['{{ obj is same as obj }}', '1'], @@ -610,3 +630,37 @@ public function getAnotherFooObject() return new self(); } } + +class ArrayLikeObject extends \ArrayObject +{ + public function offsetExists($offset): bool + { + throw new \BadMethodCallException('Should not be called'); + } + + public function offsetGet($offset): mixed + { + throw new \BadMethodCallException('Should not be called'); + } + + public function offsetSet($offset, $value): void + { + } + + public function offsetUnset($offset): void + { + } +} + +class MagicObject +{ + public function __get($name): mixed + { + throw new \BadMethodCallException('Should not be called'); + } + + public function __isset($name): bool + { + throw new \BadMethodCallException('Should not be called'); + } +} From 793e8358fbd37015f8dae4f9ca8aa0a37c951613 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Wed, 6 Nov 2024 18:45:40 +0100 Subject: [PATCH 580/812] Update CHANGELOG --- CHANGELOG | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index 44c79b13325..2068f4899bb 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,10 @@ +# 3.14.1 (2024-11-06) + + * [BC BREAK] Fix a security issue in the sandbox mode allowing an attacker to call attributes on Array-like objects + They are now checked via the property policy + * Fix a security issue in the sandbox mode allowing an attacker to be able to call `toString()` + under some circumstances on an object even if the `__toString()` method is not allowed by the security policy + # 3.14.0 (2024-09-09) * Fix a security issue when an included sandboxed template has been loaded before without the sandbox context From f405356d20fb43603bcadc8b09bfb676cb04a379 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Wed, 6 Nov 2024 19:17:38 +0100 Subject: [PATCH 581/812] Prepare the 3.14.1 release --- src/Environment.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Environment.php b/src/Environment.php index 24e55e979fe..339aac63506 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -43,11 +43,11 @@ */ class Environment { - public const VERSION = '3.14.0'; - public const VERSION_ID = 31400; + public const VERSION = '3.14.1'; + public const VERSION_ID = 31401; public const MAJOR_VERSION = 3; public const MINOR_VERSION = 14; - public const RELEASE_VERSION = 0; + public const RELEASE_VERSION = 1; public const EXTRA_VERSION = ''; private $charset; From cafc608ece310e62a35a76f17e25c04ab9ed05cc Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Fri, 25 Oct 2024 11:04:18 +0200 Subject: [PATCH 582/812] Fix sandbox handling for __toString() --- src/Extension/SandboxExtension.php | 8 ++++++++ src/NodeVisitor/SandboxNodeVisitor.php | 15 ++++++++++++++- tests/Extension/SandboxTest.php | 15 +++++++++++++-- 3 files changed, 35 insertions(+), 3 deletions(-) diff --git a/src/Extension/SandboxExtension.php b/src/Extension/SandboxExtension.php index 921df287a44..95b6295aa21 100644 --- a/src/Extension/SandboxExtension.php +++ b/src/Extension/SandboxExtension.php @@ -119,6 +119,14 @@ public function checkPropertyAllowed($obj, $property, int $lineno = -1, ?Source public function ensureToStringAllowed($obj, int $lineno = -1, ?Source $source = null) { + if (\is_array($obj)) { + foreach ($obj as $v) { + $this->ensureToStringAllowed($v, $lineno, $source); + } + + return $obj; + } + if ($this->isSandboxed($source) && \is_object($obj) && method_exists($obj, '__toString')) { try { $this->policy->checkMethodAllowed($obj, '__toString'); diff --git a/src/NodeVisitor/SandboxNodeVisitor.php b/src/NodeVisitor/SandboxNodeVisitor.php index 68020885e40..7cc1c2e9f66 100644 --- a/src/NodeVisitor/SandboxNodeVisitor.php +++ b/src/NodeVisitor/SandboxNodeVisitor.php @@ -15,12 +15,14 @@ use Twig\Node\CheckSecurityCallNode; use Twig\Node\CheckSecurityNode; use Twig\Node\CheckToStringNode; +use Twig\Node\Expression\ArrayExpression; use Twig\Node\Expression\Binary\ConcatBinary; use Twig\Node\Expression\Binary\RangeBinary; use Twig\Node\Expression\FilterExpression; use Twig\Node\Expression\FunctionExpression; use Twig\Node\Expression\GetAttrExpression; use Twig\Node\Expression\NameExpression; +use Twig\Node\Expression\Unary\SpreadUnary; use Twig\Node\ModuleNode; use Twig\Node\Node; use Twig\Node\PrintNode; @@ -120,7 +122,18 @@ private function wrapNode(Node $node, string $name): void { $expr = $node->getNode($name); if (($expr instanceof NameExpression || $expr instanceof GetAttrExpression) && !$expr->isGenerator()) { - $node->setNode($name, new CheckToStringNode($expr)); + // 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); + } elseif ($expr instanceof SpreadUnary) { + $this->wrapNode($expr, 'node'); + } elseif ($expr instanceof ArrayExpression) { + foreach ($expr as $name => $_) { + $this->wrapNode($expr, $name); + } } } diff --git a/tests/Extension/SandboxTest.php b/tests/Extension/SandboxTest.php index cbe6175787b..fe1d68a919e 100644 --- a/tests/Extension/SandboxTest.php +++ b/tests/Extension/SandboxTest.php @@ -38,6 +38,7 @@ protected function setUp(): void 'obj' => new FooObject(), 'arr' => ['obj' => new FooObject()], 'child_obj' => new ChildClass(), + 'some_array' => [5, 6, 7, new FooObject()], ]; self::$templates = [ @@ -184,10 +185,10 @@ public function testSandboxUnallowedProperty() */ public function testSandboxUnallowedToString($template) { - $twig = $this->getEnvironment(true, [], ['index' => $template], [], ['upper'], ['Twig\Tests\Extension\FooObject' => 'getAnotherFooObject'], [], ['random']); + $twig = $this->getEnvironment(true, [], ['index' => $template], [], ['upper', 'join', 'replace'], ['Twig\Tests\Extension\FooObject' => 'getAnotherFooObject'], [], ['random']); try { $twig->load('index')->render(self::$params); - $this->fail('Sandbox throws a SecurityError exception if an unallowed method (__toString()) is called in the template'); + $this->fail('Sandbox throws a SecurityError exception if an unallowed method "__toString()" method is called in the template'); } catch (SecurityNotAllowedMethodError $e) { $this->assertEquals('Twig\Tests\Extension\FooObject', $e->getClassName(), 'Exception should be raised on the "Twig\Tests\Extension\FooObject" class'); $this->assertEquals('__tostring', $e->getMethodName(), 'Exception should be raised on the "__toString" method'); @@ -210,6 +211,16 @@ public function getSandboxUnallowedToStringTests() 'object_chain_and_function' => ['{{ random(obj.anotherFooObject) }}'], 'concat' => ['{{ obj ~ "" }}'], 'concat_again' => ['{{ "" ~ obj }}'], + 'object_in_arguments' => ['{{ "__toString"|replace({"__toString": obj}) }}'], + 'object_in_array' => ['{{ [12, "foo", obj]|join(", ") }}'], + 'object_in_array_var' => ['{{ some_array|join(", ") }}'], + 'object_in_array_nested' => ['{{ [12, "foo", [12, "foo", obj]]|join(", ") }}'], + 'object_in_array_var_nested' => ['{{ [12, "foo", some_array]|join(", ") }}'], + 'object_in_array_dynamic_key' => ['{{ {(obj): "foo"}|join(", ") }}'], + 'object_in_array_dynamic_key_nested' => ['{{ {"foo": { (obj): "foo" }}|join(", ") }}'], + 'context' => ['{{ _context|join(", ") }}'], + 'spread_array_operator' => ['{{ [1, 2, ...[5, 6, 7, obj]]|join(",") }}'], + 'spread_array_operator_var' => ['{{ [1, 2, ...some_array]|join(",") }}'], ]; } From ec39a9dccc5fb4eaaba55e5d79a6f84a8dd8b69d Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Tue, 29 Oct 2024 15:00:56 +0100 Subject: [PATCH 583/812] Sandbox ArrayAccess and do sandbox checks before isset() checks --- doc/api.rst | 9 +++ src/Extension/CoreExtension.php | 64 ++++++++++++++++++--- src/Node/Expression/GetAttrExpression.php | 33 +++++++++-- tests/Extension/SandboxTest.php | 68 +++++++++++++++++++++-- 4 files changed, 155 insertions(+), 19 deletions(-) diff --git a/doc/api.rst b/doc/api.rst index 219bdec3395..d45af36278e 100644 --- a/doc/api.rst +++ b/doc/api.rst @@ -486,6 +486,15 @@ able to call the ``getTitle()`` and ``getBody()`` methods on ``Article`` objects, and the ``title`` and ``body`` public properties. Everything else won't be allowed and will generate a ``\Twig\Sandbox\SecurityError`` exception. +.. note:: + + As of Twig 1.14.1 (and on Twig 3.11.2), if the ``Article`` class implements + the ``ArrayAccess`` interface, the templates will only be able to access + the ``title`` and ``body`` attributes. + + Note that native array-like classes (like ``ArrayObject``) are always + allowed, you don't need to configure them. + The policy object is the first argument of the sandbox constructor:: $sandbox = new \Twig\Extension\SandboxExtension($policy); diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index 4b014b8df93..1e769c6a2b6 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -57,6 +57,8 @@ use Twig\Node\Expression\Unary\NotUnary; use Twig\Node\Expression\Unary\PosUnary; use Twig\NodeVisitor\MacroAutoImportNodeVisitor; +use Twig\Sandbox\SecurityNotAllowedMethodError; +use Twig\Sandbox\SecurityNotAllowedPropertyError; use Twig\Source; use Twig\Template; use Twig\TemplateWrapper; @@ -82,6 +84,20 @@ final class CoreExtension extends AbstractExtension { + public const ARRAY_LIKE_CLASSES = [ + 'ArrayIterator', + 'ArrayObject', + 'CachingIterator', + 'RecursiveArrayIterator', + 'RecursiveCachingIterator', + 'SplDoublyLinkedList', + 'SplFixedArray', + 'SplObjectStorage', + 'SplQueue', + 'SplStack', + 'WeakMap', + ]; + private $dateFormats = ['F j, Y H:i', '%d days']; private $numberFormat = [0, '.', ',']; private $timezone = null; @@ -1549,10 +1565,20 @@ public static function batch($items, $size, $fill = null, $preserveKeys = true): */ public static function getAttribute(Environment $env, Source $source, $object, $item, array $arguments = [], $type = /* Template::ANY_CALL */ 'any', $isDefinedTest = false, $ignoreStrictCheck = false, $sandboxed = false, int $lineno = -1) { + $propertyNotAllowedError = null; + // array if (/* Template::METHOD_CALL */ 'method' !== $type) { $arrayItem = \is_bool($item) || \is_float($item) ? (int) $item : $item; + if ($sandboxed && $object instanceof \ArrayAccess && !\in_array($object::class, self::ARRAY_LIKE_CLASSES, true)) { + try { + $env->getExtension(SandboxExtension::class)->checkPropertyAllowed($object, $arrayItem, $lineno, $source); + } catch (SecurityNotAllowedPropertyError $propertyNotAllowedError) { + goto methodCheck; + } + } + if (((\is_array($object) || $object instanceof \ArrayObject) && (isset($object[$arrayItem]) || \array_key_exists($arrayItem, (array) $object))) || ($object instanceof \ArrayAccess && isset($object[$arrayItem])) ) { @@ -1624,19 +1650,25 @@ public static function getAttribute(Environment $env, Source $source, $object, $ // object property if (/* Template::METHOD_CALL */ 'method' !== $type) { + if ($sandboxed) { + try { + $env->getExtension(SandboxExtension::class)->checkPropertyAllowed($object, $item, $lineno, $source); + } catch (SecurityNotAllowedPropertyError $propertyNotAllowedError) { + goto methodCheck; + } + } + if (isset($object->$item) || \array_key_exists((string) $item, (array) $object)) { if ($isDefinedTest) { return true; } - if ($sandboxed) { - $env->getExtension(SandboxExtension::class)->checkPropertyAllowed($object, $item, $lineno, $source); - } - return $object->$item; } } + methodCheck: + static $cache = []; $class = \get_class($object); @@ -1695,6 +1727,10 @@ public static function getAttribute(Environment $env, Source $source, $object, $ return false; } + if ($propertyNotAllowedError) { + throw $propertyNotAllowedError; + } + if ($ignoreStrictCheck || !$env->isStrictVariables()) { return; } @@ -1702,12 +1738,24 @@ public static function getAttribute(Environment $env, Source $source, $object, $ throw new RuntimeError(\sprintf('Neither the property "%1$s" nor one of the methods "%1$s()", "get%1$s()"/"is%1$s()"/"has%1$s()" or "__call()" exist and have public access in class "%2$s".', $item, $class), $lineno, $source); } - if ($isDefinedTest) { - return true; + if ($sandboxed) { + try { + $env->getExtension(SandboxExtension::class)->checkMethodAllowed($object, $method, $lineno, $source); + } catch (SecurityNotAllowedMethodError $e) { + if ($isDefinedTest) { + return false; + } + + if ($propertyNotAllowedError) { + throw $propertyNotAllowedError; + } + + throw $e; + } } - if ($sandboxed) { - $env->getExtension(SandboxExtension::class)->checkMethodAllowed($object, $method, $lineno, $source); + if ($isDefinedTest) { + return true; } // Some objects throw exceptions when they have __call, and the method we try diff --git a/src/Node/Expression/GetAttrExpression.php b/src/Node/Expression/GetAttrExpression.php index 29a446b881b..2181b0f7862 100644 --- a/src/Node/Expression/GetAttrExpression.php +++ b/src/Node/Expression/GetAttrExpression.php @@ -31,6 +31,7 @@ public function __construct(AbstractExpression $node, AbstractExpression $attrib public function compile(Compiler $compiler): void { $env = $compiler->getEnvironment(); + $arrayAccessSandbox = false; // optimize array calls if ( @@ -44,17 +45,35 @@ public function compile(Compiler $compiler): void ->raw('(('.$var.' = ') ->subcompile($this->getNode('node')) ->raw(') && is_array(') - ->raw($var) + ->raw($var); + + if (!$env->hasExtension(SandboxExtension::class)) { + $compiler + ->raw(') || ') + ->raw($var) + ->raw(' instanceof ArrayAccess ? (') + ->raw($var) + ->raw('[') + ->subcompile($this->getNode('attribute')) + ->raw('] ?? null) : null)') + ; + + return; + } + + $arrayAccessSandbox = true; + + $compiler ->raw(') || ') ->raw($var) - ->raw(' instanceof ArrayAccess ? (') + ->raw(' instanceof ArrayAccess && in_array(') + ->raw($var.'::class') + ->raw(', CoreExtension::ARRAY_LIKE_CLASSES, true) ? (') ->raw($var) ->raw('[') ->subcompile($this->getNode('attribute')) - ->raw('] ?? null) : null)') + ->raw('] ?? null) : ') ; - - return; } $compiler->raw('CoreExtension::getAttribute($this->env, $this->source, '); @@ -83,5 +102,9 @@ public function compile(Compiler $compiler): void ->raw(', ')->repr($this->getNode('node')->getTemplateLine()) ->raw(')') ; + + if ($arrayAccessSandbox) { + $compiler->raw(')'); + } } } diff --git a/tests/Extension/SandboxTest.php b/tests/Extension/SandboxTest.php index fe1d68a919e..cc74e8cd56e 100644 --- a/tests/Extension/SandboxTest.php +++ b/tests/Extension/SandboxTest.php @@ -39,6 +39,8 @@ protected function setUp(): void 'arr' => ['obj' => new FooObject()], 'child_obj' => new ChildClass(), 'some_array' => [5, 6, 7, new FooObject()], + 'array_like' => new ArrayLikeObject(), + 'magic' => new MagicObject(), ]; self::$templates = [ @@ -61,6 +63,7 @@ protected function setUp(): void '1_syntax_error' => '{% syntax error }}', '1_childobj_parentmethod' => '{{ child_obj.ParentMethod() }}', '1_childobj_childmethod' => '{{ child_obj.ChildMethod() }}', + '1_array_like' => '{{ array_like["foo"] }}', ]; } @@ -79,15 +82,31 @@ public function testSandboxGloballySet() $this->assertEquals('FOO', $twig->load('1_basic')->render(self::$params), 'Sandbox does nothing if it is disabled globally'); } - public function testSandboxUnallowedMethodAccessor() + public function testSandboxUnallowedPropertyAccessor() { $twig = $this->getEnvironment(true, [], self::$templates); try { - $twig->load('1_basic1')->render(self::$params); + $twig->load('1_basic1')->render(['obj' => new MagicObject()]); $this->fail('Sandbox throws a SecurityError exception if an unallowed method is called'); - } catch (SecurityNotAllowedMethodError $e) { - $this->assertEquals('Twig\Tests\Extension\FooObject', $e->getClassName(), 'Exception should be raised on the "Twig\Tests\Extension\FooObject" class'); - $this->assertEquals('foo', $e->getMethodName(), 'Exception should be raised on the "foo" method'); + } catch (SecurityNotAllowedPropertyError $e) { + $this->assertEquals('Twig\Tests\Extension\MagicObject', $e->getClassName(), 'Exception should be raised on the "Twig\Tests\Extension\MagicObject" class'); + $this->assertEquals('foo', $e->getPropertyName(), 'Exception should be raised on the "foo" property'); + } + } + + public function testSandboxUnallowedArrayIndexAccessor() + { + $twig = $this->getEnvironment(true, [], self::$templates); + + // ArrayObject and other internal array-like classes are exempted from sandbox restrictions + $this->assertSame('bar', $twig->load('1_array_like')->render(['array_like' => new \ArrayObject(['foo' => 'bar'])])); + + try { + $twig->load('1_array_like')->render(self::$params); + $this->fail('Sandbox throws a SecurityError exception if an unallowed method is called'); + } catch (SecurityNotAllowedPropertyError $e) { + $this->assertEquals('Twig\Tests\Extension\ArrayLikeObject', $e->getClassName(), 'Exception should be raised on the "Twig\Tests\Extension\ArrayLikeObject" class'); + $this->assertEquals('foo', $e->getPropertyName(), 'Exception should be raised on the "foo" property'); } } @@ -238,7 +257,8 @@ public function getSandboxAllowedToStringTests() return [ 'constant_test' => ['{{ obj is constant("PHP_INT_MAX") }}', ''], 'set_object' => ['{% set a = obj.anotherFooObject %}{{ a.foo }}', 'foo'], - 'is_defined' => ['{{ obj.anotherFooObject is defined }}', '1'], + 'is_defined1' => ['{{ obj.anotherFooObject is defined }}', '1'], + 'is_defined2' => ['{{ magic.foo is defined }}', ''], 'is_null' => ['{{ obj is null }}', ''], 'is_sameas' => ['{{ obj is same as(obj) }}', '1'], 'is_sameas_no_brackets' => ['{{ obj is same as obj }}', '1'], @@ -548,3 +568,39 @@ public function getAnotherFooObject() return new self(); } } + +class ArrayLikeObject extends \ArrayObject +{ + public function offsetExists($offset): bool + { + throw new \BadMethodCallException('Should not be called'); + } + + #[\ReturnTypeWillChange] + public function offsetGet($offset) + { + throw new \BadMethodCallException('Should not be called'); + } + + public function offsetSet($offset, $value): void + { + } + + public function offsetUnset($offset): void + { + } +} + +class MagicObject +{ + #[\ReturnTypeWillChange] + public function __get($name) + { + throw new \BadMethodCallException('Should not be called'); + } + + public function __isset($name): bool + { + throw new \BadMethodCallException('Should not be called'); + } +} From 8b5278227b18755e83533991d8647069d7b06438 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Wed, 6 Nov 2024 19:29:31 +0100 Subject: [PATCH 584/812] Update CHANGELOG --- CHANGELOG | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index 55285d681cc..6e62956410b 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,10 @@ +# 3.11.2 (2024-11-06) + + * [BC BREAK] Fix a security issue in the sandbox mode allowing an attacker to call attributes on Array-like objects + They are now checked via the property policy + * Fix a security issue in the sandbox mode allowing an attacker to be able to call `toString()` + under some circumstances on an object even if the `__toString()` method is not allowed by the security policy + # 3.11.1 (2024-09-10) * Fix a security issue when an included sandboxed template has been loaded before without the sandbox context From 94612e76021558023219f8c272d4e77a0b3c0e20 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Wed, 6 Nov 2024 19:30:53 +0100 Subject: [PATCH 585/812] Prepare the 3.11.2 release --- src/Environment.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Environment.php b/src/Environment.php index e928e63955e..84dd1d30323 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -43,11 +43,11 @@ */ class Environment { - public const VERSION = '3.11.1'; - public const VERSION_ID = 301101; + public const VERSION = '3.11.2'; + public const VERSION_ID = 301102; public const MAJOR_VERSION = 4; public const MINOR_VERSION = 11; - public const RELEASE_VERSION = 1; + public const RELEASE_VERSION = 2; public const EXTRA_VERSION = ''; private $charset; From 5b580ec1882b54c98cbd8c0f8a3ca5d1904db6b1 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Wed, 6 Nov 2024 19:39:38 +0100 Subject: [PATCH 586/812] Fix code --- src/Extension/CoreExtension.php | 2 +- src/Node/Expression/GetAttrExpression.php | 2 +- tests/Extension/CoreTest.php | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index 1e769c6a2b6..b077e796304 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -1571,7 +1571,7 @@ public static function getAttribute(Environment $env, Source $source, $object, $ if (/* Template::METHOD_CALL */ 'method' !== $type) { $arrayItem = \is_bool($item) || \is_float($item) ? (int) $item : $item; - if ($sandboxed && $object instanceof \ArrayAccess && !\in_array($object::class, self::ARRAY_LIKE_CLASSES, true)) { + if ($sandboxed && $object instanceof \ArrayAccess && !\in_array(get_class($object), self::ARRAY_LIKE_CLASSES, true)) { try { $env->getExtension(SandboxExtension::class)->checkPropertyAllowed($object, $arrayItem, $lineno, $source); } catch (SecurityNotAllowedPropertyError $propertyNotAllowedError) { diff --git a/src/Node/Expression/GetAttrExpression.php b/src/Node/Expression/GetAttrExpression.php index 2181b0f7862..f54f2f09d54 100644 --- a/src/Node/Expression/GetAttrExpression.php +++ b/src/Node/Expression/GetAttrExpression.php @@ -67,7 +67,7 @@ public function compile(Compiler $compiler): void ->raw(') || ') ->raw($var) ->raw(' instanceof ArrayAccess && in_array(') - ->raw($var.'::class') + ->raw('get_class('.$var.')') ->raw(', CoreExtension::ARRAY_LIKE_CLASSES, true) ? (') ->raw($var) ->raw('[') diff --git a/tests/Extension/CoreTest.php b/tests/Extension/CoreTest.php index 5b8268492a2..0c397b6ec88 100644 --- a/tests/Extension/CoreTest.php +++ b/tests/Extension/CoreTest.php @@ -325,7 +325,7 @@ public function testSandboxedInclude() 'index' => '{{ include("included", sandboxed=true) }}', 'included' => '{{ "included"|e }}', ])); - $policy = new SecurityPolicy(allowedFunctions: ['include']); + $policy = new SecurityPolicy([], [], [], [], ['include']); $sandbox = new SandboxExtension($policy, false); $twig->addExtension($sandbox); @@ -340,7 +340,7 @@ public function testSandboxedIncludeWithPreloadedTemplate() 'index' => '{{ include("included", sandboxed=true) }}', 'included' => '{{ "included"|e }}', ])); - $policy = new SecurityPolicy(allowedFunctions: ['include']); + $policy = new SecurityPolicy([], [], [], [], ['include']); $sandbox = new SandboxExtension($policy, false); $twig->addExtension($sandbox); From d4a302681bca9f7c6ce2835470d53609cdf3e23e Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Fri, 25 Oct 2024 11:04:18 +0200 Subject: [PATCH 587/812] Fix sandbox handling for __toString() --- src/Extension/SandboxExtension.php | 8 ++++++++ src/NodeVisitor/SandboxNodeVisitor.php | 15 ++++++++++++++- tests/Extension/SandboxTest.php | 15 +++++++++++++-- 3 files changed, 35 insertions(+), 3 deletions(-) diff --git a/src/Extension/SandboxExtension.php b/src/Extension/SandboxExtension.php index 4e96760f7d4..c9ffe6477bd 100644 --- a/src/Extension/SandboxExtension.php +++ b/src/Extension/SandboxExtension.php @@ -119,6 +119,14 @@ public function checkPropertyAllowed($obj, $property, int $lineno = -1, ?Source public function ensureToStringAllowed($obj, int $lineno = -1, ?Source $source = null) { + if (\is_array($obj)) { + foreach ($obj as $v) { + $this->ensureToStringAllowed($v, $lineno, $source); + } + + return $obj; + } + if ($this->isSandboxed($source) && $obj instanceof \Stringable) { try { $this->policy->checkMethodAllowed($obj, '__toString'); diff --git a/src/NodeVisitor/SandboxNodeVisitor.php b/src/NodeVisitor/SandboxNodeVisitor.php index ab51d33d4a0..74b686f6e94 100644 --- a/src/NodeVisitor/SandboxNodeVisitor.php +++ b/src/NodeVisitor/SandboxNodeVisitor.php @@ -15,12 +15,14 @@ use Twig\Node\CheckSecurityCallNode; use Twig\Node\CheckSecurityNode; use Twig\Node\CheckToStringNode; +use Twig\Node\Expression\ArrayExpression; use Twig\Node\Expression\Binary\ConcatBinary; use Twig\Node\Expression\Binary\RangeBinary; use Twig\Node\Expression\FilterExpression; use Twig\Node\Expression\FunctionExpression; use Twig\Node\Expression\GetAttrExpression; use Twig\Node\Expression\NameExpression; +use Twig\Node\Expression\Unary\SpreadUnary; use Twig\Node\ModuleNode; use Twig\Node\Node; use Twig\Node\Nodes; @@ -121,7 +123,18 @@ private function wrapNode(Node $node, string $name): void { $expr = $node->getNode($name); if (($expr instanceof NameExpression || $expr instanceof GetAttrExpression) && !$expr->isGenerator()) { - $node->setNode($name, new CheckToStringNode($expr)); + // 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); + } elseif ($expr instanceof SpreadUnary) { + $this->wrapNode($expr, 'node'); + } elseif ($expr instanceof ArrayExpression) { + foreach ($expr as $name => $_) { + $this->wrapNode($expr, $name); + } } } diff --git a/tests/Extension/SandboxTest.php b/tests/Extension/SandboxTest.php index e9115fa498e..479a05df4fd 100644 --- a/tests/Extension/SandboxTest.php +++ b/tests/Extension/SandboxTest.php @@ -42,6 +42,7 @@ protected function setUp(): void 'obj' => new FooObject(), 'arr' => ['obj' => new FooObject()], 'child_obj' => new ChildClass(), + 'some_array' => [5, 6, 7, new FooObject()], ]; self::$templates = [ @@ -261,10 +262,10 @@ public function testSandboxUnallowedProperty() */ public function testSandboxUnallowedToString($template) { - $twig = $this->getEnvironment(true, [], ['index' => $template], [], ['upper'], ['Twig\Tests\Extension\FooObject' => 'getAnotherFooObject'], [], ['random']); + $twig = $this->getEnvironment(true, [], ['index' => $template], [], ['upper', 'join', 'replace'], ['Twig\Tests\Extension\FooObject' => 'getAnotherFooObject'], [], ['random']); try { $twig->load('index')->render(self::$params); - $this->fail('Sandbox throws a SecurityError exception if an unallowed method (__toString()) is called in the template'); + $this->fail('Sandbox throws a SecurityError exception if an unallowed method "__toString()" method is called in the template'); } catch (SecurityNotAllowedMethodError $e) { $this->assertEquals('Twig\Tests\Extension\FooObject', $e->getClassName(), 'Exception should be raised on the "Twig\Tests\Extension\FooObject" class'); $this->assertEquals('__tostring', $e->getMethodName(), 'Exception should be raised on the "__toString" method'); @@ -287,6 +288,16 @@ public static function getSandboxUnallowedToStringTests() 'object_chain_and_function' => ['{{ random(obj.anotherFooObject) }}'], 'concat' => ['{{ obj ~ "" }}'], 'concat_again' => ['{{ "" ~ obj }}'], + 'object_in_arguments' => ['{{ "__toString"|replace({"__toString": obj}) }}'], + 'object_in_array' => ['{{ [12, "foo", obj]|join(", ") }}'], + 'object_in_array_var' => ['{{ some_array|join(", ") }}'], + 'object_in_array_nested' => ['{{ [12, "foo", [12, "foo", obj]]|join(", ") }}'], + 'object_in_array_var_nested' => ['{{ [12, "foo", some_array]|join(", ") }}'], + 'object_in_array_dynamic_key' => ['{{ {(obj): "foo"}|join(", ") }}'], + 'object_in_array_dynamic_key_nested' => ['{{ {"foo": { (obj): "foo" }}|join(", ") }}'], + 'context' => ['{{ _context|join(", ") }}'], + 'spread_array_operator' => ['{{ [1, 2, ...[5, 6, 7, obj]]|join(",") }}'], + 'spread_array_operator_var' => ['{{ [1, 2, ...some_array]|join(",") }}'], ]; } From b957e5a44cc0075d04ccff52f8fa9d8e6db3e3a0 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Tue, 29 Oct 2024 15:00:56 +0100 Subject: [PATCH 588/812] Sandbox ArrayAccess and do sandbox checks before isset() checks --- doc/sandbox.rst | 9 ++++ src/Extension/CoreExtension.php | 60 +++++++++++++++++++-- src/Node/Expression/GetAttrExpression.php | 33 ++++++++++-- tests/Extension/SandboxTest.php | 66 ++++++++++++++++++++--- 4 files changed, 153 insertions(+), 15 deletions(-) diff --git a/doc/sandbox.rst b/doc/sandbox.rst index 6f8623b5735..279d60b2ee1 100644 --- a/doc/sandbox.rst +++ b/doc/sandbox.rst @@ -37,6 +37,15 @@ able to call the ``getTitle()`` and ``getBody()`` methods on ``Article`` objects, and the ``title`` and ``body`` public properties. Everything else won't be allowed and will generate a ``\Twig\Sandbox\SecurityError`` exception. +.. note:: + + As of Twig 1.14.1 (and on Twig 3.11.2), if the ``Article`` class implements + the ``ArrayAccess`` interface, the templates will only be able to access + the ``title`` and ``body`` attributes. + + Note that native array-like classes (like ``ArrayObject``) are always + allowed, you don't need to configure them. + .. caution:: The ``extends`` and ``use`` tags are always allowed in a sandboxed diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index f8f80ac2f0c..bf41767854b 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -68,6 +68,8 @@ use Twig\Node\Node; use Twig\OperatorPrecedenceChange; use Twig\Parser; +use Twig\Sandbox\SecurityNotAllowedMethodError; +use Twig\Sandbox\SecurityNotAllowedPropertyError; use Twig\Source; use Twig\Template; use Twig\TemplateWrapper; @@ -98,6 +100,20 @@ final class CoreExtension extends AbstractExtension { private const DEFAULT_TRIM_CHARS = " \t\n\r\0\x0B"; + public const ARRAY_LIKE_CLASSES = [ + 'ArrayIterator', + 'ArrayObject', + 'CachingIterator', + 'RecursiveArrayIterator', + 'RecursiveCachingIterator', + 'SplDoublyLinkedList', + 'SplFixedArray', + 'SplObjectStorage', + 'SplQueue', + 'SplStack', + 'WeakMap', + ]; + private $dateFormats = ['F j, Y H:i', '%d days']; private $numberFormat = [0, '.', ',']; private $timezone = null; @@ -1622,10 +1638,20 @@ public static function batch($items, $size, $fill = null, $preserveKeys = true): */ public static function getAttribute(Environment $env, Source $source, $object, $item, array $arguments = [], $type = Template::ANY_CALL, $isDefinedTest = false, $ignoreStrictCheck = false, $sandboxed = false, int $lineno = -1) { + $propertyNotAllowedError = null; + // array if (Template::METHOD_CALL !== $type) { $arrayItem = \is_bool($item) || \is_float($item) ? (int) $item : $item; + if ($sandboxed && $object instanceof \ArrayAccess && !\in_array($object::class, self::ARRAY_LIKE_CLASSES, true)) { + try { + $env->getExtension(SandboxExtension::class)->checkPropertyAllowed($object, $arrayItem, $lineno, $source); + } catch (SecurityNotAllowedPropertyError $propertyNotAllowedError) { + goto methodCheck; + } + } + if (((\is_array($object) || $object instanceof \ArrayObject) && (isset($object[$arrayItem]) || \array_key_exists($arrayItem, (array) $object))) || ($object instanceof \ArrayAccess && isset($object[$arrayItem])) ) { @@ -1697,6 +1723,14 @@ public static function getAttribute(Environment $env, Source $source, $object, $ // object property if (Template::METHOD_CALL !== $type) { + if ($sandboxed) { + try { + $env->getExtension(SandboxExtension::class)->checkPropertyAllowed($object, $item, $lineno, $source); + } catch (SecurityNotAllowedPropertyError $propertyNotAllowedError) { + goto methodCheck; + } + } + if (isset($object->$item) || \array_key_exists((string) $item, (array) $object)) { if ($isDefinedTest) { return true; @@ -1722,6 +1756,8 @@ public static function getAttribute(Environment $env, Source $source, $object, $ } } + methodCheck: + static $cache = []; $class = \get_class($object); @@ -1780,6 +1816,10 @@ public static function getAttribute(Environment $env, Source $source, $object, $ return false; } + if ($propertyNotAllowedError) { + throw $propertyNotAllowedError; + } + if ($ignoreStrictCheck || !$env->isStrictVariables()) { return; } @@ -1787,12 +1827,24 @@ public static function getAttribute(Environment $env, Source $source, $object, $ throw new RuntimeError(\sprintf('Neither the property "%1$s" nor one of the methods "%1$s()", "get%1$s()"/"is%1$s()"/"has%1$s()" or "__call()" exist and have public access in class "%2$s".', $item, $class), $lineno, $source); } - if ($isDefinedTest) { - return true; + if ($sandboxed) { + try { + $env->getExtension(SandboxExtension::class)->checkMethodAllowed($object, $method, $lineno, $source); + } catch (SecurityNotAllowedMethodError $e) { + if ($isDefinedTest) { + return false; + } + + if ($propertyNotAllowedError) { + throw $propertyNotAllowedError; + } + + throw $e; + } } - if ($sandboxed) { - $env->getExtension(SandboxExtension::class)->checkMethodAllowed($object, $method, $lineno, $source); + if ($isDefinedTest) { + return true; } // Some objects throw exceptions when they have __call, and the method we try diff --git a/src/Node/Expression/GetAttrExpression.php b/src/Node/Expression/GetAttrExpression.php index 571f6aea17c..e7373de1194 100644 --- a/src/Node/Expression/GetAttrExpression.php +++ b/src/Node/Expression/GetAttrExpression.php @@ -39,6 +39,7 @@ public function __construct(AbstractExpression $node, AbstractExpression $attrib public function compile(Compiler $compiler): void { $env = $compiler->getEnvironment(); + $arrayAccessSandbox = false; // optimize array calls if ( @@ -52,17 +53,35 @@ public function compile(Compiler $compiler): void ->raw('(('.$var.' = ') ->subcompile($this->getNode('node')) ->raw(') && is_array(') - ->raw($var) + ->raw($var); + + if (!$env->hasExtension(SandboxExtension::class)) { + $compiler + ->raw(') || ') + ->raw($var) + ->raw(' instanceof ArrayAccess ? (') + ->raw($var) + ->raw('[') + ->subcompile($this->getNode('attribute')) + ->raw('] ?? null) : null)') + ; + + return; + } + + $arrayAccessSandbox = true; + + $compiler ->raw(') || ') ->raw($var) - ->raw(' instanceof ArrayAccess ? (') + ->raw(' instanceof ArrayAccess && in_array(') + ->raw($var.'::class') + ->raw(', CoreExtension::ARRAY_LIKE_CLASSES, true) ? (') ->raw($var) ->raw('[') ->subcompile($this->getNode('attribute')) - ->raw('] ?? null) : null)') + ->raw('] ?? null) : ') ; - - return; } $compiler->raw('CoreExtension::getAttribute($this->env, $this->source, '); @@ -91,5 +110,9 @@ public function compile(Compiler $compiler): void ->raw(', ')->repr($this->getNode('node')->getTemplateLine()) ->raw(')') ; + + if ($arrayAccessSandbox) { + $compiler->raw(')'); + } } } diff --git a/tests/Extension/SandboxTest.php b/tests/Extension/SandboxTest.php index 479a05df4fd..f013f7a20e1 100644 --- a/tests/Extension/SandboxTest.php +++ b/tests/Extension/SandboxTest.php @@ -43,6 +43,8 @@ protected function setUp(): void 'arr' => ['obj' => new FooObject()], 'child_obj' => new ChildClass(), 'some_array' => [5, 6, 7, new FooObject()], + 'array_like' => new ArrayLikeObject(), + 'magic' => new MagicObject(), ]; self::$templates = [ @@ -67,6 +69,7 @@ protected function setUp(): void '1_childobj_parentmethod' => '{{ child_obj.ParentMethod() }}', '1_childobj_childmethod' => '{{ child_obj.ChildMethod() }}', '1_empty' => '', + '1_array_like' => '{{ array_like["foo"] }}', ]; } @@ -141,15 +144,31 @@ public function testSandboxGloballySet() $this->assertEquals('FOO', $twig->load('1_basic')->render(self::$params), 'Sandbox does nothing if it is disabled globally'); } - public function testSandboxUnallowedMethodAccessor() + public function testSandboxUnallowedPropertyAccessor() { $twig = $this->getEnvironment(true, [], self::$templates); try { - $twig->load('1_basic1')->render(self::$params); + $twig->load('1_basic1')->render(['obj' => new MagicObject()]); $this->fail('Sandbox throws a SecurityError exception if an unallowed method is called'); - } catch (SecurityNotAllowedMethodError $e) { - $this->assertEquals('Twig\Tests\Extension\FooObject', $e->getClassName(), 'Exception should be raised on the "Twig\Tests\Extension\FooObject" class'); - $this->assertEquals('foo', $e->getMethodName(), 'Exception should be raised on the "foo" method'); + } catch (SecurityNotAllowedPropertyError $e) { + $this->assertEquals('Twig\Tests\Extension\MagicObject', $e->getClassName(), 'Exception should be raised on the "Twig\Tests\Extension\MagicObject" class'); + $this->assertEquals('foo', $e->getPropertyName(), 'Exception should be raised on the "foo" property'); + } + } + + public function testSandboxUnallowedArrayIndexAccessor() + { + $twig = $this->getEnvironment(true, [], self::$templates); + + // ArrayObject and other internal array-like classes are exempted from sandbox restrictions + $this->assertSame('bar', $twig->load('1_array_like')->render(['array_like' => new \ArrayObject(['foo' => 'bar'])])); + + try { + $twig->load('1_array_like')->render(self::$params); + $this->fail('Sandbox throws a SecurityError exception if an unallowed method is called'); + } catch (SecurityNotAllowedPropertyError $e) { + $this->assertEquals('Twig\Tests\Extension\ArrayLikeObject', $e->getClassName(), 'Exception should be raised on the "Twig\Tests\Extension\ArrayLikeObject" class'); + $this->assertEquals('foo', $e->getPropertyName(), 'Exception should be raised on the "foo" property'); } } @@ -315,7 +334,8 @@ public static function getSandboxAllowedToStringTests() return [ 'constant_test' => ['{{ obj is constant("PHP_INT_MAX") }}', ''], 'set_object' => ['{% set a = obj.anotherFooObject %}{{ a.foo }}', 'foo'], - 'is_defined' => ['{{ obj.anotherFooObject is defined }}', '1'], + 'is_defined1' => ['{{ obj.anotherFooObject is defined }}', '1'], + 'is_defined2' => ['{{ magic.foo is defined }}', ''], 'is_null' => ['{{ obj is null }}', ''], 'is_sameas' => ['{{ obj is same as(obj) }}', '1'], 'is_sameas_no_brackets' => ['{{ obj is same as obj }}', '1'], @@ -625,3 +645,37 @@ public function getAnotherFooObject() return new self(); } } + +class ArrayLikeObject extends \ArrayObject +{ + public function offsetExists($offset): bool + { + throw new \BadMethodCallException('Should not be called'); + } + + public function offsetGet($offset): mixed + { + throw new \BadMethodCallException('Should not be called'); + } + + public function offsetSet($offset, $value): void + { + } + + public function offsetUnset($offset): void + { + } +} + +class MagicObject +{ + public function __get($name): mixed + { + throw new \BadMethodCallException('Should not be called'); + } + + public function __isset($name): bool + { + throw new \BadMethodCallException('Should not be called'); + } +} From cf5d4e3bc814d47d2b3f4f799ad945dc0f8bf687 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Wed, 6 Nov 2024 18:45:40 +0100 Subject: [PATCH 589/812] Update CHANGELOG --- CHANGELOG | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index 7e6e5b153cb..5182a0e4a3d 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,9 @@ # 3.15.0 (2024-XX-XX) + * [BC BREAK] Fix a security issue in the sandbox mode allowing an attacker to call attributes on Array-like objects + They are now checked via the property policy + * Fix a security issue in the sandbox mode allowing an attacker to be able to call `toString()` + under some circumstances on an object even if the `__toString()` method is not allowed by the security policy * Add Spanish inflector support for the `plural` and `singular` filters in the String extension * Deprecate `TempNameExpression` in favor of `LocalVariable` * Deprecate `NameExpression` in favor of `ContextVariable` From 32e99c263d271c3259b70bbbf2ba76b6b4ddf455 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Wed, 6 Nov 2024 21:45:08 +0100 Subject: [PATCH 590/812] Remove obsolete code --- src/Extension/CoreExtension.php | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index bf41767854b..67a6320ef1b 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -1736,10 +1736,6 @@ public static function getAttribute(Environment $env, Source $source, $object, $ return true; } - if ($sandboxed) { - $env->getExtension(SandboxExtension::class)->checkPropertyAllowed($object, $item, $lineno, $source); - } - return isset($object->$item) ? $object->$item : ((array) $object)[(string) $item]; } From a0f775683d289dd517781d70c5962916bc4f2113 Mon Sep 17 00:00:00 2001 From: Lee Rowlands Date: Thu, 7 Nov 2024 11:49:11 +1000 Subject: [PATCH 591/812] Fix recursion when arrays contain self-references in sandboxed mode --- src/Extension/SandboxExtension.php | 11 ++++++++++- tests/Extension/SandboxTest.php | 12 ++++++++---- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/src/Extension/SandboxExtension.php b/src/Extension/SandboxExtension.php index 95b6295aa21..9603f8e9751 100644 --- a/src/Extension/SandboxExtension.php +++ b/src/Extension/SandboxExtension.php @@ -26,11 +26,14 @@ final class SandboxExtension extends AbstractExtension private $policy; private $sourcePolicy; + static array $recursionProjection = []; + public function __construct(SecurityPolicyInterface $policy, $sandboxed = false, ?SourcePolicyInterface $sourcePolicy = null) { $this->policy = $policy; $this->sandboxedGlobally = $sandboxed; $this->sourcePolicy = $sourcePolicy; + static::$recursionProjection = []; } public function getTokenParsers(): array @@ -120,10 +123,16 @@ public function checkPropertyAllowed($obj, $property, int $lineno = -1, ?Source public function ensureToStringAllowed($obj, int $lineno = -1, ?Source $source = null) { if (\is_array($obj)) { + $hash = \hash('sha256', \serialize($obj)); + if (\array_key_exists($hash, static::$recursionProjection)) { + unset(static::$recursionProjection[$hash]); + return $obj; + } + static::$recursionProjection[$hash] = TRUE; foreach ($obj as $v) { $this->ensureToStringAllowed($v, $lineno, $source); } - + unset(static::$recursionProjection[$hash]); return $obj; } diff --git a/tests/Extension/SandboxTest.php b/tests/Extension/SandboxTest.php index cc74e8cd56e..647ea29534e 100644 --- a/tests/Extension/SandboxTest.php +++ b/tests/Extension/SandboxTest.php @@ -41,7 +41,10 @@ protected function setUp(): void 'some_array' => [5, 6, 7, new FooObject()], 'array_like' => new ArrayLikeObject(), 'magic' => new MagicObject(), + 'recursion' => [4], ]; + self::$params['recursion'][] = &self::$params['recursion']; + self::$params['recursion'][] = new FooObject(); self::$templates = [ '1_basic1' => '{{ obj.foo }}', @@ -240,6 +243,7 @@ public function getSandboxUnallowedToStringTests() 'context' => ['{{ _context|join(", ") }}'], 'spread_array_operator' => ['{{ [1, 2, ...[5, 6, 7, obj]]|join(",") }}'], 'spread_array_operator_var' => ['{{ [1, 2, ...some_array]|join(",") }}'], + 'recursion' => ['{{ recursion|join(", ") }}'], ]; } @@ -573,13 +577,13 @@ class ArrayLikeObject extends \ArrayObject { public function offsetExists($offset): bool { - throw new \BadMethodCallException('Should not be called'); + throw new \BadMethodCallException('Should not be called.'); } #[\ReturnTypeWillChange] public function offsetGet($offset) { - throw new \BadMethodCallException('Should not be called'); + throw new \BadMethodCallException('Should not be called.'); } public function offsetSet($offset, $value): void @@ -596,11 +600,11 @@ class MagicObject #[\ReturnTypeWillChange] public function __get($name) { - throw new \BadMethodCallException('Should not be called'); + throw new \BadMethodCallException('Should not be called.'); } public function __isset($name): bool { - throw new \BadMethodCallException('Should not be called'); + throw new \BadMethodCallException('Should not be called.'); } } From d3fc0741713b5610782ba9b36e840257d7041887 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Thu, 7 Nov 2024 09:40:47 +0100 Subject: [PATCH 592/812] Improve detection of recursion --- doc/api.rst | 2 +- src/Extension/SandboxExtension.php | 56 +++++++++++++++++++++++------- 2 files changed, 44 insertions(+), 14 deletions(-) diff --git a/doc/api.rst b/doc/api.rst index d45af36278e..8da695037e7 100644 --- a/doc/api.rst +++ b/doc/api.rst @@ -488,7 +488,7 @@ won't be allowed and will generate a ``\Twig\Sandbox\SecurityError`` exception. .. note:: - As of Twig 1.14.1 (and on Twig 3.11.2), if the ``Article`` class implements + As of Twig 3.14.1 (and on Twig 3.11.2), if the ``Article`` class implements the ``ArrayAccess`` interface, the templates will only be able to access the ``title`` and ``body`` attributes. diff --git a/src/Extension/SandboxExtension.php b/src/Extension/SandboxExtension.php index 9603f8e9751..255f2f28dea 100644 --- a/src/Extension/SandboxExtension.php +++ b/src/Extension/SandboxExtension.php @@ -26,14 +26,11 @@ final class SandboxExtension extends AbstractExtension private $policy; private $sourcePolicy; - static array $recursionProjection = []; - public function __construct(SecurityPolicyInterface $policy, $sandboxed = false, ?SourcePolicyInterface $sourcePolicy = null) { $this->policy = $policy; $this->sandboxedGlobally = $sandboxed; $this->sourcePolicy = $sourcePolicy; - static::$recursionProjection = []; } public function getTokenParsers(): array @@ -123,16 +120,8 @@ public function checkPropertyAllowed($obj, $property, int $lineno = -1, ?Source public function ensureToStringAllowed($obj, int $lineno = -1, ?Source $source = null) { if (\is_array($obj)) { - $hash = \hash('sha256', \serialize($obj)); - if (\array_key_exists($hash, static::$recursionProjection)) { - unset(static::$recursionProjection[$hash]); - return $obj; - } - static::$recursionProjection[$hash] = TRUE; - foreach ($obj as $v) { - $this->ensureToStringAllowed($v, $lineno, $source); - } - unset(static::$recursionProjection[$hash]); + $this->ensureToStringAllowedForArray($obj, $lineno, $source); + return $obj; } @@ -149,4 +138,45 @@ public function ensureToStringAllowed($obj, int $lineno = -1, ?Source $source = return $obj; } + + private function ensureToStringAllowedForArray(array $obj, int $lineno, ?Source $source, array &$stack = []): void + { + foreach ($obj as $k => $v) { + if (!$v) { + continue; + } + + if (!\is_array($v)) { + $this->ensureToStringAllowed($v, $lineno, $source); + continue; + } + + if (\PHP_VERSION_ID < 70400) { + static $cookie; + + if ($v === $cookie ?? $cookie = new \stdClass()) { + continue; + } + + $obj[$k] = $cookie; + try { + $this->ensureToStringAllowedForArray($v, $lineno, $source, $stack); + } finally { + $obj[$k] = $v; + } + + continue; + } + + if ($r = \ReflectionReference::fromArrayElement($obj, $k)) { + if (isset($stack[$r->getId()])) { + continue; + } + + $stack[$r->getId()] = true; + } + + $this->ensureToStringAllowedForArray($v, $lineno, $source, $stack); + } + } } From 5cd1ff19777c3fb2bf122e50dda9d20b2d317af4 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Tue, 29 Oct 2024 17:41:50 +0100 Subject: [PATCH 593/812] Rely on reflection to access null properties --- src/Extension/CoreExtension.php | 51 +++++++++++++++++++++++++++++---- 1 file changed, 45 insertions(+), 6 deletions(-) diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index a193abb3dd8..766c1f9efce 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -1642,7 +1642,7 @@ public static function getAttribute(Environment $env, Source $source, $object, $ // array if (Template::METHOD_CALL !== $type) { - $arrayItem = \is_bool($item) || \is_float($item) ? (int) $item : $item; + $arrayItem = \is_bool($item) || \is_float($item) ? (int) $item : $item = (string) $item; if ($sandboxed && $object instanceof \ArrayAccess && !\in_array($object::class, self::ARRAY_LIKE_CLASSES, true)) { try { @@ -1652,9 +1652,11 @@ public static function getAttribute(Environment $env, Source $source, $object, $ } } - if (((\is_array($object) || $object instanceof \ArrayObject) && (isset($object[$arrayItem]) || \array_key_exists($arrayItem, (array) $object))) - || ($object instanceof \ArrayAccess && isset($object[$arrayItem])) - ) { + if (match (true) { + \is_array($object) => \array_key_exists($arrayItem, $object), + $object instanceof \ArrayAccess => $object->offsetExists($arrayItem), + default => false, + }) { if ($isDefinedTest) { return true; } @@ -1697,6 +1699,8 @@ public static function getAttribute(Environment $env, Source $source, $object, $ } } + $item = (string) $item; + if (!\is_object($object)) { if ($isDefinedTest) { return false; @@ -1731,12 +1735,24 @@ public static function getAttribute(Environment $env, Source $source, $object, $ } } - if (isset($object->$item) || \array_key_exists((string) $item, (array) $object)) { + static $propertyCheckers = []; + + if (isset($object->$item) + || ($propertyCheckers[$object::class][$item] ??= self::getPropertyChecker($object::class, $item))($object, $item) + ) { if ($isDefinedTest) { return true; } - return isset($object->$item) ? $object->$item : ((array) $object)[(string) $item]; + return $object->$item; + } + + if ($object instanceof \DateTimeInterface && \in_array($item, ['date', 'timezone', 'timezone_type'], true)) { + if ($isDefinedTest) { + return true; + } + + return ((array) $object)[$item]; } if (\defined($object::class.'::'.$item)) { @@ -2099,4 +2115,27 @@ public static function parseAttributeFunction(Parser $parser, Node $fakeNode, $a return new GetAttrExpression($args[0], $args[1], $args[2] ?? null, Template::ANY_CALL, $line); } + + private static function getPropertyChecker(string $class, string $property): \Closure + { + static $classReflectors = []; + + $class = $classReflectors[$class] ??= new \ReflectionClass($class); + + if (!$class->hasProperty($property)) { + static $propertyExists; + + return $propertyExists ??= \Closure::fromCallable('property_exists'); + } + + $property = $class->getProperty($property); + + if (!$property->isPublic()) { + static $false; + + return $false ??= static fn () => false; + } + + return static fn ($object) => $property->isInitialized($object); + } } From dbd734a5539c5723ba6fa7a740d24abf4ddc55fe Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Thu, 7 Nov 2024 13:34:21 +0100 Subject: [PATCH 594/812] Update CHANGELOG --- CHANGELOG | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index 6e62956410b..525e2385c69 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +# 3.11.3 (2024-11-07) + + * Fix an infinite recursion in the sandbox code + # 3.11.2 (2024-11-06) * [BC BREAK] Fix a security issue in the sandbox mode allowing an attacker to call attributes on Array-like objects From 3b06600ff3abefaf8ff55d5c336cd1c4253f8c7e Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Thu, 7 Nov 2024 13:34:41 +0100 Subject: [PATCH 595/812] Prepare the 3.11.3 release --- src/Environment.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Environment.php b/src/Environment.php index 84dd1d30323..571b4614114 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -43,11 +43,11 @@ */ class Environment { - public const VERSION = '3.11.2'; - public const VERSION_ID = 301102; + public const VERSION = '3.11.3'; + public const VERSION_ID = 301103; public const MAJOR_VERSION = 4; public const MINOR_VERSION = 11; - public const RELEASE_VERSION = 2; + public const RELEASE_VERSION = 3; public const EXTRA_VERSION = ''; private $charset; From 0b6f9d8370bb3b7f1ce5313ed8feb0fafd6e399a Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Thu, 7 Nov 2024 13:36:22 +0100 Subject: [PATCH 596/812] Prepare the 3.14.2 release --- src/Environment.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Environment.php b/src/Environment.php index 339aac63506..b6554e8e0fa 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -43,11 +43,11 @@ */ class Environment { - public const VERSION = '3.14.1'; - public const VERSION_ID = 31401; + public const VERSION = '3.14.2'; + public const VERSION_ID = 31402; public const MAJOR_VERSION = 3; public const MINOR_VERSION = 14; - public const RELEASE_VERSION = 1; + public const RELEASE_VERSION = 2; public const EXTRA_VERSION = ''; private $charset; From 151b05b2bb738715a81d634012fd4f34809d0ad1 Mon Sep 17 00:00:00 2001 From: Jacob Dreesen Date: Fri, 8 Nov 2024 19:22:16 +0100 Subject: [PATCH 597/812] Fix some typos in `html_cva` docs --- doc/functions/html_cva.rst | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/doc/functions/html_cva.rst b/doc/functions/html_cva.rst index 0c19293fb43..5f86fb5b510 100644 --- a/doc/functions/html_cva.rst +++ b/doc/functions/html_cva.rst @@ -20,7 +20,7 @@ function where you define ``base`` classes that should always be present and the {# templates/alert.html.twig #} {% set alert = html_cva( - base='alert ', + base='alert', variants={ color: { blue: 'bg-blue', @@ -45,13 +45,13 @@ Then use the ``color`` and ``size`` variants to select the needed classes: {# index.html.twig #} {{ include('alert.html.twig', {'color': 'blue', 'size': 'md'}) }} - // class="alert bg-red text-lg" + // class="alert bg-red text-md" {{ include('alert.html.twig', {'color': 'green', 'size': 'sm'}) }} // class="alert bg-green text-sm" {{ include('alert.html.twig', {'color': 'red', 'class': 'flex items-center justify-center'}) }} - // class="alert bg-red text-md flex items-center justify-center" + // class="alert bg-red flex items-center justify-center" CVA and Tailwind CSS -------------------- @@ -118,7 +118,7 @@ when multiple other variant conditions are met: // class="alert bg-green text-sm" {{ include('alert.html.twig', {color: 'red', size: 'md'}) }} - // class="alert bg-green text-lg font-bold" + // class="alert bg-green text-md font-bold" Default Variants ---------------- @@ -128,7 +128,7 @@ If no variants match, you can define a default set of classes to apply: .. code-block:: html+twig {% set alert = html_cva( - base='alert ', + base='alert', variants={ color: { blue: 'bg-blue', @@ -158,7 +158,7 @@ If no variants match, you can define a default set of classes to apply: {# index.html.twig #} {{ include('alert.html.twig', {color: 'red', size: 'lg'}) }} - // class="alert bg-red text-lg font-bold rounded-md" + // class="alert bg-red text-lg rounded-md" .. note:: From 9d9c9d81e9a80bc15e47959fd526e21b2a56a4c0 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Mon, 11 Nov 2024 09:35:00 +0100 Subject: [PATCH 598/812] Improve YieldReady support docs --- doc/deprecated.rst | 6 ++++++ src/Compiler.php | 4 ++-- src/NodeVisitor/YieldNotReadyNodeVisitor.php | 2 +- tests/EnvironmentTest.php | 6 +++--- 4 files changed, 12 insertions(+), 6 deletions(-) diff --git a/doc/deprecated.rst b/doc/deprecated.rst index fed5cb38017..63bf08f9365 100644 --- a/doc/deprecated.rst +++ b/doc/deprecated.rst @@ -183,6 +183,12 @@ Nodes Twig 3.15; use ``Twig\Node\Expression\Variable\AssignContextVariable`` instead. +* Node implementations that use ``echo`` or ``print`` should use ``yield`` + instead; all Node implementations should use the + ``#[\Twig\Attribute\YieldReady]`` attribute on their class once they've been + made ready for ``yield``; the ``use_yield`` Environment option can be turned + on when all nodes use the ``#[\Twig\Attribute\YieldReady]`` attribute. + Node Visitors ------------- diff --git a/src/Compiler.php b/src/Compiler.php index ba7b6cbaa94..3166841e380 100644 --- a/src/Compiler.php +++ b/src/Compiler.php @@ -74,7 +74,7 @@ public function compile(Node $node, int $indentation = 0) $node->compile($this); if ($this->didUseEcho) { - trigger_deprecation('twig/twig', '3.9', 'Using "%s" is deprecated, use "yield" instead in "%s", then flag the class with #[YieldReady].', $this->didUseEcho, \get_class($node)); + trigger_deprecation('twig/twig', '3.9', 'Using "%s" is deprecated, use "yield" instead in "%s", then flag the class with #[\Twig\Attribute\YieldReady].', $this->didUseEcho, \get_class($node)); } return $this; @@ -99,7 +99,7 @@ public function subcompile(Node $node, bool $raw = true) $node->compile($this); if ($this->didUseEcho) { - trigger_deprecation('twig/twig', '3.9', 'Using "%s" is deprecated, use "yield" instead in "%s", then flag the class with #[YieldReady].', $this->didUseEcho, \get_class($node)); + trigger_deprecation('twig/twig', '3.9', 'Using "%s" is deprecated, use "yield" instead in "%s", then flag the class with #[\Twig\Attribute\YieldReady].', $this->didUseEcho, \get_class($node)); } return $this; diff --git a/src/NodeVisitor/YieldNotReadyNodeVisitor.php b/src/NodeVisitor/YieldNotReadyNodeVisitor.php index 4b190b41415..2e2f8e97b2f 100644 --- a/src/NodeVisitor/YieldNotReadyNodeVisitor.php +++ b/src/NodeVisitor/YieldNotReadyNodeVisitor.php @@ -41,7 +41,7 @@ public function enterNode(Node $node, Environment $env): Node throw new \LogicException(\sprintf('You cannot enable the "use_yield" option of Twig as node "%s" is not marked as ready for it; please make it ready and then flag it with the #[YieldReady] attribute.', $class)); } - trigger_deprecation('twig/twig', '3.9', 'Twig node "%s" is not marked as ready for using "yield" instead of "echo"; please make it ready and then flag it with the #[YieldReady] attribute.', $class); + trigger_deprecation('twig/twig', '3.9', 'Twig node "%s" is not marked as ready for using "yield" instead of "echo"; please make it ready and then flag it with the #[\Twig\Attribute\YieldReady] attribute.', $class); } return $node; diff --git a/tests/EnvironmentTest.php b/tests/EnvironmentTest.php index 802f9dd3af2..f378b6b3895 100644 --- a/tests/EnvironmentTest.php +++ b/tests/EnvironmentTest.php @@ -445,11 +445,11 @@ public function testLegacyEchoingNode() if ($twig->useYield()) { $this->expectException(SyntaxError::class); - $this->expectExceptionMessage('An exception has been thrown during the compilation of a template ("You cannot enable the "use_yield" option of Twig as node "Twig\Tests\EnvironmentTest_LegacyEchoingNode" is not marked as ready for it; please make it ready and then flag it with the #[YieldReady] attribute.") in "echo_bar".'); + $this->expectExceptionMessage('An exception has been thrown during the compilation of a template ("You cannot enable the "use_yield" option of Twig as node "Twig\Tests\EnvironmentTest_LegacyEchoingNode" is not marked as ready for it; please make it ready and then flag it with the #[\Twig\Attribute\YieldReady] attribute.") in "echo_bar".'); } else { $this->expectDeprecation(<<<'EOF' -Since twig/twig 3.9: Twig node "Twig\Tests\EnvironmentTest_LegacyEchoingNode" is not marked as ready for using "yield" instead of "echo"; please make it ready and then flag it with the #[YieldReady] attribute. - Since twig/twig 3.9: Using "echo" is deprecated, use "yield" instead in "Twig\Tests\EnvironmentTest_LegacyEchoingNode", then flag the class with #[YieldReady]. +Since twig/twig 3.9: Twig node "Twig\Tests\EnvironmentTest_LegacyEchoingNode" is not marked as ready for using "yield" instead of "echo"; please make it ready and then flag it with the #[\Twig\Attribute\YieldReady] attribute. + Since twig/twig 3.9: Using "echo" is deprecated, use "yield" instead in "Twig\Tests\EnvironmentTest_LegacyEchoingNode", then flag the class with #[\Twig\Attribute\YieldReady]. EOF ); } From 728b361e9df4515f05615512877e443c487aecbd Mon Sep 17 00:00:00 2001 From: dmjohnsson23 Date: Mon, 11 Nov 2024 15:41:24 -0700 Subject: [PATCH 599/812] Clarify documentation for escape filter It was somewhat unclear from the documentation what the intended purpose of the 'js' escape strategy is. --- doc/filters/escape.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/doc/filters/escape.rst b/doc/filters/escape.rst index e9ff539b69f..91f39acb020 100644 --- a/doc/filters/escape.rst +++ b/doc/filters/escape.rst @@ -42,7 +42,9 @@ documents: * ``html``: escapes a string for the **HTML body** context, or for HTML attributes values **inside quotes**. -* ``js``: escapes a string for the **JavaScript** context. +* ``js``: escapes a string for the **JavaScript** context. This is intended for + use in JavaScript or JSON strings, and encodes values using backslash escape + sequences. * ``css``: escapes a string for the **CSS** context. CSS escaping can be applied to any string being inserted into CSS and escapes everything except From 065a45f54aec9cf0318918205e4a6a49880946f2 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sun, 17 Nov 2024 16:31:33 +0100 Subject: [PATCH 600/812] Fix CHANGELOG --- CHANGELOG | 4 ---- 1 file changed, 4 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index a0cf0d5d6fa..5af569575bb 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,9 +1,5 @@ # 3.15.0 (2024-XX-XX) - * [BC BREAK] Fix a security issue in the sandbox mode allowing an attacker to call attributes on Array-like objects - They are now checked via the property policy - * Fix a security issue in the sandbox mode allowing an attacker to be able to call `toString()` - under some circumstances on an object even if the `__toString()` method is not allowed by the security policy * Add Spanish inflector support for the `plural` and `singular` filters in the String extension * Deprecate `TempNameExpression` in favor of `LocalVariable` * Deprecate `NameExpression` in favor of `ContextVariable` From 41a7988cef89128ccabfdc830c27b0beb0022ecb Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sun, 17 Nov 2024 16:45:52 +0100 Subject: [PATCH 601/812] Simplify code --- src/Node/Expression/NameExpression.php | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/Node/Expression/NameExpression.php b/src/Node/Expression/NameExpression.php index 322fbc2e083..78ae51f0334 100644 --- a/src/Node/Expression/NameExpression.php +++ b/src/Node/Expression/NameExpression.php @@ -77,10 +77,7 @@ public function compile(Compiler $compiler): void ->string($name) ->raw(']) || array_key_exists(') ->string($name) - ->raw(', $context) ?') - ; - $compiler - ->raw(' $context[') + ->raw(', $context) ? $context[') ->string($name) ->raw('] : (function () { throw new RuntimeError(\'Variable ') ->string($name) From 2d5b3964cc21d0188633d7ddce732dc8e874db02 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sun, 17 Nov 2024 16:59:19 +0100 Subject: [PATCH 602/812] Prepare the 3.15.0 release --- src/Environment.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Environment.php b/src/Environment.php index c8862af3d9c..30d1aa49d5d 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -44,12 +44,12 @@ */ class Environment { - public const VERSION = '3.15.0-DEV'; + public const VERSION = '3.15.0'; public const VERSION_ID = 31500; public const MAJOR_VERSION = 3; public const MINOR_VERSION = 15; public const RELEASE_VERSION = 0; - public const EXTRA_VERSION = 'DEV'; + public const EXTRA_VERSION = ''; private $charset; private $loader; From 8f3f8df9cdedc8cbc66d1a75790225a7d44dac67 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sun, 17 Nov 2024 17:00:39 +0100 Subject: [PATCH 603/812] Bump version --- src/Environment.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Environment.php b/src/Environment.php index 30d1aa49d5d..9be14055fad 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -44,12 +44,12 @@ */ class Environment { - public const VERSION = '3.15.0'; - public const VERSION_ID = 31500; + public const VERSION = '3.15.1-DEV'; + public const VERSION_ID = 31501; public const MAJOR_VERSION = 3; public const MINOR_VERSION = 15; - public const RELEASE_VERSION = 0; - public const EXTRA_VERSION = ''; + public const RELEASE_VERSION = 1; + public const EXTRA_VERSION = 'DEV'; private $charset; private $loader; From fee03389c839ba97f8482bf159a49dee767b80f9 Mon Sep 17 00:00:00 2001 From: Andrey Bolonin Date: Mon, 18 Nov 2024 10:56:02 +0300 Subject: [PATCH 604/812] Update CHANGELOG --- CHANGELOG | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 5af569575bb..893f2e92dbc 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,4 +1,4 @@ -# 3.15.0 (2024-XX-XX) +# 3.15.0 (2024-11-17) * Add Spanish inflector support for the `plural` and `singular` filters in the String extension * Deprecate `TempNameExpression` in favor of `LocalVariable` From 3a438ffb9b8a016a698972bf0540b8b0c8bff46e Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Mon, 18 Nov 2024 09:23:06 +0100 Subject: [PATCH 605/812] Fix CHANGELOG --- CHANGELOG | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index 893f2e92dbc..835cabce517 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +# 3.15.1 (2024-XX-XX) + + * n/a + # 3.15.0 (2024-11-17) * Add Spanish inflector support for the `plural` and `singular` filters in the String extension From 37d99c2a59e42720d0f0820663c5dc5e59eabdee Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Tue, 19 Nov 2024 14:15:51 +0100 Subject: [PATCH 606/812] Add BC break note in CHANGELOG for 3.15 --- CHANGELOG | 3 ++- doc/deprecated.rst | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 835cabce517..7f064d741b7 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -4,6 +4,8 @@ # 3.15.0 (2024-11-17) + * [BC BREAK] Add support for accessing class constants with the dot operator; + this can be a BC break if you don't use UPPERCASE constant names * Add Spanish inflector support for the `plural` and `singular` filters in the String extension * Deprecate `TempNameExpression` in favor of `LocalVariable` * Deprecate `NameExpression` in favor of `ContextVariable` @@ -25,7 +27,6 @@ * Fix `power` expressions with a negative number in parenthesis (`(-1) ** 2`) * Deprecate instantiating `Node` directly. Use `EmptyNode` or `Nodes` instead. * Add support for inline comments - * Add support for accessing class constants with the dot operator * Add `Profile::getStartTime()` and `Profile::getEndTime()` * Fix "ignore missing" when used on an "embed" tag * Fix the possibility to override an aliased block (via use) diff --git a/doc/deprecated.rst b/doc/deprecated.rst index 63bf08f9365..b16342c9fe6 100644 --- a/doc/deprecated.rst +++ b/doc/deprecated.rst @@ -337,6 +337,9 @@ Node Operators --------- +* The ``.`` operator allows accessing class constants as of Twig 3.15. + This can be a BC break if you don't use UPPERCASE constant names. + * Using ``~`` in an expression with the ``+`` or ``-`` operators without using parentheses to clarify precedence triggers a deprecation as of Twig 3.15 (in Twig 4.0, ``+`` / ``-`` will have a higher precedence than ``~``). From 51d10ba480f418cfc628f3a53a3564273b8912d8 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Wed, 20 Nov 2024 10:18:47 +0100 Subject: [PATCH 607/812] Add missing import --- src/Extension/ExtensionInterface.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Extension/ExtensionInterface.php b/src/Extension/ExtensionInterface.php index 1b7be44c11a..717ef9cec6e 100644 --- a/src/Extension/ExtensionInterface.php +++ b/src/Extension/ExtensionInterface.php @@ -14,6 +14,7 @@ use Twig\ExpressionParser; use Twig\Node\Expression\AbstractExpression; use Twig\NodeVisitor\NodeVisitorInterface; +use Twig\OperatorPrecedenceChange; use Twig\TokenParser\TokenParserInterface; use Twig\TwigFilter; use Twig\TwigFunction; From b15ba0f963b0825d08b5e10245eecb49b2c79ae1 Mon Sep 17 00:00:00 2001 From: Christophe Coevoet Date: Wed, 20 Nov 2024 13:36:08 +0100 Subject: [PATCH 608/812] Fix the exception message to match the expected one for not ready nodes --- src/NodeVisitor/YieldNotReadyNodeVisitor.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NodeVisitor/YieldNotReadyNodeVisitor.php b/src/NodeVisitor/YieldNotReadyNodeVisitor.php index 2e2f8e97b2f..3c978627556 100644 --- a/src/NodeVisitor/YieldNotReadyNodeVisitor.php +++ b/src/NodeVisitor/YieldNotReadyNodeVisitor.php @@ -38,7 +38,7 @@ public function enterNode(Node $node, Environment $env): Node if (!$this->yieldReadyNodes[$class] = (bool) (new \ReflectionClass($class))->getAttributes(YieldReady::class)) { if ($this->useYield) { - throw new \LogicException(\sprintf('You cannot enable the "use_yield" option of Twig as node "%s" is not marked as ready for it; please make it ready and then flag it with the #[YieldReady] attribute.', $class)); + throw new \LogicException(\sprintf('You cannot enable the "use_yield" option of Twig as node "%s" is not marked as ready for it; please make it ready and then flag it with the #[\Twig\Attribute\YieldReady] attribute.', $class)); } trigger_deprecation('twig/twig', '3.9', 'Twig node "%s" is not marked as ready for using "yield" instead of "echo"; please make it ready and then flag it with the #[\Twig\Attribute\YieldReady] attribute.', $class); From abc34bd26337e42491addc6938243fcce82dee02 Mon Sep 17 00:00:00 2001 From: Christophe Coevoet Date: Wed, 20 Nov 2024 14:10:15 +0100 Subject: [PATCH 609/812] Fix the string-extra tests when running with older symfony/string --- extra/string-extra/Tests/Fixtures/plural.test | 4 ---- extra/string-extra/Tests/Fixtures/plural_es.test | 13 +++++++++++++ extra/string-extra/Tests/Fixtures/singular.test | 4 ---- extra/string-extra/Tests/Fixtures/singular_es.test | 13 +++++++++++++ 4 files changed, 26 insertions(+), 8 deletions(-) create mode 100755 extra/string-extra/Tests/Fixtures/plural_es.test create mode 100755 extra/string-extra/Tests/Fixtures/singular_es.test diff --git a/extra/string-extra/Tests/Fixtures/plural.test b/extra/string-extra/Tests/Fixtures/plural.test index d5dc3b0b19e..b561e2ad0d6 100755 --- a/extra/string-extra/Tests/Fixtures/plural.test +++ b/extra/string-extra/Tests/Fixtures/plural.test @@ -5,8 +5,6 @@ {{ 'partition'|plural('fr', all=true)|join(',') }} {{ 'person'|plural('fr') }} {{ 'person'|plural('en', all=true)|join(',') }} -{{ 'avión'|plural('es') }} -{{ 'avión'|plural('es', all=true)|join(',') }} --DATA-- return [] @@ -15,5 +13,3 @@ partitions partitions persons persons,people -aviones -aviones diff --git a/extra/string-extra/Tests/Fixtures/plural_es.test b/extra/string-extra/Tests/Fixtures/plural_es.test new file mode 100755 index 00000000000..55f82191f75 --- /dev/null +++ b/extra/string-extra/Tests/Fixtures/plural_es.test @@ -0,0 +1,13 @@ +--TEST-- +"plural" filter +--CONDITION-- +class_exists('Symfony\Component\String\Inflector\SpanishInflector') +--TEMPLATE-- +{{ 'avión'|plural('es') }} +{{ 'avión'|plural('es', all=true)|join(',') }} + +--DATA-- +return [] +--EXPECT-- +aviones +aviones diff --git a/extra/string-extra/Tests/Fixtures/singular.test b/extra/string-extra/Tests/Fixtures/singular.test index f500636a018..01e03db66a6 100755 --- a/extra/string-extra/Tests/Fixtures/singular.test +++ b/extra/string-extra/Tests/Fixtures/singular.test @@ -7,8 +7,6 @@ {{ 'persons'|singular('en', all=true)|join(',') }} {{ 'people'|singular('en') }} {{ 'people'|singular('en', all=true)|join(',') }} -{{ 'personas'|singular('es') }} -{{ 'personas'|singular('es', all=true)|join(',') }} --DATA-- return [] @@ -19,5 +17,3 @@ person person person person -persona -persona diff --git a/extra/string-extra/Tests/Fixtures/singular_es.test b/extra/string-extra/Tests/Fixtures/singular_es.test new file mode 100755 index 00000000000..9bc42a343a0 --- /dev/null +++ b/extra/string-extra/Tests/Fixtures/singular_es.test @@ -0,0 +1,13 @@ +--TEST-- +"singular" filter +--CONDITION-- +class_exists('Symfony\Component\String\Inflector\SpanishInflector') +--TEMPLATE-- +{{ 'personas'|singular('es') }} +{{ 'personas'|singular('es', all=true)|join(',') }} + +--DATA-- +return [] +--EXPECT-- +persona +persona From d69c66643fba9556482e92d073a2c97803a56c4f Mon Sep 17 00:00:00 2001 From: Christophe Coevoet Date: Wed, 20 Nov 2024 14:19:52 +0100 Subject: [PATCH 610/812] Fix the intl-extra tests symfony/intl has updated its data from ICU 75.1 to ICU 76.1, which includes new currency and scripts. --- extra/intl-extra/Tests/Fixtures/currency_names.test | 4 ++-- extra/intl-extra/Tests/Fixtures/script_names.test | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/extra/intl-extra/Tests/Fixtures/currency_names.test b/extra/intl-extra/Tests/Fixtures/currency_names.test index 7834d721a2f..dc3e9d819ff 100644 --- a/extra/intl-extra/Tests/Fixtures/currency_names.test +++ b/extra/intl-extra/Tests/Fixtures/currency_names.test @@ -10,7 +10,7 @@ return []; --EXPECT-- 0 -293 -293 +294 +294 US Dollar dollar des États-Unis diff --git a/extra/intl-extra/Tests/Fixtures/script_names.test b/extra/intl-extra/Tests/Fixtures/script_names.test index c65daf55071..76d7ddb1735 100644 --- a/extra/intl-extra/Tests/Fixtures/script_names.test +++ b/extra/intl-extra/Tests/Fixtures/script_names.test @@ -10,7 +10,7 @@ return []; --EXPECT-- 0 -201 -201 +208 +208 Marchen Marchen From 75d48db822df8e285a05d07556be68ea4536b5d8 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Wed, 20 Nov 2024 13:23:43 +0100 Subject: [PATCH 611/812] Add phpstan analysis --- .github/workflows/ci.yml | 26 +++++++++++++++++++ composer.json | 3 ++- phpstan-baseline.neon | 25 ++++++++++++++++++ phpstan.neon.dist | 9 +++++++ src/ExpressionParser.php | 8 +++--- src/ExtensionSet.php | 2 +- .../Expression/BlockReferenceExpression.php | 2 +- .../Variable/AssignTemplateVariable.php | 7 +++-- src/Node/ImportNode.php | 5 +--- src/Node/MacroNode.php | 6 +++-- src/OperatorPrecedenceChange.php | 2 +- 11 files changed, 79 insertions(+), 16 deletions(-) create mode 100644 phpstan-baseline.neon create mode 100644 phpstan.neon.dist diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3b3472cf321..735a09d59e3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -154,3 +154,29 @@ jobs: - run: bash ./tests/drupal_test.sh shell: "bash" + + phpstan: + name: "PHPStan" + + runs-on: 'ubuntu-latest' + + strategy: + matrix: + php-version: + - '8.4' + + steps: + - name: "Checkout code" + uses: actions/checkout@v4 + + - name: "Install PHP with extensions" + uses: shivammathur/setup-php@v2 + with: + coverage: "none" + php-version: ${{ matrix.php-version }} + ini-values: memory_limit=-1 + + - run: composer install + + - name: "Run tests" + run: vendor/bin/phpstan diff --git a/composer.json b/composer.json index e0c3e6c6cc1..079f1daf3b7 100644 --- a/composer.json +++ b/composer.json @@ -32,7 +32,8 @@ }, "require-dev": { "symfony/phpunit-bridge": "^5.4.9|^6.4|^7.0", - "psr/container": "^1.0|^2.0" + "psr/container": "^1.0|^2.0", + "phpstan/phpstan": "^2.0" }, "autoload": { "files": [ diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon new file mode 100644 index 00000000000..1121ae1b235 --- /dev/null +++ b/phpstan-baseline.neon @@ -0,0 +1,25 @@ +parameters: + ignoreErrors: + - # The method is dynamically generated by the CheckSecurityNode + message: '#^Call to an undefined method Twig\\Template\:\:checkSecurity\(\)\.$#' + identifier: method.notFound + count: 1 + path: src/Extension/CoreExtension.php + + - # Avoid BC-break + message: '#^Constructor of class Twig\\Node\\ForNode has an unused parameter \$ifexpr\.$#' + identifier: constructor.unusedParameter + count: 1 + path: src/Node/ForNode.php + + - # 2 parameters will be required + message: '#^Method Twig\\Node\\IncludeNode\:\:addGetTemplate\(\) invoked with 2 parameters, 1 required\.$#' + identifier: arguments.count + count: 1 + path: src/Node/IncludeNode.php + + - # int|string will be supported in 4.x + message: '#^PHPDoc tag @param for parameter $name with type int|string is not subtype of native type string\.$#' + identifier: parameter.phpDocType + count: 5 + path: src/Node/Node.php diff --git a/phpstan.neon.dist b/phpstan.neon.dist new file mode 100644 index 00000000000..6d94e410929 --- /dev/null +++ b/phpstan.neon.dist @@ -0,0 +1,9 @@ +includes: + - phpstan-baseline.neon + +parameters: + level: 3 + paths: + - src + excludePaths: + - src/Test diff --git a/src/ExpressionParser.php b/src/ExpressionParser.php index b043f3e3422..2fb528518ba 100644 --- a/src/ExpressionParser.php +++ b/src/ExpressionParser.php @@ -53,9 +53,9 @@ class ExpressionParser public const OPERATOR_LEFT = 1; public const OPERATOR_RIGHT = 2; - /** @var array}> */ + /** @var array}> */ private $unaryOperators; - /** @var array, associativity: self::OPERATOR_*}> */ + /** @var array, associativity: self::OPERATOR_*}> */ private $binaryOperators; private $readyNodes = []; private array $precedenceChanges = []; @@ -125,7 +125,7 @@ public function parseExpression($precedence = 0) $expr->setAttribute('operator', 'binary_'.$token->getValue()); - $this->triggerPrecedenceDeprecations($expr, $token); + $this->triggerPrecedenceDeprecations($expr); $token = $this->parser->getCurrentToken(); } @@ -246,7 +246,7 @@ private function getPrimary(): AbstractExpression $expr->setAttribute('operator', 'unary_'.$token->getValue()); if ($this->deprecationCheck) { - $this->triggerPrecedenceDeprecations($expr, $token); + $this->triggerPrecedenceDeprecations($expr); } return $this->parsePostfixExpression($expr); diff --git a/src/ExtensionSet.php b/src/ExtensionSet.php index 8466cb955bf..99fcfe56b75 100644 --- a/src/ExtensionSet.php +++ b/src/ExtensionSet.php @@ -48,7 +48,7 @@ final class ExtensionSet private $unaryOperators; /** @var array, associativity: ExpressionParser::OPERATOR_*}> */ private $binaryOperators; - /** @var array */ + /** @var array|null */ private $globals; private $functionCallbacks = []; private $filterCallbacks = []; diff --git a/src/Node/Expression/BlockReferenceExpression.php b/src/Node/Expression/BlockReferenceExpression.php index ed88c6094d6..0094c7adbf9 100644 --- a/src/Node/Expression/BlockReferenceExpression.php +++ b/src/Node/Expression/BlockReferenceExpression.php @@ -28,7 +28,7 @@ class BlockReferenceExpression extends AbstractExpression public function __construct(Node $name, ?Node $template, int $lineno) { if (!$name instanceof AbstractExpression) { - trigger_deprecation('twig/twig', '3.15', 'Not passing a "%s" instance to the "node" argument of "%s" is deprecated ("%s" given).', AbstractExpression::class, static::class, get_class($node)); + trigger_deprecation('twig/twig', '3.15', 'Not passing a "%s" instance to the "node" argument of "%s" is deprecated ("%s" given).', AbstractExpression::class, static::class, get_class($name)); } $nodes = ['name' => $name]; diff --git a/src/Node/Expression/Variable/AssignTemplateVariable.php b/src/Node/Expression/Variable/AssignTemplateVariable.php index aa9ed11919f..98bcdc10e46 100644 --- a/src/Node/Expression/Variable/AssignTemplateVariable.php +++ b/src/Node/Expression/Variable/AssignTemplateVariable.php @@ -23,17 +23,20 @@ public function __construct(TemplateVariable $var, bool $global = true) public function compile(Compiler $compiler): void { + /** @var TemplateVariable $var */ + $var = $this->nodes['var']; + $compiler ->addDebugInfo($this) ->write('$macros[') - ->string($this->nodes['var']->getName($compiler)) + ->string($var->getName($compiler)) ->raw('] = ') ; if ($this->getAttribute('global')) { $compiler ->raw('$this->macros[') - ->string($this->nodes['var']->getName($compiler)) + ->string($var->getName($compiler)) ->raw('] = ') ; } diff --git a/src/Node/ImportNode.php b/src/Node/ImportNode.php index ab9ca746934..124c41ba9ca 100644 --- a/src/Node/ImportNode.php +++ b/src/Node/ImportNode.php @@ -25,12 +25,9 @@ #[YieldReady] class ImportNode extends Node { - /** - * @param bool $global - */ public function __construct(AbstractExpression $expr, AbstractExpression|AssignTemplateVariable $var, int $lineno) { - if (!\is_bool(\func_num_args() > 3)) { + if (\func_num_args() > 3) { trigger_deprecation('twig/twig', '3.15', \sprintf('Passing more than 3 arguments to "%s()" is deprecated.', __METHOD__)); } diff --git a/src/Node/MacroNode.php b/src/Node/MacroNode.php index d96e98cf24e..fb2431cc42d 100644 --- a/src/Node/MacroNode.php +++ b/src/Node/MacroNode.php @@ -64,7 +64,9 @@ public function compile(Compiler $compiler): void ->write(\sprintf('public function macro_%s(', $this->getAttribute('name'))) ; - foreach ($this->getNode('arguments')->getKeyValuePairs() as $pair) { + /** @var ArrayExpression $arguments */ + $arguments = $this->getNode('arguments'); + foreach ($arguments->getKeyValuePairs() as $pair) { $name = $pair['key']; $default = $pair['value']; $compiler @@ -85,7 +87,7 @@ public function compile(Compiler $compiler): void ->indent() ; - foreach ($this->getNode('arguments')->getKeyValuePairs() as $pair) { + foreach ($arguments->getKeyValuePairs() as $pair) { $name = $pair['key']; $compiler ->write('') diff --git a/src/OperatorPrecedenceChange.php b/src/OperatorPrecedenceChange.php index 12fd98c8d24..1d9edefd11c 100644 --- a/src/OperatorPrecedenceChange.php +++ b/src/OperatorPrecedenceChange.php @@ -35,7 +35,7 @@ public function getVersion(): string return $this->version; } - public function getNewPrecedence(): string + public function getNewPrecedence(): int { return $this->newPrecedence; } From ff55f590e70607eca7aa53149a9b97ab5ce59129 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Thu, 21 Nov 2024 08:46:26 +0100 Subject: [PATCH 612/812] Forbid adding Nodes to EmptyNode --- src/Node/EmptyNode.php | 5 +++++ src/Node/ModuleNode.php | 10 +++++----- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/Node/EmptyNode.php b/src/Node/EmptyNode.php index 95ee5408e4b..fd4717ff4ba 100644 --- a/src/Node/EmptyNode.php +++ b/src/Node/EmptyNode.php @@ -25,4 +25,9 @@ public function __construct(int $lineno = 0) { parent::__construct([], [], $lineno); } + + public function setNode(string $name, Node $node): void + { + throw new \LogicException('EmptyNode cannot have children.'); + } } diff --git a/src/Node/ModuleNode.php b/src/Node/ModuleNode.php index ddb3f734622..97c2089fdba 100644 --- a/src/Node/ModuleNode.php +++ b/src/Node/ModuleNode.php @@ -44,11 +44,11 @@ public function __construct(Node $body, ?AbstractExpression $parent, Node $block 'blocks' => $blocks, 'macros' => $macros, 'traits' => $traits, - 'display_start' => new EmptyNode(), - 'display_end' => new EmptyNode(), - 'constructor_start' => new EmptyNode(), - 'constructor_end' => new EmptyNode(), - 'class_end' => new EmptyNode(), + 'display_start' => new Nodes(), + 'display_end' => new Nodes(), + 'constructor_start' => new Nodes(), + 'constructor_end' => new Nodes(), + 'class_end' => new Nodes(), ]; if (null !== $parent) { $nodes['parent'] = $parent; From 7556fe1c6f6bb09eb03b5078d6f849634dc1c8c5 Mon Sep 17 00:00:00 2001 From: Ruud Kamphuis Date: Wed, 20 Nov 2024 09:46:14 +0100 Subject: [PATCH 613/812] Simplify enum usage in docs --- doc/functions/enum.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/functions/enum.rst b/doc/functions/enum.rst index 0c1b5b200aa..66e2c9dba46 100644 --- a/doc/functions/enum.rst +++ b/doc/functions/enum.rst @@ -13,7 +13,7 @@ {{ enum('App\\MyEnum').SomeCase.value }} {# get all cases of an enum #} - {% enum('App\\MyEnum').cases() %} + {% enum('App\\MyEnum').cases %} {# call any methods of the enum class #} {% enum('App\\MyEnum').someMethod() %} From fb43e7a8544db4cff5ccf9ab0c9790681e366760 Mon Sep 17 00:00:00 2001 From: Ruud Kamphuis Date: Sat, 2 Nov 2024 14:05:46 +0100 Subject: [PATCH 614/812] Specify allow round methods This way, PHPStan can validate invalid input. --- src/Extension/CoreExtension.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index fc25b8bbf5e..429763ad143 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -602,11 +602,11 @@ public static function replace($str, $from): string /** * Rounds a number. * - * @param int|float|string|null $value The value to round - * @param int|float $precision The rounding precision - * @param string $method The method to use for rounding + * @param int|float|string|null $value The value to round + * @param int|float $precision The rounding precision + * @param 'common'|'ceil'|'floor' $method The method to use for rounding * - * @return int|float The rounded number + * @return float The rounded number * * @internal */ From 74aff6bb3827e970fc3b792cc4e338b6528823bd Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sat, 23 Nov 2024 09:08:39 +0100 Subject: [PATCH 615/812] Bump version --- CHANGELOG | 2 +- src/Environment.php | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 7f064d741b7..4fd66a68605 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,4 +1,4 @@ -# 3.15.1 (2024-XX-XX) +# 3.16.0 (2024-XX-XX) * n/a diff --git a/src/Environment.php b/src/Environment.php index 9be14055fad..2b308fc51a4 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -44,11 +44,11 @@ */ class Environment { - public const VERSION = '3.15.1-DEV'; - public const VERSION_ID = 31501; + public const VERSION = '3.16.0-DEV'; + public const VERSION_ID = 31600; public const MAJOR_VERSION = 3; - public const MINOR_VERSION = 15; - public const RELEASE_VERSION = 1; + public const MINOR_VERSION = 16; + public const RELEASE_VERSION = 0; public const EXTRA_VERSION = 'DEV'; private $charset; From e39a7af56ee7a04619c028cd29f53aa82d3a826b Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sat, 23 Nov 2024 10:51:09 +0100 Subject: [PATCH 616/812] Simplify code --- src/NodeVisitor/EscaperNodeVisitor.php | 19 +++---------------- 1 file changed, 3 insertions(+), 16 deletions(-) diff --git a/src/NodeVisitor/EscaperNodeVisitor.php b/src/NodeVisitor/EscaperNodeVisitor.php index 5334d0c0486..c3eefe2135b 100644 --- a/src/NodeVisitor/EscaperNodeVisitor.php +++ b/src/NodeVisitor/EscaperNodeVisitor.php @@ -77,7 +77,7 @@ public function leaveNode(Node $node, Environment $env): ?Node return $this->preEscapeFilterNode($node, $env); } elseif ($node instanceof PrintNode && false !== $type = $this->needEscaping()) { $expression = $node->getNode('expr'); - if ($expression instanceof ConditionalExpression && $this->shouldUnwrapConditional($expression, $env, $type)) { + if ($expression instanceof ConditionalExpression) { return new DoNode($this->unwrapConditional($expression, $env, $type), $expression->getTemplateLine()); } @@ -93,32 +93,19 @@ public function leaveNode(Node $node, Environment $env): ?Node return $node; } - private function shouldUnwrapConditional(ConditionalExpression $expression, Environment $env, string $type): bool - { - /** @var AbstractExpression $expr2 */ - $expr2 = $expression->getNode('expr2'); - /** @var AbstractExpression $expr3 */ - $expr3 = $expression->getNode('expr3'); - - $expr2Safe = $this->isSafeFor($type, $expr2, $env); - $expr3Safe = $this->isSafeFor($type, $expr3, $env); - - return $expr2Safe !== $expr3Safe; - } - private function unwrapConditional(ConditionalExpression $expression, Environment $env, string $type): ConditionalExpression { // convert "echo a ? b : c" to "a ? echo b : echo c" recursively /** @var AbstractExpression $expr2 */ $expr2 = $expression->getNode('expr2'); - if ($expr2 instanceof ConditionalExpression && $this->shouldUnwrapConditional($expr2, $env, $type)) { + if ($expr2 instanceof ConditionalExpression) { $expr2 = $this->unwrapConditional($expr2, $env, $type); } else { $expr2 = $this->escapeInlinePrintNode(new InlinePrint($expr2, $expr2->getTemplateLine()), $env, $type); } /** @var AbstractExpression $expr3 */ $expr3 = $expression->getNode('expr3'); - if ($expr3 instanceof ConditionalExpression && $this->shouldUnwrapConditional($expr3, $env, $type)) { + if ($expr3 instanceof ConditionalExpression) { $expr3 = $this->unwrapConditional($expr3, $env, $type); } else { $expr3 = $this->escapeInlinePrintNode(new InlinePrint($expr3, $expr3->getTemplateLine()), $env, $type); From e7e72ff090680eb075ddf9ddc4d4244f1766771c Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sat, 23 Nov 2024 14:58:29 +0100 Subject: [PATCH 617/812] Deprecate not passing a Source to TokenStream --- CHANGELOG | 2 +- doc/deprecated.rst | 6 ++++++ src/TokenStream.php | 11 +++++------ tests/ParserTest.php | 10 +++++----- tests/TokenStreamTest.php | 7 ++++--- 5 files changed, 21 insertions(+), 15 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 4fd66a68605..f9c68374e90 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,6 @@ # 3.16.0 (2024-XX-XX) - * n/a + * Deprecate not passing a `Source` instance to `TokenStream` # 3.15.0 (2024-11-17) diff --git a/doc/deprecated.rst b/doc/deprecated.rst index b16342c9fe6..99d918dabb6 100644 --- a/doc/deprecated.rst +++ b/doc/deprecated.rst @@ -216,6 +216,12 @@ Parser * Passing ``null`` to ``Twig\Parser::setParent()`` is deprecated as of Twig 3.12. +Lexer +----- + +* Not passing a ``Source`` instance to ``Twig\TokenStream`` constructor is + deprecated as of Twig 3.16. + Templates --------- diff --git a/src/TokenStream.php b/src/TokenStream.php index c91701bfe12..35aa9714fc7 100644 --- a/src/TokenStream.php +++ b/src/TokenStream.php @@ -27,7 +27,11 @@ public function __construct( private array $tokens, private ?Source $source = null, ) { - $this->source = $source ?: new Source('', ''); + if (null === $this->source) { + trigger_deprecation('twig/twig', '3.16', \sprintf('Not passing a "%s" object to "%s" constructor is deprecated.', Source::class, __CLASS__)); + + $this->source = new Source('', ''); + } } public function __toString() @@ -117,11 +121,6 @@ public function getCurrent(): Token return $this->tokens[$this->current]; } - /** - * Gets the source associated with this stream. - * - * @internal - */ public function getSourceContext(): Source { return $this->source; diff --git a/tests/ParserTest.php b/tests/ParserTest.php index d1d23bb1b48..1b222d0de40 100644 --- a/tests/ParserTest.php +++ b/tests/ParserTest.php @@ -36,7 +36,7 @@ public function testUnknownTag() new Token(Token::NAME_TYPE, 'foo', 1), new Token(Token::BLOCK_END_TYPE, '', 1), new Token(Token::EOF_TYPE, '', 1), - ]); + ], new Source('', '')); $parser = new Parser(new Environment(new ArrayLoader())); $this->expectException(SyntaxError::class); @@ -52,7 +52,7 @@ public function testUnknownTagWithoutSuggestions() new Token(Token::NAME_TYPE, 'foobar', 1), new Token(Token::BLOCK_END_TYPE, '', 1), new Token(Token::EOF_TYPE, '', 1), - ]); + ], new Source('', '')); $parser = new Parser(new Environment(new ArrayLoader())); $this->expectException(SyntaxError::class); @@ -153,7 +153,7 @@ public function testParseIsReentrant() new Token(Token::NAME_TYPE, 'foo', 1), new Token(Token::VAR_END_TYPE, '', 1), new Token(Token::EOF_TYPE, '', 1), - ])); + ], new Source('', ''))); $p = new \ReflectionProperty($parser, 'parent'); $p->setAccessible(true); @@ -208,7 +208,7 @@ protected function getParser() $p = new \ReflectionProperty($parser, 'stream'); $p->setAccessible(true); - $p->setValue($parser, new TokenStream([])); + $p->setValue($parser, new TokenStream([], new Source('', ''))); return $parser; } @@ -225,7 +225,7 @@ public function parse(Token $token): Node new Token(Token::STRING_TYPE, 'base', 1), new Token(Token::BLOCK_END_TYPE, '', 1), new Token(Token::EOF_TYPE, '', 1), - ])); + ], new Source('', ''))); $this->parser->getStream()->expect(Token::BLOCK_END_TYPE); diff --git a/tests/TokenStreamTest.php b/tests/TokenStreamTest.php index 8f86ac87a7b..a794bc0a43c 100644 --- a/tests/TokenStreamTest.php +++ b/tests/TokenStreamTest.php @@ -13,6 +13,7 @@ use PHPUnit\Framework\TestCase; use Twig\Error\SyntaxError; +use Twig\Source; use Twig\Token; use Twig\TokenStream; @@ -36,7 +37,7 @@ protected function setUp(): void public function testNext() { - $stream = new TokenStream(self::$tokens); + $stream = new TokenStream(self::$tokens, new Source('', '')); $repr = []; while (!$stream->isEOF()) { $token = $stream->next(); @@ -50,7 +51,7 @@ public function testEndOfTemplateNext() { $stream = new TokenStream([ new Token(Token::BLOCK_START_TYPE, 1, 1), - ]); + ], new Source('', '')); $this->expectException(SyntaxError::class); $this->expectExceptionMessage('Unexpected end of template'); @@ -64,7 +65,7 @@ public function testEndOfTemplateLook() { $stream = new TokenStream([ new Token(Token::BLOCK_START_TYPE, 1, 1), - ]); + ], new Source('', '')); $this->expectException(SyntaxError::class); $this->expectExceptionMessage('Unexpected end of template'); From c402debcb86f4ac28bf93dc80c25246453cb1f9e Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sat, 23 Nov 2024 08:35:07 +0100 Subject: [PATCH 618/812] Fix EscapeNodeVisitor::isSafeFor() --- CHANGELOG | 1 + doc/deprecated.rst | 4 +++ src/NodeVisitor/EscaperNodeVisitor.php | 2 +- src/NodeVisitor/SafeAnalysisNodeVisitor.php | 31 +++++++++++++-------- src/TwigFilter.php | 6 ++-- 5 files changed, 28 insertions(+), 16 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index f9c68374e90..e76aceca71e 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,7 @@ # 3.16.0 (2024-XX-XX) * Deprecate not passing a `Source` instance to `TokenStream` + * Deprecate returning `null` from `TwigFilter::getSafe()` and `TwigFunction::getSafe()`, return `[]` instead # 3.15.0 (2024-11-17) diff --git a/doc/deprecated.rst b/doc/deprecated.rst index 99d918dabb6..a8435bd3ea4 100644 --- a/doc/deprecated.rst +++ b/doc/deprecated.rst @@ -319,6 +319,10 @@ Functions/Filters/Tests arrow functions is deprecated as of Twig 3.15; these arguments will have a ``\Closure`` type hint in 4.0. +* Returning ``null`` from ``TwigFilter::getSafe()`` and + ``TwigFunction::getSafe()`` is deprecated as of Twig 3.16; return ``[]`` + instead. + Node ---- diff --git a/src/NodeVisitor/EscaperNodeVisitor.php b/src/NodeVisitor/EscaperNodeVisitor.php index c3eefe2135b..596b4d675cd 100644 --- a/src/NodeVisitor/EscaperNodeVisitor.php +++ b/src/NodeVisitor/EscaperNodeVisitor.php @@ -172,7 +172,7 @@ private function isSafeFor(string $type, AbstractExpression $expression, Environ { $safe = $this->safeAnalysis->getSafe($expression); - if (null === $safe) { + if (!$safe) { if (null === $this->traverser) { $this->traverser = new NodeTraverser($env, [$this->safeAnalysis]); } diff --git a/src/NodeVisitor/SafeAnalysisNodeVisitor.php b/src/NodeVisitor/SafeAnalysisNodeVisitor.php index dbe7150c933..9eda8c8e134 100644 --- a/src/NodeVisitor/SafeAnalysisNodeVisitor.php +++ b/src/NodeVisitor/SafeAnalysisNodeVisitor.php @@ -37,11 +37,14 @@ public function setSafeVars(array $safeVars): void $this->safeVars = $safeVars; } + /** + * @return array + */ public function getSafe(Node $node) { $hash = spl_object_hash($node); if (!isset($this->data[$hash])) { - return; + return []; } foreach ($this->data[$hash] as $bucket) { @@ -55,6 +58,8 @@ public function getSafe(Node $node) return $bucket['value']; } + + return []; } private function setSafe(Node $node, array $safe): void @@ -107,11 +112,14 @@ public function leaveNode(Node $node, Environment $env): ?Node if ($filter) { $safe = $filter->getSafe($node->getNode('arguments')); if (null === $safe) { + trigger_deprecation('twig/twig', '3.16', 'The "%s::getSafe()" method should not return "null" anymore, return "[]" instead.', $filter::class); + $safe = []; + } + + if (!$safe) { $safe = $this->intersectSafe($this->getSafe($node->getNode('node')), $filter->getPreservesSafety()); } $this->setSafe($node, $safe); - } else { - $this->setSafe($node, []); } } elseif ($node instanceof FunctionExpression) { // function expression is safe when the function is safe @@ -123,9 +131,12 @@ public function leaveNode(Node $node, Environment $env): ?Node } if ($function) { - $this->setSafe($node, $function->getSafe($node->getNode('arguments'))); - } else { - $this->setSafe($node, []); + $safe = $function->getSafe($node->getNode('arguments')); + if (null === $safe) { + trigger_deprecation('twig/twig', '3.16', 'The "%s::getSafe()" method should not return "null" anymore, return "[]" instead.', $function::class); + $safe = []; + } + $this->setSafe($node, $safe); } } elseif ($node instanceof MethodCallExpression || $node instanceof MacroReferenceExpression) { // all macro calls are safe @@ -134,19 +145,15 @@ public function leaveNode(Node $node, Environment $env): ?Node $name = $node->getNode('node')->getAttribute('name'); if (\in_array($name, $this->safeVars)) { $this->setSafe($node, ['all']); - } else { - $this->setSafe($node, []); } - } else { - $this->setSafe($node, []); } return $node; } - private function intersectSafe(?array $a = null, ?array $b = null): array + private function intersectSafe(array $a, array $b): array { - if (null === $a || null === $b) { + if (!$a || !$b) { return []; } diff --git a/src/TwigFilter.php b/src/TwigFilter.php index 70b1f8f3fc6..dece5184355 100644 --- a/src/TwigFilter.php +++ b/src/TwigFilter.php @@ -54,12 +54,12 @@ public function getSafe(Node $filterArgs): ?array return $this->options['is_safe_callback']($filterArgs); } - return null; + return []; } - public function getPreservesSafety(): ?array + public function getPreservesSafety(): array { - return $this->options['preserves_safety']; + return $this->options['preserves_safety'] ?? []; } public function getPreEscape(): ?string From 0176de0fa92743fc80bbc1701b9cf0795e48b5b8 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Tue, 26 Nov 2024 16:57:59 +0100 Subject: [PATCH 619/812] Fix CS --- src/Node/MacroNode.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Node/MacroNode.php b/src/Node/MacroNode.php index fb2431cc42d..86abee759a9 100644 --- a/src/Node/MacroNode.php +++ b/src/Node/MacroNode.php @@ -14,7 +14,6 @@ use Twig\Attribute\YieldReady; use Twig\Compiler; use Twig\Error\SyntaxError; -use Twig\Markup; use Twig\Node\Expression\ArrayExpression; use Twig\Node\Expression\Variable\LocalVariable; From bdb0f3c0421df6d95c9b4676ab708b825334cdf5 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Mon, 25 Nov 2024 18:27:42 +0100 Subject: [PATCH 620/812] Fix having macro variables starting with an underscore --- CHANGELOG | 1 + src/Node/Expression/TempNameExpression.php | 2 +- src/Node/MacroNode.php | 8 ++++++-- tests/Node/MacroTest.php | 8 ++++++-- 4 files changed, 14 insertions(+), 5 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index e76aceca71e..ef40b2055b9 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,6 @@ # 3.16.0 (2024-XX-XX) + * Fix having macro variables starting with an underscore * Deprecate not passing a `Source` instance to `TokenStream` * Deprecate returning `null` from `TwigFilter::getSafe()` and `TwigFunction::getSafe()`, return `[]` instead diff --git a/src/Node/Expression/TempNameExpression.php b/src/Node/Expression/TempNameExpression.php index b6ce30c8b74..925b0e7b378 100644 --- a/src/Node/Expression/TempNameExpression.php +++ b/src/Node/Expression/TempNameExpression.php @@ -32,7 +32,7 @@ public function __construct(string|int|null $name, int $lineno) if (null !== $name && (is_int($name) || ctype_digit($name))) { $name = (int) $name; } elseif (in_array($name, self::RESERVED_NAMES)) { - $name = '_'.$name.'_'; + $name = "\u{035C}".$name; } parent::__construct([], ['name' => $name], $lineno); diff --git a/src/Node/MacroNode.php b/src/Node/MacroNode.php index fb2431cc42d..e0961d1be8f 100644 --- a/src/Node/MacroNode.php +++ b/src/Node/MacroNode.php @@ -49,7 +49,7 @@ public function __construct(string $name, Node $body, Node $arguments, int $line } foreach ($arguments->getKeyValuePairs() as $pair) { - if ('_'.self::VARARGS_NAME.'_' === $pair['key']->getAttribute('name')) { + if ("\u{035C}".self::VARARGS_NAME === $pair['key']->getAttribute('name')) { throw new SyntaxError(\sprintf('The argument "%s" in macro "%s" cannot be defined because the variable "%s" is reserved for arbitrary arguments.', self::VARARGS_NAME, $name, self::VARARGS_NAME), $pair['value']->getTemplateLine(), $pair['value']->getSourceContext()); } } @@ -89,9 +89,13 @@ public function compile(Compiler $compiler): void foreach ($arguments->getKeyValuePairs() as $pair) { $name = $pair['key']; + $var = $name->getAttribute('name'); + if (str_starts_with($var, "\u{035C}")) { + $var = substr($var, \strlen("\u{035C}")); + } $compiler ->write('') - ->string(trim($name->getAttribute('name'), '_')) + ->string($var) ->raw(' => ') ->subcompile($name) ->raw(",\n") diff --git a/tests/Node/MacroTest.php b/tests/Node/MacroTest.php index fcd5db4029c..11bdc2f159b 100644 --- a/tests/Node/MacroTest.php +++ b/tests/Node/MacroTest.php @@ -42,6 +42,8 @@ public static function provideTests(): iterable new ConstantExpression(null, 1), new LocalVariable('bar', 1), new ConstantExpression('Foo', 1), + new LocalVariable('_underscore', 1), + new ConstantExpression(null, 1), ], 1); $body = new BodyNode([new TextNode('foo', 1)]); @@ -49,12 +51,13 @@ public static function provideTests(): iterable yield 'with use_yield = true' => [$node, <<macros; \$context = [ "foo" => \$foo, "bar" => \$bar, + "_underscore" => \$_underscore, "varargs" => \$varargs, ] + \$this->env->getGlobals(); @@ -71,12 +74,13 @@ public function macro_foo(\$foo = null, \$bar = "Foo", ...\$varargs): string|Mar yield 'with use_yield = false' => [$node, <<macros; \$context = [ "foo" => \$foo, "bar" => \$bar, + "_underscore" => \$_underscore, "varargs" => \$varargs, ] + \$this->env->getGlobals(); From 02cec77619f2c9a2cb50e024ae73799b7624027b Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Thu, 28 Nov 2024 11:25:19 +0100 Subject: [PATCH 621/812] Simplify EscaperNodeVisitor code --- CHANGELOG | 1 + doc/deprecated.rst | 2 ++ src/Node/Expression/InlinePrint.php | 4 +-- src/NodeVisitor/EscaperNodeVisitor.php | 44 +++++++------------------- 4 files changed, 15 insertions(+), 36 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index ef40b2055b9..6f3062d3fbf 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,6 @@ # 3.16.0 (2024-XX-XX) + * Deprecate `InlinePrint` * Fix having macro variables starting with an underscore * Deprecate not passing a `Source` instance to `TokenStream` * Deprecate returning `null` from `TwigFilter::getSafe()` and `TwigFunction::getSafe()`, return `[]` instead diff --git a/doc/deprecated.rst b/doc/deprecated.rst index a8435bd3ea4..4b0f1bb4de9 100644 --- a/doc/deprecated.rst +++ b/doc/deprecated.rst @@ -189,6 +189,8 @@ Nodes made ready for ``yield``; the ``use_yield`` Environment option can be turned on when all nodes use the ``#[\Twig\Attribute\YieldReady]`` attribute. + * The ``InlinePrint`` class is deprecated as of Twig 3.16 with no replacement. + Node Visitors ------------- diff --git a/src/Node/Expression/InlinePrint.php b/src/Node/Expression/InlinePrint.php index 82d17d626de..5509f7942b1 100644 --- a/src/Node/Expression/InlinePrint.php +++ b/src/Node/Expression/InlinePrint.php @@ -24,9 +24,7 @@ final class InlinePrint extends AbstractExpression */ public function __construct(Node $node, int $lineno) { - if (!$node instanceof AbstractExpression) { - trigger_deprecation('twig/twig', '3.15', 'Not passing a "%s" instance to the "node" argument of "%s" is deprecated ("%s" given).', AbstractExpression::class, static::class, get_class($node)); - } + trigger_deprecation('twig/twig', '3.16', \sprintf('The "%s" class is deprecated with no replacement.', static::class)); parent::__construct(['node' => $node], [], $lineno); } diff --git a/src/NodeVisitor/EscaperNodeVisitor.php b/src/NodeVisitor/EscaperNodeVisitor.php index 596b4d675cd..27ed8e1cf46 100644 --- a/src/NodeVisitor/EscaperNodeVisitor.php +++ b/src/NodeVisitor/EscaperNodeVisitor.php @@ -16,12 +16,10 @@ use Twig\Node\AutoEscapeNode; use Twig\Node\BlockNode; use Twig\Node\BlockReferenceNode; -use Twig\Node\DoNode; use Twig\Node\Expression\AbstractExpression; use Twig\Node\Expression\ConditionalExpression; use Twig\Node\Expression\ConstantExpression; use Twig\Node\Expression\FilterExpression; -use Twig\Node\Expression\InlinePrint; use Twig\Node\ImportNode; use Twig\Node\ModuleNode; use Twig\Node\Node; @@ -78,10 +76,12 @@ public function leaveNode(Node $node, Environment $env): ?Node } elseif ($node instanceof PrintNode && false !== $type = $this->needEscaping()) { $expression = $node->getNode('expr'); if ($expression instanceof ConditionalExpression) { - return new DoNode($this->unwrapConditional($expression, $env, $type), $expression->getTemplateLine()); + $node->setNode('expr', $this->escapeConditional($expression, $env, $type)); + } else { + $node->setNode('expr', $this->escapeExpression($expression, $env, $type)); } - return $this->escapePrintNode($node, $env, $type); + return $node; } if ($node instanceof AutoEscapeNode || $node instanceof BlockNode) { @@ -93,22 +93,21 @@ public function leaveNode(Node $node, Environment $env): ?Node return $node; } - private function unwrapConditional(ConditionalExpression $expression, Environment $env, string $type): ConditionalExpression + private function escapeConditional(ConditionalExpression $expression, Environment $env, string $type): ConditionalExpression { - // convert "echo a ? b : c" to "a ? echo b : echo c" recursively /** @var AbstractExpression $expr2 */ $expr2 = $expression->getNode('expr2'); if ($expr2 instanceof ConditionalExpression) { - $expr2 = $this->unwrapConditional($expr2, $env, $type); + $expr2 = $this->escapeConditional($expr2, $env, $type); } else { - $expr2 = $this->escapeInlinePrintNode(new InlinePrint($expr2, $expr2->getTemplateLine()), $env, $type); + $expr2 = $this->escapeExpression($expr2, $env, $type); } /** @var AbstractExpression $expr3 */ $expr3 = $expression->getNode('expr3'); if ($expr3 instanceof ConditionalExpression) { - $expr3 = $this->unwrapConditional($expr3, $env, $type); + $expr3 = $this->escapeConditional($expr3, $env, $type); } else { - $expr3 = $this->escapeInlinePrintNode(new InlinePrint($expr3, $expr3->getTemplateLine()), $env, $type); + $expr3 = $this->escapeExpression($expr3, $env, $type); } /** @var AbstractExpression $expr1 */ @@ -117,30 +116,9 @@ private function unwrapConditional(ConditionalExpression $expression, Environmen return new ConditionalExpression($expr1, $expr2, $expr3, $expression->getTemplateLine()); } - private function escapeInlinePrintNode(InlinePrint $node, Environment $env, string $type): AbstractExpression + private function escapeExpression(AbstractExpression $expression, Environment $env, string $type): AbstractExpression { - /** @var AbstractExpression $expression */ - $expression = $node->getNode('node'); - - if ($this->isSafeFor($type, $expression, $env)) { - return $node; - } - - return new InlinePrint($this->getEscaperFilter($env, $type, $expression), $node->getTemplateLine()); - } - - private function escapePrintNode(PrintNode $node, Environment $env, string $type): Node - { - /** @var AbstractExpression $expression */ - $expression = $node->getNode('expr'); - - if ($this->isSafeFor($type, $expression, $env)) { - return $node; - } - - $class = \get_class($node); - - return new $class($this->getEscaperFilter($env, $type, $expression), $node->getTemplateLine()); + return $this->isSafeFor($type, $expression, $env) ? $expression : $this->getEscaperFilter($env, $type, $expression); } private function preEscapeFilterNode(FilterExpression $filter, Environment $env): FilterExpression From 2337c7f8120c316e78bdc019867ef28fbfbaff64 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Thu, 28 Nov 2024 14:36:54 +0100 Subject: [PATCH 622/812] Simplify code even more --- src/NodeVisitor/EscaperNodeVisitor.php | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/src/NodeVisitor/EscaperNodeVisitor.php b/src/NodeVisitor/EscaperNodeVisitor.php index 27ed8e1cf46..9640c541b01 100644 --- a/src/NodeVisitor/EscaperNodeVisitor.php +++ b/src/NodeVisitor/EscaperNodeVisitor.php @@ -76,7 +76,7 @@ public function leaveNode(Node $node, Environment $env): ?Node } elseif ($node instanceof PrintNode && false !== $type = $this->needEscaping()) { $expression = $node->getNode('expr'); if ($expression instanceof ConditionalExpression) { - $node->setNode('expr', $this->escapeConditional($expression, $env, $type)); + $this->escapeConditional($expression, $env, $type); } else { $node->setNode('expr', $this->escapeExpression($expression, $env, $type)); } @@ -93,27 +93,23 @@ public function leaveNode(Node $node, Environment $env): ?Node return $node; } - private function escapeConditional(ConditionalExpression $expression, Environment $env, string $type): ConditionalExpression + private function escapeConditional(ConditionalExpression $expression, Environment $env, string $type): void { /** @var AbstractExpression $expr2 */ $expr2 = $expression->getNode('expr2'); if ($expr2 instanceof ConditionalExpression) { - $expr2 = $this->escapeConditional($expr2, $env, $type); + $this->escapeConditional($expr2, $env, $type); } else { - $expr2 = $this->escapeExpression($expr2, $env, $type); + $expression->setNode('expr2', $this->escapeExpression($expr2, $env, $type)); } + /** @var AbstractExpression $expr3 */ $expr3 = $expression->getNode('expr3'); if ($expr3 instanceof ConditionalExpression) { - $expr3 = $this->escapeConditional($expr3, $env, $type); + $this->escapeConditional($expr3, $env, $type); } else { - $expr3 = $this->escapeExpression($expr3, $env, $type); + $expression->setNode('expr3', $this->escapeExpression($expr3, $env, $type)); } - - /** @var AbstractExpression $expr1 */ - $expr1 = $expression->getNode('expr1'); - - return new ConditionalExpression($expr1, $expr2, $expr3, $expression->getTemplateLine()); } private function escapeExpression(AbstractExpression $expression, Environment $env, string $type): AbstractExpression From 475ad2dc97d65d8631393e721e7e44fb544f0561 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Fri, 29 Nov 2024 09:27:05 +0100 Subject: [PATCH 623/812] Prepare the 3.16.0 release --- src/Environment.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Environment.php b/src/Environment.php index 2b308fc51a4..bc064e445b6 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -44,12 +44,12 @@ */ class Environment { - public const VERSION = '3.16.0-DEV'; + public const VERSION = '3.16.0'; public const VERSION_ID = 31600; public const MAJOR_VERSION = 3; public const MINOR_VERSION = 16; public const RELEASE_VERSION = 0; - public const EXTRA_VERSION = 'DEV'; + public const EXTRA_VERSION = ''; private $charset; private $loader; From 5307894b0c32b7f532e9b211e322d35dce24f9ed Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Fri, 29 Nov 2024 09:28:25 +0100 Subject: [PATCH 624/812] Bump version --- src/Environment.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Environment.php b/src/Environment.php index bc064e445b6..1546e4b314a 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -44,12 +44,12 @@ */ class Environment { - public const VERSION = '3.16.0'; - public const VERSION_ID = 31600; + public const VERSION = '3.17.0-DEV'; + public const VERSION_ID = 31700; public const MAJOR_VERSION = 3; - public const MINOR_VERSION = 16; + public const MINOR_VERSION = 17; public const RELEASE_VERSION = 0; - public const EXTRA_VERSION = ''; + public const EXTRA_VERSION = 'DEV'; private $charset; private $loader; From 4e1cbc74effd2b940316666c8334b63e2d0cdcf5 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Fri, 29 Nov 2024 09:29:04 +0100 Subject: [PATCH 625/812] Fix CHANGELOG --- CHANGELOG | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 6f3062d3fbf..79425bedeac 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,4 +1,8 @@ -# 3.16.0 (2024-XX-XX) +# 3.17.0 (2024-XX-XX) + + * n/a + +# 3.16.0 (2024-11-29) * Deprecate `InlinePrint` * Fix having macro variables starting with an underscore From 4f8ba93600ec1f4e19b78990ffeaaa736e4d653c Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Thu, 28 Nov 2024 16:33:09 +0100 Subject: [PATCH 626/812] Enforce AbstractBinary for all binary operators --- CHANGELOG | 1 + doc/deprecated.rst | 10 ++- src/ExpressionParser.php | 22 ++--- src/Extension/CoreExtension.php | 5 +- src/Node/Expression/Binary/ElvisBinary.php | 50 +++++++++++ .../Expression/Binary/NullCoalesceBinary.php | 84 +++++++++++++++++++ src/Node/Expression/ConditionalExpression.php | 11 ++- src/Node/Expression/Filter/DefaultFilter.php | 4 +- .../Expression/NullCoalesceExpression.php | 3 + .../Expression/OperatorEscapeInterface.php | 25 ++++++ .../Expression/Ternary/ConditionalTernary.php | 42 ++++++++++ src/NodeVisitor/EscaperNodeVisitor.php | 31 ++++--- src/NodeVisitor/SafeAnalysisNodeVisitor.php | 15 ++-- src/TokenParser/TypesTokenParser.php | 8 +- .../Expression/Binary/NullCoalesceTest.php | 29 +++++++ tests/Node/Expression/ConditionalTest.php | 3 + tests/Node/Expression/NullCoalesceTest.php | 3 + .../Ternary/ConditionalTernaryTest.php | 44 ++++++++++ tests/Node/IncludeTest.php | 4 +- tests/Node/ModuleTest.php | 4 +- 20 files changed, 351 insertions(+), 47 deletions(-) create mode 100644 src/Node/Expression/Binary/ElvisBinary.php create mode 100644 src/Node/Expression/Binary/NullCoalesceBinary.php create mode 100644 src/Node/Expression/OperatorEscapeInterface.php create mode 100644 src/Node/Expression/Ternary/ConditionalTernary.php create mode 100644 tests/Node/Expression/Binary/NullCoalesceTest.php create mode 100644 tests/Node/Expression/Ternary/ConditionalTernaryTest.php diff --git a/CHANGELOG b/CHANGELOG index 79425bedeac..5b3fa9eb5a0 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -4,6 +4,7 @@ # 3.16.0 (2024-11-29) + * Deprecate `ConditionalExpression` and `NullCoalesceExpression` (use `ConditionalTernary` and `NullCoalesceBinary` instead) * Deprecate `InlinePrint` * Fix having macro variables starting with an underscore * Deprecate not passing a `Source` instance to `TokenStream` diff --git a/doc/deprecated.rst b/doc/deprecated.rst index 4b0f1bb4de9..4d4d096adbe 100644 --- a/doc/deprecated.rst +++ b/doc/deprecated.rst @@ -189,7 +189,15 @@ Nodes made ready for ``yield``; the ``use_yield`` Environment option can be turned on when all nodes use the ``#[\Twig\Attribute\YieldReady]`` attribute. - * The ``InlinePrint`` class is deprecated as of Twig 3.16 with no replacement. + * The ``Twig\Node\InlinePrint`` class is deprecated as of Twig 3.16 with no + replacement. + + * The ``Twig\Node\Expression\NullCoalesceExpression`` class is deprecated as + of Twig 3.16, use ``Twig\Node\Expression\Binary\NullCoalesceBinary`` + instead. + + * The ``Twig\Node\Expression\ConditionalExpression`` class is deprecated as of + Twig 3.16, use ``Twig\Node\Expression\Ternary\ConditionalTernary`` instead. Node Visitors ------------- diff --git a/src/ExpressionParser.php b/src/ExpressionParser.php index 2fb528518ba..3cb174de0e2 100644 --- a/src/ExpressionParser.php +++ b/src/ExpressionParser.php @@ -20,11 +20,11 @@ use Twig\Node\Expression\ArrowFunctionExpression; use Twig\Node\Expression\Binary\AbstractBinary; use Twig\Node\Expression\Binary\ConcatBinary; -use Twig\Node\Expression\ConditionalExpression; use Twig\Node\Expression\ConstantExpression; use Twig\Node\Expression\GetAttrExpression; use Twig\Node\Expression\MacroReferenceExpression; use Twig\Node\Expression\NameExpression; +use Twig\Node\Expression\Ternary\ConditionalTernary; use Twig\Node\Expression\TestExpression; use Twig\Node\Expression\Unary\AbstractUnary; use Twig\Node\Expression\Unary\NegUnary; @@ -269,22 +269,16 @@ private function getPrimary(): AbstractExpression private function parseConditionalExpression($expr): AbstractExpression { while ($this->parser->getStream()->nextIf(Token::PUNCTUATION_TYPE, '?')) { - if (!$this->parser->getStream()->nextIf(Token::PUNCTUATION_TYPE, ':')) { - $expr2 = $this->parseExpression(); - if ($this->parser->getStream()->nextIf(Token::PUNCTUATION_TYPE, ':')) { - // Ternary operator (expr ? expr2 : expr3) - $expr3 = $this->parseExpression(); - } else { - // Ternary without else (expr ? expr2) - $expr3 = new ConstantExpression('', $this->parser->getCurrentToken()->getLine()); - } - } else { - // Ternary without then (expr ?: expr3) - $expr2 = $expr; + $expr2 = $this->parseExpression(); + if ($this->parser->getStream()->nextIf(Token::PUNCTUATION_TYPE, ':')) { + // Ternary operator (expr ? expr2 : expr3) $expr3 = $this->parseExpression(); + } else { + // Ternary without else (expr ? expr2) + $expr3 = new ConstantExpression('', $this->parser->getCurrentToken()->getLine()); } - $expr = new ConditionalExpression($expr, $expr2, $expr3, $this->parser->getCurrentToken()->getLine()); + $expr = new ConditionalTernary($expr, $expr2, $expr3, $this->parser->getCurrentToken()->getLine()); } return $expr; diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index 429763ad143..cb54adc753d 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -26,6 +26,7 @@ use Twig\Node\Expression\Binary\BitwiseXorBinary; use Twig\Node\Expression\Binary\ConcatBinary; use Twig\Node\Expression\Binary\DivBinary; +use Twig\Node\Expression\Binary\ElvisBinary; use Twig\Node\Expression\Binary\EndsWithBinary; use Twig\Node\Expression\Binary\EqualBinary; use Twig\Node\Expression\Binary\FloorDivBinary; @@ -41,6 +42,7 @@ use Twig\Node\Expression\Binary\MulBinary; use Twig\Node\Expression\Binary\NotEqualBinary; use Twig\Node\Expression\Binary\NotInBinary; +use Twig\Node\Expression\Binary\NullCoalesceBinary; use Twig\Node\Expression\Binary\OrBinary; use Twig\Node\Expression\Binary\PowerBinary; use Twig\Node\Expression\Binary\RangeBinary; @@ -320,6 +322,8 @@ public function getOperators(): array '+' => ['precedence' => 500, 'class' => PosUnary::class], ], [ + '?:' => ['precedence' => 5, 'class' => ElvisBinary::class, 'associativity' => ExpressionParser::OPERATOR_RIGHT], + '??' => ['precedence' => 300, 'precedence_change' => new OperatorPrecedenceChange('twig/twig', '3.15', 5), 'class' => NullCoalesceBinary::class, 'associativity' => ExpressionParser::OPERATOR_RIGHT], 'or' => ['precedence' => 10, 'class' => OrBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT], 'xor' => ['precedence' => 12, 'class' => XorBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT], 'and' => ['precedence' => 15, 'class' => AndBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT], @@ -351,7 +355,6 @@ public function getOperators(): array 'is' => ['precedence' => 100, 'associativity' => ExpressionParser::OPERATOR_LEFT], 'is not' => ['precedence' => 100, 'associativity' => ExpressionParser::OPERATOR_LEFT], '**' => ['precedence' => 200, 'class' => PowerBinary::class, 'associativity' => ExpressionParser::OPERATOR_RIGHT], - '??' => ['precedence' => 300, 'precedence_change' => new OperatorPrecedenceChange('twig/twig', '3.15', 5), 'class' => NullCoalesceExpression::class, 'associativity' => ExpressionParser::OPERATOR_RIGHT], ], ]; } diff --git a/src/Node/Expression/Binary/ElvisBinary.php b/src/Node/Expression/Binary/ElvisBinary.php new file mode 100644 index 00000000000..205d1ff45e6 --- /dev/null +++ b/src/Node/Expression/Binary/ElvisBinary.php @@ -0,0 +1,50 @@ +setNode('test', clone $left); + $left->setAttribute('always_defined', true); + } + + public function compile(Compiler $compiler): void + { + $compiler + ->raw('((') + ->subcompile($this->getNode('test')) + ->raw(') ? (') + ->subcompile($this->getNode('left')) + ->raw(') : (') + ->subcompile($this->getNode('right')) + ->raw('))') + ; + } + + public function operator(Compiler $compiler): Compiler + { + return $compiler->raw('?:'); + } + + public function getOperandNamesToEscape(): array + { + return ['left', 'right']; + } +} diff --git a/src/Node/Expression/Binary/NullCoalesceBinary.php b/src/Node/Expression/Binary/NullCoalesceBinary.php new file mode 100644 index 00000000000..6fb088fa62d --- /dev/null +++ b/src/Node/Expression/Binary/NullCoalesceBinary.php @@ -0,0 +1,84 @@ +getTemplateLine()); + // for "block()", we don't need the null test as the return value is always a string + if (!$left instanceof BlockReferenceExpression) { + $test = new AndBinary( + $test, + new NotUnary(new NullTest($left, new TwigTest('null'), new EmptyNode(), $left->getTemplateLine()), $left->getTemplateLine()), + $left->getTemplateLine(), + ); + } + + $this->setNode('test', $test); + } else { + $left->setAttribute('always_defined', true); + } + } + + public function compile(Compiler $compiler): void + { + /* + * This optimizes only one case. PHP 7 also supports more complex expressions + * that can return null. So, for instance, if log is defined, log("foo") ?? "..." works, + * but log($a["foo"]) ?? "..." does not if $a["foo"] is not defined. More advanced + * cases might be implemented as an optimizer node visitor, but has not been done + * as benefits are probably not worth the added complexity. + */ + if ($this->hasNode('test')) { + $compiler + ->raw('((') + ->subcompile($this->getNode('test')) + ->raw(') ? (') + ->subcompile($this->getNode('left')) + ->raw(') : (') + ->subcompile($this->getNode('right')) + ->raw('))') + ; + + return; + } + + parent::compile($compiler); + } + + public function operator(Compiler $compiler): Compiler + { + return $compiler->raw('??'); + } + + public function getOperandNamesToEscape(): array + { + return $this->hasNode('test') ? ['left', 'right'] : ['right']; + } +} diff --git a/src/Node/Expression/ConditionalExpression.php b/src/Node/Expression/ConditionalExpression.php index d7db993579c..69d55a35835 100644 --- a/src/Node/Expression/ConditionalExpression.php +++ b/src/Node/Expression/ConditionalExpression.php @@ -13,11 +13,15 @@ namespace Twig\Node\Expression; use Twig\Compiler; +use Twig\Node\Expression\OperatorEscapeInterface; +use Twig\Node\Expression\Ternary\ConditionalTernary; -class ConditionalExpression extends AbstractExpression +class ConditionalExpression extends AbstractExpression implements OperatorEscapeInterface { public function __construct(AbstractExpression $expr1, AbstractExpression $expr2, AbstractExpression $expr3, int $lineno) { + trigger_deprecation('twig/twig', '3.16', \sprintf('"%s" is deprecated; use "%s" instead.', __CLASS__, ConditionalTernary::class)); + parent::__construct(['expr1' => $expr1, 'expr2' => $expr2, 'expr3' => $expr3], [], $lineno); } @@ -42,4 +46,9 @@ public function compile(Compiler $compiler): void ->raw('))'); } } + + public function getOperandNamesToEscape(): array + { + return ['expr2', 'expr3']; + } } diff --git a/src/Node/Expression/Filter/DefaultFilter.php b/src/Node/Expression/Filter/DefaultFilter.php index a3ee66e47b1..60ebc6b83da 100644 --- a/src/Node/Expression/Filter/DefaultFilter.php +++ b/src/Node/Expression/Filter/DefaultFilter.php @@ -16,11 +16,11 @@ use Twig\Extension\CoreExtension; use Twig\Node\EmptyNode; use Twig\Node\Expression\AbstractExpression; -use Twig\Node\Expression\ConditionalExpression; use Twig\Node\Expression\ConstantExpression; use Twig\Node\Expression\FilterExpression; use Twig\Node\Expression\GetAttrExpression; use Twig\Node\Expression\NameExpression; +use Twig\Node\Expression\Ternary\ConditionalTernary; use Twig\Node\Expression\Test\DefinedTest; use Twig\Node\Node; use Twig\TwigFilter; @@ -57,7 +57,7 @@ public function __construct(Node $node, TwigFilter|ConstantExpression $filter, N $test = new DefinedTest(clone $node, new TwigTest('defined'), new EmptyNode(), $node->getTemplateLine()); $false = \count($arguments) ? $arguments->getNode('0') : new ConstantExpression('', $node->getTemplateLine()); - $node = new ConditionalExpression($test, $default, $false, $node->getTemplateLine()); + $node = new ConditionalTernary($test, $default, $false, $node->getTemplateLine()); } else { $node = $default; } diff --git a/src/Node/Expression/NullCoalesceExpression.php b/src/Node/Expression/NullCoalesceExpression.php index 1a5d90286b7..5c9a276890b 100644 --- a/src/Node/Expression/NullCoalesceExpression.php +++ b/src/Node/Expression/NullCoalesceExpression.php @@ -14,6 +14,7 @@ use Twig\Compiler; use Twig\Node\EmptyNode; use Twig\Node\Expression\Binary\AndBinary; +use Twig\Node\Expression\Binary\NullCoalesceBinary; use Twig\Node\Expression\Test\DefinedTest; use Twig\Node\Expression\Test\NullTest; use Twig\Node\Expression\Unary\NotUnary; @@ -28,6 +29,8 @@ class NullCoalesceExpression extends ConditionalExpression */ public function __construct(Node $left, Node $right, int $lineno) { + trigger_deprecation('twig/twig', '3.16', \sprintf('"%s" is deprecated; use "%s" instead.', __CLASS__, NullCoalesceBinary::class)); + if (!$left instanceof AbstractExpression) { trigger_deprecation('twig/twig', '3.15', 'Not passing a "%s" instance to the "left" argument of "%s" is deprecated ("%s" given).', AbstractExpression::class, static::class, get_class($left)); } diff --git a/src/Node/Expression/OperatorEscapeInterface.php b/src/Node/Expression/OperatorEscapeInterface.php new file mode 100644 index 00000000000..7b1e43e35a9 --- /dev/null +++ b/src/Node/Expression/OperatorEscapeInterface.php @@ -0,0 +1,25 @@ + 1. + * + * @author Fabien Potencier + */ +interface OperatorEscapeInterface +{ + /** + * @return string[] + */ + public function getOperandNamesToEscape(): array; +} diff --git a/src/Node/Expression/Ternary/ConditionalTernary.php b/src/Node/Expression/Ternary/ConditionalTernary.php new file mode 100644 index 00000000000..627da7a43cf --- /dev/null +++ b/src/Node/Expression/Ternary/ConditionalTernary.php @@ -0,0 +1,42 @@ + $test, 'left' => $left, 'right' => $right], [], $lineno); + } + + public function compile(Compiler $compiler): void + { + $compiler + ->raw('((') + ->subcompile($this->getNode('test')) + ->raw(') ? (') + ->subcompile($this->getNode('left')) + ->raw(') : (') + ->subcompile($this->getNode('right')) + ->raw('))') + ; + } + + public function getOperandNamesToEscape(): array + { + return ['left', 'right']; + } +} diff --git a/src/NodeVisitor/EscaperNodeVisitor.php b/src/NodeVisitor/EscaperNodeVisitor.php index 9640c541b01..a70726bbd85 100644 --- a/src/NodeVisitor/EscaperNodeVisitor.php +++ b/src/NodeVisitor/EscaperNodeVisitor.php @@ -17,9 +17,9 @@ use Twig\Node\BlockNode; use Twig\Node\BlockReferenceNode; use Twig\Node\Expression\AbstractExpression; -use Twig\Node\Expression\ConditionalExpression; use Twig\Node\Expression\ConstantExpression; use Twig\Node\Expression\FilterExpression; +use Twig\Node\Expression\OperatorEscapeInterface; use Twig\Node\ImportNode; use Twig\Node\ModuleNode; use Twig\Node\Node; @@ -75,7 +75,7 @@ public function leaveNode(Node $node, Environment $env): ?Node return $this->preEscapeFilterNode($node, $env); } elseif ($node instanceof PrintNode && false !== $type = $this->needEscaping()) { $expression = $node->getNode('expr'); - if ($expression instanceof ConditionalExpression) { + if ($expression instanceof OperatorEscapeInterface) { $this->escapeConditional($expression, $env, $type); } else { $node->setNode('expr', $this->escapeExpression($expression, $env, $type)); @@ -93,22 +93,19 @@ public function leaveNode(Node $node, Environment $env): ?Node return $node; } - private function escapeConditional(ConditionalExpression $expression, Environment $env, string $type): void + /** + * @param AbstractExpression&OperatorEscapeInterface $expression + */ + private function escapeConditional($expression, Environment $env, string $type): void { - /** @var AbstractExpression $expr2 */ - $expr2 = $expression->getNode('expr2'); - if ($expr2 instanceof ConditionalExpression) { - $this->escapeConditional($expr2, $env, $type); - } else { - $expression->setNode('expr2', $this->escapeExpression($expr2, $env, $type)); - } - - /** @var AbstractExpression $expr3 */ - $expr3 = $expression->getNode('expr3'); - if ($expr3 instanceof ConditionalExpression) { - $this->escapeConditional($expr3, $env, $type); - } else { - $expression->setNode('expr3', $this->escapeExpression($expr3, $env, $type)); + foreach ($expression->getOperandNamesToEscape() as $name) { + /** @var AbstractExpression $operand */ + $operand = $expression->getNode($name); + if ($operand instanceof OperatorEscapeInterface) { + $this->escapeConditional($operand, $env, $type); + } else { + $expression->setNode($name, $this->escapeExpression($operand, $env, $type)); + } } } diff --git a/src/NodeVisitor/SafeAnalysisNodeVisitor.php b/src/NodeVisitor/SafeAnalysisNodeVisitor.php index 9eda8c8e134..3be82304f4b 100644 --- a/src/NodeVisitor/SafeAnalysisNodeVisitor.php +++ b/src/NodeVisitor/SafeAnalysisNodeVisitor.php @@ -13,7 +13,6 @@ use Twig\Environment; use Twig\Node\Expression\BlockReferenceExpression; -use Twig\Node\Expression\ConditionalExpression; use Twig\Node\Expression\ConstantExpression; use Twig\Node\Expression\FilterExpression; use Twig\Node\Expression\FunctionExpression; @@ -21,6 +20,7 @@ use Twig\Node\Expression\MacroReferenceExpression; use Twig\Node\Expression\MethodCallExpression; use Twig\Node\Expression\NameExpression; +use Twig\Node\Expression\OperatorEscapeInterface; use Twig\Node\Expression\ParentExpression; use Twig\Node\Node; @@ -96,10 +96,15 @@ public function leaveNode(Node $node, Environment $env): ?Node } elseif ($node instanceof ParentExpression) { // parent block is safe by definition $this->setSafe($node, ['all']); - } elseif ($node instanceof ConditionalExpression) { - // intersect safeness of both operands - $safe = $this->intersectSafe($this->getSafe($node->getNode('expr2')), $this->getSafe($node->getNode('expr3'))); - $this->setSafe($node, $safe); + } elseif ($node instanceof OperatorEscapeInterface) { + // intersect safeness of operands + $operands = $node->getOperandNamesToEscape(); + if (2 < \count($operands)) { + throw new \LogicException(\sprintf('Operators with more than 2 operands are not supported yet, got %d.', \count($operands))); + } elseif (2 === \count($operands)) { + $safe = $this->intersectSafe($this->getSafe($node->getNode($operands[0])), $this->getSafe($node->getNode($operands[1]))); + $this->setSafe($node, $safe); + } } elseif ($node instanceof FilterExpression) { // filter expression is safe when the filter is safe if ($node->hasAttribute('twig_callable')) { diff --git a/src/TokenParser/TypesTokenParser.php b/src/TokenParser/TypesTokenParser.php index b97eb3b2e4c..02aef81cc2e 100644 --- a/src/TokenParser/TypesTokenParser.php +++ b/src/TokenParser/TypesTokenParser.php @@ -63,9 +63,13 @@ private function parseSimpleMappingExpression(TokenStream $stream): array $first = false; $nameToken = $stream->expect(Token::NAME_TYPE); - $isOptional = null !== $stream->nextIf(Token::PUNCTUATION_TYPE, '?'); - $stream->expect(Token::PUNCTUATION_TYPE, ':', 'A type name must be followed by a colon (:)'); + if ($stream->nextIf(Token::OPERATOR_TYPE, '?:')) { + $isOptional = true; + } else { + $isOptional = null !== $stream->nextIf(Token::PUNCTUATION_TYPE, '?'); + $stream->expect(Token::PUNCTUATION_TYPE, ':', 'A type name must be followed by a colon (:)'); + } $valueToken = $stream->expect(Token::STRING_TYPE); diff --git a/tests/Node/Expression/Binary/NullCoalesceTest.php b/tests/Node/Expression/Binary/NullCoalesceTest.php new file mode 100644 index 00000000000..b2c79320ad6 --- /dev/null +++ b/tests/Node/Expression/Binary/NullCoalesceTest.php @@ -0,0 +1,29 @@ +assertEquals($test, $node->getNode('test')); + $this->assertEquals($left, $node->getNode('left')); + $this->assertEquals($right, $node->getNode('right')); + } + + public static function provideTests(): iterable + { + $tests = []; + + $test = new ConstantExpression(1, 1); + $left = new ConstantExpression(2, 1); + $right = new ConstantExpression(3, 1); + $node = new ConditionalTernary($test, $left, $right, 1); + $tests[] = [$node, '((1) ? (2) : (3))']; + + return $tests; + } +} diff --git a/tests/Node/IncludeTest.php b/tests/Node/IncludeTest.php index b218748d7d0..8a73a76cec5 100644 --- a/tests/Node/IncludeTest.php +++ b/tests/Node/IncludeTest.php @@ -12,8 +12,8 @@ */ use Twig\Node\Expression\ArrayExpression; -use Twig\Node\Expression\ConditionalExpression; use Twig\Node\Expression\ConstantExpression; +use Twig\Node\Expression\Ternary\ConditionalTernary; use Twig\Node\IncludeNode; use Twig\Test\NodeTestCase; @@ -46,7 +46,7 @@ public static function provideTests(): iterable EOF ]; - $expr = new ConditionalExpression( + $expr = new ConditionalTernary( new ConstantExpression(true, 1), new ConstantExpression('foo', 1), new ConstantExpression('foo', 1), diff --git a/tests/Node/ModuleTest.php b/tests/Node/ModuleTest.php index 4dbce150485..5caddd93b42 100644 --- a/tests/Node/ModuleTest.php +++ b/tests/Node/ModuleTest.php @@ -15,8 +15,8 @@ use Twig\Loader\ArrayLoader; use Twig\Node\BodyNode; use Twig\Node\EmptyNode; -use Twig\Node\Expression\ConditionalExpression; use Twig\Node\Expression\ConstantExpression; +use Twig\Node\Expression\Ternary\ConditionalTernary; use Twig\Node\Expression\Variable\AssignContextVariable; use Twig\Node\Expression\Variable\AssignTemplateVariable; use Twig\Node\Expression\Variable\TemplateVariable; @@ -223,7 +223,7 @@ public function getSourceContext(): Source $set = new SetNode(false, new Nodes([new AssignContextVariable('foo', 4)]), new Nodes([new ConstantExpression('foo', 4)]), 4); $body = new BodyNode([$set]); - $extends = new ConditionalExpression( + $extends = new ConditionalTernary( new ConstantExpression(true, 2), new ConstantExpression('foo', 2), new ConstantExpression('foo', 2), From fab565ca3460f2690b53ab20a38d7cbc996192d2 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Fri, 29 Nov 2024 19:30:47 +0100 Subject: [PATCH 627/812] Update phpdoc for operators --- src/Environment.php | 4 ++-- src/Extension/ExtensionInterface.php | 7 ++++--- src/ExtensionSet.php | 11 ++++++----- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/src/Environment.php b/src/Environment.php index 1546e4b314a..a0b74eb2983 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -863,7 +863,7 @@ public function mergeGlobals(array $context): array /** * @internal * - * @return array}> + * @return array}> */ public function getUnaryOperators(): array { @@ -873,7 +873,7 @@ public function getUnaryOperators(): array /** * @internal * - * @return array, associativity: ExpressionParser::OPERATOR_*}> + * @return array, associativity: ExpressionParser::OPERATOR_*}> */ public function getBinaryOperators(): array { diff --git a/src/Extension/ExtensionInterface.php b/src/Extension/ExtensionInterface.php index 717ef9cec6e..d51cd3ee2ff 100644 --- a/src/Extension/ExtensionInterface.php +++ b/src/Extension/ExtensionInterface.php @@ -12,7 +12,8 @@ namespace Twig\Extension; use Twig\ExpressionParser; -use Twig\Node\Expression\AbstractExpression; +use Twig\Node\Expression\Binary\AbstractBinary; +use Twig\Node\Expression\Unary\AbstractUnary; use Twig\NodeVisitor\NodeVisitorInterface; use Twig\OperatorPrecedenceChange; use Twig\TokenParser\TokenParserInterface; @@ -68,8 +69,8 @@ public function getFunctions(); * @return array First array of unary operators, second array of binary operators * * @psalm-return array{ - * array}>, - * array, associativity: ExpressionParser::OPERATOR_*}> + * array}>, + * array, associativity: ExpressionParser::OPERATOR_*}> * } */ public function getOperators(); diff --git a/src/ExtensionSet.php b/src/ExtensionSet.php index 99fcfe56b75..e8b3174cff4 100644 --- a/src/ExtensionSet.php +++ b/src/ExtensionSet.php @@ -15,7 +15,8 @@ use Twig\Extension\ExtensionInterface; use Twig\Extension\GlobalsInterface; use Twig\Extension\StagingExtension; -use Twig\Node\Expression\AbstractExpression; +use Twig\Node\Expression\Binary\AbstractBinary; +use Twig\Node\Expression\Unary\AbstractUnary; use Twig\NodeVisitor\NodeVisitorInterface; use Twig\TokenParser\TokenParserInterface; @@ -44,9 +45,9 @@ final class ExtensionSet private $functions; /** @var array */ private $dynamicFunctions; - /** @var array}> */ + /** @var array}> */ private $unaryOperators; - /** @var array, associativity: ExpressionParser::OPERATOR_*}> */ + /** @var array, associativity: ExpressionParser::OPERATOR_*}> */ private $binaryOperators; /** @var array|null */ private $globals; @@ -385,7 +386,7 @@ public function getTest(string $name): ?TwigTest } /** - * @return array}> + * @return array}> */ public function getUnaryOperators(): array { @@ -397,7 +398,7 @@ public function getUnaryOperators(): array } /** - * @return array, associativity: ExpressionParser::OPERATOR_*}> + * @return array, associativity: ExpressionParser::OPERATOR_*}> */ public function getBinaryOperators(): array { From c96aefdf045ea021f1f6af3f8f18206f8d43e5a2 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Mon, 25 Nov 2024 17:24:14 +0100 Subject: [PATCH 628/812] Add more coding standard rules --- doc/coding_standards.rst | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/doc/coding_standards.rst b/doc/coding_standards.rst index d7daa08b506..dcae56da19d 100644 --- a/doc/coding_standards.rst +++ b/doc/coding_standards.rst @@ -88,21 +88,34 @@ standards: [1, 2, 3] {'name': 'Fabien'} -* Use snake case for all variable names (provided by the application and - created in templates): +* Do not put any spaces before and after ``=`` in macro argument declarations: .. code-block:: twig - {% set name = 'Fabien' %} - {% set first_name = 'Fabien' %} + {% macro html_input(class="input") %} -* Use snake case for all function/filter/test names: +* Put exactly one space after the ``:`` sign in macro argument declarations: .. code-block:: twig + {% macro html_input(class: "input") %} + +* Use snake case for all variable names (provided by the application and + created in templates), function/filter/test names, argument names and named + arguments: + + .. code-block:: twig + + {% set name = 'Fabien' %} + {% set first_name = 'Fabien' %} + {{ 'Fabien Potencier'|to_lower_case }} {{ generate_random_number() }} + {% macro html_input(class_name) %} + + {{ html_input(class_name: 'pwd') }} + * Indent your code inside tags (use the same indentation as the one used for the target language of the rendered template): From 49f384f107ea2535256ccc608dbc1ddce6939cec Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Fri, 29 Nov 2024 21:57:12 +0100 Subject: [PATCH 629/812] Use [] instead of array() to represent a constant value in compiled code --- src/Compiler.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Compiler.php b/src/Compiler.php index 3166841e380..ce8b17c9811 100644 --- a/src/Compiler.php +++ b/src/Compiler.php @@ -170,7 +170,7 @@ public function repr($value) } elseif (\is_bool($value)) { $this->raw($value ? 'true' : 'false'); } elseif (\is_array($value)) { - $this->raw('array('); + $this->raw('['); $first = true; foreach ($value as $key => $v) { if (!$first) { @@ -181,7 +181,7 @@ public function repr($value) $this->raw(' => '); $this->repr($v); } - $this->raw(')'); + $this->raw(']'); } else { $this->string($value); } From 44516133e2f240d6f10012f9f5e8f2333e2b8ee5 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sat, 30 Nov 2024 09:33:57 +0100 Subject: [PATCH 630/812] Fix CS --- src/ExpressionParser.php | 8 ++++---- src/Extension/CoreExtension.php | 4 ++-- src/Node/Expression/Binary/AbstractBinary.php | 4 ++-- src/Node/Expression/BlockReferenceExpression.php | 2 +- src/Node/Expression/Filter/DefaultFilter.php | 2 +- src/Node/Expression/Filter/RawFilter.php | 2 +- src/Node/Expression/FilterExpression.php | 2 +- src/Node/Expression/NullCoalesceExpression.php | 4 ++-- src/Node/Expression/TempNameExpression.php | 4 ++-- src/Node/Expression/Test/DefinedTest.php | 2 +- src/Node/Expression/TestExpression.php | 2 +- src/Node/Expression/Unary/AbstractUnary.php | 2 +- src/Node/SetNode.php | 2 +- src/TokenParser/GuardTokenParser.php | 2 +- src/TokenParser/MacroTokenParser.php | 2 +- tests/NodeVisitor/OptimizerTest.php | 2 +- 16 files changed, 23 insertions(+), 23 deletions(-) diff --git a/src/ExpressionParser.php b/src/ExpressionParser.php index 3cb174de0e2..e7ba938054f 100644 --- a/src/ExpressionParser.php +++ b/src/ExpressionParser.php @@ -92,7 +92,7 @@ public function __construct( public function parseExpression($precedence = 0) { - if (func_num_args() > 1) { + if (\func_num_args() > 1) { trigger_deprecation('twig/twig', '3.15', 'Passing a second argument ($allowArrow) to "%s()" is deprecated.', __METHOD__); } @@ -153,7 +153,7 @@ private function triggerPrecedenceDeprecations(AbstractExpression $expr): void /** @var AbstractExpression $node */ $node = $expr->getNode('node'); foreach ($this->precedenceChanges as $operatorName => $changes) { - if (!in_array($unaryOp, $changes)) { + if (!\in_array($unaryOp, $changes)) { continue; } if ($node->hasAttribute('operator') && $operatorName === $node->getAttribute('operator')) { @@ -616,10 +616,10 @@ public function parseArguments() { $namedArguments = false; $definition = false; - if (func_num_args() > 1) { + if (\func_num_args() > 1) { $definition = func_get_arg(1); } - if (func_num_args() > 0) { + if (\func_num_args() > 0) { trigger_deprecation('twig/twig', '3.15', 'Passing arguments to "%s()" is deprecated.', __METHOD__); $namedArguments = func_get_arg(0); } diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index cb54adc753d..eb890b78746 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -1543,11 +1543,11 @@ public static function enumCases(string $enum): array public static function enum(string $enum): \UnitEnum { if (!enum_exists($enum)) { - throw new RuntimeError(sprintf('"%s" is not an enum.', $enum)); + throw new RuntimeError(\sprintf('"%s" is not an enum.', $enum)); } if (!$cases = $enum::cases()) { - throw new RuntimeError(sprintf('"%s" is an empty enum.', $enum)); + throw new RuntimeError(\sprintf('"%s" is an empty enum.', $enum)); } return $cases[0]; diff --git a/src/Node/Expression/Binary/AbstractBinary.php b/src/Node/Expression/Binary/AbstractBinary.php index ccd3be44fc6..09530273ab1 100644 --- a/src/Node/Expression/Binary/AbstractBinary.php +++ b/src/Node/Expression/Binary/AbstractBinary.php @@ -25,10 +25,10 @@ abstract class AbstractBinary extends AbstractExpression public function __construct(Node $left, Node $right, int $lineno) { if (!$left instanceof AbstractExpression) { - trigger_deprecation('twig/twig', '3.15', 'Not passing a "%s" instance to the "left" argument of "%s" is deprecated ("%s" given).', AbstractExpression::class, static::class, get_class($left)); + trigger_deprecation('twig/twig', '3.15', 'Not passing a "%s" instance to the "left" argument of "%s" is deprecated ("%s" given).', AbstractExpression::class, static::class, \get_class($left)); } if (!$right instanceof AbstractExpression) { - trigger_deprecation('twig/twig', '3.15', 'Not passing a "%s" instance to the "right" argument of "%s" is deprecated ("%s" given).', AbstractExpression::class, static::class, get_class($right)); + trigger_deprecation('twig/twig', '3.15', 'Not passing a "%s" instance to the "right" argument of "%s" is deprecated ("%s" given).', AbstractExpression::class, static::class, \get_class($right)); } parent::__construct(['left' => $left, 'right' => $right], [], $lineno); diff --git a/src/Node/Expression/BlockReferenceExpression.php b/src/Node/Expression/BlockReferenceExpression.php index 0094c7adbf9..a5a3cee3f77 100644 --- a/src/Node/Expression/BlockReferenceExpression.php +++ b/src/Node/Expression/BlockReferenceExpression.php @@ -28,7 +28,7 @@ class BlockReferenceExpression extends AbstractExpression public function __construct(Node $name, ?Node $template, int $lineno) { if (!$name instanceof AbstractExpression) { - trigger_deprecation('twig/twig', '3.15', 'Not passing a "%s" instance to the "node" argument of "%s" is deprecated ("%s" given).', AbstractExpression::class, static::class, get_class($name)); + trigger_deprecation('twig/twig', '3.15', 'Not passing a "%s" instance to the "node" argument of "%s" is deprecated ("%s" given).', AbstractExpression::class, static::class, \get_class($name)); } $nodes = ['name' => $name]; diff --git a/src/Node/Expression/Filter/DefaultFilter.php b/src/Node/Expression/Filter/DefaultFilter.php index 60ebc6b83da..cabd2aa1afd 100644 --- a/src/Node/Expression/Filter/DefaultFilter.php +++ b/src/Node/Expression/Filter/DefaultFilter.php @@ -42,7 +42,7 @@ class DefaultFilter extends FilterExpression public function __construct(Node $node, TwigFilter|ConstantExpression $filter, Node $arguments, int $lineno) { if (!$node instanceof AbstractExpression) { - trigger_deprecation('twig/twig', '3.15', 'Not passing a "%s" instance to the "node" argument of "%s" is deprecated ("%s" given).', AbstractExpression::class, static::class, get_class($node)); + trigger_deprecation('twig/twig', '3.15', 'Not passing a "%s" instance to the "node" argument of "%s" is deprecated ("%s" given).', AbstractExpression::class, static::class, \get_class($node)); } if ($filter instanceof TwigFilter) { diff --git a/src/Node/Expression/Filter/RawFilter.php b/src/Node/Expression/Filter/RawFilter.php index e70501e24cf..0a49e7c4fe4 100644 --- a/src/Node/Expression/Filter/RawFilter.php +++ b/src/Node/Expression/Filter/RawFilter.php @@ -32,7 +32,7 @@ class RawFilter extends FilterExpression public function __construct(Node $node, TwigFilter|ConstantExpression|null $filter = null, ?Node $arguments = null, int $lineno = 0) { if (!$node instanceof AbstractExpression) { - trigger_deprecation('twig/twig', '3.15', 'Not passing a "%s" instance to the "node" argument of "%s" is deprecated ("%s" given).', AbstractExpression::class, static::class, get_class($node)); + trigger_deprecation('twig/twig', '3.15', 'Not passing a "%s" instance to the "node" argument of "%s" is deprecated ("%s" given).', AbstractExpression::class, static::class, \get_class($node)); } parent::__construct($node, $filter ?: new TwigFilter('raw', null, ['is_safe' => ['all']]), $arguments ?: new EmptyNode(), $lineno ?: $node->getTemplateLine()); diff --git a/src/Node/Expression/FilterExpression.php b/src/Node/Expression/FilterExpression.php index 31fb90f1e09..6e0c486ab07 100644 --- a/src/Node/Expression/FilterExpression.php +++ b/src/Node/Expression/FilterExpression.php @@ -27,7 +27,7 @@ class FilterExpression extends CallExpression public function __construct(Node $node, TwigFilter|ConstantExpression $filter, Node $arguments, int $lineno) { if (!$node instanceof AbstractExpression) { - trigger_deprecation('twig/twig', '3.15', 'Not passing a "%s" instance to the "node" argument of "%s" is deprecated ("%s" given).', AbstractExpression::class, static::class, get_class($node)); + trigger_deprecation('twig/twig', '3.15', 'Not passing a "%s" instance to the "node" argument of "%s" is deprecated ("%s" given).', AbstractExpression::class, static::class, \get_class($node)); } if ($filter instanceof TwigFilter) { diff --git a/src/Node/Expression/NullCoalesceExpression.php b/src/Node/Expression/NullCoalesceExpression.php index 5c9a276890b..be2136ca579 100644 --- a/src/Node/Expression/NullCoalesceExpression.php +++ b/src/Node/Expression/NullCoalesceExpression.php @@ -32,10 +32,10 @@ public function __construct(Node $left, Node $right, int $lineno) trigger_deprecation('twig/twig', '3.16', \sprintf('"%s" is deprecated; use "%s" instead.', __CLASS__, NullCoalesceBinary::class)); if (!$left instanceof AbstractExpression) { - trigger_deprecation('twig/twig', '3.15', 'Not passing a "%s" instance to the "left" argument of "%s" is deprecated ("%s" given).', AbstractExpression::class, static::class, get_class($left)); + trigger_deprecation('twig/twig', '3.15', 'Not passing a "%s" instance to the "left" argument of "%s" is deprecated ("%s" given).', AbstractExpression::class, static::class, \get_class($left)); } if (!$right instanceof AbstractExpression) { - trigger_deprecation('twig/twig', '3.15', 'Not passing a "%s" instance to the "right" argument of "%s" is deprecated ("%s" given).', AbstractExpression::class, static::class, get_class($right)); + trigger_deprecation('twig/twig', '3.15', 'Not passing a "%s" instance to the "right" argument of "%s" is deprecated ("%s" given).', AbstractExpression::class, static::class, \get_class($right)); } $test = new DefinedTest(clone $left, new TwigTest('defined'), new EmptyNode(), $left->getTemplateLine()); diff --git a/src/Node/Expression/TempNameExpression.php b/src/Node/Expression/TempNameExpression.php index 925b0e7b378..8cb66a1936b 100644 --- a/src/Node/Expression/TempNameExpression.php +++ b/src/Node/Expression/TempNameExpression.php @@ -29,9 +29,9 @@ public function __construct(string|int|null $name, int $lineno) trigger_deprecation('twig/twig', '3.15', 'The "%s" class is deprecated.', self::class); } - if (null !== $name && (is_int($name) || ctype_digit($name))) { + if (null !== $name && (\is_int($name) || ctype_digit($name))) { $name = (int) $name; - } elseif (in_array($name, self::RESERVED_NAMES)) { + } elseif (\in_array($name, self::RESERVED_NAMES)) { $name = "\u{035C}".$name; } diff --git a/src/Node/Expression/Test/DefinedTest.php b/src/Node/Expression/Test/DefinedTest.php index b6c3ff6f963..7a0150d18b9 100644 --- a/src/Node/Expression/Test/DefinedTest.php +++ b/src/Node/Expression/Test/DefinedTest.php @@ -46,7 +46,7 @@ class DefinedTest extends TestExpression public function __construct(Node $node, TwigTest|string $name, ?Node $arguments, int $lineno) { if (!$node instanceof AbstractExpression) { - trigger_deprecation('twig/twig', '3.15', 'Not passing a "%s" instance to the "node" argument of "%s" is deprecated ("%s" given).', AbstractExpression::class, static::class, get_class($node)); + trigger_deprecation('twig/twig', '3.15', 'Not passing a "%s" instance to the "node" argument of "%s" is deprecated ("%s" given).', AbstractExpression::class, static::class, \get_class($node)); } if ($node instanceof NameExpression) { diff --git a/src/Node/Expression/TestExpression.php b/src/Node/Expression/TestExpression.php index 3ad6ac5579a..7b9a5413888 100644 --- a/src/Node/Expression/TestExpression.php +++ b/src/Node/Expression/TestExpression.php @@ -26,7 +26,7 @@ class TestExpression extends CallExpression public function __construct(Node $node, string|TwigTest $test, ?Node $arguments, int $lineno) { if (!$node instanceof AbstractExpression) { - trigger_deprecation('twig/twig', '3.15', 'Not passing a "%s" instance to the "node" argument of "%s" is deprecated ("%s" given).', AbstractExpression::class, static::class, get_class($node)); + trigger_deprecation('twig/twig', '3.15', 'Not passing a "%s" instance to the "node" argument of "%s" is deprecated ("%s" given).', AbstractExpression::class, static::class, \get_class($node)); } $nodes = ['node' => $node]; diff --git a/src/Node/Expression/Unary/AbstractUnary.php b/src/Node/Expression/Unary/AbstractUnary.php index 96739e2b8ae..d4746e73b7c 100644 --- a/src/Node/Expression/Unary/AbstractUnary.php +++ b/src/Node/Expression/Unary/AbstractUnary.php @@ -24,7 +24,7 @@ abstract class AbstractUnary extends AbstractExpression public function __construct(Node $node, int $lineno) { if (!$node instanceof AbstractExpression) { - trigger_deprecation('twig/twig', '3.15', 'Not passing a "%s" instance argument to "%s" is deprecated ("%s" given).', AbstractExpression::class, static::class, get_class($node)); + trigger_deprecation('twig/twig', '3.15', 'Not passing a "%s" instance argument to "%s" is deprecated ("%s" given).', AbstractExpression::class, static::class, \get_class($node)); } parent::__construct(['node' => $node], ['with_parentheses' => false], $lineno); diff --git a/src/Node/SetNode.php b/src/Node/SetNode.php index 6e0661edb51..4d97adb223d 100644 --- a/src/Node/SetNode.php +++ b/src/Node/SetNode.php @@ -34,7 +34,7 @@ public function __construct(bool $capture, Node $names, Node $values, int $linen if ($capture) { $safe = true; // Node::class === get_class($values) should be removed in Twig 4.0 - if (($values instanceof Nodes || Node::class === get_class($values)) && !count($values)) { + if (($values instanceof Nodes || Node::class === \get_class($values)) && !\count($values)) { $values = new ConstantExpression('', $values->getTemplateLine()); $capture = false; } elseif ($values instanceof TextNode) { diff --git a/src/TokenParser/GuardTokenParser.php b/src/TokenParser/GuardTokenParser.php index 17b221d4c85..546ae75793e 100644 --- a/src/TokenParser/GuardTokenParser.php +++ b/src/TokenParser/GuardTokenParser.php @@ -26,7 +26,7 @@ public function parse(Token $token): Node { $stream = $this->parser->getStream(); $typeToken = $stream->expect(Token::NAME_TYPE); - if (!in_array($typeToken->getValue(), ['function', 'filter', 'test'])) { + if (!\in_array($typeToken->getValue(), ['function', 'filter', 'test'])) { throw new SyntaxError(\sprintf('Supported guard types are function, filter and test, "%s" given.', $typeToken->getValue()), $typeToken->getLine(), $stream->getSourceContext()); } $method = 'get'.$typeToken->getValue(); diff --git a/src/TokenParser/MacroTokenParser.php b/src/TokenParser/MacroTokenParser.php index 7d47821b2be..33379be0319 100644 --- a/src/TokenParser/MacroTokenParser.php +++ b/src/TokenParser/MacroTokenParser.php @@ -75,7 +75,7 @@ private function parseDefinition(): ArrayExpression $stream = $this->parser->getStream(); $stream->expect(Token::PUNCTUATION_TYPE, '(', 'A list of arguments must begin with an opening parenthesis'); while (!$stream->test(Token::PUNCTUATION_TYPE, ')')) { - if (count($arguments)) { + if (\count($arguments)) { $stream->expect(Token::PUNCTUATION_TYPE, ',', 'Arguments must be separated by a comma'); // if the comma above was a trailing comma, early exit the argument parse loop diff --git a/tests/NodeVisitor/OptimizerTest.php b/tests/NodeVisitor/OptimizerTest.php index 12bdc12d127..5964b7b484a 100644 --- a/tests/NodeVisitor/OptimizerTest.php +++ b/tests/NodeVisitor/OptimizerTest.php @@ -70,7 +70,7 @@ public function testForVarOptimizer() public function checkForVarConfiguration(Node $node, $target) { foreach ($node as $n) { - if (NameExpression::class === get_class($n) && $target === $n->getAttribute('name')) { + if (NameExpression::class === \get_class($n) && $target === $n->getAttribute('name')) { $this->assertTrue($n->getAttribute('always_defined')); } else { $this->checkForVarConfiguration($n, $target); From 14fc89ebead133b80a3a2215ec332ce715b26df1 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sat, 30 Nov 2024 09:42:13 +0100 Subject: [PATCH 631/812] Fix CS --- extra/string-extra/StringExtension.php | 1 + src/ExpressionParser.php | 12 ++++-------- src/Extension/CoreExtension.php | 11 +++++------ src/Loader/FilesystemLoader.php | 2 +- src/Node/Expression/ConditionalExpression.php | 1 - src/Node/Expression/GetAttrExpression.php | 1 - src/Node/Expression/OperatorEscapeInterface.php | 2 +- src/Node/ModuleNode.php | 4 ++-- tests/LexerTest.php | 2 +- tests/Node/NodeTest.php | 2 +- 10 files changed, 16 insertions(+), 22 deletions(-) diff --git a/extra/string-extra/StringExtension.php b/extra/string-extra/StringExtension.php index 5327b9d0f82..3ee7b7f2e65 100644 --- a/extra/string-extra/StringExtension.php +++ b/extra/string-extra/StringExtension.php @@ -88,6 +88,7 @@ private function getInflector(string $locale): InflectorInterface if (!class_exists(SpanishInflector::class)) { throw new RuntimeError('SpanishInflector is not available.'); } + return $this->spanishInflector ?? $this->spanishInflector = new SpanishInflector(); case 'fr': return $this->frenchInflector ?? $this->frenchInflector = new FrenchInflector(); diff --git a/src/ExpressionParser.php b/src/ExpressionParser.php index e7ba938054f..eb60a43b3b0 100644 --- a/src/ExpressionParser.php +++ b/src/ExpressionParser.php @@ -931,10 +931,8 @@ private function parseSubscriptExpressionDot(Node $node): AbstractExpression $token = $stream->next(); if ( Token::NAME_TYPE == $token->getType() - || - Token::NUMBER_TYPE == $token->getType() - || - (Token::OPERATOR_TYPE == $token->getType() && preg_match(Lexer::REGEX_NAME, $token->getValue())) + || Token::NUMBER_TYPE == $token->getType() + || (Token::OPERATOR_TYPE == $token->getType() && preg_match(Lexer::REGEX_NAME, $token->getValue())) ) { $attribute = new ConstantExpression($token->getValue(), $token->getLine()); } else { @@ -949,11 +947,9 @@ private function parseSubscriptExpressionDot(Node $node): AbstractExpression if ( $node instanceof NameExpression - && - ( + && ( null !== $this->parser->getImportedSymbol('template', $node->getAttribute('name')) - || - '_self' === $node->getAttribute('name') && $attribute instanceof ConstantExpression + || '_self' === $node->getAttribute('name') && $attribute instanceof ConstantExpression ) ) { return new MacroReferenceExpression(new TemplateVariable($node->getAttribute('name'), $node->getTemplateLine()), 'macro_'.$attribute->getAttribute('value'), $arguments, $node->getTemplateLine()); diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index eb890b78746..a693d947e8c 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -55,7 +55,6 @@ use Twig\Node\Expression\FunctionNode\EnumCasesFunction; use Twig\Node\Expression\FunctionNode\EnumFunction; use Twig\Node\Expression\GetAttrExpression; -use Twig\Node\Expression\NullCoalesceExpression; use Twig\Node\Expression\ParentExpression; use Twig\Node\Expression\Test\ConstantTest; use Twig\Node\Expression\Test\DefinedTest; @@ -533,8 +532,8 @@ public static function dateConverter(Environment $env, $date, $format = null, $t * {# do something #} * {% endif %} * - * @param \DateTimeInterface|string|int|null $date A date, a timestamp or null to use the current time - * @param \DateTimeZone|string|false|null $timezone The target timezone, null to use the default, false to leave unchanged + * @param \DateTimeInterface|string|int|null $date A date, a timestamp or null to use the current time + * @param \DateTimeZone|string|false|null $timezone The target timezone, null to use the default, false to leave unchanged * * @return \DateTime|\DateTimeImmutable */ @@ -785,9 +784,9 @@ public static function last(string $charset, $item) * {{ [1, 2, 3]|join }} * {# returns 123 #} * - * @param iterable|array|string|float|int|bool|null $value An array - * @param string $glue The separator - * @param string|null $and The separator for the last pair + * @param iterable|array|string|float|int|bool|null $value An array + * @param string $glue The separator + * @param string|null $and The separator for the last pair * * @internal */ diff --git a/src/Loader/FilesystemLoader.php b/src/Loader/FilesystemLoader.php index cad163eadf2..49f2b891556 100644 --- a/src/Loader/FilesystemLoader.php +++ b/src/Loader/FilesystemLoader.php @@ -35,7 +35,7 @@ class FilesystemLoader implements LoaderInterface /** * @param string|string[] $paths A path or an array of paths where to look for templates - * @param string|null $rootPath The root path common to all relative paths (null for getcwd()) + * @param string|null $rootPath The root path common to all relative paths (null for getcwd()) */ public function __construct($paths = [], ?string $rootPath = null) { diff --git a/src/Node/Expression/ConditionalExpression.php b/src/Node/Expression/ConditionalExpression.php index 69d55a35835..1ca90f81131 100644 --- a/src/Node/Expression/ConditionalExpression.php +++ b/src/Node/Expression/ConditionalExpression.php @@ -13,7 +13,6 @@ namespace Twig\Node\Expression; use Twig\Compiler; -use Twig\Node\Expression\OperatorEscapeInterface; use Twig\Node\Expression\Ternary\ConditionalTernary; class ConditionalExpression extends AbstractExpression implements OperatorEscapeInterface diff --git a/src/Node/Expression/GetAttrExpression.php b/src/Node/Expression/GetAttrExpression.php index e7373de1194..0d4b96b141d 100644 --- a/src/Node/Expression/GetAttrExpression.php +++ b/src/Node/Expression/GetAttrExpression.php @@ -18,7 +18,6 @@ class GetAttrExpression extends AbstractExpression { - /** * @param ArrayExpression|NameExpression|null $arguments */ diff --git a/src/Node/Expression/OperatorEscapeInterface.php b/src/Node/Expression/OperatorEscapeInterface.php index 7b1e43e35a9..06db6c61657 100644 --- a/src/Node/Expression/OperatorEscapeInterface.php +++ b/src/Node/Expression/OperatorEscapeInterface.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ - namespace Twig\Node\Expression; +namespace Twig\Node\Expression; /** * Interface implemented by n-ary operators for n > 1. diff --git a/src/Node/ModuleNode.php b/src/Node/ModuleNode.php index 97c2089fdba..17840dd0a78 100644 --- a/src/Node/ModuleNode.php +++ b/src/Node/ModuleNode.php @@ -244,9 +244,9 @@ protected function compileConstructor(Compiler $compiler) ->string($key) ->raw(\sprintf(']; unset($_trait_%s_blocks[', $i)) ->string($key) - ->raw("]); \$this->traitAliases[") + ->raw(']); $this->traitAliases[') ->subcompile($value) - ->raw("] = ") + ->raw('] = ') ->string($key) ->raw(";\n\n") ; diff --git a/tests/LexerTest.php b/tests/LexerTest.php index d5640c8daa7..bfc7f9bb2a7 100644 --- a/tests/LexerTest.php +++ b/tests/LexerTest.php @@ -201,7 +201,7 @@ public static function getStringWithEscapedDelimiter() EOF, "\x6", ]; - yield [ + yield [ <<<'EOF' {{ '\065\x64' }} EOF, diff --git a/tests/Node/NodeTest.php b/tests/Node/NodeTest.php index a8d6b699ec5..13ad334bfd5 100644 --- a/tests/Node/NodeTest.php +++ b/tests/Node/NodeTest.php @@ -134,4 +134,4 @@ public function testNodeAttributeDeprecationWithAlternative() class NodeForTest extends Node { -} \ No newline at end of file +} From 07a4ed17d2b77afaf5f0772e1020812212d2bf3a Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Sat, 30 Nov 2024 09:51:13 +0100 Subject: [PATCH 632/812] Update coding standards --- doc/coding_standards.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/doc/coding_standards.rst b/doc/coding_standards.rst index dcae56da19d..58272883eba 100644 --- a/doc/coding_standards.rst +++ b/doc/coding_standards.rst @@ -88,17 +88,17 @@ standards: [1, 2, 3] {'name': 'Fabien'} -* Do not put any spaces before and after ``=`` in macro argument declarations: +* Put exactly one space before and after ``=`` in macro argument declarations: .. code-block:: twig - {% macro html_input(class="input") %} + {% macro html_input(class = "input") %} -* Put exactly one space after the ``:`` sign in macro argument declarations: +* Put exactly one space after the ``:`` sign when using named arguments: .. code-block:: twig - {% macro html_input(class: "input") %} + {{ html_input(class: "input") }} * Use snake case for all variable names (provided by the application and created in templates), function/filter/test names, argument names and named From 616cd06914b27a7b5c9e2361431a8d00f8ef3f8a Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sun, 1 Dec 2024 10:44:44 +0100 Subject: [PATCH 633/812] Remove deprecate usage of AssignNameExpression in For Node --- src/Node/ForNode.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Node/ForNode.php b/src/Node/ForNode.php index 53a950a90ca..4346afe5860 100644 --- a/src/Node/ForNode.php +++ b/src/Node/ForNode.php @@ -15,7 +15,7 @@ use Twig\Attribute\YieldReady; use Twig\Compiler; use Twig\Node\Expression\AbstractExpression; -use Twig\Node\Expression\AssignNameExpression; +use Twig\Node\Expression\Variable\AssignContextVariable; /** * Represents a for node. @@ -27,7 +27,7 @@ class ForNode extends Node { private $loop; - public function __construct(AssignNameExpression $keyTarget, AssignNameExpression $valueTarget, AbstractExpression $seq, ?Node $ifexpr, Node $body, ?Node $else, int $lineno) + public function __construct(AssignContextVariable $keyTarget, AssignContextVariable $valueTarget, AbstractExpression $seq, ?Node $ifexpr, Node $body, ?Node $else, int $lineno) { $body = new Nodes([$body, $this->loop = new ForLoopNode($lineno)]); From da4d96692abb9e72809f748dfe11bfed3857b06f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Andr=C3=A9?= Date: Sat, 30 Nov 2024 03:37:56 +0100 Subject: [PATCH 634/812] Support underscores in number literals --- CHANGELOG | 2 +- doc/templates.rst | 5 ++-- phpstan-baseline.neon | 6 +++++ src/Lexer.php | 16 ++++++++----- .../expressions/underscored_numbers.test | 24 +++++++++++++++++++ .../underscored_numbers_error.test | 8 +++++++ 6 files changed, 52 insertions(+), 9 deletions(-) create mode 100644 tests/Fixtures/expressions/underscored_numbers.test create mode 100644 tests/Fixtures/expressions/underscored_numbers_error.test diff --git a/CHANGELOG b/CHANGELOG index 5b3fa9eb5a0..cf6ae38ae81 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,6 @@ # 3.17.0 (2024-XX-XX) - * n/a + * Support underscores in number literals # 3.16.0 (2024-11-29) diff --git a/doc/templates.rst b/doc/templates.rst index 2f2775ab707..4235e6e9b51 100644 --- a/doc/templates.rst +++ b/doc/templates.rst @@ -612,7 +612,8 @@ exist: * ``42`` / ``42.23``: Integers and floating point numbers are created by writing the number down. If a dot is present the number is a float, - otherwise an integer. + otherwise an integer. Underscores can be used as digits separator to + improve readability (``-3_141.592_65`` is equivalent to ``-3141.59265``). * ``["first_name", "last_name"]``: Sequences are defined by a sequence of expressions separated by a comma (``,``) and wrapped with squared brackets (``[]``). @@ -1144,4 +1145,4 @@ Twig can be extended. If you want to create your own extensions, read the .. _`Modern Twig`: https://marketplace.visualstudio.com/items?itemName=Stanislav.vscode-twig .. _`Twig Language Server`: https://github.com/kaermorchen/twig-language-server/tree/master/packages/language-server .. _`Twiggy`: https://marketplace.visualstudio.com/items?itemName=moetelo.twiggy -.. _`PHP spaceship operator documentation`: https://www.php.net/manual/en/language.operators.comparison.php \ No newline at end of file +.. _`PHP spaceship operator documentation`: https://www.php.net/manual/en/language.operators.comparison.php diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 1121ae1b235..eabe060f2d6 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -23,3 +23,9 @@ parameters: identifier: parameter.phpDocType count: 5 path: src/Node/Node.php + + - # Adding 0 to the string representation of a number is valid and what we want here + message: '#^Binary operation "\+" between 0 and string results in an error\.$#' + identifier: binaryOp.invalid + count: 1 + path: src/Lexer.php diff --git a/src/Lexer.php b/src/Lexer.php index 4983313cf03..0338fd874ff 100644 --- a/src/Lexer.php +++ b/src/Lexer.php @@ -44,8 +44,16 @@ class Lexer public const STATE_INTERPOLATION = 4; public const REGEX_NAME = '/[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*/A'; - public const REGEX_NUMBER = '/[0-9]+(?:\.[0-9]+)?([Ee][\+\-][0-9]+)?/A'; public const REGEX_STRING = '/"([^#"\\\\]*(?:\\\\.[^#"\\\\]*)*)"|\'([^\'\\\\]*(?:\\\\.[^\'\\\\]*)*)\'/As'; + + public const REGEX_NUMBER = '/(?(DEFINE) + (?[0-9]+(_[0-9]+)*) # Integers (with underscores) 123_456 + (?\.(?&LNUM)) # Fractional part .456 + (?[eE][+-]?(?&LNUM)) # Exponent part E+10 + (?(?&LNUM)(?:(?&FRAC))?) # Decimal number 123_456.456 + )(?:(?&DNUM)(?:(?&EXPONENT))?) # 123_456.456E+10 + /Ax'; + public const REGEX_DQ_STRING_DELIM = '/"/A'; public const REGEX_DQ_STRING_PART = '/[^#"\\\\]*(?:(?:\\\\.|#(?!\{))[^#"\\\\]*)*/As'; public const REGEX_INLINE_COMMENT = '/#[^\n]*/A'; @@ -346,11 +354,7 @@ private function lexExpression(): void } // numbers elseif (preg_match(self::REGEX_NUMBER, $this->code, $match, 0, $this->cursor)) { - $number = (float) $match[0]; // floats - if (ctype_digit($match[0]) && $number <= \PHP_INT_MAX) { - $number = (int) $match[0]; // integers lower than the maximum - } - $this->pushToken(Token::NUMBER_TYPE, $number); + $this->pushToken(Token::NUMBER_TYPE, 0 + str_replace('_', '', $match[0])); $this->moveCursor($match[0]); } // punctuation diff --git a/tests/Fixtures/expressions/underscored_numbers.test b/tests/Fixtures/expressions/underscored_numbers.test new file mode 100644 index 00000000000..4a163ccb518 --- /dev/null +++ b/tests/Fixtures/expressions/underscored_numbers.test @@ -0,0 +1,24 @@ +--TEST-- +Twig compile numbers literals with underscores correctly +--TEMPLATE-- +{{ 0_0 is same as 0 ? 'ok' : 'ko' }} +{{ 1_23 is same as 123 ? 'ok' : 'ko' }} +{{ 12_3 is same as 123 ? 'ok' : 'ko' }} +{{ 1_2_3 is same as 123 ? 'ok' : 'ko' }} +{{ -1_2 is same as -12 ? 'ok' : 'ko' }} +{{ 1_2.3_4 is same as 12.34 ? 'ok' : 'ko' }} +{{ -1_2.3_4 is same as -12.34 ? 'ok' : 'ko' }} +{{ 1.2_3e-4 is same as 1.23e-4 ? 'ok' : 'ko' }} +{{ -1.2_3e+4 is same as -1.23e+4 ? 'ok' : 'ko' }} +--DATA-- +return [] +--EXPECT-- +ok +ok +ok +ok +ok +ok +ok +ok +ok diff --git a/tests/Fixtures/expressions/underscored_numbers_error.test b/tests/Fixtures/expressions/underscored_numbers_error.test new file mode 100644 index 00000000000..839d606f128 --- /dev/null +++ b/tests/Fixtures/expressions/underscored_numbers_error.test @@ -0,0 +1,8 @@ +--TEST-- +Twig does not allow to use 2 underscored between digits in numbers +--TEMPLATE-- +{{ 1__2 }} +--DATA-- +return [] +--EXCEPTION-- +Twig\Error\SyntaxError: Unexpected token "name" of value "__2" ("end of print statement" expected) in "index.twig" at line 2. From 5287da57cc9ada346c07f719413d21ff6a0c0af6 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Mon, 2 Dec 2024 09:57:02 +0100 Subject: [PATCH 635/812] Require "erusev/parsedown": "dev-master as 1.x-dev" --- extra/markdown-extra/composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extra/markdown-extra/composer.json b/extra/markdown-extra/composer.json index cf545836882..30adf4aa61d 100644 --- a/extra/markdown-extra/composer.json +++ b/extra/markdown-extra/composer.json @@ -21,7 +21,7 @@ }, "require-dev": { "symfony/phpunit-bridge": "^6.4|^7.0", - "erusev/parsedown": "^1.7", + "erusev/parsedown": "dev-master as 1.x-dev", "league/commonmark": "^1.0|^2.0", "league/html-to-markdown": "^4.8|^5.0", "michelf/php-markdown": "^1.8|^2.0" From f90105d0ac6deb12489f172da4480da831a8ca64 Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Mon, 2 Dec 2024 08:31:42 +0100 Subject: [PATCH 636/812] fix version numbers for deprecations --- CHANGELOG | 2 +- doc/deprecated.rst | 4 ++-- src/Node/Expression/ConditionalExpression.php | 2 +- src/Node/Expression/NullCoalesceExpression.php | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index cf6ae38ae81..2620c6af7b8 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,10 +1,10 @@ # 3.17.0 (2024-XX-XX) * Support underscores in number literals + * Deprecate `ConditionalExpression` and `NullCoalesceExpression` (use `ConditionalTernary` and `NullCoalesceBinary` instead) # 3.16.0 (2024-11-29) - * Deprecate `ConditionalExpression` and `NullCoalesceExpression` (use `ConditionalTernary` and `NullCoalesceBinary` instead) * Deprecate `InlinePrint` * Fix having macro variables starting with an underscore * Deprecate not passing a `Source` instance to `TokenStream` diff --git a/doc/deprecated.rst b/doc/deprecated.rst index 4d4d096adbe..a6679a9eb37 100644 --- a/doc/deprecated.rst +++ b/doc/deprecated.rst @@ -193,11 +193,11 @@ Nodes replacement. * The ``Twig\Node\Expression\NullCoalesceExpression`` class is deprecated as - of Twig 3.16, use ``Twig\Node\Expression\Binary\NullCoalesceBinary`` + of Twig 3.17, use ``Twig\Node\Expression\Binary\NullCoalesceBinary`` instead. * The ``Twig\Node\Expression\ConditionalExpression`` class is deprecated as of - Twig 3.16, use ``Twig\Node\Expression\Ternary\ConditionalTernary`` instead. + Twig 3.17, use ``Twig\Node\Expression\Ternary\ConditionalTernary`` instead. Node Visitors ------------- diff --git a/src/Node/Expression/ConditionalExpression.php b/src/Node/Expression/ConditionalExpression.php index 1ca90f81131..7fe309cf300 100644 --- a/src/Node/Expression/ConditionalExpression.php +++ b/src/Node/Expression/ConditionalExpression.php @@ -19,7 +19,7 @@ class ConditionalExpression extends AbstractExpression implements OperatorEscape { public function __construct(AbstractExpression $expr1, AbstractExpression $expr2, AbstractExpression $expr3, int $lineno) { - trigger_deprecation('twig/twig', '3.16', \sprintf('"%s" is deprecated; use "%s" instead.', __CLASS__, ConditionalTernary::class)); + trigger_deprecation('twig/twig', '3.17', \sprintf('"%s" is deprecated; use "%s" instead.', __CLASS__, ConditionalTernary::class)); parent::__construct(['expr1' => $expr1, 'expr2' => $expr2, 'expr3' => $expr3], [], $lineno); } diff --git a/src/Node/Expression/NullCoalesceExpression.php b/src/Node/Expression/NullCoalesceExpression.php index be2136ca579..c07bb36963a 100644 --- a/src/Node/Expression/NullCoalesceExpression.php +++ b/src/Node/Expression/NullCoalesceExpression.php @@ -29,7 +29,7 @@ class NullCoalesceExpression extends ConditionalExpression */ public function __construct(Node $left, Node $right, int $lineno) { - trigger_deprecation('twig/twig', '3.16', \sprintf('"%s" is deprecated; use "%s" instead.', __CLASS__, NullCoalesceBinary::class)); + trigger_deprecation('twig/twig', '3.17', \sprintf('"%s" is deprecated; use "%s" instead.', __CLASS__, NullCoalesceBinary::class)); if (!$left instanceof AbstractExpression) { trigger_deprecation('twig/twig', '3.15', 'Not passing a "%s" instance to the "left" argument of "%s" is deprecated ("%s" given).', AbstractExpression::class, static::class, \get_class($left)); From 32a75b7869ede17c43c572c0b15cb6b2bb33f80c Mon Sep 17 00:00:00 2001 From: Gregor Harlan Date: Thu, 5 Dec 2024 12:31:45 +0100 Subject: [PATCH 637/812] Fix ArrayAccess with objects as keys --- src/Extension/CoreExtension.php | 2 +- tests/Fixtures/expressions/array.test | 17 +++++++++++++++-- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index a693d947e8c..567c7e6771f 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -1644,7 +1644,7 @@ public static function getAttribute(Environment $env, Source $source, $object, $ // array if (Template::METHOD_CALL !== $type) { - $arrayItem = \is_bool($item) || \is_float($item) ? (int) $item : $item = (string) $item; + $arrayItem = \is_bool($item) || \is_float($item) ? (int) $item : $item; if ($sandboxed && $object instanceof \ArrayAccess && !\in_array($object::class, self::ARRAY_LIKE_CLASSES, true)) { try { diff --git a/tests/Fixtures/expressions/array.test b/tests/Fixtures/expressions/array.test index 1de76e93ee2..7d1c3b0adad 100644 --- a/tests/Fixtures/expressions/array.test +++ b/tests/Fixtures/expressions/array.test @@ -42,12 +42,19 @@ Twig supports array notation {# ArrayAccess #} {{ array_access['a'] }} +{# ObjectStorage #} +{{ object_storage[object] }} +{{ object_storage[object_storage]|default('bar') }} + {# array that does not exist #} {{ does_not_exist[0]|default('ok') }} {{ does_not_exist[0].does_not_exist_either|default('ok') }} {{ does_not_exist[0]['does_not_exist_either']|default('ok') }} --DATA-- -return ['bar' => 'bar', 'foo' => ['bar' => 'bar'], 'array_access' => new \ArrayObject(['a' => 'b'])] +$objectStorage = new SplObjectStorage(); +$object = new stdClass(); +$objectStorage[$object] = 'foo'; +return ['bar' => 'bar', 'foo' => ['bar' => 'bar'], 'array_access' => new \ArrayObject(['a' => 'b']), 'object_storage' => $objectStorage, 'object' => $object] --EXPECT-- 1,2 foo,bar @@ -71,11 +78,14 @@ a,b,c,d,e b +foo +bar + ok ok ok --DATA-- -return ['bar' => 'bar', 'foo' => ['bar' => 'bar'], 'array_access' => new \ArrayObject(['a' => 'b'])] +return ['bar' => 'bar', 'foo' => ['bar' => 'bar'], 'array_access' => new \ArrayObject(['a' => 'b']), 'object' => new stdClass()] --CONFIG-- return ['strict_variables' => false] --EXPECT-- @@ -101,6 +111,9 @@ a,b,c,d,e b + +bar + ok ok ok From e24078cd0e0f4ee07b6bc90faf5010f85c5f1ee0 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Tue, 10 Dec 2024 16:18:27 +0100 Subject: [PATCH 638/812] Update CHANGELOG --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index 2620c6af7b8..d7de8974290 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,6 @@ # 3.17.0 (2024-XX-XX) + * Fix ArrayAccess with objects as keys * Support underscores in number literals * Deprecate `ConditionalExpression` and `NullCoalesceExpression` (use `ConditionalTernary` and `NullCoalesceBinary` instead) From d3a64b742a5e74c57e3964d766e1032982145872 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Tue, 10 Dec 2024 16:19:11 +0100 Subject: [PATCH 639/812] Prepare the 3.17.0 release --- CHANGELOG | 2 +- src/Environment.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index d7de8974290..db7be52325c 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,4 +1,4 @@ -# 3.17.0 (2024-XX-XX) +# 3.17.0 (2024-12-10) * Fix ArrayAccess with objects as keys * Support underscores in number literals diff --git a/src/Environment.php b/src/Environment.php index a0b74eb2983..7bc2849dd52 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -44,12 +44,12 @@ */ class Environment { - public const VERSION = '3.17.0-DEV'; + public const VERSION = '3.17.0'; public const VERSION_ID = 31700; public const MAJOR_VERSION = 3; public const MINOR_VERSION = 17; public const RELEASE_VERSION = 0; - public const EXTRA_VERSION = 'DEV'; + public const EXTRA_VERSION = ''; private $charset; private $loader; From 31f61bd781e839f007b3f176262bffdbb8e34587 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Tue, 10 Dec 2024 16:20:20 +0100 Subject: [PATCH 640/812] Bump version --- CHANGELOG | 4 ++++ src/Environment.php | 8 ++++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index db7be52325c..481866a8887 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +# 3.17.1 (2024-XX-XX) + + * n/a + # 3.17.0 (2024-12-10) * Fix ArrayAccess with objects as keys diff --git a/src/Environment.php b/src/Environment.php index 7bc2849dd52..e18e2f8a664 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -44,12 +44,12 @@ */ class Environment { - public const VERSION = '3.17.0'; - public const VERSION_ID = 31700; + public const VERSION = '3.17.1-DEV'; + public const VERSION_ID = 31701; public const MAJOR_VERSION = 3; public const MINOR_VERSION = 17; - public const RELEASE_VERSION = 0; - public const EXTRA_VERSION = ''; + public const RELEASE_VERSION = 1; + public const EXTRA_VERSION = 'DEV'; private $charset; private $loader; From 1af334be8923f184714902848d5fec5190e6a5f1 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Wed, 11 Dec 2024 10:38:21 +0100 Subject: [PATCH 641/812] Fix the Elvis operator when there is some space between ? and : --- CHANGELOG | 2 +- src/Extension/CoreExtension.php | 1 + tests/Fixtures/expressions/ternary_operator_nothen.test | 8 ++++++++ 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 481866a8887..d6bf5cbd45a 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,6 @@ # 3.17.1 (2024-XX-XX) - * n/a + * Fix the Elvis operator when used as '? :' instead of '?:' # 3.17.0 (2024-12-10) diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index 567c7e6771f..b60944c4a4e 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -321,6 +321,7 @@ public function getOperators(): array '+' => ['precedence' => 500, 'class' => PosUnary::class], ], [ + '? :' => ['precedence' => 5, 'class' => ElvisBinary::class, 'associativity' => ExpressionParser::OPERATOR_RIGHT], '?:' => ['precedence' => 5, 'class' => ElvisBinary::class, 'associativity' => ExpressionParser::OPERATOR_RIGHT], '??' => ['precedence' => 300, 'precedence_change' => new OperatorPrecedenceChange('twig/twig', '3.15', 5), 'class' => NullCoalesceBinary::class, 'associativity' => ExpressionParser::OPERATOR_RIGHT], 'or' => ['precedence' => 10, 'class' => OrBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT], diff --git a/tests/Fixtures/expressions/ternary_operator_nothen.test b/tests/Fixtures/expressions/ternary_operator_nothen.test index ecd6b754656..53f8c0b3caf 100644 --- a/tests/Fixtures/expressions/ternary_operator_nothen.test +++ b/tests/Fixtures/expressions/ternary_operator_nothen.test @@ -3,8 +3,16 @@ Twig supports the ternary operator --TEMPLATE-- {{ 'YES' ?: 'NO' }} {{ 0 ?: 'NO' }} +{{ 'YES' ? : 'NO' }} +{{ 0 ? : 'NO' }} +{{ 'YES' ? : 'NO' }} +{{ 0 ? : 'NO' }} --DATA-- return [] --EXPECT-- YES NO +YES +NO +YES +NO From 4c5467f56aa085c721da132014cc968af1f4b508 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Thu, 12 Dec 2024 08:34:15 +0100 Subject: [PATCH 642/812] Fix the null coalescing operator when the test returns null --- CHANGELOG | 1 + src/Node/Expression/Binary/NullCoalesceBinary.php | 3 +-- tests/Fixtures/tests/null_coalesce.test | 5 ++++- tests/IntegrationTest.php | 10 ++++++++++ 4 files changed, 16 insertions(+), 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index d6bf5cbd45a..b0520220415 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,6 @@ # 3.17.1 (2024-XX-XX) + * Fix the null coalescing operator when the test returns null * Fix the Elvis operator when used as '? :' instead of '?:' # 3.17.0 (2024-12-10) diff --git a/src/Node/Expression/Binary/NullCoalesceBinary.php b/src/Node/Expression/Binary/NullCoalesceBinary.php index 6fb088fa62d..15b6e8ee937 100644 --- a/src/Node/Expression/Binary/NullCoalesceBinary.php +++ b/src/Node/Expression/Binary/NullCoalesceBinary.php @@ -29,8 +29,7 @@ public function __construct(AbstractExpression $left, AbstractExpression $right, parent::__construct($left, $right, $lineno); if (!$left instanceof NameExpression) { - $left = clone $left; - $test = new DefinedTest($left, new TwigTest('defined'), new EmptyNode(), $left->getTemplateLine()); + $test = new DefinedTest(clone $left, new TwigTest('defined'), new EmptyNode(), $left->getTemplateLine()); // for "block()", we don't need the null test as the return value is always a string if (!$left instanceof BlockReferenceExpression) { $test = new AndBinary( diff --git a/tests/Fixtures/tests/null_coalesce.test b/tests/Fixtures/tests/null_coalesce.test index f80b907178d..b73ec4634d8 100644 --- a/tests/Fixtures/tests/null_coalesce.test +++ b/tests/Fixtures/tests/null_coalesce.test @@ -13,8 +13,10 @@ Twig supports the ?? operator {{ nope ?? (nada ?? 'OK') }} {{ 1 + (nope ?? (nada ?? 2)) }} {{ 1 + (nope ?? 3) + (nada ?? 2) }} +{{ obj.null() ?? 'OK' }} +{{ obj.empty() ?? 'KO' }} --DATA-- -return ['bar' => 'OK', 'foo' => ['bar' => 'OK']] +return ['bar' => 'OK', 'foo' => ['bar' => 'OK'], 'obj' => new Twig\Tests\TwigTestFoo()] --EXPECT-- OK OK @@ -28,3 +30,4 @@ OK OK 3 6 +OK diff --git a/tests/IntegrationTest.php b/tests/IntegrationTest.php index 49ccf76cb96..2a88c6af203 100644 --- a/tests/IntegrationTest.php +++ b/tests/IntegrationTest.php @@ -77,6 +77,16 @@ public function getFoo() return 'foo'; } + public function getEmpty() + { + return ''; + } + + public function getNull() + { + return null; + } + public function getSelf() { return $this; From 677ef8da6497a03048192aeeb5aa3018e379ac71 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Thu, 12 Dec 2024 10:58:10 +0100 Subject: [PATCH 643/812] Prepare the 3.17.1 release --- src/Environment.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Environment.php b/src/Environment.php index e18e2f8a664..277b28e0e09 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -44,12 +44,12 @@ */ class Environment { - public const VERSION = '3.17.1-DEV'; + public const VERSION = '3.17.1'; public const VERSION_ID = 31701; public const MAJOR_VERSION = 3; public const MINOR_VERSION = 17; public const RELEASE_VERSION = 1; - public const EXTRA_VERSION = 'DEV'; + public const EXTRA_VERSION = ''; private $charset; private $loader; From 77376c93026e2e41ef3287276e97f94ce1ffc47e Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Thu, 12 Dec 2024 10:59:04 +0100 Subject: [PATCH 644/812] Bump version --- src/Environment.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Environment.php b/src/Environment.php index 277b28e0e09..d487c04de73 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -44,12 +44,12 @@ */ class Environment { - public const VERSION = '3.17.1'; - public const VERSION_ID = 31701; + public const VERSION = '3.17.2-DEV'; + public const VERSION_ID = 31702; public const MAJOR_VERSION = 3; public const MINOR_VERSION = 17; - public const RELEASE_VERSION = 1; - public const EXTRA_VERSION = ''; + public const RELEASE_VERSION = 2; + public const EXTRA_VERSION = 'DEV'; private $charset; private $loader; From fa8544ca4c360d789b5d84d552ddf5d6c050a529 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Thu, 12 Dec 2024 15:18:17 +0100 Subject: [PATCH 645/812] Update CHANGELOG --- CHANGELOG | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index b0520220415..d0584fcd186 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,4 +1,8 @@ -# 3.17.1 (2024-XX-XX) +# 3.17.2 (2024-XX-XX) + + * n/a + +# 3.17.1 (2024-12-12) * Fix the null coalescing operator when the test returns null * Fix the Elvis operator when used as '? :' instead of '?:' From 99ea35e1e82cf27fa2d6f1364508e9b4ad14183a Mon Sep 17 00:00:00 2001 From: seb-jean Date: Fri, 13 Dec 2024 16:24:26 +0100 Subject: [PATCH 646/812] Change `=` to `:` for named arguments --- doc/functions/html_cva.rst | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/doc/functions/html_cva.rst b/doc/functions/html_cva.rst index 5f86fb5b510..b8c1e27d698 100644 --- a/doc/functions/html_cva.rst +++ b/doc/functions/html_cva.rst @@ -20,8 +20,8 @@ function where you define ``base`` classes that should always be present and the {# templates/alert.html.twig #} {% set alert = html_cva( - base='alert', - variants={ + base: 'alert', + variants: { color: { blue: 'bg-blue', red: 'bg-red', @@ -84,8 +84,8 @@ when multiple other variant conditions are met: .. code-block:: html+twig {% set alert = html_cva( - base='alert', - variants={ + base: 'alert', + variants: { color: { blue: 'bg-blue', red: 'bg-red', @@ -97,7 +97,7 @@ when multiple other variant conditions are met: lg: 'text-lg', } }, - compoundVariants=[{ + compoundVariants: [{ // if color = red AND size = (md or lg), add the `font-bold` class color: ['red'], size: ['md', 'lg'], @@ -128,8 +128,8 @@ If no variants match, you can define a default set of classes to apply: .. code-block:: html+twig {% set alert = html_cva( - base='alert', - variants={ + base: 'alert', + variants: { color: { blue: 'bg-blue', red: 'bg-red', @@ -146,7 +146,7 @@ If no variants match, you can define a default set of classes to apply: lg: 'rounded-lg', } }, - defaultVariant={ + defaultVariant: { rounded: 'md', } ) %} From 0a61801d9aa7f64775b4f066f0aa75ec5a4f0fdd Mon Sep 17 00:00:00 2001 From: Faizan Akram Date: Thu, 12 Dec 2024 14:42:49 +0100 Subject: [PATCH 647/812] adds support for invoking closures --- CHANGELOG | 1 + src/Extension/CoreExtension.php | 7 +++++++ tests/TemplateTest.php | 4 ++++ 3 files changed, 12 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index b0520220415..f8e055b9a2a 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -2,6 +2,7 @@ * Fix the null coalescing operator when the test returns null * Fix the Elvis operator when used as '? :' instead of '?:' + * Support for invoking closures # 3.17.0 (2024-12-10) diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index b60944c4a4e..01e72856690 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -1740,6 +1740,10 @@ public static function getAttribute(Environment $env, Source $source, $object, $ static $propertyCheckers = []; + if ($object instanceof \Closure && '__invoke' === $item) { + return $isDefinedTest ? true : $object(); + } + if (isset($object->$item) || ($propertyCheckers[$object::class][$item] ??= self::getPropertyChecker($object::class, $item))($object, $item) ) { @@ -1777,6 +1781,9 @@ public static function getAttribute(Environment $env, Source $source, $object, $ // precedence: getXxx() > isXxx() > hasXxx() if (!isset($cache[$class])) { $methods = get_class_methods($object); + if ($object instanceof \Closure) { + $methods[] = '__invoke'; + } sort($methods); $lcMethods = array_map('strtolower', $methods); $classCache = []; diff --git a/tests/TemplateTest.php b/tests/TemplateTest.php index 4d2489962f7..eb0e2db7d5a 100644 --- a/tests/TemplateTest.php +++ b/tests/TemplateTest.php @@ -394,6 +394,10 @@ public static function getGetAttributeTests() [true, ['foo' => 'bar'], $arrayAccess, 'vars', [], $anyType], ]); + // test for Closure::__invoke() + $tests[] = [true, 'closure called', fn (): string => 'closure called', '__invoke', [], $anyType]; + $tests[] = [true, 'closure called', fn (): string => 'closure called', '__invoke', [], $methodType]; + // tests when input is not an array or object $tests = array_merge($tests, [ [false, null, 42, 'a', [], $anyType, 'Impossible to access an attribute ("a") on a int variable ("42") in "index.twig".'], From c72b50434d5f6d4f7d0188ac892da7b391465e9f Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Wed, 18 Dec 2024 07:29:21 +0100 Subject: [PATCH 648/812] Bump version --- CHANGELOG | 2 +- src/Environment.php | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 56e2de16654..e0bb7953f43 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,4 +1,4 @@ -# 3.17.2 (2024-XX-XX) +# 3.18.0 (2024-XX-XX) * n/a diff --git a/src/Environment.php b/src/Environment.php index d487c04de73..1a91fedb3a7 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -44,11 +44,11 @@ */ class Environment { - public const VERSION = '3.17.2-DEV'; - public const VERSION_ID = 31702; + public const VERSION = '3.18.0-DEV'; + public const VERSION_ID = 31800; public const MAJOR_VERSION = 3; - public const MINOR_VERSION = 17; - public const RELEASE_VERSION = 2; + public const MINOR_VERSION = 18; + public const RELEASE_VERSION = 0; public const EXTRA_VERSION = 'DEV'; private $charset; From 1915ee2812f8d281f9623e2e1e2bdadb8c2a92b3 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Mon, 16 Dec 2024 13:46:21 +0100 Subject: [PATCH 649/812] Add a way to stream template rendering --- CHANGELOG | 2 +- doc/api.rst | 30 +++++++++++++++++++++++++----- src/TemplateWrapper.php | 16 ++++++++++++++++ 3 files changed, 42 insertions(+), 6 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index e0bb7953f43..2411ca591ac 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,6 @@ # 3.18.0 (2024-XX-XX) - * n/a + * Add a way to stream template rendering (`TemplateWrapper::stream()` and `TemplateWrapper::streamBlock()` ) # 3.17.1 (2024-12-12) diff --git a/doc/api.rst b/doc/api.rst index 18427713340..0b35c4cc36a 100644 --- a/doc/api.rst +++ b/doc/api.rst @@ -40,15 +40,18 @@ templates from a database or other resources. the evaluated templates. For such a need, you can use any available PHP cache library. -Rendering Templates -------------------- +Loading Templates +----------------- -To load a template from a Twig environment, call the ``load()`` method which +To load a template, call the ``load()`` method on a Twig environment which returns a ``\Twig\TemplateWrapper`` instance:: $template = $twig->load('index.html'); -To render the template with some variables, call the ``render()`` method:: +Rendering Templates +------------------- + +To render a template with some variables, call the ``render()`` method:: echo $template->render(['the' => 'variables', 'go' => 'here']); @@ -56,7 +59,7 @@ To render the template with some variables, call the ``render()`` method:: The ``display()`` method is a shortcut to output the rendered template. -You can also load and render the template in one fell swoop:: +You can also load and render the template directly via the Environment:: echo $twig->render('index.html', ['the' => 'variables', 'go' => 'here']); @@ -65,6 +68,23 @@ If a template defines blocks, they can be rendered individually via the echo $template->renderBlock('block_name', ['the' => 'variables', 'go' => 'here']); +Streaming Templates +------------------- + +.. versionadded:: 3.18.0 + +To stream a template, call the ``stream()`` method`:: + + $template->stream(['the' => 'variables', 'go' => 'here']); + +To stream a specific template block, call the ``streamBlock()`` method:: + + $template->streamBlock('block_name', ['the' => 'variables', 'go' => 'here']); + +.. note:: + + The ``stream()`` and ``streamBlock()`` methods return an iterable. + .. _environment_options: Environment Options diff --git a/src/TemplateWrapper.php b/src/TemplateWrapper.php index 135c59188c2..5528037a602 100644 --- a/src/TemplateWrapper.php +++ b/src/TemplateWrapper.php @@ -30,6 +30,22 @@ public function __construct( ) { } + /** + * @return iterable + */ + public function stream(array $context = []): iterable + { + yield from $this->template->yield($context); + } + + /** + * @return iterable + */ + public function streamBlock(string $name, array $context = []): iterable + { + yield from $this->template->yieldBlock($name, $context); + } + public function render(array $context = []): string { return $this->template->render($context); From 2a6c986e851dd21204c84ee806dc13678353e87d Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Wed, 18 Dec 2024 07:45:11 +0100 Subject: [PATCH 650/812] Add a test --- extra/html-extra/Tests/Fixtures/html_classes.test | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/extra/html-extra/Tests/Fixtures/html_classes.test b/extra/html-extra/Tests/Fixtures/html_classes.test index 65ecaba6ada..9b26d1564d8 100644 --- a/extra/html-extra/Tests/Fixtures/html_classes.test +++ b/extra/html-extra/Tests/Fixtures/html_classes.test @@ -4,9 +4,14 @@ {{ html_classes('a', {'b': true, 'c': false}, 'd', false ? 'e', true ? 'f', '0') }} {% set class_a = 'a' %} {% set class_b = 'b' %} -{{ html_classes(class_a, {(class_b): true})}} +{%- set class_c -%} +c +{%- endset -%} +{{ html_classes(class_a, { (class_b): true }) }} +{{ html_classes(class_a, { (class_c): true }) }} --DATA-- return [] --EXPECT-- a b d f 0 a b +a c From 03792f643f8fc0f5e556f1701775648a630a5d1b Mon Sep 17 00:00:00 2001 From: Pepperoni1337 <107676055+Pepperoni1337@users.noreply.github.com> Date: Wed, 18 Dec 2024 08:13:12 +0100 Subject: [PATCH 651/812] Use .html.twig file extension in documentation instead of .html --- doc/api.rst | 20 ++++++++++---------- doc/deprecated.rst | 4 ++-- doc/functions/block.rst | 4 ++-- doc/functions/include.rst | 20 ++++++++++---------- doc/functions/parent.rst | 4 ++-- doc/functions/source.rst | 4 ++-- doc/intro.rst | 2 +- doc/recipes.rst | 38 +++++++++++++++++++------------------- doc/sandbox.rst | 2 +- doc/tags/deprecated.rst | 10 +++++----- doc/tags/embed.rst | 14 +++++++------- doc/tags/extends.rst | 22 +++++++++++----------- doc/tags/include.rst | 36 ++++++++++++++++++------------------ doc/tags/macro.rst | 12 ++++++------ doc/tags/sandbox.rst | 2 +- doc/tags/use.rst | 28 ++++++++++++++-------------- doc/templates.rst | 12 ++++++------ 17 files changed, 117 insertions(+), 117 deletions(-) diff --git a/doc/api.rst b/doc/api.rst index 18427713340..e091ee443e6 100644 --- a/doc/api.rst +++ b/doc/api.rst @@ -46,7 +46,7 @@ Rendering Templates To load a template from a Twig environment, call the ``load()`` method which returns a ``\Twig\TemplateWrapper`` instance:: - $template = $twig->load('index.html'); + $template = $twig->load('index.html.twig'); To render the template with some variables, call the ``render()`` method:: @@ -58,7 +58,7 @@ To render the template with some variables, call the ``render()`` method:: You can also load and render the template in one fell swoop:: - echo $twig->render('index.html', ['the' => 'variables', 'go' => 'here']); + echo $twig->render('index.html.twig', ['the' => 'variables', 'go' => 'here']); If a template defines blocks, they can be rendered individually via the ``renderBlock()`` call:: @@ -177,7 +177,7 @@ methods act on the "main" namespace):: Namespaced templates can be accessed via the special ``@namespace_name/template_path`` notation:: - $twig->render('@admin/index.html', []); + $twig->render('@admin/index.html.twig', []); ``\Twig\Loader\FilesystemLoader`` supports absolute and relative paths. Using relative paths is preferred as it makes the cache keys independent of the project root @@ -198,11 +198,11 @@ the directory might be different from the one used on production servers):: array of strings bound to template names:: $loader = new \Twig\Loader\ArrayLoader([ - 'index.html' => 'Hello {{ name }}!', + 'index.html.twig' => 'Hello {{ name }}!', ]); $twig = new \Twig\Environment($loader); - echo $twig->render('index.html', ['name' => 'Fabien']); + echo $twig->render('index.html.twig', ['name' => 'Fabien']); This loader is very useful for unit testing. It can also be used for small projects where storing all templates in a single PHP file might make sense. @@ -221,11 +221,11 @@ projects where storing all templates in a single PHP file might make sense. ``\Twig\Loader\ChainLoader`` delegates the loading of templates to other loaders:: $loader1 = new \Twig\Loader\ArrayLoader([ - 'base.html' => '{% block content %}{% endblock %}', + 'base.html.twig' => '{% block content %}{% endblock %}', ]); $loader2 = new \Twig\Loader\ArrayLoader([ - 'index.html' => '{% extends "base.html" %}{% block content %}Hello {{ name }}{% endblock %}', - 'base.html' => 'Will never be loaded', + 'index.html.twig' => '{% extends "base.html.twig" %}{% block content %}Hello {{ name }}{% endblock %}', + 'base.html.twig' => 'Will never be loaded', ]); $loader = new \Twig\Loader\ChainLoader([$loader1, $loader2]); @@ -233,8 +233,8 @@ projects where storing all templates in a single PHP file might make sense. $twig = new \Twig\Environment($loader); When looking for a template, Twig tries each loader in turn and returns as soon -as the template is found. When rendering the ``index.html`` template from the -above example, Twig will load it with ``$loader2`` but the ``base.html`` +as the template is found. When rendering the ``index.html.twig`` template from the +above example, Twig will load it with ``$loader2`` but the ``base.html.twig`` template will be loaded from ``$loader1``. .. note:: diff --git a/doc/deprecated.rst b/doc/deprecated.rst index a6679a9eb37..c3d6c32ac6a 100644 --- a/doc/deprecated.rst +++ b/doc/deprecated.rst @@ -259,12 +259,12 @@ Sandbox Before:: {% sandbox %} - {% include 'user_defined.twig' %} + {% include 'user_defined.html.twig' %} {% endsandbox %} After:: - {{ include('user_defined.twig', sandboxed: true) }} + {{ include('user_defined.html.twig', sandboxed: true) }} Testing Utilities ----------------- diff --git a/doc/functions/block.rst b/doc/functions/block.rst index ef8e5f4b614..acd13649283 100644 --- a/doc/functions/block.rst +++ b/doc/functions/block.rst @@ -17,7 +17,7 @@ template: .. code-block:: twig - {{ block("title", "common_blocks.twig") }} + {{ block("title", "common_blocks.html.twig") }} Use the ``defined`` test to check if a block exists in the context of the current template: @@ -28,7 +28,7 @@ current template: ... {% endif %} - {% if block("footer", "common_blocks.twig") is defined %} + {% if block("footer", "common_blocks.html.twig") is defined %} ... {% endif %} diff --git a/doc/functions/include.rst b/doc/functions/include.rst index c5dfc9a9f6d..3314943921f 100644 --- a/doc/functions/include.rst +++ b/doc/functions/include.rst @@ -5,7 +5,7 @@ The ``include`` function returns the rendered content of a template: .. code-block:: twig - {{ include('template.html') }} + {{ include('template.html.twig') }} {{ include(some_var) }} Included templates have access to the variables of the active context. @@ -18,8 +18,8 @@ additional variables: .. code-block:: twig - {# template.html will have access to the variables from the current context and the additional ones provided #} - {{ include('template.html', {name: 'Fabien'}) }} + {# template.html.twig will have access to the variables from the current context and the additional ones provided #} + {{ include('template.html.twig', {name: 'Fabien'}) }} You can disable access to the context by setting ``with_context`` to ``false``: @@ -27,35 +27,35 @@ You can disable access to the context by setting ``with_context`` to .. code-block:: twig {# only the name variable will be accessible #} - {{ include('template.html', {name: 'Fabien'}, with_context = false) }} + {{ include('template.html.twig', {name: 'Fabien'}, with_context = false) }} .. code-block:: twig {# no variables will be accessible #} - {{ include('template.html', with_context = false) }} + {{ include('template.html.twig', with_context = false) }} And if the expression evaluates to a ``\Twig\Template`` or a ``\Twig\TemplateWrapper`` instance, Twig will use it directly:: // {{ include(template) }} - $template = $twig->load('some_template.twig'); + $template = $twig->load('some_template.html.twig'); - $twig->display('template.twig', ['template' => $template]); + $twig->display('template.html.twig', ['template' => $template]); When you set the ``ignore_missing`` flag, Twig will return an empty string if the template does not exist: .. code-block:: twig - {{ include('sidebar.html', ignore_missing = true) }} + {{ include('sidebar.html.twig', ignore_missing = true) }} You can also provide a list of templates that are checked for existence before inclusion. The first template that exists will be rendered: .. code-block:: twig - {{ include(['page_detailed.html', 'page.html']) }} + {{ include(['page_detailed.html.twig', 'page.html.twig']) }} If ``ignore_missing`` is set, it will fall back to rendering nothing if none of the templates exist, otherwise it will throw an exception. @@ -65,7 +65,7 @@ When including a template created by an end user, you should consider .. code-block:: twig - {{ include('page.html', sandboxed: true) }} + {{ include('page.html.twig', sandboxed: true) }} Arguments --------- diff --git a/doc/functions/parent.rst b/doc/functions/parent.rst index 158bac78b1e..c9acc8733cf 100644 --- a/doc/functions/parent.rst +++ b/doc/functions/parent.rst @@ -6,7 +6,7 @@ parent block when overriding a block by using the ``parent`` function: .. code-block:: html+twig - {% extends "base.html" %} + {% extends "base.html.twig" %} {% block sidebar %}

    Table Of Contents

    @@ -15,7 +15,7 @@ parent block when overriding a block by using the ``parent`` function: {% endblock %} The ``parent()`` call will return the content of the ``sidebar`` block as -defined in the ``base.html`` template. +defined in the ``base.html.twig`` template. .. seealso:: diff --git a/doc/functions/source.rst b/doc/functions/source.rst index 080e2befe57..077ba91a402 100644 --- a/doc/functions/source.rst +++ b/doc/functions/source.rst @@ -5,7 +5,7 @@ The ``source`` function returns the content of a template without rendering it: .. code-block:: twig - {{ source('template.html') }} + {{ source('template.html.twig') }} {{ source(some_var) }} When you set the ``ignore_missing`` flag, Twig will return an empty string if @@ -13,7 +13,7 @@ the template does not exist: .. code-block:: twig - {{ source('template.html', ignore_missing = true) }} + {{ source('template.html.twig', ignore_missing = true) }} The function uses the same template loaders as the ones used to include templates. So, if you are using the filesystem loader, the templates are looked diff --git a/doc/intro.rst b/doc/intro.rst index 5b0256f224c..7d01e5beb62 100644 --- a/doc/intro.rst +++ b/doc/intro.rst @@ -69,6 +69,6 @@ filesystem loader:: 'cache' => '/path/to/compilation_cache', ]); - echo $twig->render('index.html', ['name' => 'Fabien']); + echo $twig->render('index.html.twig', ['name' => 'Fabien']); .. _`SymfonyCasts Twig Tutorial`: https://symfonycasts.com/screencast/twig diff --git a/doc/recipes.rst b/doc/recipes.rst index 5144f211139..402279d1e63 100644 --- a/doc/recipes.rst +++ b/doc/recipes.rst @@ -66,7 +66,7 @@ the request is made via Ajax and choose the layout accordingly: .. code-block:: twig - {% extends request.ajax ? "base_ajax.html" : "base.html" %} + {% extends request.ajax ? "base_ajax.html.twig" : "base.html.twig" %} {% block content %} This is the content to be displayed. @@ -80,9 +80,9 @@ instance, the name can depend on the value of a variable: .. code-block:: twig - {% include var ~ '_foo.html' %} + {% include var ~ '_foo.html.twig' %} -If ``var`` evaluates to ``index``, the ``index_foo.html`` template will be +If ``var`` evaluates to ``index``, the ``index_foo.html.twig`` template will be rendered. As a matter of fact, the template name can be any valid expression, such as @@ -90,7 +90,7 @@ the following: .. code-block:: twig - {% include var|default('index') ~ '_foo.html' %} + {% include var|default('index') ~ '_foo.html.twig' %} Overriding a Template that also extends itself ---------------------------------------------- @@ -108,13 +108,13 @@ But how do you combine both: *replace* a template that also extends itself (aka a template in a directory further in the list)? Let's say that your templates are loaded from both ``.../templates/mysite`` -and ``.../templates/default`` in this order. The ``page.twig`` template, +and ``.../templates/default`` in this order. The ``page.html.twig`` template, stored in ``.../templates/default`` reads as follows: .. code-block:: twig - {# page.twig #} - {% extends "layout.twig" %} + {# page.html.twig #} + {% extends "layout.html.twig" %} {% block content %} {% endblock %} @@ -125,8 +125,8 @@ might be tempted to write the following: .. code-block:: twig - {# page.twig in .../templates/mysite #} - {% extends "page.twig" %} {# from .../templates/default #} + {# page.html.twig in .../templates/mysite #} + {% extends "page.html.twig" %} {# from .../templates/default #} However, this will not work as Twig will always load the template from ``.../templates/mysite``. @@ -141,8 +141,8 @@ parent's full, unambiguous template path in the extends tag: .. code-block:: twig - {# page.twig in .../templates/mysite #} - {% extends "default/page.twig" %} {# from .../templates #} + {# page.html.twig in .../templates/mysite #} + {% extends "default/page.html.twig" %} {# from .../templates #} .. note:: @@ -393,15 +393,15 @@ First, let's create a temporary in-memory SQLite3 database to work with:: $dbh->exec('CREATE TABLE templates (name STRING, source STRING, last_modified INTEGER)'); $base = '{% block content %}{% endblock %}'; $index = ' - {% extends "base.twig" %} + {% extends "base.html.twig" %} {% block content %}Hello {{ name }}{% endblock %} '; $now = time(); - $dbh->prepare('INSERT INTO templates (name, source, last_modified) VALUES (?, ?, ?)')->execute(['base.twig', $base, $now]); - $dbh->prepare('INSERT INTO templates (name, source, last_modified) VALUES (?, ?, ?)')->execute(['index.twig', $index, $now]); + $dbh->prepare('INSERT INTO templates (name, source, last_modified) VALUES (?, ?, ?)')->execute(['base.html.twig', $base, $now]); + $dbh->prepare('INSERT INTO templates (name, source, last_modified) VALUES (?, ?, ?)')->execute(['index.html.twig', $index, $now]); We have created a simple ``templates`` table that hosts two templates: -``base.twig`` and ``index.twig``. +``base.html.twig`` and ``index.html.twig``. Now, let's define a loader able to use this database:: @@ -456,7 +456,7 @@ Finally, here is an example on how you can use it:: $loader = new DatabaseTwigLoader($dbh); $twig = new \Twig\Environment($loader); - echo $twig->render('index.twig', ['name' => 'Fabien']); + echo $twig->render('index.html.twig', ['name' => 'Fabien']); Using different Template Sources -------------------------------- @@ -474,15 +474,15 @@ logical name, and not the path from the filesystem:: $loader1 = new DatabaseTwigLoader($dbh); $loader2 = new \Twig\Loader\ArrayLoader([ - 'base.twig' => '{% block content %}{% endblock %}', + 'base.html.twig' => '{% block content %}{% endblock %}', ]); $loader = new \Twig\Loader\ChainLoader([$loader1, $loader2]); $twig = new \Twig\Environment($loader); - echo $twig->render('index.twig', ['name' => 'Fabien']); + echo $twig->render('index.html.twig', ['name' => 'Fabien']); -Now that the ``base.twig`` templates is defined in an array loader, you can +Now that the ``base.html.twig`` templates is defined in an array loader, you can remove it from the database, and everything else will still work as before. Loading a Template from a String diff --git a/doc/sandbox.rst b/doc/sandbox.rst index 7493fc3ea98..6e689583b89 100644 --- a/doc/sandbox.rst +++ b/doc/sandbox.rst @@ -61,7 +61,7 @@ function: .. code-block:: twig - {{ include('user.html', sandboxed: true) }} + {{ include('user.html.twig', sandboxed: true) }} You can sandbox all templates by passing ``true`` as the second argument of the extension constructor:: diff --git a/doc/tags/deprecated.rst b/doc/tags/deprecated.rst index 811e88e3ff1..da5a0b7b92b 100644 --- a/doc/tags/deprecated.rst +++ b/doc/tags/deprecated.rst @@ -6,9 +6,9 @@ PHP function) where the ``deprecated`` tag is used in a template: .. code-block:: twig - {# base.twig #} - {% deprecated 'The "base.twig" template is deprecated, use "layout.twig" instead.' %} - {% extends 'layout.twig' %} + {# base.html.twig #} + {% deprecated 'The "base.html.twig" template is deprecated, use "layout.html.twig" instead.' %} + {% extends 'layout.html.twig' %} You can also deprecate a macro in the following way: @@ -31,8 +31,8 @@ You can optionally add the package and the version that introduced the deprecati .. code-block:: twig - {% deprecated 'The "base.twig" template is deprecated, use "layout.twig" instead.' package='twig/twig' %} - {% deprecated 'The "base.twig" template is deprecated, use "layout.twig" instead.' package='twig/twig' version='3.11' %} + {% deprecated 'The "base.html.twig" template is deprecated, use "layout.html.twig" instead.' package='twig/twig' %} + {% deprecated 'The "base.html.twig" template is deprecated, use "layout.html.twig" instead.' package='twig/twig' version='3.11' %} .. note:: diff --git a/doc/tags/embed.rst b/doc/tags/embed.rst index 232403a1128..17013b9b045 100644 --- a/doc/tags/embed.rst +++ b/doc/tags/embed.rst @@ -11,8 +11,8 @@ Think of an embedded template as a "micro layout skeleton". .. code-block:: twig - {% embed "teasers_skeleton.twig" %} - {# These blocks are defined in "teasers_skeleton.twig" #} + {% embed "teasers_skeleton.html.twig" %} + {# These blocks are defined in "teasers_skeleton.html.twig" #} {# and we override them right here: #} {% block left_teaser %} Some content for the left teaser box @@ -111,14 +111,14 @@ code can live in a single base template, and the two different content structure let's call them "micro layouts" go into separate templates which are embedded as necessary: -Page template ``page_1.twig``: +Page template ``page_1.html.twig``: .. code-block:: twig - {% extends "layout_skeleton.twig" %} + {% extends "layout_skeleton.html.twig" %} {% block content %} - {% embed "vertical_boxes_skeleton.twig" %} + {% embed "vertical_boxes_skeleton.html.twig" %} {% block top %} Some content for the top box {% endblock %} @@ -129,7 +129,7 @@ Page template ``page_1.twig``: {% endembed %} {% endblock %} -And here is the code for ``vertical_boxes_skeleton.twig``: +And here is the code for ``vertical_boxes_skeleton.html.twig``: .. code-block:: html+twig @@ -145,7 +145,7 @@ And here is the code for ``vertical_boxes_skeleton.twig``: {% endblock %} -The goal of the ``vertical_boxes_skeleton.twig`` template being to factor +The goal of the ``vertical_boxes_skeleton.html.twig`` template being to factor out the HTML markup for the boxes. The ``embed`` tag takes the exact same arguments as the ``include`` tag: diff --git a/doc/tags/extends.rst b/doc/tags/extends.rst index 84d813e1742..5dc6ba0dd1a 100644 --- a/doc/tags/extends.rst +++ b/doc/tags/extends.rst @@ -9,7 +9,7 @@ The ``extends`` tag can be used to extend a template from another one. one extends tag called per rendering. However, Twig supports horizontal :doc:`reuse`. -Let's define a base template, ``base.html``, which defines a simple HTML +Let's define a base template, ``base.html.twig``, which defines a simple HTML skeleton document: .. code-block:: html+twig @@ -45,7 +45,7 @@ A child template might look like this: .. code-block:: html+twig - {% extends "base.html" %} + {% extends "base.html.twig" %} {% block title %}Index{% endblock %} {% block head %} @@ -156,16 +156,16 @@ instance, Twig will use it as the parent template:: // {% extends layout %} - $layout = $twig->load('some_layout_template.twig'); + $layout = $twig->load('some_layout_template.html.twig'); - $twig->display('template.twig', ['layout' => $layout]); + $twig->display('template.html.twig', ['layout' => $layout]); You can also provide a list of templates that are checked for existence. The first template that exists will be used as a parent: .. code-block:: twig - {% extends ['layout.html', 'base_layout.html'] %} + {% extends ['layout.html.twig', 'base_layout.html.twig'] %} Conditional Inheritance ----------------------- @@ -175,10 +175,10 @@ possible to make the inheritance mechanism conditional: .. code-block:: twig - {% extends standalone ? "minimum.html" : "base.html" %} + {% extends standalone ? "minimum.html.twig" : "base.html.twig" %} -In this example, the template will extend the "minimum.html" layout template -if the ``standalone`` variable evaluates to ``true``, and "base.html" +In this example, the template will extend the "minimum.html.twig" layout template +if the ``standalone`` variable evaluates to ``true``, and "base.html.twig" otherwise. How do blocks work? @@ -192,7 +192,7 @@ importantly, how it does not work: .. code-block:: html+twig - {# base.twig #} + {# base.html.twig #} {% for post in posts %} {% block post %}

    {{ post.title }}

    @@ -206,8 +206,8 @@ to make it overridable by a child template: .. code-block:: html+twig - {# child.twig #} - {% extends "base.twig" %} + {# child.html.twig #} + {% extends "base.html.twig" %} {% block post %}
    diff --git a/doc/tags/include.rst b/doc/tags/include.rst index c0a3c042592..3d2b2e0892b 100644 --- a/doc/tags/include.rst +++ b/doc/tags/include.rst @@ -6,9 +6,9 @@ of that file: .. code-block:: twig - {% include 'header.html' %} + {% include 'header.html.twig' %} Body - {% include 'footer.html' %} + {% include 'footer.html.twig' %} .. note:: @@ -25,17 +25,17 @@ of that file: {# Store a rendered template in a variable #} {% set content %} - {% include 'template.html' %} + {% include 'template.html.twig' %} {% endset %} {# vs #} - {% set content = include('template.html') %} + {% set content = include('template.html.twig') %} {# Apply filter on a rendered template #} {% apply upper %} - {% include 'template.html' %} + {% include 'template.html.twig' %} {% endapply %} {# vs #} - {{ include('template.html')|upper }} + {{ include('template.html.twig')|upper }} * The ``include`` function does not impose any specific order for arguments thanks to :ref:`named arguments `. @@ -49,23 +49,23 @@ You can add additional variables by passing them after the ``with`` keyword: .. code-block:: twig - {# template.html will have access to the variables from the current context and the additional ones provided #} - {% include 'template.html' with {'name': 'Fabien'} %} + {# template.html.twig will have access to the variables from the current context and the additional ones provided #} + {% include 'template.html.twig' with {'name': 'Fabien'} %} {% set vars = {'name': 'Fabien'} %} - {% include 'template.html' with vars %} + {% include 'template.html.twig' with vars %} You can disable access to the context by appending the ``only`` keyword: .. code-block:: twig {# only the name variable will be accessible #} - {% include 'template.html' with {'name': 'Fabien'} only %} + {% include 'template.html.twig' with {'name': 'Fabien'} only %} .. code-block:: twig {# no variables will be accessible #} - {% include 'template.html' only %} + {% include 'template.html.twig' only %} .. tip:: @@ -78,16 +78,16 @@ The template name can be any valid Twig expression: .. code-block:: twig {% include some_var %} - {% include ajax ? 'ajax.html' : 'not_ajax.html' %} + {% include ajax ? 'ajax.html.twig' : 'not_ajax.html.twig' %} And if the expression evaluates to a ``\Twig\Template`` or a ``\Twig\TemplateWrapper`` instance, Twig will use it directly:: // {% include template %} - $template = $twig->load('some_template.twig'); + $template = $twig->load('some_template.html.twig'); - $twig->display('template.twig', ['template' => $template]); + $twig->display('template.html.twig', ['template' => $template]); You can mark an include with ``ignore missing`` in which case Twig will ignore the statement if the template to be included does not exist. It has to be @@ -95,16 +95,16 @@ placed just after the template name. Here some valid examples: .. code-block:: twig - {% include 'sidebar.html' ignore missing %} - {% include 'sidebar.html' ignore missing with {'name': 'Fabien'} %} - {% include 'sidebar.html' ignore missing only %} + {% include 'sidebar.html.twig' ignore missing %} + {% include 'sidebar.html.twig' ignore missing with {'name': 'Fabien'} %} + {% include 'sidebar.html.twig' ignore missing only %} You can also provide a list of templates that are checked for existence before inclusion. The first template that exists will be included: .. code-block:: twig - {% include ['page_detailed.html', 'page.html'] %} + {% include ['page_detailed.html.twig', 'page.html.twig'] %} If ``ignore missing`` is given, it will fall back to rendering nothing if none of the templates exist, otherwise it will throw an exception. diff --git a/doc/tags/macro.rst b/doc/tags/macro.rst index 13c032915a9..ea173d5a877 100644 --- a/doc/tags/macro.rst +++ b/doc/tags/macro.rst @@ -49,9 +49,9 @@ tag: .. code-block:: twig - {% import "forms.twig" as forms %} + {% import "forms.html.twig" as forms %} -The above ``import`` call imports the ``forms.twig`` file (which can contain +The above ``import`` call imports the ``forms.html.twig`` file (which can contain only macros, or a template and some macros), and import the macros as attributes of the ``forms`` local variable. @@ -69,7 +69,7 @@ via the ``from`` tag: .. code-block:: html+twig - {% from 'forms.twig' import input as input_field, textarea %} + {% from 'forms.html.twig' import input as input_field, textarea %}

    {{ input_field('password', '', 'password') }}

    {{ input_field(name: 'password', type: 'password') }}

    @@ -82,7 +82,7 @@ via the ``from`` tag: .. code-block:: twig - {% from 'forms.twig' import input as include %} + {% from 'forms.html.twig' import input as include %} {# include refers to the macro and not to the built-in "include" function #} {{ include() }} @@ -126,9 +126,9 @@ You can check if a macro is defined via the ``defined`` test: .. code-block:: twig - {% import "macros.twig" as macros %} + {% import "macros.html.twig" as macros %} - {% from "macros.twig" import hello %} + {% from "macros.html.twig" import hello %} {% if macros.hello is defined -%} OK diff --git a/doc/tags/sandbox.rst b/doc/tags/sandbox.rst index 57f1e5d1e17..b9b9a8dd6c6 100644 --- a/doc/tags/sandbox.rst +++ b/doc/tags/sandbox.rst @@ -12,7 +12,7 @@ template, when sandboxing is not enabled globally for the Twig environment: .. code-block:: twig {% sandbox %} - {% include 'user.html' %} + {% include 'user.html.twig' %} {% endsandbox %} .. warning:: diff --git a/doc/tags/use.rst b/doc/tags/use.rst index 2aca6a01fb4..d26dc4f2cb4 100644 --- a/doc/tags/use.rst +++ b/doc/tags/use.rst @@ -14,7 +14,7 @@ debug: .. code-block:: twig - {% extends "base.html" %} + {% extends "base.html.twig" %} {% block title %}{% endblock %} {% block content %}{% endblock %} @@ -24,19 +24,19 @@ but without the associated complexity: .. code-block:: twig - {% extends "base.html" %} + {% extends "base.html.twig" %} - {% use "blocks.html" %} + {% use "blocks.html.twig" %} {% block title %}{% endblock %} {% block content %}{% endblock %} The ``use`` statement tells Twig to import the blocks defined in -``blocks.html`` into the current template (it's like macros, but for blocks): +``blocks.html.twig`` into the current template (it's like macros, but for blocks): .. code-block:: twig - {# blocks.html #} + {# blocks.html.twig #} {% block sidebar %}{% endblock %} @@ -46,7 +46,7 @@ imported blocks are not outputted automatically): .. code-block:: twig - {% extends "base.html" %} + {% extends "base.html.twig" %} {% block sidebar %}{% endblock %} {% block title %}{% endblock %} @@ -64,14 +64,14 @@ imported blocks are not outputted automatically): passed to the template, the template reference cannot be an expression. The main template can also override any imported block. If the template -already defines the ``sidebar`` block, then the one defined in ``blocks.html`` +already defines the ``sidebar`` block, then the one defined in ``blocks.html.twig`` is ignored. To avoid name conflicts, you can rename imported blocks: .. code-block:: twig - {% extends "base.html" %} + {% extends "base.html.twig" %} - {% use "blocks.html" with sidebar as base_sidebar, title as base_title %} + {% use "blocks.html.twig" with sidebar as base_sidebar, title as base_title %} {% block sidebar %}{% endblock %} {% block title %}{% endblock %} @@ -83,9 +83,9 @@ template: .. code-block:: twig - {% extends "base.html" %} + {% extends "base.html.twig" %} - {% use "blocks.html" %} + {% use "blocks.html.twig" %} {% block sidebar %} {{ parent() }} @@ -95,7 +95,7 @@ template: {% block content %}{% endblock %} In this example, ``parent()`` will correctly call the ``sidebar`` block from -the ``blocks.html`` template. +the ``blocks.html.twig`` template. .. tip:: @@ -103,9 +103,9 @@ the ``blocks.html`` template. .. code-block:: twig - {% extends "base.html" %} + {% extends "base.html.twig" %} - {% use "blocks.html" with sidebar as parent_sidebar %} + {% use "blocks.html.twig" with sidebar as parent_sidebar %} {% block sidebar %} {{ block('parent_sidebar') }} diff --git a/doc/templates.rst b/doc/templates.rst index 4235e6e9b51..18052f1c069 100644 --- a/doc/templates.rst +++ b/doc/templates.rst @@ -377,7 +377,7 @@ and return the rendered content of that template into the current one: .. code-block:: twig - {{ include('sidebar.html') }} + {{ include('sidebar.html.twig') }} By default, included templates have access to the same context as the template which includes them. This means that any variable defined in the main template @@ -386,10 +386,10 @@ will be available in the included template too: .. code-block:: twig {% for box in boxes %} - {{ include('render_box.html') }} + {{ include('render_box.html.twig') }} {% endfor %} -The included template ``render_box.html`` is able to access the ``box`` variable. +The included template ``render_box.html.twig`` is able to access the ``box`` variable. The name of the template depends on the template loader. For instance, the ``\Twig\Loader\FilesystemLoader`` allows you to access other templates by giving the @@ -397,7 +397,7 @@ filename. You can access templates in subdirectories with a slash: .. code-block:: twig - {{ include('sections/articles/sidebar.html') }} + {{ include('sections/articles/sidebar.html.twig') }} This behavior depends on the application embedding Twig. @@ -411,7 +411,7 @@ override. It's easier to understand the concept by starting with an example. -Let's define a base template, ``base.html``, which defines an HTML skeleton +Let's define a base template, ``base.html.twig``, which defines an HTML skeleton document that might be used for a two-column page: .. code-block:: html+twig @@ -443,7 +443,7 @@ A child template might look like this: .. code-block:: html+twig - {% extends "base.html" %} + {% extends "base.html.twig" %} {% block title %}Index{% endblock %} {% block head %} From c13ca48ca568e81a4bcb43e5437b181ec5ddaf83 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Wed, 18 Dec 2024 11:13:19 +0100 Subject: [PATCH 652/812] Use .html.twig in code example instead of .html --- src/TokenParser/FromTokenParser.php | 2 +- src/TokenParser/ImportTokenParser.php | 2 +- src/TokenParser/IncludeTokenParser.php | 4 ++-- src/TokenParser/SandboxTokenParser.php | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/TokenParser/FromTokenParser.php b/src/TokenParser/FromTokenParser.php index 3bb4201a3dd..c8732df29f6 100644 --- a/src/TokenParser/FromTokenParser.php +++ b/src/TokenParser/FromTokenParser.php @@ -21,7 +21,7 @@ /** * Imports macros. * - * {% from 'forms.html' import forms %} + * {% from 'forms.html.twig' import forms %} * * @internal */ diff --git a/src/TokenParser/ImportTokenParser.php b/src/TokenParser/ImportTokenParser.php index 5b3a5f2b8d4..f23584a5a42 100644 --- a/src/TokenParser/ImportTokenParser.php +++ b/src/TokenParser/ImportTokenParser.php @@ -20,7 +20,7 @@ /** * Imports macros. * - * {% import 'forms.html' as forms %} + * {% import 'forms.html.twig' as forms %} * * @internal */ diff --git a/src/TokenParser/IncludeTokenParser.php b/src/TokenParser/IncludeTokenParser.php index 466f2288c63..a72250cb290 100644 --- a/src/TokenParser/IncludeTokenParser.php +++ b/src/TokenParser/IncludeTokenParser.php @@ -19,9 +19,9 @@ /** * Includes a template. * - * {% include 'header.html' %} + * {% include 'header.html.twig' %} * Body - * {% include 'footer.html' %} + * {% include 'footer.html.twig' %} * * @internal */ diff --git a/src/TokenParser/SandboxTokenParser.php b/src/TokenParser/SandboxTokenParser.php index a7260ac46fc..536c14f30ba 100644 --- a/src/TokenParser/SandboxTokenParser.php +++ b/src/TokenParser/SandboxTokenParser.php @@ -22,7 +22,7 @@ * Marks a section of a template as untrusted code that must be evaluated in the sandbox mode. * * {% sandbox %} - * {% include 'user.html' %} + * {% include 'user.html.twig' %} * {% endsandbox %} * * @see https://twig.symfony.com/doc/api.html#sandbox-extension for details From d78cb00f8994439fb38cc231aa3efd90abb893f5 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Wed, 18 Dec 2024 12:17:58 +0100 Subject: [PATCH 653/812] Fix doc --- doc/tags/use.rst | 1 - 1 file changed, 1 deletion(-) diff --git a/doc/tags/use.rst b/doc/tags/use.rst index d26dc4f2cb4..52b80c43a56 100644 --- a/doc/tags/use.rst +++ b/doc/tags/use.rst @@ -37,7 +37,6 @@ The ``use`` statement tells Twig to import the blocks defined in .. code-block:: twig {# blocks.html.twig #} - {% block sidebar %}{% endblock %} In this example, the ``use`` statement imports the ``sidebar`` block into the From 84daad9a2773dc6e19c38431a6e5d70c48aa5f1e Mon Sep 17 00:00:00 2001 From: Javier Eguiluz Date: Wed, 18 Dec 2024 15:17:51 +0100 Subject: [PATCH 654/812] [Doc] Update the docs about attribute() function --- doc/functions/attribute.rst | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/doc/functions/attribute.rst b/doc/functions/attribute.rst index 7757fa08f0d..6a0d5ada9ec 100644 --- a/doc/functions/attribute.rst +++ b/doc/functions/attribute.rst @@ -3,21 +3,30 @@ .. warning:: - The ``attribute`` filter is deprecated as of Twig 3.15. Use the + The ``attribute`` function is deprecated as of Twig 3.15. Use the :ref:`dot operator ` that now accepts any expression when wrapped with parenthesis. - Note that this filter will still be available in Twig 4.0 to allow a + Note that this function will still be available in Twig 4.0 to allow a smoother upgrade path. -The ``attribute`` function can be used to access a "dynamic" attribute of a -variable: +The ``attribute`` function lets you access an attribute, method, or property of +an object or array when the name of that attribute, method, or property is stored +in a variable or dynamically generated with an expression: .. code-block:: twig - {{ attribute(object, method) }} - {{ attribute(object, method, arguments) }} - {{ attribute(array, item) }} + {# method_name is a variable that stores the method to call #} + {{ attribute(object, method_name) }} + + {# you can also pass arguments when calling a method #} + {{ attribute(object, method_name, arguments) }} + + {# the method/property name can be the result of evaluating an expression #} + {{ attribute(object, 'some_property_' ~ user.type) }} + + {# in addition to objects, this function works with plain arrays as well #} + {{ attribute(array, item_name) }} In addition, the ``defined`` test can check for the existence of a dynamic attribute: @@ -29,7 +38,7 @@ attribute: .. note:: The resolution algorithm is the same as the one used for the ``.`` - operator, except that the item can be any valid expression. + operator. Arguments --------- From b53c639f7de33cfaad50053070b4c62399f72df5 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Thu, 19 Dec 2024 08:36:59 +0100 Subject: [PATCH 655/812] Ignore exceptions from undefined handlers when using the guard tag --- CHANGELOG | 1 + src/TokenParser/GuardTokenParser.php | 6 +++++- tests/TokenParser/GuardTokenParserTest.php | 22 ++++++++++++++++++++++ 3 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 tests/TokenParser/GuardTokenParserTest.php diff --git a/CHANGELOG b/CHANGELOG index 2411ca591ac..bc1d00b8d98 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,6 @@ # 3.18.0 (2024-XX-XX) + * Ignore `SyntaxError` exceptions from undefined handlers when using the `guard` tag * Add a way to stream template rendering (`TemplateWrapper::stream()` and `TemplateWrapper::streamBlock()` ) # 3.17.1 (2024-12-12) diff --git a/src/TokenParser/GuardTokenParser.php b/src/TokenParser/GuardTokenParser.php index 546ae75793e..1fcf76cd740 100644 --- a/src/TokenParser/GuardTokenParser.php +++ b/src/TokenParser/GuardTokenParser.php @@ -33,7 +33,11 @@ public function parse(Token $token): Node $nameToken = $stream->expect(Token::NAME_TYPE); - $exists = null !== $this->parser->getEnvironment()->$method($nameToken->getValue()); + try { + $exists = null !== $this->parser->getEnvironment()->$method($nameToken->getValue()); + } catch (SyntaxError) { + $exists = false; + } $stream->expect(Token::BLOCK_END_TYPE); if ($exists) { diff --git a/tests/TokenParser/GuardTokenParserTest.php b/tests/TokenParser/GuardTokenParserTest.php new file mode 100644 index 00000000000..4c63623284a --- /dev/null +++ b/tests/TokenParser/GuardTokenParserTest.php @@ -0,0 +1,22 @@ +expectNotToPerformAssertions(); + + $env = new Environment(new ArrayLoader(), ['cache' => false, 'autoescape' => false]); + $env->registerUndefinedFunctionCallback(fn ($name) => throw new SyntaxError('boom.')); + (new Parser($env))->parse($env->tokenize(new Source('{% guard function boom %}{% endguard %}', ''))); + } +} From dc329fd44e67dee620330965be62d46b76d5f292 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Andr=C3=A9?= Date: Thu, 26 Dec 2024 10:28:13 +0100 Subject: [PATCH 656/812] Fix MacroNode $name argument can be overridden Fix #4515 --- src/Node/MacroNode.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Node/MacroNode.php b/src/Node/MacroNode.php index ab37d85a54b..db3ca458c88 100644 --- a/src/Node/MacroNode.php +++ b/src/Node/MacroNode.php @@ -41,8 +41,8 @@ public function __construct(string $name, Node $body, Node $arguments, int $line trigger_deprecation('twig/twig', '3.15', \sprintf('Not passing a "%s" instance as the "arguments" argument of the "%s" constructor is deprecated ("%s" given).', ArrayExpression::class, static::class, $arguments::class)); $args = new ArrayExpression([], $arguments->getTemplateLine()); - foreach ($arguments as $name => $default) { - $args->addElement($default, new LocalVariable($name, $default->getTemplateLine())); + foreach ($arguments as $n => $default) { + $args->addElement($default, new LocalVariable($n, $default->getTemplateLine())); } $arguments = $args; } From 4b87103468b27f4defb3f2472aac506a0cc5fb31 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Thu, 26 Dec 2024 14:52:00 +0100 Subject: [PATCH 657/812] Replace Twigfiddle by the new Twig playground --- doc/templates.rst | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/doc/templates.rst b/doc/templates.rst index 18052f1c069..2bf0a0b2c0b 100644 --- a/doc/templates.rst +++ b/doc/templates.rst @@ -41,8 +41,13 @@ There are two kinds of delimiters: ``{% ... %}`` and ``{{ ... }}``. The first one is used to execute statements such as for-loops, the latter outputs the result of an expression. -IDEs Integration ----------------- +.. tip:: + + To experiment with Twig, you can use the `Twig Playground + `_. + +Third-party Integrations +------------------------ Many IDEs support syntax highlighting and auto-completion for Twig: @@ -63,9 +68,6 @@ Many IDEs support syntax highlighting and auto-completion for Twig: You might also be interested in: -* `TwigFiddle`_: an online service that allows you to execute Twig templates - from a browser; it supports all versions of Twig - * `Twig Language Server`_: provides some language features like syntax highlighting, diagnostics, auto complete, ... @@ -1138,7 +1140,6 @@ Twig can be extended. If you want to create your own extensions, read the .. _`web-mode.el`: https://web-mode.org/ .. _`regular expressions`: https://www.php.net/manual/en/pcre.pattern.php .. _`PHP-twig for atom`: https://github.com/reesef/php-twig -.. _`TwigFiddle`: https://twigfiddle.com/ .. _`TwigQI`: https://github.com/alisqi/TwigQI .. _`TwigStan`: https://github.com/twigstan/twigstan .. _`Twig pack`: https://marketplace.visualstudio.com/items?itemName=bajdzis.vscode-twig-pack From 2cf1939d0142ba65cdea288f84624f9177da3fb7 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sat, 28 Dec 2024 11:18:00 +0100 Subject: [PATCH 658/812] Fix unary operator precedence change --- CHANGELOG | 3 ++- src/ExpressionParser.php | 2 +- tests/ExpressionParserTest.php | 24 ++++++++++++++++++++++++ 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index bc1d00b8d98..21fb43f107f 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,7 +1,8 @@ # 3.18.0 (2024-XX-XX) + * Fix unary operator precedence change * Ignore `SyntaxError` exceptions from undefined handlers when using the `guard` tag - * Add a way to stream template rendering (`TemplateWrapper::stream()` and `TemplateWrapper::streamBlock()` ) + * Add a way to stream template rendering (`TemplateWrapper::stream()` and `TemplateWrapper::streamBlock()`) # 3.17.1 (2024-12-12) diff --git a/src/ExpressionParser.php b/src/ExpressionParser.php index eb60a43b3b0..0f1b0ed366a 100644 --- a/src/ExpressionParser.php +++ b/src/ExpressionParser.php @@ -149,7 +149,6 @@ private function triggerPrecedenceDeprecations(AbstractExpression $expr): void return; } $target = explode('_', $unaryOp)[1]; - $change = $this->unaryOperators[$target]['precedence_change']; /** @var AbstractExpression $node */ $node = $expr->getNode('node'); foreach ($this->precedenceChanges as $operatorName => $changes) { @@ -157,6 +156,7 @@ private function triggerPrecedenceDeprecations(AbstractExpression $expr): void continue; } if ($node->hasAttribute('operator') && $operatorName === $node->getAttribute('operator')) { + $change = $this->unaryOperators[$target]['precedence_change']; trigger_deprecation($change->getPackage(), $change->getVersion(), \sprintf('Add explicit parentheses around the "%s" unary operator to avoid behavior change in the next major version as its precedence will change in "%s" at line %d.', $target, $this->parser->getStream()->getSourceContext()->getName(), $node->getTemplateLine())); } } diff --git a/tests/ExpressionParserTest.php b/tests/ExpressionParserTest.php index 162f8836c1e..9b9d9b50488 100644 --- a/tests/ExpressionParserTest.php +++ b/tests/ExpressionParserTest.php @@ -14,6 +14,7 @@ use PHPUnit\Framework\TestCase; use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; use Twig\Attribute\FirstClassTwigCallableReady; +use Twig\Compiler; use Twig\Environment; use Twig\Error\SyntaxError; use Twig\Extension\AbstractExtension; @@ -24,6 +25,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\Variable\ContextVariable; use Twig\Node\Node; use Twig\Parser; @@ -564,6 +566,28 @@ public function testTwoWordTestPrecedence() $this->expectNotToPerformAssertions(); } + public function testUnaryPrecedenceChange() + { + $env = new Environment(new ArrayLoader(), ['cache' => false, 'autoescape' => false]); + $env->addExtension(new class () extends AbstractExtension { + public function getOperators() + { + $class = new class (new ConstantExpression('foo', 1), 1) extends AbstractUnary { + public function operator(Compiler $compiler): Compiler + { + return $compiler->raw('!'); + } + }; + + return [['!' => ['precedence' => 50, 'class' => $class::class]], []]; + } + }); + $parser = new Parser($env); + + $parser->parse($env->tokenize(new Source('{{ !false ? "OK" : "KO" }}', 'index'))); + $this->expectNotToPerformAssertions(); + } + private static function createContextVariable(string $name, array $attributes): ContextVariable { $expression = new ContextVariable($name, 1); From 8158dabc17bbdeed89b1df9786dd716d283574fa Mon Sep 17 00:00:00 2001 From: Nicolas Lemoine Date: Fri, 22 Nov 2024 12:16:10 +0100 Subject: [PATCH 659/812] Add support for `Twig\Markup` Co-authored-by: Fabien Potencier --- extra/html-extra/HtmlExtension.php | 5 +++-- .../html-extra/Tests/Fixtures/html_classes.test | 17 ++++++++++------- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/extra/html-extra/HtmlExtension.php b/extra/html-extra/HtmlExtension.php index d8a6c0036d1..8eeee5a58fd 100644 --- a/extra/html-extra/HtmlExtension.php +++ b/extra/html-extra/HtmlExtension.php @@ -14,6 +14,7 @@ use Symfony\Component\Mime\MimeTypes; use Twig\Error\RuntimeError; use Twig\Extension\AbstractExtension; +use Twig\Markup; use Twig\TwigFilter; use Twig\TwigFunction; @@ -92,8 +93,8 @@ public static function htmlClasses(...$args): string { $classes = []; foreach ($args as $i => $arg) { - if (\is_string($arg)) { - $classes[] = $arg; + if (\is_string($arg) || $arg instanceof Markup) { + $classes[] = (string) $arg; } elseif (\is_array($arg)) { foreach ($arg as $class => $condition) { if (!\is_string($class)) { diff --git a/extra/html-extra/Tests/Fixtures/html_classes.test b/extra/html-extra/Tests/Fixtures/html_classes.test index 9b26d1564d8..7266c1c448c 100644 --- a/extra/html-extra/Tests/Fixtures/html_classes.test +++ b/extra/html-extra/Tests/Fixtures/html_classes.test @@ -3,15 +3,18 @@ --TEMPLATE-- {{ html_classes('a', {'b': true, 'c': false}, 'd', false ? 'e', true ? 'f', '0') }} {% set class_a = 'a' %} -{% set class_b = 'b' %} -{%- set class_c -%} -c +{%- set class_b -%} +b {%- endset -%} -{{ html_classes(class_a, { (class_b): true }) }} -{{ html_classes(class_a, { (class_c): true }) }} +{{ html_classes(class_a) }} +{{ html_classes(class_b) }} +{{ html_classes({ (class_a): true }) }} +{{ html_classes({ (class_b): true }) }} --DATA-- return [] --EXPECT-- a b d f 0 -a b -a c +a +b +a +b From acffa88cc2b40dbe42eaf3a5025d6c0d4600cc50 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sun, 29 Dec 2024 11:51:50 +0100 Subject: [PATCH 660/812] Prepare the 3.18.0 release --- CHANGELOG | 2 +- src/Environment.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 21fb43f107f..2d1d282a956 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,4 +1,4 @@ -# 3.18.0 (2024-XX-XX) +# 3.18.0 (2024-12-29) * Fix unary operator precedence change * Ignore `SyntaxError` exceptions from undefined handlers when using the `guard` tag diff --git a/src/Environment.php b/src/Environment.php index 1a91fedb3a7..66400b02861 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -44,12 +44,12 @@ */ class Environment { - public const VERSION = '3.18.0-DEV'; + public const VERSION = '3.18.0'; public const VERSION_ID = 31800; public const MAJOR_VERSION = 3; public const MINOR_VERSION = 18; public const RELEASE_VERSION = 0; - public const EXTRA_VERSION = 'DEV'; + public const EXTRA_VERSION = ''; private $charset; private $loader; From d184e9fe12e1a8d219a95638ee78ad577a59e299 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sun, 29 Dec 2024 11:52:41 +0100 Subject: [PATCH 661/812] Bump version --- CHANGELOG | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index 2d1d282a956..5c9a6fc5566 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +# 3.18.1 (2025-XX-XX) + + * n/a + # 3.18.0 (2024-12-29) * Fix unary operator precedence change From 81ebebb2102d37b251f11db5398105c5955eff05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Andr=C3=A9?= Date: Thu, 2 Jan 2025 07:17:52 +0100 Subject: [PATCH 662/812] Remove dead method in ModuleNode This method is not used after 1.x (it was used in the compileConstructor)https://github.com/twigphp/Twig/blob/b1f009c449e435a0384814e67205d9190a4d050e/src/Node/ModuleNode.php#L206 and has no caller since 2.x https://github.com/twigphp/Twig/blob/19185947ec75d433a3ac650af32fc05649b95ee1/src/Node/ModuleNode.php#L174 ModuleNode is marked final so I guess we can remove it safely. --- src/Node/ModuleNode.php | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/src/Node/ModuleNode.php b/src/Node/ModuleNode.php index 17840dd0a78..9e2774e872b 100644 --- a/src/Node/ModuleNode.php +++ b/src/Node/ModuleNode.php @@ -473,21 +473,4 @@ protected function compileGetSourceContext(Compiler $compiler) ->write("}\n") ; } - - protected function compileLoadTemplate(Compiler $compiler, $node, $var) - { - if ($node instanceof ConstantExpression) { - $compiler - ->write(\sprintf('%s = $this->loadTemplate(', $var)) - ->subcompile($node) - ->raw(', ') - ->repr($node->getTemplateName()) - ->raw(', ') - ->repr($node->getTemplateLine()) - ->raw(");\n") - ; - } else { - throw new \LogicException('Trait templates can only be constant nodes.'); - } - } } From 3889bc7b1868f2947225366831929a3c46c6db85 Mon Sep 17 00:00:00 2001 From: Baptiste <65629571+BaptisteChabrol@users.noreply.github.com> Date: Mon, 30 Dec 2024 15:58:07 +0100 Subject: [PATCH 663/812] Fix typo in enum twig doc functions --- doc/functions/enum.rst | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/doc/functions/enum.rst b/doc/functions/enum.rst index 66e2c9dba46..325b140b092 100644 --- a/doc/functions/enum.rst +++ b/doc/functions/enum.rst @@ -13,10 +13,12 @@ {{ enum('App\\MyEnum').SomeCase.value }} {# get all cases of an enum #} - {% enum('App\\MyEnum').cases %} + {% for case in enum('App\\MyEnum').cases %} + {{ case.value }} + {% endfor %} {# call any methods of the enum class #} - {% enum('App\\MyEnum').someMethod() %} + {{ enum('App\\MyEnum').someMethod() }} When using a string literal for the ``enum`` argument, it will be validated during compile time to be a valid enum name. From efd12ef0ccfb9ddaf310f0fc3029ca198076af8f Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Thu, 2 Jan 2025 11:17:57 +0100 Subject: [PATCH 664/812] Optimize NameExpression compilation --- src/Node/Expression/NameExpression.php | 2 +- .../Variable/ContextVariableTest.php | 49 ++++++++++++++----- 2 files changed, 39 insertions(+), 12 deletions(-) diff --git a/src/Node/Expression/NameExpression.php b/src/Node/Expression/NameExpression.php index 78ae51f0334..2872ba413ba 100644 --- a/src/Node/Expression/NameExpression.php +++ b/src/Node/Expression/NameExpression.php @@ -39,7 +39,7 @@ public function compile(Compiler $compiler): void $compiler->addDebugInfo($this); if ($this->getAttribute('is_defined_test')) { - if (isset($this->specialVars[$name])) { + if (isset($this->specialVars[$name]) || $this->getAttribute('always_defined')) { $compiler->repr(true); } elseif (\PHP_VERSION_ID >= 70400) { $compiler diff --git a/tests/Node/Expression/Variable/ContextVariableTest.php b/tests/Node/Expression/Variable/ContextVariableTest.php index be9f6917b05..46ab171a043 100644 --- a/tests/Node/Expression/Variable/ContextVariableTest.php +++ b/tests/Node/Expression/Variable/ContextVariableTest.php @@ -27,20 +27,47 @@ public function testConstructor() public static function provideTests(): iterable { - $node = new ContextVariable('foo', 1); - $self = new ContextVariable('_self', 1); - $context = new ContextVariable('_context', 1); + // special variables + foreach (['_self' => '$this->getTemplateName()', '_context' => '$context', '_charset' => '$this->env->getCharset()'] as $special => $compiled) { + $node = new ContextVariable($special, 1); + yield $special => [$node, "// line 1\n$compiled"]; + $node = new ContextVariable($special, 1); + $node->setAttribute('is_defined_test', true); + yield $special.'_defined_test' => [$node, "// line 1\ntrue"]; + } - $env = new Environment(new ArrayLoader(), ['strict_variables' => true]); - $env1 = new Environment(new ArrayLoader(), ['strict_variables' => false]); + $env = new Environment(new ArrayLoader(), ['strict_variables' => false]); + $envStrict = new Environment(new ArrayLoader(), ['strict_variables' => true]); + // regular + $node = new ContextVariable('foo', 1); $output = '(isset($context["foo"]) || array_key_exists("foo", $context) ? $context["foo"] : (function () { throw new RuntimeError(\'Variable "foo" does not exist.\', 1, $this->source); })())'; + yield 'strict' => [$node, "// line 1\n".$output, $envStrict]; + yield 'non_strict' => [$node, self::createVariableGetter('foo', 1), $env]; - return [ - [$node, "// line 1\n".$output, $env], - [$node, self::createVariableGetter('foo', 1), $env1], - [$self, "// line 1\n\$this->getTemplateName()"], - [$context, "// line 1\n\$context"], - ]; + // ignore strict check + $node = new ContextVariable('foo', 1); + $node->setAttribute('ignore_strict_check', true); + yield 'ignore_strict_check_strict' => [$node, "// line 1\n(\$context[\"foo\"] ?? null)", $envStrict]; + yield 'ignore_strict_check_non_strict' => [$node, "// line 1\n(\$context[\"foo\"] ?? null)", $env]; + + // always defined + $node = new ContextVariable('foo', 1); + $node->setAttribute('always_defined', true); + yield 'always_defined_strict' => [$node, "// line 1\n\$context[\"foo\"]", $envStrict]; + yield 'always_defined_non_strict' => [$node, "// line 1\n\$context[\"foo\"]", $env]; + + // is defined test + $node = new ContextVariable('foo', 1); + $node->setAttribute('is_defined_test', true); + yield 'is_defined_test_strict' => [$node, "// line 1\narray_key_exists(\"foo\", \$context)", $envStrict]; + yield 'is_defined_test_non_strict' => [$node, "// line 1\narray_key_exists(\"foo\", \$context)", $env]; + + // is defined test // always defined + $node = new ContextVariable('foo', 1); + $node->setAttribute('is_defined_test', true); + $node->setAttribute('always_defined', true); + yield 'is_defined_test_always_defined_strict' => [$node, "// line 1\ntrue", $envStrict]; + yield 'is_defined_test_always_defined_non_strict' => [$node, "// line 1\ntrue", $env]; } } From 91c8c19026c524b49bc0b82b0f1dfda9a973f2a8 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Thu, 2 Jan 2025 14:38:21 +0100 Subject: [PATCH 665/812] Bump version --- CHANGELOG | 2 +- src/Environment.php | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 5c9a6fc5566..66db8fff221 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,4 +1,4 @@ -# 3.18.1 (2025-XX-XX) +# 3.19.0 (2025-XX-XX) * n/a diff --git a/src/Environment.php b/src/Environment.php index 66400b02861..430d3991195 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -44,12 +44,12 @@ */ class Environment { - public const VERSION = '3.18.0'; - public const VERSION_ID = 31800; + public const VERSION = '3.19.0-DEV'; + public const VERSION_ID = 31900; public const MAJOR_VERSION = 3; - public const MINOR_VERSION = 18; + public const MINOR_VERSION = 19; public const RELEASE_VERSION = 0; - public const EXTRA_VERSION = ''; + public const EXTRA_VERSION = 'DEV'; private $charset; private $loader; From d8fe3bd1ece49e531a132bcadc4c99f7d6ccde46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Mon, 23 Dec 2024 23:58:49 +0100 Subject: [PATCH 666/812] Add `LastModifiedExtensionInterface` and implementation in `AbstractExtension` to track modification of runtime classes --- CHANGELOG | 2 +- doc/advanced.rst | 18 +++++++++++---- src/Extension/AbstractExtension.php | 19 ++++++++++++++- src/Extension/EscaperExtension.php | 8 +++++++ .../LastModifiedExtensionInterface.php | 23 +++++++++++++++++++ src/ExtensionSet.php | 14 +++++++---- tests/Extension/CoreTest.php | 5 ++++ tests/Extension/EscaperTest.php | 5 ++++ 8 files changed, 83 insertions(+), 11 deletions(-) create mode 100644 src/Extension/LastModifiedExtensionInterface.php diff --git a/CHANGELOG b/CHANGELOG index 66db8fff221..71eb108199b 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,6 @@ # 3.19.0 (2025-XX-XX) - * n/a + * Add `LastModifiedExtensionInterface` and implementation in `AbstractExtension` to track modification of runtime classes # 3.18.0 (2024-12-29) diff --git a/doc/advanced.rst b/doc/advanced.rst index e07764b8770..bc7e5d376f4 100644 --- a/doc/advanced.rst +++ b/doc/advanced.rst @@ -868,7 +868,7 @@ must be autoload-able):: // implement the logic to create an instance of $class // and inject its dependencies // most of the time, it means using your dependency injection container - if ('CustomRuntimeExtension' === $class) { + if ('CustomTwigRuntime' === $class) { return new $class(new Rot13Provider()); } else { // ... @@ -884,9 +884,9 @@ must be autoload-able):: (``\Twig\RuntimeLoader\ContainerRuntimeLoader``). It is now possible to move the runtime logic to a new -``CustomRuntimeExtension`` class and use it directly in the extension:: +``CustomTwigRuntime`` class and use it directly in the extension:: - class CustomRuntimeExtension + class CustomTwigRuntime { private $rot13Provider; @@ -906,13 +906,21 @@ It is now possible to move the runtime logic to a new public function getFunctions() { return [ - new \Twig\TwigFunction('rot13', ['CustomRuntimeExtension', 'rot13']), + new \Twig\TwigFunction('rot13', ['CustomTwigRuntime', 'rot13']), // or - new \Twig\TwigFunction('rot13', 'CustomRuntimeExtension::rot13'), + new \Twig\TwigFunction('rot13', 'CustomTwigRuntime::rot13'), ]; } } +.. note:: + + The extension class should implement the ``Twig\Extension\LastModifiedExtensionInterface`` + interface to invalidate the template cache when the runtime class is modified. + The ``AbstractExtension`` class implements this interface and tracks the + runtime class if its name is the same as the extension class but ends with + ``Runtime`` instead of ``Extension``. + Testing an Extension -------------------- diff --git a/src/Extension/AbstractExtension.php b/src/Extension/AbstractExtension.php index a1b083b6884..4234df087f6 100644 --- a/src/Extension/AbstractExtension.php +++ b/src/Extension/AbstractExtension.php @@ -11,7 +11,7 @@ namespace Twig\Extension; -abstract class AbstractExtension implements ExtensionInterface +abstract class AbstractExtension implements LastModifiedExtensionInterface { public function getTokenParsers() { @@ -42,4 +42,21 @@ public function getOperators() { return [[], []]; } + + public function getLastModified(): int + { + $filename = (new \ReflectionClass($this))->getFileName(); + if (!is_file($filename)) { + return 0; + } + + $lastModified = filemtime($filename); + + // Track modifications of the runtime class if it exists and follows the naming convention + if (str_ends_with($filename, 'Extension.php') && is_file($filename = substr($filename, 0, -13) . 'Runtime.php')) { + $lastModified = max($lastModified, filemtime($filename)); + } + + return $lastModified; + } } diff --git a/src/Extension/EscaperExtension.php b/src/Extension/EscaperExtension.php index 52531c436b5..9a05e4c7c65 100644 --- a/src/Extension/EscaperExtension.php +++ b/src/Extension/EscaperExtension.php @@ -57,6 +57,14 @@ public function getFilters(): array ]; } + public function getLastModified(): int + { + return max( + parent::getLastModified(), + filemtime((new \ReflectionClass(EscaperRuntime::class))->getFileName()), + ); + } + /** * @deprecated since Twig 3.10 */ diff --git a/src/Extension/LastModifiedExtensionInterface.php b/src/Extension/LastModifiedExtensionInterface.php new file mode 100644 index 00000000000..4bab0c07cd3 --- /dev/null +++ b/src/Extension/LastModifiedExtensionInterface.php @@ -0,0 +1,23 @@ +lastModified; } + $lastModified = 0; foreach ($this->extensions as $extension) { - $r = new \ReflectionObject($extension); - if (is_file($r->getFileName()) && ($extensionTime = filemtime($r->getFileName())) > $this->lastModified) { - $this->lastModified = $extensionTime; + if ($extension instanceof LastModifiedExtensionInterface) { + $lastModified = max($extension->getLastModified(), $lastModified); + } else { + $r = new \ReflectionObject($extension); + if (is_file($r->getFileName())) { + $lastModified = max(filemtime($r->getFileName()), $lastModified); + } } } - return $this->lastModified; + return $this->lastModified = $lastModified; } public function addExtension(ExtensionInterface $extension): void diff --git a/tests/Extension/CoreTest.php b/tests/Extension/CoreTest.php index 6bb74807f86..edce0d3eede 100644 --- a/tests/Extension/CoreTest.php +++ b/tests/Extension/CoreTest.php @@ -393,6 +393,11 @@ public function testSandboxedIncludeWithPreloadedTemplate() $this->expectException(SecurityError::class); $twig->render('index'); } + + public function testLastModified() + { + $this->assertGreaterThan(1000000000, (new CoreExtension())->getLastModified()); + } } final class CoreTestIteratorAggregate implements \IteratorAggregate diff --git a/tests/Extension/EscaperTest.php b/tests/Extension/EscaperTest.php index 436d1790f52..28df82db570 100644 --- a/tests/Extension/EscaperTest.php +++ b/tests/Extension/EscaperTest.php @@ -70,6 +70,11 @@ public function testCustomEscapersOnMultipleEnvs() $this->assertSame('foo**ISO-8859-1**UTF-8', $env1->getRuntime(EscaperRuntime::class)->escape('foo', 'foo', 'ISO-8859-1')); $this->assertSame('foo**ISO-8859-1**UTF-8**again', $env2->getRuntime(EscaperRuntime::class)->escape('foo', 'foo', 'ISO-8859-1')); } + + public function testLastModified() + { + $this->assertGreaterThan(1000000000, (new EscaperExtension())->getLastModified()); + } } function legacy_escaper(Environment $twig, $string, $charset) From 51bf2ca737be979c317c2a2956ac40ffb3276055 Mon Sep 17 00:00:00 2001 From: Jan Rosier Date: Thu, 2 Jan 2025 21:11:52 +0100 Subject: [PATCH 667/812] Use spl_object_id() instead of spl_object_hash() --- src/NodeVisitor/SafeAnalysisNodeVisitor.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/NodeVisitor/SafeAnalysisNodeVisitor.php b/src/NodeVisitor/SafeAnalysisNodeVisitor.php index 3be82304f4b..681751e98a2 100644 --- a/src/NodeVisitor/SafeAnalysisNodeVisitor.php +++ b/src/NodeVisitor/SafeAnalysisNodeVisitor.php @@ -42,7 +42,7 @@ public function setSafeVars(array $safeVars): void */ public function getSafe(Node $node) { - $hash = spl_object_hash($node); + $hash = spl_object_id($node); if (!isset($this->data[$hash])) { return []; } @@ -64,7 +64,7 @@ public function getSafe(Node $node) private function setSafe(Node $node, array $safe): void { - $hash = spl_object_hash($node); + $hash = spl_object_id($node); if (isset($this->data[$hash])) { foreach ($this->data[$hash] as &$bucket) { if ($bucket['key'] === $node) { From f2f03d7629b8ea76d67918a3993e8d89458bb76c Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Thu, 2 Jan 2025 21:23:49 +0100 Subject: [PATCH 668/812] Fix typos in the docs --- doc/api.rst | 4 ++-- doc/templates.rst | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/doc/api.rst b/doc/api.rst index 37560de3cb8..bbc4c5c3bdc 100644 --- a/doc/api.rst +++ b/doc/api.rst @@ -71,9 +71,9 @@ If a template defines blocks, they can be rendered individually via the Streaming Templates ------------------- -.. versionadded:: 3.18.0 +.. versionadded:: 3.18 -To stream a template, call the ``stream()`` method`:: +To stream a template, call the ``stream()`` method:: $template->stream(['the' => 'variables', 'go' => 'here']); diff --git a/doc/templates.rst b/doc/templates.rst index 2bf0a0b2c0b..3c9a5d134bd 100644 --- a/doc/templates.rst +++ b/doc/templates.rst @@ -194,7 +194,7 @@ filters. .. code-block:: twig {{ -1|abs }} {# returns -1 #} - {{ -1**0 }} {% returns -1 %} + {{ -1**0 }} {# returns -1 #} {# as it is equivalent to #} @@ -206,7 +206,7 @@ filters. .. code-block:: twig {{ (-1)|abs }} {# returns 1 as expected #} - {{ (-1)**0 }} {% returns 1 %} + {{ (-1)**0 }} {# returns 1 as expected #} Functions --------- From 053572ed563d16666c9947d24165f3b42e557ec6 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Fri, 3 Jan 2025 08:17:25 +0100 Subject: [PATCH 669/812] Fix CS --- doc/tags/types.rst | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/doc/tags/types.rst b/doc/tags/types.rst index 468a2b24a59..86e5dc8d57b 100644 --- a/doc/tags/types.rst +++ b/doc/tags/types.rst @@ -3,13 +3,16 @@ .. versionadded:: 3.13 - The ``types`` tag was added in Twig 3.13. This tag is **experimental** and can change based on usage and feedback. + The ``types`` tag was added in Twig 3.13. This tag is **experimental** and + can change based on usage and feedback. The ``types`` tag declares the types of template variables. -To do this, specify a :ref:`mapping ` of names to their types as strings. +To do this, specify a :ref:`mapping ` of names to their types +as strings. -Here is how to declare that ``is_correct`` is a boolean, while ``score`` is a number (see note below): +Here is how to declare that ``is_correct`` is a boolean, while ``score`` is a +number (see note below): .. code-block:: twig @@ -29,13 +32,14 @@ You can declare variables as optional by adding the ``?`` suffix: By default, this tag does not affect the template compilation or runtime behavior. -Its purpose is to enable designers and developers to document and specify the context's available -and/or required variables. While Twig itself does not validate variables or their types, this tag enables extensions -to do this. +Its purpose is to enable designers and developers to document and specify the +context's available and/or required variables. While Twig itself does not +validate variables or their types, this tag enables extensions to do this. -Additionally, :ref:`Twig extensions ` can analyze these tags to perform compile-time and -runtime analysis of templates. +Additionally, :ref:`Twig extensions ` can analyze these +tags to perform compile-time and runtime analysis of templates. .. note:: - The syntax for and contents of type strings are intentionally left out of scope. + The syntax for and contents of type strings are intentionally left out of + scope. From e7ae984e3686aa5376f93afec11c355843bf3894 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Fri, 3 Jan 2025 08:14:47 +0100 Subject: [PATCH 670/812] Add a note on the types locality --- doc/tags/types.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/doc/tags/types.rst b/doc/tags/types.rst index 86e5dc8d57b..a2924802fd4 100644 --- a/doc/tags/types.rst +++ b/doc/tags/types.rst @@ -8,6 +8,13 @@ The ``types`` tag declares the types of template variables. +.. note:: + + The types declared in a template are local to that template and must not be + propagated to included templates. This is because a template can be + included from multiple different places, each potentially having different + variable types. + To do this, specify a :ref:`mapping ` of names to their types as strings. From 36ff3fa3b80adcb6b95ca918f11980ad4e4b4c5d Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Mon, 6 Jan 2025 10:35:13 +0100 Subject: [PATCH 671/812] Remove the note about types mapping argument --- doc/tags/types.rst | 3 --- 1 file changed, 3 deletions(-) diff --git a/doc/tags/types.rst b/doc/tags/types.rst index a2924802fd4..a4e4a6c1066 100644 --- a/doc/tags/types.rst +++ b/doc/tags/types.rst @@ -15,9 +15,6 @@ The ``types`` tag declares the types of template variables. included from multiple different places, each potentially having different variable types. -To do this, specify a :ref:`mapping ` of names to their types -as strings. - Here is how to declare that ``is_correct`` is a boolean, while ``score`` is a number (see note below): From dff6fdd675a672a4b0335fe4a88b6f244a2e71e7 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Mon, 6 Jan 2025 13:51:14 +0100 Subject: [PATCH 672/812] Make {} optional for the types tag --- CHANGELOG | 1 + doc/tags/types.rst | 31 +++++++++++++++------- src/TokenParser/TypesTokenParser.php | 15 +++++------ tests/TokenParser/TypesTokenParserTest.php | 9 +++++++ 4 files changed, 39 insertions(+), 17 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 71eb108199b..f1d757d20c5 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,6 @@ # 3.19.0 (2025-XX-XX) + * Make `{}` optional for the `types` tag * Add `LastModifiedExtensionInterface` and implementation in `AbstractExtension` to track modification of runtime classes # 3.18.0 (2024-12-29) diff --git a/doc/tags/types.rst b/doc/tags/types.rst index a4e4a6c1066..c3a175392cf 100644 --- a/doc/tags/types.rst +++ b/doc/tags/types.rst @@ -6,17 +6,23 @@ The ``types`` tag was added in Twig 3.13. This tag is **experimental** and can change based on usage and feedback. -The ``types`` tag declares the types of template variables. +Use the ``types`` tag to declare the type of a variable: -.. note:: +.. code-block:: twig - The types declared in a template are local to that template and must not be - propagated to included templates. This is because a template can be - included from multiple different places, each potentially having different - variable types. + {% types is_correct: 'boolean' %} + {% types score: 'number' %} -Here is how to declare that ``is_correct`` is a boolean, while ``score`` is a -number (see note below): +Or multiple variables: + +.. code-block:: twig + + {% types + is_correct: 'boolean', + score: 'number', + %} + +You can also enclose types with ``{}``: .. code-block:: twig @@ -25,7 +31,7 @@ number (see note below): score: 'number', } %} -You can declare variables as optional by adding the ``?`` suffix: +Declare optional variables by adding a ``?`` suffix: .. code-block:: twig @@ -43,6 +49,13 @@ validate variables or their types, this tag enables extensions to do this. Additionally, :ref:`Twig extensions ` can analyze these tags to perform compile-time and runtime analysis of templates. +.. note:: + + The types declared in a template are local to that template and must not be + propagated to included templates. This is because a template can be + included from multiple different places, each potentially having different + variable types. + .. note:: The syntax for and contents of type strings are intentionally left out of diff --git a/src/TokenParser/TypesTokenParser.php b/src/TokenParser/TypesTokenParser.php index 02aef81cc2e..a7da0f5ecf4 100644 --- a/src/TokenParser/TypesTokenParser.php +++ b/src/TokenParser/TypesTokenParser.php @@ -31,9 +31,7 @@ final class TypesTokenParser extends AbstractTokenParser public function parse(Token $token): Node { $stream = $this->parser->getStream(); - $types = $this->parseSimpleMappingExpression($stream); - $stream->expect(Token::BLOCK_END_TYPE); return new TypesNode($types, $token->getLine()); @@ -46,17 +44,15 @@ public function parse(Token $token): Node */ private function parseSimpleMappingExpression(TokenStream $stream): array { - $stream->expect(Token::PUNCTUATION_TYPE, '{', 'A mapping element was expected'); - + $enclosed = null !== $stream->nextIf(Token::PUNCTUATION_TYPE, '{'); $types = []; - $first = true; - while (!$stream->test(Token::PUNCTUATION_TYPE, '}')) { + while (!($stream->test(Token::PUNCTUATION_TYPE, '}') || $stream->test(Token::BLOCK_END_TYPE))) { if (!$first) { $stream->expect(Token::PUNCTUATION_TYPE, ',', 'A type string must be followed by a comma'); // trailing ,? - if ($stream->test(Token::PUNCTUATION_TYPE, '}')) { + if ($stream->test(Token::PUNCTUATION_TYPE, '}') || $stream->test(Token::BLOCK_END_TYPE)) { break; } } @@ -78,7 +74,10 @@ private function parseSimpleMappingExpression(TokenStream $stream): array 'optional' => $isOptional, ]; } - $stream->expect(Token::PUNCTUATION_TYPE, '}', 'An opened mapping is not properly closed'); + + if ($enclosed) { + $stream->expect(Token::PUNCTUATION_TYPE, '}', 'An opened mapping is not properly closed'); + } return $types; } diff --git a/tests/TokenParser/TypesTokenParserTest.php b/tests/TokenParser/TypesTokenParserTest.php index 400820fcabb..0acbb4dd318 100644 --- a/tests/TokenParser/TypesTokenParserTest.php +++ b/tests/TokenParser/TypesTokenParserTest.php @@ -64,6 +64,15 @@ public static function getMappingTests(): array 'baz' => ['type' => 'baz', 'optional' => false], ], ], + + // without {} enclosing + [ + '{% types foo: "foo", bar: "bar" %}', + [ + 'foo' => ['type' => 'foo', 'optional' => false], + 'bar' => ['type' => 'bar', 'optional' => false], + ], + ], ]; } } From 8b432758f61f2c65dd3035704986487fbed92a5f Mon Sep 17 00:00:00 2001 From: Lorenz Date: Thu, 26 Dec 2024 20:11:14 +0100 Subject: [PATCH 673/812] invoke filter --- src/Extension/CoreExtension.php | 15 +++++++++++++++ tests/Fixtures/filters/invoke.test | 14 ++++++++++++++ 2 files changed, 29 insertions(+) create mode 100644 tests/Fixtures/filters/invoke.test diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index 01e72856690..5b900877b66 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -266,6 +266,7 @@ public function getFilters(): array // iteration and runtime new TwigFilter('default', [self::class, 'default'], ['node_class' => DefaultFilter::class]), new TwigFilter('keys', [self::class, 'keys']), + new TwigFilter('invoke', [self::class, 'invoke']), ]; } @@ -915,6 +916,20 @@ public static function keys($array): array return array_keys($array); } + /** + * Invokes a callable + * + * @param callable $callable + * @param ...$arguments + * @return mixed + * + * @internal + */ + public static function invoke(callable $callable, ...$arguments): mixed + { + return $callable(...$arguments); + } + /** * Reverses a variable. * diff --git a/tests/Fixtures/filters/invoke.test b/tests/Fixtures/filters/invoke.test new file mode 100644 index 00000000000..b4a707b97a7 --- /dev/null +++ b/tests/Fixtures/filters/invoke.test @@ -0,0 +1,14 @@ +--TEST-- +"invoke" filter +--TEMPLATE-- +{% set func = x => 'Hello '~x %} +{{ func|invoke('World') }} +{% set func2 = (x, y) => x+y %} +{{ func2|invoke(3, 2) }} +--DATA-- +return [] +--CONFIG-- +return [] +--EXPECT-- +Hello World +5 From f27115857d7b4a674ba0dc26536c1fc992811555 Mon Sep 17 00:00:00 2001 From: Lorenz Date: Mon, 30 Dec 2024 09:59:23 +0100 Subject: [PATCH 674/812] checkArrow, typehints --- src/Extension/CoreExtension.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index 5b900877b66..82db0ec3a5f 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -266,7 +266,7 @@ public function getFilters(): array // iteration and runtime new TwigFilter('default', [self::class, 'default'], ['node_class' => DefaultFilter::class]), new TwigFilter('keys', [self::class, 'keys']), - new TwigFilter('invoke', [self::class, 'invoke']), + new TwigFilter('invoke', [self::class, 'invoke'], ['needs_environment' => true]), ]; } @@ -919,15 +919,15 @@ public static function keys($array): array /** * Invokes a callable * - * @param callable $callable - * @param ...$arguments - * @return mixed + * @param \Closure $arrow * * @internal */ - public static function invoke(callable $callable, ...$arguments): mixed + public static function invoke(Environment $env, $arrow, ...$arguments): mixed { - return $callable(...$arguments); + self::checkArrow($env, $arrow, 'invoke', 'filter'); + + return $arrow(...$arguments); } /** From edecc13f0829e9fb2f787dd04f31ef4ffa87cbba Mon Sep 17 00:00:00 2001 From: Lorenz Date: Tue, 7 Jan 2025 18:33:17 +0100 Subject: [PATCH 675/812] typehint instead of checkArrow --- src/Extension/CoreExtension.php | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index 82db0ec3a5f..e9976c1390c 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -266,7 +266,7 @@ public function getFilters(): array // iteration and runtime new TwigFilter('default', [self::class, 'default'], ['node_class' => DefaultFilter::class]), new TwigFilter('keys', [self::class, 'keys']), - new TwigFilter('invoke', [self::class, 'invoke'], ['needs_environment' => true]), + new TwigFilter('invoke', [self::class, 'invoke']), ]; } @@ -919,14 +919,10 @@ public static function keys($array): array /** * Invokes a callable * - * @param \Closure $arrow - * * @internal */ - public static function invoke(Environment $env, $arrow, ...$arguments): mixed + public static function invoke(\Closure $arrow, ...$arguments): mixed { - self::checkArrow($env, $arrow, 'invoke', 'filter'); - return $arrow(...$arguments); } From d58ac9a1a8b2de91c2679c06e1d56b85508c841c Mon Sep 17 00:00:00 2001 From: Lorenz Date: Tue, 7 Jan 2025 18:45:57 +0100 Subject: [PATCH 676/812] docs and changelog --- CHANGELOG | 1 + doc/filters/invoke.rst | 19 +++++++++++++++++++ doc/templates.rst | 6 ++++++ 3 files changed, 26 insertions(+) create mode 100644 doc/filters/invoke.rst diff --git a/CHANGELOG b/CHANGELOG index f1d757d20c5..5b01ea7ffb7 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,6 @@ # 3.19.0 (2025-XX-XX) + * Add the `invoke` filter * Make `{}` optional for the `types` tag * Add `LastModifiedExtensionInterface` and implementation in `AbstractExtension` to track modification of runtime classes diff --git a/doc/filters/invoke.rst b/doc/filters/invoke.rst new file mode 100644 index 00000000000..63db1c6aa2e --- /dev/null +++ b/doc/filters/invoke.rst @@ -0,0 +1,19 @@ +``invoke`` +======= + +The ``invoke`` filter invokes an arrow function with the the given arguments: + +.. code-block:: twig + + {% set person = {first: "Bob", last: "Smith"} %} + {% set func = p => "#{p.first} #{p.last}" %} + + {{ func|invoke(person) }} + {# outputs Bob Smith #} + +Note that the arrow function has access to the current context. + +Arguments +--------- + +All given arguments are passed to the arrow function diff --git a/doc/templates.rst b/doc/templates.rst index 3c9a5d134bd..fc5a4d087eb 100644 --- a/doc/templates.rst +++ b/doc/templates.rst @@ -1018,6 +1018,12 @@ The following operators don't fit into any of the other categories: Arrow function support for functions, macros, and method calls was added in Twig 3.15 (filters and tests were already supported). + Arrow functions can be invoked using the ``invoke`` filter. + + .. versionadded:: 3.19 + + The ``invoke`` filter has been added in Twig 3.19. + Operators ~~~~~~~~~ From 09c30c3ca238037b43660936e644a4aee2aa6b2d Mon Sep 17 00:00:00 2001 From: Lorenz Date: Tue, 7 Jan 2025 18:50:01 +0100 Subject: [PATCH 677/812] fix merge --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index 5b01ea7ffb7..bd7dcb57e52 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,6 @@ # 3.19.0 (2025-XX-XX) + * Add `LastModifiedExtensionInterface` and implementation in `AbstractExtension` to track modification of runtime classes * Add the `invoke` filter * Make `{}` optional for the `types` tag * Add `LastModifiedExtensionInterface` and implementation in `AbstractExtension` to track modification of runtime classes From 2b95bd60925d0d03d15b8e1a6e63a68ec53e564e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lorenz=20Sch=C3=A4fer?= Date: Wed, 8 Jan 2025 08:07:19 +0100 Subject: [PATCH 678/812] typo Co-authored-by: Christian Flothmann --- doc/filters/invoke.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/filters/invoke.rst b/doc/filters/invoke.rst index 63db1c6aa2e..960f9949c0f 100644 --- a/doc/filters/invoke.rst +++ b/doc/filters/invoke.rst @@ -1,7 +1,7 @@ ``invoke`` ======= -The ``invoke`` filter invokes an arrow function with the the given arguments: +The ``invoke`` filter invokes an arrow function with the given arguments: .. code-block:: twig From dacbe51b3acdde300f4b4539ec4781b340d3f352 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lorenz=20Sch=C3=A4fer?= Date: Thu, 9 Jan 2025 08:01:12 +0100 Subject: [PATCH 679/812] Apply suggestions from code review Co-authored-by: Fabien Potencier --- doc/filters/invoke.rst | 6 +++--- doc/templates.rst | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/doc/filters/invoke.rst b/doc/filters/invoke.rst index 960f9949c0f..71dc5349dbd 100644 --- a/doc/filters/invoke.rst +++ b/doc/filters/invoke.rst @@ -1,11 +1,11 @@ ``invoke`` -======= +========== The ``invoke`` filter invokes an arrow function with the given arguments: .. code-block:: twig - {% set person = {first: "Bob", last: "Smith"} %} + {% set person = { first: "Bob", last: "Smith" } %} {% set func = p => "#{p.first} #{p.last}" %} {{ func|invoke(person) }} @@ -16,4 +16,4 @@ Note that the arrow function has access to the current context. Arguments --------- -All given arguments are passed to the arrow function +All given arguments are passed to the arrow function. diff --git a/doc/templates.rst b/doc/templates.rst index fc5a4d087eb..47ac6036411 100644 --- a/doc/templates.rst +++ b/doc/templates.rst @@ -1018,9 +1018,9 @@ The following operators don't fit into any of the other categories: Arrow function support for functions, macros, and method calls was added in Twig 3.15 (filters and tests were already supported). - Arrow functions can be invoked using the ``invoke`` filter. +Arrow functions can be called using the ``invoke`` filter: - .. versionadded:: 3.19 +.. versionadded:: 3.19 The ``invoke`` filter has been added in Twig 3.19. From c4fd25fad51833cfd471d6af6a7273a1e6dff7bb Mon Sep 17 00:00:00 2001 From: Lorenz Date: Thu, 9 Jan 2025 08:02:15 +0100 Subject: [PATCH 680/812] fix indentation --- doc/templates.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/templates.rst b/doc/templates.rst index 47ac6036411..822f3f8319c 100644 --- a/doc/templates.rst +++ b/doc/templates.rst @@ -1018,9 +1018,9 @@ The following operators don't fit into any of the other categories: Arrow function support for functions, macros, and method calls was added in Twig 3.15 (filters and tests were already supported). -Arrow functions can be called using the ``invoke`` filter: + Arrow functions can be called using the ``invoke`` filter: -.. versionadded:: 3.19 + .. versionadded:: 3.19 The ``invoke`` filter has been added in Twig 3.19. From 3e93e91f5971b8f01627663993220b7d48c80c63 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sun, 12 Jan 2025 18:48:19 +0100 Subject: [PATCH 681/812] Finish the work --- CHANGELOG | 1 - doc/filters/invoke.rst | 11 ++++------- doc/templates.rst | 3 ++- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index bd7dcb57e52..5b01ea7ffb7 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,5 @@ # 3.19.0 (2025-XX-XX) - * Add `LastModifiedExtensionInterface` and implementation in `AbstractExtension` to track modification of runtime classes * Add the `invoke` filter * Make `{}` optional for the `types` tag * Add `LastModifiedExtensionInterface` and implementation in `AbstractExtension` to track modification of runtime classes diff --git a/doc/filters/invoke.rst b/doc/filters/invoke.rst index 71dc5349dbd..b8e1a4936b6 100644 --- a/doc/filters/invoke.rst +++ b/doc/filters/invoke.rst @@ -1,6 +1,10 @@ ``invoke`` ========== +.. versionadded:: 3.19 + + The ``invoke`` filter has been added in Twig 3.19. + The ``invoke`` filter invokes an arrow function with the given arguments: .. code-block:: twig @@ -10,10 +14,3 @@ The ``invoke`` filter invokes an arrow function with the given arguments: {{ func|invoke(person) }} {# outputs Bob Smith #} - -Note that the arrow function has access to the current context. - -Arguments ---------- - -All given arguments are passed to the arrow function. diff --git a/doc/templates.rst b/doc/templates.rst index 822f3f8319c..7bf2d15f591 100644 --- a/doc/templates.rst +++ b/doc/templates.rst @@ -1018,7 +1018,8 @@ The following operators don't fit into any of the other categories: Arrow function support for functions, macros, and method calls was added in Twig 3.15 (filters and tests were already supported). - Arrow functions can be called using the ``invoke`` filter: + Arrow functions can be called using the :doc:`invoke ` + filter. .. versionadded:: 3.19 From a998723b7014f921cef1ec585f9f476a18b9b408 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Tue, 14 Jan 2025 17:38:45 +0100 Subject: [PATCH 682/812] Fix constant() behavior when used with ?? --- CHANGELOG | 1 + src/Node/Node.php | 7 +++++++ tests/Fixtures/functions/constant.test | 2 ++ 3 files changed, 10 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index 5b01ea7ffb7..2fafecb650e 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,6 @@ # 3.19.0 (2025-XX-XX) + * Fix `constant()` behavior when used with `??` * Add the `invoke` filter * Make `{}` optional for the `types` tag * Add `LastModifiedExtensionInterface` and implementation in `AbstractExtension` to track modification of runtime classes diff --git a/src/Node/Node.php b/src/Node/Node.php index 7b4044c3f34..ca81dd0821d 100644 --- a/src/Node/Node.php +++ b/src/Node/Node.php @@ -105,6 +105,13 @@ public function __toString() return $repr; } + public function __clone() + { + foreach ($this->nodes as $name => $node) { + $this->nodes[$name] = clone $node; + } + } + /** * @return void */ diff --git a/tests/Fixtures/functions/constant.test b/tests/Fixtures/functions/constant.test index cbad8506e2c..c7a8eb11f59 100644 --- a/tests/Fixtures/functions/constant.test +++ b/tests/Fixtures/functions/constant.test @@ -4,9 +4,11 @@ {{ constant('DATE_W3C') == expect ? 'true' : 'false' }} {{ constant('ARRAY_AS_PROPS', object) }} {{ constant('class', object) }} +{{ constant('ARRAY_AS_PROPS', object) ?? 'KO' }} --DATA-- return ['expect' => DATE_W3C, 'object' => new \ArrayObject(['hi'])] --EXPECT-- true 2 ArrayObject +2 From 76062c8d519a0bbeeb3f8468abd5a36f5c74bfcd Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sun, 19 Jan 2025 16:54:05 +0100 Subject: [PATCH 683/812] Fix CS --- extra/cache-extra/Tests/IntegrationTest.php | 2 +- extra/markdown-extra/Tests/FunctionalTest.php | 2 +- src/Extension/AbstractExtension.php | 2 +- src/Extension/CoreExtension.php | 2 +- src/Lexer.php | 2 +- src/Template.php | 3 ++- tests/DeprecatedCallableInfoTest.php | 4 ++-- tests/EnvironmentTest.php | 4 ++-- tests/ErrorTest.php | 10 +++++----- tests/ExpressionParserTest.php | 10 +++++----- tests/Extension/CoreTest.php | 2 +- tests/Extension/SandboxTest.php | 6 +++--- tests/Node/Expression/FilterTest.php | 2 +- 13 files changed, 26 insertions(+), 25 deletions(-) diff --git a/extra/cache-extra/Tests/IntegrationTest.php b/extra/cache-extra/Tests/IntegrationTest.php index c439976f9b9..ab72d6442da 100644 --- a/extra/cache-extra/Tests/IntegrationTest.php +++ b/extra/cache-extra/Tests/IntegrationTest.php @@ -29,7 +29,7 @@ public function getExtensions() protected function getRuntimeLoaders() { return [ - new class() implements RuntimeLoaderInterface { + new class implements RuntimeLoaderInterface { public function load(string $class): ?object { return CacheRuntime::class === $class ? new CacheRuntime(new ArrayAdapter()) : null; diff --git a/extra/markdown-extra/Tests/FunctionalTest.php b/extra/markdown-extra/Tests/FunctionalTest.php index 154d75a9c39..72b277e3436 100644 --- a/extra/markdown-extra/Tests/FunctionalTest.php +++ b/extra/markdown-extra/Tests/FunctionalTest.php @@ -37,7 +37,7 @@ public function testMarkdown(string $template, string $expected) ===== Great! -EOF +EOF, ])); $twig->addExtension(new MarkdownExtension()); $twig->addRuntimeLoader(new class($class) implements RuntimeLoaderInterface { diff --git a/src/Extension/AbstractExtension.php b/src/Extension/AbstractExtension.php index 4234df087f6..26c00c68066 100644 --- a/src/Extension/AbstractExtension.php +++ b/src/Extension/AbstractExtension.php @@ -53,7 +53,7 @@ public function getLastModified(): int $lastModified = filemtime($filename); // Track modifications of the runtime class if it exists and follows the naming convention - if (str_ends_with($filename, 'Extension.php') && is_file($filename = substr($filename, 0, -13) . 'Runtime.php')) { + if (str_ends_with($filename, 'Extension.php') && is_file($filename = substr($filename, 0, -13).'Runtime.php')) { $lastModified = max($lastModified, filemtime($filename)); } diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index e9976c1390c..b83a0fed1db 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -917,7 +917,7 @@ public static function keys($array): array } /** - * Invokes a callable + * Invokes a callable. * * @internal */ diff --git a/src/Lexer.php b/src/Lexer.php index 0338fd874ff..e2a030b18c2 100644 --- a/src/Lexer.php +++ b/src/Lexer.php @@ -53,7 +53,7 @@ class Lexer (?(?&LNUM)(?:(?&FRAC))?) # Decimal number 123_456.456 )(?:(?&DNUM)(?:(?&EXPONENT))?) # 123_456.456E+10 /Ax'; - + public const REGEX_DQ_STRING_DELIM = '/"/A'; public const REGEX_DQ_STRING_PART = '/[^#"\\\\]*(?:(?:\\\\.|#(?!\{))[^#"\\\\]*)*/As'; public const REGEX_INLINE_COMMENT = '/#[^\n]*/A'; diff --git a/src/Template.php b/src/Template.php index 26f5b5d8154..156752f8b37 100644 --- a/src/Template.php +++ b/src/Template.php @@ -318,6 +318,7 @@ protected function loadTemplate($template, $templateName = null, $line = null, $ /** * @internal + * * @return $this */ public function unwrap(): self @@ -492,7 +493,7 @@ protected function hasMacro(string $name, array $context): bool return $parent->hasMacro($name, $context); } - protected function getTemplateForMacro(string $name, array $context, int $line, Source $source): Template + protected function getTemplateForMacro(string $name, array $context, int $line, Source $source): self { if (method_exists($this, $name)) { return $this; diff --git a/tests/DeprecatedCallableInfoTest.php b/tests/DeprecatedCallableInfoTest.php index 4e6d1583ce5..454d826ef4c 100644 --- a/tests/DeprecatedCallableInfoTest.php +++ b/tests/DeprecatedCallableInfoTest.php @@ -30,7 +30,7 @@ public function testTriggerDeprecation($expected, DeprecatedCallableInfo $info) if (\E_USER_DEPRECATED === $type) { $deprecations[] = $msg; } - + return false; }); @@ -62,7 +62,7 @@ public function testTriggerDeprecationWithoutFileOrLine() if (\E_USER_DEPRECATED === $type) { $deprecations[] = $msg; } - + return false; }); diff --git a/tests/EnvironmentTest.php b/tests/EnvironmentTest.php index f378b6b3895..34774db1c5b 100644 --- a/tests/EnvironmentTest.php +++ b/tests/EnvironmentTest.php @@ -179,7 +179,7 @@ public function testExtensionsAreNotInitializedWhenRenderingACompiledTemplate() // force compilation $twig = new Environment($loader = new ArrayLoader(['index' => '{{ foo }}']), $options); - $twig->addExtension($extension = new class() extends AbstractExtension { + $twig->addExtension($extension = new class extends AbstractExtension { public bool $throw = false; public function getFilters(): array @@ -475,7 +475,7 @@ protected function getMockLoader($templateName, $templateContent) public function testResettingGlobals() { $twig = new Environment(new ArrayLoader(['index' => ''])); - $twig->addExtension(new class() extends AbstractExtension implements GlobalsInterface { + $twig->addExtension(new class extends AbstractExtension implements GlobalsInterface { public function getGlobals(): array { return [ diff --git a/tests/ErrorTest.php b/tests/ErrorTest.php index d29112cd741..b7da2d50518 100644 --- a/tests/ErrorTest.php +++ b/tests/ErrorTest.php @@ -41,7 +41,7 @@ public function testTwigExceptionGuessWithMissingVarAndArrayLoader() {% block foo %} {{ foo.bar }} {% endblock %} -EOHTML +EOHTML, ]); $twig = new Environment($loader, ['strict_variables' => true, 'debug' => true, 'cache' => false]); @@ -70,7 +70,7 @@ public function testTwigExceptionGuessWithExceptionAndArrayLoader() {% block foo %} {{ foo.bar }} {% endblock %} -EOHTML +EOHTML, ]); $twig = new Environment($loader, ['strict_variables' => true, 'debug' => true, 'cache' => false]); @@ -163,7 +163,7 @@ public function testTwigArrayFilterThrowsRuntimeExceptions() {% for n in variable|filter(x => x > 3) %} This list contains {{n}}. {% endfor %} -EOHTML +EOHTML, ]); $twig = new Environment($loader, ['debug' => true, 'cache' => false]); @@ -190,7 +190,7 @@ public function testTwigArrayMapThrowsRuntimeExceptions() {% for n in variable|map(x => x * 3) %} {{- n -}} {% endfor %} -EOHTML +EOHTML, ]); $twig = new Environment($loader, ['debug' => true, 'cache' => false]); @@ -215,7 +215,7 @@ public function testTwigArrayReduceThrowsRuntimeExceptions() 'reduce-null.html' => << carry + x) }} -EOHTML +EOHTML, ]); $twig = new Environment($loader, ['debug' => true, 'cache' => false]); diff --git a/tests/ExpressionParserTest.php b/tests/ExpressionParserTest.php index 9b9d9b50488..d3887e93880 100644 --- a/tests/ExpressionParserTest.php +++ b/tests/ExpressionParserTest.php @@ -408,7 +408,7 @@ public function testUnknownTestWithoutSuggestions() public function testCompiledCodeForDynamicTest() { $env = new Environment(new ArrayLoader(['index' => '{{ "a" is foo_foo_bar_bar }}']), ['cache' => false, 'autoescape' => false]); - $env->addExtension(new class() extends AbstractExtension { + $env->addExtension(new class extends AbstractExtension { public function getTests() { return [ @@ -423,7 +423,7 @@ public function getTests() public function testCompiledCodeForDynamicFunction() { $env = new Environment(new ArrayLoader(['index' => '{{ foo_foo_bar_bar("a") }}']), ['cache' => false, 'autoescape' => false]); - $env->addExtension(new class() extends AbstractExtension { + $env->addExtension(new class extends AbstractExtension { public function getFunctions() { return [ @@ -438,7 +438,7 @@ public function getFunctions() public function testCompiledCodeForDynamicFilter() { $env = new Environment(new ArrayLoader(['index' => '{{ "a"|foo_foo_bar_bar }}']), ['cache' => false, 'autoescape' => false]); - $env->addExtension(new class() extends AbstractExtension { + $env->addExtension(new class extends AbstractExtension { public function getFilters() { return [ @@ -569,10 +569,10 @@ public function testTwoWordTestPrecedence() public function testUnaryPrecedenceChange() { $env = new Environment(new ArrayLoader(), ['cache' => false, 'autoescape' => false]); - $env->addExtension(new class () extends AbstractExtension { + $env->addExtension(new class extends AbstractExtension { public function getOperators() { - $class = new class (new ConstantExpression('foo', 1), 1) extends AbstractUnary { + $class = new class(new ConstantExpression('foo', 1), 1) extends AbstractUnary { public function operator(Compiler $compiler): Compiler { return $compiler->raw('!'); diff --git a/tests/Extension/CoreTest.php b/tests/Extension/CoreTest.php index edce0d3eede..bbea3d56c2c 100644 --- a/tests/Extension/CoreTest.php +++ b/tests/Extension/CoreTest.php @@ -58,7 +58,7 @@ public static function provideCycleInvalidCases() { return [ 'empty' => [[]], - 'non-countable' => [new class() extends \ArrayObject { + 'non-countable' => [new class extends \ArrayObject { }], ]; } diff --git a/tests/Extension/SandboxTest.php b/tests/Extension/SandboxTest.php index 9993370f6d4..3fb1f65de65 100644 --- a/tests/Extension/SandboxTest.php +++ b/tests/Extension/SandboxTest.php @@ -565,7 +565,7 @@ protected function getEnvironment($sandboxed, $options, $templates, $tags = [], public function testSandboxSourcePolicyEnableReturningFalse() { - $twig = $this->getEnvironment(false, [], self::$templates, [], [], [], [], [], new class() implements \Twig\Sandbox\SourcePolicyInterface { + $twig = $this->getEnvironment(false, [], self::$templates, [], [], [], [], [], new class implements \Twig\Sandbox\SourcePolicyInterface { public function enableSandbox(Source $source): bool { return '1_basic' != $source->getName(); @@ -576,7 +576,7 @@ public function enableSandbox(Source $source): bool public function testSandboxSourcePolicyEnableReturningTrue() { - $twig = $this->getEnvironment(false, [], self::$templates, [], [], [], [], [], new class() implements \Twig\Sandbox\SourcePolicyInterface { + $twig = $this->getEnvironment(false, [], self::$templates, [], [], [], [], [], new class implements \Twig\Sandbox\SourcePolicyInterface { public function enableSandbox(Source $source): bool { return '1_basic' === $source->getName(); @@ -588,7 +588,7 @@ public function enableSandbox(Source $source): bool public function testSandboxSourcePolicyFalseDoesntOverrideOtherEnables() { - $twig = $this->getEnvironment(true, [], self::$templates, [], [], [], [], [], new class() implements \Twig\Sandbox\SourcePolicyInterface { + $twig = $this->getEnvironment(true, [], self::$templates, [], [], [], [], [], new class implements \Twig\Sandbox\SourcePolicyInterface { public function enableSandbox(Source $source): bool { return false; diff --git a/tests/Node/Expression/FilterTest.php b/tests/Node/Expression/FilterTest.php index ff4484d4efd..a7134c4f9dd 100644 --- a/tests/Node/Expression/FilterTest.php +++ b/tests/Node/Expression/FilterTest.php @@ -180,7 +180,7 @@ protected static function createEnvironment(): Environment private static function createExtension(): AbstractExtension { - return new class() extends AbstractExtension { + return new class extends AbstractExtension { public function getFilters(): array { return [ From 1d5180549f517ab85133aa6902ce2d5553eefaab Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Wed, 22 Jan 2025 09:07:59 +0100 Subject: [PATCH 684/812] Ignore static properties when using the dot operator --- CHANGELOG | 1 + src/Extension/CoreExtension.php | 2 +- tests/IntegrationTest.php | 2 ++ 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 2fafecb650e..5d1d5562686 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -4,6 +4,7 @@ * Add the `invoke` filter * Make `{}` optional for the `types` tag * Add `LastModifiedExtensionInterface` and implementation in `AbstractExtension` to track modification of runtime classes + * Ignore static properties when using the dot operator # 3.18.0 (2024-12-29) diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index b83a0fed1db..29d13e6293f 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -2151,7 +2151,7 @@ private static function getPropertyChecker(string $class, string $property): \Cl $property = $class->getProperty($property); - if (!$property->isPublic()) { + if (!$property->isPublic() || $property->isStatic()) { static $false; return $false ??= static fn () => false; diff --git a/tests/IntegrationTest.php b/tests/IntegrationTest.php index 2a88c6af203..f4889cd754b 100644 --- a/tests/IntegrationTest.php +++ b/tests/IntegrationTest.php @@ -67,6 +67,8 @@ class TwigTestFoo implements \Iterator public $position = 0; public $array = [1, 2]; + public static $foo = 'Foo'; + public function bar($param1 = null, $param2 = null) { return 'bar'.($param1 ? '_'.$param1 : '').($param2 ? '-'.$param2 : ''); From 498fb5da4c9b403f77530c09c5077637ab363f33 Mon Sep 17 00:00:00 2001 From: omarius Date: Wed, 22 Jan 2025 13:51:54 +0100 Subject: [PATCH 685/812] Fixing minor typo in Update inline_css.rst --- doc/filters/inline_css.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/filters/inline_css.rst b/doc/filters/inline_css.rst index 44b142626d8..15f2eb2b439 100644 --- a/doc/filters/inline_css.rst +++ b/doc/filters/inline_css.rst @@ -1,7 +1,7 @@ ``inline_css`` ============== -The ``inline_css`` filter inline CSS styles in HTML documents: +The ``inline_css`` filter inlines CSS styles in HTML documents: .. code-block:: html+twig From 960f6762ed8d8b4210de5dc0769257b426e1c3c6 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Wed, 22 Jan 2025 20:59:41 +0100 Subject: [PATCH 686/812] Deprecate Twig\ExpressionParser::parseOnlyArguments() and Twig\ExpressionParser::parseArguments() --- CHANGELOG | 4 ++++ doc/deprecated.rst | 10 ++++++--- .../TokenParser/CacheTokenParser.php | 2 +- extra/cache-extra/composer.json | 2 +- src/ExpressionParser.php | 22 +++++++++++++++---- 5 files changed, 31 insertions(+), 9 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 2fafecb650e..53f2d72e549 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,9 @@ # 3.19.0 (2025-XX-XX) + * Deprecate `Twig\ExpressionParser::parseOnlyArguments()` and + `Twig\ExpressionParser::parseArguments()` (use + `Twig\ExpressionParser::parseNamedArguments()` instead) + * Fix `constant()` behavior when used with `??` * Add the `invoke` filter * Make `{}` optional for the `types` tag diff --git a/doc/deprecated.rst b/doc/deprecated.rst index c3d6c32ac6a..a8a04ab850b 100644 --- a/doc/deprecated.rst +++ b/doc/deprecated.rst @@ -56,9 +56,6 @@ Nodes as of Twig 3.12 as the tag is now automatically set by the Parser when needed. -* Passing a second argument to "ExpressionParser::parseFilterExpressionRaw()" - is deprecated as of Twig 3.12. - * The following ``Twig\Node\Node`` methods will take a string or an integer (instead of just a string) in Twig 4.0 for their "name" argument: ``getNode()``, ``hasNode()``, ``setNode()``, ``removeNode()``, and @@ -213,6 +210,9 @@ Node Visitors Parser ------ +* Passing a second argument to ``ExpressionParser::parseFilterExpressionRaw()`` + is deprecated as of Twig 3.12. + * The following methods from ``Twig\Parser`` are deprecated as of Twig 3.12: ``getBlockStack()``, ``hasBlock()``, ``getBlock()``, ``hasMacro()``, ``hasTraits()``, ``getParent()``. @@ -226,6 +226,10 @@ Parser * Passing ``null`` to ``Twig\Parser::setParent()`` is deprecated as of Twig 3.12. +* The ``Twig\ExpressionParser::parseOnlyArguments()`` and + ``Twig\ExpressionParser::parseArguments()`` methods are deprecated, use + ``Twig\ExpressionParser::parseNamedArguments()`` instead. + Lexer ----- diff --git a/extra/cache-extra/TokenParser/CacheTokenParser.php b/extra/cache-extra/TokenParser/CacheTokenParser.php index a5ea4840c14..b5fa3b87ac0 100644 --- a/extra/cache-extra/TokenParser/CacheTokenParser.php +++ b/extra/cache-extra/TokenParser/CacheTokenParser.php @@ -32,7 +32,7 @@ public function parse(Token $token): Node while ($stream->test(Token::NAME_TYPE)) { $k = $stream->getCurrent()->getValue(); $stream->next(); - $args = $expressionParser->parseArguments(); + $args = $expressionParser->parseNamedArguments(); switch ($k) { case 'ttl': diff --git a/extra/cache-extra/composer.json b/extra/cache-extra/composer.json index a3650499563..8b704f34ebf 100644 --- a/extra/cache-extra/composer.json +++ b/extra/cache-extra/composer.json @@ -17,7 +17,7 @@ "require": { "php": ">=8.0.2", "symfony/cache": "^5.4|^6.4|^7.0", - "twig/twig": "^3.13|^4.0" + "twig/twig": "^3.19|^4.0" }, "require-dev": { "symfony/phpunit-bridge": "^6.4|^7.0" diff --git a/src/ExpressionParser.php b/src/ExpressionParser.php index 0f1b0ed366a..f57edf37f09 100644 --- a/src/ExpressionParser.php +++ b/src/ExpressionParser.php @@ -530,7 +530,7 @@ public function getFunctionNode($name, $line) return new MacroReferenceExpression($alias['node']->getNode('var'), $alias['name'], $this->createArguments($line), $line); } - $args = $this->parseOnlyArguments(); + $args = $this->parseNamedArguments(); $function = $this->getFunction($name, $line); if ($function->getParserCallable()) { @@ -579,7 +579,7 @@ public function parseFilterExpressionRaw($node) if (!$this->parser->getStream()->test(Token::PUNCTUATION_TYPE, '(')) { $arguments = new EmptyNode(); } else { - $arguments = $this->parseOnlyArguments(); + $arguments = $this->parseNamedArguments(); } $filter = $this->getFilter($token->getValue(), $token->getLine()); @@ -611,9 +611,13 @@ public function parseFilterExpressionRaw($node) * @return Node * * @throws SyntaxError + * + * @deprecated since Twig 3.19 Use parseNamedArguments() instead */ public function parseArguments() { + trigger_deprecation('twig/twig', '3.19', \sprintf('The "%s()" method is deprecated, use "%s::parseNamedArguments()" instead.', __METHOD__, __CLASS__)); + $namedArguments = false; $definition = false; if (\func_num_args() > 1) { @@ -738,7 +742,7 @@ private function parseTestExpression(Node $node): TestExpression $arguments = null; if ($stream->test(Token::PUNCTUATION_TYPE, '(')) { - $arguments = $this->parseOnlyArguments(); + $arguments = $this->parseNamedArguments(); } elseif ($test->hasOneMandatoryArgument()) { $arguments = new Nodes([0 => $this->getPrimary()]); } @@ -864,14 +868,24 @@ private function setDeprecationCheck(bool $deprecationCheck): bool private function createArguments(int $line): ArrayExpression { $arguments = new ArrayExpression([], $line); - foreach ($this->parseOnlyArguments() as $k => $n) { + foreach ($this->parseNamedArguments() as $k => $n) { $arguments->addElement($n, new LocalVariable($k, $line)); } return $arguments; } + /** + * @deprecated since Twig 3.19 Use parseNamedArguments() instead + */ public function parseOnlyArguments() + { + trigger_deprecation('twig/twig', '3.19', \sprintf('The "%s()" method is deprecated, use "%s::parseNamedArguments()" instead.', __METHOD__, __CLASS__)); + + return $this->parseNamedArguments(); + } + + public function parseNamedArguments(): Nodes { $args = []; $stream = $this->parser->getStream(); From 82be13429c95fbc66a980a795f8df606afbe688e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Andr=C3=A9?= Date: Sun, 5 Jan 2025 16:04:43 +0100 Subject: [PATCH 687/812] Add return type annotations on Twig 3 to prepare Twig 4 --- src/AbstractTwigCallable.php | 3 ++ src/Environment.php | 54 +++++++++++++++++++ src/Extension/EscaperExtension.php | 10 ++++ src/Extension/SandboxExtension.php | 9 +++- src/ExtensionSet.php | 3 ++ src/Lexer.php | 4 +- src/Markup.php | 2 +- src/Node/CheckSecurityCallNode.php | 3 ++ src/Node/Expression/CallExpression.php | 3 ++ src/Node/Expression/FunctionExpression.php | 3 ++ src/Node/Expression/Test/DefinedTest.php | 2 +- src/Node/IncludeNode.php | 6 +++ src/Node/ModuleNode.php | 36 +++++++++++++ src/Node/Node.php | 5 +- src/Node/TypesNode.php | 3 ++ src/NodeVisitor/EscaperNodeVisitor.php | 5 +- src/Parser.php | 12 +++++ src/Profiler/Dumper/BlackfireDumper.php | 4 +- src/Runtime/EscaperRuntime.php | 6 +++ src/Sandbox/SecurityNotAllowedFilterError.php | 2 +- .../SecurityNotAllowedFunctionError.php | 2 +- src/Sandbox/SecurityNotAllowedMethodError.php | 6 +-- .../SecurityNotAllowedPropertyError.php | 6 +-- src/Sandbox/SecurityNotAllowedTagError.php | 2 +- src/TemplateWrapper.php | 6 +++ src/Test/IntegrationTestCase.php | 14 +++++ src/Test/NodeTestCase.php | 17 ++++++ src/Token.php | 5 +- src/TokenParser/IncludeTokenParser.php | 4 ++ src/TokenStream.php | 5 +- src/Util/ReflectionCallable.php | 3 ++ 31 files changed, 225 insertions(+), 20 deletions(-) diff --git a/src/AbstractTwigCallable.php b/src/AbstractTwigCallable.php index d85f0f861f8..804f336cbe3 100644 --- a/src/AbstractTwigCallable.php +++ b/src/AbstractTwigCallable.php @@ -79,6 +79,9 @@ public function getDynamicName(): string return $this->dynamicName; } + /** + * @return callable|array{class-string, string}|null + */ public function getCallable() { return $this->callable; diff --git a/src/Environment.php b/src/Environment.php index 430d3991195..7808386ffd4 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -155,6 +155,8 @@ public function useYield(): bool /** * Enables debugging mode. + * + * @return void */ public function enableDebug() { @@ -164,6 +166,8 @@ public function enableDebug() /** * Disables debugging mode. + * + * @return void */ public function disableDebug() { @@ -183,6 +187,8 @@ public function isDebug() /** * Enables the auto_reload option. + * + * @return void */ public function enableAutoReload() { @@ -191,6 +197,8 @@ public function enableAutoReload() /** * Disables the auto_reload option. + * + * @return void */ public function disableAutoReload() { @@ -209,6 +217,8 @@ public function isAutoReload() /** * Enables the strict_variables option. + * + * @return void */ public function enableStrictVariables() { @@ -218,6 +228,8 @@ public function enableStrictVariables() /** * Disables the strict_variables option. + * + * @return void */ public function disableStrictVariables() { @@ -267,6 +279,8 @@ public function getCache($original = true) * @param CacheInterface|string|false $cache A Twig\Cache\CacheInterface implementation, * an absolute path to the compiled templates, * or false to disable cache + * + * @return void */ public function setCache($cache) { @@ -503,6 +517,9 @@ public function resolveTemplate($names): TemplateWrapper throw new LoaderError(\sprintf('Unable to find one of the following templates: "%s".', implode('", "', $names))); } + /** + * @return void + */ public function setLexer(Lexer $lexer) { $this->lexer = $lexer; @@ -520,6 +537,9 @@ public function tokenize(Source $source): TokenStream return $this->lexer->tokenize($source); } + /** + * @return void + */ public function setParser(Parser $parser) { $this->parser = $parser; @@ -539,6 +559,9 @@ public function parse(TokenStream $stream): ModuleNode return $this->parser->parse($stream); } + /** + * @return void + */ public function setCompiler(Compiler $compiler) { $this->compiler = $compiler; @@ -573,6 +596,9 @@ public function compileSource(Source $source): string } } + /** + * @return void + */ public function setLoader(LoaderInterface $loader) { $this->loader = $loader; @@ -583,6 +609,9 @@ public function getLoader(): LoaderInterface return $this->loader; } + /** + * @return void + */ public function setCharset(string $charset) { if ('UTF8' === $charset = strtoupper($charset ?: '')) { @@ -603,6 +632,9 @@ public function hasExtension(string $class): bool return $this->extensionSet->hasExtension($class); } + /** + * @return void + */ public function addRuntimeLoader(RuntimeLoaderInterface $loader) { $this->runtimeLoaders[] = $loader; @@ -650,6 +682,9 @@ public function getRuntime(string $class) throw new RuntimeError(\sprintf('Unable to load the "%s" runtime.', $class)); } + /** + * @return void + */ public function addExtension(ExtensionInterface $extension) { $this->extensionSet->addExtension($extension); @@ -658,6 +693,8 @@ public function addExtension(ExtensionInterface $extension) /** * @param ExtensionInterface[] $extensions An array of extensions + * + * @return void */ public function setExtensions(array $extensions) { @@ -673,6 +710,9 @@ public function getExtensions(): array return $this->extensionSet->getExtensions(); } + /** + * @return void + */ public function addTokenParser(TokenParserInterface $parser) { $this->extensionSet->addTokenParser($parser); @@ -701,6 +741,9 @@ public function registerUndefinedTokenParserCallback(callable $callable): void $this->extensionSet->registerUndefinedTokenParserCallback($callable); } + /** + * @return void + */ public function addNodeVisitor(NodeVisitorInterface $visitor) { $this->extensionSet->addNodeVisitor($visitor); @@ -716,6 +759,9 @@ public function getNodeVisitors(): array return $this->extensionSet->getNodeVisitors(); } + /** + * @return void + */ public function addFilter(TwigFilter $filter) { $this->extensionSet->addFilter($filter); @@ -750,6 +796,9 @@ public function getFilters(): array return $this->extensionSet->getFilters(); } + /** + * @return void + */ public function addTest(TwigTest $test) { $this->extensionSet->addTest($test); @@ -773,6 +822,9 @@ public function getTest(string $name): ?TwigTest return $this->extensionSet->getTest($name); } + /** + * @return void + */ public function addFunction(TwigFunction $function) { $this->extensionSet->addFunction($function); @@ -814,6 +866,8 @@ public function getFunctions(): array * but after, you can only update existing globals. * * @param mixed $value The global value + * + * @return void */ public function addGlobal(string $name, $value) { diff --git a/src/Extension/EscaperExtension.php b/src/Extension/EscaperExtension.php index 9a05e4c7c65..c5625fa6a41 100644 --- a/src/Extension/EscaperExtension.php +++ b/src/Extension/EscaperExtension.php @@ -80,6 +80,8 @@ public function setEnvironment(Environment $environment): void } /** + * @return void + * * @deprecated since Twig 3.10 */ public function setEscaperRuntime(EscaperRuntime $escaper) @@ -130,6 +132,8 @@ public function getDefaultStrategy(string $name) * @param string $strategy The strategy name that should be used as a strategy in the escape call * @param callable(Environment, string, string): string $callable A valid PHP callable * + * @return void + * * @deprecated since Twig 3.10 */ public function setEscaper($strategy, callable $callable) @@ -163,6 +167,8 @@ public function getEscapers() } /** + * @return void + * * @deprecated since Twig 3.10 */ public function setSafeClasses(array $safeClasses = []) @@ -177,6 +183,8 @@ public function setSafeClasses(array $safeClasses = []) } /** + * @return void + * * @deprecated since Twig 3.10 */ public function addSafeClass(string $class, array $strategies) @@ -192,6 +200,8 @@ public function addSafeClass(string $class, array $strategies) /** * @internal + * + * @return array */ public static function escapeFilterIsSafe(Node $filterArgs) { diff --git a/src/Extension/SandboxExtension.php b/src/Extension/SandboxExtension.php index bbf73964574..a9681c8d6c1 100644 --- a/src/Extension/SandboxExtension.php +++ b/src/Extension/SandboxExtension.php @@ -72,7 +72,7 @@ private function isSourceSandboxed(?Source $source): bool return $this->sourcePolicy->enableSandbox($source); } - public function setSecurityPolicy(SecurityPolicyInterface $policy) + public function setSecurityPolicy(SecurityPolicyInterface $policy): void { $this->policy = $policy; } @@ -117,6 +117,13 @@ public function checkPropertyAllowed($obj, $property, int $lineno = -1, ?Source } } + /** + * @param mixed $obj + * + * @return mixed + * + * @throws SecurityNotAllowedMethodError + */ public function ensureToStringAllowed($obj, int $lineno = -1, ?Source $source = null) { if (\is_array($obj)) { diff --git a/src/ExtensionSet.php b/src/ExtensionSet.php index 18979228a7d..2b17182b2a3 100644 --- a/src/ExtensionSet.php +++ b/src/ExtensionSet.php @@ -62,6 +62,9 @@ public function __construct() $this->staging = new StagingExtension(); } + /** + * @return void + */ public function initRuntime() { $this->runtimeInitialized = true; diff --git a/src/Lexer.php b/src/Lexer.php index 0338fd874ff..929673c6082 100644 --- a/src/Lexer.php +++ b/src/Lexer.php @@ -53,7 +53,7 @@ class Lexer (?(?&LNUM)(?:(?&FRAC))?) # Decimal number 123_456.456 )(?:(?&DNUM)(?:(?&EXPONENT))?) # 123_456.456E+10 /Ax'; - + public const REGEX_DQ_STRING_DELIM = '/"/A'; public const REGEX_DQ_STRING_PART = '/[^#"\\\\]*(?:(?:\\\\.|#(?!\{))[^#"\\\\]*)*/As'; public const REGEX_INLINE_COMMENT = '/#[^\n]*/A'; @@ -82,7 +82,7 @@ public function __construct(Environment $env, array $options = []) ], $options); } - private function initialize() + private function initialize(): void { if ($this->isInitialized) { return; diff --git a/src/Markup.php b/src/Markup.php index c7aa65bdad5..a933b69d327 100644 --- a/src/Markup.php +++ b/src/Markup.php @@ -27,7 +27,7 @@ public function __construct($content, $charset) $this->charset = $charset; } - public function __toString() + public function __toString(): string { return $this->content; } diff --git a/src/Node/CheckSecurityCallNode.php b/src/Node/CheckSecurityCallNode.php index 9c162d129c6..bb8783bc3d9 100644 --- a/src/Node/CheckSecurityCallNode.php +++ b/src/Node/CheckSecurityCallNode.php @@ -20,6 +20,9 @@ #[YieldReady] class CheckSecurityCallNode extends Node { + /** + * @return void + */ public function compile(Compiler $compiler) { $compiler diff --git a/src/Node/Expression/CallExpression.php b/src/Node/Expression/CallExpression.php index 8e999c7eb9e..330d82535c3 100644 --- a/src/Node/Expression/CallExpression.php +++ b/src/Node/Expression/CallExpression.php @@ -26,6 +26,9 @@ abstract class CallExpression extends AbstractExpression { private $reflector = null; + /** + * @return void + */ protected function compileCallable(Compiler $compiler) { $twigCallable = $this->getTwigCallable(); diff --git a/src/Node/Expression/FunctionExpression.php b/src/Node/Expression/FunctionExpression.php index 6215c6abfbc..5e22e73e89c 100644 --- a/src/Node/Expression/FunctionExpression.php +++ b/src/Node/Expression/FunctionExpression.php @@ -44,6 +44,9 @@ public function __construct(TwigFunction|string $function, Node $arguments, int $this->deprecateAttribute('dynamic_name', new NameDeprecation('twig/twig', '3.12')); } + /** + * @return void + */ public function compile(Compiler $compiler) { $name = $this->getAttribute('name'); diff --git a/src/Node/Expression/Test/DefinedTest.php b/src/Node/Expression/Test/DefinedTest.php index 7a0150d18b9..62aec921750 100644 --- a/src/Node/Expression/Test/DefinedTest.php +++ b/src/Node/Expression/Test/DefinedTest.php @@ -75,7 +75,7 @@ public function __construct(Node $node, TwigTest|string $name, ?Node $arguments, parent::__construct($node, $name, $arguments, $lineno); } - private function changeIgnoreStrictCheck(GetAttrExpression $node) + private function changeIgnoreStrictCheck(GetAttrExpression $node): void { $node->setAttribute('optimizable', false); $node->setAttribute('ignore_strict_check', true); diff --git a/src/Node/IncludeNode.php b/src/Node/IncludeNode.php index 5e0c6deb05b..4761cb83243 100644 --- a/src/Node/IncludeNode.php +++ b/src/Node/IncludeNode.php @@ -78,6 +78,9 @@ public function compile(Compiler $compiler): void } } + /** + * @return void + */ protected function addGetTemplate(Compiler $compiler/* , string $template = '' */) { $compiler @@ -91,6 +94,9 @@ protected function addGetTemplate(Compiler $compiler/* , string $template = '' * ; } + /** + * @return void + */ protected function addTemplateArguments(Compiler $compiler) { if (!$this->hasNode('variables')) { diff --git a/src/Node/ModuleNode.php b/src/Node/ModuleNode.php index 9e2774e872b..b3f4e6c2af3 100644 --- a/src/Node/ModuleNode.php +++ b/src/Node/ModuleNode.php @@ -64,6 +64,9 @@ public function __construct(Node $body, ?AbstractExpression $parent, Node $block $this->setSourceContext($source); } + /** + * @return void + */ public function setIndex($index) { $this->setAttribute('index', $index); @@ -78,6 +81,9 @@ public function compile(Compiler $compiler): void } } + /** + * @return void + */ protected function compileTemplate(Compiler $compiler) { if (!$this->getAttribute('index')) { @@ -107,6 +113,9 @@ protected function compileTemplate(Compiler $compiler) $this->compileClassFooter($compiler); } + /** + * @return void + */ protected function compileGetParent(Compiler $compiler) { if (!$this->hasNode('parent')) { @@ -142,6 +151,9 @@ protected function compileGetParent(Compiler $compiler) ; } + /** + * @return void + */ protected function compileClassHeader(Compiler $compiler) { $compiler @@ -180,6 +192,9 @@ protected function compileClassHeader(Compiler $compiler) ; } + /** + * @return void + */ protected function compileConstructor(Compiler $compiler) { $compiler @@ -319,6 +334,9 @@ protected function compileConstructor(Compiler $compiler) ; } + /** + * @return void + */ protected function compileDisplay(Compiler $compiler) { $compiler @@ -366,6 +384,9 @@ protected function compileDisplay(Compiler $compiler) ; } + /** + * @return void + */ protected function compileClassFooter(Compiler $compiler) { $compiler @@ -375,11 +396,17 @@ protected function compileClassFooter(Compiler $compiler) ; } + /** + * @return void + */ protected function compileMacros(Compiler $compiler) { $compiler->subcompile($this->getNode('macros')); } + /** + * @return void + */ protected function compileGetTemplateName(Compiler $compiler) { $compiler @@ -396,6 +423,9 @@ protected function compileGetTemplateName(Compiler $compiler) ; } + /** + * @return void + */ protected function compileIsTraitable(Compiler $compiler) { // A template can be used as a trait if: @@ -443,6 +473,9 @@ protected function compileIsTraitable(Compiler $compiler) ; } + /** + * @return void + */ protected function compileDebugInfo(Compiler $compiler) { $compiler @@ -457,6 +490,9 @@ protected function compileDebugInfo(Compiler $compiler) ; } + /** + * @return void + */ protected function compileGetSourceContext(Compiler $compiler) { $compiler diff --git a/src/Node/Node.php b/src/Node/Node.php index 7b4044c3f34..7a8856b1186 100644 --- a/src/Node/Node.php +++ b/src/Node/Node.php @@ -65,7 +65,7 @@ public function __construct(array $nodes = [], array $attributes = [], int $line } } - public function __toString() + public function __toString(): string { $repr = static::class; @@ -142,6 +142,9 @@ public function hasAttribute(string $name): bool return \array_key_exists($name, $this->attributes); } + /** + * @return mixed + */ public function getAttribute(string $name) { if (!\array_key_exists($name, $this->attributes)) { diff --git a/src/Node/TypesNode.php b/src/Node/TypesNode.php index ebb304d49f8..b5949848d1a 100644 --- a/src/Node/TypesNode.php +++ b/src/Node/TypesNode.php @@ -21,6 +21,9 @@ public function __construct(array $types, int $lineno) parent::__construct([], ['mapping' => $types], $lineno); } + /** + * @return void + */ public function compile(Compiler $compiler) { // Don't compile anything. diff --git a/src/NodeVisitor/EscaperNodeVisitor.php b/src/NodeVisitor/EscaperNodeVisitor.php index a70726bbd85..b35ae881720 100644 --- a/src/NodeVisitor/EscaperNodeVisitor.php +++ b/src/NodeVisitor/EscaperNodeVisitor.php @@ -157,7 +157,10 @@ private function isSafeFor(string $type, AbstractExpression $expression, Environ return \in_array($type, $safe) || \in_array('all', $safe); } - private function needEscaping() + /** + * @return string|false + */ + private function needEscaping(): string|bool { if (\count($this->statusStack)) { return $this->statusStack[\count($this->statusStack) - 1]; diff --git a/src/Parser.php b/src/Parser.php index 7bf51b73c20..9a8f97e2efe 100644 --- a/src/Parser.php +++ b/src/Parser.php @@ -227,6 +227,9 @@ public function getBlockStack(): array return $this->blockStack; } + /** + * @return string|null + */ public function peekBlockStack() { return $this->blockStack[\count($this->blockStack) - 1] ?? null; @@ -289,6 +292,9 @@ public function hasTraits(): bool return \count($this->traits) > 0; } + /** + * @return void + */ public function embedTemplate(ModuleNode $template) { $template->setIndex(mt_rand()); @@ -307,6 +313,9 @@ public function addImportedSymbol(string $type, string $alias, ?string $name = n $this->importedSymbols[0][$type][$alias] = ['name' => $name, 'node' => $internalRef]; } + /** + * @return array{name: string, node: AssignTemplateVariable|null}|null + */ public function getImportedSymbol(string $type, string $alias) { // if the symbol does not exist in the current scope (0), try in the main/global scope (last index) @@ -340,6 +349,9 @@ public function getParent(): ?Node return $this->parent; } + /** + * @return bool + */ public function hasInheritance() { return $this->parent || 0 < \count($this->traits); diff --git a/src/Profiler/Dumper/BlackfireDumper.php b/src/Profiler/Dumper/BlackfireDumper.php index bb3fbb52a2a..7cfae16f1c6 100644 --- a/src/Profiler/Dumper/BlackfireDumper.php +++ b/src/Profiler/Dumper/BlackfireDumper.php @@ -40,7 +40,7 @@ public function dump(Profile $profile): string return $str; } - private function dumpChildren(string $parent, Profile $profile, &$data) + private function dumpChildren(string $parent, Profile $profile, &$data): void { foreach ($profile as $p) { if ($p->isTemplate()) { @@ -53,7 +53,7 @@ private function dumpChildren(string $parent, Profile $profile, &$data) } } - private function dumpProfile(string $edge, Profile $profile, &$data) + private function dumpProfile(string $edge, Profile $profile, &$data): void { if (isset($data[$edge])) { ++$data[$edge]['ct']; diff --git a/src/Runtime/EscaperRuntime.php b/src/Runtime/EscaperRuntime.php index ce41e0a8b87..198be2ff8fc 100644 --- a/src/Runtime/EscaperRuntime.php +++ b/src/Runtime/EscaperRuntime.php @@ -36,6 +36,8 @@ public function __construct( * * @param string $strategy The strategy name that should be used as a strategy in the escape call * @param callable(string $string, string $charset): string $callable A valid PHP callable + * + * @return void */ public function setEscaper($strategy, callable $callable) { @@ -54,6 +56,8 @@ public function getEscapers() /** * @param array, string[]> $safeClasses + * + * @return void */ public function setSafeClasses(array $safeClasses = []) { @@ -67,6 +71,8 @@ public function setSafeClasses(array $safeClasses = []) /** * @param class-string<\Stringable> $class * @param string[] $strategies + * + * @return void */ public function addSafeClass(string $class, array $strategies) { diff --git a/src/Sandbox/SecurityNotAllowedFilterError.php b/src/Sandbox/SecurityNotAllowedFilterError.php index 02d306360ba..9293a3f0b93 100644 --- a/src/Sandbox/SecurityNotAllowedFilterError.php +++ b/src/Sandbox/SecurityNotAllowedFilterError.php @@ -18,7 +18,7 @@ */ final class SecurityNotAllowedFilterError extends SecurityError { - private $filterName; + private string $filterName; public function __construct(string $message, string $functionName) { diff --git a/src/Sandbox/SecurityNotAllowedFunctionError.php b/src/Sandbox/SecurityNotAllowedFunctionError.php index 4f76dc6ece4..71c9f02bc5b 100644 --- a/src/Sandbox/SecurityNotAllowedFunctionError.php +++ b/src/Sandbox/SecurityNotAllowedFunctionError.php @@ -18,7 +18,7 @@ */ final class SecurityNotAllowedFunctionError extends SecurityError { - private $functionName; + private string $functionName; public function __construct(string $message, string $functionName) { diff --git a/src/Sandbox/SecurityNotAllowedMethodError.php b/src/Sandbox/SecurityNotAllowedMethodError.php index 8df9d0baa96..98e8e434d93 100644 --- a/src/Sandbox/SecurityNotAllowedMethodError.php +++ b/src/Sandbox/SecurityNotAllowedMethodError.php @@ -18,8 +18,8 @@ */ final class SecurityNotAllowedMethodError extends SecurityError { - private $className; - private $methodName; + private string $className; + private string $methodName; public function __construct(string $message, string $className, string $methodName) { @@ -33,7 +33,7 @@ public function getClassName(): string return $this->className; } - public function getMethodName() + public function getMethodName(): string { return $this->methodName; } diff --git a/src/Sandbox/SecurityNotAllowedPropertyError.php b/src/Sandbox/SecurityNotAllowedPropertyError.php index 42ec4f38694..e74ffeddb25 100644 --- a/src/Sandbox/SecurityNotAllowedPropertyError.php +++ b/src/Sandbox/SecurityNotAllowedPropertyError.php @@ -18,8 +18,8 @@ */ final class SecurityNotAllowedPropertyError extends SecurityError { - private $className; - private $propertyName; + private string $className; + private string $propertyName; public function __construct(string $message, string $className, string $propertyName) { @@ -33,7 +33,7 @@ public function getClassName(): string return $this->className; } - public function getPropertyName() + public function getPropertyName(): string { return $this->propertyName; } diff --git a/src/Sandbox/SecurityNotAllowedTagError.php b/src/Sandbox/SecurityNotAllowedTagError.php index 4522150e1b7..f9cd625b4c0 100644 --- a/src/Sandbox/SecurityNotAllowedTagError.php +++ b/src/Sandbox/SecurityNotAllowedTagError.php @@ -18,7 +18,7 @@ */ final class SecurityNotAllowedTagError extends SecurityError { - private $tagName; + private string $tagName; public function __construct(string $message, string $tagName) { diff --git a/src/TemplateWrapper.php b/src/TemplateWrapper.php index 5528037a602..265ce3e1ccc 100644 --- a/src/TemplateWrapper.php +++ b/src/TemplateWrapper.php @@ -51,6 +51,9 @@ public function render(array $context = []): string return $this->template->render($context); } + /** + * @return void + */ public function display(array $context = []) { // using func_get_args() allows to not expose the blocks argument @@ -76,6 +79,9 @@ public function renderBlock(string $name, array $context = []): string return $this->template->renderBlock($name, $context + $this->env->getGlobals()); } + /** + * @return void + */ public function displayBlock(string $name, array $context = []) { $context += $this->env->getGlobals(); diff --git a/src/Test/IntegrationTestCase.php b/src/Test/IntegrationTestCase.php index 8c9d41f2a44..1fb3c313b35 100644 --- a/src/Test/IntegrationTestCase.php +++ b/src/Test/IntegrationTestCase.php @@ -86,6 +86,8 @@ protected function getTwigTests() /** * @dataProvider getTests + * + * @return void */ public function testIntegration($file, $message, $condition, $templates, $exception, $outputs, $deprecation = '') { @@ -96,6 +98,8 @@ public function testIntegration($file, $message, $condition, $templates, $except * @dataProvider getLegacyTests * * @group legacy + * + * @return void */ public function testLegacyIntegration($file, $message, $condition, $templates, $exception, $outputs, $deprecation = '') { @@ -103,6 +107,8 @@ public function testLegacyIntegration($file, $message, $condition, $templates, $ } /** + * @return iterable + * * @final since Twig 3.13 */ public function getTests($name, $legacyTests = false) @@ -159,12 +165,17 @@ public function getTests($name, $legacyTests = false) /** * @final since Twig 3.13 + * + * @return iterable */ public function getLegacyTests() { return $this->getTests('testLegacyIntegration', true); } + /** + * @return void + */ protected function doIntegrationTest($file, $message, $condition, $templates, $exception, $outputs, $deprecation = '') { if (!$outputs) { @@ -275,6 +286,9 @@ protected function doIntegrationTest($file, $message, $condition, $templates, $e } } + /** + * @return array + */ protected static function parseTemplates($test) { $templates = []; diff --git a/src/Test/NodeTestCase.php b/src/Test/NodeTestCase.php index bac0ea6d036..0cb5b2fab05 100644 --- a/src/Test/NodeTestCase.php +++ b/src/Test/NodeTestCase.php @@ -26,6 +26,9 @@ abstract class NodeTestCase extends TestCase */ private $currentEnv; + /** + * @return iterable + */ public function getTests() { return []; @@ -44,6 +47,8 @@ public static function provideTests(): iterable /** * @dataProvider getTests * @dataProvider provideTests + * + * @return void */ #[DataProvider('getTests'), DataProvider('provideTests')] public function testCompile($node, $source, $environment = null, $isPattern = false) @@ -51,6 +56,9 @@ public function testCompile($node, $source, $environment = null, $isPattern = fa $this->assertNodeCompilation($source, $node, $environment, $isPattern); } + /** + * @return void + */ public function assertNodeCompilation($source, Node $node, ?Environment $environment = null, $isPattern = false) { $compiler = $this->getCompiler($environment); @@ -63,12 +71,17 @@ public function assertNodeCompilation($source, Node $node, ?Environment $environ } } + /** + * @return Compiler + */ protected function getCompiler(?Environment $environment = null) { return new Compiler($environment ?? $this->getEnvironment()); } /** + * @return Environment + * * @final since Twig 3.13 */ protected function getEnvironment() @@ -82,6 +95,8 @@ protected static function createEnvironment(): Environment } /** + * @return string + * * @deprecated since Twig 3.13, use createVariableGetter() instead. */ protected function getVariableGetter($name, $line = false) @@ -99,6 +114,8 @@ final protected static function createVariableGetter(string $name, bool $line = } /** + * @return string + * * @deprecated since Twig 3.13, use createAttributeGetter() instead. */ protected function getAttributeGetter() diff --git a/src/Token.php b/src/Token.php index 237634ad137..c3c656f0605 100644 --- a/src/Token.php +++ b/src/Token.php @@ -40,7 +40,7 @@ public function __construct( ) { } - public function __toString() + public function __toString(): string { return \sprintf('%s(%s)', self::typeToString($this->type, true), $this->value); } @@ -80,6 +80,9 @@ public function getType(): int return $this->type; } + /** + * @return mixed + */ public function getValue() { return $this->value; diff --git a/src/TokenParser/IncludeTokenParser.php b/src/TokenParser/IncludeTokenParser.php index a72250cb290..c5ce180ad2a 100644 --- a/src/TokenParser/IncludeTokenParser.php +++ b/src/TokenParser/IncludeTokenParser.php @@ -12,6 +12,7 @@ namespace Twig\TokenParser; +use Twig\Node\Expression\AbstractExpression; use Twig\Node\IncludeNode; use Twig\Node\Node; use Twig\Token; @@ -36,6 +37,9 @@ public function parse(Token $token): Node return new IncludeNode($expr, $variables, $only, $ignoreMissing, $token->getLine()); } + /** + * @return array{0: ?AbstractExpression, 1: bool, 2: bool} + */ protected function parseArguments() { $stream = $this->parser->getStream(); diff --git a/src/TokenStream.php b/src/TokenStream.php index 35aa9714fc7..ec6ac959257 100644 --- a/src/TokenStream.php +++ b/src/TokenStream.php @@ -34,11 +34,14 @@ public function __construct( } } - public function __toString() + public function __toString(): string { return implode("\n", $this->tokens); } + /** + * @return void + */ public function injectTokens(array $tokens) { $this->tokens = array_merge(\array_slice($this->tokens, 0, $this->current), $tokens, \array_slice($this->tokens, $this->current)); diff --git a/src/Util/ReflectionCallable.php b/src/Util/ReflectionCallable.php index 16734d9df78..0298e291de3 100644 --- a/src/Util/ReflectionCallable.php +++ b/src/Util/ReflectionCallable.php @@ -80,6 +80,9 @@ public function getReflector(): \ReflectionFunctionAbstract return $this->reflector; } + /** + * @return callable + */ public function getCallable() { return $this->callable; From b6d9dd62eeb58fc72407acdcddcfe8ebb39e9f4d Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Thu, 23 Jan 2025 18:39:15 +0100 Subject: [PATCH 688/812] Fix inheritance logic to be able to use instanceof ContextVariable instead of NameExpression --- src/ExpressionParser.php | 8 ++++---- src/Node/Expression/ArrayExpression.php | 3 ++- src/Node/Expression/AssignNameExpression.php | 3 ++- src/Node/Expression/Binary/NullCoalesceBinary.php | 4 ++-- src/Node/Expression/Filter/DefaultFilter.php | 4 ++-- src/Node/Expression/GetAttrExpression.php | 3 ++- src/Node/Expression/MethodCallExpression.php | 3 ++- src/Node/Expression/NullCoalesceExpression.php | 3 ++- src/Node/Expression/Test/DefinedTest.php | 3 ++- src/Node/Expression/Variable/ContextVariable.php | 2 +- src/Node/ImportNode.php | 3 ++- src/NodeVisitor/OptimizerNodeVisitor.php | 7 ++++--- src/NodeVisitor/SafeAnalysisNodeVisitor.php | 3 ++- src/NodeVisitor/SandboxNodeVisitor.php | 3 ++- src/TokenParser/EmbedTokenParser.php | 3 ++- 15 files changed, 33 insertions(+), 22 deletions(-) diff --git a/src/ExpressionParser.php b/src/ExpressionParser.php index 0f1b0ed366a..f03df9733b3 100644 --- a/src/ExpressionParser.php +++ b/src/ExpressionParser.php @@ -655,7 +655,7 @@ public function parseArguments() $name = null; if ($namedArguments && (($token = $stream->nextIf(Token::OPERATOR_TYPE, '=')) || (!$definition && $token = $stream->nextIf(Token::PUNCTUATION_TYPE, ':')))) { - if (!$value instanceof NameExpression) { + if (!$value instanceof ContextVariable) { throw new SyntaxError(\sprintf('A parameter name must be a string, "%s" given.', \get_class($value)), $token->getLine(), $stream->getSourceContext()); } $name = $value->getAttribute('name'); @@ -743,7 +743,7 @@ private function parseTestExpression(Node $node): TestExpression $arguments = new Nodes([0 => $this->getPrimary()]); } - if ('defined' === $test->getName() && $node instanceof NameExpression && null !== $alias = $this->parser->getImportedSymbol('function', $node->getAttribute('name'))) { + if ('defined' === $test->getName() && $node instanceof ContextVariable && null !== $alias = $this->parser->getImportedSymbol('function', $node->getAttribute('name'))) { $node = new MacroReferenceExpression($alias['node']->getNode('var'), $alias['name'], new ArrayExpression([], $node->getTemplateLine()), $node->getTemplateLine()); } @@ -898,7 +898,7 @@ public function parseOnlyArguments() $name = null; if (($token = $stream->nextIf(Token::OPERATOR_TYPE, '=')) || ($token = $stream->nextIf(Token::PUNCTUATION_TYPE, ':'))) { - if (!$value instanceof NameExpression) { + if (!$value instanceof ContextVariable) { throw new SyntaxError(\sprintf('A parameter name must be a string, "%s" given.', \get_class($value)), $token->getLine(), $stream->getSourceContext()); } $name = $value->getAttribute('name'); @@ -946,7 +946,7 @@ private function parseSubscriptExpressionDot(Node $node): AbstractExpression } if ( - $node instanceof NameExpression + $node instanceof ContextVariable && ( null !== $this->parser->getImportedSymbol('template', $node->getAttribute('name')) || '_self' === $node->getAttribute('name') && $attribute instanceof ConstantExpression diff --git a/src/Node/Expression/ArrayExpression.php b/src/Node/Expression/ArrayExpression.php index 9769b719e36..87d5cb8d749 100644 --- a/src/Node/Expression/ArrayExpression.php +++ b/src/Node/Expression/ArrayExpression.php @@ -13,6 +13,7 @@ use Twig\Compiler; use Twig\Node\Expression\Unary\StringCastUnary; +use Twig\Node\Expression\Variable\ContextVariable; class ArrayExpression extends AbstractExpression { @@ -99,7 +100,7 @@ public function compile(Compiler $compiler): void ++$nextIndex; } else { $key = null; - if ($pair['key'] instanceof NameExpression) { + if ($pair['key'] instanceof ContextVariable) { $pair['key'] = new StringCastUnary($pair['key'], $pair['key']->getTemplateLine()); } if ($pair['key'] instanceof TempNameExpression) { diff --git a/src/Node/Expression/AssignNameExpression.php b/src/Node/Expression/AssignNameExpression.php index c2cbb8e4a43..c194660daa8 100644 --- a/src/Node/Expression/AssignNameExpression.php +++ b/src/Node/Expression/AssignNameExpression.php @@ -15,8 +15,9 @@ use Twig\Compiler; use Twig\Error\SyntaxError; use Twig\Node\Expression\Variable\AssignContextVariable; +use Twig\Node\Expression\Variable\ContextVariable; -class AssignNameExpression extends NameExpression +class AssignNameExpression extends ContextVariable { public function __construct(string $name, int $lineno) { diff --git a/src/Node/Expression/Binary/NullCoalesceBinary.php b/src/Node/Expression/Binary/NullCoalesceBinary.php index 15b6e8ee937..1472e8e2e7a 100644 --- a/src/Node/Expression/Binary/NullCoalesceBinary.php +++ b/src/Node/Expression/Binary/NullCoalesceBinary.php @@ -15,11 +15,11 @@ use Twig\Node\EmptyNode; use Twig\Node\Expression\AbstractExpression; use Twig\Node\Expression\BlockReferenceExpression; -use Twig\Node\Expression\NameExpression; use Twig\Node\Expression\OperatorEscapeInterface; use Twig\Node\Expression\Test\DefinedTest; use Twig\Node\Expression\Test\NullTest; use Twig\Node\Expression\Unary\NotUnary; +use Twig\Node\Expression\Variable\ContextVariable; use Twig\TwigTest; final class NullCoalesceBinary extends AbstractBinary implements OperatorEscapeInterface @@ -28,7 +28,7 @@ public function __construct(AbstractExpression $left, AbstractExpression $right, { parent::__construct($left, $right, $lineno); - if (!$left instanceof NameExpression) { + if (!$left instanceof ContextVariable) { $test = new DefinedTest(clone $left, new TwigTest('defined'), new EmptyNode(), $left->getTemplateLine()); // for "block()", we don't need the null test as the return value is always a string if (!$left instanceof BlockReferenceExpression) { diff --git a/src/Node/Expression/Filter/DefaultFilter.php b/src/Node/Expression/Filter/DefaultFilter.php index cabd2aa1afd..bccd7f0a4a7 100644 --- a/src/Node/Expression/Filter/DefaultFilter.php +++ b/src/Node/Expression/Filter/DefaultFilter.php @@ -19,9 +19,9 @@ use Twig\Node\Expression\ConstantExpression; use Twig\Node\Expression\FilterExpression; use Twig\Node\Expression\GetAttrExpression; -use Twig\Node\Expression\NameExpression; use Twig\Node\Expression\Ternary\ConditionalTernary; use Twig\Node\Expression\Test\DefinedTest; +use Twig\Node\Expression\Variable\ContextVariable; use Twig\Node\Node; use Twig\TwigFilter; use Twig\TwigTest; @@ -53,7 +53,7 @@ public function __construct(Node $node, TwigFilter|ConstantExpression $filter, N $default = new FilterExpression($node, new TwigFilter('default', [CoreExtension::class, 'default']), $arguments, $node->getTemplateLine()); } - if ('default' === $name && ($node instanceof NameExpression || $node instanceof GetAttrExpression)) { + if ('default' === $name && ($node instanceof ContextVariable || $node instanceof GetAttrExpression)) { $test = new DefinedTest(clone $node, new TwigTest('defined'), new EmptyNode(), $node->getTemplateLine()); $false = \count($arguments) ? $arguments->getNode('0') : new ConstantExpression('', $node->getTemplateLine()); diff --git a/src/Node/Expression/GetAttrExpression.php b/src/Node/Expression/GetAttrExpression.php index 0d4b96b141d..e072f2a0b54 100644 --- a/src/Node/Expression/GetAttrExpression.php +++ b/src/Node/Expression/GetAttrExpression.php @@ -14,6 +14,7 @@ use Twig\Compiler; use Twig\Extension\SandboxExtension; +use Twig\Node\Expression\Variable\ContextVariable; use Twig\Template; class GetAttrExpression extends AbstractExpression @@ -28,7 +29,7 @@ public function __construct(AbstractExpression $node, AbstractExpression $attrib $nodes['arguments'] = $arguments; } - if ($arguments && !$arguments instanceof ArrayExpression && !$arguments instanceof NameExpression) { + if ($arguments && !$arguments instanceof ArrayExpression && !$arguments instanceof ContextVariable) { trigger_deprecation('twig/twig', '3.15', \sprintf('Not passing a "%s" instance as the "arguments" argument of the "%s" constructor is deprecated ("%s" given).', ArrayExpression::class, static::class, $arguments::class)); } diff --git a/src/Node/Expression/MethodCallExpression.php b/src/Node/Expression/MethodCallExpression.php index 9aede826ca0..922b98b10bc 100644 --- a/src/Node/Expression/MethodCallExpression.php +++ b/src/Node/Expression/MethodCallExpression.php @@ -12,6 +12,7 @@ namespace Twig\Node\Expression; use Twig\Compiler; +use Twig\Node\Expression\Variable\ContextVariable; class MethodCallExpression extends AbstractExpression { @@ -21,7 +22,7 @@ public function __construct(AbstractExpression $node, string $method, ArrayExpre parent::__construct(['node' => $node, 'arguments' => $arguments], ['method' => $method, 'safe' => false, 'is_defined_test' => false], $lineno); - if ($node instanceof NameExpression) { + if ($node instanceof ContextVariable) { $node->setAttribute('always_defined', true); } } diff --git a/src/Node/Expression/NullCoalesceExpression.php b/src/Node/Expression/NullCoalesceExpression.php index c07bb36963a..74ddaf79194 100644 --- a/src/Node/Expression/NullCoalesceExpression.php +++ b/src/Node/Expression/NullCoalesceExpression.php @@ -18,6 +18,7 @@ use Twig\Node\Expression\Test\DefinedTest; use Twig\Node\Expression\Test\NullTest; use Twig\Node\Expression\Unary\NotUnary; +use Twig\Node\Expression\Variable\ContextVariable; use Twig\Node\Node; use Twig\TwigTest; @@ -60,7 +61,7 @@ public function compile(Compiler $compiler): void * cases might be implemented as an optimizer node visitor, but has not been done * as benefits are probably not worth the added complexity. */ - if ($this->getNode('expr2') instanceof NameExpression) { + if ($this->getNode('expr2') instanceof ContextVariable) { $this->getNode('expr2')->setAttribute('always_defined', true); $compiler ->raw('((') diff --git a/src/Node/Expression/Test/DefinedTest.php b/src/Node/Expression/Test/DefinedTest.php index 62aec921750..626cd4f3e8c 100644 --- a/src/Node/Expression/Test/DefinedTest.php +++ b/src/Node/Expression/Test/DefinedTest.php @@ -24,6 +24,7 @@ use Twig\Node\Expression\MethodCallExpression; use Twig\Node\Expression\NameExpression; use Twig\Node\Expression\TestExpression; +use Twig\Node\Expression\Variable\ContextVariable; use Twig\Node\Node; use Twig\TwigTest; @@ -49,7 +50,7 @@ public function __construct(Node $node, TwigTest|string $name, ?Node $arguments, trigger_deprecation('twig/twig', '3.15', 'Not passing a "%s" instance to the "node" argument of "%s" is deprecated ("%s" given).', AbstractExpression::class, static::class, \get_class($node)); } - if ($node instanceof NameExpression) { + if ($node instanceof ContextVariable) { $node->setAttribute('is_defined_test', true); } elseif ($node instanceof GetAttrExpression) { $node->setAttribute('is_defined_test', true); diff --git a/src/Node/Expression/Variable/ContextVariable.php b/src/Node/Expression/Variable/ContextVariable.php index cabc16a43f4..01bbcb71183 100644 --- a/src/Node/Expression/Variable/ContextVariable.php +++ b/src/Node/Expression/Variable/ContextVariable.php @@ -13,6 +13,6 @@ use Twig\Node\Expression\NameExpression; -final class ContextVariable extends NameExpression +class ContextVariable extends NameExpression { } diff --git a/src/Node/ImportNode.php b/src/Node/ImportNode.php index 124c41ba9ca..49e2fd19d23 100644 --- a/src/Node/ImportNode.php +++ b/src/Node/ImportNode.php @@ -16,6 +16,7 @@ use Twig\Node\Expression\AbstractExpression; use Twig\Node\Expression\NameExpression; use Twig\Node\Expression\Variable\AssignTemplateVariable; +use Twig\Node\Expression\Variable\ContextVariable; /** * Represents an import node. @@ -44,7 +45,7 @@ public function compile(Compiler $compiler): void { $compiler->subcompile($this->getNode('var')); - if ($this->getNode('expr') instanceof NameExpression && '_self' === $this->getNode('expr')->getAttribute('name')) { + if ($this->getNode('expr') instanceof ContextVariable && '_self' === $this->getNode('expr')->getAttribute('name')) { $compiler->raw('$this'); } else { $compiler diff --git a/src/NodeVisitor/OptimizerNodeVisitor.php b/src/NodeVisitor/OptimizerNodeVisitor.php index a943f45c309..b82060f6bad 100644 --- a/src/NodeVisitor/OptimizerNodeVisitor.php +++ b/src/NodeVisitor/OptimizerNodeVisitor.php @@ -19,6 +19,7 @@ use Twig\Node\Expression\GetAttrExpression; use Twig\Node\Expression\NameExpression; use Twig\Node\Expression\ParentExpression; +use Twig\Node\Expression\Variable\ContextVariable; use Twig\Node\ForNode; use Twig\Node\IncludeNode; use Twig\Node\Node; @@ -137,13 +138,13 @@ private function enterOptimizeFor(Node $node): void // when do we need to add the loop variable back? // the loop variable is referenced for the current loop - elseif ($node instanceof NameExpression && 'loop' === $node->getAttribute('name')) { + elseif ($node instanceof ContextVariable && 'loop' === $node->getAttribute('name')) { $node->setAttribute('always_defined', true); $this->addLoopToCurrent(); } // optimize access to loop targets - elseif ($node instanceof NameExpression && \in_array($node->getAttribute('name'), $this->loopsTargets)) { + elseif ($node instanceof ContextVariable && \in_array($node->getAttribute('name'), $this->loopsTargets)) { $node->setAttribute('always_defined', true); } @@ -173,7 +174,7 @@ private function enterOptimizeFor(Node $node): void || 'parent' === $node->getNode('attribute')->getAttribute('value') ) && (true === $this->loops[0]->getAttribute('with_loop') - || ($node->getNode('node') instanceof NameExpression + || ($node->getNode('node') instanceof ContextVariable && 'loop' === $node->getNode('node')->getAttribute('name') ) ) diff --git a/src/NodeVisitor/SafeAnalysisNodeVisitor.php b/src/NodeVisitor/SafeAnalysisNodeVisitor.php index 681751e98a2..3030ba80b10 100644 --- a/src/NodeVisitor/SafeAnalysisNodeVisitor.php +++ b/src/NodeVisitor/SafeAnalysisNodeVisitor.php @@ -22,6 +22,7 @@ use Twig\Node\Expression\NameExpression; use Twig\Node\Expression\OperatorEscapeInterface; use Twig\Node\Expression\ParentExpression; +use Twig\Node\Expression\Variable\ContextVariable; use Twig\Node\Node; /** @@ -146,7 +147,7 @@ public function leaveNode(Node $node, Environment $env): ?Node } elseif ($node instanceof MethodCallExpression || $node instanceof MacroReferenceExpression) { // all macro calls are safe $this->setSafe($node, ['all']); - } elseif ($node instanceof GetAttrExpression && $node->getNode('node') instanceof NameExpression) { + } elseif ($node instanceof GetAttrExpression && $node->getNode('node') instanceof ContextVariable) { $name = $node->getNode('node')->getAttribute('name'); if (\in_array($name, $this->safeVars)) { $this->setSafe($node, ['all']); diff --git a/src/NodeVisitor/SandboxNodeVisitor.php b/src/NodeVisitor/SandboxNodeVisitor.php index 74b686f6e94..0cdff62e9d0 100644 --- a/src/NodeVisitor/SandboxNodeVisitor.php +++ b/src/NodeVisitor/SandboxNodeVisitor.php @@ -23,6 +23,7 @@ use Twig\Node\Expression\GetAttrExpression; use Twig\Node\Expression\NameExpression; use Twig\Node\Expression\Unary\SpreadUnary; +use Twig\Node\Expression\Variable\ContextVariable; use Twig\Node\ModuleNode; use Twig\Node\Node; use Twig\Node\Nodes; @@ -122,7 +123,7 @@ public function leaveNode(Node $node, Environment $env): ?Node private function wrapNode(Node $node, string $name): void { $expr = $node->getNode($name); - if (($expr instanceof NameExpression || $expr instanceof GetAttrExpression) && !$expr->isGenerator()) { + 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')) { diff --git a/src/TokenParser/EmbedTokenParser.php b/src/TokenParser/EmbedTokenParser.php index 7bf3233e238..a19a7b31b85 100644 --- a/src/TokenParser/EmbedTokenParser.php +++ b/src/TokenParser/EmbedTokenParser.php @@ -14,6 +14,7 @@ use Twig\Node\EmbedNode; use Twig\Node\Expression\ConstantExpression; use Twig\Node\Expression\NameExpression; +use Twig\Node\Expression\Variable\ContextVariable; use Twig\Node\Node; use Twig\Token; @@ -35,7 +36,7 @@ public function parse(Token $token): Node $parentToken = $fakeParentToken = new Token(Token::STRING_TYPE, '__parent__', $token->getLine()); if ($parent instanceof ConstantExpression) { $parentToken = new Token(Token::STRING_TYPE, $parent->getAttribute('value'), $token->getLine()); - } elseif ($parent instanceof NameExpression) { + } elseif ($parent instanceof ContextVariable) { $parentToken = new Token(Token::NAME_TYPE, $parent->getAttribute('name'), $token->getLine()); } From 9d16de6e3d14f4e168755e32a9332915be826539 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Fri, 24 Jan 2025 14:23:29 +0100 Subject: [PATCH 689/812] Deprecate passing $ifexpr to ForNode --- phpstan-baseline.neon | 6 ------ src/Node/ForNode.php | 4 ++++ 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index eabe060f2d6..131ed97b4ff 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -6,12 +6,6 @@ parameters: count: 1 path: src/Extension/CoreExtension.php - - # Avoid BC-break - message: '#^Constructor of class Twig\\Node\\ForNode has an unused parameter \$ifexpr\.$#' - identifier: constructor.unusedParameter - count: 1 - path: src/Node/ForNode.php - - # 2 parameters will be required message: '#^Method Twig\\Node\\IncludeNode\:\:addGetTemplate\(\) invoked with 2 parameters, 1 required\.$#' identifier: arguments.count diff --git a/src/Node/ForNode.php b/src/Node/ForNode.php index 4346afe5860..1aabd6e7e0b 100644 --- a/src/Node/ForNode.php +++ b/src/Node/ForNode.php @@ -31,6 +31,10 @@ public function __construct(AssignContextVariable $keyTarget, AssignContextVaria { $body = new Nodes([$body, $this->loop = new ForLoopNode($lineno)]); + if (null !== $ifexpr) { + trigger_deprecation('twig/twig', '3.19', \sprintf('Passing not-null to the "ifexpr" argument of the "%s" constructor is deprecated.', static::class)); + } + $nodes = ['key_target' => $keyTarget, 'value_target' => $valueTarget, 'seq' => $seq, 'body' => $body]; if (null !== $else) { $nodes['else'] = $else; From 609767522a90721d1c4a8321a764bc876e034388 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Fri, 24 Jan 2025 15:19:00 +0100 Subject: [PATCH 690/812] Add ForElseNode --- CHANGELOG | 2 +- src/Node/ForElseNode.php | 41 ++++++++++++++++++++++++++++++ src/Node/ForNode.php | 14 +++++----- src/TokenParser/ForTokenParser.php | 3 ++- tests/Node/ForTest.php | 7 +++-- 5 files changed, 56 insertions(+), 11 deletions(-) create mode 100644 src/Node/ForElseNode.php diff --git a/CHANGELOG b/CHANGELOG index 87bb095cbba..96db77d0755 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,9 +1,9 @@ # 3.19.0 (2025-XX-XX) + * Add `ForElseNode` * Deprecate `Twig\ExpressionParser::parseOnlyArguments()` and `Twig\ExpressionParser::parseArguments()` (use `Twig\ExpressionParser::parseNamedArguments()` instead) - * Fix `constant()` behavior when used with `??` * Add the `invoke` filter * Make `{}` optional for the `types` tag diff --git a/src/Node/ForElseNode.php b/src/Node/ForElseNode.php new file mode 100644 index 00000000000..56d6646bf3b --- /dev/null +++ b/src/Node/ForElseNode.php @@ -0,0 +1,41 @@ + + */ +#[YieldReady] +class ForElseNode extends Node +{ + public function __construct(Node $body, int $lineno) + { + parent::__construct(['body' => $body], [], $lineno); + } + + public function compile(Compiler $compiler): void + { + $compiler + ->addDebugInfo($this) + ->write("if (!\$context['_iterated']) {\n") + ->indent() + ->subcompile($this->getNode('body')) + ->outdent() + ->write("}\n") + ; + } +} diff --git a/src/Node/ForNode.php b/src/Node/ForNode.php index 1aabd6e7e0b..2c86622d473 100644 --- a/src/Node/ForNode.php +++ b/src/Node/ForNode.php @@ -35,6 +35,12 @@ public function __construct(AssignContextVariable $keyTarget, AssignContextVaria trigger_deprecation('twig/twig', '3.19', \sprintf('Passing not-null to the "ifexpr" argument of the "%s" constructor is deprecated.', static::class)); } + if (null !== $else && !$else instanceof ForElseNode) { + trigger_deprecation('twig/twig', '3.19', \sprintf('Not passing an instance of "%s" to the "else" argument of the "%s" constructor is deprecated.', ForElseNode::class, static::class)); + + $else = new ForElseNode($else, $else->getTemplateLine()); + } + $nodes = ['key_target' => $keyTarget, 'value_target' => $valueTarget, 'seq' => $seq, 'body' => $body]; if (null !== $else) { $nodes['else'] = $else; @@ -93,13 +99,7 @@ public function compile(Compiler $compiler): void ; if ($this->hasNode('else')) { - $compiler - ->write("if (!\$context['_iterated']) {\n") - ->indent() - ->subcompile($this->getNode('else')) - ->outdent() - ->write("}\n") - ; + $compiler->subcompile($this->getNode('else')); } $compiler->write("\$_parent = \$context['_parent'];\n"); diff --git a/src/TokenParser/ForTokenParser.php b/src/TokenParser/ForTokenParser.php index c0a0e3c2950..632737b8f3b 100644 --- a/src/TokenParser/ForTokenParser.php +++ b/src/TokenParser/ForTokenParser.php @@ -13,6 +13,7 @@ namespace Twig\TokenParser; use Twig\Node\Expression\Variable\AssignContextVariable; +use Twig\Node\ForElseNode; use Twig\Node\ForNode; use Twig\Node\Node; use Twig\Token; @@ -42,7 +43,7 @@ public function parse(Token $token): Node $body = $this->parser->subparse([$this, 'decideForFork']); if ('else' == $stream->next()->getValue()) { $stream->expect(Token::BLOCK_END_TYPE); - $else = $this->parser->subparse([$this, 'decideForEnd'], true); + $else = new ForElseNode($this->parser->subparse([$this, 'decideForEnd'], true), $stream->getCurrent()->getLine()); } else { $else = null; } diff --git a/tests/Node/ForTest.php b/tests/Node/ForTest.php index 814b6086a58..8ae358b6ea2 100644 --- a/tests/Node/ForTest.php +++ b/tests/Node/ForTest.php @@ -13,6 +13,7 @@ use Twig\Node\Expression\Variable\AssignContextVariable; use Twig\Node\Expression\Variable\ContextVariable; +use Twig\Node\ForElseNode; use Twig\Node\ForNode; use Twig\Node\Nodes; use Twig\Node\PrintNode; @@ -36,7 +37,7 @@ public function testConstructor() $this->assertEquals($body, $node->getNode('body')->getNode('0')); $this->assertFalse($node->hasNode('else')); - $else = new PrintNode(new ContextVariable('foo', 1), 1); + $else = new ForElseNode(new PrintNode(new ContextVariable('foo', 1), 1), 5); $node = new ForNode($keyTarget, $valueTarget, $seq, null, $body, $else, 1); $node->setAttribute('with_loop', false); $this->assertEquals($else, $node->getNode('else')); @@ -159,7 +160,7 @@ public static function provideTests(): iterable $valueTarget = new AssignContextVariable('v', 1); $seq = new ContextVariable('values', 1); $body = new Nodes([new PrintNode(new ContextVariable('foo', 1), 1)], 1); - $else = new PrintNode(new ContextVariable('foo', 1), 1); + $else = new ForElseNode(new PrintNode(new ContextVariable('foo', 6), 6), 5); $node = new ForNode($keyTarget, $valueTarget, $seq, null, $body, $else, 1); $node->setAttribute('with_loop', true); @@ -193,7 +194,9 @@ public static function provideTests(): iterable \$context['loop']['last'] = 0 === \$context['loop']['revindex0']; } } +// line 5 if (!\$context['_iterated']) { + // line 6 yield $fooGetter; } \$_parent = \$context['_parent']; From ade8bce51662c63840d0f7bcc3116e1b32fe2f97 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Mon, 20 Jan 2025 10:25:07 +0100 Subject: [PATCH 691/812] Deprecate Token::getType() --- CHANGELOG | 2 ++ doc/deprecated.rst | 3 +++ src/ExpressionParser.php | 30 ++++++++++++------------- src/NodeVisitor/AbstractNodeVisitor.php | 2 +- src/Parser.php | 10 ++++----- src/Token.php | 12 +++++++++- src/TokenStream.php | 4 ++-- tests/LexerTest.php | 2 +- 8 files changed, 40 insertions(+), 25 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 96db77d0755..5eda0f3ec49 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,7 @@ # 3.19.0 (2025-XX-XX) + * Deprecate `Token::getType()`, use `Token::test()` instead + * Add `Token::toEnglish()` * Add `ForElseNode` * Deprecate `Twig\ExpressionParser::parseOnlyArguments()` and `Twig\ExpressionParser::parseArguments()` (use diff --git a/doc/deprecated.rst b/doc/deprecated.rst index a8a04ab850b..b3397d8ddd7 100644 --- a/doc/deprecated.rst +++ b/doc/deprecated.rst @@ -236,6 +236,9 @@ Lexer * Not passing a ``Source`` instance to ``Twig\TokenStream`` constructor is deprecated as of Twig 3.16. +* The ``Token::getType()`` method is deprecated as of Twig 3.19, use + ``Token::test()`` instead. + Templates --------- diff --git a/src/ExpressionParser.php b/src/ExpressionParser.php index 2e409c9e48d..49aa2295ee2 100644 --- a/src/ExpressionParser.php +++ b/src/ExpressionParser.php @@ -297,8 +297,8 @@ private function isBinary(Token $token): bool public function parsePrimaryExpression() { $token = $this->parser->getCurrentToken(); - switch ($token->getType()) { - case Token::NAME_TYPE: + switch (true) { + case $token->test(Token::NAME_TYPE): $this->parser->getStream()->next(); switch ($token->getValue()) { case 'true': @@ -327,25 +327,25 @@ public function parsePrimaryExpression() } break; - case Token::NUMBER_TYPE: + case $token->test(Token::NUMBER_TYPE): $this->parser->getStream()->next(); $node = new ConstantExpression($token->getValue(), $token->getLine()); break; - case Token::STRING_TYPE: - case Token::INTERPOLATION_START_TYPE: + case $token->test(Token::STRING_TYPE): + case $token->test(Token::INTERPOLATION_START_TYPE) : $node = $this->parseStringExpression(); break; - case Token::PUNCTUATION_TYPE: + case $token->test(Token::PUNCTUATION_TYPE): $node = match ($token->getValue()) { '[' => $this->parseSequenceExpression(), '{' => $this->parseMappingExpression(), - default => throw new SyntaxError(\sprintf('Unexpected token "%s" of value "%s".', Token::typeToEnglish($token->getType()), $token->getValue()), $token->getLine(), $this->parser->getStream()->getSourceContext()), + default => throw new SyntaxError(\sprintf('Unexpected token "%s" of value "%s".', $token->toEnglish(), $token->getValue()), $token->getLine(), $this->parser->getStream()->getSourceContext()), }; break; - case Token::OPERATOR_TYPE: + case $token->test(Token::OPERATOR_TYPE): if (preg_match(Lexer::REGEX_NAME, $token->getValue(), $matches) && $matches[0] == $token->getValue()) { // in this context, string operators are variable names $this->parser->getStream()->next(); @@ -359,7 +359,7 @@ public function parsePrimaryExpression() // no break default: - throw new SyntaxError(\sprintf('Unexpected token "%s" of value "%s".', Token::typeToEnglish($token->getType()), $token->getValue()), $token->getLine(), $this->parser->getStream()->getSourceContext()); + throw new SyntaxError(\sprintf('Unexpected token "%s" of value "%s".', $token->toEnglish(), $token->getValue()), $token->getLine(), $this->parser->getStream()->getSourceContext()); } return $this->parsePostfixExpression($node); @@ -491,7 +491,7 @@ public function parseMappingExpression() } else { $current = $stream->getCurrent(); - throw new SyntaxError(\sprintf('A mapping key must be a quoted string, a number, a name, or an expression enclosed in parentheses (unexpected token "%s" of value "%s".', Token::typeToEnglish($current->getType()), $current->getValue()), $current->getLine(), $stream->getSourceContext()); + throw new SyntaxError(\sprintf('A mapping key must be a quoted string, a number, a name, or an expression enclosed in parentheses (unexpected token "%s" of value "%s".', $current->toEnglish(), $current->getValue()), $current->getLine(), $stream->getSourceContext()); } $stream->expect(Token::PUNCTUATION_TYPE, ':', 'A mapping key must be followed by a colon (:)'); @@ -508,7 +508,7 @@ public function parsePostfixExpression($node) { while (true) { $token = $this->parser->getCurrentToken(); - if (Token::PUNCTUATION_TYPE == $token->getType()) { + if ($token->test(Token::PUNCTUATION_TYPE)) { if ('.' == $token->getValue() || '[' == $token->getValue()) { $node = $this->parseSubscriptExpression($node); } elseif ('|' == $token->getValue()) { @@ -944,13 +944,13 @@ private function parseSubscriptExpressionDot(Node $node): AbstractExpression } else { $token = $stream->next(); if ( - Token::NAME_TYPE == $token->getType() - || Token::NUMBER_TYPE == $token->getType() - || (Token::OPERATOR_TYPE == $token->getType() && preg_match(Lexer::REGEX_NAME, $token->getValue())) + $token->test(Token::NAME_TYPE) + || $token->test(Token::NUMBER_TYPE) + || ($token->test(Token::OPERATOR_TYPE) && preg_match(Lexer::REGEX_NAME, $token->getValue())) ) { $attribute = new ConstantExpression($token->getValue(), $token->getLine()); } else { - throw new SyntaxError(\sprintf('Expected name or number, got value "%s" of type %s.', $token->getValue(), Token::typeToEnglish($token->getType())), $token->getLine(), $stream->getSourceContext()); + throw new SyntaxError(\sprintf('Expected name or number, got value "%s" of type %s.', $token->getValue(), $token->toEnglish()), $token->getLine(), $stream->getSourceContext()); } } diff --git a/src/NodeVisitor/AbstractNodeVisitor.php b/src/NodeVisitor/AbstractNodeVisitor.php index 5de35fd096a..38b1ec9d04f 100644 --- a/src/NodeVisitor/AbstractNodeVisitor.php +++ b/src/NodeVisitor/AbstractNodeVisitor.php @@ -19,7 +19,7 @@ * * @author Fabien Potencier * - * @deprecated since 3.9 (to be removed in 4.0) + * @deprecated since Twig 3.9 (to be removed in 4.0) */ abstract class AbstractNodeVisitor implements NodeVisitorInterface { diff --git a/src/Parser.php b/src/Parser.php index 9a8f97e2efe..ff1772c16bb 100644 --- a/src/Parser.php +++ b/src/Parser.php @@ -147,24 +147,24 @@ public function subparse($test, bool $dropNeedle = false): Node $lineno = $this->getCurrentToken()->getLine(); $rv = []; while (!$this->stream->isEOF()) { - switch ($this->getCurrentToken()->getType()) { - case Token::TEXT_TYPE: + switch (true) { + case $this->stream->getCurrent()->test(Token::TEXT_TYPE): $token = $this->stream->next(); $rv[] = new TextNode($token->getValue(), $token->getLine()); break; - case Token::VAR_START_TYPE: + case $this->stream->getCurrent()->test(Token::VAR_START_TYPE): $token = $this->stream->next(); $expr = $this->expressionParser->parseExpression(); $this->stream->expect(Token::VAR_END_TYPE); $rv[] = new PrintNode($expr, $token->getLine()); break; - case Token::BLOCK_START_TYPE: + case $this->stream->getCurrent()->test(Token::BLOCK_START_TYPE): $this->stream->next(); $token = $this->getCurrentToken(); - if (Token::NAME_TYPE !== $token->getType()) { + if (!$token->test(Token::NAME_TYPE)) { throw new SyntaxError('A block must start with a tag name.', $token->getLine(), $this->stream->getSourceContext()); } diff --git a/src/Token.php b/src/Token.php index c3c656f0605..e14c886c3bc 100644 --- a/src/Token.php +++ b/src/Token.php @@ -42,7 +42,7 @@ public function __construct( public function __toString(): string { - return \sprintf('%s(%s)', self::typeToString($this->type, true), $this->value); + return \sprintf('%s(%s)', $this->toEnglish(), $this->value); } /** @@ -75,8 +75,13 @@ public function getLine(): int return $this->lineno; } + /** + * @deprecated since Twig 3.19 + */ public function getType(): int { + trigger_deprecation('twig/twig', '3.19', sprintf('The "%s" method is deprecated.', __METHOD__)); + return $this->type; } @@ -88,6 +93,11 @@ public function getValue() return $this->value; } + public function toEnglish(): string + { + return self::typeToEnglish($this->type); + } + public static function typeToString(int $type, bool $short = false): string { switch ($type) { diff --git a/src/TokenStream.php b/src/TokenStream.php index ec6ac959257..7ee7539f1a3 100644 --- a/src/TokenStream.php +++ b/src/TokenStream.php @@ -79,7 +79,7 @@ public function expect($type, $value = null, ?string $message = null): Token $line = $token->getLine(); throw new SyntaxError(\sprintf('%sUnexpected token "%s"%s ("%s" expected%s).', $message ? $message.'. ' : '', - Token::typeToEnglish($token->getType()), + $token->toEnglish(), $token->getValue() ? \sprintf(' of value "%s"', $token->getValue()) : '', Token::typeToEnglish($type), $value ? \sprintf(' with value "%s"', $value) : ''), $line, @@ -116,7 +116,7 @@ public function test($primary, $secondary = null): bool */ public function isEOF(): bool { - return Token::EOF_TYPE === $this->tokens[$this->current]->getType(); + return $this->tokens[$this->current]->test(Token::EOF_TYPE); } public function getCurrent(): Token diff --git a/tests/LexerTest.php b/tests/LexerTest.php index bfc7f9bb2a7..273e5b88275 100644 --- a/tests/LexerTest.php +++ b/tests/LexerTest.php @@ -72,7 +72,7 @@ protected function countToken($template, $type, $value = null) $count = 0; while (!$stream->isEOF()) { $token = $stream->next(); - if ($type === $token->getType()) { + if ($token->test($type)) { if (null === $value || $value === $token->getValue()) { ++$count; } From 431697fc05d55e219c5047df09dd471d163965fa Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Fri, 24 Jan 2025 20:59:56 +0100 Subject: [PATCH 692/812] add missing parentheses to deprecation message --- src/Token.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Token.php b/src/Token.php index e14c886c3bc..8edfe5aca90 100644 --- a/src/Token.php +++ b/src/Token.php @@ -80,7 +80,7 @@ public function getLine(): int */ public function getType(): int { - trigger_deprecation('twig/twig', '3.19', sprintf('The "%s" method is deprecated.', __METHOD__)); + trigger_deprecation('twig/twig', '3.19', sprintf('The "%s()" method is deprecated.', __METHOD__)); return $this->type; } From 18007078ba96eb8d208dbc59ad9df94eb984e405 Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Fri, 24 Jan 2025 21:10:08 +0100 Subject: [PATCH 693/812] fix intl-extra tests --- .../Tests/Fixtures/format_date.test | 2 ++ .../Tests/Fixtures/format_date_ICU72.test | 28 +++++++++++++++++++ .../Tests/Fixtures/format_date_php8.test | 2 +- .../Fixtures/format_date_php8_ICU72.test | 12 ++++++++ 4 files changed, 43 insertions(+), 1 deletion(-) create mode 100644 extra/intl-extra/Tests/Fixtures/format_date_ICU72.test create mode 100644 extra/intl-extra/Tests/Fixtures/format_date_php8_ICU72.test diff --git a/extra/intl-extra/Tests/Fixtures/format_date.test b/extra/intl-extra/Tests/Fixtures/format_date.test index 75844e1e4bb..08cd873d417 100644 --- a/extra/intl-extra/Tests/Fixtures/format_date.test +++ b/extra/intl-extra/Tests/Fixtures/format_date.test @@ -1,5 +1,7 @@ --TEST-- "format_date" filter +--CONDITION-- +version_compare(Symfony\Component\Intl\Intl::getIcuVersion(), '72.1', '<') --TEMPLATE-- {{ '2019-08-07 23:39:12'|format_datetime() }} {{ '2019-08-07 23:39:12'|format_datetime(locale='fr') }} diff --git a/extra/intl-extra/Tests/Fixtures/format_date_ICU72.test b/extra/intl-extra/Tests/Fixtures/format_date_ICU72.test new file mode 100644 index 00000000000..ea427605df2 --- /dev/null +++ b/extra/intl-extra/Tests/Fixtures/format_date_ICU72.test @@ -0,0 +1,28 @@ +--TEST-- +"format_date" filter +--CONDITION-- +version_compare(Symfony\Component\Intl\Intl::getIcuVersion(), '72.1', '>=') +--TEMPLATE-- +{{ '2019-08-07 23:39:12'|format_datetime() }} +{{ '2019-08-07 23:39:12'|format_datetime(locale='fr') }} +{{ '2019-08-07 23:39:12'|format_datetime('none', 'short', locale='fr') }} +{{ '2019-08-07 23:39:12'|format_datetime('short', 'none', locale='fr') }} +{{ '2019-08-07 23:39:12'|format_datetime('full', 'full', locale='fr') }} +{{ '2019-08-07 23:39:12'|format_datetime(pattern="hh 'oclock' a, zzzz") }} + +{{ '2019-08-07 23:39:12'|format_date }} +{{ '2019-08-07 23:39:12'|format_date(locale='fr') }} +{{ '2019-08-07 23:39:12'|format_time }} +--DATA-- +return []; +--EXPECT-- +Aug 7, 2019, 11:39:12 PM +7 août 2019, 23:39:12 +23:39 +07/08/2019 +mercredi 7 août 2019 à 23:39:12 temps universel coordonné +11 oclock PM, Coordinated Universal Time + +Aug 7, 2019 +7 août 2019 +11:39:12 PM diff --git a/extra/intl-extra/Tests/Fixtures/format_date_php8.test b/extra/intl-extra/Tests/Fixtures/format_date_php8.test index 67e0e6f4dbe..5d694e52ae1 100644 --- a/extra/intl-extra/Tests/Fixtures/format_date_php8.test +++ b/extra/intl-extra/Tests/Fixtures/format_date_php8.test @@ -1,7 +1,7 @@ --TEST-- "format_date" filter --CONDITION-- -PHP_VERSION_ID >= 80000 +PHP_VERSION_ID >= 80000 && version_compare(Symfony\Component\Intl\Intl::getIcuVersion(), '72.1', '<') --TEMPLATE-- {{ 'today 23:39:12'|format_datetime('relative_short', 'none', locale='fr') }} {{ 'today 23:39:12'|format_datetime('relative_full', 'full', locale='fr') }} diff --git a/extra/intl-extra/Tests/Fixtures/format_date_php8_ICU72.test b/extra/intl-extra/Tests/Fixtures/format_date_php8_ICU72.test new file mode 100644 index 00000000000..3162ae54d93 --- /dev/null +++ b/extra/intl-extra/Tests/Fixtures/format_date_php8_ICU72.test @@ -0,0 +1,12 @@ +--TEST-- +"format_date" filter +--CONDITION-- +PHP_VERSION_ID >= 80000 && version_compare(Symfony\Component\Intl\Intl::getIcuVersion(), '72.1', '>=') +--TEMPLATE-- +{{ 'today 23:39:12'|format_datetime('relative_short', 'none', locale='fr') }} +{{ 'today 23:39:12'|format_datetime('relative_full', 'full', locale='fr') }} +--DATA-- +return []; +--EXPECT-- +aujourd’hui +aujourd’hui, 23:39:12 temps universel coordonné From 11535e9058b289d55e5057bf9e701ba64af97ac6 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sat, 25 Jan 2025 16:41:07 +0100 Subject: [PATCH 694/812] Fix Token::__toString() --- src/Token.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Token.php b/src/Token.php index e14c886c3bc..b8f337af9f1 100644 --- a/src/Token.php +++ b/src/Token.php @@ -42,7 +42,7 @@ public function __construct( public function __toString(): string { - return \sprintf('%s(%s)', $this->toEnglish(), $this->value); + return \sprintf('%s(%s)', self::typeToString($this->type, true), $this->value); } /** From f594248a40b4c31cd69561d5a08c456ef9b5b26e Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sun, 26 Jan 2025 11:54:00 +0100 Subject: [PATCH 695/812] Fix CS --- src/Environment.php | 2 +- src/ExpressionParser.php | 1 - src/Node/Expression/Test/DefinedTest.php | 1 - src/Node/ImportNode.php | 1 - src/NodeVisitor/OptimizerNodeVisitor.php | 1 - src/NodeVisitor/SafeAnalysisNodeVisitor.php | 1 - src/NodeVisitor/SandboxNodeVisitor.php | 1 - src/Runtime/EscaperRuntime.php | 2 +- src/Token.php | 2 +- src/TokenParser/EmbedTokenParser.php | 1 - 10 files changed, 3 insertions(+), 10 deletions(-) diff --git a/src/Environment.php b/src/Environment.php index 7808386ffd4..ff8e2b18966 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -693,7 +693,7 @@ public function addExtension(ExtensionInterface $extension) /** * @param ExtensionInterface[] $extensions An array of extensions - * + * * @return void */ public function setExtensions(array $extensions) diff --git a/src/ExpressionParser.php b/src/ExpressionParser.php index 49aa2295ee2..bb01551b7d3 100644 --- a/src/ExpressionParser.php +++ b/src/ExpressionParser.php @@ -23,7 +23,6 @@ use Twig\Node\Expression\ConstantExpression; use Twig\Node\Expression\GetAttrExpression; use Twig\Node\Expression\MacroReferenceExpression; -use Twig\Node\Expression\NameExpression; use Twig\Node\Expression\Ternary\ConditionalTernary; use Twig\Node\Expression\TestExpression; use Twig\Node\Expression\Unary\AbstractUnary; diff --git a/src/Node/Expression/Test/DefinedTest.php b/src/Node/Expression/Test/DefinedTest.php index 626cd4f3e8c..5e32c38bb85 100644 --- a/src/Node/Expression/Test/DefinedTest.php +++ b/src/Node/Expression/Test/DefinedTest.php @@ -22,7 +22,6 @@ use Twig\Node\Expression\GetAttrExpression; use Twig\Node\Expression\MacroReferenceExpression; use Twig\Node\Expression\MethodCallExpression; -use Twig\Node\Expression\NameExpression; use Twig\Node\Expression\TestExpression; use Twig\Node\Expression\Variable\ContextVariable; use Twig\Node\Node; diff --git a/src/Node/ImportNode.php b/src/Node/ImportNode.php index 49e2fd19d23..77a9af93936 100644 --- a/src/Node/ImportNode.php +++ b/src/Node/ImportNode.php @@ -14,7 +14,6 @@ use Twig\Attribute\YieldReady; use Twig\Compiler; use Twig\Node\Expression\AbstractExpression; -use Twig\Node\Expression\NameExpression; use Twig\Node\Expression\Variable\AssignTemplateVariable; use Twig\Node\Expression\Variable\ContextVariable; diff --git a/src/NodeVisitor/OptimizerNodeVisitor.php b/src/NodeVisitor/OptimizerNodeVisitor.php index b82060f6bad..9283737f50d 100644 --- a/src/NodeVisitor/OptimizerNodeVisitor.php +++ b/src/NodeVisitor/OptimizerNodeVisitor.php @@ -17,7 +17,6 @@ use Twig\Node\Expression\ConstantExpression; use Twig\Node\Expression\FunctionExpression; use Twig\Node\Expression\GetAttrExpression; -use Twig\Node\Expression\NameExpression; use Twig\Node\Expression\ParentExpression; use Twig\Node\Expression\Variable\ContextVariable; use Twig\Node\ForNode; diff --git a/src/NodeVisitor/SafeAnalysisNodeVisitor.php b/src/NodeVisitor/SafeAnalysisNodeVisitor.php index 3030ba80b10..a5361fbf7be 100644 --- a/src/NodeVisitor/SafeAnalysisNodeVisitor.php +++ b/src/NodeVisitor/SafeAnalysisNodeVisitor.php @@ -19,7 +19,6 @@ use Twig\Node\Expression\GetAttrExpression; use Twig\Node\Expression\MacroReferenceExpression; use Twig\Node\Expression\MethodCallExpression; -use Twig\Node\Expression\NameExpression; use Twig\Node\Expression\OperatorEscapeInterface; use Twig\Node\Expression\ParentExpression; use Twig\Node\Expression\Variable\ContextVariable; diff --git a/src/NodeVisitor/SandboxNodeVisitor.php b/src/NodeVisitor/SandboxNodeVisitor.php index 0cdff62e9d0..7e89ef83a1a 100644 --- a/src/NodeVisitor/SandboxNodeVisitor.php +++ b/src/NodeVisitor/SandboxNodeVisitor.php @@ -21,7 +21,6 @@ use Twig\Node\Expression\FilterExpression; use Twig\Node\Expression\FunctionExpression; use Twig\Node\Expression\GetAttrExpression; -use Twig\Node\Expression\NameExpression; use Twig\Node\Expression\Unary\SpreadUnary; use Twig\Node\Expression\Variable\ContextVariable; use Twig\Node\ModuleNode; diff --git a/src/Runtime/EscaperRuntime.php b/src/Runtime/EscaperRuntime.php index 198be2ff8fc..719a5696a5e 100644 --- a/src/Runtime/EscaperRuntime.php +++ b/src/Runtime/EscaperRuntime.php @@ -72,7 +72,7 @@ public function setSafeClasses(array $safeClasses = []) * @param class-string<\Stringable> $class * @param string[] $strategies * - * @return void + * @return void */ public function addSafeClass(string $class, array $strategies) { diff --git a/src/Token.php b/src/Token.php index 56f7debc7a5..a4da548cbf2 100644 --- a/src/Token.php +++ b/src/Token.php @@ -80,7 +80,7 @@ public function getLine(): int */ public function getType(): int { - trigger_deprecation('twig/twig', '3.19', sprintf('The "%s()" method is deprecated.', __METHOD__)); + trigger_deprecation('twig/twig', '3.19', \sprintf('The "%s()" method is deprecated.', __METHOD__)); return $this->type; } diff --git a/src/TokenParser/EmbedTokenParser.php b/src/TokenParser/EmbedTokenParser.php index a19a7b31b85..f1acbf1ef00 100644 --- a/src/TokenParser/EmbedTokenParser.php +++ b/src/TokenParser/EmbedTokenParser.php @@ -13,7 +13,6 @@ use Twig\Node\EmbedNode; use Twig\Node\Expression\ConstantExpression; -use Twig\Node\Expression\NameExpression; use Twig\Node\Expression\Variable\ContextVariable; use Twig\Node\Node; use Twig\Token; From 83da53c3f13a70222f9a1bc7bf33acf7eb1a379a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matthieu=20P=C3=A9tel?= Date: Tue, 28 Jan 2025 10:51:09 +0100 Subject: [PATCH 696/812] Fix typo for html_cva code example Fix a typo where 'red' should be 'blue' according to the above code. --- doc/functions/html_cva.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/functions/html_cva.rst b/doc/functions/html_cva.rst index b8c1e27d698..099526b54e3 100644 --- a/doc/functions/html_cva.rst +++ b/doc/functions/html_cva.rst @@ -45,7 +45,7 @@ Then use the ``color`` and ``size`` variants to select the needed classes: {# index.html.twig #} {{ include('alert.html.twig', {'color': 'blue', 'size': 'md'}) }} - // class="alert bg-red text-md" + // class="alert bg-blue text-md" {{ include('alert.html.twig', {'color': 'green', 'size': 'sm'}) }} // class="alert bg-green text-sm" From 445f74cfe05c310e381fe17a756c5afde694d567 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Thu, 16 Jan 2025 18:11:11 +0100 Subject: [PATCH 697/812] =?UTF-8?q?[SECURITY]=C2=A0Fix=20a=20security=20is?= =?UTF-8?q?sue=20where=20escaping=20was=20missing=20when=20using=20=3F=3F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG | 1 + .../Expression/Binary/NullCoalesceBinary.php | 59 +++++++------------ .../expressions/ternary_operator.test | 6 +- .../expressions/ternary_operator_noelse.test | 4 +- .../expressions/ternary_operator_nothen.test | 4 +- tests/Fixtures/tests/null_coalesce.test | 5 +- .../Expression/Binary/NullCoalesceTest.php | 2 +- 7 files changed, 37 insertions(+), 44 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 5eda0f3ec49..10d87e482ae 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,6 @@ # 3.19.0 (2025-XX-XX) + * Fix a security issue where escaping was missing when using `??` * Deprecate `Token::getType()`, use `Token::test()` instead * Add `Token::toEnglish()` * Add `ForElseNode` diff --git a/src/Node/Expression/Binary/NullCoalesceBinary.php b/src/Node/Expression/Binary/NullCoalesceBinary.php index 1472e8e2e7a..b4fb750321c 100644 --- a/src/Node/Expression/Binary/NullCoalesceBinary.php +++ b/src/Node/Expression/Binary/NullCoalesceBinary.php @@ -19,7 +19,6 @@ use Twig\Node\Expression\Test\DefinedTest; use Twig\Node\Expression\Test\NullTest; use Twig\Node\Expression\Unary\NotUnary; -use Twig\Node\Expression\Variable\ContextVariable; use Twig\TwigTest; final class NullCoalesceBinary extends AbstractBinary implements OperatorEscapeInterface @@ -28,47 +27,31 @@ public function __construct(AbstractExpression $left, AbstractExpression $right, { parent::__construct($left, $right, $lineno); - if (!$left instanceof ContextVariable) { - $test = new DefinedTest(clone $left, new TwigTest('defined'), new EmptyNode(), $left->getTemplateLine()); - // for "block()", we don't need the null test as the return value is always a string - if (!$left instanceof BlockReferenceExpression) { - $test = new AndBinary( - $test, - new NotUnary(new NullTest($left, new TwigTest('null'), new EmptyNode(), $left->getTemplateLine()), $left->getTemplateLine()), - $left->getTemplateLine(), - ); - } - - $this->setNode('test', $test); - } else { - $left->setAttribute('always_defined', true); + $test = new DefinedTest(clone $left, new TwigTest('defined'), new EmptyNode(), $left->getTemplateLine()); + // for "block()", we don't need the null test as the return value is always a string + if (!$left instanceof BlockReferenceExpression) { + $test = new AndBinary( + $test, + new NotUnary(new NullTest($left, new TwigTest('null'), new EmptyNode(), $left->getTemplateLine()), $left->getTemplateLine()), + $left->getTemplateLine(), + ); } + + $left->setAttribute('always_defined', true); + $this->setNode('test', $test); } public function compile(Compiler $compiler): void { - /* - * This optimizes only one case. PHP 7 also supports more complex expressions - * that can return null. So, for instance, if log is defined, log("foo") ?? "..." works, - * but log($a["foo"]) ?? "..." does not if $a["foo"] is not defined. More advanced - * cases might be implemented as an optimizer node visitor, but has not been done - * as benefits are probably not worth the added complexity. - */ - if ($this->hasNode('test')) { - $compiler - ->raw('((') - ->subcompile($this->getNode('test')) - ->raw(') ? (') - ->subcompile($this->getNode('left')) - ->raw(') : (') - ->subcompile($this->getNode('right')) - ->raw('))') - ; - - return; - } - - parent::compile($compiler); + $compiler + ->raw('((') + ->subcompile($this->getNode('test')) + ->raw(') ? (') + ->subcompile($this->getNode('left')) + ->raw(') : (') + ->subcompile($this->getNode('right')) + ->raw('))') + ; } public function operator(Compiler $compiler): Compiler @@ -78,6 +61,6 @@ public function operator(Compiler $compiler): Compiler public function getOperandNamesToEscape(): array { - return $this->hasNode('test') ? ['left', 'right'] : ['right']; + return ['left', 'right']; } } diff --git a/tests/Fixtures/expressions/ternary_operator.test b/tests/Fixtures/expressions/ternary_operator.test index 37eccc0f545..3617a8eb0db 100644 --- a/tests/Fixtures/expressions/ternary_operator.test +++ b/tests/Fixtures/expressions/ternary_operator.test @@ -5,10 +5,11 @@ Twig supports the ternary operator {{ 0 ? 'YES' : 'NO' }} {{ 0 ? 'YES' : (1 ? 'YES1' : 'NO1') }} {{ 0 ? 'YES' : (0 ? 'YES1' : 'NO1') }} -{{ 1 == 1 ? 'foo
    ':'' }} +{{ 1 == 1 ? 'foo
    ' : '' }} {{ foo ~ (bar ? ('-' ~ bar) : '') }} +{{ true ? tag : 'KO' }} --DATA-- -return ['foo' => 'foo', 'bar' => 'bar'] +return ['foo' => 'foo', 'bar' => 'bar', 'tag' => '
    '] --EXPECT-- YES NO @@ -16,3 +17,4 @@ YES1 NO1 foo
    foo-bar +<br> diff --git a/tests/Fixtures/expressions/ternary_operator_noelse.test b/tests/Fixtures/expressions/ternary_operator_noelse.test index 8b0f7284b9b..e82f465554d 100644 --- a/tests/Fixtures/expressions/ternary_operator_noelse.test +++ b/tests/Fixtures/expressions/ternary_operator_noelse.test @@ -3,8 +3,10 @@ Twig supports the ternary operator --TEMPLATE-- {{ 1 ? 'YES' }} {{ 0 ? 'YES' }} +{{ tag ? tag }} --DATA-- -return [] +return ['tag' => '
    '] --EXPECT-- YES +<br> diff --git a/tests/Fixtures/expressions/ternary_operator_nothen.test b/tests/Fixtures/expressions/ternary_operator_nothen.test index 53f8c0b3caf..4aebb778677 100644 --- a/tests/Fixtures/expressions/ternary_operator_nothen.test +++ b/tests/Fixtures/expressions/ternary_operator_nothen.test @@ -7,8 +7,9 @@ Twig supports the ternary operator {{ 0 ? : 'NO' }} {{ 'YES' ? : 'NO' }} {{ 0 ? : 'NO' }} +{{ tag ?: 'KO' }} --DATA-- -return [] +return ['tag' => '
    '] --EXPECT-- YES NO @@ -16,3 +17,4 @@ YES NO YES NO +<br> diff --git a/tests/Fixtures/tests/null_coalesce.test b/tests/Fixtures/tests/null_coalesce.test index b73ec4634d8..685415758bb 100644 --- a/tests/Fixtures/tests/null_coalesce.test +++ b/tests/Fixtures/tests/null_coalesce.test @@ -15,8 +15,9 @@ Twig supports the ?? operator {{ 1 + (nope ?? 3) + (nada ?? 2) }} {{ obj.null() ?? 'OK' }} {{ obj.empty() ?? 'KO' }} +{{ tag ?? 'KO' }} --DATA-- -return ['bar' => 'OK', 'foo' => ['bar' => 'OK'], 'obj' => new Twig\Tests\TwigTestFoo()] +return ['bar' => 'OK', 'foo' => ['bar' => 'OK'], 'obj' => new Twig\Tests\TwigTestFoo(), 'tag' => '
    '] --EXPECT-- OK OK @@ -31,3 +32,5 @@ OK 3 6 OK + +<br> diff --git a/tests/Node/Expression/Binary/NullCoalesceTest.php b/tests/Node/Expression/Binary/NullCoalesceTest.php index b2c79320ad6..294dd14613a 100644 --- a/tests/Node/Expression/Binary/NullCoalesceTest.php +++ b/tests/Node/Expression/Binary/NullCoalesceTest.php @@ -24,6 +24,6 @@ public static function provideTests(): iterable $right = new ConstantExpression(2, 1); $node = new NullCoalesceBinary($left, $right, 1); - return [[$node, "(// line 1\n\$context[\"foo\"] ?? 2)"]]; + return [[$node, "(((// line 1\narray_key_exists(\"foo\", \$context) && !(null === \$context[\"foo\"]))) ? (\$context[\"foo\"]) : (2))"]]; } } From d4f8c2b86374f08efc859323dbcd95c590f7124e Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Wed, 29 Jan 2025 08:06:14 +0100 Subject: [PATCH 698/812] Prepare the 3.19.0 release --- CHANGELOG | 2 +- src/Environment.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 10d87e482ae..0dfb14b5ca9 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,4 +1,4 @@ -# 3.19.0 (2025-XX-XX) +# 3.19.0 (2025-01-28) * Fix a security issue where escaping was missing when using `??` * Deprecate `Token::getType()`, use `Token::test()` instead diff --git a/src/Environment.php b/src/Environment.php index ff8e2b18966..6d51f2c2163 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -44,12 +44,12 @@ */ class Environment { - public const VERSION = '3.19.0-DEV'; + public const VERSION = '3.19.0'; public const VERSION_ID = 31900; public const MAJOR_VERSION = 3; public const MINOR_VERSION = 19; public const RELEASE_VERSION = 0; - public const EXTRA_VERSION = 'DEV'; + public const EXTRA_VERSION = ''; private $charset; private $loader; From 69f8d40448ac55ebbe9398cf92f122a5a5ec7a31 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Wed, 29 Jan 2025 08:07:20 +0100 Subject: [PATCH 699/812] Bump version --- CHANGELOG | 4 ++++ src/Environment.php | 8 ++++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 0dfb14b5ca9..c19eca486e6 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +# 3.20.0 (2025-XX-XX) + + * n/a + # 3.19.0 (2025-01-28) * Fix a security issue where escaping was missing when using `??` diff --git a/src/Environment.php b/src/Environment.php index 6d51f2c2163..1e69ea5d2dc 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -44,12 +44,12 @@ */ class Environment { - public const VERSION = '3.19.0'; - public const VERSION_ID = 31900; + public const VERSION = '3.20.0-DEV'; + public const VERSION_ID = 32000; public const MAJOR_VERSION = 3; - public const MINOR_VERSION = 19; + public const MINOR_VERSION = 20; public const RELEASE_VERSION = 0; - public const EXTRA_VERSION = ''; + public const EXTRA_VERSION = 'DEV'; private $charset; private $loader; From 3b605c4180abe8f293127240da41887d51b1f9da Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Thu, 30 Jan 2025 13:38:40 +0100 Subject: [PATCH 700/812] Add some tests on balanced brackets --- tests/LexerTest.php | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/tests/LexerTest.php b/tests/LexerTest.php index 273e5b88275..85c502740be 100644 --- a/tests/LexerTest.php +++ b/tests/LexerTest.php @@ -632,4 +632,25 @@ public static function getTemplateForInlineCommentsForComment() Some regular comment # this is an inline comment #}']; } + + /** + * @dataProvider getTemplateForUnclosedBracketInExpression + */ + public function testUnclosedBracketInExpression(string $template, string $bracket) + { + $lexer = new Lexer(new Environment(new ArrayLoader())); + + $this->expectException(SyntaxError::class); + $this->expectExceptionMessage(\sprintf('Unclosed "%s" in "index" at line 1.', $bracket)); + + $lexer->tokenize(new Source($template, 'index')); + } + + public static function getTemplateForUnclosedBracketInExpression() + { + yield ['{{ (1 + 3 }}', '(']; + yield ['{{ obj["a" }}', '[']; + yield ['{{ ({ a: 1) }}', '{']; + yield ['{{ (([1]) + 3 }}', '(']; + } } From e48abfe47c5784edc5ebcfa9e685d07b2ff60ea0 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Fri, 31 Jan 2025 19:03:58 +0100 Subject: [PATCH 701/812] Add even more tests --- tests/LexerTest.php | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/tests/LexerTest.php b/tests/LexerTest.php index 85c502740be..806b6559178 100644 --- a/tests/LexerTest.php +++ b/tests/LexerTest.php @@ -653,4 +653,25 @@ public static function getTemplateForUnclosedBracketInExpression() yield ['{{ ({ a: 1) }}', '{']; yield ['{{ (([1]) + 3 }}', '(']; } + + /** + * @dataProvider getTemplateForUnexpectedBracketInExpression + */ + public function testUnexpectedBracketInExpression(string $template, string $bracket) + { + $lexer = new Lexer(new Environment(new ArrayLoader())); + + $this->expectException(SyntaxError::class); + $this->expectExceptionMessage(\sprintf('Unexpected "%s" in "index" at line 1.', $bracket)); + + $lexer->tokenize(new Source($template, 'index')); + } + + public static function getTemplateForUnexpectedBracketInExpression() + { + yield ['{{ 1 + 3) }}', ')']; + yield ['{{ obj] }}', ']']; + yield ['{{ { a: 1 }}', '}']; + yield ['{{ ([1] + 3)) }}', ')']; + } } From dc6356268be010d9db468c15694889854b8b02cf Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Fri, 31 Jan 2025 21:33:47 +0100 Subject: [PATCH 702/812] Bump min PHP version to 8.1 --- .github/workflows/ci.yml | 2 -- CHANGELOG | 2 +- composer.json | 5 ++--- doc/intro.rst | 2 +- extra/cache-extra/composer.json | 2 +- extra/cssinliner-extra/composer.json | 2 +- extra/html-extra/composer.json | 2 +- extra/inky-extra/composer.json | 2 +- extra/intl-extra/composer.json | 2 +- extra/markdown-extra/composer.json | 2 +- extra/string-extra/composer.json | 2 +- extra/twig-extra-bundle/composer.json | 2 +- 12 files changed, 12 insertions(+), 15 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 735a09d59e3..188e4bfda08 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,7 +19,6 @@ jobs: strategy: matrix: php-version: - - '8.0' - '8.1' - '8.2' - '8.3' @@ -68,7 +67,6 @@ jobs: strategy: matrix: php-version: - - '8.0' - '8.1' - '8.2' - '8.3' diff --git a/CHANGELOG b/CHANGELOG index c19eca486e6..d2682d6b701 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,6 @@ # 3.20.0 (2025-XX-XX) - * n/a + * Bump minimum PHP version to 8.1 # 3.19.0 (2025-01-28) diff --git a/composer.json b/composer.json index 079f1daf3b7..366236637c3 100644 --- a/composer.json +++ b/composer.json @@ -24,11 +24,10 @@ } ], "require": { - "php": ">=8.0.2", + "php": ">=8.1.0", "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-mbstring": "^1.3", - "symfony/polyfill-ctype": "^1.8", - "symfony/polyfill-php81": "^1.29" + "symfony/polyfill-ctype": "^1.8" }, "require-dev": { "symfony/phpunit-bridge": "^5.4.9|^6.4|^7.0", diff --git a/doc/intro.rst b/doc/intro.rst index 7d01e5beb62..13d13aa0e27 100644 --- a/doc/intro.rst +++ b/doc/intro.rst @@ -30,7 +30,7 @@ Slim, Yii, Laravel, and Codeigniter — just to name a few. Prerequisites ------------- -Twig 3.x needs at least **PHP 8.0.2** to run. +Twig 3.x needs at least **PHP 8.1.0** to run. Installation ------------ diff --git a/extra/cache-extra/composer.json b/extra/cache-extra/composer.json index 8b704f34ebf..4ae0621cd4d 100644 --- a/extra/cache-extra/composer.json +++ b/extra/cache-extra/composer.json @@ -15,7 +15,7 @@ } ], "require": { - "php": ">=8.0.2", + "php": ">=8.1.0", "symfony/cache": "^5.4|^6.4|^7.0", "twig/twig": "^3.19|^4.0" }, diff --git a/extra/cssinliner-extra/composer.json b/extra/cssinliner-extra/composer.json index 229843f50e2..f8cce58e03d 100644 --- a/extra/cssinliner-extra/composer.json +++ b/extra/cssinliner-extra/composer.json @@ -15,7 +15,7 @@ } ], "require": { - "php": ">=8.0.2", + "php": ">=8.1.0", "symfony/deprecation-contracts": "^2.5|^3", "tijsverkoyen/css-to-inline-styles": "^2.0", "twig/twig": "^3.13|^4.0" diff --git a/extra/html-extra/composer.json b/extra/html-extra/composer.json index d902b396742..55555a03d6b 100644 --- a/extra/html-extra/composer.json +++ b/extra/html-extra/composer.json @@ -15,7 +15,7 @@ } ], "require": { - "php": ">=8.0.2", + "php": ">=8.1.0", "symfony/deprecation-contracts": "^2.5|^3", "symfony/mime": "^5.4|^6.4|^7.0", "twig/twig": "^3.13|^4.0" diff --git a/extra/inky-extra/composer.json b/extra/inky-extra/composer.json index cb630b96efa..3d6ed29b892 100644 --- a/extra/inky-extra/composer.json +++ b/extra/inky-extra/composer.json @@ -15,7 +15,7 @@ } ], "require": { - "php": ">=8.0.2", + "php": ">=8.1.0", "symfony/deprecation-contracts": "^2.5|^3", "lorenzo/pinky": "^1.0.5", "twig/twig": "^3.13|^4.0" diff --git a/extra/intl-extra/composer.json b/extra/intl-extra/composer.json index 8355df43ce9..b728753c4c7 100644 --- a/extra/intl-extra/composer.json +++ b/extra/intl-extra/composer.json @@ -15,7 +15,7 @@ } ], "require": { - "php": ">=8.0.2", + "php": ">=8.1.0", "twig/twig": "^3.13|^4.0", "symfony/intl": "^5.4|^6.4|^7.0" }, diff --git a/extra/markdown-extra/composer.json b/extra/markdown-extra/composer.json index 30adf4aa61d..5abaf5040f8 100644 --- a/extra/markdown-extra/composer.json +++ b/extra/markdown-extra/composer.json @@ -15,7 +15,7 @@ } ], "require": { - "php": ">=8.0.2", + "php": ">=8.1.0", "symfony/deprecation-contracts": "^2.5|^3", "twig/twig": "^3.13|^4.0" }, diff --git a/extra/string-extra/composer.json b/extra/string-extra/composer.json index becf9de89c8..6b366e1697d 100644 --- a/extra/string-extra/composer.json +++ b/extra/string-extra/composer.json @@ -15,7 +15,7 @@ } ], "require": { - "php": ">=8.0.2", + "php": ">=8.1.0", "symfony/string": "^5.4|^6.4|^7.0", "symfony/translation-contracts": "^1.1|^2|^3", "twig/twig": "^3.13|^4.0" diff --git a/extra/twig-extra-bundle/composer.json b/extra/twig-extra-bundle/composer.json index ad3bb807b12..88ee8107cf2 100644 --- a/extra/twig-extra-bundle/composer.json +++ b/extra/twig-extra-bundle/composer.json @@ -15,7 +15,7 @@ } ], "require": { - "php": ">=8.0.2", + "php": ">=8.1.0", "symfony/framework-bundle": "^5.4|^6.4|^7.0", "symfony/twig-bundle": "^5.4|^6.4|^7.0", "twig/twig": "^3.2|^4.0" From b0399992bee7498cb4b1124b63a110118a00ea1b Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Fri, 31 Jan 2025 21:30:50 +0100 Subject: [PATCH 703/812] Fix wrong array index --- CHANGELOG | 1 + src/Node/Expression/ArrayExpression.php | 3 --- tests/Fixtures/expressions/array.test | 9 +++++++++ 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index d2682d6b701..93264d3efeb 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,6 @@ # 3.20.0 (2025-XX-XX) + * Fix wrong array index * Bump minimum PHP version to 8.1 # 3.19.0 (2025-01-28) diff --git a/src/Node/Expression/ArrayExpression.php b/src/Node/Expression/ArrayExpression.php index 87d5cb8d749..20c4c7bc555 100644 --- a/src/Node/Expression/ArrayExpression.php +++ b/src/Node/Expression/ArrayExpression.php @@ -112,9 +112,6 @@ public function compile(Compiler $compiler): void } if ($nextIndex !== $key) { - if (\is_int($key)) { - $nextIndex = $key + 1; - } $compiler ->subcompile($pair['key']) ->raw(' => ') diff --git a/tests/Fixtures/expressions/array.test b/tests/Fixtures/expressions/array.test index 7d1c3b0adad..eb7f400d96e 100644 --- a/tests/Fixtures/expressions/array.test +++ b/tests/Fixtures/expressions/array.test @@ -50,6 +50,11 @@ Twig supports array notation {{ does_not_exist[0]|default('ok') }} {{ does_not_exist[0].does_not_exist_either|default('ok') }} {{ does_not_exist[0]['does_not_exist_either']|default('ok') }} + +{# indexes are kept #} +{% set trad = {194:'ABC',141:'DEF',100:'GHI',170:'JKL',110:'MNO',111:'PQR'} %} +{% set trad2 = {'194':'ABC','141':'DEF','100':'GHI','170':'JKL','110':'MNO','111':'PQR'} %} +{{ trad == trad2 ? 'OK' : 'KO' }} --DATA-- $objectStorage = new SplObjectStorage(); $object = new stdClass(); @@ -84,6 +89,8 @@ bar ok ok ok + +OK --DATA-- return ['bar' => 'bar', 'foo' => ['bar' => 'bar'], 'array_access' => new \ArrayObject(['a' => 'b']), 'object' => new stdClass()] --CONFIG-- @@ -117,3 +124,5 @@ bar ok ok ok + +OK From 7f080086dc65f3dcb44fc0bfb4cf3152ac890ea0 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sat, 1 Feb 2025 16:15:31 +0100 Subject: [PATCH 704/812] Fix wrong array index --- src/Node/Expression/ArrayExpression.php | 3 +-- tests/Fixtures/expressions/array.test | 35 +++++++++++++++++++++++-- 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/src/Node/Expression/ArrayExpression.php b/src/Node/Expression/ArrayExpression.php index 20c4c7bc555..61a5063f384 100644 --- a/src/Node/Expression/ArrayExpression.php +++ b/src/Node/Expression/ArrayExpression.php @@ -116,9 +116,8 @@ public function compile(Compiler $compiler): void ->subcompile($pair['key']) ->raw(' => ') ; - } else { - ++$nextIndex; } + ++$nextIndex; $compiler->subcompile($pair['value']); } diff --git a/tests/Fixtures/expressions/array.test b/tests/Fixtures/expressions/array.test index eb7f400d96e..ac1c8ca0e36 100644 --- a/tests/Fixtures/expressions/array.test +++ b/tests/Fixtures/expressions/array.test @@ -55,11 +55,25 @@ Twig supports array notation {% set trad = {194:'ABC',141:'DEF',100:'GHI',170:'JKL',110:'MNO',111:'PQR'} %} {% set trad2 = {'194':'ABC','141':'DEF','100':'GHI','170':'JKL','110':'MNO','111':'PQR'} %} {{ trad == trad2 ? 'OK' : 'KO' }} + +{# indexes are kept #} +{{ { 1: "first", 0: "second" } == { '1': "first", '0': "second" } ? 'OK' : 'KO' }} +{{ { 1: "first", 0: "second" } == indices_1 ? 'OK' : 'KO' }} +{{ { 1: "first", 'foo': "second", 2: "third" } == { '1': "first", 'foo': "second", '2': "third" } ? 'OK' : 'KO' }} +{{ { 1: "first", 'foo': "second", 2: "third" } == indices_2 ? 'OK' : 'KO' }} --DATA-- $objectStorage = new SplObjectStorage(); $object = new stdClass(); $objectStorage[$object] = 'foo'; -return ['bar' => 'bar', 'foo' => ['bar' => 'bar'], 'array_access' => new \ArrayObject(['a' => 'b']), 'object_storage' => $objectStorage, 'object' => $object] +return [ + 'bar' => 'bar', + 'foo' => ['bar' => 'bar'], + 'array_access' => new \ArrayObject(['a' => 'b']), + 'object_storage' => $objectStorage, + 'object' => $object, + 'indices_1' => [ 1 => 'first', 0 => 'second' ], + 'indices_2' => [ 1 => 'first', 'foo' => 'second', 2 => 'third' ], +] --EXPECT-- 1,2 foo,bar @@ -90,9 +104,21 @@ ok ok ok +OK + +OK +OK +OK OK --DATA-- -return ['bar' => 'bar', 'foo' => ['bar' => 'bar'], 'array_access' => new \ArrayObject(['a' => 'b']), 'object' => new stdClass()] +return [ + 'bar' => 'bar', + 'foo' => ['bar' => 'bar'], + 'array_access' => new \ArrayObject(['a' => 'b']), + 'object' => new stdClass(), + 'indices_1' => [ 1 => 'first', 0 => 'second' ], + 'indices_2' => [ 1 => 'first', 'foo' => 'second', 2 => 'third' ], +] --CONFIG-- return ['strict_variables' => false] --EXPECT-- @@ -126,3 +152,8 @@ ok ok OK + +OK +OK +OK +OK From af5f4c34fbe8d2cf27f519bb574aa330db1836a1 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Mon, 3 Feb 2025 08:48:19 +0100 Subject: [PATCH 705/812] Fix CS --- src/ExpressionParser.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ExpressionParser.php b/src/ExpressionParser.php index bb01551b7d3..d82e9c0e79a 100644 --- a/src/ExpressionParser.php +++ b/src/ExpressionParser.php @@ -332,7 +332,7 @@ public function parsePrimaryExpression() break; case $token->test(Token::STRING_TYPE): - case $token->test(Token::INTERPOLATION_START_TYPE) : + case $token->test(Token::INTERPOLATION_START_TYPE): $node = $this->parseStringExpression(); break; From 3288d8d98d58e5dc09e225db500cc149abe173be Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Mon, 3 Feb 2025 12:37:23 +0100 Subject: [PATCH 706/812] Reimplement the cache tag in an adhoc way --- .../TokenParser/CacheTokenParser.php | 34 ++++++++++--------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/extra/cache-extra/TokenParser/CacheTokenParser.php b/extra/cache-extra/TokenParser/CacheTokenParser.php index b5fa3b87ac0..dcc2ddd288f 100644 --- a/extra/cache-extra/TokenParser/CacheTokenParser.php +++ b/extra/cache-extra/TokenParser/CacheTokenParser.php @@ -31,24 +31,26 @@ public function parse(Token $token): Node $tags = null; while ($stream->test(Token::NAME_TYPE)) { $k = $stream->getCurrent()->getValue(); + if (!in_array($k, ['ttl', 'tags'])) { + throw new SyntaxError(\sprintf('Unknown "%s" configuration.', $k), $stream->getCurrent()->getLine(), $stream->getSourceContext()); + } + $stream->next(); - $args = $expressionParser->parseNamedArguments(); + $stream->expect(Token::PUNCTUATION_TYPE, '('); + $line = $stream->getCurrent()->getLine(); + if ($stream->test(Token::PUNCTUATION_TYPE, ')')) { + throw new SyntaxError(\sprintf('The "%s" modifier takes exactly one argument (0 given).', $k), $line, $stream->getSourceContext()); + } + $arg = $expressionParser->parseExpression(); + if ($stream->test(Token::PUNCTUATION_TYPE, ',')) { + throw new SyntaxError(\sprintf('The "%s" modifier takes exactly one argument (2 given).', $k), $line, $stream->getSourceContext()); + } + $stream->expect(Token::PUNCTUATION_TYPE, ')'); - switch ($k) { - case 'ttl': - if (1 !== \count($args)) { - throw new SyntaxError(\sprintf('The "ttl" modifier takes exactly one argument (%d given).', \count($args)), $stream->getCurrent()->getLine(), $stream->getSourceContext()); - } - $ttl = $args->getNode('0'); - break; - case 'tags': - if (1 !== \count($args)) { - throw new SyntaxError(\sprintf('The "tags" modifier takes exactly one argument (%d given).', \count($args)), $stream->getCurrent()->getLine(), $stream->getSourceContext()); - } - $tags = $args->getNode('0'); - break; - default: - throw new SyntaxError(\sprintf('Unknown "%s" configuration.', $k), $stream->getCurrent()->getLine(), $stream->getSourceContext()); + if ('ttl' === $k) { + $ttl = $arg; + } elseif ('tags' === $k) { + $tags = $arg; } } From fa1ffb5b299bfa037ca4667de47b8670e4d021b5 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Mon, 3 Feb 2025 18:28:47 +0100 Subject: [PATCH 707/812] Add BinaryInterface --- src/Node/Expression/Binary/AbstractBinary.php | 2 +- .../Expression/Binary/BinaryInterface.php | 22 +++++++++++++++++++ src/Node/Expression/Binary/ElvisBinary.php | 7 +++++- .../Expression/Binary/NullCoalesceBinary.php | 7 +++++- 4 files changed, 35 insertions(+), 3 deletions(-) create mode 100644 src/Node/Expression/Binary/BinaryInterface.php diff --git a/src/Node/Expression/Binary/AbstractBinary.php b/src/Node/Expression/Binary/AbstractBinary.php index 09530273ab1..bd6cc6c0251 100644 --- a/src/Node/Expression/Binary/AbstractBinary.php +++ b/src/Node/Expression/Binary/AbstractBinary.php @@ -16,7 +16,7 @@ use Twig\Node\Expression\AbstractExpression; use Twig\Node\Node; -abstract class AbstractBinary extends AbstractExpression +abstract class AbstractBinary extends AbstractExpression implements BinaryInterface { /** * @param AbstractExpression $left diff --git a/src/Node/Expression/Binary/BinaryInterface.php b/src/Node/Expression/Binary/BinaryInterface.php new file mode 100644 index 00000000000..eeeb2eb99ec --- /dev/null +++ b/src/Node/Expression/Binary/BinaryInterface.php @@ -0,0 +1,22 @@ + Date: Mon, 3 Feb 2025 21:23:35 +0100 Subject: [PATCH 708/812] Add UnaryInterface --- src/Node/Expression/Unary/AbstractUnary.php | 2 +- src/Node/Expression/Unary/UnaryInterface.php | 22 ++++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 src/Node/Expression/Unary/UnaryInterface.php diff --git a/src/Node/Expression/Unary/AbstractUnary.php b/src/Node/Expression/Unary/AbstractUnary.php index d4746e73b7c..b00027d1ab8 100644 --- a/src/Node/Expression/Unary/AbstractUnary.php +++ b/src/Node/Expression/Unary/AbstractUnary.php @@ -16,7 +16,7 @@ use Twig\Node\Expression\AbstractExpression; use Twig\Node\Node; -abstract class AbstractUnary extends AbstractExpression +abstract class AbstractUnary extends AbstractExpression implements UnaryInterface { /** * @param AbstractExpression $node diff --git a/src/Node/Expression/Unary/UnaryInterface.php b/src/Node/Expression/Unary/UnaryInterface.php new file mode 100644 index 00000000000..b094ef4f4ce --- /dev/null +++ b/src/Node/Expression/Unary/UnaryInterface.php @@ -0,0 +1,22 @@ + Date: Tue, 4 Feb 2025 10:28:21 +0100 Subject: [PATCH 709/812] Add support for registered undefined element callbacks in tests --- CHANGELOG | 3 ++- src/Environment.php | 9 ++++++++ src/ExtensionSet.php | 12 +++++++++++ src/Test/IntegrationTestCase.php | 37 ++++++++++++++++++++++++++++++++ 4 files changed, 60 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 93264d3efeb..a8cb1f81d66 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -2,6 +2,7 @@ * Fix wrong array index * Bump minimum PHP version to 8.1 + * Add support for registering callbacks for undefined functions, filters or token parsers in the IntegrationTestCase # 3.19.0 (2025-01-28) @@ -44,7 +45,7 @@ * Deprecate returning `null` from `TwigFilter::getSafe()` and `TwigFunction::getSafe()`, return `[]` instead # 3.15.0 (2024-11-17) - + * [BC BREAK] Add support for accessing class constants with the dot operator; this can be a BC break if you don't use UPPERCASE constant names * Add Spanish inflector support for the `plural` and `singular` filters in the String extension diff --git a/src/Environment.php b/src/Environment.php index 1e69ea5d2dc..fddab050a8a 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -736,6 +736,9 @@ public function getTokenParser(string $name): ?TokenParserInterface return $this->extensionSet->getTokenParser($name); } + /** + * @param callable(string): (TokenParserInterface|false) $callable + */ public function registerUndefinedTokenParserCallback(callable $callable): void { $this->extensionSet->registerUndefinedTokenParserCallback($callable); @@ -775,6 +778,9 @@ public function getFilter(string $name): ?TwigFilter return $this->extensionSet->getFilter($name); } + /** + * @param callable(string): (TwigFilter|false) $callable + */ public function registerUndefinedFilterCallback(callable $callable): void { $this->extensionSet->registerUndefinedFilterCallback($callable); @@ -838,6 +844,9 @@ public function getFunction(string $name): ?TwigFunction return $this->extensionSet->getFunction($name); } + /** + * @param callable(string): (TwigFunction|false) $callable + */ public function registerUndefinedFunctionCallback(callable $callable): void { $this->extensionSet->registerUndefinedFunctionCallback($callable); diff --git a/src/ExtensionSet.php b/src/ExtensionSet.php index 2b17182b2a3..b069232b44f 100644 --- a/src/ExtensionSet.php +++ b/src/ExtensionSet.php @@ -52,8 +52,11 @@ final class ExtensionSet private $binaryOperators; /** @var array|null */ private $globals; + /** @var array */ private $functionCallbacks = []; + /** @var array */ private $filterCallbacks = []; + /** @var array */ private $parserCallbacks = []; private $lastModified = 0; @@ -198,6 +201,9 @@ public function getFunction(string $name): ?TwigFunction return null; } + /** + * @param callable(string): (TwigFunction|false) $callable + */ public function registerUndefinedFunctionCallback(callable $callable): void { $this->functionCallbacks[] = $callable; @@ -251,6 +257,9 @@ public function getFilter(string $name): ?TwigFilter return null; } + /** + * @param callable(string): (TwigFilter|false) $callable + */ public function registerUndefinedFilterCallback(callable $callable): void { $this->filterCallbacks[] = $callable; @@ -317,6 +326,9 @@ public function getTokenParser(string $name): ?TokenParserInterface return null; } + /** + * @param callable(string): (TokenParserInterface|false) $callable + */ public function registerUndefinedTokenParserCallback(callable $callable): void { $this->parserCallbacks[] = $callable; diff --git a/src/Test/IntegrationTestCase.php b/src/Test/IntegrationTestCase.php index 1fb3c313b35..f4a5dc7e534 100644 --- a/src/Test/IntegrationTestCase.php +++ b/src/Test/IntegrationTestCase.php @@ -17,6 +17,7 @@ use Twig\Extension\ExtensionInterface; use Twig\Loader\ArrayLoader; use Twig\RuntimeLoader\RuntimeLoaderInterface; +use Twig\TokenParser\TokenParserInterface; use Twig\TwigFilter; use Twig\TwigFunction; use Twig\TwigTest; @@ -84,6 +85,30 @@ protected function getTwigTests() return []; } + /** + * @return array + */ + protected function getUndefinedFilterCallbacks(): array + { + return []; + } + + /** + * @return array + */ + protected function getUndefinedFunctionCallbacks(): array + { + return []; + } + + /** + * @return array + */ + protected function getUndefinedTokenParserCallbacks(): array + { + return []; + } + /** * @dataProvider getTests * @@ -222,6 +247,18 @@ protected function doIntegrationTest($file, $message, $condition, $templates, $e $twig->addFunction($function); } + foreach ($this->getUndefinedFilterCallbacks() as $callback) { + $twig->registerUndefinedFilterCallback($callback); + } + + foreach ($this->getUndefinedFunctionCallbacks() as $callback) { + $twig->registerUndefinedFunctionCallback($callback); + } + + foreach ($this->getUndefinedTokenParserCallbacks() as $callback) { + $twig->registerUndefinedTokenParserCallback($callback); + } + $deprecations = []; try { $prevHandler = set_error_handler(function ($type, $msg, $file, $line, $context = []) use (&$deprecations, &$prevHandler) { From a84e602b4f411bca0e6058e4fb4afbb1edcbe7a0 Mon Sep 17 00:00:00 2001 From: Ruud Kamphuis Date: Fri, 7 Feb 2025 14:48:08 +0100 Subject: [PATCH 710/812] Use correct lineno for ForElseNode It should take the line number of the `{% else %}` block. --- src/TokenParser/ForTokenParser.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/TokenParser/ForTokenParser.php b/src/TokenParser/ForTokenParser.php index 632737b8f3b..3e08b22fa8a 100644 --- a/src/TokenParser/ForTokenParser.php +++ b/src/TokenParser/ForTokenParser.php @@ -42,8 +42,9 @@ public function parse(Token $token): Node $stream->expect(Token::BLOCK_END_TYPE); $body = $this->parser->subparse([$this, 'decideForFork']); if ('else' == $stream->next()->getValue()) { + $elseLineno = $stream->getCurrent()->getLine(); $stream->expect(Token::BLOCK_END_TYPE); - $else = new ForElseNode($this->parser->subparse([$this, 'decideForEnd'], true), $stream->getCurrent()->getLine()); + $else = new ForElseNode($this->parser->subparse([$this, 'decideForEnd'], true), $elseLineno); } else { $else = null; } From f69531fa009c38ba291ea005287881831d979378 Mon Sep 17 00:00:00 2001 From: PrinsFrank <25006490+PrinsFrank@users.noreply.github.com> Date: Mon, 20 Jan 2025 15:49:36 +0100 Subject: [PATCH 711/812] Fix timezone conversion on strings --- src/Extension/CoreExtension.php | 2 +- .../filters/date_time_zone_conversion.test | 91 +++++++++++++++++++ 2 files changed, 92 insertions(+), 1 deletion(-) create mode 100644 tests/Fixtures/filters/date_time_zone_conversion.test diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index b83a0fed1db..ee5f3a0da67 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -576,7 +576,7 @@ public function convertDate($date = null, $timezone = null) if (ctype_digit($asString) || ('' !== $asString && '-' === $asString[0] && ctype_digit(substr($asString, 1)))) { $date = new \DateTime('@'.$date); } else { - $date = new \DateTime($date, $this->getTimezone()); + $date = new \DateTime($date); } if (false !== $timezone) { diff --git a/tests/Fixtures/filters/date_time_zone_conversion.test b/tests/Fixtures/filters/date_time_zone_conversion.test new file mode 100644 index 00000000000..3ba042b709b --- /dev/null +++ b/tests/Fixtures/filters/date_time_zone_conversion.test @@ -0,0 +1,91 @@ +--TEST-- +"date" filter with time zone conversion +--TEMPLATE-- +{{ date1|date }} +{{ date1|date('d/m/Y') }} +{{ date1|date('d/m/Y H:i:s', 'Asia/Hong_Kong') }} +{{ date1|date('d/m/Y H:i:s P', 'Asia/Hong_Kong') }} +{{ date1|date('d/m/Y H:i:s P', 'America/Chicago') }} +{{ date1|date('e') }} +{{ date1|date('d/m/Y H:i:s') }} + +{{ date2|date }} +{{ date2|date('d/m/Y') }} +{{ date2|date('d/m/Y H:i:s', 'Asia/Hong_Kong') }} +{{ date2|date('d/m/Y H:i:s', timezone1) }} +{{ date2|date('d/m/Y H:i:s') }} + +{{ date3|date }} +{{ date3|date('d/m/Y') }} + +{{ date4|date }} +{{ date4|date('d/m/Y') }} + +{{ date5|date }} +{{ date5|date('d/m/Y') }} + +{{ date6|date('d/m/Y H:i:s P', 'Europe/Paris') }} +{{ date6|date('d/m/Y H:i:s P', 'Asia/Hong_Kong') }} +{{ date6|date('d/m/Y H:i:s P', false) }} +{{ date6|date('e', 'Europe/Paris') }} +{{ date6|date('e', false) }} + +{{ date7|date }} +{{ date7|date(timezone='Europe/Paris') }} +{{ date7|date(timezone='Asia/Hong_Kong') }} +{{ date7|date(timezone=false) }} +{{ date7|date(timezone='Indian/Mauritius') }} + +{{ '2010-01-28 15:00:00'|date(timezone="Europe/Paris") }} +{{ '2010-01-28 15:00:00'|date(timezone="Asia/Hong_Kong") }} +--DATA-- +date_default_timezone_set('Europe/Paris'); +$twig->getExtension(\Twig\Extension\CoreExtension::class)->setTimezone('UTC'); +return [ + 'date1' => mktime(13, 45, 0, 10, 4, 2010), + 'date2' => new \DateTime('2010-10-04 13:45'), + 'date3' => '2010-10-04 13:45', + 'date4' => 1286199900, // \DateTime::createFromFormat('Y-m-d H:i', '2010-10-04 13:45', new \DateTimeZone('UTC'))->getTimestamp() -- A unixtimestamp is always GMT + 'date5' => -189291360, // \DateTime::createFromFormat('Y-m-d H:i', '1964-01-02 03:04', new \DateTimeZone('UTC'))->getTimestamp(), + 'date6' => new \DateTime('2010-10-04 13:45', new \DateTimeZone('America/New_York')), + 'date7' => '2010-01-28T15:00:00+04:00', + 'timezone1' => new \DateTimeZone('America/New_York'), +] +--EXPECT-- +October 4, 2010 11:45 +04/10/2010 +04/10/2010 19:45:00 +04/10/2010 19:45:00 +08:00 +04/10/2010 06:45:00 -05:00 +UTC +04/10/2010 11:45:00 + +October 4, 2010 11:45 +04/10/2010 +04/10/2010 19:45:00 +04/10/2010 07:45:00 +04/10/2010 11:45:00 + +October 4, 2010 11:45 +04/10/2010 + +October 4, 2010 13:45 +04/10/2010 + +January 2, 1964 03:04 +02/01/1964 + +04/10/2010 19:45:00 +02:00 +05/10/2010 01:45:00 +08:00 +04/10/2010 13:45:00 -04:00 +Europe/Paris +America/New_York + +January 28, 2010 11:00 +January 28, 2010 12:00 +January 28, 2010 19:00 +January 28, 2010 15:00 +January 28, 2010 15:00 + +January 28, 2010 15:00 +January 28, 2010 22:00 From 40a2d5b57f3434f411c34e13e69da9c8c2dee4b8 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sat, 8 Feb 2025 10:47:15 +0100 Subject: [PATCH 712/812] Add missing CHANGELOG --- CHANGELOG | 1 + extra/twig-extra-bundle/LeagueCommonMarkConverterFactory.php | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index a8cb1f81d66..fcc43c45356 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,6 @@ # 3.20.0 (2025-XX-XX) + * Add configuration for Commonmark * Fix wrong array index * Bump minimum PHP version to 8.1 * Add support for registering callbacks for undefined functions, filters or token parsers in the IntegrationTestCase diff --git a/extra/twig-extra-bundle/LeagueCommonMarkConverterFactory.php b/extra/twig-extra-bundle/LeagueCommonMarkConverterFactory.php index 834ac9983a2..a484d8b6b09 100644 --- a/extra/twig-extra-bundle/LeagueCommonMarkConverterFactory.php +++ b/extra/twig-extra-bundle/LeagueCommonMarkConverterFactory.php @@ -20,7 +20,6 @@ final class LeagueCommonMarkConverterFactory { private $extensions; - private $config; /** From 3794efe66210873ea4598aa3a276b8f5a795fec3 Mon Sep 17 00:00:00 2001 From: Christophe Coevoet Date: Sun, 9 Feb 2025 14:48:10 +0100 Subject: [PATCH 713/812] Fix support for ignoring syntax erros in an undefined handler in guard --- src/ExpressionParser.php | 23 ++++++++++++++-- .../Fixtures/tags/guard/throwing_handler.test | 22 +++++++++++++++ tests/IntegrationTest.php | 27 +++++++++++++++++++ 3 files changed, 70 insertions(+), 2 deletions(-) create mode 100644 tests/Fixtures/tags/guard/throwing_handler.test diff --git a/src/ExpressionParser.php b/src/ExpressionParser.php index d82e9c0e79a..233139ee4ab 100644 --- a/src/ExpressionParser.php +++ b/src/ExpressionParser.php @@ -799,7 +799,17 @@ private function getTest(int $line): TwigTest private function getFunction(string $name, int $line): TwigFunction { - if (!$function = $this->env->getFunction($name)) { + try { + $function = $this->env->getFunction($name); + } catch (SyntaxError $e) { + if (!$this->parser->shouldIgnoreUnknownTwigCallables()) { + throw $e; + } + + $function = null; + } + + if (!$function) { if ($this->parser->shouldIgnoreUnknownTwigCallables()) { return new TwigFunction($name, fn () => ''); } @@ -819,7 +829,16 @@ private function getFunction(string $name, int $line): TwigFunction private function getFilter(string $name, int $line): TwigFilter { - if (!$filter = $this->env->getFilter($name)) { + try { + $filter = $this->env->getFilter($name); + } catch (SyntaxError $e) { + if (!$this->parser->shouldIgnoreUnknownTwigCallables()) { + throw $e; + } + + $filter = null; + } + if (!$filter) { if ($this->parser->shouldIgnoreUnknownTwigCallables()) { return new TwigFilter($name, fn () => ''); } diff --git a/tests/Fixtures/tags/guard/throwing_handler.test b/tests/Fixtures/tags/guard/throwing_handler.test new file mode 100644 index 00000000000..37e32ef6c00 --- /dev/null +++ b/tests/Fixtures/tags/guard/throwing_handler.test @@ -0,0 +1,22 @@ +--TEST-- +"guard" creates a compilation time condition on Twig callables availability +--TEMPLATE-- +{% guard filter throwing_undefined_filter %} + NEVER + {{ 'a'|throwing_undefined_filter }} +{% else -%} + The throwing_undefined_filter filter doesn't exist +{% endguard %} + +{% guard function throwing_undefined_function -%} + NEVER + {{ throwing_undefined_function() }} +{% else -%} + The throwing_undefined_function function doesn't exist +{% endguard %} +--DATA-- +return [] +--EXPECT-- +The throwing_undefined_filter filter doesn't exist + +The throwing_undefined_function function doesn't exist diff --git a/tests/IntegrationTest.php b/tests/IntegrationTest.php index f4889cd754b..273324d1061 100644 --- a/tests/IntegrationTest.php +++ b/tests/IntegrationTest.php @@ -12,6 +12,7 @@ */ use Twig\DeprecatedCallableInfo; +use Twig\Error\SyntaxError; use Twig\Extension\AbstractExtension; use Twig\Extension\DebugExtension; use Twig\Extension\SandboxExtension; @@ -49,6 +50,32 @@ public function getExtensions() ]; } + protected function getUndefinedFunctionCallbacks(): array + { + return [ + static function (string $name) { + if ('throwing_undefined_function' === $name) { + throw new SyntaxError('This function is undefined in the tests.'); + } + + return false; + }, + ]; + } + + protected function getUndefinedTokenParserCallbacks(): array + { + return [ + static function (string $name) { + if ('throwing_undefined_filter' === $name) { + throw new SyntaxError('This filter is undefined in the tests.'); + } + + return false; + }, + ]; + } + protected static function getFixturesDirectory(): string { return __DIR__.'/Fixtures/'; From d1737bd894e458c1da808c4eb014b442a1ed1d06 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sun, 9 Feb 2025 18:11:45 +0100 Subject: [PATCH 714/812] Fix CHANGELOG --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index fcc43c45356..3e9954190be 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,6 @@ # 3.20.0 (2025-XX-XX) + * Fix support for ignoring syntax erros in an undefined handler in guard * Add configuration for Commonmark * Fix wrong array index * Bump minimum PHP version to 8.1 From f1c9039106ecc5a8182e1536027a6940ac4a8ab8 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sun, 9 Feb 2025 18:38:06 +0100 Subject: [PATCH 715/812] Fix typo --- CHANGELOG | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 3e9954190be..8ecff2fba21 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,6 @@ # 3.20.0 (2025-XX-XX) - * Fix support for ignoring syntax erros in an undefined handler in guard + * Fix support for ignoring syntax errors in an undefined handler in guard * Add configuration for Commonmark * Fix wrong array index * Bump minimum PHP version to 8.1 From e456a3189bee98924a723e3bb87d7c22d7365c96 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Thu, 13 Feb 2025 09:34:02 +0100 Subject: [PATCH 716/812] Update CHANGELOG --- CHANGELOG | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index 8ecff2fba21..57640dd04ee 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -5,6 +5,8 @@ * Fix wrong array index * Bump minimum PHP version to 8.1 * Add support for registering callbacks for undefined functions, filters or token parsers in the IntegrationTestCase + * Use correct line number for `ForElseNode` + * Fix timezone conversion on strings # 3.19.0 (2025-01-28) From 3468920399451a384bef53cf7996965f7cd40183 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Thu, 13 Feb 2025 09:34:43 +0100 Subject: [PATCH 717/812] Prepare the 3.20.0 release --- CHANGELOG | 2 +- src/Environment.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 57640dd04ee..5d7ea9527c2 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,4 +1,4 @@ -# 3.20.0 (2025-XX-XX) +# 3.20.0 (2025-02-13) * Fix support for ignoring syntax errors in an undefined handler in guard * Add configuration for Commonmark diff --git a/src/Environment.php b/src/Environment.php index fddab050a8a..17765cd2bde 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -44,12 +44,12 @@ */ class Environment { - public const VERSION = '3.20.0-DEV'; + public const VERSION = '3.20.0'; public const VERSION_ID = 32000; public const MAJOR_VERSION = 3; public const MINOR_VERSION = 20; public const RELEASE_VERSION = 0; - public const EXTRA_VERSION = 'DEV'; + public const EXTRA_VERSION = ''; private $charset; private $loader; From 586e3decdd2544ca362e703307f4caa4f08b2cfc Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Thu, 13 Feb 2025 09:35:50 +0100 Subject: [PATCH 718/812] Bump version --- CHANGELOG | 4 ++++ src/Environment.php | 8 ++++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 5d7ea9527c2..37c45e7a40b 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +# 3.20.1 (2025-XX-XX) + + * n/a + # 3.20.0 (2025-02-13) * Fix support for ignoring syntax errors in an undefined handler in guard diff --git a/src/Environment.php b/src/Environment.php index 17765cd2bde..e1e88c656ed 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -44,12 +44,12 @@ */ class Environment { - public const VERSION = '3.20.0'; - public const VERSION_ID = 32000; + public const VERSION = '3.20.1-DEV'; + public const VERSION_ID = 32001; public const MAJOR_VERSION = 3; public const MINOR_VERSION = 20; - public const RELEASE_VERSION = 0; - public const EXTRA_VERSION = ''; + public const RELEASE_VERSION = 1; + public const EXTRA_VERSION = 'DEV'; private $charset; private $loader; From 229f54f8cf6d334236a7e18de3900f0e858e083a Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Fri, 14 Feb 2025 08:36:54 +0100 Subject: [PATCH 719/812] Bump version to 3.21 --- CHANGELOG | 2 +- src/Environment.php | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 37c45e7a40b..30730ffeb84 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,4 +1,4 @@ -# 3.20.1 (2025-XX-XX) +# 3.21.0 (2025-XX-XX) * n/a diff --git a/src/Environment.php b/src/Environment.php index e1e88c656ed..6e00b3d3b24 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -44,11 +44,11 @@ */ class Environment { - public const VERSION = '3.20.1-DEV'; - public const VERSION_ID = 32001; + public const VERSION = '3.21.0-DEV'; + public const VERSION_ID = 32100; public const MAJOR_VERSION = 3; - public const MINOR_VERSION = 20; - public const RELEASE_VERSION = 1; + public const MINOR_VERSION = 21; + public const RELEASE_VERSION = 0; public const EXTRA_VERSION = 'DEV'; private $charset; From df877a1d53291f405f7719a5757c1e3644ff1f45 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sun, 19 Jan 2025 09:38:58 +0100 Subject: [PATCH 720/812] Introduce operator classes to describe operators provided by extensions instead of arrays --- CHANGELOG | 2 +- doc/advanced.rst | 23 +-- doc/deprecated.rst | 23 +++ src/Environment.php | 19 +-- src/ExpressionParser.php | 100 ++++-------- src/Extension/AbstractExtension.php | 2 +- src/Extension/CoreExtension.php | 150 +++++++++--------- src/Extension/ExtensionInterface.php | 12 +- src/ExtensionSet.php | 141 ++++++++++++---- src/Lexer.php | 9 +- src/Operator/AbstractOperator.php | 32 ++++ .../Binary/AbstractBinaryOperator.php | 34 ++++ src/Operator/Binary/AddBinaryOperator.php | 32 ++++ src/Operator/Binary/AndBinaryOperator.php | 32 ++++ .../Binary/BitwiseAndBinaryOperator.php | 32 ++++ .../Binary/BitwiseOrBinaryOperator.php | 32 ++++ .../Binary/BitwiseXorBinaryOperator.php | 32 ++++ src/Operator/Binary/ConcatBinaryOperator.php | 38 +++++ src/Operator/Binary/DivBinaryOperator.php | 32 ++++ src/Operator/Binary/ElvisBinaryOperator.php | 43 +++++ .../Binary/EndsWithBinaryOperator.php | 32 ++++ src/Operator/Binary/EqualBinaryOperator.php | 32 ++++ .../Binary/FloorDivBinaryOperator.php | 32 ++++ src/Operator/Binary/GreaterBinaryOperator.php | 32 ++++ .../Binary/GreaterEqualBinaryOperator.php | 32 ++++ .../Binary/HasEveryBinaryOperator.php | 32 ++++ src/Operator/Binary/HasSomeBinaryOperator.php | 32 ++++ src/Operator/Binary/InBinaryOperator.php | 32 ++++ src/Operator/Binary/IsBinaryOperator.php | 30 ++++ src/Operator/Binary/IsNotBinaryOperator.php | 30 ++++ src/Operator/Binary/LessBinaryOperator.php | 32 ++++ .../Binary/LessEqualBinaryOperator.php | 32 ++++ src/Operator/Binary/MatchesBinaryOperator.php | 32 ++++ src/Operator/Binary/ModBinaryOperator.php | 32 ++++ src/Operator/Binary/MulBinaryOperator.php | 32 ++++ .../Binary/NotEqualBinaryOperator.php | 32 ++++ src/Operator/Binary/NotInBinaryOperator.php | 32 ++++ .../Binary/NullCoalesceBinaryOperator.php | 44 +++++ src/Operator/Binary/OrBinaryOperator.php | 32 ++++ src/Operator/Binary/PowerBinaryOperator.php | 38 +++++ src/Operator/Binary/RangeBinaryOperator.php | 32 ++++ .../Binary/SpaceshipBinaryOperator.php | 32 ++++ .../Binary/StartsWithBinaryOperator.php | 32 ++++ src/Operator/Binary/SubBinaryOperator.php | 32 ++++ src/Operator/Binary/XorBinaryOperator.php | 32 ++++ src/Operator/OperatorArity.php | 19 +++ src/Operator/OperatorAssociativity.php | 18 +++ src/Operator/OperatorInterface.php | 36 +++++ src/Operator/Operators.php | 93 +++++++++++ .../Ternary/AbstractTernaryOperator.php | 23 +++ src/Operator/Unary/AbstractUnaryOperator.php | 23 +++ src/Operator/Unary/NegUnaryOperator.php | 32 ++++ src/Operator/Unary/NotUnaryOperator.php | 38 +++++ src/Operator/Unary/PosUnaryOperator.php | 32 ++++ tests/CustomExtensionTest.php | 4 +- tests/EnvironmentTest.php | 40 ++++- tests/ExpressionParserTest.php | 34 +++- 57 files changed, 1789 insertions(+), 237 deletions(-) create mode 100644 src/Operator/AbstractOperator.php create mode 100644 src/Operator/Binary/AbstractBinaryOperator.php create mode 100644 src/Operator/Binary/AddBinaryOperator.php create mode 100644 src/Operator/Binary/AndBinaryOperator.php create mode 100644 src/Operator/Binary/BitwiseAndBinaryOperator.php create mode 100644 src/Operator/Binary/BitwiseOrBinaryOperator.php create mode 100644 src/Operator/Binary/BitwiseXorBinaryOperator.php create mode 100644 src/Operator/Binary/ConcatBinaryOperator.php create mode 100644 src/Operator/Binary/DivBinaryOperator.php create mode 100644 src/Operator/Binary/ElvisBinaryOperator.php create mode 100644 src/Operator/Binary/EndsWithBinaryOperator.php create mode 100644 src/Operator/Binary/EqualBinaryOperator.php create mode 100644 src/Operator/Binary/FloorDivBinaryOperator.php create mode 100644 src/Operator/Binary/GreaterBinaryOperator.php create mode 100644 src/Operator/Binary/GreaterEqualBinaryOperator.php create mode 100644 src/Operator/Binary/HasEveryBinaryOperator.php create mode 100644 src/Operator/Binary/HasSomeBinaryOperator.php create mode 100644 src/Operator/Binary/InBinaryOperator.php create mode 100644 src/Operator/Binary/IsBinaryOperator.php create mode 100644 src/Operator/Binary/IsNotBinaryOperator.php create mode 100644 src/Operator/Binary/LessBinaryOperator.php create mode 100644 src/Operator/Binary/LessEqualBinaryOperator.php create mode 100644 src/Operator/Binary/MatchesBinaryOperator.php create mode 100644 src/Operator/Binary/ModBinaryOperator.php create mode 100644 src/Operator/Binary/MulBinaryOperator.php create mode 100644 src/Operator/Binary/NotEqualBinaryOperator.php create mode 100644 src/Operator/Binary/NotInBinaryOperator.php create mode 100644 src/Operator/Binary/NullCoalesceBinaryOperator.php create mode 100644 src/Operator/Binary/OrBinaryOperator.php create mode 100644 src/Operator/Binary/PowerBinaryOperator.php create mode 100644 src/Operator/Binary/RangeBinaryOperator.php create mode 100644 src/Operator/Binary/SpaceshipBinaryOperator.php create mode 100644 src/Operator/Binary/StartsWithBinaryOperator.php create mode 100644 src/Operator/Binary/SubBinaryOperator.php create mode 100644 src/Operator/Binary/XorBinaryOperator.php create mode 100644 src/Operator/OperatorArity.php create mode 100644 src/Operator/OperatorAssociativity.php create mode 100644 src/Operator/OperatorInterface.php create mode 100644 src/Operator/Operators.php create mode 100644 src/Operator/Ternary/AbstractTernaryOperator.php create mode 100644 src/Operator/Unary/AbstractUnaryOperator.php create mode 100644 src/Operator/Unary/NegUnaryOperator.php create mode 100644 src/Operator/Unary/NotUnaryOperator.php create mode 100644 src/Operator/Unary/PosUnaryOperator.php diff --git a/CHANGELOG b/CHANGELOG index 30730ffeb84..bd25ae8b3f2 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,6 @@ # 3.21.0 (2025-XX-XX) - * n/a + * Introduce operator classes to describe operators provided by extensions instead of arrays # 3.20.0 (2025-02-13) diff --git a/doc/advanced.rst b/doc/advanced.rst index bc7e5d376f4..963aecf86d0 100644 --- a/doc/advanced.rst +++ b/doc/advanced.rst @@ -775,26 +775,9 @@ responsible for parsing the tag and compiling it to PHP. Operators ~~~~~~~~~ -The ``getOperators()`` methods lets you add new operators. Here is how to add -the ``!``, ``||``, and ``&&`` operators:: - - class CustomTwigExtension extends \Twig\Extension\AbstractExtension - { - public function getOperators() - { - return [ - [ - '!' => ['precedence' => 50, 'class' => \Twig\Node\Expression\Unary\NotUnary::class], - ], - [ - '||' => ['precedence' => 10, 'class' => \Twig\Node\Expression\Binary\OrBinary::class, 'associativity' => \Twig\ExpressionParser::OPERATOR_LEFT], - '&&' => ['precedence' => 15, 'class' => \Twig\Node\Expression\Binary\AndBinary::class, 'associativity' => \Twig\ExpressionParser::OPERATOR_LEFT], - ], - ]; - } - - // ... - } +The ``getOperators()`` methods lets you add new operators. To implement a new +one, have a look at the default operators provided by +``Twig\Extension\CoreExtension``. Tests ~~~~~ diff --git a/doc/deprecated.rst b/doc/deprecated.rst index b3397d8ddd7..74e9e695b43 100644 --- a/doc/deprecated.rst +++ b/doc/deprecated.rst @@ -418,3 +418,26 @@ Operators {# or #} {{ (not 1) * 2 }} {# this is equivalent to what Twig 4.x will do without the parentheses #} + +* Operators are now instances of ``Twig\Operator\OperatorInterface`` instead of + arrays. The ``ExtensionInterface::getOperators()`` method should now return an + array of ``Twig\Operator\OperatorInterface`` instances. + + Before: + + public function getOperators(): array { + return [ + 'not' => [ + 'precedence' => 10, + 'class' => NotUnaryOperator::class, + ], + ]; + } + + After: + + public function getOperators(): array { + return [ + new NotUnaryOperator(), + ]; + } diff --git a/src/Environment.php b/src/Environment.php index 6e00b3d3b24..e367835acf4 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -27,11 +27,10 @@ use Twig\Loader\ArrayLoader; use Twig\Loader\ChainLoader; use Twig\Loader\LoaderInterface; -use Twig\Node\Expression\Binary\AbstractBinary; -use Twig\Node\Expression\Unary\AbstractUnary; use Twig\Node\ModuleNode; use Twig\Node\Node; use Twig\NodeVisitor\NodeVisitorInterface; +use Twig\Operator\Operators; use Twig\Runtime\EscaperRuntime; use Twig\RuntimeLoader\FactoryRuntimeLoader; use Twig\RuntimeLoader\RuntimeLoaderInterface; @@ -925,22 +924,10 @@ public function mergeGlobals(array $context): array /** * @internal - * - * @return array}> - */ - public function getUnaryOperators(): array - { - return $this->extensionSet->getUnaryOperators(); - } - - /** - * @internal - * - * @return array, associativity: ExpressionParser::OPERATOR_*}> */ - public function getBinaryOperators(): array + public function getOperators(): Operators { - return $this->extensionSet->getBinaryOperators(); + return $this->extensionSet->getOperators(); } private function updateOptionsHash(): void diff --git a/src/ExpressionParser.php b/src/ExpressionParser.php index 233139ee4ab..db0094948a9 100644 --- a/src/ExpressionParser.php +++ b/src/ExpressionParser.php @@ -18,14 +18,12 @@ use Twig\Node\Expression\AbstractExpression; use Twig\Node\Expression\ArrayExpression; use Twig\Node\Expression\ArrowFunctionExpression; -use Twig\Node\Expression\Binary\AbstractBinary; use Twig\Node\Expression\Binary\ConcatBinary; use Twig\Node\Expression\ConstantExpression; use Twig\Node\Expression\GetAttrExpression; use Twig\Node\Expression\MacroReferenceExpression; use Twig\Node\Expression\Ternary\ConditionalTernary; use Twig\Node\Expression\TestExpression; -use Twig\Node\Expression\Unary\AbstractUnary; use Twig\Node\Expression\Unary\NegUnary; use Twig\Node\Expression\Unary\NotUnary; use Twig\Node\Expression\Unary\PosUnary; @@ -36,6 +34,9 @@ use Twig\Node\Expression\Variable\TemplateVariable; use Twig\Node\Node; use Twig\Node\Nodes; +use Twig\Operator\OperatorArity; +use Twig\Operator\OperatorAssociativity; +use Twig\Operator\Operators; /** * Parses expressions. @@ -49,44 +50,19 @@ */ class ExpressionParser { + // deprecated, to be removed in 4.0 public const OPERATOR_LEFT = 1; public const OPERATOR_RIGHT = 2; - /** @var array}> */ - private $unaryOperators; - /** @var array, associativity: self::OPERATOR_*}> */ - private $binaryOperators; + private Operators $operators; private $readyNodes = []; - private array $precedenceChanges = []; private bool $deprecationCheck = true; public function __construct( private Parser $parser, private Environment $env, ) { - $this->unaryOperators = $env->getUnaryOperators(); - $this->binaryOperators = $env->getBinaryOperators(); - - $ops = []; - foreach ($this->unaryOperators as $n => $c) { - $ops[] = $c + ['name' => $n, 'type' => 'unary']; - } - foreach ($this->binaryOperators as $n => $c) { - $ops[] = $c + ['name' => $n, 'type' => 'binary']; - } - foreach ($ops as $config) { - if (!isset($config['precedence_change'])) { - continue; - } - $name = $config['type'].'_'.$config['name']; - $min = min($config['precedence_change']->getNewPrecedence(), $config['precedence']); - $max = max($config['precedence_change']->getNewPrecedence(), $config['precedence']); - foreach ($ops as $c) { - if ($c['precedence'] > $min && $c['precedence'] < $max) { - $this->precedenceChanges[$c['type'].'_'.$c['name']][] = $name; - } - } - } + $this->operators = $env->getOperators(); } public function parseExpression($precedence = 0) @@ -101,28 +77,30 @@ public function parseExpression($precedence = 0) $expr = $this->getPrimary(); $token = $this->parser->getCurrentToken(); - while ($this->isBinary($token) && $this->binaryOperators[$token->getValue()]['precedence'] >= $precedence) { - $op = $this->binaryOperators[$token->getValue()]; + while ($token->test(Token::OPERATOR_TYPE) && ($op = $this->operators->getBinary($token->getValue())) && $op->getPrecedence() >= $precedence) { $this->parser->getStream()->next(); if ('is not' === $token->getValue()) { $expr = $this->parseNotTestExpression($expr); } elseif ('is' === $token->getValue()) { $expr = $this->parseTestExpression($expr); - } elseif (isset($op['callable'])) { - $expr = $op['callable']($this->parser, $expr); + } elseif (null !== $op->getCallable()) { + $expr = $op->getCallable()($this->parser, $expr); } else { $previous = $this->setDeprecationCheck(true); try { - $expr1 = $this->parseExpression(self::OPERATOR_LEFT === $op['associativity'] ? $op['precedence'] + 1 : $op['precedence']); + $expr1 = $this->parseExpression(OperatorAssociativity::Left === $op->getAssociativity() ? $op->getPrecedence() + 1 : $op->getPrecedence()); } finally { $this->setDeprecationCheck($previous); } - $class = $op['class']; + $class = $op->getNodeClass(); + if (!$class) { + throw new \LogicException(\sprintf('Operator "%s" must have a Node class.', $op->getOperator())); + } $expr = new $class($expr, $expr1, $token->getLine()); } - $expr->setAttribute('operator', 'binary_'.$token->getValue()); + $expr->setAttribute('operator', $op); $this->triggerPrecedenceDeprecations($expr); @@ -138,35 +116,35 @@ public function parseExpression($precedence = 0) private function triggerPrecedenceDeprecations(AbstractExpression $expr): void { + $precedenceChanges = $this->operators->getPrecedenceChanges(); // Check that the all nodes that are between the 2 precedences have explicit parentheses - if (!$expr->hasAttribute('operator') || !isset($this->precedenceChanges[$expr->getAttribute('operator')])) { + if (!$expr->hasAttribute('operator') || !isset($precedenceChanges[$expr->getAttribute('operator')])) { return; } - if (str_starts_with($unaryOp = $expr->getAttribute('operator'), 'unary')) { + if (OperatorArity::Unary === $expr->getAttribute('operator')->getArity()) { if ($expr->hasExplicitParentheses()) { return; } - $target = explode('_', $unaryOp)[1]; + $operator = $expr->getAttribute('operator'); /** @var AbstractExpression $node */ $node = $expr->getNode('node'); - foreach ($this->precedenceChanges as $operatorName => $changes) { - if (!\in_array($unaryOp, $changes)) { + foreach ($precedenceChanges as $op => $changes) { + if (!\in_array($operator, $changes, true)) { continue; } - if ($node->hasAttribute('operator') && $operatorName === $node->getAttribute('operator')) { - $change = $this->unaryOperators[$target]['precedence_change']; - trigger_deprecation($change->getPackage(), $change->getVersion(), \sprintf('Add explicit parentheses around the "%s" unary operator to avoid behavior change in the next major version as its precedence will change in "%s" at line %d.', $target, $this->parser->getStream()->getSourceContext()->getName(), $node->getTemplateLine())); + if ($node->hasAttribute('operator') && $op === $node->getAttribute('operator')) { + $change = $operator->getPrecedenceChange(); + trigger_deprecation($change->getPackage(), $change->getVersion(), \sprintf('Add explicit parentheses around the "%s" unary operator to avoid behavior change in the next major version as its precedence will change in "%s" at line %d.', $operator->getOperator(), $this->parser->getStream()->getSourceContext()->getName(), $node->getTemplateLine())); } } } else { - foreach ($this->precedenceChanges[$expr->getAttribute('operator')] as $operatorName) { + foreach ($precedenceChanges[$expr->getAttribute('operator')] as $operator) { foreach ($expr as $node) { /** @var AbstractExpression $node */ - if ($node->hasAttribute('operator') && $operatorName === $node->getAttribute('operator') && !$node->hasExplicitParentheses()) { - $op = explode('_', $operatorName)[1]; - $change = $this->binaryOperators[$op]['precedence_change']; - trigger_deprecation($change->getPackage(), $change->getVersion(), \sprintf('Add explicit parentheses around the "%s" binary operator to avoid behavior change in the next major version as its precedence will change in "%s" at line %d.', $op, $this->parser->getStream()->getSourceContext()->getName(), $node->getTemplateLine())); + if ($node->hasAttribute('operator') && $operator === $node->getAttribute('operator') && !$node->hasExplicitParentheses()) { + $change = $operator->getPrecedenceChange(); + trigger_deprecation($change->getPackage(), $change->getVersion(), \sprintf('Add explicit parentheses around the "%s" binary operator to avoid behavior change in the next major version as its precedence will change in "%s" at line %d.', $operator->getOperator(), $this->parser->getStream()->getSourceContext()->getName(), $node->getTemplateLine())); } } } @@ -235,14 +213,16 @@ private function getPrimary(): AbstractExpression { $token = $this->parser->getCurrentToken(); - if ($this->isUnary($token)) { - $operator = $this->unaryOperators[$token->getValue()]; + if ($token->test(Token::OPERATOR_TYPE) && $operator = $this->operators->getUnary($token->getValue())) { $this->parser->getStream()->next(); - $expr = $this->parseExpression($operator['precedence']); - $class = $operator['class']; + $expr = $this->parseExpression($operator->getPrecedence()); + $class = $operator->getNodeClass(); + if (!$class) { + throw new \LogicException(\sprintf('Operator "%s" must have a Node class.', $operator->getOperator())); + } $expr = new $class($expr, $token->getLine()); - $expr->setAttribute('operator', 'unary_'.$token->getValue()); + $expr->setAttribute('operator', $operator); if ($this->deprecationCheck) { $this->triggerPrecedenceDeprecations($expr); @@ -283,16 +263,6 @@ private function parseConditionalExpression($expr): AbstractExpression return $expr; } - private function isUnary(Token $token): bool - { - return $token->test(Token::OPERATOR_TYPE) && isset($this->unaryOperators[$token->getValue()]); - } - - private function isBinary(Token $token): bool - { - return $token->test(Token::OPERATOR_TYPE) && isset($this->binaryOperators[$token->getValue()]); - } - public function parsePrimaryExpression() { $token = $this->parser->getCurrentToken(); diff --git a/src/Extension/AbstractExtension.php b/src/Extension/AbstractExtension.php index 26c00c68066..02767f7c37c 100644 --- a/src/Extension/AbstractExtension.php +++ b/src/Extension/AbstractExtension.php @@ -40,7 +40,7 @@ public function getFunctions() public function getOperators() { - return [[], []]; + return []; } public function getLastModified(): int diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index a351f570a18..53e04271074 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -16,40 +16,8 @@ use Twig\Error\LoaderError; use Twig\Error\RuntimeError; use Twig\Error\SyntaxError; -use Twig\ExpressionParser; use Twig\Markup; use Twig\Node\Expression\AbstractExpression; -use Twig\Node\Expression\Binary\AddBinary; -use Twig\Node\Expression\Binary\AndBinary; -use Twig\Node\Expression\Binary\BitwiseAndBinary; -use Twig\Node\Expression\Binary\BitwiseOrBinary; -use Twig\Node\Expression\Binary\BitwiseXorBinary; -use Twig\Node\Expression\Binary\ConcatBinary; -use Twig\Node\Expression\Binary\DivBinary; -use Twig\Node\Expression\Binary\ElvisBinary; -use Twig\Node\Expression\Binary\EndsWithBinary; -use Twig\Node\Expression\Binary\EqualBinary; -use Twig\Node\Expression\Binary\FloorDivBinary; -use Twig\Node\Expression\Binary\GreaterBinary; -use Twig\Node\Expression\Binary\GreaterEqualBinary; -use Twig\Node\Expression\Binary\HasEveryBinary; -use Twig\Node\Expression\Binary\HasSomeBinary; -use Twig\Node\Expression\Binary\InBinary; -use Twig\Node\Expression\Binary\LessBinary; -use Twig\Node\Expression\Binary\LessEqualBinary; -use Twig\Node\Expression\Binary\MatchesBinary; -use Twig\Node\Expression\Binary\ModBinary; -use Twig\Node\Expression\Binary\MulBinary; -use Twig\Node\Expression\Binary\NotEqualBinary; -use Twig\Node\Expression\Binary\NotInBinary; -use Twig\Node\Expression\Binary\NullCoalesceBinary; -use Twig\Node\Expression\Binary\OrBinary; -use Twig\Node\Expression\Binary\PowerBinary; -use Twig\Node\Expression\Binary\RangeBinary; -use Twig\Node\Expression\Binary\SpaceshipBinary; -use Twig\Node\Expression\Binary\StartsWithBinary; -use Twig\Node\Expression\Binary\SubBinary; -use Twig\Node\Expression\Binary\XorBinary; use Twig\Node\Expression\BlockReferenceExpression; use Twig\Node\Expression\Filter\DefaultFilter; use Twig\Node\Expression\FunctionNode\EnumCasesFunction; @@ -63,11 +31,43 @@ use Twig\Node\Expression\Test\NullTest; use Twig\Node\Expression\Test\OddTest; 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\Node; -use Twig\OperatorPrecedenceChange; +use Twig\Operator\Binary\AddBinaryOperator; +use Twig\Operator\Binary\AndBinaryOperator; +use Twig\Operator\Binary\BitwiseAndBinaryOperator; +use Twig\Operator\Binary\BitwiseOrBinaryOperator; +use Twig\Operator\Binary\BitwiseXorBinaryOperator; +use Twig\Operator\Binary\ConcatBinaryOperator; +use Twig\Operator\Binary\DivBinaryOperator; +use Twig\Operator\Binary\ElvisBinaryOperator; +use Twig\Operator\Binary\EndsWithBinaryOperator; +use Twig\Operator\Binary\EqualBinaryOperator; +use Twig\Operator\Binary\FloorDivBinaryOperator; +use Twig\Operator\Binary\GreaterBinaryOperator; +use Twig\Operator\Binary\GreaterEqualBinaryOperator; +use Twig\Operator\Binary\HasEveryBinaryOperator; +use Twig\Operator\Binary\HasSomeBinaryOperator; +use Twig\Operator\Binary\InBinaryOperator; +use Twig\Operator\Binary\IsBinaryOperator; +use Twig\Operator\Binary\IsNotBinaryOperator; +use Twig\Operator\Binary\LessBinaryOperator; +use Twig\Operator\Binary\LessEqualBinaryOperator; +use Twig\Operator\Binary\MatchesBinaryOperator; +use Twig\Operator\Binary\ModBinaryOperator; +use Twig\Operator\Binary\MulBinaryOperator; +use Twig\Operator\Binary\NotEqualBinaryOperator; +use Twig\Operator\Binary\NotInBinaryOperator; +use Twig\Operator\Binary\NullCoalesceBinaryOperator; +use Twig\Operator\Binary\OrBinaryOperator; +use Twig\Operator\Binary\PowerBinaryOperator; +use Twig\Operator\Binary\RangeBinaryOperator; +use Twig\Operator\Binary\SpaceshipBinaryOperator; +use Twig\Operator\Binary\StartsWithBinaryOperator; +use Twig\Operator\Binary\SubBinaryOperator; +use Twig\Operator\Binary\XorBinaryOperator; +use Twig\Operator\Unary\NegUnaryOperator; +use Twig\Operator\Unary\NotUnaryOperator; +use Twig\Operator\Unary\PosUnaryOperator; use Twig\Parser; use Twig\Sandbox\SecurityNotAllowedMethodError; use Twig\Sandbox\SecurityNotAllowedPropertyError; @@ -316,47 +316,43 @@ public function getNodeVisitors(): array public function getOperators(): array { return [ - [ - 'not' => ['precedence' => 50, 'precedence_change' => new OperatorPrecedenceChange('twig/twig', '3.15', 70), 'class' => NotUnary::class], - '-' => ['precedence' => 500, 'class' => NegUnary::class], - '+' => ['precedence' => 500, 'class' => PosUnary::class], - ], - [ - '? :' => ['precedence' => 5, 'class' => ElvisBinary::class, 'associativity' => ExpressionParser::OPERATOR_RIGHT], - '?:' => ['precedence' => 5, 'class' => ElvisBinary::class, 'associativity' => ExpressionParser::OPERATOR_RIGHT], - '??' => ['precedence' => 300, 'precedence_change' => new OperatorPrecedenceChange('twig/twig', '3.15', 5), 'class' => NullCoalesceBinary::class, 'associativity' => ExpressionParser::OPERATOR_RIGHT], - 'or' => ['precedence' => 10, 'class' => OrBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT], - 'xor' => ['precedence' => 12, 'class' => XorBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT], - 'and' => ['precedence' => 15, 'class' => AndBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT], - 'b-or' => ['precedence' => 16, 'class' => BitwiseOrBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT], - 'b-xor' => ['precedence' => 17, 'class' => BitwiseXorBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT], - 'b-and' => ['precedence' => 18, 'class' => BitwiseAndBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT], - '==' => ['precedence' => 20, 'class' => EqualBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT], - '!=' => ['precedence' => 20, 'class' => NotEqualBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT], - '<=>' => ['precedence' => 20, 'class' => SpaceshipBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT], - '<' => ['precedence' => 20, 'class' => LessBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT], - '>' => ['precedence' => 20, 'class' => GreaterBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT], - '>=' => ['precedence' => 20, 'class' => GreaterEqualBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT], - '<=' => ['precedence' => 20, 'class' => LessEqualBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT], - 'not in' => ['precedence' => 20, 'class' => NotInBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT], - 'in' => ['precedence' => 20, 'class' => InBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT], - 'matches' => ['precedence' => 20, 'class' => MatchesBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT], - 'starts with' => ['precedence' => 20, 'class' => StartsWithBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT], - 'ends with' => ['precedence' => 20, 'class' => EndsWithBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT], - 'has some' => ['precedence' => 20, 'class' => HasSomeBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT], - 'has every' => ['precedence' => 20, 'class' => HasEveryBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT], - '..' => ['precedence' => 25, 'class' => RangeBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT], - '+' => ['precedence' => 30, 'class' => AddBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT], - '-' => ['precedence' => 30, 'class' => SubBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT], - '~' => ['precedence' => 40, 'precedence_change' => new OperatorPrecedenceChange('twig/twig', '3.15', 27), 'class' => ConcatBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT], - '*' => ['precedence' => 60, 'class' => MulBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT], - '/' => ['precedence' => 60, 'class' => DivBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT], - '//' => ['precedence' => 60, 'class' => FloorDivBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT], - '%' => ['precedence' => 60, 'class' => ModBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT], - 'is' => ['precedence' => 100, 'associativity' => ExpressionParser::OPERATOR_LEFT], - 'is not' => ['precedence' => 100, 'associativity' => ExpressionParser::OPERATOR_LEFT], - '**' => ['precedence' => 200, 'class' => PowerBinary::class, 'associativity' => ExpressionParser::OPERATOR_RIGHT], - ], + new NotUnaryOperator(), + new NegUnaryOperator(), + new PosUnaryOperator(), + + new ElvisBinaryOperator(), + new NullCoalesceBinaryOperator(), + new OrBinaryOperator(), + new XorBinaryOperator(), + new AndBinaryOperator(), + new BitwiseOrBinaryOperator(), + new BitwiseXorBinaryOperator(), + new BitwiseAndBinaryOperator(), + new EqualBinaryOperator(), + new NotEqualBinaryOperator(), + new SpaceshipBinaryOperator(), + new LessBinaryOperator(), + new GreaterBinaryOperator(), + new GreaterEqualBinaryOperator(), + new LessEqualBinaryOperator(), + new NotInBinaryOperator(), + new InBinaryOperator(), + new MatchesBinaryOperator(), + new StartsWithBinaryOperator(), + new EndsWithBinaryOperator(), + new HasSomeBinaryOperator(), + new HasEveryBinaryOperator(), + new RangeBinaryOperator(), + new AddBinaryOperator(), + new SubBinaryOperator(), + new ConcatBinaryOperator(), + new MulBinaryOperator(), + new DivBinaryOperator(), + new FloorDivBinaryOperator(), + new ModBinaryOperator(), + new IsBinaryOperator(), + new IsNotBinaryOperator(), + new PowerBinaryOperator(), ]; } diff --git a/src/Extension/ExtensionInterface.php b/src/Extension/ExtensionInterface.php index d51cd3ee2ff..6d5e4b5fa7f 100644 --- a/src/Extension/ExtensionInterface.php +++ b/src/Extension/ExtensionInterface.php @@ -11,11 +11,8 @@ namespace Twig\Extension; -use Twig\ExpressionParser; -use Twig\Node\Expression\Binary\AbstractBinary; -use Twig\Node\Expression\Unary\AbstractUnary; use Twig\NodeVisitor\NodeVisitorInterface; -use Twig\OperatorPrecedenceChange; +use Twig\Operator\OperatorInterface; use Twig\TokenParser\TokenParserInterface; use Twig\TwigFilter; use Twig\TwigFunction; @@ -66,12 +63,7 @@ public function getFunctions(); /** * Returns a list of operators to add to the existing list. * - * @return array First array of unary operators, second array of binary operators - * - * @psalm-return array{ - * array}>, - * array, associativity: ExpressionParser::OPERATOR_*}> - * } + * @return OperatorInterface[] */ public function getOperators(); } diff --git a/src/ExtensionSet.php b/src/ExtensionSet.php index b069232b44f..7a924f7b141 100644 --- a/src/ExtensionSet.php +++ b/src/ExtensionSet.php @@ -16,9 +16,12 @@ use Twig\Extension\GlobalsInterface; use Twig\Extension\LastModifiedExtensionInterface; use Twig\Extension\StagingExtension; -use Twig\Node\Expression\Binary\AbstractBinary; -use Twig\Node\Expression\Unary\AbstractUnary; use Twig\NodeVisitor\NodeVisitorInterface; +use Twig\Operator\Binary\AbstractBinaryOperator; +use Twig\Operator\OperatorAssociativity; +use Twig\Operator\OperatorInterface; +use Twig\Operator\Operators; +use Twig\Operator\Unary\AbstractUnaryOperator; use Twig\TokenParser\TokenParserInterface; /** @@ -46,10 +49,8 @@ final class ExtensionSet private $functions; /** @var array */ private $dynamicFunctions; - /** @var array}> */ - private $unaryOperators; - /** @var array, associativity: ExpressionParser::OPERATOR_*}> */ - private $binaryOperators; + /** @var Operators */ + private $operators; /** @var array|null */ private $globals; /** @var array */ @@ -406,28 +407,13 @@ public function getTest(string $name): ?TwigTest return null; } - /** - * @return array}> - */ - public function getUnaryOperators(): array + public function getOperators(): Operators { if (!$this->initialized) { $this->initExtensions(); } - return $this->unaryOperators; - } - - /** - * @return array, associativity: ExpressionParser::OPERATOR_*}> - */ - public function getBinaryOperators(): array - { - if (!$this->initialized) { - $this->initExtensions(); - } - - return $this->binaryOperators; + return $this->operators; } private function initExtensions(): void @@ -440,8 +426,7 @@ private function initExtensions(): void $this->dynamicFunctions = []; $this->dynamicTests = []; $this->visitors = []; - $this->unaryOperators = []; - $this->binaryOperators = []; + $this->operators = new Operators(); foreach ($this->extensions as $extension) { $this->initExtension($extension); @@ -497,12 +482,110 @@ private function initExtension(ExtensionInterface $extension): void throw new \InvalidArgumentException(\sprintf('"%s::getOperators()" must return an array with operators, got "%s".', \get_class($extension), get_debug_type($operators).(\is_resource($operators) ? '' : '#'.$operators))); } - if (2 !== \count($operators)) { - throw new \InvalidArgumentException(\sprintf('"%s::getOperators()" must return an array of 2 elements, got %d.', \get_class($extension), \count($operators))); + // new signature? + $legacy = false; + foreach ($operators as $op) { + if (!$op instanceof OperatorInterface) { + $legacy = true; + + break; + } } - $this->unaryOperators = array_merge($this->unaryOperators, $operators[0]); - $this->binaryOperators = array_merge($this->binaryOperators, $operators[1]); + if ($legacy) { + if (2 !== \count($operators)) { + throw new \InvalidArgumentException(\sprintf('"%s::getOperators()" must return an array of 2 elements, got %d.', \get_class($extension), \count($operators))); + } + + trigger_deprecation('twig/twig', '3.19.0', \sprintf('Extension "%s" uses the old signature for "getOperators()", please update it to return an array of "OperatorInterface" objects.', \get_class($extension))); + + $ops = []; + foreach ($operators[0] as $n => $op) { + $ops[] = $op instanceof OperatorInterface ? $op : $this->convertUnaryOperators($n, $op); + } + foreach ($operators[1] as $n => $op) { + $ops[] = $op instanceof OperatorInterface ? $op : $this->convertBinaryOperators($n, $op); + } + $this->operators->add($ops); + } else { + $this->operators->add($operators); + } } } + + private function convertUnaryOperators(string $n, array $op): OperatorInterface + { + trigger_deprecation('twig/twig', '3.19.0', \sprintf('Using a non-OperatorInterface object to define the "%s" unary operator is deprecated.', $n)); + + return new class($op, $n) extends AbstractUnaryOperator { + public function __construct(private array $op, private string $operator) + { + } + + public function getOperator(): string + { + return $this->operator; + } + + public function getPrecedence(): int + { + return $this->op['precedence']; + } + + public function getPrecedenceChange(): ?OperatorPrecedenceChange + { + return $this->op['precedence_change'] ?? null; + } + + public function getNodeClass(): ?string + { + return $this->op['class'] ?? null; + } + }; + } + + private function convertBinaryOperators(string $n, array $op): OperatorInterface + { + trigger_deprecation('twig/twig', '3.19.0', \sprintf('Using a non-OperatorInterface object to define the "%s" binary operator is deprecated.', $n)); + + return new class($op, $n) extends AbstractBinaryOperator { + public function __construct(private array $op, private string $operator) + { + } + + public function getOperator(): string + { + return $this->operator; + } + + public function getPrecedence(): int + { + return $this->op['precedence']; + } + + public function getPrecedenceChange(): ?OperatorPrecedenceChange + { + return $this->op['precedence_change'] ?? null; + } + + public function getNodeClass(): ?string + { + return $this->op['class'] ?? null; + } + + public function getAssociativity(): OperatorAssociativity + { + return match ($this->op['associativity']) { + 1 => OperatorAssociativity::Left, + 2 => OperatorAssociativity::Right, + default => throw new \InvalidArgumentException(\sprintf('Invalid associativity "%s" for operator "%s".', $this->op['associativity'], $this->getOperator())), + }; + } + + public function getCallable(): ?callable + { + return $this->op['callable'] ?? null; + } + }; + } } diff --git a/src/Lexer.php b/src/Lexer.php index 929673c6082..215da8e9a5c 100644 --- a/src/Lexer.php +++ b/src/Lexer.php @@ -544,11 +544,10 @@ private function moveCursor($text): void private function getOperatorRegex(): string { - $operators = array_merge( - ['='], - array_keys($this->env->getUnaryOperators()), - array_keys($this->env->getBinaryOperators()) - ); + $operators = ['=']; + foreach ($this->env->getOperators() as $operator) { + $operators = array_merge($operators, [$operator->getOperator()], $operator->getAliases()); + } $operators = array_combine($operators, array_map('strlen', $operators)); arsort($operators); diff --git a/src/Operator/AbstractOperator.php b/src/Operator/AbstractOperator.php new file mode 100644 index 00000000000..c18904f35ae --- /dev/null +++ b/src/Operator/AbstractOperator.php @@ -0,0 +1,32 @@ +getArity()->value, $this->getOperator()); + } + + public function getPrecedenceChange(): ?OperatorPrecedenceChange + { + return null; + } + + public function getAliases(): array + { + return []; + } +} diff --git a/src/Operator/Binary/AbstractBinaryOperator.php b/src/Operator/Binary/AbstractBinaryOperator.php new file mode 100644 index 00000000000..2eec2537f3c --- /dev/null +++ b/src/Operator/Binary/AbstractBinaryOperator.php @@ -0,0 +1,34 @@ +'; + } + + public function getPrecedence(): int + { + return 20; + } + + public function getNodeClass(): ?string + { + return GreaterBinary::class; + } +} diff --git a/src/Operator/Binary/GreaterEqualBinaryOperator.php b/src/Operator/Binary/GreaterEqualBinaryOperator.php new file mode 100644 index 00000000000..aa709b55c39 --- /dev/null +++ b/src/Operator/Binary/GreaterEqualBinaryOperator.php @@ -0,0 +1,32 @@ +='; + } + + public function getPrecedence(): int + { + return 20; + } +} diff --git a/src/Operator/Binary/HasEveryBinaryOperator.php b/src/Operator/Binary/HasEveryBinaryOperator.php new file mode 100644 index 00000000000..98a74d32dbb --- /dev/null +++ b/src/Operator/Binary/HasEveryBinaryOperator.php @@ -0,0 +1,32 @@ +'; + } + + public function getPrecedence(): int + { + return 20; + } + + public function getNodeClass(): ?string + { + return SpaceshipBinary::class; + } +} diff --git a/src/Operator/Binary/StartsWithBinaryOperator.php b/src/Operator/Binary/StartsWithBinaryOperator.php new file mode 100644 index 00000000000..e28953b74a4 --- /dev/null +++ b/src/Operator/Binary/StartsWithBinaryOperator.php @@ -0,0 +1,32 @@ + + */ + public function getNodeClass(): ?string; + + public function getArity(): OperatorArity; + + public function getPrecedence(): int; + + public function getPrecedenceChange(): ?OperatorPrecedenceChange; + + /** + * @return array + */ + public function getAliases(): array; +} diff --git a/src/Operator/Operators.php b/src/Operator/Operators.php new file mode 100644 index 00000000000..19b92444224 --- /dev/null +++ b/src/Operator/Operators.php @@ -0,0 +1,93 @@ +add($operators); + } + + /** + * @param array $operators + * + * @return $this + */ + public function add(array $operators): self + { + $this->precedenceChanges = null; + foreach ($operators as $operator) { + $this->operators[$operator->getArity()->value][$operator->getOperator()] = $operator; + foreach ($operator->getAliases() as $alias) { + $this->aliases[$operator->getArity()->value][$alias] = $operator; + } + } + + return $this; + } + + public function getUnary(string $name): ?AbstractUnaryOperator + { + return $this->operators[OperatorArity::Unary->value][$name] ?? ($this->aliases[OperatorArity::Unary->value][$name] ?? null); + } + + public function getBinary(string $name): ?AbstractBinaryOperator + { + return $this->operators[OperatorArity::Binary->value][$name] ?? ($this->aliases[OperatorArity::Binary->value][$name] ?? null); + } + + public function getIterator(): \Traversable + { + foreach ($this->operators as $operators) { + // we don't yield the keys + yield from $operators; + } + } + + /** + * @internal + * + * @return \WeakMap> + */ + public function getPrecedenceChanges(): \WeakMap + { + if (null === $this->precedenceChanges) { + $this->precedenceChanges = new \WeakMap(); + foreach ($this as $op) { + if (!$op->getPrecedenceChange()) { + continue; + } + $min = min($op->getPrecedenceChange()->getNewPrecedence(), $op->getPrecedence()); + $max = max($op->getPrecedenceChange()->getNewPrecedence(), $op->getPrecedence()); + foreach ($this as $o) { + if ($o->getPrecedence() > $min && $o->getPrecedence() < $max) { + if (!isset($this->precedenceChanges[$o])) { + $this->precedenceChanges[$o] = []; + } + $this->precedenceChanges[$o][] = $op; + } + } + } + } + + return $this->precedenceChanges; + } +} diff --git a/src/Operator/Ternary/AbstractTernaryOperator.php b/src/Operator/Ternary/AbstractTernaryOperator.php new file mode 100644 index 00000000000..eceaa79a1e0 --- /dev/null +++ b/src/Operator/Ternary/AbstractTernaryOperator.php @@ -0,0 +1,23 @@ +expectException(\InvalidArgumentException::class); $this->expectExceptionMessage($expectedExceptionMessage); - $env->getUnaryOperators(); + $env->getOperators(); } public static function provideInvalidExtensions() diff --git a/tests/EnvironmentTest.php b/tests/EnvironmentTest.php index 34774db1c5b..5bc90b58215 100644 --- a/tests/EnvironmentTest.php +++ b/tests/EnvironmentTest.php @@ -26,6 +26,8 @@ use Twig\Loader\LoaderInterface; use Twig\Node\Node; use Twig\NodeVisitor\NodeVisitorInterface; +use Twig\Operator\Binary\AbstractBinaryOperator; +use Twig\Operator\Unary\AbstractUnaryOperator; use Twig\RuntimeLoader\RuntimeLoaderInterface; use Twig\Source; use Twig\Token; @@ -307,8 +309,8 @@ public function testAddExtension() $this->assertArrayHasKey('foo_filter', $twig->getFilters()); $this->assertArrayHasKey('foo_function', $twig->getFunctions()); $this->assertArrayHasKey('foo_test', $twig->getTests()); - $this->assertArrayHasKey('foo_unary', $twig->getUnaryOperators()); - $this->assertArrayHasKey('foo_binary', $twig->getBinaryOperators()); + $this->assertNotNull($twig->getOperators()->getUnary('foo_unary')); + $this->assertNotNull($twig->getOperators()->getBinary('foo_binary')); $this->assertArrayHasKey('foo_global', $twig->getGlobals()); $visitors = $twig->getNodeVisitors(); $found = false; @@ -597,8 +599,38 @@ public function getFunctions(): array public function getOperators(): array { return [ - ['foo_unary' => ['precedence' => 0]], - ['foo_binary' => ['precedence' => 0]], + new class extends AbstractUnaryOperator { + public function getOperator(): string + { + return 'foo_unary'; + } + + public function getPrecedence(): int + { + return 0; + } + + public function getNodeClass(): string + { + return ''; + } + }, + new class extends AbstractBinaryOperator { + public function getOperator(): string + { + return 'foo_binary'; + } + + public function getPrecedence(): int + { + return 0; + } + + public function getNodeClass(): string + { + return ''; + } + }, ]; } diff --git a/tests/ExpressionParserTest.php b/tests/ExpressionParserTest.php index d3887e93880..f98bade0841 100644 --- a/tests/ExpressionParserTest.php +++ b/tests/ExpressionParserTest.php @@ -28,6 +28,7 @@ use Twig\Node\Expression\Unary\AbstractUnary; use Twig\Node\Expression\Variable\ContextVariable; use Twig\Node\Node; +use Twig\Operator\Unary\AbstractUnaryOperator; use Twig\Parser; use Twig\Source; use Twig\TwigFilter; @@ -572,14 +573,31 @@ public function testUnaryPrecedenceChange() $env->addExtension(new class extends AbstractExtension { public function getOperators() { - $class = new class(new ConstantExpression('foo', 1), 1) extends AbstractUnary { - public function operator(Compiler $compiler): Compiler - { - return $compiler->raw('!'); - } - }; - - return [['!' => ['precedence' => 50, 'class' => $class::class]], []]; + return [ + new class extends AbstractUnaryOperator { + public function getOperator(): string + { + return '!'; + } + + public function getPrecedence(): int + { + return 50; + } + + public function getNodeClass(): string + { + $class = new class(new ConstantExpression('foo', 1), 1) extends AbstractUnary { + public function operator(Compiler $compiler): Compiler + { + return $compiler->raw('!'); + } + }; + + return $class::class; + } + }, + ]; } }); $parser = new Parser($env); From 42c82e1e9b88af365b15ed35cb5488f165802710 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sun, 19 Jan 2025 21:21:19 +0100 Subject: [PATCH 721/812] Extract operators logic from ExpressionParser to their own classes --- src/ExpressionParser.php | 552 ++++-------------- src/Extension/CoreExtension.php | 15 + src/Extension/ExtensionInterface.php | 7 +- src/ExtensionSet.php | 29 +- src/Lexer.php | 53 +- .../Expression/ArrowFunctionExpression.php | 25 +- src/Node/Expression/ListExpression.php | 48 ++ .../Binary/AbstractBinaryOperator.php | 20 +- src/Operator/Binary/AddBinaryOperator.php | 2 +- src/Operator/Binary/AndBinaryOperator.php | 2 +- src/Operator/Binary/ArrowBinaryOperator.php | 49 ++ .../Binary/BinaryOperatorInterface.php | 25 + .../Binary/BitwiseAndBinaryOperator.php | 2 +- .../Binary/BitwiseOrBinaryOperator.php | 2 +- .../Binary/BitwiseXorBinaryOperator.php | 2 +- src/Operator/Binary/ConcatBinaryOperator.php | 2 +- src/Operator/Binary/DivBinaryOperator.php | 2 +- src/Operator/Binary/DotBinaryOperator.php | 93 +++ src/Operator/Binary/ElvisBinaryOperator.php | 2 +- .../Binary/EndsWithBinaryOperator.php | 2 +- src/Operator/Binary/EqualBinaryOperator.php | 2 +- src/Operator/Binary/FilterBinaryOperator.php | 73 +++ .../Binary/FloorDivBinaryOperator.php | 2 +- .../Binary/FunctionBinaryOperator.php | 84 +++ src/Operator/Binary/GreaterBinaryOperator.php | 2 +- .../Binary/GreaterEqualBinaryOperator.php | 2 +- .../Binary/HasEveryBinaryOperator.php | 2 +- src/Operator/Binary/HasSomeBinaryOperator.php | 2 +- src/Operator/Binary/InBinaryOperator.php | 2 +- src/Operator/Binary/IsBinaryOperator.php | 58 +- src/Operator/Binary/IsNotBinaryOperator.php | 16 +- src/Operator/Binary/LessBinaryOperator.php | 2 +- .../Binary/LessEqualBinaryOperator.php | 2 +- src/Operator/Binary/MatchesBinaryOperator.php | 2 +- src/Operator/Binary/ModBinaryOperator.php | 2 +- src/Operator/Binary/MulBinaryOperator.php | 2 +- .../Binary/NotEqualBinaryOperator.php | 2 +- src/Operator/Binary/NotInBinaryOperator.php | 2 +- .../Binary/NullCoalesceBinaryOperator.php | 2 +- src/Operator/Binary/OrBinaryOperator.php | 2 +- src/Operator/Binary/PowerBinaryOperator.php | 2 +- src/Operator/Binary/RangeBinaryOperator.php | 2 +- .../Binary/SpaceshipBinaryOperator.php | 2 +- .../Binary/SquareBracketBinaryOperator.php | 87 +++ .../Binary/StartsWithBinaryOperator.php | 2 +- src/Operator/Binary/SubBinaryOperator.php | 2 +- src/Operator/Binary/XorBinaryOperator.php | 2 +- src/Operator/OperatorInterface.php | 8 +- src/Operator/Operators.php | 37 +- .../Ternary/AbstractTernaryOperator.php | 8 +- .../Ternary/ConditionalTernaryOperator.php | 50 ++ .../Ternary/TernaryOperatorInterface.php | 25 + src/Operator/Unary/AbstractUnaryOperator.php | 15 +- src/Operator/Unary/NegUnaryOperator.php | 2 +- src/Operator/Unary/NotUnaryOperator.php | 2 +- .../Unary/ParenthesisUnaryOperator.php | 74 +++ src/Operator/Unary/PosUnaryOperator.php | 2 +- src/Operator/Unary/UnaryOperatorInterface.php | 22 + src/Parser.php | 92 +++ src/Token.php | 40 +- src/TokenParser/AbstractTokenParser.php | 31 + src/TokenParser/ApplyTokenParser.php | 10 +- src/TokenParser/ForTokenParser.php | 2 +- src/TokenParser/MacroTokenParser.php | 2 +- src/TokenParser/SetTokenParser.php | 18 +- src/TokenParser/TypesTokenParser.php | 2 +- .../filters/arrow_reserved_names.test | 2 +- 67 files changed, 1191 insertions(+), 549 deletions(-) create mode 100644 src/Node/Expression/ListExpression.php create mode 100644 src/Operator/Binary/ArrowBinaryOperator.php create mode 100644 src/Operator/Binary/BinaryOperatorInterface.php create mode 100644 src/Operator/Binary/DotBinaryOperator.php create mode 100644 src/Operator/Binary/FilterBinaryOperator.php create mode 100644 src/Operator/Binary/FunctionBinaryOperator.php create mode 100644 src/Operator/Binary/SquareBracketBinaryOperator.php create mode 100644 src/Operator/Ternary/ConditionalTernaryOperator.php create mode 100644 src/Operator/Ternary/TernaryOperatorInterface.php create mode 100644 src/Operator/Unary/ParenthesisUnaryOperator.php create mode 100644 src/Operator/Unary/UnaryOperatorInterface.php diff --git a/src/ExpressionParser.php b/src/ExpressionParser.php index db0094948a9..ad4a05257f6 100644 --- a/src/ExpressionParser.php +++ b/src/ExpressionParser.php @@ -12,30 +12,20 @@ namespace Twig; -use Twig\Attribute\FirstClassTwigCallableReady; use Twig\Error\SyntaxError; -use Twig\Node\EmptyNode; use Twig\Node\Expression\AbstractExpression; use Twig\Node\Expression\ArrayExpression; -use Twig\Node\Expression\ArrowFunctionExpression; use Twig\Node\Expression\Binary\ConcatBinary; use Twig\Node\Expression\ConstantExpression; -use Twig\Node\Expression\GetAttrExpression; -use Twig\Node\Expression\MacroReferenceExpression; -use Twig\Node\Expression\Ternary\ConditionalTernary; -use Twig\Node\Expression\TestExpression; 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\Expression\Variable\AssignContextVariable; use Twig\Node\Expression\Variable\ContextVariable; use Twig\Node\Expression\Variable\LocalVariable; -use Twig\Node\Expression\Variable\TemplateVariable; use Twig\Node\Node; use Twig\Node\Nodes; use Twig\Operator\OperatorArity; -use Twig\Operator\OperatorAssociativity; use Twig\Operator\Operators; /** @@ -50,12 +40,16 @@ */ class ExpressionParser { - // deprecated, to be removed in 4.0 + /** + * @deprecated since Twig 3.20 + */ public const OPERATOR_LEFT = 1; + /** + * @deprecated since Twig 3.20 + */ public const OPERATOR_RIGHT = 2; private Operators $operators; - private $readyNodes = []; private bool $deprecationCheck = true; public function __construct( @@ -65,52 +59,57 @@ public function __construct( $this->operators = $env->getOperators(); } + /** + * @internal + */ + public function getParser(): Parser + { + return $this->parser; + } + + /** + * @internal + */ + public function getStream(): TokenStream + { + return $this->parser->getStream(); + } + + /** + * @internal + */ + public function getImportedSymbol(string $type, string $name) + { + return $this->parser->getImportedSymbol($type, $name); + } + public function parseExpression($precedence = 0) { if (\func_num_args() > 1) { trigger_deprecation('twig/twig', '3.15', 'Passing a second argument ($allowArrow) to "%s()" is deprecated.', __METHOD__); } - if ($arrow = $this->parseArrow()) { - return $arrow; - } - - $expr = $this->getPrimary(); + $expr = $this->parsePrimary(); $token = $this->parser->getCurrentToken(); - while ($token->test(Token::OPERATOR_TYPE) && ($op = $this->operators->getBinary($token->getValue())) && $op->getPrecedence() >= $precedence) { + while ( + $token->test(Token::OPERATOR_TYPE) + && ( + ($op = $this->operators->getTernary($token->getValue())) && $op->getPrecedence() >= $precedence + || ($op = $this->operators->getBinary($token->getValue())) && $op->getPrecedence() >= $precedence + ) + ) { $this->parser->getStream()->next(); - - if ('is not' === $token->getValue()) { - $expr = $this->parseNotTestExpression($expr); - } elseif ('is' === $token->getValue()) { - $expr = $this->parseTestExpression($expr); - } elseif (null !== $op->getCallable()) { - $expr = $op->getCallable()($this->parser, $expr); - } else { - $previous = $this->setDeprecationCheck(true); - try { - $expr1 = $this->parseExpression(OperatorAssociativity::Left === $op->getAssociativity() ? $op->getPrecedence() + 1 : $op->getPrecedence()); - } finally { - $this->setDeprecationCheck($previous); - } - $class = $op->getNodeClass(); - if (!$class) { - throw new \LogicException(\sprintf('Operator "%s" must have a Node class.', $op->getOperator())); - } - $expr = new $class($expr, $expr1, $token->getLine()); + $previous = $this->setDeprecationCheck(true); + try { + $expr = $op->parse($this, $expr, $token); + } finally { + $this->setDeprecationCheck($previous); } - $expr->setAttribute('operator', $op); - $this->triggerPrecedenceDeprecations($expr); - $token = $this->parser->getCurrentToken(); } - if (0 === $precedence) { - return $this->parseConditionalExpression($expr); - } - return $expr; } @@ -152,115 +151,29 @@ private function triggerPrecedenceDeprecations(AbstractExpression $expr): void } /** - * @return ArrowFunctionExpression|null + * @internal */ - private function parseArrow() - { - $stream = $this->parser->getStream(); - - // short array syntax (one argument, no parentheses)? - if ($stream->look(1)->test(Token::ARROW_TYPE)) { - $line = $stream->getCurrent()->getLine(); - $token = $stream->expect(Token::NAME_TYPE); - $names = [new AssignContextVariable($token->getValue(), $token->getLine())]; - $stream->expect(Token::ARROW_TYPE); - - return new ArrowFunctionExpression($this->parseExpression(), new Nodes($names), $line); - } - - // first, determine if we are parsing an arrow function by finding => (long form) - $i = 0; - if (!$stream->look($i)->test(Token::PUNCTUATION_TYPE, '(')) { - return null; - } - ++$i; - while (true) { - // variable name - ++$i; - if (!$stream->look($i)->test(Token::PUNCTUATION_TYPE, ',')) { - break; - } - ++$i; - } - if (!$stream->look($i)->test(Token::PUNCTUATION_TYPE, ')')) { - return null; - } - ++$i; - if (!$stream->look($i)->test(Token::ARROW_TYPE)) { - return null; - } - - // yes, let's parse it properly - $token = $stream->expect(Token::PUNCTUATION_TYPE, '('); - $line = $token->getLine(); - - $names = []; - while (true) { - $token = $stream->expect(Token::NAME_TYPE); - $names[] = new AssignContextVariable($token->getValue(), $token->getLine()); - - if (!$stream->nextIf(Token::PUNCTUATION_TYPE, ',')) { - break; - } - } - $stream->expect(Token::PUNCTUATION_TYPE, ')'); - $stream->expect(Token::ARROW_TYPE); - - return new ArrowFunctionExpression($this->parseExpression(), new Nodes($names), $line); - } - - private function getPrimary(): AbstractExpression + public function parsePrimary(): AbstractExpression { $token = $this->parser->getCurrentToken(); - if ($token->test(Token::OPERATOR_TYPE) && $operator = $this->operators->getUnary($token->getValue())) { - $this->parser->getStream()->next(); - $expr = $this->parseExpression($operator->getPrecedence()); - $class = $operator->getNodeClass(); - if (!$class) { - throw new \LogicException(\sprintf('Operator "%s" must have a Node class.', $operator->getOperator())); - } - - $expr = new $class($expr, $token->getLine()); - $expr->setAttribute('operator', $operator); - - if ($this->deprecationCheck) { - $this->triggerPrecedenceDeprecations($expr); - } - - return $this->parsePostfixExpression($expr); - } elseif ($token->test(Token::PUNCTUATION_TYPE, '(')) { $this->parser->getStream()->next(); $previous = $this->setDeprecationCheck(false); try { - $expr = $this->parseExpression()->setExplicitParentheses(); + $expr = $operator->parse($this, $token); } finally { $this->setDeprecationCheck($previous); } - $this->parser->getStream()->expect(Token::PUNCTUATION_TYPE, ')', 'An opened parenthesis is not properly closed'); - - return $this->parsePostfixExpression($expr); - } - - return $this->parsePrimaryExpression(); - } + $expr->setAttribute('operator', $operator); - private function parseConditionalExpression($expr): AbstractExpression - { - while ($this->parser->getStream()->nextIf(Token::PUNCTUATION_TYPE, '?')) { - $expr2 = $this->parseExpression(); - if ($this->parser->getStream()->nextIf(Token::PUNCTUATION_TYPE, ':')) { - // Ternary operator (expr ? expr2 : expr3) - $expr3 = $this->parseExpression(); - } else { - // Ternary without else (expr ? expr2) - $expr3 = new ConstantExpression('', $this->parser->getCurrentToken()->getLine()); + if ($this->deprecationCheck) { + $this->triggerPrecedenceDeprecations($expr); } - $expr = new ConditionalTernary($expr, $expr2, $expr3, $this->parser->getCurrentToken()->getLine()); + return $expr; } - return $expr; + return $this->parsePrimaryExpression(); } public function parsePrimaryExpression() @@ -272,54 +185,52 @@ public function parsePrimaryExpression() switch ($token->getValue()) { case 'true': case 'TRUE': - $node = new ConstantExpression(true, $token->getLine()); - break; + return new ConstantExpression(true, $token->getLine()); case 'false': case 'FALSE': - $node = new ConstantExpression(false, $token->getLine()); - break; + return new ConstantExpression(false, $token->getLine()); case 'none': case 'NONE': case 'null': case 'NULL': - $node = new ConstantExpression(null, $token->getLine()); - break; + return new ConstantExpression(null, $token->getLine()); default: - if ('(' === $this->parser->getCurrentToken()->getValue()) { - $node = $this->getFunctionNode($token->getValue(), $token->getLine()); - } else { - $node = new ContextVariable($token->getValue(), $token->getLine()); - } + return new ContextVariable($token->getValue(), $token->getLine()); } - break; + // no break case $token->test(Token::NUMBER_TYPE): $this->parser->getStream()->next(); - $node = new ConstantExpression($token->getValue(), $token->getLine()); - break; + + return new ConstantExpression($token->getValue(), $token->getLine()); case $token->test(Token::STRING_TYPE): case $token->test(Token::INTERPOLATION_START_TYPE): - $node = $this->parseStringExpression(); - break; + return $this->parseStringExpression(); case $token->test(Token::PUNCTUATION_TYPE): - $node = match ($token->getValue()) { - '[' => $this->parseSequenceExpression(), + // In 4.0, we should always return the node or throw an error for default + if ($node = match ($token->getValue()) { '{' => $this->parseMappingExpression(), - default => throw new SyntaxError(\sprintf('Unexpected token "%s" of value "%s".', $token->toEnglish(), $token->getValue()), $token->getLine(), $this->parser->getStream()->getSourceContext()), - }; - break; + default => null, + }) { + return $node; + } + // no break case $token->test(Token::OPERATOR_TYPE): + if ('[' === $token->getValue()) { + return $this->parseSequenceExpression(); + } + if (preg_match(Lexer::REGEX_NAME, $token->getValue(), $matches) && $matches[0] == $token->getValue()) { // in this context, string operators are variable names $this->parser->getStream()->next(); - $node = new ContextVariable($token->getValue(), $token->getLine()); - break; + + return new ContextVariable($token->getValue(), $token->getLine()); } if ('=' === $token->getValue() && ('==' === $this->parser->getStream()->look(-1)->getValue() || '!=' === $this->parser->getStream()->look(-1)->getValue())) { @@ -330,8 +241,6 @@ public function parsePrimaryExpression() default: throw new SyntaxError(\sprintf('Unexpected token "%s" of value "%s".', $token->toEnglish(), $token->getValue()), $token->getLine(), $this->parser->getStream()->getSourceContext()); } - - return $this->parsePostfixExpression($node); } public function parseStringExpression() @@ -375,7 +284,7 @@ public function parseArrayExpression() public function parseSequenceExpression() { $stream = $this->parser->getStream(); - $stream->expect(Token::PUNCTUATION_TYPE, '[', 'A sequence element was expected'); + $stream->expect(Token::OPERATOR_TYPE, '[', 'A sequence element was expected'); $node = new ArrayExpression([], $stream->getCurrent()->getLine()); $first = true; @@ -455,7 +364,7 @@ public function parseMappingExpression() } } elseif (($token = $stream->nextIf(Token::STRING_TYPE)) || $token = $stream->nextIf(Token::NUMBER_TYPE)) { $key = new ConstantExpression($token->getValue(), $token->getLine()); - } elseif ($stream->test(Token::PUNCTUATION_TYPE, '(')) { + } elseif ($stream->test(Token::OPERATOR_TYPE, '(')) { $key = $this->parseExpression(); } else { $current = $stream->getCurrent(); @@ -473,8 +382,13 @@ public function parseMappingExpression() return $node; } + /** + * @deprecated since Twig 3.20 + */ public function parsePostfixExpression($node) { + trigger_deprecation('twig/twig', '3.20', 'The "%s()" method is deprecated.', __METHOD__); + while (true) { $token = $this->parser->getCurrentToken(); if ($token->test(Token::PUNCTUATION_TYPE)) { @@ -493,81 +407,45 @@ public function parsePostfixExpression($node) return $node; } - public function getFunctionNode($name, $line) - { - if (null !== $alias = $this->parser->getImportedSymbol('function', $name)) { - return new MacroReferenceExpression($alias['node']->getNode('var'), $alias['name'], $this->createArguments($line), $line); - } - - $args = $this->parseNamedArguments(); - $function = $this->getFunction($name, $line); - - if ($function->getParserCallable()) { - $fakeNode = new EmptyNode($line); - $fakeNode->setSourceContext($this->parser->getStream()->getSourceContext()); - - return ($function->getParserCallable())($this->parser, $fakeNode, $args, $line); - } - - if (!isset($this->readyNodes[$class = $function->getNodeClass()])) { - $this->readyNodes[$class] = (bool) (new \ReflectionClass($class))->getConstructor()->getAttributes(FirstClassTwigCallableReady::class); - } - - if (!$ready = $this->readyNodes[$class]) { - trigger_deprecation('twig/twig', '3.12', 'Twig node "%s" is not marked as ready for passing a "TwigFunction" in the constructor instead of its name; please update your code and then add #[FirstClassTwigCallableReady] attribute to the constructor.', $class); - } - - return new $class($ready ? $function : $function->getName(), $args, $line); - } - + /** + * @deprecated since Twig 3.20 + */ public function parseSubscriptExpression($node) { + trigger_deprecation('twig/twig', '3.20', 'The "%s()" method is deprecated.', __METHOD__); + if ('.' === $this->parser->getStream()->next()->getValue()) { - return $this->parseSubscriptExpressionDot($node); + return $this->operators->getBinary('.')->parse($this, $node, $this->parser->getCurrentToken()); } - return $this->parseSubscriptExpressionArray($node); + return $this->operators->getBinary('[')->parse($this, $node, $this->parser->getCurrentToken()); } + /** + * @deprecated since Twig 3.20 + */ public function parseFilterExpression($node) { + trigger_deprecation('twig/twig', '3.20', 'The "%s()" method is deprecated.', __METHOD__); + $this->parser->getStream()->next(); return $this->parseFilterExpressionRaw($node); } + /** + * @deprecated since Twig 3.20 + */ public function parseFilterExpressionRaw($node) { - if (\func_num_args() > 1) { - trigger_deprecation('twig/twig', '3.12', 'Passing a second argument to "%s()" is deprecated.', __METHOD__); - } + trigger_deprecation('twig/twig', '3.20', 'The "%s()" method is deprecated.', __METHOD__); + $op = $this->operators->getBinary('|'); while (true) { - $token = $this->parser->getStream()->expect(Token::NAME_TYPE); - - if (!$this->parser->getStream()->test(Token::PUNCTUATION_TYPE, '(')) { - $arguments = new EmptyNode(); - } else { - $arguments = $this->parseNamedArguments(); - } - - $filter = $this->getFilter($token->getValue(), $token->getLine()); - - $ready = true; - if (!isset($this->readyNodes[$class = $filter->getNodeClass()])) { - $this->readyNodes[$class] = (bool) (new \ReflectionClass($class))->getConstructor()->getAttributes(FirstClassTwigCallableReady::class); - } - - if (!$ready = $this->readyNodes[$class]) { - trigger_deprecation('twig/twig', '3.12', 'Twig node "%s" is not marked as ready for passing a "TwigFilter" in the constructor instead of its name; please update your code and then add #[FirstClassTwigCallableReady] attribute to the constructor.', $class); - } - - $node = new $class($node, $ready ? $filter : new ConstantExpression($filter->getName(), $token->getLine()), $arguments, $token->getLine()); - - if (!$this->parser->getStream()->test(Token::PUNCTUATION_TYPE, '|')) { + $node = $op->parse($this, $node, $this->parser->getCurrentToken()); + if (!$this->parser->getStream()->test(Token::OPERATOR_TYPE, '|')) { break; } - $this->parser->getStream()->next(); } @@ -600,7 +478,7 @@ public function parseArguments() $args = []; $stream = $this->parser->getStream(); - $stream->expect(Token::PUNCTUATION_TYPE, '(', 'A list of arguments must begin with an opening parenthesis'); + $stream->expect(Token::OPERATOR_TYPE, '(', 'A list of arguments must begin with an opening parenthesis'); $hasSpread = false; while (!$stream->test(Token::PUNCTUATION_TYPE, ')')) { if ($args) { @@ -634,7 +512,7 @@ public function parseArguments() $name = $value->getAttribute('name'); if ($definition) { - $value = $this->getPrimary(); + $value = $this->parsePrimary(); if (!$this->checkConstantExpression($value)) { throw new SyntaxError('A default value for an argument must be a constant (a boolean, a string, a number, a sequence, or a mapping).', $token->getLine(), $stream->getSourceContext()); @@ -664,8 +542,13 @@ public function parseArguments() return new Nodes($args); } + /** + * @deprecated since Twig 3.20, use "AbstractTokenParser::parseAssignmentExpression()" instead + */ public function parseAssignmentExpression() { + trigger_deprecation('twig/twig', '3.20', 'The "%s()" method is deprecated, use "AbstractTokenParser::parseAssignmentExpression()" instead.', __METHOD__); + $stream = $this->parser->getStream(); $targets = []; while (true) { @@ -686,8 +569,13 @@ public function parseAssignmentExpression() return new Nodes($targets); } + /** + * @deprecated since Twig 3.20 + */ public function parseMultitargetExpression() { + trigger_deprecation('twig/twig', '3.20', 'The "%s()" method is deprecated.', __METHOD__); + $targets = []; while (true) { $targets[] = $this->parseExpression(); @@ -699,131 +587,19 @@ public function parseMultitargetExpression() return new Nodes($targets); } - private function parseNotTestExpression(Node $node): NotUnary + public function getTest(int $line): TwigTest { - return new NotUnary($this->parseTestExpression($node), $this->parser->getCurrentToken()->getLine()); + return $this->parser->getTest($line); } - private function parseTestExpression(Node $node): TestExpression + public function getFunction(string $name, int $line): TwigFunction { - $stream = $this->parser->getStream(); - $test = $this->getTest($node->getTemplateLine()); - - $arguments = null; - if ($stream->test(Token::PUNCTUATION_TYPE, '(')) { - $arguments = $this->parseNamedArguments(); - } elseif ($test->hasOneMandatoryArgument()) { - $arguments = new Nodes([0 => $this->getPrimary()]); - } - - if ('defined' === $test->getName() && $node instanceof ContextVariable && null !== $alias = $this->parser->getImportedSymbol('function', $node->getAttribute('name'))) { - $node = new MacroReferenceExpression($alias['node']->getNode('var'), $alias['name'], new ArrayExpression([], $node->getTemplateLine()), $node->getTemplateLine()); - } - - $ready = $test instanceof TwigTest; - if (!isset($this->readyNodes[$class = $test->getNodeClass()])) { - $this->readyNodes[$class] = (bool) (new \ReflectionClass($class))->getConstructor()->getAttributes(FirstClassTwigCallableReady::class); - } - - if (!$ready = $this->readyNodes[$class]) { - trigger_deprecation('twig/twig', '3.12', 'Twig node "%s" is not marked as ready for passing a "TwigTest" in the constructor instead of its name; please update your code and then add #[FirstClassTwigCallableReady] attribute to the constructor.', $class); - } - - return new $class($node, $ready ? $test : $test->getName(), $arguments, $this->parser->getCurrentToken()->getLine()); + return $this->parser->getFunction($name, $line); } - private function getTest(int $line): TwigTest + public function getFilter(string $name, int $line): TwigFilter { - $stream = $this->parser->getStream(); - $name = $stream->expect(Token::NAME_TYPE)->getValue(); - - if ($stream->test(Token::NAME_TYPE)) { - // try 2-words tests - $name = $name.' '.$this->parser->getCurrentToken()->getValue(); - - if ($test = $this->env->getTest($name)) { - $stream->next(); - } - } else { - $test = $this->env->getTest($name); - } - - if (!$test) { - if ($this->parser->shouldIgnoreUnknownTwigCallables()) { - return new TwigTest($name, fn () => ''); - } - $e = new SyntaxError(\sprintf('Unknown "%s" test.', $name), $line, $stream->getSourceContext()); - $e->addSuggestions($name, array_keys($this->env->getTests())); - - throw $e; - } - - if ($test->isDeprecated()) { - $stream = $this->parser->getStream(); - $src = $stream->getSourceContext(); - $test->triggerDeprecation($src->getPath() ?: $src->getName(), $stream->getCurrent()->getLine()); - } - - return $test; - } - - private function getFunction(string $name, int $line): TwigFunction - { - try { - $function = $this->env->getFunction($name); - } catch (SyntaxError $e) { - if (!$this->parser->shouldIgnoreUnknownTwigCallables()) { - throw $e; - } - - $function = null; - } - - if (!$function) { - if ($this->parser->shouldIgnoreUnknownTwigCallables()) { - return new TwigFunction($name, fn () => ''); - } - $e = new SyntaxError(\sprintf('Unknown "%s" function.', $name), $line, $this->parser->getStream()->getSourceContext()); - $e->addSuggestions($name, array_keys($this->env->getFunctions())); - - throw $e; - } - - if ($function->isDeprecated()) { - $src = $this->parser->getStream()->getSourceContext(); - $function->triggerDeprecation($src->getPath() ?: $src->getName(), $line); - } - - return $function; - } - - private function getFilter(string $name, int $line): TwigFilter - { - try { - $filter = $this->env->getFilter($name); - } catch (SyntaxError $e) { - if (!$this->parser->shouldIgnoreUnknownTwigCallables()) { - throw $e; - } - - $filter = null; - } - if (!$filter) { - if ($this->parser->shouldIgnoreUnknownTwigCallables()) { - return new TwigFilter($name, fn () => ''); - } - $e = new SyntaxError(\sprintf('Unknown "%s" filter.', $name), $line, $this->parser->getStream()->getSourceContext()); - $e->addSuggestions($name, array_keys($this->env->getFilters())); - - throw $e; - } - - if ($filter->isDeprecated()) { - $src = $this->parser->getStream()->getSourceContext(); - $filter->triggerDeprecation($src->getPath() ?: $src->getName(), $line); - } - - return $filter; + return $this->parser->getFilter($name, $line); } // checks that the node only contains "constant" elements @@ -853,10 +629,13 @@ private function setDeprecationCheck(bool $deprecationCheck): bool return $current; } - private function createArguments(int $line): ArrayExpression + /** + * @internal + */ + public function parseCallableArguments(int $line, bool $parseOpenParenthesis = true): ArrayExpression { $arguments = new ArrayExpression([], $line); - foreach ($this->parseNamedArguments() as $k => $n) { + foreach ($this->parseNamedArguments($parseOpenParenthesis) as $k => $n) { $arguments->addElement($n, new LocalVariable($k, $line)); } @@ -873,11 +652,13 @@ public function parseOnlyArguments() return $this->parseNamedArguments(); } - public function parseNamedArguments(): Nodes + public function parseNamedArguments(bool $parseOpenParenthesis = true): Nodes { $args = []; $stream = $this->parser->getStream(); - $stream->expect(Token::PUNCTUATION_TYPE, '(', 'A list of arguments must begin with an opening parenthesis'); + if ($parseOpenParenthesis) { + $stream->expect(Token::OPERATOR_TYPE, '(', 'A list of arguments must begin with an opening parenthesis'); + } $hasSpread = false; while (!$stream->test(Token::PUNCTUATION_TYPE, ')')) { if ($args) { @@ -917,87 +698,4 @@ public function parseNamedArguments(): Nodes return new Nodes($args); } - - private function parseSubscriptExpressionDot(Node $node): AbstractExpression - { - $stream = $this->parser->getStream(); - $token = $stream->getCurrent(); - $lineno = $token->getLine(); - $arguments = new ArrayExpression([], $lineno); - $type = Template::ANY_CALL; - - if ($stream->nextIf(Token::PUNCTUATION_TYPE, '(')) { - $attribute = $this->parseExpression(); - $stream->expect(Token::PUNCTUATION_TYPE, ')'); - } else { - $token = $stream->next(); - if ( - $token->test(Token::NAME_TYPE) - || $token->test(Token::NUMBER_TYPE) - || ($token->test(Token::OPERATOR_TYPE) && preg_match(Lexer::REGEX_NAME, $token->getValue())) - ) { - $attribute = new ConstantExpression($token->getValue(), $token->getLine()); - } else { - throw new SyntaxError(\sprintf('Expected name or number, got value "%s" of type %s.', $token->getValue(), $token->toEnglish()), $token->getLine(), $stream->getSourceContext()); - } - } - - if ($stream->test(Token::PUNCTUATION_TYPE, '(')) { - $type = Template::METHOD_CALL; - $arguments = $this->createArguments($token->getLine()); - } - - if ( - $node instanceof ContextVariable - && ( - null !== $this->parser->getImportedSymbol('template', $node->getAttribute('name')) - || '_self' === $node->getAttribute('name') && $attribute instanceof ConstantExpression - ) - ) { - return new MacroReferenceExpression(new TemplateVariable($node->getAttribute('name'), $node->getTemplateLine()), 'macro_'.$attribute->getAttribute('value'), $arguments, $node->getTemplateLine()); - } - - return new GetAttrExpression($node, $attribute, $arguments, $type, $lineno); - } - - private function parseSubscriptExpressionArray(Node $node): AbstractExpression - { - $stream = $this->parser->getStream(); - $token = $stream->getCurrent(); - $lineno = $token->getLine(); - $arguments = new ArrayExpression([], $lineno); - - // slice? - $slice = false; - if ($stream->test(Token::PUNCTUATION_TYPE, ':')) { - $slice = true; - $attribute = new ConstantExpression(0, $token->getLine()); - } else { - $attribute = $this->parseExpression(); - } - - if ($stream->nextIf(Token::PUNCTUATION_TYPE, ':')) { - $slice = true; - } - - if ($slice) { - if ($stream->test(Token::PUNCTUATION_TYPE, ']')) { - $length = new ConstantExpression(null, $token->getLine()); - } else { - $length = $this->parseExpression(); - } - - $filter = $this->getFilter('slice', $token->getLine()); - $arguments = new Nodes([$attribute, $length]); - $filter = new ($filter->getNodeClass())($node, $filter, $arguments, $token->getLine()); - - $stream->expect(Token::PUNCTUATION_TYPE, ']'); - - return $filter; - } - - $stream->expect(Token::PUNCTUATION_TYPE, ']'); - - return new GetAttrExpression($node, $attribute, $arguments, Template::ARRAY_CALL, $lineno); - } } diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index 53e04271074..074a965f749 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -34,15 +34,19 @@ use Twig\Node\Node; use Twig\Operator\Binary\AddBinaryOperator; use Twig\Operator\Binary\AndBinaryOperator; +use Twig\Operator\Binary\ArrowBinaryOperator; use Twig\Operator\Binary\BitwiseAndBinaryOperator; use Twig\Operator\Binary\BitwiseOrBinaryOperator; use Twig\Operator\Binary\BitwiseXorBinaryOperator; use Twig\Operator\Binary\ConcatBinaryOperator; use Twig\Operator\Binary\DivBinaryOperator; +use Twig\Operator\Binary\DotBinaryOperator; use Twig\Operator\Binary\ElvisBinaryOperator; use Twig\Operator\Binary\EndsWithBinaryOperator; use Twig\Operator\Binary\EqualBinaryOperator; +use Twig\Operator\Binary\FilterBinaryOperator; use Twig\Operator\Binary\FloorDivBinaryOperator; +use Twig\Operator\Binary\FunctionBinaryOperator; use Twig\Operator\Binary\GreaterBinaryOperator; use Twig\Operator\Binary\GreaterEqualBinaryOperator; use Twig\Operator\Binary\HasEveryBinaryOperator; @@ -62,11 +66,14 @@ use Twig\Operator\Binary\PowerBinaryOperator; use Twig\Operator\Binary\RangeBinaryOperator; use Twig\Operator\Binary\SpaceshipBinaryOperator; +use Twig\Operator\Binary\SquareBracketBinaryOperator; use Twig\Operator\Binary\StartsWithBinaryOperator; use Twig\Operator\Binary\SubBinaryOperator; use Twig\Operator\Binary\XorBinaryOperator; +use Twig\Operator\Ternary\ConditionalTernaryOperator; use Twig\Operator\Unary\NegUnaryOperator; use Twig\Operator\Unary\NotUnaryOperator; +use Twig\Operator\Unary\ParenthesisUnaryOperator; use Twig\Operator\Unary\PosUnaryOperator; use Twig\Parser; use Twig\Sandbox\SecurityNotAllowedMethodError; @@ -319,6 +326,7 @@ public function getOperators(): array new NotUnaryOperator(), new NegUnaryOperator(), new PosUnaryOperator(), + new ParenthesisUnaryOperator(), new ElvisBinaryOperator(), new NullCoalesceBinaryOperator(), @@ -353,6 +361,13 @@ public function getOperators(): array new IsBinaryOperator(), new IsNotBinaryOperator(), new PowerBinaryOperator(), + new FilterBinaryOperator(), + new DotBinaryOperator(), + new SquareBracketBinaryOperator(), + new FunctionBinaryOperator(), + new ArrowBinaryOperator(), + + new ConditionalTernaryOperator(), ]; } diff --git a/src/Extension/ExtensionInterface.php b/src/Extension/ExtensionInterface.php index 6d5e4b5fa7f..7eef100f904 100644 --- a/src/Extension/ExtensionInterface.php +++ b/src/Extension/ExtensionInterface.php @@ -63,7 +63,12 @@ public function getFunctions(); /** * Returns a list of operators to add to the existing list. * - * @return OperatorInterface[] + * @return OperatorInterface[]|array + * + * @psalm-return OperatorInterface[]|array{ + * array}>, + * array, associativity: ExpressionParser::OPERATOR_*}> + * } */ public function getOperators(); } diff --git a/src/ExtensionSet.php b/src/ExtensionSet.php index 7a924f7b141..ad6ee7d0713 100644 --- a/src/ExtensionSet.php +++ b/src/ExtensionSet.php @@ -16,12 +16,15 @@ use Twig\Extension\GlobalsInterface; use Twig\Extension\LastModifiedExtensionInterface; use Twig\Extension\StagingExtension; +use Twig\Node\Expression\AbstractExpression; use Twig\NodeVisitor\NodeVisitorInterface; use Twig\Operator\Binary\AbstractBinaryOperator; +use Twig\Operator\Binary\BinaryOperatorInterface; use Twig\Operator\OperatorAssociativity; use Twig\Operator\OperatorInterface; use Twig\Operator\Operators; use Twig\Operator\Unary\AbstractUnaryOperator; +use Twig\Operator\Unary\UnaryOperatorInterface; use Twig\TokenParser\TokenParserInterface; /** @@ -497,7 +500,7 @@ private function initExtension(ExtensionInterface $extension): void throw new \InvalidArgumentException(\sprintf('"%s::getOperators()" must return an array of 2 elements, got %d.', \get_class($extension), \count($operators))); } - trigger_deprecation('twig/twig', '3.19.0', \sprintf('Extension "%s" uses the old signature for "getOperators()", please update it to return an array of "OperatorInterface" objects.', \get_class($extension))); + trigger_deprecation('twig/twig', '3.20', \sprintf('Extension "%s" uses the old signature for "getOperators()", please update it to return an array of "OperatorInterface" objects.', \get_class($extension))); $ops = []; foreach ($operators[0] as $n => $op) { @@ -515,9 +518,9 @@ private function initExtension(ExtensionInterface $extension): void private function convertUnaryOperators(string $n, array $op): OperatorInterface { - trigger_deprecation('twig/twig', '3.19.0', \sprintf('Using a non-OperatorInterface object to define the "%s" unary operator is deprecated.', $n)); + trigger_deprecation('twig/twig', '3.20', \sprintf('Using a non-OperatorInterface object to define the "%s" unary operator is deprecated.', $n)); - return new class($op, $n) extends AbstractUnaryOperator { + return new class($op, $n) extends AbstractUnaryOperator implements UnaryOperatorInterface { public function __construct(private array $op, private string $operator) { } @@ -537,18 +540,18 @@ public function getPrecedenceChange(): ?OperatorPrecedenceChange return $this->op['precedence_change'] ?? null; } - public function getNodeClass(): ?string + protected function getNodeClass(): string { - return $this->op['class'] ?? null; + return $this->op['class'] ?? ''; } }; } private function convertBinaryOperators(string $n, array $op): OperatorInterface { - trigger_deprecation('twig/twig', '3.19.0', \sprintf('Using a non-OperatorInterface object to define the "%s" binary operator is deprecated.', $n)); + trigger_deprecation('twig/twig', '3.20', \sprintf('Using a non-OperatorInterface object to define the "%s" binary operator is deprecated.', $n)); - return new class($op, $n) extends AbstractBinaryOperator { + return new class($op, $n) extends AbstractBinaryOperator implements BinaryOperatorInterface { public function __construct(private array $op, private string $operator) { } @@ -568,9 +571,9 @@ public function getPrecedenceChange(): ?OperatorPrecedenceChange return $this->op['precedence_change'] ?? null; } - public function getNodeClass(): ?string + protected function getNodeClass(): string { - return $this->op['class'] ?? null; + return $this->op['class'] ?? ''; } public function getAssociativity(): OperatorAssociativity @@ -582,9 +585,13 @@ public function getAssociativity(): OperatorAssociativity }; } - public function getCallable(): ?callable + public function parse(ExpressionParser $parser, AbstractExpression $expr, Token $token): AbstractExpression { - return $this->op['callable'] ?? null; + if ($this->op['callable']) { + return $this->op['callable']($parser, $expr); + } + + return parent::parse($parser, $expr, $token); } }; } diff --git a/src/Lexer.php b/src/Lexer.php index 215da8e9a5c..84b29de32a7 100644 --- a/src/Lexer.php +++ b/src/Lexer.php @@ -36,6 +36,8 @@ class Lexer private $position; private $positions; private $currentVarBlockLine; + private array $openingBrackets = ['{', '(', '[']; + private array $closingBrackets = ['}', ')', ']']; public const STATE_DATA = 0; public const STATE_BLOCK = 1; @@ -337,14 +339,18 @@ private function lexExpression(): void $this->pushToken(Token::SPREAD_TYPE, '...'); $this->moveCursor('...'); } - // arrow function - elseif ('=' === $this->code[$this->cursor] && ($this->cursor + 1 < $this->end) && '>' === $this->code[$this->cursor + 1]) { - $this->pushToken(Token::ARROW_TYPE, '=>'); - $this->moveCursor('=>'); - } // operators elseif (preg_match($this->regexes['operator'], $this->code, $match, 0, $this->cursor)) { - $this->pushToken(Token::OPERATOR_TYPE, preg_replace('/\s+/', ' ', $match[0])); + $operator = preg_replace('/\s+/', ' ', $match[0]); + $type = Token::OPERATOR_TYPE; + // to be removed in 4.0 + if (str_contains(self::PUNCTUATION, $operator)) { + $type = Token::PUNCTUATION_TYPE; + } + if (in_array($operator, $this->openingBrackets)) { + $this->checkBrackets($operator); + } + $this->pushToken($type, $operator); $this->moveCursor($match[0]); } // names @@ -359,22 +365,7 @@ private function lexExpression(): void } // punctuation elseif (str_contains(self::PUNCTUATION, $this->code[$this->cursor])) { - // opening bracket - if (str_contains('([{', $this->code[$this->cursor])) { - $this->brackets[] = [$this->code[$this->cursor], $this->lineno]; - } - // closing bracket - elseif (str_contains(')]}', $this->code[$this->cursor])) { - if (!$this->brackets) { - throw new SyntaxError(\sprintf('Unexpected "%s".', $this->code[$this->cursor]), $this->lineno, $this->source); - } - - [$expect, $lineno] = array_pop($this->brackets); - if ($this->code[$this->cursor] != strtr($expect, '([{', ')]}')) { - throw new SyntaxError(\sprintf('Unclosed "%s".', $expect), $lineno, $this->source); - } - } - + $this->checkBrackets($this->code[$this->cursor]); $this->pushToken(Token::PUNCTUATION_TYPE, $this->code[$this->cursor]); ++$this->cursor; } @@ -589,4 +580,22 @@ private function popState(): void $this->state = array_pop($this->states); } + + private function checkBrackets(string $code): void + { + // opening bracket + if (in_array($code, $this->openingBrackets)) { + $this->brackets[] = [$code, $this->lineno]; + } elseif (in_array($code, $this->closingBrackets)) { + // closing bracket + if (!$this->brackets) { + throw new SyntaxError(\sprintf('Unexpected "%s".', $code), $this->lineno, $this->source); + } + + [$expect, $lineno] = array_pop($this->brackets); + if ($code !== str_replace($this->openingBrackets, $this->closingBrackets, $expect)) { + throw new SyntaxError(\sprintf('Unclosed "%s".', $expect), $lineno, $this->source); + } + } + } } diff --git a/src/Node/Expression/ArrowFunctionExpression.php b/src/Node/Expression/ArrowFunctionExpression.php index 2bae4edd75f..552b8fe9115 100644 --- a/src/Node/Expression/ArrowFunctionExpression.php +++ b/src/Node/Expression/ArrowFunctionExpression.php @@ -12,6 +12,9 @@ namespace Twig\Node\Expression; use Twig\Compiler; +use Twig\Error\SyntaxError; +use Twig\Node\Expression\Variable\AssignContextVariable; +use Twig\Node\Expression\Variable\ContextVariable; use Twig\Node\Node; /** @@ -23,6 +26,14 @@ class ArrowFunctionExpression extends AbstractExpression { public function __construct(AbstractExpression $expr, Node $names, $lineno) { + if (!$names instanceof ListExpression && !$names instanceof ContextVariable) { + throw new SyntaxError('The arrow function argument must be a list of variables or a single variable.', $names->getTemplateLine(), $names->getSourceContext()); + } + + if ($names instanceof ContextVariable) { + $names = new ListExpression([new AssignContextVariable($names->getAttribute('name'), $names->getTemplateLine())], $lineno); + } + parent::__construct(['expr' => $expr, 'names' => $names], [], $lineno); } @@ -31,19 +42,7 @@ public function compile(Compiler $compiler): void $compiler ->addDebugInfo($this) ->raw('function (') - ; - foreach ($this->getNode('names') as $i => $name) { - if ($i) { - $compiler->raw(', '); - } - - $compiler - ->raw('$__') - ->raw($name->getAttribute('name')) - ->raw('__') - ; - } - $compiler + ->subcompile($this->getNode('names')) ->raw(') use ($context, $macros) { ') ; foreach ($this->getNode('names') as $name) { diff --git a/src/Node/Expression/ListExpression.php b/src/Node/Expression/ListExpression.php new file mode 100644 index 00000000000..8a774a19581 --- /dev/null +++ b/src/Node/Expression/ListExpression.php @@ -0,0 +1,48 @@ + $items + */ + public function __construct(array $items, int $lineno) + { + foreach ($items as $item) { + if (!$item instanceof ContextVariable) { + throw new SyntaxError('All elements of a list expression must be variable names.'.get_class($item), $item->getTemplateLine(), $item->getSourceContext()); + } + } + + parent::__construct($items, [], $lineno); + } + + public function compile(Compiler $compiler): void + { + foreach ($this as $i => $name) { + if ($i) { + $compiler->raw(', '); + } + + $compiler + ->raw('$__') + ->raw($name->getAttribute('name')) + ->raw('__') + ; + } + } +} diff --git a/src/Operator/Binary/AbstractBinaryOperator.php b/src/Operator/Binary/AbstractBinaryOperator.php index 2eec2537f3c..64fc9033991 100644 --- a/src/Operator/Binary/AbstractBinaryOperator.php +++ b/src/Operator/Binary/AbstractBinaryOperator.php @@ -11,12 +11,22 @@ namespace Twig\Operator\Binary; +use Twig\ExpressionParser; +use Twig\Node\Expression\AbstractExpression; use Twig\Operator\AbstractOperator; use Twig\Operator\OperatorArity; use Twig\Operator\OperatorAssociativity; +use Twig\Token; -abstract class AbstractBinaryOperator extends AbstractOperator +abstract class AbstractBinaryOperator extends AbstractOperator implements BinaryOperatorInterface { + public function parse(ExpressionParser $parser, AbstractExpression $left, Token $token): AbstractExpression + { + $right = $parser->parseExpression(OperatorAssociativity::Left === $this->getAssociativity() ? $this->getPrecedence() + 1 : $this->getPrecedence()); + + return new ($this->getNodeClass())($left, $right, $token->getLine()); + } + public function getArity(): OperatorArity { return OperatorArity::Binary; @@ -27,8 +37,8 @@ public function getAssociativity(): OperatorAssociativity return OperatorAssociativity::Left; } - public function getCallable(): ?callable - { - return null; - } + /** + * @return class-string + */ + abstract protected function getNodeClass(): string; } diff --git a/src/Operator/Binary/AddBinaryOperator.php b/src/Operator/Binary/AddBinaryOperator.php index f840f2cffbf..7e708c38004 100644 --- a/src/Operator/Binary/AddBinaryOperator.php +++ b/src/Operator/Binary/AddBinaryOperator.php @@ -25,7 +25,7 @@ public function getPrecedence(): int return 30; } - public function getNodeClass(): ?string + protected function getNodeClass(): string { return AddBinary::class; } diff --git a/src/Operator/Binary/AndBinaryOperator.php b/src/Operator/Binary/AndBinaryOperator.php index 28a06721f5f..78a79d18324 100644 --- a/src/Operator/Binary/AndBinaryOperator.php +++ b/src/Operator/Binary/AndBinaryOperator.php @@ -25,7 +25,7 @@ public function getPrecedence(): int return 15; } - public function getNodeClass(): ?string + protected function getNodeClass(): string { return AndBinary::class; } diff --git a/src/Operator/Binary/ArrowBinaryOperator.php b/src/Operator/Binary/ArrowBinaryOperator.php new file mode 100644 index 00000000000..253b00fc78c --- /dev/null +++ b/src/Operator/Binary/ArrowBinaryOperator.php @@ -0,0 +1,49 @@ +parseExpression(), $expr, $token->getLine()); + } + + public function getOperator(): string + { + return '=>'; + } + + public function getPrecedence(): int + { + return 250; + } + + public function getArity(): OperatorArity + { + return OperatorArity::Binary; + } + + public function getAssociativity(): OperatorAssociativity + { + return OperatorAssociativity::Left; + } +} diff --git a/src/Operator/Binary/BinaryOperatorInterface.php b/src/Operator/Binary/BinaryOperatorInterface.php new file mode 100644 index 00000000000..5ff2c4b9a70 --- /dev/null +++ b/src/Operator/Binary/BinaryOperatorInterface.php @@ -0,0 +1,25 @@ +getStream(); + $token = $stream->getCurrent(); + $lineno = $token->getLine(); + $arguments = new ArrayExpression([], $lineno); + $type = Template::ANY_CALL; + + if ($stream->nextIf(Token::OPERATOR_TYPE, '(')) { + $attribute = $parser->parseExpression(); + $stream->expect(Token::PUNCTUATION_TYPE, ')'); + } else { + $token = $stream->next(); + if ( + $token->test(Token::NAME_TYPE) + || $token->test(Token::NUMBER_TYPE) + || ($token->test(Token::OPERATOR_TYPE) && preg_match(Lexer::REGEX_NAME, $token->getValue())) + ) { + $attribute = new ConstantExpression($token->getValue(), $token->getLine()); + } else { + throw new SyntaxError(\sprintf('Expected name or number, got value "%s" of type %s.', $token->getValue(), $token->toEnglish()), $token->getLine(), $stream->getSourceContext()); + } + } + + if ($stream->test(Token::OPERATOR_TYPE, '(')) { + $type = Template::METHOD_CALL; + $arguments = $parser->parseCallableArguments($token->getLine()); + } + + if ( + $expr instanceof NameExpression + && ( + null !== $parser->getImportedSymbol('template', $expr->getAttribute('name')) + || '_self' === $expr->getAttribute('name') && $attribute instanceof ConstantExpression + ) + ) { + return new MacroReferenceExpression(new TemplateVariable($expr->getAttribute('name'), $expr->getTemplateLine()), 'macro_'.$attribute->getAttribute('value'), $arguments, $expr->getTemplateLine()); + } + + return new GetAttrExpression($expr, $attribute, $arguments, $type, $lineno); + } + + public function getOperator(): string + { + return '.'; + } + + public function getPrecedence(): int + { + return 300; + } + + public function getArity(): OperatorArity + { + return OperatorArity::Binary; + } + + public function getAssociativity(): OperatorAssociativity + { + return OperatorAssociativity::Left; + } +} diff --git a/src/Operator/Binary/ElvisBinaryOperator.php b/src/Operator/Binary/ElvisBinaryOperator.php index c1dac1ec9ac..dacc53819a0 100644 --- a/src/Operator/Binary/ElvisBinaryOperator.php +++ b/src/Operator/Binary/ElvisBinaryOperator.php @@ -26,7 +26,7 @@ public function getAliases(): array return ['? :']; } - public function getNodeClass(): ?string + protected function getNodeClass(): string { return ElvisBinary::class; } diff --git a/src/Operator/Binary/EndsWithBinaryOperator.php b/src/Operator/Binary/EndsWithBinaryOperator.php index 430448b2f9a..1073d3cb15b 100644 --- a/src/Operator/Binary/EndsWithBinaryOperator.php +++ b/src/Operator/Binary/EndsWithBinaryOperator.php @@ -25,7 +25,7 @@ public function getPrecedence(): int return 20; } - public function getNodeClass(): ?string + protected function getNodeClass(): string { return EndsWithBinary::class; } diff --git a/src/Operator/Binary/EqualBinaryOperator.php b/src/Operator/Binary/EqualBinaryOperator.php index c1d46c6777b..7bcaeb4e1fb 100644 --- a/src/Operator/Binary/EqualBinaryOperator.php +++ b/src/Operator/Binary/EqualBinaryOperator.php @@ -25,7 +25,7 @@ public function getPrecedence(): int return 20; } - public function getNodeClass(): ?string + protected function getNodeClass(): string { return EqualBinary::class; } diff --git a/src/Operator/Binary/FilterBinaryOperator.php b/src/Operator/Binary/FilterBinaryOperator.php new file mode 100644 index 00000000000..9dc6333898f --- /dev/null +++ b/src/Operator/Binary/FilterBinaryOperator.php @@ -0,0 +1,73 @@ +getStream(); + $token = $stream->expect(Token::NAME_TYPE); + $line = $token->getLine(); + + if (!$stream->test(Token::OPERATOR_TYPE, '(')) { + $arguments = new EmptyNode(); + } else { + $arguments = $parser->parseNamedArguments(); + } + + $filter = $parser->getFilter($token->getValue(), $line); + + $ready = true; + if (!isset($this->readyNodes[$class = $filter->getNodeClass()])) { + $this->readyNodes[$class] = (bool) (new \ReflectionClass($class))->getConstructor()->getAttributes(FirstClassTwigCallableReady::class); + } + + if (!$ready = $this->readyNodes[$class]) { + trigger_deprecation('twig/twig', '3.12', 'Twig node "%s" is not marked as ready for passing a "TwigFilter" in the constructor instead of its name; please update your code and then add #[FirstClassTwigCallableReady] attribute to the constructor.', $class); + } + + return new $class($expr, $ready ? $filter : new ConstantExpression($filter->getName(), $line), $arguments, $line); + } + + public function getOperator(): string + { + return '|'; + } + + public function getPrecedence(): int + { + return 300; + } + + public function getArity(): OperatorArity + { + return OperatorArity::Binary; + } + + public function getAssociativity(): OperatorAssociativity + { + return OperatorAssociativity::Left; + } +} diff --git a/src/Operator/Binary/FloorDivBinaryOperator.php b/src/Operator/Binary/FloorDivBinaryOperator.php index 62d5e0a4e67..05ef8480902 100644 --- a/src/Operator/Binary/FloorDivBinaryOperator.php +++ b/src/Operator/Binary/FloorDivBinaryOperator.php @@ -25,7 +25,7 @@ public function getPrecedence(): int return 60; } - public function getNodeClass(): ?string + protected function getNodeClass(): string { return FloorDivBinary::class; } diff --git a/src/Operator/Binary/FunctionBinaryOperator.php b/src/Operator/Binary/FunctionBinaryOperator.php new file mode 100644 index 00000000000..740c1ce5948 --- /dev/null +++ b/src/Operator/Binary/FunctionBinaryOperator.php @@ -0,0 +1,84 @@ +getLine(); + if (!$expr instanceof NameExpression) { + throw new SyntaxError('Function name must be an identifier.', $line, $parser->getStream()->getSourceContext()); + } + + $name = $expr->getAttribute('name'); + + if (null !== $alias = $parser->getImportedSymbol('function', $name)) { + return new MacroReferenceExpression($alias['node']->getNode('var'), $alias['name'], $parser->parseCallableArguments($line, false), $line); + } + + $args = $parser->parseNamedArguments(false); + + $function = $parser->getFunction($name, $line); + + if ($function->getParserCallable()) { + $fakeNode = new EmptyNode($line); + $fakeNode->setSourceContext($parser->getStream()->getSourceContext()); + + return ($function->getParserCallable())($parser->getParser(), $fakeNode, $args, $line); + } + + if (!isset($this->readyNodes[$class = $function->getNodeClass()])) { + $this->readyNodes[$class] = (bool) (new \ReflectionClass($class))->getConstructor()->getAttributes(FirstClassTwigCallableReady::class); + } + + if (!$ready = $this->readyNodes[$class]) { + trigger_deprecation('twig/twig', '3.12', 'Twig node "%s" is not marked as ready for passing a "TwigFunction" in the constructor instead of its name; please update your code and then add #[FirstClassTwigCallableReady] attribute to the constructor.', $class); + } + + return new $class($ready ? $function : $function->getName(), $args, $line); + } + + public function getOperator(): string + { + return '('; + } + + public function getPrecedence(): int + { + return 300; + } + + public function getArity(): OperatorArity + { + return OperatorArity::Binary; + } + + public function getAssociativity(): OperatorAssociativity + { + return OperatorAssociativity::Left; + } +} diff --git a/src/Operator/Binary/GreaterBinaryOperator.php b/src/Operator/Binary/GreaterBinaryOperator.php index 9a25be4adae..b6ef2349572 100644 --- a/src/Operator/Binary/GreaterBinaryOperator.php +++ b/src/Operator/Binary/GreaterBinaryOperator.php @@ -25,7 +25,7 @@ public function getPrecedence(): int return 20; } - public function getNodeClass(): ?string + protected function getNodeClass(): string { return GreaterBinary::class; } diff --git a/src/Operator/Binary/GreaterEqualBinaryOperator.php b/src/Operator/Binary/GreaterEqualBinaryOperator.php index aa709b55c39..69a5ad203bb 100644 --- a/src/Operator/Binary/GreaterEqualBinaryOperator.php +++ b/src/Operator/Binary/GreaterEqualBinaryOperator.php @@ -15,7 +15,7 @@ class GreaterEqualBinaryOperator extends AbstractBinaryOperator { - public function getNodeClass(): ?string + protected function getNodeClass(): string { return GreaterEqualBinary::class; } diff --git a/src/Operator/Binary/HasEveryBinaryOperator.php b/src/Operator/Binary/HasEveryBinaryOperator.php index 98a74d32dbb..1312640aed0 100644 --- a/src/Operator/Binary/HasEveryBinaryOperator.php +++ b/src/Operator/Binary/HasEveryBinaryOperator.php @@ -25,7 +25,7 @@ public function getPrecedence(): int return 20; } - public function getNodeClass(): ?string + protected function getNodeClass(): string { return HasEveryBinary::class; } diff --git a/src/Operator/Binary/HasSomeBinaryOperator.php b/src/Operator/Binary/HasSomeBinaryOperator.php index 4aa4150d8be..13dd25c2964 100644 --- a/src/Operator/Binary/HasSomeBinaryOperator.php +++ b/src/Operator/Binary/HasSomeBinaryOperator.php @@ -25,7 +25,7 @@ public function getPrecedence(): int return 20; } - public function getNodeClass(): ?string + protected function getNodeClass(): string { return HasSomeBinary::class; } diff --git a/src/Operator/Binary/InBinaryOperator.php b/src/Operator/Binary/InBinaryOperator.php index 70058c86b1a..b103e147ac7 100644 --- a/src/Operator/Binary/InBinaryOperator.php +++ b/src/Operator/Binary/InBinaryOperator.php @@ -25,7 +25,7 @@ public function getPrecedence(): int return 20; } - public function getNodeClass(): ?string + protected function getNodeClass(): string { return InBinary::class; } diff --git a/src/Operator/Binary/IsBinaryOperator.php b/src/Operator/Binary/IsBinaryOperator.php index 160c6d07847..4236b769a44 100644 --- a/src/Operator/Binary/IsBinaryOperator.php +++ b/src/Operator/Binary/IsBinaryOperator.php @@ -11,20 +11,68 @@ namespace Twig\Operator\Binary; -class IsBinaryOperator extends AbstractBinaryOperator +use Twig\Attribute\FirstClassTwigCallableReady; +use Twig\ExpressionParser; +use Twig\Node\Expression\AbstractExpression; +use Twig\Node\Expression\ArrayExpression; +use Twig\Node\Expression\MacroReferenceExpression; +use Twig\Node\Expression\NameExpression; +use Twig\Node\Nodes; +use Twig\Operator\AbstractOperator; +use Twig\Operator\OperatorArity; +use Twig\Operator\OperatorAssociativity; +use Twig\Token; +use Twig\TwigTest; + +class IsBinaryOperator extends AbstractOperator implements BinaryOperatorInterface { - public function getPrecedence(): int + private $readyNodes = []; + + public function parse(ExpressionParser $parser, AbstractExpression $expr, Token $token): AbstractExpression { - return 100; + $stream = $parser->getStream(); + $test = $parser->getTest($token->getLine()); + + $arguments = null; + if ($stream->test(Token::OPERATOR_TYPE, '(')) { + $arguments = $parser->parseNamedArguments(); + } elseif ($test->hasOneMandatoryArgument()) { + $arguments = new Nodes([0 => $parser->parseExpression($this->getPrecedence())]); + } + + if ('defined' === $test->getName() && $expr instanceof NameExpression && null !== $alias = $parser->getImportedSymbol('function', $expr->getAttribute('name'))) { + $expr = new MacroReferenceExpression($alias['node']->getNode('var'), $alias['name'], new ArrayExpression([], $expr->getTemplateLine()), $expr->getTemplateLine()); + } + + $ready = $test instanceof TwigTest; + if (!isset($this->readyNodes[$class = $test->getNodeClass()])) { + $this->readyNodes[$class] = (bool) (new \ReflectionClass($class))->getConstructor()->getAttributes(FirstClassTwigCallableReady::class); + } + + if (!$ready = $this->readyNodes[$class]) { + trigger_deprecation('twig/twig', '3.12', 'Twig node "%s" is not marked as ready for passing a "TwigTest" in the constructor instead of its name; please update your code and then add #[FirstClassTwigCallableReady] attribute to the constructor.', $class); + } + + return new $class($expr, $ready ? $test : $test->getName(), $arguments, $stream->getCurrent()->getLine()); } - public function getNodeClass(): ?string + public function getPrecedence(): int { - return null; + return 100; } public function getOperator(): string { return 'is'; } + + public function getArity(): OperatorArity + { + return OperatorArity::Binary; + } + + public function getAssociativity(): OperatorAssociativity + { + return OperatorAssociativity::Left; + } } diff --git a/src/Operator/Binary/IsNotBinaryOperator.php b/src/Operator/Binary/IsNotBinaryOperator.php index 4e1c8e7a58d..2455f738714 100644 --- a/src/Operator/Binary/IsNotBinaryOperator.php +++ b/src/Operator/Binary/IsNotBinaryOperator.php @@ -11,16 +11,16 @@ namespace Twig\Operator\Binary; -class IsNotBinaryOperator extends AbstractBinaryOperator -{ - public function getPrecedence(): int - { - return 100; - } +use Twig\ExpressionParser; +use Twig\Node\Expression\AbstractExpression; +use Twig\Node\Expression\Unary\NotUnary; +use Twig\Token; - public function getNodeClass(): ?string +class IsNotBinaryOperator extends IsBinaryOperator +{ + public function parse(ExpressionParser $parser, AbstractExpression $expr, Token $token): AbstractExpression { - return null; + return new NotUnary(parent::parse($parser, $expr, $token), $token->getLine()); } public function getOperator(): string diff --git a/src/Operator/Binary/LessBinaryOperator.php b/src/Operator/Binary/LessBinaryOperator.php index 56961dc94e9..f6e13095694 100644 --- a/src/Operator/Binary/LessBinaryOperator.php +++ b/src/Operator/Binary/LessBinaryOperator.php @@ -25,7 +25,7 @@ public function getPrecedence(): int return 20; } - public function getNodeClass(): ?string + protected function getNodeClass(): string { return LessBinary::class; } diff --git a/src/Operator/Binary/LessEqualBinaryOperator.php b/src/Operator/Binary/LessEqualBinaryOperator.php index 374449356ae..0eb43d6ff2f 100644 --- a/src/Operator/Binary/LessEqualBinaryOperator.php +++ b/src/Operator/Binary/LessEqualBinaryOperator.php @@ -25,7 +25,7 @@ public function getPrecedence(): int return 20; } - public function getNodeClass(): ?string + protected function getNodeClass(): string { return LessEqualBinary::class; } diff --git a/src/Operator/Binary/MatchesBinaryOperator.php b/src/Operator/Binary/MatchesBinaryOperator.php index 12b04ceafe7..2e7828f5ec0 100644 --- a/src/Operator/Binary/MatchesBinaryOperator.php +++ b/src/Operator/Binary/MatchesBinaryOperator.php @@ -25,7 +25,7 @@ public function getPrecedence(): int return 20; } - public function getNodeClass(): ?string + protected function getNodeClass(): string { return MatchesBinary::class; } diff --git a/src/Operator/Binary/ModBinaryOperator.php b/src/Operator/Binary/ModBinaryOperator.php index 42efbed088c..9aaa239a9b0 100644 --- a/src/Operator/Binary/ModBinaryOperator.php +++ b/src/Operator/Binary/ModBinaryOperator.php @@ -25,7 +25,7 @@ public function getPrecedence(): int return 60; } - public function getNodeClass(): ?string + protected function getNodeClass(): string { return ModBinary::class; } diff --git a/src/Operator/Binary/MulBinaryOperator.php b/src/Operator/Binary/MulBinaryOperator.php index 19e784ee211..d9077b662f0 100644 --- a/src/Operator/Binary/MulBinaryOperator.php +++ b/src/Operator/Binary/MulBinaryOperator.php @@ -25,7 +25,7 @@ public function getPrecedence(): int return 60; } - public function getNodeClass(): ?string + protected function getNodeClass(): string { return MulBinary::class; } diff --git a/src/Operator/Binary/NotEqualBinaryOperator.php b/src/Operator/Binary/NotEqualBinaryOperator.php index d3b474bcf69..1636ca2adc5 100644 --- a/src/Operator/Binary/NotEqualBinaryOperator.php +++ b/src/Operator/Binary/NotEqualBinaryOperator.php @@ -25,7 +25,7 @@ public function getPrecedence(): int return 20; } - public function getNodeClass(): ?string + protected function getNodeClass(): string { return NotEqualBinary::class; } diff --git a/src/Operator/Binary/NotInBinaryOperator.php b/src/Operator/Binary/NotInBinaryOperator.php index e8d24469b4e..6e35b77b819 100644 --- a/src/Operator/Binary/NotInBinaryOperator.php +++ b/src/Operator/Binary/NotInBinaryOperator.php @@ -25,7 +25,7 @@ public function getPrecedence(): int return 20; } - public function getNodeClass(): ?string + protected function getNodeClass(): string { return NotInBinary::class; } diff --git a/src/Operator/Binary/NullCoalesceBinaryOperator.php b/src/Operator/Binary/NullCoalesceBinaryOperator.php index c8603d7879b..f89c0d3862f 100644 --- a/src/Operator/Binary/NullCoalesceBinaryOperator.php +++ b/src/Operator/Binary/NullCoalesceBinaryOperator.php @@ -32,7 +32,7 @@ public function getPrecedenceChange(): ?OperatorPrecedenceChange return new OperatorPrecedenceChange('twig/twig', '3.15', 5); } - public function getNodeClass(): ?string + protected function getNodeClass(): string { return NullCoalesceBinary::class; } diff --git a/src/Operator/Binary/OrBinaryOperator.php b/src/Operator/Binary/OrBinaryOperator.php index 2a4565b1264..59fcd3067c2 100644 --- a/src/Operator/Binary/OrBinaryOperator.php +++ b/src/Operator/Binary/OrBinaryOperator.php @@ -20,7 +20,7 @@ public function getOperator(): string return 'or'; } - public function getNodeClass(): ?string + protected function getNodeClass(): string { return OrBinary::class; } diff --git a/src/Operator/Binary/PowerBinaryOperator.php b/src/Operator/Binary/PowerBinaryOperator.php index be26723b75c..98797b75bc3 100644 --- a/src/Operator/Binary/PowerBinaryOperator.php +++ b/src/Operator/Binary/PowerBinaryOperator.php @@ -26,7 +26,7 @@ public function getPrecedence(): int return 200; } - public function getNodeClass(): ?string + protected function getNodeClass(): string { return PowerBinary::class; } diff --git a/src/Operator/Binary/RangeBinaryOperator.php b/src/Operator/Binary/RangeBinaryOperator.php index 63082263135..84c77d60c5c 100644 --- a/src/Operator/Binary/RangeBinaryOperator.php +++ b/src/Operator/Binary/RangeBinaryOperator.php @@ -25,7 +25,7 @@ public function getPrecedence(): int return 25; } - public function getNodeClass(): ?string + protected function getNodeClass(): string { return RangeBinary::class; } diff --git a/src/Operator/Binary/SpaceshipBinaryOperator.php b/src/Operator/Binary/SpaceshipBinaryOperator.php index eead28ff921..ef67c9e3f09 100644 --- a/src/Operator/Binary/SpaceshipBinaryOperator.php +++ b/src/Operator/Binary/SpaceshipBinaryOperator.php @@ -25,7 +25,7 @@ public function getPrecedence(): int return 20; } - public function getNodeClass(): ?string + protected function getNodeClass(): string { return SpaceshipBinary::class; } diff --git a/src/Operator/Binary/SquareBracketBinaryOperator.php b/src/Operator/Binary/SquareBracketBinaryOperator.php new file mode 100644 index 00000000000..c2e8b500652 --- /dev/null +++ b/src/Operator/Binary/SquareBracketBinaryOperator.php @@ -0,0 +1,87 @@ +getStream(); + $lineno = $token->getLine(); + $arguments = new ArrayExpression([], $lineno); + + // slice? + $slice = false; + if ($stream->test(Token::PUNCTUATION_TYPE, ':')) { + $slice = true; + $attribute = new ConstantExpression(0, $token->getLine()); + } else { + $attribute = $parser->parseExpression(); + } + + if ($stream->nextIf(Token::PUNCTUATION_TYPE, ':')) { + $slice = true; + } + + if ($slice) { + if ($stream->test(Token::PUNCTUATION_TYPE, ']')) { + $length = new ConstantExpression(null, $token->getLine()); + } else { + $length = $parser->parseExpression(); + } + + $filter = $parser->getFilter('slice', $token->getLine()); + $arguments = new Nodes([$attribute, $length]); + $filter = new ($filter->getNodeClass())($expr, $filter, $arguments, $token->getLine()); + + $stream->expect(Token::PUNCTUATION_TYPE, ']'); + + return $filter; + } + + $stream->expect(Token::PUNCTUATION_TYPE, ']'); + + return new GetAttrExpression($expr, $attribute, $arguments, Template::ARRAY_CALL, $lineno); + } + + public function getOperator(): string + { + return '['; + } + + public function getPrecedence(): int + { + return 300; + } + + public function getArity(): OperatorArity + { + return OperatorArity::Binary; + } + + public function getAssociativity(): OperatorAssociativity + { + return OperatorAssociativity::Left; + } +} diff --git a/src/Operator/Binary/StartsWithBinaryOperator.php b/src/Operator/Binary/StartsWithBinaryOperator.php index e28953b74a4..4d543454d0a 100644 --- a/src/Operator/Binary/StartsWithBinaryOperator.php +++ b/src/Operator/Binary/StartsWithBinaryOperator.php @@ -25,7 +25,7 @@ public function getPrecedence(): int return 20; } - public function getNodeClass(): ?string + protected function getNodeClass(): string { return StartsWithBinary::class; } diff --git a/src/Operator/Binary/SubBinaryOperator.php b/src/Operator/Binary/SubBinaryOperator.php index 4fb413c5de6..c36c90707f8 100644 --- a/src/Operator/Binary/SubBinaryOperator.php +++ b/src/Operator/Binary/SubBinaryOperator.php @@ -25,7 +25,7 @@ public function getPrecedence(): int return 30; } - public function getNodeClass(): ?string + protected function getNodeClass(): string { return SubBinary::class; } diff --git a/src/Operator/Binary/XorBinaryOperator.php b/src/Operator/Binary/XorBinaryOperator.php index 1ea03413262..72a3b96429f 100644 --- a/src/Operator/Binary/XorBinaryOperator.php +++ b/src/Operator/Binary/XorBinaryOperator.php @@ -25,7 +25,7 @@ public function getPrecedence(): int return 12; } - public function getNodeClass(): ?string + protected function getNodeClass(): string { return XorBinary::class; } diff --git a/src/Operator/OperatorInterface.php b/src/Operator/OperatorInterface.php index f7fcfb735e0..8512bdeade6 100644 --- a/src/Operator/OperatorInterface.php +++ b/src/Operator/OperatorInterface.php @@ -11,17 +11,13 @@ namespace Twig\Operator; -use Twig\Node\Expression\AbstractExpression; use Twig\OperatorPrecedenceChange; interface OperatorInterface { - public function getOperator(): string; + public function __toString(): string; - /** - * @return class-string - */ - public function getNodeClass(): ?string; + public function getOperator(): string; public function getArity(): OperatorArity; diff --git a/src/Operator/Operators.php b/src/Operator/Operators.php index 19b92444224..65b7934b658 100644 --- a/src/Operator/Operators.php +++ b/src/Operator/Operators.php @@ -11,15 +11,33 @@ namespace Twig\Operator; -use Twig\Operator\Binary\AbstractBinaryOperator; -use Twig\Operator\Unary\AbstractUnaryOperator; +use Twig\Operator\Binary\BinaryOperatorInterface; +use Twig\Operator\Ternary\TernaryOperatorInterface; +use Twig\Operator\Unary\UnaryOperatorInterface; -class Operators implements \IteratorAggregate +/** + * @template-implements \IteratorAggregate + */ +final class Operators implements \IteratorAggregate { + /** + * @var array, array> + */ private array $operators = []; + + /** + * @var array, array> + */ private array $aliases = []; + + /** + * @var \WeakMap>|null + */ private ?\WeakMap $precedenceChanges = null; + /** + * @param array $operators + */ public function __construct( array $operators = [], ) { @@ -27,7 +45,7 @@ public function __construct( } /** - * @param array $operators + * @param array $operators * * @return $this */ @@ -44,16 +62,21 @@ public function add(array $operators): self return $this; } - public function getUnary(string $name): ?AbstractUnaryOperator + public function getUnary(string $name): ?UnaryOperatorInterface { return $this->operators[OperatorArity::Unary->value][$name] ?? ($this->aliases[OperatorArity::Unary->value][$name] ?? null); } - public function getBinary(string $name): ?AbstractBinaryOperator + public function getBinary(string $name): ?BinaryOperatorInterface { return $this->operators[OperatorArity::Binary->value][$name] ?? ($this->aliases[OperatorArity::Binary->value][$name] ?? null); } + public function getTernary(string $name): ?TernaryOperatorInterface + { + return $this->operators[OperatorArity::Ternary->value][$name] ?? ($this->aliases[OperatorArity::Ternary->value][$name] ?? null); + } + public function getIterator(): \Traversable { foreach ($this->operators as $operators) { @@ -65,7 +88,7 @@ public function getIterator(): \Traversable /** * @internal * - * @return \WeakMap> + * @return \WeakMap> */ public function getPrecedenceChanges(): \WeakMap { diff --git a/src/Operator/Ternary/AbstractTernaryOperator.php b/src/Operator/Ternary/AbstractTernaryOperator.php index eceaa79a1e0..3a88247ff30 100644 --- a/src/Operator/Ternary/AbstractTernaryOperator.php +++ b/src/Operator/Ternary/AbstractTernaryOperator.php @@ -13,11 +13,17 @@ use Twig\Operator\AbstractOperator; use Twig\Operator\OperatorArity; +use Twig\Operator\OperatorAssociativity; -abstract class AbstractTernaryOperator extends AbstractOperator +abstract class AbstractTernaryOperator extends AbstractOperator implements TernaryOperatorInterface { public function getArity(): OperatorArity { return OperatorArity::Ternary; } + + public function getAssociativity(): OperatorAssociativity + { + return OperatorAssociativity::Left; + } } diff --git a/src/Operator/Ternary/ConditionalTernaryOperator.php b/src/Operator/Ternary/ConditionalTernaryOperator.php new file mode 100644 index 00000000000..7c20963db57 --- /dev/null +++ b/src/Operator/Ternary/ConditionalTernaryOperator.php @@ -0,0 +1,50 @@ +parseExpression($this->getPrecedence()); + if ($parser->getStream()->nextIf(Token::PUNCTUATION_TYPE, $this->getElseOperator())) { + // Ternary operator (expr ? expr2 : expr3) + $else = $parser->parseExpression($this->getPrecedence()); + } else { + // Ternary without else (expr ? expr2) + $else = new ConstantExpression('', $token->getLine()); + } + + return new ConditionalTernary($left, $then, $else, $token->getLine()); + } + + public function getOperator(): string + { + return '?'; + } + + public function getPrecedence(): int + { + return 0; + } + + private function getElseOperator(): string + { + return ':'; + } +} diff --git a/src/Operator/Ternary/TernaryOperatorInterface.php b/src/Operator/Ternary/TernaryOperatorInterface.php new file mode 100644 index 00000000000..86628203e03 --- /dev/null +++ b/src/Operator/Ternary/TernaryOperatorInterface.php @@ -0,0 +1,25 @@ +getNodeClass())($parser->parseExpression($this->getPrecedence()), $token->getLine()); + } + public function getArity(): OperatorArity { return OperatorArity::Unary; } + + /** + * @return class-string + */ + abstract protected function getNodeClass(): string; } diff --git a/src/Operator/Unary/NegUnaryOperator.php b/src/Operator/Unary/NegUnaryOperator.php index eb72d1b6f82..de01a3b5176 100644 --- a/src/Operator/Unary/NegUnaryOperator.php +++ b/src/Operator/Unary/NegUnaryOperator.php @@ -25,7 +25,7 @@ public function getPrecedence(): int return 500; } - public function getNodeClass(): ?string + protected function getNodeClass(): string { return NegUnary::class; } diff --git a/src/Operator/Unary/NotUnaryOperator.php b/src/Operator/Unary/NotUnaryOperator.php index b0bb4cc10dd..6ee1a0c32e8 100644 --- a/src/Operator/Unary/NotUnaryOperator.php +++ b/src/Operator/Unary/NotUnaryOperator.php @@ -31,7 +31,7 @@ public function getPrecedenceChange(): ?OperatorPrecedenceChange return new OperatorPrecedenceChange('twig/twig', '3.15', 70); } - public function getNodeClass(): ?string + protected function getNodeClass(): string { return NotUnary::class; } diff --git a/src/Operator/Unary/ParenthesisUnaryOperator.php b/src/Operator/Unary/ParenthesisUnaryOperator.php new file mode 100644 index 00000000000..8191cb82045 --- /dev/null +++ b/src/Operator/Unary/ParenthesisUnaryOperator.php @@ -0,0 +1,74 @@ +getStream(); + $expr = $parser->parseExpression($this->getPrecedence()); + + if ($stream->nextIf(Token::PUNCTUATION_TYPE, ')')) { + if (!$stream->test(Token::OPERATOR_TYPE, '=>')) { + return $expr->setExplicitParentheses(); + } + + return new ListExpression([$expr], $token->getLine()); + } + + // determine if we are parsing an arrow function arguments + if (!$stream->test(Token::PUNCTUATION_TYPE, ',')) { + $stream->expect(Token::PUNCTUATION_TYPE, ')', 'An opened parenthesis is not properly closed'); + } + + $names = [$expr]; + while (true) { + if ($stream->nextIf(Token::PUNCTUATION_TYPE, ')')) { + break; + } + $stream->expect(Token::PUNCTUATION_TYPE, ','); + $token = $stream->expect(Token::NAME_TYPE); + $names[] = new ContextVariable($token->getValue(), $token->getLine()); + } + + if (!$stream->test(Token::OPERATOR_TYPE, '=>')) { + throw new SyntaxError('A list of variables must be followed by an arrow.', $stream->getCurrent()->getLine(), $stream->getSourceContext()); + } + + return new ListExpression($names, $token->getLine()); + } + + public function getOperator(): string + { + return '('; + } + + public function getPrecedence(): int + { + return 0; + } + + public function getArity(): OperatorArity + { + return OperatorArity::Unary; + } +} diff --git a/src/Operator/Unary/PosUnaryOperator.php b/src/Operator/Unary/PosUnaryOperator.php index 255085182a9..88059530534 100644 --- a/src/Operator/Unary/PosUnaryOperator.php +++ b/src/Operator/Unary/PosUnaryOperator.php @@ -25,7 +25,7 @@ public function getPrecedence(): int return 500; } - public function getNodeClass(): ?string + protected function getNodeClass(): string { return PosUnary::class; } diff --git a/src/Operator/Unary/UnaryOperatorInterface.php b/src/Operator/Unary/UnaryOperatorInterface.php new file mode 100644 index 00000000000..71c599acf65 --- /dev/null +++ b/src/Operator/Unary/UnaryOperatorInterface.php @@ -0,0 +1,22 @@ +stream->getCurrent(); } + public function getFunction(string $name, int $line): TwigFunction + { + try { + $function = $this->env->getFunction($name); + } catch (SyntaxError $e) { + if (!$this->shouldIgnoreUnknownTwigCallables()) { + throw $e; + } + + $function = null; + } + + if (!$function) { + if ($this->shouldIgnoreUnknownTwigCallables()) { + return new TwigFunction($name, fn () => ''); + } + $e = new SyntaxError(\sprintf('Unknown "%s" function.', $name), $line, $this->stream->getSourceContext()); + $e->addSuggestions($name, array_keys($this->env->getFunctions())); + + throw $e; + } + + if ($function->isDeprecated()) { + $src = $this->stream->getSourceContext(); + $function->triggerDeprecation($src->getPath() ?: $src->getName(), $line); + } + + return $function; + } + + public function getFilter(string $name, int $line): TwigFilter + { + try { + $filter = $this->env->getFilter($name); + } catch (SyntaxError $e) { + if (!$this->shouldIgnoreUnknownTwigCallables()) { + throw $e; + } + + $filter = null; + } + if (!$filter) { + if ($this->shouldIgnoreUnknownTwigCallables()) { + return new TwigFilter($name, fn () => ''); + } + $e = new SyntaxError(\sprintf('Unknown "%s" filter.', $name), $line, $this->stream->getSourceContext()); + $e->addSuggestions($name, array_keys($this->env->getFilters())); + + throw $e; + } + + if ($filter->isDeprecated()) { + $src = $this->stream->getSourceContext(); + $filter->triggerDeprecation($src->getPath() ?: $src->getName(), $line); + } + + return $filter; + } + + public function getTest(int $line): TwigTest + { + $name = $this->stream->expect(Token::NAME_TYPE)->getValue(); + + if ($this->stream->test(Token::NAME_TYPE)) { + // try 2-words tests + $name = $name.' '.$this->getCurrentToken()->getValue(); + + if ($test = $this->env->getTest($name)) { + $this->stream->next(); + } + } else { + $test = $this->env->getTest($name); + } + + if (!$test) { + if ($this->shouldIgnoreUnknownTwigCallables()) { + return new TwigTest($name, fn () => ''); + } + $e = new SyntaxError(\sprintf('Unknown "%s" test.', $name), $line, $this->stream->getSourceContext()); + $e->addSuggestions($name, array_keys($this->env->getTests())); + + throw $e; + } + + if ($test->isDeprecated()) { + $src = $this->stream->getSourceContext(); + $test->triggerDeprecation($src->getPath() ?: $src->getName(), $this->stream->getCurrent()->getLine()); + } + + return $test; + } + private function filterBodyNodes(Node $node, bool $nested = false): ?Node { // check that the body does not contain non-empty output nodes diff --git a/src/Token.php b/src/Token.php index a4da548cbf2..6390472e026 100644 --- a/src/Token.php +++ b/src/Token.php @@ -30,6 +30,9 @@ final class Token public const PUNCTUATION_TYPE = 9; public const INTERPOLATION_START_TYPE = 10; public const INTERPOLATION_END_TYPE = 11; + /** + * @deprecated since Twig 3.20, "arrow" is now an operator + */ public const ARROW_TYPE = 12; public const SPREAD_TYPE = 13; @@ -38,6 +41,9 @@ public function __construct( private $value, private int $lineno, ) { + if (self::ARROW_TYPE === $type) { + trigger_deprecation('twig/twig', '3.20', 'The "%s" token type is deprecated, "arrow" is now an operator.', self::ARROW_TYPE); + } } public function __toString(): string @@ -63,7 +69,39 @@ public function test($type, $values = null): bool $type = self::NAME_TYPE; } - return ($this->type === $type) && ( + if (self::ARROW_TYPE === $type) { + trigger_deprecation('twig/twig', '3.20', 'The "%s" token type is deprecated, "arrow" is now an operator.', self::typeToEnglish(self::ARROW_TYPE)); + + return self::OPERATOR_TYPE === $this->type && '=>' === $this->value; + } + + $typeMatches = $this->type === $type; + if ($typeMatches && self::PUNCTUATION_TYPE === $type && \in_array($this->value, ['(', '[', '|', '.', '?', '?:']) && $values) { + foreach ((array) $values as $value) { + if (\in_array($value, ['(', '[', '|', '.', '?', '?:'])) { + trigger_deprecation('twig/twig', '3.20', 'The "%s" token is now an "%s" token instead of a "%s" one.', $this->value, self::typeToEnglish(self::OPERATOR_TYPE), $this->toEnglish()); + + break; + } + } + } + if (!$typeMatches) { + if (self::OPERATOR_TYPE === $type && self::PUNCTUATION_TYPE === $this->type) { + if ($values) { + foreach ((array) $values as $value) { + if (\in_array($value, ['(', '[', '|', '.', '?', '?:'])) { + $typeMatches = true; + + break; + } + } + } else { + $typeMatches = true; + } + } + } + + return $typeMatches && ( null === $values || (\is_array($values) && \in_array($this->value, $values)) || $this->value == $values diff --git a/src/TokenParser/AbstractTokenParser.php b/src/TokenParser/AbstractTokenParser.php index 720ea676283..30bef15a340 100644 --- a/src/TokenParser/AbstractTokenParser.php +++ b/src/TokenParser/AbstractTokenParser.php @@ -11,7 +11,11 @@ namespace Twig\TokenParser; +use Twig\Lexer; +use Twig\Node\Expression\Variable\AssignContextVariable; +use Twig\Node\Nodes; use Twig\Parser; +use Twig\Token; /** * Base class for all token parsers. @@ -29,4 +33,31 @@ public function setParser(Parser $parser): void { $this->parser = $parser; } + + /** + * Parses an assignment expression like "a, b". + * + * @return Nodes + */ + protected function parseAssignmentExpression(): Nodes + { + $stream = $this->parser->getStream(); + $targets = []; + while (true) { + $token = $stream->getCurrent(); + if ($stream->test(Token::OPERATOR_TYPE) && preg_match(Lexer::REGEX_NAME, $token->getValue())) { + // in this context, string operators are variable names + $stream->next(); + } else { + $stream->expect(Token::NAME_TYPE, null, 'Only variables can be assigned to'); + } + $targets[] = new AssignContextVariable($token->getValue(), $token->getLine()); + + if (!$stream->nextIf(Token::PUNCTUATION_TYPE, ',')) { + break; + } + } + + return new Nodes($targets); + } } diff --git a/src/TokenParser/ApplyTokenParser.php b/src/TokenParser/ApplyTokenParser.php index 0c95074828a..68ef7c17e6b 100644 --- a/src/TokenParser/ApplyTokenParser.php +++ b/src/TokenParser/ApplyTokenParser.php @@ -33,7 +33,15 @@ public function parse(Token $token): Node { $lineno = $token->getLine(); $ref = new LocalVariable(null, $lineno); - $filter = $this->parser->getExpressionParser()->parseFilterExpressionRaw($ref); + $filter = $ref; + $op = $this->parser->getEnvironment()->getOperators()->getBinary('|'); + while (true) { + $filter = $op->parse($this->parser->getExpressionParser(), $filter, $this->parser->getCurrentToken()); + if (!$this->parser->getStream()->test(Token::OPERATOR_TYPE, '|')) { + break; + } + $this->parser->getStream()->next(); + } $this->parser->getStream()->expect(Token::BLOCK_END_TYPE); $body = $this->parser->subparse([$this, 'decideApplyEnd'], true); diff --git a/src/TokenParser/ForTokenParser.php b/src/TokenParser/ForTokenParser.php index 3e08b22fa8a..b098737fa6f 100644 --- a/src/TokenParser/ForTokenParser.php +++ b/src/TokenParser/ForTokenParser.php @@ -35,7 +35,7 @@ public function parse(Token $token): Node { $lineno = $token->getLine(); $stream = $this->parser->getStream(); - $targets = $this->parser->getExpressionParser()->parseAssignmentExpression(); + $targets = $this->parseAssignmentExpression(); $stream->expect(Token::OPERATOR_TYPE, 'in'); $seq = $this->parser->getExpressionParser()->parseExpression(); diff --git a/src/TokenParser/MacroTokenParser.php b/src/TokenParser/MacroTokenParser.php index 33379be0319..1d857730011 100644 --- a/src/TokenParser/MacroTokenParser.php +++ b/src/TokenParser/MacroTokenParser.php @@ -73,7 +73,7 @@ private function parseDefinition(): ArrayExpression { $arguments = new ArrayExpression([], $this->parser->getCurrentToken()->getLine()); $stream = $this->parser->getStream(); - $stream->expect(Token::PUNCTUATION_TYPE, '(', 'A list of arguments must begin with an opening parenthesis'); + $stream->expect(Token::OPERATOR_TYPE, '(', 'A list of arguments must begin with an opening parenthesis'); while (!$stream->test(Token::PUNCTUATION_TYPE, ')')) { if (\count($arguments)) { $stream->expect(Token::PUNCTUATION_TYPE, ',', 'Arguments must be separated by a comma'); diff --git a/src/TokenParser/SetTokenParser.php b/src/TokenParser/SetTokenParser.php index bb43907bd24..c9ebceb0bf8 100644 --- a/src/TokenParser/SetTokenParser.php +++ b/src/TokenParser/SetTokenParser.php @@ -13,6 +13,7 @@ use Twig\Error\SyntaxError; use Twig\Node\Node; +use Twig\Node\Nodes; use Twig\Node\SetNode; use Twig\Token; @@ -34,11 +35,11 @@ public function parse(Token $token): Node { $lineno = $token->getLine(); $stream = $this->parser->getStream(); - $names = $this->parser->getExpressionParser()->parseAssignmentExpression(); + $names = $this->parseAssignmentExpression(); $capture = false; if ($stream->nextIf(Token::OPERATOR_TYPE, '=')) { - $values = $this->parser->getExpressionParser()->parseMultitargetExpression(); + $values = $this->parseMultitargetExpression(); $stream->expect(Token::BLOCK_END_TYPE); @@ -70,4 +71,17 @@ public function getTag(): string { return 'set'; } + + private function parseMultitargetExpression() + { + $targets = []; + while (true) { + $targets[] = $this->parser->getExpressionParser()->parseExpression(); + if (!$this->parser->getStream()->nextIf(Token::PUNCTUATION_TYPE, ',')) { + break; + } + } + + return new Nodes($targets); + } } diff --git a/src/TokenParser/TypesTokenParser.php b/src/TokenParser/TypesTokenParser.php index a7da0f5ecf4..2c7b77c024b 100644 --- a/src/TokenParser/TypesTokenParser.php +++ b/src/TokenParser/TypesTokenParser.php @@ -63,7 +63,7 @@ private function parseSimpleMappingExpression(TokenStream $stream): array if ($stream->nextIf(Token::OPERATOR_TYPE, '?:')) { $isOptional = true; } else { - $isOptional = null !== $stream->nextIf(Token::PUNCTUATION_TYPE, '?'); + $isOptional = null !== $stream->nextIf(Token::OPERATOR_TYPE, '?'); $stream->expect(Token::PUNCTUATION_TYPE, ':', 'A type name must be followed by a colon (:)'); } diff --git a/tests/Fixtures/filters/arrow_reserved_names.test b/tests/Fixtures/filters/arrow_reserved_names.test index 3e5d0722b1a..188373feee5 100644 --- a/tests/Fixtures/filters/arrow_reserved_names.test +++ b/tests/Fixtures/filters/arrow_reserved_names.test @@ -5,4 +5,4 @@ --DATA-- return [] --EXCEPTION-- -Twig\Error\SyntaxError: You cannot assign a value to "true" in "index.twig" at line 2. +Twig\Error\SyntaxError: The arrow function argument must be a list of variables or a single variable in "index.twig" at line 2. From 9334a7064abe162b6e52c07a334d84cd14979204 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sun, 26 Jan 2025 15:19:24 +0100 Subject: [PATCH 722/812] Add a script to update operator precedence documentation --- bin/generate_operators_precedence.php | 64 +++++++++++++++++++++++++++ doc/operators_precedence.rst | 56 +++++++++++++++++++++++ doc/templates.rst | 32 ++------------ 3 files changed, 123 insertions(+), 29 deletions(-) create mode 100644 bin/generate_operators_precedence.php create mode 100644 doc/operators_precedence.rst diff --git a/bin/generate_operators_precedence.php b/bin/generate_operators_precedence.php new file mode 100644 index 00000000000..97cdf016d09 --- /dev/null +++ b/bin/generate_operators_precedence.php @@ -0,0 +1,64 @@ +getPrecedenceChange() ? $a->getPrecedenceChange()->getNewPrecedence() : $a->getPrecedence(); + $bPrecedence = $b->getPrecedenceChange() ? $b->getPrecedenceChange()->getNewPrecedence() : $b->getPrecedence(); + return $bPrecedence - $aPrecedence; + }); + + $current = \PHP_INT_MAX; + foreach ($operators as $operator) { + $precedence = $operator->getPrecedenceChange() ? $operator->getPrecedenceChange()->getNewPrecedence() : $operator->getPrecedence(); + if ($precedence !== $current) { + $current = $precedence; + if ($withAssociativity) { + fwrite($output, \sprintf("\n%-11d %-11s %s", $precedence, $operator->getOperator(), OperatorAssociativity::Left === $operator->getAssociativity() ? 'Left' : 'Right')); + } else { + fwrite($output, \sprintf("\n%-11d %s", $precedence, $operator->getOperator())); + } + } else { + fwrite($output, "\n".str_repeat(' ', 12).$operator->getOperator()); + } + } + fwrite($output, "\n"); +} + +$output = fopen(dirname(__DIR__).'/doc/operators_precedence.rst', 'w'); + +$twig = new Environment(new ArrayLoader([])); +$unaryOperators = []; +$notUnaryOperators = []; +foreach ($twig->getOperators() as $operator) { + if ($operator->getArity()->value == OperatorArity::Unary->value) { + $unaryOperators[] = $operator; + } else { + $notUnaryOperators[] = $operator; + } +} + +fwrite($output, "Unary operators precedence:\n"); +printOperators($output, $unaryOperators); + +fwrite($output, "\nBinary and Ternary operators precedence:\n"); +printOperators($output, $notUnaryOperators, true); + +fclose($output); diff --git a/doc/operators_precedence.rst b/doc/operators_precedence.rst new file mode 100644 index 00000000000..6ce1b521bbd --- /dev/null +++ b/doc/operators_precedence.rst @@ -0,0 +1,56 @@ +Unary operators precedence: + +=========== =========== +Precedence Operator +=========== =========== + +500 - + + +70 not +0 ( + +Binary and Ternary operators precedence: + +=========== =========== ============= +Precedence Operator Associativity +=========== =========== ============= + +300 | Left + . + [ + ( +250 => Left +200 ** Right +100 is Left + is not +60 * Left + / + // + % +30 + Left + - +27 ~ Left +25 .. Left +20 == Left + != + <=> + < + > + >= + <= + not in + in + matches + starts with + ends with + has some + has every +18 b-and Left +17 b-xor Left +16 b-or Left +15 and Left +12 xor Left +10 or Left +5 ?: Right + ?? +0 ? Left diff --git a/doc/templates.rst b/doc/templates.rst index 7bf2d15f591..960093152b7 100644 --- a/doc/templates.rst +++ b/doc/templates.rst @@ -1033,35 +1033,9 @@ Understanding the precedence of these operators is crucial for writing correct and efficient Twig templates. The operator precedence rules are as follows, with the lowest-precedence -operators listed first: - -============================= =================================== ===================================================== -Operator Score of precedence Description -============================= =================================== ===================================================== -``?:`` 0 Ternary operator, conditional statement -``or`` 10 Logical OR operation between two boolean expressions -``xor`` 12 Logical XOR operation between two boolean expressions -``and`` 15 Logical AND operation between two boolean expressions -``b-or`` 16 Bitwise OR operation on integers -``b-xor`` 17 Bitwise XOR operation on integers -``b-and`` 18 Bitwise AND operation on integers -``==``, ``!=``, ``<=>``, 20 Comparison operators -``<``, ``>``, ``>=``, -``<=``, ``not in``, ``in``, -``matches``, ``starts with``, -``ends with``, ``has some``, -``has every`` -``..`` 25 Range of values -``+``, ``-`` 30 Addition and subtraction on numbers -``~`` 40 String concatenation -``not`` 50 Negates a statement -``*``, ``/``, ``//``, ``%`` 60 Arithmetic operations on numbers -``is``, ``is not`` 100 Tests -``**`` 200 Raises a number to the power of another -``??`` 300 Default value when a variable is null -``+``, ``-`` 500 Unary operations on numbers -``|``,``[]``,``.`` - Filters, sequence, mapping, and attribute access -============================= =================================== ===================================================== +operators listed first. + +.. include:: operators_precedence.rst Without using any parentheses, the operator precedence rules are used to determine how to convert the code to PHP: From f67078b5c44d0976a91c020491cf651ef28a8fae Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Mon, 3 Feb 2025 08:24:29 +0100 Subject: [PATCH 723/812] Move Operators to ExpressionParsers, deprecate ExpressionParser --- bin/generate_operators_precedence.php | 37 +- doc/operators_precedence.rst | 5 +- .../TokenParser/CacheTokenParser.php | 5 +- extra/cache-extra/composer.json | 2 +- src/Environment.php | 6 +- src/ExpressionParser.php | 428 ++---------------- .../AbstractExpressionParser.php} | 10 +- .../ExpressionParserInterface.php} | 12 +- src/ExpressionParser/ExpressionParserType.php | 33 ++ src/ExpressionParser/ExpressionParsers.php | 140 ++++++ src/ExpressionParser/Infix/ArgumentsTrait.php | 81 ++++ .../Infix/ArrowExpressionParser.php} | 28 +- .../Infix/BinaryOperatorExpressionParser.php | 73 +++ .../ConditionalTernaryExpressionParser.php} | 22 +- .../Infix/DotExpressionParser.php} | 32 +- .../Infix/FilterExpressionParser.php} | 32 +- .../Infix/FunctionExpressionParser.php} | 36 +- .../Infix/IsExpressionParser.php} | 32 +- .../Infix/IsNotExpressionParser.php} | 13 +- .../Infix/SquareBracketExpressionParser.php} | 28 +- .../InfixAssociativity.php} | 4 +- .../InfixExpressionParserInterface.php | 23 + src/ExpressionParser/PrecedenceChange.php | 42 ++ .../Prefix/GroupingExpressionParser.php} | 22 +- .../Prefix/LiteralExpressionParser.php | 243 ++++++++++ .../Prefix/UnaryOperatorExpressionParser.php | 64 +++ .../PrefixExpressionParserInterface.php} | 9 +- src/Extension/AbstractExtension.php | 5 + src/Extension/CoreExtension.php | 187 ++++---- src/Extension/ExtensionInterface.php | 13 +- src/ExtensionSet.php | 162 +++---- src/Lexer.php | 18 +- src/Node/Expression/ListExpression.php | 7 - .../Binary/AbstractBinaryOperator.php | 44 -- src/Operator/Binary/AddBinaryOperator.php | 32 -- src/Operator/Binary/AndBinaryOperator.php | 32 -- .../Binary/BinaryOperatorInterface.php | 25 - .../Binary/BitwiseAndBinaryOperator.php | 32 -- .../Binary/BitwiseOrBinaryOperator.php | 32 -- .../Binary/BitwiseXorBinaryOperator.php | 32 -- src/Operator/Binary/ConcatBinaryOperator.php | 38 -- src/Operator/Binary/DivBinaryOperator.php | 32 -- src/Operator/Binary/ElvisBinaryOperator.php | 43 -- .../Binary/EndsWithBinaryOperator.php | 32 -- src/Operator/Binary/EqualBinaryOperator.php | 32 -- .../Binary/FloorDivBinaryOperator.php | 32 -- src/Operator/Binary/GreaterBinaryOperator.php | 32 -- .../Binary/GreaterEqualBinaryOperator.php | 32 -- .../Binary/HasEveryBinaryOperator.php | 32 -- src/Operator/Binary/HasSomeBinaryOperator.php | 32 -- src/Operator/Binary/InBinaryOperator.php | 32 -- src/Operator/Binary/LessBinaryOperator.php | 32 -- .../Binary/LessEqualBinaryOperator.php | 32 -- src/Operator/Binary/MatchesBinaryOperator.php | 32 -- src/Operator/Binary/ModBinaryOperator.php | 32 -- src/Operator/Binary/MulBinaryOperator.php | 32 -- .../Binary/NotEqualBinaryOperator.php | 32 -- src/Operator/Binary/NotInBinaryOperator.php | 32 -- .../Binary/NullCoalesceBinaryOperator.php | 44 -- src/Operator/Binary/OrBinaryOperator.php | 32 -- src/Operator/Binary/PowerBinaryOperator.php | 38 -- src/Operator/Binary/RangeBinaryOperator.php | 32 -- .../Binary/SpaceshipBinaryOperator.php | 32 -- .../Binary/StartsWithBinaryOperator.php | 32 -- src/Operator/Binary/SubBinaryOperator.php | 32 -- src/Operator/Binary/XorBinaryOperator.php | 32 -- src/Operator/OperatorArity.php | 19 - src/Operator/Operators.php | 116 ----- .../Ternary/AbstractTernaryOperator.php | 29 -- .../Ternary/TernaryOperatorInterface.php | 25 - src/Operator/Unary/AbstractUnaryOperator.php | 36 -- src/Operator/Unary/NegUnaryOperator.php | 32 -- src/Operator/Unary/NotUnaryOperator.php | 38 -- src/Operator/Unary/PosUnaryOperator.php | 32 -- src/OperatorPrecedenceChange.php | 24 +- src/Parser.php | 81 +++- src/TokenParser/AbstractTokenParser.php | 2 - src/TokenParser/ApplyTokenParser.php | 5 +- src/TokenParser/AutoEscapeTokenParser.php | 2 +- src/TokenParser/BlockTokenParser.php | 2 +- src/TokenParser/DeprecatedTokenParser.php | 7 +- src/TokenParser/DoTokenParser.php | 2 +- src/TokenParser/EmbedTokenParser.php | 2 +- src/TokenParser/ExtendsTokenParser.php | 2 +- src/TokenParser/ForTokenParser.php | 2 +- src/TokenParser/FromTokenParser.php | 2 +- src/TokenParser/IfTokenParser.php | 4 +- src/TokenParser/ImportTokenParser.php | 2 +- src/TokenParser/IncludeTokenParser.php | 4 +- src/TokenParser/MacroTokenParser.php | 2 +- src/TokenParser/SetTokenParser.php | 4 +- src/TokenParser/UseTokenParser.php | 2 +- src/TokenParser/WithTokenParser.php | 2 +- tests/CustomExtensionTest.php | 2 +- tests/EnvironmentTest.php | 44 +- tests/ExpressionParserTest.php | 35 +- tests/Fixtures/operators/not_precedence.test | 2 +- 97 files changed, 1212 insertions(+), 2301 deletions(-) rename src/{Operator/AbstractOperator.php => ExpressionParser/AbstractExpressionParser.php} (57%) rename src/{Operator/OperatorInterface.php => ExpressionParser/ExpressionParserInterface.php} (60%) create mode 100644 src/ExpressionParser/ExpressionParserType.php create mode 100644 src/ExpressionParser/ExpressionParsers.php create mode 100644 src/ExpressionParser/Infix/ArgumentsTrait.php rename src/{Operator/Binary/ArrowBinaryOperator.php => ExpressionParser/Infix/ArrowExpressionParser.php} (52%) create mode 100644 src/ExpressionParser/Infix/BinaryOperatorExpressionParser.php rename src/{Operator/Ternary/ConditionalTernaryOperator.php => ExpressionParser/Infix/ConditionalTernaryExpressionParser.php} (61%) rename src/{Operator/Binary/DotBinaryOperator.php => ExpressionParser/Infix/DotExpressionParser.php} (78%) rename src/{Operator/Binary/FilterBinaryOperator.php => ExpressionParser/Infix/FilterExpressionParser.php} (70%) rename src/{Operator/Binary/FunctionBinaryOperator.php => ExpressionParser/Infix/FunctionExpressionParser.php} (69%) rename src/{Operator/Binary/IsBinaryOperator.php => ExpressionParser/Infix/IsExpressionParser.php} (75%) rename src/{Operator/Binary/IsNotBinaryOperator.php => ExpressionParser/Infix/IsNotExpressionParser.php} (61%) rename src/{Operator/Binary/SquareBracketBinaryOperator.php => ExpressionParser/Infix/SquareBracketExpressionParser.php} (74%) rename src/{Operator/OperatorAssociativity.php => ExpressionParser/InfixAssociativity.php} (80%) create mode 100644 src/ExpressionParser/InfixExpressionParserInterface.php create mode 100644 src/ExpressionParser/PrecedenceChange.php rename src/{Operator/Unary/ParenthesisUnaryOperator.php => ExpressionParser/Prefix/GroupingExpressionParser.php} (80%) create mode 100644 src/ExpressionParser/Prefix/LiteralExpressionParser.php create mode 100644 src/ExpressionParser/Prefix/UnaryOperatorExpressionParser.php rename src/{Operator/Unary/UnaryOperatorInterface.php => ExpressionParser/PrefixExpressionParserInterface.php} (52%) delete mode 100644 src/Operator/Binary/AbstractBinaryOperator.php delete mode 100644 src/Operator/Binary/AddBinaryOperator.php delete mode 100644 src/Operator/Binary/AndBinaryOperator.php delete mode 100644 src/Operator/Binary/BinaryOperatorInterface.php delete mode 100644 src/Operator/Binary/BitwiseAndBinaryOperator.php delete mode 100644 src/Operator/Binary/BitwiseOrBinaryOperator.php delete mode 100644 src/Operator/Binary/BitwiseXorBinaryOperator.php delete mode 100644 src/Operator/Binary/ConcatBinaryOperator.php delete mode 100644 src/Operator/Binary/DivBinaryOperator.php delete mode 100644 src/Operator/Binary/ElvisBinaryOperator.php delete mode 100644 src/Operator/Binary/EndsWithBinaryOperator.php delete mode 100644 src/Operator/Binary/EqualBinaryOperator.php delete mode 100644 src/Operator/Binary/FloorDivBinaryOperator.php delete mode 100644 src/Operator/Binary/GreaterBinaryOperator.php delete mode 100644 src/Operator/Binary/GreaterEqualBinaryOperator.php delete mode 100644 src/Operator/Binary/HasEveryBinaryOperator.php delete mode 100644 src/Operator/Binary/HasSomeBinaryOperator.php delete mode 100644 src/Operator/Binary/InBinaryOperator.php delete mode 100644 src/Operator/Binary/LessBinaryOperator.php delete mode 100644 src/Operator/Binary/LessEqualBinaryOperator.php delete mode 100644 src/Operator/Binary/MatchesBinaryOperator.php delete mode 100644 src/Operator/Binary/ModBinaryOperator.php delete mode 100644 src/Operator/Binary/MulBinaryOperator.php delete mode 100644 src/Operator/Binary/NotEqualBinaryOperator.php delete mode 100644 src/Operator/Binary/NotInBinaryOperator.php delete mode 100644 src/Operator/Binary/NullCoalesceBinaryOperator.php delete mode 100644 src/Operator/Binary/OrBinaryOperator.php delete mode 100644 src/Operator/Binary/PowerBinaryOperator.php delete mode 100644 src/Operator/Binary/RangeBinaryOperator.php delete mode 100644 src/Operator/Binary/SpaceshipBinaryOperator.php delete mode 100644 src/Operator/Binary/StartsWithBinaryOperator.php delete mode 100644 src/Operator/Binary/SubBinaryOperator.php delete mode 100644 src/Operator/Binary/XorBinaryOperator.php delete mode 100644 src/Operator/OperatorArity.php delete mode 100644 src/Operator/Operators.php delete mode 100644 src/Operator/Ternary/AbstractTernaryOperator.php delete mode 100644 src/Operator/Ternary/TernaryOperatorInterface.php delete mode 100644 src/Operator/Unary/AbstractUnaryOperator.php delete mode 100644 src/Operator/Unary/NegUnaryOperator.php delete mode 100644 src/Operator/Unary/NotUnaryOperator.php delete mode 100644 src/Operator/Unary/PosUnaryOperator.php diff --git a/bin/generate_operators_precedence.php b/bin/generate_operators_precedence.php index 97cdf016d09..c22c81938d7 100644 --- a/bin/generate_operators_precedence.php +++ b/bin/generate_operators_precedence.php @@ -1,13 +1,14 @@ getPrecedenceChange() ? $a->getPrecedenceChange()->getNewPrecedence() : $a->getPrecedence(); $bPrecedence = $b->getPrecedenceChange() ? $b->getPrecedenceChange()->getNewPrecedence() : $b->getPrecedence(); return $bPrecedence - $aPrecedence; }); $current = \PHP_INT_MAX; - foreach ($operators as $operator) { - $precedence = $operator->getPrecedenceChange() ? $operator->getPrecedenceChange()->getNewPrecedence() : $operator->getPrecedence(); + foreach ($expressionParsers as $expressionParser) { + $precedence = $expressionParser->getPrecedenceChange() ? $expressionParser->getPrecedenceChange()->getNewPrecedence() : $expressionParser->getPrecedence(); if ($precedence !== $current) { $current = $precedence; if ($withAssociativity) { - fwrite($output, \sprintf("\n%-11d %-11s %s", $precedence, $operator->getOperator(), OperatorAssociativity::Left === $operator->getAssociativity() ? 'Left' : 'Right')); + fwrite($output, \sprintf("\n%-11d %-11s %s", $precedence, $expressionParser->getName(), InfixAssociativity::Left === $expressionParser->getAssociativity() ? 'Left' : 'Right')); } else { - fwrite($output, \sprintf("\n%-11d %s", $precedence, $operator->getOperator())); + fwrite($output, \sprintf("\n%-11d %s", $precedence, $expressionParser->getName())); } } else { - fwrite($output, "\n".str_repeat(' ', 12).$operator->getOperator()); + fwrite($output, "\n".str_repeat(' ', 12).$expressionParser->getName()); } } fwrite($output, "\n"); @@ -45,20 +46,20 @@ function printOperators($output, array $operators, bool $withAssociativity = fal $output = fopen(dirname(__DIR__).'/doc/operators_precedence.rst', 'w'); $twig = new Environment(new ArrayLoader([])); -$unaryOperators = []; -$notUnaryOperators = []; -foreach ($twig->getOperators() as $operator) { - if ($operator->getArity()->value == OperatorArity::Unary->value) { - $unaryOperators[] = $operator; - } else { - $notUnaryOperators[] = $operator; +$prefixExpressionParsers = []; +$infixExpressionParsers = []; +foreach ($twig->getExpressionParsers() as $expressionParser) { + if ($expressionParser instanceof PrefixExpressionParserInterface) { + $prefixExpressionParsers[] = $expressionParser; + } elseif ($expressionParser instanceof InfixExpressionParserInterface) { + $infixExpressionParsers[] = $expressionParser; } } fwrite($output, "Unary operators precedence:\n"); -printOperators($output, $unaryOperators); +printExpressionParsers($output, $prefixExpressionParsers); fwrite($output, "\nBinary and Ternary operators precedence:\n"); -printOperators($output, $notUnaryOperators, true); +printExpressionParsers($output, $infixExpressionParsers, true); fclose($output); diff --git a/doc/operators_precedence.rst b/doc/operators_precedence.rst index 6ce1b521bbd..032582fbe5e 100644 --- a/doc/operators_precedence.rst +++ b/doc/operators_precedence.rst @@ -8,6 +8,7 @@ Precedence Operator + 70 not 0 ( + literal Binary and Ternary operators precedence: @@ -15,9 +16,9 @@ Binary and Ternary operators precedence: Precedence Operator Associativity =========== =========== ============= -300 | Left - . +300 . Left [ + | ( 250 => Left 200 ** Right diff --git a/extra/cache-extra/TokenParser/CacheTokenParser.php b/extra/cache-extra/TokenParser/CacheTokenParser.php index dcc2ddd288f..086fad88eb6 100644 --- a/extra/cache-extra/TokenParser/CacheTokenParser.php +++ b/extra/cache-extra/TokenParser/CacheTokenParser.php @@ -24,8 +24,7 @@ class CacheTokenParser extends AbstractTokenParser public function parse(Token $token): Node { $stream = $this->parser->getStream(); - $expressionParser = $this->parser->getExpressionParser(); - $key = $expressionParser->parseExpression(); + $key = $this->parser->parseExpression(); $ttl = null; $tags = null; @@ -41,7 +40,7 @@ public function parse(Token $token): Node if ($stream->test(Token::PUNCTUATION_TYPE, ')')) { throw new SyntaxError(\sprintf('The "%s" modifier takes exactly one argument (0 given).', $k), $line, $stream->getSourceContext()); } - $arg = $expressionParser->parseExpression(); + $arg = $this->parser->parseExpression(); if ($stream->test(Token::PUNCTUATION_TYPE, ',')) { throw new SyntaxError(\sprintf('The "%s" modifier takes exactly one argument (2 given).', $k), $line, $stream->getSourceContext()); } diff --git a/extra/cache-extra/composer.json b/extra/cache-extra/composer.json index 4ae0621cd4d..cd7919eddb0 100644 --- a/extra/cache-extra/composer.json +++ b/extra/cache-extra/composer.json @@ -17,7 +17,7 @@ "require": { "php": ">=8.1.0", "symfony/cache": "^5.4|^6.4|^7.0", - "twig/twig": "^3.19|^4.0" + "twig/twig": "^3.20|^4.0" }, "require-dev": { "symfony/phpunit-bridge": "^6.4|^7.0" diff --git a/src/Environment.php b/src/Environment.php index e367835acf4..46e0f3da972 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -19,6 +19,7 @@ use Twig\Error\LoaderError; use Twig\Error\RuntimeError; use Twig\Error\SyntaxError; +use Twig\ExpressionParser\ExpressionParsers; use Twig\Extension\CoreExtension; use Twig\Extension\EscaperExtension; use Twig\Extension\ExtensionInterface; @@ -30,7 +31,6 @@ use Twig\Node\ModuleNode; use Twig\Node\Node; use Twig\NodeVisitor\NodeVisitorInterface; -use Twig\Operator\Operators; use Twig\Runtime\EscaperRuntime; use Twig\RuntimeLoader\FactoryRuntimeLoader; use Twig\RuntimeLoader\RuntimeLoaderInterface; @@ -925,9 +925,9 @@ public function mergeGlobals(array $context): array /** * @internal */ - public function getOperators(): Operators + public function getExpressionParsers(): ExpressionParsers { - return $this->extensionSet->getOperators(); + return $this->extensionSet->getExpressionParsers(); } private function updateOptionsHash(): void diff --git a/src/ExpressionParser.php b/src/ExpressionParser.php index ad4a05257f6..9922d11ec9b 100644 --- a/src/ExpressionParser.php +++ b/src/ExpressionParser.php @@ -13,20 +13,18 @@ namespace Twig; use Twig\Error\SyntaxError; -use Twig\Node\Expression\AbstractExpression; +use Twig\ExpressionParser\Infix\DotExpressionParser; +use Twig\ExpressionParser\Infix\FilterExpressionParser; +use Twig\ExpressionParser\Infix\SquareBracketExpressionParser; use Twig\Node\Expression\ArrayExpression; -use Twig\Node\Expression\Binary\ConcatBinary; use Twig\Node\Expression\ConstantExpression; use Twig\Node\Expression\Unary\NegUnary; use Twig\Node\Expression\Unary\PosUnary; use Twig\Node\Expression\Unary\SpreadUnary; use Twig\Node\Expression\Variable\AssignContextVariable; use Twig\Node\Expression\Variable\ContextVariable; -use Twig\Node\Expression\Variable\LocalVariable; use Twig\Node\Node; use Twig\Node\Nodes; -use Twig\Operator\OperatorArity; -use Twig\Operator\Operators; /** * Parses expressions. @@ -37,6 +35,8 @@ * @see https://en.wikipedia.org/wiki/Operator-precedence_parser * * @author Fabien Potencier + * + * @deprecated since Twig 3.20 */ class ExpressionParser { @@ -49,38 +49,11 @@ class ExpressionParser */ public const OPERATOR_RIGHT = 2; - private Operators $operators; - private bool $deprecationCheck = true; - public function __construct( private Parser $parser, private Environment $env, ) { - $this->operators = $env->getOperators(); - } - - /** - * @internal - */ - public function getParser(): Parser - { - return $this->parser; - } - - /** - * @internal - */ - public function getStream(): TokenStream - { - return $this->parser->getStream(); - } - - /** - * @internal - */ - public function getImportedSymbol(string $type, string $name) - { - return $this->parser->getImportedSymbol($type, $name); + trigger_deprecation('twig/twig', '3.20', 'Class "%s" is deprecated, use "Parser::parseExpression()" instead.', __CLASS__); } public function parseExpression($precedence = 0) @@ -89,297 +62,69 @@ public function parseExpression($precedence = 0) trigger_deprecation('twig/twig', '3.15', 'Passing a second argument ($allowArrow) to "%s()" is deprecated.', __METHOD__); } - $expr = $this->parsePrimary(); - $token = $this->parser->getCurrentToken(); - while ( - $token->test(Token::OPERATOR_TYPE) - && ( - ($op = $this->operators->getTernary($token->getValue())) && $op->getPrecedence() >= $precedence - || ($op = $this->operators->getBinary($token->getValue())) && $op->getPrecedence() >= $precedence - ) - ) { - $this->parser->getStream()->next(); - $previous = $this->setDeprecationCheck(true); - try { - $expr = $op->parse($this, $expr, $token); - } finally { - $this->setDeprecationCheck($previous); - } - $expr->setAttribute('operator', $op); - $this->triggerPrecedenceDeprecations($expr); - $token = $this->parser->getCurrentToken(); - } - - return $expr; - } - - private function triggerPrecedenceDeprecations(AbstractExpression $expr): void - { - $precedenceChanges = $this->operators->getPrecedenceChanges(); - // Check that the all nodes that are between the 2 precedences have explicit parentheses - if (!$expr->hasAttribute('operator') || !isset($precedenceChanges[$expr->getAttribute('operator')])) { - return; - } + trigger_deprecation('twig/twig', '3.20', 'The "%s()" method is deprecated, use "Parser::parseExpression()" instead.', __METHOD__); - if (OperatorArity::Unary === $expr->getAttribute('operator')->getArity()) { - if ($expr->hasExplicitParentheses()) { - return; - } - $operator = $expr->getAttribute('operator'); - /** @var AbstractExpression $node */ - $node = $expr->getNode('node'); - foreach ($precedenceChanges as $op => $changes) { - if (!\in_array($operator, $changes, true)) { - continue; - } - if ($node->hasAttribute('operator') && $op === $node->getAttribute('operator')) { - $change = $operator->getPrecedenceChange(); - trigger_deprecation($change->getPackage(), $change->getVersion(), \sprintf('Add explicit parentheses around the "%s" unary operator to avoid behavior change in the next major version as its precedence will change in "%s" at line %d.', $operator->getOperator(), $this->parser->getStream()->getSourceContext()->getName(), $node->getTemplateLine())); - } - } - } else { - foreach ($precedenceChanges[$expr->getAttribute('operator')] as $operator) { - foreach ($expr as $node) { - /** @var AbstractExpression $node */ - if ($node->hasAttribute('operator') && $operator === $node->getAttribute('operator') && !$node->hasExplicitParentheses()) { - $change = $operator->getPrecedenceChange(); - trigger_deprecation($change->getPackage(), $change->getVersion(), \sprintf('Add explicit parentheses around the "%s" binary operator to avoid behavior change in the next major version as its precedence will change in "%s" at line %d.', $operator->getOperator(), $this->parser->getStream()->getSourceContext()->getName(), $node->getTemplateLine())); - } - } - } - } + return $this->parser->parseExpression((int) $precedence); } /** - * @internal + * @deprecated since Twig 3.20 */ - public function parsePrimary(): AbstractExpression - { - $token = $this->parser->getCurrentToken(); - if ($token->test(Token::OPERATOR_TYPE) && $operator = $this->operators->getUnary($token->getValue())) { - $this->parser->getStream()->next(); - $previous = $this->setDeprecationCheck(false); - try { - $expr = $operator->parse($this, $token); - } finally { - $this->setDeprecationCheck($previous); - } - $expr->setAttribute('operator', $operator); - - if ($this->deprecationCheck) { - $this->triggerPrecedenceDeprecations($expr); - } - - return $expr; - } - - return $this->parsePrimaryExpression(); - } - public function parsePrimaryExpression() { - $token = $this->parser->getCurrentToken(); - switch (true) { - case $token->test(Token::NAME_TYPE): - $this->parser->getStream()->next(); - switch ($token->getValue()) { - case 'true': - case 'TRUE': - return new ConstantExpression(true, $token->getLine()); - - case 'false': - case 'FALSE': - return new ConstantExpression(false, $token->getLine()); - - case 'none': - case 'NONE': - case 'null': - case 'NULL': - return new ConstantExpression(null, $token->getLine()); - - default: - return new ContextVariable($token->getValue(), $token->getLine()); - } - - // no break - case $token->test(Token::NUMBER_TYPE): - $this->parser->getStream()->next(); - - return new ConstantExpression($token->getValue(), $token->getLine()); - - case $token->test(Token::STRING_TYPE): - case $token->test(Token::INTERPOLATION_START_TYPE): - return $this->parseStringExpression(); - - case $token->test(Token::PUNCTUATION_TYPE): - // In 4.0, we should always return the node or throw an error for default - if ($node = match ($token->getValue()) { - '{' => $this->parseMappingExpression(), - default => null, - }) { - return $node; - } - - // no break - case $token->test(Token::OPERATOR_TYPE): - if ('[' === $token->getValue()) { - return $this->parseSequenceExpression(); - } - - if (preg_match(Lexer::REGEX_NAME, $token->getValue(), $matches) && $matches[0] == $token->getValue()) { - // in this context, string operators are variable names - $this->parser->getStream()->next(); - - return new ContextVariable($token->getValue(), $token->getLine()); - } - - if ('=' === $token->getValue() && ('==' === $this->parser->getStream()->look(-1)->getValue() || '!=' === $this->parser->getStream()->look(-1)->getValue())) { - throw new SyntaxError(\sprintf('Unexpected operator of value "%s". Did you try to use "===" or "!==" for strict comparison? Use "is same as(value)" instead.', $token->getValue()), $token->getLine(), $this->parser->getStream()->getSourceContext()); - } + trigger_deprecation('twig/twig', '3.20', 'The "%s()" method is deprecated.', __METHOD__); - // no break - default: - throw new SyntaxError(\sprintf('Unexpected token "%s" of value "%s".', $token->toEnglish(), $token->getValue()), $token->getLine(), $this->parser->getStream()->getSourceContext()); - } + return $this->parseExpression(); } + /** + * @deprecated since Twig 3.20 + */ public function parseStringExpression() { - $stream = $this->parser->getStream(); - - $nodes = []; - // a string cannot be followed by another string in a single expression - $nextCanBeString = true; - while (true) { - if ($nextCanBeString && $token = $stream->nextIf(Token::STRING_TYPE)) { - $nodes[] = new ConstantExpression($token->getValue(), $token->getLine()); - $nextCanBeString = false; - } elseif ($stream->nextIf(Token::INTERPOLATION_START_TYPE)) { - $nodes[] = $this->parseExpression(); - $stream->expect(Token::INTERPOLATION_END_TYPE); - $nextCanBeString = true; - } else { - break; - } - } - - $expr = array_shift($nodes); - foreach ($nodes as $node) { - $expr = new ConcatBinary($expr, $node, $node->getTemplateLine()); - } + trigger_deprecation('twig/twig', '3.20', 'The "%s()" method is deprecated.', __METHOD__); - return $expr; + return $this->parseExpression(); } /** - * @deprecated since Twig 3.11, use parseSequenceExpression() instead + * @deprecated since Twig 3.11, use parseExpression() instead */ public function parseArrayExpression() { - trigger_deprecation('twig/twig', '3.11', 'Calling "%s()" is deprecated, use "parseSequenceExpression()" instead.', __METHOD__); + trigger_deprecation('twig/twig', '3.11', 'Calling "%s()" is deprecated, use "parseExpression()" instead.', __METHOD__); - return $this->parseSequenceExpression(); + return $this->parseExpression(); } + /** + * @deprecated since Twig 3.20 + */ public function parseSequenceExpression() { - $stream = $this->parser->getStream(); - $stream->expect(Token::OPERATOR_TYPE, '[', 'A sequence element was expected'); - - $node = new ArrayExpression([], $stream->getCurrent()->getLine()); - $first = true; - while (!$stream->test(Token::PUNCTUATION_TYPE, ']')) { - if (!$first) { - $stream->expect(Token::PUNCTUATION_TYPE, ',', 'A sequence element must be followed by a comma'); - - // trailing ,? - if ($stream->test(Token::PUNCTUATION_TYPE, ']')) { - break; - } - } - $first = false; - - if ($stream->nextIf(Token::SPREAD_TYPE)) { - $expr = $this->parseExpression(); - $expr->setAttribute('spread', true); - $node->addElement($expr); - } else { - $node->addElement($this->parseExpression()); - } - } - $stream->expect(Token::PUNCTUATION_TYPE, ']', 'An opened sequence is not properly closed'); + trigger_deprecation('twig/twig', '3.20', 'The "%s()" method is deprecated.', __METHOD__); - return $node; + return $this->parseExpression(); } /** - * @deprecated since Twig 3.11, use parseMappingExpression() instead + * @deprecated since Twig 3.11, use parseExpression() instead */ public function parseHashExpression() { - trigger_deprecation('twig/twig', '3.11', 'Calling "%s()" is deprecated, use "parseMappingExpression()" instead.', __METHOD__); + trigger_deprecation('twig/twig', '3.11', 'Calling "%s()" is deprecated, use "parseExpression()" instead.', __METHOD__); - return $this->parseMappingExpression(); + return $this->parseExpression(); } + /** + * @deprecated since Twig 3.20 + */ public function parseMappingExpression() { - $stream = $this->parser->getStream(); - $stream->expect(Token::PUNCTUATION_TYPE, '{', 'A mapping element was expected'); - - $node = new ArrayExpression([], $stream->getCurrent()->getLine()); - $first = true; - while (!$stream->test(Token::PUNCTUATION_TYPE, '}')) { - if (!$first) { - $stream->expect(Token::PUNCTUATION_TYPE, ',', 'A mapping value must be followed by a comma'); - - // trailing ,? - if ($stream->test(Token::PUNCTUATION_TYPE, '}')) { - break; - } - } - $first = false; - - if ($stream->nextIf(Token::SPREAD_TYPE)) { - $value = $this->parseExpression(); - $value->setAttribute('spread', true); - $node->addElement($value); - continue; - } - - // a mapping key can be: - // - // * a number -- 12 - // * a string -- 'a' - // * a name, which is equivalent to a string -- a - // * an expression, which must be enclosed in parentheses -- (1 + 2) - if ($token = $stream->nextIf(Token::NAME_TYPE)) { - $key = new ConstantExpression($token->getValue(), $token->getLine()); - - // {a} is a shortcut for {a:a} - if ($stream->test(Token::PUNCTUATION_TYPE, [',', '}'])) { - $value = new ContextVariable($key->getAttribute('value'), $key->getTemplateLine()); - $node->addElement($value, $key); - continue; - } - } elseif (($token = $stream->nextIf(Token::STRING_TYPE)) || $token = $stream->nextIf(Token::NUMBER_TYPE)) { - $key = new ConstantExpression($token->getValue(), $token->getLine()); - } elseif ($stream->test(Token::OPERATOR_TYPE, '(')) { - $key = $this->parseExpression(); - } else { - $current = $stream->getCurrent(); - - throw new SyntaxError(\sprintf('A mapping key must be a quoted string, a number, a name, or an expression enclosed in parentheses (unexpected token "%s" of value "%s".', $current->toEnglish(), $current->getValue()), $current->getLine(), $stream->getSourceContext()); - } - - $stream->expect(Token::PUNCTUATION_TYPE, ':', 'A mapping key must be followed by a colon (:)'); - $value = $this->parseExpression(); - - $node->addElement($value, $key); - } - $stream->expect(Token::PUNCTUATION_TYPE, '}', 'An opened mapping is not properly closed'); + trigger_deprecation('twig/twig', '3.20', 'The "%s()" method is deprecated.', __METHOD__); - return $node; + return $this->parseExpression(); } /** @@ -414,11 +159,13 @@ public function parseSubscriptExpression($node) { trigger_deprecation('twig/twig', '3.20', 'The "%s()" method is deprecated.', __METHOD__); + $parsers = new \ReflectionProperty($this->parser, 'parsers'); + if ('.' === $this->parser->getStream()->next()->getValue()) { - return $this->operators->getBinary('.')->parse($this, $node, $this->parser->getCurrentToken()); + return $parsers->getValue($this->parser)->getInfixByClass(DotExpressionParser::class)->parse($this->parser, $node, $this->parser->getCurrentToken()); } - return $this->operators->getBinary('[')->parse($this, $node, $this->parser->getCurrentToken()); + return $parsers->getValue($this->parser)->getInfixByClass(SquareBracketExpressionParser::class)->parse($this->parser, $node, $this->parser->getCurrentToken()); } /** @@ -440,9 +187,11 @@ public function parseFilterExpressionRaw($node) { trigger_deprecation('twig/twig', '3.20', 'The "%s()" method is deprecated.', __METHOD__); - $op = $this->operators->getBinary('|'); + $parsers = new \ReflectionProperty($this->parser, 'parsers'); + + $op = $parsers->getValue($this->parser)->getInfixByClass(FilterExpressionParser::class); while (true) { - $node = $op->parse($this, $node, $this->parser->getCurrentToken()); + $node = $op->parse($this->parser, $node, $this->parser->getCurrentToken()); if (!$this->parser->getStream()->test(Token::OPERATOR_TYPE, '|')) { break; } @@ -459,11 +208,13 @@ public function parseFilterExpressionRaw($node) * * @throws SyntaxError * - * @deprecated since Twig 3.19 Use parseNamedArguments() instead + * @deprecated since Twig 3.19 Use Twig\ExpressionParser\Infix\ArgumentsTrait::parseNamedArguments() instead */ public function parseArguments() { - trigger_deprecation('twig/twig', '3.19', \sprintf('The "%s()" method is deprecated, use "%s::parseNamedArguments()" instead.', __METHOD__, __CLASS__)); + trigger_deprecation('twig/twig', '3.19', \sprintf('The "%s()" method is deprecated, use "Twig\ExpressionParser\Infix\ArgumentsTrait::parseNamedArguments()" instead.', __METHOD__)); + + $parsePrimary = new \ReflectionMethod($this->parser, 'parsePrimary'); $namedArguments = false; $definition = false; @@ -512,7 +263,7 @@ public function parseArguments() $name = $value->getAttribute('name'); if ($definition) { - $value = $this->parsePrimary(); + $value = $parsePrimary->invoke($this->parser); if (!$this->checkConstantExpression($value)) { throw new SyntaxError('A default value for an argument must be a constant (a boolean, a string, a number, a sequence, or a mapping).', $token->getLine(), $stream->getSourceContext()); @@ -587,21 +338,6 @@ public function parseMultitargetExpression() return new Nodes($targets); } - public function getTest(int $line): TwigTest - { - return $this->parser->getTest($line); - } - - public function getFunction(string $name, int $line): TwigFunction - { - return $this->parser->getFunction($name, $line); - } - - public function getFilter(string $name, int $line): TwigFilter - { - return $this->parser->getFilter($name, $line); - } - // checks that the node only contains "constant" elements // to be removed in 4.0 private function checkConstantExpression(Node $node): bool @@ -621,81 +357,13 @@ private function checkConstantExpression(Node $node): bool return true; } - private function setDeprecationCheck(bool $deprecationCheck): bool - { - $current = $this->deprecationCheck; - $this->deprecationCheck = $deprecationCheck; - - return $current; - } - /** - * @internal - */ - public function parseCallableArguments(int $line, bool $parseOpenParenthesis = true): ArrayExpression - { - $arguments = new ArrayExpression([], $line); - foreach ($this->parseNamedArguments($parseOpenParenthesis) as $k => $n) { - $arguments->addElement($n, new LocalVariable($k, $line)); - } - - return $arguments; - } - - /** - * @deprecated since Twig 3.19 Use parseNamedArguments() instead + * @deprecated since Twig 3.19 Use Twig\ExpressionParser\Infix\ArgumentsTrait::parseNamedArguments() instead */ public function parseOnlyArguments() { - trigger_deprecation('twig/twig', '3.19', \sprintf('The "%s()" method is deprecated, use "%s::parseNamedArguments()" instead.', __METHOD__, __CLASS__)); - - return $this->parseNamedArguments(); - } - - public function parseNamedArguments(bool $parseOpenParenthesis = true): Nodes - { - $args = []; - $stream = $this->parser->getStream(); - if ($parseOpenParenthesis) { - $stream->expect(Token::OPERATOR_TYPE, '(', 'A list of arguments must begin with an opening parenthesis'); - } - $hasSpread = false; - while (!$stream->test(Token::PUNCTUATION_TYPE, ')')) { - if ($args) { - $stream->expect(Token::PUNCTUATION_TYPE, ',', 'Arguments must be separated by a comma'); - - // if the comma above was a trailing comma, early exit the argument parse loop - if ($stream->test(Token::PUNCTUATION_TYPE, ')')) { - break; - } - } + trigger_deprecation('twig/twig', '3.19', \sprintf('The "%s()" method is deprecated, use "Twig\ExpressionParser\Infix\ArgumentsTrait::parseNamedArguments()" instead.', __METHOD__)); - if ($stream->nextIf(Token::SPREAD_TYPE)) { - $hasSpread = true; - $value = new SpreadUnary($this->parseExpression(), $stream->getCurrent()->getLine()); - } elseif ($hasSpread) { - throw new SyntaxError('Normal arguments must be placed before argument unpacking.', $stream->getCurrent()->getLine(), $stream->getSourceContext()); - } else { - $value = $this->parseExpression(); - } - - $name = null; - if (($token = $stream->nextIf(Token::OPERATOR_TYPE, '=')) || ($token = $stream->nextIf(Token::PUNCTUATION_TYPE, ':'))) { - if (!$value instanceof ContextVariable) { - throw new SyntaxError(\sprintf('A parameter name must be a string, "%s" given.', \get_class($value)), $token->getLine(), $stream->getSourceContext()); - } - $name = $value->getAttribute('name'); - $value = $this->parseExpression(); - } - - if (null === $name) { - $args[] = $value; - } else { - $args[$name] = $value; - } - } - $stream->expect(Token::PUNCTUATION_TYPE, ')', 'A list of arguments must be closed by a parenthesis'); - - return new Nodes($args); + return $this->parseArguments(); } } diff --git a/src/Operator/AbstractOperator.php b/src/ExpressionParser/AbstractExpressionParser.php similarity index 57% rename from src/Operator/AbstractOperator.php rename to src/ExpressionParser/AbstractExpressionParser.php index c18904f35ae..bc05bfa051e 100644 --- a/src/Operator/AbstractOperator.php +++ b/src/ExpressionParser/AbstractExpressionParser.php @@ -9,18 +9,16 @@ * file that was distributed with this source code. */ -namespace Twig\Operator; +namespace Twig\ExpressionParser; -use Twig\OperatorPrecedenceChange; - -abstract class AbstractOperator implements OperatorInterface +abstract class AbstractExpressionParser implements ExpressionParserInterface { public function __toString(): string { - return \sprintf('%s(%s)', $this->getArity()->value, $this->getOperator()); + return \sprintf('%s(%s)', ExpressionParserType::getType($this)->value, $this->getName()); } - public function getPrecedenceChange(): ?OperatorPrecedenceChange + public function getPrecedenceChange(): ?PrecedenceChange { return null; } diff --git a/src/Operator/OperatorInterface.php b/src/ExpressionParser/ExpressionParserInterface.php similarity index 60% rename from src/Operator/OperatorInterface.php rename to src/ExpressionParser/ExpressionParserInterface.php index 8512bdeade6..86576aec49a 100644 --- a/src/Operator/OperatorInterface.php +++ b/src/ExpressionParser/ExpressionParserInterface.php @@ -9,21 +9,17 @@ * file that was distributed with this source code. */ -namespace Twig\Operator; +namespace Twig\ExpressionParser; -use Twig\OperatorPrecedenceChange; - -interface OperatorInterface +interface ExpressionParserInterface { public function __toString(): string; - public function getOperator(): string; - - public function getArity(): OperatorArity; + public function getName(): string; public function getPrecedence(): int; - public function getPrecedenceChange(): ?OperatorPrecedenceChange; + public function getPrecedenceChange(): ?PrecedenceChange; /** * @return array diff --git a/src/ExpressionParser/ExpressionParserType.php b/src/ExpressionParser/ExpressionParserType.php new file mode 100644 index 00000000000..0a980a8ec40 --- /dev/null +++ b/src/ExpressionParser/ExpressionParserType.php @@ -0,0 +1,33 @@ + + * + * @internal + */ +final class ExpressionParsers implements \IteratorAggregate +{ + /** + * @var array, array> + */ + private array $parsers = []; + + /** + * @var array, array, ExpressionParserInterface>> + */ + private array $parsersByClass = []; + + /** + * @var array, array> + */ + private array $aliases = []; + + /** + * @var \WeakMap>|null + */ + private ?\WeakMap $precedenceChanges = null; + + /** + * @param array $parsers + */ + public function __construct( + array $parsers = [], + ) { + $this->precedenceChanges = null; + $this->add($parsers); + } + + /** + * @param array $parsers + * + * @return $this + */ + public function add(array $parsers): self + { + foreach ($parsers as $operator) { + $type = ExpressionParserType::getType($operator); + $this->parsers[$type->value][$operator->getName()] = $operator; + $this->parsersByClass[$type->value][get_class($operator)] = $operator; + foreach ($operator->getAliases() as $alias) { + $this->aliases[$type->value][$alias] = $operator; + } + } + + return $this; + } + + /** + * @param class-string $name + */ + public function getPrefixByClass(string $name): ?PrefixExpressionParserInterface + { + return $this->parsersByClass[ExpressionParserType::Prefix->value][$name] ?? null; + } + + public function getPrefix(string $name): ?PrefixExpressionParserInterface + { + return + $this->parsers[ExpressionParserType::Prefix->value][$name] + ?? $this->aliases[ExpressionParserType::Prefix->value][$name] + ?? null + ; + } + + /** + * @param class-string $name + */ + public function getInfixByClass(string $name): ?InfixExpressionParserInterface + { + return $this->parsersByClass[ExpressionParserType::Infix->value][$name] ?? null; + } + + public function getInfix(string $name): ?InfixExpressionParserInterface + { + return + $this->parsers[ExpressionParserType::Infix->value][$name] + ?? $this->aliases[ExpressionParserType::Infix->value][$name] + ?? null + ; + } + + public function getIterator(): \Traversable + { + foreach ($this->parsers as $parsers) { + // we don't yield the keys + yield from $parsers; + } + } + + /** + * @internal + * + * @return \WeakMap> + */ + public function getPrecedenceChanges(): \WeakMap + { + if (null === $this->precedenceChanges) { + $this->precedenceChanges = new \WeakMap(); + foreach ($this as $ep) { + if (!$ep->getPrecedenceChange()) { + continue; + } + $min = min($ep->getPrecedenceChange()->getNewPrecedence(), $ep->getPrecedence()); + $max = max($ep->getPrecedenceChange()->getNewPrecedence(), $ep->getPrecedence()); + foreach ($this as $e) { + if ($e->getPrecedence() > $min && $e->getPrecedence() < $max) { + if (!isset($this->precedenceChanges[$e])) { + $this->precedenceChanges[$e] = []; + } + $this->precedenceChanges[$e][] = $ep; + } + } + } + } + + return $this->precedenceChanges; + } +} diff --git a/src/ExpressionParser/Infix/ArgumentsTrait.php b/src/ExpressionParser/Infix/ArgumentsTrait.php new file mode 100644 index 00000000000..b60a8481053 --- /dev/null +++ b/src/ExpressionParser/Infix/ArgumentsTrait.php @@ -0,0 +1,81 @@ +parseNamedArguments($parser, $parseOpenParenthesis) as $k => $n) { + $arguments->addElement($n, new LocalVariable($k, $line)); + } + + return $arguments; + } + + private function parseNamedArguments(Parser $parser, bool $parseOpenParenthesis = true): Nodes + { + $args = []; + $stream = $parser->getStream(); + if ($parseOpenParenthesis) { + $stream->expect(Token::OPERATOR_TYPE, '(', 'A list of arguments must begin with an opening parenthesis'); + } + $hasSpread = false; + while (!$stream->test(Token::PUNCTUATION_TYPE, ')')) { + if ($args) { + $stream->expect(Token::PUNCTUATION_TYPE, ',', 'Arguments must be separated by a comma'); + + // if the comma above was a trailing comma, early exit the argument parse loop + if ($stream->test(Token::PUNCTUATION_TYPE, ')')) { + break; + } + } + + if ($stream->nextIf(Token::SPREAD_TYPE)) { + $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; + if (($token = $stream->nextIf(Token::OPERATOR_TYPE, '=')) || ($token = $stream->nextIf(Token::PUNCTUATION_TYPE, ':'))) { + if (!$value instanceof ContextVariable) { + throw new SyntaxError(\sprintf('A parameter name must be a string, "%s" given.', \get_class($value)), $token->getLine(), $stream->getSourceContext()); + } + $name = $value->getAttribute('name'); + $value = $parser->parseExpression(); + } + + if (null === $name) { + $args[] = $value; + } else { + $args[$name] = $value; + } + } + $stream->expect(Token::PUNCTUATION_TYPE, ')', 'A list of arguments must be closed by a parenthesis'); + + return new Nodes($args); + } +} diff --git a/src/Operator/Binary/ArrowBinaryOperator.php b/src/ExpressionParser/Infix/ArrowExpressionParser.php similarity index 52% rename from src/Operator/Binary/ArrowBinaryOperator.php rename to src/ExpressionParser/Infix/ArrowExpressionParser.php index 253b00fc78c..698497b0e8d 100644 --- a/src/Operator/Binary/ArrowBinaryOperator.php +++ b/src/ExpressionParser/Infix/ArrowExpressionParser.php @@ -9,25 +9,28 @@ * file that was distributed with this source code. */ -namespace Twig\Operator\Binary; +namespace Twig\ExpressionParser\Infix; -use Twig\ExpressionParser; +use Twig\ExpressionParser\AbstractExpressionParser; +use Twig\ExpressionParser\InfixAssociativity; +use Twig\ExpressionParser\InfixExpressionParserInterface; use Twig\Node\Expression\AbstractExpression; use Twig\Node\Expression\ArrowFunctionExpression; -use Twig\Operator\AbstractOperator; -use Twig\Operator\OperatorArity; -use Twig\Operator\OperatorAssociativity; +use Twig\Parser; use Twig\Token; -class ArrowBinaryOperator extends AbstractOperator implements BinaryOperatorInterface +/** + * @internal + */ +final class ArrowExpressionParser extends AbstractExpressionParser implements InfixExpressionParserInterface { - public function parse(ExpressionParser $parser, AbstractExpression $expr, Token $token): AbstractExpression + public function parse(Parser $parser, AbstractExpression $expr, Token $token): AbstractExpression { // As the expression of the arrow function is independent from the current precedence, we want a precedence of 0 return new ArrowFunctionExpression($parser->parseExpression(), $expr, $token->getLine()); } - public function getOperator(): string + public function getName(): string { return '=>'; } @@ -37,13 +40,8 @@ public function getPrecedence(): int return 250; } - public function getArity(): OperatorArity - { - return OperatorArity::Binary; - } - - public function getAssociativity(): OperatorAssociativity + public function getAssociativity(): InfixAssociativity { - return OperatorAssociativity::Left; + return InfixAssociativity::Left; } } diff --git a/src/ExpressionParser/Infix/BinaryOperatorExpressionParser.php b/src/ExpressionParser/Infix/BinaryOperatorExpressionParser.php new file mode 100644 index 00000000000..ce650b424ea --- /dev/null +++ b/src/ExpressionParser/Infix/BinaryOperatorExpressionParser.php @@ -0,0 +1,73 @@ + */ + private string $nodeClass, + private string $name, + private int $precedence, + private InfixAssociativity $associativity = InfixAssociativity::Left, + private ?PrecedenceChange $precedenceChange = null, + private array $aliases = [], + ) { + } + + /** + * @return AbstractBinary + */ + public function parse(Parser $parser, AbstractExpression $left, Token $token): AbstractExpression + { + $right = $parser->parseExpression(InfixAssociativity::Left === $this->getAssociativity() ? $this->getPrecedence() + 1 : $this->getPrecedence()); + + return new ($this->nodeClass)($left, $right, $token->getLine()); + } + + public function getAssociativity(): InfixAssociativity + { + return $this->associativity; + } + + public function getName(): string + { + return $this->name; + } + + public function getPrecedence(): int + { + return $this->precedence; + } + + public function getPrecedenceChange(): ?PrecedenceChange + { + return $this->precedenceChange; + } + + public function getAliases(): array + { + return $this->aliases; + } +} diff --git a/src/Operator/Ternary/ConditionalTernaryOperator.php b/src/ExpressionParser/Infix/ConditionalTernaryExpressionParser.php similarity index 61% rename from src/Operator/Ternary/ConditionalTernaryOperator.php rename to src/ExpressionParser/Infix/ConditionalTernaryExpressionParser.php index 7c20963db57..2bb5fc92c79 100644 --- a/src/Operator/Ternary/ConditionalTernaryOperator.php +++ b/src/ExpressionParser/Infix/ConditionalTernaryExpressionParser.php @@ -9,20 +9,26 @@ * file that was distributed with this source code. */ -namespace Twig\Operator\Ternary; +namespace Twig\ExpressionParser\Infix; -use Twig\ExpressionParser; +use Twig\ExpressionParser\AbstractExpressionParser; +use Twig\ExpressionParser\InfixAssociativity; +use Twig\ExpressionParser\InfixExpressionParserInterface; use Twig\Node\Expression\AbstractExpression; use Twig\Node\Expression\ConstantExpression; use Twig\Node\Expression\Ternary\ConditionalTernary; +use Twig\Parser; use Twig\Token; -class ConditionalTernaryOperator extends AbstractTernaryOperator +/** + * @internal + */ +final class ConditionalTernaryExpressionParser extends AbstractExpressionParser implements InfixExpressionParserInterface { - public function parse(ExpressionParser $parser, AbstractExpression $left, Token $token): AbstractExpression + public function parse(Parser $parser, AbstractExpression $left, Token $token): AbstractExpression { $then = $parser->parseExpression($this->getPrecedence()); - if ($parser->getStream()->nextIf(Token::PUNCTUATION_TYPE, $this->getElseOperator())) { + if ($parser->getStream()->nextIf(Token::PUNCTUATION_TYPE, ':')) { // Ternary operator (expr ? expr2 : expr3) $else = $parser->parseExpression($this->getPrecedence()); } else { @@ -33,7 +39,7 @@ public function parse(ExpressionParser $parser, AbstractExpression $left, Token return new ConditionalTernary($left, $then, $else, $token->getLine()); } - public function getOperator(): string + public function getName(): string { return '?'; } @@ -43,8 +49,8 @@ public function getPrecedence(): int return 0; } - private function getElseOperator(): string + public function getAssociativity(): InfixAssociativity { - return ':'; + return InfixAssociativity::Left; } } diff --git a/src/Operator/Binary/DotBinaryOperator.php b/src/ExpressionParser/Infix/DotExpressionParser.php similarity index 78% rename from src/Operator/Binary/DotBinaryOperator.php rename to src/ExpressionParser/Infix/DotExpressionParser.php index 2aecf0b10cc..d83f4bfbbfb 100644 --- a/src/Operator/Binary/DotBinaryOperator.php +++ b/src/ExpressionParser/Infix/DotExpressionParser.php @@ -9,10 +9,12 @@ * file that was distributed with this source code. */ -namespace Twig\Operator\Binary; +namespace Twig\ExpressionParser\Infix; use Twig\Error\SyntaxError; -use Twig\ExpressionParser; +use Twig\ExpressionParser\AbstractExpressionParser; +use Twig\ExpressionParser\InfixAssociativity; +use Twig\ExpressionParser\InfixExpressionParserInterface; use Twig\Lexer; use Twig\Node\Expression\AbstractExpression; use Twig\Node\Expression\ArrayExpression; @@ -21,15 +23,18 @@ use Twig\Node\Expression\MacroReferenceExpression; use Twig\Node\Expression\NameExpression; use Twig\Node\Expression\Variable\TemplateVariable; -use Twig\Operator\AbstractOperator; -use Twig\Operator\OperatorArity; -use Twig\Operator\OperatorAssociativity; +use Twig\Parser; use Twig\Template; use Twig\Token; -class DotBinaryOperator extends AbstractOperator implements BinaryOperatorInterface +/** + * @internal + */ +final class DotExpressionParser extends AbstractExpressionParser implements InfixExpressionParserInterface { - public function parse(ExpressionParser $parser, AbstractExpression $expr, Token $token): AbstractExpression + use ArgumentsTrait; + + public function parse(Parser $parser, AbstractExpression $expr, Token $token): AbstractExpression { $stream = $parser->getStream(); $token = $stream->getCurrent(); @@ -55,7 +60,7 @@ public function parse(ExpressionParser $parser, AbstractExpression $expr, Token if ($stream->test(Token::OPERATOR_TYPE, '(')) { $type = Template::METHOD_CALL; - $arguments = $parser->parseCallableArguments($token->getLine()); + $arguments = $this->parseCallableArguments($parser, $token->getLine()); } if ( @@ -71,7 +76,7 @@ public function parse(ExpressionParser $parser, AbstractExpression $expr, Token return new GetAttrExpression($expr, $attribute, $arguments, $type, $lineno); } - public function getOperator(): string + public function getName(): string { return '.'; } @@ -81,13 +86,8 @@ public function getPrecedence(): int return 300; } - public function getArity(): OperatorArity - { - return OperatorArity::Binary; - } - - public function getAssociativity(): OperatorAssociativity + public function getAssociativity(): InfixAssociativity { - return OperatorAssociativity::Left; + return InfixAssociativity::Left; } } diff --git a/src/Operator/Binary/FilterBinaryOperator.php b/src/ExpressionParser/Infix/FilterExpressionParser.php similarity index 70% rename from src/Operator/Binary/FilterBinaryOperator.php rename to src/ExpressionParser/Infix/FilterExpressionParser.php index 9dc6333898f..98e4b3b3a90 100644 --- a/src/Operator/Binary/FilterBinaryOperator.php +++ b/src/ExpressionParser/Infix/FilterExpressionParser.php @@ -9,23 +9,28 @@ * file that was distributed with this source code. */ -namespace Twig\Operator\Binary; +namespace Twig\ExpressionParser\Infix; use Twig\Attribute\FirstClassTwigCallableReady; -use Twig\ExpressionParser; +use Twig\ExpressionParser\AbstractExpressionParser; +use Twig\ExpressionParser\InfixAssociativity; +use Twig\ExpressionParser\InfixExpressionParserInterface; use Twig\Node\EmptyNode; use Twig\Node\Expression\AbstractExpression; use Twig\Node\Expression\ConstantExpression; -use Twig\Operator\AbstractOperator; -use Twig\Operator\OperatorArity; -use Twig\Operator\OperatorAssociativity; +use Twig\Parser; use Twig\Token; -class FilterBinaryOperator extends AbstractOperator implements BinaryOperatorInterface +/** + * @internal + */ +final class FilterExpressionParser extends AbstractExpressionParser implements InfixExpressionParserInterface { + use ArgumentsTrait; + private $readyNodes = []; - public function parse(ExpressionParser $parser, AbstractExpression $expr, Token $token): AbstractExpression + public function parse(Parser $parser, AbstractExpression $expr, Token $token): AbstractExpression { $stream = $parser->getStream(); $token = $stream->expect(Token::NAME_TYPE); @@ -34,7 +39,7 @@ public function parse(ExpressionParser $parser, AbstractExpression $expr, Token if (!$stream->test(Token::OPERATOR_TYPE, '(')) { $arguments = new EmptyNode(); } else { - $arguments = $parser->parseNamedArguments(); + $arguments = $this->parseNamedArguments($parser); } $filter = $parser->getFilter($token->getValue(), $line); @@ -51,7 +56,7 @@ public function parse(ExpressionParser $parser, AbstractExpression $expr, Token return new $class($expr, $ready ? $filter : new ConstantExpression($filter->getName(), $line), $arguments, $line); } - public function getOperator(): string + public function getName(): string { return '|'; } @@ -61,13 +66,8 @@ public function getPrecedence(): int return 300; } - public function getArity(): OperatorArity - { - return OperatorArity::Binary; - } - - public function getAssociativity(): OperatorAssociativity + public function getAssociativity(): InfixAssociativity { - return OperatorAssociativity::Left; + return InfixAssociativity::Left; } } diff --git a/src/Operator/Binary/FunctionBinaryOperator.php b/src/ExpressionParser/Infix/FunctionExpressionParser.php similarity index 69% rename from src/Operator/Binary/FunctionBinaryOperator.php rename to src/ExpressionParser/Infix/FunctionExpressionParser.php index 740c1ce5948..b1d627f7b9b 100644 --- a/src/Operator/Binary/FunctionBinaryOperator.php +++ b/src/ExpressionParser/Infix/FunctionExpressionParser.php @@ -9,25 +9,30 @@ * file that was distributed with this source code. */ -namespace Twig\Operator\Binary; +namespace Twig\ExpressionParser\Infix; use Twig\Attribute\FirstClassTwigCallableReady; use Twig\Error\SyntaxError; -use Twig\ExpressionParser; +use Twig\ExpressionParser\AbstractExpressionParser; +use Twig\ExpressionParser\InfixAssociativity; +use Twig\ExpressionParser\InfixExpressionParserInterface; use Twig\Node\EmptyNode; use Twig\Node\Expression\AbstractExpression; use Twig\Node\Expression\MacroReferenceExpression; use Twig\Node\Expression\NameExpression; -use Twig\Operator\AbstractOperator; -use Twig\Operator\OperatorArity; -use Twig\Operator\OperatorAssociativity; +use Twig\Parser; use Twig\Token; -class FunctionBinaryOperator extends AbstractOperator implements BinaryOperatorInterface +/** + * @internal + */ +final class FunctionExpressionParser extends AbstractExpressionParser implements InfixExpressionParserInterface { + use ArgumentsTrait; + private $readyNodes = []; - public function parse(ExpressionParser $parser, AbstractExpression $expr, Token $token): AbstractExpression + public function parse(Parser $parser, AbstractExpression $expr, Token $token): AbstractExpression { $line = $token->getLine(); if (!$expr instanceof NameExpression) { @@ -37,10 +42,10 @@ public function parse(ExpressionParser $parser, AbstractExpression $expr, Token $name = $expr->getAttribute('name'); if (null !== $alias = $parser->getImportedSymbol('function', $name)) { - return new MacroReferenceExpression($alias['node']->getNode('var'), $alias['name'], $parser->parseCallableArguments($line, false), $line); + return new MacroReferenceExpression($alias['node']->getNode('var'), $alias['name'], $this->parseCallableArguments($parser, $line, false), $line); } - $args = $parser->parseNamedArguments(false); + $args = $this->parseNamedArguments($parser, false); $function = $parser->getFunction($name, $line); @@ -48,7 +53,7 @@ public function parse(ExpressionParser $parser, AbstractExpression $expr, Token $fakeNode = new EmptyNode($line); $fakeNode->setSourceContext($parser->getStream()->getSourceContext()); - return ($function->getParserCallable())($parser->getParser(), $fakeNode, $args, $line); + return ($function->getParserCallable())($parser, $fakeNode, $args, $line); } if (!isset($this->readyNodes[$class = $function->getNodeClass()])) { @@ -62,7 +67,7 @@ public function parse(ExpressionParser $parser, AbstractExpression $expr, Token return new $class($ready ? $function : $function->getName(), $args, $line); } - public function getOperator(): string + public function getName(): string { return '('; } @@ -72,13 +77,8 @@ public function getPrecedence(): int return 300; } - public function getArity(): OperatorArity - { - return OperatorArity::Binary; - } - - public function getAssociativity(): OperatorAssociativity + public function getAssociativity(): InfixAssociativity { - return OperatorAssociativity::Left; + return InfixAssociativity::Left; } } diff --git a/src/Operator/Binary/IsBinaryOperator.php b/src/ExpressionParser/Infix/IsExpressionParser.php similarity index 75% rename from src/Operator/Binary/IsBinaryOperator.php rename to src/ExpressionParser/Infix/IsExpressionParser.php index 4236b769a44..d63b495e24e 100644 --- a/src/Operator/Binary/IsBinaryOperator.php +++ b/src/ExpressionParser/Infix/IsExpressionParser.php @@ -9,33 +9,38 @@ * file that was distributed with this source code. */ -namespace Twig\Operator\Binary; + namespace Twig\ExpressionParser\Infix; use Twig\Attribute\FirstClassTwigCallableReady; -use Twig\ExpressionParser; +use Twig\ExpressionParser\AbstractExpressionParser; +use Twig\ExpressionParser\InfixAssociativity; +use Twig\ExpressionParser\InfixExpressionParserInterface; use Twig\Node\Expression\AbstractExpression; use Twig\Node\Expression\ArrayExpression; use Twig\Node\Expression\MacroReferenceExpression; use Twig\Node\Expression\NameExpression; use Twig\Node\Nodes; -use Twig\Operator\AbstractOperator; -use Twig\Operator\OperatorArity; -use Twig\Operator\OperatorAssociativity; +use Twig\Parser; use Twig\Token; use Twig\TwigTest; -class IsBinaryOperator extends AbstractOperator implements BinaryOperatorInterface +/** + * @internal + */ +class IsExpressionParser extends AbstractExpressionParser implements InfixExpressionParserInterface { + use ArgumentsTrait; + private $readyNodes = []; - public function parse(ExpressionParser $parser, AbstractExpression $expr, Token $token): AbstractExpression + public function parse(Parser $parser, AbstractExpression $expr, Token $token): AbstractExpression { $stream = $parser->getStream(); $test = $parser->getTest($token->getLine()); $arguments = null; if ($stream->test(Token::OPERATOR_TYPE, '(')) { - $arguments = $parser->parseNamedArguments(); + $arguments = $this->parseNamedArguments($parser); } elseif ($test->hasOneMandatoryArgument()) { $arguments = new Nodes([0 => $parser->parseExpression($this->getPrecedence())]); } @@ -61,18 +66,13 @@ public function getPrecedence(): int return 100; } - public function getOperator(): string + public function getName(): string { return 'is'; } - public function getArity(): OperatorArity - { - return OperatorArity::Binary; - } - - public function getAssociativity(): OperatorAssociativity + public function getAssociativity(): InfixAssociativity { - return OperatorAssociativity::Left; + return InfixAssociativity::Left; } } diff --git a/src/Operator/Binary/IsNotBinaryOperator.php b/src/ExpressionParser/Infix/IsNotExpressionParser.php similarity index 61% rename from src/Operator/Binary/IsNotBinaryOperator.php rename to src/ExpressionParser/Infix/IsNotExpressionParser.php index 2455f738714..55c0844ced7 100644 --- a/src/Operator/Binary/IsNotBinaryOperator.php +++ b/src/ExpressionParser/Infix/IsNotExpressionParser.php @@ -9,21 +9,24 @@ * file that was distributed with this source code. */ -namespace Twig\Operator\Binary; + namespace Twig\ExpressionParser\Infix; -use Twig\ExpressionParser; use Twig\Node\Expression\AbstractExpression; use Twig\Node\Expression\Unary\NotUnary; +use Twig\Parser; use Twig\Token; -class IsNotBinaryOperator extends IsBinaryOperator +/** + * @internal + */ +final class IsNotExpressionParser extends IsExpressionParser { - public function parse(ExpressionParser $parser, AbstractExpression $expr, Token $token): AbstractExpression + public function parse(Parser $parser, AbstractExpression $expr, Token $token): AbstractExpression { return new NotUnary(parent::parse($parser, $expr, $token), $token->getLine()); } - public function getOperator(): string + public function getName(): string { return 'is not'; } diff --git a/src/Operator/Binary/SquareBracketBinaryOperator.php b/src/ExpressionParser/Infix/SquareBracketExpressionParser.php similarity index 74% rename from src/Operator/Binary/SquareBracketBinaryOperator.php rename to src/ExpressionParser/Infix/SquareBracketExpressionParser.php index c2e8b500652..1037dcb8193 100644 --- a/src/Operator/Binary/SquareBracketBinaryOperator.php +++ b/src/ExpressionParser/Infix/SquareBracketExpressionParser.php @@ -9,23 +9,26 @@ * file that was distributed with this source code. */ -namespace Twig\Operator\Binary; + namespace Twig\ExpressionParser\Infix; -use Twig\ExpressionParser; +use Twig\ExpressionParser\AbstractExpressionParser; +use Twig\ExpressionParser\InfixAssociativity; +use Twig\ExpressionParser\InfixExpressionParserInterface; use Twig\Node\Expression\AbstractExpression; use Twig\Node\Expression\ArrayExpression; use Twig\Node\Expression\ConstantExpression; use Twig\Node\Expression\GetAttrExpression; use Twig\Node\Nodes; -use Twig\Operator\AbstractOperator; -use Twig\Operator\OperatorArity; -use Twig\Operator\OperatorAssociativity; +use Twig\Parser; use Twig\Template; use Twig\Token; -class SquareBracketBinaryOperator extends AbstractOperator implements BinaryOperatorInterface +/** + * @internal + */ +final class SquareBracketExpressionParser extends AbstractExpressionParser implements InfixExpressionParserInterface { - public function parse(ExpressionParser $parser, AbstractExpression $expr, Token $token): AbstractExpression + public function parse(Parser $parser, AbstractExpression $expr, Token $token): AbstractExpression { $stream = $parser->getStream(); $lineno = $token->getLine(); @@ -65,7 +68,7 @@ public function parse(ExpressionParser $parser, AbstractExpression $expr, Token return new GetAttrExpression($expr, $attribute, $arguments, Template::ARRAY_CALL, $lineno); } - public function getOperator(): string + public function getName(): string { return '['; } @@ -75,13 +78,8 @@ public function getPrecedence(): int return 300; } - public function getArity(): OperatorArity - { - return OperatorArity::Binary; - } - - public function getAssociativity(): OperatorAssociativity + public function getAssociativity(): InfixAssociativity { - return OperatorAssociativity::Left; + return InfixAssociativity::Left; } } diff --git a/src/Operator/OperatorAssociativity.php b/src/ExpressionParser/InfixAssociativity.php similarity index 80% rename from src/Operator/OperatorAssociativity.php rename to src/ExpressionParser/InfixAssociativity.php index 638cdda1576..3aeccce4565 100644 --- a/src/Operator/OperatorAssociativity.php +++ b/src/ExpressionParser/InfixAssociativity.php @@ -9,9 +9,9 @@ * file that was distributed with this source code. */ -namespace Twig\Operator; +namespace Twig\ExpressionParser; -enum OperatorAssociativity +enum InfixAssociativity { case Left; case Right; diff --git a/src/ExpressionParser/InfixExpressionParserInterface.php b/src/ExpressionParser/InfixExpressionParserInterface.php new file mode 100644 index 00000000000..8d0ac674c83 --- /dev/null +++ b/src/ExpressionParser/InfixExpressionParserInterface.php @@ -0,0 +1,23 @@ + + */ +class PrecedenceChange +{ + public function __construct( + private string $package, + private string $version, + private int $newPrecedence, + ) { + } + + public function getPackage(): string + { + return $this->package; + } + + public function getVersion(): string + { + return $this->version; + } + + public function getNewPrecedence(): int + { + return $this->newPrecedence; + } +} diff --git a/src/Operator/Unary/ParenthesisUnaryOperator.php b/src/ExpressionParser/Prefix/GroupingExpressionParser.php similarity index 80% rename from src/Operator/Unary/ParenthesisUnaryOperator.php rename to src/ExpressionParser/Prefix/GroupingExpressionParser.php index 8191cb82045..ac9f6c9dbe3 100644 --- a/src/Operator/Unary/ParenthesisUnaryOperator.php +++ b/src/ExpressionParser/Prefix/GroupingExpressionParser.php @@ -9,20 +9,23 @@ * file that was distributed with this source code. */ -namespace Twig\Operator\Unary; +namespace Twig\ExpressionParser\Prefix; use Twig\Error\SyntaxError; -use Twig\ExpressionParser; +use Twig\ExpressionParser\AbstractExpressionParser; +use Twig\ExpressionParser\PrefixExpressionParserInterface; use Twig\Node\Expression\AbstractExpression; use Twig\Node\Expression\ListExpression; use Twig\Node\Expression\Variable\ContextVariable; -use Twig\Operator\AbstractOperator; -use Twig\Operator\OperatorArity; +use Twig\Parser; use Twig\Token; -class ParenthesisUnaryOperator extends AbstractOperator implements UnaryOperatorInterface +/** + * @internal + */ +final class GroupingExpressionParser extends AbstractExpressionParser implements PrefixExpressionParserInterface { - public function parse(ExpressionParser $parser, Token $token): AbstractExpression + public function parse(Parser $parser, Token $token): AbstractExpression { $stream = $parser->getStream(); $expr = $parser->parseExpression($this->getPrecedence()); @@ -57,7 +60,7 @@ public function parse(ExpressionParser $parser, Token $token): AbstractExpressio return new ListExpression($names, $token->getLine()); } - public function getOperator(): string + public function getName(): string { return '('; } @@ -66,9 +69,4 @@ public function getPrecedence(): int { return 0; } - - public function getArity(): OperatorArity - { - return OperatorArity::Unary; - } } diff --git a/src/ExpressionParser/Prefix/LiteralExpressionParser.php b/src/ExpressionParser/Prefix/LiteralExpressionParser.php new file mode 100644 index 00000000000..92540de75fd --- /dev/null +++ b/src/ExpressionParser/Prefix/LiteralExpressionParser.php @@ -0,0 +1,243 @@ +getStream(); + switch (true) { + case $token->test(Token::NAME_TYPE): + $stream->next(); + switch ($token->getValue()) { + case 'true': + case 'TRUE': + $this->type = 'constant'; + return new ConstantExpression(true, $token->getLine()); + + case 'false': + case 'FALSE': + $this->type = 'constant'; + return new ConstantExpression(false, $token->getLine()); + + case 'none': + case 'NONE': + case 'null': + case 'NULL': + $this->type = 'constant'; + return new ConstantExpression(null, $token->getLine()); + + default: + $this->type = 'variable'; + return new ContextVariable($token->getValue(), $token->getLine()); + } + + // no break + case $token->test(Token::NUMBER_TYPE): + $stream->next(); + $this->type = 'constant'; + + return new ConstantExpression($token->getValue(), $token->getLine()); + + case $token->test(Token::STRING_TYPE): + case $token->test(Token::INTERPOLATION_START_TYPE): + $this->type = 'string'; + + return $this->parseStringExpression($parser); + + case $token->test(Token::PUNCTUATION_TYPE): + // In 4.0, we should always return the node or throw an error for default + if ($node = match ($token->getValue()) { + '{' => $this->parseMappingExpression($parser), + default => null, + }) { + return $node; + } + + // no break + case $token->test(Token::OPERATOR_TYPE): + if ('[' === $token->getValue()) { + return $this->parseSequenceExpression($parser); + } + + if (preg_match(Lexer::REGEX_NAME, $token->getValue(), $matches) && $matches[0] == $token->getValue()) { + // in this context, string operators are variable names + $stream->next(); + $this->type = 'variable'; + + return new ContextVariable($token->getValue(), $token->getLine()); + } + + if ('=' === $token->getValue() && ('==' === $stream->look(-1)->getValue() || '!=' === $stream->look(-1)->getValue())) { + throw new SyntaxError(\sprintf('Unexpected operator of value "%s". Did you try to use "===" or "!==" for strict comparison? Use "is same as(value)" instead.', $token->getValue()), $token->getLine(), $stream->getSourceContext()); + } + + // no break + default: + throw new SyntaxError(\sprintf('Unexpected token "%s" of value "%s".', $token->toEnglish(), $token->getValue()), $token->getLine(), $stream->getSourceContext()); + } + } + + public function getName(): string + { + return $this->type; + } + + public function getPrecedence(): int + { + // not used + return 0; + } + + private function parseStringExpression(Parser $parser) + { + $stream = $parser->getStream(); + + $nodes = []; + // a string cannot be followed by another string in a single expression + $nextCanBeString = true; + while (true) { + if ($nextCanBeString && $token = $stream->nextIf(Token::STRING_TYPE)) { + $nodes[] = new ConstantExpression($token->getValue(), $token->getLine()); + $nextCanBeString = false; + } elseif ($stream->nextIf(Token::INTERPOLATION_START_TYPE)) { + $nodes[] = $parser->parseExpression(); + $stream->expect(Token::INTERPOLATION_END_TYPE); + $nextCanBeString = true; + } else { + break; + } + } + + $expr = array_shift($nodes); + foreach ($nodes as $node) { + $expr = new ConcatBinary($expr, $node, $node->getTemplateLine()); + } + + return $expr; + } + + private function parseSequenceExpression(Parser $parser) + { + $this->type = 'sequence'; + + $stream = $parser->getStream(); + $stream->expect(Token::OPERATOR_TYPE, '[', 'A sequence element was expected'); + + $node = new ArrayExpression([], $stream->getCurrent()->getLine()); + $first = true; + while (!$stream->test(Token::PUNCTUATION_TYPE, ']')) { + if (!$first) { + $stream->expect(Token::PUNCTUATION_TYPE, ',', 'A sequence element must be followed by a comma'); + + // trailing ,? + if ($stream->test(Token::PUNCTUATION_TYPE, ']')) { + break; + } + } + $first = false; + + if ($stream->nextIf(Token::SPREAD_TYPE)) { + $expr = $parser->parseExpression(); + $expr->setAttribute('spread', true); + $node->addElement($expr); + } else { + $node->addElement($parser->parseExpression()); + } + } + $stream->expect(Token::PUNCTUATION_TYPE, ']', 'An opened sequence is not properly closed'); + + return $node; + } + + private function parseMappingExpression(Parser $parser) + { + $this->type = 'mapping'; + + $stream = $parser->getStream(); + $stream->expect(Token::PUNCTUATION_TYPE, '{', 'A mapping element was expected'); + + $node = new ArrayExpression([], $stream->getCurrent()->getLine()); + $first = true; + while (!$stream->test(Token::PUNCTUATION_TYPE, '}')) { + if (!$first) { + $stream->expect(Token::PUNCTUATION_TYPE, ',', 'A mapping value must be followed by a comma'); + + // trailing ,? + if ($stream->test(Token::PUNCTUATION_TYPE, '}')) { + break; + } + } + $first = false; + + if ($stream->nextIf(Token::SPREAD_TYPE)) { + $value = $parser->parseExpression(); + $value->setAttribute('spread', true); + $node->addElement($value); + continue; + } + + // a mapping key can be: + // + // * a number -- 12 + // * a string -- 'a' + // * a name, which is equivalent to a string -- a + // * an expression, which must be enclosed in parentheses -- (1 + 2) + if ($token = $stream->nextIf(Token::NAME_TYPE)) { + $key = new ConstantExpression($token->getValue(), $token->getLine()); + + // {a} is a shortcut for {a:a} + if ($stream->test(Token::PUNCTUATION_TYPE, [',', '}'])) { + $value = new ContextVariable($key->getAttribute('value'), $key->getTemplateLine()); + $node->addElement($value, $key); + continue; + } + } elseif (($token = $stream->nextIf(Token::STRING_TYPE)) || $token = $stream->nextIf(Token::NUMBER_TYPE)) { + $key = new ConstantExpression($token->getValue(), $token->getLine()); + } elseif ($stream->test(Token::OPERATOR_TYPE, '(')) { + $key = $parser->parseExpression(); + } else { + $current = $stream->getCurrent(); + + throw new SyntaxError(\sprintf('A mapping key must be a quoted string, a number, a name, or an expression enclosed in parentheses (unexpected token "%s" of value "%s".', $current->toEnglish(), $current->getValue()), $current->getLine(), $stream->getSourceContext()); + } + + $stream->expect(Token::PUNCTUATION_TYPE, ':', 'A mapping key must be followed by a colon (:)'); + $value = $parser->parseExpression(); + + $node->addElement($value, $key); + } + $stream->expect(Token::PUNCTUATION_TYPE, '}', 'An opened mapping is not properly closed'); + + return $node; + } +} diff --git a/src/ExpressionParser/Prefix/UnaryOperatorExpressionParser.php b/src/ExpressionParser/Prefix/UnaryOperatorExpressionParser.php new file mode 100644 index 00000000000..4357d4ff6ab --- /dev/null +++ b/src/ExpressionParser/Prefix/UnaryOperatorExpressionParser.php @@ -0,0 +1,64 @@ + */ + private string $nodeClass, + private string $name, + private int $precedence, + private ?PrecedenceChange $precedenceChange = null, + private array $aliases = [], + ) { + } + + /** + * @return AbstractUnary + */ + public function parse(Parser $parser, Token $token): AbstractExpression + { + return new ($this->nodeClass)($parser->parseExpression($this->precedence), $token->getLine()); + } + + public function getName(): string + { + return $this->name; + } + + public function getPrecedence(): int + { + return $this->precedence; + } + + public function getPrecedenceChange(): ?PrecedenceChange + { + return $this->precedenceChange; + } + + public function getAliases(): array + { + return $this->aliases; + } +} diff --git a/src/Operator/Unary/UnaryOperatorInterface.php b/src/ExpressionParser/PrefixExpressionParserInterface.php similarity index 52% rename from src/Operator/Unary/UnaryOperatorInterface.php rename to src/ExpressionParser/PrefixExpressionParserInterface.php index 71c599acf65..587997c51a2 100644 --- a/src/Operator/Unary/UnaryOperatorInterface.php +++ b/src/ExpressionParser/PrefixExpressionParserInterface.php @@ -9,14 +9,13 @@ * file that was distributed with this source code. */ -namespace Twig\Operator\Unary; +namespace Twig\ExpressionParser; -use Twig\ExpressionParser; use Twig\Node\Expression\AbstractExpression; -use Twig\Operator\OperatorInterface; +use Twig\Parser; use Twig\Token; -interface UnaryOperatorInterface extends OperatorInterface +interface PrefixExpressionParserInterface extends ExpressionParserInterface { - public function parse(ExpressionParser $parser, Token $token): AbstractExpression; + public function parse(Parser $parser, Token $token): AbstractExpression; } diff --git a/src/Extension/AbstractExtension.php b/src/Extension/AbstractExtension.php index 02767f7c37c..351fb0698df 100644 --- a/src/Extension/AbstractExtension.php +++ b/src/Extension/AbstractExtension.php @@ -39,6 +39,11 @@ public function getFunctions() } public function getOperators() + { + return [[], []]; + } + + public function getExpressionParsers(): array { return []; } diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index 074a965f749..2b6a9f05878 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -16,8 +16,53 @@ use Twig\Error\LoaderError; use Twig\Error\RuntimeError; use Twig\Error\SyntaxError; +use Twig\ExpressionParser\Infix\ArrowExpressionParser; +use Twig\ExpressionParser\Infix\BinaryOperatorExpressionParser; +use Twig\ExpressionParser\Infix\ConditionalTernaryExpressionParser; +use Twig\ExpressionParser\Infix\DotExpressionParser; +use Twig\ExpressionParser\Infix\FilterExpressionParser; +use Twig\ExpressionParser\Infix\FunctionExpressionParser; +use Twig\ExpressionParser\Infix\IsExpressionParser; +use Twig\ExpressionParser\Infix\IsNotExpressionParser; +use Twig\ExpressionParser\Infix\SquareBracketExpressionParser; +use Twig\ExpressionParser\InfixAssociativity; +use Twig\ExpressionParser\PrecedenceChange; +use Twig\ExpressionParser\Prefix\GroupingExpressionParser; +use Twig\ExpressionParser\Prefix\LiteralExpressionParser; +use Twig\ExpressionParser\Prefix\UnaryOperatorExpressionParser; use Twig\Markup; use Twig\Node\Expression\AbstractExpression; +use Twig\Node\Expression\Binary\AddBinary; +use Twig\Node\Expression\Binary\AndBinary; +use Twig\Node\Expression\Binary\BitwiseAndBinary; +use Twig\Node\Expression\Binary\BitwiseOrBinary; +use Twig\Node\Expression\Binary\BitwiseXorBinary; +use Twig\Node\Expression\Binary\ConcatBinary; +use Twig\Node\Expression\Binary\DivBinary; +use Twig\Node\Expression\Binary\ElvisBinary; +use Twig\Node\Expression\Binary\EndsWithBinary; +use Twig\Node\Expression\Binary\EqualBinary; +use Twig\Node\Expression\Binary\FloorDivBinary; +use Twig\Node\Expression\Binary\GreaterBinary; +use Twig\Node\Expression\Binary\GreaterEqualBinary; +use Twig\Node\Expression\Binary\HasEveryBinary; +use Twig\Node\Expression\Binary\HasSomeBinary; +use Twig\Node\Expression\Binary\InBinary; +use Twig\Node\Expression\Binary\LessBinary; +use Twig\Node\Expression\Binary\LessEqualBinary; +use Twig\Node\Expression\Binary\MatchesBinary; +use Twig\Node\Expression\Binary\ModBinary; +use Twig\Node\Expression\Binary\MulBinary; +use Twig\Node\Expression\Binary\NotEqualBinary; +use Twig\Node\Expression\Binary\NotInBinary; +use Twig\Node\Expression\Binary\NullCoalesceBinary; +use Twig\Node\Expression\Binary\OrBinary; +use Twig\Node\Expression\Binary\PowerBinary; +use Twig\Node\Expression\Binary\RangeBinary; +use Twig\Node\Expression\Binary\SpaceshipBinary; +use Twig\Node\Expression\Binary\StartsWithBinary; +use Twig\Node\Expression\Binary\SubBinary; +use Twig\Node\Expression\Binary\XorBinary; use Twig\Node\Expression\BlockReferenceExpression; use Twig\Node\Expression\Filter\DefaultFilter; use Twig\Node\Expression\FunctionNode\EnumCasesFunction; @@ -31,50 +76,10 @@ use Twig\Node\Expression\Test\NullTest; use Twig\Node\Expression\Test\OddTest; 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\Node; -use Twig\Operator\Binary\AddBinaryOperator; -use Twig\Operator\Binary\AndBinaryOperator; -use Twig\Operator\Binary\ArrowBinaryOperator; -use Twig\Operator\Binary\BitwiseAndBinaryOperator; -use Twig\Operator\Binary\BitwiseOrBinaryOperator; -use Twig\Operator\Binary\BitwiseXorBinaryOperator; -use Twig\Operator\Binary\ConcatBinaryOperator; -use Twig\Operator\Binary\DivBinaryOperator; -use Twig\Operator\Binary\DotBinaryOperator; -use Twig\Operator\Binary\ElvisBinaryOperator; -use Twig\Operator\Binary\EndsWithBinaryOperator; -use Twig\Operator\Binary\EqualBinaryOperator; -use Twig\Operator\Binary\FilterBinaryOperator; -use Twig\Operator\Binary\FloorDivBinaryOperator; -use Twig\Operator\Binary\FunctionBinaryOperator; -use Twig\Operator\Binary\GreaterBinaryOperator; -use Twig\Operator\Binary\GreaterEqualBinaryOperator; -use Twig\Operator\Binary\HasEveryBinaryOperator; -use Twig\Operator\Binary\HasSomeBinaryOperator; -use Twig\Operator\Binary\InBinaryOperator; -use Twig\Operator\Binary\IsBinaryOperator; -use Twig\Operator\Binary\IsNotBinaryOperator; -use Twig\Operator\Binary\LessBinaryOperator; -use Twig\Operator\Binary\LessEqualBinaryOperator; -use Twig\Operator\Binary\MatchesBinaryOperator; -use Twig\Operator\Binary\ModBinaryOperator; -use Twig\Operator\Binary\MulBinaryOperator; -use Twig\Operator\Binary\NotEqualBinaryOperator; -use Twig\Operator\Binary\NotInBinaryOperator; -use Twig\Operator\Binary\NullCoalesceBinaryOperator; -use Twig\Operator\Binary\OrBinaryOperator; -use Twig\Operator\Binary\PowerBinaryOperator; -use Twig\Operator\Binary\RangeBinaryOperator; -use Twig\Operator\Binary\SpaceshipBinaryOperator; -use Twig\Operator\Binary\SquareBracketBinaryOperator; -use Twig\Operator\Binary\StartsWithBinaryOperator; -use Twig\Operator\Binary\SubBinaryOperator; -use Twig\Operator\Binary\XorBinaryOperator; -use Twig\Operator\Ternary\ConditionalTernaryOperator; -use Twig\Operator\Unary\NegUnaryOperator; -use Twig\Operator\Unary\NotUnaryOperator; -use Twig\Operator\Unary\ParenthesisUnaryOperator; -use Twig\Operator\Unary\PosUnaryOperator; use Twig\Parser; use Twig\Sandbox\SecurityNotAllowedMethodError; use Twig\Sandbox\SecurityNotAllowedPropertyError; @@ -320,54 +325,58 @@ public function getNodeVisitors(): array return []; } - public function getOperators(): array + public function getExpressionParsers(): array { return [ - new NotUnaryOperator(), - new NegUnaryOperator(), - new PosUnaryOperator(), - new ParenthesisUnaryOperator(), - - new ElvisBinaryOperator(), - new NullCoalesceBinaryOperator(), - new OrBinaryOperator(), - new XorBinaryOperator(), - new AndBinaryOperator(), - new BitwiseOrBinaryOperator(), - new BitwiseXorBinaryOperator(), - new BitwiseAndBinaryOperator(), - new EqualBinaryOperator(), - new NotEqualBinaryOperator(), - new SpaceshipBinaryOperator(), - new LessBinaryOperator(), - new GreaterBinaryOperator(), - new GreaterEqualBinaryOperator(), - new LessEqualBinaryOperator(), - new NotInBinaryOperator(), - new InBinaryOperator(), - new MatchesBinaryOperator(), - new StartsWithBinaryOperator(), - new EndsWithBinaryOperator(), - new HasSomeBinaryOperator(), - new HasEveryBinaryOperator(), - new RangeBinaryOperator(), - new AddBinaryOperator(), - new SubBinaryOperator(), - new ConcatBinaryOperator(), - new MulBinaryOperator(), - new DivBinaryOperator(), - new FloorDivBinaryOperator(), - new ModBinaryOperator(), - new IsBinaryOperator(), - new IsNotBinaryOperator(), - new PowerBinaryOperator(), - new FilterBinaryOperator(), - new DotBinaryOperator(), - new SquareBracketBinaryOperator(), - new FunctionBinaryOperator(), - new ArrowBinaryOperator(), - - new ConditionalTernaryOperator(), + new UnaryOperatorExpressionParser(NotUnary::class, 'not', 50, new PrecedenceChange('twig/twig', '3.15', 70)), + new UnaryOperatorExpressionParser(NegUnary::class, '-', 500), + new UnaryOperatorExpressionParser(PosUnary::class, '+', 500), + + new BinaryOperatorExpressionParser(ElvisBinary::class, '?:', 5, InfixAssociativity::Right, aliases: ['? :']), + new BinaryOperatorExpressionParser(NullCoalesceBinary::class, '??', 300, InfixAssociativity::Right, new PrecedenceChange('twig/twig', '3.15', 5)), + new BinaryOperatorExpressionParser(OrBinary::class, 'or', 10), + new BinaryOperatorExpressionParser(XorBinary::class, 'xor', 12), + new BinaryOperatorExpressionParser(AndBinary::class, 'and', 15), + new BinaryOperatorExpressionParser(BitwiseOrBinary::class, 'b-or', 16), + new BinaryOperatorExpressionParser(BitwiseXorBinary::class, 'b-xor', 17), + new BinaryOperatorExpressionParser(BitwiseAndBinary::class, 'b-and', 18), + new BinaryOperatorExpressionParser(EqualBinary::class, '==', 20), + new BinaryOperatorExpressionParser(NotEqualBinary::class, '!=', 20), + new BinaryOperatorExpressionParser(SpaceshipBinary::class, '<=>', 20), + new BinaryOperatorExpressionParser(LessBinary::class, '<', 20), + new BinaryOperatorExpressionParser(GreaterBinary::class, '>', 20), + new BinaryOperatorExpressionParser(GreaterEqualBinary::class, '>=', 20), + new BinaryOperatorExpressionParser(LessEqualBinary::class, '<=', 20), + new BinaryOperatorExpressionParser(NotInBinary::class, 'not in', 20), + new BinaryOperatorExpressionParser(InBinary::class, 'in', 20), + new BinaryOperatorExpressionParser(MatchesBinary::class, 'matches', 20), + new BinaryOperatorExpressionParser(StartsWithBinary::class, 'starts with', 20), + new BinaryOperatorExpressionParser(EndsWithBinary::class, 'ends with', 20), + new BinaryOperatorExpressionParser(HasSomeBinary::class, 'has some', 20), + new BinaryOperatorExpressionParser(HasEveryBinary::class, 'has every', 20), + new BinaryOperatorExpressionParser(RangeBinary::class, '..', 25), + new BinaryOperatorExpressionParser(AddBinary::class, '+', 30), + new BinaryOperatorExpressionParser(SubBinary::class, '-', 30), + new BinaryOperatorExpressionParser(ConcatBinary::class, '~', 40, precedenceChange: new PrecedenceChange('twig/twig', '3.15', 27)), + new BinaryOperatorExpressionParser(MulBinary::class, '*', 60), + new BinaryOperatorExpressionParser(DivBinary::class, '/', 60), + new BinaryOperatorExpressionParser(FloorDivBinary::class, '//', 60), + new BinaryOperatorExpressionParser(ModBinary::class, '%', 60), + new BinaryOperatorExpressionParser(PowerBinary::class, '**', 200, InfixAssociativity::Right), + + new ConditionalTernaryExpressionParser(), + + new IsExpressionParser(), + new IsNotExpressionParser(), + new DotExpressionParser(), + new SquareBracketExpressionParser(), + + new GroupingExpressionParser(), + new FilterExpressionParser(), + new FunctionExpressionParser(), + new ArrowExpressionParser(), + + new LiteralExpressionParser(), ]; } diff --git a/src/Extension/ExtensionInterface.php b/src/Extension/ExtensionInterface.php index 7eef100f904..44356f62769 100644 --- a/src/Extension/ExtensionInterface.php +++ b/src/Extension/ExtensionInterface.php @@ -11,8 +11,9 @@ namespace Twig\Extension; +use Twig\ExpressionParser\ExpressionParserInterface; +use Twig\ExpressionParser\PrecedenceChange; use Twig\NodeVisitor\NodeVisitorInterface; -use Twig\Operator\OperatorInterface; use Twig\TokenParser\TokenParserInterface; use Twig\TwigFilter; use Twig\TwigFunction; @@ -22,6 +23,8 @@ * Interface implemented by extension classes. * * @author Fabien Potencier + * + * @method array getExpressionParsers() */ interface ExtensionInterface { @@ -63,11 +66,11 @@ public function getFunctions(); /** * Returns a list of operators to add to the existing list. * - * @return OperatorInterface[]|array + * @return array * - * @psalm-return OperatorInterface[]|array{ - * array}>, - * array, associativity: ExpressionParser::OPERATOR_*}> + * @psalm-return array{ + * array}>, + * array, associativity: ExpressionParser::OPERATOR_*}> * } */ public function getOperators(); diff --git a/src/ExtensionSet.php b/src/ExtensionSet.php index ad6ee7d0713..262d1262579 100644 --- a/src/ExtensionSet.php +++ b/src/ExtensionSet.php @@ -12,19 +12,18 @@ namespace Twig; use Twig\Error\RuntimeError; +use Twig\ExpressionParser\ExpressionParsers; +use Twig\ExpressionParser\Infix\BinaryOperatorExpressionParser; +use Twig\ExpressionParser\InfixAssociativity; +use Twig\ExpressionParser\InfixExpressionParserInterface; +use Twig\ExpressionParser\PrecedenceChange; +use Twig\ExpressionParser\Prefix\UnaryOperatorExpressionParser; use Twig\Extension\ExtensionInterface; use Twig\Extension\GlobalsInterface; use Twig\Extension\LastModifiedExtensionInterface; use Twig\Extension\StagingExtension; use Twig\Node\Expression\AbstractExpression; use Twig\NodeVisitor\NodeVisitorInterface; -use Twig\Operator\Binary\AbstractBinaryOperator; -use Twig\Operator\Binary\BinaryOperatorInterface; -use Twig\Operator\OperatorAssociativity; -use Twig\Operator\OperatorInterface; -use Twig\Operator\Operators; -use Twig\Operator\Unary\AbstractUnaryOperator; -use Twig\Operator\Unary\UnaryOperatorInterface; use Twig\TokenParser\TokenParserInterface; /** @@ -52,8 +51,7 @@ final class ExtensionSet private $functions; /** @var array */ private $dynamicFunctions; - /** @var Operators */ - private $operators; + private ExpressionParsers $expressionParsers; /** @var array|null */ private $globals; /** @var array */ @@ -410,13 +408,13 @@ public function getTest(string $name): ?TwigTest return null; } - public function getOperators(): Operators + public function getExpressionParsers(): ExpressionParsers { if (!$this->initialized) { $this->initExtensions(); } - return $this->operators; + return $this->expressionParsers; } private function initExtensions(): void @@ -429,7 +427,7 @@ private function initExtensions(): void $this->dynamicFunctions = []; $this->dynamicTests = []; $this->visitors = []; - $this->operators = new Operators(); + $this->expressionParsers = new ExpressionParsers(); foreach ($this->extensions as $extension) { $this->initExtension($extension); @@ -479,119 +477,65 @@ private function initExtension(ExtensionInterface $extension): void $this->visitors[] = $visitor; } - // operators - if ($operators = $extension->getOperators()) { - if (!\is_array($operators)) { - throw new \InvalidArgumentException(\sprintf('"%s::getOperators()" must return an array with operators, got "%s".', \get_class($extension), get_debug_type($operators).(\is_resource($operators) ? '' : '#'.$operators))); - } - - // new signature? - $legacy = false; - foreach ($operators as $op) { - if (!$op instanceof OperatorInterface) { - $legacy = true; + // expression parsers + if (method_exists($extension, 'getExpressionParsers')) { + $this->expressionParsers->add($extension->getExpressionParsers()); + } - break; - } - } + $operators = $extension->getOperators(); + if (!\is_array($operators)) { + throw new \InvalidArgumentException(\sprintf('"%s::getOperators()" must return an array with operators, got "%s".', \get_class($extension), get_debug_type($operators).(\is_resource($operators) ? '' : '#'.$operators))); + } - if ($legacy) { - if (2 !== \count($operators)) { - throw new \InvalidArgumentException(\sprintf('"%s::getOperators()" must return an array of 2 elements, got %d.', \get_class($extension), \count($operators))); - } + if (2 !== \count($operators)) { + throw new \InvalidArgumentException(\sprintf('"%s::getOperators()" must return an array of 2 elements, got %d.', \get_class($extension), \count($operators))); + } - trigger_deprecation('twig/twig', '3.20', \sprintf('Extension "%s" uses the old signature for "getOperators()", please update it to return an array of "OperatorInterface" objects.', \get_class($extension))); + $expressionParsers = []; + foreach ($operators[0] as $operator => $op) { + $expressionParsers[] = new UnaryOperatorExpressionParser($op['class'], $operator, $op['precedence'], $op['precedence_change'] ?? null, $op['aliases'] ?? []); + } + foreach ($operators[1] as $operator => $op) { + $op['associativity'] = match ($op['associativity']) { + 1 => InfixAssociativity::Left, + 2 => InfixAssociativity::Right, + default => throw new \InvalidArgumentException(\sprintf('Invalid associativity "%s" for operator "%s".', $op['associativity'], $operator)), + }; - $ops = []; - foreach ($operators[0] as $n => $op) { - $ops[] = $op instanceof OperatorInterface ? $op : $this->convertUnaryOperators($n, $op); - } - foreach ($operators[1] as $n => $op) { - $ops[] = $op instanceof OperatorInterface ? $op : $this->convertBinaryOperators($n, $op); - } - $this->operators->add($ops); + if ($op['callable']) { + $expressionParsers[] = $this->convertInfixExpressionParser($op['class'], $operator, $op['precedence'], $op['associativity'], $op['precedence_change'] ?? null, $op['aliases'] ?? [], $op['callable']); } else { - $this->operators->add($operators); + $expressionParsers[] = new BinaryOperatorExpressionParser($op['class'], $operator, $op['precedence'], $op['associativity'], $op['precedence_change'] ?? null, $op['aliases'] ?? []); } } - } - private function convertUnaryOperators(string $n, array $op): OperatorInterface - { - trigger_deprecation('twig/twig', '3.20', \sprintf('Using a non-OperatorInterface object to define the "%s" unary operator is deprecated.', $n)); - - return new class($op, $n) extends AbstractUnaryOperator implements UnaryOperatorInterface { - public function __construct(private array $op, private string $operator) - { - } - - public function getOperator(): string - { - return $this->operator; - } - - public function getPrecedence(): int - { - return $this->op['precedence']; - } + if (count($expressionParsers)) { + trigger_deprecation('twig/twig', '3.20', \sprintf('Extension "%s" uses the old signature for "getOperators()", please implement "getExpressionParsers()" instead.', \get_class($extension))); - public function getPrecedenceChange(): ?OperatorPrecedenceChange - { - return $this->op['precedence_change'] ?? null; - } - - protected function getNodeClass(): string - { - return $this->op['class'] ?? ''; - } - }; + $this->expressionParsers->add($expressionParsers); + } } - private function convertBinaryOperators(string $n, array $op): OperatorInterface + private function convertInfixExpressionParser(string $nodeClass, string $operator, int $precedence, InfixAssociativity $associativity, ?PrecedenceChange $precedenceChange, array $aliases, callable $callable): InfixExpressionParserInterface { - trigger_deprecation('twig/twig', '3.20', \sprintf('Using a non-OperatorInterface object to define the "%s" binary operator is deprecated.', $n)); - - return new class($op, $n) extends AbstractBinaryOperator implements BinaryOperatorInterface { - public function __construct(private array $op, private string $operator) - { - } - - public function getOperator(): string - { - return $this->operator; - } + trigger_deprecation('twig/twig', '3.20', \sprintf('Using a non-ExpressionParserInterface object to define the "%s" binary operator is deprecated.', $operator)); - public function getPrecedence(): int - { - return $this->op['precedence']; + return new class($nodeClass, $operator, $precedence, $associativity, $precedenceChange, $aliases, $callable) extends BinaryOperatorExpressionParser { + public function __construct( + string $nodeClass, + string $operator, + int $precedence, + InfixAssociativity $associativity = InfixAssociativity::Left, + ?PrecedenceChange $precedenceChange = null, + array $aliases = [], + private $callable = null, + ) { + parent::__construct($nodeClass, $operator, $precedence, $associativity, $precedenceChange, $aliases); } - public function getPrecedenceChange(): ?OperatorPrecedenceChange + public function parse(Parser $parser, AbstractExpression $expr, Token $token): AbstractExpression { - return $this->op['precedence_change'] ?? null; - } - - protected function getNodeClass(): string - { - return $this->op['class'] ?? ''; - } - - public function getAssociativity(): OperatorAssociativity - { - return match ($this->op['associativity']) { - 1 => OperatorAssociativity::Left, - 2 => OperatorAssociativity::Right, - default => throw new \InvalidArgumentException(\sprintf('Invalid associativity "%s" for operator "%s".', $this->op['associativity'], $this->getOperator())), - }; - } - - public function parse(ExpressionParser $parser, AbstractExpression $expr, Token $token): AbstractExpression - { - if ($this->op['callable']) { - return $this->op['callable']($parser, $expr); - } - - return parent::parse($parser, $expr, $token); + return ($this->callable)($parser, $expr); } }; } diff --git a/src/Lexer.php b/src/Lexer.php index 84b29de32a7..26d8fa42437 100644 --- a/src/Lexer.php +++ b/src/Lexer.php @@ -535,25 +535,25 @@ private function moveCursor($text): void private function getOperatorRegex(): string { - $operators = ['=']; - foreach ($this->env->getOperators() as $operator) { - $operators = array_merge($operators, [$operator->getOperator()], $operator->getAliases()); + $expressionParsers = ['=']; + foreach ($this->env->getExpressionParsers() as $expressionParser) { + $expressionParsers = array_merge($expressionParsers, [$expressionParser->getName()], $expressionParser->getAliases()); } - $operators = array_combine($operators, array_map('strlen', $operators)); - arsort($operators); + $expressionParsers = array_combine($expressionParsers, array_map('strlen', $expressionParsers)); + arsort($expressionParsers); $regex = []; - foreach ($operators as $operator => $length) { + foreach ($expressionParsers as $expressionParser => $length) { // an operator that ends with a character must be followed by // a whitespace, a parenthesis, an opening map [ or sequence { - $r = preg_quote($operator, '/'); - if (ctype_alpha($operator[$length - 1])) { + $r = preg_quote($expressionParser, '/'); + if (ctype_alpha($expressionParser[$length - 1])) { $r .= '(?=[\s()\[{])'; } // an operator that begins with a character must not have a dot or pipe before - if (ctype_alpha($operator[0])) { + if (ctype_alpha($expressionParser[0])) { $r = '(?getTemplateLine(), $item->getSourceContext()); - } - } - parent::__construct($items, [], $lineno); } diff --git a/src/Operator/Binary/AbstractBinaryOperator.php b/src/Operator/Binary/AbstractBinaryOperator.php deleted file mode 100644 index 64fc9033991..00000000000 --- a/src/Operator/Binary/AbstractBinaryOperator.php +++ /dev/null @@ -1,44 +0,0 @@ -parseExpression(OperatorAssociativity::Left === $this->getAssociativity() ? $this->getPrecedence() + 1 : $this->getPrecedence()); - - return new ($this->getNodeClass())($left, $right, $token->getLine()); - } - - public function getArity(): OperatorArity - { - return OperatorArity::Binary; - } - - public function getAssociativity(): OperatorAssociativity - { - return OperatorAssociativity::Left; - } - - /** - * @return class-string - */ - abstract protected function getNodeClass(): string; -} diff --git a/src/Operator/Binary/AddBinaryOperator.php b/src/Operator/Binary/AddBinaryOperator.php deleted file mode 100644 index 7e708c38004..00000000000 --- a/src/Operator/Binary/AddBinaryOperator.php +++ /dev/null @@ -1,32 +0,0 @@ -'; - } - - public function getPrecedence(): int - { - return 20; - } - - protected function getNodeClass(): string - { - return GreaterBinary::class; - } -} diff --git a/src/Operator/Binary/GreaterEqualBinaryOperator.php b/src/Operator/Binary/GreaterEqualBinaryOperator.php deleted file mode 100644 index 69a5ad203bb..00000000000 --- a/src/Operator/Binary/GreaterEqualBinaryOperator.php +++ /dev/null @@ -1,32 +0,0 @@ -='; - } - - public function getPrecedence(): int - { - return 20; - } -} diff --git a/src/Operator/Binary/HasEveryBinaryOperator.php b/src/Operator/Binary/HasEveryBinaryOperator.php deleted file mode 100644 index 1312640aed0..00000000000 --- a/src/Operator/Binary/HasEveryBinaryOperator.php +++ /dev/null @@ -1,32 +0,0 @@ -'; - } - - public function getPrecedence(): int - { - return 20; - } - - protected function getNodeClass(): string - { - return SpaceshipBinary::class; - } -} diff --git a/src/Operator/Binary/StartsWithBinaryOperator.php b/src/Operator/Binary/StartsWithBinaryOperator.php deleted file mode 100644 index 4d543454d0a..00000000000 --- a/src/Operator/Binary/StartsWithBinaryOperator.php +++ /dev/null @@ -1,32 +0,0 @@ - - */ -final class Operators implements \IteratorAggregate -{ - /** - * @var array, array> - */ - private array $operators = []; - - /** - * @var array, array> - */ - private array $aliases = []; - - /** - * @var \WeakMap>|null - */ - private ?\WeakMap $precedenceChanges = null; - - /** - * @param array $operators - */ - public function __construct( - array $operators = [], - ) { - $this->add($operators); - } - - /** - * @param array $operators - * - * @return $this - */ - public function add(array $operators): self - { - $this->precedenceChanges = null; - foreach ($operators as $operator) { - $this->operators[$operator->getArity()->value][$operator->getOperator()] = $operator; - foreach ($operator->getAliases() as $alias) { - $this->aliases[$operator->getArity()->value][$alias] = $operator; - } - } - - return $this; - } - - public function getUnary(string $name): ?UnaryOperatorInterface - { - return $this->operators[OperatorArity::Unary->value][$name] ?? ($this->aliases[OperatorArity::Unary->value][$name] ?? null); - } - - public function getBinary(string $name): ?BinaryOperatorInterface - { - return $this->operators[OperatorArity::Binary->value][$name] ?? ($this->aliases[OperatorArity::Binary->value][$name] ?? null); - } - - public function getTernary(string $name): ?TernaryOperatorInterface - { - return $this->operators[OperatorArity::Ternary->value][$name] ?? ($this->aliases[OperatorArity::Ternary->value][$name] ?? null); - } - - public function getIterator(): \Traversable - { - foreach ($this->operators as $operators) { - // we don't yield the keys - yield from $operators; - } - } - - /** - * @internal - * - * @return \WeakMap> - */ - public function getPrecedenceChanges(): \WeakMap - { - if (null === $this->precedenceChanges) { - $this->precedenceChanges = new \WeakMap(); - foreach ($this as $op) { - if (!$op->getPrecedenceChange()) { - continue; - } - $min = min($op->getPrecedenceChange()->getNewPrecedence(), $op->getPrecedence()); - $max = max($op->getPrecedenceChange()->getNewPrecedence(), $op->getPrecedence()); - foreach ($this as $o) { - if ($o->getPrecedence() > $min && $o->getPrecedence() < $max) { - if (!isset($this->precedenceChanges[$o])) { - $this->precedenceChanges[$o] = []; - } - $this->precedenceChanges[$o][] = $op; - } - } - } - } - - return $this->precedenceChanges; - } -} diff --git a/src/Operator/Ternary/AbstractTernaryOperator.php b/src/Operator/Ternary/AbstractTernaryOperator.php deleted file mode 100644 index 3a88247ff30..00000000000 --- a/src/Operator/Ternary/AbstractTernaryOperator.php +++ /dev/null @@ -1,29 +0,0 @@ -getNodeClass())($parser->parseExpression($this->getPrecedence()), $token->getLine()); - } - - public function getArity(): OperatorArity - { - return OperatorArity::Unary; - } - - /** - * @return class-string - */ - abstract protected function getNodeClass(): string; -} diff --git a/src/Operator/Unary/NegUnaryOperator.php b/src/Operator/Unary/NegUnaryOperator.php deleted file mode 100644 index de01a3b5176..00000000000 --- a/src/Operator/Unary/NegUnaryOperator.php +++ /dev/null @@ -1,32 +0,0 @@ - + * + * @deprecated since Twig 1.20 Use Twig\ExpressionParser\PrecedenceChange instead */ -class OperatorPrecedenceChange +class OperatorPrecedenceChange extends PrecedenceChange { public function __construct( private string $package, private string $version, private int $newPrecedence, ) { - } - - public function getPackage(): string - { - return $this->package; - } - - public function getVersion(): string - { - return $this->version; - } + trigger_deprecation('twig/twig', '3.20', 'The "%s" class is deprecated since Twig 3.20. Use "%s" instead.', self::class, PrecedenceChange::class); - public function getNewPrecedence(): int - { - return $this->newPrecedence; + parent::__construct($package, $version, $newPrecedence); } } diff --git a/src/Parser.php b/src/Parser.php index c2468cffe91..1ddbae9813f 100644 --- a/src/Parser.php +++ b/src/Parser.php @@ -13,6 +13,10 @@ namespace Twig; use Twig\Error\SyntaxError; +use Twig\ExpressionParser\ExpressionParserInterface; +use Twig\ExpressionParser\ExpressionParsers; +use Twig\ExpressionParser\Prefix\LiteralExpressionParser; +use Twig\ExpressionParser\PrefixExpressionParserInterface; use Twig\Node\BlockNode; use Twig\Node\BlockReferenceNode; use Twig\Node\BodyNode; @@ -49,10 +53,12 @@ class Parser private $embeddedTemplates = []; private $varNameSalt = 0; private $ignoreUnknownTwigCallables = false; + private ExpressionParsers $parsers; public function __construct( private Environment $env, ) { + $this->parsers = $env->getExpressionParsers(); } public function getEnvironment(): Environment @@ -78,10 +84,6 @@ public function parse(TokenStream $stream, $test = null, bool $dropNeedle = fals $this->visitors = $this->env->getNodeVisitors(); } - if (null === $this->expressionParser) { - $this->expressionParser = new ExpressionParser($this, $this->env); - } - $this->stream = $stream; $this->parent = null; $this->blocks = []; @@ -155,7 +157,7 @@ public function subparse($test, bool $dropNeedle = false): Node case $this->stream->getCurrent()->test(Token::VAR_START_TYPE): $token = $this->stream->next(); - $expr = $this->expressionParser->parseExpression(); + $expr = $this->parseExpression(); $this->stream->expect(Token::VAR_END_TYPE); $rv[] = new PrintNode($expr, $token->getLine()); break; @@ -337,11 +339,42 @@ public function popLocalScope(): void array_shift($this->importedSymbols); } + /** + * @deprecated since Twig 3.20 + */ public function getExpressionParser(): ExpressionParser { + trigger_deprecation('twig/twig', '3.20', 'Method "%s()" is deprecated, use "parseExpression()" instead.', __METHOD__); + + if (null === $this->expressionParser) { + $this->expressionParser = new ExpressionParser($this, $this->env); + } + return $this->expressionParser; } + public function parseExpression(int $precedence = 0): AbstractExpression + { + $token = $this->getCurrentToken(); + if ($token->test(Token::OPERATOR_TYPE) && $ep = $this->parsers->getPrefix($token->getValue())) { + $this->getStream()->next(); + $expr = $ep->parse($this, $token); + $this->checkPrecedenceDeprecations($ep, $expr); + } else { + $expr = $this->parsers->getPrefixByClass(LiteralExpressionParser::class)->parse($this, $token); + } + + $token = $this->getCurrentToken(); + while ($token->test(Token::OPERATOR_TYPE) && ($ep = $this->parsers->getInfix($token->getValue())) && $ep->getPrecedence() >= $precedence) { + $this->getStream()->next(); + $expr = $ep->parse($this, $expr, $token); + $this->checkPrecedenceDeprecations($ep, $expr); + $token = $this->getCurrentToken(); + } + + return $expr; + } + public function getParent(): ?Node { trigger_deprecation('twig/twig', '3.12', 'Method "%s()" is deprecated.', __METHOD__); @@ -519,4 +552,42 @@ private function filterBodyNodes(Node $node, bool $nested = false): ?Node return $node; } + + private function checkPrecedenceDeprecations(ExpressionParserInterface $expressionParser, AbstractExpression $expr) + { + $expr->setAttribute('expression_parser', $expressionParser); + $precedenceChanges = $this->parsers->getPrecedenceChanges(); + + // Check that the all nodes that are between the 2 precedences have explicit parentheses + if (!isset($precedenceChanges[$expressionParser])) { + return; + } + + if ($expressionParser instanceof PrefixExpressionParserInterface) { + if ($expr->hasExplicitParentheses()) { + return; + } + /** @var AbstractExpression $node */ + $node = $expr->getNode('node'); + foreach ($precedenceChanges as $ep => $changes) { + if (!\in_array($expressionParser, $changes, true)) { + continue; + } + if ($node->hasAttribute('expression_parser') && $ep === $node->getAttribute('expression_parser')) { + $change = $expressionParser->getPrecedenceChange(); + trigger_deprecation($change->getPackage(), $change->getVersion(), \sprintf('Add explicit parentheses around the "%s" unary operator to avoid behavior change in the next major version as its precedence will change in "%s" at line %d.', $expressionParser->getName(), $this->getStream()->getSourceContext()->getName(), $node->getTemplateLine())); + } + } + } else { + foreach ($precedenceChanges[$expressionParser] as $ep) { + foreach ($expr as $node) { + /** @var AbstractExpression $node */ + if ($node->hasAttribute('expression_parser') && $ep === $node->getAttribute('expression_parser') && !$node->hasExplicitParentheses()) { + $change = $ep->getPrecedenceChange(); + trigger_deprecation($change->getPackage(), $change->getVersion(), \sprintf('Add explicit parentheses around the "%s" binary operator to avoid behavior change in the next major version as its precedence will change in "%s" at line %d.', $ep->getName(), $this->getStream()->getSourceContext()->getName(), $node->getTemplateLine())); + } + } + } + } + } } diff --git a/src/TokenParser/AbstractTokenParser.php b/src/TokenParser/AbstractTokenParser.php index 30bef15a340..8acaa6f56e9 100644 --- a/src/TokenParser/AbstractTokenParser.php +++ b/src/TokenParser/AbstractTokenParser.php @@ -36,8 +36,6 @@ public function setParser(Parser $parser): void /** * Parses an assignment expression like "a, b". - * - * @return Nodes */ protected function parseAssignmentExpression(): Nodes { diff --git a/src/TokenParser/ApplyTokenParser.php b/src/TokenParser/ApplyTokenParser.php index 68ef7c17e6b..e4e3cfaebf0 100644 --- a/src/TokenParser/ApplyTokenParser.php +++ b/src/TokenParser/ApplyTokenParser.php @@ -11,6 +11,7 @@ namespace Twig\TokenParser; +use Twig\ExpressionParser\Infix\FilterExpressionParser; use Twig\Node\Expression\Variable\LocalVariable; use Twig\Node\Node; use Twig\Node\Nodes; @@ -34,9 +35,9 @@ public function parse(Token $token): Node $lineno = $token->getLine(); $ref = new LocalVariable(null, $lineno); $filter = $ref; - $op = $this->parser->getEnvironment()->getOperators()->getBinary('|'); + $op = $this->parser->getEnvironment()->getExpressionParsers()->getInfixByClass(FilterExpressionParser::class); while (true) { - $filter = $op->parse($this->parser->getExpressionParser(), $filter, $this->parser->getCurrentToken()); + $filter = $op->parse($this->parser, $filter, $this->parser->getCurrentToken()); if (!$this->parser->getStream()->test(Token::OPERATOR_TYPE, '|')) { break; } diff --git a/src/TokenParser/AutoEscapeTokenParser.php b/src/TokenParser/AutoEscapeTokenParser.php index b50b29e659e..86feb27e621 100644 --- a/src/TokenParser/AutoEscapeTokenParser.php +++ b/src/TokenParser/AutoEscapeTokenParser.php @@ -32,7 +32,7 @@ public function parse(Token $token): Node if ($stream->test(Token::BLOCK_END_TYPE)) { $value = 'html'; } else { - $expr = $this->parser->getExpressionParser()->parseExpression(); + $expr = $this->parser->parseExpression(); if (!$expr instanceof ConstantExpression) { throw new SyntaxError('An escaping strategy must be a string or false.', $stream->getCurrent()->getLine(), $stream->getSourceContext()); } diff --git a/src/TokenParser/BlockTokenParser.php b/src/TokenParser/BlockTokenParser.php index 3561b99cdd7..452b323e533 100644 --- a/src/TokenParser/BlockTokenParser.php +++ b/src/TokenParser/BlockTokenParser.php @@ -53,7 +53,7 @@ public function parse(Token $token): Node } } else { $body = new Nodes([ - new PrintNode($this->parser->getExpressionParser()->parseExpression(), $lineno), + new PrintNode($this->parser->parseExpression(), $lineno), ]); } $stream->expect(Token::BLOCK_END_TYPE); diff --git a/src/TokenParser/DeprecatedTokenParser.php b/src/TokenParser/DeprecatedTokenParser.php index 164ef26eec3..df1ba381f44 100644 --- a/src/TokenParser/DeprecatedTokenParser.php +++ b/src/TokenParser/DeprecatedTokenParser.php @@ -33,8 +33,7 @@ final class DeprecatedTokenParser extends AbstractTokenParser public function parse(Token $token): Node { $stream = $this->parser->getStream(); - $expressionParser = $this->parser->getExpressionParser(); - $expr = $expressionParser->parseExpression(); + $expr = $this->parser->parseExpression(); $node = new DeprecatedNode($expr, $token->getLine()); while ($stream->test(Token::NAME_TYPE)) { @@ -44,10 +43,10 @@ public function parse(Token $token): Node switch ($k) { case 'package': - $node->setNode('package', $expressionParser->parseExpression()); + $node->setNode('package', $this->parser->parseExpression()); break; case 'version': - $node->setNode('version', $expressionParser->parseExpression()); + $node->setNode('version', $this->parser->parseExpression()); break; default: throw new SyntaxError(\sprintf('Unknown "%s" option.', $k), $stream->getCurrent()->getLine(), $stream->getSourceContext()); diff --git a/src/TokenParser/DoTokenParser.php b/src/TokenParser/DoTokenParser.php index 8afd4855937..ca9d03d454f 100644 --- a/src/TokenParser/DoTokenParser.php +++ b/src/TokenParser/DoTokenParser.php @@ -24,7 +24,7 @@ final class DoTokenParser extends AbstractTokenParser { public function parse(Token $token): Node { - $expr = $this->parser->getExpressionParser()->parseExpression(); + $expr = $this->parser->parseExpression(); $this->parser->getStream()->expect(Token::BLOCK_END_TYPE); diff --git a/src/TokenParser/EmbedTokenParser.php b/src/TokenParser/EmbedTokenParser.php index f1acbf1ef00..fa279104614 100644 --- a/src/TokenParser/EmbedTokenParser.php +++ b/src/TokenParser/EmbedTokenParser.php @@ -28,7 +28,7 @@ public function parse(Token $token): Node { $stream = $this->parser->getStream(); - $parent = $this->parser->getExpressionParser()->parseExpression(); + $parent = $this->parser->parseExpression(); [$variables, $only, $ignoreMissing] = $this->parseArguments(); diff --git a/src/TokenParser/ExtendsTokenParser.php b/src/TokenParser/ExtendsTokenParser.php index a93afe8cd59..8f64698187d 100644 --- a/src/TokenParser/ExtendsTokenParser.php +++ b/src/TokenParser/ExtendsTokenParser.php @@ -36,7 +36,7 @@ public function parse(Token $token): Node throw new SyntaxError('Cannot use "extend" in a macro.', $token->getLine(), $stream->getSourceContext()); } - $this->parser->setParent($this->parser->getExpressionParser()->parseExpression()); + $this->parser->setParent($this->parser->parseExpression()); $stream->expect(Token::BLOCK_END_TYPE); diff --git a/src/TokenParser/ForTokenParser.php b/src/TokenParser/ForTokenParser.php index b098737fa6f..21166fc1fab 100644 --- a/src/TokenParser/ForTokenParser.php +++ b/src/TokenParser/ForTokenParser.php @@ -37,7 +37,7 @@ public function parse(Token $token): Node $stream = $this->parser->getStream(); $targets = $this->parseAssignmentExpression(); $stream->expect(Token::OPERATOR_TYPE, 'in'); - $seq = $this->parser->getExpressionParser()->parseExpression(); + $seq = $this->parser->parseExpression(); $stream->expect(Token::BLOCK_END_TYPE); $body = $this->parser->subparse([$this, 'decideForFork']); diff --git a/src/TokenParser/FromTokenParser.php b/src/TokenParser/FromTokenParser.php index c8732df29f6..1c80a171777 100644 --- a/src/TokenParser/FromTokenParser.php +++ b/src/TokenParser/FromTokenParser.php @@ -29,7 +29,7 @@ final class FromTokenParser extends AbstractTokenParser { public function parse(Token $token): Node { - $macro = $this->parser->getExpressionParser()->parseExpression(); + $macro = $this->parser->parseExpression(); $stream = $this->parser->getStream(); $stream->expect(Token::NAME_TYPE, 'import'); diff --git a/src/TokenParser/IfTokenParser.php b/src/TokenParser/IfTokenParser.php index 6b90105633b..4e3588e5be5 100644 --- a/src/TokenParser/IfTokenParser.php +++ b/src/TokenParser/IfTokenParser.php @@ -36,7 +36,7 @@ final class IfTokenParser extends AbstractTokenParser public function parse(Token $token): Node { $lineno = $token->getLine(); - $expr = $this->parser->getExpressionParser()->parseExpression(); + $expr = $this->parser->parseExpression(); $stream = $this->parser->getStream(); $stream->expect(Token::BLOCK_END_TYPE); $body = $this->parser->subparse([$this, 'decideIfFork']); @@ -52,7 +52,7 @@ public function parse(Token $token): Node break; case 'elseif': - $expr = $this->parser->getExpressionParser()->parseExpression(); + $expr = $this->parser->parseExpression(); $stream->expect(Token::BLOCK_END_TYPE); $body = $this->parser->subparse([$this, 'decideIfFork']); $tests[] = $expr; diff --git a/src/TokenParser/ImportTokenParser.php b/src/TokenParser/ImportTokenParser.php index f23584a5a42..6dcb7662cbf 100644 --- a/src/TokenParser/ImportTokenParser.php +++ b/src/TokenParser/ImportTokenParser.php @@ -28,7 +28,7 @@ final class ImportTokenParser extends AbstractTokenParser { public function parse(Token $token): Node { - $macro = $this->parser->getExpressionParser()->parseExpression(); + $macro = $this->parser->parseExpression(); $this->parser->getStream()->expect(Token::NAME_TYPE, 'as'); $name = $this->parser->getStream()->expect(Token::NAME_TYPE)->getValue(); $var = new AssignTemplateVariable(new TemplateVariable($name, $token->getLine()), $this->parser->isMainScope()); diff --git a/src/TokenParser/IncludeTokenParser.php b/src/TokenParser/IncludeTokenParser.php index c5ce180ad2a..55ac1516c4e 100644 --- a/src/TokenParser/IncludeTokenParser.php +++ b/src/TokenParser/IncludeTokenParser.php @@ -30,7 +30,7 @@ class IncludeTokenParser extends AbstractTokenParser { public function parse(Token $token): Node { - $expr = $this->parser->getExpressionParser()->parseExpression(); + $expr = $this->parser->parseExpression(); [$variables, $only, $ignoreMissing] = $this->parseArguments(); @@ -53,7 +53,7 @@ protected function parseArguments() $variables = null; if ($stream->nextIf(Token::NAME_TYPE, 'with')) { - $variables = $this->parser->getExpressionParser()->parseExpression(); + $variables = $this->parser->parseExpression(); } $only = false; diff --git a/src/TokenParser/MacroTokenParser.php b/src/TokenParser/MacroTokenParser.php index 1d857730011..38e66c81073 100644 --- a/src/TokenParser/MacroTokenParser.php +++ b/src/TokenParser/MacroTokenParser.php @@ -87,7 +87,7 @@ private function parseDefinition(): ArrayExpression $token = $stream->expect(Token::NAME_TYPE, null, 'An argument must be a name'); $name = new LocalVariable($token->getValue(), $this->parser->getCurrentToken()->getLine()); if ($token = $stream->nextIf(Token::OPERATOR_TYPE, '=')) { - $default = $this->parser->getExpressionParser()->parseExpression(); + $default = $this->parser->parseExpression(); } else { $default = new ConstantExpression(null, $this->parser->getCurrentToken()->getLine()); $default->setAttribute('is_implicit', true); diff --git a/src/TokenParser/SetTokenParser.php b/src/TokenParser/SetTokenParser.php index c9ebceb0bf8..1aabbf582b1 100644 --- a/src/TokenParser/SetTokenParser.php +++ b/src/TokenParser/SetTokenParser.php @@ -72,11 +72,11 @@ public function getTag(): string return 'set'; } - private function parseMultitargetExpression() + private function parseMultitargetExpression(): Nodes { $targets = []; while (true) { - $targets[] = $this->parser->getExpressionParser()->parseExpression(); + $targets[] = $this->parser->parseExpression(); if (!$this->parser->getStream()->nextIf(Token::PUNCTUATION_TYPE, ',')) { break; } diff --git a/src/TokenParser/UseTokenParser.php b/src/TokenParser/UseTokenParser.php index ebd95aa317f..41386c8b479 100644 --- a/src/TokenParser/UseTokenParser.php +++ b/src/TokenParser/UseTokenParser.php @@ -36,7 +36,7 @@ final class UseTokenParser extends AbstractTokenParser { public function parse(Token $token): Node { - $template = $this->parser->getExpressionParser()->parseExpression(); + $template = $this->parser->parseExpression(); $stream = $this->parser->getStream(); if (!$template instanceof ConstantExpression) { diff --git a/src/TokenParser/WithTokenParser.php b/src/TokenParser/WithTokenParser.php index 8ce4f02b2c5..83470d8651f 100644 --- a/src/TokenParser/WithTokenParser.php +++ b/src/TokenParser/WithTokenParser.php @@ -31,7 +31,7 @@ public function parse(Token $token): Node $variables = null; $only = false; if (!$stream->test(Token::BLOCK_END_TYPE)) { - $variables = $this->parser->getExpressionParser()->parseExpression(); + $variables = $this->parser->parseExpression(); $only = (bool) $stream->nextIf(Token::NAME_TYPE, 'only'); } diff --git a/tests/CustomExtensionTest.php b/tests/CustomExtensionTest.php index f89a900df52..174ad5e73f4 100644 --- a/tests/CustomExtensionTest.php +++ b/tests/CustomExtensionTest.php @@ -31,7 +31,7 @@ public function testGetInvalidOperators(ExtensionInterface $extension, $expected $this->expectException(\InvalidArgumentException::class); $this->expectExceptionMessage($expectedExceptionMessage); - $env->getOperators(); + $env->getExpressionParsers(); } public static function provideInvalidExtensions() diff --git a/tests/EnvironmentTest.php b/tests/EnvironmentTest.php index 5bc90b58215..5ddf07009bd 100644 --- a/tests/EnvironmentTest.php +++ b/tests/EnvironmentTest.php @@ -18,6 +18,8 @@ use Twig\Environment; use Twig\Error\RuntimeError; use Twig\Error\SyntaxError; +use Twig\ExpressionParser\Infix\BinaryOperatorExpressionParser; +use Twig\ExpressionParser\Prefix\UnaryOperatorExpressionParser; use Twig\Extension\AbstractExtension; use Twig\Extension\ExtensionInterface; use Twig\Extension\GlobalsInterface; @@ -26,8 +28,6 @@ use Twig\Loader\LoaderInterface; use Twig\Node\Node; use Twig\NodeVisitor\NodeVisitorInterface; -use Twig\Operator\Binary\AbstractBinaryOperator; -use Twig\Operator\Unary\AbstractUnaryOperator; use Twig\RuntimeLoader\RuntimeLoaderInterface; use Twig\Source; use Twig\Token; @@ -309,8 +309,8 @@ public function testAddExtension() $this->assertArrayHasKey('foo_filter', $twig->getFilters()); $this->assertArrayHasKey('foo_function', $twig->getFunctions()); $this->assertArrayHasKey('foo_test', $twig->getTests()); - $this->assertNotNull($twig->getOperators()->getUnary('foo_unary')); - $this->assertNotNull($twig->getOperators()->getBinary('foo_binary')); + $this->assertNotNull($twig->getExpressionParsers()->getPrefix('foo_unary')); + $this->assertNotNull($twig->getExpressionParsers()->getInfix('foo_binary')); $this->assertArrayHasKey('foo_global', $twig->getGlobals()); $visitors = $twig->getNodeVisitors(); $found = false; @@ -596,41 +596,11 @@ public function getFunctions(): array ]; } - public function getOperators(): array + public function getExpressionParsers(): array { return [ - new class extends AbstractUnaryOperator { - public function getOperator(): string - { - return 'foo_unary'; - } - - public function getPrecedence(): int - { - return 0; - } - - public function getNodeClass(): string - { - return ''; - } - }, - new class extends AbstractBinaryOperator { - public function getOperator(): string - { - return 'foo_binary'; - } - - public function getPrecedence(): int - { - return 0; - } - - public function getNodeClass(): string - { - return ''; - } - }, + new UnaryOperatorExpressionParser('', 'foo_unary', 0), + new BinaryOperatorExpressionParser('', 'foo_binary', 0), ]; } diff --git a/tests/ExpressionParserTest.php b/tests/ExpressionParserTest.php index f98bade0841..7d263fbf66a 100644 --- a/tests/ExpressionParserTest.php +++ b/tests/ExpressionParserTest.php @@ -17,6 +17,7 @@ use Twig\Compiler; use Twig\Environment; use Twig\Error\SyntaxError; +use Twig\ExpressionParser\Prefix\UnaryOperatorExpressionParser; use Twig\Extension\AbstractExtension; use Twig\Loader\ArrayLoader; use Twig\Node\Expression\ArrayExpression; @@ -28,7 +29,6 @@ use Twig\Node\Expression\Unary\AbstractUnary; use Twig\Node\Expression\Variable\ContextVariable; use Twig\Node\Node; -use Twig\Operator\Unary\AbstractUnaryOperator; use Twig\Parser; use Twig\Source; use Twig\TwigFilter; @@ -571,32 +571,17 @@ public function testUnaryPrecedenceChange() { $env = new Environment(new ArrayLoader(), ['cache' => false, 'autoescape' => false]); $env->addExtension(new class extends AbstractExtension { - public function getOperators() + public function getExpressionParsers(): array { + $class = new class(new ConstantExpression('foo', 1), 1) extends AbstractUnary { + public function operator(Compiler $compiler): Compiler + { + return $compiler->raw('!'); + } + }; + return [ - new class extends AbstractUnaryOperator { - public function getOperator(): string - { - return '!'; - } - - public function getPrecedence(): int - { - return 50; - } - - public function getNodeClass(): string - { - $class = new class(new ConstantExpression('foo', 1), 1) extends AbstractUnary { - public function operator(Compiler $compiler): Compiler - { - return $compiler->raw('!'); - } - }; - - return $class::class; - } - }, + new UnaryOperatorExpressionParser($class::class, '!', 50), ]; } }); diff --git a/tests/Fixtures/operators/not_precedence.test b/tests/Fixtures/operators/not_precedence.test index 592b1c33440..f21a0861653 100644 --- a/tests/Fixtures/operators/not_precedence.test +++ b/tests/Fixtures/operators/not_precedence.test @@ -2,7 +2,7 @@ *, /, //, and % will have a higher precedence over not in Twig 4.0 --TEMPLATE-- {{ (not 1) * 2 }} -{{ (not 1 * 2) }} +{{ not (1 * 2) }} --DATA-- return [] --EXPECT-- From 0cb5c0e8d0150177a4104c6500c4576895745718 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sun, 26 Jan 2025 20:53:23 +0100 Subject: [PATCH 724/812] Add deprecation notices in CHANGELOG and docs --- CHANGELOG | 5 +++- doc/deprecated.rst | 57 ++++++++++++++++++++++++++++++---------------- 2 files changed, 41 insertions(+), 21 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index bd25ae8b3f2..df3f54bd338 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,9 @@ # 3.21.0 (2025-XX-XX) - * Introduce operator classes to describe operators provided by extensions instead of arrays + * Introduce expression parser classes to describe operators and operands provided by extensions + instead of arrays (it comes with many deprecations that are documented in + the ``deprecated`` documentation chapter) + * Deprecate the `Twig\ExpressionParser`, and `Twig\OperatorPrecedenceChange` classes # 3.20.0 (2025-02-13) diff --git a/doc/deprecated.rst b/doc/deprecated.rst index 74e9e695b43..e81eafaae57 100644 --- a/doc/deprecated.rst +++ b/doc/deprecated.rst @@ -210,27 +210,35 @@ Node Visitors Parser ------ -* Passing a second argument to ``ExpressionParser::parseFilterExpressionRaw()`` - is deprecated as of Twig 3.12. - * The following methods from ``Twig\Parser`` are deprecated as of Twig 3.12: ``getBlockStack()``, ``hasBlock()``, ``getBlock()``, ``hasMacro()``, ``hasTraits()``, ``getParent()``. -* The ``Twig\ExpressionParser::parseHashExpression()`` method is deprecated, use - ``Twig\ExpressionParser::parseMappingExpression()`` instead. - -* The ``Twig\ExpressionParser::parseArrayExpression()`` method is deprecated, use - ``Twig\ExpressionParser::parseSequenceExpression()`` instead. - * Passing ``null`` to ``Twig\Parser::setParent()`` is deprecated as of Twig 3.12. -* The ``Twig\ExpressionParser::parseOnlyArguments()`` and - ``Twig\ExpressionParser::parseArguments()`` methods are deprecated, use - ``Twig\ExpressionParser::parseNamedArguments()`` instead. - -Lexer +* The ``Twig\Parser::getExpressionParser()`` method is deprecated as of Twig + 3.20, use ``Twig\Parser::parseExpression()`` instead. + +* The ``Twig\ExpressionParser`` class is deprecated as of Twig 3.20: + + * ``parseExpression()``, use ``Parser::parseExpression()`` + * ``parsePrimaryExpression()``, use ``Parser::parseExpression()`` + * ``parseStringExpression()``, use ``Parser::parseExpression()`` + * ``parseHashExpression()``, use ``Parser::parseExpression()`` + * ``parseMappingExpression()``, use ``Parser::parseExpression()`` + * ``parseArrayExpression()``, use ``Parser::parseExpression()`` + * ``parseSequenceExpression()``, use ``Parser::parseExpression()`` + * ``parsePostfixExpression`` + * ``parseSubscriptExpression`` + * ``parseFilterExpression`` + * ``parseFilterExpressionRaw`` + * ``parseArguments()``, use ``Twig\ExpressionParser\Infix\ArgumentsTrait::parseNamedArguments()`` + * ``parseAssignmentExpression``, use ``AbstractTokenParser::parseAssignmentExpression`` + * ``parseMultitargetExpression`` + * ``parseOnlyArguments()``, use ``Twig\ExpressionParser\Infix\ArgumentsTrait::parseNamedArguments()`` + +Token ----- * Not passing a ``Source`` instance to ``Twig\TokenStream`` constructor is @@ -239,6 +247,12 @@ Lexer * The ``Token::getType()`` method is deprecated as of Twig 3.19, use ``Token::test()`` instead. +* The ``Token::ARROW_TYPE`` constant is deprecated as of Twig 3.20, the arrow + ``=>`` is now an operator (``Token::OPERATOR_TYPE``). + +* The ``Token::PUNCTUATION_TYPE`` with values ``(``, ``[``, ``|``, ``.``, + ``?``, or ``?:`` are now of the ``Token::OPERATOR_TYPE`` type. + Templates --------- @@ -419,9 +433,9 @@ Operators {{ (not 1) * 2 }} {# this is equivalent to what Twig 4.x will do without the parentheses #} -* Operators are now instances of ``Twig\Operator\OperatorInterface`` instead of - arrays. The ``ExtensionInterface::getOperators()`` method should now return an - array of ``Twig\Operator\OperatorInterface`` instances. +* The ``Twig\Extension\ExtensionInterface::getOperators()`` method is deprecated + as of Twig 3.20, use ``Twig\Extension\ExtensionInterface::getExpressionParsers()`` + instead: Before: @@ -429,15 +443,18 @@ Operators return [ 'not' => [ 'precedence' => 10, - 'class' => NotUnaryOperator::class, + 'class' => NotUnary::class, ], ]; } After: - public function getOperators(): array { + public function getExpressionParsers(): array { return [ - new NotUnaryOperator(), + new UnaryOperatorExpressionParser(NotUnary::class, 'not', 10), ]; } + +* The ``Twig\OperatorPrecedenceChange`` class is deprecated as of Twig 3.20, + use ``Twig\ExpressionParser\PrecedenceChange`` instead. From 8ff19090f39fa6381314753b6f07a1dab4763b84 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Mon, 3 Feb 2025 22:22:16 +0100 Subject: [PATCH 725/812] Fix precedence rules --- .gitattributes | 1 + CHANGELOG | 2 + bin/generate_operators_precedence.php | 115 +++++++------ doc/deprecated.rst | 18 +++ doc/filters/number_format.rst | 14 +- doc/operators_precedence.rst | 153 ++++++++++++------ doc/templates.rst | 28 +--- .../ExpressionParserDescriptionInterface.php | 17 ++ src/ExpressionParser/ExpressionParsers.php | 17 +- .../Infix/ArrowExpressionParser.php | 8 +- .../Infix/BinaryOperatorExpressionParser.php | 9 +- .../ConditionalTernaryExpressionParser.php | 8 +- .../Infix/DotExpressionParser.php | 10 +- .../Infix/FilterExpressionParser.php | 16 +- .../Infix/FunctionExpressionParser.php | 10 +- .../Infix/IsExpressionParser.php | 8 +- .../Infix/SquareBracketExpressionParser.php | 10 +- .../Prefix/GroupingExpressionParser.php | 8 +- .../Prefix/LiteralExpressionParser.php | 10 +- .../Prefix/UnaryOperatorExpressionParser.php | 9 +- src/Extension/CoreExtension.php | 22 ++- src/ExtensionSet.php | 2 +- src/Parser.php | 26 +-- tests/ExpressionParserTest.php | 80 +++++++++ tests/Fixtures/expressions/postfix.test | 4 +- .../operators/contat_vs_add_sub.legacy.test | 4 +- .../operators/minus_vs_pipe.legacy.test | 10 ++ .../operators/not_precedence.legacy.test | 2 +- .../Fixtures/tests/null_coalesce.legacy.test | 20 +-- 29 files changed, 456 insertions(+), 185 deletions(-) create mode 100644 src/ExpressionParser/ExpressionParserDescriptionInterface.php create mode 100644 tests/Fixtures/operators/minus_vs_pipe.legacy.test diff --git a/.gitattributes b/.gitattributes index 86b9ef413df..c07b0dfb56e 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,4 +1,5 @@ /.github/ export-ignore +/bin/ export-ignore /doc/ export-ignore /extra/ export-ignore /tests/ export-ignore diff --git a/CHANGELOG b/CHANGELOG index df3f54bd338..cb70fd0c780 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,7 @@ # 3.21.0 (2025-XX-XX) + * Deprecate using the `|` operator in an expression with `+` or `-` without using parentheses to clarify precedence + * Deprecate operator precedence outside of the [0, 512] range * Introduce expression parser classes to describe operators and operands provided by extensions instead of arrays (it comes with many deprecations that are documented in the ``deprecated`` documentation chapter) diff --git a/bin/generate_operators_precedence.php b/bin/generate_operators_precedence.php index c22c81938d7..926989e6646 100644 --- a/bin/generate_operators_precedence.php +++ b/bin/generate_operators_precedence.php @@ -1,65 +1,88 @@ getPrecedenceChange() ? $a->getPrecedenceChange()->getNewPrecedence() : $a->getPrecedence(); - $bPrecedence = $b->getPrecedenceChange() ? $b->getPrecedenceChange()->getNewPrecedence() : $b->getPrecedence(); - return $bPrecedence - $aPrecedence; - }); - - $current = \PHP_INT_MAX; - foreach ($expressionParsers as $expressionParser) { - $precedence = $expressionParser->getPrecedenceChange() ? $expressionParser->getPrecedenceChange()->getNewPrecedence() : $expressionParser->getPrecedence(); - if ($precedence !== $current) { - $current = $precedence; - if ($withAssociativity) { - fwrite($output, \sprintf("\n%-11d %-11s %s", $precedence, $expressionParser->getName(), InfixAssociativity::Left === $expressionParser->getAssociativity() ? 'Left' : 'Right')); - } else { - fwrite($output, \sprintf("\n%-11d %s", $precedence, $expressionParser->getName())); - } - } else { - fwrite($output, "\n".str_repeat(' ', 12).$expressionParser->getName()); - } - } - fwrite($output, "\n"); -} - $output = fopen(dirname(__DIR__).'/doc/operators_precedence.rst', 'w'); $twig = new Environment(new ArrayLoader([])); -$prefixExpressionParsers = []; -$infixExpressionParsers = []; +$expressionParsers = []; foreach ($twig->getExpressionParsers() as $expressionParser) { - if ($expressionParser instanceof PrefixExpressionParserInterface) { - $prefixExpressionParsers[] = $expressionParser; - } elseif ($expressionParser instanceof InfixExpressionParserInterface) { - $infixExpressionParsers[] = $expressionParser; + $expressionParsers[] = $expressionParser; +} + +fwrite($output, "\n=========== ================ ======= ============= ===========\n"); +fwrite($output, "Precedence Operator Type Associativity Description\n"); +fwrite($output, "=========== ================ ======= ============= ==========="); + +usort($expressionParsers, fn ($a, $b) => $b->getPrecedence() <=> $a->getPrecedence()); + +$previous = null; +foreach ($expressionParsers as $expressionParser) { + $precedence = $expressionParser->getPrecedence(); + $previousPrecedence = $previous ? $previous->getPrecedence() : \PHP_INT_MAX; + $associativity = $expressionParser instanceof InfixExpressionParserInterface ? (InfixAssociativity::Left === $expressionParser->getAssociativity() ? 'Left' : 'Right') : 'n/a'; + $previousAssociativity = $previous ? ($previous instanceof InfixExpressionParserInterface ? (InfixAssociativity::Left === $previous->getAssociativity() ? 'Left' : 'Right') : 'n/a') : 'n/a'; + if ($previousPrecedence !== $precedence) { + $previous = null; } + fwrite($output, rtrim(\sprintf("\n%-11s %-16s %-7s %-13s %s\n", + (!$previous || $previousPrecedence !== $precedence ? $precedence : '').($expressionParser->getPrecedenceChange() ? ' => '.$expressionParser->getPrecedenceChange()->getNewPrecedence() : ''), + '``'.$expressionParser->getName().'``', + !$previous || ExpressionParserType::getType($previous) !== ExpressionParserType::getType($expressionParser) ? ExpressionParserType::getType($expressionParser)->value : '', + !$previous || $previousAssociativity !== $associativity ? $associativity : '', + $expressionParser instanceof ExpressionParserDescriptionInterface ? $expressionParser->getDescription() : '', + ))); + $previous = $expressionParser; } +fwrite($output, "\n=========== ================ ======= ============= ===========\n"); +fwrite($output, "\nWhen a precedence will change in 4.0, the new precedence is indicated by the arrow ``=>``.\n"); + +fwrite($output, "\nHere is the same table for Twig 4.0 with adjusted precedences:\n"); -fwrite($output, "Unary operators precedence:\n"); -printExpressionParsers($output, $prefixExpressionParsers); +fwrite($output, "\n=========== ================ ======= ============= ===========\n"); +fwrite($output, "Precedence Operator Type Associativity Description\n"); +fwrite($output, "=========== ================ ======= ============= ==========="); -fwrite($output, "\nBinary and Ternary operators precedence:\n"); -printExpressionParsers($output, $infixExpressionParsers, true); +usort($expressionParsers, function($a, $b) { + $aPrecedence = $a->getPrecedenceChange() ? $a->getPrecedenceChange()->getNewPrecedence() : $a->getPrecedence(); + $bPrecedence = $b->getPrecedenceChange() ? $b->getPrecedenceChange()->getNewPrecedence() : $b->getPrecedence(); + return $bPrecedence - $aPrecedence; +}); + +$previous = null; +foreach ($expressionParsers as $expressionParser) { + $precedence = $expressionParser->getPrecedenceChange() ? $expressionParser->getPrecedenceChange()->getNewPrecedence() : $expressionParser->getPrecedence(); + $previousPrecedence = $previous ? ($previous->getPrecedenceChange() ? $previous->getPrecedenceChange()->getNewPrecedence() : $previous->getPrecedence()) : \PHP_INT_MAX; + $associativity = $expressionParser instanceof InfixExpressionParserInterface ? (InfixAssociativity::Left === $expressionParser->getAssociativity() ? 'Left' : 'Right') : 'n/a'; + $previousAssociativity = $previous ? ($previous instanceof InfixExpressionParserInterface ? (InfixAssociativity::Left === $previous->getAssociativity() ? 'Left' : 'Right') : 'n/a') : 'n/a'; + if ($previousPrecedence !== $precedence) { + $previous = null; + } + fwrite($output, rtrim(\sprintf("\n%-11s %-16s %-7s %-13s %s\n", + !$previous || $previousPrecedence !== $precedence ? $precedence : '', + '``'.$expressionParser->getName().'``', + !$previous || ExpressionParserType::getType($previous) !== ExpressionParserType::getType($expressionParser) ? ExpressionParserType::getType($expressionParser)->value : '', + !$previous || $previousAssociativity !== $associativity ? $associativity : '', + $expressionParser instanceof ExpressionParserDescriptionInterface ? $expressionParser->getDescription() : '', + ))); + $previous = $expressionParser; +} +fwrite($output, "\n=========== ================ ======= ============= ===========\n"); fclose($output); diff --git a/doc/deprecated.rst b/doc/deprecated.rst index e81eafaae57..0b69f913957 100644 --- a/doc/deprecated.rst +++ b/doc/deprecated.rst @@ -378,6 +378,8 @@ Node Operators --------- +* An operator precedence must be part of the [0, 512] range as of Twig 3.20. + * The ``.`` operator allows accessing class constants as of Twig 3.15. This can be a BC break if you don't use UPPERCASE constant names. @@ -433,6 +435,22 @@ Operators {{ (not 1) * 2 }} {# this is equivalent to what Twig 4.x will do without the parentheses #} +* Using the ``|`` operator in an expression with ``+`` or ``-`` without explicit + parentheses to clarify precedence triggers a deprecation as of Twig 3.20 (in + Twig 4.0, ``|`` will have a higher precedence than ``+`` and ``-``). + + For example, the following expression will trigger a deprecation in Twig 3.20:: + + {{ -1|abs }} + + To avoid the deprecation, add parentheses to clarify the precedence:: + + {{ -(1|abs) }} {# this is equivalent to what Twig 3.x does without the parentheses #} + + {# or #} + + {{ (-1)|abs }} {# this is equivalent to what Twig 4.x will do without the parentheses #} + * The ``Twig\Extension\ExtensionInterface::getOperators()`` method is deprecated as of Twig 3.20, use ``Twig\Extension\ExtensionInterface::getExpressionParsers()`` instead: diff --git a/doc/filters/number_format.rst b/doc/filters/number_format.rst index 047249d6718..f9e9d718b25 100644 --- a/doc/filters/number_format.rst +++ b/doc/filters/number_format.rst @@ -15,15 +15,21 @@ separator using the additional arguments: {{ 9800.333|number_format(2, '.', ',') }} -To format negative numbers or math calculation, wrap the previous statement -with parentheses (needed because of Twig's :ref:`precedence of operators -`): +To format negative numbers, wrap the previous statement with parentheses (note +that as of Twig 3.20, not using parentheses is deprecated as the filter +operator will change precedence in Twig 4.0): .. code-block:: twig {{ -9800.333|number_format(2, '.', ',') }} {# outputs : -9 #} {{ (-9800.333)|number_format(2, '.', ',') }} {# outputs : -9,800.33 #} - {{ 1 + 0.2|number_format(2) }} {# outputs : 1.2 #} + +To format math calculation, wrap the previous statement with parentheses +(needed because of Twig's :ref:`precedence of operators -`): + +.. code-block:: twig + + {{ 1 + 0.2|number_format(2) }} {# outputs : 1.2 #} {{ (1 + 0.2)|number_format(2) }} {# outputs : 1.20 #} If no formatting options are provided then Twig will use the default formatting diff --git a/doc/operators_precedence.rst b/doc/operators_precedence.rst index 032582fbe5e..f603127f3dd 100644 --- a/doc/operators_precedence.rst +++ b/doc/operators_precedence.rst @@ -1,57 +1,104 @@ -Unary operators precedence: -=========== =========== -Precedence Operator -=========== =========== +=========== ================ ======= ============= =========== +Precedence Operator Type Associativity Description +=========== ================ ======= ============= =========== +512 => 300 ``|`` infix Left Twig filter call + ``(`` Twig function call + ``.`` Get an attribute on a variable + ``[`` Array access +500 ``-`` prefix n/a + ``+`` +300 => 5 ``??`` infix Right Null coalescing operator (a ?? b) +250 ``=>`` infix Left Arrow function (x => expr) +200 ``**`` infix Right Exponentiation operator +100 ``is`` infix Left Twig tests + ``is not`` Twig tests +60 ``*`` infix Left + ``/`` + ``//`` Floor division + ``%`` +50 => 70 ``not`` prefix n/a +40 => 27 ``~`` infix Left +30 ``+`` infix Left + ``-`` +25 ``..`` infix Left +20 ``==`` infix Left + ``!=`` + ``<=>`` + ``<`` + ``>`` + ``>=`` + ``<=`` + ``not in`` + ``in`` + ``matches`` + ``starts with`` + ``ends with`` + ``has some`` + ``has every`` +18 ``b-and`` infix Left +17 ``b-xor`` infix Left +16 ``b-or`` infix Left +15 ``and`` infix Left +12 ``xor`` infix Left +10 ``or`` infix Left +5 ``?:`` infix Right Elvis operator (a ?: b) + ``?:`` Elvis operator (a ?: b) +0 ``(`` prefix n/a Explicit group expression (a) + ``literal`` A literal value (boolean, string, number, sequence, mapping, ...) + ``?`` infix Left Conditional operator (a ? b : c) +=========== ================ ======= ============= =========== -500 - - + -70 not -0 ( - literal +When a precedence will change in 4.0, the new precedence is indicated by the arrow ``=>``. -Binary and Ternary operators precedence: +Here is the same table for Twig 4.0 with adjusted precedences: -=========== =========== ============= -Precedence Operator Associativity -=========== =========== ============= - -300 . Left - [ - | - ( -250 => Left -200 ** Right -100 is Left - is not -60 * Left - / - // - % -30 + Left - - -27 ~ Left -25 .. Left -20 == Left - != - <=> - < - > - >= - <= - not in - in - matches - starts with - ends with - has some - has every -18 b-and Left -17 b-xor Left -16 b-or Left -15 and Left -12 xor Left -10 or Left -5 ?: Right - ?? -0 ? Left +=========== ============== ======= ============= =========== +Precedence Operator Type Associativity Description +=========== ============== ======= ============= =========== +512 `(` infix Left Twig function call + `.` Get an attribute on a variable + `[` Array access +500 `-` prefix n/a + `+` +300 `|` infix Left Twig filter call +250 `=>` infix Left Arrow function (x => expr) +200 `**` infix Right Exponentiation operator +100 `is` infix Left Twig tests + `is not` Twig tests +70 `not` prefix n/a +60 `*` infix Left + `/` + `//` Floor division + `%` +30 `+` infix Left + `-` +27 `~` infix Left +25 `..` infix Left +20 `==` infix Left + `!=` + `<=>` + `<` + `>` + `>=` + `<=` + `not in` + `in` + `matches` + `starts with` + `ends with` + `has some` + `has every` +18 `b-and` infix Left +17 `b-xor` infix Left +16 `b-or` infix Left +15 `and` infix Left +12 `xor` infix Left +10 `or` infix Left +5 `??` infix Right Null coalescing operator (a ?? b) + `?:` Elvis operator (a ?: b) + `?:` Elvis operator (a ?: b) +0 `(` prefix n/a Explicit group expression (a) + `literal` A literal value (boolean, string, number, sequence, mapping, ...) + `?` infix Left Conditional operator (a ? b : c) +=========== ============== ======= ============= =========== diff --git a/doc/templates.rst b/doc/templates.rst index 960093152b7..33a32e89e1a 100644 --- a/doc/templates.rst +++ b/doc/templates.rst @@ -186,28 +186,6 @@ filters. {{ ('HELLO' ~ 'FABIEN')|lower }} - A common mistake is to forget using parentheses for filters on negative - numbers as a negative number in Twig is represented by the ``-`` operator - followed by a positive number. As the ``-`` operator has a lower precedence - than the filter operator, it can lead to confusion: - - .. code-block:: twig - - {{ -1|abs }} {# returns -1 #} - {{ -1**0 }} {# returns -1 #} - - {# as it is equivalent to #} - - {{ -(1|abs) }} - {{ -(1**0) }} - - For such cases, use parentheses to force the precedence: - - .. code-block:: twig - - {{ (-1)|abs }} {# returns 1 as expected #} - {{ (-1)**0 }} {# returns 1 as expected #} - Functions --------- @@ -703,14 +681,16 @@ Twig allows you to do math in templates; the following operators are supported: ``4``. * ``//``: Divides two numbers and returns the floored integer result. ``{{ 20 - // 7 }}`` is ``2``, ``{{ -20 // 7 }}`` is ``-3`` (this is just syntactic + // 7 }}`` is ``2``, ``{{ -20 // 7 }}`` is ``-3`` (this is just syntactic sugar for the :doc:`round` filter). * ``*``: Multiplies the left operand with the right one. ``{{ 2 * 2 }}`` would return ``4``. * ``**``: Raises the left operand to the power of the right operand. ``{{ 2 ** - 3 }}`` would return ``8``. + 3 }}`` would return ``8``. Be careful as the ``**`` operator is right + associative, which means that ``{{ -1**0 }}`` is equivalent to ``{{ -(1**0) + }}`` and not ``{{ (-1)**0 }}``. .. _template_logic: diff --git a/src/ExpressionParser/ExpressionParserDescriptionInterface.php b/src/ExpressionParser/ExpressionParserDescriptionInterface.php new file mode 100644 index 00000000000..686f8a59f1e --- /dev/null +++ b/src/ExpressionParser/ExpressionParserDescriptionInterface.php @@ -0,0 +1,17 @@ +precedenceChanges = null; $this->add($parsers); } @@ -55,12 +54,16 @@ public function __construct( */ public function add(array $parsers): self { - foreach ($parsers as $operator) { - $type = ExpressionParserType::getType($operator); - $this->parsers[$type->value][$operator->getName()] = $operator; - $this->parsersByClass[$type->value][get_class($operator)] = $operator; - foreach ($operator->getAliases() as $alias) { - $this->aliases[$type->value][$alias] = $operator; + foreach ($parsers as $parser) { + if ($parser->getPrecedence() > 512 || $parser->getPrecedence() < 0) { + trigger_deprecation('twig/twig', '3.20', 'Precedence for "%s" must be between 0 and 512, got %d.', $parser->getName(), $parser->getPrecedence()); + // throw new \InvalidArgumentException(\sprintf('Precedence for "%s" must be between 0 and 512, got %d.', $parser->getName(), $parser->getPrecedence())); + } + $type = ExpressionParserType::getType($parser); + $this->parsers[$type->value][$parser->getName()] = $parser; + $this->parsersByClass[$type->value][get_class($parser)] = $parser; + foreach ($parser->getAliases() as $alias) { + $this->aliases[$type->value][$alias] = $parser; } } diff --git a/src/ExpressionParser/Infix/ArrowExpressionParser.php b/src/ExpressionParser/Infix/ArrowExpressionParser.php index 698497b0e8d..c8630da41e7 100644 --- a/src/ExpressionParser/Infix/ArrowExpressionParser.php +++ b/src/ExpressionParser/Infix/ArrowExpressionParser.php @@ -12,6 +12,7 @@ namespace Twig\ExpressionParser\Infix; use Twig\ExpressionParser\AbstractExpressionParser; +use Twig\ExpressionParser\ExpressionParserDescriptionInterface; use Twig\ExpressionParser\InfixAssociativity; use Twig\ExpressionParser\InfixExpressionParserInterface; use Twig\Node\Expression\AbstractExpression; @@ -22,7 +23,7 @@ /** * @internal */ -final class ArrowExpressionParser extends AbstractExpressionParser implements InfixExpressionParserInterface +final class ArrowExpressionParser extends AbstractExpressionParser implements InfixExpressionParserInterface, ExpressionParserDescriptionInterface { public function parse(Parser $parser, AbstractExpression $expr, Token $token): AbstractExpression { @@ -35,6 +36,11 @@ public function getName(): string return '=>'; } + public function getDescription(): string + { + return 'Arrow function (x => expr)'; + } + public function getPrecedence(): int { return 250; diff --git a/src/ExpressionParser/Infix/BinaryOperatorExpressionParser.php b/src/ExpressionParser/Infix/BinaryOperatorExpressionParser.php index ce650b424ea..4c66da73bc1 100644 --- a/src/ExpressionParser/Infix/BinaryOperatorExpressionParser.php +++ b/src/ExpressionParser/Infix/BinaryOperatorExpressionParser.php @@ -12,6 +12,7 @@ namespace Twig\ExpressionParser\Infix; use Twig\ExpressionParser\AbstractExpressionParser; +use Twig\ExpressionParser\ExpressionParserDescriptionInterface; use Twig\ExpressionParser\InfixAssociativity; use Twig\ExpressionParser\InfixExpressionParserInterface; use Twig\ExpressionParser\PrecedenceChange; @@ -23,7 +24,7 @@ /** * @internal */ -class BinaryOperatorExpressionParser extends AbstractExpressionParser implements InfixExpressionParserInterface +class BinaryOperatorExpressionParser extends AbstractExpressionParser implements InfixExpressionParserInterface, ExpressionParserDescriptionInterface { public function __construct( /** @var class-string */ @@ -32,6 +33,7 @@ public function __construct( private int $precedence, private InfixAssociativity $associativity = InfixAssociativity::Left, private ?PrecedenceChange $precedenceChange = null, + private ?string $description = null, private array $aliases = [], ) { } @@ -56,6 +58,11 @@ public function getName(): string return $this->name; } + public function getDescription(): string + { + return $this->description ?? ''; + } + public function getPrecedence(): int { return $this->precedence; diff --git a/src/ExpressionParser/Infix/ConditionalTernaryExpressionParser.php b/src/ExpressionParser/Infix/ConditionalTernaryExpressionParser.php index 2bb5fc92c79..9707c0a04bd 100644 --- a/src/ExpressionParser/Infix/ConditionalTernaryExpressionParser.php +++ b/src/ExpressionParser/Infix/ConditionalTernaryExpressionParser.php @@ -12,6 +12,7 @@ namespace Twig\ExpressionParser\Infix; use Twig\ExpressionParser\AbstractExpressionParser; +use Twig\ExpressionParser\ExpressionParserDescriptionInterface; use Twig\ExpressionParser\InfixAssociativity; use Twig\ExpressionParser\InfixExpressionParserInterface; use Twig\Node\Expression\AbstractExpression; @@ -23,7 +24,7 @@ /** * @internal */ -final class ConditionalTernaryExpressionParser extends AbstractExpressionParser implements InfixExpressionParserInterface +final class ConditionalTernaryExpressionParser extends AbstractExpressionParser implements InfixExpressionParserInterface, ExpressionParserDescriptionInterface { public function parse(Parser $parser, AbstractExpression $left, Token $token): AbstractExpression { @@ -44,6 +45,11 @@ public function getName(): string return '?'; } + public function getDescription(): string + { + return 'Conditional operator (a ? b : c)'; + } + public function getPrecedence(): int { return 0; diff --git a/src/ExpressionParser/Infix/DotExpressionParser.php b/src/ExpressionParser/Infix/DotExpressionParser.php index d83f4bfbbfb..7d1cf505827 100644 --- a/src/ExpressionParser/Infix/DotExpressionParser.php +++ b/src/ExpressionParser/Infix/DotExpressionParser.php @@ -13,6 +13,7 @@ use Twig\Error\SyntaxError; use Twig\ExpressionParser\AbstractExpressionParser; +use Twig\ExpressionParser\ExpressionParserDescriptionInterface; use Twig\ExpressionParser\InfixAssociativity; use Twig\ExpressionParser\InfixExpressionParserInterface; use Twig\Lexer; @@ -30,7 +31,7 @@ /** * @internal */ -final class DotExpressionParser extends AbstractExpressionParser implements InfixExpressionParserInterface +final class DotExpressionParser extends AbstractExpressionParser implements InfixExpressionParserInterface, ExpressionParserDescriptionInterface { use ArgumentsTrait; @@ -81,9 +82,14 @@ public function getName(): string return '.'; } + public function getDescription(): string + { + return 'Get an attribute on a variable'; + } + public function getPrecedence(): int { - return 300; + return 512; } public function getAssociativity(): InfixAssociativity diff --git a/src/ExpressionParser/Infix/FilterExpressionParser.php b/src/ExpressionParser/Infix/FilterExpressionParser.php index 98e4b3b3a90..e47d3fe6773 100644 --- a/src/ExpressionParser/Infix/FilterExpressionParser.php +++ b/src/ExpressionParser/Infix/FilterExpressionParser.php @@ -13,8 +13,10 @@ use Twig\Attribute\FirstClassTwigCallableReady; use Twig\ExpressionParser\AbstractExpressionParser; +use Twig\ExpressionParser\ExpressionParserDescriptionInterface; use Twig\ExpressionParser\InfixAssociativity; use Twig\ExpressionParser\InfixExpressionParserInterface; +use Twig\ExpressionParser\PrecedenceChange; use Twig\Node\EmptyNode; use Twig\Node\Expression\AbstractExpression; use Twig\Node\Expression\ConstantExpression; @@ -24,7 +26,7 @@ /** * @internal */ -final class FilterExpressionParser extends AbstractExpressionParser implements InfixExpressionParserInterface +final class FilterExpressionParser extends AbstractExpressionParser implements InfixExpressionParserInterface, ExpressionParserDescriptionInterface { use ArgumentsTrait; @@ -61,9 +63,19 @@ public function getName(): string return '|'; } + public function getDescription(): string + { + return 'Twig filter call'; + } + public function getPrecedence(): int { - return 300; + return 512; + } + + public function getPrecedenceChange(): ?PrecedenceChange + { + return new PrecedenceChange('twig/twig', '3.20', 300); } public function getAssociativity(): InfixAssociativity diff --git a/src/ExpressionParser/Infix/FunctionExpressionParser.php b/src/ExpressionParser/Infix/FunctionExpressionParser.php index b1d627f7b9b..e9cd7751793 100644 --- a/src/ExpressionParser/Infix/FunctionExpressionParser.php +++ b/src/ExpressionParser/Infix/FunctionExpressionParser.php @@ -14,6 +14,7 @@ use Twig\Attribute\FirstClassTwigCallableReady; use Twig\Error\SyntaxError; use Twig\ExpressionParser\AbstractExpressionParser; +use Twig\ExpressionParser\ExpressionParserDescriptionInterface; use Twig\ExpressionParser\InfixAssociativity; use Twig\ExpressionParser\InfixExpressionParserInterface; use Twig\Node\EmptyNode; @@ -26,7 +27,7 @@ /** * @internal */ -final class FunctionExpressionParser extends AbstractExpressionParser implements InfixExpressionParserInterface +final class FunctionExpressionParser extends AbstractExpressionParser implements InfixExpressionParserInterface, ExpressionParserDescriptionInterface { use ArgumentsTrait; @@ -72,9 +73,14 @@ public function getName(): string return '('; } + public function getDescription(): string + { + return 'Twig function call'; + } + public function getPrecedence(): int { - return 300; + return 512; } public function getAssociativity(): InfixAssociativity diff --git a/src/ExpressionParser/Infix/IsExpressionParser.php b/src/ExpressionParser/Infix/IsExpressionParser.php index d63b495e24e..1614c3cb991 100644 --- a/src/ExpressionParser/Infix/IsExpressionParser.php +++ b/src/ExpressionParser/Infix/IsExpressionParser.php @@ -13,6 +13,7 @@ use Twig\Attribute\FirstClassTwigCallableReady; use Twig\ExpressionParser\AbstractExpressionParser; +use Twig\ExpressionParser\ExpressionParserDescriptionInterface; use Twig\ExpressionParser\InfixAssociativity; use Twig\ExpressionParser\InfixExpressionParserInterface; use Twig\Node\Expression\AbstractExpression; @@ -27,7 +28,7 @@ /** * @internal */ -class IsExpressionParser extends AbstractExpressionParser implements InfixExpressionParserInterface +class IsExpressionParser extends AbstractExpressionParser implements InfixExpressionParserInterface, ExpressionParserDescriptionInterface { use ArgumentsTrait; @@ -71,6 +72,11 @@ public function getName(): string return 'is'; } + public function getDescription(): string + { + return 'Twig tests'; + } + public function getAssociativity(): InfixAssociativity { return InfixAssociativity::Left; diff --git a/src/ExpressionParser/Infix/SquareBracketExpressionParser.php b/src/ExpressionParser/Infix/SquareBracketExpressionParser.php index 1037dcb8193..25fb153c7cf 100644 --- a/src/ExpressionParser/Infix/SquareBracketExpressionParser.php +++ b/src/ExpressionParser/Infix/SquareBracketExpressionParser.php @@ -12,6 +12,7 @@ namespace Twig\ExpressionParser\Infix; use Twig\ExpressionParser\AbstractExpressionParser; +use Twig\ExpressionParser\ExpressionParserDescriptionInterface; use Twig\ExpressionParser\InfixAssociativity; use Twig\ExpressionParser\InfixExpressionParserInterface; use Twig\Node\Expression\AbstractExpression; @@ -26,7 +27,7 @@ /** * @internal */ -final class SquareBracketExpressionParser extends AbstractExpressionParser implements InfixExpressionParserInterface +final class SquareBracketExpressionParser extends AbstractExpressionParser implements InfixExpressionParserInterface, ExpressionParserDescriptionInterface { public function parse(Parser $parser, AbstractExpression $expr, Token $token): AbstractExpression { @@ -73,9 +74,14 @@ public function getName(): string return '['; } + public function getDescription(): string + { + return 'Array access'; + } + public function getPrecedence(): int { - return 300; + return 512; } public function getAssociativity(): InfixAssociativity diff --git a/src/ExpressionParser/Prefix/GroupingExpressionParser.php b/src/ExpressionParser/Prefix/GroupingExpressionParser.php index ac9f6c9dbe3..5c6608da401 100644 --- a/src/ExpressionParser/Prefix/GroupingExpressionParser.php +++ b/src/ExpressionParser/Prefix/GroupingExpressionParser.php @@ -13,6 +13,7 @@ use Twig\Error\SyntaxError; use Twig\ExpressionParser\AbstractExpressionParser; +use Twig\ExpressionParser\ExpressionParserDescriptionInterface; use Twig\ExpressionParser\PrefixExpressionParserInterface; use Twig\Node\Expression\AbstractExpression; use Twig\Node\Expression\ListExpression; @@ -23,7 +24,7 @@ /** * @internal */ -final class GroupingExpressionParser extends AbstractExpressionParser implements PrefixExpressionParserInterface +final class GroupingExpressionParser extends AbstractExpressionParser implements PrefixExpressionParserInterface, ExpressionParserDescriptionInterface { public function parse(Parser $parser, Token $token): AbstractExpression { @@ -65,6 +66,11 @@ public function getName(): string return '('; } + public function getDescription(): string + { + return 'Explicit group expression (a)'; + } + public function getPrecedence(): int { return 0; diff --git a/src/ExpressionParser/Prefix/LiteralExpressionParser.php b/src/ExpressionParser/Prefix/LiteralExpressionParser.php index 92540de75fd..e0e513273fb 100644 --- a/src/ExpressionParser/Prefix/LiteralExpressionParser.php +++ b/src/ExpressionParser/Prefix/LiteralExpressionParser.php @@ -13,8 +13,7 @@ use Twig\Error\SyntaxError; use Twig\ExpressionParser\AbstractExpressionParser; -use Twig\ExpressionParser\ExpressionParserType; -use Twig\ExpressionParser\PrecedenceChange; +use Twig\ExpressionParser\ExpressionParserDescriptionInterface; use Twig\ExpressionParser\PrefixExpressionParserInterface; use Twig\Lexer; use Twig\Node\Expression\AbstractExpression; @@ -28,7 +27,7 @@ /** * @internal */ -final class LiteralExpressionParser extends AbstractExpressionParser implements PrefixExpressionParserInterface +final class LiteralExpressionParser extends AbstractExpressionParser implements PrefixExpressionParserInterface, ExpressionParserDescriptionInterface { private string $type = 'literal'; @@ -112,6 +111,11 @@ public function getName(): string return $this->type; } + public function getDescription(): string + { + return 'A literal value (boolean, string, number, sequence, mapping, ...)'; + } + public function getPrecedence(): int { // not used diff --git a/src/ExpressionParser/Prefix/UnaryOperatorExpressionParser.php b/src/ExpressionParser/Prefix/UnaryOperatorExpressionParser.php index 4357d4ff6ab..35468940a14 100644 --- a/src/ExpressionParser/Prefix/UnaryOperatorExpressionParser.php +++ b/src/ExpressionParser/Prefix/UnaryOperatorExpressionParser.php @@ -12,6 +12,7 @@ namespace Twig\ExpressionParser\Prefix; use Twig\ExpressionParser\AbstractExpressionParser; +use Twig\ExpressionParser\ExpressionParserDescriptionInterface; use Twig\ExpressionParser\PrecedenceChange; use Twig\ExpressionParser\PrefixExpressionParserInterface; use Twig\Node\Expression\AbstractExpression; @@ -22,7 +23,7 @@ /** * @internal */ -final class UnaryOperatorExpressionParser extends AbstractExpressionParser implements PrefixExpressionParserInterface +final class UnaryOperatorExpressionParser extends AbstractExpressionParser implements PrefixExpressionParserInterface, ExpressionParserDescriptionInterface { public function __construct( /** @var class-string */ @@ -30,6 +31,7 @@ public function __construct( private string $name, private int $precedence, private ?PrecedenceChange $precedenceChange = null, + private ?string $description = null, private array $aliases = [], ) { } @@ -47,6 +49,11 @@ public function getName(): string return $this->name; } + public function getDescription(): string + { + return $this->description ?? ''; + } + public function getPrecedence(): int { return $this->precedence; diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index 2b6a9f05878..89bc2cf6f0d 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -328,12 +328,14 @@ public function getNodeVisitors(): array public function getExpressionParsers(): array { return [ + // unary operators new UnaryOperatorExpressionParser(NotUnary::class, 'not', 50, new PrecedenceChange('twig/twig', '3.15', 70)), new UnaryOperatorExpressionParser(NegUnary::class, '-', 500), new UnaryOperatorExpressionParser(PosUnary::class, '+', 500), - new BinaryOperatorExpressionParser(ElvisBinary::class, '?:', 5, InfixAssociativity::Right, aliases: ['? :']), - new BinaryOperatorExpressionParser(NullCoalesceBinary::class, '??', 300, InfixAssociativity::Right, new PrecedenceChange('twig/twig', '3.15', 5)), + // binary operators + new BinaryOperatorExpressionParser(ElvisBinary::class, '?:', 5, InfixAssociativity::Right, description: 'Elvis operator (a ?: b)', aliases: ['? :']), + new BinaryOperatorExpressionParser(NullCoalesceBinary::class, '??', 300, InfixAssociativity::Right, new PrecedenceChange('twig/twig', '3.15', 5), description: 'Null coalescing operator (a ?? b)'), new BinaryOperatorExpressionParser(OrBinary::class, 'or', 10), new BinaryOperatorExpressionParser(XorBinary::class, 'xor', 12), new BinaryOperatorExpressionParser(AndBinary::class, 'and', 15), @@ -360,22 +362,30 @@ public function getExpressionParsers(): array new BinaryOperatorExpressionParser(ConcatBinary::class, '~', 40, precedenceChange: new PrecedenceChange('twig/twig', '3.15', 27)), new BinaryOperatorExpressionParser(MulBinary::class, '*', 60), new BinaryOperatorExpressionParser(DivBinary::class, '/', 60), - new BinaryOperatorExpressionParser(FloorDivBinary::class, '//', 60), + new BinaryOperatorExpressionParser(FloorDivBinary::class, '//', 60, description: 'Floor division'), new BinaryOperatorExpressionParser(ModBinary::class, '%', 60), - new BinaryOperatorExpressionParser(PowerBinary::class, '**', 200, InfixAssociativity::Right), + new BinaryOperatorExpressionParser(PowerBinary::class, '**', 200, InfixAssociativity::Right, description: 'Exponentiation operator'), + // ternary operator new ConditionalTernaryExpressionParser(), + // Twig callables new IsExpressionParser(), new IsNotExpressionParser(), + new FilterExpressionParser(), + new FunctionExpressionParser(), + + // get attribute operators new DotExpressionParser(), new SquareBracketExpressionParser(), + // group expression new GroupingExpressionParser(), - new FilterExpressionParser(), - new FunctionExpressionParser(), + + // arrow function new ArrowExpressionParser(), + // all literals new LiteralExpressionParser(), ]; } diff --git a/src/ExtensionSet.php b/src/ExtensionSet.php index 262d1262579..c5e3321cc10 100644 --- a/src/ExtensionSet.php +++ b/src/ExtensionSet.php @@ -493,7 +493,7 @@ private function initExtension(ExtensionInterface $extension): void $expressionParsers = []; foreach ($operators[0] as $operator => $op) { - $expressionParsers[] = new UnaryOperatorExpressionParser($op['class'], $operator, $op['precedence'], $op['precedence_change'] ?? null, $op['aliases'] ?? []); + $expressionParsers[] = new UnaryOperatorExpressionParser($op['class'], $operator, $op['precedence'], $op['precedence_change'] ?? null, '', $op['aliases'] ?? []); } foreach ($operators[1] as $operator => $op) { $op['associativity'] = match ($op['associativity']) { diff --git a/src/Parser.php b/src/Parser.php index 1ddbae9813f..a1fd5927647 100644 --- a/src/Parser.php +++ b/src/Parser.php @@ -15,6 +15,7 @@ use Twig\Error\SyntaxError; use Twig\ExpressionParser\ExpressionParserInterface; use Twig\ExpressionParser\ExpressionParsers; +use Twig\ExpressionParser\ExpressionParserType; use Twig\ExpressionParser\Prefix\LiteralExpressionParser; use Twig\ExpressionParser\PrefixExpressionParserInterface; use Twig\Node\BlockNode; @@ -563,10 +564,11 @@ private function checkPrecedenceDeprecations(ExpressionParserInterface $expressi return; } + if ($expr->hasExplicitParentheses()) { + return; + } + if ($expressionParser instanceof PrefixExpressionParserInterface) { - if ($expr->hasExplicitParentheses()) { - return; - } /** @var AbstractExpression $node */ $node = $expr->getNode('node'); foreach ($precedenceChanges as $ep => $changes) { @@ -575,17 +577,17 @@ private function checkPrecedenceDeprecations(ExpressionParserInterface $expressi } if ($node->hasAttribute('expression_parser') && $ep === $node->getAttribute('expression_parser')) { $change = $expressionParser->getPrecedenceChange(); - trigger_deprecation($change->getPackage(), $change->getVersion(), \sprintf('Add explicit parentheses around the "%s" unary operator to avoid behavior change in the next major version as its precedence will change in "%s" at line %d.', $expressionParser->getName(), $this->getStream()->getSourceContext()->getName(), $node->getTemplateLine())); + trigger_deprecation($change->getPackage(), $change->getVersion(), \sprintf('As the "%s" %s operator will change its precedence in the next major version, add explicit parentheses to avoid behavior change in "%s" at line %d.', $expressionParser->getName(), ExpressionParserType::getType($expressionParser)->value, $this->getStream()->getSourceContext()->getName(), $node->getTemplateLine())); } } - } else { - foreach ($precedenceChanges[$expressionParser] as $ep) { - foreach ($expr as $node) { - /** @var AbstractExpression $node */ - if ($node->hasAttribute('expression_parser') && $ep === $node->getAttribute('expression_parser') && !$node->hasExplicitParentheses()) { - $change = $ep->getPrecedenceChange(); - trigger_deprecation($change->getPackage(), $change->getVersion(), \sprintf('Add explicit parentheses around the "%s" binary operator to avoid behavior change in the next major version as its precedence will change in "%s" at line %d.', $ep->getName(), $this->getStream()->getSourceContext()->getName(), $node->getTemplateLine())); - } + } + + foreach ($precedenceChanges[$expressionParser] as $ep) { + foreach ($expr as $node) { + /** @var AbstractExpression $node */ + if ($node->hasAttribute('expression_parser') && $ep === $node->getAttribute('expression_parser') && !$node->hasExplicitParentheses()) { + $change = $ep->getPrecedenceChange(); + trigger_deprecation($change->getPackage(), $change->getVersion(), \sprintf('As the "%s" %s operator will change its precedence in the next major version, add explicit parentheses to avoid behavior change in "%s" at line %d.', $ep->getName(), ExpressionParserType::getType($ep)->value, $this->getStream()->getSourceContext()->getName(), $node->getTemplateLine())); } } } diff --git a/tests/ExpressionParserTest.php b/tests/ExpressionParserTest.php index 7d263fbf66a..7808e6b6223 100644 --- a/tests/ExpressionParserTest.php +++ b/tests/ExpressionParserTest.php @@ -600,6 +600,86 @@ private static function createContextVariable(string $name, array $attributes): return $expression; } + + /** + * @dataProvider getBindingPowerTests + */ + public function testBindingPower(string $expression, string $expectedExpression, mixed $expectedResult, array $context = []) + { + $env = new Environment(new ArrayLoader([ + 'expression' => $expression, + 'expected' => $expectedExpression, + ])); + + $this->assertSame($env->render('expected', $context), $env->render('expression', $context)); + $this->assertEquals($expectedResult, $env->render('expression', $context)); + } + + public static function getBindingPowerTests(): iterable + { + // * / // % stronger than + - + foreach (['*', '/', '//', '%'] as $op1) { + foreach (['+', '-'] as $op2) { + $e = "12 $op1 6 $op2 3"; + if ('//' === $op1) { + $php = eval("return (int) floor(12 / 6) $op2 3;"); + } else { + $php = eval("return $e;"); + } + yield "$op1 vs $op2" => ["{{ $e }}", "{{ (12 $op1 6) $op2 3 }}", $php]; + + $e = "12 $op2 6 $op1 3"; + if ('//' === $op1) { + $php = eval("return 12 $op2 (int) floor(6 / 3);"); + } else { + $php = eval("return $e;"); + } + yield "$op2 vs $op1" => ["{{ $e }}", "{{ 12 $op2 (6 $op1 3) }}", $php]; + } + } + + // + - * / // % stronger than == != <=> < > >= <= `not in` `in` `matches` `starts with` `ends with` `has some` `has every` + foreach (['+', '-', '*', '/', '//', '%'] as $op1) { + foreach (['==', '!=', '<=>', '<', '>', '>=', '<='] as $op2) { + $e = "12 $op1 6 $op2 3"; + if ('//' === $op1) { + $php = eval("return (int) floor(12 / 6) $op2 3;"); + } else { + $php = eval("return $e;"); + } + yield "$op1 vs $op2" => ["{{ $e }}", "{{ (12 $op1 6) $op2 3 }}", $php]; + } + } + yield '+ vs not in' => ['{{ 1 + 2 not in [3, 4] }}', '{{ (1 + 2) not in [3, 4] }}', eval("return !in_array(1 + 2, [3, 4]);")]; + yield '+ vs in' => ['{{ 1 + 2 in [3, 4] }}', '{{ (1 + 2) in [3, 4] }}', eval("return in_array(1 + 2, [3, 4]);")]; + yield '+ vs matches' => ['{{ 1 + 2 matches "/^3$/" }}', '{{ (1 + 2) matches "/^3$/" }}', eval("return preg_match('/^3$/', 1 + 2);")]; + + // ~ stronger than `starts with` `ends with` + yield '~ vs starts with' => ['{{ "a" ~ "b" starts with "a" }}', '{{ ("a" ~ "b") starts with "a" }}', eval("return str_starts_with('ab', 'a');")]; + yield '~ vs ends with' => ['{{ "a" ~ "b" ends with "b" }}', '{{ ("a" ~ "b") ends with "b" }}', eval("return str_ends_with('ab', 'b');")]; + + // [] . stronger than anything else + $context = ['a' => ['b' => 1, 'c' => ['d' => 2]]]; + yield '[] vs unary -' => ['{{ -a["b"] + 3 }}', '{{ -(a["b"]) + 3 }}', eval("\$a = ['b' => 1]; return -\$a['b'] + 3;"), $context]; + yield '[] vs unary - (multiple levels)' => ['{{ -a["c"]["d"] }}', '{{ -((a["c"])["d"]) }}', eval("\$a = ['c' => ['d' => 2]]; return -\$a['c']['d'];"), $context]; + yield '. vs unary -' => ['{{ -a.b }}', '{{ -(a.b) }}', eval("\$a = ['b' => 1]; return -\$a['b'];"), $context]; + yield '. vs unary - (multiple levels)' => ['{{ -a.c.d }}', '{{ -((a.c).d) }}', eval("\$a = ['c' => ['d' => 2]]; return -\$a['c']['d'];"), $context]; + yield '. [] vs unary -' => ['{{ -a.c["d"] }}', '{{ -((a.c)["d"]) }}', eval("\$a = ['c' => ['d' => 2]]; return -\$a['c']['d'];"), $context]; + yield '[] . vs unary -' => ['{{ -a["c"].d }}', '{{ -((a["c"]).d) }}', eval("\$a = ['c' => ['d' => 2]]; return -\$a['c']['d'];"), $context]; + + // () stronger than anything else + yield '() vs unary -' => ['{{ -random(1, 1) + 3 }}', '{{ -(random(1, 1)) + 3 }}', eval("return -rand(1, 1) + 3;")]; + + // + - stronger than | + yield '+ vs |' => ['{{ 10 + 2|length }}', '{{ 10 + (2|length) }}', eval("return 10 + strlen(2);"), $context]; + + // - unary stronger than | + // To be uncomment in Twig 4.0 + //yield '- vs |' => ['{{ -1|abs }}', '{{ (-1)|abs }}', eval("return abs(-1);"), $context]; + + // ?? stronger than () + //yield '?? vs ()' => ['{{ (1 ?? "a") }}', '{{ ((1 ?? "a")) }}', eval("return 1;")]; + } } class NotReadyFunctionExpression extends FunctionExpression diff --git a/tests/Fixtures/expressions/postfix.test b/tests/Fixtures/expressions/postfix.test index 276cbf197d1..6217a8410a5 100644 --- a/tests/Fixtures/expressions/postfix.test +++ b/tests/Fixtures/expressions/postfix.test @@ -8,7 +8,7 @@ Twig parses postfix expressions {{ 'a' }} {{ 'a'|upper }} {{ ('a')|upper }} -{{ -1|upper }} +{{ (-1)|abs }} {{ macros.foo() }} {{ (macros).foo() }} --DATA-- @@ -17,6 +17,6 @@ return [] a A A --1 +1 foo foo diff --git a/tests/Fixtures/operators/contat_vs_add_sub.legacy.test b/tests/Fixtures/operators/contat_vs_add_sub.legacy.test index 541e4f7cb8a..a1370d2caed 100644 --- a/tests/Fixtures/operators/contat_vs_add_sub.legacy.test +++ b/tests/Fixtures/operators/contat_vs_add_sub.legacy.test @@ -1,8 +1,8 @@ --TEST-- +/- will have a higher precedence over ~ in Twig 4.0 --DEPRECATION-- -Since twig/twig 3.15: Add explicit parentheses around the "~" binary operator to avoid behavior change in the next major version as its precedence will change in "index.twig" at line 2. -Since twig/twig 3.15: Add explicit parentheses around the "~" binary operator to avoid behavior change in the next major version as its precedence will change in "index.twig" at line 3. +Since twig/twig 3.15: As the "~" infix operator will change its precedence in the next major version, add explicit parentheses to avoid behavior change in "index.twig" at line 2. +Since twig/twig 3.15: As the "~" infix operator will change its precedence in the next major version, add explicit parentheses to avoid behavior change in "index.twig" at line 3. --TEMPLATE-- {{ '42' ~ 1 + 41 }} {{ '42' ~ 43 - 1 }} diff --git a/tests/Fixtures/operators/minus_vs_pipe.legacy.test b/tests/Fixtures/operators/minus_vs_pipe.legacy.test new file mode 100644 index 00000000000..84eddeb21aa --- /dev/null +++ b/tests/Fixtures/operators/minus_vs_pipe.legacy.test @@ -0,0 +1,10 @@ +--TEST-- +| will have a higher precedence over + and - in Twig 4.0 +--DEPRECATION-- +Since twig/twig 3.20: As the "|" infix operator will change its precedence in the next major version, add explicit parentheses to avoid behavior change in "index.twig" at line 2. +--TEMPLATE-- +{{ -1|abs }} +--DATA-- +return [] +--EXPECT-- +-1 diff --git a/tests/Fixtures/operators/not_precedence.legacy.test b/tests/Fixtures/operators/not_precedence.legacy.test index 5178288e950..3a2f4a7ec3f 100644 --- a/tests/Fixtures/operators/not_precedence.legacy.test +++ b/tests/Fixtures/operators/not_precedence.legacy.test @@ -1,7 +1,7 @@ --TEST-- *, /, //, and % will have a higher precedence over not in Twig 4.0 --DEPRECATION-- -Since twig/twig 3.15: Add explicit parentheses around the "not" unary operator to avoid behavior change in the next major version as its precedence will change in "index.twig" at line 2. +Since twig/twig 3.15: As the "not" prefix operator will change its precedence in the next major version, add explicit parentheses to avoid behavior change in "index.twig" at line 2. --TEMPLATE-- {{ not 1 * 2 }} --DATA-- diff --git a/tests/Fixtures/tests/null_coalesce.legacy.test b/tests/Fixtures/tests/null_coalesce.legacy.test index 2b2036660c9..4aaec83743a 100644 --- a/tests/Fixtures/tests/null_coalesce.legacy.test +++ b/tests/Fixtures/tests/null_coalesce.legacy.test @@ -1,16 +1,16 @@ --TEST-- Twig supports the ?? operator --DEPRECATION-- -Since twig/twig 3.15: Add explicit parentheses around the "??" binary operator to avoid behavior change in the next major version as its precedence will change in "index.twig" at line 4. -Since twig/twig 3.15: Add explicit parentheses around the "??" binary operator to avoid behavior change in the next major version as its precedence will change in "index.twig" at line 5. -Since twig/twig 3.15: Add explicit parentheses around the "??" binary operator to avoid behavior change in the next major version as its precedence will change in "index.twig" at line 6. -Since twig/twig 3.15: Add explicit parentheses around the "??" binary operator to avoid behavior change in the next major version as its precedence will change in "index.twig" at line 7. -Since twig/twig 3.15: Add explicit parentheses around the "??" binary operator to avoid behavior change in the next major version as its precedence will change in "index.twig" at line 10. -Since twig/twig 3.15: Add explicit parentheses around the "~" binary operator to avoid behavior change in the next major version as its precedence will change in "index.twig" at line 9. -Since twig/twig 3.15: Add explicit parentheses around the "~" binary operator to avoid behavior change in the next major version as its precedence will change in "index.twig" at line 11. -Since twig/twig 3.15: Add explicit parentheses around the "??" binary operator to avoid behavior change in the next major version as its precedence will change in "index.twig" at line 16. -Since twig/twig 3.15: Add explicit parentheses around the "~" binary operator to avoid behavior change in the next major version as its precedence will change in "index.twig" at line 15. -Since twig/twig 3.15: Add explicit parentheses around the "~" binary operator to avoid behavior change in the next major version as its precedence will change in "index.twig" at line 17. +Since twig/twig 3.15: As the "??" infix operator will change its precedence in the next major version, add explicit parentheses to avoid behavior change in "index.twig" at line 4. +Since twig/twig 3.15: As the "??" infix operator will change its precedence in the next major version, add explicit parentheses to avoid behavior change in "index.twig" at line 5. +Since twig/twig 3.15: As the "??" infix operator will change its precedence in the next major version, add explicit parentheses to avoid behavior change in "index.twig" at line 6. +Since twig/twig 3.15: As the "??" infix operator will change its precedence in the next major version, add explicit parentheses to avoid behavior change in "index.twig" at line 7. +Since twig/twig 3.15: As the "??" infix operator will change its precedence in the next major version, add explicit parentheses to avoid behavior change in "index.twig" at line 10. +Since twig/twig 3.15: As the "~" infix operator will change its precedence in the next major version, add explicit parentheses to avoid behavior change in "index.twig" at line 9. +Since twig/twig 3.15: As the "~" infix operator will change its precedence in the next major version, add explicit parentheses to avoid behavior change in "index.twig" at line 11. +Since twig/twig 3.15: As the "??" infix operator will change its precedence in the next major version, add explicit parentheses to avoid behavior change in "index.twig" at line 16. +Since twig/twig 3.15: As the "~" infix operator will change its precedence in the next major version, add explicit parentheses to avoid behavior change in "index.twig" at line 15. +Since twig/twig 3.15: As the "~" infix operator will change its precedence in the next major version, add explicit parentheses to avoid behavior change in "index.twig" at line 17. --TEMPLATE-- {{ nope ?? nada ?? 'OK' -}} {# no deprecation as the operators have the same precedence #} From 4b57f48a656b3514d124e782b74b1be576d8ad05 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Thu, 6 Feb 2025 08:26:34 +0100 Subject: [PATCH 726/812] Use generics in ExpressionParsers --- src/ExpressionParser.php | 6 +-- src/ExpressionParser/ExpressionParsers.php | 59 ++++++++-------------- src/Parser.php | 7 +-- src/TokenParser/ApplyTokenParser.php | 2 +- tests/EnvironmentTest.php | 6 ++- 5 files changed, 34 insertions(+), 46 deletions(-) diff --git a/src/ExpressionParser.php b/src/ExpressionParser.php index 9922d11ec9b..d90d5f90bfd 100644 --- a/src/ExpressionParser.php +++ b/src/ExpressionParser.php @@ -162,10 +162,10 @@ public function parseSubscriptExpression($node) $parsers = new \ReflectionProperty($this->parser, 'parsers'); if ('.' === $this->parser->getStream()->next()->getValue()) { - return $parsers->getValue($this->parser)->getInfixByClass(DotExpressionParser::class)->parse($this->parser, $node, $this->parser->getCurrentToken()); + return $parsers->getValue($this->parser)->getByClass(DotExpressionParser::class)->parse($this->parser, $node, $this->parser->getCurrentToken()); } - return $parsers->getValue($this->parser)->getInfixByClass(SquareBracketExpressionParser::class)->parse($this->parser, $node, $this->parser->getCurrentToken()); + return $parsers->getValue($this->parser)->getByClass(SquareBracketExpressionParser::class)->parse($this->parser, $node, $this->parser->getCurrentToken()); } /** @@ -189,7 +189,7 @@ public function parseFilterExpressionRaw($node) $parsers = new \ReflectionProperty($this->parser, 'parsers'); - $op = $parsers->getValue($this->parser)->getInfixByClass(FilterExpressionParser::class); + $op = $parsers->getValue($this->parser)->getByClass(FilterExpressionParser::class); while (true) { $node = $op->parse($this->parser, $node, $this->parser->getCurrentToken()); if (!$this->parser->getStream()->test(Token::OPERATOR_TYPE, '|')) { diff --git a/src/ExpressionParser/ExpressionParsers.php b/src/ExpressionParser/ExpressionParsers.php index 9e4b468c565..61d6e961bd5 100644 --- a/src/ExpressionParser/ExpressionParsers.php +++ b/src/ExpressionParser/ExpressionParsers.php @@ -19,20 +19,15 @@ final class ExpressionParsers implements \IteratorAggregate { /** - * @var array, array> + * @var array, array> */ - private array $parsers = []; + private array $parsersByName = []; /** - * @var array, array, ExpressionParserInterface>> + * @var array, ExpressionParserInterface> */ private array $parsersByClass = []; - /** - * @var array, array> - */ - private array $aliases = []; - /** * @var \WeakMap>|null */ @@ -59,11 +54,11 @@ public function add(array $parsers): self trigger_deprecation('twig/twig', '3.20', 'Precedence for "%s" must be between 0 and 512, got %d.', $parser->getName(), $parser->getPrecedence()); // throw new \InvalidArgumentException(\sprintf('Precedence for "%s" must be between 0 and 512, got %d.', $parser->getName(), $parser->getPrecedence())); } - $type = ExpressionParserType::getType($parser); - $this->parsers[$type->value][$parser->getName()] = $parser; - $this->parsersByClass[$type->value][get_class($parser)] = $parser; + $interface = $parser instanceof PrefixExpressionParserInterface ? PrefixExpressionParserInterface::class : InfixExpressionParserInterface::class; + $this->parsersByName[$interface][$parser->getName()] = $parser; + $this->parsersByClass[get_class($parser)] = $parser; foreach ($parser->getAliases() as $alias) { - $this->aliases[$type->value][$alias] = $parser; + $this->parsersByName[$interface][$alias] = $parser; } } @@ -71,42 +66,32 @@ public function add(array $parsers): self } /** - * @param class-string $name + * @template T of ExpressionParserInterface + * + * @param class-string $class + * + * @return T|null */ - public function getPrefixByClass(string $name): ?PrefixExpressionParserInterface + public function getByClass(string $class): ?ExpressionParserInterface { - return $this->parsersByClass[ExpressionParserType::Prefix->value][$name] ?? null; - } - - public function getPrefix(string $name): ?PrefixExpressionParserInterface - { - return - $this->parsers[ExpressionParserType::Prefix->value][$name] - ?? $this->aliases[ExpressionParserType::Prefix->value][$name] - ?? null - ; + return $this->parsersByClass[$class] ?? null; } /** - * @param class-string $name + * @template T of ExpressionParserInterface + * + * @param class-string $interface + * + * @return T|null */ - public function getInfixByClass(string $name): ?InfixExpressionParserInterface - { - return $this->parsersByClass[ExpressionParserType::Infix->value][$name] ?? null; - } - - public function getInfix(string $name): ?InfixExpressionParserInterface + public function getByName(string $interface, string $name): ?ExpressionParserInterface { - return - $this->parsers[ExpressionParserType::Infix->value][$name] - ?? $this->aliases[ExpressionParserType::Infix->value][$name] - ?? null - ; + return $this->parsersByName[$interface][$name] ?? null; } public function getIterator(): \Traversable { - foreach ($this->parsers as $parsers) { + foreach ($this->parsersByName as $parsers) { // we don't yield the keys yield from $parsers; } diff --git a/src/Parser.php b/src/Parser.php index a1fd5927647..01c49b8d856 100644 --- a/src/Parser.php +++ b/src/Parser.php @@ -16,6 +16,7 @@ use Twig\ExpressionParser\ExpressionParserInterface; use Twig\ExpressionParser\ExpressionParsers; use Twig\ExpressionParser\ExpressionParserType; +use Twig\ExpressionParser\InfixExpressionParserInterface; use Twig\ExpressionParser\Prefix\LiteralExpressionParser; use Twig\ExpressionParser\PrefixExpressionParserInterface; use Twig\Node\BlockNode; @@ -357,16 +358,16 @@ public function getExpressionParser(): ExpressionParser public function parseExpression(int $precedence = 0): AbstractExpression { $token = $this->getCurrentToken(); - if ($token->test(Token::OPERATOR_TYPE) && $ep = $this->parsers->getPrefix($token->getValue())) { + if ($token->test(Token::OPERATOR_TYPE) && $ep = $this->parsers->getByName(PrefixExpressionParserInterface::class, $token->getValue())) { $this->getStream()->next(); $expr = $ep->parse($this, $token); $this->checkPrecedenceDeprecations($ep, $expr); } else { - $expr = $this->parsers->getPrefixByClass(LiteralExpressionParser::class)->parse($this, $token); + $expr = $this->parsers->getByClass(LiteralExpressionParser::class)->parse($this, $token); } $token = $this->getCurrentToken(); - while ($token->test(Token::OPERATOR_TYPE) && ($ep = $this->parsers->getInfix($token->getValue())) && $ep->getPrecedence() >= $precedence) { + while ($token->test(Token::OPERATOR_TYPE) && ($ep = $this->parsers->getByName(InfixExpressionParserInterface::class, $token->getValue())) && $ep->getPrecedence() >= $precedence) { $this->getStream()->next(); $expr = $ep->parse($this, $expr, $token); $this->checkPrecedenceDeprecations($ep, $expr); diff --git a/src/TokenParser/ApplyTokenParser.php b/src/TokenParser/ApplyTokenParser.php index e4e3cfaebf0..5b560e74916 100644 --- a/src/TokenParser/ApplyTokenParser.php +++ b/src/TokenParser/ApplyTokenParser.php @@ -35,7 +35,7 @@ public function parse(Token $token): Node $lineno = $token->getLine(); $ref = new LocalVariable(null, $lineno); $filter = $ref; - $op = $this->parser->getEnvironment()->getExpressionParsers()->getInfixByClass(FilterExpressionParser::class); + $op = $this->parser->getEnvironment()->getExpressionParsers()->getByClass(FilterExpressionParser::class); while (true) { $filter = $op->parse($this->parser, $filter, $this->parser->getCurrentToken()); if (!$this->parser->getStream()->test(Token::OPERATOR_TYPE, '|')) { diff --git a/tests/EnvironmentTest.php b/tests/EnvironmentTest.php index 5ddf07009bd..19f2fce4677 100644 --- a/tests/EnvironmentTest.php +++ b/tests/EnvironmentTest.php @@ -19,7 +19,9 @@ use Twig\Error\RuntimeError; use Twig\Error\SyntaxError; use Twig\ExpressionParser\Infix\BinaryOperatorExpressionParser; +use Twig\ExpressionParser\InfixExpressionParserInterface; use Twig\ExpressionParser\Prefix\UnaryOperatorExpressionParser; +use Twig\ExpressionParser\PrefixExpressionParserInterface; use Twig\Extension\AbstractExtension; use Twig\Extension\ExtensionInterface; use Twig\Extension\GlobalsInterface; @@ -309,8 +311,8 @@ public function testAddExtension() $this->assertArrayHasKey('foo_filter', $twig->getFilters()); $this->assertArrayHasKey('foo_function', $twig->getFunctions()); $this->assertArrayHasKey('foo_test', $twig->getTests()); - $this->assertNotNull($twig->getExpressionParsers()->getPrefix('foo_unary')); - $this->assertNotNull($twig->getExpressionParsers()->getInfix('foo_binary')); + $this->assertNotNull($twig->getExpressionParsers()->getByName(PrefixExpressionParserInterface::class, 'foo_unary')); + $this->assertNotNull($twig->getExpressionParsers()->getByName(InfixExpressionParserInterface::class, 'foo_binary')); $this->assertArrayHasKey('foo_global', $twig->getGlobals()); $visitors = $twig->getNodeVisitors(); $found = false; From 561c6629e126696a3292ba1a0c1edaca7932457b Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Fri, 14 Feb 2025 08:38:17 +0100 Subject: [PATCH 727/812] Remove obsolete code --- src/Lexer.php | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/Lexer.php b/src/Lexer.php index 26d8fa42437..982079cd941 100644 --- a/src/Lexer.php +++ b/src/Lexer.php @@ -342,15 +342,10 @@ private function lexExpression(): void // operators elseif (preg_match($this->regexes['operator'], $this->code, $match, 0, $this->cursor)) { $operator = preg_replace('/\s+/', ' ', $match[0]); - $type = Token::OPERATOR_TYPE; - // to be removed in 4.0 - if (str_contains(self::PUNCTUATION, $operator)) { - $type = Token::PUNCTUATION_TYPE; - } if (in_array($operator, $this->openingBrackets)) { $this->checkBrackets($operator); } - $this->pushToken($type, $operator); + $this->pushToken(Token::OPERATOR_TYPE, $operator); $this->moveCursor($match[0]); } // names From 251c0b5da92d03002f94bc96213b78e1f416fbdc Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Fri, 14 Feb 2025 08:43:56 +0100 Subject: [PATCH 728/812] Fix CS --- bin/generate_operators_precedence.php | 11 ++++++----- extra/cache-extra/TokenParser/CacheTokenParser.php | 2 +- src/ExpressionParser/ExpressionParserType.php | 4 ++-- src/ExpressionParser/ExpressionParsers.php | 9 ++++----- src/ExpressionParser/Infix/ArgumentsTrait.php | 2 +- src/ExpressionParser/Infix/IsExpressionParser.php | 2 +- src/ExpressionParser/Infix/IsNotExpressionParser.php | 2 +- .../Infix/SquareBracketExpressionParser.php | 2 +- .../Prefix/LiteralExpressionParser.php | 4 ++++ src/ExtensionSet.php | 4 ++-- src/Lexer.php | 6 +++--- tests/ExpressionParserTest.php | 12 ++++++------ 12 files changed, 32 insertions(+), 28 deletions(-) diff --git a/bin/generate_operators_precedence.php b/bin/generate_operators_precedence.php index 926989e6646..a95f18f57fb 100644 --- a/bin/generate_operators_precedence.php +++ b/bin/generate_operators_precedence.php @@ -16,9 +16,9 @@ use Twig\ExpressionParser\InfixExpressionParserInterface; use Twig\Loader\ArrayLoader; -require_once dirname(__DIR__).'/vendor/autoload.php'; +require_once \dirname(__DIR__).'/vendor/autoload.php'; -$output = fopen(dirname(__DIR__).'/doc/operators_precedence.rst', 'w'); +$output = fopen(\dirname(__DIR__).'/doc/operators_precedence.rst', 'w'); $twig = new Environment(new ArrayLoader([])); $expressionParsers = []; @@ -28,7 +28,7 @@ fwrite($output, "\n=========== ================ ======= ============= ===========\n"); fwrite($output, "Precedence Operator Type Associativity Description\n"); -fwrite($output, "=========== ================ ======= ============= ==========="); +fwrite($output, '=========== ================ ======= ============= ==========='); usort($expressionParsers, fn ($a, $b) => $b->getPrecedence() <=> $a->getPrecedence()); @@ -57,11 +57,12 @@ fwrite($output, "\n=========== ================ ======= ============= ===========\n"); fwrite($output, "Precedence Operator Type Associativity Description\n"); -fwrite($output, "=========== ================ ======= ============= ==========="); +fwrite($output, '=========== ================ ======= ============= ==========='); -usort($expressionParsers, function($a, $b) { +usort($expressionParsers, function ($a, $b) { $aPrecedence = $a->getPrecedenceChange() ? $a->getPrecedenceChange()->getNewPrecedence() : $a->getPrecedence(); $bPrecedence = $b->getPrecedenceChange() ? $b->getPrecedenceChange()->getNewPrecedence() : $b->getPrecedence(); + return $bPrecedence - $aPrecedence; }); diff --git a/extra/cache-extra/TokenParser/CacheTokenParser.php b/extra/cache-extra/TokenParser/CacheTokenParser.php index 086fad88eb6..dc33b1f5c5e 100644 --- a/extra/cache-extra/TokenParser/CacheTokenParser.php +++ b/extra/cache-extra/TokenParser/CacheTokenParser.php @@ -30,7 +30,7 @@ public function parse(Token $token): Node $tags = null; while ($stream->test(Token::NAME_TYPE)) { $k = $stream->getCurrent()->getValue(); - if (!in_array($k, ['ttl', 'tags'])) { + if (!\in_array($k, ['ttl', 'tags'])) { throw new SyntaxError(\sprintf('Unknown "%s" configuration.', $k), $stream->getCurrent()->getLine(), $stream->getSourceContext()); } diff --git a/src/ExpressionParser/ExpressionParserType.php b/src/ExpressionParser/ExpressionParserType.php index 0a980a8ec40..8c21a8d7633 100644 --- a/src/ExpressionParser/ExpressionParserType.php +++ b/src/ExpressionParser/ExpressionParserType.php @@ -19,7 +19,7 @@ enum ExpressionParserType: string case Prefix = 'prefix'; case Infix = 'infix'; - static public function getType(object $object): ExpressionParserType + public static function getType(object $object): ExpressionParserType { if ($object instanceof PrefixExpressionParserInterface) { return self::Prefix; @@ -28,6 +28,6 @@ static public function getType(object $object): ExpressionParserType return self::Infix; } - throw new \InvalidArgumentException(\sprintf('Unsupported expression parser type: %s', \get_class($object))); + throw new \InvalidArgumentException(\sprintf('Unsupported expression parser type: %s', $object::class)); } } diff --git a/src/ExpressionParser/ExpressionParsers.php b/src/ExpressionParser/ExpressionParsers.php index 61d6e961bd5..7b8da28756a 100644 --- a/src/ExpressionParser/ExpressionParsers.php +++ b/src/ExpressionParser/ExpressionParsers.php @@ -36,9 +36,8 @@ final class ExpressionParsers implements \IteratorAggregate /** * @param array $parsers */ - public function __construct( - array $parsers = [], - ) { + public function __construct(array $parsers = []) + { $this->add($parsers); } @@ -47,7 +46,7 @@ public function __construct( * * @return $this */ - public function add(array $parsers): self + public function add(array $parsers): static { foreach ($parsers as $parser) { if ($parser->getPrecedence() > 512 || $parser->getPrecedence() < 0) { @@ -56,7 +55,7 @@ public function add(array $parsers): self } $interface = $parser instanceof PrefixExpressionParserInterface ? PrefixExpressionParserInterface::class : InfixExpressionParserInterface::class; $this->parsersByName[$interface][$parser->getName()] = $parser; - $this->parsersByClass[get_class($parser)] = $parser; + $this->parsersByClass[$parser::class] = $parser; foreach ($parser->getAliases() as $alias) { $this->parsersByName[$interface][$alias] = $parser; } diff --git a/src/ExpressionParser/Infix/ArgumentsTrait.php b/src/ExpressionParser/Infix/ArgumentsTrait.php index b60a8481053..185ec51a0b5 100644 --- a/src/ExpressionParser/Infix/ArgumentsTrait.php +++ b/src/ExpressionParser/Infix/ArgumentsTrait.php @@ -62,7 +62,7 @@ private function parseNamedArguments(Parser $parser, bool $parseOpenParenthesis $name = null; if (($token = $stream->nextIf(Token::OPERATOR_TYPE, '=')) || ($token = $stream->nextIf(Token::PUNCTUATION_TYPE, ':'))) { if (!$value instanceof ContextVariable) { - throw new SyntaxError(\sprintf('A parameter name must be a string, "%s" given.', \get_class($value)), $token->getLine(), $stream->getSourceContext()); + throw new SyntaxError(\sprintf('A parameter name must be a string, "%s" given.', $value::class), $token->getLine(), $stream->getSourceContext()); } $name = $value->getAttribute('name'); $value = $parser->parseExpression(); diff --git a/src/ExpressionParser/Infix/IsExpressionParser.php b/src/ExpressionParser/Infix/IsExpressionParser.php index 1614c3cb991..88d54f70a7b 100644 --- a/src/ExpressionParser/Infix/IsExpressionParser.php +++ b/src/ExpressionParser/Infix/IsExpressionParser.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ - namespace Twig\ExpressionParser\Infix; +namespace Twig\ExpressionParser\Infix; use Twig\Attribute\FirstClassTwigCallableReady; use Twig\ExpressionParser\AbstractExpressionParser; diff --git a/src/ExpressionParser/Infix/IsNotExpressionParser.php b/src/ExpressionParser/Infix/IsNotExpressionParser.php index 55c0844ced7..1e1085aa835 100644 --- a/src/ExpressionParser/Infix/IsNotExpressionParser.php +++ b/src/ExpressionParser/Infix/IsNotExpressionParser.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ - namespace Twig\ExpressionParser\Infix; +namespace Twig\ExpressionParser\Infix; use Twig\Node\Expression\AbstractExpression; use Twig\Node\Expression\Unary\NotUnary; diff --git a/src/ExpressionParser/Infix/SquareBracketExpressionParser.php b/src/ExpressionParser/Infix/SquareBracketExpressionParser.php index 25fb153c7cf..c47c91dee36 100644 --- a/src/ExpressionParser/Infix/SquareBracketExpressionParser.php +++ b/src/ExpressionParser/Infix/SquareBracketExpressionParser.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ - namespace Twig\ExpressionParser\Infix; +namespace Twig\ExpressionParser\Infix; use Twig\ExpressionParser\AbstractExpressionParser; use Twig\ExpressionParser\ExpressionParserDescriptionInterface; diff --git a/src/ExpressionParser/Prefix/LiteralExpressionParser.php b/src/ExpressionParser/Prefix/LiteralExpressionParser.php index e0e513273fb..67bae6c3241 100644 --- a/src/ExpressionParser/Prefix/LiteralExpressionParser.php +++ b/src/ExpressionParser/Prefix/LiteralExpressionParser.php @@ -41,11 +41,13 @@ public function parse(Parser $parser, Token $token): AbstractExpression case 'true': case 'TRUE': $this->type = 'constant'; + return new ConstantExpression(true, $token->getLine()); case 'false': case 'FALSE': $this->type = 'constant'; + return new ConstantExpression(false, $token->getLine()); case 'none': @@ -53,10 +55,12 @@ public function parse(Parser $parser, Token $token): AbstractExpression case 'null': case 'NULL': $this->type = 'constant'; + return new ConstantExpression(null, $token->getLine()); default: $this->type = 'variable'; + return new ContextVariable($token->getValue(), $token->getLine()); } diff --git a/src/ExtensionSet.php b/src/ExtensionSet.php index c5e3321cc10..b1f4fc82cac 100644 --- a/src/ExtensionSet.php +++ b/src/ExtensionSet.php @@ -509,8 +509,8 @@ private function initExtension(ExtensionInterface $extension): void } } - if (count($expressionParsers)) { - trigger_deprecation('twig/twig', '3.20', \sprintf('Extension "%s" uses the old signature for "getOperators()", please implement "getExpressionParsers()" instead.', \get_class($extension))); + if (\count($expressionParsers)) { + trigger_deprecation('twig/twig', '3.20', \sprintf('Extension "%s" uses the old signature for "getOperators()", please implement "getExpressionParsers()" instead.', $extension::class)); $this->expressionParsers->add($expressionParsers); } diff --git a/src/Lexer.php b/src/Lexer.php index 982079cd941..c9f2f0adc20 100644 --- a/src/Lexer.php +++ b/src/Lexer.php @@ -342,7 +342,7 @@ private function lexExpression(): void // operators elseif (preg_match($this->regexes['operator'], $this->code, $match, 0, $this->cursor)) { $operator = preg_replace('/\s+/', ' ', $match[0]); - if (in_array($operator, $this->openingBrackets)) { + if (\in_array($operator, $this->openingBrackets)) { $this->checkBrackets($operator); } $this->pushToken(Token::OPERATOR_TYPE, $operator); @@ -579,9 +579,9 @@ private function popState(): void private function checkBrackets(string $code): void { // opening bracket - if (in_array($code, $this->openingBrackets)) { + if (\in_array($code, $this->openingBrackets)) { $this->brackets[] = [$code, $this->lineno]; - } elseif (in_array($code, $this->closingBrackets)) { + } elseif (\in_array($code, $this->closingBrackets)) { // closing bracket if (!$this->brackets) { throw new SyntaxError(\sprintf('Unexpected "%s".', $code), $this->lineno, $this->source); diff --git a/tests/ExpressionParserTest.php b/tests/ExpressionParserTest.php index 7808e6b6223..bed218cc0fb 100644 --- a/tests/ExpressionParserTest.php +++ b/tests/ExpressionParserTest.php @@ -650,8 +650,8 @@ public static function getBindingPowerTests(): iterable yield "$op1 vs $op2" => ["{{ $e }}", "{{ (12 $op1 6) $op2 3 }}", $php]; } } - yield '+ vs not in' => ['{{ 1 + 2 not in [3, 4] }}', '{{ (1 + 2) not in [3, 4] }}', eval("return !in_array(1 + 2, [3, 4]);")]; - yield '+ vs in' => ['{{ 1 + 2 in [3, 4] }}', '{{ (1 + 2) in [3, 4] }}', eval("return in_array(1 + 2, [3, 4]);")]; + yield '+ vs not in' => ['{{ 1 + 2 not in [3, 4] }}', '{{ (1 + 2) not in [3, 4] }}', eval('return !in_array(1 + 2, [3, 4]);')]; + yield '+ vs in' => ['{{ 1 + 2 in [3, 4] }}', '{{ (1 + 2) in [3, 4] }}', eval('return in_array(1 + 2, [3, 4]);')]; yield '+ vs matches' => ['{{ 1 + 2 matches "/^3$/" }}', '{{ (1 + 2) matches "/^3$/" }}', eval("return preg_match('/^3$/', 1 + 2);")]; // ~ stronger than `starts with` `ends with` @@ -668,17 +668,17 @@ public static function getBindingPowerTests(): iterable yield '[] . vs unary -' => ['{{ -a["c"].d }}', '{{ -((a["c"]).d) }}', eval("\$a = ['c' => ['d' => 2]]; return -\$a['c']['d'];"), $context]; // () stronger than anything else - yield '() vs unary -' => ['{{ -random(1, 1) + 3 }}', '{{ -(random(1, 1)) + 3 }}', eval("return -rand(1, 1) + 3;")]; + yield '() vs unary -' => ['{{ -random(1, 1) + 3 }}', '{{ -(random(1, 1)) + 3 }}', eval('return -rand(1, 1) + 3;')]; // + - stronger than | - yield '+ vs |' => ['{{ 10 + 2|length }}', '{{ 10 + (2|length) }}', eval("return 10 + strlen(2);"), $context]; + yield '+ vs |' => ['{{ 10 + 2|length }}', '{{ 10 + (2|length) }}', eval('return 10 + strlen(2);'), $context]; // - unary stronger than | // To be uncomment in Twig 4.0 - //yield '- vs |' => ['{{ -1|abs }}', '{{ (-1)|abs }}', eval("return abs(-1);"), $context]; + // yield '- vs |' => ['{{ -1|abs }}', '{{ (-1)|abs }}', eval("return abs(-1);"), $context]; // ?? stronger than () - //yield '?? vs ()' => ['{{ (1 ?? "a") }}', '{{ ((1 ?? "a")) }}', eval("return 1;")]; + // yield '?? vs ()' => ['{{ (1 ?? "a") }}', '{{ ((1 ?? "a")) }}', eval("return 1;")]; } } From 79d828b09db324bc36a56b99d5d6536f979e36c5 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Fri, 14 Feb 2025 08:47:54 +0100 Subject: [PATCH 729/812] Fix version --- doc/deprecated.rst | 16 +++--- doc/filters/number_format.rst | 2 +- src/ExpressionParser.php | 50 +++++++++---------- src/ExpressionParser/ExpressionParsers.php | 2 +- .../Infix/FilterExpressionParser.php | 2 +- src/ExtensionSet.php | 4 +- src/OperatorPrecedenceChange.php | 2 +- src/Parser.php | 4 +- src/Token.php | 8 +-- .../operators/minus_vs_pipe.legacy.test | 2 +- 10 files changed, 46 insertions(+), 46 deletions(-) diff --git a/doc/deprecated.rst b/doc/deprecated.rst index 0b69f913957..6e8212fb0e4 100644 --- a/doc/deprecated.rst +++ b/doc/deprecated.rst @@ -218,9 +218,9 @@ Parser 3.12. * The ``Twig\Parser::getExpressionParser()`` method is deprecated as of Twig - 3.20, use ``Twig\Parser::parseExpression()`` instead. + 3.21, use ``Twig\Parser::parseExpression()`` instead. -* The ``Twig\ExpressionParser`` class is deprecated as of Twig 3.20: +* The ``Twig\ExpressionParser`` class is deprecated as of Twig 3.21: * ``parseExpression()``, use ``Parser::parseExpression()`` * ``parsePrimaryExpression()``, use ``Parser::parseExpression()`` @@ -247,7 +247,7 @@ Token * The ``Token::getType()`` method is deprecated as of Twig 3.19, use ``Token::test()`` instead. -* The ``Token::ARROW_TYPE`` constant is deprecated as of Twig 3.20, the arrow +* The ``Token::ARROW_TYPE`` constant is deprecated as of Twig 3.21, the arrow ``=>`` is now an operator (``Token::OPERATOR_TYPE``). * The ``Token::PUNCTUATION_TYPE`` with values ``(``, ``[``, ``|``, ``.``, @@ -378,7 +378,7 @@ Node Operators --------- -* An operator precedence must be part of the [0, 512] range as of Twig 3.20. +* An operator precedence must be part of the [0, 512] range as of Twig 3.21. * The ``.`` operator allows accessing class constants as of Twig 3.15. This can be a BC break if you don't use UPPERCASE constant names. @@ -436,10 +436,10 @@ Operators {{ (not 1) * 2 }} {# this is equivalent to what Twig 4.x will do without the parentheses #} * Using the ``|`` operator in an expression with ``+`` or ``-`` without explicit - parentheses to clarify precedence triggers a deprecation as of Twig 3.20 (in + parentheses to clarify precedence triggers a deprecation as of Twig 3.21 (in Twig 4.0, ``|`` will have a higher precedence than ``+`` and ``-``). - For example, the following expression will trigger a deprecation in Twig 3.20:: + For example, the following expression will trigger a deprecation in Twig 3.21:: {{ -1|abs }} @@ -452,7 +452,7 @@ Operators {{ (-1)|abs }} {# this is equivalent to what Twig 4.x will do without the parentheses #} * The ``Twig\Extension\ExtensionInterface::getOperators()`` method is deprecated - as of Twig 3.20, use ``Twig\Extension\ExtensionInterface::getExpressionParsers()`` + as of Twig 3.21, use ``Twig\Extension\ExtensionInterface::getExpressionParsers()`` instead: Before: @@ -474,5 +474,5 @@ Operators ]; } -* The ``Twig\OperatorPrecedenceChange`` class is deprecated as of Twig 3.20, +* The ``Twig\OperatorPrecedenceChange`` class is deprecated as of Twig 3.21, use ``Twig\ExpressionParser\PrecedenceChange`` instead. diff --git a/doc/filters/number_format.rst b/doc/filters/number_format.rst index f9e9d718b25..4f68596fbe1 100644 --- a/doc/filters/number_format.rst +++ b/doc/filters/number_format.rst @@ -16,7 +16,7 @@ separator using the additional arguments: {{ 9800.333|number_format(2, '.', ',') }} To format negative numbers, wrap the previous statement with parentheses (note -that as of Twig 3.20, not using parentheses is deprecated as the filter +that as of Twig 3.21, not using parentheses is deprecated as the filter operator will change precedence in Twig 4.0): .. code-block:: twig diff --git a/src/ExpressionParser.php b/src/ExpressionParser.php index d90d5f90bfd..60ebcb66787 100644 --- a/src/ExpressionParser.php +++ b/src/ExpressionParser.php @@ -36,16 +36,16 @@ * * @author Fabien Potencier * - * @deprecated since Twig 3.20 + * @deprecated since Twig 3.21 */ class ExpressionParser { /** - * @deprecated since Twig 3.20 + * @deprecated since Twig 3.21 */ public const OPERATOR_LEFT = 1; /** - * @deprecated since Twig 3.20 + * @deprecated since Twig 3.21 */ public const OPERATOR_RIGHT = 2; @@ -53,7 +53,7 @@ public function __construct( private Parser $parser, private Environment $env, ) { - trigger_deprecation('twig/twig', '3.20', 'Class "%s" is deprecated, use "Parser::parseExpression()" instead.', __CLASS__); + trigger_deprecation('twig/twig', '3.21', 'Class "%s" is deprecated, use "Parser::parseExpression()" instead.', __CLASS__); } public function parseExpression($precedence = 0) @@ -62,27 +62,27 @@ public function parseExpression($precedence = 0) trigger_deprecation('twig/twig', '3.15', 'Passing a second argument ($allowArrow) to "%s()" is deprecated.', __METHOD__); } - trigger_deprecation('twig/twig', '3.20', 'The "%s()" method is deprecated, use "Parser::parseExpression()" instead.', __METHOD__); + trigger_deprecation('twig/twig', '3.21', 'The "%s()" method is deprecated, use "Parser::parseExpression()" instead.', __METHOD__); return $this->parser->parseExpression((int) $precedence); } /** - * @deprecated since Twig 3.20 + * @deprecated since Twig 3.21 */ public function parsePrimaryExpression() { - trigger_deprecation('twig/twig', '3.20', 'The "%s()" method is deprecated.', __METHOD__); + trigger_deprecation('twig/twig', '3.21', 'The "%s()" method is deprecated.', __METHOD__); return $this->parseExpression(); } /** - * @deprecated since Twig 3.20 + * @deprecated since Twig 3.21 */ public function parseStringExpression() { - trigger_deprecation('twig/twig', '3.20', 'The "%s()" method is deprecated.', __METHOD__); + trigger_deprecation('twig/twig', '3.21', 'The "%s()" method is deprecated.', __METHOD__); return $this->parseExpression(); } @@ -98,11 +98,11 @@ public function parseArrayExpression() } /** - * @deprecated since Twig 3.20 + * @deprecated since Twig 3.21 */ public function parseSequenceExpression() { - trigger_deprecation('twig/twig', '3.20', 'The "%s()" method is deprecated.', __METHOD__); + trigger_deprecation('twig/twig', '3.21', 'The "%s()" method is deprecated.', __METHOD__); return $this->parseExpression(); } @@ -118,21 +118,21 @@ public function parseHashExpression() } /** - * @deprecated since Twig 3.20 + * @deprecated since Twig 3.21 */ public function parseMappingExpression() { - trigger_deprecation('twig/twig', '3.20', 'The "%s()" method is deprecated.', __METHOD__); + trigger_deprecation('twig/twig', '3.21', 'The "%s()" method is deprecated.', __METHOD__); return $this->parseExpression(); } /** - * @deprecated since Twig 3.20 + * @deprecated since Twig 3.21 */ public function parsePostfixExpression($node) { - trigger_deprecation('twig/twig', '3.20', 'The "%s()" method is deprecated.', __METHOD__); + trigger_deprecation('twig/twig', '3.21', 'The "%s()" method is deprecated.', __METHOD__); while (true) { $token = $this->parser->getCurrentToken(); @@ -153,11 +153,11 @@ public function parsePostfixExpression($node) } /** - * @deprecated since Twig 3.20 + * @deprecated since Twig 3.21 */ public function parseSubscriptExpression($node) { - trigger_deprecation('twig/twig', '3.20', 'The "%s()" method is deprecated.', __METHOD__); + trigger_deprecation('twig/twig', '3.21', 'The "%s()" method is deprecated.', __METHOD__); $parsers = new \ReflectionProperty($this->parser, 'parsers'); @@ -169,11 +169,11 @@ public function parseSubscriptExpression($node) } /** - * @deprecated since Twig 3.20 + * @deprecated since Twig 3.21 */ public function parseFilterExpression($node) { - trigger_deprecation('twig/twig', '3.20', 'The "%s()" method is deprecated.', __METHOD__); + trigger_deprecation('twig/twig', '3.21', 'The "%s()" method is deprecated.', __METHOD__); $this->parser->getStream()->next(); @@ -181,11 +181,11 @@ public function parseFilterExpression($node) } /** - * @deprecated since Twig 3.20 + * @deprecated since Twig 3.21 */ public function parseFilterExpressionRaw($node) { - trigger_deprecation('twig/twig', '3.20', 'The "%s()" method is deprecated.', __METHOD__); + trigger_deprecation('twig/twig', '3.21', 'The "%s()" method is deprecated.', __METHOD__); $parsers = new \ReflectionProperty($this->parser, 'parsers'); @@ -294,11 +294,11 @@ public function parseArguments() } /** - * @deprecated since Twig 3.20, use "AbstractTokenParser::parseAssignmentExpression()" instead + * @deprecated since Twig 3.21, use "AbstractTokenParser::parseAssignmentExpression()" instead */ public function parseAssignmentExpression() { - trigger_deprecation('twig/twig', '3.20', 'The "%s()" method is deprecated, use "AbstractTokenParser::parseAssignmentExpression()" instead.', __METHOD__); + trigger_deprecation('twig/twig', '3.21', 'The "%s()" method is deprecated, use "AbstractTokenParser::parseAssignmentExpression()" instead.', __METHOD__); $stream = $this->parser->getStream(); $targets = []; @@ -321,11 +321,11 @@ public function parseAssignmentExpression() } /** - * @deprecated since Twig 3.20 + * @deprecated since Twig 3.21 */ public function parseMultitargetExpression() { - trigger_deprecation('twig/twig', '3.20', 'The "%s()" method is deprecated.', __METHOD__); + trigger_deprecation('twig/twig', '3.21', 'The "%s()" method is deprecated.', __METHOD__); $targets = []; while (true) { diff --git a/src/ExpressionParser/ExpressionParsers.php b/src/ExpressionParser/ExpressionParsers.php index 7b8da28756a..fb35a690e29 100644 --- a/src/ExpressionParser/ExpressionParsers.php +++ b/src/ExpressionParser/ExpressionParsers.php @@ -50,7 +50,7 @@ public function add(array $parsers): static { foreach ($parsers as $parser) { if ($parser->getPrecedence() > 512 || $parser->getPrecedence() < 0) { - trigger_deprecation('twig/twig', '3.20', 'Precedence for "%s" must be between 0 and 512, got %d.', $parser->getName(), $parser->getPrecedence()); + trigger_deprecation('twig/twig', '3.21', 'Precedence for "%s" must be between 0 and 512, got %d.', $parser->getName(), $parser->getPrecedence()); // throw new \InvalidArgumentException(\sprintf('Precedence for "%s" must be between 0 and 512, got %d.', $parser->getName(), $parser->getPrecedence())); } $interface = $parser instanceof PrefixExpressionParserInterface ? PrefixExpressionParserInterface::class : InfixExpressionParserInterface::class; diff --git a/src/ExpressionParser/Infix/FilterExpressionParser.php b/src/ExpressionParser/Infix/FilterExpressionParser.php index e47d3fe6773..0bbe6b40969 100644 --- a/src/ExpressionParser/Infix/FilterExpressionParser.php +++ b/src/ExpressionParser/Infix/FilterExpressionParser.php @@ -75,7 +75,7 @@ public function getPrecedence(): int public function getPrecedenceChange(): ?PrecedenceChange { - return new PrecedenceChange('twig/twig', '3.20', 300); + return new PrecedenceChange('twig/twig', '3.21', 300); } public function getAssociativity(): InfixAssociativity diff --git a/src/ExtensionSet.php b/src/ExtensionSet.php index b1f4fc82cac..5afa729f9ed 100644 --- a/src/ExtensionSet.php +++ b/src/ExtensionSet.php @@ -510,7 +510,7 @@ private function initExtension(ExtensionInterface $extension): void } if (\count($expressionParsers)) { - trigger_deprecation('twig/twig', '3.20', \sprintf('Extension "%s" uses the old signature for "getOperators()", please implement "getExpressionParsers()" instead.', $extension::class)); + trigger_deprecation('twig/twig', '3.21', \sprintf('Extension "%s" uses the old signature for "getOperators()", please implement "getExpressionParsers()" instead.', $extension::class)); $this->expressionParsers->add($expressionParsers); } @@ -518,7 +518,7 @@ private function initExtension(ExtensionInterface $extension): void private function convertInfixExpressionParser(string $nodeClass, string $operator, int $precedence, InfixAssociativity $associativity, ?PrecedenceChange $precedenceChange, array $aliases, callable $callable): InfixExpressionParserInterface { - trigger_deprecation('twig/twig', '3.20', \sprintf('Using a non-ExpressionParserInterface object to define the "%s" binary operator is deprecated.', $operator)); + trigger_deprecation('twig/twig', '3.21', \sprintf('Using a non-ExpressionParserInterface object to define the "%s" binary operator is deprecated.', $operator)); return new class($nodeClass, $operator, $precedence, $associativity, $precedenceChange, $aliases, $callable) extends BinaryOperatorExpressionParser { public function __construct( diff --git a/src/OperatorPrecedenceChange.php b/src/OperatorPrecedenceChange.php index e015301718e..31ebaef48cb 100644 --- a/src/OperatorPrecedenceChange.php +++ b/src/OperatorPrecedenceChange.php @@ -27,7 +27,7 @@ public function __construct( private string $version, private int $newPrecedence, ) { - trigger_deprecation('twig/twig', '3.20', 'The "%s" class is deprecated since Twig 3.20. Use "%s" instead.', self::class, PrecedenceChange::class); + trigger_deprecation('twig/twig', '3.21', 'The "%s" class is deprecated since Twig 3.21. Use "%s" instead.', self::class, PrecedenceChange::class); parent::__construct($package, $version, $newPrecedence); } diff --git a/src/Parser.php b/src/Parser.php index 01c49b8d856..5bbeadfd060 100644 --- a/src/Parser.php +++ b/src/Parser.php @@ -342,11 +342,11 @@ public function popLocalScope(): void } /** - * @deprecated since Twig 3.20 + * @deprecated since Twig 3.21 */ public function getExpressionParser(): ExpressionParser { - trigger_deprecation('twig/twig', '3.20', 'Method "%s()" is deprecated, use "parseExpression()" instead.', __METHOD__); + trigger_deprecation('twig/twig', '3.21', 'Method "%s()" is deprecated, use "parseExpression()" instead.', __METHOD__); if (null === $this->expressionParser) { $this->expressionParser = new ExpressionParser($this, $this->env); diff --git a/src/Token.php b/src/Token.php index 6390472e026..0d8b385ebfa 100644 --- a/src/Token.php +++ b/src/Token.php @@ -31,7 +31,7 @@ final class Token public const INTERPOLATION_START_TYPE = 10; public const INTERPOLATION_END_TYPE = 11; /** - * @deprecated since Twig 3.20, "arrow" is now an operator + * @deprecated since Twig 3.21, "arrow" is now an operator */ public const ARROW_TYPE = 12; public const SPREAD_TYPE = 13; @@ -42,7 +42,7 @@ public function __construct( private int $lineno, ) { if (self::ARROW_TYPE === $type) { - trigger_deprecation('twig/twig', '3.20', 'The "%s" token type is deprecated, "arrow" is now an operator.', self::ARROW_TYPE); + trigger_deprecation('twig/twig', '3.21', 'The "%s" token type is deprecated, "arrow" is now an operator.', self::ARROW_TYPE); } } @@ -70,7 +70,7 @@ public function test($type, $values = null): bool } if (self::ARROW_TYPE === $type) { - trigger_deprecation('twig/twig', '3.20', 'The "%s" token type is deprecated, "arrow" is now an operator.', self::typeToEnglish(self::ARROW_TYPE)); + trigger_deprecation('twig/twig', '3.21', 'The "%s" token type is deprecated, "arrow" is now an operator.', self::typeToEnglish(self::ARROW_TYPE)); return self::OPERATOR_TYPE === $this->type && '=>' === $this->value; } @@ -79,7 +79,7 @@ public function test($type, $values = null): bool if ($typeMatches && self::PUNCTUATION_TYPE === $type && \in_array($this->value, ['(', '[', '|', '.', '?', '?:']) && $values) { foreach ((array) $values as $value) { if (\in_array($value, ['(', '[', '|', '.', '?', '?:'])) { - trigger_deprecation('twig/twig', '3.20', 'The "%s" token is now an "%s" token instead of a "%s" one.', $this->value, self::typeToEnglish(self::OPERATOR_TYPE), $this->toEnglish()); + trigger_deprecation('twig/twig', '3.21', 'The "%s" token is now an "%s" token instead of a "%s" one.', $this->value, self::typeToEnglish(self::OPERATOR_TYPE), $this->toEnglish()); break; } diff --git a/tests/Fixtures/operators/minus_vs_pipe.legacy.test b/tests/Fixtures/operators/minus_vs_pipe.legacy.test index 84eddeb21aa..ac6cd3278a2 100644 --- a/tests/Fixtures/operators/minus_vs_pipe.legacy.test +++ b/tests/Fixtures/operators/minus_vs_pipe.legacy.test @@ -1,7 +1,7 @@ --TEST-- | will have a higher precedence over + and - in Twig 4.0 --DEPRECATION-- -Since twig/twig 3.20: As the "|" infix operator will change its precedence in the next major version, add explicit parentheses to avoid behavior change in "index.twig" at line 2. +Since twig/twig 3.21: As the "|" infix operator will change its precedence in the next major version, add explicit parentheses to avoid behavior change in "index.twig" at line 2. --TEMPLATE-- {{ -1|abs }} --DATA-- From 4695e33f732905afceafdc0714d0fa6558d26789 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Fri, 14 Feb 2025 11:27:48 +0100 Subject: [PATCH 730/812] Fix cache-extra impl --- extra/cache-extra/TokenParser/CacheTokenParser.php | 2 +- extra/cache-extra/composer.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/extra/cache-extra/TokenParser/CacheTokenParser.php b/extra/cache-extra/TokenParser/CacheTokenParser.php index dc33b1f5c5e..2dee747e1de 100644 --- a/extra/cache-extra/TokenParser/CacheTokenParser.php +++ b/extra/cache-extra/TokenParser/CacheTokenParser.php @@ -35,7 +35,7 @@ public function parse(Token $token): Node } $stream->next(); - $stream->expect(Token::PUNCTUATION_TYPE, '('); + $stream->expect(Token::OPERATOR_TYPE, '('); $line = $stream->getCurrent()->getLine(); if ($stream->test(Token::PUNCTUATION_TYPE, ')')) { throw new SyntaxError(\sprintf('The "%s" modifier takes exactly one argument (0 given).', $k), $line, $stream->getSourceContext()); diff --git a/extra/cache-extra/composer.json b/extra/cache-extra/composer.json index cd7919eddb0..d14bbbe13a3 100644 --- a/extra/cache-extra/composer.json +++ b/extra/cache-extra/composer.json @@ -17,7 +17,7 @@ "require": { "php": ">=8.1.0", "symfony/cache": "^5.4|^6.4|^7.0", - "twig/twig": "^3.20|^4.0" + "twig/twig": "^3.21|^4.0" }, "require-dev": { "symfony/phpunit-bridge": "^6.4|^7.0" From d6f539bb01ac208ee4182f59afc375033e7f39bc Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Fri, 14 Feb 2025 11:17:21 +0100 Subject: [PATCH 731/812] Avoid storing expression parser instances in Node attributes --- src/Parser.php | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/Parser.php b/src/Parser.php index 5bbeadfd060..63ecccbe405 100644 --- a/src/Parser.php +++ b/src/Parser.php @@ -43,6 +43,7 @@ class Parser { private $stack = []; + private ?\WeakMap $expressionRefs = null; private $stream; private $parent; private $visitors; @@ -94,6 +95,7 @@ public function parse(TokenStream $stream, $test = null, bool $dropNeedle = fals $this->blockStack = []; $this->importedSymbols = [[]]; $this->embeddedTemplates = []; + $this->expressionRefs = new \WeakMap(); try { $body = $this->subparse($test, $dropNeedle); @@ -111,6 +113,8 @@ public function parse(TokenStream $stream, $test = null, bool $dropNeedle = fals } throw $e; + } finally { + $this->expressionRefs = null; } $node = new ModuleNode(new BodyNode([$body]), $this->parent, new Nodes($this->blocks), new Nodes($this->macros), new Nodes($this->traits), $this->embeddedTemplates, $stream->getSourceContext()); @@ -557,7 +561,7 @@ private function filterBodyNodes(Node $node, bool $nested = false): ?Node private function checkPrecedenceDeprecations(ExpressionParserInterface $expressionParser, AbstractExpression $expr) { - $expr->setAttribute('expression_parser', $expressionParser); + $this->expressionRefs[$expr] = $expressionParser; $precedenceChanges = $this->parsers->getPrecedenceChanges(); // Check that the all nodes that are between the 2 precedences have explicit parentheses @@ -576,7 +580,7 @@ private function checkPrecedenceDeprecations(ExpressionParserInterface $expressi if (!\in_array($expressionParser, $changes, true)) { continue; } - if ($node->hasAttribute('expression_parser') && $ep === $node->getAttribute('expression_parser')) { + if (isset($this->expressionRefs[$node]) && $ep === $this->expressionRefs[$node]) { $change = $expressionParser->getPrecedenceChange(); trigger_deprecation($change->getPackage(), $change->getVersion(), \sprintf('As the "%s" %s operator will change its precedence in the next major version, add explicit parentheses to avoid behavior change in "%s" at line %d.', $expressionParser->getName(), ExpressionParserType::getType($expressionParser)->value, $this->getStream()->getSourceContext()->getName(), $node->getTemplateLine())); } @@ -586,7 +590,7 @@ private function checkPrecedenceDeprecations(ExpressionParserInterface $expressi foreach ($precedenceChanges[$expressionParser] as $ep) { foreach ($expr as $node) { /** @var AbstractExpression $node */ - if ($node->hasAttribute('expression_parser') && $ep === $node->getAttribute('expression_parser') && !$node->hasExplicitParentheses()) { + if (isset($this->expressionRefs[$node]) && $ep === $this->expressionRefs[$node] && !$node->hasExplicitParentheses()) { $change = $ep->getPrecedenceChange(); trigger_deprecation($change->getPackage(), $change->getVersion(), \sprintf('As the "%s" %s operator will change its precedence in the next major version, add explicit parentheses to avoid behavior change in "%s" at line %d.', $ep->getName(), ExpressionParserType::getType($ep)->value, $this->getStream()->getSourceContext()->getName(), $node->getTemplateLine())); } From 20e677b6c970934099e60e9f5fd2349cf9f99453 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Fri, 14 Feb 2025 11:33:18 +0100 Subject: [PATCH 732/812] Fix docs --- doc/operators_precedence.rst | 98 ++++++++++++++++++------------------ 1 file changed, 49 insertions(+), 49 deletions(-) diff --git a/doc/operators_precedence.rst b/doc/operators_precedence.rst index f603127f3dd..e681d752d46 100644 --- a/doc/operators_precedence.rst +++ b/doc/operators_precedence.rst @@ -53,52 +53,52 @@ When a precedence will change in 4.0, the new precedence is indicated by the arr Here is the same table for Twig 4.0 with adjusted precedences: -=========== ============== ======= ============= =========== -Precedence Operator Type Associativity Description -=========== ============== ======= ============= =========== -512 `(` infix Left Twig function call - `.` Get an attribute on a variable - `[` Array access -500 `-` prefix n/a - `+` -300 `|` infix Left Twig filter call -250 `=>` infix Left Arrow function (x => expr) -200 `**` infix Right Exponentiation operator -100 `is` infix Left Twig tests - `is not` Twig tests -70 `not` prefix n/a -60 `*` infix Left - `/` - `//` Floor division - `%` -30 `+` infix Left - `-` -27 `~` infix Left -25 `..` infix Left -20 `==` infix Left - `!=` - `<=>` - `<` - `>` - `>=` - `<=` - `not in` - `in` - `matches` - `starts with` - `ends with` - `has some` - `has every` -18 `b-and` infix Left -17 `b-xor` infix Left -16 `b-or` infix Left -15 `and` infix Left -12 `xor` infix Left -10 `or` infix Left -5 `??` infix Right Null coalescing operator (a ?? b) - `?:` Elvis operator (a ?: b) - `?:` Elvis operator (a ?: b) -0 `(` prefix n/a Explicit group expression (a) - `literal` A literal value (boolean, string, number, sequence, mapping, ...) - `?` infix Left Conditional operator (a ? b : c) -=========== ============== ======= ============= =========== +=========== ================ ======= ============= =========== +Precedence Operator Type Associativity Description +=========== ================ ======= ============= =========== +512 ``(`` infix Left Twig function call + ``.`` Get an attribute on a variable + ``[`` Array access +500 ``-`` prefix n/a + ``+`` +300 ``|`` infix Left Twig filter call +250 ``=>`` infix Left Arrow function (x => expr) +200 ``**`` infix Right Exponentiation operator +100 ``is`` infix Left Twig tests + ``is not`` Twig tests +70 ``not`` prefix n/a +60 ``*`` infix Left + ``/`` + ``//`` Floor division + ``%`` +30 ``+`` infix Left + ``-`` +27 ``~`` infix Left +25 ``..`` infix Left +20 ``==`` infix Left + ``!=`` + ``<=>`` + ``<`` + ``>`` + ``>=`` + ``<=`` + ``not in`` + ``in`` + ``matches`` + ``starts with`` + ``ends with`` + ``has some`` + ``has every`` +18 ``b-and`` infix Left +17 ``b-xor`` infix Left +16 ``b-or`` infix Left +15 ``and`` infix Left +12 ``xor`` infix Left +10 ``or`` infix Left +5 ``??`` infix Right Null coalescing operator (a ?? b) + ``?:`` Elvis operator (a ?: b) + ``?:`` Elvis operator (a ?: b) +0 ``(`` prefix n/a Explicit group expression (a) + ``literal`` A literal value (boolean, string, number, sequence, mapping, ...) + ``?`` infix Left Conditional operator (a ? b : c) +=========== ================ ======= ============= =========== From 3964aeba78df09eea3f288a33fd8b6c4c39bf56d Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Fri, 14 Feb 2025 10:34:17 +0100 Subject: [PATCH 733/812] Add a proper prefix spread operator --- doc/operators_precedence.rst | 6 +- src/ExpressionParser/Infix/ArgumentsTrait.php | 6 +- .../Prefix/LiteralExpressionParser.php | 16 ++-- src/Extension/CoreExtension.php | 2 + src/Lexer.php | 7 +- src/Node/Expression/ArrayExpression.php | 80 +++++-------------- src/NodeVisitor/SandboxNodeVisitor.php | 7 +- src/Token.php | 11 +++ tests/ExpressionParserTest.php | 15 +--- tests/LexerTest.php | 10 --- 10 files changed, 50 insertions(+), 110 deletions(-) diff --git a/doc/operators_precedence.rst b/doc/operators_precedence.rst index e681d752d46..6791e8b255a 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 185ec51a0b5..1c2ae49dd3a 100644 --- a/src/ExpressionParser/Infix/ArgumentsTrait.php +++ b/src/ExpressionParser/Infix/ArgumentsTrait.php @@ -50,13 +50,11 @@ private function parseNamedArguments(Parser $parser, bool $parseOpenParenthesis } } - 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 67bae6c3241..188b924453f 100644 --- a/src/ExpressionParser/Prefix/LiteralExpressionParser.php +++ b/src/ExpressionParser/Prefix/LiteralExpressionParser.php @@ -20,6 +20,7 @@ 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 @@ private function parseSequenceExpression(Parser $parser) } $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 @@ private function parseMappingExpression(Parser $parser) } $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 89bc2cf6f0d..039a36f439e 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -79,6 +79,7 @@ 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 @@ public function getExpressionParsers(): array 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 c9f2f0adc20..34715f5eae9 100644 --- a/src/Lexer.php +++ b/src/Lexer.php @@ -334,13 +334,8 @@ private function lexExpression(): void } } - // 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 61a5063f384..c9b3a3ec1ac 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 @@ public function addElement(AbstractExpression $value, ?AbstractExpression $key = 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 ($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 (!$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 ($nextIndex !== $key && !$pair['value'] instanceof SpreadUnary) { + $compiler + ->subcompile($pair['key']) + ->raw(' => ') + ; } - } + ++$nextIndex; - return false; + $compiler->subcompile($pair['value']); + } + $compiler->raw(']'); } } diff --git a/src/NodeVisitor/SandboxNodeVisitor.php b/src/NodeVisitor/SandboxNodeVisitor.php index 7e89ef83a1a..9dd48f5be95 100644 --- a/src/NodeVisitor/SandboxNodeVisitor.php +++ b/src/NodeVisitor/SandboxNodeVisitor.php @@ -123,12 +123,7 @@ private function wrapNode(Node $node, string $name): void { $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 0d8b385ebfa..73fd02e1d13 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 @@ public function __construct( 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 @@ public function test($type, $values = null): bool 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 bed218cc0fb..f134052927f 100644 --- a/tests/ExpressionParserTest.php +++ b/tests/ExpressionParserTest.php @@ -27,6 +27,7 @@ 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 @@ public static function getTestsForSequence() 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 @@ public static function getTestsForSequence() 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 @@ public function operator(Compiler $compiler): Compiler $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 806b6559178..3a5ff988f40 100644 --- a/tests/LexerTest.php +++ b/tests/LexerTest.php @@ -54,16 +54,6 @@ public function testBracketsNesting() $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())); From 151aa4ecda51b2512c77f2cdc31cc7d1be8a80e2 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Fri, 14 Feb 2025 12:56:13 +0100 Subject: [PATCH 734/812] Fix CS --- extra/html-extra/HtmlExtension.php | 2 +- .../DependencyInjection/Configuration.php | 4 ++-- .../TwigExtraExtensionTest.php | 6 +++--- src/Compiler.php | 4 ++-- src/ExpressionParser.php | 2 +- .../Prefix/LiteralExpressionParser.php | 1 - src/Extension/CoreExtension.php | 17 +++++------------ src/Extension/SandboxExtension.php | 4 ---- src/ExtensionSet.php | 6 +++--- src/Loader/ChainLoader.php | 4 ++-- src/Markup.php | 3 --- src/Node/Expression/Binary/AbstractBinary.php | 4 ++-- .../Expression/BlockReferenceExpression.php | 2 +- src/Node/Expression/Filter/DefaultFilter.php | 2 +- src/Node/Expression/Filter/RawFilter.php | 2 +- src/Node/Expression/FilterExpression.php | 2 +- src/Node/Expression/NullCoalesceExpression.php | 4 ++-- src/Node/Expression/Test/DefinedTest.php | 2 +- src/Node/Expression/TestExpression.php | 2 +- src/Node/Expression/Unary/AbstractUnary.php | 2 +- src/Node/Node.php | 3 --- src/Node/SetNode.php | 2 +- src/NodeVisitor/YieldNotReadyNodeVisitor.php | 2 +- src/Parser.php | 2 +- src/Runtime/EscaperRuntime.php | 2 +- src/Sandbox/SecurityPolicy.php | 4 ++-- src/Test/IntegrationTestCase.php | 10 +++++----- src/Token.php | 3 --- src/Util/TemplateDirIterator.php | 6 ------ tests/EnvironmentTest.php | 2 +- tests/IntegrationTest.php | 2 +- tests/NodeVisitor/OptimizerTest.php | 2 +- 32 files changed, 44 insertions(+), 71 deletions(-) diff --git a/extra/html-extra/HtmlExtension.php b/extra/html-extra/HtmlExtension.php index 8eeee5a58fd..fd67582f557 100644 --- a/extra/html-extra/HtmlExtension.php +++ b/extra/html-extra/HtmlExtension.php @@ -77,7 +77,7 @@ public function dataUri(string $data, ?string $mime = null, array $parameters = $repr .= ';'.$key.'='.rawurlencode($value); } - if (0 === strpos($mime, 'text/')) { + if (str_starts_with($mime, 'text/')) { $repr .= ','.rawurlencode($data); } else { $repr .= ';base64,'.base64_encode($data); diff --git a/extra/twig-extra-bundle/DependencyInjection/Configuration.php b/extra/twig-extra-bundle/DependencyInjection/Configuration.php index 1718593b96b..fc94eaca362 100644 --- a/extra/twig-extra-bundle/DependencyInjection/Configuration.php +++ b/extra/twig-extra-bundle/DependencyInjection/Configuration.php @@ -58,7 +58,7 @@ private function addCommonMarkConfiguration(ArrayNodeDefinition $rootNode): void ->end() ->enumNode('html_input') ->info('How to handle HTML input.') - ->values(['strip','allow','escape']) + ->values(['strip', 'allow', 'escape']) ->end() ->booleanNode('allow_unsafe_links') ->info('Remove risky link and image URLs by setting this to false.') @@ -66,7 +66,7 @@ private function addCommonMarkConfiguration(ArrayNodeDefinition $rootNode): void ->end() ->integerNode('max_nesting_level') ->info('The maximum nesting level for blocks.') - ->defaultValue(PHP_INT_MAX) + ->defaultValue(\PHP_INT_MAX) ->end() ->arrayNode('slug_normalizer') ->info('Array of options for configuring how URL-safe slugs are created.') diff --git a/extra/twig-extra-bundle/Tests/DependencyInjection/TwigExtraExtensionTest.php b/extra/twig-extra-bundle/Tests/DependencyInjection/TwigExtraExtensionTest.php index b17c040a7e6..ce263ac2c38 100644 --- a/extra/twig-extra-bundle/Tests/DependencyInjection/TwigExtraExtensionTest.php +++ b/extra/twig-extra-bundle/Tests/DependencyInjection/TwigExtraExtensionTest.php @@ -27,12 +27,12 @@ public function testDefaultConfiguration() ])); $container->registerExtension(new TwigExtraExtension()); $container->loadFromExtension('twig_extra', [ - 'commonmark' => [ + 'commonmark' => [ 'extra_key' => true, 'renderer' => [ 'block_separator' => "\n", 'inner_separator' => "\n", - 'soft_break' => "\n", + 'soft_break' => "\n", ], 'commonmark' => [ 'enable_em' => true, @@ -43,7 +43,7 @@ public function testDefaultConfiguration() ], 'html_input' => 'escape', 'allow_unsafe_links' => false, - 'max_nesting_level' => PHP_INT_MAX, + 'max_nesting_level' => \PHP_INT_MAX, 'slug_normalizer' => [ 'max_length' => 255, ], diff --git a/src/Compiler.php b/src/Compiler.php index ce8b17c9811..6f62c091978 100644 --- a/src/Compiler.php +++ b/src/Compiler.php @@ -74,7 +74,7 @@ public function compile(Node $node, int $indentation = 0) $node->compile($this); if ($this->didUseEcho) { - trigger_deprecation('twig/twig', '3.9', 'Using "%s" is deprecated, use "yield" instead in "%s", then flag the class with #[\Twig\Attribute\YieldReady].', $this->didUseEcho, \get_class($node)); + trigger_deprecation('twig/twig', '3.9', 'Using "%s" is deprecated, use "yield" instead in "%s", then flag the class with #[\Twig\Attribute\YieldReady].', $this->didUseEcho, $node::class); } return $this; @@ -99,7 +99,7 @@ public function subcompile(Node $node, bool $raw = true) $node->compile($this); if ($this->didUseEcho) { - trigger_deprecation('twig/twig', '3.9', 'Using "%s" is deprecated, use "yield" instead in "%s", then flag the class with #[\Twig\Attribute\YieldReady].', $this->didUseEcho, \get_class($node)); + trigger_deprecation('twig/twig', '3.9', 'Using "%s" is deprecated, use "yield" instead in "%s", then flag the class with #[\Twig\Attribute\YieldReady].', $this->didUseEcho, $node::class); } return $this; diff --git a/src/ExpressionParser.php b/src/ExpressionParser.php index 60ebcb66787..727cf7eba61 100644 --- a/src/ExpressionParser.php +++ b/src/ExpressionParser.php @@ -258,7 +258,7 @@ public function parseArguments() $name = null; if ($namedArguments && (($token = $stream->nextIf(Token::OPERATOR_TYPE, '=')) || (!$definition && $token = $stream->nextIf(Token::PUNCTUATION_TYPE, ':')))) { if (!$value instanceof ContextVariable) { - throw new SyntaxError(\sprintf('A parameter name must be a string, "%s" given.', \get_class($value)), $token->getLine(), $stream->getSourceContext()); + throw new SyntaxError(\sprintf('A parameter name must be a string, "%s" given.', $value::class), $token->getLine(), $stream->getSourceContext()); } $name = $value->getAttribute('name'); diff --git a/src/ExpressionParser/Prefix/LiteralExpressionParser.php b/src/ExpressionParser/Prefix/LiteralExpressionParser.php index 188b924453f..d98c9adf1f9 100644 --- a/src/ExpressionParser/Prefix/LiteralExpressionParser.php +++ b/src/ExpressionParser/Prefix/LiteralExpressionParser.php @@ -20,7 +20,6 @@ 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; diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index 039a36f439e..f44b6ad6d86 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -542,7 +542,6 @@ public function modifyDate($date, $modifier) * Returns a formatted string. * * @param string|null $format - * @param ...$values * * @internal */ @@ -1001,8 +1000,6 @@ public static function reverse(string $charset, $item, $preserveKeys = false) * * @param array|\Traversable|string|null $item * - * @return mixed - * * @internal */ public static function shuffle(string $charset, $item) @@ -1437,8 +1434,6 @@ public static function testEmpty($value): bool * {# ... #} * {% endif %} * - * @param mixed $value - * * @internal */ public static function testSequence($value): bool @@ -1462,8 +1457,6 @@ public static function testSequence($value): bool * {# ... #} * {% endif %} * - * @param mixed $value - * * @internal */ public static function testMapping($value): bool @@ -1613,10 +1606,10 @@ public static function constant($constant, $object = null, bool $checkDefined = { if (null !== $object) { if ('class' === $constant) { - return $checkDefined ? true : \get_class($object); + return $checkDefined ? true : $object::class; } - $constant = \get_class($object).'::'.$constant; + $constant = $object::class.'::'.$constant; } if (!\defined($constant)) { @@ -1720,9 +1713,9 @@ public static function getAttribute(Environment $env, Source $source, $object, $ } if ($object instanceof \ArrayAccess) { - $message = \sprintf('Key "%s" in object with ArrayAccess of class "%s" does not exist.', $arrayItem, \get_class($object)); + $message = \sprintf('Key "%s" in object with ArrayAccess of class "%s" does not exist.', $arrayItem, $object::class); } elseif (\is_object($object)) { - $message = \sprintf('Impossible to access a key "%s" on an object of class "%s" that does not implement ArrayAccess interface.', $item, \get_class($object)); + $message = \sprintf('Impossible to access a key "%s" on an object of class "%s" that does not implement ArrayAccess interface.', $item, $object::class); } elseif (\is_array($object)) { if (!$object) { $message = \sprintf('Key "%s" does not exist as the sequence/mapping is empty.', $arrayItem); @@ -1818,7 +1811,7 @@ public static function getAttribute(Environment $env, Source $source, $object, $ static $cache = []; - $class = \get_class($object); + $class = $object::class; // object method // precedence: getXxx() > isXxx() > hasXxx() diff --git a/src/Extension/SandboxExtension.php b/src/Extension/SandboxExtension.php index a9681c8d6c1..5d0f6444316 100644 --- a/src/Extension/SandboxExtension.php +++ b/src/Extension/SandboxExtension.php @@ -118,10 +118,6 @@ public function checkPropertyAllowed($obj, $property, int $lineno = -1, ?Source } /** - * @param mixed $obj - * - * @return mixed - * * @throws SecurityNotAllowedMethodError */ public function ensureToStringAllowed($obj, int $lineno = -1, ?Source $source = null) diff --git a/src/ExtensionSet.php b/src/ExtensionSet.php index 5afa729f9ed..14b1d3e2958 100644 --- a/src/ExtensionSet.php +++ b/src/ExtensionSet.php @@ -142,7 +142,7 @@ public function getLastModified(): int public function addExtension(ExtensionInterface $extension): void { - $class = \get_class($extension); + $class = $extension::class; if ($this->initialized) { throw new \LogicException(\sprintf('Unable to register extension "%s" as extensions have already been initialized.', $class)); @@ -484,11 +484,11 @@ private function initExtension(ExtensionInterface $extension): void $operators = $extension->getOperators(); if (!\is_array($operators)) { - throw new \InvalidArgumentException(\sprintf('"%s::getOperators()" must return an array with operators, got "%s".', \get_class($extension), get_debug_type($operators).(\is_resource($operators) ? '' : '#'.$operators))); + throw new \InvalidArgumentException(\sprintf('"%s::getOperators()" must return an array with operators, got "%s".', $extension::class, get_debug_type($operators).(\is_resource($operators) ? '' : '#'.$operators))); } if (2 !== \count($operators)) { - throw new \InvalidArgumentException(\sprintf('"%s::getOperators()" must return an array of 2 elements, got %d.', \get_class($extension), \count($operators))); + throw new \InvalidArgumentException(\sprintf('"%s::getOperators()" must return an array of 2 elements, got %d.', $extension::class, \count($operators))); } $expressionParsers = []; diff --git a/src/Loader/ChainLoader.php b/src/Loader/ChainLoader.php index 6e4f9511c5d..0859dcd2f76 100644 --- a/src/Loader/ChainLoader.php +++ b/src/Loader/ChainLoader.php @@ -104,7 +104,7 @@ public function getCacheKey(string $name): string try { return $loader->getCacheKey($name); } catch (LoaderError $e) { - $exceptions[] = \get_class($loader).': '.$e->getMessage(); + $exceptions[] = $loader::class.': '.$e->getMessage(); } } @@ -123,7 +123,7 @@ public function isFresh(string $name, int $time): bool try { return $loader->isFresh($name, $time); } catch (LoaderError $e) { - $exceptions[] = \get_class($loader).': '.$e->getMessage(); + $exceptions[] = $loader::class.': '.$e->getMessage(); } } diff --git a/src/Markup.php b/src/Markup.php index a933b69d327..c8efddb6fd2 100644 --- a/src/Markup.php +++ b/src/Markup.php @@ -46,9 +46,6 @@ public function count() return mb_strlen($this->content, $this->charset); } - /** - * @return mixed - */ #[\ReturnTypeWillChange] public function jsonSerialize() { diff --git a/src/Node/Expression/Binary/AbstractBinary.php b/src/Node/Expression/Binary/AbstractBinary.php index bd6cc6c0251..b4bf6662eda 100644 --- a/src/Node/Expression/Binary/AbstractBinary.php +++ b/src/Node/Expression/Binary/AbstractBinary.php @@ -25,10 +25,10 @@ abstract class AbstractBinary extends AbstractExpression implements BinaryInterf public function __construct(Node $left, Node $right, int $lineno) { if (!$left instanceof AbstractExpression) { - trigger_deprecation('twig/twig', '3.15', 'Not passing a "%s" instance to the "left" argument of "%s" is deprecated ("%s" given).', AbstractExpression::class, static::class, \get_class($left)); + trigger_deprecation('twig/twig', '3.15', 'Not passing a "%s" instance to the "left" argument of "%s" is deprecated ("%s" given).', AbstractExpression::class, static::class, $left::class); } if (!$right instanceof AbstractExpression) { - trigger_deprecation('twig/twig', '3.15', 'Not passing a "%s" instance to the "right" argument of "%s" is deprecated ("%s" given).', AbstractExpression::class, static::class, \get_class($right)); + trigger_deprecation('twig/twig', '3.15', 'Not passing a "%s" instance to the "right" argument of "%s" is deprecated ("%s" given).', AbstractExpression::class, static::class, $right::class); } parent::__construct(['left' => $left, 'right' => $right], [], $lineno); diff --git a/src/Node/Expression/BlockReferenceExpression.php b/src/Node/Expression/BlockReferenceExpression.php index a5a3cee3f77..508ca2d8382 100644 --- a/src/Node/Expression/BlockReferenceExpression.php +++ b/src/Node/Expression/BlockReferenceExpression.php @@ -28,7 +28,7 @@ class BlockReferenceExpression extends AbstractExpression public function __construct(Node $name, ?Node $template, int $lineno) { if (!$name instanceof AbstractExpression) { - trigger_deprecation('twig/twig', '3.15', 'Not passing a "%s" instance to the "node" argument of "%s" is deprecated ("%s" given).', AbstractExpression::class, static::class, \get_class($name)); + trigger_deprecation('twig/twig', '3.15', 'Not passing a "%s" instance to the "node" argument of "%s" is deprecated ("%s" given).', AbstractExpression::class, static::class, $name::class); } $nodes = ['name' => $name]; diff --git a/src/Node/Expression/Filter/DefaultFilter.php b/src/Node/Expression/Filter/DefaultFilter.php index bccd7f0a4a7..04ef06cc4ee 100644 --- a/src/Node/Expression/Filter/DefaultFilter.php +++ b/src/Node/Expression/Filter/DefaultFilter.php @@ -42,7 +42,7 @@ class DefaultFilter extends FilterExpression public function __construct(Node $node, TwigFilter|ConstantExpression $filter, Node $arguments, int $lineno) { if (!$node instanceof AbstractExpression) { - trigger_deprecation('twig/twig', '3.15', 'Not passing a "%s" instance to the "node" argument of "%s" is deprecated ("%s" given).', AbstractExpression::class, static::class, \get_class($node)); + trigger_deprecation('twig/twig', '3.15', 'Not passing a "%s" instance to the "node" argument of "%s" is deprecated ("%s" given).', AbstractExpression::class, static::class, $node::class); } if ($filter instanceof TwigFilter) { diff --git a/src/Node/Expression/Filter/RawFilter.php b/src/Node/Expression/Filter/RawFilter.php index 0a49e7c4fe4..707e8ec245c 100644 --- a/src/Node/Expression/Filter/RawFilter.php +++ b/src/Node/Expression/Filter/RawFilter.php @@ -32,7 +32,7 @@ class RawFilter extends FilterExpression public function __construct(Node $node, TwigFilter|ConstantExpression|null $filter = null, ?Node $arguments = null, int $lineno = 0) { if (!$node instanceof AbstractExpression) { - trigger_deprecation('twig/twig', '3.15', 'Not passing a "%s" instance to the "node" argument of "%s" is deprecated ("%s" given).', AbstractExpression::class, static::class, \get_class($node)); + trigger_deprecation('twig/twig', '3.15', 'Not passing a "%s" instance to the "node" argument of "%s" is deprecated ("%s" given).', AbstractExpression::class, static::class, $node::class); } parent::__construct($node, $filter ?: new TwigFilter('raw', null, ['is_safe' => ['all']]), $arguments ?: new EmptyNode(), $lineno ?: $node->getTemplateLine()); diff --git a/src/Node/Expression/FilterExpression.php b/src/Node/Expression/FilterExpression.php index 6e0c486ab07..a66b0266d73 100644 --- a/src/Node/Expression/FilterExpression.php +++ b/src/Node/Expression/FilterExpression.php @@ -27,7 +27,7 @@ class FilterExpression extends CallExpression public function __construct(Node $node, TwigFilter|ConstantExpression $filter, Node $arguments, int $lineno) { if (!$node instanceof AbstractExpression) { - trigger_deprecation('twig/twig', '3.15', 'Not passing a "%s" instance to the "node" argument of "%s" is deprecated ("%s" given).', AbstractExpression::class, static::class, \get_class($node)); + trigger_deprecation('twig/twig', '3.15', 'Not passing a "%s" instance to the "node" argument of "%s" is deprecated ("%s" given).', AbstractExpression::class, static::class, $node::class); } if ($filter instanceof TwigFilter) { diff --git a/src/Node/Expression/NullCoalesceExpression.php b/src/Node/Expression/NullCoalesceExpression.php index 74ddaf79194..f397f71f039 100644 --- a/src/Node/Expression/NullCoalesceExpression.php +++ b/src/Node/Expression/NullCoalesceExpression.php @@ -33,10 +33,10 @@ public function __construct(Node $left, Node $right, int $lineno) trigger_deprecation('twig/twig', '3.17', \sprintf('"%s" is deprecated; use "%s" instead.', __CLASS__, NullCoalesceBinary::class)); if (!$left instanceof AbstractExpression) { - trigger_deprecation('twig/twig', '3.15', 'Not passing a "%s" instance to the "left" argument of "%s" is deprecated ("%s" given).', AbstractExpression::class, static::class, \get_class($left)); + trigger_deprecation('twig/twig', '3.15', 'Not passing a "%s" instance to the "left" argument of "%s" is deprecated ("%s" given).', AbstractExpression::class, static::class, $left::class); } if (!$right instanceof AbstractExpression) { - trigger_deprecation('twig/twig', '3.15', 'Not passing a "%s" instance to the "right" argument of "%s" is deprecated ("%s" given).', AbstractExpression::class, static::class, \get_class($right)); + trigger_deprecation('twig/twig', '3.15', 'Not passing a "%s" instance to the "right" argument of "%s" is deprecated ("%s" given).', AbstractExpression::class, static::class, $right::class); } $test = new DefinedTest(clone $left, new TwigTest('defined'), new EmptyNode(), $left->getTemplateLine()); diff --git a/src/Node/Expression/Test/DefinedTest.php b/src/Node/Expression/Test/DefinedTest.php index 5e32c38bb85..9612892be0c 100644 --- a/src/Node/Expression/Test/DefinedTest.php +++ b/src/Node/Expression/Test/DefinedTest.php @@ -46,7 +46,7 @@ class DefinedTest extends TestExpression public function __construct(Node $node, TwigTest|string $name, ?Node $arguments, int $lineno) { if (!$node instanceof AbstractExpression) { - trigger_deprecation('twig/twig', '3.15', 'Not passing a "%s" instance to the "node" argument of "%s" is deprecated ("%s" given).', AbstractExpression::class, static::class, \get_class($node)); + trigger_deprecation('twig/twig', '3.15', 'Not passing a "%s" instance to the "node" argument of "%s" is deprecated ("%s" given).', AbstractExpression::class, static::class, $node::class); } if ($node instanceof ContextVariable) { diff --git a/src/Node/Expression/TestExpression.php b/src/Node/Expression/TestExpression.php index 7b9a5413888..27e1526a187 100644 --- a/src/Node/Expression/TestExpression.php +++ b/src/Node/Expression/TestExpression.php @@ -26,7 +26,7 @@ class TestExpression extends CallExpression public function __construct(Node $node, string|TwigTest $test, ?Node $arguments, int $lineno) { if (!$node instanceof AbstractExpression) { - trigger_deprecation('twig/twig', '3.15', 'Not passing a "%s" instance to the "node" argument of "%s" is deprecated ("%s" given).', AbstractExpression::class, static::class, \get_class($node)); + trigger_deprecation('twig/twig', '3.15', 'Not passing a "%s" instance to the "node" argument of "%s" is deprecated ("%s" given).', AbstractExpression::class, static::class, $node::class); } $nodes = ['node' => $node]; diff --git a/src/Node/Expression/Unary/AbstractUnary.php b/src/Node/Expression/Unary/AbstractUnary.php index b00027d1ab8..09f3d0984a5 100644 --- a/src/Node/Expression/Unary/AbstractUnary.php +++ b/src/Node/Expression/Unary/AbstractUnary.php @@ -24,7 +24,7 @@ abstract class AbstractUnary extends AbstractExpression implements UnaryInterfac public function __construct(Node $node, int $lineno) { if (!$node instanceof AbstractExpression) { - trigger_deprecation('twig/twig', '3.15', 'Not passing a "%s" instance argument to "%s" is deprecated ("%s" given).', AbstractExpression::class, static::class, \get_class($node)); + trigger_deprecation('twig/twig', '3.15', 'Not passing a "%s" instance argument to "%s" is deprecated ("%s" given).', AbstractExpression::class, static::class, $node::class); } parent::__construct(['node' => $node], ['with_parentheses' => false], $lineno); diff --git a/src/Node/Node.php b/src/Node/Node.php index 389119b55fb..dcf912c2198 100644 --- a/src/Node/Node.php +++ b/src/Node/Node.php @@ -149,9 +149,6 @@ public function hasAttribute(string $name): bool return \array_key_exists($name, $this->attributes); } - /** - * @return mixed - */ public function getAttribute(string $name) { if (!\array_key_exists($name, $this->attributes)) { diff --git a/src/Node/SetNode.php b/src/Node/SetNode.php index 4d97adb223d..7b063b00b2a 100644 --- a/src/Node/SetNode.php +++ b/src/Node/SetNode.php @@ -34,7 +34,7 @@ public function __construct(bool $capture, Node $names, Node $values, int $linen if ($capture) { $safe = true; // Node::class === get_class($values) should be removed in Twig 4.0 - if (($values instanceof Nodes || Node::class === \get_class($values)) && !\count($values)) { + if (($values instanceof Nodes || Node::class === $values::class) && !\count($values)) { $values = new ConstantExpression('', $values->getTemplateLine()); $capture = false; } elseif ($values instanceof TextNode) { diff --git a/src/NodeVisitor/YieldNotReadyNodeVisitor.php b/src/NodeVisitor/YieldNotReadyNodeVisitor.php index 3c978627556..4d6cf60a0ae 100644 --- a/src/NodeVisitor/YieldNotReadyNodeVisitor.php +++ b/src/NodeVisitor/YieldNotReadyNodeVisitor.php @@ -30,7 +30,7 @@ public function __construct( public function enterNode(Node $node, Environment $env): Node { - $class = \get_class($node); + $class = $node::class; if ($node instanceof AbstractExpression || isset($this->yieldReadyNodes[$class])) { return $node; diff --git a/src/Parser.php b/src/Parser.php index 63ecccbe405..b40a9231748 100644 --- a/src/Parser.php +++ b/src/Parser.php @@ -549,7 +549,7 @@ private function filterBodyNodes(Node $node, bool $nested = false): ?Node // here, $nested means "being at the root level of a child template" // we need to discard the wrapping "Node" for the "body" node // Node::class !== \get_class($node) should be removed in Twig 4.0 - $nested = $nested || (Node::class !== \get_class($node) && !$node instanceof Nodes); + $nested = $nested || (Node::class !== $node::class && !$node instanceof Nodes); foreach ($node as $k => $n) { if (null !== $n && null === $this->filterBodyNodes($n, $nested)) { $node->removeNode($k); diff --git a/src/Runtime/EscaperRuntime.php b/src/Runtime/EscaperRuntime.php index 719a5696a5e..ff7913f38f2 100644 --- a/src/Runtime/EscaperRuntime.php +++ b/src/Runtime/EscaperRuntime.php @@ -106,7 +106,7 @@ public function escape($string, string $strategy = 'html', ?string $charset = nu if (!\is_string($string)) { if ($string instanceof \Stringable) { if ($autoescape) { - $c = \get_class($string); + $c = $string::class; if (!isset($this->safeClasses[$c])) { $this->safeClasses[$c] = []; foreach (class_parents($string) + class_implements($string) as $class) { diff --git a/src/Sandbox/SecurityPolicy.php b/src/Sandbox/SecurityPolicy.php index b0d054260f1..8dd68ae99b2 100644 --- a/src/Sandbox/SecurityPolicy.php +++ b/src/Sandbox/SecurityPolicy.php @@ -107,7 +107,7 @@ public function checkMethodAllowed($obj, $method): void } if (!$allowed) { - $class = \get_class($obj); + $class = $obj::class; throw new SecurityNotAllowedMethodError(\sprintf('Calling "%s" method on a "%s" object is not allowed.', $method, $class), $class, $method); } } @@ -123,7 +123,7 @@ public function checkPropertyAllowed($obj, $property): void } if (!$allowed) { - $class = \get_class($obj); + $class = $obj::class; throw new SecurityNotAllowedPropertyError(\sprintf('Calling "%s" property on a "%s" object is not allowed.', $property, $class), $class, $property); } } diff --git a/src/Test/IntegrationTestCase.php b/src/Test/IntegrationTestCase.php index f4a5dc7e534..f3f7adcee64 100644 --- a/src/Test/IntegrationTestCase.php +++ b/src/Test/IntegrationTestCase.php @@ -275,14 +275,14 @@ protected function doIntegrationTest($file, $message, $condition, $templates, $e } catch (\Exception $e) { if (false !== $exception) { $message = $e->getMessage(); - $this->assertSame(trim($exception), trim(\sprintf('%s: %s', \get_class($e), $message))); + $this->assertSame(trim($exception), trim(\sprintf('%s: %s', $e::class, $message))); $last = substr($message, \strlen($message) - 1); $this->assertTrue('.' === $last || '?' === $last, 'Exception message must end with a dot or a question mark.'); return; } - throw new Error(\sprintf('%s: %s', \get_class($e), $e->getMessage()), -1, null, $e); + throw new Error(\sprintf('%s: %s', $e::class, $e->getMessage()), -1, null, $e); } finally { restore_error_handler(); } @@ -293,14 +293,14 @@ protected function doIntegrationTest($file, $message, $condition, $templates, $e $output = trim($template->render(eval($match[1].';')), "\n "); } catch (\Exception $e) { if (false !== $exception) { - $this->assertStringMatchesFormat(trim($exception), trim(\sprintf('%s: %s', \get_class($e), $e->getMessage()))); + $this->assertStringMatchesFormat(trim($exception), trim(\sprintf('%s: %s', $e::class, $e->getMessage()))); return; } - $e = new Error(\sprintf('%s: %s', \get_class($e), $e->getMessage()), -1, null, $e); + $e = new Error(\sprintf('%s: %s', $e::class, $e->getMessage()), -1, null, $e); - $output = trim(\sprintf('%s: %s', \get_class($e), $e->getMessage())); + $output = trim(\sprintf('%s: %s', $e::class, $e->getMessage())); } if (false !== $exception) { diff --git a/src/Token.php b/src/Token.php index 73fd02e1d13..7059619f82d 100644 --- a/src/Token.php +++ b/src/Token.php @@ -134,9 +134,6 @@ public function getType(): int return $this->type; } - /** - * @return mixed - */ public function getValue() { return $this->value; diff --git a/src/Util/TemplateDirIterator.php b/src/Util/TemplateDirIterator.php index 3bef14beec3..8125341bd81 100644 --- a/src/Util/TemplateDirIterator.php +++ b/src/Util/TemplateDirIterator.php @@ -16,18 +16,12 @@ */ class TemplateDirIterator extends \IteratorIterator { - /** - * @return mixed - */ #[\ReturnTypeWillChange] public function current() { return file_get_contents(parent::current()); } - /** - * @return mixed - */ #[\ReturnTypeWillChange] public function key() { diff --git a/tests/EnvironmentTest.php b/tests/EnvironmentTest.php index 19f2fce4677..fb9090c73af 100644 --- a/tests/EnvironmentTest.php +++ b/tests/EnvironmentTest.php @@ -332,7 +332,7 @@ public function testAddMockExtension() $twig = new Environment($loader); $twig->addExtension($extension); - $this->assertInstanceOf(ExtensionInterface::class, $twig->getExtension(\get_class($extension))); + $this->assertInstanceOf(ExtensionInterface::class, $twig->getExtension($extension::class)); $this->assertTrue($twig->isTemplateFresh('page', time())); } diff --git a/tests/IntegrationTest.php b/tests/IntegrationTest.php index 273324d1061..91671880ea2 100644 --- a/tests/IntegrationTest.php +++ b/tests/IntegrationTest.php @@ -307,7 +307,7 @@ public function br() public function is_multi_word($value) { - return false !== strpos($value, ' '); + return str_contains($value, ' '); } public function __call($method, $arguments) diff --git a/tests/NodeVisitor/OptimizerTest.php b/tests/NodeVisitor/OptimizerTest.php index 5964b7b484a..859f4175c75 100644 --- a/tests/NodeVisitor/OptimizerTest.php +++ b/tests/NodeVisitor/OptimizerTest.php @@ -70,7 +70,7 @@ public function testForVarOptimizer() public function checkForVarConfiguration(Node $node, $target) { foreach ($node as $n) { - if (NameExpression::class === \get_class($n) && $target === $n->getAttribute('name')) { + if (NameExpression::class === $n::class && $target === $n->getAttribute('name')) { $this->assertTrue($n->getAttribute('always_defined')); } else { $this->checkForVarConfiguration($n, $target); From 5f6c67f838b3d86d25bed03474c10408a55f3f4b Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Fri, 14 Feb 2025 13:36:34 +0100 Subject: [PATCH 735/812] re-add mixed return type --- src/Markup.php | 3 +++ src/Util/TemplateDirIterator.php | 6 ++++++ 2 files changed, 9 insertions(+) diff --git a/src/Markup.php b/src/Markup.php index c8efddb6fd2..a933b69d327 100644 --- a/src/Markup.php +++ b/src/Markup.php @@ -46,6 +46,9 @@ public function count() return mb_strlen($this->content, $this->charset); } + /** + * @return mixed + */ #[\ReturnTypeWillChange] public function jsonSerialize() { diff --git a/src/Util/TemplateDirIterator.php b/src/Util/TemplateDirIterator.php index 8125341bd81..d739b285f2c 100644 --- a/src/Util/TemplateDirIterator.php +++ b/src/Util/TemplateDirIterator.php @@ -16,12 +16,18 @@ */ class TemplateDirIterator extends \IteratorIterator { + /** + * @return string + */ #[\ReturnTypeWillChange] public function current() { return file_get_contents(parent::current()); } + /** + * @return string + */ #[\ReturnTypeWillChange] public function key() { From a544dc8b33d2793859ca7c061956872b64722d6e Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Sun, 16 Feb 2025 22:31:07 +0100 Subject: [PATCH 736/812] fix the rst syntax of the operator precedence table --- bin/generate_operators_precedence.php | 28 ++- doc/operators_precedence.rst | 290 +++++++++++++++++--------- 2 files changed, 208 insertions(+), 110 deletions(-) diff --git a/bin/generate_operators_precedence.php b/bin/generate_operators_precedence.php index a95f18f57fb..31477be6236 100644 --- a/bin/generate_operators_precedence.php +++ b/bin/generate_operators_precedence.php @@ -21,19 +21,24 @@ $output = fopen(\dirname(__DIR__).'/doc/operators_precedence.rst', 'w'); $twig = new Environment(new ArrayLoader([])); +$descriptionLength = 11; $expressionParsers = []; foreach ($twig->getExpressionParsers() as $expressionParser) { $expressionParsers[] = $expressionParser; + $descriptionLength = max($descriptionLength, $expressionParser instanceof ExpressionParserDescriptionInterface ? strlen($expressionParser->getDescription()) : ''); } -fwrite($output, "\n=========== ================ ======= ============= ===========\n"); -fwrite($output, "Precedence Operator Type Associativity Description\n"); -fwrite($output, '=========== ================ ======= ============= ==========='); +fwrite($output, "\n+------------+------------------+---------+---------------+".str_repeat('-', $descriptionLength + 2)."+\n"); +fwrite($output, "| Precedence | Operator | Type | Associativity | Description".str_repeat(' ', $descriptionLength - 11)." |\n"); +fwrite($output, '+============+==================+=========+===============+'.str_repeat('=', $descriptionLength + 2).'+'); usort($expressionParsers, fn ($a, $b) => $b->getPrecedence() <=> $a->getPrecedence()); $previous = null; foreach ($expressionParsers as $expressionParser) { + if (null !== $previous) { + fwrite($output, "\n+------------+------------------+---------+---------------+".str_repeat('-', $descriptionLength + 2).'+'); + } $precedence = $expressionParser->getPrecedence(); $previousPrecedence = $previous ? $previous->getPrecedence() : \PHP_INT_MAX; $associativity = $expressionParser instanceof InfixExpressionParserInterface ? (InfixAssociativity::Left === $expressionParser->getAssociativity() ? 'Left' : 'Right') : 'n/a'; @@ -41,7 +46,7 @@ if ($previousPrecedence !== $precedence) { $previous = null; } - fwrite($output, rtrim(\sprintf("\n%-11s %-16s %-7s %-13s %s\n", + fwrite($output, rtrim(\sprintf("\n| %-10s | %-16s | %-7s | %-13s | %-{$descriptionLength}s |\n", (!$previous || $previousPrecedence !== $precedence ? $precedence : '').($expressionParser->getPrecedenceChange() ? ' => '.$expressionParser->getPrecedenceChange()->getNewPrecedence() : ''), '``'.$expressionParser->getName().'``', !$previous || ExpressionParserType::getType($previous) !== ExpressionParserType::getType($expressionParser) ? ExpressionParserType::getType($expressionParser)->value : '', @@ -50,14 +55,14 @@ ))); $previous = $expressionParser; } -fwrite($output, "\n=========== ================ ======= ============= ===========\n"); +fwrite($output, "\n+------------+------------------+---------+---------------+".str_repeat('-', $descriptionLength + 2)."+\n"); fwrite($output, "\nWhen a precedence will change in 4.0, the new precedence is indicated by the arrow ``=>``.\n"); fwrite($output, "\nHere is the same table for Twig 4.0 with adjusted precedences:\n"); -fwrite($output, "\n=========== ================ ======= ============= ===========\n"); -fwrite($output, "Precedence Operator Type Associativity Description\n"); -fwrite($output, '=========== ================ ======= ============= ==========='); +fwrite($output, "\n+------------+------------------+---------+---------------+".str_repeat('-', $descriptionLength + 2)."+\n"); +fwrite($output, "| Precedence | Operator | Type | Associativity | Description".str_repeat(' ', $descriptionLength - 11)." |\n"); +fwrite($output, '+============+==================+=========+===============+'.str_repeat('=', $descriptionLength + 2).'+'); usort($expressionParsers, function ($a, $b) { $aPrecedence = $a->getPrecedenceChange() ? $a->getPrecedenceChange()->getNewPrecedence() : $a->getPrecedence(); @@ -68,6 +73,9 @@ $previous = null; foreach ($expressionParsers as $expressionParser) { + if (null !== $previous) { + fwrite($output, "\n+------------+------------------+---------+---------------+".str_repeat('-', $descriptionLength + 2).'+'); + } $precedence = $expressionParser->getPrecedenceChange() ? $expressionParser->getPrecedenceChange()->getNewPrecedence() : $expressionParser->getPrecedence(); $previousPrecedence = $previous ? ($previous->getPrecedenceChange() ? $previous->getPrecedenceChange()->getNewPrecedence() : $previous->getPrecedence()) : \PHP_INT_MAX; $associativity = $expressionParser instanceof InfixExpressionParserInterface ? (InfixAssociativity::Left === $expressionParser->getAssociativity() ? 'Left' : 'Right') : 'n/a'; @@ -75,7 +83,7 @@ if ($previousPrecedence !== $precedence) { $previous = null; } - fwrite($output, rtrim(\sprintf("\n%-11s %-16s %-7s %-13s %s\n", + fwrite($output, rtrim(\sprintf("\n| %-10s | %-16s | %-7s | %-13s | %-{$descriptionLength}s |\n", !$previous || $previousPrecedence !== $precedence ? $precedence : '', '``'.$expressionParser->getName().'``', !$previous || ExpressionParserType::getType($previous) !== ExpressionParserType::getType($expressionParser) ? ExpressionParserType::getType($expressionParser)->value : '', @@ -84,6 +92,6 @@ ))); $previous = $expressionParser; } -fwrite($output, "\n=========== ================ ======= ============= ===========\n"); +fwrite($output, "\n+------------+------------------+---------+---------------+".str_repeat('-', $descriptionLength + 2)."+\n"); fclose($output); diff --git a/doc/operators_precedence.rst b/doc/operators_precedence.rst index 6791e8b255a..472e39529f6 100644 --- a/doc/operators_precedence.rst +++ b/doc/operators_precedence.rst @@ -1,106 +1,196 @@ -=========== ================ ======= ============= =========== -Precedence Operator Type Associativity Description -=========== ================ ======= ============= =========== -512 ``...`` prefix n/a Spread operator - => 300 ``|`` infix Left Twig filter call - ``(`` Twig function call - ``.`` Get an attribute on a variable - ``[`` Array access -500 ``-`` prefix n/a - ``+`` -300 => 5 ``??`` infix Right Null coalescing operator (a ?? b) -250 ``=>`` infix Left Arrow function (x => expr) -200 ``**`` infix Right Exponentiation operator -100 ``is`` infix Left Twig tests - ``is not`` Twig tests -60 ``*`` infix Left - ``/`` - ``//`` Floor division - ``%`` -50 => 70 ``not`` prefix n/a -40 => 27 ``~`` infix Left -30 ``+`` infix Left - ``-`` -25 ``..`` infix Left -20 ``==`` infix Left - ``!=`` - ``<=>`` - ``<`` - ``>`` - ``>=`` - ``<=`` - ``not in`` - ``in`` - ``matches`` - ``starts with`` - ``ends with`` - ``has some`` - ``has every`` -18 ``b-and`` infix Left -17 ``b-xor`` infix Left -16 ``b-or`` infix Left -15 ``and`` infix Left -12 ``xor`` infix Left -10 ``or`` infix Left -5 ``?:`` infix Right Elvis operator (a ?: b) - ``?:`` Elvis operator (a ?: b) -0 ``(`` prefix n/a Explicit group expression (a) - ``literal`` A literal value (boolean, string, number, sequence, mapping, ...) - ``?`` infix Left Conditional operator (a ? b : c) -=========== ================ ======= ============= =========== ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| Precedence | Operator | Type | Associativity | Description | ++============+==================+=========+===============+===================================================================+ +| 512 | ``...`` | prefix | n/a | Spread operator | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| => 300 | ``|`` | infix | Left | Twig filter call | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| | ``(`` | | | Twig function call | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| | ``.`` | | | Get an attribute on a variable | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| | ``[`` | | | Array access | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| 500 | ``-`` | prefix | n/a | | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| | ``+`` | | | | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| 300 => 5 | ``??`` | infix | Right | Null coalescing operator (a ?? b) | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| 250 | ``=>`` | infix | Left | Arrow function (x => expr) | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| 200 | ``**`` | infix | Right | Exponentiation operator | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| 100 | ``is`` | infix | Left | Twig tests | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| | ``is not`` | | | Twig tests | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| 60 | ``*`` | infix | Left | | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| | ``/`` | | | | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| | ``//`` | | | Floor division | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| | ``%`` | | | | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| 50 => 70 | ``not`` | prefix | n/a | | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| 40 => 27 | ``~`` | infix | Left | | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| 30 | ``+`` | infix | Left | | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| | ``-`` | | | | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| 25 | ``..`` | infix | Left | | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| 20 | ``==`` | infix | Left | | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| | ``!=`` | | | | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| | ``<=>`` | | | | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| | ``<`` | | | | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| | ``>`` | | | | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| | ``>=`` | | | | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| | ``<=`` | | | | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| | ``not in`` | | | | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| | ``in`` | | | | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| | ``matches`` | | | | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| | ``starts with`` | | | | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| | ``ends with`` | | | | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| | ``has some`` | | | | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| | ``has every`` | | | | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| 18 | ``b-and`` | infix | Left | | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| 17 | ``b-xor`` | infix | Left | | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| 16 | ``b-or`` | infix | Left | | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| 15 | ``and`` | infix | Left | | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| 12 | ``xor`` | infix | Left | | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| 10 | ``or`` | infix | Left | | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| 5 | ``?:`` | infix | Right | Elvis operator (a ?: b) | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| | ``?:`` | | | Elvis operator (a ?: b) | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| 0 | ``(`` | prefix | n/a | Explicit group expression (a) | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| | ``literal`` | | | A literal value (boolean, string, number, sequence, mapping, ...) | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| | ``?`` | infix | Left | Conditional operator (a ? b : c) | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ When a precedence will change in 4.0, the new precedence is indicated by the arrow ``=>``. Here is the same table for Twig 4.0 with adjusted precedences: -=========== ================ ======= ============= =========== -Precedence Operator Type Associativity Description -=========== ================ ======= ============= =========== -512 ``...`` prefix n/a Spread operator - ``(`` infix Left Twig function call - ``.`` Get an attribute on a variable - ``[`` Array access -500 ``-`` prefix n/a - ``+`` -300 ``|`` infix Left Twig filter call -250 ``=>`` infix Left Arrow function (x => expr) -200 ``**`` infix Right Exponentiation operator -100 ``is`` infix Left Twig tests - ``is not`` Twig tests -70 ``not`` prefix n/a -60 ``*`` infix Left - ``/`` - ``//`` Floor division - ``%`` -30 ``+`` infix Left - ``-`` -27 ``~`` infix Left -25 ``..`` infix Left -20 ``==`` infix Left - ``!=`` - ``<=>`` - ``<`` - ``>`` - ``>=`` - ``<=`` - ``not in`` - ``in`` - ``matches`` - ``starts with`` - ``ends with`` - ``has some`` - ``has every`` -18 ``b-and`` infix Left -17 ``b-xor`` infix Left -16 ``b-or`` infix Left -15 ``and`` infix Left -12 ``xor`` infix Left -10 ``or`` infix Left -5 ``??`` infix Right Null coalescing operator (a ?? b) - ``?:`` Elvis operator (a ?: b) - ``?:`` Elvis operator (a ?: b) -0 ``(`` prefix n/a Explicit group expression (a) - ``literal`` A literal value (boolean, string, number, sequence, mapping, ...) - ``?`` infix Left Conditional operator (a ? b : c) -=========== ================ ======= ============= =========== ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| Precedence | Operator | Type | Associativity | Description | ++============+==================+=========+===============+===================================================================+ +| 512 | ``...`` | prefix | n/a | Spread operator | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| | ``(`` | infix | Left | Twig function call | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| | ``.`` | | | Get an attribute on a variable | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| | ``[`` | | | Array access | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| 500 | ``-`` | prefix | n/a | | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| | ``+`` | | | | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| 300 | ``|`` | infix | Left | Twig filter call | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| 250 | ``=>`` | infix | Left | Arrow function (x => expr) | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| 200 | ``**`` | infix | Right | Exponentiation operator | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| 100 | ``is`` | infix | Left | Twig tests | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| | ``is not`` | | | Twig tests | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| 70 | ``not`` | prefix | n/a | | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| 60 | ``*`` | infix | Left | | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| | ``/`` | | | | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| | ``//`` | | | Floor division | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| | ``%`` | | | | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| 30 | ``+`` | infix | Left | | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| | ``-`` | | | | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| 27 | ``~`` | infix | Left | | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| 25 | ``..`` | infix | Left | | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| 20 | ``==`` | infix | Left | | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| | ``!=`` | | | | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| | ``<=>`` | | | | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| | ``<`` | | | | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| | ``>`` | | | | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| | ``>=`` | | | | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| | ``<=`` | | | | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| | ``not in`` | | | | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| | ``in`` | | | | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| | ``matches`` | | | | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| | ``starts with`` | | | | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| | ``ends with`` | | | | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| | ``has some`` | | | | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| | ``has every`` | | | | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| 18 | ``b-and`` | infix | Left | | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| 17 | ``b-xor`` | infix | Left | | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| 16 | ``b-or`` | infix | Left | | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| 15 | ``and`` | infix | Left | | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| 12 | ``xor`` | infix | Left | | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| 10 | ``or`` | infix | Left | | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| 5 | ``??`` | infix | Right | Null coalescing operator (a ?? b) | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| | ``?:`` | | | Elvis operator (a ?: b) | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| | ``?:`` | | | Elvis operator (a ?: b) | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| 0 | ``(`` | prefix | n/a | Explicit group expression (a) | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| | ``literal`` | | | A literal value (boolean, string, number, sequence, mapping, ...) | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ +| | ``?`` | infix | Left | Conditional operator (a ? b : c) | ++------------+------------------+---------+---------------+-------------------------------------------------------------------+ From 4c818122f3ff9129ebffb1e52573c4e736d64668 Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Tue, 18 Feb 2025 15:14:50 +0100 Subject: [PATCH 737/812] fix typo --- doc/advanced.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/advanced.rst b/doc/advanced.rst index 963aecf86d0..43cdc69a8ae 100644 --- a/doc/advanced.rst +++ b/doc/advanced.rst @@ -775,7 +775,7 @@ responsible for parsing the tag and compiling it to PHP. Operators ~~~~~~~~~ -The ``getOperators()`` methods lets you add new operators. To implement a new +The ``getOperators()`` method lets you add new operators. To implement a new one, have a look at the default operators provided by ``Twig\Extension\CoreExtension``. From 9c6b95f2a6ca3025c442d2c36ba313be7f562159 Mon Sep 17 00:00:00 2001 From: Alexandre Daubois Date: Wed, 19 Feb 2025 15:29:33 +0100 Subject: [PATCH 738/812] Make `in_array()` calls strict --- extra/cache-extra/TokenParser/CacheTokenParser.php | 2 +- extra/html-extra/Cva.php | 2 +- extra/twig-extra-bundle/Extensions.php | 6 +++--- src/Extension/CoreExtension.php | 2 +- src/FileExtensionEscapingStrategy.php | 2 +- src/Lexer.php | 6 +++--- src/Node/Expression/AssignNameExpression.php | 2 +- src/Node/Expression/TempNameExpression.php | 4 ++-- src/NodeVisitor/EscaperNodeVisitor.php | 2 +- src/NodeVisitor/OptimizerNodeVisitor.php | 2 +- src/NodeVisitor/SafeAnalysisNodeVisitor.php | 8 ++++---- src/Runtime/EscaperRuntime.php | 2 +- src/Sandbox/SecurityPolicy.php | 10 +++++----- src/Token.php | 8 ++++---- src/TokenParser/GuardTokenParser.php | 2 +- 15 files changed, 30 insertions(+), 30 deletions(-) diff --git a/extra/cache-extra/TokenParser/CacheTokenParser.php b/extra/cache-extra/TokenParser/CacheTokenParser.php index 2dee747e1de..e57a8b3fd62 100644 --- a/extra/cache-extra/TokenParser/CacheTokenParser.php +++ b/extra/cache-extra/TokenParser/CacheTokenParser.php @@ -30,7 +30,7 @@ public function parse(Token $token): Node $tags = null; while ($stream->test(Token::NAME_TYPE)) { $k = $stream->getCurrent()->getValue(); - if (!\in_array($k, ['ttl', 'tags'])) { + if (!\in_array($k, ['ttl', 'tags'], true)) { throw new SyntaxError(\sprintf('Unknown "%s" configuration.', $k), $stream->getCurrent()->getLine(), $stream->getSourceContext()); } diff --git a/extra/html-extra/Cva.php b/extra/html-extra/Cva.php index a77d7dfdf20..9355565ce75 100644 --- a/extra/html-extra/Cva.php +++ b/extra/html-extra/Cva.php @@ -119,7 +119,7 @@ private function resolveCompoundVariant(array $compound, array $recipes): array if ('class' === $compoundName) { continue; } - if (!isset($recipes[$compoundName]) || !\in_array($recipes[$compoundName], (array) $compoundValues)) { + if (!isset($recipes[$compoundName]) || !\in_array($recipes[$compoundName], (array) $compoundValues, true)) { return []; } } diff --git a/extra/twig-extra-bundle/Extensions.php b/extra/twig-extra-bundle/Extensions.php index e542604e1fa..306de75a358 100644 --- a/extra/twig-extra-bundle/Extensions.php +++ b/extra/twig-extra-bundle/Extensions.php @@ -99,7 +99,7 @@ public static function getClasses(): array public static function getFilter(string $name): array { foreach (self::EXTENSIONS as $extension) { - if (\in_array($name, $extension['filters'])) { + if (\in_array($name, $extension['filters'], true)) { return [$extension['class_name'], $extension['package']]; } } @@ -110,7 +110,7 @@ public static function getFilter(string $name): array public static function getFunction(string $name): array { foreach (self::EXTENSIONS as $extension) { - if (\in_array($name, $extension['functions'])) { + if (\in_array($name, $extension['functions'], true)) { return [$extension['class_name'], $extension['package']]; } } @@ -121,7 +121,7 @@ public static function getFunction(string $name): array public static function getTag(string $name): array { foreach (self::EXTENSIONS as $extension) { - if (\in_array($name, $extension['tags'])) { + if (\in_array($name, $extension['tags'], true)) { return [$extension['class_name'], $extension['package']]; } } diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index f44b6ad6d86..57d77e7d70b 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -1836,7 +1836,7 @@ public static function getAttribute(Environment $env, Source $source, $object, $ } elseif ('h' === $lcName[0] && str_starts_with($lcName, 'has')) { $name = substr($method, 3); $lcName = substr($lcName, 3); - if (\in_array('is'.$lcName, $lcMethods)) { + if (\in_array('is'.$lcName, $lcMethods, true)) { continue; } } else { diff --git a/src/FileExtensionEscapingStrategy.php b/src/FileExtensionEscapingStrategy.php index 5308158d36a..2785ab7f497 100644 --- a/src/FileExtensionEscapingStrategy.php +++ b/src/FileExtensionEscapingStrategy.php @@ -33,7 +33,7 @@ class FileExtensionEscapingStrategy */ public static function guess(string $name) { - if (\in_array(substr($name, -1), ['/', '\\'])) { + if (\in_array(substr($name, -1), ['/', '\\'], true)) { return 'html'; // return html for directories } diff --git a/src/Lexer.php b/src/Lexer.php index 34715f5eae9..027771accb9 100644 --- a/src/Lexer.php +++ b/src/Lexer.php @@ -337,7 +337,7 @@ private function lexExpression(): void // operators if (preg_match($this->regexes['operator'], $this->code, $match, 0, $this->cursor)) { $operator = preg_replace('/\s+/', ' ', $match[0]); - if (\in_array($operator, $this->openingBrackets)) { + if (\in_array($operator, $this->openingBrackets, true)) { $this->checkBrackets($operator); } $this->pushToken(Token::OPERATOR_TYPE, $operator); @@ -574,9 +574,9 @@ private function popState(): void private function checkBrackets(string $code): void { // opening bracket - if (\in_array($code, $this->openingBrackets)) { + if (\in_array($code, $this->openingBrackets, true)) { $this->brackets[] = [$code, $this->lineno]; - } elseif (\in_array($code, $this->closingBrackets)) { + } elseif (\in_array($code, $this->closingBrackets, true)) { // closing bracket if (!$this->brackets) { throw new SyntaxError(\sprintf('Unexpected "%s".', $code), $this->lineno, $this->source); diff --git a/src/Node/Expression/AssignNameExpression.php b/src/Node/Expression/AssignNameExpression.php index c194660daa8..9a7f0f92bca 100644 --- a/src/Node/Expression/AssignNameExpression.php +++ b/src/Node/Expression/AssignNameExpression.php @@ -26,7 +26,7 @@ public function __construct(string $name, int $lineno) } // All names supported by ExpressionParser::parsePrimaryExpression() should be excluded - if (\in_array(strtolower($name), ['true', 'false', 'none', 'null'])) { + if (\in_array(strtolower($name), ['true', 'false', 'none', 'null'], true)) { throw new SyntaxError(\sprintf('You cannot assign a value to "%s".', $name), $lineno); } diff --git a/src/Node/Expression/TempNameExpression.php b/src/Node/Expression/TempNameExpression.php index 8cb66a1936b..f996aab05de 100644 --- a/src/Node/Expression/TempNameExpression.php +++ b/src/Node/Expression/TempNameExpression.php @@ -21,7 +21,7 @@ class TempNameExpression extends AbstractExpression public function __construct(string|int|null $name, int $lineno) { // All names supported by ExpressionParser::parsePrimaryExpression() should be excluded - if ($name && \in_array(strtolower($name), ['true', 'false', 'none', 'null'])) { + if ($name && \in_array(strtolower($name), ['true', 'false', 'none', 'null'], true)) { throw new SyntaxError(\sprintf('You cannot assign a value to "%s".', $name), $lineno); } @@ -31,7 +31,7 @@ public function __construct(string|int|null $name, int $lineno) if (null !== $name && (\is_int($name) || ctype_digit($name))) { $name = (int) $name; - } elseif (\in_array($name, self::RESERVED_NAMES)) { + } elseif (\in_array($name, self::RESERVED_NAMES, true)) { $name = "\u{035C}".$name; } diff --git a/src/NodeVisitor/EscaperNodeVisitor.php b/src/NodeVisitor/EscaperNodeVisitor.php index b35ae881720..a9f829770a2 100644 --- a/src/NodeVisitor/EscaperNodeVisitor.php +++ b/src/NodeVisitor/EscaperNodeVisitor.php @@ -154,7 +154,7 @@ private function isSafeFor(string $type, AbstractExpression $expression, Environ $safe = $this->safeAnalysis->getSafe($expression); } - return \in_array($type, $safe) || \in_array('all', $safe); + return \in_array($type, $safe, true) || \in_array('all', $safe, true); } /** diff --git a/src/NodeVisitor/OptimizerNodeVisitor.php b/src/NodeVisitor/OptimizerNodeVisitor.php index 9283737f50d..b778ba40efa 100644 --- a/src/NodeVisitor/OptimizerNodeVisitor.php +++ b/src/NodeVisitor/OptimizerNodeVisitor.php @@ -143,7 +143,7 @@ private function enterOptimizeFor(Node $node): void } // optimize access to loop targets - elseif ($node instanceof ContextVariable && \in_array($node->getAttribute('name'), $this->loopsTargets)) { + elseif ($node instanceof ContextVariable && \in_array($node->getAttribute('name'), $this->loopsTargets, true)) { $node->setAttribute('always_defined', true); } diff --git a/src/NodeVisitor/SafeAnalysisNodeVisitor.php b/src/NodeVisitor/SafeAnalysisNodeVisitor.php index a5361fbf7be..8cb5f7a39a5 100644 --- a/src/NodeVisitor/SafeAnalysisNodeVisitor.php +++ b/src/NodeVisitor/SafeAnalysisNodeVisitor.php @@ -52,7 +52,7 @@ public function getSafe(Node $node) continue; } - if (\in_array('html_attr', $bucket['value'])) { + if (\in_array('html_attr', $bucket['value'], true)) { $bucket['value'][] = 'html'; } @@ -148,7 +148,7 @@ public function leaveNode(Node $node, Environment $env): ?Node $this->setSafe($node, ['all']); } elseif ($node instanceof GetAttrExpression && $node->getNode('node') instanceof ContextVariable) { $name = $node->getNode('node')->getAttribute('name'); - if (\in_array($name, $this->safeVars)) { + if (\in_array($name, $this->safeVars, true)) { $this->setSafe($node, ['all']); } } @@ -162,11 +162,11 @@ private function intersectSafe(array $a, array $b): array return []; } - if (\in_array('all', $a)) { + if (\in_array('all', $a, true)) { return $b; } - if (\in_array('all', $b)) { + if (\in_array('all', $b, true)) { return $a; } diff --git a/src/Runtime/EscaperRuntime.php b/src/Runtime/EscaperRuntime.php index ff7913f38f2..17ed76cc955 100644 --- a/src/Runtime/EscaperRuntime.php +++ b/src/Runtime/EscaperRuntime.php @@ -124,7 +124,7 @@ public function escape($string, string $strategy = 'html', ?string $charset = nu } $string = (string) $string; - } elseif (\in_array($strategy, ['html', 'js', 'css', 'html_attr', 'url'])) { + } elseif (\in_array($strategy, ['html', 'js', 'css', 'html_attr', 'url'], true)) { // we return the input as is (which can be of any type) return $string; } diff --git a/src/Sandbox/SecurityPolicy.php b/src/Sandbox/SecurityPolicy.php index 8dd68ae99b2..b2c83ee106c 100644 --- a/src/Sandbox/SecurityPolicy.php +++ b/src/Sandbox/SecurityPolicy.php @@ -67,7 +67,7 @@ public function setAllowedFunctions(array $functions): void public function checkSecurity($tags, $filters, $functions): void { foreach ($tags as $tag) { - if (!\in_array($tag, $this->allowedTags)) { + if (!\in_array($tag, $this->allowedTags, true)) { if ('extends' === $tag) { trigger_deprecation('twig/twig', '3.12', 'The "extends" tag is always allowed in sandboxes, but won\'t be in 4.0, please enable it explicitly in your sandbox policy if needed.'); } elseif ('use' === $tag) { @@ -79,13 +79,13 @@ public function checkSecurity($tags, $filters, $functions): void } foreach ($filters as $filter) { - if (!\in_array($filter, $this->allowedFilters)) { + if (!\in_array($filter, $this->allowedFilters, true)) { throw new SecurityNotAllowedFilterError(\sprintf('Filter "%s" is not allowed.', $filter), $filter); } } foreach ($functions as $function) { - if (!\in_array($function, $this->allowedFunctions)) { + if (!\in_array($function, $this->allowedFunctions, true)) { throw new SecurityNotAllowedFunctionError(\sprintf('Function "%s" is not allowed.', $function), $function); } } @@ -100,7 +100,7 @@ public function checkMethodAllowed($obj, $method): void $allowed = false; $method = strtolower($method); foreach ($this->allowedMethods as $class => $methods) { - if ($obj instanceof $class && \in_array($method, $methods)) { + if ($obj instanceof $class && \in_array($method, $methods, true)) { $allowed = true; break; } @@ -116,7 +116,7 @@ public function checkPropertyAllowed($obj, $property): void { $allowed = false; foreach ($this->allowedProperties as $class => $properties) { - if ($obj instanceof $class && \in_array($property, \is_array($properties) ? $properties : [$properties])) { + if ($obj instanceof $class && \in_array($property, \is_array($properties) ? $properties : [$properties], true)) { $allowed = true; break; } diff --git a/src/Token.php b/src/Token.php index 7059619f82d..823c7738769 100644 --- a/src/Token.php +++ b/src/Token.php @@ -87,9 +87,9 @@ public function test($type, $values = null): bool } $typeMatches = $this->type === $type; - if ($typeMatches && self::PUNCTUATION_TYPE === $type && \in_array($this->value, ['(', '[', '|', '.', '?', '?:']) && $values) { + if ($typeMatches && self::PUNCTUATION_TYPE === $type && \in_array($this->value, ['(', '[', '|', '.', '?', '?:'], true) && $values) { foreach ((array) $values as $value) { - if (\in_array($value, ['(', '[', '|', '.', '?', '?:'])) { + if (\in_array($value, ['(', '[', '|', '.', '?', '?:'], true)) { trigger_deprecation('twig/twig', '3.21', 'The "%s" token is now an "%s" token instead of a "%s" one.', $this->value, self::typeToEnglish(self::OPERATOR_TYPE), $this->toEnglish()); break; @@ -100,7 +100,7 @@ public function test($type, $values = null): bool if (self::OPERATOR_TYPE === $type && self::PUNCTUATION_TYPE === $this->type) { if ($values) { foreach ((array) $values as $value) { - if (\in_array($value, ['(', '[', '|', '.', '?', '?:'])) { + if (\in_array($value, ['(', '[', '|', '.', '?', '?:'], true)) { $typeMatches = true; break; @@ -114,7 +114,7 @@ public function test($type, $values = null): bool return $typeMatches && ( null === $values - || (\is_array($values) && \in_array($this->value, $values)) + || (\is_array($values) && \in_array($this->value, $values, true)) || $this->value == $values ); } diff --git a/src/TokenParser/GuardTokenParser.php b/src/TokenParser/GuardTokenParser.php index 1fcf76cd740..656766af516 100644 --- a/src/TokenParser/GuardTokenParser.php +++ b/src/TokenParser/GuardTokenParser.php @@ -26,7 +26,7 @@ public function parse(Token $token): Node { $stream = $this->parser->getStream(); $typeToken = $stream->expect(Token::NAME_TYPE); - if (!\in_array($typeToken->getValue(), ['function', 'filter', 'test'])) { + if (!\in_array($typeToken->getValue(), ['function', 'filter', 'test'], true)) { throw new SyntaxError(\sprintf('Supported guard types are function, filter and test, "%s" given.', $typeToken->getValue()), $typeToken->getLine(), $stream->getSourceContext()); } $method = 'get'.$typeToken->getValue(); From 2d84abfd08c669d89d8125793752af68a8d5521a Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Fri, 21 Feb 2025 08:30:25 +0100 Subject: [PATCH 739/812] Make the defined test implementation more generic --- CHANGELOG | 1 + doc/deprecated.rst | 3 ++ src/Node/Expression/ArrayExpression.php | 10 ++++- .../Expression/BlockReferenceExpression.php | 9 ++-- src/Node/Expression/ConstantExpression.php | 6 ++- src/Node/Expression/FunctionExpression.php | 16 +++++-- src/Node/Expression/GetAttrExpression.php | 27 ++++++++++-- .../Expression/MacroReferenceExpression.php | 9 ++-- src/Node/Expression/MethodCallExpression.php | 9 ++-- src/Node/Expression/NameExpression.php | 11 +++-- .../SupportDefinedTestDeprecationTrait.php | 44 +++++++++++++++++++ .../SupportDefinedTestInterface.php | 24 ++++++++++ .../Expression/SupportDefinedTestTrait.php | 27 ++++++++++++ src/Node/Expression/Test/DefinedTest.php | 30 ++----------- .../Variable/ContextVariableTest.php | 6 +-- 15 files changed, 180 insertions(+), 52 deletions(-) create mode 100644 src/Node/Expression/SupportDefinedTestDeprecationTrait.php create mode 100644 src/Node/Expression/SupportDefinedTestInterface.php create mode 100644 src/Node/Expression/SupportDefinedTestTrait.php diff --git a/CHANGELOG b/CHANGELOG index cb70fd0c780..d36aa696fa2 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,6 @@ # 3.21.0 (2025-XX-XX) + * Add `SupportDefinedTestInterface` for expression nodes supporting the `defined` test * Deprecate using the `|` operator in an expression with `+` or `-` without using parentheses to clarify precedence * Deprecate operator precedence outside of the [0, 512] range * Introduce expression parser classes to describe operators and operands provided by extensions diff --git a/doc/deprecated.rst b/doc/deprecated.rst index 6e8212fb0e4..55c5e25a6f1 100644 --- a/doc/deprecated.rst +++ b/doc/deprecated.rst @@ -196,6 +196,9 @@ Nodes * The ``Twig\Node\Expression\ConditionalExpression`` class is deprecated as of Twig 3.17, use ``Twig\Node\Expression\Ternary\ConditionalTernary`` instead. + * The ``is_defined_test`` attribute is deprecated as of Twig 3.21, use + ``Twig\Node\Expression\SupportDefinedTestInterface`` instead. + Node Visitors ------------- diff --git a/src/Node/Expression/ArrayExpression.php b/src/Node/Expression/ArrayExpression.php index c9b3a3ec1ac..e58f43c91d5 100644 --- a/src/Node/Expression/ArrayExpression.php +++ b/src/Node/Expression/ArrayExpression.php @@ -16,8 +16,10 @@ use Twig\Node\Expression\Unary\StringCastUnary; use Twig\Node\Expression\Variable\ContextVariable; -class ArrayExpression extends AbstractExpression +class ArrayExpression extends AbstractExpression implements SupportDefinedTestInterface { + use SupportDefinedTestTrait; + private $index; public function __construct(array $elements, int $lineno) @@ -69,6 +71,12 @@ public function addElement(AbstractExpression $value, ?AbstractExpression $key = public function compile(Compiler $compiler): void { + if ($this->definedTest) { + $compiler->repr(true); + + return; + } + $compiler->raw('['); $first = true; $nextIndex = 0; diff --git a/src/Node/Expression/BlockReferenceExpression.php b/src/Node/Expression/BlockReferenceExpression.php index 508ca2d8382..8fe3f8b1253 100644 --- a/src/Node/Expression/BlockReferenceExpression.php +++ b/src/Node/Expression/BlockReferenceExpression.php @@ -20,8 +20,11 @@ * * @author Fabien Potencier */ -class BlockReferenceExpression extends AbstractExpression +class BlockReferenceExpression extends AbstractExpression implements SupportDefinedTestInterface { + use SupportDefinedTestDeprecationTrait; + use SupportDefinedTestTrait; + /** * @param AbstractExpression $name */ @@ -36,12 +39,12 @@ public function __construct(Node $name, ?Node $template, int $lineno) $nodes['template'] = $template; } - parent::__construct($nodes, ['is_defined_test' => false, 'output' => false], $lineno); + parent::__construct($nodes, ['output' => false], $lineno); } public function compile(Compiler $compiler): void { - if ($this->getAttribute('is_defined_test')) { + if ($this->definedTest) { $this->compileTemplateCall($compiler, 'hasBlock'); } else { if ($this->getAttribute('output')) { diff --git a/src/Node/Expression/ConstantExpression.php b/src/Node/Expression/ConstantExpression.php index 2a8909d5469..8f8b2ffe96b 100644 --- a/src/Node/Expression/ConstantExpression.php +++ b/src/Node/Expression/ConstantExpression.php @@ -17,8 +17,10 @@ /** * @final */ -class ConstantExpression extends AbstractExpression +class ConstantExpression extends AbstractExpression implements SupportDefinedTestInterface { + use SupportDefinedTestTrait; + public function __construct($value, int $lineno) { parent::__construct([], ['value' => $value], $lineno); @@ -26,6 +28,6 @@ public function __construct($value, int $lineno) public function compile(Compiler $compiler): void { - $compiler->repr($this->getAttribute('value')); + $compiler->repr($this->definedTest ? true : $this->getAttribute('value')); } } diff --git a/src/Node/Expression/FunctionExpression.php b/src/Node/Expression/FunctionExpression.php index 5e22e73e89c..183145c4148 100644 --- a/src/Node/Expression/FunctionExpression.php +++ b/src/Node/Expression/FunctionExpression.php @@ -17,8 +17,11 @@ use Twig\Node\Node; use Twig\TwigFunction; -class FunctionExpression extends CallExpression +class FunctionExpression extends CallExpression implements SupportDefinedTestInterface { + use SupportDefinedTestDeprecationTrait; + use SupportDefinedTestTrait; + #[FirstClassTwigCallableReady] public function __construct(TwigFunction|string $function, Node $arguments, int $lineno) { @@ -29,7 +32,7 @@ public function __construct(TwigFunction|string $function, Node $arguments, int trigger_deprecation('twig/twig', '3.12', 'Not passing an instance of "TwigFunction" when creating a "%s" function of type "%s" is deprecated.', $name, static::class); } - parent::__construct(['arguments' => $arguments], ['name' => $name, 'type' => 'function', 'is_defined_test' => false], $lineno); + parent::__construct(['arguments' => $arguments], ['name' => $name, 'type' => 'function'], $lineno); if ($function instanceof TwigFunction) { $this->setAttribute('twig_callable', $function); @@ -44,6 +47,13 @@ public function __construct(TwigFunction|string $function, Node $arguments, int $this->deprecateAttribute('dynamic_name', new NameDeprecation('twig/twig', '3.12')); } + public function enableDefinedTest(): void + { + if ('constant' === $this->getAttribute('name')) { + $this->definedTest = true; + } + } + /** * @return void */ @@ -62,7 +72,7 @@ public function compile(Compiler $compiler) $this->setAttribute('twig_callable', $compiler->getEnvironment()->getFunction($name)); } - if ('constant' === $name && $this->getAttribute('is_defined_test')) { + if ('constant' === $name && $this->isDefinedTestEnabled()) { $this->getNode('arguments')->setNode('checkDefined', new ConstantExpression(true, $this->getTemplateLine())); } diff --git a/src/Node/Expression/GetAttrExpression.php b/src/Node/Expression/GetAttrExpression.php index e072f2a0b54..781c8af3868 100644 --- a/src/Node/Expression/GetAttrExpression.php +++ b/src/Node/Expression/GetAttrExpression.php @@ -17,8 +17,11 @@ use Twig\Node\Expression\Variable\ContextVariable; use Twig\Template; -class GetAttrExpression extends AbstractExpression +class GetAttrExpression extends AbstractExpression implements SupportDefinedTestInterface { + use SupportDefinedTestDeprecationTrait; + use SupportDefinedTestTrait; + /** * @param ArrayExpression|NameExpression|null $arguments */ @@ -33,7 +36,13 @@ public function __construct(AbstractExpression $node, AbstractExpression $attrib trigger_deprecation('twig/twig', '3.15', \sprintf('Not passing a "%s" instance as the "arguments" argument of the "%s" constructor is deprecated ("%s" given).', ArrayExpression::class, static::class, $arguments::class)); } - parent::__construct($nodes, ['type' => $type, 'is_defined_test' => false, 'ignore_strict_check' => false, 'optimizable' => true], $lineno); + parent::__construct($nodes, ['type' => $type, 'ignore_strict_check' => false, 'optimizable' => true], $lineno); + } + + public function enableDefinedTest(): void + { + $this->definedTest = true; + $this->changeIgnoreStrictCheck($this); } public function compile(Compiler $compiler): void @@ -45,7 +54,7 @@ public function compile(Compiler $compiler): void if ( $this->getAttribute('optimizable') && (!$env->isStrictVariables() || $this->getAttribute('ignore_strict_check')) - && !$this->getAttribute('is_defined_test') + && !$this->definedTest && Template::ARRAY_CALL === $this->getAttribute('type') ) { $var = '$'.$compiler->getVarName(); @@ -104,7 +113,7 @@ public function compile(Compiler $compiler): void $compiler->raw(', ') ->repr($this->getAttribute('type')) - ->raw(', ')->repr($this->getAttribute('is_defined_test')) + ->raw(', ')->repr($this->definedTest) ->raw(', ')->repr($this->getAttribute('ignore_strict_check')) ->raw(', ')->repr($env->hasExtension(SandboxExtension::class)) ->raw(', ')->repr($this->getNode('node')->getTemplateLine()) @@ -115,4 +124,14 @@ public function compile(Compiler $compiler): void $compiler->raw(')'); } } + + private function changeIgnoreStrictCheck(GetAttrExpression $node): void + { + $node->setAttribute('optimizable', false); + $node->setAttribute('ignore_strict_check', true); + + if ($node->getNode('node') instanceof GetAttrExpression) { + $this->changeIgnoreStrictCheck($node->getNode('node')); + } + } } diff --git a/src/Node/Expression/MacroReferenceExpression.php b/src/Node/Expression/MacroReferenceExpression.php index abe99aa3564..fd7f1e733a9 100644 --- a/src/Node/Expression/MacroReferenceExpression.php +++ b/src/Node/Expression/MacroReferenceExpression.php @@ -19,16 +19,19 @@ * * @author Fabien Potencier */ -class MacroReferenceExpression extends AbstractExpression +class MacroReferenceExpression extends AbstractExpression implements SupportDefinedTestInterface { + use SupportDefinedTestDeprecationTrait; + use SupportDefinedTestTrait; + public function __construct(TemplateVariable $template, string $name, AbstractExpression $arguments, int $lineno) { - parent::__construct(['template' => $template, 'arguments' => $arguments], ['name' => $name, 'is_defined_test' => false], $lineno); + parent::__construct(['template' => $template, 'arguments' => $arguments], ['name' => $name], $lineno); } public function compile(Compiler $compiler): void { - if ($this->getAttribute('is_defined_test')) { + if ($this->definedTest) { $compiler ->subcompile($this->getNode('template')) ->raw('->hasMacro(') diff --git a/src/Node/Expression/MethodCallExpression.php b/src/Node/Expression/MethodCallExpression.php index 922b98b10bc..4b180534df9 100644 --- a/src/Node/Expression/MethodCallExpression.php +++ b/src/Node/Expression/MethodCallExpression.php @@ -14,13 +14,16 @@ use Twig\Compiler; use Twig\Node\Expression\Variable\ContextVariable; -class MethodCallExpression extends AbstractExpression +class MethodCallExpression extends AbstractExpression implements SupportDefinedTestInterface { + use SupportDefinedTestDeprecationTrait; + use SupportDefinedTestTrait; + public function __construct(AbstractExpression $node, string $method, ArrayExpression $arguments, int $lineno) { trigger_deprecation('twig/twig', '3.15', 'The "%s" class is deprecated, use "%s" instead.', __CLASS__, MacroReferenceExpression::class); - parent::__construct(['node' => $node, 'arguments' => $arguments], ['method' => $method, 'safe' => false, 'is_defined_test' => false], $lineno); + parent::__construct(['node' => $node, 'arguments' => $arguments], ['method' => $method, 'safe' => false], $lineno); if ($node instanceof ContextVariable) { $node->setAttribute('always_defined', true); @@ -29,7 +32,7 @@ public function __construct(AbstractExpression $node, string $method, ArrayExpre public function compile(Compiler $compiler): void { - if ($this->getAttribute('is_defined_test')) { + if ($this->definedTest) { $compiler ->raw('method_exists($macros[') ->repr($this->getNode('node')->getAttribute('name')) diff --git a/src/Node/Expression/NameExpression.php b/src/Node/Expression/NameExpression.php index 2872ba413ba..0e036742086 100644 --- a/src/Node/Expression/NameExpression.php +++ b/src/Node/Expression/NameExpression.php @@ -15,8 +15,11 @@ use Twig\Compiler; use Twig\Node\Expression\Variable\ContextVariable; -class NameExpression extends AbstractExpression +class NameExpression extends AbstractExpression implements SupportDefinedTestInterface { + use SupportDefinedTestDeprecationTrait; + use SupportDefinedTestTrait; + private $specialVars = [ '_self' => '$this->getTemplateName()', '_context' => '$context', @@ -29,7 +32,7 @@ public function __construct(string $name, int $lineno) trigger_deprecation('twig/twig', '3.15', 'The "%s" class is deprecated, use "%s" instead.', self::class, ContextVariable::class); } - parent::__construct([], ['name' => $name, 'is_defined_test' => false, 'ignore_strict_check' => false, 'always_defined' => false], $lineno); + parent::__construct([], ['name' => $name, 'ignore_strict_check' => false, 'always_defined' => false], $lineno); } public function compile(Compiler $compiler): void @@ -38,7 +41,7 @@ public function compile(Compiler $compiler): void $compiler->addDebugInfo($this); - if ($this->getAttribute('is_defined_test')) { + if ($this->definedTest) { if (isset($this->specialVars[$name]) || $this->getAttribute('always_defined')) { $compiler->repr(true); } elseif (\PHP_VERSION_ID >= 70400) { @@ -107,6 +110,6 @@ public function isSimple() { trigger_deprecation('twig/twig', '3.11', 'The "%s()" method is deprecated and will be removed in Twig 4.0.', __METHOD__); - return !$this->isSpecial() && !$this->getAttribute('is_defined_test'); + return !isset($this->specialVars[$this->getAttribute('name')]) && !$this->definedTest; } } diff --git a/src/Node/Expression/SupportDefinedTestDeprecationTrait.php b/src/Node/Expression/SupportDefinedTestDeprecationTrait.php new file mode 100644 index 00000000000..664464bb2e7 --- /dev/null +++ b/src/Node/Expression/SupportDefinedTestDeprecationTrait.php @@ -0,0 +1,44 @@ + + */ +trait SupportDefinedTestDeprecationTrait +{ + public function getAttribute($name, $default = null) + { + if ('is_defined_test' === $name) { + trigger_deprecation('twig/twig', '3.21', 'The "is_defined_test" attribute is deprecated, call "isDefinedTestEnabled()" instead.'); + + return $this->isDefinedTestEnabled(); + } + + return parent::getAttribute($name, $default); + } + + public function setAttribute(string $name, $value): void + { + if ('is_defined_test' === $name) { + trigger_deprecation('twig/twig', '3.21', 'The "is_defined_test" attribute is deprecated, call "enableDefinedTest()" instead.'); + + $this->definedTest = (bool) $value; + } else { + parent::setAttribute($name, $value); + } + } +} diff --git a/src/Node/Expression/SupportDefinedTestInterface.php b/src/Node/Expression/SupportDefinedTestInterface.php new file mode 100644 index 00000000000..450c691c59e --- /dev/null +++ b/src/Node/Expression/SupportDefinedTestInterface.php @@ -0,0 +1,24 @@ + + */ +interface SupportDefinedTestInterface +{ + public function enableDefinedTest(): void; + + public function isDefinedTestEnabled(): bool; +} diff --git a/src/Node/Expression/SupportDefinedTestTrait.php b/src/Node/Expression/SupportDefinedTestTrait.php new file mode 100644 index 00000000000..4cf1a58d537 --- /dev/null +++ b/src/Node/Expression/SupportDefinedTestTrait.php @@ -0,0 +1,27 @@ +definedTest = true; + } + + public function isDefinedTestEnabled(): bool + { + return $this->definedTest; + } +} diff --git a/src/Node/Expression/Test/DefinedTest.php b/src/Node/Expression/Test/DefinedTest.php index 9612892be0c..f17715bc61c 100644 --- a/src/Node/Expression/Test/DefinedTest.php +++ b/src/Node/Expression/Test/DefinedTest.php @@ -22,6 +22,7 @@ use Twig\Node\Expression\GetAttrExpression; use Twig\Node\Expression\MacroReferenceExpression; use Twig\Node\Expression\MethodCallExpression; +use Twig\Node\Expression\SupportDefinedTestInterface; use Twig\Node\Expression\TestExpression; use Twig\Node\Expression\Variable\ContextVariable; use Twig\Node\Node; @@ -49,25 +50,12 @@ public function __construct(Node $node, TwigTest|string $name, ?Node $arguments, trigger_deprecation('twig/twig', '3.15', 'Not passing a "%s" instance to the "node" argument of "%s" is deprecated ("%s" given).', AbstractExpression::class, static::class, $node::class); } - if ($node instanceof ContextVariable) { - $node->setAttribute('is_defined_test', true); - } elseif ($node instanceof GetAttrExpression) { - $node->setAttribute('is_defined_test', true); - $this->changeIgnoreStrictCheck($node); - } elseif ($node instanceof BlockReferenceExpression) { - $node->setAttribute('is_defined_test', true); - } elseif ($node instanceof MacroReferenceExpression) { - $node->setAttribute('is_defined_test', true); - } elseif ($node instanceof FunctionExpression && 'constant' === $node->getAttribute('name')) { - $node->setAttribute('is_defined_test', true); - } elseif ($node instanceof ConstantExpression || $node instanceof ArrayExpression) { - $node = new ConstantExpression(true, $node->getTemplateLine()); - } elseif ($node instanceof MethodCallExpression) { - $node->setAttribute('is_defined_test', true); - } else { + if (!$node instanceof SupportDefinedTestInterface) { throw new SyntaxError('The "defined" test only works with simple variables.', $lineno); } + $node->enableDefinedTest(); + if (\is_string($name) && 'defined' !== $name) { trigger_deprecation('twig/twig', '3.12', 'Creating a "DefinedTest" instance with a test name that is not "defined" is deprecated.'); } @@ -75,16 +63,6 @@ public function __construct(Node $node, TwigTest|string $name, ?Node $arguments, parent::__construct($node, $name, $arguments, $lineno); } - private function changeIgnoreStrictCheck(GetAttrExpression $node): void - { - $node->setAttribute('optimizable', false); - $node->setAttribute('ignore_strict_check', true); - - if ($node->getNode('node') instanceof GetAttrExpression) { - $this->changeIgnoreStrictCheck($node->getNode('node')); - } - } - public function compile(Compiler $compiler): void { $compiler->subcompile($this->getNode('node')); diff --git a/tests/Node/Expression/Variable/ContextVariableTest.php b/tests/Node/Expression/Variable/ContextVariableTest.php index 46ab171a043..e98aa7219b5 100644 --- a/tests/Node/Expression/Variable/ContextVariableTest.php +++ b/tests/Node/Expression/Variable/ContextVariableTest.php @@ -32,7 +32,7 @@ public static function provideTests(): iterable $node = new ContextVariable($special, 1); yield $special => [$node, "// line 1\n$compiled"]; $node = new ContextVariable($special, 1); - $node->setAttribute('is_defined_test', true); + $node->enableDefinedTest(); yield $special.'_defined_test' => [$node, "// line 1\ntrue"]; } @@ -59,13 +59,13 @@ public static function provideTests(): iterable // is defined test $node = new ContextVariable('foo', 1); - $node->setAttribute('is_defined_test', true); + $node->enableDefinedTest(); yield 'is_defined_test_strict' => [$node, "// line 1\narray_key_exists(\"foo\", \$context)", $envStrict]; yield 'is_defined_test_non_strict' => [$node, "// line 1\narray_key_exists(\"foo\", \$context)", $env]; // is defined test // always defined $node = new ContextVariable('foo', 1); - $node->setAttribute('is_defined_test', true); + $node->enableDefinedTest(); $node->setAttribute('always_defined', true); yield 'is_defined_test_always_defined_strict' => [$node, "// line 1\ntrue", $envStrict]; yield 'is_defined_test_always_defined_non_strict' => [$node, "// line 1\ntrue", $env]; From d10bcadd75633729cf81b1feeb77b6b3354f551c Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Fri, 21 Feb 2025 11:00:21 +0100 Subject: [PATCH 740/812] Simplify Error implementation --- src/Error/Error.php | 60 +++++++++++++-------------------------------- 1 file changed, 17 insertions(+), 43 deletions(-) diff --git a/src/Error/Error.php b/src/Error/Error.php index 61c309fa16e..31924d942c5 100644 --- a/src/Error/Error.php +++ b/src/Error/Error.php @@ -39,10 +39,8 @@ class Error extends \Exception { private $lineno; - private $name; private $rawMessage; - private $sourcePath; - private $sourceCode; + private ?Source $source; /** * Constructor. @@ -57,16 +55,8 @@ public function __construct(string $message, int $lineno = -1, ?Source $source = { parent::__construct('', 0, $previous); - if (null === $source) { - $name = null; - } else { - $name = $source->getName(); - $this->sourceCode = $source->getCode(); - $this->sourcePath = $source->getPath(); - } - $this->lineno = $lineno; - $this->name = $name; + $this->source = $source; $this->rawMessage = $message; $this->updateRepr(); } @@ -84,25 +74,17 @@ public function getTemplateLine(): int public function setTemplateLine(int $lineno): void { $this->lineno = $lineno; - $this->updateRepr(); } public function getSourceContext(): ?Source { - return $this->name ? new Source($this->sourceCode, $this->name, $this->sourcePath) : null; + return $this->source; } public function setSourceContext(?Source $source = null): void { - if (null === $source) { - $this->sourceCode = $this->name = $this->sourcePath = null; - } else { - $this->sourceCode = $source->getCode(); - $this->name = $source->getName(); - $this->sourcePath = $source->getPath(); - } - + $this->source = $source; $this->updateRepr(); } @@ -122,8 +104,8 @@ private function updateRepr(): void { $this->message = $this->rawMessage; - if ($this->sourcePath && $this->lineno > 0) { - $this->file = $this->sourcePath; + if ($this->source && $this->source->getPath() && $this->lineno > 0) { + $this->file = $this->source->getPath(); $this->line = $this->lineno; return; @@ -141,11 +123,12 @@ private function updateRepr(): void $questionMark = true; } - if ($this->name) { - if (\is_string($this->name) || $this->name instanceof \Stringable) { - $name = \sprintf('"%s"', $this->name); + if ($this->source && $this->source->getName()) { + $name = $this->source->getName(); + if (\is_string($name) || $name instanceof \Stringable) { + $name = \sprintf('"%s"', $name); } else { - $name = json_encode($this->name); + $name = json_encode($name); } $this->message .= \sprintf(' in %s', $name); } @@ -165,34 +148,25 @@ private function updateRepr(): void private function guessTemplateInfo(): void { + // $this->source is never null here (see guess() usage in Template) + $template = null; $templateClass = null; - $backtrace = debug_backtrace(\DEBUG_BACKTRACE_IGNORE_ARGS | \DEBUG_BACKTRACE_PROVIDE_OBJECT); foreach ($backtrace as $trace) { if (isset($trace['object']) && $trace['object'] instanceof Template) { $currentClass = \get_class($trace['object']); $isEmbedContainer = null === $templateClass ? false : str_starts_with($templateClass, $currentClass); - if (null === $this->name || ($this->name == $trace['object']->getTemplateName() && !$isEmbedContainer)) { + if ($this->source->getName() === $trace['object']->getTemplateName() && !$isEmbedContainer) { $template = $trace['object']; $templateClass = \get_class($trace['object']); } } } - // update template name - if (null !== $template && null === $this->name) { - $this->name = $template->getTemplateName(); - } - - // update template path if any - if (null !== $template && null === $this->sourcePath) { - $src = $template->getSourceContext(); - $this->sourceCode = $src->getCode(); - $this->sourcePath = $src->getPath(); - } - - if (null === $template || $this->lineno > -1) { + if ($template) { + $this->source = $template->getSourceContext(); + } elseif ($this->lineno > -1) { return; } From d72c1c6b1efcb6de856ec9ea052a7ffa296ac0b3 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Fri, 21 Feb 2025 07:23:14 +0100 Subject: [PATCH 741/812] Fix testing and expression when it evaluates to an instance of Markup --- CHANGELOG | 2 ++ src/Extension/CoreExtension.php | 2 ++ src/Node/Expression/ArrayExpression.php | 2 +- src/Node/Expression/Binary/AddBinary.php | 3 +- src/Node/Expression/Binary/AndBinary.php | 3 +- .../Expression/Binary/BitwiseAndBinary.php | 3 +- .../Expression/Binary/BitwiseOrBinary.php | 3 +- .../Expression/Binary/BitwiseXorBinary.php | 3 +- src/Node/Expression/Binary/ConcatBinary.php | 3 +- src/Node/Expression/Binary/DivBinary.php | 3 +- src/Node/Expression/Binary/EndsWithBinary.php | 3 +- src/Node/Expression/Binary/EqualBinary.php | 3 +- src/Node/Expression/Binary/FloorDivBinary.php | 3 +- src/Node/Expression/Binary/GreaterBinary.php | 3 +- .../Expression/Binary/GreaterEqualBinary.php | 3 +- src/Node/Expression/Binary/HasEveryBinary.php | 3 +- src/Node/Expression/Binary/HasSomeBinary.php | 3 +- src/Node/Expression/Binary/InBinary.php | 3 +- src/Node/Expression/Binary/LessBinary.php | 3 +- .../Expression/Binary/LessEqualBinary.php | 3 +- src/Node/Expression/Binary/MatchesBinary.php | 3 +- src/Node/Expression/Binary/ModBinary.php | 3 +- src/Node/Expression/Binary/MulBinary.php | 3 +- src/Node/Expression/Binary/NotEqualBinary.php | 3 +- src/Node/Expression/Binary/NotInBinary.php | 3 +- src/Node/Expression/Binary/OrBinary.php | 3 +- src/Node/Expression/Binary/PowerBinary.php | 3 +- src/Node/Expression/Binary/RangeBinary.php | 3 +- .../Expression/Binary/SpaceshipBinary.php | 3 +- .../Expression/Binary/StartsWithBinary.php | 3 +- src/Node/Expression/Binary/SubBinary.php | 3 +- src/Node/Expression/Binary/XorBinary.php | 3 +- src/Node/Expression/ConstantExpression.php | 2 +- src/Node/Expression/ReturnArrayInterface.php | 16 +++++++++ src/Node/Expression/ReturnBoolInterface.php | 16 +++++++++ src/Node/Expression/ReturnNumberInterface.php | 16 +++++++++ .../ReturnPrimitiveTypeInterface.php | 16 +++++++++ src/Node/Expression/ReturnStringInterface.php | 16 +++++++++ .../Expression/Ternary/ConditionalTernary.php | 7 ++++ src/Node/Expression/Test/NullTest.php | 2 +- src/Node/Expression/Test/TrueTest.php | 34 +++++++++++++++++++ src/Node/Expression/TestExpression.php | 2 +- src/Node/IfNode.php | 9 +++++ tests/Fixtures/regression/markup_test.test | 18 ++++++++++ 44 files changed, 214 insertions(+), 33 deletions(-) create mode 100644 src/Node/Expression/ReturnArrayInterface.php create mode 100644 src/Node/Expression/ReturnBoolInterface.php create mode 100644 src/Node/Expression/ReturnNumberInterface.php create mode 100644 src/Node/Expression/ReturnPrimitiveTypeInterface.php create mode 100644 src/Node/Expression/ReturnStringInterface.php create mode 100644 src/Node/Expression/Test/TrueTest.php create mode 100644 tests/Fixtures/regression/markup_test.test diff --git a/CHANGELOG b/CHANGELOG index d36aa696fa2..23970440ba2 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,7 @@ # 3.21.0 (2025-XX-XX) + * Fix testing and expression when it evaluates to an instance of `Markup` + * Add `ReturnPrimitiveTypeInterface` (and sub-interfaces for number, boolean, string, and array) * Add `SupportDefinedTestInterface` for expression nodes supporting the `defined` test * Deprecate using the `|` operator in an expression with `+` or `-` without using parentheses to clarify precedence * Deprecate operator precedence outside of the [0, 512] range diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index f44b6ad6d86..e41b5ba3c38 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -76,6 +76,7 @@ use Twig\Node\Expression\Test\NullTest; use Twig\Node\Expression\Test\OddTest; use Twig\Node\Expression\Test\SameasTest; +use Twig\Node\Expression\Test\TrueTest; use Twig\Node\Expression\Unary\NegUnary; use Twig\Node\Expression\Unary\NotUnary; use Twig\Node\Expression\Unary\PosUnary; @@ -318,6 +319,7 @@ public function getTests(): array new TwigTest('iterable', 'is_iterable'), new TwigTest('sequence', [self::class, 'testSequence']), new TwigTest('mapping', [self::class, 'testMapping']), + new TwigTest('true', null, ['node_class' => TrueTest::class]), ]; } diff --git a/src/Node/Expression/ArrayExpression.php b/src/Node/Expression/ArrayExpression.php index e58f43c91d5..9678be26c0a 100644 --- a/src/Node/Expression/ArrayExpression.php +++ b/src/Node/Expression/ArrayExpression.php @@ -16,7 +16,7 @@ use Twig\Node\Expression\Unary\StringCastUnary; use Twig\Node\Expression\Variable\ContextVariable; -class ArrayExpression extends AbstractExpression implements SupportDefinedTestInterface +class ArrayExpression extends AbstractExpression implements SupportDefinedTestInterface, ReturnArrayInterface { use SupportDefinedTestTrait; diff --git a/src/Node/Expression/Binary/AddBinary.php b/src/Node/Expression/Binary/AddBinary.php index ee4307e33e2..42377aea056 100644 --- a/src/Node/Expression/Binary/AddBinary.php +++ b/src/Node/Expression/Binary/AddBinary.php @@ -13,8 +13,9 @@ namespace Twig\Node\Expression\Binary; use Twig\Compiler; +use Twig\Node\Expression\ReturnNumberInterface; -class AddBinary extends AbstractBinary +class AddBinary extends AbstractBinary implements ReturnNumberInterface { public function operator(Compiler $compiler): Compiler { diff --git a/src/Node/Expression/Binary/AndBinary.php b/src/Node/Expression/Binary/AndBinary.php index 5f2380da545..454ea70e5b2 100644 --- a/src/Node/Expression/Binary/AndBinary.php +++ b/src/Node/Expression/Binary/AndBinary.php @@ -13,8 +13,9 @@ namespace Twig\Node\Expression\Binary; use Twig\Compiler; +use Twig\Node\Expression\ReturnBoolInterface; -class AndBinary extends AbstractBinary +class AndBinary extends AbstractBinary implements ReturnBoolInterface { public function operator(Compiler $compiler): Compiler { diff --git a/src/Node/Expression/Binary/BitwiseAndBinary.php b/src/Node/Expression/Binary/BitwiseAndBinary.php index db7d6d612dd..1c26f98933b 100644 --- a/src/Node/Expression/Binary/BitwiseAndBinary.php +++ b/src/Node/Expression/Binary/BitwiseAndBinary.php @@ -13,8 +13,9 @@ namespace Twig\Node\Expression\Binary; use Twig\Compiler; +use Twig\Node\Expression\ReturnNumberInterface; -class BitwiseAndBinary extends AbstractBinary +class BitwiseAndBinary extends AbstractBinary implements ReturnNumberInterface { public function operator(Compiler $compiler): Compiler { diff --git a/src/Node/Expression/Binary/BitwiseOrBinary.php b/src/Node/Expression/Binary/BitwiseOrBinary.php index ce803dd9027..ec17e2280b5 100644 --- a/src/Node/Expression/Binary/BitwiseOrBinary.php +++ b/src/Node/Expression/Binary/BitwiseOrBinary.php @@ -13,8 +13,9 @@ namespace Twig\Node\Expression\Binary; use Twig\Compiler; +use Twig\Node\Expression\ReturnNumberInterface; -class BitwiseOrBinary extends AbstractBinary +class BitwiseOrBinary extends AbstractBinary implements ReturnNumberInterface { public function operator(Compiler $compiler): Compiler { diff --git a/src/Node/Expression/Binary/BitwiseXorBinary.php b/src/Node/Expression/Binary/BitwiseXorBinary.php index 5c29785014d..e6432a7aeb6 100644 --- a/src/Node/Expression/Binary/BitwiseXorBinary.php +++ b/src/Node/Expression/Binary/BitwiseXorBinary.php @@ -13,8 +13,9 @@ namespace Twig\Node\Expression\Binary; use Twig\Compiler; +use Twig\Node\Expression\ReturnNumberInterface; -class BitwiseXorBinary extends AbstractBinary +class BitwiseXorBinary extends AbstractBinary implements ReturnNumberInterface { public function operator(Compiler $compiler): Compiler { diff --git a/src/Node/Expression/Binary/ConcatBinary.php b/src/Node/Expression/Binary/ConcatBinary.php index f825ab874d6..75ee654731d 100644 --- a/src/Node/Expression/Binary/ConcatBinary.php +++ b/src/Node/Expression/Binary/ConcatBinary.php @@ -13,8 +13,9 @@ namespace Twig\Node\Expression\Binary; use Twig\Compiler; +use Twig\Node\Expression\ReturnStringInterface; -class ConcatBinary extends AbstractBinary +class ConcatBinary extends AbstractBinary implements ReturnStringInterface { public function operator(Compiler $compiler): Compiler { diff --git a/src/Node/Expression/Binary/DivBinary.php b/src/Node/Expression/Binary/DivBinary.php index e3817d1cd7f..11c061e18ef 100644 --- a/src/Node/Expression/Binary/DivBinary.php +++ b/src/Node/Expression/Binary/DivBinary.php @@ -13,8 +13,9 @@ namespace Twig\Node\Expression\Binary; use Twig\Compiler; +use Twig\Node\Expression\ReturnNumberInterface; -class DivBinary extends AbstractBinary +class DivBinary extends AbstractBinary implements ReturnNumberInterface { public function operator(Compiler $compiler): Compiler { diff --git a/src/Node/Expression/Binary/EndsWithBinary.php b/src/Node/Expression/Binary/EndsWithBinary.php index a73a5608dd9..e689d668a67 100644 --- a/src/Node/Expression/Binary/EndsWithBinary.php +++ b/src/Node/Expression/Binary/EndsWithBinary.php @@ -12,8 +12,9 @@ namespace Twig\Node\Expression\Binary; use Twig\Compiler; +use Twig\Node\Expression\ReturnBoolInterface; -class EndsWithBinary extends AbstractBinary +class EndsWithBinary extends AbstractBinary implements ReturnBoolInterface { public function compile(Compiler $compiler): void { diff --git a/src/Node/Expression/Binary/EqualBinary.php b/src/Node/Expression/Binary/EqualBinary.php index 5f423196fee..8c365035583 100644 --- a/src/Node/Expression/Binary/EqualBinary.php +++ b/src/Node/Expression/Binary/EqualBinary.php @@ -12,8 +12,9 @@ namespace Twig\Node\Expression\Binary; use Twig\Compiler; +use Twig\Node\Expression\ReturnBoolInterface; -class EqualBinary extends AbstractBinary +class EqualBinary extends AbstractBinary implements ReturnBoolInterface { public function compile(Compiler $compiler): void { diff --git a/src/Node/Expression/Binary/FloorDivBinary.php b/src/Node/Expression/Binary/FloorDivBinary.php index d7e7980efde..a60ab3b2129 100644 --- a/src/Node/Expression/Binary/FloorDivBinary.php +++ b/src/Node/Expression/Binary/FloorDivBinary.php @@ -12,8 +12,9 @@ namespace Twig\Node\Expression\Binary; use Twig\Compiler; +use Twig\Node\Expression\ReturnNumberInterface; -class FloorDivBinary extends AbstractBinary +class FloorDivBinary extends AbstractBinary implements ReturnNumberInterface { public function compile(Compiler $compiler): void { diff --git a/src/Node/Expression/Binary/GreaterBinary.php b/src/Node/Expression/Binary/GreaterBinary.php index f42de3f8645..71a980b3ee4 100644 --- a/src/Node/Expression/Binary/GreaterBinary.php +++ b/src/Node/Expression/Binary/GreaterBinary.php @@ -12,8 +12,9 @@ namespace Twig\Node\Expression\Binary; use Twig\Compiler; +use Twig\Node\Expression\ReturnBoolInterface; -class GreaterBinary extends AbstractBinary +class GreaterBinary extends AbstractBinary implements ReturnBoolInterface { public function compile(Compiler $compiler): void { diff --git a/src/Node/Expression/Binary/GreaterEqualBinary.php b/src/Node/Expression/Binary/GreaterEqualBinary.php index 0c4f43fd94a..c92e61b3796 100644 --- a/src/Node/Expression/Binary/GreaterEqualBinary.php +++ b/src/Node/Expression/Binary/GreaterEqualBinary.php @@ -12,8 +12,9 @@ namespace Twig\Node\Expression\Binary; use Twig\Compiler; +use Twig\Node\Expression\ReturnBoolInterface; -class GreaterEqualBinary extends AbstractBinary +class GreaterEqualBinary extends AbstractBinary implements ReturnBoolInterface { public function compile(Compiler $compiler): void { diff --git a/src/Node/Expression/Binary/HasEveryBinary.php b/src/Node/Expression/Binary/HasEveryBinary.php index c57bb20e916..22b38011810 100644 --- a/src/Node/Expression/Binary/HasEveryBinary.php +++ b/src/Node/Expression/Binary/HasEveryBinary.php @@ -12,8 +12,9 @@ namespace Twig\Node\Expression\Binary; use Twig\Compiler; +use Twig\Node\Expression\ReturnBoolInterface; -class HasEveryBinary extends AbstractBinary +class HasEveryBinary extends AbstractBinary implements ReturnBoolInterface { public function compile(Compiler $compiler): void { diff --git a/src/Node/Expression/Binary/HasSomeBinary.php b/src/Node/Expression/Binary/HasSomeBinary.php index 12293f84cb4..a2a363e99a8 100644 --- a/src/Node/Expression/Binary/HasSomeBinary.php +++ b/src/Node/Expression/Binary/HasSomeBinary.php @@ -12,8 +12,9 @@ namespace Twig\Node\Expression\Binary; use Twig\Compiler; +use Twig\Node\Expression\ReturnBoolInterface; -class HasSomeBinary extends AbstractBinary +class HasSomeBinary extends AbstractBinary implements ReturnBoolInterface { public function compile(Compiler $compiler): void { diff --git a/src/Node/Expression/Binary/InBinary.php b/src/Node/Expression/Binary/InBinary.php index 68a98fe15b3..31a21e7d696 100644 --- a/src/Node/Expression/Binary/InBinary.php +++ b/src/Node/Expression/Binary/InBinary.php @@ -12,8 +12,9 @@ namespace Twig\Node\Expression\Binary; use Twig\Compiler; +use Twig\Node\Expression\ReturnBoolInterface; -class InBinary extends AbstractBinary +class InBinary extends AbstractBinary implements ReturnBoolInterface { public function compile(Compiler $compiler): void { diff --git a/src/Node/Expression/Binary/LessBinary.php b/src/Node/Expression/Binary/LessBinary.php index fb3264a2d47..293d98d5175 100644 --- a/src/Node/Expression/Binary/LessBinary.php +++ b/src/Node/Expression/Binary/LessBinary.php @@ -12,8 +12,9 @@ namespace Twig\Node\Expression\Binary; use Twig\Compiler; +use Twig\Node\Expression\ReturnBoolInterface; -class LessBinary extends AbstractBinary +class LessBinary extends AbstractBinary implements ReturnBoolInterface { public function compile(Compiler $compiler): void { diff --git a/src/Node/Expression/Binary/LessEqualBinary.php b/src/Node/Expression/Binary/LessEqualBinary.php index 8f3653892d6..239d9fdfeae 100644 --- a/src/Node/Expression/Binary/LessEqualBinary.php +++ b/src/Node/Expression/Binary/LessEqualBinary.php @@ -12,8 +12,9 @@ namespace Twig\Node\Expression\Binary; use Twig\Compiler; +use Twig\Node\Expression\ReturnBoolInterface; -class LessEqualBinary extends AbstractBinary +class LessEqualBinary extends AbstractBinary implements ReturnBoolInterface { public function compile(Compiler $compiler): void { diff --git a/src/Node/Expression/Binary/MatchesBinary.php b/src/Node/Expression/Binary/MatchesBinary.php index 0a523c21611..32e8d34e45f 100644 --- a/src/Node/Expression/Binary/MatchesBinary.php +++ b/src/Node/Expression/Binary/MatchesBinary.php @@ -13,10 +13,11 @@ use Twig\Compiler; use Twig\Error\SyntaxError; +use Twig\Node\Expression\ReturnBoolInterface; use Twig\Node\Expression\ConstantExpression; use Twig\Node\Node; -class MatchesBinary extends AbstractBinary +class MatchesBinary extends AbstractBinary implements ReturnBoolInterface { public function __construct(Node $left, Node $right, int $lineno) { diff --git a/src/Node/Expression/Binary/ModBinary.php b/src/Node/Expression/Binary/ModBinary.php index 271b45cac88..aef48f3d0a2 100644 --- a/src/Node/Expression/Binary/ModBinary.php +++ b/src/Node/Expression/Binary/ModBinary.php @@ -13,8 +13,9 @@ namespace Twig\Node\Expression\Binary; use Twig\Compiler; +use Twig\Node\Expression\ReturnNumberInterface; -class ModBinary extends AbstractBinary +class ModBinary extends AbstractBinary implements ReturnNumberInterface { public function operator(Compiler $compiler): Compiler { diff --git a/src/Node/Expression/Binary/MulBinary.php b/src/Node/Expression/Binary/MulBinary.php index 6d4c1e0b6b8..beb881ae38d 100644 --- a/src/Node/Expression/Binary/MulBinary.php +++ b/src/Node/Expression/Binary/MulBinary.php @@ -13,8 +13,9 @@ namespace Twig\Node\Expression\Binary; use Twig\Compiler; +use Twig\Node\Expression\ReturnNumberInterface; -class MulBinary extends AbstractBinary +class MulBinary extends AbstractBinary implements ReturnNumberInterface { public function operator(Compiler $compiler): Compiler { diff --git a/src/Node/Expression/Binary/NotEqualBinary.php b/src/Node/Expression/Binary/NotEqualBinary.php index d137ef62738..fd24ef9115e 100644 --- a/src/Node/Expression/Binary/NotEqualBinary.php +++ b/src/Node/Expression/Binary/NotEqualBinary.php @@ -12,8 +12,9 @@ namespace Twig\Node\Expression\Binary; use Twig\Compiler; +use Twig\Node\Expression\ReturnBoolInterface; -class NotEqualBinary extends AbstractBinary +class NotEqualBinary extends AbstractBinary implements ReturnBoolInterface { public function compile(Compiler $compiler): void { diff --git a/src/Node/Expression/Binary/NotInBinary.php b/src/Node/Expression/Binary/NotInBinary.php index 80c8755d8aa..9fd27311fec 100644 --- a/src/Node/Expression/Binary/NotInBinary.php +++ b/src/Node/Expression/Binary/NotInBinary.php @@ -12,8 +12,9 @@ namespace Twig\Node\Expression\Binary; use Twig\Compiler; +use Twig\Node\Expression\ReturnBoolInterface; -class NotInBinary extends AbstractBinary +class NotInBinary extends AbstractBinary implements ReturnBoolInterface { public function compile(Compiler $compiler): void { diff --git a/src/Node/Expression/Binary/OrBinary.php b/src/Node/Expression/Binary/OrBinary.php index 21f87c91b4f..82dcb7e953b 100644 --- a/src/Node/Expression/Binary/OrBinary.php +++ b/src/Node/Expression/Binary/OrBinary.php @@ -13,8 +13,9 @@ namespace Twig\Node\Expression\Binary; use Twig\Compiler; +use Twig\Node\Expression\ReturnBoolInterface; -class OrBinary extends AbstractBinary +class OrBinary extends AbstractBinary implements ReturnBoolInterface { public function operator(Compiler $compiler): Compiler { diff --git a/src/Node/Expression/Binary/PowerBinary.php b/src/Node/Expression/Binary/PowerBinary.php index c9f4c6697df..5325e8eb0b2 100644 --- a/src/Node/Expression/Binary/PowerBinary.php +++ b/src/Node/Expression/Binary/PowerBinary.php @@ -12,8 +12,9 @@ namespace Twig\Node\Expression\Binary; use Twig\Compiler; +use Twig\Node\Expression\ReturnNumberInterface; -class PowerBinary extends AbstractBinary +class PowerBinary extends AbstractBinary implements ReturnNumberInterface { public function operator(Compiler $compiler): Compiler { diff --git a/src/Node/Expression/Binary/RangeBinary.php b/src/Node/Expression/Binary/RangeBinary.php index 55982c819d7..f318d8e5548 100644 --- a/src/Node/Expression/Binary/RangeBinary.php +++ b/src/Node/Expression/Binary/RangeBinary.php @@ -12,8 +12,9 @@ namespace Twig\Node\Expression\Binary; use Twig\Compiler; +use Twig\Node\Expression\ReturnArrayInterface; -class RangeBinary extends AbstractBinary +class RangeBinary extends AbstractBinary implements ReturnArrayInterface { public function compile(Compiler $compiler): void { diff --git a/src/Node/Expression/Binary/SpaceshipBinary.php b/src/Node/Expression/Binary/SpaceshipBinary.php index ae5a4a49373..c0a28b0a88f 100644 --- a/src/Node/Expression/Binary/SpaceshipBinary.php +++ b/src/Node/Expression/Binary/SpaceshipBinary.php @@ -12,8 +12,9 @@ namespace Twig\Node\Expression\Binary; use Twig\Compiler; +use Twig\Node\Expression\ReturnNumberInterface; -class SpaceshipBinary extends AbstractBinary +class SpaceshipBinary extends AbstractBinary implements ReturnNumberInterface { public function operator(Compiler $compiler): Compiler { diff --git a/src/Node/Expression/Binary/StartsWithBinary.php b/src/Node/Expression/Binary/StartsWithBinary.php index 4519f30d9fb..ef2fc950242 100644 --- a/src/Node/Expression/Binary/StartsWithBinary.php +++ b/src/Node/Expression/Binary/StartsWithBinary.php @@ -12,8 +12,9 @@ namespace Twig\Node\Expression\Binary; use Twig\Compiler; +use Twig\Node\Expression\ReturnBoolInterface; -class StartsWithBinary extends AbstractBinary +class StartsWithBinary extends AbstractBinary implements ReturnBoolInterface { public function compile(Compiler $compiler): void { diff --git a/src/Node/Expression/Binary/SubBinary.php b/src/Node/Expression/Binary/SubBinary.php index eeb87faca96..10663f5c1ef 100644 --- a/src/Node/Expression/Binary/SubBinary.php +++ b/src/Node/Expression/Binary/SubBinary.php @@ -13,8 +13,9 @@ namespace Twig\Node\Expression\Binary; use Twig\Compiler; +use Twig\Node\Expression\ReturnNumberInterface; -class SubBinary extends AbstractBinary +class SubBinary extends AbstractBinary implements ReturnNumberInterface { public function operator(Compiler $compiler): Compiler { diff --git a/src/Node/Expression/Binary/XorBinary.php b/src/Node/Expression/Binary/XorBinary.php index d8ccd785362..6f412d22fe2 100644 --- a/src/Node/Expression/Binary/XorBinary.php +++ b/src/Node/Expression/Binary/XorBinary.php @@ -13,8 +13,9 @@ namespace Twig\Node\Expression\Binary; use Twig\Compiler; +use Twig\Node\Expression\ReturnBoolInterface; -class XorBinary extends AbstractBinary +class XorBinary extends AbstractBinary implements ReturnBoolInterface { public function operator(Compiler $compiler): Compiler { diff --git a/src/Node/Expression/ConstantExpression.php b/src/Node/Expression/ConstantExpression.php index 8f8b2ffe96b..12dc0621fea 100644 --- a/src/Node/Expression/ConstantExpression.php +++ b/src/Node/Expression/ConstantExpression.php @@ -17,7 +17,7 @@ /** * @final */ -class ConstantExpression extends AbstractExpression implements SupportDefinedTestInterface +class ConstantExpression extends AbstractExpression implements SupportDefinedTestInterface, ReturnPrimitiveTypeInterface { use SupportDefinedTestTrait; diff --git a/src/Node/Expression/ReturnArrayInterface.php b/src/Node/Expression/ReturnArrayInterface.php new file mode 100644 index 00000000000..a74864b5dbf --- /dev/null +++ b/src/Node/Expression/ReturnArrayInterface.php @@ -0,0 +1,16 @@ +getTemplateLine()); + } + parent::__construct(['test' => $test, 'left' => $left, 'right' => $right], [], $lineno); } diff --git a/src/Node/Expression/Test/NullTest.php b/src/Node/Expression/Test/NullTest.php index 45b54ae3709..be5d3889199 100644 --- a/src/Node/Expression/Test/NullTest.php +++ b/src/Node/Expression/Test/NullTest.php @@ -15,7 +15,7 @@ use Twig\Node\Expression\TestExpression; /** - * Checks that a variable is null. + * Checks that an expression is null. * * {{ var is none }} * diff --git a/src/Node/Expression/Test/TrueTest.php b/src/Node/Expression/Test/TrueTest.php new file mode 100644 index 00000000000..22186a6898a --- /dev/null +++ b/src/Node/Expression/Test/TrueTest.php @@ -0,0 +1,34 @@ + + */ +class TrueTest extends TestExpression +{ + public function compile(Compiler $compiler): void + { + $compiler + ->raw('(($tmp = ') + ->subcompile($this->getNode('node')) + ->raw(') && $tmp instanceof Markup ? (string) $tmp : $tmp)') + ; + } +} diff --git a/src/Node/Expression/TestExpression.php b/src/Node/Expression/TestExpression.php index 27e1526a187..3b51dd320d5 100644 --- a/src/Node/Expression/TestExpression.php +++ b/src/Node/Expression/TestExpression.php @@ -17,7 +17,7 @@ use Twig\Node\Node; use Twig\TwigTest; -class TestExpression extends CallExpression +class TestExpression extends CallExpression implements ReturnBoolInterface { #[FirstClassTwigCallableReady] /** diff --git a/src/Node/IfNode.php b/src/Node/IfNode.php index 2af48fa8159..2c0e2a8e91b 100644 --- a/src/Node/IfNode.php +++ b/src/Node/IfNode.php @@ -14,6 +14,9 @@ use Twig\Attribute\YieldReady; use Twig\Compiler; +use Twig\Node\Expression\ReturnPrimitiveTypeInterface; +use Twig\Node\Expression\Test\TrueTest; +use Twig\TwigTest; /** * Represents an if node. @@ -25,6 +28,12 @@ class IfNode extends Node { public function __construct(Node $tests, ?Node $else, int $lineno) { + for ($i = 0, $count = \count($tests); $i < $count; $i += 2) { + $test = $tests->getNode((string) $i); + if (!$test instanceof ReturnPrimitiveTypeInterface) { + $tests->setNode($i, new TrueTest($test, new TwigTest('true'), null, $test->getTemplateLine())); + } + } $nodes = ['tests' => $tests]; if (null !== $else) { $nodes['else'] = $else; diff --git a/tests/Fixtures/regression/markup_test.test b/tests/Fixtures/regression/markup_test.test new file mode 100644 index 00000000000..e03c5291b91 --- /dev/null +++ b/tests/Fixtures/regression/markup_test.test @@ -0,0 +1,18 @@ +--TEST-- +Twig outputs 0 nodes correctly +--TEMPLATE-- +{{ empty|trim ? 'KO' : 'ok' }} +{{ spaces|trim ? 'KO' : 'ok' }} +{% if empty %}KO{% else %}ok{% endif %} + +{% if spaces|trim %}KO{% else %}ok{% endif %} + +{% set bar %} {% endset %}{{ bar|trim ? 'KO' : 'ok' }} +--DATA-- +return ['spaces' => new Twig\Markup(' ', 'UTF-8'), 'empty' => new Twig\Markup('', 'UTF-8')] +--EXPECT-- +ok +ok +ok +ok +ok From d7702840da089b551717f4a54641435964586bcb Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Fri, 14 Feb 2025 16:18:00 +0100 Subject: [PATCH 742/812] Remove $templateName from Template::loadTemplate() --- src/Node/EmbedNode.php | 4 +- .../Expression/BlockReferenceExpression.php | 4 +- src/Node/ImportNode.php | 4 +- src/Node/IncludeNode.php | 4 +- src/Node/ModuleNode.php | 12 +--- src/Template.php | 40 ++++++++---- tests/Fixtures/templates/include.twig | 4 ++ tests/Fixtures/templates/index.twig | 1 + tests/Node/ExtendsTest.php | 65 +++++++++++++++++++ tests/Node/ImportTest.php | 2 +- tests/Node/IncludeTest.php | 10 +-- tests/Node/ModuleTest.php | 6 +- 12 files changed, 114 insertions(+), 42 deletions(-) create mode 100644 tests/Fixtures/templates/include.twig create mode 100644 tests/Fixtures/templates/index.twig create mode 100644 tests/Node/ExtendsTest.php diff --git a/src/Node/EmbedNode.php b/src/Node/EmbedNode.php index 597f95e4413..fe4365b5710 100644 --- a/src/Node/EmbedNode.php +++ b/src/Node/EmbedNode.php @@ -36,11 +36,9 @@ public function __construct(string $name, int $index, ?AbstractExpression $varia protected function addGetTemplate(Compiler $compiler, string $template = ''): void { $compiler - ->raw('$this->loadTemplate(') + ->raw('$this->load(') ->string($this->getAttribute('name')) ->raw(', ') - ->repr($this->getTemplateName()) - ->raw(', ') ->repr($this->getTemplateLine()) ->raw(', ') ->string($this->getAttribute('index')) diff --git a/src/Node/Expression/BlockReferenceExpression.php b/src/Node/Expression/BlockReferenceExpression.php index 8fe3f8b1253..cb7d38c5755 100644 --- a/src/Node/Expression/BlockReferenceExpression.php +++ b/src/Node/Expression/BlockReferenceExpression.php @@ -66,11 +66,9 @@ private function compileTemplateCall(Compiler $compiler, string $method): Compil $compiler->write('$this'); } else { $compiler - ->write('$this->loadTemplate(') + ->write('$this->load(') ->subcompile($this->getNode('template')) ->raw(', ') - ->repr($this->getTemplateName()) - ->raw(', ') ->repr($this->getTemplateLine()) ->raw(')') ; diff --git a/src/Node/ImportNode.php b/src/Node/ImportNode.php index 77a9af93936..92bdd5ebf84 100644 --- a/src/Node/ImportNode.php +++ b/src/Node/ImportNode.php @@ -48,11 +48,9 @@ public function compile(Compiler $compiler): void $compiler->raw('$this'); } else { $compiler - ->raw('$this->loadTemplate(') + ->raw('$this->load(') ->subcompile($this->getNode('expr')) ->raw(', ') - ->repr($this->getTemplateName()) - ->raw(', ') ->repr($this->getTemplateLine()) ->raw(')->unwrap()') ; diff --git a/src/Node/IncludeNode.php b/src/Node/IncludeNode.php index 4761cb83243..6e17300f075 100644 --- a/src/Node/IncludeNode.php +++ b/src/Node/IncludeNode.php @@ -84,11 +84,9 @@ public function compile(Compiler $compiler): void protected function addGetTemplate(Compiler $compiler/* , string $template = '' */) { $compiler - ->raw('$this->loadTemplate(') + ->raw('$this->load(') ->subcompile($this->getNode('expr')) ->raw(', ') - ->repr($this->getTemplateName()) - ->raw(', ') ->repr($this->getTemplateLine()) ->raw(')') ; diff --git a/src/Node/ModuleNode.php b/src/Node/ModuleNode.php index b3f4e6c2af3..648cf955c6f 100644 --- a/src/Node/ModuleNode.php +++ b/src/Node/ModuleNode.php @@ -134,11 +134,9 @@ protected function compileGetParent(Compiler $compiler) $compiler->subcompile($parent); } else { $compiler - ->raw('$this->loadTemplate(') + ->raw('$this->load(') ->subcompile($parent) ->raw(', ') - ->repr($this->getSourceContext()->getName()) - ->raw(', ') ->repr($parent->getTemplateLine()) ->raw(')') ; @@ -218,11 +216,9 @@ protected function compileConstructor(Compiler $compiler) $compiler ->addDebugInfo($node) - ->write(\sprintf('$_trait_%s = $this->loadTemplate(', $i)) + ->write(\sprintf('$_trait_%s = $this->load(', $i)) ->subcompile($node) ->raw(', ') - ->repr($node->getTemplateName()) - ->raw(', ') ->repr($node->getTemplateLine()) ->raw(");\n") ->write(\sprintf("if (!\$_trait_%s->unwrap()->isTraitable()) {\n", $i)) @@ -353,11 +349,9 @@ protected function compileDisplay(Compiler $compiler) $compiler->addDebugInfo($parent); if ($parent instanceof ConstantExpression) { $compiler - ->write('$this->parent = $this->loadTemplate(') + ->write('$this->parent = $this->load(') ->subcompile($parent) ->raw(', ') - ->repr($this->getSourceContext()->getName()) - ->raw(', ') ->repr($parent->getTemplateLine()) ->raw(");\n") ; diff --git a/src/Template.php b/src/Template.php index 156752f8b37..4973aa754f3 100644 --- a/src/Template.php +++ b/src/Template.php @@ -89,7 +89,7 @@ public function getParent(array $context): self|TemplateWrapper|false } if (!isset($this->parents[$parent])) { - $this->parents[$parent] = $this->loadTemplate($parent); + $this->parents[$parent] = $this->load($parent, -1); } return $this->parents[$parent]; @@ -270,21 +270,15 @@ public function getBlockNames(array $context, array $blocks = []): array /** * @param string|TemplateWrapper|array $template */ - protected function loadTemplate($template, $templateName = null, $line = null, $index = null): self|TemplateWrapper + protected function load(string|TemplateWrapper|array $template, int $line, int|null $index = null): self { try { if (\is_array($template)) { - return $this->env->resolveTemplate($template); + return $this->env->resolveTemplate($template)->unwrap(); } if ($template instanceof TemplateWrapper) { - return $template; - } - - if ($template instanceof self) { - trigger_deprecation('twig/twig', '3.9', 'Passing a "%s" instance to "%s" is deprecated.', self::class, __METHOD__); - - return $template; + return $template->unwrap(); } if ($template === $this->getTemplateName()) { @@ -299,14 +293,14 @@ protected function loadTemplate($template, $templateName = null, $line = null, $ return $this->env->loadTemplate($class, $template, $index); } catch (Error $e) { if (!$e->getSourceContext()) { - $e->setSourceContext($templateName ? new Source('', $templateName) : $this->getSourceContext()); + $e->setSourceContext($this->getSourceContext()); } if ($e->getTemplateLine() > 0) { throw $e; } - if (!$line) { + if (-1 === $line) { $e->guess(); } else { $e->setTemplateLine($line); @@ -316,6 +310,28 @@ protected function loadTemplate($template, $templateName = null, $line = null, $ } } + /** + * @param string|TemplateWrapper|array $template + */ + protected function loadTemplate($template, $templateName = null, int|null $line = null, int|null $index = null): self|TemplateWrapper + { + trigger_deprecation('twig/twig', '3.21', 'The "%s" method is deprecated.', __METHOD__); + + if (null === $line) { + trigger_deprecation('twig/twig', '3.21', 'Passing a "null" line number to "%s" is deprecated.', __METHOD__); + + $line = -1; + } + + if ($template instanceof self) { + trigger_deprecation('twig/twig', '3.9', 'Passing a "%s" instance to "%s" is deprecated.', self::class, __METHOD__); + + return $template; + } + + return $this->load($template, $line, $index); + } + /** * @internal * diff --git a/tests/Fixtures/templates/include.twig b/tests/Fixtures/templates/include.twig new file mode 100644 index 00000000000..597c9f063de --- /dev/null +++ b/tests/Fixtures/templates/include.twig @@ -0,0 +1,4 @@ + + + +{% extends 'invalid.twig' %} diff --git a/tests/Fixtures/templates/index.twig b/tests/Fixtures/templates/index.twig new file mode 100644 index 00000000000..a15e87d18f0 --- /dev/null +++ b/tests/Fixtures/templates/index.twig @@ -0,0 +1 @@ +{% include "include.twig" %} diff --git a/tests/Node/ExtendsTest.php b/tests/Node/ExtendsTest.php new file mode 100644 index 00000000000..a47302d4086 --- /dev/null +++ b/tests/Node/ExtendsTest.php @@ -0,0 +1,65 @@ + '{% include "include.twig" %}', + 'include.twig' => $include = << true]); + try { + $twig->render('index.twig'); + $this->fail('Expected LoaderError to be thrown'); + } catch (LoaderError $e) { + $this->assertSame('Template "invalid.twig" is not defined.', $e->getRawMessage()); + $this->assertSame(4, $e->getTemplateLine()); + $this->assertSame('include.twig', $e->getSourceContext()->getName()); + $this->assertSame($include, $e->getSourceContext()->getCode()); + } + } + + public function testErrorFromFilesystemLoader() + { + $twig = new Environment(new FilesystemLoader([ + $dir = dirname(__DIR__).'/Fixtures/templates', + ]), ['debug' => true]); + $include = file_get_contents($dir.'/include.twig'); + try { + $twig->render('index.twig'); + $this->fail('Expected LoaderError to be thrown'); + } catch (LoaderError $e) { + $this->assertStringContainsString('Unable to find template "invalid.twig"', $e->getRawMessage()); + $this->assertSame(4, $e->getTemplateLine()); + $this->assertSame('include.twig', $e->getSourceContext()->getName()); + $this->assertSame($include, $e->getSourceContext()->getCode()); + } + } + + public static function provideTests(): iterable + { + return []; + } +} diff --git a/tests/Node/ImportTest.php b/tests/Node/ImportTest.php index a55137063cc..fb8dd9ba410 100644 --- a/tests/Node/ImportTest.php +++ b/tests/Node/ImportTest.php @@ -37,7 +37,7 @@ public static function provideTests(): iterable $tests[] = [$node, <<macros["macro"] = \$this->loadTemplate("foo.twig", null, 1)->unwrap(); +\$macros["macro"] = \$this->macros["macro"] = \$this->load("foo.twig", 1)->unwrap(); EOF ]; diff --git a/tests/Node/IncludeTest.php b/tests/Node/IncludeTest.php index 8a73a76cec5..ea80979bf0c 100644 --- a/tests/Node/IncludeTest.php +++ b/tests/Node/IncludeTest.php @@ -42,7 +42,7 @@ public static function provideTests(): iterable $node = new IncludeNode($expr, null, false, false, 1); $tests[] = [$node, <<<'EOF' // line 1 -yield from $this->loadTemplate("foo.twig", null, 1)->unwrap()->yield($context); +yield from $this->load("foo.twig", 1)->unwrap()->yield($context); EOF ]; @@ -55,7 +55,7 @@ public static function provideTests(): iterable $node = new IncludeNode($expr, null, false, false, 1); $tests[] = [$node, <<<'EOF' // line 1 -yield from $this->loadTemplate(((true) ? ("foo") : ("foo")), null, 1)->unwrap()->yield($context); +yield from $this->load(((true) ? ("foo") : ("foo")), 1)->unwrap()->yield($context); EOF ]; @@ -64,14 +64,14 @@ public static function provideTests(): iterable $node = new IncludeNode($expr, $vars, false, false, 1); $tests[] = [$node, <<<'EOF' // line 1 -yield from $this->loadTemplate("foo.twig", null, 1)->unwrap()->yield(CoreExtension::merge($context, ["foo" => true])); +yield from $this->load("foo.twig", 1)->unwrap()->yield(CoreExtension::merge($context, ["foo" => true])); EOF ]; $node = new IncludeNode($expr, $vars, true, false, 1); $tests[] = [$node, <<<'EOF' // line 1 -yield from $this->loadTemplate("foo.twig", null, 1)->unwrap()->yield(CoreExtension::toArray(["foo" => true])); +yield from $this->load("foo.twig", 1)->unwrap()->yield(CoreExtension::toArray(["foo" => true])); EOF ]; @@ -79,7 +79,7 @@ public static function provideTests(): iterable $tests[] = [$node, <<loadTemplate("foo.twig", null, 1); + \$_v%s = \$this->load("foo.twig", 1); } catch (LoaderError \$e) { // ignore missing template \$_v%s = null; diff --git a/tests/Node/ModuleTest.php b/tests/Node/ModuleTest.php index 5caddd93b42..a2032ad8c22 100644 --- a/tests/Node/ModuleTest.php +++ b/tests/Node/ModuleTest.php @@ -183,9 +183,9 @@ protected function doDisplay(array \$context, array \$blocks = []): iterable { \$macros = \$this->macros; // line 2 - \$macros["macro"] = \$this->macros["macro"] = \$this->loadTemplate("foo.twig", "foo.twig", 2)->unwrap(); + \$macros["macro"] = \$this->macros["macro"] = \$this->load("foo.twig", 2)->unwrap(); // line 1 - \$this->parent = \$this->loadTemplate("layout.twig", "foo.twig", 1); + \$this->parent = \$this->load("layout.twig", 1); yield from \$this->parent->unwrap()->yield(\$context, array_merge(\$this->blocks, \$blocks)); } @@ -271,7 +271,7 @@ public function __construct(Environment \$env) protected function doGetParent(array \$context): bool|string|Template|TemplateWrapper { // line 2 - return \$this->loadTemplate(((true) ? ("foo") : ("foo")), "foo.twig", 2); + return \$this->load(((true) ? ("foo") : ("foo")), 2); } protected function doDisplay(array \$context, array \$blocks = []): iterable From 68481a2a3a79c5ac3ac71712702747dae44d09ac Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sat, 22 Feb 2025 00:00:19 +0100 Subject: [PATCH 743/812] Simplify code --- src/Error/Error.php | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/Error/Error.php b/src/Error/Error.php index 31924d942c5..c726fb6dc73 100644 --- a/src/Error/Error.php +++ b/src/Error/Error.php @@ -90,6 +90,10 @@ public function setSourceContext(?Source $source = null): void public function guess(): void { + if ($this->lineno > -1) { + return; + } + $this->guessTemplateInfo(); $this->updateRepr(); } @@ -164,12 +168,6 @@ private function guessTemplateInfo(): void } } - if ($template) { - $this->source = $template->getSourceContext(); - } elseif ($this->lineno > -1) { - return; - } - $r = new \ReflectionObject($template); $file = $r->getFileName(); From 6a651ccfdc5c80a572496e69f2c74cdeaf082c18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Andr=C3=A9?= Date: Sat, 22 Feb 2025 04:07:17 +0100 Subject: [PATCH 744/812] Simplify Error --- src/Error/Error.php | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/Error/Error.php b/src/Error/Error.php index 31924d942c5..4dcbe295e64 100644 --- a/src/Error/Error.php +++ b/src/Error/Error.php @@ -124,16 +124,10 @@ private function updateRepr(): void } if ($this->source && $this->source->getName()) { - $name = $this->source->getName(); - if (\is_string($name) || $name instanceof \Stringable) { - $name = \sprintf('"%s"', $name); - } else { - $name = json_encode($name); - } - $this->message .= \sprintf(' in %s', $name); + $this->message .= \sprintf(' in "%s"', $this->source->getName()); } - if ($this->lineno && $this->lineno >= 0) { + if ($this->lineno > 0) { $this->message .= \sprintf(' at line %d', $this->lineno); } From d5dc3eee99bdc9af8a45dfb65cdd42fb1a443e96 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sat, 22 Feb 2025 10:22:21 +0100 Subject: [PATCH 745/812] Improve error reporting --- src/Error/Error.php | 31 ++++++++----------------------- tests/ErrorTest.php | 4 ++-- 2 files changed, 10 insertions(+), 25 deletions(-) diff --git a/src/Error/Error.php b/src/Error/Error.php index a21b0c479e6..08b78b1ba01 100644 --- a/src/Error/Error.php +++ b/src/Error/Error.php @@ -106,41 +106,26 @@ public function appendMessage($rawMessage): void private function updateRepr(): void { - $this->message = $this->rawMessage; - - if ($this->source && $this->source->getPath() && $this->lineno > 0) { - $this->file = $this->source->getPath(); + if ($this->lineno > 0) { $this->line = $this->lineno; - - return; } - - $dot = false; - if (str_ends_with($this->message, '.')) { - $this->message = substr($this->message, 0, -1); - $dot = true; + if ($this->source && $this->source->getPath()) { + $this->file = $this->source->getPath(); } - $questionMark = false; - if (str_ends_with($this->message, '?')) { + $this->message = $this->rawMessage; + $last = substr($this->message, -1); + if ($punctuation = '.' === $last || '?' === $last ? $last : '') { $this->message = substr($this->message, 0, -1); - $questionMark = true; } - if ($this->source && $this->source->getName()) { $this->message .= \sprintf(' in "%s"', $this->source->getName()); } - if ($this->lineno > 0) { $this->message .= \sprintf(' at line %d', $this->lineno); } - - if ($dot) { - $this->message .= '.'; - } - - if ($questionMark) { - $this->message .= '?'; + if ($punctuation) { + $this->message .= $punctuation; } } diff --git a/tests/ErrorTest.php b/tests/ErrorTest.php index b7da2d50518..e52dfbfa932 100644 --- a/tests/ErrorTest.php +++ b/tests/ErrorTest.php @@ -97,7 +97,7 @@ public function testTwigExceptionGuessWithMissingVarAndFilesystemLoader() $this->fail(); } catch (RuntimeError $e) { - $this->assertEquals('Variable "foo" does not exist.', $e->getMessage()); + $this->assertEquals('Variable "foo" does not exist in "index.html" at line 3.', $e->getMessage()); $this->assertEquals(3, $e->getTemplateLine()); $this->assertEquals('index.html', $e->getSourceContext()->getName()); $this->assertEquals(3, $e->getLine()); @@ -116,7 +116,7 @@ public function testTwigExceptionGuessWithExceptionAndFilesystemLoader() $this->fail(); } catch (RuntimeError $e) { - $this->assertEquals('An exception has been thrown during the rendering of a template ("Runtime error...").', $e->getMessage()); + $this->assertEquals('An exception has been thrown during the rendering of a template ("Runtime error...") in "index.html" at line 3.', $e->getMessage()); $this->assertEquals(3, $e->getTemplateLine()); $this->assertEquals('index.html', $e->getSourceContext()->getName()); $this->assertEquals(3, $e->getLine()); From 4a121d90c69550a8718fb1b923a9e9da66d6b527 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Wed, 26 Feb 2025 13:11:07 +0100 Subject: [PATCH 746/812] Sync Error file and line --- src/Error/Error.php | 7 ++++--- tests/ErrorTest.php | 19 +++++++++++++++++++ 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/src/Error/Error.php b/src/Error/Error.php index 08b78b1ba01..8cbc95b67ca 100644 --- a/src/Error/Error.php +++ b/src/Error/Error.php @@ -106,11 +106,12 @@ public function appendMessage($rawMessage): void private function updateRepr(): void { - if ($this->lineno > 0) { - $this->line = $this->lineno; - } if ($this->source && $this->source->getPath()) { + // we only update the file and the line together $this->file = $this->source->getPath(); + if ($this->lineno > 0) { + $this->line = $this->lineno; + } } $this->message = $this->rawMessage; diff --git a/tests/ErrorTest.php b/tests/ErrorTest.php index e52dfbfa932..b0a62694029 100644 --- a/tests/ErrorTest.php +++ b/tests/ErrorTest.php @@ -15,6 +15,7 @@ use Twig\Environment; use Twig\Error\Error; use Twig\Error\RuntimeError; +use Twig\Error\SyntaxError; use Twig\Loader\ArrayLoader; use Twig\Loader\FilesystemLoader; use Twig\Source; @@ -234,6 +235,24 @@ public function testTwigArrayReduceThrowsRuntimeExceptions() } } + public function testTwigExceptionUpdateFileAndLineTogether() + { + $twig = new Environment(new ArrayLoader([ + 'index' => "\n\n\n\n{{ foo() }}", + ]), ['debug' => true, 'cache' => false]); + + try { + $twig->load('index')->render([]); + } catch (SyntaxError $e) { + $this->assertSame('Unknown "foo" function in "index" at line 5.', $e->getMessage()); + $this->assertSame(5, $e->getTemplateLine()); + // as we are using an ArrayLoader, we don't have a file, so the line should not be the template line, + // but the line of the error in the Parser.php file + $this->assertStringContainsString('Parser.php', $e->getFile()); + $this->assertNotSame(5, $e->getLine()); + } + } + public static function getErroredTemplates() { return [ From 7b43bd0fd79eb92a0e1bc012a1889c78089d5f0d Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Wed, 26 Feb 2025 15:09:38 +0100 Subject: [PATCH 747/812] Fix Error when the trace has Twig file/line information instead of the original PHP info --- src/Error/Error.php | 12 +- tests/ErrorTest.php | 116 ++++++++++++++++++ .../errors/no_line_and_context_exception.twig | 3 + ..._and_context_exception_include_line_1.twig | 1 + ..._and_context_exception_include_line_5.twig | 5 + 5 files changed, 135 insertions(+), 2 deletions(-) create mode 100644 tests/Fixtures/errors/no_line_and_context_exception.twig create mode 100644 tests/Fixtures/errors/no_line_and_context_exception_include_line_1.twig create mode 100644 tests/Fixtures/errors/no_line_and_context_exception_include_line_5.twig diff --git a/src/Error/Error.php b/src/Error/Error.php index 8cbc95b67ca..6c317bca651 100644 --- a/src/Error/Error.php +++ b/src/Error/Error.php @@ -41,6 +41,8 @@ class Error extends \Exception private $lineno; private $rawMessage; private ?Source $source; + private string $phpFile; + private int $phpLine; /** * Constructor. @@ -55,6 +57,8 @@ public function __construct(string $message, int $lineno = -1, ?Source $source = { parent::__construct('', 0, $previous); + $this->phpFile = $this->getFile(); + $this->phpLine = $this->getLine(); $this->lineno = $lineno; $this->source = $source; $this->rawMessage = $message; @@ -111,6 +115,8 @@ private function updateRepr(): void $this->file = $this->source->getPath(); if ($this->lineno > 0) { $this->line = $this->lineno; + } else { + $this->line = -1; } } @@ -134,6 +140,7 @@ private function guessTemplateInfo(): void { // $this->source is never null here (see guess() usage in Template) + $this->lineno = 0; $template = null; $templateClass = null; $backtrace = debug_backtrace(\DEBUG_BACKTRACE_IGNORE_ARGS | \DEBUG_BACKTRACE_PROVIDE_OBJECT); @@ -144,6 +151,8 @@ private function guessTemplateInfo(): void if ($this->source->getName() === $trace['object']->getTemplateName() && !$isEmbedContainer) { $template = $trace['object']; $templateClass = \get_class($trace['object']); + + break; } } } @@ -158,8 +167,7 @@ private function guessTemplateInfo(): void while ($e = array_pop($exceptions)) { $traces = $e->getTrace(); - array_unshift($traces, ['file' => $e->getFile(), 'line' => $e->getLine()]); - + array_unshift($traces, ['file' => $e instanceof Error ? $e->phpFile : $e->getFile(), 'line' => $e instanceof Error ? $e->phpLine : $e->getLine()]); while ($trace = array_shift($traces)) { if (!isset($trace['file']) || !isset($trace['line']) || $file != $trace['file']) { continue; diff --git a/tests/ErrorTest.php b/tests/ErrorTest.php index b0a62694029..79103445e6d 100644 --- a/tests/ErrorTest.php +++ b/tests/ErrorTest.php @@ -12,13 +12,19 @@ */ use PHPUnit\Framework\TestCase; +use Twig\Attribute\YieldReady; +use Twig\Compiler; use Twig\Environment; use Twig\Error\Error; use Twig\Error\RuntimeError; use Twig\Error\SyntaxError; use Twig\Loader\ArrayLoader; use Twig\Loader\FilesystemLoader; +use Twig\Loader\LoaderInterface; +use Twig\Node\Node; use Twig\Source; +use Twig\Token; +use Twig\TokenParser\AbstractTokenParser; class ErrorTest extends TestCase { @@ -253,6 +259,116 @@ public function testTwigExceptionUpdateFileAndLineTogether() } } + /** + * @dataProvider getErrorWithoutLineAndContextData + */ + public function testErrorWithoutLineAndContext(LoaderInterface $loader, bool $debug, bool $addDebugInfo, bool $exceptionWithLineAndContext, int $errorLine) + { + $twig = new Environment($loader, ['debug' => $debug, 'cache' => false]); + $twig->removeCache('no_line_and_context_exception.twig'); + $twig->removeCache('no_line_and_context_exception_include_line_5.twig'); + $twig->removeCache('no_line_and_context_exception_include_line_1.twig'); + $twig->addTokenParser(new class($addDebugInfo, $exceptionWithLineAndContext) extends AbstractTokenParser { + public function __construct(private bool $addDebugInfo, private bool $exceptionWithLineAndContext) + { + } + + public function parse(Token $token) + { + $stream = $this->parser->getStream(); + $lineno = $stream->getCurrent()->getLine(); + $stream->expect(Token::BLOCK_END_TYPE); + + return new #[YieldReady]class($lineno, $this->addDebugInfo, $this->exceptionWithLineAndContext) extends Node + { + public function __construct(int $lineno, private bool $addDebugInfo, private bool $exceptionWithLineAndContext) + { + parent::__construct([], [], $lineno); + } + + public function compile(Compiler $compiler): void + { + if ($this->addDebugInfo) { + $compiler->addDebugInfo($this); + } + if ($this->exceptionWithLineAndContext) { + $compiler + ->write('throw new \Twig\Error\RuntimeError("Runtime error.", ') + ->repr($this->lineno)->raw(", \$this->getSourceContext()") + ->raw(");\n") + ; + } else { + $compiler->write('throw new \Twig\Error\RuntimeError("Runtime error.");'); + } + } + }; + } + + public function getTag() + { + return 'foo'; + } + }); + + try { + $twig->render('no_line_and_context_exception.twig', ['line' => $errorLine]); + $this->fail(); + } catch (RuntimeError $e) { + if (1 === $errorLine && !$addDebugInfo && !$exceptionWithLineAndContext) { + // When the template only has the custom node that throws the error, we cannot find the line of the error + // as we have no debug info and no line and context in the exception + $this->assertSame(\sprintf('Runtime error in "no_line_and_context_exception_include_line_%d.twig".', $errorLine), $e->getMessage()); + $this->assertSame(0, $e->getTemplateLine()); + } else { + // When the template has some space before the custom node, the associated TextNode outputs some debug info at line 1 + // that's why the line is 1 when we have no debug info and no line and context in the exception + $line = $addDebugInfo || $exceptionWithLineAndContext ? $errorLine : 1; + $this->assertSame(\sprintf('Runtime error in "no_line_and_context_exception_include_line_%d.twig" at line %d.', $errorLine, $line), $e->getMessage()); + $this->assertSame($line, $e->getTemplateLine()); + } + + $line = $addDebugInfo || $exceptionWithLineAndContext ? $errorLine : 1; + if ($loader instanceof FilesystemLoader) { + $this->assertStringContainsString(\sprintf('errors/no_line_and_context_exception_include_line_%d.twig', $errorLine), $e->getFile()); + $line = $addDebugInfo || $exceptionWithLineAndContext ? $errorLine : (1 === $errorLine ? -1 : 1); + $this->assertSame($line, $e->getLine()); + } else { + $this->assertStringContainsString('Environment.php', $e->getFile()); + $this->assertNotSame($line, $e->getLine()); + } + } + } + + public static function getErrorWithoutLineAndContextData(): iterable + { + $fileLoaders = [ + new ArrayLoader([ + 'no_line_and_context_exception.twig' => "\n\n{{ include('no_line_and_context_exception_include_line_' ~ line ~ '.twig') }}", + 'no_line_and_context_exception_include_line_5.twig' => "\n\n\n\n{% foo %}", + 'no_line_and_context_exception_include_line_1.twig' => '{% foo %}', + ]), + new FilesystemLoader(__DIR__.'/Fixtures/errors'), + ]; + + foreach ($fileLoaders as $loader) { + foreach ([false, true] as $exceptionWithLineAndContext) { + foreach ([false, true] as $addDebugInfo) { + foreach ([false, true] as $debug) { + foreach ([5, 1] as $line) { + $name = ($loader instanceof FilesystemLoader ? 'filesystem' : 'array') + .($debug ? '_with_debug' : '_without_debug') + .($addDebugInfo ? '_with_debug_info' : '_without_debug_info') + .($exceptionWithLineAndContext ? '_with_context' : '_without_context') + .('_line_'.$line) + ; + yield $name => [$loader, $debug, $addDebugInfo, $exceptionWithLineAndContext, $line]; + } + } + } + } + } + } + public static function getErroredTemplates() { return [ diff --git a/tests/Fixtures/errors/no_line_and_context_exception.twig b/tests/Fixtures/errors/no_line_and_context_exception.twig new file mode 100644 index 00000000000..4059c7485a4 --- /dev/null +++ b/tests/Fixtures/errors/no_line_and_context_exception.twig @@ -0,0 +1,3 @@ + + +{{ include('no_line_and_context_exception_include_line_' ~ line ~ '.twig') }} diff --git a/tests/Fixtures/errors/no_line_and_context_exception_include_line_1.twig b/tests/Fixtures/errors/no_line_and_context_exception_include_line_1.twig new file mode 100644 index 00000000000..9a2e6ffa6aa --- /dev/null +++ b/tests/Fixtures/errors/no_line_and_context_exception_include_line_1.twig @@ -0,0 +1 @@ +{% foo %} diff --git a/tests/Fixtures/errors/no_line_and_context_exception_include_line_5.twig b/tests/Fixtures/errors/no_line_and_context_exception_include_line_5.twig new file mode 100644 index 00000000000..bb229015a1e --- /dev/null +++ b/tests/Fixtures/errors/no_line_and_context_exception_include_line_5.twig @@ -0,0 +1,5 @@ + + + + +{% foo %} From 56204e951a0cfd92be59710e21184c0b37a6b3b0 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Wed, 26 Feb 2025 22:08:43 +0100 Subject: [PATCH 748/812] Move some tests --- tests/ErrorTest.php | 39 +++++++++++++ tests/Fixtures/errors/extends/include.twig | 4 ++ tests/Fixtures/errors/extends/index.twig | 1 + tests/Node/ExtendsTest.php | 65 ---------------------- 4 files changed, 44 insertions(+), 65 deletions(-) create mode 100644 tests/Fixtures/errors/extends/include.twig create mode 100644 tests/Fixtures/errors/extends/index.twig delete mode 100644 tests/Node/ExtendsTest.php diff --git a/tests/ErrorTest.php b/tests/ErrorTest.php index 79103445e6d..db310cb4f61 100644 --- a/tests/ErrorTest.php +++ b/tests/ErrorTest.php @@ -16,6 +16,7 @@ use Twig\Compiler; use Twig\Environment; use Twig\Error\Error; +use Twig\Error\LoaderError; use Twig\Error\RuntimeError; use Twig\Error\SyntaxError; use Twig\Loader\ArrayLoader; @@ -417,6 +418,44 @@ public static function getErroredTemplates() ], ]; } + + public function testErrorFromArrayLoader() + { + $templates = [ + 'index.twig' => '{% include "include.twig" %}', + 'include.twig' => $include = << true, 'cache' => false]); + try { + $twig->render('index.twig'); + $this->fail('Expected LoaderError to be thrown'); + } catch (LoaderError $e) { + $this->assertSame('Template "invalid.twig" is not defined.', $e->getRawMessage()); + $this->assertSame(4, $e->getTemplateLine()); + $this->assertSame('include.twig', $e->getSourceContext()->getName()); + $this->assertSame($include, $e->getSourceContext()->getCode()); + } + } + + public function testErrorFromFilesystemLoader() + { + $twig = new Environment(new FilesystemLoader([$dir = __DIR__.'/Fixtures/errors/extends']), ['debug' => true, 'cache' => false]); + $include = file_get_contents($dir.'/include.twig'); + try { + $twig->render('index.twig'); + $this->fail('Expected LoaderError to be thrown'); + } catch (LoaderError $e) { + $this->assertStringContainsString('Unable to find template "invalid.twig"', $e->getRawMessage()); + $this->assertSame(4, $e->getTemplateLine()); + $this->assertSame('include.twig', $e->getSourceContext()->getName()); + $this->assertSame($include, $e->getSourceContext()->getCode()); + } + } } class ErrorTest_Foo diff --git a/tests/Fixtures/errors/extends/include.twig b/tests/Fixtures/errors/extends/include.twig new file mode 100644 index 00000000000..597c9f063de --- /dev/null +++ b/tests/Fixtures/errors/extends/include.twig @@ -0,0 +1,4 @@ + + + +{% extends 'invalid.twig' %} diff --git a/tests/Fixtures/errors/extends/index.twig b/tests/Fixtures/errors/extends/index.twig new file mode 100644 index 00000000000..a15e87d18f0 --- /dev/null +++ b/tests/Fixtures/errors/extends/index.twig @@ -0,0 +1 @@ +{% include "include.twig" %} diff --git a/tests/Node/ExtendsTest.php b/tests/Node/ExtendsTest.php deleted file mode 100644 index a47302d4086..00000000000 --- a/tests/Node/ExtendsTest.php +++ /dev/null @@ -1,65 +0,0 @@ - '{% include "include.twig" %}', - 'include.twig' => $include = << true]); - try { - $twig->render('index.twig'); - $this->fail('Expected LoaderError to be thrown'); - } catch (LoaderError $e) { - $this->assertSame('Template "invalid.twig" is not defined.', $e->getRawMessage()); - $this->assertSame(4, $e->getTemplateLine()); - $this->assertSame('include.twig', $e->getSourceContext()->getName()); - $this->assertSame($include, $e->getSourceContext()->getCode()); - } - } - - public function testErrorFromFilesystemLoader() - { - $twig = new Environment(new FilesystemLoader([ - $dir = dirname(__DIR__).'/Fixtures/templates', - ]), ['debug' => true]); - $include = file_get_contents($dir.'/include.twig'); - try { - $twig->render('index.twig'); - $this->fail('Expected LoaderError to be thrown'); - } catch (LoaderError $e) { - $this->assertStringContainsString('Unable to find template "invalid.twig"', $e->getRawMessage()); - $this->assertSame(4, $e->getTemplateLine()); - $this->assertSame('include.twig', $e->getSourceContext()->getName()); - $this->assertSame($include, $e->getSourceContext()->getCode()); - } - } - - public static function provideTests(): iterable - { - return []; - } -} From b86fd0a05940349da73d26a2934fcac352d4ccb7 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Wed, 26 Feb 2025 22:13:11 +0100 Subject: [PATCH 749/812] Remove obsolete files --- tests/Fixtures/templates/include.twig | 4 ---- tests/Fixtures/templates/index.twig | 1 - 2 files changed, 5 deletions(-) delete mode 100644 tests/Fixtures/templates/include.twig delete mode 100644 tests/Fixtures/templates/index.twig diff --git a/tests/Fixtures/templates/include.twig b/tests/Fixtures/templates/include.twig deleted file mode 100644 index 597c9f063de..00000000000 --- a/tests/Fixtures/templates/include.twig +++ /dev/null @@ -1,4 +0,0 @@ - - - -{% extends 'invalid.twig' %} diff --git a/tests/Fixtures/templates/index.twig b/tests/Fixtures/templates/index.twig deleted file mode 100644 index a15e87d18f0..00000000000 --- a/tests/Fixtures/templates/index.twig +++ /dev/null @@ -1 +0,0 @@ -{% include "include.twig" %} From 4e1fa3aa43de15a70ad028453f405cf09c564654 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Wed, 26 Feb 2025 22:19:38 +0100 Subject: [PATCH 750/812] Remove obsolete comment --- src/Error/Error.php | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/Error/Error.php b/src/Error/Error.php index 6c317bca651..d0d2ea36b86 100644 --- a/src/Error/Error.php +++ b/src/Error/Error.php @@ -29,10 +29,7 @@ * Whenever possible, you must set these information (original template name * and line number) yourself by passing them to the constructor. If some or all * these information are not available from where you throw the exception, then - * this class will guess them automatically (when the line number is set to -1 - * and/or the name is set to null). As this is a costly operation, this - * can be disabled by passing false for both the name and the line number - * when creating a new instance of this class. + * this class will guess them automatically. * * @author Fabien Potencier */ From 53facb74c19d98d06d70dd69283ba5a5466ffbaf Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Thu, 27 Feb 2025 09:41:30 +0100 Subject: [PATCH 751/812] remove not needed code --- src/Error/Error.php | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/Error/Error.php b/src/Error/Error.php index d0d2ea36b86..13e8eefad23 100644 --- a/src/Error/Error.php +++ b/src/Error/Error.php @@ -139,15 +139,11 @@ private function guessTemplateInfo(): void $this->lineno = 0; $template = null; - $templateClass = null; $backtrace = debug_backtrace(\DEBUG_BACKTRACE_IGNORE_ARGS | \DEBUG_BACKTRACE_PROVIDE_OBJECT); foreach ($backtrace as $trace) { if (isset($trace['object']) && $trace['object'] instanceof Template) { - $currentClass = \get_class($trace['object']); - $isEmbedContainer = null === $templateClass ? false : str_starts_with($templateClass, $currentClass); - if ($this->source->getName() === $trace['object']->getTemplateName() && !$isEmbedContainer) { + if ($this->source->getName() === $trace['object']->getTemplateName()) { $template = $trace['object']; - $templateClass = \get_class($trace['object']); break; } From 96dade342fe189f931420c5cb089af8a204d531f Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Thu, 27 Feb 2025 08:23:12 +0100 Subject: [PATCH 752/812] Improve docs on creating new tags --- doc/advanced.rst | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/doc/advanced.rst b/doc/advanced.rst index 43cdc69a8ae..432892aefe8 100644 --- a/doc/advanced.rst +++ b/doc/advanced.rst @@ -504,6 +504,7 @@ Now, let's see the actual code of this class:: public function parse(\Twig\Token $token) { $parser = $this->parser; + $lineno = $token->getLine(); $stream = $parser->getStream(); $name = $stream->expect(\Twig\Token::NAME_TYPE)->getValue(); @@ -511,7 +512,7 @@ Now, let's see the actual code of this class:: $value = $parser->getExpressionParser()->parseExpression(); $stream->expect(\Twig\Token::BLOCK_END_TYPE); - return new CustomSetNode($name, $value, $token->getLine()); + return new CustomSetNode($name, $value, $lineno); } public function getTag() @@ -546,6 +547,18 @@ from the token stream (``$this->parser->getStream()``): Parsing expressions is done by calling the ``parseExpression()`` like we did for the ``set`` tag. +When encountering a syntax error during parsing, throw an exception:: + + throw new SyntaxError('Some error message.', $stream->getCurrent()->getLine(), $stream->getSourceContext()); + +For better error reporting to the user, follow these recommendations: + + * Use ``\Twig\Error\SyntaxError``; + + * **Always** pass the line number of the node and the source context; + + * End the exception message with a dot. + .. tip:: Reading the existing ``TokenParser`` classes is the best way to learn all @@ -590,7 +603,8 @@ developer generate beautiful and readable PHP code: ``\Twig\Node\ForNode`` for a usage example). * ``addDebugInfo()``: Adds the line of the original template file related to - the current node as a comment. + the current node as a comment. It's highly recommended to call this method + when implementing custom nodes. * ``indent()``: Indents the generated code (see ``\Twig\Node\BlockNode`` for a usage example). @@ -598,6 +612,10 @@ developer generate beautiful and readable PHP code: * ``outdent()``: Outdents the generated code (see ``\Twig\Node\BlockNode`` for a usage example). +For structural nodes, always call ``addDebugInfo()`` early on in the +compilation process to improve error reporting to the user in case the code +would throw an exception. + .. _creating_extensions: Creating an Extension From 71f83ecd0e585f722b45fd50f9f8b3bdfac061f0 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Fri, 28 Feb 2025 07:30:52 +0100 Subject: [PATCH 753/812] Tweak Sandbox docs --- doc/sandbox.rst | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/doc/sandbox.rst b/doc/sandbox.rst index 6e689583b89..f2ba9e9d118 100644 --- a/doc/sandbox.rst +++ b/doc/sandbox.rst @@ -17,7 +17,7 @@ The sandbox security is managed by a policy instance, which must be passed to the ``SandboxExtension`` constructor. By default, Twig comes with one policy class: ``\Twig\Sandbox\SecurityPolicy``. -This class allows you to allow-list some tags, filters, functions, but also +This class allows you to allow-list some tags, filters, functions, and properties and methods on objects:: $tags = ['if']; @@ -31,11 +31,11 @@ properties and methods on objects:: $functions = ['range']; $policy = new \Twig\Sandbox\SecurityPolicy($tags, $filters, $methods, $properties, $functions); -With the previous configuration, the security policy will only allow usage of -the ``if`` tag, and the ``upper`` filter. Moreover, the templates will only be -able to call the ``getTitle()`` and ``getBody()`` methods on ``Article`` -objects, and the ``title`` and ``body`` public properties. Everything else -won't be allowed and will generate a ``\Twig\Sandbox\SecurityError`` exception. +With the above configuration, the security policy will only allow usage of the +``if`` tag, and the ``upper`` filter. Moreover, the templates will only be able +to call the ``getTitle()`` and ``getBody()`` methods on ``Article`` objects, +and the ``title`` and ``body`` public properties. Everything else won't be +allowed and will generate a ``\Twig\Sandbox\SecurityError`` exception. .. note:: @@ -66,7 +66,7 @@ function: You can sandbox all templates by passing ``true`` as the second argument of the extension constructor:: - $sandbox = new \Twig\Extension\SandboxExtension($policy, true); + $twig->addExtension(new \Twig\Extension\SandboxExtension($policy, true)); Accepting Callables Arguments ----------------------------- From da64c42dc22ef4dd5b6f718fc6269bfbb376c225 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Fri, 28 Feb 2025 08:52:34 +0100 Subject: [PATCH 754/812] Add tests --- tests/ErrorTest.php | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/tests/ErrorTest.php b/tests/ErrorTest.php index db310cb4f61..43ab90708fb 100644 --- a/tests/ErrorTest.php +++ b/tests/ErrorTest.php @@ -416,6 +416,31 @@ public static function getErroredTemplates() ], 'index', 3, ], + + // error occurs in an embed tag + [ + [ + 'index' => " + {% embed 'base' %} + {% endembed %}", + 'base' => '{% block foo %}{{ foo.bar }}{% endblock %}', + ], + 'base', 1, + ], + + // error occurs in an overridden block from an embed tag + [ + [ + 'index' => " + {% embed 'base' %} + {% block foo %} + {{ foo.bar }} + {% endblock %} + {% endembed %}", + 'base' => '{% block foo %}{% endblock %}', + ], + 'index', 4, + ], ]; } From 719bba9d1ec98f969187ae2a58084683a9786266 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Fri, 28 Feb 2025 08:54:29 +0100 Subject: [PATCH 755/812] Simplify code --- src/Error/Error.php | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/Error/Error.php b/src/Error/Error.php index 13e8eefad23..015085e32b6 100644 --- a/src/Error/Error.php +++ b/src/Error/Error.php @@ -141,12 +141,10 @@ private function guessTemplateInfo(): void $template = null; $backtrace = debug_backtrace(\DEBUG_BACKTRACE_IGNORE_ARGS | \DEBUG_BACKTRACE_PROVIDE_OBJECT); foreach ($backtrace as $trace) { - if (isset($trace['object']) && $trace['object'] instanceof Template) { - if ($this->source->getName() === $trace['object']->getTemplateName()) { - $template = $trace['object']; + if (isset($trace['object']) && $trace['object'] instanceof Template && $this->source->getName() === $trace['object']->getTemplateName()) { + $template = $trace['object']; - break; - } + break; } } From 3d760aec1c5c42fc804e7ac97be01b0c7ae27f2f Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Fri, 28 Feb 2025 08:48:51 +0100 Subject: [PATCH 756/812] Avoid polluting ModuleNode::toString() with embedded templates --- src/Node/ModuleNode.php | 5 +++++ src/Parser.php | 10 +++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/Node/ModuleNode.php b/src/Node/ModuleNode.php index 648cf955c6f..f5b4292f3dd 100644 --- a/src/Node/ModuleNode.php +++ b/src/Node/ModuleNode.php @@ -38,6 +38,11 @@ public function __construct(Node $body, ?AbstractExpression $parent, Node $block if (!$body instanceof BodyNode) { trigger_deprecation('twig/twig', '3.12', \sprintf('Not passing a "%s" instance as the "body" argument of the "%s" constructor is deprecated.', BodyNode::class, static::class)); } + if (!$embeddedTemplates instanceof Node) { + trigger_deprecation('twig/twig', '3.21', \sprintf('Not passing a "%s" instance as the "embedded_templates" argument of the "%s" constructor is deprecated.', Node::class, static::class)); + + $embeddedTemplates = new Nodes($embeddedTemplates); + } $nodes = [ 'body' => $body, diff --git a/src/Parser.php b/src/Parser.php index b40a9231748..1937b7e15b2 100644 --- a/src/Parser.php +++ b/src/Parser.php @@ -117,7 +117,15 @@ public function parse(TokenStream $stream, $test = null, bool $dropNeedle = fals $this->expressionRefs = null; } - $node = new ModuleNode(new BodyNode([$body]), $this->parent, new Nodes($this->blocks), new Nodes($this->macros), new Nodes($this->traits), $this->embeddedTemplates, $stream->getSourceContext()); + $node = new ModuleNode( + new BodyNode([$body]), + $this->parent, + $this->blocks ? new Nodes($this->blocks) : new EmptyNode(), + $this->macros ? new Nodes($this->macros) : new EmptyNode(), + $this->traits ? new Nodes($this->traits) : new EmptyNode(), + $this->embeddedTemplates ? new Nodes($this->embeddedTemplates) : new EmptyNode(), + $stream->getSourceContext(), + ); $traverser = new NodeTraverser($this->env, $this->visitors); From 76b86aa9da40594dfbafe1c9507c28ed64780e57 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Fri, 28 Feb 2025 09:01:15 +0100 Subject: [PATCH 757/812] Add missing @deprecated tag --- src/Template.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Template.php b/src/Template.php index 4973aa754f3..720f18ecf9e 100644 --- a/src/Template.php +++ b/src/Template.php @@ -312,6 +312,8 @@ protected function load(string|TemplateWrapper|array $template, int $line, int|n /** * @param string|TemplateWrapper|array $template + * + * @deprecated since 3.21 and will be removed in 4.0. Use Template::load() instead. */ protected function loadTemplate($template, $templateName = null, int|null $line = null, int|null $index = null): self|TemplateWrapper { From 155d0faab45906e184c96b90e7d59de6e0d469e6 Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Fri, 28 Feb 2025 14:28:16 +0100 Subject: [PATCH 758/812] reduce the number of deprecations being triggered Triggering deprecations for specific arguments is not needed as the method itself is deprecated as well. --- CHANGELOG | 1 + doc/deprecated.rst | 6 +++--- src/Template.php | 6 +----- 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 23970440ba2..44ef08c232a 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,6 @@ # 3.21.0 (2025-XX-XX) + * Deprecate `Template::loadTemplate()` * Fix testing and expression when it evaluates to an instance of `Markup` * Add `ReturnPrimitiveTypeInterface` (and sub-interfaces for number, boolean, string, and array) * Add `SupportDefinedTestInterface` for expression nodes supporting the `defined` test diff --git a/doc/deprecated.rst b/doc/deprecated.rst index 55c5e25a6f1..1b2efe2e9f8 100644 --- a/doc/deprecated.rst +++ b/doc/deprecated.rst @@ -259,10 +259,10 @@ Token Templates --------- +* The method ``Template::loadTemplate()`` is deprecated. * Passing ``Twig\Template`` instances to Twig public API is deprecated (like - in ``Environment::resolveTemplate()``, ``Environment::load()``, and - ``Template::loadTemplate()``); pass instances of ``Twig\TemplateWrapper`` - instead. + in ``Environment::resolveTemplate()`` and ``Environment::load()``); pass + instances of ``Twig\TemplateWrapper`` instead. Filters ------- diff --git a/src/Template.php b/src/Template.php index 720f18ecf9e..faf7aee1e0d 100644 --- a/src/Template.php +++ b/src/Template.php @@ -313,21 +313,17 @@ protected function load(string|TemplateWrapper|array $template, int $line, int|n /** * @param string|TemplateWrapper|array $template * - * @deprecated since 3.21 and will be removed in 4.0. Use Template::load() instead. + * @deprecated since Twig 3.21 and will be removed in 4.0. Use Template::load() instead. */ protected function loadTemplate($template, $templateName = null, int|null $line = null, int|null $index = null): self|TemplateWrapper { trigger_deprecation('twig/twig', '3.21', 'The "%s" method is deprecated.', __METHOD__); if (null === $line) { - trigger_deprecation('twig/twig', '3.21', 'Passing a "null" line number to "%s" is deprecated.', __METHOD__); - $line = -1; } if ($template instanceof self) { - trigger_deprecation('twig/twig', '3.9', 'Passing a "%s" instance to "%s" is deprecated.', self::class, __METHOD__); - return $template; } From f8b01ebd983b410657fa8fbd56af00c2c72cf54d Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Fri, 28 Feb 2025 14:40:03 +0100 Subject: [PATCH 759/812] merge the Nodes and Node sections --- doc/deprecated.rst | 39 ++++++++++++++++++--------------------- 1 file changed, 18 insertions(+), 21 deletions(-) diff --git a/doc/deprecated.rst b/doc/deprecated.rst index 55c5e25a6f1..c2d3afe908f 100644 --- a/doc/deprecated.rst +++ b/doc/deprecated.rst @@ -199,6 +199,24 @@ Nodes * The ``is_defined_test`` attribute is deprecated as of Twig 3.21, use ``Twig\Node\Expression\SupportDefinedTestInterface`` instead. +* Instantiating ``Twig\Node\Node`` directly is deprecated as of Twig 3.15. Use + ``EmptyNode`` or ``Nodes`` instead depending on the use case. The + ``Twig\Node\Node`` class will be abstract in Twig 4.0. + +* Not passing ``AbstractExpression`` arguments to the following ``Node`` class + constructors is deprecated as of Twig 3.15: + + * ``AbstractBinary`` + * ``AbstractUnary`` + * ``BlockReferenceExpression`` + * ``TestExpression`` + * ``DefinedTest`` + * ``FilterExpression`` + * ``RawFilter`` + * ``DefaultFilter`` + * ``InlinePrint`` + * ``NullCoalesceExpression`` + Node Visitors ------------- @@ -357,27 +375,6 @@ Functions/Filters/Tests ``TwigFunction::getSafe()`` is deprecated as of Twig 3.16; return ``[]`` instead. -Node ----- - -* Instantiating ``Twig\Node\Node`` directly is deprecated as of Twig 3.15. Use - ``EmptyNode`` or ``Nodes`` instead depending on the use case. The - ``Twig\Node\Node`` class will be abstract in Twig 4.0. - -* Not passing ``AbstractExpression`` arguments to the following ``Node`` class - constructors is deprecated as of Twig 3.15: - - * ``AbstractBinary`` - * ``AbstractUnary`` - * ``BlockReferenceExpression`` - * ``TestExpression`` - * ``DefinedTest`` - * ``FilterExpression`` - * ``RawFilter`` - * ``DefaultFilter`` - * ``InlinePrint`` - * ``NullCoalesceExpression`` - Operators --------- From 81c1fcc7d63158794f36d8acb4a174137721e708 Mon Sep 17 00:00:00 2001 From: Alexandre Daubois Date: Fri, 28 Feb 2025 20:12:40 +0100 Subject: [PATCH 760/812] Fix `ModuleNode` instanciation when `$embeddedTemplates` is null --- src/Node/ModuleNode.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Node/ModuleNode.php b/src/Node/ModuleNode.php index f5b4292f3dd..57ef5b286a4 100644 --- a/src/Node/ModuleNode.php +++ b/src/Node/ModuleNode.php @@ -41,7 +41,7 @@ public function __construct(Node $body, ?AbstractExpression $parent, Node $block if (!$embeddedTemplates instanceof Node) { trigger_deprecation('twig/twig', '3.21', \sprintf('Not passing a "%s" instance as the "embedded_templates" argument of the "%s" constructor is deprecated.', Node::class, static::class)); - $embeddedTemplates = new Nodes($embeddedTemplates); + $embeddedTemplates = new Nodes($embeddedTemplates ?? []); } $nodes = [ From e31a6e453494106b908c3784c6fa9986e2d184d1 Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Fri, 28 Feb 2025 22:12:08 +0100 Subject: [PATCH 761/812] use EmptyNode instead of an Nodes instance without children --- src/Node/ModuleNode.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Node/ModuleNode.php b/src/Node/ModuleNode.php index 57ef5b286a4..71c57201982 100644 --- a/src/Node/ModuleNode.php +++ b/src/Node/ModuleNode.php @@ -41,7 +41,11 @@ public function __construct(Node $body, ?AbstractExpression $parent, Node $block if (!$embeddedTemplates instanceof Node) { trigger_deprecation('twig/twig', '3.21', \sprintf('Not passing a "%s" instance as the "embedded_templates" argument of the "%s" constructor is deprecated.', Node::class, static::class)); - $embeddedTemplates = new Nodes($embeddedTemplates ?? []); + if (null !== $embeddedTemplates) { + $embeddedTemplates = new Nodes($embeddedTemplates); + } else { + $embeddedTemplates = new EmptyNode(); + } } $nodes = [ From ccec90c85c86c49e399e302058b177007520482e Mon Sep 17 00:00:00 2001 From: Jacob Dreesen Date: Thu, 6 Mar 2025 17:34:44 +0100 Subject: [PATCH 762/812] Use `:` instead of `=` for named argument in the docs --- doc/functions/include.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/functions/include.rst b/doc/functions/include.rst index 3314943921f..646b2676725 100644 --- a/doc/functions/include.rst +++ b/doc/functions/include.rst @@ -48,7 +48,7 @@ the template does not exist: .. code-block:: twig - {{ include('sidebar.html.twig', ignore_missing = true) }} + {{ include('sidebar.html.twig', ignore_missing: true) }} You can also provide a list of templates that are checked for existence before inclusion. The first template that exists will be rendered: From 1a46d12033015bdadafc688ec315e71395dfb479 Mon Sep 17 00:00:00 2001 From: seb-jean Date: Wed, 12 Mar 2025 18:12:07 +0100 Subject: [PATCH 763/812] Update html_cva.rst --- doc/functions/html_cva.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/functions/html_cva.rst b/doc/functions/html_cva.rst index 099526b54e3..07c077c8e5e 100644 --- a/doc/functions/html_cva.rst +++ b/doc/functions/html_cva.rst @@ -97,7 +97,7 @@ when multiple other variant conditions are met: lg: 'text-lg', } }, - compoundVariants: [{ + compound_variants: [{ // if color = red AND size = (md or lg), add the `font-bold` class color: ['red'], size: ['md', 'lg'], From c32591272496049610b774fdbf23512cfd4a9ac5 Mon Sep 17 00:00:00 2001 From: Bertrand Zuchuat Date: Fri, 14 Mar 2025 21:10:34 +0100 Subject: [PATCH 764/812] fix: deprecated documentation Co-authored-by: Bertrand Zuchuat --- doc/deprecated.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/deprecated.rst b/doc/deprecated.rst index 98719267bb9..3348c59cb28 100644 --- a/doc/deprecated.rst +++ b/doc/deprecated.rst @@ -455,7 +455,7 @@ Operators as of Twig 3.21, use ``Twig\Extension\ExtensionInterface::getExpressionParsers()`` instead: - Before: + Before:: public function getOperators(): array { return [ @@ -466,7 +466,7 @@ Operators ]; } - After: + After:: public function getExpressionParsers(): array { return [ From 84b0499dbdf5dad8d2befe9ad6ab732db19955cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Andr=C3=A9?= Date: Thu, 20 Mar 2025 07:17:23 +0100 Subject: [PATCH 765/812] [Docs] Replace `=` by `:` in code examples * function/include * filters/format_number --- doc/filters/format_number.rst | 34 +++++++++++++++++----------------- doc/functions/include.rst | 4 ++-- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/doc/filters/format_number.rst b/doc/filters/format_number.rst index 9bda44b076b..5a18e957be9 100644 --- a/doc/filters/format_number.rst +++ b/doc/filters/format_number.rst @@ -22,37 +22,37 @@ The list of supported options: * ``grouping_used``: Specifies whether to use grouping separator for thousands:: {# 1,234,567.89 #} - {{ 1234567.89|format_number({grouping_used:true}, locale='en') }} + {{ 1234567.89|format_number({grouping_used:true}, locale: 'en') }} * ``decimal_always_shown``: Specifies whether to always show the decimal part, even if it's zero:: {# 123. #} - {{ 123|format_number({decimal_always_shown:true}, locale='en') }} + {{ 123|format_number({decimal_always_shown:true}, locale: 'en') }} * ``max_integer_digit``: * ``min_integer_digit``: * ``integer_digit``: Define constraints on the integer part:: {# 345.679 #} - {{ 12345.6789|format_number({max_integer_digit:3, min_integer_digit:2}, locale='en') }} + {{ 12345.6789|format_number({max_integer_digit:3, min_integer_digit:2}, locale: 'en') }} * ``max_fraction_digit``: * ``min_fraction_digit``: * ``fraction_digit``: Define constraints on the fraction part:: {# 123.46 #} - {{ 123.456789|format_number({max_fraction_digit:2, min_fraction_digit:1}, locale='en') }} + {{ 123.456789|format_number({max_fraction_digit:2, min_fraction_digit:1}, locale: 'en') }} * ``multiplier``: Multiplies the value before formatting:: {# 123,000 #} - {{ 123|format_number({multiplier:1000}, locale='en') }} + {{ 123|format_number({multiplier:1000}, locale: 'en') }} * ``grouping_size``: * ``secondary_grouping_size``: Set the size of the primary and secondary grouping separators:: {# 1,23,45,678 #} - {{ 12345678|format_number({grouping_size:3, secondary_grouping_size:2}, locale='en') }} + {{ 12345678|format_number({grouping_size:3, secondary_grouping_size:2}, locale: 'en') }} * ``rounding_mode``: * ``rounding_increment``: Control rounding behavior, here is a list of all rounding_mode available: @@ -67,7 +67,7 @@ The list of supported options: .. code-block:: twig {# 123.5 #} - {{ 123.456|format_number({rounding_mode:'ceiling', rounding_increment:0.05}, locale='en') }} + {{ 123.456|format_number({rounding_mode:'ceiling', rounding_increment:0.05}, locale: 'en') }} * ``format_width``: * ``padding_position``: Set width and padding for the formatted number, here is a list of all padding_position available: @@ -79,19 +79,19 @@ The list of supported options: .. code-block:: twig {# 123 #} - {{ 123|format_number({format_width:10, padding_position:'before_suffix'}, locale='en') }} + {{ 123|format_number({format_width:10, padding_position:'before_suffix'}, locale: 'en') }} * ``significant_digits_used``: * ``min_significant_digits_used``: * ``max_significant_digits_used``: Control significant digits in formatting:: {# 123.4568 #} - {{ 123.456789|format_number({significant_digits_used:true, min_significant_digits_used:4, max_significant_digits_used:7}, locale='en') }} + {{ 123.456789|format_number({significant_digits_used:true, min_significant_digits_used:4, max_significant_digits_used:7}, locale: 'en') }} * ``lenient_parse``: If true, allows lenient parsing of the input:: {# 123 #} - {{ 123|format_number({lenient_parse:true}, locale='en') }} + {{ 123|format_number({lenient_parse:true}, locale: 'en') }} Besides plain numbers, the filter can also format numbers in various styles:: @@ -109,37 +109,37 @@ The list of supported styles: * ``decimal``:: {# 1,234.568 #} - {{ 1234.56789 | format_number(style='decimal', locale='en') }} + {{ 1234.56789 | format_number(style: 'decimal', locale: 'en') }} * ``currency``:: {# $1,234.56 #} - {{ 1234.56 | format_number(style='currency', locale='en') }} + {{ 1234.56 | format_number(style: 'currency', locale: 'en') }} * ``percent``:: {# 12% #} - {{ 0.1234 | format_number(style='percent', locale='en') }} + {{ 0.1234 | format_number(style: 'percent', locale: 'en') }} * ``scientific``:: {# 1.23456789e+3 #} - {{ 1234.56789 | format_number(style='scientific', locale='en') }} + {{ 1234.56789 | format_number(style: 'scientific', locale: 'en') }} * ``spellout``:: {# one thousand two hundred thirty-four point five six seven eight nine #} - {{ 1234.56789 | format_number(style='spellout', locale='en') }} + {{ 1234.56789 | format_number(style: 'spellout', locale: 'en') }} * ``ordinal``:: {# 1st #} - {{ 1 | format_number(style='ordinal', locale='en') }} + {{ 1 | format_number(style: 'ordinal', locale: 'en') }} * ``duration``:: {# 2:30:00 #} - {{ 9000 | format_number(style='duration', locale='en') }} + {{ 9000 | format_number(style: 'duration', locale: 'en') }} As a shortcut, you can use the ``format_*_number`` filters by replacing ``*`` with a style:: diff --git a/doc/functions/include.rst b/doc/functions/include.rst index 646b2676725..3700018e5aa 100644 --- a/doc/functions/include.rst +++ b/doc/functions/include.rst @@ -27,12 +27,12 @@ You can disable access to the context by setting ``with_context`` to .. code-block:: twig {# only the name variable will be accessible #} - {{ include('template.html.twig', {name: 'Fabien'}, with_context = false) }} + {{ include('template.html.twig', {name: 'Fabien'}, with_context: false) }} .. code-block:: twig {# no variables will be accessible #} - {{ include('template.html.twig', with_context = false) }} + {{ include('template.html.twig', with_context: false) }} And if the expression evaluates to a ``\Twig\Template`` or a ``\Twig\TemplateWrapper`` instance, Twig will use it directly:: From a7c0482dc995fb052e753e6b322da3419ad4fd0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Andr=C3=A9?= Date: Thu, 20 Mar 2025 07:30:17 +0100 Subject: [PATCH 766/812] [Doc] Fix `code-block` in html_cva There is no `terminal` code-block in the current Twig documentation. Using `bash` here instead is more in line with the other pages. (...and will allow to simplify CSS needed for code blocks) --- doc/functions/html_cva.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/functions/html_cva.rst b/doc/functions/html_cva.rst index 099526b54e3..e8cc31b1040 100644 --- a/doc/functions/html_cva.rst +++ b/doc/functions/html_cva.rst @@ -61,7 +61,7 @@ To "merge" conflicting classes together and keep only the ones you need, use the ``tailwind_merge()`` filter from `tales-from-a-dev/twig-tailwind-extra`_ with the ``html_cva()`` function: -.. code-block:: terminal +.. code-block:: bash $ composer require tales-from-a-dev/twig-tailwind-extra From 239a060d5995dbb81a13ef291a254f6a9ccb1d46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Andr=C3=A9?= Date: Thu, 20 Mar 2025 08:35:48 +0100 Subject: [PATCH 767/812] fix: update extension references in docs to use backticks --- doc/api.rst | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/doc/api.rst b/doc/api.rst index bbc4c5c3bdc..5469dca722d 100644 --- a/doc/api.rst +++ b/doc/api.rst @@ -327,23 +327,23 @@ extension via the ``addExtension()`` method:: Twig comes bundled with the following extensions: -* *Twig\Extension\CoreExtension*: Defines all the core features of Twig. +* ``\Twig\Extension\CoreExtension``: Defines all the core features of Twig. -* *Twig\Extension\DebugExtension*: Defines the ``dump`` function to help debug +* ``\Twig\Extension\DebugExtension``: Defines the ``dump`` function to help debug template variables. -* *Twig\Extension\EscaperExtension*: Adds automatic output-escaping and the +* ``\Twig\Extension\EscaperExtension``: Adds automatic output-escaping and the possibility to escape/unescape blocks of code. -* *Twig\Extension\SandboxExtension*: Adds a sandbox mode to the default Twig +* ``\Twig\Extension\SandboxExtension``: Adds a sandbox mode to the default Twig environment, making it safe to evaluate untrusted code. -* *Twig\Extension\ProfilerExtension*: Enables the built-in Twig profiler. +* ``\Twig\Extension\ProfilerExtension``: Enables the built-in Twig profiler. -* *Twig\Extension\OptimizerExtension*: Optimizes the node tree before +* ``\Twig\Extension\OptimizerExtension``: Optimizes the node tree before compilation. -* *Twig\Extension\StringLoaderExtension*: Defines the ``template_from_string`` +* ``\Twig\Extension\StringLoaderExtension``: Defines the ``template_from_string`` function to allow loading templates from string in a template. The Core, Escaper, and Optimizer extensions are registered by default. From 09f9a22310ca90789f6a97f576a1bc96fa9b7238 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Fri, 21 Mar 2025 09:39:08 +0100 Subject: [PATCH 768/812] Fix wrong array index (again) --- CHANGELOG | 1 + src/Node/Expression/ArrayExpression.php | 21 ++++++++++----------- tests/Fixtures/expressions/array.test | 5 +++++ 3 files changed, 16 insertions(+), 11 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 44ef08c232a..f7b3040ce6a 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,6 @@ # 3.21.0 (2025-XX-XX) + * Fix wrong array index * Deprecate `Template::loadTemplate()` * Fix testing and expression when it evaluates to an instance of `Markup` * Add `ReturnPrimitiveTypeInterface` (and sub-interfaces for number, boolean, string, and array) diff --git a/src/Node/Expression/ArrayExpression.php b/src/Node/Expression/ArrayExpression.php index 9678be26c0a..b6f8a6ba48f 100644 --- a/src/Node/Expression/ArrayExpression.php +++ b/src/Node/Expression/ArrayExpression.php @@ -78,33 +78,32 @@ public function compile(Compiler $compiler): void } $compiler->raw('['); - $first = true; - $nextIndex = 0; - foreach ($this->getKeyValuePairs() as $pair) { - if (!$first) { + $isSequence = true; + foreach ($this->getKeyValuePairs() as $i => $pair) { + if (0 !== $i) { $compiler->raw(', '); } - $first = false; $key = null; if ($pair['key'] instanceof ContextVariable) { $pair['key'] = new StringCastUnary($pair['key'], $pair['key']->getTemplateLine()); - } - if ($pair['key'] instanceof TempNameExpression) { + } elseif ($pair['key'] instanceof TempNameExpression) { $key = $pair['key']->getAttribute('name'); $pair['key'] = new ConstantExpression($key, $pair['key']->getTemplateLine()); - } - if ($pair['key'] instanceof ConstantExpression) { + } elseif ($pair['key'] instanceof ConstantExpression) { $key = $pair['key']->getAttribute('value'); } - if ($nextIndex !== $key && !$pair['value'] instanceof SpreadUnary) { + if ($key !== $i) { + $isSequence = false; + } + + if (!$isSequence && !$pair['value'] instanceof SpreadUnary) { $compiler ->subcompile($pair['key']) ->raw(' => ') ; } - ++$nextIndex; $compiler->subcompile($pair['value']); } diff --git a/tests/Fixtures/expressions/array.test b/tests/Fixtures/expressions/array.test index ac1c8ca0e36..72efbf9eef9 100644 --- a/tests/Fixtures/expressions/array.test +++ b/tests/Fixtures/expressions/array.test @@ -55,6 +55,9 @@ Twig supports array notation {% set trad = {194:'ABC',141:'DEF',100:'GHI',170:'JKL',110:'MNO',111:'PQR'} %} {% set trad2 = {'194':'ABC','141':'DEF','100':'GHI','170':'JKL','110':'MNO','111':'PQR'} %} {{ trad == trad2 ? 'OK' : 'KO' }} +{% set trad = {11: 'ABC', 2: 'DEF', 4: 'GHI', 3: 'JKL'} %} +{% set trad2 = {'11': 'ABC', '2': 'DEF', '4': 'GHI', '3': 'JKL'} %} +{{ trad == trad2 ? 'OK' : 'KO' }} {# indexes are kept #} {{ { 1: "first", 0: "second" } == { '1': "first", '0': "second" } ? 'OK' : 'KO' }} @@ -104,6 +107,7 @@ ok ok ok +OK OK OK @@ -151,6 +155,7 @@ ok ok ok +OK OK OK From 5886907b28198ab3352ce007af279592ec1cf730 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Sun, 26 Nov 2023 00:43:09 +0100 Subject: [PATCH 769/812] Create attributes `AsTwigFilter`, `AsTwigFunction` and `AsTwigTest` to ease extension development --- doc/advanced.rst | 101 ++++++++++ src/Attribute/AsTwigFilter.php | 56 ++++++ src/Attribute/AsTwigFunction.php | 52 ++++++ src/Attribute/AsTwigTest.php | 48 +++++ src/Extension/AttributeExtension.php | 174 ++++++++++++++++++ src/ExtensionSet.php | 7 +- tests/Extension/AttributeExtensionTest.php | 160 ++++++++++++++++ .../Fixtures/ExtensionWithAttributes.php | 112 +++++++++++ .../Extension/Fixtures/FilterWithoutValue.php | 13 ++ tests/Extension/Fixtures/TestWithoutValue.php | 13 ++ 10 files changed, 735 insertions(+), 1 deletion(-) create mode 100644 src/Attribute/AsTwigFilter.php create mode 100644 src/Attribute/AsTwigFunction.php create mode 100644 src/Attribute/AsTwigTest.php create mode 100644 src/Extension/AttributeExtension.php create mode 100644 tests/Extension/AttributeExtensionTest.php create mode 100644 tests/Extension/Fixtures/ExtensionWithAttributes.php create mode 100644 tests/Extension/Fixtures/FilterWithoutValue.php create mode 100644 tests/Extension/Fixtures/TestWithoutValue.php diff --git a/doc/advanced.rst b/doc/advanced.rst index 432892aefe8..47683c44d69 100644 --- a/doc/advanced.rst +++ b/doc/advanced.rst @@ -814,6 +814,107 @@ The ``getTests()`` method lets you add new test functions:: // ... } +Using PHP Attributes to define Extensions +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. versionadded:: 3.21 + + The attribute classes were added in Twig 3.21. + +You can add the ``#[AsTwigFilter]``, ``#[AsTwigFunction]``, and ``#[AsTwigTest]`` +attributes to public methods of any class to define filters, functions, and tests. + +Create a class using these attributes:: + + use Twig\Attribute\AsTwigFilter; + use Twig\Attribute\AsTwigFunction; + use Twig\Attribute\AsTwigTest; + + class ProjectExtension + { + #[AsTwigFilter('rot13')] + public static function rot13(string $string): string + { + // ... + } + + #[AsTwigFunction('lipsum')] + public static function lipsum(int $count): string + { + // ... + } + + #[AsTwigTest('even')] + public static function isEven(int $number): bool + { + // ... + } + } + +Then register the ``Twig\Extension\AttributeExtension`` with the class name:: + + $twig = new \Twig\Environment($loader); + $twig->addExtension(new \Twig\Extension\AttributeExtension(ProjectExtension::class)); + +If all the methods are static, you are done. The ``ProjectExtension`` class will +never be instantiated and the class attributes will be scanned only when a template +is compiled. + +Otherwise, if some methods are not static, you need to register the class as +a runtime extension using one of the runtime loaders:: + + use Twig\Attribute\AsTwigFunction; + + class ProjectExtension + { + // Inject hypothetical dependencies + public function __construct(private LipsumProvider $lipsumProvider) {} + + #[AsTwigFunction('lipsum')] + public function lipsum(int $count): string + { + return $this->lipsumProvider->lipsum($count); + } + } + + $twig = new \Twig\Environment($loader); + $twig->addExtension(new \Twig\Extension\AttributeExtension(ProjectExtension::class); + $twig->addRuntimeLoader(new \Twig\RuntimeLoader\FactoryLoader([ + ProjectExtension::class => function () use ($lipsumProvider) { + return new ProjectExtension($lipsumProvider); + }, + ])); + +If you want to access the current environment instance in your filter or function, +add the ``Twig\Environment`` type to the first argument of the method:: + + class ProjectExtension + { + #[AsTwigFunction('lipsum')] + public function lipsum(\Twig\Environment $env, int $count): string + { + // ... + } + } + +``#[AsTwigFilter]`` and ``#[AsTwigFunction]`` support variadic arguments +automatically when applied to variadic methods:: + + class ProjectExtension + { + #[AsTwigFilter('thumbnail')] + public function thumbnail(string $file, mixed ...$options): string + { + // ... + } + } + +The attributes support other options used to configure the Twig Callables: + + * ``AsTwigFilter``: ``needsCharset``, ``needsEnvironment``, ``needsContext``, ``isSafe``, ``isSafeCallback``, ``preEscape``, ``preservesSafety``, ``deprecationInfo`` + * ``AsTwigFunction``: ``needsCharset``, ``needsEnvironment``, ``needsContext``, ``isSafe``, ``isSafeCallback``, ``deprecationInfo`` + * ``AsTwigTest``: ``needsCharset``, ``needsEnvironment``, ``needsContext``, ``deprecationInfo`` + Definition vs Runtime ~~~~~~~~~~~~~~~~~~~~~ diff --git a/src/Attribute/AsTwigFilter.php b/src/Attribute/AsTwigFilter.php new file mode 100644 index 00000000000..395531d8650 --- /dev/null +++ b/src/Attribute/AsTwigFilter.php @@ -0,0 +1,56 @@ + + */ +final class AttributeExtension extends AbstractExtension +{ + private array $filters; + private array $functions; + private array $tests; + + /** + * Use a runtime class using PHP attributes to define filters, functions, and tests. + * + * @param class-string $class + */ + public function __construct(private string $class) + { + } + + /** + * @return class-string + */ + public function getClass(): string + { + return $this->class; + } + + public function getFilters(): array + { + if (!isset($this->filters)) { + $this->initFromAttributes(); + } + + return $this->filters; + } + + public function getFunctions(): array + { + if (!isset($this->functions)) { + $this->initFromAttributes(); + } + + return $this->functions; + } + + public function getTests(): array + { + if (!isset($this->tests)) { + $this->initFromAttributes(); + } + + return $this->tests; + } + + public function getLastModified(): int + { + return max( + filemtime(__FILE__), + is_file($filename = (new \ReflectionClass($this->getClass()))->getFileName()) ? filemtime($filename) : 0, + ); + } + + private function initFromAttributes(): void + { + $filters = $functions = $tests = []; + $reflectionClass = new \ReflectionClass($this->getClass()); + foreach ($reflectionClass->getMethods() as $method) { + foreach ($method->getAttributes(AsTwigFilter::class) as $reflectionAttribute) { + /** @var AsTwigFilter $attribute */ + $attribute = $reflectionAttribute->newInstance(); + + $callable = new TwigFilter($attribute->name, [$reflectionClass->name, $method->getName()], [ + 'needs_context' => $attribute->needsContext ?? false, + 'needs_environment' => $attribute->needsEnvironment ?? $this->needsEnvironment($method), + 'needs_charset' => $attribute->needsCharset ?? false, + 'is_variadic' => $method->isVariadic(), + 'is_safe' => $attribute->isSafe, + 'is_safe_callback' => $attribute->isSafeCallback, + 'pre_escape' => $attribute->preEscape, + 'preserves_safety' => $attribute->preservesSafety, + 'deprecation_info' => $attribute->deprecationInfo, + ]); + + if ($callable->getMinimalNumberOfRequiredArguments() > $method->getNumberOfParameters()) { + throw new \LogicException(sprintf('"%s::%s()" needs at least %d arguments to be used AsTwigFilter, but only %d defined.', $reflectionClass->getName(), $method->getName(), $callable->getMinimalNumberOfRequiredArguments(), $method->getNumberOfParameters())); + } + + $filters[$attribute->name] = $callable; + } + + foreach ($method->getAttributes(AsTwigFunction::class) as $reflectionAttribute) { + /** @var AsTwigFunction $attribute */ + $attribute = $reflectionAttribute->newInstance(); + + $callable = new TwigFunction($attribute->name, [$reflectionClass->name, $method->getName()], [ + 'needs_context' => $attribute->needsContext ?? false, + 'needs_environment' => $attribute->needsEnvironment ?? $this->needsEnvironment($method), + 'needs_charset' => $attribute->needsCharset ?? false, + 'is_variadic' => $method->isVariadic(), + 'is_safe' => $attribute->isSafe, + 'is_safe_callback' => $attribute->isSafeCallback, + 'deprecation_info' => $attribute->deprecationInfo, + ]); + + if ($callable->getMinimalNumberOfRequiredArguments() > $method->getNumberOfParameters()) { + throw new \LogicException(sprintf('"%s::%s()" needs at least %d arguments to be used AsTwigFunction, but only %d defined.', $reflectionClass->getName(), $method->getName(), $callable->getMinimalNumberOfRequiredArguments(), $method->getNumberOfParameters())); + } + + $functions[$attribute->name] = $callable; + } + + foreach ($method->getAttributes(AsTwigTest::class) as $reflectionAttribute) { + + /** @var AsTwigTest $attribute */ + $attribute = $reflectionAttribute->newInstance(); + + $callable = new TwigTest($attribute->name, [$reflectionClass->name, $method->getName()], [ + 'needs_context' => $attribute->needsContext ?? false, + 'needs_environment' => $attribute->needsEnvironment ?? $this->needsEnvironment($method), + 'needs_charset' => $attribute->needsCharset ?? false, + 'is_variadic' => $method->isVariadic(), + 'deprecation_info' => $attribute->deprecationInfo, + ]); + + if ($callable->getMinimalNumberOfRequiredArguments() > $method->getNumberOfParameters()) { + throw new \LogicException(sprintf('"%s::%s()" needs at least %d arguments to be used AsTwigTest, but only %d defined.', $reflectionClass->getName(), $method->getName(), $callable->getMinimalNumberOfRequiredArguments(), $method->getNumberOfParameters())); + } + + $tests[$attribute->name] = $callable; + } + } + + // Assign all at the end to avoid inconsistent state in case of exception + $this->filters = array_values($filters); + $this->functions = array_values($functions); + $this->tests = array_values($tests); + } + + /** + * Detect if the first argument of the method is the environment. + */ + private function needsEnvironment(\ReflectionFunctionAbstract $function): bool + { + if (!$parameters = $function->getParameters()) { + return false; + } + + return $parameters[0]->getType() instanceof \ReflectionNamedType + && Environment::class === $parameters[0]->getType()->getName() + && !$parameters[0]->isVariadic(); + } +} diff --git a/src/ExtensionSet.php b/src/ExtensionSet.php index 14b1d3e2958..abca18f14a3 100644 --- a/src/ExtensionSet.php +++ b/src/ExtensionSet.php @@ -18,6 +18,7 @@ use Twig\ExpressionParser\InfixExpressionParserInterface; use Twig\ExpressionParser\PrecedenceChange; use Twig\ExpressionParser\Prefix\UnaryOperatorExpressionParser; +use Twig\Extension\AttributeExtension; use Twig\Extension\ExtensionInterface; use Twig\Extension\GlobalsInterface; use Twig\Extension\LastModifiedExtensionInterface; @@ -142,7 +143,11 @@ public function getLastModified(): int public function addExtension(ExtensionInterface $extension): void { - $class = $extension::class; + if ($extension instanceof AttributeExtension) { + $class = $extension->getClass(); + } else { + $class = $extension::class; + } if ($this->initialized) { throw new \LogicException(\sprintf('Unable to register extension "%s" as extensions have already been initialized.', $class)); diff --git a/tests/Extension/AttributeExtensionTest.php b/tests/Extension/AttributeExtensionTest.php new file mode 100644 index 00000000000..68e3bfeef99 --- /dev/null +++ b/tests/Extension/AttributeExtensionTest.php @@ -0,0 +1,160 @@ +getFilters() as $filter) { + if ($filter->getName() === $name) { + $this->assertEquals(new TwigFilter($name, [ExtensionWithAttributes::class, $method], $options), $filter); + + return; + } + } + + $this->fail(sprintf('Filter "%s" is not registered.', $name)); + } + + public static function provideFilters() + { + yield 'with name' => ['foo', 'fooFilter', ['is_safe' => ['html']]]; + yield 'with env' => ['with_env_filter', 'withEnvFilter', ['needs_environment' => true]]; + yield 'with context' => ['with_context_filter', 'withContextFilter', ['needs_context' => true]]; + yield 'with env and context' => ['with_env_and_context_filter', 'withEnvAndContextFilter', ['needs_environment' => true, 'needs_context' => true]]; + yield 'variadic' => ['variadic_filter', 'variadicFilter', ['is_variadic' => true]]; + yield 'deprecated' => ['deprecated_filter', 'deprecatedFilter', ['deprecation_info' => new DeprecatedCallableInfo('foo/bar', '1.2')]]; + yield 'pattern' => ['pattern_*_filter', 'patternFilter', []]; + } + + /** + * @dataProvider provideFunctions + */ + public function testFunction(string $name, string $method, array $options) + { + $extension = new AttributeExtension(ExtensionWithAttributes::class); + foreach ($extension->getFunctions() as $function) { + if ($function->getName() === $name) { + $this->assertEquals(new TwigFunction($name, [ExtensionWithAttributes::class, $method], $options), $function); + + return; + } + } + + $this->fail(sprintf('Function "%s" is not registered.', $name)); + } + + public static function provideFunctions() + { + yield 'with name' => ['foo', 'fooFunction', ['is_safe' => ['html']]]; + yield 'with env' => ['with_env_function', 'withEnvFunction', ['needs_environment' => true]]; + yield 'with context' => ['with_context_function', 'withContextFunction', ['needs_context' => true]]; + yield 'with env and context' => ['with_env_and_context_function', 'withEnvAndContextFunction', ['needs_environment' => true, 'needs_context' => true]]; + yield 'no argument' => ['no_arg_function', 'noArgFunction', []]; + yield 'variadic' => ['variadic_function', 'variadicFunction', ['is_variadic' => true]]; + yield 'deprecated' => ['deprecated_function', 'deprecatedFunction', ['deprecation_info' => new DeprecatedCallableInfo('foo/bar', '1.2')]]; + } + + /** + * @dataProvider provideTests + */ + public function testTest(string $name, string $method, array $options) + { + $extension = new AttributeExtension(ExtensionWithAttributes::class); + foreach ($extension->getTests() as $test) { + if ($test->getName() === $name) { + $this->assertEquals(new TwigTest($name, [ExtensionWithAttributes::class, $method], $options), $test); + + return; + } + } + + $this->fail(sprintf('Test "%s" is not registered.', $name)); + } + + public static function provideTests() + { + yield 'with name' => ['foo', 'fooTest', []]; + yield 'with env' => ['with_env_test', 'withEnvTest', ['needs_environment' => true]]; + yield 'with context' => ['with_context_test', 'withContextTest', ['needs_context' => true]]; + yield 'with env and context' => ['with_env_and_context_test', 'withEnvAndContextTest', ['needs_environment' => true, 'needs_context' => true]]; + yield 'variadic' => ['variadic_test', 'variadicTest', ['is_variadic' => true]]; + yield 'deprecated' => ['deprecated_test', 'deprecatedTest', ['deprecation_info' => new DeprecatedCallableInfo('foo/bar', '1.2')]]; + } + + public function testFilterRequireOneArgument() + { + $extension = new AttributeExtension(FilterWithoutValue::class); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('"'.FilterWithoutValue::class.'::myFilter()" needs at least 1 arguments to be used AsTwigFilter, but only 0 defined.'); + + $extension->getTests(); + } + + public function testTestRequireOneArgument() + { + $extension = new AttributeExtension(TestWithoutValue::class); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('"'.TestWithoutValue::class.'::myTest()" needs at least 1 arguments to be used AsTwigTest, but only 0 defined.'); + + $extension->getTests(); + } + + public function testLastModifiedWithObject() + { + $extension = new AttributeExtension(\stdClass::class); + + $this->assertSame(filemtime((new \ReflectionClass(AttributeExtension::class))->getFileName()), $extension->getLastModified()); + } + + public function testLastModifiedWithClass() + { + $extension = new AttributeExtension('__CLASS_FOR_TEST_LAST_MODIFIED__'); + + $filename = tempnam(sys_get_temp_dir(), 'twig'); + try { + file_put_contents($filename, 'assertSame(filemtime($filename), $extension->getLastModified()); + } finally { + unlink($filename); + } + } + + public function testMultipleRegistrations() + { + $extensionSet = new ExtensionSet(); + $extensionSet->addExtension($extension1 = new AttributeExtension(ExtensionWithAttributes::class)); + $extensionSet->addExtension($extension2 = new AttributeExtension(\stdClass::class)); + + $this->assertCount(2, $extensionSet->getExtensions()); + $this->assertNotNull($extensionSet->getFilter('foo')); + + $this->assertSame($extension1, $extensionSet->getExtension(ExtensionWithAttributes::class)); + $this->assertSame($extension2, $extensionSet->getExtension(\stdClass::class)); + + $this->expectException(RuntimeError::class); + $this->expectExceptionMessage('The "Twig\Extension\AttributeExtension" extension is not enabled.'); + $extensionSet->getExtension(AttributeExtension::class); + } +} diff --git a/tests/Extension/Fixtures/ExtensionWithAttributes.php b/tests/Extension/Fixtures/ExtensionWithAttributes.php new file mode 100644 index 00000000000..2e6b0ee1011 --- /dev/null +++ b/tests/Extension/Fixtures/ExtensionWithAttributes.php @@ -0,0 +1,112 @@ + Date: Tue, 25 Mar 2025 10:37:47 +0300 Subject: [PATCH 770/812] Update CHANGELOG --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index f7b3040ce6a..6db5507769c 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -11,6 +11,7 @@ instead of arrays (it comes with many deprecations that are documented in the ``deprecated`` documentation chapter) * Deprecate the `Twig\ExpressionParser`, and `Twig\OperatorPrecedenceChange` classes + * Add attributes `AsTwigFilter`, `AsTwigFunction`, and `AsTwigTest` to ease extension development # 3.20.0 (2025-02-13) From f14f2208f944807373ff2903e59287a04a21b9d1 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sun, 30 Mar 2025 09:50:31 +0200 Subject: [PATCH 771/812] Tweaks html_cva docs --- doc/functions/html_cva.rst | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/doc/functions/html_cva.rst b/doc/functions/html_cva.rst index bff0541707b..61c094ea50b 100644 --- a/doc/functions/html_cva.rst +++ b/doc/functions/html_cva.rst @@ -5,7 +5,6 @@ The ``html_cva`` function was added in Twig 3.12. - `CVA (Class Variant Authority)`_ is a concept from the JavaScript world and used by the well-known `shadcn/ui`_ library. The CVA concept is used to render multiple variations of components, applying @@ -45,13 +44,13 @@ Then use the ``color`` and ``size`` variants to select the needed classes: {# index.html.twig #} {{ include('alert.html.twig', {'color': 'blue', 'size': 'md'}) }} - // class="alert bg-blue text-md" + {# class="alert bg-blue text-md" #} {{ include('alert.html.twig', {'color': 'green', 'size': 'sm'}) }} - // class="alert bg-green text-sm" + {# class="alert bg-green text-sm" #} {{ include('alert.html.twig', {'color': 'red', 'class': 'flex items-center justify-center'}) }} - // class="alert bg-red flex items-center justify-center" + {# class="alert bg-red flex items-center justify-center" #} CVA and Tailwind CSS -------------------- @@ -68,11 +67,11 @@ with the ``html_cva()`` function: .. code-block:: html+twig {% set alert = html_cva( - // ... + ... ) %}
    - ... + ...
    Compound Variants @@ -98,27 +97,27 @@ when multiple other variant conditions are met: } }, compound_variants: [{ - // if color = red AND size = (md or lg), add the `font-bold` class + # if color = red AND size = (md or lg), add the `font-bold` class color: ['red'], size: ['md', 'lg'], - class: 'font-bold' + class: 'font-bold', }] ) %}
    - ... + ...
    {# index.html.twig #} {{ include('alert.html.twig', {color: 'red', size: 'lg'}) }} - // class="alert bg-red text-lg font-bold" + {# class="alert bg-red text-lg font-bold" #} {{ include('alert.html.twig', {color: 'green', size: 'sm'}) }} - // class="alert bg-green text-sm" + {# class="alert bg-green text-sm" #} {{ include('alert.html.twig', {color: 'red', size: 'md'}) }} - // class="alert bg-green text-md font-bold" + {# class="alert bg-green text-md font-bold" #} Default Variants ---------------- @@ -146,7 +145,7 @@ If no variants match, you can define a default set of classes to apply: lg: 'rounded-lg', } }, - defaultVariant: { + default_variant: { rounded: 'md', } ) %} @@ -158,7 +157,7 @@ If no variants match, you can define a default set of classes to apply: {# index.html.twig #} {{ include('alert.html.twig', {color: 'red', size: 'lg'}) }} - // class="alert bg-red text-lg rounded-md" + {# class="alert bg-red text-lg rounded-md" #} .. note:: From b54a265712a475da0aef560e184d68a9affd3ace Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Fri, 2 May 2025 15:18:20 +0200 Subject: [PATCH 772/812] Prepare the 3.21.0 release --- CHANGELOG | 2 +- src/Environment.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 6db5507769c..058f486e9a6 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,4 +1,4 @@ -# 3.21.0 (2025-XX-XX) +# 3.21.0 (2025-05-02) * Fix wrong array index * Deprecate `Template::loadTemplate()` diff --git a/src/Environment.php b/src/Environment.php index 46e0f3da972..bd5bbdcaf55 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -43,12 +43,12 @@ */ class Environment { - public const VERSION = '3.21.0-DEV'; + public const VERSION = '3.21.0'; public const VERSION_ID = 32100; public const MAJOR_VERSION = 3; public const MINOR_VERSION = 21; public const RELEASE_VERSION = 0; - public const EXTRA_VERSION = 'DEV'; + public const EXTRA_VERSION = ''; private $charset; private $loader; From 06eb9eafe23924056bc47965551e3dbe77bb8243 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Fri, 2 May 2025 15:19:55 +0200 Subject: [PATCH 773/812] Bump version --- CHANGELOG | 4 ++++ src/Environment.php | 8 ++++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 058f486e9a6..7f043df6719 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +# 3.21.1 (2025-XX-XX) + + * n/a + # 3.21.0 (2025-05-02) * Fix wrong array index diff --git a/src/Environment.php b/src/Environment.php index bd5bbdcaf55..de0ced3ddcd 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -43,12 +43,12 @@ */ class Environment { - public const VERSION = '3.21.0'; - public const VERSION_ID = 32100; + public const VERSION = '3.21.1-DEV'; + public const VERSION_ID = 32101; public const MAJOR_VERSION = 3; public const MINOR_VERSION = 21; - public const RELEASE_VERSION = 0; - public const EXTRA_VERSION = ''; + public const RELEASE_VERSION = 1; + public const EXTRA_VERSION = 'DEV'; private $charset; private $loader; From 00d29e6657b36ae08672a8b92aec7f38c2ce1eda Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Fri, 2 May 2025 17:39:47 +0200 Subject: [PATCH 774/812] Fix instantiation --- src/ExtensionSet.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ExtensionSet.php b/src/ExtensionSet.php index abca18f14a3..04392b2e6ea 100644 --- a/src/ExtensionSet.php +++ b/src/ExtensionSet.php @@ -510,7 +510,7 @@ private function initExtension(ExtensionInterface $extension): void if ($op['callable']) { $expressionParsers[] = $this->convertInfixExpressionParser($op['class'], $operator, $op['precedence'], $op['associativity'], $op['precedence_change'] ?? null, $op['aliases'] ?? [], $op['callable']); } else { - $expressionParsers[] = new BinaryOperatorExpressionParser($op['class'], $operator, $op['precedence'], $op['associativity'], $op['precedence_change'] ?? null, $op['aliases'] ?? []); + $expressionParsers[] = new BinaryOperatorExpressionParser($op['class'], $operator, $op['precedence'], $op['associativity'], $op['precedence_change'] ?? null, '', $op['aliases'] ?? []); } } From e14a2474fbcf0e97fc6b0b7e30de8a6924e0ab2a Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Fri, 2 May 2025 17:41:32 +0200 Subject: [PATCH 775/812] Fix warning --- src/ExtensionSet.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ExtensionSet.php b/src/ExtensionSet.php index 04392b2e6ea..85a98cf3c32 100644 --- a/src/ExtensionSet.php +++ b/src/ExtensionSet.php @@ -507,7 +507,7 @@ private function initExtension(ExtensionInterface $extension): void default => throw new \InvalidArgumentException(\sprintf('Invalid associativity "%s" for operator "%s".', $op['associativity'], $operator)), }; - if ($op['callable']) { + if (isset($op['callable'])) { $expressionParsers[] = $this->convertInfixExpressionParser($op['class'], $operator, $op['precedence'], $op['associativity'], $op['precedence_change'] ?? null, $op['aliases'] ?? [], $op['callable']); } else { $expressionParsers[] = new BinaryOperatorExpressionParser($op['class'], $operator, $op['precedence'], $op['associativity'], $op['precedence_change'] ?? null, '', $op['aliases'] ?? []); From a4293d7d2991a0a68f34fb7056b0aa295c764db5 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sat, 3 May 2025 09:21:26 +0200 Subject: [PATCH 776/812] Update CHANGELOG --- CHANGELOG | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 7f043df6719..8ea59632758 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,6 @@ # 3.21.1 (2025-XX-XX) - * n/a + * Fix ExtensionSet usage of BinaryOperatorExpressionParser # 3.21.0 (2025-05-02) From 285123877d4dd97dd7c11842ac5fb7e86e60d81d Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sat, 3 May 2025 09:21:55 +0200 Subject: [PATCH 777/812] Prepare the 3.21.1 release --- CHANGELOG | 2 +- src/Environment.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 8ea59632758..cf202b044bf 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,4 +1,4 @@ -# 3.21.1 (2025-XX-XX) +# 3.21.1 (2025-05-03) * Fix ExtensionSet usage of BinaryOperatorExpressionParser diff --git a/src/Environment.php b/src/Environment.php index de0ced3ddcd..ff3f0c588bd 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -43,12 +43,12 @@ */ class Environment { - public const VERSION = '3.21.1-DEV'; + public const VERSION = '3.21.1'; public const VERSION_ID = 32101; public const MAJOR_VERSION = 3; public const MINOR_VERSION = 21; public const RELEASE_VERSION = 1; - public const EXTRA_VERSION = 'DEV'; + public const EXTRA_VERSION = ''; private $charset; private $loader; From 12a40e714cc046a99be956962a2fe35e2985cb56 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sat, 3 May 2025 09:23:07 +0200 Subject: [PATCH 778/812] Bump version --- CHANGELOG | 4 ++++ src/Environment.php | 8 ++++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index cf202b044bf..af9bcb1d621 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +# 3.21.2 (2025-XX-XX) + + * n/a + # 3.21.1 (2025-05-03) * Fix ExtensionSet usage of BinaryOperatorExpressionParser diff --git a/src/Environment.php b/src/Environment.php index ff3f0c588bd..7a7a1374b35 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -43,12 +43,12 @@ */ class Environment { - public const VERSION = '3.21.1'; - public const VERSION_ID = 32101; + public const VERSION = '3.21.2-DEV'; + public const VERSION_ID = 32102; public const MAJOR_VERSION = 3; public const MINOR_VERSION = 21; - public const RELEASE_VERSION = 1; - public const EXTRA_VERSION = ''; + public const RELEASE_VERSION = 2; + public const EXTRA_VERSION = 'DEV'; private $charset; private $loader; From bb6382c1bb7147acfe50891b9e8dc38c534481f0 Mon Sep 17 00:00:00 2001 From: AndCycle <4460370+AndCycle@users.noreply.github.com> Date: Mon, 12 May 2025 16:54:49 +0800 Subject: [PATCH 779/812] Update reduce.rst remove note, better argument for example reduce doesn't use array_reduce anymore https://github.com/twigphp/Twig/issues/3802 --- doc/filters/reduce.rst | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/doc/filters/reduce.rst b/doc/filters/reduce.rst index ff529cdc83c..ac8089fd88d 100644 --- a/doc/filters/reduce.rst +++ b/doc/filters/reduce.rst @@ -10,14 +10,14 @@ value and key of the sequence or mapping: {% set numbers = [1, 2, 3] %} - {{ numbers|reduce((carry, v, k) => carry + v * k) }} + {{ numbers|reduce((carry, value, key) => carry + value * key) }} {# output 8 #} The ``reduce`` filter takes an ``initial`` value as a second argument: .. code-block:: twig - {{ numbers|reduce((carry, v, k) => carry + v * k, 10) }} + {{ numbers|reduce((carry, value, key) => carry + value * key, 10) }} {# output 18 #} Note that the arrow function has access to the current context. @@ -27,9 +27,3 @@ Arguments * ``arrow``: The arrow function * ``initial``: The initial value - -.. note:: - - Internally, Twig uses the PHP `array_reduce`_ function. - -.. _`array_reduce`: https://www.php.net/array_reduce From 2184db3b9bbc713889c8cd13588a9ec38dfb1d7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Andr=C3=A9?= Date: Fri, 16 May 2025 00:37:43 +0200 Subject: [PATCH 780/812] Add TwigCsFixer in tools list Twig CS Fixer is already present in the code style / conventions page, but it feels to me very logical we see it on the "integration / tools" page. --- doc/templates.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/templates.rst b/doc/templates.rst index 33a32e89e1a..94312cd88c1 100644 --- a/doc/templates.rst +++ b/doc/templates.rst @@ -68,11 +68,10 @@ Many IDEs support syntax highlighting and auto-completion for Twig: You might also be interested in: +* `Twig CS Fixer`_: a tool to check/fix your templates code style * `Twig Language Server`_: provides some language features like syntax highlighting, diagnostics, auto complete, ... - * `TwigQI`_: an extension which analyzes your templates for common bugs during compilation - * `TwigStan`_: a static analyzer for Twig templates powered by PHPStan Variables @@ -1105,6 +1104,7 @@ Twig can be extended. If you want to create your own extensions, read the .. _`TwigStan`: https://github.com/twigstan/twigstan .. _`Twig pack`: https://marketplace.visualstudio.com/items?itemName=bajdzis.vscode-twig-pack .. _`Modern Twig`: https://marketplace.visualstudio.com/items?itemName=Stanislav.vscode-twig +.. _`Twig CS Fixer`: https://github.com/VincentLanglet/Twig-CS-Fixer .. _`Twig Language Server`: https://github.com/kaermorchen/twig-language-server/tree/master/packages/language-server .. _`Twiggy`: https://marketplace.visualstudio.com/items?itemName=moetelo.twiggy .. _`PHP spaceship operator documentation`: https://www.php.net/manual/en/language.operators.comparison.php From 057ea922748579724017bab9dd326e3411c29965 Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Sat, 17 May 2025 21:08:00 +0200 Subject: [PATCH 781/812] replace typographic quote with straigt single quote make DOCtor-RST 1.68 happy and account for OskarStark/doctor-rst#2011 --- doc/functions/date.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/functions/date.rst b/doc/functions/date.rst index 16e1d48740e..3c6b6316bae 100644 --- a/doc/functions/date.rst +++ b/doc/functions/date.rst @@ -9,7 +9,7 @@ Converts an argument to a date to allow date comparison: {# do something #} {% endif %} -The argument must be in one of PHP’s supported `date and time formats`_. +The argument must be in one of PHP's supported `date and time formats`_. You can pass a timezone as the second argument: From a8aadc3e99361597172a9a6f7027c77ba5627239 Mon Sep 17 00:00:00 2001 From: Doeke Norg Date: Fri, 23 May 2025 09:29:36 +0200 Subject: [PATCH 782/812] Update configuration keys + allow extra keys for extensions --- .../DependencyInjection/Configuration.php | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/extra/twig-extra-bundle/DependencyInjection/Configuration.php b/extra/twig-extra-bundle/DependencyInjection/Configuration.php index fc94eaca362..8b6f33c58e7 100644 --- a/extra/twig-extra-bundle/DependencyInjection/Configuration.php +++ b/extra/twig-extra-bundle/DependencyInjection/Configuration.php @@ -39,14 +39,14 @@ public function getConfigTreeBuilder(): TreeBuilder } /** - * Full configuration from {@link https://commonmark.thephpleague.com/2.3/configuration}. + * Full configuration from {@link https://commonmark.thephpleague.com/2.7/configuration}. */ private function addCommonMarkConfiguration(ArrayNodeDefinition $rootNode): void { $rootNode ->children() ->arrayNode('commonmark') - ->ignoreExtraKeys() + ->ignoreExtraKeys(false) ->children() ->arrayNode('renderer') ->info('Array of options for rendering HTML.') @@ -68,6 +68,10 @@ private function addCommonMarkConfiguration(ArrayNodeDefinition $rootNode): void ->info('The maximum nesting level for blocks.') ->defaultValue(\PHP_INT_MAX) ->end() + ->integerNode('max_delimiters_per_line') + ->info('The maximum number of strong/emphasis delimiters per line.') + ->defaultValue(\PHP_INT_MAX) + ->end() ->arrayNode('slug_normalizer') ->info('Array of options for configuring how URL-safe slugs are created.') ->children() From a26f43a6a6641b68b15ccdd52ab3e2a141d6f5ef Mon Sep 17 00:00:00 2001 From: Thomas Landauer Date: Thu, 5 Jun 2025 23:22:30 +0200 Subject: [PATCH 783/812] Update templates.rst: Removing duplication Page: https://twig.symfony.com/doc/3.x/templates.html#dot_operator Reason: This is repeated a few lines further down. --- doc/templates.rst | 3 --- 1 file changed, 3 deletions(-) diff --git a/doc/templates.rst b/doc/templates.rst index 94312cd88c1..ad281e4e6c0 100644 --- a/doc/templates.rst +++ b/doc/templates.rst @@ -871,9 +871,6 @@ The following operators don't fit into any of the other categories: {{ user.name }} - Twig supports a specific syntax via the ``[]`` operator for accessing items - on sequences and mappings, like in ``user['name']``: - After the ``.``, you can use any expression by wrapping it with parenthesis ``()``. From 0b93a1fa5aa0a29c14f5fd31139746643b53639a Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Thu, 12 Jun 2025 08:47:12 +0200 Subject: [PATCH 784/812] Stof suggestion about empty content --- doc/coding_standards.rst | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/doc/coding_standards.rst b/doc/coding_standards.rst index 58272883eba..5b10ef74a75 100644 --- a/doc/coding_standards.rst +++ b/doc/coding_standards.rst @@ -11,12 +11,13 @@ When writing Twig templates, we recommend you to follow these official coding standards: * Put exactly one space after the start of a delimiter (``{{``, ``{%``, - and ``{#``) and before the end of a delimiter (``}}``, ``%}``, and ``#}``): + and ``{#``) and before the end of a delimiter (``}}``, ``%}``, and ``#}``) + if the content is non empty: .. code-block:: twig {{ user }} - {# comment #} + {# comment #} {##} {% if user %}{% endif %} When using the whitespace control character, do not put any spaces between @@ -25,7 +26,7 @@ standards: .. code-block:: twig {{- user -}} - {#- comment -#} + {#- comment -#} {#--#} {%- if user -%}{%- endif -%} * Put exactly one space before and after the following operators: From 81e66e96bf0e55e78e171e8ec7fc4b0b1b6214dd Mon Sep 17 00:00:00 2001 From: Hannes <31671206+xJuvi@users.noreply.github.com> Date: Tue, 17 Jun 2025 23:57:57 +0200 Subject: [PATCH 785/812] Update LeagueMarkdown.php --- extra/markdown-extra/LeagueMarkdown.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/extra/markdown-extra/LeagueMarkdown.php b/extra/markdown-extra/LeagueMarkdown.php index be266770240..edd2bfd6c01 100644 --- a/extra/markdown-extra/LeagueMarkdown.php +++ b/extra/markdown-extra/LeagueMarkdown.php @@ -12,13 +12,14 @@ namespace Twig\Extra\Markdown; use League\CommonMark\CommonMarkConverter; +use League\CommonMark\MarkdownConverter; class LeagueMarkdown implements MarkdownInterface { private $converter; private $legacySupport; - public function __construct(?CommonMarkConverter $converter = null) + public function __construct(?MarkdownConverter $converter = null) { $this->converter = $converter ?: new CommonMarkConverter(); $this->legacySupport = !method_exists($this->converter, 'convert'); From b8827b412d3a7a779bb281bf386196553b72bf72 Mon Sep 17 00:00:00 2001 From: hschletz Date: Wed, 2 Jul 2025 12:45:53 +0200 Subject: [PATCH 786/812] Add documentation for use_yield option Documentation was present in the source code, but missing in the docs. --- doc/api.rst | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/doc/api.rst b/doc/api.rst index 5469dca722d..24b1baea743 100644 --- a/doc/api.rst +++ b/doc/api.rst @@ -145,6 +145,17 @@ The following options are available: (default to ``-1`` -- all optimizations are enabled; set it to ``0`` to disable). +* ``use_yield`` *boolean* + + ``true``: forces templates to exclusively use ``yield`` instead of ``echo`` + (all extensions must be yield ready) + + ``false`` (default): allows templates to use a mix of ``yield`` and ``echo`` + calls to allow for a progressive migration. + + Switch to ``true`` when possible as this will be the only supported mode in + Twig 4.0. + Loaders ------- From b6a105c952b827dcac587a23486f3e57887ff62a Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Thu, 10 Jul 2025 11:59:09 +0200 Subject: [PATCH 787/812] Fix compatibility with Symfony 8 --- .../MissingExtensionSuggestorPass.php | 29 +++++++++++++++---- .../TwigExtraExtension.php | 27 +++++++++++++++-- extra/twig-extra-bundle/TwigExtraBundle.php | 24 +++++++++++---- 3 files changed, 67 insertions(+), 13 deletions(-) diff --git a/extra/twig-extra-bundle/DependencyInjection/Compiler/MissingExtensionSuggestorPass.php b/extra/twig-extra-bundle/DependencyInjection/Compiler/MissingExtensionSuggestorPass.php index 83c6643f09d..79d7baa44d6 100644 --- a/extra/twig-extra-bundle/DependencyInjection/Compiler/MissingExtensionSuggestorPass.php +++ b/extra/twig-extra-bundle/DependencyInjection/Compiler/MissingExtensionSuggestorPass.php @@ -15,12 +15,31 @@ use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Reference; -class MissingExtensionSuggestorPass implements CompilerPassInterface -{ - /** @return void */ - public function process(ContainerBuilder $container) +if (!method_exists(ContainerBuilder::class, 'getAutoconfiguredAttributes')) { + class MissingExtensionSuggestorPass implements CompilerPassInterface { - if ($container->getParameter('kernel.debug')) { + public function process(ContainerBuilder $container): void + { + if (!$container->getParameter('kernel.debug')) { + return; + } + $twigDefinition = $container->getDefinition('twig'); + $twigDefinition + ->addMethodCall('registerUndefinedFilterCallback', [[new Reference('twig.missing_extension_suggestor'), 'suggestFilter']]) + ->addMethodCall('registerUndefinedFunctionCallback', [[new Reference('twig.missing_extension_suggestor'), 'suggestFunction']]) + ->addMethodCall('registerUndefinedTokenParserCallback', [[new Reference('twig.missing_extension_suggestor'), 'suggestTag']]) + ; + } + } +} else { + class MissingExtensionSuggestorPass implements CompilerPassInterface + { + /** @return void */ + public function process(ContainerBuilder $container) + { + if (!$container->getParameter('kernel.debug')) { + return; + } $twigDefinition = $container->getDefinition('twig'); $twigDefinition ->addMethodCall('registerUndefinedFilterCallback', [[new Reference('twig.missing_extension_suggestor'), 'suggestFilter']]) diff --git a/extra/twig-extra-bundle/DependencyInjection/TwigExtraExtension.php b/extra/twig-extra-bundle/DependencyInjection/TwigExtraExtension.php index 8c399b891ce..7c5a7c7db16 100644 --- a/extra/twig-extra-bundle/DependencyInjection/TwigExtraExtension.php +++ b/extra/twig-extra-bundle/DependencyInjection/TwigExtraExtension.php @@ -18,13 +18,36 @@ use Symfony\Component\DependencyInjection\Loader\PhpFileLoader; use Twig\Extra\TwigExtraBundle\Extensions; +if (!method_exists(ContainerBuilder::class, 'getAutoconfiguredAttributes')) { + /** @internal */ + trait TwigExtraExtensionTrait + { + public function load(array $configs, ContainerBuilder $container): void + { + $this->doLoad($configs, $container); + } + } +} else { + /** @internal */ + trait TwigExtraExtensionTrait + { + /** @return void */ + public function load(array $configs, ContainerBuilder $container) + { + $this->doLoad($configs, $container); + } + } + +} + /** * @author Fabien Potencier */ class TwigExtraExtension extends Extension { - /** @return void */ - public function load(array $configs, ContainerBuilder $container) + use TwigExtraExtensionTrait; + + private function doLoad(array $configs, ContainerBuilder $container): void { $loader = new PhpFileLoader($container, new FileLocator(\dirname(__DIR__).'/Resources/config')); $configuration = $this->getConfiguration($configs, $container); diff --git a/extra/twig-extra-bundle/TwigExtraBundle.php b/extra/twig-extra-bundle/TwigExtraBundle.php index a9c8f734bf8..e2d181145f6 100644 --- a/extra/twig-extra-bundle/TwigExtraBundle.php +++ b/extra/twig-extra-bundle/TwigExtraBundle.php @@ -15,13 +15,25 @@ use Symfony\Component\HttpKernel\Bundle\Bundle; use Twig\Extra\TwigExtraBundle\DependencyInjection\Compiler\MissingExtensionSuggestorPass; -class TwigExtraBundle extends Bundle -{ - /** @return void */ - public function build(ContainerBuilder $container) +if (!method_exists(ContainerBuilder::class, 'getAutoconfiguredAttributes')) { + class TwigExtraBundle extends Bundle { - parent::build($container); + public function build(ContainerBuilder $container): void + { + parent::build($container); - $container->addCompilerPass(new MissingExtensionSuggestorPass()); + $container->addCompilerPass(new MissingExtensionSuggestorPass()); + } + } +} else { + class TwigExtraBundle extends Bundle + { + /** @return void */ + public function build(ContainerBuilder $container) + { + parent::build($container); + + $container->addCompilerPass(new MissingExtensionSuggestorPass()); + } } } From 823f50297b3b63830b9077d6d07fee6592629bce Mon Sep 17 00:00:00 2001 From: Gregor Harlan Date: Sat, 5 Jul 2025 15:01:44 +0200 Subject: [PATCH 788/812] Escaper performance: avoid static variables --- src/Runtime/EscaperRuntime.php | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/Runtime/EscaperRuntime.php b/src/Runtime/EscaperRuntime.php index 17ed76cc955..9ed8775dd2f 100644 --- a/src/Runtime/EscaperRuntime.php +++ b/src/Runtime/EscaperRuntime.php @@ -140,6 +140,10 @@ public function escape($string, string $strategy = 'html', ?string $charset = nu case 'html': // see https://www.php.net/htmlspecialchars + if ('UTF-8' === $charset) { + return htmlspecialchars($string, \ENT_QUOTES | \ENT_SUBSTITUTE, 'UTF-8'); + } + // Using a static variable to avoid initializing the array // each time the function is called. Moving the declaration on the // top of the function slow downs other escaping strategies. @@ -195,7 +199,7 @@ public function escape($string, string $strategy = 'html', ?string $charset = nu * Escape sequences supported only by JavaScript, not JSON, are omitted. * \" is also supported but omitted, because the resulting string is not HTML safe. */ - static $shortMap = [ + $short = match ($char) { '\\' => '\\\\', '/' => '\\/', "\x08" => '\b', @@ -203,10 +207,11 @@ public function escape($string, string $strategy = 'html', ?string $charset = nu "\x0A" => '\n', "\x0D" => '\r', "\x09" => '\t', - ]; + default => false, + }; - if (isset($shortMap[$char])) { - return $shortMap[$char]; + if ($short) { + return $short; } $codepoint = mb_ord($char, 'UTF-8'); @@ -288,18 +293,13 @@ public function escape($string, string $strategy = 'html', ?string $charset = nu * entities that XML supports. Using HTML entities would result in this error: * XML Parsing Error: undefined entity */ - static $entityMap = [ + return match ($ord) { 34 => '"', /* quotation mark */ 38 => '&', /* ampersand */ 60 => '<', /* less-than sign */ 62 => '>', /* greater-than sign */ - ]; - - if (isset($entityMap[$ord])) { - return $entityMap[$ord]; - } - - return \sprintf('&#x%02X;', $ord); + default => \sprintf('&#x%02X;', $ord), + }; } /* From a1ea8edc9492ad5331db144535f129167a71a339 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Mon, 28 Jul 2025 18:49:18 +0200 Subject: [PATCH 789/812] Enable Fabbot as a GHA --- .github/workflows/fabbot.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 .github/workflows/fabbot.yml diff --git a/.github/workflows/fabbot.yml b/.github/workflows/fabbot.yml new file mode 100644 index 00000000000..7aa4bc682d6 --- /dev/null +++ b/.github/workflows/fabbot.yml @@ -0,0 +1,14 @@ +name: CS + +on: + pull_request: + +permissions: + contents: read + +jobs: + call-fabbot: + name: Fabbot + uses: symfony-tools/fabbot/.github/workflows/fabbot.yml@main + with: + package: Twig From 85a4817128d4df83d47a315270d29e94dd94bf62 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Thu, 10 Jul 2025 11:37:14 +0200 Subject: [PATCH 790/812] CS fixes --- .php-cs-fixer.dist.php | 14 +++++++++++--- bin/generate_operators_precedence.php | 12 ++++++------ doc/_build/build.php | 13 +++++++++++-- extra/cssinliner-extra/CssInlinerExtension.php | 4 ++-- extra/cssinliner-extra/Resources/functions.php | 4 ++-- extra/inky-extra/InkyExtension.php | 4 ++-- extra/inky-extra/Resources/functions.php | 4 ++-- extra/markdown-extra/DefaultMarkdown.php | 3 +-- extra/markdown-extra/ErusevMarkdown.php | 6 ++---- .../Compiler/MissingExtensionSuggestorPass.php | 4 ++-- .../twig-extra-bundle/Tests/Fixture/Kernel.php | 9 +++++++++ .../Tests/IntegrationTest.php | 9 +++++++++ src/Attribute/AsTwigFilter.php | 18 +++++++++--------- src/Attribute/AsTwigFunction.php | 14 +++++++------- src/Attribute/AsTwigTest.php | 10 +++++----- src/Error/Error.php | 2 +- src/Extension/AttributeExtension.php | 7 +++---- src/Node/Expression/Binary/MatchesBinary.php | 2 +- .../FunctionNode/EnumCasesFunction.php | 9 +++++++++ .../Expression/FunctionNode/EnumFunction.php | 9 +++++++++ src/Node/Expression/GetAttrExpression.php | 4 ++-- src/Node/Expression/Test/DefinedTest.php | 8 -------- src/Node/TypesNode.php | 9 +++++++++ src/Resources/debug.php | 4 ++-- src/Resources/string_loader.php | 4 ++-- src/Runtime/EscaperRuntime.php | 2 +- src/Template.php | 4 ++-- tests/Cache/ChainTest.php | 9 +++++++++ tests/Cache/FilesystemTest.php | 9 +++++++++ tests/Cache/ReadOnlyFilesystemTest.php | 9 +++++++++ tests/CompilerTest.php | 9 +++++++++ tests/ContainerRuntimeLoaderTest.php | 9 +++++++++ tests/CustomExtensionTest.php | 9 +++++++++ tests/DeprecatedCallableInfoTest.php | 9 +++++++++ tests/DummyBackedEnum.php | 9 +++++++++ tests/DummyUnitEnum.php | 9 +++++++++ tests/EnvironmentTest.php | 9 +++++++++ tests/ErrorTest.php | 14 +++++++++++--- tests/ExpressionParserTest.php | 9 +++++++++ tests/Extension/AttributeExtensionTest.php | 17 +++++++++++++---- tests/Extension/CoreTest.php | 9 +++++++++ tests/Extension/EscaperTest.php | 9 +++++++++ .../Fixtures/ExtensionWithAttributes.php | 9 +++++++++ .../Extension/Fixtures/FilterWithoutValue.php | 9 +++++++++ tests/Extension/Fixtures/TestWithoutValue.php | 9 +++++++++ tests/Extension/SandboxTest.php | 9 +++++++++ tests/FactoryRuntimeLoaderTest.php | 9 +++++++++ tests/FileExtensionEscapingStrategyTest.php | 9 +++++++++ tests/FilesystemHelper.php | 9 +++++++++ tests/IntegrationTest.php | 9 +++++++++ tests/LexerTest.php | 9 +++++++++ tests/Loader/ArrayTest.php | 9 +++++++++ tests/Loader/ChainTest.php | 9 +++++++++ tests/Loader/FilesystemTest.php | 9 +++++++++ tests/Node/AutoEscapeTest.php | 9 +++++++++ tests/Node/BlockReferenceTest.php | 9 +++++++++ tests/Node/BlockTest.php | 9 +++++++++ tests/Node/DeprecatedTest.php | 9 +++++++++ tests/Node/DoTest.php | 9 +++++++++ tests/Node/Expression/ArrayTest.php | 9 +++++++++ tests/Node/Expression/Binary/AddTest.php | 9 +++++++++ tests/Node/Expression/Binary/AndTest.php | 9 +++++++++ tests/Node/Expression/Binary/ConcatTest.php | 9 +++++++++ tests/Node/Expression/Binary/DivTest.php | 9 +++++++++ tests/Node/Expression/Binary/FloorDivTest.php | 9 +++++++++ tests/Node/Expression/Binary/ModTest.php | 9 +++++++++ tests/Node/Expression/Binary/MulTest.php | 9 +++++++++ .../Expression/Binary/NullCoalesceTest.php | 9 +++++++++ tests/Node/Expression/Binary/OrTest.php | 9 +++++++++ tests/Node/Expression/Binary/SubTest.php | 9 +++++++++ tests/Node/Expression/CallTest.php | 9 +++++++++ tests/Node/Expression/ConditionalTest.php | 9 +++++++++ tests/Node/Expression/ConstantTest.php | 9 +++++++++ tests/Node/Expression/Filter/RawTest.php | 9 +++++++++ tests/Node/Expression/FilterTest.php | 9 +++++++++ tests/Node/Expression/FilterTestExtension.php | 9 +++++++++ tests/Node/Expression/FunctionTest.php | 9 +++++++++ tests/Node/Expression/GetAttrTest.php | 9 +++++++++ tests/Node/Expression/NullCoalesceTest.php | 9 +++++++++ tests/Node/Expression/ParentTest.php | 9 +++++++++ .../Ternary/ConditionalTernaryTest.php | 9 +++++++++ tests/Node/Expression/TestTest.php | 9 +++++++++ tests/Node/Expression/Unary/NegTest.php | 9 +++++++++ tests/Node/Expression/Unary/NotTest.php | 9 +++++++++ tests/Node/Expression/Unary/PosTest.php | 9 +++++++++ .../Variable/AssignContextVariableTest.php | 9 +++++++++ .../Variable/ContextVariableTest.php | 9 +++++++++ tests/Node/ForTest.php | 9 +++++++++ tests/Node/IfTest.php | 9 +++++++++ tests/Node/ImportTest.php | 9 +++++++++ tests/Node/IncludeTest.php | 9 +++++++++ tests/Node/MacroTest.php | 9 +++++++++ tests/Node/ModuleTest.php | 9 +++++++++ tests/Node/NodeTest.php | 9 +++++++++ tests/Node/PrintTest.php | 9 +++++++++ tests/Node/SandboxTest.php | 9 +++++++++ tests/Node/SetTest.php | 9 +++++++++ tests/Node/TextTest.php | 9 +++++++++ tests/Node/TypesTest.php | 9 +++++++++ tests/NodeVisitor/OptimizerTest.php | 9 +++++++++ tests/NodeVisitor/SandboxTest.php | 9 +++++++++ tests/ParserTest.php | 9 +++++++++ tests/Profiler/Dumper/BlackfireTest.php | 9 +++++++++ tests/Profiler/Dumper/HtmlTest.php | 9 +++++++++ tests/Profiler/Dumper/ProfilerTestCase.php | 9 +++++++++ tests/Profiler/Dumper/TextTest.php | 9 +++++++++ tests/Profiler/ProfileTest.php | 9 +++++++++ tests/Runtime/EscaperRuntimeTest.php | 9 +++++++++ tests/TemplateTest.php | 9 +++++++++ tests/TemplateWrapperTest.php | 9 +++++++++ tests/TokenParser/GuardTokenParserTest.php | 9 +++++++++ tests/TokenParser/TypesTokenParserTest.php | 9 +++++++++ tests/TokenStreamTest.php | 9 +++++++++ tests/Util/CallableArgumentsExtractorTest.php | 9 +++++++++ tests/Util/DeprecationCollectorTest.php | 9 +++++++++ 115 files changed, 919 insertions(+), 78 deletions(-) diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php index 5c3a731a16e..b57df306ac6 100644 --- a/.php-cs-fixer.dist.php +++ b/.php-cs-fixer.dist.php @@ -13,9 +13,17 @@ 'heredoc_to_nowdoc' => false, 'ordered_imports' => true, 'phpdoc_types_order' => ['null_adjustment' => 'always_last', 'sort_algorithm' => 'none'], - // TODO: Remove once the "compiler_optimized" set includes "sprintf" - 'native_function_invocation' => ['include' => ['@compiler_optimized', 'sprintf'], 'scope' => 'all'], - ]) + 'header_comment' => [ + 'header' => <<setRiskyAllowed(true) ->setFinder((new PhpCsFixer\Finder())->in(__DIR__)) ; diff --git a/bin/generate_operators_precedence.php b/bin/generate_operators_precedence.php index 31477be6236..185a0147ed6 100644 --- a/bin/generate_operators_precedence.php +++ b/bin/generate_operators_precedence.php @@ -16,9 +16,9 @@ use Twig\ExpressionParser\InfixExpressionParserInterface; use Twig\Loader\ArrayLoader; -require_once \dirname(__DIR__).'/vendor/autoload.php'; +require_once dirname(__DIR__).'/vendor/autoload.php'; -$output = fopen(\dirname(__DIR__).'/doc/operators_precedence.rst', 'w'); +$output = fopen(dirname(__DIR__).'/doc/operators_precedence.rst', 'w'); $twig = new Environment(new ArrayLoader([])); $descriptionLength = 11; @@ -29,7 +29,7 @@ } fwrite($output, "\n+------------+------------------+---------+---------------+".str_repeat('-', $descriptionLength + 2)."+\n"); -fwrite($output, "| Precedence | Operator | Type | Associativity | Description".str_repeat(' ', $descriptionLength - 11)." |\n"); +fwrite($output, '| Precedence | Operator | Type | Associativity | Description'.str_repeat(' ', $descriptionLength - 11)." |\n"); fwrite($output, '+============+==================+=========+===============+'.str_repeat('=', $descriptionLength + 2).'+'); usort($expressionParsers, fn ($a, $b) => $b->getPrecedence() <=> $a->getPrecedence()); @@ -46,7 +46,7 @@ if ($previousPrecedence !== $precedence) { $previous = null; } - fwrite($output, rtrim(\sprintf("\n| %-10s | %-16s | %-7s | %-13s | %-{$descriptionLength}s |\n", + fwrite($output, rtrim(sprintf("\n| %-10s | %-16s | %-7s | %-13s | %-{$descriptionLength}s |\n", (!$previous || $previousPrecedence !== $precedence ? $precedence : '').($expressionParser->getPrecedenceChange() ? ' => '.$expressionParser->getPrecedenceChange()->getNewPrecedence() : ''), '``'.$expressionParser->getName().'``', !$previous || ExpressionParserType::getType($previous) !== ExpressionParserType::getType($expressionParser) ? ExpressionParserType::getType($expressionParser)->value : '', @@ -61,7 +61,7 @@ fwrite($output, "\nHere is the same table for Twig 4.0 with adjusted precedences:\n"); fwrite($output, "\n+------------+------------------+---------+---------------+".str_repeat('-', $descriptionLength + 2)."+\n"); -fwrite($output, "| Precedence | Operator | Type | Associativity | Description".str_repeat(' ', $descriptionLength - 11)." |\n"); +fwrite($output, '| Precedence | Operator | Type | Associativity | Description'.str_repeat(' ', $descriptionLength - 11)." |\n"); fwrite($output, '+============+==================+=========+===============+'.str_repeat('=', $descriptionLength + 2).'+'); usort($expressionParsers, function ($a, $b) { @@ -83,7 +83,7 @@ if ($previousPrecedence !== $precedence) { $previous = null; } - fwrite($output, rtrim(\sprintf("\n| %-10s | %-16s | %-7s | %-13s | %-{$descriptionLength}s |\n", + fwrite($output, rtrim(sprintf("\n| %-10s | %-16s | %-7s | %-13s | %-{$descriptionLength}s |\n", !$previous || $previousPrecedence !== $precedence ? $precedence : '', '``'.$expressionParser->getName().'``', !$previous || ExpressionParserType::getType($previous) !== ExpressionParserType::getType($expressionParser) ? ExpressionParserType::getType($expressionParser)->value : '', diff --git a/doc/_build/build.php b/doc/_build/build.php index 2b183b1867b..25950f78977 100755 --- a/doc/_build/build.php +++ b/doc/_build/build.php @@ -1,6 +1,15 @@ #!/usr/bin/env php success(\sprintf('The Twig docs were successfully built at %s', realpath($outputDir))); + $io->success(sprintf('The Twig docs were successfully built at %s', realpath($outputDir))); } else { - $io->error(\sprintf("There were some errors while building the docs:\n\n%s\n", $result->getErrorTrace())); + $io->error(sprintf("There were some errors while building the docs:\n\n%s\n", $result->getErrorTrace())); $io->newLine(); $io->comment('Tip: you can add the -v, -vv or -vvv flags to this command to get debug information.'); diff --git a/extra/cssinliner-extra/CssInlinerExtension.php b/extra/cssinliner-extra/CssInlinerExtension.php index 94d3c4b7f4d..0447b4df037 100644 --- a/extra/cssinliner-extra/CssInlinerExtension.php +++ b/extra/cssinliner-extra/CssInlinerExtension.php @@ -1,9 +1,9 @@ + * (c) Fabien Potencier * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. diff --git a/extra/cssinliner-extra/Resources/functions.php b/extra/cssinliner-extra/Resources/functions.php index 60305e231c5..d2bcecafda7 100644 --- a/extra/cssinliner-extra/Resources/functions.php +++ b/extra/cssinliner-extra/Resources/functions.php @@ -1,9 +1,9 @@ + * (c) Fabien Potencier * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. diff --git a/extra/inky-extra/InkyExtension.php b/extra/inky-extra/InkyExtension.php index 9ee4f823abc..b8ac22da9e9 100644 --- a/extra/inky-extra/InkyExtension.php +++ b/extra/inky-extra/InkyExtension.php @@ -1,9 +1,9 @@ + * (c) Fabien Potencier * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. diff --git a/extra/inky-extra/Resources/functions.php b/extra/inky-extra/Resources/functions.php index 0fa4111debb..9ebe920454b 100644 --- a/extra/inky-extra/Resources/functions.php +++ b/extra/inky-extra/Resources/functions.php @@ -1,9 +1,9 @@ + * (c) Fabien Potencier * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. diff --git a/extra/markdown-extra/DefaultMarkdown.php b/extra/markdown-extra/DefaultMarkdown.php index 6650a661a53..a20993d45da 100644 --- a/extra/markdown-extra/DefaultMarkdown.php +++ b/extra/markdown-extra/DefaultMarkdown.php @@ -13,7 +13,6 @@ use League\CommonMark\CommonMarkConverter; use Michelf\MarkdownExtra; -use Parsedown; class DefaultMarkdown implements MarkdownInterface { @@ -25,7 +24,7 @@ public function __construct() $this->converter = new LeagueMarkdown(); } elseif (class_exists(MarkdownExtra::class)) { $this->converter = new MichelfMarkdown(); - } elseif (class_exists(Parsedown::class)) { + } elseif (class_exists(\Parsedown::class)) { $this->converter = new ErusevMarkdown(); } else { throw new \LogicException('You cannot use the "markdown_to_html" filter as no Markdown library is available; try running "composer require league/commonmark".'); diff --git a/extra/markdown-extra/ErusevMarkdown.php b/extra/markdown-extra/ErusevMarkdown.php index 47b030893e5..923cf0eebcd 100644 --- a/extra/markdown-extra/ErusevMarkdown.php +++ b/extra/markdown-extra/ErusevMarkdown.php @@ -11,15 +11,13 @@ namespace Twig\Extra\Markdown; -use Parsedown; - class ErusevMarkdown implements MarkdownInterface { private $converter; - public function __construct(?Parsedown $converter = null) + public function __construct(?\Parsedown $converter = null) { - $this->converter = $converter ?: new Parsedown(); + $this->converter = $converter ?: new \Parsedown(); } public function convert(string $body): string diff --git a/extra/twig-extra-bundle/DependencyInjection/Compiler/MissingExtensionSuggestorPass.php b/extra/twig-extra-bundle/DependencyInjection/Compiler/MissingExtensionSuggestorPass.php index 83c6643f09d..0ab51cb2db3 100644 --- a/extra/twig-extra-bundle/DependencyInjection/Compiler/MissingExtensionSuggestorPass.php +++ b/extra/twig-extra-bundle/DependencyInjection/Compiler/MissingExtensionSuggestorPass.php @@ -1,9 +1,9 @@ + * (c) Fabien Potencier * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. diff --git a/extra/twig-extra-bundle/Tests/Fixture/Kernel.php b/extra/twig-extra-bundle/Tests/Fixture/Kernel.php index faad85c187a..8986f8ecd92 100644 --- a/extra/twig-extra-bundle/Tests/Fixture/Kernel.php +++ b/extra/twig-extra-bundle/Tests/Fixture/Kernel.php @@ -1,5 +1,14 @@ getTrace(); - array_unshift($traces, ['file' => $e instanceof Error ? $e->phpFile : $e->getFile(), 'line' => $e instanceof Error ? $e->phpLine : $e->getLine()]); + array_unshift($traces, ['file' => $e instanceof self ? $e->phpFile : $e->getFile(), 'line' => $e instanceof self ? $e->phpLine : $e->getLine()]); while ($trace = array_shift($traces)) { if (!isset($trace['file']) || !isset($trace['line']) || $file != $trace['file']) { continue; diff --git a/src/Extension/AttributeExtension.php b/src/Extension/AttributeExtension.php index 44e4f3f6bdd..74fcbb85706 100644 --- a/src/Extension/AttributeExtension.php +++ b/src/Extension/AttributeExtension.php @@ -104,7 +104,7 @@ private function initFromAttributes(): void ]); if ($callable->getMinimalNumberOfRequiredArguments() > $method->getNumberOfParameters()) { - throw new \LogicException(sprintf('"%s::%s()" needs at least %d arguments to be used AsTwigFilter, but only %d defined.', $reflectionClass->getName(), $method->getName(), $callable->getMinimalNumberOfRequiredArguments(), $method->getNumberOfParameters())); + throw new \LogicException(\sprintf('"%s::%s()" needs at least %d arguments to be used AsTwigFilter, but only %d defined.', $reflectionClass->getName(), $method->getName(), $callable->getMinimalNumberOfRequiredArguments(), $method->getNumberOfParameters())); } $filters[$attribute->name] = $callable; @@ -125,14 +125,13 @@ private function initFromAttributes(): void ]); if ($callable->getMinimalNumberOfRequiredArguments() > $method->getNumberOfParameters()) { - throw new \LogicException(sprintf('"%s::%s()" needs at least %d arguments to be used AsTwigFunction, but only %d defined.', $reflectionClass->getName(), $method->getName(), $callable->getMinimalNumberOfRequiredArguments(), $method->getNumberOfParameters())); + throw new \LogicException(\sprintf('"%s::%s()" needs at least %d arguments to be used AsTwigFunction, but only %d defined.', $reflectionClass->getName(), $method->getName(), $callable->getMinimalNumberOfRequiredArguments(), $method->getNumberOfParameters())); } $functions[$attribute->name] = $callable; } foreach ($method->getAttributes(AsTwigTest::class) as $reflectionAttribute) { - /** @var AsTwigTest $attribute */ $attribute = $reflectionAttribute->newInstance(); @@ -145,7 +144,7 @@ private function initFromAttributes(): void ]); if ($callable->getMinimalNumberOfRequiredArguments() > $method->getNumberOfParameters()) { - throw new \LogicException(sprintf('"%s::%s()" needs at least %d arguments to be used AsTwigTest, but only %d defined.', $reflectionClass->getName(), $method->getName(), $callable->getMinimalNumberOfRequiredArguments(), $method->getNumberOfParameters())); + throw new \LogicException(\sprintf('"%s::%s()" needs at least %d arguments to be used AsTwigTest, but only %d defined.', $reflectionClass->getName(), $method->getName(), $callable->getMinimalNumberOfRequiredArguments(), $method->getNumberOfParameters())); } $tests[$attribute->name] = $callable; diff --git a/src/Node/Expression/Binary/MatchesBinary.php b/src/Node/Expression/Binary/MatchesBinary.php index 32e8d34e45f..569dfde05f1 100644 --- a/src/Node/Expression/Binary/MatchesBinary.php +++ b/src/Node/Expression/Binary/MatchesBinary.php @@ -13,8 +13,8 @@ use Twig\Compiler; use Twig\Error\SyntaxError; -use Twig\Node\Expression\ReturnBoolInterface; use Twig\Node\Expression\ConstantExpression; +use Twig\Node\Expression\ReturnBoolInterface; use Twig\Node\Node; class MatchesBinary extends AbstractBinary implements ReturnBoolInterface diff --git a/src/Node/Expression/FunctionNode/EnumCasesFunction.php b/src/Node/Expression/FunctionNode/EnumCasesFunction.php index 7e5c25ff46a..170d0a13b93 100644 --- a/src/Node/Expression/FunctionNode/EnumCasesFunction.php +++ b/src/Node/Expression/FunctionNode/EnumCasesFunction.php @@ -1,5 +1,14 @@ setAttribute('optimizable', false); $node->setAttribute('ignore_strict_check', true); - if ($node->getNode('node') instanceof GetAttrExpression) { + if ($node->getNode('node') instanceof self) { $this->changeIgnoreStrictCheck($node->getNode('node')); } } diff --git a/src/Node/Expression/Test/DefinedTest.php b/src/Node/Expression/Test/DefinedTest.php index f17715bc61c..d735029901f 100644 --- a/src/Node/Expression/Test/DefinedTest.php +++ b/src/Node/Expression/Test/DefinedTest.php @@ -15,16 +15,8 @@ use Twig\Compiler; use Twig\Error\SyntaxError; use Twig\Node\Expression\AbstractExpression; -use Twig\Node\Expression\ArrayExpression; -use Twig\Node\Expression\BlockReferenceExpression; -use Twig\Node\Expression\ConstantExpression; -use Twig\Node\Expression\FunctionExpression; -use Twig\Node\Expression\GetAttrExpression; -use Twig\Node\Expression\MacroReferenceExpression; -use Twig\Node\Expression\MethodCallExpression; use Twig\Node\Expression\SupportDefinedTestInterface; use Twig\Node\Expression\TestExpression; -use Twig\Node\Expression\Variable\ContextVariable; use Twig\Node\Node; use Twig\TwigTest; diff --git a/src/Node/TypesNode.php b/src/Node/TypesNode.php index b5949848d1a..a1828808385 100644 --- a/src/Node/TypesNode.php +++ b/src/Node/TypesNode.php @@ -1,5 +1,14 @@ + * (c) Fabien Potencier * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. diff --git a/src/Resources/string_loader.php b/src/Resources/string_loader.php index 8f0e6492aab..c499e5ec2b0 100644 --- a/src/Resources/string_loader.php +++ b/src/Resources/string_loader.php @@ -1,9 +1,9 @@ + * (c) Fabien Potencier * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. diff --git a/src/Runtime/EscaperRuntime.php b/src/Runtime/EscaperRuntime.php index 17ed76cc955..3b3148fae4b 100644 --- a/src/Runtime/EscaperRuntime.php +++ b/src/Runtime/EscaperRuntime.php @@ -17,7 +17,7 @@ final class EscaperRuntime implements RuntimeExtensionInterface { - /** @var array */ + /** @var array */ private $escapers = []; /** @internal */ diff --git a/src/Template.php b/src/Template.php index faf7aee1e0d..c3720928746 100644 --- a/src/Template.php +++ b/src/Template.php @@ -270,7 +270,7 @@ public function getBlockNames(array $context, array $blocks = []): array /** * @param string|TemplateWrapper|array $template */ - protected function load(string|TemplateWrapper|array $template, int $line, int|null $index = null): self + protected function load(string|TemplateWrapper|array $template, int $line, ?int $index = null): self { try { if (\is_array($template)) { @@ -315,7 +315,7 @@ protected function load(string|TemplateWrapper|array $template, int $line, int|n * * @deprecated since Twig 3.21 and will be removed in 4.0. Use Template::load() instead. */ - protected function loadTemplate($template, $templateName = null, int|null $line = null, int|null $index = null): self|TemplateWrapper + protected function loadTemplate($template, $templateName = null, ?int $line = null, ?int $index = null): self|TemplateWrapper { trigger_deprecation('twig/twig', '3.21', 'The "%s" method is deprecated.', __METHOD__); diff --git a/tests/Cache/ChainTest.php b/tests/Cache/ChainTest.php index 3120ab18861..4383e603430 100644 --- a/tests/Cache/ChainTest.php +++ b/tests/Cache/ChainTest.php @@ -1,5 +1,14 @@ getCurrent()->getLine(); $stream->expect(Token::BLOCK_END_TYPE); - return new #[YieldReady]class($lineno, $this->addDebugInfo, $this->exceptionWithLineAndContext) extends Node - { + return new #[YieldReady] class($lineno, $this->addDebugInfo, $this->exceptionWithLineAndContext) extends Node { public function __construct(int $lineno, private bool $addDebugInfo, private bool $exceptionWithLineAndContext) { parent::__construct([], [], $lineno); @@ -295,7 +303,7 @@ public function compile(Compiler $compiler): void if ($this->exceptionWithLineAndContext) { $compiler ->write('throw new \Twig\Error\RuntimeError("Runtime error.", ') - ->repr($this->lineno)->raw(", \$this->getSourceContext()") + ->repr($this->lineno)->raw(', $this->getSourceContext()') ->raw(");\n") ; } else { diff --git a/tests/ExpressionParserTest.php b/tests/ExpressionParserTest.php index f134052927f..4f16858208d 100644 --- a/tests/ExpressionParserTest.php +++ b/tests/ExpressionParserTest.php @@ -1,5 +1,14 @@ fail(sprintf('Filter "%s" is not registered.', $name)); + $this->fail(\sprintf('Filter "%s" is not registered.', $name)); } public static function provideFilters() @@ -58,7 +67,7 @@ public function testFunction(string $name, string $method, array $options) } } - $this->fail(sprintf('Function "%s" is not registered.', $name)); + $this->fail(\sprintf('Function "%s" is not registered.', $name)); } public static function provideFunctions() @@ -86,7 +95,7 @@ public function testTest(string $name, string $method, array $options) } } - $this->fail(sprintf('Test "%s" is not registered.', $name)); + $this->fail(\sprintf('Test "%s" is not registered.', $name)); } public static function provideTests() diff --git a/tests/Extension/CoreTest.php b/tests/Extension/CoreTest.php index bbea3d56c2c..10d7e881daa 100644 --- a/tests/Extension/CoreTest.php +++ b/tests/Extension/CoreTest.php @@ -1,5 +1,14 @@ Date: Mon, 4 Aug 2025 21:13:52 +0200 Subject: [PATCH 791/812] #4677: Add use statements for classes referenced in the getOperators @psalm-return doc --- src/Extension/ExtensionInterface.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Extension/ExtensionInterface.php b/src/Extension/ExtensionInterface.php index 44356f62769..60ca35b4e6a 100644 --- a/src/Extension/ExtensionInterface.php +++ b/src/Extension/ExtensionInterface.php @@ -11,8 +11,11 @@ namespace Twig\Extension; +use Twig\ExpressionParser; use Twig\ExpressionParser\ExpressionParserInterface; use Twig\ExpressionParser\PrecedenceChange; +use Twig\Node\Expression\Binary\AbstractBinary; +use Twig\Node\Expression\Unary\AbstractUnary; use Twig\NodeVisitor\NodeVisitorInterface; use Twig\TokenParser\TokenParserInterface; use Twig\TwigFilter; From b40934891bc679bd70ebd28d2819f4b3a5895e56 Mon Sep 17 00:00:00 2001 From: bodendorfer-simplethings <36152444+bodendorfer-simplethings@users.noreply.github.com> Date: Fri, 15 Aug 2025 14:30:41 +0200 Subject: [PATCH 792/812] Fix wrong rounding_mode values --- doc/filters/format_number.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/filters/format_number.rst b/doc/filters/format_number.rst index 5a18e957be9..0b51a00d852 100644 --- a/doc/filters/format_number.rst +++ b/doc/filters/format_number.rst @@ -60,9 +60,9 @@ The list of supported options: * ``floor``: Floor rounding * ``down``: Rounding towards zero * ``up``: Rounding away from zero - * ``half_even``: Round halves to the nearest even integer - * ``half_up``: Round halves up - * ``half_down``: Round halves down + * ``halfeven``: Round halves to the nearest even integer + * ``halfup``: Round halves up + * ``halfdown``: Round halves down .. code-block:: twig From 806049f7a24f8c359d1b9f493482c4aa340712ad Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sun, 17 Aug 2025 19:23:48 +0200 Subject: [PATCH 793/812] Fix compatibility layer --- src/ExpressionParser.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ExpressionParser.php b/src/ExpressionParser.php index 727cf7eba61..3ba94d076fa 100644 --- a/src/ExpressionParser.php +++ b/src/ExpressionParser.php @@ -214,7 +214,7 @@ public function parseArguments() { trigger_deprecation('twig/twig', '3.19', \sprintf('The "%s()" method is deprecated, use "Twig\ExpressionParser\Infix\ArgumentsTrait::parseNamedArguments()" instead.', __METHOD__)); - $parsePrimary = new \ReflectionMethod($this->parser, 'parsePrimary'); + $parsePrimaryExpression = new \ReflectionMethod($this->parser, 'parsePrimaryExpression'); $namedArguments = false; $definition = false; @@ -263,7 +263,7 @@ public function parseArguments() $name = $value->getAttribute('name'); if ($definition) { - $value = $parsePrimary->invoke($this->parser); + $value = $parsePrimaryExpression->invoke($this->parser); if (!$this->checkConstantExpression($value)) { throw new SyntaxError('A default value for an argument must be a constant (a boolean, a string, a number, a sequence, or a mapping).', $token->getLine(), $stream->getSourceContext()); From d0177b94cd58bd20c4299739eae98e6efaa7ea6d Mon Sep 17 00:00:00 2001 From: LucileDT Date: Sat, 23 Aug 2025 22:02:14 +0200 Subject: [PATCH 794/812] Add note to format_datetime explaining how to install required extensions --- doc/filters/format_datetime.rst | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/doc/filters/format_datetime.rst b/doc/filters/format_datetime.rst index 5f3a49b0c16..9b27af28e6d 100644 --- a/doc/filters/format_datetime.rst +++ b/doc/filters/format_datetime.rst @@ -8,6 +8,29 @@ The ``format_datetime`` filter formats a date time: {# Aug 7, 2019, 11:39:12 PM #} {{ '2019-08-07 23:39:12'|format_datetime() }} +.. note:: + + The ``format_datetime`` filter is part of the ``IntlExtension`` which is not + installed by default. Install it first: + + .. code-block:: bash + + $ composer require twig/intl-extra + + Then, on Symfony projects, install the ``twig/extra-bundle``: + + .. code-block:: bash + + $ composer require twig/extra-bundle + + Otherwise, add the extension explicitly on the Twig environment:: + + use Twig\Extra\Intl\IntlExtension; + + $twig = new \Twig\Environment(...); + $twig->addExtension(new IntlExtension()); + + Format ------ From 62747cee2767fcd856bebfbd6682f03440718ac8 Mon Sep 17 00:00:00 2001 From: Christophe Coevoet Date: Tue, 2 Sep 2025 14:21:58 +0200 Subject: [PATCH 795/812] Avoid errors when failing to guess the template info for an error --- src/Error/Error.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Error/Error.php b/src/Error/Error.php index 22ec219e9ac..97ed2df9913 100644 --- a/src/Error/Error.php +++ b/src/Error/Error.php @@ -148,6 +148,10 @@ private function guessTemplateInfo(): void } } + if (null === $template) { + return; // Impossible to guess the info as the template was not found in the backtrace + } + $r = new \ReflectionObject($template); $file = $r->getFileName(); From def4abbd5e5db49e56b8584d3e2fcf6721b9a01b Mon Sep 17 00:00:00 2001 From: Hugo Alliaume Date: Fri, 12 Sep 2025 14:33:34 +0200 Subject: [PATCH 796/812] Improve documentation examples for `enum` and `enum_cases` It may be obvious, but I first thought initializing an BackendEnum with a certain value could be possible with `enum(FQCN, value)`, but it does not, it always returns the first case instance (if exists). Instead, `enum(FQCN).from(value)` must be used, I think it's worth to improve `enum()` examples. --- doc/functions/enum.rst | 12 +++++++++--- doc/functions/enum_cases.rst | 3 ++- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/doc/functions/enum.rst b/doc/functions/enum.rst index 325b140b092..a49f25c2c64 100644 --- a/doc/functions/enum.rst +++ b/doc/functions/enum.rst @@ -10,15 +10,21 @@ .. code-block:: twig {# display one specific case of a backed enum #} - {{ enum('App\\MyEnum').SomeCase.value }} + {{ enum('App\\CardSuite').Clubs.value }} {# "clubs" #} {# get all cases of an enum #} - {% for case in enum('App\\MyEnum').cases %} + {% for case in enum('App\\CardSuite').cases %} {{ case.value }} {% endfor %} + {# "clubs", "spades", "hearts", "diamonds" #} + + {# get a specific case of an enum by value #} + {% set card_suite = enum('App\\CardSuite').from('hearts') %} + {{ card_suite.name }} {# "Hearts" #} + {{ card_suite.value }} {# "hearts" #} {# call any methods of the enum class #} - {{ enum('App\\MyEnum').someMethod() }} + {{ enum('App\\CardSuite').someMethod() }} When using a string literal for the ``enum`` argument, it will be validated during compile time to be a valid enum name. diff --git a/doc/functions/enum_cases.rst b/doc/functions/enum_cases.rst index 2e6f883486e..480c022d611 100644 --- a/doc/functions/enum_cases.rst +++ b/doc/functions/enum_cases.rst @@ -9,9 +9,10 @@ .. code-block:: twig - {% for case in enum_cases('App\\MyEnum') %} + {% for case in enum_cases('App\\CardSuite') %} {{ case.value }} {% endfor %} + {# "clubs", "spades", "hearts", "diamonds" #} When using a string literal for the ``enum`` argument, it will be validated during compile time to be a valid enum name. From 57a2f85783a29353386a2d804264fb13f01e70b8 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Mon, 15 Sep 2025 07:48:29 +0200 Subject: [PATCH 797/812] Bump version --- CHANGELOG | 2 +- src/Environment.php | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index af9bcb1d621..168e6fe7d0a 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,4 +1,4 @@ -# 3.21.2 (2025-XX-XX) +# 3.22.0 (2025-XX-XX) * n/a diff --git a/src/Environment.php b/src/Environment.php index 7a7a1374b35..ba151e1bd91 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -43,11 +43,11 @@ */ class Environment { - public const VERSION = '3.21.2-DEV'; - public const VERSION_ID = 32102; + public const VERSION = '3.22.0-DEV'; + public const VERSION_ID = 32200; public const MAJOR_VERSION = 3; - public const MINOR_VERSION = 21; - public const RELEASE_VERSION = 2; + public const MINOR_VERSION = 22; + public const RELEASE_VERSION = 0; public const EXTRA_VERSION = 'DEV'; private $charset; From 30977bdea9b2e84503a8710a9ce1fa1fcf3d8f42 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Fri, 5 Sep 2025 11:15:29 +0200 Subject: [PATCH 798/812] Support two words test guard --- CHANGELOG | 4 ++-- src/TokenParser/GuardTokenParser.php | 8 +++++++- tests/Fixtures/tags/guard/basic.test | 17 +++++++++++++++++ 3 files changed, 26 insertions(+), 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index af9bcb1d621..8592d72473a 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,6 @@ -# 3.21.2 (2025-XX-XX) +# 3.22.0 (2025-XX-XX) - * n/a + * Add support for two words test in guard tag # 3.21.1 (2025-05-03) diff --git a/src/TokenParser/GuardTokenParser.php b/src/TokenParser/GuardTokenParser.php index 656766af516..eb48865795c 100644 --- a/src/TokenParser/GuardTokenParser.php +++ b/src/TokenParser/GuardTokenParser.php @@ -32,9 +32,15 @@ public function parse(Token $token): Node $method = 'get'.$typeToken->getValue(); $nameToken = $stream->expect(Token::NAME_TYPE); + $name = $nameToken->getValue(); + if ('test' === $typeToken->getValue() && $stream->test(Token::NAME_TYPE)) { + // try 2-words tests + $name .= ' '.$stream->getCurrent()->getValue(); + $stream->next(); + } try { - $exists = null !== $this->parser->getEnvironment()->$method($nameToken->getValue()); + $exists = null !== $this->parser->getEnvironment()->$method($name); } catch (SyntaxError) { $exists = false; } diff --git a/tests/Fixtures/tags/guard/basic.test b/tests/Fixtures/tags/guard/basic.test index 2c27a5ae7a4..bfa66eebb25 100644 --- a/tests/Fixtures/tags/guard/basic.test +++ b/tests/Fixtures/tags/guard/basic.test @@ -13,9 +13,26 @@ {% else %} NEVER {% endguard %} + +{% guard test foobar %} + NEVER + {{ 'a'|foobar }} +{% else -%} + The foobar test doesn't exist +{% endguard %} + +{% guard test divisible by -%} + The divisible by function does exist +{% else %} + NEVER +{% endguard %} --DATA-- return [] --EXPECT-- The foobar filter doesn't exist The constant function does exist + +The foobar test doesn't exist + +The divisible by function does exist From 8f970764e66525732166ba6091518c5bac6b9a48 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Mon, 15 Sep 2025 07:57:37 +0200 Subject: [PATCH 799/812] Bump minimum Commonmark requirement --- extra/markdown-extra/composer.json | 2 +- extra/twig-extra-bundle/composer.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/extra/markdown-extra/composer.json b/extra/markdown-extra/composer.json index 5abaf5040f8..703b25c20fb 100644 --- a/extra/markdown-extra/composer.json +++ b/extra/markdown-extra/composer.json @@ -22,7 +22,7 @@ "require-dev": { "symfony/phpunit-bridge": "^6.4|^7.0", "erusev/parsedown": "dev-master as 1.x-dev", - "league/commonmark": "^1.0|^2.0", + "league/commonmark": "^2.7", "league/html-to-markdown": "^4.8|^5.0", "michelf/php-markdown": "^1.8|^2.0" }, diff --git a/extra/twig-extra-bundle/composer.json b/extra/twig-extra-bundle/composer.json index 88ee8107cf2..28aca766a31 100644 --- a/extra/twig-extra-bundle/composer.json +++ b/extra/twig-extra-bundle/composer.json @@ -21,7 +21,7 @@ "twig/twig": "^3.2|^4.0" }, "require-dev": { - "league/commonmark": "^1.0|^2.0", + "league/commonmark": "^2.7", "symfony/phpunit-bridge": "^6.4|^7.0", "twig/cache-extra": "^3.0", "twig/cssinliner-extra": "^3.0", From 41bfb6bd8b7a27f8a13dea4bd71065a0773f1356 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Mon, 15 Sep 2025 08:05:04 +0200 Subject: [PATCH 800/812] Fix intl test --- extra/intl-extra/Tests/Fixtures/country_timezones.test | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/extra/intl-extra/Tests/Fixtures/country_timezones.test b/extra/intl-extra/Tests/Fixtures/country_timezones.test index 3c81440c11e..04af954ab56 100644 --- a/extra/intl-extra/Tests/Fixtures/country_timezones.test +++ b/extra/intl-extra/Tests/Fixtures/country_timezones.test @@ -3,10 +3,10 @@ --TEMPLATE-- {{ country_timezones('UNKNOWN')|length }} {{ country_timezones('FR')|join(', ') }} -{{ country_timezones('US')|join(', ') }} +{{ country_timezones('US')[0:2]|join(', ') }} --DATA-- return []; --EXPECT-- 0 Europe/Paris -America/Adak, America/Anchorage, America/Boise, America/Chicago, America/Denver, America/Detroit, America/Indiana/Knox, America/Indiana/Marengo, America/Indiana/Petersburg, America/Indiana/Tell_City, America/Indiana/Vevay, America/Indiana/Vincennes, America/Indiana/Winamac, America/Indianapolis, America/Juneau, America/Kentucky/Monticello, America/Los_Angeles, America/Louisville, America/Menominee, America/Metlakatla, America/New_York, America/Nome, America/North_Dakota/Beulah, America/North_Dakota/Center, America/North_Dakota/New_Salem, America/Phoenix, America/Sitka, America/Yakutat, Pacific/Honolulu +America/Adak, America/Anchorage From cc12df995cca520603d56343b355487848f7a637 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Fri, 29 Aug 2025 20:34:23 +0200 Subject: [PATCH 801/812] Introduce registerUndefinedTestCallback --- CHANGELOG | 1 + doc/recipes.rst | 16 +++++++++---- src/Environment.php | 8 +++++++ src/ExtensionSet.php | 16 +++++++++++++ src/Parser.php | 21 ++++++++++++++--- src/Test/IntegrationTestCase.php | 12 ++++++++++ tests/EnvironmentTest.php | 16 +++++++++++++ .../Fixtures/tags/guard/throwing_handler.test | 18 +++++++++++++++ tests/IntegrationTest.php | 23 ++++++++++++++++++- 9 files changed, 122 insertions(+), 9 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 8592d72473a..f072385485c 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,7 @@ # 3.22.0 (2025-XX-XX) * Add support for two words test in guard tag + * Add `Environment::registerUndefinedTestCallback()` # 3.21.1 (2025-05-03) diff --git a/doc/recipes.rst b/doc/recipes.rst index 402279d1e63..2864fdab7e3 100644 --- a/doc/recipes.rst +++ b/doc/recipes.rst @@ -271,13 +271,19 @@ Defining undefined Functions, Filters, and Tags on the Fly The ``registerUndefinedTokenParserCallback()`` method was added in Twig 3.2. -When a function/filter/tag is not defined, Twig defaults to throw a +.. versionadded:: 3.22 + + The ``registerUndefinedTestCallback()`` method was added in Twig + 3.22. + +When a function/filter/test/tag is not defined, Twig defaults to throw a ``\Twig\Error\SyntaxError`` exception. However, it can also call a `callback`_ -(any valid PHP callable) which should return a function/filter/tag. +(any valid PHP callable) which should return a function/filter/test/tag. For tags, register callbacks with ``registerUndefinedTokenParserCallback()``. For filters, register callbacks with ``registerUndefinedFilterCallback()``. -For functions, use ``registerUndefinedFunctionCallback()``:: +For functions, use ``registerUndefinedFunctionCallback()``. +For tests, use ``registerUndefinedTestCallback()``:: // auto-register all native PHP functions as Twig functions // NEVER do this in a project as it's NOT secure @@ -289,7 +295,7 @@ For functions, use ``registerUndefinedFunctionCallback()``:: return false; }); -If the callable is not able to return a valid function/filter/tag, it must +If the callable is not able to return a valid function/filter/test/tag, it must return ``false``. If you register more than one callback, Twig will call them in turn until one @@ -297,7 +303,7 @@ does not return ``false``. .. tip:: - As the resolution of functions/filters/tags is done during compilation, + As the resolution of functions/filters/tests/tags is done during compilation, there is no overhead when registering these callbacks. .. warning:: diff --git a/src/Environment.php b/src/Environment.php index ba151e1bd91..c602af87cb3 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -827,6 +827,14 @@ public function getTest(string $name): ?TwigTest return $this->extensionSet->getTest($name); } + /** + * @param callable(string): (TwigTest|false) $callable + */ + public function registerUndefinedTestCallback(callable $callable): void + { + $this->extensionSet->registerUndefinedTestCallback($callable); + } + /** * @return void */ diff --git a/src/ExtensionSet.php b/src/ExtensionSet.php index 85a98cf3c32..66467b0b498 100644 --- a/src/ExtensionSet.php +++ b/src/ExtensionSet.php @@ -59,6 +59,8 @@ final class ExtensionSet private $functionCallbacks = []; /** @var array */ private $filterCallbacks = []; + /** @var array */ + private $testCallbacks = []; /** @var array */ private $parserCallbacks = []; private $lastModified = 0; @@ -410,9 +412,23 @@ public function getTest(string $name): ?TwigTest } } + foreach ($this->testCallbacks as $callback) { + if (false !== $test = $callback($name)) { + return $test; + } + } + return null; } + /** + * @param callable(string): (TwigTest|false) $callable + */ + public function registerUndefinedTestCallback(callable $callable): void + { + $this->testCallbacks[] = $callable; + } + public function getExpressionParsers(): ExpressionParsers { if (!$this->initialized) { diff --git a/src/Parser.php b/src/Parser.php index 1937b7e15b2..acc1a4dc99c 100644 --- a/src/Parser.php +++ b/src/Parser.php @@ -494,11 +494,26 @@ public function getTest(int $line): TwigTest // try 2-words tests $name = $name.' '.$this->getCurrentToken()->getValue(); - if ($test = $this->env->getTest($name)) { - $this->stream->next(); + try { + $test = $this->env->getTest($name); + } catch (SyntaxError $e) { + if (!$this->shouldIgnoreUnknownTwigCallables()) { + throw $e; + } + + $test = null; } + $this->stream->next(); } else { - $test = $this->env->getTest($name); + try { + $test = $this->env->getTest($name); + } catch (SyntaxError $e) { + if (!$this->shouldIgnoreUnknownTwigCallables()) { + throw $e; + } + + $test = null; + } } if (!$test) { diff --git a/src/Test/IntegrationTestCase.php b/src/Test/IntegrationTestCase.php index f3f7adcee64..a4a3b561d78 100644 --- a/src/Test/IntegrationTestCase.php +++ b/src/Test/IntegrationTestCase.php @@ -101,6 +101,14 @@ protected function getUndefinedFunctionCallbacks(): array return []; } + /** + * @return array + */ + protected function getUndefinedTestCallbacks(): array + { + return []; + } + /** * @return array */ @@ -255,6 +263,10 @@ protected function doIntegrationTest($file, $message, $condition, $templates, $e $twig->registerUndefinedFunctionCallback($callback); } + foreach ($this->getUndefinedTestCallbacks() as $callback) { + $twig->registerUndefinedTestCallback($callback); + } + foreach ($this->getUndefinedTokenParserCallbacks() as $callback) { $twig->registerUndefinedTokenParserCallback($callback); } diff --git a/tests/EnvironmentTest.php b/tests/EnvironmentTest.php index 10f16fc5850..2c568d79bd7 100644 --- a/tests/EnvironmentTest.php +++ b/tests/EnvironmentTest.php @@ -425,6 +425,22 @@ public function testUndefinedFilterCallback() $this->assertSame('dynamic', $filter->getName()); } + public function testUndefinedTestCallback() + { + $twig = new Environment(new ArrayLoader()); + $twig->registerUndefinedTestCallback(function (string $name) { + if ('dynamic' === $name) { + return new TwigTest('dynamic', function () { return 'dynamic'; }); + } + + return false; + }); + + $this->assertNull($twig->getTest('does_not_exist')); + $this->assertInstanceOf(TwigTest::class, $test = $twig->getTest('dynamic')); + $this->assertSame('dynamic', $test->getName()); + } + public function testUndefinedTokenParserCallback() { $twig = new Environment(new ArrayLoader()); diff --git a/tests/Fixtures/tags/guard/throwing_handler.test b/tests/Fixtures/tags/guard/throwing_handler.test index 37e32ef6c00..8b9f0708f7c 100644 --- a/tests/Fixtures/tags/guard/throwing_handler.test +++ b/tests/Fixtures/tags/guard/throwing_handler.test @@ -14,9 +14,27 @@ {% else -%} The throwing_undefined_function function doesn't exist {% endguard %} + +{% guard test throwing_undefined_test -%} + NEVER + {% if 'a' is throwing_undefined_test('b') %}{% endif %} +{% else -%} + The throwing_undefined_test test doesn't exist +{% endguard %} + +{% guard test throwing_undefined_two words_test -%} + NEVER + {% if 'a' is throwing_undefined_test words_test('b') %}{% endif %} +{% else -%} + The throwing_undefined_two words_test test doesn't exist +{% endguard %} --DATA-- return [] --EXPECT-- The throwing_undefined_filter filter doesn't exist The throwing_undefined_function function doesn't exist + +The throwing_undefined_test test doesn't exist + +The throwing_undefined_two words_test test doesn't exist diff --git a/tests/IntegrationTest.php b/tests/IntegrationTest.php index 57e4767f034..c00f8154dd5 100644 --- a/tests/IntegrationTest.php +++ b/tests/IntegrationTest.php @@ -72,7 +72,28 @@ static function (string $name) { ]; } - protected function getUndefinedTokenParserCallbacks(): array + protected function getUndefinedTestCallbacks(): array + { + return [ + static function (string $name) { + if ('throwing_undefined_test' === $name) { + throw new SyntaxError('This test is undefined in the tests.'); + } + if ('throwing_undefined_two words_test' === $name) { + throw new SyntaxError('This test is undefined in the tests.'); + } + + // Ensure this does not conflict with `divisible by` and `same as`. + if (\in_array($name, ['divisible', 'same'], true)) { + return new TwigTest($name, fn () => ''); + } + + return false; + }, + ]; + } + + protected function getUndefinedFilterCallbacks(): array { return [ static function (string $name) { From e4d79157024902561c40c812eca006e8fb16e4ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Andr=C3=A9?= Date: Thu, 25 Sep 2025 08:28:51 +0200 Subject: [PATCH 802/812] Compile 'index' with repr (not string) in EmbedNode Before this fix, the generated Template code had quotes around the index (integer) parameter value. --- src/Node/EmbedNode.php | 2 +- tests/Node/EmbedTest.php | 91 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 92 insertions(+), 1 deletion(-) create mode 100644 tests/Node/EmbedTest.php diff --git a/src/Node/EmbedNode.php b/src/Node/EmbedNode.php index fe4365b5710..2de39ebb942 100644 --- a/src/Node/EmbedNode.php +++ b/src/Node/EmbedNode.php @@ -41,7 +41,7 @@ protected function addGetTemplate(Compiler $compiler, string $template = ''): vo ->raw(', ') ->repr($this->getTemplateLine()) ->raw(', ') - ->string($this->getAttribute('index')) + ->repr($this->getAttribute('index')) ->raw(')') ; if ($this->getAttribute('ignore_missing')) { diff --git a/tests/Node/EmbedTest.php b/tests/Node/EmbedTest.php new file mode 100644 index 00000000000..13ff588d4f2 --- /dev/null +++ b/tests/Node/EmbedTest.php @@ -0,0 +1,91 @@ +assertFalse($node->hasNode('variables')); + $this->assertEquals('foo.twig', $node->getAttribute('name')); + $this->assertEquals(0, $node->getAttribute('index')); + $this->assertFalse($node->getAttribute('only')); + $this->assertFalse($node->getAttribute('ignore_missing')); + + $vars = new ArrayExpression([new ConstantExpression('foo', 1), new ConstantExpression(true, 1)], 1); + $node = new EmbedNode('bar.twig', 1, $vars, true, false, 1); + $this->assertEquals($vars, $node->getNode('variables')); + $this->assertTrue($node->getAttribute('only')); + $this->assertEquals('bar.twig', $node->getAttribute('name')); + $this->assertEquals(1, $node->getAttribute('index')); + } + + public static function provideTests(): iterable + { + $tests = []; + + $node = new EmbedNode('foo.twig', 0, null, false, false, 1); + $tests[] = [$node, <<<'EOF' +// line 1 +yield from $this->load("foo.twig", 1, 0)->unwrap()->yield($context); +EOF + ]; + + $node = new EmbedNode('foo.twig', 1, null, false, false, 1); + $tests[] = [$node, <<<'EOF' +// line 1 +yield from $this->load("foo.twig", 1, 1)->unwrap()->yield($context); +EOF + ]; + + $vars = new ArrayExpression([new ConstantExpression('foo', 1), new ConstantExpression(true, 1)], 1); + $node = new EmbedNode('foo.twig', 0, $vars, false, false, 1); + $tests[] = [$node, <<<'EOF' +// line 1 +yield from $this->load("foo.twig", 1, 0)->unwrap()->yield(CoreExtension::merge($context, ["foo" => true])); +EOF + ]; + + $node = new EmbedNode('foo.twig', 0, $vars, true, false, 1); + $tests[] = [$node, <<<'EOF' +// line 1 +yield from $this->load("foo.twig", 1, 0)->unwrap()->yield(CoreExtension::toArray(["foo" => true])); +EOF + ]; + + $node = new EmbedNode('foo.twig', 2, $vars, true, true, 1); + $tests[] = [$node, <<load("foo.twig", 1, 2); + \$_v0->getParent(\$context); +; +} catch (LoaderError \$e) { + // ignore missing template + \$_v0 = null; +} +if (\$_v0) { + yield from \$_v0->unwrap()->yield(CoreExtension::toArray(["foo" => true])); +} +EOF + ]; + + return $tests; + } +} From 50327b5019a4e278725e84a86c5325cdac256168 Mon Sep 17 00:00:00 2001 From: Javier Eguiluz Date: Thu, 25 Sep 2025 11:22:21 +0200 Subject: [PATCH 803/812] [Doc] Tweaks in the escaping article --- doc/filters/escape.rst | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/doc/filters/escape.rst b/doc/filters/escape.rst index 91f39acb020..6da700794fe 100644 --- a/doc/filters/escape.rst +++ b/doc/filters/escape.rst @@ -53,8 +53,9 @@ documents: * ``url``: escapes a string for the **URI or parameter** contexts. This should not be used to escape an entire URI; only a subcomponent being inserted. -* ``html_attr``: escapes a string for the **HTML attribute** context, - **without quotes** around HTML attribute values. +* ``html_attr``: escapes a string when used as an **HTML attribute** name, and + also when used as the value of an HTML attribute **without quotes** + (e.g. ``data-attribute={{ some_value }}``). Note that doing contextual escaping in HTML documents is hard and choosing the right escaping strategy depends on a lot of factors. Please, read related @@ -96,23 +97,22 @@ to learn more about this topic. .. tip:: - The ``html_attr`` escaping strategy can be useful when you need to - escape a **dynamic HTML attribute name**: + The ``html_attr`` escaping strategy can be useful when you need to escape a + **dynamic HTML attribute name**: .. code-block:: html+twig

    - It can also be used for escaping a **dynamic HTML attribute value** - if it is not quoted, but this is **less performant**. - Instead, it is recommended to quote the HTML attribute value and use - the ``html`` escaping strategy: + It can also be used for escaping a **dynamic HTML attribute value** if it is + not quoted, but this is **less performant**. Instead, it is recommended to + quote the HTML attribute value and use the ``html`` escaping strategy: .. code-block:: html+twig

    - {# is equivalent to, but is less performant #} + {# this is equivalent, but less performant #}

    Custom Escapers From df3a281fc1ec5ba15538e78cbb37d7b34da9ca1d Mon Sep 17 00:00:00 2001 From: Brett Date: Tue, 7 Oct 2025 07:09:13 +0800 Subject: [PATCH 804/812] Update replace.rst Missing `fruits` variable declaration. --- doc/filters/replace.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/filters/replace.rst b/doc/filters/replace.rst index 63e7f4800c5..8053ae1eb39 100644 --- a/doc/filters/replace.rst +++ b/doc/filters/replace.rst @@ -6,6 +6,8 @@ format is free-form): .. code-block:: twig + {% set fruit = 'apples' %} + {{ "I like %this% and %that%."|replace({'%this%': fruit, '%that%': "oranges"}) }} {# if the "fruit" variable is set to "apples", #} {# it outputs "I like apples and oranges" #} From b9a4e04731e790abf5b24c7c353f8cc446b2ed15 Mon Sep 17 00:00:00 2001 From: Thomas Landauer Date: Sun, 19 Oct 2025 19:03:40 +0200 Subject: [PATCH 805/812] Update inky_to_html.rst: Updating link Page: https://twig.symfony.com/doc/3.x/filters/inky_to_html.html --- doc/filters/inky_to_html.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/filters/inky_to_html.rst b/doc/filters/inky_to_html.rst index 563baba363a..839e41935e8 100644 --- a/doc/filters/inky_to_html.rst +++ b/doc/filters/inky_to_html.rst @@ -2,7 +2,7 @@ ================ The ``inky_to_html`` filter processes an `inky email template -`_: +`_: .. code-block:: html+twig From 9ae75d315cbeb09477c90f76da33e022cc2a0895 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Fri, 24 Oct 2025 14:15:21 +0200 Subject: [PATCH 806/812] Fix accessing arrays with stringable objects as key --- src/Extension/CoreExtension.php | 10 ++++++--- .../4701-block-inheritance-issue.test | 21 +++++++++++++++++++ tests/TemplateTest.php | 2 +- 3 files changed, 29 insertions(+), 4 deletions(-) create mode 100644 tests/Fixtures/regression/4701-block-inheritance-issue.test diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index f7e4250ae50..fcfbcb11656 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -1694,7 +1694,7 @@ public static function getAttribute(Environment $env, Source $source, $object, $ } if (match (true) { - \is_array($object) => \array_key_exists($arrayItem, $object), + \is_array($object) => \array_key_exists($arrayItem = (string) $arrayItem, $object), $object instanceof \ArrayAccess => $object->offsetExists($arrayItem), default => false, }) { @@ -1715,9 +1715,13 @@ public static function getAttribute(Environment $env, Source $source, $object, $ } if ($object instanceof \ArrayAccess) { - $message = \sprintf('Key "%s" in object with ArrayAccess of class "%s" does not exist.', $arrayItem, $object::class); + if (\is_object($arrayItem) || \is_array($arrayItem)) { + $message = \sprintf('Key of type "%s" does not exist in ArrayAccess-able object of class "%s".', get_debug_type($arrayItem), get_debug_type($object)); + } else { + $message = \sprintf('Key "%s" does not exist in ArrayAccess-able object of class "%s".', $arrayItem, get_debug_type($object)); + } } elseif (\is_object($object)) { - $message = \sprintf('Impossible to access a key "%s" on an object of class "%s" that does not implement ArrayAccess interface.', $item, $object::class); + $message = \sprintf('Impossible to access a key "%s" on an object of class "%s" that does not implement ArrayAccess interface.', $item, get_debug_type($object)); } elseif (\is_array($object)) { if (!$object) { $message = \sprintf('Key "%s" does not exist as the sequence/mapping is empty.', $arrayItem); diff --git a/tests/Fixtures/regression/4701-block-inheritance-issue.test b/tests/Fixtures/regression/4701-block-inheritance-issue.test new file mode 100644 index 00000000000..f964be87143 --- /dev/null +++ b/tests/Fixtures/regression/4701-block-inheritance-issue.test @@ -0,0 +1,21 @@ +--TEST-- +#4701 Accessing arrays with stringable objects as key +--TEMPLATE-- +{% set hash = { + 'foo': 'FOO', + 'bar': 'BAR', +} %} + +{{ hash[key] }} +--DATA-- +class MyObj { + public function __toString() { + return 'foo'; + } +} + +return [ + 'key' => new MyObj(), +]; +--EXPECT-- +FOO diff --git a/tests/TemplateTest.php b/tests/TemplateTest.php index 2b14635c167..0d851ab9b53 100644 --- a/tests/TemplateTest.php +++ b/tests/TemplateTest.php @@ -77,7 +77,7 @@ public static function getAttributeExceptions() ['{{ null["a"] }}', 'Impossible to access a key ("a") on a null variable in "%s" at line 1.'], ['{{ empty_array["a"] }}', 'Key "a" does not exist as the sequence/mapping is empty in "%s" at line 1.'], ['{{ array["a"] }}', 'Key "a" for sequence/mapping with keys "foo" does not exist in "%s" at line 1.'], - ['{{ array_access["a"] }}', 'Key "a" in object with ArrayAccess of class "Twig\Tests\TemplateArrayAccessObject" does not exist in "%s" at line 1.'], + ['{{ array_access["a"] }}', 'Key "a" does not exist in ArrayAccess-able object of class "Twig\Tests\TemplateArrayAccessObject" in "%s" at line 1.'], ['{{ string.a }}', 'Impossible to access an attribute ("a") on a string variable ("foo") in "%s" at line 1.'], ['{{ string.a() }}', 'Impossible to invoke a method ("a") on a string variable ("foo") in "%s" at line 1.'], ['{{ null.a }}', 'Impossible to access an attribute ("a") on a null variable in "%s" at line 1.'], From a5487c8e0563a232a4287eca926a8e1448b89e99 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Wed, 29 Oct 2025 11:53:10 -0400 Subject: [PATCH 807/812] Update CHANGELOG --- CHANGELOG | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index f072385485c..afbd669ef03 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -2,6 +2,13 @@ * Add support for two words test in guard tag * Add `Environment::registerUndefinedTestCallback()` + * Fix compatibility with Symfony 8 + * Fix accessing arrays with stringable objects as key + * Avoid errors when failing to guess the template info for an error + * Fix expression parser compatibility layer + * Fix compiling 'index' with repr (not string) in EmbedNode + * Update configuration keys + allow extra keys for CommonMark extensions + * Allow usage of other Markdown converters than CommonMark in LeagueMarkdown # 3.21.1 (2025-05-03) From 4509984193026de413baf4ba80f68590a7f2c51d Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Wed, 29 Oct 2025 11:56:47 -0400 Subject: [PATCH 808/812] Prepare the 3.22.0 release --- CHANGELOG | 2 +- src/Environment.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index afbd669ef03..a435131eb30 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,4 +1,4 @@ -# 3.22.0 (2025-XX-XX) +# 3.22.0 (2025-10-29) * Add support for two words test in guard tag * Add `Environment::registerUndefinedTestCallback()` diff --git a/src/Environment.php b/src/Environment.php index c602af87cb3..74b060f1825 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -43,12 +43,12 @@ */ class Environment { - public const VERSION = '3.22.0-DEV'; + public const VERSION = '3.22.0'; public const VERSION_ID = 32200; public const MAJOR_VERSION = 3; public const MINOR_VERSION = 22; public const RELEASE_VERSION = 0; - public const EXTRA_VERSION = 'DEV'; + public const EXTRA_VERSION = ''; private $charset; private $loader; From f663cc345b863573a53da4cd029ea970426f4758 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Wed, 29 Oct 2025 11:57:52 -0400 Subject: [PATCH 809/812] Bump version --- CHANGELOG | 4 ++++ src/Environment.php | 8 ++++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index a435131eb30..7dccdc4bc57 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +# 3.22.1 (2025-XX-XX) + + * n/a + # 3.22.0 (2025-10-29) * Add support for two words test in guard tag diff --git a/src/Environment.php b/src/Environment.php index 74b060f1825..a8c7678ebfb 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -43,12 +43,12 @@ */ class Environment { - public const VERSION = '3.22.0'; - public const VERSION_ID = 32200; + public const VERSION = '3.22.1-DEV'; + public const VERSION_ID = 32201; public const MAJOR_VERSION = 3; public const MINOR_VERSION = 22; - public const RELEASE_VERSION = 0; - public const EXTRA_VERSION = ''; + public const RELEASE_VERSION = 1; + public const EXTRA_VERSION = 'DEV'; private $charset; private $loader; From 64c87eeaa3ecfe77dd7e3ca900a5e498ada6c2ad Mon Sep 17 00:00:00 2001 From: Younes ENNAJI Date: Thu, 30 Oct 2025 01:26:03 +0100 Subject: [PATCH 810/812] Fix array typehint for $variants in HtmlExtension --- extra/html-extra/HtmlExtension.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extra/html-extra/HtmlExtension.php b/extra/html-extra/HtmlExtension.php index fd67582f557..cba22427ba1 100644 --- a/extra/html-extra/HtmlExtension.php +++ b/extra/html-extra/HtmlExtension.php @@ -115,7 +115,7 @@ public static function htmlClasses(...$args): string /** * @param string|list $base - * @param array> $variants + * @param array>> $variants * @param array>> $compoundVariants * @param array $defaultVariant * From 9a8a1dc1dd91b2a546a3acad3e67e731cbda3c2e Mon Sep 17 00:00:00 2001 From: Javier Eguiluz Date: Sun, 2 Nov 2025 12:00:49 +0100 Subject: [PATCH 811/812] Allow Symfony 8 packages in Twig extra packages --- extra/cache-extra/composer.json | 2 +- extra/html-extra/composer.json | 2 +- extra/intl-extra/composer.json | 2 +- extra/string-extra/composer.json | 2 +- extra/twig-extra-bundle/composer.json | 4 ++-- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/extra/cache-extra/composer.json b/extra/cache-extra/composer.json index d14bbbe13a3..8b21310eed9 100644 --- a/extra/cache-extra/composer.json +++ b/extra/cache-extra/composer.json @@ -16,7 +16,7 @@ ], "require": { "php": ">=8.1.0", - "symfony/cache": "^5.4|^6.4|^7.0", + "symfony/cache": "^5.4|^6.4|^7.0|^8.0", "twig/twig": "^3.21|^4.0" }, "require-dev": { diff --git a/extra/html-extra/composer.json b/extra/html-extra/composer.json index 55555a03d6b..db43e18bbdf 100644 --- a/extra/html-extra/composer.json +++ b/extra/html-extra/composer.json @@ -17,7 +17,7 @@ "require": { "php": ">=8.1.0", "symfony/deprecation-contracts": "^2.5|^3", - "symfony/mime": "^5.4|^6.4|^7.0", + "symfony/mime": "^5.4|^6.4|^7.0|^8.0", "twig/twig": "^3.13|^4.0" }, "require-dev": { diff --git a/extra/intl-extra/composer.json b/extra/intl-extra/composer.json index b728753c4c7..21e3956b30f 100644 --- a/extra/intl-extra/composer.json +++ b/extra/intl-extra/composer.json @@ -17,7 +17,7 @@ "require": { "php": ">=8.1.0", "twig/twig": "^3.13|^4.0", - "symfony/intl": "^5.4|^6.4|^7.0" + "symfony/intl": "^5.4|^6.4|^7.0|^8.0" }, "require-dev": { "symfony/phpunit-bridge": "^6.4|^7.0" diff --git a/extra/string-extra/composer.json b/extra/string-extra/composer.json index 6b366e1697d..27a0f346af9 100644 --- a/extra/string-extra/composer.json +++ b/extra/string-extra/composer.json @@ -16,7 +16,7 @@ ], "require": { "php": ">=8.1.0", - "symfony/string": "^5.4|^6.4|^7.0", + "symfony/string": "^5.4|^6.4|^7.0|^8.0", "symfony/translation-contracts": "^1.1|^2|^3", "twig/twig": "^3.13|^4.0" }, diff --git a/extra/twig-extra-bundle/composer.json b/extra/twig-extra-bundle/composer.json index 28aca766a31..35421f84a7c 100644 --- a/extra/twig-extra-bundle/composer.json +++ b/extra/twig-extra-bundle/composer.json @@ -16,8 +16,8 @@ ], "require": { "php": ">=8.1.0", - "symfony/framework-bundle": "^5.4|^6.4|^7.0", - "symfony/twig-bundle": "^5.4|^6.4|^7.0", + "symfony/framework-bundle": "^5.4|^6.4|^7.0|^8.0", + "symfony/twig-bundle": "^5.4|^6.4|^7.0|^8.0", "twig/twig": "^3.2|^4.0" }, "require-dev": { From 02c5a4b0b6d039569045a17e7bd012d5e3c7113f Mon Sep 17 00:00:00 2001 From: Andreas Erhard Date: Tue, 4 Nov 2025 11:55:35 +0100 Subject: [PATCH 812/812] Add caution note for random function usage --- doc/functions/random.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/doc/functions/random.rst b/doc/functions/random.rst index aac2986c387..3ebf40cb140 100644 --- a/doc/functions/random.rst +++ b/doc/functions/random.rst @@ -10,6 +10,11 @@ parameter type: * a random integer between the integer parameter (when negative) and 0 (inclusive). * a random integer between the first integer and the second integer parameter (inclusive). +.. caution:: + + The ``random`` function does not produce cryptographically secure random numbers. + Do not use them for purposes that require returned values to be unguessable. + .. code-block:: twig {{ random(['apple', 'orange', 'citrus']) }} {# example output: orange #}