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
12use Twig\Environment;
13use Twig\Node\Expression\ArrayExpression;
14use Twig\Node\Expression\Binary\ConcatBinary;
15use Twig\Node\Expression\ConstantExpression;
16use Twig\Node\Expression\NameExpression;
17use Twig\Parser;
18use Twig\Source;
19
20class Twig_Tests_ExpressionParserTest extends \PHPUnit\Framework\TestCase
21{
22    /**
23     * @expectedException \Twig\Error\SyntaxError
24     * @dataProvider getFailingTestsForAssignment
25     */
26    public function testCanOnlyAssignToNames($template)
27    {
28        $env = new Environment($this->getMockBuilder('\Twig\Loader\LoaderInterface')->getMock(), ['cache' => false, 'autoescape' => false]);
29        $parser = new Parser($env);
30
31        $parser->parse($env->tokenize(new Source($template, 'index')));
32    }
33
34    public function getFailingTestsForAssignment()
35    {
36        return [
37            ['{% set false = "foo" %}'],
38            ['{% set FALSE = "foo" %}'],
39            ['{% set true = "foo" %}'],
40            ['{% set TRUE = "foo" %}'],
41            ['{% set none = "foo" %}'],
42            ['{% set NONE = "foo" %}'],
43            ['{% set null = "foo" %}'],
44            ['{% set NULL = "foo" %}'],
45            ['{% set 3 = "foo" %}'],
46            ['{% set 1 + 2 = "foo" %}'],
47            ['{% set "bar" = "foo" %}'],
48            ['{% set %}{% endset %}'],
49        ];
50    }
51
52    /**
53     * @dataProvider getTestsForArray
54     */
55    public function testArrayExpression($template, $expected)
56    {
57        $env = new Environment($this->getMockBuilder('\Twig\Loader\LoaderInterface')->getMock(), ['cache' => false, 'autoescape' => false]);
58        $stream = $env->tokenize(new Source($template, ''));
59        $parser = new Parser($env);
60
61        $this->assertEquals($expected, $parser->parse($stream)->getNode('body')->getNode(0)->getNode('expr'));
62    }
63
64    /**
65     * @expectedException \Twig\Error\SyntaxError
66     * @dataProvider getFailingTestsForArray
67     */
68    public function testArraySyntaxError($template)
69    {
70        $env = new Environment($this->getMockBuilder('\Twig\Loader\LoaderInterface')->getMock(), ['cache' => false, 'autoescape' => false]);
71        $parser = new Parser($env);
72
73        $parser->parse($env->tokenize(new Source($template, 'index')));
74    }
75
76    public function getFailingTestsForArray()
77    {
78        return [
79            ['{{ [1, "a": "b"] }}'],
80            ['{{ {"a": "b", 2} }}'],
81        ];
82    }
83
84    public function getTestsForArray()
85    {
86        return [
87            // simple array
88            ['{{ [1, 2] }}', new ArrayExpression([
89                  new ConstantExpression(0, 1),
90                  new ConstantExpression(1, 1),
91
92                  new ConstantExpression(1, 1),
93                  new ConstantExpression(2, 1),
94                ], 1),
95            ],
96
97            // array with trailing ,
98            ['{{ [1, 2, ] }}', new ArrayExpression([
99                  new ConstantExpression(0, 1),
100                  new ConstantExpression(1, 1),
101
102                  new ConstantExpression(1, 1),
103                  new ConstantExpression(2, 1),
104                ], 1),
105            ],
106
107            // simple hash
108            ['{{ {"a": "b", "b": "c"} }}', new ArrayExpression([
109                  new ConstantExpression('a', 1),
110                  new ConstantExpression('b', 1),
111
112                  new ConstantExpression('b', 1),
113                  new ConstantExpression('c', 1),
114                ], 1),
115            ],
116
117            // hash with trailing ,
118            ['{{ {"a": "b", "b": "c", } }}', new ArrayExpression([
119                  new ConstantExpression('a', 1),
120                  new ConstantExpression('b', 1),
121
122                  new ConstantExpression('b', 1),
123                  new ConstantExpression('c', 1),
124                ], 1),
125            ],
126
127            // hash in an array
128            ['{{ [1, {"a": "b", "b": "c"}] }}', new ArrayExpression([
129                  new ConstantExpression(0, 1),
130                  new ConstantExpression(1, 1),
131
132                  new ConstantExpression(1, 1),
133                  new ArrayExpression([
134                        new ConstantExpression('a', 1),
135                        new ConstantExpression('b', 1),
136
137                        new ConstantExpression('b', 1),
138                        new ConstantExpression('c', 1),
139                      ], 1),
140                ], 1),
141            ],
142
143            // array in a hash
144            ['{{ {"a": [1, 2], "b": "c"} }}', new ArrayExpression([
145                  new ConstantExpression('a', 1),
146                  new ArrayExpression([
147                        new ConstantExpression(0, 1),
148                        new ConstantExpression(1, 1),
149
150                        new ConstantExpression(1, 1),
151                        new ConstantExpression(2, 1),
152                      ], 1),
153                  new ConstantExpression('b', 1),
154                  new ConstantExpression('c', 1),
155                ], 1),
156            ],
157        ];
158    }
159
160    /**
161     * @expectedException \Twig\Error\SyntaxError
162     */
163    public function testStringExpressionDoesNotConcatenateTwoConsecutiveStrings()
164    {
165        $env = new Environment($this->getMockBuilder('\Twig\Loader\LoaderInterface')->getMock(), ['cache' => false, 'autoescape' => false, 'optimizations' => 0]);
166        $stream = $env->tokenize(new Source('{{ "a" "b" }}', 'index'));
167        $parser = new Parser($env);
168
169        $parser->parse($stream);
170    }
171
172    /**
173     * @dataProvider getTestsForString
174     */
175    public function testStringExpression($template, $expected)
176    {
177        $env = new Environment($this->getMockBuilder('\Twig\Loader\LoaderInterface')->getMock(), ['cache' => false, 'autoescape' => false, 'optimizations' => 0]);
178        $stream = $env->tokenize(new Source($template, ''));
179        $parser = new Parser($env);
180
181        $this->assertEquals($expected, $parser->parse($stream)->getNode('body')->getNode(0)->getNode('expr'));
182    }
183
184    public function getTestsForString()
185    {
186        return [
187            [
188                '{{ "foo" }}', new ConstantExpression('foo', 1),
189            ],
190            [
191                '{{ "foo #{bar}" }}', new ConcatBinary(
192                    new ConstantExpression('foo ', 1),
193                    new NameExpression('bar', 1),
194                    1
195                ),
196            ],
197            [
198                '{{ "foo #{bar} baz" }}', new ConcatBinary(
199                    new ConcatBinary(
200                        new ConstantExpression('foo ', 1),
201                        new NameExpression('bar', 1),
202                        1
203                    ),
204                    new ConstantExpression(' baz', 1),
205                    1
206                ),
207            ],
208
209            [
210                '{{ "foo #{"foo #{bar} baz"} baz" }}', new ConcatBinary(
211                    new ConcatBinary(
212                        new ConstantExpression('foo ', 1),
213                        new ConcatBinary(
214                            new ConcatBinary(
215                                new ConstantExpression('foo ', 1),
216                                new NameExpression('bar', 1),
217                                1
218                            ),
219                            new ConstantExpression(' baz', 1),
220                            1
221                        ),
222                        1
223                    ),
224                    new ConstantExpression(' baz', 1),
225                    1
226                ),
227            ],
228        ];
229    }
230
231    /**
232     * @expectedException \Twig\Error\SyntaxError
233     */
234    public function testAttributeCallDoesNotSupportNamedArguments()
235    {
236        $env = new Environment($this->getMockBuilder('\Twig\Loader\LoaderInterface')->getMock(), ['cache' => false, 'autoescape' => false]);
237        $parser = new Parser($env);
238
239        $parser->parse($env->tokenize(new Source('{{ foo.bar(name="Foo") }}', 'index')));
240    }
241
242    /**
243     * @expectedException \Twig\Error\SyntaxError
244     */
245    public function testMacroCallDoesNotSupportNamedArguments()
246    {
247        $env = new Environment($this->getMockBuilder('\Twig\Loader\LoaderInterface')->getMock(), ['cache' => false, 'autoescape' => false]);
248        $parser = new Parser($env);
249
250        $parser->parse($env->tokenize(new Source('{% from _self import foo %}{% macro foo() %}{% endmacro %}{{ foo(name="Foo") }}', 'index')));
251    }
252
253    /**
254     * @expectedException        \Twig\Error\SyntaxError
255     * @expectedExceptionMessage An argument must be a name. Unexpected token "string" of value "a" ("name" expected) in "index" at line 1.
256     */
257    public function testMacroDefinitionDoesNotSupportNonNameVariableName()
258    {
259        $env = new Environment($this->getMockBuilder('\Twig\Loader\LoaderInterface')->getMock(), ['cache' => false, 'autoescape' => false]);
260        $parser = new Parser($env);
261
262        $parser->parse($env->tokenize(new Source('{% macro foo("a") %}{% endmacro %}', 'index')));
263    }
264
265    /**
266     * @expectedException        \Twig\Error\SyntaxError
267     * @expectedExceptionMessage A default value for an argument must be a constant (a boolean, a string, a number, or an array) in "index" at line 1
268     * @dataProvider             getMacroDefinitionDoesNotSupportNonConstantDefaultValues
269     */
270    public function testMacroDefinitionDoesNotSupportNonConstantDefaultValues($template)
271    {
272        $env = new Environment($this->getMockBuilder('\Twig\Loader\LoaderInterface')->getMock(), ['cache' => false, 'autoescape' => false]);
273        $parser = new Parser($env);
274
275        $parser->parse($env->tokenize(new Source($template, 'index')));
276    }
277
278    public function getMacroDefinitionDoesNotSupportNonConstantDefaultValues()
279    {
280        return [
281            ['{% macro foo(name = "a #{foo} a") %}{% endmacro %}'],
282            ['{% macro foo(name = [["b", "a #{foo} a"]]) %}{% endmacro %}'],
283        ];
284    }
285
286    /**
287     * @dataProvider getMacroDefinitionSupportsConstantDefaultValues
288     */
289    public function testMacroDefinitionSupportsConstantDefaultValues($template)
290    {
291        $env = new Environment($this->getMockBuilder('\Twig\Loader\LoaderInterface')->getMock(), ['cache' => false, 'autoescape' => false]);
292        $parser = new Parser($env);
293
294        $parser->parse($env->tokenize(new Source($template, 'index')));
295
296        // add a dummy assertion here to satisfy PHPUnit, the only thing we want to test is that the code above
297        // can be executed without throwing any exceptions
298        $this->addToAssertionCount(1);
299    }
300
301    public function getMacroDefinitionSupportsConstantDefaultValues()
302    {
303        return [
304            ['{% macro foo(name = "aa") %}{% endmacro %}'],
305            ['{% macro foo(name = 12) %}{% endmacro %}'],
306            ['{% macro foo(name = true) %}{% endmacro %}'],
307            ['{% macro foo(name = ["a"]) %}{% endmacro %}'],
308            ['{% macro foo(name = [["a"]]) %}{% endmacro %}'],
309            ['{% macro foo(name = {a: "a"}) %}{% endmacro %}'],
310            ['{% macro foo(name = {a: {b: "a"}}) %}{% endmacro %}'],
311        ];
312    }
313
314    /**
315     * @expectedException        \Twig\Error\SyntaxError
316     * @expectedExceptionMessage Unknown "cycl" function. Did you mean "cycle" in "index" at line 1?
317     */
318    public function testUnknownFunction()
319    {
320        $env = new Environment($this->getMockBuilder('\Twig\Loader\LoaderInterface')->getMock(), ['cache' => false, 'autoescape' => false]);
321        $parser = new Parser($env);
322
323        $parser->parse($env->tokenize(new Source('{{ cycl() }}', 'index')));
324    }
325
326    /**
327     * @expectedException        \Twig\Error\SyntaxError
328     * @expectedExceptionMessage Unknown "foobar" function in "index" at line 1.
329     */
330    public function testUnknownFunctionWithoutSuggestions()
331    {
332        $env = new Environment($this->getMockBuilder('\Twig\Loader\LoaderInterface')->getMock(), ['cache' => false, 'autoescape' => false]);
333        $parser = new Parser($env);
334
335        $parser->parse($env->tokenize(new Source('{{ foobar() }}', 'index')));
336    }
337
338    /**
339     * @expectedException        \Twig\Error\SyntaxError
340     * @expectedExceptionMessage Unknown "lowe" filter. Did you mean "lower" in "index" at line 1?
341     */
342    public function testUnknownFilter()
343    {
344        $env = new Environment($this->getMockBuilder('\Twig\Loader\LoaderInterface')->getMock(), ['cache' => false, 'autoescape' => false]);
345        $parser = new Parser($env);
346
347        $parser->parse($env->tokenize(new Source('{{ 1|lowe }}', 'index')));
348    }
349
350    /**
351     * @expectedException        \Twig\Error\SyntaxError
352     * @expectedExceptionMessage Unknown "foobar" filter in "index" at line 1.
353     */
354    public function testUnknownFilterWithoutSuggestions()
355    {
356        $env = new Environment($this->getMockBuilder('\Twig\Loader\LoaderInterface')->getMock(), ['cache' => false, 'autoescape' => false]);
357        $parser = new Parser($env);
358
359        $parser->parse($env->tokenize(new Source('{{ 1|foobar }}', 'index')));
360    }
361
362    /**
363     * @expectedException        \Twig\Error\SyntaxError
364     * @expectedExceptionMessage Unknown "nul" test. Did you mean "null" in "index" at line 1
365     */
366    public function testUnknownTest()
367    {
368        $env = new Environment($this->getMockBuilder('\Twig\Loader\LoaderInterface')->getMock(), ['cache' => false, 'autoescape' => false]);
369        $parser = new Parser($env);
370        $stream = $env->tokenize(new Source('{{ 1 is nul }}', 'index'));
371        $parser->parse($stream);
372    }
373
374    /**
375     * @expectedException        \Twig\Error\SyntaxError
376     * @expectedExceptionMessage Unknown "foobar" test in "index" at line 1.
377     */
378    public function testUnknownTestWithoutSuggestions()
379    {
380        $env = new Environment($this->getMockBuilder('\Twig\Loader\LoaderInterface')->getMock(), ['cache' => false, 'autoescape' => false]);
381        $parser = new Parser($env);
382
383        $parser->parse($env->tokenize(new Source('{{ 1 is foobar }}', 'index')));
384    }
385}
386