Completed
Push — master ( 8cc02b...5173bc )
by Christian
02:41
created

Parser::parsePrimaryExpression()   C

Complexity

Conditions 17
Paths 17

Size

Total Lines 65
Code Lines 42

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 32
CRAP Score 18.1377

Importance

Changes 0
Metric Value
dl 0
loc 65
ccs 32
cts 38
cp 0.8421
rs 5.9044
c 0
b 0
f 0
cc 17
eloc 42
nc 17
nop 0
crap 18.1377

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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()) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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.

Loading history...
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);
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...
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) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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.

Loading history...
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) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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.

Loading history...
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 {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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.

Loading history...
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