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