blob: 7ac75e41ad38268ee0f35852b085b12591843792 [file] [log] [blame]
Matthias Andreas Benkard12a57352021-12-28 18:02:04 +01001<?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
12namespace Twig\NodeVisitor;
13
14use Twig\Environment;
15use Twig\Node\BlockReferenceNode;
16use Twig\Node\Expression\BlockReferenceExpression;
17use Twig\Node\Expression\ConstantExpression;
18use Twig\Node\Expression\FilterExpression;
19use Twig\Node\Expression\FunctionExpression;
20use Twig\Node\Expression\GetAttrExpression;
21use Twig\Node\Expression\NameExpression;
22use Twig\Node\Expression\ParentExpression;
23use Twig\Node\ForNode;
24use Twig\Node\IncludeNode;
25use Twig\Node\Node;
26use Twig\Node\PrintNode;
27
28/**
29 * Tries to optimize the AST.
30 *
31 * This visitor is always the last registered one.
32 *
33 * You can configure which optimizations you want to activate via the
34 * optimizer mode.
35 *
36 * @author Fabien Potencier <fabien@symfony.com>
37 *
38 * @internal
39 */
40final class OptimizerNodeVisitor implements NodeVisitorInterface
41{
42 public const OPTIMIZE_ALL = -1;
43 public const OPTIMIZE_NONE = 0;
44 public const OPTIMIZE_FOR = 2;
45 public const OPTIMIZE_RAW_FILTER = 4;
46
47 private $loops = [];
48 private $loopsTargets = [];
49 private $optimizers;
50
51 /**
52 * @param int $optimizers The optimizer mode
53 */
54 public function __construct(int $optimizers = -1)
55 {
56 if ($optimizers > (self::OPTIMIZE_FOR | self::OPTIMIZE_RAW_FILTER)) {
57 throw new \InvalidArgumentException(sprintf('Optimizer mode "%s" is not valid.', $optimizers));
58 }
59
60 $this->optimizers = $optimizers;
61 }
62
63 public function enterNode(Node $node, Environment $env): Node
64 {
65 if (self::OPTIMIZE_FOR === (self::OPTIMIZE_FOR & $this->optimizers)) {
66 $this->enterOptimizeFor($node, $env);
67 }
68
69 return $node;
70 }
71
72 public function leaveNode(Node $node, Environment $env): ?Node
73 {
74 if (self::OPTIMIZE_FOR === (self::OPTIMIZE_FOR & $this->optimizers)) {
75 $this->leaveOptimizeFor($node, $env);
76 }
77
78 if (self::OPTIMIZE_RAW_FILTER === (self::OPTIMIZE_RAW_FILTER & $this->optimizers)) {
79 $node = $this->optimizeRawFilter($node, $env);
80 }
81
82 $node = $this->optimizePrintNode($node, $env);
83
84 return $node;
85 }
86
87 /**
88 * Optimizes print nodes.
89 *
90 * It replaces:
91 *
92 * * "echo $this->render(Parent)Block()" with "$this->display(Parent)Block()"
93 */
94 private function optimizePrintNode(Node $node, Environment $env): Node
95 {
96 if (!$node instanceof PrintNode) {
97 return $node;
98 }
99
100 $exprNode = $node->getNode('expr');
101 if (
102 $exprNode instanceof BlockReferenceExpression ||
103 $exprNode instanceof ParentExpression
104 ) {
105 $exprNode->setAttribute('output', true);
106
107 return $exprNode;
108 }
109
110 return $node;
111 }
112
113 /**
114 * Removes "raw" filters.
115 */
116 private function optimizeRawFilter(Node $node, Environment $env): Node
117 {
118 if ($node instanceof FilterExpression && 'raw' == $node->getNode('filter')->getAttribute('value')) {
119 return $node->getNode('node');
120 }
121
122 return $node;
123 }
124
125 /**
126 * Optimizes "for" tag by removing the "loop" variable creation whenever possible.
127 */
128 private function enterOptimizeFor(Node $node, Environment $env): void
129 {
130 if ($node instanceof ForNode) {
131 // disable the loop variable by default
132 $node->setAttribute('with_loop', false);
133 array_unshift($this->loops, $node);
134 array_unshift($this->loopsTargets, $node->getNode('value_target')->getAttribute('name'));
135 array_unshift($this->loopsTargets, $node->getNode('key_target')->getAttribute('name'));
136 } elseif (!$this->loops) {
137 // we are outside a loop
138 return;
139 }
140
141 // when do we need to add the loop variable back?
142
143 // the loop variable is referenced for the current loop
144 elseif ($node instanceof NameExpression && 'loop' === $node->getAttribute('name')) {
145 $node->setAttribute('always_defined', true);
146 $this->addLoopToCurrent();
147 }
148
149 // optimize access to loop targets
150 elseif ($node instanceof NameExpression && \in_array($node->getAttribute('name'), $this->loopsTargets)) {
151 $node->setAttribute('always_defined', true);
152 }
153
154 // block reference
155 elseif ($node instanceof BlockReferenceNode || $node instanceof BlockReferenceExpression) {
156 $this->addLoopToCurrent();
157 }
158
159 // include without the only attribute
160 elseif ($node instanceof IncludeNode && !$node->getAttribute('only')) {
161 $this->addLoopToAll();
162 }
163
164 // include function without the with_context=false parameter
165 elseif ($node instanceof FunctionExpression
166 && 'include' === $node->getAttribute('name')
167 && (!$node->getNode('arguments')->hasNode('with_context')
168 || false !== $node->getNode('arguments')->getNode('with_context')->getAttribute('value')
169 )
170 ) {
171 $this->addLoopToAll();
172 }
173
174 // the loop variable is referenced via an attribute
175 elseif ($node instanceof GetAttrExpression
176 && (!$node->getNode('attribute') instanceof ConstantExpression
177 || 'parent' === $node->getNode('attribute')->getAttribute('value')
178 )
179 && (true === $this->loops[0]->getAttribute('with_loop')
180 || ($node->getNode('node') instanceof NameExpression
181 && 'loop' === $node->getNode('node')->getAttribute('name')
182 )
183 )
184 ) {
185 $this->addLoopToAll();
186 }
187 }
188
189 /**
190 * Optimizes "for" tag by removing the "loop" variable creation whenever possible.
191 */
192 private function leaveOptimizeFor(Node $node, Environment $env): void
193 {
194 if ($node instanceof ForNode) {
195 array_shift($this->loops);
196 array_shift($this->loopsTargets);
197 array_shift($this->loopsTargets);
198 }
199 }
200
201 private function addLoopToCurrent(): void
202 {
203 $this->loops[0]->setAttribute('with_loop', true);
204 }
205
206 private function addLoopToAll(): void
207 {
208 foreach ($this->loops as $loop) {
209 $loop->setAttribute('with_loop', true);
210 }
211 }
212
213 public function getPriority(): int
214 {
215 return 255;
216 }
217}