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 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\FunctionExpression; 19use Twig\Node\Expression\GetAttrExpression; 20use Twig\Node\Expression\ParentExpression; 21use Twig\Node\Expression\Variable\ContextVariable; 22use Twig\Node\ForNode; 23use Twig\Node\IncludeNode; 24use Twig\Node\Node; 25use Twig\Node\PrintNode; 26use Twig\Node\TextNode; 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 public const OPTIMIZE_TEXT_NODES = 8; 47 48 private $loops = []; 49 private $loopsTargets = []; 50 51 /** 52 * @param int $optimizers The optimizer mode 53 */ 54 public function __construct( 55 private int $optimizers = -1, 56 ) { 57 if ($optimizers > (self::OPTIMIZE_FOR | self::OPTIMIZE_RAW_FILTER | self::OPTIMIZE_TEXT_NODES)) { 58 throw new \InvalidArgumentException(\sprintf('Optimizer mode "%s" is not valid.', $optimizers)); 59 } 60 61 if (-1 !== $optimizers && self::OPTIMIZE_RAW_FILTER === (self::OPTIMIZE_RAW_FILTER & $optimizers)) { 62 trigger_deprecation('twig/twig', '3.11', 'The "Twig\NodeVisitor\OptimizerNodeVisitor::OPTIMIZE_RAW_FILTER" option is deprecated and does nothing.'); 63 } 64 65 if (-1 !== $optimizers && self::OPTIMIZE_TEXT_NODES === (self::OPTIMIZE_TEXT_NODES & $optimizers)) { 66 trigger_deprecation('twig/twig', '3.12', 'The "Twig\NodeVisitor\OptimizerNodeVisitor::OPTIMIZE_TEXT_NODES" option is deprecated and does nothing.'); 67 } 68 } 69 70 public function enterNode(Node $node, Environment $env): Node 71 { 72 if (self::OPTIMIZE_FOR === (self::OPTIMIZE_FOR & $this->optimizers)) { 73 $this->enterOptimizeFor($node); 74 } 75 76 return $node; 77 } 78 79 public function leaveNode(Node $node, Environment $env): ?Node 80 { 81 if (self::OPTIMIZE_FOR === (self::OPTIMIZE_FOR & $this->optimizers)) { 82 $this->leaveOptimizeFor($node); 83 } 84 85 $node = $this->optimizePrintNode($node); 86 87 return $node; 88 } 89 90 /** 91 * Optimizes print nodes. 92 * 93 * It replaces: 94 * 95 * * "echo $this->render(Parent)Block()" with "$this->display(Parent)Block()" 96 */ 97 private function optimizePrintNode(Node $node): Node 98 { 99 if (!$node instanceof PrintNode) { 100 return $node; 101 } 102 103 $exprNode = $node->getNode('expr'); 104 105 if ($exprNode instanceof ConstantExpression && \is_string($exprNode->getAttribute('value'))) { 106 return new TextNode($exprNode->getAttribute('value'), $exprNode->getTemplateLine()); 107 } 108 109 if ( 110 $exprNode instanceof BlockReferenceExpression 111 || $exprNode instanceof ParentExpression 112 ) { 113 $exprNode->setAttribute('output', true); 114 115 return $exprNode; 116 } 117 118 return $node; 119 } 120 121 /** 122 * Optimizes "for" tag by removing the "loop" variable creation whenever possible. 123 */ 124 private function enterOptimizeFor(Node $node): void 125 { 126 if ($node instanceof ForNode) { 127 // disable the loop variable by default 128 $node->setAttribute('with_loop', false); 129 array_unshift($this->loops, $node); 130 array_unshift($this->loopsTargets, $node->getNode('value_target')->getAttribute('name')); 131 array_unshift($this->loopsTargets, $node->getNode('key_target')->getAttribute('name')); 132 } elseif (!$this->loops) { 133 // we are outside a loop 134 return; 135 } 136 137 // when do we need to add the loop variable back? 138 139 // the loop variable is referenced for the current loop 140 elseif ($node instanceof ContextVariable && 'loop' === $node->getAttribute('name')) { 141 $node->setAttribute('always_defined', true); 142 $this->addLoopToCurrent(); 143 } 144 145 // optimize access to loop targets 146 elseif ($node instanceof ContextVariable && \in_array($node->getAttribute('name'), $this->loopsTargets, true)) { 147 $node->setAttribute('always_defined', true); 148 } 149 150 // block reference 151 elseif ($node instanceof BlockReferenceNode || $node instanceof BlockReferenceExpression) { 152 $this->addLoopToCurrent(); 153 } 154 155 // include without the only attribute 156 elseif ($node instanceof IncludeNode && !$node->getAttribute('only')) { 157 $this->addLoopToAll(); 158 } 159 160 // include function without the with_context=false parameter 161 elseif ($node instanceof FunctionExpression 162 && 'include' === $node->getAttribute('name') 163 && (!$node->getNode('arguments')->hasNode('with_context') 164 || false !== $node->getNode('arguments')->getNode('with_context')->getAttribute('value') 165 ) 166 ) { 167 $this->addLoopToAll(); 168 } 169 170 // the loop variable is referenced via an attribute 171 elseif ($node instanceof GetAttrExpression 172 && (!$node->getNode('attribute') instanceof ConstantExpression 173 || 'parent' === $node->getNode('attribute')->getAttribute('value') 174 ) 175 && (true === $this->loops[0]->getAttribute('with_loop') 176 || ($node->getNode('node') instanceof ContextVariable 177 && 'loop' === $node->getNode('node')->getAttribute('name') 178 ) 179 ) 180 ) { 181 $this->addLoopToAll(); 182 } 183 } 184 185 /** 186 * Optimizes "for" tag by removing the "loop" variable creation whenever possible. 187 */ 188 private function leaveOptimizeFor(Node $node): void 189 { 190 if ($node instanceof ForNode) { 191 array_shift($this->loops); 192 array_shift($this->loopsTargets); 193 array_shift($this->loopsTargets); 194 } 195 } 196 197 private function addLoopToCurrent(): void 198 { 199 $this->loops[0]->setAttribute('with_loop', true); 200 } 201 202 private function addLoopToAll(): void 203 { 204 foreach ($this->loops as $loop) { 205 $loop->setAttribute('with_loop', true); 206 } 207 } 208 209 public function getPriority(): int 210 { 211 return 255; 212 } 213} 214