Passed
Push — master ( 38c7e9...702ac6 )
by Pierrick
05:37 queued 01:38
created

Parser::getAstFromString()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 15
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 2

Importance

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