mirror of
https://github.com/twigphp/Twig.git
synced 2026-09-01 13:07:22 +00:00
Make the defined test implementation more generic
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
# 3.21.0 (2025-XX-XX)
|
||||
|
||||
* Add `SupportDefinedTestInterface` for expression nodes supporting the `defined` test
|
||||
* Deprecate using the `|` operator in an expression with `+` or `-` without using parentheses to clarify precedence
|
||||
* Deprecate operator precedence outside of the [0, 512] range
|
||||
* Introduce expression parser classes to describe operators and operands provided by extensions
|
||||
|
||||
@@ -196,6 +196,9 @@ Nodes
|
||||
* The ``Twig\Node\Expression\ConditionalExpression`` class is deprecated as of
|
||||
Twig 3.17, use ``Twig\Node\Expression\Ternary\ConditionalTernary`` instead.
|
||||
|
||||
* The ``is_defined_test`` attribute is deprecated as of Twig 3.21, use
|
||||
``Twig\Node\Expression\SupportDefinedTestInterface`` instead.
|
||||
|
||||
Node Visitors
|
||||
-------------
|
||||
|
||||
|
||||
@@ -16,8 +16,10 @@ use Twig\Node\Expression\Unary\SpreadUnary;
|
||||
use Twig\Node\Expression\Unary\StringCastUnary;
|
||||
use Twig\Node\Expression\Variable\ContextVariable;
|
||||
|
||||
class ArrayExpression extends AbstractExpression
|
||||
class ArrayExpression extends AbstractExpression implements SupportDefinedTestInterface
|
||||
{
|
||||
use SupportDefinedTestTrait;
|
||||
|
||||
private $index;
|
||||
|
||||
public function __construct(array $elements, int $lineno)
|
||||
@@ -69,6 +71,12 @@ class ArrayExpression extends AbstractExpression
|
||||
|
||||
public function compile(Compiler $compiler): void
|
||||
{
|
||||
if ($this->definedTest) {
|
||||
$compiler->repr(true);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$compiler->raw('[');
|
||||
$first = true;
|
||||
$nextIndex = 0;
|
||||
|
||||
@@ -20,8 +20,11 @@ use Twig\Node\Node;
|
||||
*
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
*/
|
||||
class BlockReferenceExpression extends AbstractExpression
|
||||
class BlockReferenceExpression extends AbstractExpression implements SupportDefinedTestInterface
|
||||
{
|
||||
use SupportDefinedTestDeprecationTrait;
|
||||
use SupportDefinedTestTrait;
|
||||
|
||||
/**
|
||||
* @param AbstractExpression $name
|
||||
*/
|
||||
@@ -36,12 +39,12 @@ class BlockReferenceExpression extends AbstractExpression
|
||||
$nodes['template'] = $template;
|
||||
}
|
||||
|
||||
parent::__construct($nodes, ['is_defined_test' => false, 'output' => false], $lineno);
|
||||
parent::__construct($nodes, ['output' => false], $lineno);
|
||||
}
|
||||
|
||||
public function compile(Compiler $compiler): void
|
||||
{
|
||||
if ($this->getAttribute('is_defined_test')) {
|
||||
if ($this->definedTest) {
|
||||
$this->compileTemplateCall($compiler, 'hasBlock');
|
||||
} else {
|
||||
if ($this->getAttribute('output')) {
|
||||
|
||||
@@ -17,8 +17,10 @@ use Twig\Compiler;
|
||||
/**
|
||||
* @final
|
||||
*/
|
||||
class ConstantExpression extends AbstractExpression
|
||||
class ConstantExpression extends AbstractExpression implements SupportDefinedTestInterface
|
||||
{
|
||||
use SupportDefinedTestTrait;
|
||||
|
||||
public function __construct($value, int $lineno)
|
||||
{
|
||||
parent::__construct([], ['value' => $value], $lineno);
|
||||
@@ -26,6 +28,6 @@ class ConstantExpression extends AbstractExpression
|
||||
|
||||
public function compile(Compiler $compiler): void
|
||||
{
|
||||
$compiler->repr($this->getAttribute('value'));
|
||||
$compiler->repr($this->definedTest ? true : $this->getAttribute('value'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,8 +17,11 @@ use Twig\Node\NameDeprecation;
|
||||
use Twig\Node\Node;
|
||||
use Twig\TwigFunction;
|
||||
|
||||
class FunctionExpression extends CallExpression
|
||||
class FunctionExpression extends CallExpression implements SupportDefinedTestInterface
|
||||
{
|
||||
use SupportDefinedTestDeprecationTrait;
|
||||
use SupportDefinedTestTrait;
|
||||
|
||||
#[FirstClassTwigCallableReady]
|
||||
public function __construct(TwigFunction|string $function, Node $arguments, int $lineno)
|
||||
{
|
||||
@@ -29,7 +32,7 @@ class FunctionExpression extends CallExpression
|
||||
trigger_deprecation('twig/twig', '3.12', 'Not passing an instance of "TwigFunction" when creating a "%s" function of type "%s" is deprecated.', $name, static::class);
|
||||
}
|
||||
|
||||
parent::__construct(['arguments' => $arguments], ['name' => $name, 'type' => 'function', 'is_defined_test' => false], $lineno);
|
||||
parent::__construct(['arguments' => $arguments], ['name' => $name, 'type' => 'function'], $lineno);
|
||||
|
||||
if ($function instanceof TwigFunction) {
|
||||
$this->setAttribute('twig_callable', $function);
|
||||
@@ -44,6 +47,13 @@ class FunctionExpression extends CallExpression
|
||||
$this->deprecateAttribute('dynamic_name', new NameDeprecation('twig/twig', '3.12'));
|
||||
}
|
||||
|
||||
public function enableDefinedTest(): void
|
||||
{
|
||||
if ('constant' === $this->getAttribute('name')) {
|
||||
$this->definedTest = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
@@ -62,7 +72,7 @@ class FunctionExpression extends CallExpression
|
||||
$this->setAttribute('twig_callable', $compiler->getEnvironment()->getFunction($name));
|
||||
}
|
||||
|
||||
if ('constant' === $name && $this->getAttribute('is_defined_test')) {
|
||||
if ('constant' === $name && $this->isDefinedTestEnabled()) {
|
||||
$this->getNode('arguments')->setNode('checkDefined', new ConstantExpression(true, $this->getTemplateLine()));
|
||||
}
|
||||
|
||||
|
||||
@@ -17,8 +17,11 @@ use Twig\Extension\SandboxExtension;
|
||||
use Twig\Node\Expression\Variable\ContextVariable;
|
||||
use Twig\Template;
|
||||
|
||||
class GetAttrExpression extends AbstractExpression
|
||||
class GetAttrExpression extends AbstractExpression implements SupportDefinedTestInterface
|
||||
{
|
||||
use SupportDefinedTestDeprecationTrait;
|
||||
use SupportDefinedTestTrait;
|
||||
|
||||
/**
|
||||
* @param ArrayExpression|NameExpression|null $arguments
|
||||
*/
|
||||
@@ -33,7 +36,13 @@ class GetAttrExpression extends AbstractExpression
|
||||
trigger_deprecation('twig/twig', '3.15', \sprintf('Not passing a "%s" instance as the "arguments" argument of the "%s" constructor is deprecated ("%s" given).', ArrayExpression::class, static::class, $arguments::class));
|
||||
}
|
||||
|
||||
parent::__construct($nodes, ['type' => $type, 'is_defined_test' => false, 'ignore_strict_check' => false, 'optimizable' => true], $lineno);
|
||||
parent::__construct($nodes, ['type' => $type, 'ignore_strict_check' => false, 'optimizable' => true], $lineno);
|
||||
}
|
||||
|
||||
public function enableDefinedTest(): void
|
||||
{
|
||||
$this->definedTest = true;
|
||||
$this->changeIgnoreStrictCheck($this);
|
||||
}
|
||||
|
||||
public function compile(Compiler $compiler): void
|
||||
@@ -45,7 +54,7 @@ class GetAttrExpression extends AbstractExpression
|
||||
if (
|
||||
$this->getAttribute('optimizable')
|
||||
&& (!$env->isStrictVariables() || $this->getAttribute('ignore_strict_check'))
|
||||
&& !$this->getAttribute('is_defined_test')
|
||||
&& !$this->definedTest
|
||||
&& Template::ARRAY_CALL === $this->getAttribute('type')
|
||||
) {
|
||||
$var = '$'.$compiler->getVarName();
|
||||
@@ -104,7 +113,7 @@ class GetAttrExpression extends AbstractExpression
|
||||
|
||||
$compiler->raw(', ')
|
||||
->repr($this->getAttribute('type'))
|
||||
->raw(', ')->repr($this->getAttribute('is_defined_test'))
|
||||
->raw(', ')->repr($this->definedTest)
|
||||
->raw(', ')->repr($this->getAttribute('ignore_strict_check'))
|
||||
->raw(', ')->repr($env->hasExtension(SandboxExtension::class))
|
||||
->raw(', ')->repr($this->getNode('node')->getTemplateLine())
|
||||
@@ -115,4 +124,14 @@ class GetAttrExpression extends AbstractExpression
|
||||
$compiler->raw(')');
|
||||
}
|
||||
}
|
||||
|
||||
private function changeIgnoreStrictCheck(GetAttrExpression $node): void
|
||||
{
|
||||
$node->setAttribute('optimizable', false);
|
||||
$node->setAttribute('ignore_strict_check', true);
|
||||
|
||||
if ($node->getNode('node') instanceof GetAttrExpression) {
|
||||
$this->changeIgnoreStrictCheck($node->getNode('node'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,16 +19,19 @@ use Twig\Node\Expression\Variable\TemplateVariable;
|
||||
*
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
*/
|
||||
class MacroReferenceExpression extends AbstractExpression
|
||||
class MacroReferenceExpression extends AbstractExpression implements SupportDefinedTestInterface
|
||||
{
|
||||
use SupportDefinedTestDeprecationTrait;
|
||||
use SupportDefinedTestTrait;
|
||||
|
||||
public function __construct(TemplateVariable $template, string $name, AbstractExpression $arguments, int $lineno)
|
||||
{
|
||||
parent::__construct(['template' => $template, 'arguments' => $arguments], ['name' => $name, 'is_defined_test' => false], $lineno);
|
||||
parent::__construct(['template' => $template, 'arguments' => $arguments], ['name' => $name], $lineno);
|
||||
}
|
||||
|
||||
public function compile(Compiler $compiler): void
|
||||
{
|
||||
if ($this->getAttribute('is_defined_test')) {
|
||||
if ($this->definedTest) {
|
||||
$compiler
|
||||
->subcompile($this->getNode('template'))
|
||||
->raw('->hasMacro(')
|
||||
|
||||
@@ -14,13 +14,16 @@ namespace Twig\Node\Expression;
|
||||
use Twig\Compiler;
|
||||
use Twig\Node\Expression\Variable\ContextVariable;
|
||||
|
||||
class MethodCallExpression extends AbstractExpression
|
||||
class MethodCallExpression extends AbstractExpression implements SupportDefinedTestInterface
|
||||
{
|
||||
use SupportDefinedTestDeprecationTrait;
|
||||
use SupportDefinedTestTrait;
|
||||
|
||||
public function __construct(AbstractExpression $node, string $method, ArrayExpression $arguments, int $lineno)
|
||||
{
|
||||
trigger_deprecation('twig/twig', '3.15', 'The "%s" class is deprecated, use "%s" instead.', __CLASS__, MacroReferenceExpression::class);
|
||||
|
||||
parent::__construct(['node' => $node, 'arguments' => $arguments], ['method' => $method, 'safe' => false, 'is_defined_test' => false], $lineno);
|
||||
parent::__construct(['node' => $node, 'arguments' => $arguments], ['method' => $method, 'safe' => false], $lineno);
|
||||
|
||||
if ($node instanceof ContextVariable) {
|
||||
$node->setAttribute('always_defined', true);
|
||||
@@ -29,7 +32,7 @@ class MethodCallExpression extends AbstractExpression
|
||||
|
||||
public function compile(Compiler $compiler): void
|
||||
{
|
||||
if ($this->getAttribute('is_defined_test')) {
|
||||
if ($this->definedTest) {
|
||||
$compiler
|
||||
->raw('method_exists($macros[')
|
||||
->repr($this->getNode('node')->getAttribute('name'))
|
||||
|
||||
@@ -15,8 +15,11 @@ namespace Twig\Node\Expression;
|
||||
use Twig\Compiler;
|
||||
use Twig\Node\Expression\Variable\ContextVariable;
|
||||
|
||||
class NameExpression extends AbstractExpression
|
||||
class NameExpression extends AbstractExpression implements SupportDefinedTestInterface
|
||||
{
|
||||
use SupportDefinedTestDeprecationTrait;
|
||||
use SupportDefinedTestTrait;
|
||||
|
||||
private $specialVars = [
|
||||
'_self' => '$this->getTemplateName()',
|
||||
'_context' => '$context',
|
||||
@@ -29,7 +32,7 @@ class NameExpression extends AbstractExpression
|
||||
trigger_deprecation('twig/twig', '3.15', 'The "%s" class is deprecated, use "%s" instead.', self::class, ContextVariable::class);
|
||||
}
|
||||
|
||||
parent::__construct([], ['name' => $name, 'is_defined_test' => false, 'ignore_strict_check' => false, 'always_defined' => false], $lineno);
|
||||
parent::__construct([], ['name' => $name, 'ignore_strict_check' => false, 'always_defined' => false], $lineno);
|
||||
}
|
||||
|
||||
public function compile(Compiler $compiler): void
|
||||
@@ -38,7 +41,7 @@ class NameExpression extends AbstractExpression
|
||||
|
||||
$compiler->addDebugInfo($this);
|
||||
|
||||
if ($this->getAttribute('is_defined_test')) {
|
||||
if ($this->definedTest) {
|
||||
if (isset($this->specialVars[$name]) || $this->getAttribute('always_defined')) {
|
||||
$compiler->repr(true);
|
||||
} elseif (\PHP_VERSION_ID >= 70400) {
|
||||
@@ -107,6 +110,6 @@ class NameExpression extends AbstractExpression
|
||||
{
|
||||
trigger_deprecation('twig/twig', '3.11', 'The "%s()" method is deprecated and will be removed in Twig 4.0.', __METHOD__);
|
||||
|
||||
return !$this->isSpecial() && !$this->getAttribute('is_defined_test');
|
||||
return !isset($this->specialVars[$this->getAttribute('name')]) && !$this->definedTest;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
<?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;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* To be removed in 4.0
|
||||
*
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
*/
|
||||
trait SupportDefinedTestDeprecationTrait
|
||||
{
|
||||
public function getAttribute($name, $default = null)
|
||||
{
|
||||
if ('is_defined_test' === $name) {
|
||||
trigger_deprecation('twig/twig', '3.21', 'The "is_defined_test" attribute is deprecated, call "isDefinedTestEnabled()" instead.');
|
||||
|
||||
return $this->isDefinedTestEnabled();
|
||||
}
|
||||
|
||||
return parent::getAttribute($name, $default);
|
||||
}
|
||||
|
||||
public function setAttribute(string $name, $value): void
|
||||
{
|
||||
if ('is_defined_test' === $name) {
|
||||
trigger_deprecation('twig/twig', '3.21', 'The "is_defined_test" attribute is deprecated, call "enableDefinedTest()" instead.');
|
||||
|
||||
$this->definedTest = (bool) $value;
|
||||
} else {
|
||||
parent::setAttribute($name, $value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?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;
|
||||
|
||||
/**
|
||||
* Interface implemented by expressions that support the defined test.
|
||||
*
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
*/
|
||||
interface SupportDefinedTestInterface
|
||||
{
|
||||
public function enableDefinedTest(): void;
|
||||
|
||||
public function isDefinedTestEnabled(): bool;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?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;
|
||||
|
||||
trait SupportDefinedTestTrait
|
||||
{
|
||||
private bool $definedTest = false;
|
||||
|
||||
public function enableDefinedTest(): void
|
||||
{
|
||||
$this->definedTest = true;
|
||||
}
|
||||
|
||||
public function isDefinedTestEnabled(): bool
|
||||
{
|
||||
return $this->definedTest;
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,7 @@ use Twig\Node\Expression\FunctionExpression;
|
||||
use Twig\Node\Expression\GetAttrExpression;
|
||||
use Twig\Node\Expression\MacroReferenceExpression;
|
||||
use Twig\Node\Expression\MethodCallExpression;
|
||||
use Twig\Node\Expression\SupportDefinedTestInterface;
|
||||
use Twig\Node\Expression\TestExpression;
|
||||
use Twig\Node\Expression\Variable\ContextVariable;
|
||||
use Twig\Node\Node;
|
||||
@@ -49,25 +50,12 @@ class DefinedTest extends TestExpression
|
||||
trigger_deprecation('twig/twig', '3.15', 'Not passing a "%s" instance to the "node" argument of "%s" is deprecated ("%s" given).', AbstractExpression::class, static::class, $node::class);
|
||||
}
|
||||
|
||||
if ($node instanceof ContextVariable) {
|
||||
$node->setAttribute('is_defined_test', true);
|
||||
} elseif ($node instanceof GetAttrExpression) {
|
||||
$node->setAttribute('is_defined_test', true);
|
||||
$this->changeIgnoreStrictCheck($node);
|
||||
} elseif ($node instanceof BlockReferenceExpression) {
|
||||
$node->setAttribute('is_defined_test', true);
|
||||
} elseif ($node instanceof MacroReferenceExpression) {
|
||||
$node->setAttribute('is_defined_test', true);
|
||||
} elseif ($node instanceof FunctionExpression && 'constant' === $node->getAttribute('name')) {
|
||||
$node->setAttribute('is_defined_test', true);
|
||||
} elseif ($node instanceof ConstantExpression || $node instanceof ArrayExpression) {
|
||||
$node = new ConstantExpression(true, $node->getTemplateLine());
|
||||
} elseif ($node instanceof MethodCallExpression) {
|
||||
$node->setAttribute('is_defined_test', true);
|
||||
} else {
|
||||
if (!$node instanceof SupportDefinedTestInterface) {
|
||||
throw new SyntaxError('The "defined" test only works with simple variables.', $lineno);
|
||||
}
|
||||
|
||||
$node->enableDefinedTest();
|
||||
|
||||
if (\is_string($name) && 'defined' !== $name) {
|
||||
trigger_deprecation('twig/twig', '3.12', 'Creating a "DefinedTest" instance with a test name that is not "defined" is deprecated.');
|
||||
}
|
||||
@@ -75,16 +63,6 @@ class DefinedTest extends TestExpression
|
||||
parent::__construct($node, $name, $arguments, $lineno);
|
||||
}
|
||||
|
||||
private function changeIgnoreStrictCheck(GetAttrExpression $node): void
|
||||
{
|
||||
$node->setAttribute('optimizable', false);
|
||||
$node->setAttribute('ignore_strict_check', true);
|
||||
|
||||
if ($node->getNode('node') instanceof GetAttrExpression) {
|
||||
$this->changeIgnoreStrictCheck($node->getNode('node'));
|
||||
}
|
||||
}
|
||||
|
||||
public function compile(Compiler $compiler): void
|
||||
{
|
||||
$compiler->subcompile($this->getNode('node'));
|
||||
|
||||
@@ -32,7 +32,7 @@ class ContextVariableTest extends NodeTestCase
|
||||
$node = new ContextVariable($special, 1);
|
||||
yield $special => [$node, "// line 1\n$compiled"];
|
||||
$node = new ContextVariable($special, 1);
|
||||
$node->setAttribute('is_defined_test', true);
|
||||
$node->enableDefinedTest();
|
||||
yield $special.'_defined_test' => [$node, "// line 1\ntrue"];
|
||||
}
|
||||
|
||||
@@ -59,13 +59,13 @@ class ContextVariableTest extends NodeTestCase
|
||||
|
||||
// is defined test
|
||||
$node = new ContextVariable('foo', 1);
|
||||
$node->setAttribute('is_defined_test', true);
|
||||
$node->enableDefinedTest();
|
||||
yield 'is_defined_test_strict' => [$node, "// line 1\narray_key_exists(\"foo\", \$context)", $envStrict];
|
||||
yield 'is_defined_test_non_strict' => [$node, "// line 1\narray_key_exists(\"foo\", \$context)", $env];
|
||||
|
||||
// is defined test // always defined
|
||||
$node = new ContextVariable('foo', 1);
|
||||
$node->setAttribute('is_defined_test', true);
|
||||
$node->enableDefinedTest();
|
||||
$node->setAttribute('always_defined', true);
|
||||
yield 'is_defined_test_always_defined_strict' => [$node, "// line 1\ntrue", $envStrict];
|
||||
yield 'is_defined_test_always_defined_non_strict' => [$node, "// line 1\ntrue", $env];
|
||||
|
||||
Reference in New Issue
Block a user