Passed
Push — master ( 651556...010605 )
by Pierrick
10:35
created

Parser::doIncludeFileContent()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 11
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 4.125

Importance

Changes 0
Metric Value
cc 3
eloc 5
c 0
b 0
f 0
nc 3
nop 2
dl 0
loc 11
ccs 3
cts 6
cp 0.5
crap 4.125
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 577
    public function __construct(Lexer $lexer = null)
24 99
    {
25 577
        $this->lexer = $lexer ?: new Lexer();
26 577
    }
27
28 577
    public function registerMacro(MacroInterface $macro)
29
    {
30 577
        if (isset($this->macro[$macro->getName()])) {
31
            throw new Exception('Macro with the same name already registered.');
32
        }
33
34 577
        $this->macro[$macro->getName()] = [ $macro, 'execute' ];
35 577
    }
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 576
    public function parse($str, $filename = 'nacl string')
46
    {
47 576
        $token = $this->token;
48 576
        $this->lexer->push($str, $filename);
49 576
        $this->nextToken();
50
51 575
        if ('[' == $this->token->type) {
52 70
            $result = $this->parseArray();
53 70
        } else {
54 505
            $result = $this->parseObject();
55
        }
56
57 571
        $this->consume(Token::T_EOF);
58 570
        $this->lexer->pop();
59 570
        $this->token = $token;
60
61 570
        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 505
    private function parseObject()
86
    {
87 505
        $opened = $this->consumeOptional('{');
88 505
        $object = $this->parseInnerObject();
89 501
        if ($opened) {
90 379
            $this->consume('}');
91 379
        }
92
93 501
        return $object;
94
    }
95
96 505
    private function parseInnerObject()
97
    {
98 505
        $object = new ObjectNode;
99
        do {
100 505
            $name     = null;
101 505
            $continue = false;
102 505
            switch ($this->token->type) {
103 505
                case Token::T_END_STR:
104 86
                    $name = $this->parseString()->getNativeValue();
105
                    /* no break */
106 505
                case Token::T_NAME:
107 503
                    if (null === $name) {
108 482
                        $name = $this->token->value;
109 482
                        $this->nextToken();
110 482
                    }
111 503
                    $this->consumeOptionalAssignementOperator();
112 501
                    $val = $this->parseValue();
113 500
                    if ($val instanceof ObjectNode && isset($object[$name]) && $object[$name] instanceof ObjectNode) {
114 3
                        $object[$name] = $object[$name]->merge($val);
115 3
                    } else {
116 500
                        $object[$name] = $val;
117
                    }
118 500
                    $separator = $this->consumeOptionalSeparator();
119 500
                    $continue  = is_object($val) || $separator;
120 500
                    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 501
            }
141 501
        } while ($continue);
142
143 501
        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 572
    private function parseValue($required = true, &$found = true)
170
    {
171 572
        $found = true;
172 572
        switch ($this->token->type) {
173 572
            case Token::T_STRING:
174 572
            case Token::T_ENCAPSED_VAR:
175 4
                $value = $this->parseString();
176 4
                break;
177 572
            case Token::T_END_STR:
178 572
            case Token::T_NAME:
179 562
                $value     = $this->parseScalar();
180 562
                $required  = $this->consumeOptionalAssignementOperator();
181 562
                $realValue = $this->parseValue($required, $valueIsKey);
182 562
                if ($valueIsKey) {
183 99
                    return new ObjectNode([ $value => $realValue ]);
184
                }
185 561
                break;
186 572
            case Token::T_BOOL:
187 572
            case Token::T_NULL:
188 142
                $value = $this->parseScalar();
189 142
                break;
190 568
            case Token::T_NUM:
191 568
            case Token::T_VAR:
192 568
            case '+':
193 568
            case '-':
194 568
            case '(':
195 146
                $value = $this->parseMathExpr();
196 146
                break;
197 564
            case '{':
198 344
                $value = $this->parseObject();
199 344
                break;
200 563
            case '[':
201 271
                $value = $this->parseArray();
202 271
                break;
203 563
            case '.':
204 10
                $value = $this->parseMacro();
205 10
                break;
206 562
            default:
207 562
                if ($required) {
208 1
                    $this->syntaxError();
209
                } else {
210 561
                    $found = false;
211
212 561
                    return;
213
                }
214 572
        }
215
216 571
        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 566
    private function parseScalar()
223
    {
224 566
        $value = $this->token->value;
225 566
        $this->nextToken();
226
227 566
        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 17
    private function parseMacro()
282
    {
283 17
        $this->consume('.');
284 17
        $result = null;
285
286 17
        if (Token::T_NAME != $this->token->type) {
287
            $this->syntaxError();
288
        }
289
290 17
        $name = $this->token->value;
291 17
        $this->nextToken();
292
293 17
        if ($this->consumeOptional('(')) {
294 4
            $options = $this->parseInnerObject();
295 4
            $this->consume(')');
296 4
        } else {
297 16
            $options = new ObjectNode;
298
        }
299
300 17
        $param = $this->parseValue();
301
302
        switch ($name) {
303 17
            case 'include':
304 8
                $result = $this->doInclude($param, $options);
305 8
                break;
306 11
            case 'ref':
307 5
                $result = new ReferenceNode($param, $this->lexer->getFilename(), $this->lexer->getLine());
308 5
                break;
309 7
            case 'file':
310 1
                $result = $this->doIncludeFileContent($param, $options);
311 1
                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 17
        return $result;
321
    }
322
323 1
    private function doIncludeFileContent($fileName, $options)
324
    {
325 1
        if ($fileName = $this->resolvePath($fileName)) {
326 1
            return file_get_contents($fileName);
327
        }
328
329
        if (array_key_exists('default', $options)) {
330
            return $options['default'];
331
        }
332
333
        $this->error("Unable to read file '${parameter}'");
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $parameter seems to be never defined.
Loading history...
334
    }
335
336
    private function doInclude($fileName, $options)
337
    {
338 8
        $value = new ObjectNode;
339
340 8
        if (isset($options['glob']) ? $options['glob'] : false) {
341 1
            $files = $this->glob($fileName);
342 1
        } else {
343 7
            if (!$path = $this->resolvePath($fileName)) {
344 1
                if (isset($options['required']) ? $options['required'] : true) {
345
                    $this->error('Unable to include file \'' . $fileName . '\'');
346
                }
347
348 1
                return $value;
349
            }
350
351 7
            $files = [ $path ];
352
        }
353
354 8
        $token = $this->token;
355
356 8
        foreach ($files as $file) {
357 8
            $this->lexer->push(file_get_contents($file), $file);
358 8
            $this->nextToken();
359 8
            if ('[' == $this->token->type) {
360 1
                $value = $this->parseArray();
361 8
            } elseif ($value instanceof ObjectNode) {
362 8
                $value = $value->merge($this->parseObject());
363 8
            } else {
364
                $value = $this->parseObject();
365
            }
366 8
            $this->consume(Token::T_EOF);
367 8
            $this->lexer->pop();
368 8
        }
369
370 8
        $this->token = $token;
371
372 8
        return $value;
373
    }
374
375
    public function resolvePath($file)
376
    {
377 7
        $cwd = getcwd();
378 7
        if (file_exists($this->lexer->getFilename())) {
379 7
            chdir(dirname($this->lexer->getFilename()));
380 7
        }
381 7
        $file = realpath($file);
382 7
        chdir($cwd);
383
384 7
        return $file;
385
    }
386
387
    private function glob($pattern)
388
    {
389 1
        $cwd = getcwd();
390 1
        if (file_exists($this->lexer->getFilename())) {
391 1
            chdir(dirname($this->lexer->getFilename()));
392 1
        }
393 1
        $files = array_map('realpath', glob($pattern) ?: []);
394 1
        chdir($cwd);
395
396 1
        return $files;
397
    }
398
399
    /**
400
     * MathExpr ::= OrOperand { "|" OrOperand }*
401
     */
402
    private function parseMathExpr()
403
    {
404 146
        $value = $this->parseOrOperand();
405
406 146
        while ($this->consumeOptional('|')) {
407 1
            $value = new OperationNode($value, $this->parseOrOperand(), OperationNode::OR_OPERATOR);
408 1
        }
409
410 146
        return $value;
411
    }
412
413
    /**
414
     * OrOperand ::= AndOperand { "&" AndOperand }*
415
     */
416
    private function parseOrOperand()
417
    {
418 146
        $value = $this->parseAndOperand();
419
420 146
        while ($this->consumeOptional('&')) {
421 1
            $value = new OperationNode($value, $this->parseAndOperand(), OperationNode::AND_OPERATOR);
422 1
        }
423
424 146
        return $value;
425
    }
426
427
    /**
428
     * AndOperand ::= ShiftOperand { ( "<<" | ">>" ) ShiftOperand }*
429
     */
430
    private function parseAndOperand()
431
    {
432 146
        $value = $this->parseShiftOperand();
433
434 146
        $continue = true;
435
        do {
436 146
            switch ($this->token->type) {
437 146
                case '<<':
438 1
                    $this->nextToken();
439 1
                    $value = new OperationNode($value, $this->parseShiftOperand(), OperationNode::SHIFT_LEFT);
440 1
                    break;
441 146
                case '>>':
442 1
                    $this->nextToken();
443 1
                    $value = new OperationNode($value, $this->parseShiftOperand(), OperationNode::SHIFT_RIGHT);
444 1
                    break;
445 146
                default:
446 146
                    $continue = false;
447 146
            }
448 146
        } while ($continue);
449
450 146
        return $value;
451
    }
452
453
    /**
454
     * ShiftOperand ::= MathTerm { ( "+" | "-" ) MathTerm }*
455
     */
456
    private function parseShiftOperand()
457
    {
458 146
        $value = $this->parseMathTerm();
459
460 146
        $continue = true;
461
        do {
462 146
            switch ($this->token->type) {
463 146
                case '+':
464 1
                    $this->nextToken();
465 1
                    $value = new OperationNode($value, $this->parseMathTerm(), OperationNode::ADD);
466 1
                    break;
467 146
                case '-':
468 1
                    $this->nextToken();
469 1
                    $value = new OperationNode($value, $this->parseMathTerm(), OperationNode::SUB);
470 1
                    break;
471 146
                default:
472 146
                    $continue = false;
473 146
            }
474 146
        } while ($continue);
475
476 146
        return $value;
477
    }
478
479
    /**
480
     * MathTerm ::= MathFactor { ( ( "*" | "%" | "/" ) MathFactor ) | ( "(" MathExpr ")" ) }*
481
     */
482
    private function parseMathTerm()
483
    {
484 146
        $value = $this->parseMathFactor();
485
486 146
        $continue = true;
487
        do {
488 146
            switch ($this->token->type) {
489 146
                case '*':
490 1
                    $this->nextToken();
491 1
                    $value = new OperationNode($value, $this->parseMathFactor(), OperationNode::MUL);
492 1
                    break;
493 146
                case '(':
494 1
                    $this->nextToken();
495 1
                    $value = new OperationNode($value, $this->parseMathExpr(), OperationNode::MUL);
496 1
                    $this->consume(')');
497 1
                    break;
498 146
                case '%':
499 1
                    $this->nextToken();
500 1
                    $value = new OperationNode($value, $this->parseMathExpr(), OperationNode::MOD);
501 1
                    break;
502 146
                case '/':
503 1
                    $this->nextToken();
504 1
                    $value = new OperationNode($value, $this->parseMathFactor(), OperationNode::DIV);
505 1
                    break;
506 146
                default:
507 146
                    $continue = false;
508 146
            }
509 146
        } while ($continue);
510
511 146
        return $value;
512
    }
513
514
    /**
515
     * MathFactor ::= ( "(" MathExpr ")" ) | T_NUM | T_VAR | ( ("+"|"-") MathTerm ) [ "^" MathFactor ]
516
     */
517
    private function parseMathFactor()
518
    {
519 146
        switch ($this->token->type) {
520 146
            case '(':
521 1
                $this->nextToken();
522 1
                $value = $this->parseMathExpr();
523 1
                $this->consume(')');
524 1
                break;
525 146
            case Token::T_NUM:
526 142
                $value = $this->token->value;
527 142
                $this->nextToken();
528 142
                break;
529 6
            case Token::T_VAR:
530 5
                $value = $this->getVariable($this->token->value);
531 5
                $this->nextToken();
532 5
                break;
533 2
            case '+':
534 1
                $this->nextToken();
535 1
                $value = $this->parseMathTerm();
536 1
                break;
537 2
            case '-':
538 2
                $this->nextToken();
539 2
                $value = new OperationNode(0, $this->parseMathTerm(), OperationNode::SUB);
540 2
                break;
541
            default:
542
                $this->syntaxError();
543 146
        }
544
545 146
        if ($this->consumeOptional('^')) {
546 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...
547 1
        }
548
549 146
        return $value;
550
    }
551
552
    private function consume($type)
553
    {
554 572
        if ($type !== $this->token->type) {
555 1
            $this->syntaxError();
556
        }
557
558 572
        $this->nextToken();
559 572
    }
560
561
    private function consumeOptionalSeparator()
562
    {
563 570
        if (',' !== $this->token->type && ';' !== $this->token->type) {
564 439
            return false;
565
        }
566
567 534
        $this->nextToken();
568
569 534
        return true;
570
    }
571
572
    private function consumeOptionalAssignementOperator()
573
    {
574 573
        if (':' !== $this->token->type && '=' !== $this->token->type) {
575 571
            return false;
576
        }
577
578 22
        $this->nextToken();
579
580 20
        return true;
581
    }
582
583
    private function consumeOptional($type)
584
    {
585 506
        if ($type !== $this->token->type) {
586 420
            return false;
587
        }
588
589 382
        $this->nextToken();
590
591 382
        return true;
592
    }
593
594
    private function nextToken()
595
    {
596 576
        $this->token = $this->lexer->yylex();
597 575
    }
598
599
    private function syntaxError()
600
    {
601 2
        $literal = Token::getLiteral($this->token->type);
602 2
        $value   = (strlen((string) $this->token->value) > 10) ? substr($this->token->value, 0, 10) . '...' : $this->token->value;
603
604 2
        $message = 'Syntax error, unexpected \'' . $value . '\'';
605 2
        if ($literal !== $value) {
606 1
            $message .= ' (' . $literal . ')';
607 1
        }
608 2
        $this->error($message);
609
    }
610
611
    public function error($message)
612
    {
613 3
        throw new ParsingException($message, $this->lexer->getFilename(), $this->lexer->getLine());
614
    }
615
}
616