diff --git a/extra/html-extra/HtmlExtension.php b/extra/html-extra/HtmlExtension.php index b85895201..4721fa37f 100644 --- a/extra/html-extra/HtmlExtension.php +++ b/extra/html-extra/HtmlExtension.php @@ -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))); } } diff --git a/extra/html-extra/Tests/Fixtures/html_classes_with_unsupported_arg.test b/extra/html-extra/Tests/Fixtures/html_classes_with_unsupported_arg.test index 85faed624..21ca373f8 100644 --- a/extra/html-extra/Tests/Fixtures/html_classes_with_unsupported_arg.test +++ b/extra/html-extra/Tests/Fixtures/html_classes_with_unsupported_arg.test @@ -5,4 +5,4 @@ --DATA-- return [] --EXCEPTION-- -Twig\Error\RuntimeError: The html_classes function argument 0 should be either a string or an array, got "boolean" in "index.twig" at line 2. +Twig\Error\RuntimeError: The "html_classes" function argument 0 should be either a string or an array, got "bool" in "index.twig" at line 2. diff --git a/extra/html-extra/Tests/Fixtures/html_classes_with_unsupported_key.test b/extra/html-extra/Tests/Fixtures/html_classes_with_unsupported_key.test index b74748c58..708cb255b 100644 --- a/extra/html-extra/Tests/Fixtures/html_classes_with_unsupported_key.test +++ b/extra/html-extra/Tests/Fixtures/html_classes_with_unsupported_key.test @@ -5,4 +5,4 @@ --DATA-- return [] --EXCEPTION-- -Twig\Error\RuntimeError: The html_classes function argument 0 (key 0) should be a string, got "integer" in "index.twig" at line 2. +Twig\Error\RuntimeError: The "html_classes" function argument 0 (key 0) should be a string, got "int" in "index.twig" at line 2. diff --git a/src/ExpressionParser.php b/src/ExpressionParser.php index 467b6d465..402b4aaa9 100644 --- a/src/ExpressionParser.php +++ b/src/ExpressionParser.php @@ -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(); diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index 8db7f7ee6..9c4b56807 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -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'); diff --git a/src/Node/Expression/AbstractExpression.php b/src/Node/Expression/AbstractExpression.php index 1692f5671..22d8617cd 100644 --- a/src/Node/Expression/AbstractExpression.php +++ b/src/Node/Expression/AbstractExpression.php @@ -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'); + } } diff --git a/src/Node/Expression/Binary/AbstractBinary.php b/src/Node/Expression/Binary/AbstractBinary.php index c424e5cc5..ccd3be44f 100644 --- a/src/Node/Expression/Binary/AbstractBinary.php +++ b/src/Node/Expression/Binary/AbstractBinary.php @@ -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); } diff --git a/src/Node/Expression/BlockReferenceExpression.php b/src/Node/Expression/BlockReferenceExpression.php index acd231e15..ed88c6094 100644 --- a/src/Node/Expression/BlockReferenceExpression.php +++ b/src/Node/Expression/BlockReferenceExpression.php @@ -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; diff --git a/src/Node/Expression/Filter/DefaultFilter.php b/src/Node/Expression/Filter/DefaultFilter.php index f0bc8e57c..e81da7632 100644 --- a/src/Node/Expression/Filter/DefaultFilter.php +++ b/src/Node/Expression/Filter/DefaultFilter.php @@ -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()); diff --git a/src/Node/Expression/Filter/RawFilter.php b/src/Node/Expression/Filter/RawFilter.php index 229320710..c9c71a7b6 100644 --- a/src/Node/Expression/Filter/RawFilter.php +++ b/src/Node/Expression/Filter/RawFilter.php @@ -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()); } diff --git a/src/Node/Expression/FilterExpression.php b/src/Node/Expression/FilterExpression.php index b0800e417..c463fb3d0 100644 --- a/src/Node/Expression/FilterExpression.php +++ b/src/Node/Expression/FilterExpression.php @@ -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); } diff --git a/src/Node/Expression/InlinePrint.php b/src/Node/Expression/InlinePrint.php index 0a3c2e4f9..82d17d626 100644 --- a/src/Node/Expression/InlinePrint.php +++ b/src/Node/Expression/InlinePrint.php @@ -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); } diff --git a/src/Node/Expression/NullCoalesceExpression.php b/src/Node/Expression/NullCoalesceExpression.php index dd2384f95..1a5d90286 100644 --- a/src/Node/Expression/NullCoalesceExpression.php +++ b/src/Node/Expression/NullCoalesceExpression.php @@ -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) { diff --git a/src/Node/Expression/Test/DefinedTest.php b/src/Node/Expression/Test/DefinedTest.php index d3ad881a0..d09159df5 100644 --- a/src/Node/Expression/Test/DefinedTest.php +++ b/src/Node/Expression/Test/DefinedTest.php @@ -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) { diff --git a/src/Node/Expression/TestExpression.php b/src/Node/Expression/TestExpression.php index bca8c01ce..83745121a 100644 --- a/src/Node/Expression/TestExpression.php +++ b/src/Node/Expression/TestExpression.php @@ -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); } diff --git a/src/Node/Expression/Unary/AbstractUnary.php b/src/Node/Expression/Unary/AbstractUnary.php index b9b44ed4f..96739e2b8 100644 --- a/src/Node/Expression/Unary/AbstractUnary.php +++ b/src/Node/Expression/Unary/AbstractUnary.php @@ -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(')'); } } diff --git a/src/NodeVisitor/EscaperNodeVisitor.php b/src/NodeVisitor/EscaperNodeVisitor.php index 83fe5087d..74db066b1 100644 --- a/src/NodeVisitor/EscaperNodeVisitor.php +++ b/src/NodeVisitor/EscaperNodeVisitor.php @@ -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'); diff --git a/tests/Fixtures/exceptions/exception_in_extension_extends.test b/tests/Fixtures/exceptions/exception_in_extension_extends.test index d3361bf09..3b9ddeec8 100644 --- a/tests/Fixtures/exceptions/exception_in_extension_extends.test +++ b/tests/Fixtures/exceptions/exception_in_extension_extends.test @@ -9,4 +9,4 @@ Exception thrown from a child for an extension error --DATA-- return [] --EXCEPTION-- -Twig\Error\RuntimeError: The "random" function cannot pick from an empty sequence/mapping in "base.twig" at line 4. +Twig\Error\RuntimeError: The "random" function cannot pick from an empty sequence or mapping in "base.twig" at line 4. diff --git a/tests/Fixtures/exceptions/exception_in_extension_include.test b/tests/Fixtures/exceptions/exception_in_extension_include.test index be25db3b8..42927c2f6 100644 --- a/tests/Fixtures/exceptions/exception_in_extension_include.test +++ b/tests/Fixtures/exceptions/exception_in_extension_include.test @@ -9,4 +9,4 @@ Exception thrown from an include for an extension error --DATA-- return [] --EXCEPTION-- -Twig\Error\RuntimeError: The "random" function cannot pick from an empty sequence/mapping in "content.twig" at line 4. +Twig\Error\RuntimeError: The "random" function cannot pick from an empty sequence or mapping in "content.twig" at line 4. diff --git a/tests/Fixtures/filters/replace_invalid_arg.test b/tests/Fixtures/filters/replace_invalid_arg.test index ea1632500..3b1429c90 100644 --- a/tests/Fixtures/filters/replace_invalid_arg.test +++ b/tests/Fixtures/filters/replace_invalid_arg.test @@ -5,4 +5,4 @@ Exception for invalid argument type in replace call --DATA-- return ['stdClass' => new \stdClass()] --EXCEPTION-- -Twig\Error\RuntimeError: The "replace" filter expects a sequence/mapping or "Traversable" as replace values, got "stdClass" in "index.twig" at line 2. +Twig\Error\RuntimeError: The "replace" filter expects a sequence or a mapping, got "stdClass" in "index.twig" at line 2. diff --git a/tests/Fixtures/functions/cycle_empty_mapping.test b/tests/Fixtures/functions/cycle_empty_mapping.test index ca241d8f3..65b1d9491 100644 --- a/tests/Fixtures/functions/cycle_empty_mapping.test +++ b/tests/Fixtures/functions/cycle_empty_mapping.test @@ -5,4 +5,4 @@ --DATA-- return [] --EXCEPTION-- -Twig\Error\RuntimeError: The "cycle" function does not work on empty sequences in "index.twig" at line 2. +Twig\Error\RuntimeError: The "cycle" function expects a non-empty sequence in "index.twig" at line 2. diff --git a/tests/Fixtures/functions/cycle_empty_sequence.test b/tests/Fixtures/functions/cycle_empty_sequence.test index 846913b20..5d60bb1fa 100644 --- a/tests/Fixtures/functions/cycle_empty_sequence.test +++ b/tests/Fixtures/functions/cycle_empty_sequence.test @@ -5,4 +5,4 @@ --DATA-- return [] --EXCEPTION-- -Twig\Error\RuntimeError: The "cycle" function does not work on empty sequences in "index.twig" at line 2. +Twig\Error\RuntimeError: The "cycle" function expects a non-empty sequence in "index.twig" at line 2. diff --git a/tests/Fixtures/operators/concat_vs_add_sub.test b/tests/Fixtures/operators/concat_vs_add_sub.test new file mode 100644 index 000000000..298a3499b --- /dev/null +++ b/tests/Fixtures/operators/concat_vs_add_sub.test @@ -0,0 +1,16 @@ +--TEST-- ++/- will have a higher precedence over ~ in Twig 4.0 +--TEMPLATE-- +{{ 1 + 41 }} +{{ '42==' ~ '42' }} +{{ '42==' ~ (1 + 41) }} +{{ '42==' ~ (43 - 1) }} +{{ ('42' ~ 43) - 1 }} +--DATA-- +return [] +--EXPECT-- +42 +42==42 +42==42 +42==42 +4242 diff --git a/tests/Fixtures/operators/contat_vs_add_sub.legacy.test b/tests/Fixtures/operators/contat_vs_add_sub.legacy.test new file mode 100644 index 000000000..dc4a1ef50 --- /dev/null +++ b/tests/Fixtures/operators/contat_vs_add_sub.legacy.test @@ -0,0 +1,13 @@ +--TEST-- ++/- will have a higher precedence over ~ in Twig 4.0 +--DEPRECATION-- +Since twig/twig 3.15: As "+" / "-" will have a higher precedence than "~" in Twig 4.0, please add parentheses to keep the current behavior in "index.twig" at line 2. +Since twig/twig 3.15: As "+" / "-" will have a higher precedence than "~" in Twig 4.0, please add parentheses to keep the current behavior in "index.twig" at line 3. +--TEMPLATE-- +{{ '42' ~ 1 + 41 }} +{{ '42' ~ 43 - 1 }} +--DATA-- +return [] +--EXPECT-- +462 +4242