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