This project does not seem to handle request data directly as such no vulnerable execution paths were found.
include
, or for example
via PHP's auto-loading mechanism.
These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more
1 | <?php |
||
2 | |||
3 | namespace uuf6429\ExpressionLanguage; |
||
4 | |||
5 | use uuf6429\ExpressionLanguage\Node\ArrowFuncNode; |
||
6 | use Symfony\Component\ExpressionLanguage\Node\ArgumentsNode; |
||
7 | use Symfony\Component\ExpressionLanguage\Node\ArrayNode; |
||
8 | use Symfony\Component\ExpressionLanguage\Node\BinaryNode; |
||
9 | use Symfony\Component\ExpressionLanguage\Node\ConditionalNode; |
||
10 | use Symfony\Component\ExpressionLanguage\Node\ConstantNode; |
||
11 | use Symfony\Component\ExpressionLanguage\Node\FunctionNode; |
||
12 | use Symfony\Component\ExpressionLanguage\Node\GetAttrNode; |
||
13 | use Symfony\Component\ExpressionLanguage\Node\NameNode; |
||
14 | use Symfony\Component\ExpressionLanguage\Node\Node; |
||
15 | use Symfony\Component\ExpressionLanguage\Node\UnaryNode; |
||
16 | use Symfony\Component\ExpressionLanguage\Token; |
||
17 | use Symfony\Component\ExpressionLanguage\SyntaxError; |
||
18 | use Symfony\Component\ExpressionLanguage\Parser as SymfonyParser; |
||
19 | use Symfony\Component\ExpressionLanguage\TokenStream as SymfonyTokenStream; |
||
20 | |||
21 | class Parser extends SymfonyParser |
||
22 | { |
||
23 | public const OPERATOR_LEFT = 1; |
||
24 | public const OPERATOR_RIGHT = 2; |
||
25 | |||
26 | private const TOKEN_REPLACEMENT_TYPE = 'replacement'; |
||
27 | |||
28 | /** @var TokenStream */ |
||
29 | private $stream; |
||
0 ignored issues
–
show
Comprehensibility
introduced
by
![]() |
|||
30 | |||
31 | /** @var array */ |
||
32 | private $unaryOperators; |
||
0 ignored issues
–
show
|
|||
33 | |||
34 | /** @var array */ |
||
35 | private $binaryOperators; |
||
0 ignored issues
–
show
|
|||
36 | |||
37 | /** @var array */ |
||
38 | private $functions; |
||
0 ignored issues
–
show
|
|||
39 | |||
40 | /** @var string[] */ |
||
41 | private $names; |
||
0 ignored issues
–
show
|
|||
42 | |||
43 | /** @var Node[] */ |
||
44 | 27 | private $replacementNodes; |
|
45 | |||
46 | 27 | public function __construct(array $functions) |
|
47 | { |
||
48 | 27 | parent::__construct($functions); |
|
49 | |||
50 | $this->functions = $functions; |
||
51 | |||
52 | $this->unaryOperators = array( |
||
53 | 'not' => array('precedence' => 50), |
||
54 | 27 | '!' => array('precedence' => 50), |
|
55 | 27 | '-' => array('precedence' => 500), |
|
56 | 27 | '+' => array('precedence' => 500), |
|
57 | 27 | ); |
|
58 | 27 | $this->binaryOperators = array( |
|
59 | 27 | '->' => array('precedence' => 5, 'associativity' => self::OPERATOR_LEFT), |
|
60 | 27 | 'or' => array('precedence' => 10, 'associativity' => self::OPERATOR_LEFT), |
|
61 | 27 | '||' => array('precedence' => 10, 'associativity' => self::OPERATOR_LEFT), |
|
62 | 27 | 'and' => array('precedence' => 15, 'associativity' => self::OPERATOR_LEFT), |
|
63 | 27 | '&&' => array('precedence' => 15, 'associativity' => self::OPERATOR_LEFT), |
|
64 | 27 | '|' => array('precedence' => 16, 'associativity' => self::OPERATOR_LEFT), |
|
65 | 27 | '^' => array('precedence' => 17, 'associativity' => self::OPERATOR_LEFT), |
|
66 | 27 | '&' => array('precedence' => 18, 'associativity' => self::OPERATOR_LEFT), |
|
67 | 27 | '==' => array('precedence' => 20, 'associativity' => self::OPERATOR_LEFT), |
|
68 | 27 | '===' => array('precedence' => 20, 'associativity' => self::OPERATOR_LEFT), |
|
69 | 27 | '!=' => array('precedence' => 20, 'associativity' => self::OPERATOR_LEFT), |
|
70 | 27 | '!==' => array('precedence' => 20, 'associativity' => self::OPERATOR_LEFT), |
|
71 | 27 | '<' => array('precedence' => 20, 'associativity' => self::OPERATOR_LEFT), |
|
72 | 27 | '>' => array('precedence' => 20, 'associativity' => self::OPERATOR_LEFT), |
|
73 | 27 | '>=' => array('precedence' => 20, 'associativity' => self::OPERATOR_LEFT), |
|
74 | 27 | '<=' => array('precedence' => 20, 'associativity' => self::OPERATOR_LEFT), |
|
75 | 27 | 'not in' => array('precedence' => 20, 'associativity' => self::OPERATOR_LEFT), |
|
76 | 27 | 'in' => array('precedence' => 20, 'associativity' => self::OPERATOR_LEFT), |
|
77 | 27 | 'matches' => array('precedence' => 20, 'associativity' => self::OPERATOR_LEFT), |
|
78 | 27 | '..' => array('precedence' => 25, 'associativity' => self::OPERATOR_LEFT), |
|
79 | 27 | '+' => array('precedence' => 30, 'associativity' => self::OPERATOR_LEFT), |
|
80 | 27 | '-' => array('precedence' => 30, 'associativity' => self::OPERATOR_LEFT), |
|
81 | 27 | '~' => array('precedence' => 40, 'associativity' => self::OPERATOR_LEFT), |
|
82 | '*' => array('precedence' => 60, 'associativity' => self::OPERATOR_LEFT), |
||
83 | '/' => array('precedence' => 60, 'associativity' => self::OPERATOR_LEFT), |
||
84 | 27 | '%' => array('precedence' => 60, 'associativity' => self::OPERATOR_LEFT), |
|
85 | 27 | '**' => array('precedence' => 200, 'associativity' => self::OPERATOR_RIGHT), |
|
86 | ); |
||
87 | |||
88 | $this->replacementNodes = array(); |
||
89 | } |
||
90 | |||
91 | /** |
||
92 | * Converts a token stream to a node tree. |
||
93 | * |
||
94 | * The valid names is an array where the values |
||
95 | * are the names that the user can use in an expression. |
||
96 | * |
||
97 | * If the variable name in the compiled PHP code must be |
||
98 | * different, define it as the key. |
||
99 | * |
||
100 | * For instance, ['this' => 'container'] means that the |
||
101 | * variable 'container' can be used in the expression |
||
102 | * but the compiled code will use 'this'. |
||
103 | * |
||
104 | * @param TokenStream $stream A token stream instance |
||
105 | * @param array $names An array of valid names |
||
106 | * |
||
107 | 27 | * @return Node A node tree |
|
108 | * |
||
109 | 27 | * @throws SyntaxError |
|
110 | */ |
||
111 | 27 | public function parse(SymfonyTokenStream $stream, $names = array()) |
|
112 | { |
||
113 | 27 | $this->names = $names; |
|
114 | 27 | ||
115 | $stream = $this->preParseArrowFuncs($stream); |
||
0 ignored issues
–
show
$stream of type object<Symfony\Component...onLanguage\TokenStream> is not a sub-type of object<uuf6429\ExpressionLanguage\TokenStream> . It seems like you assume a child class of the class Symfony\Component\ExpressionLanguage\TokenStream to be always present.
This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass. Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type. ![]() |
|||
116 | 21 | ||
117 | $this->stream = $stream; |
||
118 | $node = $this->parseExpression(); |
||
119 | |||
120 | if (!$stream->isEOF()) { |
||
121 | throw new SyntaxError( |
||
122 | sprintf( |
||
123 | 'Unexpected token "%s" of value "%s"', |
||
124 | $stream->current->type, |
||
125 | $stream->current->value |
||
126 | ), |
||
127 | 21 | $stream->current->cursor |
|
128 | ); |
||
129 | } |
||
130 | |||
131 | return $node; |
||
132 | } |
||
133 | |||
134 | /** |
||
135 | * Replaces all anonymous functions with placeholder tokens. |
||
136 | * |
||
137 | 27 | * @param TokenStream $stream |
|
138 | * |
||
139 | 27 | * @return TokenStream |
|
140 | 27 | */ |
|
141 | 3 | protected function preParseArrowFuncs(TokenStream $stream) |
|
142 | 3 | { |
|
143 | 3 | while (!$stream->isEOF()) { |
|
144 | 3 | if ($stream->current->test(Token::OPERATOR_TYPE, '->')) { |
|
145 | $operatorPos = $stream->position(); |
||
146 | $operatorCursor = $stream->current->cursor; |
||
147 | 3 | $replacementNodeIndex = count($this->replacementNodes); |
|
148 | 3 | $this->replacementNodes[$replacementNodeIndex] = null; |
|
149 | 3 | ||
150 | 3 | // parse parameters |
|
151 | 3 | $parameterNames = array(); |
|
152 | 3 | $parameterNodes = array(); |
|
153 | 3 | $expectParam = true; |
|
154 | 3 | $stream->prev(); |
|
155 | 3 | $stream->expectPrev(Token::PUNCTUATION_TYPE, ')', 'Parameter list must end with parenthesis'); |
|
156 | 3 | while (!$stream->current->test(Token::PUNCTUATION_TYPE, '(')) { |
|
157 | 3 | if ($expectParam) { |
|
158 | $stream->current->test(Token::NAME_TYPE); |
||
0 ignored issues
–
show
The call to the method
Symfony\Component\ExpressionLanguage\Token::test() seems un-needed as the method has no side-effects.
PHP Analyzer performs a side-effects analysis of your code. A side-effect is basically anything that might be visible after the scope of the method is left. Let’s take a look at an example: class User
{
private $email;
public function getEmail()
{
return $this->email;
}
public function setEmail($email)
{
$this->email = $email;
}
}
If we look at the $user = new User();
$user->getEmail(); // This line could safely be removed as it has no effect.
On the hand, if we look at the $user = new User();
$user->setEmail('email@domain'); // This line has a side-effect (it changes an
// instance variable).
![]() |
|||
159 | 2 | array_unshift($parameterNames, $stream->current->value); |
|
160 | array_unshift($parameterNodes, new NameNode($stream->current->value)); |
||
161 | 3 | $stream->prev(); |
|
162 | } else { |
||
163 | 3 | $stream->expectPrev(Token::PUNCTUATION_TYPE, ',', 'Parameters must be separated by a comma'); |
|
164 | } |
||
165 | $expectParam = !$expectParam; |
||
166 | 3 | } |
|
167 | 3 | $startPos = $stream->position(); |
|
168 | 3 | ||
169 | 3 | // parse body |
|
170 | 3 | $stream->seek($operatorPos, SEEK_SET); |
|
171 | 3 | $stream->next(); |
|
172 | 3 | $stream->expect(Token::PUNCTUATION_TYPE, '{', 'Anonymous function body must start with a curly bracket'); |
|
173 | 1 | $bodyTokens = array(); |
|
174 | $openingBracketCount = 1; |
||
175 | while ($openingBracketCount != 0) { |
||
176 | 3 | if ($stream->current->test(Token::PUNCTUATION_TYPE, '{')) { |
|
177 | 3 | ++$openingBracketCount; |
|
178 | } |
||
179 | |||
180 | 3 | if ($stream->current->test(Token::PUNCTUATION_TYPE, '}')) { |
|
181 | 3 | --$openingBracketCount; |
|
182 | 3 | } |
|
183 | |||
184 | 3 | if (!$openingBracketCount) { |
|
185 | 3 | $currentNames = $this->names; |
|
186 | 3 | $currentStream = $this->stream; |
|
187 | |||
188 | $bodyTokens[] = new Token(Token::EOF_TYPE, null, 0); |
||
189 | $bodyNode = $this->parse( |
||
190 | 3 | new TokenStream($bodyTokens), |
|
191 | 3 | array_merge($currentNames, $parameterNames) |
|
192 | 3 | ); |
|
193 | |||
194 | $this->names = $currentNames; |
||
195 | 3 | $this->stream = $currentStream; |
|
196 | 3 | break; |
|
197 | } |
||
198 | 3 | ||
199 | 3 | $bodyTokens[] = $stream->current; |
|
200 | $stream->next(); |
||
201 | } |
||
202 | 3 | $stream->expect(Token::PUNCTUATION_TYPE, '}', 'Anonymous function body must end with a curly bracket'); |
|
203 | 3 | $endPos = $stream->position(); |
|
204 | 3 | ||
205 | // update token stream |
||
206 | $this->replacementNodes[$replacementNodeIndex] = new ArrowFuncNode($parameterNodes, $bodyNode); |
||
0 ignored issues
–
show
The variable
$bodyNode does not seem to be defined for all execution paths leading up to this point.
If you define a variable conditionally, it can happen that it is not defined for all execution paths. Let’s take a look at an example: function myFunction($a) {
switch ($a) {
case 'foo':
$x = 1;
break;
case 'bar':
$x = 2;
break;
}
// $x is potentially undefined here.
echo $x;
}
In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined. Available Fixes
![]() |
|||
207 | 3 | $replacement = new Token(static::TOKEN_REPLACEMENT_TYPE, $replacementNodeIndex, $operatorCursor); |
|
208 | $stream = $stream->splice($startPos, $endPos - $startPos, array($replacement)); |
||
209 | |||
210 | 27 | // keep parsing anonymous functions |
|
211 | $stream->seek($startPos, SEEK_SET); |
||
212 | 27 | } |
|
213 | |||
214 | 27 | $stream->next(); |
|
215 | } |
||
216 | $stream->rewind(); |
||
217 | 27 | ||
218 | return $stream; |
||
219 | 27 | } |
|
220 | 21 | ||
221 | 21 | public function parseExpression($precedence = 0) |
|
222 | 6 | { |
|
223 | 6 | $expr = $this->getPrimary(); |
|
224 | $token = $this->stream->current; |
||
225 | 6 | while ($token->test(Token::OPERATOR_TYPE) && isset($this->binaryOperators[$token->value]) && $this->binaryOperators[$token->value]['precedence'] >= $precedence) { |
|
226 | 6 | $op = $this->binaryOperators[$token->value]; |
|
227 | $this->stream->next(); |
||
228 | 6 | ||
229 | $expr1 = $this->parseExpression(self::OPERATOR_LEFT === $op['associativity'] ? $op['precedence'] + 1 : $op['precedence']); |
||
230 | $expr = new BinaryNode($token->value, $expr, $expr1); |
||
231 | 21 | ||
232 | 21 | $token = $this->stream->current; |
|
233 | } |
||
234 | |||
235 | 7 | if (0 === $precedence) { |
|
236 | return $this->parseConditionalExpression($expr); |
||
237 | } |
||
238 | 27 | ||
239 | return $expr; |
||
240 | 27 | } |
|
241 | |||
242 | 27 | protected function getPrimary() |
|
243 | 1 | { |
|
244 | 1 | $token = $this->stream->current; |
|
245 | 1 | ||
246 | if ($token->test(Token::OPERATOR_TYPE) && isset($this->unaryOperators[$token->value])) { |
||
247 | 1 | $operator = $this->unaryOperators[$token->value]; |
|
248 | $this->stream->next(); |
||
249 | $expr = $this->parseExpression($operator['precedence']); |
||
250 | 27 | ||
251 | 1 | return $this->parsePostfixExpression(new UnaryNode($token->value, $expr)); |
|
252 | 1 | } |
|
253 | 1 | ||
254 | if ($token->test(Token::PUNCTUATION_TYPE, '(')) { |
||
255 | 1 | $this->stream->next(); |
|
256 | $expr = $this->parseExpression(); |
||
257 | $this->stream->expect(Token::PUNCTUATION_TYPE, ')', 'An opened parenthesis is not properly closed'); |
||
258 | 27 | ||
259 | return $this->parsePostfixExpression($expr); |
||
260 | } |
||
261 | 21 | ||
262 | return $this->parsePrimaryExpression(); |
||
263 | 21 | } |
|
264 | 1 | ||
265 | 1 | protected function parseConditionalExpression($expr): Node |
|
266 | 1 | { |
|
267 | 1 | while ($this->stream->current->test(Token::PUNCTUATION_TYPE, '?')) { |
|
268 | 1 | $this->stream->next(); |
|
269 | 1 | if (!$this->stream->current->test(Token::PUNCTUATION_TYPE, ':')) { |
|
270 | $expr2 = $this->parseExpression(); |
||
271 | 1 | if ($this->stream->current->test(Token::PUNCTUATION_TYPE, ':')) { |
|
272 | $this->stream->next(); |
||
273 | $expr3 = $this->parseExpression(); |
||
274 | } else { |
||
275 | $expr3 = new ConstantNode(null); |
||
276 | } |
||
277 | } else { |
||
278 | $this->stream->next(); |
||
279 | 1 | $expr2 = $expr; |
|
280 | $expr3 = $this->parseExpression(); |
||
281 | } |
||
282 | 21 | ||
283 | $expr = new ConditionalNode($expr, $expr2, $expr3); |
||
284 | } |
||
285 | 27 | ||
286 | return $expr; |
||
287 | 27 | } |
|
288 | 27 | ||
289 | 27 | public function parsePrimaryExpression() |
|
290 | 21 | { |
|
291 | 21 | $token = $this->stream->current; |
|
292 | 21 | switch ($token->type) { |
|
293 | 20 | case Token::NAME_TYPE: |
|
294 | 3 | $this->stream->next(); |
|
295 | switch ($token->value) { |
||
296 | 20 | case 'true': |
|
297 | 18 | case 'TRUE': |
|
298 | 2 | return new ConstantNode(true); |
|
299 | |||
300 | 18 | case 'false': |
|
301 | 17 | case 'FALSE': |
|
302 | 1 | return new ConstantNode(false); |
|
303 | |||
304 | case 'null': |
||
305 | 17 | case 'NULL': |
|
306 | 2 | return new ConstantNode(null); |
|
307 | |||
308 | default: |
||
309 | if ('(' === $this->stream->current->value) { |
||
310 | 2 | if (false === isset($this->functions[$token->value])) { |
|
311 | throw new SyntaxError(sprintf('The function "%s" does not exist', $token->value), $token->cursor); |
||
312 | 17 | } |
|
313 | 2 | ||
314 | $node = new FunctionNode($token->value, $this->parseArguments()); |
||
315 | } else { |
||
316 | if (!in_array($token->value, $this->names, true)) { |
||
317 | throw new SyntaxError(sprintf('Variable "%s" is not valid', $token->value), $token->cursor); |
||
318 | 15 | } |
|
319 | 14 | ||
320 | // is the name used in the compiled code different |
||
321 | // from the name used in the expression? |
||
322 | 15 | if (is_int($name = array_search($token->value, $this->names))) { |
|
323 | $name = $token->value; |
||
324 | } |
||
325 | 15 | ||
326 | $node = new NameNode($name); |
||
327 | 12 | } |
|
328 | 6 | } |
|
329 | 10 | break; |
|
330 | |||
331 | 10 | case Token::NUMBER_TYPE: |
|
332 | case Token::STRING_TYPE: |
||
333 | 3 | $this->stream->next(); |
|
334 | 3 | ||
335 | return new ConstantNode($token->value); |
||
336 | 3 | ||
337 | case static::TOKEN_REPLACEMENT_TYPE: |
||
338 | $this->stream->next(); |
||
339 | |||
340 | return $this->replacementNodes[$token->value]; |
||
341 | |||
342 | default: |
||
343 | if ($token->test(Token::PUNCTUATION_TYPE, '[')) { |
||
344 | $node = $this->parseArrayExpression(); |
||
345 | } elseif ($token->test(Token::PUNCTUATION_TYPE, '{')) { |
||
346 | $node = $this->parseHashExpression(); |
||
347 | } else { |
||
348 | 15 | throw new SyntaxError(sprintf('Unexpected token "%s" of value "%s"', $token->type, $token->value), $token->cursor); |
|
349 | } |
||
350 | } |
||
351 | |||
352 | return $this->parsePostfixExpression($node); |
||
353 | } |
||
354 | |||
355 | public function parseArrayExpression(): ArrayNode |
||
356 | { |
||
357 | $this->stream->expect(Token::PUNCTUATION_TYPE, '[', 'An array element was expected'); |
||
358 | |||
359 | $node = new ArrayNode(); |
||
360 | $first = true; |
||
361 | while (!$this->stream->current->test(Token::PUNCTUATION_TYPE, ']')) { |
||
362 | if (!$first) { |
||
363 | $this->stream->expect(Token::PUNCTUATION_TYPE, ',', 'An array element must be followed by a comma'); |
||
364 | |||
365 | // trailing ,? |
||
366 | if ($this->stream->current->test(Token::PUNCTUATION_TYPE, ']')) { |
||
367 | break; |
||
368 | } |
||
369 | } |
||
370 | $first = false; |
||
371 | |||
372 | $node->addElement($this->parseExpression()); |
||
373 | } |
||
374 | $this->stream->expect(Token::PUNCTUATION_TYPE, ']', 'An opened array is not properly closed'); |
||
375 | |||
376 | return $node; |
||
377 | } |
||
378 | |||
379 | public function parseHashExpression(): ArrayNode |
||
380 | { |
||
381 | $this->stream->expect(Token::PUNCTUATION_TYPE, '{', 'A hash element was expected'); |
||
382 | |||
383 | $node = new ArrayNode(); |
||
384 | $first = true; |
||
385 | while (!$this->stream->current->test(Token::PUNCTUATION_TYPE, '}')) { |
||
386 | if (!$first) { |
||
387 | $this->stream->expect(Token::PUNCTUATION_TYPE, ',', 'A hash value must be followed by a comma'); |
||
388 | |||
389 | // trailing ,? |
||
390 | if ($this->stream->current->test(Token::PUNCTUATION_TYPE, '}')) { |
||
391 | break; |
||
392 | } |
||
393 | } |
||
394 | $first = false; |
||
395 | |||
396 | // a hash key can be: |
||
397 | |||
398 | // * a number -- 12 |
||
399 | // * a string -- 'a' |
||
400 | // * a name, which is equivalent to a string -- a |
||
401 | // * an expression, which must be enclosed in parentheses -- (1 + 2) |
||
402 | if ($this->stream->current->test(Token::STRING_TYPE) || $this->stream->current->test(Token::NAME_TYPE) || $this->stream->current->test(Token::NUMBER_TYPE)) { |
||
403 | $key = new ConstantNode($this->stream->current->value); |
||
404 | $this->stream->next(); |
||
405 | } elseif ($this->stream->current->test(Token::PUNCTUATION_TYPE, '(')) { |
||
406 | $key = $this->parseExpression(); |
||
407 | } else { |
||
408 | $current = $this->stream->current; |
||
409 | |||
410 | throw new SyntaxError(sprintf('A hash key must be a quoted string, a number, a name, or an expression enclosed in parentheses (unexpected token "%s" of value "%s"', $current->type, $current->value), $current->cursor); |
||
411 | } |
||
412 | |||
413 | $this->stream->expect(Token::PUNCTUATION_TYPE, ':', 'A hash key must be followed by a colon (:)'); |
||
414 | $value = $this->parseExpression(); |
||
415 | |||
416 | $node->addElement($value, $key); |
||
417 | } |
||
418 | $this->stream->expect(Token::PUNCTUATION_TYPE, '}', 'An opened hash is not properly closed'); |
||
419 | 17 | ||
420 | return $node; |
||
421 | 17 | } |
|
422 | 17 | ||
423 | 12 | public function parsePostfixExpression($node): Node |
|
424 | 9 | { |
|
425 | 9 | $token = $this->stream->current; |
|
426 | 9 | while ($token->type === Token::PUNCTUATION_TYPE) { |
|
427 | if ('.' === $token->value) { |
||
428 | $this->stream->next(); |
||
429 | 9 | $token = $this->stream->current; |
|
430 | $this->stream->next(); |
||
431 | |||
432 | if ( |
||
433 | $token->type !== Token::NAME_TYPE |
||
434 | && |
||
435 | // Operators like "not" and "matches" are valid method or property names, |
||
436 | |||
437 | // In other words, besides NAME_TYPE, OPERATOR_TYPE could also be parsed as a property or method. |
||
438 | // This is because operators are processed by the lexer prior to names. So "not" in "foo.not()" or "matches" in "foo.matches" will be recognized as an operator first. |
||
439 | // But in fact, "not" and "matches" in such expressions shall be parsed as method or property names. |
||
440 | |||
441 | // And this ONLY works if the operator consists of valid characters for a property or method name. |
||
442 | 9 | ||
443 | // Other types, such as STRING_TYPE and NUMBER_TYPE, can't be parsed as property nor method names. |
||
444 | 4 | ||
445 | // As a result, if $token is NOT an operator OR $token->value is NOT a valid property or method name, an exception shall be thrown. |
||
446 | ($token->type !== Token::OPERATOR_TYPE || !preg_match('/[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*/A', $token->value)) |
||
447 | 5 | ) { |
|
448 | throw new SyntaxError('Expected name', $token->cursor); |
||
449 | 5 | } |
|
450 | 5 | ||
451 | 4 | $arg = new NameNode($token->value); |
|
452 | 4 | ||
453 | 4 | $arguments = new ArgumentsNode(); |
|
454 | if ($this->stream->current->test(Token::PUNCTUATION_TYPE, '(')) { |
||
455 | $type = GetAttrNode::METHOD_CALL; |
||
456 | 2 | foreach ($this->parseArguments()->nodes as $n) { |
|
457 | $arguments->addElement($n); |
||
458 | } |
||
459 | 5 | } else { |
|
460 | 4 | $type = GetAttrNode::PROPERTY_CALL; |
|
461 | 2 | } |
|
462 | 2 | ||
463 | 2 | $node = new GetAttrNode($node, $arg, $arguments, $type); |
|
464 | } elseif ('[' === $token->value) { |
||
465 | 2 | $this->stream->next(); |
|
466 | $arg = $this->parseExpression(); |
||
467 | 2 | $this->stream->expect(Token::PUNCTUATION_TYPE, ']'); |
|
468 | |||
469 | $node = new GetAttrNode($node, $arg, new ArgumentsNode(), GetAttrNode::ARRAY_CALL); |
||
470 | 6 | } else { |
|
471 | break; |
||
472 | } |
||
473 | 13 | ||
474 | $token = $this->stream->current; |
||
475 | } |
||
476 | |||
477 | return $node; |
||
478 | } |
||
479 | 6 | ||
480 | /** |
||
481 | 6 | * Parses arguments. |
|
482 | 6 | */ |
|
483 | 6 | public function parseArguments(): Node |
|
484 | 3 | { |
|
485 | 3 | $args = array(); |
|
486 | $this->stream->expect(Token::PUNCTUATION_TYPE, '(', 'A list of arguments must begin with an opening parenthesis'); |
||
487 | while (!$this->stream->current->test(Token::PUNCTUATION_TYPE, ')')) { |
||
488 | 3 | if (!empty($args)) { |
|
489 | $this->stream->expect(Token::PUNCTUATION_TYPE, ',', 'Arguments must be separated by a comma'); |
||
490 | 6 | } |
|
491 | |||
492 | 6 | $args[] = $this->parseExpression(); |
|
493 | } |
||
494 | $this->stream->expect(Token::PUNCTUATION_TYPE, ')', 'A list of arguments must be closed by a parenthesis'); |
||
495 | |||
496 | return new Node($args); |
||
497 | } |
||
498 | 1 | ||
499 | /** |
||
500 | 1 | * @param array $functions |
|
501 | 1 | */ |
|
502 | public function setFunctions(array $functions): void |
||
503 | { |
||
504 | $this->functions = $functions; |
||
505 | } |
||
506 | } |
||
507 |