Passed
Push — master ( 4f8b22...d37b16 )
by Pierrick
05:47
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 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