Skip to content

Commit

Permalink
feature #4590 Make the defined test implementation more generic (fabpot)
Browse files Browse the repository at this point in the history
This PR was merged into the 3.x branch.

Discussion
----------

Make the defined test implementation more generic

Commits
-------

2d84abf Make the defined test implementation more generic
  • Loading branch information
fabpot committed Feb 21, 2025
2 parents 9b4db2e + 2d84abf commit 9f275f6
Show file tree
Hide file tree
Showing 15 changed files with 180 additions and 52 deletions.
1 change: 1 addition & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
@@ -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
Expand Down
3 changes: 3 additions & 0 deletions doc/deprecated.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
-------------

Expand Down
10 changes: 9 additions & 1 deletion src/Node/Expression/ArrayExpression.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@
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)
Expand Down Expand Up @@ -69,6 +71,12 @@ public function addElement(AbstractExpression $value, ?AbstractExpression $key =

public function compile(Compiler $compiler): void
{
if ($this->definedTest) {
$compiler->repr(true);

return;
}

$compiler->raw('[');
$first = true;
$nextIndex = 0;
Expand Down
9 changes: 6 additions & 3 deletions src/Node/Expression/BlockReferenceExpression.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,11 @@
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class BlockReferenceExpression extends AbstractExpression
class BlockReferenceExpression extends AbstractExpression implements SupportDefinedTestInterface
{
use SupportDefinedTestDeprecationTrait;
use SupportDefinedTestTrait;

/**
* @param AbstractExpression $name
*/
Expand All @@ -36,12 +39,12 @@ public function __construct(Node $name, ?Node $template, int $lineno)
$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')) {
Expand Down
6 changes: 4 additions & 2 deletions src/Node/Expression/ConstantExpression.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,17 @@
/**
* @final
*/
class ConstantExpression extends AbstractExpression
class ConstantExpression extends AbstractExpression implements SupportDefinedTestInterface
{
use SupportDefinedTestTrait;

public function __construct($value, int $lineno)
{
parent::__construct([], ['value' => $value], $lineno);
}

public function compile(Compiler $compiler): void
{
$compiler->repr($this->getAttribute('value'));
$compiler->repr($this->definedTest ? true : $this->getAttribute('value'));
}
}
16 changes: 13 additions & 3 deletions src/Node/Expression/FunctionExpression.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,11 @@
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)
{
Expand All @@ -29,7 +32,7 @@ public function __construct(TwigFunction|string $function, Node $arguments, int
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);
Expand All @@ -44,6 +47,13 @@ public function __construct(TwigFunction|string $function, Node $arguments, int
$this->deprecateAttribute('dynamic_name', new NameDeprecation('twig/twig', '3.12'));
}

public function enableDefinedTest(): void
{
if ('constant' === $this->getAttribute('name')) {
$this->definedTest = true;
}
}

/**
* @return void
*/
Expand All @@ -62,7 +72,7 @@ public function compile(Compiler $compiler)
$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()));
}

Expand Down
27 changes: 23 additions & 4 deletions src/Node/Expression/GetAttrExpression.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,11 @@
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
*/
Expand All @@ -33,7 +36,13 @@ public function __construct(AbstractExpression $node, AbstractExpression $attrib
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
Expand All @@ -45,7 +54,7 @@ public function compile(Compiler $compiler): void
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();
Expand Down Expand Up @@ -104,7 +113,7 @@ public function compile(Compiler $compiler): void

$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())
Expand All @@ -115,4 +124,14 @@ public function compile(Compiler $compiler): void
$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'));
}
}
}
9 changes: 6 additions & 3 deletions src/Node/Expression/MacroReferenceExpression.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,19 @@
*
* @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(')
Expand Down
9 changes: 6 additions & 3 deletions src/Node/Expression/MethodCallExpression.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,16 @@
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);
Expand All @@ -29,7 +32,7 @@ public function __construct(AbstractExpression $node, string $method, ArrayExpre

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'))
Expand Down
11 changes: 7 additions & 4 deletions src/Node/Expression/NameExpression.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,11 @@
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',
Expand All @@ -29,7 +32,7 @@ public function __construct(string $name, int $lineno)
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
Expand All @@ -38,7 +41,7 @@ public function compile(Compiler $compiler): void

$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) {
Expand Down Expand Up @@ -107,6 +110,6 @@ public function isSimple()
{
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;
}
}
44 changes: 44 additions & 0 deletions src/Node/Expression/SupportDefinedTestDeprecationTrait.php
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
24 changes: 24 additions & 0 deletions src/Node/Expression/SupportDefinedTestInterface.php
Original file line number Diff line number Diff line change
@@ -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;
}
27 changes: 27 additions & 0 deletions src/Node/Expression/SupportDefinedTestTrait.php
Original file line number Diff line number Diff line change
@@ -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;
}
}
Loading

0 comments on commit 9f275f6

Please sign in to comment.