Make the defined test implementation more generic

This commit is contained in:
Fabien Potencier
2025-02-21 08:30:25 +01:00
parent cfeb51b67f
commit 2d84abfd08
15 changed files with 180 additions and 52 deletions
+1
View File
@@ -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
+3
View File
@@ -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
-------------
+9 -1
View File
@@ -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')) {
+4 -2
View File
@@ -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'));
}
}
+13 -3
View File
@@ -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()));
}
+23 -4
View File
@@ -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(')
+6 -3
View File
@@ -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'))
+7 -4
View File
@@ -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;
}
}
+4 -26
View File
@@ -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];