mirror of
https://github.com/twigphp/Twig.git
synced 2026-08-30 03:57:21 +00:00
Merge branch '3.x' into 4.x
* 3.x: Refactor code Add some type hints Deprecate using ~ with + or - in an expression without using parentheses to clarify precedence Change some type hints to be more precise Deprecate not passing AbstractExpression args to most constructor arguments for classes extending AbstractExpression Remove obsolete method Add support for detecting if an expression had explicit parentheses Fix tests Unify error messages when a filter expects a mapping/sequence
This commit is contained in:
@@ -97,7 +97,7 @@ final class HtmlExtension extends AbstractExtension
|
||||
} 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 @@ final class HtmlExtension extends AbstractExtension
|
||||
$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)));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -19,7 +19,9 @@ 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\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;
|
||||
@@ -89,6 +91,8 @@ class ExpressionParser
|
||||
$token = $this->parser->getCurrentToken();
|
||||
}
|
||||
|
||||
$this->triggerPrecedenceDeprecations($expr, $token);
|
||||
|
||||
if (0 === $precedence) {
|
||||
return $this->parseConditionalExpression($expr);
|
||||
}
|
||||
@@ -96,6 +100,24 @@ class ExpressionParser
|
||||
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()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function parseArrow(): ?ArrowFunctionExpression
|
||||
{
|
||||
$stream = $this->parser->getStream();
|
||||
@@ -164,15 +186,10 @@ class ExpressionParser
|
||||
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();
|
||||
|
||||
@@ -327,6 +327,7 @@ final class CoreExtension extends AbstractExtension
|
||||
'..' => ['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],
|
||||
@@ -365,7 +366,7 @@ final class CoreExtension extends AbstractExtension
|
||||
}
|
||||
|
||||
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];
|
||||
@@ -435,7 +436,7 @@ final class CoreExtension extends AbstractExtension
|
||||
$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)];
|
||||
@@ -568,7 +569,7 @@ final class CoreExtension extends AbstractExtension
|
||||
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));
|
||||
@@ -665,7 +666,7 @@ final class CoreExtension extends AbstractExtension
|
||||
|
||||
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 = [...$result, ...$array];
|
||||
@@ -968,7 +969,7 @@ final class CoreExtension extends AbstractExtension
|
||||
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) {
|
||||
@@ -1536,7 +1537,7 @@ final class CoreExtension extends AbstractExtension
|
||||
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);
|
||||
@@ -1783,7 +1784,7 @@ final class CoreExtension extends AbstractExtension
|
||||
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) {
|
||||
@@ -1818,7 +1819,7 @@ final class CoreExtension extends AbstractExtension
|
||||
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');
|
||||
@@ -1838,7 +1839,7 @@ final class CoreExtension extends AbstractExtension
|
||||
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');
|
||||
@@ -1857,7 +1858,7 @@ final class CoreExtension extends AbstractExtension
|
||||
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');
|
||||
@@ -1876,7 +1877,7 @@ final class CoreExtension extends AbstractExtension
|
||||
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');
|
||||
@@ -1896,7 +1897,7 @@ final class CoreExtension extends AbstractExtension
|
||||
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');
|
||||
|
||||
@@ -25,4 +25,19 @@ abstract class AbstractExpression extends Node
|
||||
{
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,8 +18,19 @@ use Twig\Node\Node;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
@@ -22,8 +22,15 @@ use Twig\Node\Node;
|
||||
*/
|
||||
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;
|
||||
|
||||
@@ -14,6 +14,7 @@ namespace Twig\Node\Expression\Filter;
|
||||
use Twig\Attribute\FirstClassTwigCallableReady;
|
||||
use Twig\Compiler;
|
||||
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,7 +35,7 @@ use Twig\TwigTest;
|
||||
class DefaultFilter extends FilterExpression
|
||||
{
|
||||
#[FirstClassTwigCallableReady]
|
||||
public function __construct(Node $node, TwigFilter $filter, Node $arguments, int $lineno)
|
||||
public function __construct(AbstractExpression $node, TwigFilter $filter, Node $arguments, int $lineno)
|
||||
{
|
||||
$name = $filter->getName();
|
||||
$default = new FilterExpression($node, $filter, $arguments, $node->getTemplateLine());
|
||||
|
||||
@@ -14,6 +14,7 @@ namespace Twig\Node\Expression\Filter;
|
||||
use Twig\Attribute\FirstClassTwigCallableReady;
|
||||
use Twig\Compiler;
|
||||
use Twig\Node\EmptyNode;
|
||||
use Twig\Node\Expression\AbstractExpression;
|
||||
use Twig\Node\Expression\FilterExpression;
|
||||
use Twig\Node\Node;
|
||||
use Twig\TwigFilter;
|
||||
@@ -23,9 +24,16 @@ use Twig\TwigFilter;
|
||||
*/
|
||||
class RawFilter extends FilterExpression
|
||||
{
|
||||
/**
|
||||
* @param AbstractExpression $node
|
||||
*/
|
||||
#[FirstClassTwigCallableReady]
|
||||
public function __construct(Node $node, ?TwigFilter $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());
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ use Twig\TwigFilter;
|
||||
class FilterExpression extends CallExpression
|
||||
{
|
||||
#[FirstClassTwigCallableReady]
|
||||
public function __construct(Node $node, TwigFilter $filter, Node $arguments, int $lineno)
|
||||
public function __construct(AbstractExpression $node, TwigFilter $filter, Node $arguments, int $lineno)
|
||||
{
|
||||
parent::__construct(['node' => $node, 'arguments' => $arguments], ['name' => $filter->getName(), 'type' => 'filter', 'twig_callable' => $filter], $lineno);
|
||||
}
|
||||
|
||||
@@ -19,8 +19,15 @@ use Twig\Node\Node;
|
||||
*/
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
@@ -22,8 +22,19 @@ use Twig\TwigTest;
|
||||
|
||||
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) {
|
||||
|
||||
@@ -14,6 +14,7 @@ namespace Twig\Node\Expression\Test;
|
||||
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 @@ use Twig\TwigTest;
|
||||
*/
|
||||
class DefinedTest extends TestExpression
|
||||
{
|
||||
/**
|
||||
* @param AbstractExpression $node
|
||||
*/
|
||||
#[FirstClassTwigCallableReady]
|
||||
public function __construct(Node $node, TwigTest $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) {
|
||||
|
||||
@@ -19,7 +19,7 @@ use Twig\TwigTest;
|
||||
class TestExpression extends CallExpression
|
||||
{
|
||||
#[FirstClassTwigCallableReady]
|
||||
public function __construct(Node $node, TwigTest $test, ?Node $arguments, int $lineno)
|
||||
public function __construct(AbstractExpression $node, TwigTest $test, ?Node $arguments, int $lineno)
|
||||
{
|
||||
parent::__construct(['node' => $node, 'arguments' => $arguments ?: new EmptyNode()], ['name' => $test->getName(), 'type' => 'test', 'twig_callable' => $test], $lineno);
|
||||
}
|
||||
|
||||
@@ -18,26 +18,28 @@ use Twig\Node\Node;
|
||||
|
||||
abstract class AbstractUnary extends AbstractExpression
|
||||
{
|
||||
/**
|
||||
* @param AbstractExpression $node
|
||||
*/
|
||||
public function __construct(Node $node, int $lineno)
|
||||
{
|
||||
parent::__construct(['node' => $node], ['with_parentheses' => false], $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));
|
||||
}
|
||||
|
||||
public function wrapInParentheses(): void
|
||||
{
|
||||
$this->setAttribute('with_parentheses', true);
|
||||
parent::__construct(['node' => $node], ['with_parentheses' => false], $lineno);
|
||||
}
|
||||
|
||||
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(')');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ 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;
|
||||
@@ -100,8 +101,13 @@ final class EscaperNodeVisitor implements NodeVisitorInterface
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -109,12 +115,14 @@ final class EscaperNodeVisitor implements NodeVisitorInterface
|
||||
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);
|
||||
@@ -122,11 +130,15 @@ final class EscaperNodeVisitor implements NodeVisitorInterface
|
||||
$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)) {
|
||||
@@ -138,6 +150,7 @@ final class EscaperNodeVisitor implements NodeVisitorInterface
|
||||
|
||||
private function escapePrintNode(PrintNode $node, Environment $env, string $type): Node
|
||||
{
|
||||
/** @var AbstractExpression $expression */
|
||||
$expression = $node->getNode('expr');
|
||||
|
||||
if ($this->isSafeFor($type, $expression, $env)) {
|
||||
@@ -155,6 +168,7 @@ final class EscaperNodeVisitor implements NodeVisitorInterface
|
||||
return $filter;
|
||||
}
|
||||
|
||||
/** @var AbstractExpression $node */
|
||||
$node = $filter->getNode('node');
|
||||
if ($this->isSafeFor($type, $node, $env)) {
|
||||
return $filter;
|
||||
@@ -165,7 +179,7 @@ final class EscaperNodeVisitor implements NodeVisitorInterface
|
||||
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);
|
||||
|
||||
@@ -192,7 +206,7 @@ final class EscaperNodeVisitor implements NodeVisitorInterface
|
||||
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');
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user