1<?php 2 3/* 4 * This file is part of Twig. 5 * 6 * (c) Fabien Potencier 7 * (c) Armin Ronacher 8 * 9 * For the full copyright and license information, please view the LICENSE 10 * file that was distributed with this source code. 11 */ 12 13namespace Twig; 14 15use Twig\Error\SyntaxError; 16use Twig\ExpressionParser\Infix\DotExpressionParser; 17use Twig\ExpressionParser\Infix\FilterExpressionParser; 18use Twig\ExpressionParser\Infix\SquareBracketExpressionParser; 19use Twig\Node\Expression\ArrayExpression; 20use Twig\Node\Expression\ConstantExpression; 21use Twig\Node\Expression\Unary\NegUnary; 22use Twig\Node\Expression\Unary\PosUnary; 23use Twig\Node\Expression\Unary\SpreadUnary; 24use Twig\Node\Expression\Variable\AssignContextVariable; 25use Twig\Node\Expression\Variable\ContextVariable; 26use Twig\Node\Node; 27use Twig\Node\Nodes; 28 29/** 30 * Parses expressions. 31 * 32 * This parser implements a "Precedence climbing" algorithm. 33 * 34 * @see https://www.engr.mun.ca/~theo/Misc/exp_parsing.htm 35 * @see https://en.wikipedia.org/wiki/Operator-precedence_parser 36 * 37 * @author Fabien Potencier <fabien@symfony.com> 38 * 39 * @deprecated since Twig 3.21 40 */ 41class ExpressionParser 42{ 43 /** 44 * @deprecated since Twig 3.21 45 */ 46 public const OPERATOR_LEFT = 1; 47 /** 48 * @deprecated since Twig 3.21 49 */ 50 public const OPERATOR_RIGHT = 2; 51 52 public function __construct( 53 private Parser $parser, 54 private Environment $env, 55 ) { 56 trigger_deprecation('twig/twig', '3.21', 'Class "%s" is deprecated, use "Parser::parseExpression()" instead.', __CLASS__); 57 } 58 59 public function parseExpression($precedence = 0) 60 { 61 if (\func_num_args() > 1) { 62 trigger_deprecation('twig/twig', '3.15', 'Passing a second argument ($allowArrow) to "%s()" is deprecated.', __METHOD__); 63 } 64 65 trigger_deprecation('twig/twig', '3.21', 'The "%s()" method is deprecated, use "Parser::parseExpression()" instead.', __METHOD__); 66 67 return $this->parser->parseExpression((int) $precedence); 68 } 69 70 /** 71 * @deprecated since Twig 3.21 72 */ 73 public function parsePrimaryExpression() 74 { 75 trigger_deprecation('twig/twig', '3.21', 'The "%s()" method is deprecated.', __METHOD__); 76 77 return $this->parseExpression(); 78 } 79 80 /** 81 * @deprecated since Twig 3.21 82 */ 83 public function parseStringExpression() 84 { 85 trigger_deprecation('twig/twig', '3.21', 'The "%s()" method is deprecated.', __METHOD__); 86 87 return $this->parseExpression(); 88 } 89 90 /** 91 * @deprecated since Twig 3.11, use parseExpression() instead 92 */ 93 public function parseArrayExpression() 94 { 95 trigger_deprecation('twig/twig', '3.11', 'Calling "%s()" is deprecated, use "parseExpression()" instead.', __METHOD__); 96 97 return $this->parseExpression(); 98 } 99 100 /** 101 * @deprecated since Twig 3.21 102 */ 103 public function parseSequenceExpression() 104 { 105 trigger_deprecation('twig/twig', '3.21', 'The "%s()" method is deprecated.', __METHOD__); 106 107 return $this->parseExpression(); 108 } 109 110 /** 111 * @deprecated since Twig 3.11, use parseExpression() instead 112 */ 113 public function parseHashExpression() 114 { 115 trigger_deprecation('twig/twig', '3.11', 'Calling "%s()" is deprecated, use "parseExpression()" instead.', __METHOD__); 116 117 return $this->parseExpression(); 118 } 119 120 /** 121 * @deprecated since Twig 3.21 122 */ 123 public function parseMappingExpression() 124 { 125 trigger_deprecation('twig/twig', '3.21', 'The "%s()" method is deprecated.', __METHOD__); 126 127 return $this->parseExpression(); 128 } 129 130 /** 131 * @deprecated since Twig 3.21 132 */ 133 public function parsePostfixExpression($node) 134 { 135 trigger_deprecation('twig/twig', '3.21', 'The "%s()" method is deprecated.', __METHOD__); 136 137 while (true) { 138 $token = $this->parser->getCurrentToken(); 139 if ($token->test(Token::PUNCTUATION_TYPE)) { 140 if ('.' == $token->getValue() || '[' == $token->getValue()) { 141 $node = $this->parseSubscriptExpression($node); 142 } elseif ('|' == $token->getValue()) { 143 $node = $this->parseFilterExpression($node); 144 } else { 145 break; 146 } 147 } else { 148 break; 149 } 150 } 151 152 return $node; 153 } 154 155 /** 156 * @deprecated since Twig 3.21 157 */ 158 public function parseSubscriptExpression($node) 159 { 160 trigger_deprecation('twig/twig', '3.21', 'The "%s()" method is deprecated.', __METHOD__); 161 162 $parsers = new \ReflectionProperty($this->parser, 'parsers'); 163 164 if ('.' === $this->parser->getStream()->next()->getValue()) { 165 return $parsers->getValue($this->parser)->getByClass(DotExpressionParser::class)->parse($this->parser, $node, $this->parser->getCurrentToken()); 166 } 167 168 return $parsers->getValue($this->parser)->getByClass(SquareBracketExpressionParser::class)->parse($this->parser, $node, $this->parser->getCurrentToken()); 169 } 170 171 /** 172 * @deprecated since Twig 3.21 173 */ 174 public function parseFilterExpression($node) 175 { 176 trigger_deprecation('twig/twig', '3.21', 'The "%s()" method is deprecated.', __METHOD__); 177 178 $this->parser->getStream()->next(); 179 180 return $this->parseFilterExpressionRaw($node); 181 } 182 183 /** 184 * @deprecated since Twig 3.21 185 */ 186 public function parseFilterExpressionRaw($node) 187 { 188 trigger_deprecation('twig/twig', '3.21', 'The "%s()" method is deprecated.', __METHOD__); 189 190 $parsers = new \ReflectionProperty($this->parser, 'parsers'); 191 192 $op = $parsers->getValue($this->parser)->getByClass(FilterExpressionParser::class); 193 while (true) { 194 $node = $op->parse($this->parser, $node, $this->parser->getCurrentToken()); 195 if (!$this->parser->getStream()->test(Token::OPERATOR_TYPE, '|')) { 196 break; 197 } 198 $this->parser->getStream()->next(); 199 } 200 201 return $node; 202 } 203 204 /** 205 * Parses arguments. 206 * 207 * @return Node 208 * 209 * @throws SyntaxError 210 * 211 * @deprecated since Twig 3.19 Use Twig\ExpressionParser\Infix\ArgumentsTrait::parseNamedArguments() instead 212 */ 213 public function parseArguments() 214 { 215 trigger_deprecation('twig/twig', '3.19', \sprintf('The "%s()" method is deprecated, use "Twig\ExpressionParser\Infix\ArgumentsTrait::parseNamedArguments()" instead.', __METHOD__)); 216 217 $parsePrimaryExpression = new \ReflectionMethod($this->parser, 'parsePrimaryExpression'); 218 219 $namedArguments = false; 220 $definition = false; 221 if (\func_num_args() > 1) { 222 $definition = func_get_arg(1); 223 } 224 if (\func_num_args() > 0) { 225 trigger_deprecation('twig/twig', '3.15', 'Passing arguments to "%s()" is deprecated.', __METHOD__); 226 $namedArguments = func_get_arg(0); 227 } 228 229 $args = []; 230 $stream = $this->parser->getStream(); 231 232 $stream->expect(Token::OPERATOR_TYPE, '(', 'A list of arguments must begin with an opening parenthesis'); 233 $hasSpread = false; 234 while (!$stream->test(Token::PUNCTUATION_TYPE, ')')) { 235 if ($args) { 236 $stream->expect(Token::PUNCTUATION_TYPE, ',', 'Arguments must be separated by a comma'); 237 238 // if the comma above was a trailing comma, early exit the argument parse loop 239 if ($stream->test(Token::PUNCTUATION_TYPE, ')')) { 240 break; 241 } 242 } 243 244 if ($definition) { 245 $token = $stream->expect(Token::NAME_TYPE, null, 'An argument must be a name'); 246 $value = new ContextVariable($token->getValue(), $this->parser->getCurrentToken()->getLine()); 247 } else { 248 if ($stream->nextIf(Token::SPREAD_TYPE)) { 249 $hasSpread = true; 250 $value = new SpreadUnary($this->parseExpression(), $stream->getCurrent()->getLine()); 251 } elseif ($hasSpread) { 252 throw new SyntaxError('Normal arguments must be placed before argument unpacking.', $stream->getCurrent()->getLine(), $stream->getSourceContext()); 253 } else { 254 $value = $this->parseExpression(); 255 } 256 } 257 258 $name = null; 259 if ($namedArguments && (($token = $stream->nextIf(Token::OPERATOR_TYPE, '=')) || (!$definition && $token = $stream->nextIf(Token::PUNCTUATION_TYPE, ':')))) { 260 if (!$value instanceof ContextVariable) { 261 throw new SyntaxError(\sprintf('A parameter name must be a string, "%s" given.', $value::class), $token->getLine(), $stream->getSourceContext()); 262 } 263 $name = $value->getAttribute('name'); 264 265 if ($definition) { 266 $value = $parsePrimaryExpression->invoke($this->parser); 267 268 if (!$this->checkConstantExpression($value)) { 269 throw new SyntaxError('A default value for an argument must be a constant (a boolean, a string, a number, a sequence, or a mapping).', $token->getLine(), $stream->getSourceContext()); 270 } 271 } else { 272 $value = $this->parseExpression(); 273 } 274 } 275 276 if ($definition) { 277 if (null === $name) { 278 $name = $value->getAttribute('name'); 279 $value = new ConstantExpression(null, $this->parser->getCurrentToken()->getLine()); 280 $value->setAttribute('is_implicit', true); 281 } 282 $args[$name] = $value; 283 } else { 284 if (null === $name) { 285 $args[] = $value; 286 } else { 287 $args[$name] = $value; 288 } 289 } 290 } 291 $stream->expect(Token::PUNCTUATION_TYPE, ')', 'A list of arguments must be closed by a parenthesis'); 292 293 return new Nodes($args); 294 } 295 296 /** 297 * @deprecated since Twig 3.21, use "AbstractTokenParser::parseAssignmentExpression()" instead 298 */ 299 public function parseAssignmentExpression() 300 { 301 trigger_deprecation('twig/twig', '3.21', 'The "%s()" method is deprecated, use "AbstractTokenParser::parseAssignmentExpression()" instead.', __METHOD__); 302 303 $stream = $this->parser->getStream(); 304 $targets = []; 305 while (true) { 306 $token = $this->parser->getCurrentToken(); 307 if ($stream->test(Token::OPERATOR_TYPE) && preg_match(Lexer::REGEX_NAME, $token->getValue())) { 308 // in this context, string operators are variable names 309 $this->parser->getStream()->next(); 310 } else { 311 $stream->expect(Token::NAME_TYPE, null, 'Only variables can be assigned to'); 312 } 313 $targets[] = new AssignContextVariable($token->getValue(), $token->getLine()); 314 315 if (!$stream->nextIf(Token::PUNCTUATION_TYPE, ',')) { 316 break; 317 } 318 } 319 320 return new Nodes($targets); 321 } 322 323 /** 324 * @deprecated since Twig 3.21 325 */ 326 public function parseMultitargetExpression() 327 { 328 trigger_deprecation('twig/twig', '3.21', 'The "%s()" method is deprecated.', __METHOD__); 329 330 $targets = []; 331 while (true) { 332 $targets[] = $this->parseExpression(); 333 if (!$this->parser->getStream()->nextIf(Token::PUNCTUATION_TYPE, ',')) { 334 break; 335 } 336 } 337 338 return new Nodes($targets); 339 } 340 341 // checks that the node only contains "constant" elements 342 // to be removed in 4.0 343 private function checkConstantExpression(Node $node): bool 344 { 345 if (!($node instanceof ConstantExpression || $node instanceof ArrayExpression 346 || $node instanceof NegUnary || $node instanceof PosUnary 347 )) { 348 return false; 349 } 350 351 foreach ($node as $n) { 352 if (!$this->checkConstantExpression($n)) { 353 return false; 354 } 355 } 356 357 return true; 358 } 359 360 /** 361 * @deprecated since Twig 3.19 Use Twig\ExpressionParser\Infix\ArgumentsTrait::parseNamedArguments() instead 362 */ 363 public function parseOnlyArguments() 364 { 365 trigger_deprecation('twig/twig', '3.19', \sprintf('The "%s()" method is deprecated, use "Twig\ExpressionParser\Infix\ArgumentsTrait::parseNamedArguments()" instead.', __METHOD__)); 366 367 return $this->parseArguments(); 368 } 369} 370