tests/ExpressionParserTest.php
4505200d
 <?php
 
2119e60c
 namespace Twig\Tests;
 
4505200d
 /*
  * 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.
  */
 
34bdab4d
 use PHPUnit\Framework\TestCase;
90d579e4
 use Twig\Environment;
5ebcecf2
 use Twig\Error\SyntaxError;
90d579e4
 use Twig\Loader\LoaderInterface;
 use Twig\Node\Expression\ArrayExpression;
 use Twig\Node\Expression\Binary\ConcatBinary;
 use Twig\Node\Expression\ConstantExpression;
 use Twig\Node\Expression\NameExpression;
 use Twig\Parser;
 use Twig\Source;
 
34bdab4d
 class ExpressionParserTest extends TestCase
4505200d
 {
     /**
14675864
      * @dataProvider getFailingTestsForAssignment
      */
     public function testCanOnlyAssignToNames($template)
     {
5ebcecf2
         $this->expectException(SyntaxError::class);
b776e41f
 
656c295e
         $env = new Environment($this->createMock(LoaderInterface::class), ['cache' => false, 'autoescape' => false]);
90d579e4
         $parser = new Parser($env);
         $parser->parse($env->tokenize(new Source($template, 'index')));
14675864
     }
 
     public function getFailingTestsForAssignment()
     {
5c55243d
         return [
             ['{% set false = "foo" %}'],
             ['{% set FALSE = "foo" %}'],
             ['{% set true = "foo" %}'],
             ['{% set TRUE = "foo" %}'],
             ['{% set none = "foo" %}'],
             ['{% set NONE = "foo" %}'],
             ['{% set null = "foo" %}'],
             ['{% set NULL = "foo" %}'],
             ['{% set 3 = "foo" %}'],
             ['{% set 1 + 2 = "foo" %}'],
             ['{% set "bar" = "foo" %}'],
             ['{% set %}{% endset %}'],
         ];
14675864
     }
 
     /**
4505200d
      * @dataProvider getTestsForArray
      */
     public function testArrayExpression($template, $expected)
     {
656c295e
         $env = new Environment($this->createMock(LoaderInterface::class), ['cache' => false, 'autoescape' => false]);
d4db9153
         $stream = $env->tokenize($source = new Source($template, ''));
90d579e4
         $parser = new Parser($env);
d4db9153
         $expected->setSourceContext($source);
4505200d
 
a6842511
         $this->assertEquals($expected, $parser->parse($stream)->getNode('body')->getNode(0)->getNode('expr'));
4505200d
     }
 
     /**
      * @dataProvider getFailingTestsForArray
      */
     public function testArraySyntaxError($template)
     {
5ebcecf2
         $this->expectException(SyntaxError::class);
b776e41f
 
656c295e
         $env = new Environment($this->createMock(LoaderInterface::class), ['cache' => false, 'autoescape' => false]);
90d579e4
         $parser = new Parser($env);
         $parser->parse($env->tokenize(new Source($template, 'index')));
4505200d
     }
 
     public function getFailingTestsForArray()
     {
5c55243d
         return [
             ['{{ [1, "a": "b"] }}'],
             ['{{ {"a": "b", 2} }}'],
         ];
4505200d
     }
 
     public function getTestsForArray()
     {
5c55243d
         return [
4505200d
             // simple array
90d579e4
             ['{{ [1, 2] }}', new ArrayExpression([
                   new ConstantExpression(0, 1),
                   new ConstantExpression(1, 1),
591f982b
 
90d579e4
                   new ConstantExpression(1, 1),
                   new ConstantExpression(2, 1),
5c55243d
                 ], 1),
             ],
4505200d
 
             // array with trailing ,
90d579e4
             ['{{ [1, 2, ] }}', new ArrayExpression([
                   new ConstantExpression(0, 1),
                   new ConstantExpression(1, 1),
591f982b
 
90d579e4
                   new ConstantExpression(1, 1),
                   new ConstantExpression(2, 1),
5c55243d
                 ], 1),
             ],
4505200d
 
             // simple hash
90d579e4
             ['{{ {"a": "b", "b": "c"} }}', new ArrayExpression([
                   new ConstantExpression('a', 1),
                   new ConstantExpression('b', 1),
591f982b
 
90d579e4
                   new ConstantExpression('b', 1),
                   new ConstantExpression('c', 1),
5c55243d
                 ], 1),
             ],
4505200d
 
             // hash with trailing ,
90d579e4
             ['{{ {"a": "b", "b": "c", } }}', new ArrayExpression([
                   new ConstantExpression('a', 1),
                   new ConstantExpression('b', 1),
591f982b
 
90d579e4
                   new ConstantExpression('b', 1),
                   new ConstantExpression('c', 1),
5c55243d
                 ], 1),
             ],
4505200d
 
             // hash in an array
90d579e4
             ['{{ [1, {"a": "b", "b": "c"}] }}', new ArrayExpression([
                   new ConstantExpression(0, 1),
                   new ConstantExpression(1, 1),
591f982b
 
90d579e4
                   new ConstantExpression(1, 1),
                   new ArrayExpression([
                         new ConstantExpression('a', 1),
                         new ConstantExpression('b', 1),
591f982b
 
90d579e4
                         new ConstantExpression('b', 1),
                         new ConstantExpression('c', 1),
5c55243d
                       ], 1),
                 ], 1),
             ],
4505200d
 
             // array in a hash
90d579e4
             ['{{ {"a": [1, 2], "b": "c"} }}', new ArrayExpression([
                   new ConstantExpression('a', 1),
                   new ArrayExpression([
                         new ConstantExpression(0, 1),
                         new ConstantExpression(1, 1),
 
                         new ConstantExpression(1, 1),
                         new ConstantExpression(2, 1),
5c55243d
                       ], 1),
90d579e4
                   new ConstantExpression('b', 1),
                   new ConstantExpression('c', 1),
5c55243d
                 ], 1),
             ],
         ];
4505200d
     }
773fff59
 
ee39215c
     public function testStringExpressionDoesNotConcatenateTwoConsecutiveStrings()
     {
5ebcecf2
         $this->expectException(SyntaxError::class);
b776e41f
 
656c295e
         $env = new Environment($this->createMock(LoaderInterface::class), ['cache' => false, 'autoescape' => false, 'optimizations' => 0]);
90d579e4
         $stream = $env->tokenize(new Source('{{ "a" "b" }}', 'index'));
         $parser = new Parser($env);
ee39215c
 
         $parser->parse($stream);
     }
 
     /**
773fff59
      * @dataProvider getTestsForString
      */
     public function testStringExpression($template, $expected)
     {
656c295e
         $env = new Environment($this->createMock(LoaderInterface::class), ['cache' => false, 'autoescape' => false, 'optimizations' => 0]);
d4db9153
         $stream = $env->tokenize($source = new Source($template, ''));
90d579e4
         $parser = new Parser($env);
d4db9153
         $expected->setSourceContext($source);
773fff59
 
         $this->assertEquals($expected, $parser->parse($stream)->getNode('body')->getNode(0)->getNode('expr'));
     }
 
     public function getTestsForString()
     {
5c55243d
         return [
             [
90d579e4
                 '{{ "foo" }}', new ConstantExpression('foo', 1),
5c55243d
             ],
             [
90d579e4
                 '{{ "foo #{bar}" }}', new ConcatBinary(
                     new ConstantExpression('foo ', 1),
                     new NameExpression('bar', 1),
773fff59
                     1
                 ),
5c55243d
             ],
             [
90d579e4
                 '{{ "foo #{bar} baz" }}', new ConcatBinary(
                     new ConcatBinary(
                         new ConstantExpression('foo ', 1),
                         new NameExpression('bar', 1),
773fff59
                         1
                     ),
90d579e4
                     new ConstantExpression(' baz', 1),
773fff59
                     1
89df3c20
                 ),
5c55243d
             ],
d41d10ca
 
5c55243d
             [
90d579e4
                 '{{ "foo #{"foo #{bar} baz"} baz" }}', new ConcatBinary(
                     new ConcatBinary(
                         new ConstantExpression('foo ', 1),
                         new ConcatBinary(
                             new ConcatBinary(
                                 new ConstantExpression('foo ', 1),
                                 new NameExpression('bar', 1),
773fff59
                                 1
                             ),
90d579e4
                             new ConstantExpression(' baz', 1),
773fff59
                             1
                         ),
                         1
                     ),
90d579e4
                     new ConstantExpression(' baz', 1),
773fff59
                     1
                 ),
5c55243d
             ],
         ];
773fff59
     }
e1ad2bde
 
a59dcde3
     public function testAttributeCallDoesNotSupportNamedArguments()
     {
5ebcecf2
         $this->expectException(SyntaxError::class);
b776e41f
 
656c295e
         $env = new Environment($this->createMock(LoaderInterface::class), ['cache' => false, 'autoescape' => false]);
90d579e4
         $parser = new Parser($env);
a59dcde3
 
90d579e4
         $parser->parse($env->tokenize(new Source('{{ foo.bar(name="Foo") }}', 'index')));
a59dcde3
     }
 
74586e9f
     public function testMacroCallDoesNotSupportNamedArguments()
     {
5ebcecf2
         $this->expectException(SyntaxError::class);
b776e41f
 
656c295e
         $env = new Environment($this->createMock(LoaderInterface::class), ['cache' => false, 'autoescape' => false]);
90d579e4
         $parser = new Parser($env);
74586e9f
 
90d579e4
         $parser->parse($env->tokenize(new Source('{% from _self import foo %}{% macro foo() %}{% endmacro %}{{ foo(name="Foo") }}', 'index')));
74586e9f
     }
 
4647913e
     public function testMacroDefinitionDoesNotSupportNonNameVariableName()
     {
5ebcecf2
         $this->expectException(SyntaxError::class);
b776e41f
         $this->expectExceptionMessage('An argument must be a name. Unexpected token "string" of value "a" ("name" expected) in "index" at line 1.');
 
656c295e
         $env = new Environment($this->createMock(LoaderInterface::class), ['cache' => false, 'autoescape' => false]);
90d579e4
         $parser = new Parser($env);
4647913e
 
90d579e4
         $parser->parse($env->tokenize(new Source('{% macro foo("a") %}{% endmacro %}', 'index')));
4647913e
     }
 
     /**
      * @dataProvider             getMacroDefinitionDoesNotSupportNonConstantDefaultValues
      */
     public function testMacroDefinitionDoesNotSupportNonConstantDefaultValues($template)
     {
5ebcecf2
         $this->expectException(SyntaxError::class);
b776e41f
         $this->expectExceptionMessage('A default value for an argument must be a constant (a boolean, a string, a number, or an array) in "index" at line 1');
 
656c295e
         $env = new Environment($this->createMock(LoaderInterface::class), ['cache' => false, 'autoescape' => false]);
90d579e4
         $parser = new Parser($env);
4647913e
 
90d579e4
         $parser->parse($env->tokenize(new Source($template, 'index')));
4647913e
     }
 
     public function getMacroDefinitionDoesNotSupportNonConstantDefaultValues()
     {
5c55243d
         return [
             ['{% macro foo(name = "a #{foo} a") %}{% endmacro %}'],
             ['{% macro foo(name = [["b", "a #{foo} a"]]) %}{% endmacro %}'],
         ];
4647913e
     }
 
     /**
      * @dataProvider getMacroDefinitionSupportsConstantDefaultValues
      */
     public function testMacroDefinitionSupportsConstantDefaultValues($template)
     {
656c295e
         $env = new Environment($this->createMock(LoaderInterface::class), ['cache' => false, 'autoescape' => false]);
90d579e4
         $parser = new Parser($env);
4647913e
 
90d579e4
         $parser->parse($env->tokenize(new Source($template, 'index')));
7259e52f
 
         // add a dummy assertion here to satisfy PHPUnit, the only thing we want to test is that the code above
         // can be executed without throwing any exceptions
         $this->addToAssertionCount(1);
4647913e
     }
 
     public function getMacroDefinitionSupportsConstantDefaultValues()
     {
5c55243d
         return [
             ['{% macro foo(name = "aa") %}{% endmacro %}'],
             ['{% macro foo(name = 12) %}{% endmacro %}'],
             ['{% macro foo(name = true) %}{% endmacro %}'],
             ['{% macro foo(name = ["a"]) %}{% endmacro %}'],
             ['{% macro foo(name = [["a"]]) %}{% endmacro %}'],
             ['{% macro foo(name = {a: "a"}) %}{% endmacro %}'],
             ['{% macro foo(name = {a: {b: "a"}}) %}{% endmacro %}'],
         ];
4647913e
     }
 
e1ad2bde
     public function testUnknownFunction()
     {
5ebcecf2
         $this->expectException(SyntaxError::class);
b776e41f
         $this->expectExceptionMessage('Unknown "cycl" function. Did you mean "cycle" in "index" at line 1?');
 
656c295e
         $env = new Environment($this->createMock(LoaderInterface::class), ['cache' => false, 'autoescape' => false]);
90d579e4
         $parser = new Parser($env);
e1ad2bde
 
90d579e4
         $parser->parse($env->tokenize(new Source('{{ cycl() }}', 'index')));
e1ad2bde
     }
 
d0a5ef8c
     public function testUnknownFunctionWithoutSuggestions()
     {
5ebcecf2
         $this->expectException(SyntaxError::class);
b776e41f
         $this->expectExceptionMessage('Unknown "foobar" function in "index" at line 1.');
 
656c295e
         $env = new Environment($this->createMock(LoaderInterface::class), ['cache' => false, 'autoescape' => false]);
90d579e4
         $parser = new Parser($env);
d0a5ef8c
 
90d579e4
         $parser->parse($env->tokenize(new Source('{{ foobar() }}', 'index')));
d0a5ef8c
     }
 
e1ad2bde
     public function testUnknownFilter()
     {
5ebcecf2
         $this->expectException(SyntaxError::class);
b776e41f
         $this->expectExceptionMessage('Unknown "lowe" filter. Did you mean "lower" in "index" at line 1?');
 
656c295e
         $env = new Environment($this->createMock(LoaderInterface::class), ['cache' => false, 'autoescape' => false]);
90d579e4
         $parser = new Parser($env);
e1ad2bde
 
90d579e4
         $parser->parse($env->tokenize(new Source('{{ 1|lowe }}', 'index')));
e1ad2bde
     }
 
d0a5ef8c
     public function testUnknownFilterWithoutSuggestions()
     {
5ebcecf2
         $this->expectException(SyntaxError::class);
b776e41f
         $this->expectExceptionMessage('Unknown "foobar" filter in "index" at line 1.');
 
656c295e
         $env = new Environment($this->createMock(LoaderInterface::class), ['cache' => false, 'autoescape' => false]);
90d579e4
         $parser = new Parser($env);
d0a5ef8c
 
90d579e4
         $parser->parse($env->tokenize(new Source('{{ 1|foobar }}', 'index')));
d0a5ef8c
     }
 
e1ad2bde
     public function testUnknownTest()
     {
5ebcecf2
         $this->expectException(SyntaxError::class);
b776e41f
         $this->expectExceptionMessage('Unknown "nul" test. Did you mean "null" in "index" at line 1');
 
656c295e
         $env = new Environment($this->createMock(LoaderInterface::class), ['cache' => false, 'autoescape' => false]);
90d579e4
         $parser = new Parser($env);
         $stream = $env->tokenize(new Source('{{ 1 is nul }}', 'index'));
6c76583e
         $parser->parse($stream);
e1ad2bde
     }
d0a5ef8c
 
     public function testUnknownTestWithoutSuggestions()
     {
5ebcecf2
         $this->expectException(SyntaxError::class);
b776e41f
         $this->expectExceptionMessage('Unknown "foobar" test in "index" at line 1.');
 
656c295e
         $env = new Environment($this->createMock(LoaderInterface::class), ['cache' => false, 'autoescape' => false]);
90d579e4
         $parser = new Parser($env);
d0a5ef8c
 
90d579e4
         $parser->parse($env->tokenize(new Source('{{ 1 is foobar }}', 'index')));
d0a5ef8c
     }
4505200d
 }