Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
341 / 341
100.00% covered (success)
100.00%
27 / 27
CRAP
100.00% covered (success)
100.00%
1 / 1
AFPTreeParser
100.00% covered (success)
100.00%
341 / 341
100.00% covered (success)
100.00%
27 / 27
112
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 setFilter
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 resetState
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 move
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getNextToken
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getState
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setState
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 parse
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 buildSyntaxTree
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 doLevelEntry
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
 doLevelSemicolon
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
7
 doLevelSet
100.00% covered (success)
100.00%
34 / 34
100.00% covered (success)
100.00%
1 / 1
13
 doLevelConditions
100.00% covered (success)
100.00%
56 / 56
100.00% covered (success)
100.00%
1 / 1
13
 doLevelBoolOps
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
3
 doLevelCompares
100.00% covered (success)
100.00%
18 / 18
100.00% covered (success)
100.00%
1 / 1
4
 doLevelSumRels
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
3
 doLevelMulRels
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
3
 doLevelPow
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 doLevelBoolInvert
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 doLevelKeywordOperators
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
3
 doLevelUnarys
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
4
 doLevelArrayElements
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
5
 doLevelParenthesis
100.00% covered (success)
100.00%
21 / 21
100.00% covered (success)
100.00%
1 / 1
7
 doLevelFunction
100.00% covered (success)
100.00%
37 / 37
100.00% covered (success)
100.00%
1 / 1
10
 doLevelAtom
100.00% covered (success)
100.00%
46 / 46
100.00% covered (success)
100.00%
1 / 1
16
 checkLogDeprecatedVar
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 functionIsVariadic
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
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
11namespace MediaWiki\Extension\AbuseFilter\Parser;
12
13use IBufferingStatsdDataFactory;
14use InvalidArgumentException;
15use MediaWiki\Extension\AbuseFilter\KeywordsManager;
16use MediaWiki\Extension\AbuseFilter\Parser\Exception\UserVisibleException;
17use Psr\Log\LoggerInterface;
18
19/**
20 * A parser that transforms the text of the filter into a parse tree.
21 */
22class 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}