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