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

Parser::parseValue()   C

Complexity

Conditions 17
Paths 18

Size

Total Lines 49
Code Lines 42

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 43
CRAP Score 17.0033

Importance

Changes 3
Bugs 0 Features 1
Metric Value
cc 17
eloc 42
c 3
b 0
f 1
nc 18
nop 2
dl 0
loc 49
ccs 43
cts 44
cp 0.9773
crap 17.0033
rs 5.2166

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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