Passed
Push — master ( 4c267c...1bfd55 )
by Pierrick
10:36
created

Parser::doInclude()   B

Complexity

Conditions 10
Paths 36

Size

Total Lines 37
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 20
CRAP Score 10.0107

Importance

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

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