Parser   F
last analyzed

Complexity

Total Complexity 68

Size/Duplication

Total Lines 486
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 15

Test Coverage

Coverage 80.08%

Importance

Changes 0
Metric Value
wmc 68
lcom 1
cbo 15
dl 0
loc 486
ccs 197
cts 246
cp 0.8008
rs 2.96
c 0
b 0
f 0

12 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 44 1
A parse() 0 22 2
C preParseArrowFuncs() 0 79 9
B parseExpression() 0 20 6
A getPrimary() 0 22 4
A parseConditionalExpression() 0 23 4
C parsePrimaryExpression() 0 65 17
A parseArrayExpression() 0 23 4
B parseHashExpression() 0 43 8
B parsePostfixExpression() 0 56 9
A parseArguments() 0 15 3
A setFunctions() 0 4 1

How to fix   Complexity   

Complex Class

Complex classes like Parser often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Parser, and based on these observations, apply Extract Interface, too.

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
Consider using a different property name as you override a private property of the parent class.
Loading history...
30
31
    /** @var array */
32
    private $unaryOperators;
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
33
34
    /** @var array */
35
    private $binaryOperators;
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
36
37
    /** @var array */
38
    private $functions;
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
39
40
    /** @var string[] */
41
    private $names;
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
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
Compatibility introduced by
$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.

Loading history...
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
Unused Code introduced by
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 getEmail() method, we can see that it has no side-effect. Whether you call this method or not, no future calls to other methods are affected by this. As such code as the following is useless:

$user = new User();
$user->getEmail(); // This line could safely be removed as it has no effect.

On the hand, if we look at the setEmail(), this method _has_ side-effects. In the following case, we could not remove the method call:

$user = new User();
$user->setEmail('email@domain'); // This line has a side-effect (it changes an
                                 // instance variable).
Loading history...
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
Bug introduced by
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

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
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