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:
Fabien Potencier
2024-09-30 22:46:04 +02:00
24 changed files with 175 additions and 44 deletions
+2 -2
View File
@@ -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.
+24 -7
View File
@@ -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();
+13 -12
View File
@@ -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;
+2 -1
View File
@@ -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());
+8
View File
@@ -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());
}
+1 -1
View File
@@ -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);
}
+7
View File
@@ -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) {
+8
View File
@@ -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) {
+1 -1
View File
@@ -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);
}
+9 -7
View File
@@ -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(')');
}
}
+20 -6
View File
@@ -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