Passed
Push — master ( 4f8b22...d37b16 )
by Pierrick
05:47
created

Parser::relativeToCurrentFile()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 2

Importance

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