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

Parser::doInclude()   C

Complexity

Conditions 12
Paths 36

Size

Total Lines 37
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 20
CRAP Score 12.0155

Importance

Changes 6
Bugs 0 Features 0
Metric Value
cc 12
eloc 22
c 6
b 0
f 0
nc 36
nop 2
dl 0
loc 37
ccs 20
cts 21
cp 0.9524
crap 12.0155
rs 6.9666

How to fix   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
 * 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