Passed
Branch master (ef264b)
by Pierrick
01:53
created

Parser   F

Complexity

Total Complexity 135

Size/Duplication

Total Lines 664
Duplicated Lines 0 %

Test Coverage

Coverage 96.05%

Importance

Changes 21
Bugs 1 Features 1
Metric Value
eloc 366
c 21
b 1
f 1
dl 0
loc 664
ccs 365
cts 380
cp 0.9605
rs 2
wmc 135

33 Methods

Rating   Name   Duplication   Size   Complexity  
A getAstFromString() 0 25 5
C doInclude() 0 37 12
A parseString() 0 22 5
A parseMathExpr() 0 9 2
A consume() 0 7 2
A parseObject() 0 7 1
A parse() 0 5 2
A __construct() 0 3 2
A glob() 0 4 2
B parseMathFactor() 0 34 7
A consumeOptionalSeparator() 0 9 3
A resolvePath() 0 4 1
A parseOrOperand() 0 9 2
A consumeOptional() 0 9 2
A parseAndOperand() 0 21 4
A parseShiftOperand() 0 21 4
C parseValue() 0 49 17
A consumeOptionalAssignementOperator() 0 9 3
A getVariable() 0 15 4
A parseScalar() 0 6 1
A relativeToCurrentFile() 0 10 2
A nextToken() 0 3 1
A setVariable() 0 3 1
B parseMacro() 0 40 7
A error() 0 3 1
C parseInnerObject() 0 48 14
A registerMacro() 0 7 2
B parseMathTerm() 0 30 6
A parseArray() 0 14 3
B parseRootValue() 0 34 7
A syntaxError() 0 10 3
A parseFile() 0 12 4
A doIncludeFileContent() 0 12 3

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.

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
 * This file is part of NACL.
4
 *
5
 * For the full copyright and license information, please view the LICENSE
6
 * file that was distributed with this source code.
7
 *
8
 * @copyright 2019 Nuglif (2018) Inc.
9
 * @license   http://www.opensource.org/licenses/mit-license.html  MIT License
10
 * @author    Pierrick Charron <[email protected]>
11
 * @author    Charle Demers <[email protected]>
12
 */
13
14
declare(strict_types=1);
15
16
namespace Nuglif\Nacl;
17
18
class Parser
19
{
20
    private $lexer;
21
    private $token;
22
    private $macro     = [];
23
    private $variables = [];
24
25 590
    public function __construct(Lexer $lexer = null)
26
    {
27 590
        $this->lexer = $lexer ?: new Lexer();
28 590
    }
29
30 590
    public function registerMacro(MacroInterface $macro)
31
    {
32 590
        if (isset($this->macro[$macro->getName()])) {
33
            throw new Exception('Macro with the same name already registered.');
34
        }
35
36 590
        $this->macro[$macro->getName()] = [ $macro, 'execute' ];
37 590
    }
38
39 39
    public function setVariable($name, $value)
40
    {
41 39
        $this->variables[$name] = $value;
42 39
    }
43
44 589
    public function parse($str, $filename = 'nacl string')
45
    {
46 589
        $result = $this->getAstFromString($str, $filename);
47
48 581
        return $result instanceof Node ? $result->getNativeValue() : $result;
49
    }
50
51
    /**
52
     * Nacl ::= RootValue | InnerObject
53
     */
54 589
    private function getAstFromString($str, $filename)
55
    {
56 589
        $this->lexer->push($str, $filename);
57 589
        $this->nextToken();
58
59 588
        if ('[' == $this->token->type) {
60 72
            $result = $this->parseArray();
61 518
        } elseif ('{' == $this->token->type) {
62 120
            $result = $this->parseObject();
63
        } else {
64 398
            $result = $this->parseRootValue(false, $found);
65 394
            if (!$found) {
66 4
                $result = $this->parseInnerObject();
67
            } else {
68 393
                $this->consumeOptionalSeparator();
69 393
                if ($result instanceof ObjectNode) {
70 387
                    $result = $this->parseInnerObject($result);
71
                }
72
            }
73
        }
74
75 583
        $this->consume(Token::T_EOF);
76 581
        $this->lexer->pop();
77
78 581
        return $result;
79
    }
80
81 47
    public function parseFile($file)
82
    {
83 47
        $filename = realpath($file);
84 47
        if (!$filename) {
85 1
            throw new \InvalidArgumentException('File not found: ' . $file);
86 46
        } elseif (!is_file($filename)) {
87
            throw new \InvalidArgumentException($file . ' is not a file.');
88 46
        } elseif (!is_readable($filename)) {
89
            throw new \InvalidArgumentException($file . ' is not readable');
90
        }
91
92 46
        return $this->parse(file_get_contents($file), $filename);
93
    }
94
95
    /**
96
     * RootValue               ::= [ VariableAssignationList ] Value
97
     * VariableAssignationList ::= VariableAssignation [ Separator [ VariableAssignationList ] ]
98
     * VariableAssignation     ::= T_VAR OptionalAssignementOperator Value
99
     */
100 398
    private function parseRootValue($required = true, &$found = true)
101
    {
102 398
        $value = null;
103
104
        do {
105 398
            $found = true;
106 398
            $continue = false;
107 398
            switch ($this->token->type) {
108 398
                case Token::T_VAR:
109 5
                    $name = $this->token->value;
110 5
                    $this->nextToken();
111 5
                    $variableValueRequired = $this->consumeOptionalAssignementOperator();
112 5
                    if (!$variableValueRequired && $this->consumeOptionalSeparator()) {
113 1
                        $value = $this->getVariable($name);
114 1
                        break;
115
                    }
116 5
                    $variableValue = $this->parseValue($variableValueRequired, $variableValueFound);
117 5
                    if ($variableValueFound) {
118 5
                        $this->setVariable($name, $variableValue);
119 5
                        $continue = $this->consumeOptionalSeparator();
120 5
                        $found = false;
121 5
                        break;
122
                    }
123
                    $value = $this->getVariable($name);
124
                    break;
125
                default:
126 397
                    $value = $this->parseValue($required, $found);
127 393
                    if ($value instanceof MacroNode) {
128 3
                        $value = $value->execute();
129
                    }
130
            }
131 394
        } while ($continue);
132
133 394
        return $value;
134
    }
135
136
    /**
137
     * Object ::= "{" InnerObject "}"
138
     */
139 380
    private function parseObject()
140
    {
141 380
        $this->consume('{');
142 380
        $object = $this->parseInnerObject();
143 380
        $this->consume('}');
144
145 380
        return $object;
146
    }
147
148
    /**
149
     * InnerObject  ::= [ KeyValueList ]
150
     * KeyValueList ::= VariableAssignation|KeyValue [ Separator [ KeyValueList ] ]
151
     * KeyValue     ::= ( ( T_END_STR | T_NAME ) OptionalAssignementOperator Value ) | MacroCall
152
     */
153 508
    private function parseInnerObject(ObjectNode $object = null)
154
    {
155 508
        $object = $object ?: new ObjectNode();
156
        do {
157 508
            $name     = null;
158 508
            $continue = false;
159 508
            switch ($this->token->type) {
160 508
                case Token::T_END_STR:
161 82
                    $name = $this->parseString()->getNativeValue();
162
                    /* no break */
163 506
                case Token::T_NAME:
164 399
                    if (null === $name) {
165 382
                        $name = $this->token->value;
166 382
                        $this->nextToken();
167
                    }
168 399
                    $this->consumeOptionalAssignementOperator();
169 399
                    $val = $this->parseValue();
170 399
                    if ($val instanceof ObjectNode && isset($object[$name]) && $object[$name] instanceof ObjectNode) {
171 3
                        $object[$name] = $object[$name]->merge($val);
172
                    } else {
173 398
                        $object[$name] = $val;
174
                    }
175 399
                    $separator = $this->consumeOptionalSeparator();
176 399
                    $continue  = is_object($val) || $separator;
177 399
                    break;
178 488
                case Token::T_VAR:
179 5
                    $name = $this->token->value;
180 5
                    $this->nextToken();
181 5
                    $this->consumeOptionalAssignementOperator();
182 5
                    $val = $this->parseValue();
183 5
                    $this->setVariable($name, $val);
184 5
                    $continue = $this->consumeOptionalSeparator();
185 5
                    break;
186 488
                case '.':
187 11
                    $val = $this->parseMacro();
188 11
                    if ($val instanceof MacroNode) {
189 3
                        $val = $val->execute();
190
                    }
191 11
                    if (!$val instanceof ObjectNode) {
192 1
                        $this->error('Macro without assignation key must return an object');
193
                    }
194 10
                    $object   = $object->merge($val);
195 10
                    $continue = $this->consumeOptionalSeparator();
196 10
                    break;
197
            }
198 507
        } while ($continue);
199
200 507
        return $object;
201
    }
202
203
    /**
204
     * Array     ::= "[" [ ValueList ] "]"
205
     * ValueList ::= Value [ Separator [ ValueList ] ]
206
     */
207 343
    private function parseArray()
208
    {
209 343
        $array = new ArrayNode();
210 343
        $this->consume('[');
211
212 343
        $continue = true;
213 343
        while ($continue && ']' !== $this->token->type) {
214 341
            $array->add($this->parseValue());
215 341
            $continue = $this->consumeOptionalSeparator();
216
        }
217
218 343
        $this->consume(']');
219
220 343
        return $array;
221
    }
222
223
    /**
224
     * Value ::= {T_END_STR | T_NAME }* ( String | Scalar | MathExpr | Variable | Object | Array | MacroCall )
225
     */
226 588
    private function parseValue($required = true, &$found = true)
227
    {
228 588
        $value = null;
229 588
        $found = true;
230 588
        switch ($this->token->type) {
231 588
            case Token::T_STRING:
232 588
            case Token::T_ENCAPSED_VAR:
233 7
                $value = $this->parseString();
234 7
                break;
235 588
            case Token::T_END_STR:
236 588
            case Token::T_NAME:
237 583
                $value     = $this->parseScalar();
238 583
                $required  = $this->consumeOptionalAssignementOperator();
239 581
                $realValue = $this->parseValue($required, $valueIsKey);
240 581
                if ($valueIsKey) {
241 385
                    return new ObjectNode([ $value => $realValue ]);
242
                }
243 573
                break;
244 586
            case Token::T_BOOL:
245 583
            case Token::T_NULL:
246 144
                $value = $this->parseScalar();
247 144
                break;
248 582
            case Token::T_NUM:
249 578
            case Token::T_VAR:
250 578
            case '+':
251 577
            case '-':
252 576
            case '(':
253 149
                $value = $this->parseMathExpr();
254 148
                break;
255 576
            case '{':
256 345
                $value = $this->parseObject();
257 345
                break;
258 575
            case '[':
259 271
                $value = $this->parseArray();
260 271
                break;
261 575
            case '.':
262 21
                $value = $this->parseMacro();
263 20
                break;
264
            default:
265 574
                if ($required) {
266
                    $this->syntaxError();
267
                } else {
268 574
                    $found = false;
269
270 574
                    return;
271
                }
272
        }
273
274 584
        return $value;
275
    }
276
277
    /**
278
     * Scalar ::= T_END_STR | T_NAME | T_BOOL | T_NUM | T_NULL
279
     */
280 584
    private function parseScalar()
281
    {
282 584
        $value = $this->token->value;
283 584
        $this->nextToken();
284
285 584
        return $value;
286
    }
287
288
    /**
289
     * String ::= { T_ENCAPSED_VAR | T_STRING }* T_END_STR
290
     */
291 89
    private function parseString()
292
    {
293 89
        $value    = '';
294 89
        $continue = true;
295
296
        do {
297 89
            switch ($this->token->type) {
298 89
                case Token::T_ENCAPSED_VAR:
299 7
                    $value = new OperationNode($value, $this->getVariable($this->token->value), OperationNode::CONCAT);
300 7
                    break;
301 89
                case Token::T_END_STR:
302 89
                    $continue = false;
303
                    /* no break */
304 5
                case Token::T_STRING:
305 89
                    $value = new OperationNode($value, $this->token->value, OperationNode::CONCAT);
306 89
                    break;
307
            }
308
309 89
            $this->nextToken();
310 89
        } while ($continue);
311
312 89
        return $value;
313
    }
314
315
    /**
316
     * Variable ::= T_VAR
317
     */
318 11
    public function getVariable($name)
319
    {
320 11
        switch ($name) {
321 11
            case '__FILE__':
322
                return realpath($this->lexer->getFilename());
323 11
            case '__DIR__':
324
                return dirname(realpath($this->lexer->getFilename()));
325
            default:
326 11
                if (!isset($this->variables[$name])) {
327
                    trigger_error('Undefined variable ' . $name);
328
329
                    return '';
330
                }
331
332 11
                return $this->variables[$name];
333
        }
334
    }
335
336
    /**
337
     * MacroCall ::= "." T_NAME [ "(" InnerObject ")" ] Value
338
     */
339 26
    private function parseMacro()
340
    {
341 26
        $this->consume('.');
342 26
        $result = null;
343
344 26
        if (Token::T_NAME != $this->token->type) {
345
            $this->syntaxError();
346
        }
347
348 26
        $name = $this->token->value;
349 26
        $this->nextToken();
350
351 26
        if ($this->consumeOptional('(')) {
352 9
            $options = $this->parseInnerObject();
353 9
            $this->consume(')');
354
        } else {
355 22
            $options = new ObjectNode();
356
        }
357
358 26
        $param = $this->parseValue();
359
360 26
        switch ($name) {
361 26
            case 'include':
362 10
                $result = $this->doInclude($param, $options);
363 10
                break;
364 19
            case 'ref':
365 7
                $result = new ReferenceNode($param, $this->lexer->getFilename(), $this->lexer->getLine(), $options);
366 7
                break;
367 13
            case 'file':
368 4
                $result = $this->doIncludeFileContent($param, $options);
369 3
                break;
370
            default:
371 9
                if (!isset($this->macro[$name])) {
372
                    $this->error('Unknown macro \'' . $name . '\'');
373
                }
374 9
                $result = new MacroNode($this->macro[$name], $param, $options);
375 9
                break;
376
        }
377
378 25
        return $result;
379
    }
380
381 4
    private function doIncludeFileContent($fileName, $options)
382
    {
383 4
        if ($realpath = $this->resolvePath($fileName)) {
384 2
            return file_get_contents($realpath);
385
        }
386
387 2
        $options = $options->getNativeValue();
388 2
        if (array_key_exists('default', $options)) {
389 1
            return $options['default'];
390
        }
391
392 1
        $this->error("Unable to read file '${fileName}'");
393
    }
394
395 10
    private function doInclude($fileName, $options)
396
    {
397 10
        $includeValue = new ObjectNode();
398
399 10
        $fileName = $fileName instanceof Node ? $fileName->getNativeValue() : $fileName;
400 10
        if (isset($options['glob']) ? $options['glob'] : false) {
401 3
            $files = $this->glob($fileName);
402
        } else {
403 9
            if (!$path = $this->resolvePath($fileName)) {
404 1
                if (isset($options['required']) ? $options['required'] : true) {
405
                    $this->error('Unable to include file \'' . $fileName . '\'');
406
                }
407
408 1
                return $includeValue;
409
            }
410
411 9
            $files = [ $path ];
412
        }
413
414 10
        $token = $this->token;
415 10
        $filenameKey = isset($options['filenameKey']) && $options['filenameKey'];
416
417 10
        foreach ($files as $file) {
418 10
            $value = $this->getAstFromString(file_get_contents($file), $file);
419
420 10
            if ($filenameKey) {
421 1
                $includeValue[pathinfo($file, PATHINFO_FILENAME)] = $value;
422 9
            } elseif ($value instanceof ObjectNode && $includeValue instanceof ObjectNode) {
423 9
                $includeValue = $includeValue->merge($value);
424
            } else {
425 2
                $includeValue = $value;
426
            }
427
        }
428
429 10
        $this->token = $token;
430
431 10
        return $includeValue;
432
    }
433
434 11
    public function resolvePath($file)
435
    {
436
        return $this->relativeToCurrentFile(function () use ($file) {
437 11
            return realpath($file);
438 11
        });
439
    }
440
441 3
    private function glob($pattern)
442
    {
443
        return $this->relativeToCurrentFile(function () use ($pattern) {
444 3
            return array_map('realpath', glob($pattern) ?: []);
445 3
        });
446
    }
447
448 12
    private function relativeToCurrentFile(callable $cb)
449
    {
450 12
        $cwd = getcwd();
451 12
        if (file_exists($this->lexer->getFilename())) {
452 10
            chdir(dirname($this->lexer->getFilename()));
453
        }
454 12
        $result = $cb();
455 12
        chdir($cwd);
456
457 12
        return $result;
458
    }
459
460
    /**
461
     * MathExpr ::= OrOperand { "|" OrOperand }*
462
     */
463 149
    private function parseMathExpr()
464
    {
465 149
        $value = $this->parseOrOperand();
466
467 148
        while ($this->consumeOptional('|')) {
468 1
            $value = new OperationNode($value, $this->parseOrOperand(), OperationNode::OR_OPERATOR);
469
        }
470
471 148
        return $value;
472
    }
473
474
    /**
475
     * OrOperand ::= AndOperand { "&" AndOperand }*
476
     */
477 149
    private function parseOrOperand()
478
    {
479 149
        $value = $this->parseAndOperand();
480
481 148
        while ($this->consumeOptional('&')) {
482 1
            $value = new OperationNode($value, $this->parseAndOperand(), OperationNode::AND_OPERATOR);
483
        }
484
485 148
        return $value;
486
    }
487
488
    /**
489
     * AndOperand ::= ShiftOperand { ( "<<" | ">>" ) ShiftOperand }*
490
     */
491 149
    private function parseAndOperand()
492
    {
493 149
        $value = $this->parseShiftOperand();
494
495 148
        $continue = true;
496
        do {
497 148
            switch ($this->token->type) {
498 148
                case '<<':
499 1
                    $this->nextToken();
500 1
                    $value = new OperationNode($value, $this->parseShiftOperand(), OperationNode::SHIFT_LEFT);
501 1
                    break;
502 148
                case '>>':
503 1
                    $this->nextToken();
504 1
                    $value = new OperationNode($value, $this->parseShiftOperand(), OperationNode::SHIFT_RIGHT);
505 1
                    break;
506
                default:
507 148
                    $continue = false;
508
            }
509 148
        } while ($continue);
510
511 148
        return $value;
512
    }
513
514
    /**
515
     * ShiftOperand ::= MathTerm { ( "+" | "-" ) MathTerm }*
516
     */
517 149
    private function parseShiftOperand()
518
    {
519 149
        $value = $this->parseMathTerm();
520
521 148
        $continue = true;
522
        do {
523 148
            switch ($this->token->type) {
524 148
                case '+':
525 2
                    $this->nextToken();
526 2
                    $value = new OperationNode($value, $this->parseMathTerm(), OperationNode::ADD);
527 2
                    break;
528 148
                case '-':
529 1
                    $this->nextToken();
530 1
                    $value = new OperationNode($value, $this->parseMathTerm(), OperationNode::SUB);
531 1
                    break;
532
                default:
533 148
                    $continue = false;
534
            }
535 148
        } while ($continue);
536
537 148
        return $value;
538
    }
539
540
    /**
541
     * MathTerm ::= MathFactor { ( ( "*" | "%" | "/" ) MathFactor ) | ( "(" MathExpr ")" ) }*
542
     */
543 149
    private function parseMathTerm()
544
    {
545 149
        $value = $this->parseMathFactor();
546
547 148
        $continue = true;
548
        do {
549 148
            switch ($this->token->type) {
550 148
                case '*':
551 1
                    $this->nextToken();
552 1
                    $value = new OperationNode($value, $this->parseMathFactor(), OperationNode::MUL);
553 1
                    break;
554 148
                case '(':
555 1
                    $this->nextToken();
556 1
                    $value = new OperationNode($value, $this->parseMathExpr(), OperationNode::MUL);
557 1
                    $this->consume(')');
558 1
                    break;
559 148
                case '%':
560 1
                    $this->nextToken();
561 1
                    $value = new OperationNode($value, $this->parseMathFactor(), OperationNode::MOD);
562 1
                    break;
563 148
                case '/':
564 1
                    $this->nextToken();
565 1
                    $value = new OperationNode($value, $this->parseMathFactor(), OperationNode::DIV);
566 1
                    break;
567
                default:
568 148
                    $continue = false;
569
            }
570 148
        } while ($continue);
571
572 148
        return $value;
573
    }
574
575
    /**
576
     * MathFactor ::= (( "(" MathExpr ")" ) | T_NUM | T_VAR | ( ("+"|"-") MathTerm )) [ "^" MathFactor ]
577
     */
578 149
    private function parseMathFactor()
579
    {
580 149
        $value = null;
581 149
        switch ($this->token->type) {
582 149
            case '(':
583 1
                $this->nextToken();
584 1
                $value = $this->parseMathExpr();
585 1
                $this->consume(')');
586 1
                break;
587 149
            case Token::T_NUM:
588 143
                $value = $this->token->value;
589 143
                $this->nextToken();
590 143
                break;
591 8
            case Token::T_VAR:
592 6
                $value = $this->getVariable($this->token->value);
593 6
                $this->nextToken();
594 6
                break;
595 3
            case '+':
596 2
                $this->nextToken();
597 2
                $value = $this->parseMathTerm();
598 1
                break;
599 3
            case '-':
600 2
                $this->nextToken();
601 2
                $value = new OperationNode(0, $this->parseMathTerm(), OperationNode::SUB);
602 2
                break;
603
            default:
604 1
                $this->syntaxError();
605
        }
606
607 148
        if ($this->consumeOptional('^')) {
608 1
            $value = new OperationNode($value, $this->parseMathFactor(), OperationNode::POW);
609
        }
610
611 148
        return $value;
612
    }
613
614 585
    private function consume($type)
615
    {
616 585
        if ($type !== $this->token->type) {
617 2
            $this->syntaxError();
618
        }
619
620 585
        $this->nextToken();
621 585
    }
622
623
    /**
624
     * Separator ::= [ ";" | "," ]
625
     */
626 583
    private function consumeOptionalSeparator()
627
    {
628 583
        if (',' !== $this->token->type && ';' !== $this->token->type) {
629 449
            return false;
630
        }
631
632 543
        $this->nextToken();
633
634 543
        return true;
635
    }
636
637
    /**
638
     * OptionalAssignementOperator ::= [ ":" | "=" ]
639
     */
640 584
    private function consumeOptionalAssignementOperator()
641
    {
642 584
        if (':' !== $this->token->type && '=' !== $this->token->type) {
643 582
            return false;
644
        }
645
646 30
        $this->nextToken();
647
648 28
        return true;
649
    }
650
651 168
    private function consumeOptional($type)
652
    {
653 168
        if ($type !== $this->token->type) {
654 164
            return false;
655
        }
656
657 10
        $this->nextToken();
658
659 10
        return true;
660
    }
661
662 589
    private function nextToken()
663
    {
664 589
        $this->token = $this->lexer->yylex();
665 588
    }
666
667 3
    private function syntaxError()
668
    {
669 3
        $literal = Token::getLiteral($this->token->type);
670 3
        $value   = (strlen((string) $this->token->value) > 10) ? substr($this->token->value, 0, 10) . '...' : $this->token->value;
671
672 3
        $message = 'Syntax error, unexpected \'' . $value . '\'';
673 3
        if ($literal !== $value) {
674 2
            $message .= ' (' . $literal . ')';
675
        }
676 3
        $this->error($message);
677
    }
678
679 5
    public function error($message)
680
    {
681 5
        throw new ParsingException($message, $this->lexer->getFilename(), $this->lexer->getLine());
682
    }
683
}
684