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\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 */
38final class OptimizerNodeVisitor extends AbstractNodeVisitor
39{
40    public const OPTIMIZE_ALL = -1;
41    public const OPTIMIZE_NONE = 0;
42    public const OPTIMIZE_FOR = 2;
43    public const OPTIMIZE_RAW_FILTER = 4;
44    // obsolete, does not do anything
45    public const OPTIMIZE_VAR_ACCESS = 8;
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 (!\is_int($optimizers) || $optimizers > (self::OPTIMIZE_FOR | self::OPTIMIZE_RAW_FILTER | self::OPTIMIZE_VAR_ACCESS)) {
57            throw new \InvalidArgumentException(sprintf('Optimizer mode "%s" is not valid.', $optimizers));
58        }
59
60        $this->optimizers = $optimizers;
61    }
62
63    protected function doEnterNode(Node $node, Environment $env)
64    {
65        if (self::OPTIMIZE_FOR === (self::OPTIMIZE_FOR & $this->optimizers)) {
66            $this->enterOptimizeFor($node, $env);
67        }
68
69        return $node;
70    }
71
72    protected function doLeaveNode(Node $node, Environment $env)
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)
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)
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()
202    {
203        $this->loops[0]->setAttribute('with_loop', true);
204    }
205
206    private function addLoopToAll()
207    {
208        foreach ($this->loops as $loop) {
209            $loop->setAttribute('with_loop', true);
210        }
211    }
212
213    public function getPriority()
214    {
215        return 255;
216    }
217}
218
219class_alias('Twig\NodeVisitor\OptimizerNodeVisitor', 'Twig_NodeVisitor_Optimizer');
220