From 6b9a5c6d4ecc00e1f1d70c1f35bc0d4ebf5c0ee9 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Wed, 1 Jan 2025 14:38:42 +0100 Subject: [PATCH] Add VariableOptimizerNodeVisitor --- .../VariableOptimizerNodeVisitor.php | 56 +++++++++++++++++++ .../VariableOptimizerNodeVisitorTest.php | 51 +++++++++++++++++ 2 files changed, 107 insertions(+) create mode 100644 src/NodeVisitor/VariableOptimizerNodeVisitor.php create mode 100644 tests/NodeVisitor/VariableOptimizerNodeVisitorTest.php diff --git a/src/NodeVisitor/VariableOptimizerNodeVisitor.php b/src/NodeVisitor/VariableOptimizerNodeVisitor.php new file mode 100644 index 00000000000..af243137d7b --- /dev/null +++ b/src/NodeVisitor/VariableOptimizerNodeVisitor.php @@ -0,0 +1,56 @@ + + * + * @internal + */ +final class VariableOptimizerNodeVisitor implements NodeVisitorInterface +{ + private array $types = []; + + public function enterNode(Node $node, Environment $env): Node + { + if ($node instanceof TypesNode) { + $this->types = array_merge($this->types, $node->getAttribute('mapping')); + } + + // A NameExpression is always defined if the variable is typed and not optional + if ($node instanceof NameExpression && isset($this->types[$node->getAttribute('name')]) && !$this->types[$node->getAttribute('name')]['optional']) { + $node->setAttribute('always_defined', true); + } + + return $node; + } + + public function leaveNode(Node $node, Environment $env): ?Node + { + if ($node instanceof ModuleNode) { + $this->types = []; + } + + return $node; + } + + public function getPriority(): int + { + return 255; + } +} diff --git a/tests/NodeVisitor/VariableOptimizerNodeVisitorTest.php b/tests/NodeVisitor/VariableOptimizerNodeVisitorTest.php new file mode 100644 index 00000000000..e640a6e6b26 --- /dev/null +++ b/tests/NodeVisitor/VariableOptimizerNodeVisitorTest.php @@ -0,0 +1,51 @@ + false]); + $node = $env->parse($env->tokenize(new Source($template, 'index'))); + $traverser = new NodeTraverser($env, [new VariableOptimizerNodeVisitor()]); + $node = $traverser->traverse($node); + + $this->assertSame($expected, $this->stripCode($env->compile($node->getNode('body')))); + } + + public function getTypedVariablesData() + { + yield 'regular' => ['{{ foo }}', 'yield ($context["foo"] ?? null);']; + yield 'typed_optional' => ['{% types { foo?: "string" } %}{{ foo }}', 'yield ($context["foo"] ?? null);']; + yield 'typed' => ['{% types { foo: "string" } %}{{ foo }}', 'yield $context["foo"];']; + + yield 'is_defined_test' => ['{{ foo is defined }}', 'yield array_key_exists("foo", $context);']; + yield 'is_defined_test_typed_optional' => ['{% types { foo?: "string" } %}{{ foo is defined }}', 'yield array_key_exists("foo", $context);']; + yield 'is_defined_test_typed' => ['{% types { foo: "string" } %}{{ foo is defined }}', 'yield true;']; + } + + private function stripCode(string $code): string + { + return trim(preg_replace("{^//.*\n}", '', $code)); + } +}