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

Parser::parseValue()   C

Complexity

Conditions 17
Paths 18

Size

Total Lines 49
Code Lines 42

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 40
CRAP Score 17.0041

Importance

Changes 3
Bugs 0 Features 1
Metric Value
cc 17
eloc 42
c 3
b 0
f 1
nc 18
nop 2
dl 0
loc 49
ccs 40
cts 41
cp 0.9756
crap 17.0041
rs 5.2166

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