Passed
Push — master ( 010605...4f8b22 )
by Pierrick
06:05
created

Parser::doIncludeFileContent()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 12
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 3

Importance

Changes 0
Metric Value
cc 3
eloc 6
nc 3
nop 2
dl 0
loc 12
ccs 7
cts 7
cp 1
crap 3
rs 10
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 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
        $found = true;
172 574
        switch ($this->token->type) {
173 574
            case Token::T_STRING:
174 574
            case Token::T_ENCAPSED_VAR:
175 4
                $value = $this->parseString();
176 4
                break;
177 574
            case Token::T_END_STR:
178 574
            case Token::T_NAME:
179 564
                $value     = $this->parseScalar();
180 564
                $required  = $this->consumeOptionalAssignementOperator();
181 564
                $realValue = $this->parseValue($required, $valueIsKey);
182 564
                if ($valueIsKey) {
183 99
                    return new ObjectNode([ $value => $realValue ]);
184
                }
185 563
                break;
186 574
            case Token::T_BOOL:
187 574
            case Token::T_NULL:
188 142
                $value = $this->parseScalar();
189 142
                break;
190 570
            case Token::T_NUM:
191 570
            case Token::T_VAR:
192 570
            case '+':
193 570
            case '-':
194 570
            case '(':
195 146
                $value = $this->parseMathExpr();
196 146
                break;
197 566
            case '{':
198 344
                $value = $this->parseObject();
199 344
                break;
200 565
            case '[':
201 271
                $value = $this->parseArray();
202 271
                break;
203 565
            case '.':
204 12
                $value = $this->parseMacro();
205 11
                break;
206 564
            default:
207 564
                if ($required) {
208 1
                    $this->syntaxError();
209
                } else {
210 563
                    $found = false;
211
212 563
                    return;
213
                }
214 574
        }
215
216 573
        return $value;
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $value does not seem to be defined for all execution paths leading up to this point.
Loading history...
217
    }
218
219
    /**
220
     * Scalar ::= T_END_STR | T_NAME | T_BOOL | T_NUM | T_NULL
221
     */
222 568
    private function parseScalar()
223
    {
224 568
        $value = $this->token->value;
225 568
        $this->nextToken();
226
227 568
        return $value;
228
    }
229
230
    /**
231
     * String ::= { T_ENCAPSED_VAR | T_STRING }* T_END_STR
232
     */
233 90
    private function parseString()
234
    {
235 90
        $value    = '';
236 90
        $continue = true;
237
238
        do {
239 90
            switch ($this->token->type) {
240 90
                case Token::T_ENCAPSED_VAR:
241 4
                    $value = new OperationNode($value, $this->getVariable($this->token->value), OperationNode::CONCAT);
242 4
                    break;
243 90
                case Token::T_END_STR:
244 90
                    $continue = false;
245
                    /* no break */
246 90
                case Token::T_STRING:
247 90
                    $value = new OperationNode($value, $this->token->value, OperationNode::CONCAT);
248 90
                    break;
249 90
            }
250
251 90
            $this->nextToken();
252 90
        } while ($continue);
253
254 90
        return $value;
255
    }
256
257
    /**
258
     * Variable ::= T_VAR
259
     */
260 7
    public function getVariable($name)
261
    {
262
        switch ($name) {
263 7
            case '__FILE__':
264
                return realpath($this->lexer->getFilename());
265 7
            case '__DIR__':
266
                return dirname(realpath($this->lexer->getFilename()));
267 7
            default:
268 7
                if (!isset($this->variables[$name])) {
269
                    trigger_error('Undefined variable ' . $name);
270
271
                    return '';
272
                }
273
274 7
                return $this->variables[$name];
275 7
        }
276
    }
277
278
    /**
279
     * MacroCall ::= "." T_NAME [ "(" [ Object ] ")" ] Value
280
     */
281 19
    private function parseMacro()
282
    {
283 19
        $this->consume('.');
284 19
        $result = null;
285
286 19
        if (Token::T_NAME != $this->token->type) {
287
            $this->syntaxError();
288
        }
289
290 19
        $name = $this->token->value;
291 19
        $this->nextToken();
292
293 19
        if ($this->consumeOptional('(')) {
294 5
            $options = $this->parseInnerObject();
295 5
            $this->consume(')');
296 5
        } else {
297 17
            $options = new ObjectNode;
298
        }
299
300 19
        $param = $this->parseValue();
301
302
        switch ($name) {
303 19
            case 'include':
304 8
                $result = $this->doInclude($param, $options);
305 8
                break;
306 13
            case 'ref':
307 5
                $result = new ReferenceNode($param, $this->lexer->getFilename(), $this->lexer->getLine());
308 5
                break;
309 9
            case 'file':
310 3
                $result = $this->doIncludeFileContent($param, $options);
311 2
                break;
312 6
            default:
313 6
                if (!isset($this->macro[$name])) {
314
                    $this->error('Unknown macro \'' . $name . '\'');
315
                }
316 6
                $result = new MacroNode($this->macro[$name], $param, $options);
317 6
                break;
318 6
        }
319
320 18
        return $result;
321
    }
322
323 3
    private function doIncludeFileContent($fileName, $options)
324
    {
325 3
        if ($realpath = $this->resolvePath($fileName)) {
326 1
            return file_get_contents($realpath);
327
        }
328
329 2
        $options = $options->getNativeValue();
330 2
        if (array_key_exists('default', $options)) {
331 1
            return $options['default'];
332
        }
333
334 1
        $this->error("Unable to read file '${fileName}'");
335
    }
336
337
    private function doInclude($fileName, $options)
338
    {
339 8
        $value = new ObjectNode;
340
341 8
        if (isset($options['glob']) ? $options['glob'] : false) {
342 1
            $files = $this->glob($fileName);
343 1
        } else {
344 7
            if (!$path = $this->resolvePath($fileName)) {
345 1
                if (isset($options['required']) ? $options['required'] : true) {
346
                    $this->error('Unable to include file \'' . $fileName . '\'');
347
                }
348
349 1
                return $value;
350
            }
351
352 7
            $files = [ $path ];
353
        }
354
355 8
        $token = $this->token;
356
357 8
        foreach ($files as $file) {
358 8
            $this->lexer->push(file_get_contents($file), $file);
359 8
            $this->nextToken();
360 8
            if ('[' == $this->token->type) {
361 1
                $value = $this->parseArray();
362 8
            } elseif ($value instanceof ObjectNode) {
363 8
                $value = $value->merge($this->parseObject());
364 8
            } else {
365
                $value = $this->parseObject();
366
            }
367 8
            $this->consume(Token::T_EOF);
368 8
            $this->lexer->pop();
369 8
        }
370
371 8
        $this->token = $token;
372
373 8
        return $value;
374
    }
375
376
    public function resolvePath($file)
377
    {
378 9
        $cwd = getcwd();
379 9
        if (file_exists($this->lexer->getFilename())) {
380 7
            chdir(dirname($this->lexer->getFilename()));
381 7
        }
382 9
        $file = realpath($file);
383 9
        chdir($cwd);
384
385 9
        return $file;
386
    }
387
388
    private function glob($pattern)
389
    {
390 1
        $cwd = getcwd();
391 1
        if (file_exists($this->lexer->getFilename())) {
392 1
            chdir(dirname($this->lexer->getFilename()));
393 1
        }
394 1
        $files = array_map('realpath', glob($pattern) ?: []);
395 1
        chdir($cwd);
396
397 1
        return $files;
398
    }
399
400
    /**
401
     * MathExpr ::= OrOperand { "|" OrOperand }*
402
     */
403
    private function parseMathExpr()
404
    {
405 146
        $value = $this->parseOrOperand();
406
407 146
        while ($this->consumeOptional('|')) {
408 1
            $value = new OperationNode($value, $this->parseOrOperand(), OperationNode::OR_OPERATOR);
409 1
        }
410
411 146
        return $value;
412
    }
413
414
    /**
415
     * OrOperand ::= AndOperand { "&" AndOperand }*
416
     */
417
    private function parseOrOperand()
418
    {
419 146
        $value = $this->parseAndOperand();
420
421 146
        while ($this->consumeOptional('&')) {
422 1
            $value = new OperationNode($value, $this->parseAndOperand(), OperationNode::AND_OPERATOR);
423 1
        }
424
425 146
        return $value;
426
    }
427
428
    /**
429
     * AndOperand ::= ShiftOperand { ( "<<" | ">>" ) ShiftOperand }*
430
     */
431
    private function parseAndOperand()
432
    {
433 146
        $value = $this->parseShiftOperand();
434
435 146
        $continue = true;
436
        do {
437 146
            switch ($this->token->type) {
438 146
                case '<<':
439 1
                    $this->nextToken();
440 1
                    $value = new OperationNode($value, $this->parseShiftOperand(), OperationNode::SHIFT_LEFT);
441 1
                    break;
442 146
                case '>>':
443 1
                    $this->nextToken();
444 1
                    $value = new OperationNode($value, $this->parseShiftOperand(), OperationNode::SHIFT_RIGHT);
445 1
                    break;
446 146
                default:
447 146
                    $continue = false;
448 146
            }
449 146
        } while ($continue);
450
451 146
        return $value;
452
    }
453
454
    /**
455
     * ShiftOperand ::= MathTerm { ( "+" | "-" ) MathTerm }*
456
     */
457
    private function parseShiftOperand()
458
    {
459 146
        $value = $this->parseMathTerm();
460
461 146
        $continue = true;
462
        do {
463 146
            switch ($this->token->type) {
464 146
                case '+':
465 1
                    $this->nextToken();
466 1
                    $value = new OperationNode($value, $this->parseMathTerm(), OperationNode::ADD);
467 1
                    break;
468 146
                case '-':
469 1
                    $this->nextToken();
470 1
                    $value = new OperationNode($value, $this->parseMathTerm(), OperationNode::SUB);
471 1
                    break;
472 146
                default:
473 146
                    $continue = false;
474 146
            }
475 146
        } while ($continue);
476
477 146
        return $value;
478
    }
479
480
    /**
481
     * MathTerm ::= MathFactor { ( ( "*" | "%" | "/" ) MathFactor ) | ( "(" MathExpr ")" ) }*
482
     */
483
    private function parseMathTerm()
484
    {
485 146
        $value = $this->parseMathFactor();
486
487 146
        $continue = true;
488
        do {
489 146
            switch ($this->token->type) {
490 146
                case '*':
491 1
                    $this->nextToken();
492 1
                    $value = new OperationNode($value, $this->parseMathFactor(), OperationNode::MUL);
493 1
                    break;
494 146
                case '(':
495 1
                    $this->nextToken();
496 1
                    $value = new OperationNode($value, $this->parseMathExpr(), OperationNode::MUL);
497 1
                    $this->consume(')');
498 1
                    break;
499 146
                case '%':
500 1
                    $this->nextToken();
501 1
                    $value = new OperationNode($value, $this->parseMathExpr(), OperationNode::MOD);
502 1
                    break;
503 146
                case '/':
504 1
                    $this->nextToken();
505 1
                    $value = new OperationNode($value, $this->parseMathFactor(), OperationNode::DIV);
506 1
                    break;
507 146
                default:
508 146
                    $continue = false;
509 146
            }
510 146
        } while ($continue);
511
512 146
        return $value;
513
    }
514
515
    /**
516
     * MathFactor ::= ( "(" MathExpr ")" ) | T_NUM | T_VAR | ( ("+"|"-") MathTerm ) [ "^" MathFactor ]
517
     */
518
    private function parseMathFactor()
519
    {
520 146
        switch ($this->token->type) {
521 146
            case '(':
522 1
                $this->nextToken();
523 1
                $value = $this->parseMathExpr();
524 1
                $this->consume(')');
525 1
                break;
526 146
            case Token::T_NUM:
527 142
                $value = $this->token->value;
528 142
                $this->nextToken();
529 142
                break;
530 6
            case Token::T_VAR:
531 5
                $value = $this->getVariable($this->token->value);
532 5
                $this->nextToken();
533 5
                break;
534 2
            case '+':
535 1
                $this->nextToken();
536 1
                $value = $this->parseMathTerm();
537 1
                break;
538 2
            case '-':
539 2
                $this->nextToken();
540 2
                $value = new OperationNode(0, $this->parseMathTerm(), OperationNode::SUB);
541 2
                break;
542
            default:
543
                $this->syntaxError();
544 146
        }
545
546 146
        if ($this->consumeOptional('^')) {
547 1
            $value = new OperationNode($value, $this->parseMathFactor(), OperationNode::POW);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $value does not seem to be defined for all execution paths leading up to this point.
Loading history...
548 1
        }
549
550 146
        return $value;
551
    }
552
553
    private function consume($type)
554
    {
555 574
        if ($type !== $this->token->type) {
556 1
            $this->syntaxError();
557
        }
558
559 574
        $this->nextToken();
560 574
    }
561
562
    private function consumeOptionalSeparator()
563
    {
564 571
        if (',' !== $this->token->type && ';' !== $this->token->type) {
565 440
            return false;
566
        }
567
568 534
        $this->nextToken();
569
570 534
        return true;
571
    }
572
573
    private function consumeOptionalAssignementOperator()
574
    {
575 575
        if (':' !== $this->token->type && '=' !== $this->token->type) {
576 573
            return false;
577
        }
578
579 23
        $this->nextToken();
580
581 21
        return true;
582
    }
583
584
    private function consumeOptional($type)
585
    {
586 508
        if ($type !== $this->token->type) {
587 422
            return false;
588
        }
589
590 383
        $this->nextToken();
591
592 383
        return true;
593
    }
594
595
    private function nextToken()
596
    {
597 578
        $this->token = $this->lexer->yylex();
598 577
    }
599
600
    private function syntaxError()
601
    {
602 2
        $literal = Token::getLiteral($this->token->type);
603 2
        $value   = (strlen((string) $this->token->value) > 10) ? substr($this->token->value, 0, 10) . '...' : $this->token->value;
604
605 2
        $message = 'Syntax error, unexpected \'' . $value . '\'';
606 2
        if ($literal !== $value) {
607 1
            $message .= ' (' . $literal . ')';
608 1
        }
609 2
        $this->error($message);
610
    }
611
612
    public function error($message)
613
    {
614 4
        throw new ParsingException($message, $this->lexer->getFilename(), $this->lexer->getLine());
615
    }
616
}
617