Skip to content

Commit faf8471

Browse files
committed
feature #4335 Deprecate instantiating Node directly, introduce EmptyNode and Nodes (fabpot)
This PR was merged into the 3.x branch. Discussion ---------- Deprecate instantiating Node directly, introduce EmptyNode and Nodes Based on some comments from `@stof`: See #4292 (comment) See #4333 (comment) First interesting usage here: 65ee72a Commits ------- 8b27898 Deprecate using Node directly, introduce EmptyNode and Nodes
2 parents a1daf1e + 8b27898 commit faf8471

37 files changed

+230
-130
lines changed

CHANGELOG

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# 3.15.0 (2024-XX-XX)
22

3+
* Deprecate instantiating `Node` directly. Use `EmptyNode` or `Nodes` instead.
34
* Add support for inline comments
45
* Add support for accessing class constants with the dot operator
56
* Add `Profile::getStartTime()` and `Profile::getEndTime()`

doc/deprecated.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,3 +289,10 @@ Functions/Filters/Tests
289289

290290
* For variadic arguments, use snake-case for the argument name to ease the
291291
transition to 4.0.
292+
293+
Node
294+
----
295+
296+
* Instantiating ``Twig\Node\Node`` directly is deprecated as of Twig 3.15. Use
297+
``EmptyNode`` or ``Nodes`` instead depending on the use case. The
298+
``Twig\Node\Node`` class will be abstract in Twig 4.0.

src/ExpressionParser.php

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
use Twig\Attribute\FirstClassTwigCallableReady;
1616
use Twig\Error\SyntaxError;
17+
use Twig\Node\EmptyNode;
1718
use Twig\Node\Expression\AbstractExpression;
1819
use Twig\Node\Expression\ArrayExpression;
1920
use Twig\Node\Expression\ArrowFunctionExpression;
@@ -32,6 +33,7 @@
3233
use Twig\Node\Expression\Unary\PosUnary;
3334
use Twig\Node\Expression\Unary\SpreadUnary;
3435
use Twig\Node\Node;
36+
use Twig\Node\Nodes;
3537

3638
/**
3739
* Parses expressions.
@@ -110,7 +112,7 @@ private function parseArrow()
110112
$names = [new AssignNameExpression($token->getValue(), $token->getLine())];
111113
$stream->expect(Token::ARROW_TYPE);
112114

113-
return new ArrowFunctionExpression($this->parseExpression(0), new Node($names), $line);
115+
return new ArrowFunctionExpression($this->parseExpression(0), new Nodes($names), $line);
114116
}
115117

116118
// first, determine if we are parsing an arrow function by finding => (long form)
@@ -151,7 +153,7 @@ private function parseArrow()
151153
$stream->expect(Token::PUNCTUATION_TYPE, ')');
152154
$stream->expect(Token::ARROW_TYPE);
153155

154-
return new ArrowFunctionExpression($this->parseExpression(0), new Node($names), $line);
156+
return new ArrowFunctionExpression($this->parseExpression(0), new Nodes($names), $line);
155157
}
156158

157159
private function getPrimary(): AbstractExpression
@@ -467,7 +469,7 @@ public function getFunctionNode($name, $line)
467469
$function = $this->getFunction($name, $line);
468470

469471
if ($function->getParserCallable()) {
470-
$fakeNode = new Node(lineno: $line);
472+
$fakeNode = new EmptyNode($line);
471473
$fakeNode->setSourceContext($this->parser->getStream()->getSourceContext());
472474

473475
return ($function->getParserCallable())($this->parser, $fakeNode, $args, $line);
@@ -556,7 +558,7 @@ public function parseSubscriptExpression($node)
556558
}
557559

558560
$filter = $this->getFilter('slice', $token->getLine());
559-
$arguments = new Node([$arg, $length]);
561+
$arguments = new Nodes([$arg, $length]);
560562
$filter = new ($filter->getNodeClass())($node, $filter, $arguments, $token->getLine());
561563

562564
$stream->expect(Token::PUNCTUATION_TYPE, ']');
@@ -587,7 +589,7 @@ public function parseFilterExpressionRaw($node)
587589
$token = $this->parser->getStream()->expect(Token::NAME_TYPE);
588590

589591
if (!$this->parser->getStream()->test(Token::PUNCTUATION_TYPE, '(')) {
590-
$arguments = new Node();
592+
$arguments = new EmptyNode();
591593
} else {
592594
$arguments = $this->parseArguments(true, false, true);
593595
}
@@ -691,7 +693,7 @@ public function parseArguments($namedArguments = false, $definition = false, $al
691693
}
692694
$stream->expect(Token::PUNCTUATION_TYPE, ')', 'A list of arguments must be closed by a parenthesis');
693695

694-
return new Node($args);
696+
return new Nodes($args);
695697
}
696698

697699
public function parseAssignmentExpression()
@@ -717,7 +719,7 @@ public function parseAssignmentExpression()
717719
}
718720
}
719721

720-
return new Node($targets);
722+
return new Nodes($targets);
721723
}
722724

723725
public function parseMultitargetExpression()
@@ -730,7 +732,7 @@ public function parseMultitargetExpression()
730732
}
731733
}
732734

733-
return new Node($targets);
735+
return new Nodes($targets);
734736
}
735737

736738
private function parseNotTestExpression(Node $node): NotUnary
@@ -747,7 +749,7 @@ private function parseTestExpression(Node $node): TestExpression
747749
if ($stream->test(Token::PUNCTUATION_TYPE, '(')) {
748750
$arguments = $this->parseArguments(true);
749751
} elseif ($test->hasOneMandatoryArgument()) {
750-
$arguments = new Node([0 => $this->parsePrimaryExpression()]);
752+
$arguments = new Nodes([0 => $this->parsePrimaryExpression()]);
751753
}
752754

753755
if ('defined' === $test->getName() && $node instanceof NameExpression && null !== $alias = $this->parser->getImportedSymbol('function', $node->getAttribute('name'))) {

src/Node/EmptyNode.php

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
/*
4+
* This file is part of Twig.
5+
*
6+
* (c) Fabien Potencier
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Twig\Node;
13+
14+
use Twig\Attribute\YieldReady;
15+
16+
/**
17+
* Represents an empty node.
18+
*
19+
* @author Fabien Potencier <[email protected]>
20+
*/
21+
#[YieldReady]
22+
final class EmptyNode extends Node
23+
{
24+
public function __construct(int $lineno = 0)
25+
{
26+
parent::__construct([], [], $lineno);
27+
}
28+
}

src/Node/Expression/Filter/DefaultFilter.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use Twig\Attribute\FirstClassTwigCallableReady;
1515
use Twig\Compiler;
1616
use Twig\Extension\CoreExtension;
17+
use Twig\Node\EmptyNode;
1718
use Twig\Node\Expression\ConditionalExpression;
1819
use Twig\Node\Expression\ConstantExpression;
1920
use Twig\Node\Expression\FilterExpression;
@@ -45,7 +46,7 @@ public function __construct(Node $node, TwigFilter|ConstantExpression $filter, N
4546
}
4647

4748
if ('default' === $name && ($node instanceof NameExpression || $node instanceof GetAttrExpression)) {
48-
$test = new DefinedTest(clone $node, new TwigTest('defined'), new Node(), $node->getTemplateLine());
49+
$test = new DefinedTest(clone $node, new TwigTest('defined'), new EmptyNode(), $node->getTemplateLine());
4950
$false = \count($arguments) ? $arguments->getNode('0') : new ConstantExpression('', $node->getTemplateLine());
5051

5152
$node = new ConditionalExpression($test, $default, $false, $node->getTemplateLine());

src/Node/Expression/Filter/RawFilter.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
use Twig\Attribute\FirstClassTwigCallableReady;
1515
use Twig\Compiler;
16+
use Twig\Node\EmptyNode;
1617
use Twig\Node\Expression\ConstantExpression;
1718
use Twig\Node\Expression\FilterExpression;
1819
use Twig\Node\Node;
@@ -26,7 +27,7 @@ class RawFilter extends FilterExpression
2627
#[FirstClassTwigCallableReady]
2728
public function __construct(Node $node, TwigFilter|ConstantExpression|null $filter = null, ?Node $arguments = null, int $lineno = 0)
2829
{
29-
parent::__construct($node, $filter ?: new TwigFilter('raw', null, ['is_safe' => ['all']]), $arguments ?: new Node(), $lineno ?: $node->getTemplateLine());
30+
parent::__construct($node, $filter ?: new TwigFilter('raw', null, ['is_safe' => ['all']]), $arguments ?: new EmptyNode(), $lineno ?: $node->getTemplateLine());
3031
}
3132

3233
public function compile(Compiler $compiler): void

src/Node/Expression/NullCoalesceExpression.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
namespace Twig\Node\Expression;
1313

1414
use Twig\Compiler;
15+
use Twig\Node\EmptyNode;
1516
use Twig\Node\Expression\Binary\AndBinary;
1617
use Twig\Node\Expression\Test\DefinedTest;
1718
use Twig\Node\Expression\Test\NullTest;
@@ -23,12 +24,12 @@ class NullCoalesceExpression extends ConditionalExpression
2324
{
2425
public function __construct(Node $left, Node $right, int $lineno)
2526
{
26-
$test = new DefinedTest(clone $left, new TwigTest('defined'), new Node(), $left->getTemplateLine());
27+
$test = new DefinedTest(clone $left, new TwigTest('defined'), new EmptyNode(), $left->getTemplateLine());
2728
// for "block()", we don't need the null test as the return value is always a string
2829
if (!$left instanceof BlockReferenceExpression) {
2930
$test = new AndBinary(
3031
$test,
31-
new NotUnary(new NullTest($left, new TwigTest('null'), new Node(), $left->getTemplateLine()), $left->getTemplateLine()),
32+
new NotUnary(new NullTest($left, new TwigTest('null'), new EmptyNode(), $left->getTemplateLine()), $left->getTemplateLine()),
3233
$left->getTemplateLine()
3334
);
3435
}

src/Node/ForNode.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ class ForNode extends Node
2929

3030
public function __construct(AssignNameExpression $keyTarget, AssignNameExpression $valueTarget, AbstractExpression $seq, ?Node $ifexpr, Node $body, ?Node $else, int $lineno)
3131
{
32-
$body = new Node([$body, $this->loop = new ForLoopNode($lineno)]);
32+
$body = new Nodes([$body, $this->loop = new ForLoopNode($lineno)]);
3333

3434
$nodes = ['key_target' => $keyTarget, 'value_target' => $valueTarget, 'seq' => $seq, 'body' => $body];
3535
if (null !== $else) {

src/Node/ModuleNode.php

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,11 @@ public function __construct(Node $body, ?AbstractExpression $parent, Node $block
4444
'blocks' => $blocks,
4545
'macros' => $macros,
4646
'traits' => $traits,
47-
'display_start' => new Node(),
48-
'display_end' => new Node(),
49-
'constructor_start' => new Node(),
50-
'constructor_end' => new Node(),
51-
'class_end' => new Node(),
47+
'display_start' => new EmptyNode(),
48+
'display_end' => new EmptyNode(),
49+
'constructor_start' => new EmptyNode(),
50+
'constructor_end' => new EmptyNode(),
51+
'class_end' => new EmptyNode(),
5252
];
5353
if (null !== $parent) {
5454
$nodes['parent'] = $parent;
@@ -414,7 +414,7 @@ protected function compileIsTraitable(Compiler $compiler)
414414
}
415415

416416
if (!\count($nodes)) {
417-
$nodes = new Node([$nodes]);
417+
$nodes = new Nodes([$nodes]);
418418
}
419419

420420
foreach ($nodes as $node) {

src/Node/Node.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,10 @@ class Node implements \Countable, \IteratorAggregate
4747
*/
4848
public function __construct(array $nodes = [], array $attributes = [], int $lineno = 0)
4949
{
50+
if (self::class === static::class) {
51+
trigger_deprecation('twig/twig', '3.15', \sprintf('Instantiating "%s" directly is deprecated; the class will become abstract in 4.0.', self::class));
52+
}
53+
5054
foreach ($nodes as $name => $node) {
5155
if (!$node instanceof self) {
5256
throw new \InvalidArgumentException(\sprintf('Using "%s" for the value of node "%s" of "%s" is not supported. You must pass a \Twig\Node\Node instance.', \is_object($node) ? $node::class : (null === $node ? 'null' : \gettype($node)), $name, static::class));

0 commit comments

Comments
 (0)