Completed
Push — master ( 895d73...5bc057 )
by Federico
02:36
created

Parser   F

Complexity

Total Complexity 142

Size/Duplication

Total Lines 756
Duplicated Lines 2.65 %

Coupling/Cohesion

Components 1
Dependencies 17

Importance

Changes 1
Bugs 0 Features 1
Metric Value
c 1
b 0
f 1
dl 20
loc 756
rs 1.263
wmc 142
lcom 1
cbo 17

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like Parser often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Parser, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace Jade;
4
5
use Jade\Parser\Exception as ParserException;
6
use Jade\Parser\ExtensionsHelper;
7
8
class Parser
9
{
10
    public static $includeNotFound = ".alert.alert-danger.\n\tPage not found.";
11
12
    protected $allowMixedIndent;
13
    protected $basedir;
14
    protected $extending;
15
    protected $extension;
16
    protected $filename;
17
    protected $input;
18
    protected $lexer;
19
    protected $notFound;
20
    protected $options = array();
21
    protected $preRender;
22
23
    protected $blocks = array();
24
    protected $mixins = array();
25
    protected $contexts = array();
26
27
    public function __construct($input, $filename = null, array $options = array())
28
    {
29
        $defaultOptions = array(
30
            'allowMixedIndent' => true,
31
            'basedir' => null,
32
            'customKeywords' => array(),
33
            'extension' => array('.pug', '.jade'),
34
            'notFound' => null,
35
            'preRender' => null,
36
        );
37
        foreach ($defaultOptions as $key => $default) {
38
            $this->$key = isset($options[$key]) ? $options[$key] : $default;
39
            $this->options[$key] = $this->$key;
40
        }
41
42
        $this->setInput($filename, $input);
43
44
        if ($this->input && $this->input[0] === "\xef" && $this->input[1] === "\xbb" && $this->input[2] === "\xbf") {
45
            $this->input = substr($this->input, 3);
46
        }
47
48
        $this->lexer = new Lexer($this->input, $this->options);
49
        array_push($this->contexts, $this);
50
    }
51
52
    protected function getExtensions()
53
    {
54
        $extensions = new ExtensionsHelper($this->extension);
55
56
        return $extensions->getExtensions();
57
    }
58
59
    protected function hasValidTemplateExtension($path)
60
    {
61
        $extensions = new ExtensionsHelper($this->extension);
62
63
        return $extensions->hasValidTemplateExtension($path);
64
    }
65
66
    protected function getTemplatePath($path)
67
    {
68
        $isAbsolutePath = substr($path, 0, 1) === '/';
69
        if ($isAbsolutePath && !isset($this->options['basedir'])) {
70
            throw new \ErrorException("The 'basedir' option need to be set to use absolute path like $path", 29);
71
        }
72
73
        $path = ($isAbsolutePath
74
            ? rtrim($this->options['basedir'], '/\\')
75
            : dirname($this->filename)
76
        ) . DIRECTORY_SEPARATOR . $path;
77
        $extensions = new ExtensionsHelper($this->extension);
78
79
        return $extensions->findValidTemplatePath($path, '');
80
    }
81
82
    protected function getTemplateContents($path, $value = null)
83
    {
84
        if ($path !== null) {
85
            $contents = file_get_contents($path);
86
            if (is_callable($this->preRender)) {
87
                $contents = call_user_func($this->preRender, $contents);
88
            }
89
90
            return $contents;
91
        }
92
93
        $notFound = isset($this->options['notFound'])
94
            ? $this->options['notFound']
95
            : static::$includeNotFound;
96
97
        if ($notFound !== false) {
98
            return $notFound;
99
        }
100
101
        $value = $value ?: $path;
102
        throw new \InvalidArgumentException("The included file '$value' does not exists.", 22);
103
    }
104
105
    protected function setInput($filename, $input)
106
    {
107
        if ($filename === null && file_exists($input)) {
108
            $filename = $input;
109
            $input = file_get_contents($input);
110
        }
111
112
        $this->input = preg_replace('`\r\n|\r`', "\n", $input);
113
        if (is_callable($this->preRender)) {
114
            $this->input = call_user_func($this->preRender, $this->input);
115
        }
116
        $this->filename = $filename;
117
    }
118
119
    public function getFilename()
120
    {
121
        return $this->filename;
122
    }
123
124
    /**
125
     * Get a parser with the same settings.
126
     *
127
     * @return Parser
128
     */
129
    public function subParser($input)
130
    {
131
        return new static($input, $this->filename, $this->options);
132
    }
133
134
    public function context($parser = null)
135
    {
136
        if ($parser === null) {
137
            return array_pop($this->contexts);
138
        }
139
        array_push($this->contexts, $parser);
140
    }
141
142
    public function advance()
143
    {
144
        return $this->lexer->advance();
145
    }
146
147
    public function skip($n)
148
    {
149
        while ($n--) {
150
            $this->advance();
151
        }
152
    }
153
154
    public function peek()
155
    {
156
        return $this->lookahead(1);
157
    }
158
159
    public function line()
160
    {
161
        return $this->lexer->lineno;
162
    }
163
164
    public function lookahead($n = 1)
165
    {
166
        return $this->lexer->lookahead($n);
167
    }
168
169
    public function parse()
170
    {
171
        $block = new Nodes\Block();
172
        $block->line = $this->line();
173
174
        while ($this->peekType() !== 'eos') {
175
            if ($this->peekType() === 'newline') {
176
                $this->advance();
177
                continue;
178
            }
179
            $block->push($this->parseExpression());
180
        }
181
182
        if ($parser = $this->extending) {
183
            $this->context($parser);
184
            // $parser->blocks = $this->blocks;
185
            try {
186
                $ast = $parser->parse();
187
            } catch (\Exception $e) {
188
                throw new ParserException($parser->getFilename() . ' (' . $block->line . ') : ' . $e->getMessage(), 23, $e);
189
            }
190
            $this->context();
191
192
            foreach ($this->mixins as $name => $v) {
193
                $ast->unshift($this->mixins[$name]);
194
            }
195
196
            return $ast;
197
        }
198
199
        return $block;
200
    }
201
202
    protected function expect($type)
203
    {
204
        if ($this->peekType() === $type) {
205
            return $this->lexer->advance();
206
        }
207
208
        $lineNumber = $this->line();
209
        $lines = explode("\n", $this->input);
210
        $lineString = isset($lines[$lineNumber]) ? $lines[$lineNumber] : '';
211
        throw new \ErrorException("\n" . sprintf('Expected %s, but got %s in %dth line : %s', $type, $this->peekType(), $lineNumber, $lineString) . "\n", 24);
212
    }
213
214
    protected function accept($type)
215
    {
216
        if ($this->peekType() === $type) {
217
            return $this->advance();
218
        }
219
    }
220
221
    protected function parseExpression()
222
    {
223
        $_types = array('tag', 'mixin', 'block', 'case', 'when', 'default', 'extends', 'include', 'doctype', 'filter', 'comment', 'text', 'each', 'customKeyword', 'code', 'call', 'interpolation');
224
225
        if (in_array($this->peekType(), $_types)) {
226
            $_method = 'parse' . ucfirst($this->peekType());
227
228
            return $this->$_method();
229
        }
230
231
        switch ($this->peekType()) {
232
            case 'yield':
233
                $this->advance();
234
                $block = new Nodes\Block();
235
                $block->yield = true;
236
237
                return $block;
238
239
            case 'id':
240
            case 'class':
241
                $token = $this->advance();
242
                $this->lexer->defer($this->lexer->token('tag', 'div'));
243
                $this->lexer->defer($token);
244
245
                return $this->parseExpression();
246
247
            default:
248
                throw new \ErrorException($this->filename . ' (' . $this->line() . ') : Unexpected token "' . $this->peekType() . '"', 25);
249
        }
250
    }
251
252
    protected function parseText()
253
    {
254
        $token = $this->expect('text');
255
        if (preg_match('/^(.*?)#\[([^\]\n]+)\]/', $token->value)) {
256
            $block = new Nodes\Block();
257
            $this->parseInlineTags($block, $token->value);
258
259
            return $block;
260
        }
261
        $node = new Nodes\Text($token->value);
262
        $node->line = $this->line();
263
264
        return $node;
265
    }
266
267
    protected function parseBlockExpansion()
268
    {
269
        if (':' === $this->peekType()) {
270
            $this->advance();
271
272
            return new Nodes\Block($this->parseExpression());
273
        }
274
275
        return $this->block();
276
    }
277
278
    protected function parseCase()
279
    {
280
        $value = $this->expect('case')->value;
281
        $node = new Nodes\CaseNode($value);
282
        $node->line = $this->line();
283
        $node->block = $this->block();
284
285
        return $node;
286
    }
287
288
    protected function parseWhen()
289
    {
290
        $value = $this->expect('when')->value;
291
292
        return new Nodes\When($value, $this->parseBlockExpansion());
293
    }
294
295
    protected function parseDefault()
296
    {
297
        $this->expect('default');
298
299
        return new Nodes\When('default', $this->parseBlockExpansion());
300
    }
301
302
    protected function parseCode()
303
    {
304
        $token = $this->expect('code');
305
        $buffer = isset($token->buffer) ? $token->buffer : false;
306
        $escape = isset($token->escape) ? $token->escape : true;
307
        $node = new Nodes\Code($token->value, $buffer, $escape);
308
        $node->line = $this->line();
309
310
        $i = 1;
311
        while ($this->lookahead($i)->type === 'newline') {
312
            $i++;
313
        }
314
315
        if ($this->lookahead($i)->type === 'indent') {
316
            $this->skip($i - 1);
317
            $node->block = $this->block();
318
        }
319
320
        return $node;
321
    }
322
323
    protected function parseComment()
324
    {
325
        $token = $this->expect('comment');
326
        $node = new Nodes\Comment($token->value, $token->buffer);
327
        $node->line = $this->line();
328
329
        return $node;
330
    }
331
332
    protected function parseDoctype()
333
    {
334
        $token = $this->expect('doctype');
335
        $node = new Nodes\Doctype($token->value);
336
        $node->line = $this->line();
337
338
        return $node;
339
    }
340
341
    protected function parseFilter()
342
    {
343
        $token = $this->expect('filter');
344
        $attributes = $this->accept('attributes');
345
346
        $this->lexer->pipeless = true;
347
        $block = $this->parseTextBlock();
348
        $this->lexer->pipeless = false;
349
350
        $node = new Nodes\Filter($token->value, $block, $attributes);
351
        $node->line = $this->line();
352
353
        return $node;
354
    }
355
356
    protected function parseEach()
357
    {
358
        $token = $this->expect('each');
359
        $node = new Nodes\Each($token->code, $token->value, $token->key);
360
        $node->line = $this->line();
361
        $node->block = $this->block();
362
        if ($this->peekType() === 'code' && $this->peek()->value === 'else') {
363
            $this->advance();
364
            $node->alternative = $this->block();
365
        }
366
367
        return $node;
368
    }
369
370
    protected function parseCustomKeyword()
371
    {
372
        $token = $this->expect('customKeyword');
373
        $node = new Nodes\CustomKeyword($token->value, $token->args);
374
        $node->line = $this->line();
375
        if ('indent' === $this->peekType()) {
376
            $node->block = $this->block();
377
        }
378
379
        return $node;
380
    }
381
382
    protected function parseExtends()
383
    {
384
        $extendValue = $this->expect('extends')->value;
385
        $path = $this->getTemplatePath($extendValue);
386
387
        $string = $this->getTemplateContents($path, $extendValue);
388
        $parser = new static($string, $path, $this->options);
389
        // need to be a reference, or be seted after the parse loop
390
        $parser->blocks = &$this->blocks;
391
        $parser->contexts = $this->contexts;
392
        $this->extending = $parser;
393
394
        return new Nodes\Literal('');
395
    }
396
397
    protected function parseBlock()
398
    {
399
        $block = $this->expect('block');
400
        $mode = $block->mode;
401
        $name = trim($block->value);
402
403
        $block = 'indent' === $this->peekType()
404
            ? $this->block()
405
            : new Nodes\Block(empty($name)
406
                ? new Nodes\MixinBlock()
407
                : new Nodes\Literal('')
408
            );
409
410
        if (isset($this->blocks[$name])) {
411
            $prev = &$this->blocks[$name];
412
413
            switch ($prev->mode) {
414
                case 'append':
415
                    $block->nodes = array_merge($block->nodes, $prev->nodes);
416
                    $prev = $block;
417
                    break;
418
419
                case 'prepend':
420
                    $block->nodes = array_merge($prev->nodes, $block->nodes);
421
                    $prev = $block;
422
                    break;
423
424
                case 'replace':
425
                default:
426
                    break;
427
            }
428
429
            return $this->blocks[$name];
430
        }
431
432
        $block->mode = $mode;
433
        $this->blocks[$name] = $block;
434
435
        return $block;
436
    }
437
438
    protected function parseInclude()
439
    {
440
        $token = $this->expect('include');
441
        $includeValue = trim($token->value);
442
        $path = $this->getTemplatePath($includeValue);
443
444
        if ($path && !$this->hasValidTemplateExtension($path)) {
445
            return new Nodes\Text(file_get_contents($path));
446
        }
447
448
        $string = $this->getTemplateContents($path, $includeValue);
449
450
        $parser = new static($string, $path, $this->options);
451
        $parser->blocks = $this->blocks;
452
        $parser->mixins = $this->mixins;
453
454
        $this->context($parser);
455
        try {
456
            $ast = $parser->parse();
457
        } catch (\Exception $e) {
458
            throw new \ErrorException($path . ' (' . $parser->lexer->lineno . ') : ' . $e->getMessage(), 27);
459
        }
460
        $this->context();
461
        $ast->filename = $path;
462
463
        if ('indent' === $this->peekType() && method_exists($ast, 'includeBlock')) {
464
            $block = $ast->includeBlock();
465
            if (is_object($block)) {
466
                $handler = count($block->nodes) === 1 && isset($block->nodes[0]->block)
467
                    ? $block->nodes[0]->block
468
                    : $block;
469
                $handler->push($this->block());
470
            }
471
        }
472
473
        return $ast;
474
    }
475
476
    protected function parseCall()
477
    {
478
        $token = $this->expect('call');
479
        $name = $token->value;
480
481
        $arguments = isset($token->arguments)
482
            ? $token->arguments
483
            : null;
484
485
        $mixin = new Nodes\Mixin($name, $arguments, new Nodes\Block(), true);
486
487
        $this->tag($mixin);
488
489
        if ($mixin->block->isEmpty()) {
490
            $mixin->block = null;
491
        }
492
493
        return $mixin;
494
    }
495
496
    protected function parseMixin()
497
    {
498
        $token = $this->expect('mixin');
499
        $name = $token->value;
500
        $arguments = $token->arguments;
501
502
        // definition
503
        if ('indent' === $this->peekType()) {
504
            $mixin = new Nodes\Mixin($name, $arguments, $this->block(), false);
505
            $this->mixins[$name] = $mixin;
506
507
            return $mixin;
508
        }
509
510
        // call
511
        return new Nodes\Mixin($name, $arguments, null, true);
512
    }
513
514
    protected function parseTextBlock()
515
    {
516
        $block = new Nodes\Block();
517
        $block->line = $this->line();
518
        $spaces = $this->expect('indent')->value;
519
520
        if (!isset($this->_spaces)) {
521
            $this->_spaces = $spaces;
522
        }
523
524
        $indent = str_repeat(' ', $spaces - $this->_spaces + 1);
525
526
        while ($this->peekType() != 'outdent') {
527
            switch ($this->peekType()) {
528
                case 'newline':
529
                    $this->lexer->advance();
530
                    break;
531
532
                case 'indent':
533
                    foreach ($this->parseTextBlock()->nodes as $n) {
534
                        $block->push($n);
535
                    }
536
                    break;
537
538
                default:
539
                    $this->parseInlineTags($block, $indent . $this->advance()->value);
540
            }
541
        }
542
543
        if (isset($this->_spaces) && $spaces === $this->_spaces) {
544
            unset($this->_spaces);
545
        }
546
547
        $this->expect('outdent');
548
549
        return $block;
550
    }
551
552
    protected function block()
553
    {
554
        $block = new Nodes\Block();
555
        $block->line = $this->line();
556
        $this->expect('indent');
557
558
        while ($this->peekType() !== 'outdent') {
559
            if ($this->peekType() === 'newline') {
560
                $this->lexer->advance();
561
                continue;
562
            }
563
564
            $block->push($this->parseExpression());
565
        }
566
567
        $this->expect('outdent');
568
569
        return $block;
570
    }
571
572
    protected function parseInterpolation()
573
    {
574
        $token = $this->advance();
575
        $tag = new Nodes\Tag($token->value);
576
        $tag->buffer = true;
577
578
        return $this->tag($tag);
579
    }
580
581
    protected function parseASTFilter()
582
    {
583
        $token = $this->expect('tag');
584
        $attributes = $this->accept('attributes');
585
        $this->expect(':');
586
        $block = $this->block();
587
        $node = new Nodes\Filter($token->value, $block, $attributes);
588
        $node->line = $this->line();
589
590
        return $node;
591
    }
592
593
    protected function parseTag()
594
    {
595
        $i = 2;
596
597
        if ('attributes' === $this->lookahead($i)->type) {
598
            $i++;
599
        }
600
601
        if (':' === $this->lookahead($i)->type) {
602
            $i++;
603
604
            if ('indent' === $this->lookahead($i)->type) {
605
                return $this->parseASTFilter();
606
            }
607
        }
608
609
        $token = $this->advance();
610
        $tag = new Nodes\Tag($token->value);
611
612
        $tag->selfClosing = isset($token->selfClosing)
613
            ? $token->selfClosing
614
            : false;
615
616
        return $this->tag($tag);
617
    }
618
619
    public function parseInlineTags($block, $str)
620
    {
621
        while (preg_match('/^(.*?)#\[([^\]\n]+)\]/', $str, $matches)) {
622
            if (!empty($matches[1])) {
623
                $text = new Nodes\Text($matches[1]);
624
                $text->line = $this->line();
625
                $block->push($text);
626
            }
627
            $parser = $this->subParser($matches[2]);
628
            $tag = $parser->parse();
629
            $tag->line = $this->line();
630
            $block->push($tag);
631
            $str = substr($str, strlen($matches[0]));
632
        }
633
        if (substr($str, 0, 1) === ' ') {
634
            $str = substr($str, 1);
635
        }
636
        $text = new Nodes\Text($str);
637
        $text->line = $this->line();
638
        $block->push($text);
639
    }
640
641
    protected function peekType()
642
    {
643
        return ($peek = $this->peek())
644
            ? $peek->type
645
            : null;
646
    }
647
648
    protected function tag($tag)
649
    {
650
        $tag->line = $this->line();
651
652
        while (true) {
653
            switch ($type = $this->peekType()) {
654
                case 'id':
655
                    $token = $this->advance();
656
                    $peek = $this->peek();
657
                    $escaped = isset($peek->escaped, $peek->escaped[$type]) && $peek->escaped[$type];
658
                    $value = $escaped || !isset($peek->attributes, $peek->attributes[$type])
659
                        ? "'" . $token->value . "'"
660
                        : $peek->attributes[$type];
661
                    $tag->setAttribute($token->type, $value, $escaped);
662
                    unset($peek->attributes[$type]);
663
                    continue;
664
665
                case 'class':
666
                    $token = $this->advance();
667
                    $tag->setAttribute($token->type, "'" . $token->value . "'");
668
                    continue;
669
670
                case 'attributes':
671
                    $token = $this->advance();
672
                    $obj = $token->attributes;
673
                    $escaped = $token->escaped;
674
                    $nameList = array_keys($obj);
675
676
                    if ($token->selfClosing) {
677
                        $tag->selfClosing = true;
678
                    }
679
680
                    foreach ($nameList as $name) {
681
                        $value = $obj[$name];
682
                        $normalizedValue = strtolower($value);
683
                        if ($normalizedValue === 'true' || $normalizedValue === 'false') {
684
                            $value = $normalizedValue === 'true';
685
                        }
686
                        $tag->setAttribute($name, $value, $escaped[$name]);
687
                    }
688
                    continue;
689
690
                case '&attributes':
691
                    $token = $this->advance();
692
                    $tag->setAttribute('&attributes', $token->value);
693
                    continue;
694
695
                default:
696
                    break 2;
697
            }
698
        }
699
700
        $dot = false;
701
        $tag->textOnly = false;
702
        if ('.' === $this->peek()->value) {
703
            $dot = $tag->textOnly = true;
704
            $this->advance();
705
        }
706
707
        switch ($this->peekType()) {
708
            case 'text':
709
                $this->parseInlineTags($tag->block, $this->expect('text')->value);
710
                break;
711
712
            case 'code':
713
                $tag->code = $this->parseCode();
714
                break;
715
716
            case ':':
717
                $this->advance();
718
                $tag->block = new Nodes\Block();
719
                $tag->block->push($this->parseExpression());
720
                break;
721
        }
722
723
        while ('newline' === $this->peekType()) {
724
            $this->advance();
725
        }
726
727
        if ('script' === $tag->name) {
728
            $type = $tag->getAttribute('type');
729
730
            if ($type !== null) {
731
                $type = preg_replace('/^[\'\"]|[\'\"]$/', '', $type);
732
733
                if (!$dot && 'text/javascript' != $type['value']) {
734
                    $tag->textOnly = false;
735
                }
736
            }
737
        }
738
739
        if ('indent' === $this->peekType()) {
740
            if ($tag->textOnly) {
741
                $this->lexer->pipeless = true;
742
                $tag->block = $this->parseTextBlock();
743
                $this->lexer->pipeless = false;
744
745
                return $tag;
746
            }
747
748
            $block = $this->block();
749
750
            if ($tag->block && !$tag->block->isEmpty()) {
751
                foreach ($block->nodes as $n) {
752
                    $tag->block->push($n);
753
                }
754
755
                return $tag;
756
            }
757
758
            $tag->block = $block;
759
        }
760
761
        return $tag;
762
    }
763
}
764