Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
100.00% |
341 / 341 |
|
100.00% |
27 / 27 |
CRAP | |
100.00% |
1 / 1 |
AFPTreeParser | |
100.00% |
341 / 341 |
|
100.00% |
27 / 27 |
112 | |
100.00% |
1 / 1 |
__construct | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
setFilter | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
resetState | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
move | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getNextToken | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getState | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setState | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
parse | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
buildSyntaxTree | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
doLevelEntry | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
2 | |||
doLevelSemicolon | |
100.00% |
16 / 16 |
|
100.00% |
1 / 1 |
7 | |||
doLevelSet | |
100.00% |
34 / 34 |
|
100.00% |
1 / 1 |
13 | |||
doLevelConditions | |
100.00% |
56 / 56 |
|
100.00% |
1 / 1 |
13 | |||
doLevelBoolOps | |
100.00% |
13 / 13 |
|
100.00% |
1 / 1 |
3 | |||
doLevelCompares | |
100.00% |
18 / 18 |
|
100.00% |
1 / 1 |
4 | |||
doLevelSumRels | |
100.00% |
13 / 13 |
|
100.00% |
1 / 1 |
3 | |||
doLevelMulRels | |
100.00% |
13 / 13 |
|
100.00% |
1 / 1 |
3 | |||
doLevelPow | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
3 | |||
doLevelBoolInvert | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
3 | |||
doLevelKeywordOperators | |
100.00% |
13 / 13 |
|
100.00% |
1 / 1 |
3 | |||
doLevelUnarys | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
4 | |||
doLevelArrayElements | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
5 | |||
doLevelParenthesis | |
100.00% |
21 / 21 |
|
100.00% |
1 / 1 |
7 | |||
doLevelFunction | |
100.00% |
37 / 37 |
|
100.00% |
1 / 1 |
10 | |||
doLevelAtom | |
100.00% |
46 / 46 |
|
100.00% |
1 / 1 |
16 | |||
checkLogDeprecatedVar | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
functionIsVariadic | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 |
1 | <?php |
2 | |
3 | /** |
4 | * A version of the abuse filter parser that separates parsing the filter and |
5 | * evaluating it into different passes, allowing the parse tree to be cached. |
6 | * |
7 | * @file |
8 | * @phan-file-suppress PhanPossiblyInfiniteRecursionSameParams Recursion controlled by class props |
9 | */ |
10 | |
11 | namespace MediaWiki\Extension\AbuseFilter\Parser; |
12 | |
13 | use IBufferingStatsdDataFactory; |
14 | use InvalidArgumentException; |
15 | use MediaWiki\Extension\AbuseFilter\KeywordsManager; |
16 | use MediaWiki\Extension\AbuseFilter\Parser\Exception\UserVisibleException; |
17 | use Psr\Log\LoggerInterface; |
18 | |
19 | /** |
20 | * A parser that transforms the text of the filter into a parse tree. |
21 | */ |
22 | class AFPTreeParser { |
23 | /** |
24 | * @var array[] Contains the AFPTokens for the code being parsed |
25 | * @phan-var array<int,array{0:AFPToken,1:int}> |
26 | */ |
27 | private $mTokens; |
28 | /** |
29 | * @var AFPToken The current token |
30 | */ |
31 | private $mCur; |
32 | /** @var int The position of the current token */ |
33 | private $mPos; |
34 | |
35 | /** |
36 | * @var string|null The ID of the filter being parsed, if available. Can also be "global-$ID" |
37 | */ |
38 | private $mFilter; |
39 | |
40 | public const CACHE_VERSION = 2; |
41 | |
42 | /** |
43 | * @var LoggerInterface Used for debugging |
44 | */ |
45 | private $logger; |
46 | |
47 | /** |
48 | * @var IBufferingStatsdDataFactory |
49 | */ |
50 | private $statsd; |
51 | |
52 | /** @var KeywordsManager */ |
53 | private $keywordsManager; |
54 | |
55 | /** |
56 | * @param LoggerInterface $logger Used for debugging |
57 | * @param IBufferingStatsdDataFactory $statsd |
58 | * @param KeywordsManager $keywordsManager |
59 | */ |
60 | public function __construct( |
61 | LoggerInterface $logger, |
62 | IBufferingStatsdDataFactory $statsd, |
63 | KeywordsManager $keywordsManager |
64 | ) { |
65 | $this->logger = $logger; |
66 | $this->statsd = $statsd; |
67 | $this->keywordsManager = $keywordsManager; |
68 | $this->resetState(); |
69 | } |
70 | |
71 | /** |
72 | * @param string $filter |
73 | */ |
74 | public function setFilter( $filter ) { |
75 | $this->mFilter = $filter; |
76 | } |
77 | |
78 | /** |
79 | * Resets the state |
80 | */ |
81 | private function resetState() { |
82 | $this->mTokens = []; |
83 | $this->mPos = 0; |
84 | $this->mFilter = null; |
85 | } |
86 | |
87 | /** |
88 | * Advances the parser to the next token in the filter code. |
89 | */ |
90 | private function move() { |
91 | [ $this->mCur, $this->mPos ] = $this->mTokens[$this->mPos]; |
92 | } |
93 | |
94 | /** |
95 | * Get the next token. This is similar to move() but doesn't change class members, |
96 | * allowing to look ahead without rolling back the state. |
97 | * |
98 | * @return AFPToken |
99 | */ |
100 | private function getNextToken() { |
101 | return $this->mTokens[$this->mPos][0]; |
102 | } |
103 | |
104 | /** |
105 | * getState() function allows parser state to be rollbacked to several tokens |
106 | * back. |
107 | * |
108 | * @return AFPParserState |
109 | */ |
110 | private function getState() { |
111 | return new AFPParserState( $this->mCur, $this->mPos ); |
112 | } |
113 | |
114 | /** |
115 | * setState() function allows parser state to be rollbacked to several tokens |
116 | * back. |
117 | * |
118 | * @param AFPParserState $state |
119 | */ |
120 | private function setState( AFPParserState $state ) { |
121 | $this->mCur = $state->token; |
122 | $this->mPos = $state->pos; |
123 | } |
124 | |
125 | /** |
126 | * Parse the supplied filter source code into a tree. |
127 | * |
128 | * @param array[] $tokens |
129 | * @phan-param array<int,array{0:AFPToken,1:int}> $tokens |
130 | * @return AFPSyntaxTree |
131 | * @throws UserVisibleException |
132 | */ |
133 | public function parse( array $tokens ): AFPSyntaxTree { |
134 | $this->mTokens = $tokens; |
135 | $this->mPos = 0; |
136 | |
137 | return $this->buildSyntaxTree(); |
138 | } |
139 | |
140 | /** |
141 | * @return AFPSyntaxTree |
142 | */ |
143 | private function buildSyntaxTree(): AFPSyntaxTree { |
144 | $startTime = microtime( true ); |
145 | $root = $this->doLevelEntry(); |
146 | $this->statsd->timing( 'abusefilter_cachingParser_buildtree', microtime( true ) - $startTime ); |
147 | return new AFPSyntaxTree( $root ); |
148 | } |
149 | |
150 | /* Levels */ |
151 | |
152 | /** |
153 | * Handles unexpected characters after the expression. |
154 | * @return AFPTreeNode|null Null only if no statements |
155 | * @throws UserVisibleException |
156 | */ |
157 | private function doLevelEntry() { |
158 | $result = $this->doLevelSemicolon(); |
159 | |
160 | if ( $this->mCur->type !== AFPToken::TNONE ) { |
161 | throw new UserVisibleException( |
162 | 'unexpectedatend', |
163 | $this->mPos, [ $this->mCur->type ] |
164 | ); |
165 | } |
166 | |
167 | return $result; |
168 | } |
169 | |
170 | /** |
171 | * Handles the semicolon operator. |
172 | * |
173 | * @return AFPTreeNode|null |
174 | */ |
175 | private function doLevelSemicolon() { |
176 | $statements = []; |
177 | |
178 | do { |
179 | $this->move(); |
180 | $position = $this->mPos; |
181 | |
182 | if ( |
183 | $this->mCur->type === AFPToken::TNONE || |
184 | ( $this->mCur->type === AFPToken::TBRACE && $this->mCur->value == ')' ) |
185 | ) { |
186 | // Handle special cases which the other parser handled in doLevelAtom |
187 | break; |
188 | } |
189 | |
190 | // Allow empty statements. |
191 | if ( $this->mCur->type === AFPToken::TSTATEMENTSEPARATOR ) { |
192 | continue; |
193 | } |
194 | |
195 | $statements[] = $this->doLevelSet(); |
196 | $position = $this->mPos; |
197 | } while ( $this->mCur->type === AFPToken::TSTATEMENTSEPARATOR ); |
198 | |
199 | // Flatten the tree if possible. |
200 | if ( count( $statements ) === 0 ) { |
201 | return null; |
202 | } elseif ( count( $statements ) === 1 ) { |
203 | return $statements[0]; |
204 | } else { |
205 | return new AFPTreeNode( AFPTreeNode::SEMICOLON, $statements, $position ); |
206 | } |
207 | } |
208 | |
209 | /** |
210 | * Handles variable assignment. |
211 | * |
212 | * @return AFPTreeNode |
213 | * @throws UserVisibleException |
214 | */ |
215 | private function doLevelSet() { |
216 | if ( $this->mCur->type === AFPToken::TID ) { |
217 | $varname = (string)$this->mCur->value; |
218 | |
219 | // Speculatively parse the assignment statement assuming it can |
220 | // potentially be an assignment, but roll back if it isn't. |
221 | // @todo Use $this->getNextToken for clearer code |
222 | $initialState = $this->getState(); |
223 | $this->move(); |
224 | |
225 | if ( $this->mCur->type === AFPToken::TOP && $this->mCur->value === ':=' ) { |
226 | $position = $this->mPos; |
227 | $this->move(); |
228 | $value = $this->doLevelSet(); |
229 | |
230 | return new AFPTreeNode( AFPTreeNode::ASSIGNMENT, [ $varname, $value ], $position ); |
231 | } |
232 | |
233 | if ( $this->mCur->type === AFPToken::TSQUAREBRACKET && $this->mCur->value === '[' ) { |
234 | $this->move(); |
235 | |
236 | if ( $this->mCur->type === AFPToken::TSQUAREBRACKET && $this->mCur->value === ']' ) { |
237 | $index = 'append'; |
238 | } else { |
239 | // Parse index offset. |
240 | $this->setState( $initialState ); |
241 | $this->move(); |
242 | $index = $this->doLevelSemicolon(); |
243 | if ( !( $this->mCur->type === AFPToken::TSQUAREBRACKET && $this->mCur->value === ']' ) ) { |
244 | throw new UserVisibleException( 'expectednotfound', $this->mPos, |
245 | [ ']', $this->mCur->type, $this->mCur->value ] ); |
246 | } |
247 | } |
248 | |
249 | $this->move(); |
250 | if ( $this->mCur->type === AFPToken::TOP && $this->mCur->value === ':=' ) { |
251 | $position = $this->mPos; |
252 | $this->move(); |
253 | $value = $this->doLevelSet(); |
254 | if ( $index === 'append' ) { |
255 | return new AFPTreeNode( |
256 | AFPTreeNode::ARRAY_APPEND, [ $varname, $value ], $position ); |
257 | } else { |
258 | return new AFPTreeNode( |
259 | AFPTreeNode::INDEX_ASSIGNMENT, |
260 | [ $varname, $index, $value ], |
261 | $position |
262 | ); |
263 | } |
264 | } |
265 | } |
266 | |
267 | // If we reached this point, we did not find an assignment. Roll back |
268 | // and assume this was just a literal. |
269 | $this->setState( $initialState ); |
270 | } |
271 | |
272 | return $this->doLevelConditions(); |
273 | } |
274 | |
275 | /** |
276 | * Handles ternary operator and if-then-else-end. |
277 | * |
278 | * @return AFPTreeNode |
279 | * @throws UserVisibleException |
280 | */ |
281 | private function doLevelConditions() { |
282 | if ( $this->mCur->type === AFPToken::TKEYWORD && $this->mCur->value === 'if' ) { |
283 | $position = $this->mPos; |
284 | $this->move(); |
285 | $condition = $this->doLevelBoolOps(); |
286 | |
287 | if ( !( $this->mCur->type === AFPToken::TKEYWORD && $this->mCur->value === 'then' ) ) { |
288 | throw new UserVisibleException( 'expectednotfound', |
289 | $this->mPos, |
290 | [ |
291 | 'then', |
292 | $this->mCur->type, |
293 | $this->mCur->value |
294 | ] |
295 | ); |
296 | } |
297 | $this->move(); |
298 | |
299 | $valueIfTrue = $this->doLevelConditions(); |
300 | |
301 | if ( $this->mCur->type === AFPToken::TKEYWORD && $this->mCur->value === 'else' ) { |
302 | $this->move(); |
303 | $valueIfFalse = $this->doLevelConditions(); |
304 | } else { |
305 | $valueIfFalse = null; |
306 | } |
307 | |
308 | if ( !( $this->mCur->type === AFPToken::TKEYWORD && $this->mCur->value === 'end' ) ) { |
309 | throw new UserVisibleException( 'expectednotfound', |
310 | $this->mPos, |
311 | [ |
312 | 'end', |
313 | $this->mCur->type, |
314 | $this->mCur->value |
315 | ] |
316 | ); |
317 | } |
318 | $this->move(); |
319 | |
320 | return new AFPTreeNode( |
321 | AFPTreeNode::CONDITIONAL, |
322 | [ $condition, $valueIfTrue, $valueIfFalse ], |
323 | $position |
324 | ); |
325 | } |
326 | |
327 | $condition = $this->doLevelBoolOps(); |
328 | if ( $this->mCur->type === AFPToken::TOP && $this->mCur->value === '?' ) { |
329 | $position = $this->mPos; |
330 | $this->move(); |
331 | |
332 | $valueIfTrue = $this->doLevelConditions(); |
333 | if ( !( $this->mCur->type === AFPToken::TOP && $this->mCur->value === ':' ) ) { |
334 | throw new UserVisibleException( 'expectednotfound', |
335 | $this->mPos, |
336 | [ |
337 | ':', |
338 | $this->mCur->type, |
339 | $this->mCur->value |
340 | ] |
341 | ); |
342 | } |
343 | $this->move(); |
344 | |
345 | $valueIfFalse = $this->doLevelConditions(); |
346 | return new AFPTreeNode( |
347 | AFPTreeNode::CONDITIONAL, |
348 | [ $condition, $valueIfTrue, $valueIfFalse ], |
349 | $position |
350 | ); |
351 | } |
352 | |
353 | return $condition; |
354 | } |
355 | |
356 | /** |
357 | * Handles logic operators. |
358 | * |
359 | * @return AFPTreeNode |
360 | */ |
361 | private function doLevelBoolOps() { |
362 | $leftOperand = $this->doLevelCompares(); |
363 | $ops = [ '&', '|', '^' ]; |
364 | while ( $this->mCur->type === AFPToken::TOP && in_array( $this->mCur->value, $ops ) ) { |
365 | $op = $this->mCur->value; |
366 | $position = $this->mPos; |
367 | $this->move(); |
368 | |
369 | $rightOperand = $this->doLevelCompares(); |
370 | |
371 | $leftOperand = new AFPTreeNode( |
372 | AFPTreeNode::LOGIC, |
373 | [ $op, $leftOperand, $rightOperand ], |
374 | $position |
375 | ); |
376 | } |
377 | return $leftOperand; |
378 | } |
379 | |
380 | /** |
381 | * Handles comparison operators. |
382 | * |
383 | * @return AFPTreeNode |
384 | */ |
385 | private function doLevelCompares() { |
386 | $leftOperand = $this->doLevelSumRels(); |
387 | $equalityOps = [ '==', '===', '!=', '!==', '=' ]; |
388 | $orderOps = [ '<', '>', '<=', '>=' ]; |
389 | // Only allow either a single operation, or a combination of a single equalityOps and a single |
390 | // orderOps. This resembles what PHP does, and allows `a < b == c` while rejecting `a < b < c` |
391 | $allowedOps = array_merge( $equalityOps, $orderOps ); |
392 | while ( $this->mCur->type === AFPToken::TOP && in_array( $this->mCur->value, $allowedOps ) ) { |
393 | $op = $this->mCur->value; |
394 | $allowedOps = in_array( $op, $equalityOps ) ? |
395 | array_diff( $allowedOps, $equalityOps ) : |
396 | array_diff( $allowedOps, $orderOps ); |
397 | $position = $this->mPos; |
398 | $this->move(); |
399 | $rightOperand = $this->doLevelSumRels(); |
400 | $leftOperand = new AFPTreeNode( |
401 | AFPTreeNode::COMPARE, |
402 | [ $op, $leftOperand, $rightOperand ], |
403 | $position |
404 | ); |
405 | } |
406 | return $leftOperand; |
407 | } |
408 | |
409 | /** |
410 | * Handle addition and subtraction. |
411 | * |
412 | * @return AFPTreeNode |
413 | */ |
414 | private function doLevelSumRels() { |
415 | $leftOperand = $this->doLevelMulRels(); |
416 | $ops = [ '+', '-' ]; |
417 | while ( $this->mCur->type === AFPToken::TOP && in_array( $this->mCur->value, $ops ) ) { |
418 | $op = $this->mCur->value; |
419 | $position = $this->mPos; |
420 | $this->move(); |
421 | $rightOperand = $this->doLevelMulRels(); |
422 | $leftOperand = new AFPTreeNode( |
423 | AFPTreeNode::SUM_REL, |
424 | [ $op, $leftOperand, $rightOperand ], |
425 | $position |
426 | ); |
427 | } |
428 | return $leftOperand; |
429 | } |
430 | |
431 | /** |
432 | * Handles multiplication and division. |
433 | * |
434 | * @return AFPTreeNode |
435 | */ |
436 | private function doLevelMulRels() { |
437 | $leftOperand = $this->doLevelPow(); |
438 | $ops = [ '*', '/', '%' ]; |
439 | while ( $this->mCur->type === AFPToken::TOP && in_array( $this->mCur->value, $ops ) ) { |
440 | $op = $this->mCur->value; |
441 | $position = $this->mPos; |
442 | $this->move(); |
443 | $rightOperand = $this->doLevelPow(); |
444 | $leftOperand = new AFPTreeNode( |
445 | AFPTreeNode::MUL_REL, |
446 | [ $op, $leftOperand, $rightOperand ], |
447 | $position |
448 | ); |
449 | } |
450 | return $leftOperand; |
451 | } |
452 | |
453 | /** |
454 | * Handles exponentiation. |
455 | * |
456 | * @return AFPTreeNode |
457 | */ |
458 | private function doLevelPow() { |
459 | $base = $this->doLevelBoolInvert(); |
460 | while ( $this->mCur->type === AFPToken::TOP && $this->mCur->value === '**' ) { |
461 | $position = $this->mPos; |
462 | $this->move(); |
463 | $exponent = $this->doLevelBoolInvert(); |
464 | $base = new AFPTreeNode( AFPTreeNode::POW, [ $base, $exponent ], $position ); |
465 | } |
466 | return $base; |
467 | } |
468 | |
469 | /** |
470 | * Handles boolean inversion. |
471 | * |
472 | * @return AFPTreeNode |
473 | */ |
474 | private function doLevelBoolInvert() { |
475 | if ( $this->mCur->type === AFPToken::TOP && $this->mCur->value === '!' ) { |
476 | $position = $this->mPos; |
477 | $this->move(); |
478 | $argument = $this->doLevelKeywordOperators(); |
479 | return new AFPTreeNode( AFPTreeNode::BOOL_INVERT, [ $argument ], $position ); |
480 | } |
481 | |
482 | return $this->doLevelKeywordOperators(); |
483 | } |
484 | |
485 | /** |
486 | * Handles keyword operators. |
487 | * |
488 | * @return AFPTreeNode |
489 | */ |
490 | private function doLevelKeywordOperators() { |
491 | $leftOperand = $this->doLevelUnarys(); |
492 | $keyword = strtolower( $this->mCur->value ); |
493 | if ( $this->mCur->type === AFPToken::TKEYWORD && |
494 | isset( FilterEvaluator::KEYWORDS[$keyword] ) |
495 | ) { |
496 | $position = $this->mPos; |
497 | $this->move(); |
498 | $rightOperand = $this->doLevelUnarys(); |
499 | |
500 | return new AFPTreeNode( |
501 | AFPTreeNode::KEYWORD_OPERATOR, |
502 | [ $keyword, $leftOperand, $rightOperand ], |
503 | $position |
504 | ); |
505 | } |
506 | |
507 | return $leftOperand; |
508 | } |
509 | |
510 | /** |
511 | * Handles unary operators. |
512 | * |
513 | * @return AFPTreeNode |
514 | */ |
515 | private function doLevelUnarys() { |
516 | $op = $this->mCur->value; |
517 | if ( $this->mCur->type === AFPToken::TOP && ( $op === "+" || $op === "-" ) ) { |
518 | $position = $this->mPos; |
519 | $this->move(); |
520 | $argument = $this->doLevelArrayElements(); |
521 | return new AFPTreeNode( AFPTreeNode::UNARY, [ $op, $argument ], $position ); |
522 | } |
523 | return $this->doLevelArrayElements(); |
524 | } |
525 | |
526 | /** |
527 | * Handles accessing an array element by an offset. |
528 | * |
529 | * @return AFPTreeNode |
530 | * @throws UserVisibleException |
531 | */ |
532 | private function doLevelArrayElements() { |
533 | $array = $this->doLevelParenthesis(); |
534 | while ( $this->mCur->type === AFPToken::TSQUAREBRACKET && $this->mCur->value === '[' ) { |
535 | $position = $this->mPos; |
536 | $index = $this->doLevelSemicolon(); |
537 | $array = new AFPTreeNode( AFPTreeNode::ARRAY_INDEX, [ $array, $index ], $position ); |
538 | |
539 | if ( !( $this->mCur->type === AFPToken::TSQUAREBRACKET && $this->mCur->value === ']' ) ) { |
540 | throw new UserVisibleException( 'expectednotfound', $this->mPos, |
541 | [ ']', $this->mCur->type, $this->mCur->value ] ); |
542 | } |
543 | $this->move(); |
544 | } |
545 | |
546 | return $array; |
547 | } |
548 | |
549 | /** |
550 | * Handles parenthesis. |
551 | * |
552 | * @return AFPTreeNode |
553 | * @throws UserVisibleException |
554 | */ |
555 | private function doLevelParenthesis() { |
556 | if ( $this->mCur->type === AFPToken::TBRACE && $this->mCur->value === '(' ) { |
557 | $next = $this->getNextToken(); |
558 | if ( $next->type === AFPToken::TBRACE && $next->value === ')' ) { |
559 | // Empty parentheses are never allowed |
560 | throw new UserVisibleException( |
561 | 'unexpectedtoken', |
562 | $this->mPos, |
563 | [ |
564 | $this->mCur->type, |
565 | $this->mCur->value |
566 | ] |
567 | ); |
568 | } |
569 | $result = $this->doLevelSemicolon(); |
570 | |
571 | if ( !( $this->mCur->type === AFPToken::TBRACE && $this->mCur->value === ')' ) ) { |
572 | throw new UserVisibleException( |
573 | 'expectednotfound', |
574 | $this->mPos, |
575 | [ ')', $this->mCur->type, $this->mCur->value ] |
576 | ); |
577 | } |
578 | $this->move(); |
579 | |
580 | return $result; |
581 | } |
582 | |
583 | return $this->doLevelFunction(); |
584 | } |
585 | |
586 | /** |
587 | * Handles function calls. |
588 | * |
589 | * @return AFPTreeNode |
590 | * @throws UserVisibleException |
591 | */ |
592 | private function doLevelFunction() { |
593 | $next = $this->getNextToken(); |
594 | if ( $this->mCur->type === AFPToken::TID && |
595 | $next->type === AFPToken::TBRACE && |
596 | $next->value === '(' |
597 | ) { |
598 | $func = $this->mCur->value; |
599 | $position = $this->mPos; |
600 | $this->move(); |
601 | |
602 | $args = []; |
603 | $next = $this->getNextToken(); |
604 | if ( $next->type !== AFPToken::TBRACE || $next->value !== ')' ) { |
605 | do { |
606 | $thisArg = $this->doLevelSemicolon(); |
607 | if ( $thisArg !== null ) { |
608 | $args[] = $thisArg; |
609 | } elseif ( !$this->functionIsVariadic( $func ) ) { |
610 | throw new UserVisibleException( |
611 | 'unexpectedtoken', |
612 | $this->mPos, |
613 | [ |
614 | $this->mCur->type, |
615 | $this->mCur->value |
616 | ] |
617 | ); |
618 | } |
619 | } while ( $this->mCur->type === AFPToken::TCOMMA ); |
620 | } else { |
621 | $this->move(); |
622 | } |
623 | |
624 | if ( $this->mCur->type !== AFPToken::TBRACE || $this->mCur->value !== ')' ) { |
625 | throw new UserVisibleException( 'expectednotfound', |
626 | $this->mPos, |
627 | [ |
628 | ')', |
629 | $this->mCur->type, |
630 | $this->mCur->value |
631 | ] |
632 | ); |
633 | } |
634 | $this->move(); |
635 | |
636 | array_unshift( $args, $func ); |
637 | return new AFPTreeNode( AFPTreeNode::FUNCTION_CALL, $args, $position ); |
638 | } |
639 | |
640 | return $this->doLevelAtom(); |
641 | } |
642 | |
643 | /** |
644 | * Handle literals. |
645 | * @return AFPTreeNode |
646 | * @throws UserVisibleException |
647 | */ |
648 | private function doLevelAtom() { |
649 | $tok = $this->mCur->value; |
650 | switch ( $this->mCur->type ) { |
651 | case AFPToken::TID: |
652 | $this->checkLogDeprecatedVar( strtolower( $tok ) ); |
653 | // Fallthrough intended |
654 | case AFPToken::TSTRING: |
655 | case AFPToken::TFLOAT: |
656 | case AFPToken::TINT: |
657 | $result = new AFPTreeNode( AFPTreeNode::ATOM, $this->mCur, $this->mPos ); |
658 | break; |
659 | case AFPToken::TKEYWORD: |
660 | if ( in_array( $tok, [ "true", "false", "null" ] ) ) { |
661 | $result = new AFPTreeNode( AFPTreeNode::ATOM, $this->mCur, $this->mPos ); |
662 | break; |
663 | } |
664 | |
665 | throw new UserVisibleException( |
666 | 'unrecognisedkeyword', |
667 | $this->mPos, |
668 | [ $tok ] |
669 | ); |
670 | /** @noinspection PhpMissingBreakStatementInspection */ |
671 | case AFPToken::TSQUAREBRACKET: |
672 | if ( $this->mCur->value === '[' ) { |
673 | $array = []; |
674 | while ( true ) { |
675 | $this->move(); |
676 | if ( $this->mCur->type === AFPToken::TSQUAREBRACKET && $this->mCur->value === ']' ) { |
677 | break; |
678 | } |
679 | |
680 | $array[] = $this->doLevelSet(); |
681 | |
682 | if ( $this->mCur->type === AFPToken::TSQUAREBRACKET && $this->mCur->value === ']' ) { |
683 | break; |
684 | } |
685 | if ( $this->mCur->type !== AFPToken::TCOMMA ) { |
686 | throw new UserVisibleException( |
687 | 'expectednotfound', |
688 | $this->mPos, |
689 | [ ', or ]', $this->mCur->type, $this->mCur->value ] |
690 | ); |
691 | } |
692 | } |
693 | |
694 | $result = new AFPTreeNode( AFPTreeNode::ARRAY_DEFINITION, $array, $this->mPos ); |
695 | break; |
696 | } |
697 | |
698 | // Fallthrough expected |
699 | default: |
700 | throw new UserVisibleException( |
701 | 'unexpectedtoken', |
702 | $this->mPos, |
703 | [ |
704 | $this->mCur->type, |
705 | $this->mCur->value |
706 | ] |
707 | ); |
708 | } |
709 | |
710 | $this->move(); |
711 | // @phan-suppress-next-next-line PhanPossiblyUndeclaredVariable |
712 | // @phan-suppress-next-line PhanTypeMismatchReturnNullable Until phan can understand the switch |
713 | return $result; |
714 | } |
715 | |
716 | /** |
717 | * Given a variable name, check if the variable is deprecated. If it is, log the use. |
718 | * Do that here, and not every time the AST is eval'ed. This means less logging, but more |
719 | * performance. |
720 | * @param string $varname |
721 | */ |
722 | private function checkLogDeprecatedVar( $varname ) { |
723 | if ( $this->keywordsManager->isVarDeprecated( $varname ) ) { |
724 | $this->logger->debug( "Deprecated variable $varname used in filter {$this->mFilter}." ); |
725 | } |
726 | } |
727 | |
728 | /** |
729 | * @param string $fname |
730 | * @return bool |
731 | */ |
732 | private function functionIsVariadic( string $fname ): bool { |
733 | if ( !array_key_exists( $fname, FilterEvaluator::FUNC_ARG_COUNT ) ) { |
734 | // @codeCoverageIgnoreStart |
735 | throw new InvalidArgumentException( "Function $fname is not valid" ); |
736 | // @codeCoverageIgnoreEnd |
737 | } |
738 | return FilterEvaluator::FUNC_ARG_COUNT[$fname][1] === INF; |
739 | } |
740 | } |