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