From 2d84abfd08c669d89d8125793752af68a8d5521a Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Fri, 21 Feb 2025 08:30:25 +0100 Subject: [PATCH] Make the defined test implementation more generic --- CHANGELOG | 1 + doc/deprecated.rst | 3 ++ src/Node/Expression/ArrayExpression.php | 10 ++++- .../Expression/BlockReferenceExpression.php | 9 ++-- src/Node/Expression/ConstantExpression.php | 6 ++- src/Node/Expression/FunctionExpression.php | 16 +++++-- src/Node/Expression/GetAttrExpression.php | 27 ++++++++++-- .../Expression/MacroReferenceExpression.php | 9 ++-- src/Node/Expression/MethodCallExpression.php | 9 ++-- src/Node/Expression/NameExpression.php | 11 +++-- .../SupportDefinedTestDeprecationTrait.php | 44 +++++++++++++++++++ .../SupportDefinedTestInterface.php | 24 ++++++++++ .../Expression/SupportDefinedTestTrait.php | 27 ++++++++++++ src/Node/Expression/Test/DefinedTest.php | 30 ++----------- .../Variable/ContextVariableTest.php | 6 +-- 15 files changed, 180 insertions(+), 52 deletions(-) create mode 100644 src/Node/Expression/SupportDefinedTestDeprecationTrait.php create mode 100644 src/Node/Expression/SupportDefinedTestInterface.php create mode 100644 src/Node/Expression/SupportDefinedTestTrait.php diff --git a/CHANGELOG b/CHANGELOG index cb70fd0c7..d36aa696f 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -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 diff --git a/doc/deprecated.rst b/doc/deprecated.rst index 6e8212fb0..55c5e25a6 100644 --- a/doc/deprecated.rst +++ b/doc/deprecated.rst @@ -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 ------------- diff --git a/src/Node/Expression/ArrayExpression.php b/src/Node/Expression/ArrayExpression.php index c9b3a3ec1..e58f43c91 100644 --- a/src/Node/Expression/ArrayExpression.php +++ b/src/Node/Expression/ArrayExpression.php @@ -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; diff --git a/src/Node/Expression/BlockReferenceExpression.php b/src/Node/Expression/BlockReferenceExpression.php index 508ca2d83..8fe3f8b12 100644 --- a/src/Node/Expression/BlockReferenceExpression.php +++ b/src/Node/Expression/BlockReferenceExpression.php @@ -20,8 +20,11 @@ use Twig\Node\Node; * * @author Fabien Potencier */ -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')) { diff --git a/src/Node/Expression/ConstantExpression.php b/src/Node/Expression/ConstantExpression.php index 2a8909d54..8f8b2ffe9 100644 --- a/src/Node/Expression/ConstantExpression.php +++ b/src/Node/Expression/ConstantExpression.php @@ -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')); } } diff --git a/src/Node/Expression/FunctionExpression.php b/src/Node/Expression/FunctionExpression.php index 5e22e73e8..183145c41 100644 --- a/src/Node/Expression/FunctionExpression.php +++ b/src/Node/Expression/FunctionExpression.php @@ -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())); } diff --git a/src/Node/Expression/GetAttrExpression.php b/src/Node/Expression/GetAttrExpression.php index e072f2a0b..781c8af38 100644 --- a/src/Node/Expression/GetAttrExpression.php +++ b/src/Node/Expression/GetAttrExpression.php @@ -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')); + } + } } diff --git a/src/Node/Expression/MacroReferenceExpression.php b/src/Node/Expression/MacroReferenceExpression.php index abe99aa35..fd7f1e733 100644 --- a/src/Node/Expression/MacroReferenceExpression.php +++ b/src/Node/Expression/MacroReferenceExpression.php @@ -19,16 +19,19 @@ use Twig\Node\Expression\Variable\TemplateVariable; * * @author Fabien Potencier */ -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(') diff --git a/src/Node/Expression/MethodCallExpression.php b/src/Node/Expression/MethodCallExpression.php index 922b98b10..4b180534d 100644 --- a/src/Node/Expression/MethodCallExpression.php +++ b/src/Node/Expression/MethodCallExpression.php @@ -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')) diff --git a/src/Node/Expression/NameExpression.php b/src/Node/Expression/NameExpression.php index 2872ba413..0e0367420 100644 --- a/src/Node/Expression/NameExpression.php +++ b/src/Node/Expression/NameExpression.php @@ -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; } } diff --git a/src/Node/Expression/SupportDefinedTestDeprecationTrait.php b/src/Node/Expression/SupportDefinedTestDeprecationTrait.php new file mode 100644 index 000000000..664464bb2 --- /dev/null +++ b/src/Node/Expression/SupportDefinedTestDeprecationTrait.php @@ -0,0 +1,44 @@ + + */ +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); + } + } +} diff --git a/src/Node/Expression/SupportDefinedTestInterface.php b/src/Node/Expression/SupportDefinedTestInterface.php new file mode 100644 index 000000000..450c691c5 --- /dev/null +++ b/src/Node/Expression/SupportDefinedTestInterface.php @@ -0,0 +1,24 @@ + + */ +interface SupportDefinedTestInterface +{ + public function enableDefinedTest(): void; + + public function isDefinedTestEnabled(): bool; +} diff --git a/src/Node/Expression/SupportDefinedTestTrait.php b/src/Node/Expression/SupportDefinedTestTrait.php new file mode 100644 index 000000000..4cf1a58d5 --- /dev/null +++ b/src/Node/Expression/SupportDefinedTestTrait.php @@ -0,0 +1,27 @@ +definedTest = true; + } + + public function isDefinedTestEnabled(): bool + { + return $this->definedTest; + } +} diff --git a/src/Node/Expression/Test/DefinedTest.php b/src/Node/Expression/Test/DefinedTest.php index 9612892be..f17715bc6 100644 --- a/src/Node/Expression/Test/DefinedTest.php +++ b/src/Node/Expression/Test/DefinedTest.php @@ -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')); diff --git a/tests/Node/Expression/Variable/ContextVariableTest.php b/tests/Node/Expression/Variable/ContextVariableTest.php index 46ab171a0..e98aa7219 100644 --- a/tests/Node/Expression/Variable/ContextVariableTest.php +++ b/tests/Node/Expression/Variable/ContextVariableTest.php @@ -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];