N°5809 Update Symfony artifacts from 6.4.0 to 6.4.2

symfony/console
symfony/dotenv
symfony/framework-bundle
symfony/http-foundation
symfony/http-kernel
symfony/var-dumper
symfony/web-profiler-bundle
This commit is contained in:
Pierre Goiffon
2024-01-26 09:55:51 +01:00
parent ddce3058be
commit dfb5a4875a
110 changed files with 1115 additions and 929 deletions

View File

@@ -1,18 +0,0 @@
; top-most EditorConfig file
root = true
; Unix-style newlines
[*]
end_of_line = LF
[*.php]
indent_style = space
indent_size = 4
[*.test]
indent_style = space
indent_size = 4
[*.rst]
indent_style = space
indent_size = 4

View File

@@ -1,4 +0,0 @@
/doc/ export-ignore
/extra/ export-ignore
/tests/ export-ignore
/phpunit.xml.dist export-ignore

View File

@@ -1,149 +0,0 @@
name: "CI"
on:
pull_request:
push:
branches:
- '3.x'
env:
SYMFONY_PHPUNIT_DISABLE_RESULT_CACHE: 1
permissions:
contents: read
jobs:
tests:
name: "PHP ${{ matrix.php-version }}"
runs-on: 'ubuntu-latest'
continue-on-error: ${{ matrix.experimental }}
strategy:
matrix:
php-version:
- '7.2.5'
- '7.3'
- '7.4'
- '8.0'
- '8.1'
experimental: [false]
steps:
- name: "Checkout code"
uses: actions/checkout@v2
- name: "Install PHP with extensions"
uses: shivammathur/setup-php@v2
with:
coverage: "none"
php-version: ${{ matrix.php-version }}
ini-values: memory_limit=-1
- name: "Add PHPUnit matcher"
run: echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json"
- run: composer install
- name: "Install PHPUnit"
run: vendor/bin/simple-phpunit install
- name: "PHPUnit version"
run: vendor/bin/simple-phpunit --version
- name: "Run tests"
run: vendor/bin/simple-phpunit
extension-tests:
needs:
- 'tests'
name: "${{ matrix.extension }} with PHP ${{ matrix.php-version }}"
runs-on: 'ubuntu-latest'
continue-on-error: true
strategy:
matrix:
php-version:
- '7.2.5'
- '7.3'
- '7.4'
- '8.0'
- '8.1'
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'
experimental: [false]
steps:
- name: "Checkout code"
uses: actions/checkout@v2
- name: "Install PHP with extensions"
uses: shivammathur/setup-php@v2
with:
coverage: "none"
php-version: ${{ matrix.php-version }}
ini-values: memory_limit=-1
- name: "Add PHPUnit matcher"
run: echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json"
- run: composer install
- name: "Install PHPUnit"
run: vendor/bin/simple-phpunit install
- name: "PHPUnit version"
run: vendor/bin/simple-phpunit --version
- name: "Composer install"
working-directory: ${{ matrix.extension}}
run: composer install
- name: "Run tests"
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@v2
#
# - 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"

View File

@@ -1,64 +0,0 @@
name: "Documentation"
on:
pull_request:
push:
branches:
- '2.x'
- '3.x'
permissions:
contents: read
jobs:
build:
name: "Build"
runs-on: ubuntu-latest
steps:
- name: "Checkout code"
uses: actions/checkout@v2
- name: "Set-up PHP"
uses: shivammathur/setup-php@v2
with:
php-version: 8.1
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)"
- name: Cache dependencies
uses: actions/cache@v2
with:
path: ${{ steps.composercache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
restore-keys: ${{ runner.os }}-composer-
- name: "Install dependencies"
working-directory: doc/_build
run: composer install --prefer-dist --no-progress
- name: "Build the docs"
working-directory: doc/_build
run: php build.php --disable-cache
doctor-rst:
name: "DOCtor-RST"
runs-on: ubuntu-latest
steps:
- name: "Checkout code"
uses: actions/checkout@v2
- name: "Run DOCtor-RST"
uses: docker://oskarstark/doctor-rst
with:
args: --short
env:
DOCS_DIR: 'doc/'

View File

@@ -1,6 +0,0 @@
/doc/_build/vendor
/doc/_build/output
/composer.lock
/phpunit.xml
/vendor
.phpunit.result.cache

View File

@@ -1,20 +0,0 @@
<?php
return (new PhpCsFixer\Config())
->setRules([
'@Symfony' => true,
'@Symfony:risky' => true,
'@PHPUnit75Migration:risky' => true,
'php_unit_dedicate_assert' => ['target' => '5.6'],
'array_syntax' => ['syntax' => 'short'],
'php_unit_fqcn_annotation' => true,
'no_unreachable_default_argument_value' => false,
'braces' => ['allow_single_line_closure' => true],
'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'],
])
->setRiskyAllowed(true)
->setFinder((new PhpCsFixer\Finder())->in(__DIR__))
;

View File

@@ -1,3 +1,48 @@
# 3.8.0 (2023-11-21)
* 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)
* Fix some phpdocs
# 3.7.0 (2023-07-26)
* Add support for the ...spread operator on arrays and hashes
# 3.6.1 (2023-06-08)
* Suppress some native return type deprecation messages
# 3.6.0 (2023-05-03)
* 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)
* 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
* 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
* 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
# 3.4.3 (2022-09-28)
* Fix a security issue on filesystem loader (possibility to load a template outside a configured directory)

View File

@@ -1,4 +1,4 @@
Copyright (c) 2009-2022 by the Twig Team.
Copyright (c) 2009-present by the Twig Team.
All rights reserved.

View File

@@ -25,12 +25,13 @@
],
"require": {
"php": ">=7.2.5",
"symfony/polyfill-php80": "^1.22",
"symfony/polyfill-mbstring": "^1.3",
"symfony/polyfill-ctype": "^1.8"
},
"require-dev": {
"symfony/phpunit-bridge": "^4.4.9|^5.0.9|^6.0",
"psr/container": "^1.0"
"symfony/phpunit-bridge": "^5.4.9|^6.3|^7.0",
"psr/container": "^1.0|^2.0"
},
"autoload": {
"psr-4" : {
@@ -41,10 +42,5 @@
"psr-4" : {
"Twig\\Tests\\" : "tests/"
}
},
"extra": {
"branch-alias": {
"dev-master": "3.4-dev"
}
}
}

View File

@@ -63,7 +63,7 @@ class FilesystemCache implements CacheInterface
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);

View File

@@ -46,7 +46,7 @@ class Compiler
/**
* @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 @@ class Compiler
$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;

View File

@@ -25,6 +25,8 @@ use Twig\Extension\OptimizerExtension;
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;
@@ -38,11 +40,11 @@ use Twig\TokenParser\TokenParserInterface;
*/
class Environment
{
public const VERSION = '3.4.3';
public const VERSION_ID = 30403;
public const VERSION = '3.8.0';
public const VERSION_ID = 30800;
public const MAJOR_VERSION = 3;
public const MINOR_VERSION = 4;
public const RELEASE_VERSION = 3;
public const MINOR_VERSION = 8;
public const RELEASE_VERSION = 0;
public const EXTRA_VERSION = '';
private $charset;
@@ -53,6 +55,7 @@ class Environment
private $lexer;
private $parser;
private $compiler;
/** @var array<string, mixed> */
private $globals = [];
private $resolvedGlobals;
private $loadedTemplates;
@@ -342,7 +345,6 @@ class Environment
$this->cache->load($key);
}
$source = null;
if (!class_exists($cls, false)) {
$source = $this->getLoader()->getSourceContext($name);
$content = $this->compileSource($source);
@@ -775,6 +777,8 @@ class Environment
/**
* @internal
*
* @return array<string, mixed>
*/
public function getGlobals(): array
{
@@ -804,6 +808,8 @@ class Environment
/**
* @internal
*
* @return array<string, array{precedence: int, class: class-string<AbstractUnary>}>
*/
public function getUnaryOperators(): array
{
@@ -812,6 +818,8 @@ class Environment
/**
* @internal
*
* @return array<string, array{precedence: int, class: class-string<AbstractBinary>, associativity: ExpressionParser::OPERATOR_*}>
*/
public function getBinaryOperators(): array
{

View File

@@ -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);
@@ -130,13 +130,13 @@ class Error extends \Exception
}
$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 @@ class Error extends \Exception
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']);

View File

@@ -30,7 +30,7 @@ class SyntaxError extends Error
$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;
}
}

View File

@@ -17,6 +17,7 @@ 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\BlockReferenceExpression;
use Twig\Node\Expression\ConditionalExpression;
@@ -26,6 +27,7 @@ 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;
@@ -40,8 +42,6 @@ use Twig\Node\Node;
* @see https://en.wikipedia.org/wiki/Operator-precedence_parser
*
* @author Fabien Potencier <fabien@symfony.com>
*
* @internal
*/
class ExpressionParser
{
@@ -50,7 +50,9 @@ class ExpressionParser
private $parser;
private $env;
/** @var array<string, array{precedence: int, class: class-string<AbstractUnary>}> */
private $unaryOperators;
/** @var array<string, array{precedence: int, class: class-string<AbstractBinary>, associativity: self::OPERATOR_*}> */
private $binaryOperators;
public function __construct(Parser $parser, Environment $env)
@@ -80,7 +82,7 @@ class ExpressionParser
} 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());
}
@@ -181,11 +183,14 @@ class ExpressionParser
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();
}
@@ -332,7 +337,14 @@ class ExpressionParser
}
$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');
@@ -357,6 +369,14 @@ class ExpressionParser
}
$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
@@ -489,10 +509,6 @@ class ExpressionParser
}
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);

View File

@@ -23,6 +23,8 @@ 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;
@@ -249,7 +251,7 @@ final class CoreExtension extends AbstractExtension
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'),
];
}
@@ -284,6 +286,8 @@ final class CoreExtension extends AbstractExtension
'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],
@@ -339,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)
{
@@ -360,7 +364,6 @@ function twig_random(Environment $env, $values = null, $max = null)
}
} else {
$min = $values;
$max = $max;
}
return mt_rand((int) $min, (int) $max);
@@ -388,7 +391,7 @@ function twig_random(Environment $env, $values = null, $max = null)
}
}
if (!twig_test_iterable($values)) {
if (!is_iterable($values)) {
return $values;
}
@@ -525,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)));
}
@@ -605,32 +608,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 (!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, twig_to_array($array));
}
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)));
}
return array_merge(twig_to_array($arr1), twig_to_array($arr2));
return $result;
}
/**
* Slices a variable.
*
@@ -650,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 [];
}
@@ -663,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());
}
/**
@@ -716,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;
}
@@ -762,7 +767,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);
}
@@ -920,7 +925,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;
@@ -1015,6 +1020,23 @@ function twig_compare($a, $b)
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();
}
}
/**
* Returns a trimmed string.
*
@@ -1097,7 +1119,7 @@ function twig_length_filter(Environment $env, $thing)
return 0;
}
if (is_scalar($thing)) {
if (\is_scalar($thing)) {
return mb_strlen($thing, $env->getCharset());
}
@@ -1207,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;
}
@@ -1270,21 +1292,23 @@ 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)
{
return $value instanceof \Traversable || \is_array($value);
return is_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
*/
@@ -1366,6 +1390,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);
}
@@ -1401,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)));
}
@@ -1543,13 +1571,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)) {
@@ -1645,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)));
}
@@ -1675,15 +1703,42 @@ 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)));
}
$array = iterator_to_array($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)));
}
return array_reduce($array, $arrow, $initial);
$accumulator = $initial;
foreach ($array as $key => $value) {
$accumulator = $arrow($accumulator, $value, $key);
}
return $accumulator;
}
function twig_array_some(Environment $env, $array, $arrow)
{
twig_check_arrow_in_sandbox($env, $arrow, 'has some', 'operator');
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, 'has every', 'operator');
foreach ($array as $k => $v) {
if (!$arrow($v, $k)) {
return false;
}
}
return true;
}
function twig_check_arrow_in_sandbox(Environment $env, $arrow, $thing, $type)

View File

@@ -19,10 +19,10 @@ final class DebugExtension extends AbstractExtension
// 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
;

View File

@@ -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 '&#xFFFD;';
}
@@ -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);
}

View File

@@ -11,6 +11,9 @@
namespace Twig\Extension;
use Twig\ExpressionParser;
use Twig\Node\Expression\Binary\AbstractBinary;
use Twig\Node\Expression\Unary\AbstractUnary;
use Twig\NodeVisitor\NodeVisitorInterface;
use Twig\TokenParser\TokenParserInterface;
use Twig\TwigFilter;
@@ -63,6 +66,11 @@ interface ExtensionInterface
* Returns a list of operators to add to the existing list.
*
* @return array<array> First array of unary operators, second array of binary operators
*
* @psalm-return array{
* array<string, array{precedence: int, class: class-string<AbstractUnary>}>,
* array<string, array{precedence: int, class: class-string<AbstractBinary>, associativity: ExpressionParser::OPERATOR_*}>
* }
*/
public function getOperators();
}

View File

@@ -21,5 +21,8 @@ namespace Twig\Extension;
*/
interface GlobalsInterface
{
/**
* @return array<string, mixed>
*/
public function getGlobals(): array;
}

View File

@@ -15,6 +15,8 @@ use Twig\Error\RuntimeError;
use Twig\Extension\ExtensionInterface;
use Twig\Extension\GlobalsInterface;
use Twig\Extension\StagingExtension;
use Twig\Node\Expression\Binary\AbstractBinary;
use Twig\Node\Expression\Unary\AbstractUnary;
use Twig\NodeVisitor\NodeVisitorInterface;
use Twig\TokenParser\TokenParserInterface;
@@ -31,11 +33,17 @@ final class ExtensionSet
private $staging;
private $parsers;
private $visitors;
/** @var array<string, TwigFilter> */
private $filters;
/** @var array<string, TwigTest> */
private $tests;
/** @var array<string, TwigFunction> */
private $functions;
/** @var array<string, array{precedence: int, class: class-string<AbstractUnary>}> */
private $unaryOperators;
/** @var array<string, array{precedence: int, class: class-string<AbstractBinary>, associativity: ExpressionParser::OPERATOR_*}> */
private $binaryOperators;
/** @var array<string, mixed> */
private $globals;
private $functionCallbacks = [];
private $filterCallbacks = [];
@@ -305,6 +313,9 @@ final class ExtensionSet
$this->parserCallbacks[] = $callable;
}
/**
* @return array<string, mixed>
*/
public function getGlobals(): array
{
if (null !== $this->globals) {
@@ -379,6 +390,9 @@ final class ExtensionSet
return null;
}
/**
* @return array<string, array{precedence: int, class: class-string<AbstractUnary>}>
*/
public function getUnaryOperators(): array
{
if (!$this->initialized) {
@@ -388,6 +402,9 @@ final class ExtensionSet
return $this->unaryOperators;
}
/**
* @return array<string, array{precedence: int, class: class-string<AbstractBinary>, associativity: ExpressionParser::OPERATOR_*}>
*/
public function getBinaryOperators(): array
{
if (!$this->initialized) {

View File

@@ -37,7 +37,7 @@ class FileExtensionEscapingStrategy
return 'html'; // return html for directories
}
if ('.twig' === substr($name, -5)) {
if (str_ends_with($name, '.twig')) {
$name = substr($name, 0, -5);
}

View File

@@ -19,6 +19,8 @@ use Twig\Error\SyntaxError;
*/
class Lexer
{
private $isInitialized = false;
private $tokens;
private $code;
private $cursor;
@@ -61,6 +63,15 @@ class Lexer
'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 = [
@@ -153,6 +164,8 @@ class Lexer
public function tokenize(Source $source): TokenStream
{
$this->initialize();
$this->source = $source;
$this->code = str_replace(["\r\n", "\r"], "\n", $source->getCode());
$this->cursor = 0;
@@ -302,8 +315,13 @@ class Lexer
}
}
// spread operator
if ('.' === $this->code[$this->cursor] && ($this->cursor + 2 < $this->end) && '.' === $this->code[$this->cursor + 1] && '.' === $this->code[$this->cursor + 2]) {
$this->pushToken(Token::SPREAD_TYPE, '...');
$this->moveCursor('...');
}
// 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('=>');
}
@@ -327,13 +345,13 @@ class Lexer
$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);
}
@@ -404,7 +422,7 @@ class Lexer
$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)) {

View File

@@ -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;
}
@@ -250,7 +250,7 @@ class FilesystemLoader implements LoaderInterface
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.');
}

View File

@@ -66,20 +66,70 @@ class ArrayExpression extends AbstractExpression
public function compile(Compiler $compiler): void
{
$keyValuePairs = $this->getKeyValuePairs();
$needsArrayMergeSpread = \PHP_VERSION_ID < 80100 && $this->hasSpreadItem($keyValuePairs);
if ($needsArrayMergeSpread) {
$compiler->raw('twig_array_merge(');
}
$compiler->raw('[');
$first = true;
foreach ($this->getKeyValuePairs() as $pair) {
$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;
}
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']);
++$nextIndex;
} else {
$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']);
}
}
$compiler->raw(']');
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;
}
}
return false;
}
}

View File

@@ -24,7 +24,7 @@ class EndsWithBinary extends AbstractBinary
->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))
;
}

View File

@@ -0,0 +1,33 @@
<?php
/*
* This file is part of Twig.
*
* (c) Fabien Potencier
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Twig\Node\Expression\Binary;
use Twig\Compiler;
class HasEveryBinary extends AbstractBinary
{
public function compile(Compiler $compiler): void
{
$compiler
->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('');
}
}

View File

@@ -0,0 +1,33 @@
<?php
/*
* This file is part of Twig.
*
* (c) Fabien Potencier
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Twig\Node\Expression\Binary;
use Twig\Compiler;
class HasSomeBinary extends AbstractBinary
{
public function compile(Compiler $compiler): void
{
$compiler
->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('');
}
}

View File

@@ -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'))

View File

@@ -24,7 +24,7 @@ class StartsWithBinary extends AbstractBinary
->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))
;
}

View File

@@ -24,7 +24,7 @@ abstract class CallExpression extends AbstractExpression
{
$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,14 +297,16 @@ abstract class CallExpression extends AbstractExpression
}
$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;
} elseif ($class = $r->getClosureScopeClass()) {
$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;

View File

@@ -23,14 +23,23 @@ class ConditionalExpression extends AbstractExpression
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('))');
}
}
}

View File

@@ -50,8 +50,11 @@ class IfNode extends Node
->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')) {

View File

@@ -355,6 +355,9 @@ final class ModuleNode extends Node
protected function compileGetTemplateName(Compiler $compiler)
{
$compiler
->write("/**\n")
->write(" * @codeCoverageIgnore\n")
->write(" */\n")
->write("public function getTemplateName()\n", "{\n")
->indent()
->write('return ')
@@ -409,6 +412,9 @@ final class ModuleNode extends Node
}
$compiler
->write("/**\n")
->write(" * @codeCoverageIgnore\n")
->write(" */\n")
->write("public function isTraitable()\n", "{\n")
->indent()
->write(sprintf("return %s;\n", $traitable ? 'true' : 'false'))
@@ -420,6 +426,9 @@ final class ModuleNode extends Node
protected function compileDebugInfo(Compiler $compiler)
{
$compiler
->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))))

View File

@@ -27,7 +27,6 @@ class Node implements \Countable, \IteratorAggregate
protected $lineno;
protected $tag;
private $name;
private $sourceContext;
/**

View File

@@ -45,7 +45,7 @@ class WithNode extends Node
->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())

View File

@@ -57,7 +57,7 @@ final class EscaperNodeVisitor implements NodeVisitorInterface
} 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 @@ final class EscaperNodeVisitor implements NodeVisitorInterface
$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 @@ final class EscaperNodeVisitor implements NodeVisitorInterface
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,13 +183,13 @@ final class EscaperNodeVisitor implements NodeVisitorInterface
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;
return $this->defaultStrategy ?: false;
}
private function getEscaperFilter(string $type, Node $node): FilterExpression

View File

@@ -50,10 +50,10 @@ final class MacroAutoImportNodeVisitor implements NodeVisitorInterface
}
} 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;

View File

@@ -63,7 +63,7 @@ final class OptimizerNodeVisitor implements NodeVisitorInterface
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 @@ final class OptimizerNodeVisitor implements NodeVisitorInterface
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 @@ final class OptimizerNodeVisitor implements NodeVisitorInterface
*
* * "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;
@@ -99,8 +99,8 @@ final class OptimizerNodeVisitor implements NodeVisitorInterface
$exprNode = $node->getNode('expr');
if (
$exprNode instanceof BlockReferenceExpression ||
$exprNode instanceof ParentExpression
$exprNode instanceof BlockReferenceExpression
|| $exprNode instanceof ParentExpression
) {
$exprNode->setAttribute('output', true);
@@ -113,7 +113,7 @@ final class OptimizerNodeVisitor implements NodeVisitorInterface
/**
* 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 @@ final class OptimizerNodeVisitor implements NodeVisitorInterface
/**
* 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
@@ -166,7 +166,7 @@ final class OptimizerNodeVisitor implements NodeVisitorInterface
&& '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 @@ final class OptimizerNodeVisitor implements NodeVisitorInterface
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();
}
@@ -189,7 +189,7 @@ final class OptimizerNodeVisitor implements NodeVisitorInterface
/**
* 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);

View File

@@ -303,10 +303,9 @@ class Parser
// 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))) {
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

View File

@@ -37,7 +37,7 @@ final class HtmlDumper extends BaseDumper
protected function formatNonTemplate(Profile $profile, $prefix): string
{
return sprintf('%s└ %s::%s(<span style="background-color: %s">%s</span>)', $prefix, $profile->getTemplate(), $profile->getType(), isset(self::$colors[$profile->getType()]) ? self::$colors[$profile->getType()] : 'auto', $profile->getName());
return sprintf('%s└ %s::%s(<span style="background-color: %s">%s</span>)', $prefix, $profile->getTemplate(), $profile->getType(), self::$colors[$profile->getType()] ?? 'auto', $profile->getName());
}
protected function formatTime(Profile $profile, $percent): string

View File

@@ -32,7 +32,7 @@ final class Profile implements \IteratorAggregate, \Serializable
{
$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();
}

View File

@@ -94,9 +94,8 @@ final class SecurityPolicy implements SecurityPolicyInterface
$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 @@ final class SecurityPolicy implements SecurityPolicyInterface
{
$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;
}
}

View File

@@ -181,7 +181,7 @@ abstract class Template
}
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 @@ abstract class Template
}
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();

View File

@@ -35,9 +35,7 @@ final class TemplateWrapper
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 = [])

View File

@@ -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)
{
@@ -67,9 +68,9 @@ final class Token
}
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
);
}
@@ -133,6 +134,9 @@ final class Token
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 @@ final class Token
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));
}

View File

@@ -32,7 +32,7 @@ final class FromTokenParser extends AbstractTokenParser
$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 @@ final class FromTokenParser extends AbstractTokenParser
if (!$stream->nextIf(/* Token::PUNCTUATION_TYPE */ 9, ',')) {
break;
}
} while (true);
}
$stream->expect(/* Token::BLOCK_END_TYPE */ 3);

View File

@@ -43,7 +43,7 @@ final class UseTokenParser extends AbstractTokenParser
$targets = [];
if ($stream->nextIf('with')) {
do {
while (true) {
$name = $stream->expect(/* Token::NAME_TYPE */ 5)->getValue();
$alias = $name;
@@ -56,7 +56,7 @@ final class UseTokenParser extends AbstractTokenParser
if (!$stream->nextIf(/* Token::PUNCTUATION_TYPE */ 9, ',')) {
break;
}
} while (true);
}
}
$stream->expect(/* Token::BLOCK_END_TYPE */ 3);

View File

@@ -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{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 @@ final class TwigFilter
/**
* Returns the callable to execute for this filter.
*
* @return callable|null
* @return callable|array{class-string, string}|null
*/
public function getCallable()
{

View File

@@ -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{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 @@ final class TwigFunction
/**
* Returns the callable to execute for this function.
*
* @return callable|null
* @return callable|array{class-string, string}|null
*/
public function getCallable()
{

View File

@@ -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{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 @@ final class TwigTest
/**
* Returns the callable to execute for this test.
*
* @return callable|null
* @return callable|array{class-string, string}|null
*/
public function getCallable()
{