added an Optimizer extension
authorFabien Potencier <fabien.potencier@gmail.com>
Sun, 28 Nov 2010 14:30:05 +0000 (15:30 +0100)
committerFabien Potencier <fabien.potencier@gmail.com>
Sun, 28 Nov 2010 14:30:05 +0000 (15:30 +0100)
CHANGELOG
lib/Twig/Extension/Optimizer.php [new file with mode: 0644]
lib/Twig/NodeVisitor/Optimizer.php [new file with mode: 0644]
test/Twig/Tests/NodeVisitor/OptimizerTest.php [new file with mode: 0644]

index f19d607..492e832 100644 (file)
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,5 +1,6 @@
 * 0.9.10
 
+ * added an Optimizer extension
  * added priority to node visitors
 
 * 0.9.9 (2010-11-28)
diff --git a/lib/Twig/Extension/Optimizer.php b/lib/Twig/Extension/Optimizer.php
new file mode 100644 (file)
index 0000000..013fcb6
--- /dev/null
@@ -0,0 +1,35 @@
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) 2010 Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+class Twig_Extension_Optimizer extends Twig_Extension
+{
+    protected $optimizers;
+
+    public function __construct($optimizers = -1)
+    {
+        $this->optimizers = $optimizers;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getNodeVisitors()
+    {
+        return array(new Twig_NodeVisitor_Optimizer($this->optimizers));
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getName()
+    {
+        return 'optimizer';
+    }
+}
diff --git a/lib/Twig/NodeVisitor/Optimizer.php b/lib/Twig/NodeVisitor/Optimizer.php
new file mode 100644 (file)
index 0000000..e94d3c9
--- /dev/null
@@ -0,0 +1,114 @@
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) 2010 Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Twig_NodeVisitor_Optimizer tries to optimizes the AST.
+ *
+ * This visitor is always the last registered one.
+ *
+ * You can configure which optimizations you want to activate via the
+ * optimizer mode.
+ *
+ * @package twig
+ * @author  Fabien Potencier <fabien.potencier@symfony-project.com>
+ */
+class Twig_NodeVisitor_Optimizer implements Twig_NodeVisitorInterface
+{
+    const OPTIMIZE_ALL  = -1;
+    const OPTIMIZE_NONE = 0;
+    const OPTIMIZE_FOR  = 2;
+
+    protected $loops = array();
+    protected $optimizers;
+
+    /**
+     * Constructor.
+     *
+     * @param integer $optimizers The optimizer mode
+     */
+    public function __construct($optimizers = -1)
+    {
+        if (null === $optimizers) {
+            $mode = self::OPTIMIZE_ALL;
+        } elseif (!is_int($optimizers) || $optimizers > 2) {
+            throw new \InvalidArgumentException(sprintf('Optimizer mode "%s" is not valid.', $optimizers));
+        }
+
+        $this->optimizers = $optimizers;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function enterNode(Twig_NodeInterface $node, Twig_Environment $env)
+    {
+        if (self::OPTIMIZE_FOR === (self::OPTIMIZE_FOR & $this->optimizers)) {
+            $this->enterOptimizeFor($node, $env);
+        }
+
+        return $node;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function leaveNode(Twig_NodeInterface $node, Twig_Environment $env)
+    {
+        if (self::OPTIMIZE_FOR === (self::OPTIMIZE_FOR & $this->optimizers)) {
+            $this->leaveOptimizeFor($node, $env);
+        }
+
+        return $node;
+    }
+
+    /**
+     * Optimizes "for" tag.
+     *
+     * This method removes the creation of the "loop" variable when:
+     *
+     *  * "loop" is not used in the "for" tag
+     *  * and there is no include tag without the "only" attribute
+     *  * and there is inner-for tag (in which case we would need to check parent.loop usage)
+     *
+     * This method should be able to optimize for with inner-for tags.
+     */
+    protected function enterOptimizeFor($node, $env)
+    {
+        if ($node instanceof Twig_Node_For) {
+            $node->setAttribute('with_loop', false);
+
+            if ($this->loops) {
+                $this->loops[0]->setAttribute('with_loop', true);
+            }
+
+            array_unshift($this->loops, $node);
+        } elseif ($this->loops && $node instanceof Twig_Node_Expression_Name && 'loop' === $node->getAttribute('name')) {
+            $this->loops[0]->setAttribute('with_loop', true);
+        } elseif ($this->loops && $node instanceof Twig_Node_Include && !$node->getAttribute('only')) {
+            $this->loops[0]->setAttribute('with_loop', true);
+        }
+    }
+
+    protected function leaveOptimizeFor($node, $env)
+    {
+        if ($node instanceof Twig_Node_For) {
+            array_shift($this->loops);
+        }
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getPriority()
+    {
+        return 255;
+    }
+}
diff --git a/test/Twig/Tests/NodeVisitor/OptimizerTest.php b/test/Twig/Tests/NodeVisitor/OptimizerTest.php
new file mode 100644 (file)
index 0000000..b3a9a6f
--- /dev/null
@@ -0,0 +1,59 @@
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+class Twig_Tests_NodeVisitor_OptimizerTest extends PHPUnit_Framework_TestCase
+{
+    /**
+     * @dataProvider getTestsForForOptimizer
+     */
+    public function testForOptimizer($template, $expected)
+    {
+        $env = new Twig_Environment(new Twig_Loader_String(), array('cache' => false));
+        $env->addExtension(new Twig_Extension_Optimizer());
+
+        $stream = $env->parse($env->tokenize($template, 'index'));
+
+        foreach ($expected as $target => $withLoop) {
+            $this->assertTrue($this->checkForConfiguration($stream, $target, $withLoop));
+        }
+    }
+
+    public function getTestsForForOptimizer()
+    {
+        return array(
+            array('{% for i in foo %}{% endfor %}', array('i' => false)),
+            array('{% for i in foo %}{{ loop.index }}{% endfor %}', array('i' => true)),
+            array('{% for i in foo %}{% for j in foo %}{% endfor %}{% endfor %}', array('i' => true, 'j' => false)),
+            array('{% for i in foo %}{% include "foo" %}{% endfor %}', array('i' => true)),
+            array('{% for i in foo %}{% include "foo" only %}{% endfor %}', array('i' => false)),
+            array('{% for i in foo %}{% for j in foo %}{{ loop.index }}{% endfor %}{% endfor %}', array('i' => true, 'j' => true)),
+        );
+    }
+
+    public function checkForConfiguration(Twig_NodeInterface $node = null, $target, $withLoop)
+    {
+        if (null === $node) {
+            return;
+        }
+
+        foreach ($node as $n) {
+            if ($n instanceof Twig_Node_For) {
+                if ($target === $n->getNode('value_target')->getAttribute('name')) {
+                    return $withLoop == $n->getAttribute('with_loop');
+                }
+            }
+
+            $ret = $this->checkForConfiguration($n, $target, $withLoop);
+            if (null !== $ret) {
+                return $ret;
+            }
+        }
+    }
+}