Passed
Push — master ( 702ac6...0ae4a6 )
by Pierrick
05:52
created

Parser::parseRootValue()   B

Complexity

Conditions 7
Paths 5

Size

Total Lines 34
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 26
CRAP Score 7.0178

Importance

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